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
|
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编码的请求体
|
// 解析URL编码的请求体
|
||||||
this.app.use(express.urlencoded({
|
this.app.use(express.urlencoded({
|
||||||
extended: true,
|
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 { useChatStore } from './chat'
|
||||||
export { useConversationStore } from './conversation'
|
export { useConversationStore } from './conversation'
|
||||||
export { useUserStore } from './user'
|
export { useUserStore } from './user'
|
||||||
|
export { useSettingsStore } from './settings'
|
||||||
|
|
||||||
// 如果需要,可以在这里添加全局的 store 初始化逻辑
|
// 如果需要,可以在这里添加全局的 store 初始化逻辑
|
||||||
export const initializeStores = () => {
|
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