feat(settings): 新增设置模块和API字符集编码

feat: 添加设置存储模块和设置页面
fix: 为API响应添加UTF-8字符集编码
docs: 新增项目运行和错误修复设计方案
style: 添加自动导入类型定义文件
This commit is contained in:
dzq 2025-08-23 15:24:14 +08:00
parent 6422aa85ee
commit 67939c108b
9 changed files with 10991 additions and 541 deletions

View File

@ -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响应时间测试
- 并发请求处理测试
- 内存使用监控

View File

@ -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 命令行工具]
```

View File

@ -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,

89
frontend/auto-imports.d.ts vendored Normal file
View File

@ -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')
}

37
frontend/components.d.ts vendored Normal file
View File

@ -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']
}
}

View File

@ -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 = () => {

View File

@ -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
}
})

View File

@ -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>

10044
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff