feat(settings): 新增设置模块和API字符集编码
feat: 添加设置存储模块和设置页面 fix: 为API响应添加UTF-8字符集编码 docs: 新增项目运行和错误修复设计方案 style: 添加自动导入类型定义文件
This commit is contained in:
parent
6422aa85ee
commit
67939c108b
|
@ -1,541 +0,0 @@
|
|||
# 大语言模型聊天网站设计文档
|
||||
|
||||
## 概述
|
||||
|
||||
本项目是一个全栈大语言模型聊天网站,支持用户与AI进行实时对话。系统采用前后端分离架构,前端使用Vue.js构建用户界面,后端使用Node.js提供API服务,通过调用兼容OpenAI格式的第三方API实现智能对话功能。
|
||||
|
||||
### 核心特性
|
||||
- 实时AI对话体验
|
||||
- 对话历史记录与管理
|
||||
- 用户会话状态保持
|
||||
- 响应式Web界面
|
||||
- 高性能缓存机制
|
||||
- 轻量级本地数据存储
|
||||
|
||||
### 技术栈
|
||||
- **前端**: Vue.js 3, Vue Router, Pinia, Axios, Element Plus
|
||||
- **后端**: Node.js, Express.js, SQLite3, LRU-Cache
|
||||
- **包管理**: pnpm
|
||||
- **数据库**: SQLite (本地文件数据库)
|
||||
- **第三方集成**: OpenAI兼容API服务
|
||||
|
||||
## 系统架构
|
||||
|
||||
### 整体架构图
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "客户端"
|
||||
A[Vue.js前端]
|
||||
A1[聊天界面]
|
||||
A2[历史记录]
|
||||
A3[用户设置]
|
||||
end
|
||||
|
||||
subgraph "服务端"
|
||||
B[Node.js后端]
|
||||
B1[Express路由]
|
||||
B2[会话管理]
|
||||
B3[缓存层]
|
||||
B4[数据访问层]
|
||||
end
|
||||
|
||||
subgraph "数据存储"
|
||||
C[SQLite数据库]
|
||||
C1[用户表]
|
||||
C2[对话表]
|
||||
C3[消息表]
|
||||
end
|
||||
|
||||
subgraph "外部服务"
|
||||
D[第三方LLM API]
|
||||
D1[OpenAI兼容接口]
|
||||
end
|
||||
|
||||
A -->|HTTP/WebSocket| B
|
||||
B -->|SQL查询| C
|
||||
B -->|API调用| D
|
||||
B3 -->|LRU缓存| B2
|
||||
```
|
||||
|
||||
### 数据流架构
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as 用户
|
||||
participant F as Vue前端
|
||||
participant B as Node.js后端
|
||||
participant C as LRU缓存
|
||||
participant DB as SQLite
|
||||
participant API as 第三方LLM API
|
||||
|
||||
U->>F: 发送消息
|
||||
F->>B: POST /api/chat/send
|
||||
B->>C: 检查缓存
|
||||
alt 缓存命中
|
||||
C-->>B: 返回缓存结果
|
||||
else 缓存未命中
|
||||
B->>DB: 保存用户消息
|
||||
B->>API: 调用LLM API
|
||||
API-->>B: 返回AI回复
|
||||
B->>DB: 保存AI回复
|
||||
B->>C: 更新缓存
|
||||
end
|
||||
B-->>F: 返回对话结果
|
||||
F-->>U: 显示AI回复
|
||||
```
|
||||
|
||||
## 前端架构
|
||||
|
||||
### 组件层次结构
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[App.vue] --> B[Layout组件]
|
||||
B --> C[Header组件]
|
||||
B --> D[Sidebar组件]
|
||||
B --> E[Main组件]
|
||||
|
||||
E --> F[ChatView]
|
||||
E --> G[HistoryView]
|
||||
E --> H[SettingsView]
|
||||
|
||||
F --> I[ChatInput]
|
||||
F --> J[MessageList]
|
||||
F --> K[TypingIndicator]
|
||||
|
||||
J --> L[MessageItem]
|
||||
L --> M[UserMessage]
|
||||
L --> N[AIMessage]
|
||||
|
||||
D --> O[ConversationList]
|
||||
O --> P[ConversationItem]
|
||||
```
|
||||
|
||||
### 组件定义
|
||||
|
||||
#### 核心聊天组件
|
||||
|
||||
**ChatView.vue** - 主聊天界面
|
||||
- 管理当前对话状态
|
||||
- 处理消息发送逻辑
|
||||
- 实现消息流式显示
|
||||
|
||||
**MessageList.vue** - 消息列表容器
|
||||
- 虚拟滚动优化
|
||||
- 消息懒加载
|
||||
- 自动滚动到底部
|
||||
|
||||
**ChatInput.vue** - 消息输入组件
|
||||
- 多行文本输入
|
||||
- 发送按钮状态管理
|
||||
- 快捷键支持(Ctrl+Enter)
|
||||
|
||||
#### 布局组件
|
||||
|
||||
**Layout.vue** - 主布局容器
|
||||
- 响应式布局设计
|
||||
- 侧边栏展开/收缩
|
||||
- 移动端适配
|
||||
|
||||
**Sidebar.vue** - 侧边栏组件
|
||||
- 对话历史列表
|
||||
- 新建对话按钮
|
||||
- 对话搜索功能
|
||||
|
||||
### 状态管理架构
|
||||
|
||||
使用Pinia进行状态管理,主要包含以下Store:
|
||||
|
||||
**chatStore.js** - 聊天状态管理
|
||||
```javascript
|
||||
{
|
||||
currentConversation: null,
|
||||
messages: [],
|
||||
isLoading: false,
|
||||
isTyping: false
|
||||
}
|
||||
```
|
||||
|
||||
**conversationStore.js** - 对话管理
|
||||
```javascript
|
||||
{
|
||||
conversations: [],
|
||||
activeConversationId: null,
|
||||
searchQuery: ''
|
||||
}
|
||||
```
|
||||
|
||||
**userStore.js** - 用户设置
|
||||
```javascript
|
||||
{
|
||||
settings: {
|
||||
theme: 'light',
|
||||
fontSize: 'medium',
|
||||
autoSave: true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 路由配置
|
||||
|
||||
```javascript
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
component: Layout,
|
||||
children: [
|
||||
{ path: '', redirect: '/chat' },
|
||||
{ path: '/chat/:id?', component: ChatView },
|
||||
{ path: '/history', component: HistoryView },
|
||||
{ path: '/settings', component: SettingsView }
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### API集成层
|
||||
|
||||
**apiClient.js** - HTTP客户端封装
|
||||
- Axios实例配置
|
||||
- 请求/响应拦截器
|
||||
- 错误处理机制
|
||||
- 重试逻辑
|
||||
|
||||
**chatApi.js** - 聊天相关API
|
||||
```javascript
|
||||
export const chatApi = {
|
||||
sendMessage: (conversationId, message) => {},
|
||||
getConversations: () => {},
|
||||
getMessages: (conversationId) => {},
|
||||
createConversation: () => {},
|
||||
deleteConversation: (id) => {}
|
||||
}
|
||||
```
|
||||
|
||||
## 后端架构
|
||||
|
||||
### API端点设计
|
||||
|
||||
#### 对话管理端点
|
||||
|
||||
| 方法 | 路径 | 描述 | 请求体 | 响应 |
|
||||
|------|------|------|--------|------|
|
||||
| POST | `/api/conversations` | 创建新对话 | `{ title?: string }` | `{ id, title, createdAt }` |
|
||||
| GET | `/api/conversations` | 获取对话列表 | - | `[{ id, title, lastMessage, updatedAt }]` |
|
||||
| GET | `/api/conversations/:id` | 获取对话详情 | - | `{ id, title, messages }` |
|
||||
| DELETE | `/api/conversations/:id` | 删除对话 | - | `{ success: true }` |
|
||||
|
||||
#### 消息处理端点
|
||||
|
||||
| 方法 | 路径 | 描述 | 请求体 | 响应 |
|
||||
|------|------|------|--------|------|
|
||||
| POST | `/api/chat/send` | 发送消息 | `{ conversationId, message, stream? }` | `{ message, response }` |
|
||||
| GET | `/api/messages/:conversationId` | 获取消息历史 | - | `[{ id, role, content, timestamp }]` |
|
||||
|
||||
### 数据模型设计
|
||||
|
||||
#### 数据库表结构
|
||||
|
||||
**conversations表**
|
||||
```sql
|
||||
CREATE TABLE conversations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL DEFAULT '新对话',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
**messages表**
|
||||
```sql
|
||||
CREATE TABLE messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
conversation_id INTEGER NOT NULL,
|
||||
role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
|
||||
content TEXT NOT NULL,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
### 业务逻辑层
|
||||
|
||||
#### 对话服务 (ConversationService)
|
||||
|
||||
```javascript
|
||||
class ConversationService {
|
||||
// 创建新对话
|
||||
async createConversation(title) {}
|
||||
|
||||
// 获取对话列表
|
||||
async getConversations() {}
|
||||
|
||||
// 获取对话详情
|
||||
async getConversationById(id) {}
|
||||
|
||||
// 删除对话
|
||||
async deleteConversation(id) {}
|
||||
|
||||
// 更新对话标题
|
||||
async updateConversationTitle(id, title) {}
|
||||
}
|
||||
```
|
||||
|
||||
#### 聊天服务 (ChatService)
|
||||
|
||||
```javascript
|
||||
class ChatService {
|
||||
constructor(llmApiClient, cache) {
|
||||
this.llmApiClient = llmApiClient;
|
||||
this.cache = cache;
|
||||
}
|
||||
|
||||
// 处理用户消息
|
||||
async processMessage(conversationId, userMessage) {
|
||||
// 1. 保存用户消息到数据库
|
||||
// 2. 构建上下文(包含历史消息)
|
||||
// 3. 调用LLM API
|
||||
// 4. 保存AI回复到数据库
|
||||
// 5. 更新缓存
|
||||
// 6. 返回结果
|
||||
}
|
||||
|
||||
// 流式响应处理
|
||||
async processStreamMessage(conversationId, userMessage) {}
|
||||
}
|
||||
```
|
||||
|
||||
#### LLM API客户端 (LLMApiClient)
|
||||
|
||||
```javascript
|
||||
class LLMApiClient {
|
||||
constructor(apiConfig) {
|
||||
this.baseURL = apiConfig.baseURL;
|
||||
this.apiKey = apiConfig.apiKey;
|
||||
this.model = apiConfig.model;
|
||||
}
|
||||
|
||||
// 调用聊天完成API
|
||||
async createChatCompletion(messages, options = {}) {
|
||||
const response = await fetch(`${this.baseURL}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: this.model,
|
||||
messages: messages,
|
||||
stream: options.stream || false,
|
||||
max_tokens: options.maxTokens || 2000,
|
||||
temperature: options.temperature || 0.7
|
||||
})
|
||||
});
|
||||
return response;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 缓存策略
|
||||
|
||||
#### LRU缓存配置
|
||||
|
||||
```javascript
|
||||
const LRU = require('lru-cache');
|
||||
|
||||
const cache = new LRU({
|
||||
max: 500, // 最大缓存条目数
|
||||
maxAge: 1000 * 60 * 30 // 30分钟过期
|
||||
});
|
||||
|
||||
// 缓存键策略
|
||||
const getCacheKey = (conversationId, messageHash) => {
|
||||
return `conv:${conversationId}:msg:${messageHash}`;
|
||||
};
|
||||
```
|
||||
|
||||
#### 缓存使用场景
|
||||
|
||||
1. **对话上下文缓存** - 缓存最近的对话上下文,减少数据库查询
|
||||
2. **API响应缓存** - 对相同输入的LLM响应进行缓存
|
||||
3. **用户会话缓存** - 缓存用户会话信息
|
||||
|
||||
### 中间件系统
|
||||
|
||||
#### 请求日志中间件
|
||||
|
||||
```javascript
|
||||
const requestLogger = (req, res, next) => {
|
||||
console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`);
|
||||
next();
|
||||
};
|
||||
```
|
||||
|
||||
#### 错误处理中间件
|
||||
|
||||
```javascript
|
||||
const errorHandler = (err, req, res, next) => {
|
||||
console.error('Error:', err);
|
||||
res.status(500).json({
|
||||
error: 'Internal Server Error',
|
||||
message: process.env.NODE_ENV === 'development' ? err.message : '服务器内部错误'
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
#### 速率限制中间件
|
||||
|
||||
```javascript
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
const chatRateLimit = rateLimit({
|
||||
windowMs: 1 * 60 * 1000, // 1分钟
|
||||
max: 20, // 限制每个IP每分钟最多20次请求
|
||||
message: '请求过于频繁,请稍后再试'
|
||||
});
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
chat-website/
|
||||
├── package.json
|
||||
├── pnpm-workspace.yaml
|
||||
├── README.md
|
||||
├── .gitignore
|
||||
├── frontend/
|
||||
│ ├── package.json
|
||||
│ ├── vite.config.js
|
||||
│ ├── index.html
|
||||
│ ├── src/
|
||||
│ │ ├── main.js
|
||||
│ │ ├── App.vue
|
||||
│ │ ├── components/
|
||||
│ │ │ ├── layout/
|
||||
│ │ │ │ ├── Layout.vue
|
||||
│ │ │ │ ├── Header.vue
|
||||
│ │ │ │ └── Sidebar.vue
|
||||
│ │ │ ├── chat/
|
||||
│ │ │ │ ├── ChatView.vue
|
||||
│ │ │ │ ├── MessageList.vue
|
||||
│ │ │ │ ├── MessageItem.vue
|
||||
│ │ │ │ ├── ChatInput.vue
|
||||
│ │ │ │ └── TypingIndicator.vue
|
||||
│ │ │ └── common/
|
||||
│ │ │ ├── Loading.vue
|
||||
│ │ │ └── ErrorMessage.vue
|
||||
│ │ ├── stores/
|
||||
│ │ │ ├── index.js
|
||||
│ │ │ ├── chat.js
|
||||
│ │ │ ├── conversation.js
|
||||
│ │ │ └── user.js
|
||||
│ │ ├── api/
|
||||
│ │ │ ├── client.js
|
||||
│ │ │ ├── chat.js
|
||||
│ │ │ └── conversation.js
|
||||
│ │ ├── router/
|
||||
│ │ │ └── index.js
|
||||
│ │ ├── styles/
|
||||
│ │ │ ├── main.css
|
||||
│ │ │ ├── variables.css
|
||||
│ │ │ └── components.css
|
||||
│ │ └── utils/
|
||||
│ │ ├── helpers.js
|
||||
│ │ └── constants.js
|
||||
│ └── public/
|
||||
│ └── favicon.ico
|
||||
└── backend/
|
||||
├── package.json
|
||||
├── app.js
|
||||
├── server.js
|
||||
├── config/
|
||||
│ ├── database.js
|
||||
│ └── llm.js
|
||||
├── routes/
|
||||
│ ├── index.js
|
||||
│ ├── chat.js
|
||||
│ └── conversations.js
|
||||
├── services/
|
||||
│ ├── ConversationService.js
|
||||
│ ├── ChatService.js
|
||||
│ └── LLMApiClient.js
|
||||
├── models/
|
||||
│ ├── database.js
|
||||
│ ├── Conversation.js
|
||||
│ └── Message.js
|
||||
├── middleware/
|
||||
│ ├── auth.js
|
||||
│ ├── rateLimit.js
|
||||
│ ├── logger.js
|
||||
│ └── errorHandler.js
|
||||
├── utils/
|
||||
│ ├── cache.js
|
||||
│ └── helpers.js
|
||||
└── database/
|
||||
├── schema.sql
|
||||
└── chat.db
|
||||
```
|
||||
|
||||
## 测试策略
|
||||
|
||||
### 前端测试
|
||||
|
||||
#### 单元测试 (Vitest)
|
||||
- 组件逻辑测试
|
||||
- Store状态管理测试
|
||||
- 工具函数测试
|
||||
|
||||
#### 组件测试 (Vue Test Utils)
|
||||
- 组件渲染测试
|
||||
- 用户交互测试
|
||||
- Props和Events测试
|
||||
|
||||
#### E2E测试 (Playwright)
|
||||
- 完整对话流程测试
|
||||
- 跨浏览器兼容性测试
|
||||
- 移动端响应式测试
|
||||
|
||||
### 后端测试
|
||||
|
||||
#### 单元测试 (Jest)
|
||||
|
||||
**API端点测试**
|
||||
```javascript
|
||||
describe('Chat API', () => {
|
||||
test('should send message successfully', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/chat/send')
|
||||
.send({
|
||||
conversationId: 1,
|
||||
message: '你好'
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty('response');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**服务层测试**
|
||||
```javascript
|
||||
describe('ChatService', () => {
|
||||
test('should process message correctly', async () => {
|
||||
const chatService = new ChatService(mockApiClient, mockCache);
|
||||
const result = await chatService.processMessage(1, '测试消息');
|
||||
|
||||
expect(result).toHaveProperty('userMessage');
|
||||
expect(result).toHaveProperty('aiResponse');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### 集成测试
|
||||
- 数据库操作测试
|
||||
- 第三方API集成测试
|
||||
- 缓存机制测试
|
||||
|
||||
#### 性能测试
|
||||
- API响应时间测试
|
||||
- 并发请求处理测试
|
||||
- 内存使用监控
|
|
@ -0,0 +1,256 @@
|
|||
# 运行项目与错误修复设计方案
|
||||
|
||||
## 项目概述
|
||||
|
||||
LLM Chat Website 是一个基于 Node.js 后端和 Vue.js 前端的全栈聊天应用,使用 pnpm workspace 进行依赖管理。项目采用现代化技术栈,包含完整的聊天功能和大语言模型集成。
|
||||
|
||||
## 技术架构分析
|
||||
|
||||
### 后端架构 (Node.js + Express)
|
||||
- **框架**: Express.js 4.18.2
|
||||
- **数据库**: SQLite3 5.1.6
|
||||
- **缓存**: LRU Cache 10.0.1
|
||||
- **HTTP客户端**: Axios 1.6.2
|
||||
- **开发工具**: Nodemon 3.0.2
|
||||
- **端口**: 3000 (默认)
|
||||
|
||||
### 前端架构 (Vue 3)
|
||||
- **框架**: Vue 3.3.8
|
||||
- **构建工具**: Vite 5.0.0
|
||||
- **UI框架**: Element Plus 2.4.4
|
||||
- **状态管理**: Pinia 2.1.7
|
||||
- **路由**: Vue Router 4.2.5
|
||||
- **端口**: 5173 (默认)
|
||||
|
||||
### 依赖管理
|
||||
- **包管理器**: pnpm (workspace 模式)
|
||||
- **配置文件**: pnpm-workspace.yaml
|
||||
- **根目录脚本**: concurrently 并发运行前后端
|
||||
|
||||
## 项目启动流程设计
|
||||
|
||||
### 启动顺序架构
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[检查 pnpm 安装] --> B[验证 workspace 配置]
|
||||
B --> C[检查依赖安装状态]
|
||||
C --> D[启动后端服务]
|
||||
D --> E[初始化数据库]
|
||||
E --> F[启动前端服务]
|
||||
F --> G[验证服务通信]
|
||||
G --> H[功能验证测试]
|
||||
```
|
||||
|
||||
### 环境变量配置架构
|
||||
|
||||
| 变量名 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| PORT | 3000 | 后端服务端口 |
|
||||
| NODE_ENV | development | 运行环境 |
|
||||
| LLM_API_KEY | - | 大语言模型API密钥 |
|
||||
| LLM_API_BASE_URL | https://api.openai.com/v1 | API基础地址 |
|
||||
| LLM_MODEL | gpt-3.5-turbo | 使用的模型 |
|
||||
| CORS_ORIGIN | http://localhost:5173 | 跨域设置 |
|
||||
|
||||
## 潜在错误分析与解决方案
|
||||
|
||||
### 数据库初始化错误
|
||||
|
||||
**错误场景**: SQLite 数据库文件不存在或权限问题
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[数据库连接失败] --> B{检查文件权限}
|
||||
B -->|权限正常| C[创建数据库目录]
|
||||
B -->|权限异常| D[修复文件权限]
|
||||
C --> E[执行 schema.sql]
|
||||
D --> E
|
||||
E --> F[验证表结构]
|
||||
```
|
||||
|
||||
**解决策略**:
|
||||
1. 确保 backend/database 目录存在且可写
|
||||
2. 自动创建数据库文件
|
||||
3. 执行 schema.sql 初始化表结构
|
||||
4. 验证示例数据插入
|
||||
|
||||
### 端口冲突错误
|
||||
|
||||
**错误场景**: 3000 或 5173 端口被占用
|
||||
```mermaid
|
||||
graph TD
|
||||
A[启动失败] --> B{检查端口占用}
|
||||
B -->|端口被占用| C[终止占用进程]
|
||||
B -->|端口正常| D[检查其他配置]
|
||||
C --> E[重新启动服务]
|
||||
D --> F[排查网络配置]
|
||||
```
|
||||
|
||||
**解决策略**:
|
||||
1. 检测端口占用情况
|
||||
2. 提供端口配置选项
|
||||
3. 实现优雅的端口切换
|
||||
|
||||
### LLM API 配置错误
|
||||
|
||||
**错误模式**:
|
||||
- API_KEY 未配置或无效
|
||||
- 网络连接问题
|
||||
- 请求频率限制
|
||||
|
||||
**修复流程**:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant App as 应用
|
||||
participant Config as 配置验证
|
||||
participant LLM as LLM服务
|
||||
|
||||
App->>Config: 验证API配置
|
||||
Config->>Config: 检查必要环境变量
|
||||
alt 配置缺失
|
||||
Config->>App: 返回警告信息
|
||||
App->>App: 使用默认配置
|
||||
else 配置完整
|
||||
Config->>LLM: 测试连接
|
||||
LLM->>Config: 返回连接状态
|
||||
end
|
||||
```
|
||||
|
||||
### 前端构建错误
|
||||
|
||||
**常见问题**:
|
||||
1. 依赖版本冲突
|
||||
2. 样式预处理器错误
|
||||
3. TypeScript 类型检查错误
|
||||
|
||||
**解决方案架构**:
|
||||
- 依赖冲突: 使用 pnpm 的严格依赖解析
|
||||
- 样式错误: 验证 SCSS 变量文件路径
|
||||
- 类型错误: 配置 unplugin-auto-import
|
||||
|
||||
## 服务启动验证策略
|
||||
|
||||
### 后端健康检查
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[GET /] --> B{响应状态}
|
||||
B -->|200| C[服务正常]
|
||||
B -->|非200| D[服务异常]
|
||||
|
||||
E[GET /api] --> F{API状态}
|
||||
F -->|可访问| G[API正常]
|
||||
F -->|不可访问| H[API异常]
|
||||
```
|
||||
|
||||
### 前端资源加载验证
|
||||
|
||||
**验证点**:
|
||||
1. 静态资源加载 (CSS, JS)
|
||||
2. Vue 应用挂载成功
|
||||
3. Element Plus 组件正常渲染
|
||||
4. 路由导航功能
|
||||
|
||||
### 前后端通信验证
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant F as Frontend
|
||||
participant P as Proxy
|
||||
participant B as Backend
|
||||
|
||||
F->>P: 发送 /api 请求
|
||||
P->>B: 转发到 localhost:3000
|
||||
B->>P: 返回响应
|
||||
P->>F: 返回结果
|
||||
|
||||
Note over F,B: 验证 CORS 配置
|
||||
Note over F,B: 验证代理设置
|
||||
```
|
||||
|
||||
## 功能验证测试方案
|
||||
|
||||
### 核心功能测试架构
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[页面加载测试] --> B[创建新对话]
|
||||
B --> C[发送消息测试]
|
||||
C --> D[接收响应测试]
|
||||
D --> E[对话历史测试]
|
||||
E --> F[UI交互测试]
|
||||
```
|
||||
|
||||
### 测试用例设计
|
||||
|
||||
| 测试项 | 验证内容 | 期望结果 |
|
||||
|--------|----------|----------|
|
||||
| 页面访问 | http://localhost:5173 | 页面正常加载 |
|
||||
| 新建对话 | 点击新建对话按钮 | 创建空白对话界面 |
|
||||
| 消息发送 | 输入测试消息并发送 | 消息显示在界面 |
|
||||
| API通信 | 后端接收并处理请求 | 返回预期响应 |
|
||||
| 历史记录 | 查看对话历史 | 正确显示历史对话 |
|
||||
|
||||
## 错误监控与日志策略
|
||||
|
||||
### 日志架构设计
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[请求日志] --> D[Morgan中间件]
|
||||
B[错误日志] --> E[自定义ErrorHandler]
|
||||
C[应用日志] --> F[Console Logger]
|
||||
|
||||
D --> G[请求统计]
|
||||
E --> H[错误追踪]
|
||||
F --> I[调试信息]
|
||||
```
|
||||
|
||||
### 监控指标
|
||||
|
||||
- **性能指标**: 响应时间、内存使用、CPU占用
|
||||
- **错误指标**: 错误率、异常类型、失败请求
|
||||
- **业务指标**: API调用次数、用户会话数
|
||||
|
||||
## 部署前置条件检查
|
||||
|
||||
### 环境依赖验证
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Node.js 版本] -->|>=16.0.0| B[pnpm 安装]
|
||||
B --> C[workspace 配置]
|
||||
C --> D[依赖安装状态]
|
||||
D --> E[环境变量配置]
|
||||
E --> F[网络连接测试]
|
||||
F --> G[启动准备就绪]
|
||||
```
|
||||
|
||||
### 配置文件验证
|
||||
|
||||
1. **package.json**: 验证脚本配置和依赖版本
|
||||
2. **pnpm-workspace.yaml**: 确认 workspace 配置格式
|
||||
3. **vite.config.js**: 验证构建和代理配置
|
||||
4. **.env**: 检查必要环境变量
|
||||
|
||||
## 故障排除指南
|
||||
|
||||
### 常见问题快速诊断
|
||||
|
||||
| 问题类型 | 症状 | 诊断步骤 | 解决方案 |
|
||||
|----------|------|----------|----------|
|
||||
| 依赖问题 | 模块找不到 | 检查 node_modules | 重新安装依赖 |
|
||||
| 端口冲突 | EADDRINUSE | 检查端口占用 | 更换端口或终止进程 |
|
||||
| 数据库错误 | 连接失败 | 检查文件权限 | 重新初始化数据库 |
|
||||
| API调用失败 | 网络错误 | 检查配置和网络 | 修复配置或网络问题 |
|
||||
| 前端白屏 | 页面不显示 | 检查控制台错误 | 修复组件或路由错误 |
|
||||
|
||||
### 调试工具配置
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[后端调试] --> B[nodemon + console.log]
|
||||
C[前端调试] --> D[Vue DevTools]
|
||||
E[网络调试] --> F[浏览器 Network 面板]
|
||||
G[数据库调试] --> H[SQLite 命令行工具]
|
||||
```
|
|
@ -66,6 +66,16 @@ class App {
|
|||
strict: true
|
||||
}));
|
||||
|
||||
// 设置响应字符集编码
|
||||
this.app.use((req, res, next) => {
|
||||
const originalJson = res.json;
|
||||
res.json = function(data) {
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
return originalJson.call(this, data);
|
||||
};
|
||||
next();
|
||||
});
|
||||
|
||||
// 解析URL编码的请求体
|
||||
this.app.use(express.urlencoded({
|
||||
extended: true,
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const ElMessage: typeof import('element-plus/es')['ElMessage']
|
||||
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
|
||||
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const createPinia: typeof import('pinia')['createPinia']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const defineStore: typeof import('pinia')['defineStore']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const h: typeof import('vue')['h']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const mapActions: typeof import('pinia')['mapActions']
|
||||
const mapGetters: typeof import('pinia')['mapGetters']
|
||||
const mapState: typeof import('pinia')['mapState']
|
||||
const mapStores: typeof import('pinia')['mapStores']
|
||||
const mapWritableState: typeof import('pinia')['mapWritableState']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const setActivePinia: typeof import('pinia')['setActivePinia']
|
||||
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const toValue: typeof import('vue')['toValue']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useId: typeof import('vue')['useId']
|
||||
const useLink: typeof import('vue-router')['useLink']
|
||||
const useModel: typeof import('vue')['useModel']
|
||||
const useRoute: typeof import('vue-router')['useRoute']
|
||||
const useRouter: typeof import('vue-router')['useRouter']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ConversationItem: typeof import('./src/components/chat/ConversationItem.vue')['default']
|
||||
ConversationSkeleton: typeof import('./src/components/common/ConversationSkeleton.vue')['default']
|
||||
ElBacktop: typeof import('element-plus/es')['ElBacktop']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElLoading: typeof import('element-plus/es')['ElLoading']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
||||
ElSkeletonItem: typeof import('element-plus/es')['ElSkeletonItem']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
Header: typeof import('./src/components/layout/Header.vue')['default']
|
||||
Layout: typeof import('./src/components/layout/Layout.vue')['default']
|
||||
MessageItem: typeof import('./src/components/chat/MessageItem.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
Sidebar: typeof import('./src/components/layout/Sidebar.vue')['default']
|
||||
}
|
||||
export interface ComponentCustomProperties {
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ export { useAppStore } from './app'
|
|||
export { useChatStore } from './chat'
|
||||
export { useConversationStore } from './conversation'
|
||||
export { useUserStore } from './user'
|
||||
export { useSettingsStore } from './settings'
|
||||
|
||||
// 如果需要,可以在这里添加全局的 store 初始化逻辑
|
||||
export const initializeStores = () => {
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export const useSettingsStore = defineStore('settings', () => {
|
||||
// 状态
|
||||
const settings = ref({
|
||||
theme: 'light', // light | dark
|
||||
fontSize: 14,
|
||||
showMessageBubbles: true,
|
||||
autoSave: true,
|
||||
streamResponse: true,
|
||||
maxTokens: 1024
|
||||
})
|
||||
|
||||
const apiSettings = ref({
|
||||
baseURL: 'http://localhost:3000',
|
||||
apiKey: '',
|
||||
model: 'gpt-3.5-turbo'
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const isDark = computed(() => settings.value.theme === 'dark')
|
||||
|
||||
// 初始化设置
|
||||
const initializeSettings = () => {
|
||||
try {
|
||||
const savedSettings = localStorage.getItem('chat_settings')
|
||||
if (savedSettings) {
|
||||
settings.value = { ...settings.value, ...JSON.parse(savedSettings) }
|
||||
}
|
||||
|
||||
const savedApiSettings = localStorage.getItem('chat_api_settings')
|
||||
if (savedApiSettings) {
|
||||
apiSettings.value = { ...apiSettings.value, ...JSON.parse(savedApiSettings) }
|
||||
}
|
||||
|
||||
// 应用主题
|
||||
applyTheme()
|
||||
} catch (error) {
|
||||
console.error('加载设置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 应用主题
|
||||
const applyTheme = () => {
|
||||
const html = document.documentElement
|
||||
if (isDark.value) {
|
||||
html.classList.add('dark')
|
||||
} else {
|
||||
html.classList.remove('dark')
|
||||
}
|
||||
}
|
||||
|
||||
// 保存设置到本地存储
|
||||
const saveSettings = () => {
|
||||
try {
|
||||
localStorage.setItem('chat_settings', JSON.stringify(settings.value))
|
||||
} catch (error) {
|
||||
console.error('保存设置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const saveApiSettings = () => {
|
||||
try {
|
||||
localStorage.setItem('chat_api_settings', JSON.stringify(apiSettings.value))
|
||||
} catch (error) {
|
||||
console.error('保存API设置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置方法
|
||||
const setTheme = (theme) => {
|
||||
settings.value.theme = theme
|
||||
applyTheme()
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
const setFontSize = (size) => {
|
||||
settings.value.fontSize = size
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
const setShowMessageBubbles = (show) => {
|
||||
settings.value.showMessageBubbles = show
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
const setAutoSave = (autoSave) => {
|
||||
settings.value.autoSave = autoSave
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
const setStreamResponse = (stream) => {
|
||||
settings.value.streamResponse = stream
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
const setMaxTokens = (maxTokens) => {
|
||||
settings.value.maxTokens = maxTokens
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
const setApiSettings = (newApiSettings) => {
|
||||
apiSettings.value = { ...apiSettings.value, ...newApiSettings }
|
||||
saveApiSettings()
|
||||
}
|
||||
|
||||
// 清除所有缓存
|
||||
const clearCache = () => {
|
||||
try {
|
||||
// 清除本地存储
|
||||
localStorage.removeItem('chat_settings')
|
||||
localStorage.removeItem('chat_api_settings')
|
||||
localStorage.removeItem('chat_conversations')
|
||||
localStorage.removeItem('chat_messages')
|
||||
|
||||
// 重置到默认值
|
||||
settings.value = {
|
||||
theme: 'light',
|
||||
fontSize: 14,
|
||||
showMessageBubbles: true,
|
||||
autoSave: true,
|
||||
streamResponse: true,
|
||||
maxTokens: 1024
|
||||
}
|
||||
|
||||
apiSettings.value = {
|
||||
baseURL: 'http://localhost:3000',
|
||||
apiKey: '',
|
||||
model: 'gpt-3.5-turbo'
|
||||
}
|
||||
|
||||
applyTheme()
|
||||
} catch (error) {
|
||||
console.error('清除缓存失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 重置设置
|
||||
const resetSettings = () => {
|
||||
settings.value = {
|
||||
theme: 'light',
|
||||
fontSize: 14,
|
||||
showMessageBubbles: true,
|
||||
autoSave: true,
|
||||
streamResponse: true,
|
||||
maxTokens: 1024
|
||||
}
|
||||
applyTheme()
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
// 初始化
|
||||
initializeSettings()
|
||||
|
||||
return {
|
||||
// 状态
|
||||
settings,
|
||||
apiSettings,
|
||||
isDark,
|
||||
|
||||
// 方法
|
||||
setTheme,
|
||||
setFontSize,
|
||||
setShowMessageBubbles,
|
||||
setAutoSave,
|
||||
setStreamResponse,
|
||||
setMaxTokens,
|
||||
setApiSettings,
|
||||
clearCache,
|
||||
resetSettings,
|
||||
initializeSettings,
|
||||
applyTheme
|
||||
}
|
||||
})
|
|
@ -0,0 +1,379 @@
|
|||
<template>
|
||||
<div class="settings-view">
|
||||
<div class="settings-header">
|
||||
<h1 class="settings-title">
|
||||
<el-icon><Setting /></el-icon>
|
||||
设置
|
||||
</h1>
|
||||
<p class="settings-subtitle">个性化您的聊天体验</p>
|
||||
</div>
|
||||
|
||||
<div class="settings-content">
|
||||
<el-card class="settings-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon><Brush /></el-icon>
|
||||
<span>界面设置</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<h4>主题模式</h4>
|
||||
<p>选择浅色或深色主题</p>
|
||||
</div>
|
||||
<el-switch
|
||||
v-model="settings.theme"
|
||||
active-value="dark"
|
||||
inactive-value="light"
|
||||
active-text="深色"
|
||||
inactive-text="浅色"
|
||||
@change="handleThemeChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<h4>字体大小</h4>
|
||||
<p>调整聊天内容的字体大小</p>
|
||||
</div>
|
||||
<el-slider
|
||||
v-model="settings.fontSize"
|
||||
:min="12"
|
||||
:max="20"
|
||||
:step="1"
|
||||
:marks="fontSizeMarks"
|
||||
style="width: 200px"
|
||||
@change="handleFontSizeChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<h4>消息气泡</h4>
|
||||
<p>显示或隐藏消息气泡样式</p>
|
||||
</div>
|
||||
<el-switch
|
||||
v-model="settings.showMessageBubbles"
|
||||
@change="handleBubbleChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="settings-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon><ChatDotRound /></el-icon>
|
||||
<span>聊天设置</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<h4>自动保存</h4>
|
||||
<p>自动保存对话到历史记录</p>
|
||||
</div>
|
||||
<el-switch
|
||||
v-model="settings.autoSave"
|
||||
@change="handleAutoSaveChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<h4>流式输出</h4>
|
||||
<p>实时显示AI回复的生成过程</p>
|
||||
</div>
|
||||
<el-switch
|
||||
v-model="settings.streamResponse"
|
||||
@change="handleStreamChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<h4>回复长度</h4>
|
||||
<p>控制AI回复的最大长度</p>
|
||||
</div>
|
||||
<el-select
|
||||
v-model="settings.maxTokens"
|
||||
placeholder="选择长度"
|
||||
style="width: 120px"
|
||||
@change="handleMaxTokensChange"
|
||||
>
|
||||
<el-option label="短" :value="512" />
|
||||
<el-option label="中" :value="1024" />
|
||||
<el-option label="长" :value="2048" />
|
||||
</el-select>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="settings-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon><Operation /></el-icon>
|
||||
<span>高级设置</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<h4>API设置</h4>
|
||||
<p>配置LLM服务连接</p>
|
||||
</div>
|
||||
<el-button type="primary" text @click="showApiDialog = true">
|
||||
配置
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<h4>清除缓存</h4>
|
||||
<p>清除应用缓存数据</p>
|
||||
</div>
|
||||
<el-button type="danger" text @click="handleClearCache">
|
||||
清除
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- API配置对话框 -->
|
||||
<el-dialog
|
||||
v-model="showApiDialog"
|
||||
title="API配置"
|
||||
width="500px"
|
||||
>
|
||||
<el-form :model="apiSettings" label-width="100px">
|
||||
<el-form-item label="API地址">
|
||||
<el-input v-model="apiSettings.baseURL" placeholder="http://localhost:3000" />
|
||||
</el-form-item>
|
||||
<el-form-item label="API密钥">
|
||||
<el-input v-model="apiSettings.apiKey" type="password" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="模型名称">
|
||||
<el-input v-model="apiSettings.model" placeholder="gpt-3.5-turbo" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showApiDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSaveApiSettings">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Setting, Brush, ChatDotRound, Operation } from '@element-plus/icons-vue'
|
||||
import { useSettingsStore } from '@/stores'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const showApiDialog = ref(false)
|
||||
|
||||
// 设置数据
|
||||
const settings = reactive({
|
||||
theme: 'light',
|
||||
fontSize: 14,
|
||||
showMessageBubbles: true,
|
||||
autoSave: true,
|
||||
streamResponse: true,
|
||||
maxTokens: 1024
|
||||
})
|
||||
|
||||
// API设置
|
||||
const apiSettings = reactive({
|
||||
baseURL: '',
|
||||
apiKey: '',
|
||||
model: ''
|
||||
})
|
||||
|
||||
// 字体大小标记
|
||||
const fontSizeMarks = {
|
||||
12: '12px',
|
||||
14: '14px',
|
||||
16: '16px',
|
||||
18: '18px',
|
||||
20: '20px'
|
||||
}
|
||||
|
||||
// 处理主题变更
|
||||
const handleThemeChange = (theme) => {
|
||||
settingsStore.setTheme(theme)
|
||||
ElMessage.success(`已切换到${theme === 'dark' ? '深色' : '浅色'}主题`)
|
||||
}
|
||||
|
||||
// 处理字体大小变更
|
||||
const handleFontSizeChange = (size) => {
|
||||
settingsStore.setFontSize(size)
|
||||
ElMessage.success(`字体大小已设置为 ${size}px`)
|
||||
}
|
||||
|
||||
// 处理消息气泡变更
|
||||
const handleBubbleChange = (show) => {
|
||||
settingsStore.setShowMessageBubbles(show)
|
||||
ElMessage.success(show ? '已启用消息气泡' : '已禁用消息气泡')
|
||||
}
|
||||
|
||||
// 处理自动保存变更
|
||||
const handleAutoSaveChange = (autoSave) => {
|
||||
settingsStore.setAutoSave(autoSave)
|
||||
ElMessage.success(autoSave ? '已启用自动保存' : '已禁用自动保存')
|
||||
}
|
||||
|
||||
// 处理流式输出变更
|
||||
const handleStreamChange = (stream) => {
|
||||
settingsStore.setStreamResponse(stream)
|
||||
ElMessage.success(stream ? '已启用流式输出' : '已禁用流式输出')
|
||||
}
|
||||
|
||||
// 处理最大token变更
|
||||
const handleMaxTokensChange = (maxTokens) => {
|
||||
settingsStore.setMaxTokens(maxTokens)
|
||||
const lengthText = maxTokens === 512 ? '短' : maxTokens === 1024 ? '中' : '长'
|
||||
ElMessage.success(`回复长度已设置为${lengthText}`)
|
||||
}
|
||||
|
||||
// 保存API设置
|
||||
const handleSaveApiSettings = () => {
|
||||
settingsStore.setApiSettings(apiSettings)
|
||||
showApiDialog.value = false
|
||||
ElMessage.success('API设置已保存')
|
||||
}
|
||||
|
||||
// 清除缓存
|
||||
const handleClearCache = () => {
|
||||
ElMessageBox.confirm(
|
||||
'确定要清除所有缓存数据吗?这将清除所有本地存储的设置和历史记录。',
|
||||
'警告',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
settingsStore.clearCache()
|
||||
ElMessage.success('缓存已清除')
|
||||
// 重新加载页面
|
||||
window.location.reload()
|
||||
}).catch(() => {
|
||||
// 用户取消
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化设置
|
||||
onMounted(() => {
|
||||
// 加载设置
|
||||
Object.assign(settings, settingsStore.settings)
|
||||
Object.assign(apiSettings, settingsStore.apiSettings)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.settings-view {
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
|
||||
.settings-title {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
|
||||
.el-icon {
|
||||
font-size: 36px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.settings-subtitle {
|
||||
font-size: 16px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
|
||||
.el-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.setting-info {
|
||||
h4 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.settings-view {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
.settings-title {
|
||||
font-size: 24px;
|
||||
|
||||
.el-icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
|
||||
.setting-info {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue