feat: 自动提交 - 周一 2025/09/22 16:28:00.19

This commit is contained in:
赵杰
2025-09-22 16:28:00 +01:00
parent f75176ec69
commit d6c88d87dd
58 changed files with 1197 additions and 11922 deletions

View File

@@ -1,218 +0,0 @@
# AI建议权限问题最终解决方案
## 🔍 问题确认
通过深度诊断工具确认:
-**访问令牌获取正常**tenant_access_token获取成功
-**表格访问正常**:可以成功访问表格和字段信息
-**记录读取正常**:可以成功读取表格记录
-**AI建议字段存在**:确认表格中有"AI建议"字段
-**记录写入失败**403 Forbidden权限不足
**根本原因**:飞书应用缺少**记录写入权限**
## 🛠️ 最终解决方案
### 步骤1飞书开放平台权限配置
#### 1.1 登录飞书开放平台
1. 访问https://open.feishu.cn/
2. 使用管理员账号登录
3. 进入"应用管理" → "我的应用"
#### 1.2 找到您的应用
- 应用ID`cli_a8b50ec0eed1500d`
- 应用名称您的TSP智能助手应用
#### 1.3 添加必需权限
在"权限管理"页面,确保应用具有以下权限:
**核心权限**
```
bitable:app # 多维表格应用权限
bitable:app:readonly # 多维表格只读权限
bitable:app:readwrite # 多维表格读写权限 ⭐ 关键权限
base:record:write # 记录写入权限 ⭐ 关键权限
```
#### 1.4 重新发布应用
- 权限修改后,点击"发布"或"上线"
- 等待权限生效通常需要1-5分钟
### 步骤2飞书多维表格协作者权限
#### 2.1 打开飞书多维表格
- 使用浏览器打开您的飞书多维表格
- 确保您有表格的管理权限
#### 2.2 添加应用为协作者
1. 点击表格右上角的"分享"按钮
2. 点击"添加协作者"
3. 搜索您的飞书应用名称或应用ID
4. 将应用添加为协作者
#### 2.3 设置协作者权限
**重要**:将权限设置为以下之一:
- **编辑者** ✅ (推荐)
- **管理员** ✅ (完全权限)
**不要设置为**
- **查看者** ❌ (只能读取,无法写入)
### 步骤3验证修复结果
#### 3.1 使用Web界面验证
1. 打开TSP智能助手主页面
2. 点击"飞书同步"标签页
3. 点击"权限检查"按钮
4. 查看检查结果
#### 3.2 测试AI建议功能
1. 点击"同步+AI建议"按钮
2. 查看是否还有403错误
3. 检查飞书表格中是否出现AI建议
## 📋 详细修复步骤
### 🔧 飞书开放平台操作
1. **登录飞书开放平台**
```
URL: https://open.feishu.cn/
账号: 管理员账号
```
2. **进入应用管理**
```
路径: 应用管理 → 我的应用
应用ID: cli_a8b50ec0eed1500d
```
3. **权限配置**
```
页面: 权限管理
操作: 添加权限
权限列表:
- bitable:app
- bitable:app:readonly
- bitable:app:readwrite ⭐
- base:record:write ⭐
```
4. **发布应用**
```
操作: 点击"发布"按钮
等待: 1-5分钟权限生效
```
### 🔧 飞书多维表格操作
1. **打开表格**
```
方式: 浏览器访问飞书多维表格
权限: 确保有管理权限
```
2. **添加协作者**
```
操作: 点击右上角"分享"按钮
步骤: 添加协作者 → 搜索应用名称
应用: 您的TSP智能助手应用
```
3. **设置权限**
```
权限级别: 编辑者 或 管理员
不要选择: 查看者
保存: 确认设置
```
## 🚨 常见问题解决
### 问题1找不到权限设置
**解决方案**
- 确保使用管理员账号登录飞书开放平台
- 检查应用是否已发布
- 联系飞书技术支持
### 问题2权限添加后仍然失败
**解决方案**
- 等待5-10分钟让权限生效
- 重新发布应用
- 清除浏览器缓存后重试
### 问题3找不到应用名称
**解决方案**
- 使用应用ID`cli_a8b50ec0eed1500d`
- 在飞书开放平台搜索应用ID
- 确认应用状态为"已发布"
### 问题4表格分享设置找不到
**解决方案**
- 确保您有表格的管理权限
- 使用表格创建者账号
- 联系表格管理员协助设置
## 📊 权限配置检查清单
### ✅ 飞书开放平台
- [ ] 应用已启用并发布
- [ ] 已添加`bitable:app`权限
- [ ] 已添加`bitable:app:readonly`权限
- [ ] 已添加`bitable:app:readwrite`权限 ⭐
- [ ] 已添加`base:record:write`权限 ⭐
- [ ] 权限修改后已重新发布
### ✅ 飞书多维表格
- [ ] 应用已添加为表格协作者
- [ ] 协作者权限设置为"编辑者"或"管理员"
- [ ] 表格未被锁定或设置为只读
- [ ] "AI建议"字段存在且类型正确
## 🎯 验证成功标志
修复成功后,您应该看到:
1. **权限检查通过**
```
✅ 访问令牌获取成功
✅ 表格访问权限正常
✅ 记录读取权限正常
✅ 记录写入权限正常
✅ AI建议字段存在
```
2. **AI建议功能正常**
```
- 点击"同步+AI建议"无403错误
- 飞书表格中出现AI建议内容
- 日志显示"更新飞书AI建议成功"
```
3. **系统日志正常**
```
2025-09-22 XX:XX:XX - INFO - 更新飞书AI建议成功
2025-09-22 XX:XX:XX - INFO - 飞书同步完成
```
## 📞 技术支持
如果按照以上步骤仍然无法解决问题,请:
1. **收集信息**
- 飞书应用ID`cli_a8b50ec0eed1500d`
- 表格ID`tblnl3vJPpgMTSiP`
- 完整的错误日志
- 权限检查结果截图
2. **联系支持**
- 飞书开放平台技术支持
- TSP智能助手技术支持
3. **检查企业设置**
- 确认是否有企业级权限限制
- 联系企业飞书管理员
---
**重要提醒**403权限错误通常需要飞书管理员权限才能解决建议联系相关技术人员协助配置。修复完成后AI建议功能将可以正常工作🎉

View File

@@ -1,259 +0,0 @@
# TSP智能助手 Linux 使用说明
## 🐧 Linux环境部署指南
### 1. 系统要求
- **操作系统**: Ubuntu 18.04+, CentOS 7+, Debian 9+, Arch Linux
- **Node.js**: 18.x 或更高版本
- **Python**: 3.7 或更高版本
- **内存**: 至少 2GB RAM
- **磁盘**: 至少 1GB 可用空间
### 2. 快速开始
#### 方法一:一键安装(推荐)
```bash
# 下载并运行安装脚本
chmod +x install_dependencies.sh
./install_dependencies.sh
```
#### 方法二:手动安装
```bash
# 安装Node.js (Ubuntu/Debian)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
# 安装Node.js (CentOS/RHEL)
curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -
sudo yum install -y nodejs
# 安装Python依赖
pip3 install -r requirements.txt
# 安装前端依赖
cd frontend
npm install
cd ..
```
### 3. 启动服务
#### 启动传统版本(立即可用)
```bash
chmod +x start_traditional.sh
./start_traditional.sh
```
访问http://localhost:5000
#### 启动现代化前端需要Node.js
```bash
chmod +x start_frontend.sh
./start_frontend.sh
```
访问http://localhost:3000
#### 构建生产版本
```bash
chmod +x build_frontend.sh
./build_frontend.sh
```
### 4. 功能对比
| 功能 | 传统版本 | 现代化前端 |
|------|----------|------------|
| 基础功能 | ✅ 完整支持 | ✅ 完整支持 |
| 聊天系统 | ✅ 支持 | ✅ 统一组件 |
| 预警管理 | ✅ 支持 | ✅ 增强体验 |
| 国际化 | ❌ 仅中文 | ✅ 中英文切换 |
| 主题切换 | ❌ 固定主题 | ✅ 暗色/亮色 |
| 响应式设计 | ⚠️ 基础支持 | ✅ 完整支持 |
| 开发体验 | ⚠️ 传统开发 | ✅ 现代化开发 |
### 5. 常见问题
#### Q: Node.js安装失败
```bash
# 检查Node.js版本
node --version
npm --version
# 如果版本过低,重新安装
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
```
#### Q: Python依赖安装失败
```bash
# 升级pip
pip3 install --upgrade pip
# 重新安装依赖
pip3 install -r requirements.txt
```
#### Q: 前端依赖安装失败
```bash
# 清理缓存
npm cache clean --force
# 删除node_modules重新安装
rm -rf frontend/node_modules
cd frontend
npm install
```
#### Q: 端口被占用
```bash
# 查看端口占用
sudo netstat -tlnp | grep :5000
sudo netstat -tlnp | grep :3000
# 杀死占用进程
sudo kill -9 <PID>
```
### 6. 开发环境配置
#### 使用VS Code开发
```bash
# 安装VS Code
wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > packages.microsoft.gpg
sudo install -o root -g root -m 644 packages.microsoft.gpg /etc/apt/trusted.gpg.d/
sudo sh -c 'echo "deb [arch=amd64,arm64,armhf signed-by=/etc/apt/trusted.gpg.d/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main" > /etc/apt/sources.list.d/vscode.list'
sudo apt update
sudo apt install code
# 安装推荐扩展
code --install-extension ms-python.python
code --install-extension vue.volar
code --install-extension bradlc.vscode-tailwindcss
```
#### 使用Docker开发
```bash
# 创建Dockerfile
cat > Dockerfile << EOF
FROM node:20-alpine
WORKDIR /app
COPY frontend/package*.json ./
RUN npm install
COPY frontend/ .
EXPOSE 3000
CMD ["npm", "run", "dev"]
EOF
# 构建并运行
docker build -t tsp-frontend .
docker run -p 3000:3000 tsp-frontend
```
### 7. 生产部署
#### 使用Nginx反向代理
```bash
# 安装Nginx
sudo apt install nginx
# 配置Nginx
sudo tee /etc/nginx/sites-available/tsp-assistant << EOF
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
}
location /api/ {
proxy_pass http://localhost:5000;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
}
}
EOF
# 启用站点
sudo ln -s /etc/nginx/sites-available/tsp-assistant /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
```
#### 使用PM2进程管理
```bash
# 安装PM2
npm install -g pm2
# 启动应用
pm2 start src/web/app.py --name "tsp-backend" --interpreter python3
pm2 start frontend/package.json --name "tsp-frontend"
# 保存配置
pm2 save
pm2 startup
```
### 8. 监控和日志
```bash
# 查看应用状态
pm2 status
# 查看日志
pm2 logs tsp-backend
pm2 logs tsp-frontend
# 重启应用
pm2 restart tsp-backend
pm2 restart tsp-frontend
```
### 9. 备份和恢复
```bash
# 备份数据库
cp tsp_assistant.db tsp_assistant_backup_$(date +%Y%m%d).db
# 备份配置文件
tar -czf config_backup_$(date +%Y%m%d).tar.gz config/
# 恢复数据库
cp tsp_assistant_backup_20240101.db tsp_assistant.db
```
---
## 🚀 快速命令参考
```bash
# 一键启动传统版本
./start_traditional.sh
# 一键启动现代化前端
./start_frontend.sh
# 构建生产版本
./build_frontend.sh
# 安装所有依赖
./install_dependencies.sh
```
现在您可以在Linux环境中愉快地使用TSP智能助手了🎉

View File

@@ -1,54 +0,0 @@
@echo off
echo 构建TSP智能助手前端...
echo.
cd frontend
echo 检查Node.js环境...
node --version >nul 2>&1
if %errorlevel% neq 0 (
echo 错误: 未找到Node.js请先安装Node.js
echo 下载地址: https://nodejs.org/
pause
exit /b 1
)
echo 检查npm环境...
npm --version >nul 2>&1
if %errorlevel% neq 0 (
echo 错误: 未找到npm请检查Node.js安装
pause
exit /b 1
)
echo 检查依赖包...
if not exist "node_modules" (
echo 安装依赖包...
npm install
if %errorlevel% neq 0 (
echo 错误: 依赖包安装失败
pause
exit /b 1
)
)
echo 运行类型检查...
npm run type-check
if %errorlevel% neq 0 (
echo 警告: 类型检查失败,但继续构建...
)
echo 构建生产版本...
npm run build
if %errorlevel% neq 0 (
echo 错误: 构建失败
pause
exit /b 1
)
echo.
echo 构建完成!
echo 构建文件已输出到: src/web/static/dist
echo.
pause

View File

@@ -1,82 +0,0 @@
#!/bin/bash
echo "构建TSP智能助手前端..."
echo
# 检查Node.js环境
echo "检查Node.js环境..."
if ! command -v node &> /dev/null; then
echo "错误: 未找到Node.js请先安装Node.js"
echo "安装命令:"
echo " Ubuntu/Debian: sudo apt update && sudo apt install nodejs npm"
echo " CentOS/RHEL: sudo yum install nodejs npm"
echo " 或访问: https://nodejs.org/"
exit 1
fi
# 检查npm环境
echo "检查npm环境..."
if ! command -v npm &> /dev/null; then
echo "错误: 未找到npm请检查Node.js安装"
exit 1
fi
echo "Node.js版本: $(node --version)"
echo "npm版本: $(npm --version)"
echo
# 检查Node.js版本兼容性
NODE_VERSION=$(node --version | cut -d'v' -f2 | cut -d'.' -f1)
if [ "$NODE_VERSION" -ge 22 ]; then
echo "检测到Node.js v22+,使用兼容性构建模式..."
SKIP_TYPE_CHECK=true
else
echo "使用标准构建模式..."
SKIP_TYPE_CHECK=false
fi
# 进入前端目录
cd frontend
# 检查依赖包
echo "检查依赖包..."
if [ ! -d "node_modules" ]; then
echo "安装依赖包..."
npm install
if [ $? -ne 0 ]; then
echo "错误: 依赖包安装失败"
exit 1
fi
fi
# 运行类型检查根据Node.js版本决定
if [ "$SKIP_TYPE_CHECK" = true ]; then
echo "跳过类型检查Node.js v22+兼容性模式)..."
else
echo "运行类型检查..."
npm run type-check
if [ $? -ne 0 ]; then
echo "警告: 类型检查失败,但继续构建..."
fi
fi
# 构建生产版本
echo "构建生产版本..."
if [ "$SKIP_TYPE_CHECK" = true ]; then
# 直接使用Vite构建跳过vue-tsc
echo "使用Vite直接构建跳过TypeScript检查..."
npx vite build
else
# 标准构建流程
npm run build
fi
if [ $? -ne 0 ]; then
echo "错误: 构建失败"
exit 1
fi
echo
echo "构建完成!"
echo "构建文件已输出到: ../src/web/static/dist"
echo

View File

@@ -307,17 +307,17 @@
},
"field_priorities": {
"order_id": 3,
"description": 1,
"category": 1,
"priority": 1,
"status": 1,
"created_at": 1,
"source": 2,
"solution": 2,
"resolution": 2,
"created_by": 2,
"vehicle_type": 2,
"vin_sim": 2,
"description": 3,
"category": 3,
"priority": 3,
"status": 3,
"created_at": 3,
"source": 3,
"solution": 3,
"resolution": 3,
"created_by": 3,
"vehicle_type": 3,
"vin_sim": 3,
"module": 3,
"wilfulness": 3,
"date_of_close": 3,
@@ -327,7 +327,7 @@
"has_updated_same_day": 3,
"operating_time": 3,
"ai_suggestion": 3,
"updated_at": 2
"updated_at": 3
},
"auto_mapping_enabled": true,
"similarity_threshold": 0.6

View File

@@ -1,15 +0,0 @@
@echo off
echo 下载便携版Node.js...
echo.
echo 正在下载Node.js便携版...
echo 请手动下载Node.js便携版
echo 1. 访问: https://nodejs.org/dist/v20.10.0/node-v20.10.0-win-x64.zip
echo 2. 下载并解压到 frontend\nodejs\ 目录
echo 3. 确保解压后的目录结构为: frontend\nodejs\node.exe
echo.
echo 下载完成后,请运行: .\start_frontend_portable.bat
echo.
pause

View File

@@ -1,211 +0,0 @@
# TSP智能助手前端
基于 Vue 3 + TypeScript + Element Plus 的现代化前端应用。
## 技术栈
- **Vue 3** - 渐进式 JavaScript 框架
- **TypeScript** - JavaScript 的超集,提供类型安全
- **Element Plus** - Vue 3 组件库
- **Vite** - 快速构建工具
- **Vue Router** - 官方路由管理器
- **Pinia** - 状态管理库
- **Vue I18n** - 国际化解决方案
- **Socket.IO** - 实时通信
- **ECharts** - 数据可视化
## 项目结构
```
frontend/
├── src/
│ ├── components/ # 公共组件
│ │ └── ChatWidget.vue # 聊天组件
│ ├── views/ # 页面组件
│ │ ├── Dashboard.vue # 仪表板
│ │ ├── Chat.vue # 聊天页面
│ │ ├── Alerts.vue # 预警管理
│ │ ├── AlertRules.vue # 预警规则
│ │ ├── Knowledge.vue # 知识库
│ │ ├── FieldMapping.vue # 字段映射
│ │ └── System.vue # 系统设置
│ ├── stores/ # 状态管理
│ │ ├── useAppStore.ts # 应用状态
│ │ ├── useChatStore.ts # 聊天状态
│ │ └── useAlertStore.ts # 预警状态
│ ├── router/ # 路由配置
│ ├── i18n/ # 国际化
│ ├── App.vue # 根组件
│ └── main.ts # 入口文件
├── package.json
├── vite.config.ts
├── tsconfig.json
└── README.md
```
## 功能特性
### 🎯 核心功能
- **统一聊天系统** - 支持首页和独立页面的聊天功能
- **预警管理** - 实时预警监控和规则管理
- **知识库管理** - 智能知识库的增删改查
- **字段映射** - 灵活的字段映射配置
- **系统监控** - 系统状态和性能监控
### 🌍 国际化支持
- 支持中文/英文切换
- 完整的国际化文本覆盖
- Element Plus 组件国际化
### 🎨 现代化UI
- 响应式设计,支持移动端
- 暗色/亮色主题切换
- 优雅的动画效果
- 统一的视觉风格
### 🔧 开发体验
- TypeScript 类型安全
- 组件自动导入
- 热重载开发
- ESLint 代码规范
## 快速开始
### 安装依赖
```bash
cd frontend
npm install
```
### 开发模式
```bash
npm run dev
```
访问 http://localhost:3000
### 构建生产版本
```bash
npm run build
```
构建文件将输出到 `../src/web/static/dist`
### 类型检查
```bash
npm run type-check
```
## 开发指南
### 添加新页面
1.`src/views/` 创建新的 Vue 组件
2.`src/router/index.ts` 添加路由配置
3.`src/i18n/locales/` 添加国际化文本
4.`src/layout/index.vue` 添加导航菜单
### 添加新组件
1.`src/components/` 创建组件
2. 使用 TypeScript 定义 props 和 emits
3. 添加必要的样式
### 状态管理
使用 Pinia 进行状态管理:
```typescript
// stores/useExampleStore.ts
import { defineStore } from 'pinia'
export const useExampleStore = defineStore('example', () => {
const state = ref('')
const action = () => {
// 状态更新逻辑
}
return { state, action }
})
```
### 国际化
在组件中使用:
```vue
<template>
<div>{{ $t('common.save') }}</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
console.log(t('common.save'))
</script>
```
## API 集成
### HTTP 请求
使用 axios 进行 API 调用:
```typescript
import axios from 'axios'
const response = await axios.get('/api/alerts')
```
### WebSocket 连接
使用 Socket.IO 进行实时通信:
```typescript
import { io } from 'socket.io-client'
const socket = io('ws://localhost:8765')
socket.on('message', (data) => {
// 处理消息
})
```
## 部署说明
### 开发环境
前端开发服务器运行在端口 3000通过代理访问后端 API
- API 请求代理到 `http://localhost:5000`
- WebSocket 代理到 `ws://localhost:8765`
### 生产环境
1. 运行 `npm run build` 构建生产版本
2. 构建文件输出到 `../src/web/static/dist`
3. 后端 Flask 应用会直接提供静态文件服务
## 浏览器支持
- Chrome >= 87
- Firefox >= 78
- Safari >= 14
- Edge >= 88
## 贡献指南
1. Fork 项目
2. 创建功能分支
3. 提交更改
4. 推送到分支
5. 创建 Pull Request
## 许可证
MIT License

View File

@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TSP智能助手</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -1,35 +0,0 @@
{
"name": "tsp-assistant-frontend",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "tsp-assistant-frontend",
"version": "1.0.0",
"type": "module",
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
"@vitejs/plugin-vue": "^4.2.3",
"axios": "^1.4.0",
"echarts": "^5.4.2",
"element-plus": "^2.3.8",
"pinia": "^2.1.6",
"sass": "^1.64.1",
"socket.io-client": "^4.7.2",
"typescript": "^5.0.2",
"unplugin-auto-import": "^0.16.6",
"unplugin-vue-components": "^0.25.1",
"vite": "^4.4.5",
"vue": "^3.3.4",
"vue-echarts": "^6.6.0",
"vue-i18n": "^9.2.2",
"vue-router": "^4.2.4",
"vue-tsc": "^1.8.5"
},
"devDependencies": {
"@types/node": "^20.4.5"
}
}
}
}

View File

@@ -1,35 +0,0 @@
{
"name": "tsp-assistant-frontend",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build-with-check": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"type-check": "vue-tsc --noEmit",
"build-safe": "vite build"
},
"dependencies": {
"vue": "^3.4.0",
"vue-router": "^4.2.5",
"pinia": "^2.1.7",
"element-plus": "^2.4.0",
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.6.0",
"vue-i18n": "^9.8.0",
"socket.io-client": "^4.7.4",
"echarts": "^5.4.3",
"vue-echarts": "^6.6.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"typescript": "^5.3.0",
"vue-tsc": "^1.8.25",
"vite": "^5.0.0",
"@types/node": "^20.10.0",
"sass": "^1.69.0",
"unplugin-auto-import": "^0.17.0",
"unplugin-vue-components": "^0.26.0"
}
}

View File

@@ -1,38 +0,0 @@
<template>
<div id="app">
<el-config-provider :locale="locale">
<router-view />
</el-config-provider>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import en from 'element-plus/dist/locale/en.mjs'
const { locale: i18nLocale } = useI18n()
const locale = computed(() => {
return i18nLocale.value === 'zh' ? zhCn : en
})
</script>
<style>
#app {
height: 100vh;
margin: 0;
padding: 0;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
}
</style>

View File

@@ -1,475 +0,0 @@
<template>
<div class="chat-widget" :class="{ 'chat-widget--expanded': isExpanded }">
<!-- 聊天按钮 -->
<el-button
v-if="!isExpanded"
type="primary"
circle
size="large"
class="chat-toggle-btn"
@click="toggleExpanded"
>
<el-icon><ChatDotRound /></el-icon>
</el-button>
<!-- 聊天窗口 -->
<div v-else class="chat-window">
<!-- 聊天头部 -->
<div class="chat-header">
<div class="chat-title">
<el-icon><Robot /></el-icon>
<span>{{ $t('chat.title') }}</span>
</div>
<div class="chat-actions">
<el-button
type="text"
size="small"
@click="toggleExpanded"
>
<el-icon><Close /></el-icon>
</el-button>
</div>
</div>
<!-- 消息列表 -->
<div class="chat-messages" ref="messagesContainer">
<div v-if="messages.length === 0" class="chat-empty">
<el-icon><ChatDotRound /></el-icon>
<p>{{ $t('chat.welcome') }}</p>
<p class="chat-empty-desc">{{ $t('chat.welcomeDesc') }}</p>
</div>
<div
v-for="message in messages"
:key="message.id"
class="chat-message"
:class="`chat-message--${message.role}`"
>
<div class="message-avatar">
<el-icon v-if="message.role === 'user'"><User /></el-icon>
<el-icon v-else-if="message.role === 'assistant'"><Robot /></el-icon>
<el-icon v-else><InfoFilled /></el-icon>
</div>
<div class="message-content">
<div class="message-text" v-html="message.content"></div>
<div class="message-time">{{ formatTime(message.timestamp) }}</div>
<!-- 元数据 -->
<div v-if="message.metadata" class="message-metadata">
<div v-if="message.metadata.knowledge_used?.length" class="knowledge-info">
<el-icon><Lightbulb /></el-icon>
<span>基于 {{ message.metadata.knowledge_used.length }} 条知识库信息生成</span>
</div>
<div v-if="message.metadata.confidence_score" class="confidence-score">
置信度: {{ (message.metadata.confidence_score * 100).toFixed(1) }}%
</div>
<div v-if="message.metadata.work_order_id" class="work-order-info">
<el-icon><Ticket /></el-icon>
<span>关联工单: {{ message.metadata.work_order_id }}</span>
</div>
</div>
</div>
</div>
<!-- 打字指示器 -->
<div v-if="isTyping" class="typing-indicator">
<div class="message-avatar">
<el-icon><Robot /></el-icon>
</div>
<div class="message-content">
<div class="typing-dots">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
</div>
<!-- 输入区域 -->
<div class="chat-input">
<div class="input-group">
<el-input
v-model="inputMessage"
:placeholder="$t('chat.inputPlaceholder')"
:disabled="!hasActiveSession"
@keyup.enter="handleSendMessage"
class="message-input"
/>
<el-button
type="primary"
:disabled="!hasActiveSession || !inputMessage.trim()"
@click="handleSendMessage"
class="send-btn"
>
<el-icon><Position /></el-icon>
</el-button>
</div>
<!-- 快速操作 -->
<div v-if="hasActiveSession" class="quick-actions">
<el-button
v-for="action in quickActions"
:key="action.key"
size="small"
@click="handleQuickAction(action.message)"
class="quick-action-btn"
>
{{ action.label }}
</el-button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, nextTick, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useChatStore } from '@/stores/useChatStore'
import { ElMessage } from 'element-plus'
const { t } = useI18n()
const chatStore = useChatStore()
// 状态
const isExpanded = ref(false)
const inputMessage = ref('')
const messagesContainer = ref<HTMLElement>()
// 计算属性
const messages = computed(() => chatStore.messages)
const isTyping = computed(() => chatStore.isTyping)
const hasActiveSession = computed(() => chatStore.hasActiveSession)
// 快速操作
const quickActions = computed(() => [
{ key: 'remoteStart', label: t('chat.quickActions.remoteStart'), message: '我的车辆无法远程启动' },
{ key: 'appDisplay', label: t('chat.quickActions.appDisplay'), message: 'APP显示车辆信息错误' },
{ key: 'bluetoothAuth', label: t('chat.quickActions.bluetoothAuth'), message: '蓝牙授权失败' },
{ key: 'unbindVehicle', label: t('chat.quickActions.unbindVehicle'), message: '如何解绑车辆' }
])
// 方法
const toggleExpanded = () => {
isExpanded.value = !isExpanded.value
}
const handleSendMessage = async () => {
if (!inputMessage.value.trim() || !hasActiveSession.value) {
return
}
const message = inputMessage.value.trim()
inputMessage.value = ''
try {
await chatStore.sendMessage(message)
} catch (error) {
ElMessage.error('发送消息失败')
console.error('发送消息失败:', error)
}
}
const handleQuickAction = async (message: string) => {
inputMessage.value = message
await handleSendMessage()
}
const formatTime = (timestamp: Date) => {
const now = new Date()
const diff = now.getTime() - timestamp.getTime()
if (diff < 60000) { // 1分钟内
return '刚刚'
} else if (diff < 3600000) { // 1小时内
return `${Math.floor(diff / 60000)}分钟前`
} else if (diff < 86400000) { // 1天内
return `${Math.floor(diff / 3600000)}小时前`
} else {
return timestamp.toLocaleDateString()
}
}
const scrollToBottom = () => {
nextTick(() => {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
}
})
}
// 监听消息变化,自动滚动到底部
watch(messages, () => {
scrollToBottom()
}, { deep: true })
// 监听打字状态变化
watch(isTyping, () => {
scrollToBottom()
})
</script>
<style scoped lang="scss">
.chat-widget {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
&--expanded {
width: 400px;
height: 600px;
background: var(--el-bg-color);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
border: 1px solid var(--el-border-color);
display: flex;
flex-direction: column;
}
}
.chat-toggle-btn {
width: 60px;
height: 60px;
font-size: 24px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.chat-window {
height: 100%;
display: flex;
flex-direction: column;
}
.chat-header {
padding: 16px;
border-bottom: 1px solid var(--el-border-color);
display: flex;
align-items: center;
justify-content: space-between;
background: var(--el-color-primary);
color: white;
border-radius: 12px 12px 0 0;
.chat-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
}
.chat-actions {
.el-button {
color: white;
}
}
}
.chat-messages {
flex: 1;
padding: 16px;
overflow-y: auto;
background: var(--el-bg-color-page);
}
.chat-empty {
text-align: center;
padding: 40px 20px;
color: var(--el-text-color-secondary);
.el-icon {
font-size: 48px;
margin-bottom: 16px;
}
p {
margin: 8px 0;
}
.chat-empty-desc {
font-size: 14px;
color: var(--el-text-color-placeholder);
}
}
.chat-message {
display: flex;
margin-bottom: 16px;
align-items: flex-start;
gap: 8px;
&--user {
flex-direction: row-reverse;
.message-content {
background: var(--el-color-primary);
color: white;
border-radius: 18px 18px 4px 18px;
}
}
&--assistant {
.message-content {
background: var(--el-bg-color);
color: var(--el-text-color-primary);
border: 1px solid var(--el-border-color);
border-radius: 18px 18px 18px 4px;
}
}
&--system {
justify-content: center;
.message-content {
background: var(--el-color-info-light-9);
color: var(--el-color-info);
border-radius: 12px;
font-size: 14px;
text-align: center;
}
}
}
.message-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--el-color-primary);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
flex-shrink: 0;
}
.message-content {
max-width: 70%;
padding: 12px 16px;
position: relative;
}
.message-text {
line-height: 1.5;
word-wrap: break-word;
}
.message-time {
font-size: 12px;
opacity: 0.7;
margin-top: 4px;
}
.message-metadata {
margin-top: 8px;
.knowledge-info,
.confidence-score,
.work-order-info {
font-size: 12px;
opacity: 0.8;
margin-top: 4px;
display: flex;
align-items: center;
gap: 4px;
}
.knowledge-info {
color: var(--el-color-info);
}
.confidence-score {
color: var(--el-color-success);
}
.work-order-info {
color: var(--el-color-warning);
}
}
.typing-indicator {
display: flex;
align-items: flex-start;
gap: 8px;
margin-bottom: 16px;
}
.typing-dots {
display: flex;
gap: 4px;
padding: 12px 16px;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color);
border-radius: 18px 18px 18px 4px;
span {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--el-color-primary);
animation: typing 1.4s infinite ease-in-out;
&:nth-child(1) { animation-delay: -0.32s; }
&:nth-child(2) { animation-delay: -0.16s; }
}
}
@keyframes typing {
0%, 80%, 100% {
transform: scale(0.8);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
.chat-input {
padding: 16px;
border-top: 1px solid var(--el-border-color);
background: var(--el-bg-color);
border-radius: 0 0 12px 12px;
}
.input-group {
display: flex;
gap: 8px;
margin-bottom: 12px;
.message-input {
flex: 1;
}
.send-btn {
flex-shrink: 0;
}
}
.quick-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.quick-action-btn {
font-size: 12px;
padding: 4px 8px;
height: auto;
}
// 暗色主题适配
:global(.dark) {
.chat-widget--expanded {
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.chat-toggle-btn {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
}
}
</style>

View File

@@ -1,17 +0,0 @@
import { createI18n } from 'vue-i18n'
import zh from './locales/zh.json'
import en from './locales/en.json'
export function setupI18n() {
const i18n = createI18n({
legacy: false,
locale: 'zh',
fallbackLocale: 'en',
messages: {
zh,
en
}
})
return i18n
}

View File

@@ -1,252 +0,0 @@
{
"common": {
"confirm": "Confirm",
"cancel": "Cancel",
"save": "Save",
"delete": "Delete",
"edit": "Edit",
"add": "Add",
"search": "Search",
"refresh": "Refresh",
"loading": "Loading...",
"success": "Success",
"error": "Error",
"warning": "Warning",
"info": "Info",
"yes": "Yes",
"no": "No",
"close": "Close",
"submit": "Submit",
"reset": "Reset",
"back": "Back",
"next": "Next",
"previous": "Previous",
"finish": "Finish",
"start": "Start",
"stop": "Stop",
"pause": "Pause",
"resume": "Resume",
"enable": "Enable",
"disable": "Disable",
"status": "Status",
"time": "Time",
"name": "Name",
"description": "Description",
"type": "Type",
"level": "Level",
"priority": "Priority",
"category": "Category",
"action": "Action",
"result": "Result",
"message": "Message",
"details": "Details",
"settings": "Settings",
"config": "Configuration",
"version": "Version",
"author": "Author",
"created": "Created",
"updated": "Updated"
},
"dashboard": {
"title": "Dashboard",
"overview": "Overview",
"systemHealth": "System Health",
"activeAlerts": "Active Alerts",
"recentActivity": "Recent Activity",
"quickActions": "Quick Actions",
"monitoring": "Monitoring",
"startMonitoring": "Start Monitoring",
"stopMonitoring": "Stop Monitoring",
"checkAlerts": "Check Alerts",
"healthScore": "Health Score",
"status": "Status",
"excellent": "Excellent",
"good": "Good",
"fair": "Fair",
"poor": "Poor",
"critical": "Critical",
"unknown": "Unknown"
},
"chat": {
"title": "Intelligent Chat",
"startChat": "Start Chat",
"endChat": "End Chat",
"sendMessage": "Send Message",
"inputPlaceholder": "Please enter your question...",
"userId": "User ID",
"workOrderId": "Work Order ID",
"workOrderIdPlaceholder": "Leave empty to auto-create",
"createWorkOrder": "Create Work Order",
"quickActions": "Quick Actions",
"sessionInfo": "Session Info",
"connectionStatus": "Connection Status",
"connected": "Connected",
"disconnected": "Disconnected",
"typing": "Assistant is thinking...",
"welcome": "Welcome to TSP Intelligent Assistant",
"welcomeDesc": "Please click 'Start Chat' button to begin chatting",
"chatStarted": "Chat started, please describe your problem.",
"chatEnded": "Chat ended.",
"workOrderCreated": "Work order created successfully! Order ID: {orderId}",
"quickActions": {
"remoteStart": "Remote Start Issue",
"appDisplay": "APP Display Issue",
"bluetoothAuth": "Bluetooth Auth Issue",
"unbindVehicle": "Unbind Vehicle"
},
"workOrder": {
"title": "Create Work Order",
"titleLabel": "Work Order Title",
"descriptionLabel": "Problem Description",
"categoryLabel": "Problem Category",
"priorityLabel": "Priority",
"categories": {
"technical": "Technical Issue",
"app": "APP Function",
"remoteControl": "Remote Control",
"vehicleBinding": "Vehicle Binding",
"other": "Other"
},
"priorities": {
"low": "Low",
"medium": "Medium",
"high": "High",
"urgent": "Urgent"
}
}
},
"alerts": {
"title": "Alert Management",
"rules": {
"title": "Alert Rules",
"addRule": "Add Rule",
"editRule": "Edit Rule",
"deleteRule": "Delete Rule",
"ruleName": "Rule Name",
"ruleType": "Alert Type",
"ruleLevel": "Alert Level",
"threshold": "Threshold",
"condition": "Condition Expression",
"checkInterval": "Check Interval (seconds)",
"cooldown": "Cooldown (seconds)",
"enabled": "Enable Rule",
"types": {
"performance": "Performance Alert",
"quality": "Quality Alert",
"volume": "Volume Alert",
"system": "System Alert",
"business": "Business Alert"
},
"levels": {
"info": "Info",
"warning": "Warning",
"error": "Error",
"critical": "Critical"
},
"presetTemplates": "Preset Templates",
"presetCategories": {
"performance": "Performance Alert Templates",
"business": "Business Alert Templates",
"system": "System Alert Templates",
"quality": "Quality Alert Templates"
}
},
"statistics": {
"critical": "Critical Alerts",
"warning": "Warning Alerts",
"info": "Info Alerts",
"total": "Total Alerts"
},
"filters": {
"all": "All Alerts",
"critical": "Critical",
"error": "Error",
"warning": "Warning",
"info": "Info"
},
"sort": {
"timeDesc": "Time Descending",
"timeAsc": "Time Ascending",
"levelDesc": "Level Descending",
"levelAsc": "Level Ascending"
},
"actions": {
"resolve": "Resolve",
"refresh": "Refresh"
},
"empty": {
"title": "No Active Alerts",
"description": "System is running normally, no alerts to handle"
}
},
"knowledge": {
"title": "Knowledge Management",
"add": "Add Knowledge",
"edit": "Edit Knowledge",
"delete": "Delete Knowledge",
"search": "Search Knowledge",
"category": "Category",
"title": "Title",
"content": "Content",
"tags": "Tags",
"status": "Status",
"actions": "Actions"
},
"fieldMapping": {
"title": "Field Mapping",
"sourceField": "Source Field",
"targetField": "Target Field",
"mappingType": "Mapping Type",
"transformation": "Transformation Rule",
"addMapping": "Add Mapping",
"editMapping": "Edit Mapping",
"deleteMapping": "Delete Mapping",
"testMapping": "Test Mapping",
"importMapping": "Import Mapping",
"exportMapping": "Export Mapping"
},
"system": {
"title": "System Settings",
"general": "General Settings",
"monitoring": "Monitoring Settings",
"alerts": "Alert Settings",
"integrations": "Integration Settings",
"backup": "Backup Settings",
"logs": "Log Management",
"performance": "Performance Monitoring",
"users": "User Management",
"permissions": "Permission Management"
},
"navigation": {
"dashboard": "Dashboard",
"chat": "Intelligent Chat",
"alerts": "Alert Management",
"knowledge": "Knowledge Base",
"fieldMapping": "Field Mapping",
"system": "System Settings",
"logout": "Logout"
},
"notifications": {
"monitoringStarted": "Monitoring service started",
"monitoringStopped": "Monitoring service stopped",
"alertsChecked": "Check completed, found {count} alerts",
"alertResolved": "Alert resolved",
"ruleCreated": "Rule created successfully",
"ruleUpdated": "Rule updated successfully",
"ruleDeleted": "Rule deleted successfully",
"workOrderCreated": "Work order created successfully",
"error": {
"startMonitoring": "Failed to start monitoring",
"stopMonitoring": "Failed to stop monitoring",
"checkAlerts": "Failed to check alerts",
"resolveAlert": "Failed to resolve alert",
"createRule": "Failed to create rule",
"updateRule": "Failed to update rule",
"deleteRule": "Failed to delete rule",
"createWorkOrder": "Failed to create work order",
"websocketConnection": "WebSocket connection failed, please check if server is running",
"requestTimeout": "Request timeout",
"networkError": "Network error"
}
}
}

View File

@@ -1,252 +0,0 @@
{
"common": {
"confirm": "确认",
"cancel": "取消",
"save": "保存",
"delete": "删除",
"edit": "编辑",
"add": "添加",
"search": "搜索",
"refresh": "刷新",
"loading": "加载中...",
"success": "成功",
"error": "错误",
"warning": "警告",
"info": "信息",
"yes": "是",
"no": "否",
"close": "关闭",
"submit": "提交",
"reset": "重置",
"back": "返回",
"next": "下一步",
"previous": "上一步",
"finish": "完成",
"start": "开始",
"stop": "停止",
"pause": "暂停",
"resume": "继续",
"enable": "启用",
"disable": "禁用",
"status": "状态",
"time": "时间",
"name": "名称",
"description": "描述",
"type": "类型",
"level": "级别",
"priority": "优先级",
"category": "分类",
"action": "操作",
"result": "结果",
"message": "消息",
"details": "详情",
"settings": "设置",
"config": "配置",
"version": "版本",
"author": "作者",
"created": "创建时间",
"updated": "更新时间"
},
"dashboard": {
"title": "仪表板",
"overview": "概览",
"systemHealth": "系统健康",
"activeAlerts": "活跃预警",
"recentActivity": "最近活动",
"quickActions": "快速操作",
"monitoring": "监控",
"startMonitoring": "启动监控",
"stopMonitoring": "停止监控",
"checkAlerts": "检查预警",
"healthScore": "健康评分",
"status": "状态",
"excellent": "优秀",
"good": "良好",
"fair": "一般",
"poor": "较差",
"critical": "严重",
"unknown": "未知"
},
"chat": {
"title": "智能对话",
"startChat": "开始对话",
"endChat": "结束对话",
"sendMessage": "发送消息",
"inputPlaceholder": "请输入您的问题...",
"userId": "用户ID",
"workOrderId": "工单ID",
"workOrderIdPlaceholder": "留空则自动创建",
"createWorkOrder": "创建工单",
"quickActions": "快速操作",
"sessionInfo": "会话信息",
"connectionStatus": "连接状态",
"connected": "已连接",
"disconnected": "未连接",
"typing": "助手正在思考中...",
"welcome": "欢迎使用TSP智能助手",
"welcomeDesc": "请点击\"开始对话\"按钮开始聊天",
"chatStarted": "对话已开始,请描述您的问题。",
"chatEnded": "对话已结束。",
"workOrderCreated": "工单创建成功!工单号: {orderId}",
"quickActions": {
"remoteStart": "远程启动问题",
"appDisplay": "APP显示问题",
"bluetoothAuth": "蓝牙授权问题",
"unbindVehicle": "解绑车辆"
},
"workOrder": {
"title": "创建工单",
"titleLabel": "工单标题",
"descriptionLabel": "问题描述",
"categoryLabel": "问题分类",
"priorityLabel": "优先级",
"categories": {
"technical": "技术问题",
"app": "APP功能",
"remoteControl": "远程控制",
"vehicleBinding": "车辆绑定",
"other": "其他"
},
"priorities": {
"low": "低",
"medium": "中",
"high": "高",
"urgent": "紧急"
}
}
},
"alerts": {
"title": "预警管理",
"rules": {
"title": "预警规则",
"addRule": "添加规则",
"editRule": "编辑规则",
"deleteRule": "删除规则",
"ruleName": "规则名称",
"ruleType": "预警类型",
"ruleLevel": "预警级别",
"threshold": "阈值",
"condition": "条件表达式",
"checkInterval": "检查间隔(秒)",
"cooldown": "冷却时间(秒)",
"enabled": "启用规则",
"types": {
"performance": "性能预警",
"quality": "质量预警",
"volume": "量级预警",
"system": "系统预警",
"business": "业务预警"
},
"levels": {
"info": "信息",
"warning": "警告",
"error": "错误",
"critical": "严重"
},
"presetTemplates": "预设模板",
"presetCategories": {
"performance": "性能预警模板",
"business": "业务预警模板",
"system": "系统预警模板",
"quality": "质量预警模板"
}
},
"statistics": {
"critical": "严重预警",
"warning": "警告预警",
"info": "信息预警",
"total": "总预警数"
},
"filters": {
"all": "全部预警",
"critical": "严重",
"error": "错误",
"warning": "警告",
"info": "信息"
},
"sort": {
"timeDesc": "时间降序",
"timeAsc": "时间升序",
"levelDesc": "级别降序",
"levelAsc": "级别升序"
},
"actions": {
"resolve": "解决",
"refresh": "刷新"
},
"empty": {
"title": "暂无活跃预警",
"description": "系统运行正常,没有需要处理的预警"
}
},
"knowledge": {
"title": "知识库管理",
"add": "添加知识",
"edit": "编辑知识",
"delete": "删除知识",
"search": "搜索知识",
"category": "分类",
"title": "标题",
"content": "内容",
"tags": "标签",
"status": "状态",
"actions": "操作"
},
"fieldMapping": {
"title": "字段映射",
"sourceField": "源字段",
"targetField": "目标字段",
"mappingType": "映射类型",
"transformation": "转换规则",
"addMapping": "添加映射",
"editMapping": "编辑映射",
"deleteMapping": "删除映射",
"testMapping": "测试映射",
"importMapping": "导入映射",
"exportMapping": "导出映射"
},
"system": {
"title": "系统设置",
"general": "常规设置",
"monitoring": "监控设置",
"alerts": "预警设置",
"integrations": "集成设置",
"backup": "备份设置",
"logs": "日志管理",
"performance": "性能监控",
"users": "用户管理",
"permissions": "权限管理"
},
"navigation": {
"dashboard": "仪表板",
"chat": "智能对话",
"alerts": "预警管理",
"knowledge": "知识库",
"fieldMapping": "字段映射",
"system": "系统设置",
"logout": "退出登录"
},
"notifications": {
"monitoringStarted": "监控服务已启动",
"monitoringStopped": "监控服务已停止",
"alertsChecked": "检查完成,发现 {count} 个预警",
"alertResolved": "预警已解决",
"ruleCreated": "规则创建成功",
"ruleUpdated": "规则更新成功",
"ruleDeleted": "规则删除成功",
"workOrderCreated": "工单创建成功",
"error": {
"startMonitoring": "启动监控失败",
"stopMonitoring": "停止监控失败",
"checkAlerts": "检查预警失败",
"resolveAlert": "解决预警失败",
"createRule": "创建规则失败",
"updateRule": "更新规则失败",
"deleteRule": "删除规则失败",
"createWorkOrder": "创建工单失败",
"websocketConnection": "WebSocket连接失败请检查服务器是否启动",
"requestTimeout": "请求超时",
"networkError": "网络错误"
}
}
}

View File

@@ -1,304 +0,0 @@
<template>
<el-container class="layout-container">
<!-- 侧边栏 -->
<el-aside :width="isCollapse ? '64px' : '200px'" class="sidebar">
<div class="logo">
<el-icon v-if="!isCollapse"><Shield /></el-icon>
<span v-if="!isCollapse">{{ $t('navigation.dashboard') }}</span>
</div>
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:unique-opened="true"
router
class="sidebar-menu"
>
<el-menu-item index="/">
<el-icon><Monitor /></el-icon>
<template #title>{{ $t('navigation.dashboard') }}</template>
</el-menu-item>
<el-menu-item index="/chat">
<el-icon><ChatDotRound /></el-icon>
<template #title>{{ $t('navigation.chat') }}</template>
</el-menu-item>
<el-sub-menu index="/alerts">
<template #title>
<el-icon><Bell /></el-icon>
<span>{{ $t('navigation.alerts') }}</span>
</template>
<el-menu-item index="/alerts">{{ $t('alerts.title') }}</el-menu-item>
<el-menu-item index="/alerts/rules">{{ $t('alerts.rules.title') }}</el-menu-item>
</el-sub-menu>
<el-menu-item index="/knowledge">
<el-icon><Document /></el-icon>
<template #title>{{ $t('navigation.knowledge') }}</template>
</el-menu-item>
<el-menu-item index="/field-mapping">
<el-icon><Connection /></el-icon>
<template #title>{{ $t('navigation.fieldMapping') }}</template>
</el-menu-item>
<el-menu-item index="/system">
<el-icon><Setting /></el-icon>
<template #title>{{ $t('navigation.system') }}</template>
</el-menu-item>
</el-menu>
</el-aside>
<!-- 主内容区 -->
<el-container>
<!-- 顶部导航 -->
<el-header class="header">
<div class="header-left">
<el-button
type="text"
@click="toggleCollapse"
class="collapse-btn"
>
<el-icon><Fold v-if="!isCollapse" /><Expand v-else /></el-icon>
</el-button>
<el-breadcrumb separator="/" class="breadcrumb">
<el-breadcrumb-item
v-for="item in breadcrumbs"
:key="item.path"
:to="item.path"
>
{{ item.title }}
</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="header-right">
<!-- 语言切换 -->
<el-dropdown @command="handleLanguageChange">
<el-button type="text" class="language-btn">
<el-icon><Globe /></el-icon>
<span>{{ currentLanguage }}</span>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="zh">中文</el-dropdown-item>
<el-dropdown-item command="en">English</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 主题切换 -->
<el-button
type="text"
@click="toggleTheme"
class="theme-btn"
>
<el-icon><Moon v-if="isDark" /><Sunny v-else /></el-icon>
</el-button>
<!-- 用户菜单 -->
<el-dropdown @command="handleUserAction">
<el-button type="text" class="user-btn">
<el-icon><User /></el-icon>
<span>Admin</span>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile">{{ $t('common.profile') }}</el-dropdown-item>
<el-dropdown-item command="settings">{{ $t('common.settings') }}</el-dropdown-item>
<el-dropdown-item divided command="logout">{{ $t('navigation.logout') }}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header>
<!-- 主内容 -->
<el-main class="main-content">
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { ElMessage } from 'element-plus'
const route = useRoute()
const router = useRouter()
const { locale } = useI18n()
// 侧边栏折叠状态
const isCollapse = ref(false)
// 主题状态
const isDark = ref(false)
// 当前语言
const currentLanguage = computed(() => {
return locale.value === 'zh' ? '中文' : 'English'
})
// 当前激活的菜单
const activeMenu = computed(() => {
return route.path
})
// 面包屑导航
const breadcrumbs = computed(() => {
const matched = route.matched.filter(item => item.meta && item.meta.title)
return matched.map(item => ({
path: item.path,
title: item.meta?.title as string
}))
})
// 切换侧边栏折叠状态
const toggleCollapse = () => {
isCollapse.value = !isCollapse.value
}
// 切换主题
const toggleTheme = () => {
isDark.value = !isDark.value
document.documentElement.classList.toggle('dark', isDark.value)
}
// 切换语言
const handleLanguageChange = (lang: string) => {
locale.value = lang
localStorage.setItem('language', lang)
ElMessage.success(lang === 'zh' ? '语言已切换为中文' : 'Language switched to English')
}
// 用户操作
const handleUserAction = (command: string) => {
switch (command) {
case 'profile':
ElMessage.info('个人资料功能开发中...')
break
case 'settings':
ElMessage.info('设置功能开发中...')
break
case 'logout':
ElMessage.info('退出登录功能开发中...')
break
}
}
// 初始化
const init = () => {
// 恢复语言设置
const savedLanguage = localStorage.getItem('language')
if (savedLanguage && ['zh', 'en'].includes(savedLanguage)) {
locale.value = savedLanguage
}
// 恢复主题设置
const savedTheme = localStorage.getItem('theme')
if (savedTheme === 'dark') {
isDark.value = true
document.documentElement.classList.add('dark')
}
}
// 监听主题变化,保存到本地存储
watch(isDark, (newVal) => {
localStorage.setItem('theme', newVal ? 'dark' : 'light')
})
init()
</script>
<style scoped lang="scss">
.layout-container {
height: 100vh;
}
.sidebar {
background-color: var(--el-bg-color);
border-right: 1px solid var(--el-border-color);
transition: width 0.3s;
.logo {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: bold;
color: var(--el-color-primary);
border-bottom: 1px solid var(--el-border-color);
.el-icon {
margin-right: 8px;
font-size: 24px;
}
}
.sidebar-menu {
border: none;
height: calc(100vh - 60px);
}
}
.header {
background-color: var(--el-bg-color);
border-bottom: 1px solid var(--el-border-color);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
.header-left {
display: flex;
align-items: center;
.collapse-btn {
margin-right: 20px;
font-size: 18px;
}
.breadcrumb {
font-size: 14px;
}
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
.language-btn,
.theme-btn,
.user-btn {
display: flex;
align-items: center;
gap: 4px;
}
}
}
.main-content {
background-color: var(--el-bg-color-page);
padding: 20px;
overflow-y: auto;
}
// 暗色主题
:global(.dark) {
.sidebar,
.header {
background-color: var(--el-bg-color);
}
.main-content {
background-color: var(--el-bg-color-page);
}
}
</style>

View File

@@ -1,27 +0,0 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import App from './App.vue'
import router from './router'
import { setupI18n } from './i18n'
const app = createApp(App)
// 注册Element Plus图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(createPinia())
app.use(router)
app.use(ElementPlus, {
locale: zhCn,
})
app.use(setupI18n())
app.mount('#app')

View File

@@ -1,61 +0,0 @@
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Layout',
component: () => import('@/layout/index.vue'),
children: [
{
path: '',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { title: 'dashboard.title' }
},
{
path: '/chat',
name: 'Chat',
component: () => import('@/views/Chat.vue'),
meta: { title: 'chat.title' }
},
{
path: '/alerts',
name: 'Alerts',
component: () => import('@/views/Alerts.vue'),
meta: { title: 'alerts.title' }
},
{
path: '/alerts/rules',
name: 'AlertRules',
component: () => import('@/views/AlertRules.vue'),
meta: { title: 'alerts.rules.title' }
},
{
path: '/knowledge',
name: 'Knowledge',
component: () => import('@/views/Knowledge.vue'),
meta: { title: 'knowledge.title' }
},
{
path: '/field-mapping',
name: 'FieldMapping',
component: () => import('@/views/FieldMapping.vue'),
meta: { title: 'fieldMapping.title' }
},
{
path: '/system',
name: 'System',
component: () => import('@/views/System.vue'),
meta: { title: 'system.title' }
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router

View File

@@ -1,286 +0,0 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import axios from 'axios'
export interface Alert {
id: number
rule_name: string
message: string
level: 'critical' | 'error' | 'warning' | 'info'
alert_type: 'performance' | 'quality' | 'volume' | 'system' | 'business'
data?: any
created_at: string
resolved_at?: string
status: 'active' | 'resolved'
}
export interface AlertRule {
name: string
description?: string
alert_type: 'performance' | 'quality' | 'volume' | 'system' | 'business'
level: 'critical' | 'error' | 'warning' | 'info'
threshold: number
condition: string
enabled: boolean
check_interval: number
cooldown: number
}
export interface SystemHealth {
health_score: number
status: 'excellent' | 'good' | 'fair' | 'poor' | 'critical' | 'unknown'
details?: any
}
export interface MonitorStatus {
monitor_status: 'running' | 'stopped' | 'unknown'
}
export const useAlertStore = defineStore('alert', () => {
// 状态
const alerts = ref<Alert[]>([])
const rules = ref<AlertRule[]>([])
const health = ref<SystemHealth>({ health_score: 0, status: 'unknown' })
const monitorStatus = ref<MonitorStatus>({ monitor_status: 'unknown' })
const loading = ref(false)
const alertFilter = ref('all')
const alertSort = ref('time-desc')
// 计算属性
const filteredAlerts = computed(() => {
let filtered = alerts.value
// 应用过滤
if (alertFilter.value !== 'all') {
filtered = filtered.filter(alert => alert.level === alertFilter.value)
}
// 应用排序
filtered.sort((a, b) => {
switch (alertSort.value) {
case 'time-desc':
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
case 'time-asc':
return new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
case 'level-desc':
const levelOrder = { 'critical': 4, 'error': 3, 'warning': 2, 'info': 1 }
return (levelOrder[b.level] || 0) - (levelOrder[a.level] || 0)
case 'level-asc':
const levelOrderAsc = { 'critical': 4, 'error': 3, 'warning': 2, 'info': 1 }
return (levelOrderAsc[a.level] || 0) - (levelOrderAsc[b.level] || 0)
default:
return 0
}
})
return filtered
})
const alertStatistics = computed(() => {
const stats = alerts.value.reduce((acc, alert) => {
acc[alert.level] = (acc[alert.level] || 0) + 1
acc.total = (acc.total || 0) + 1
return acc
}, {} as Record<string, number>)
return {
critical: stats.critical || 0,
warning: stats.warning || 0,
info: stats.info || 0,
total: stats.total || 0
}
})
// 动作
const loadAlerts = async () => {
try {
loading.value = true
const response = await axios.get('/api/alerts')
alerts.value = response.data
} catch (error) {
console.error('加载预警失败:', error)
throw error
} finally {
loading.value = false
}
}
const loadRules = async () => {
try {
loading.value = true
const response = await axios.get('/api/rules')
rules.value = response.data
} catch (error) {
console.error('加载规则失败:', error)
throw error
} finally {
loading.value = false
}
}
const loadHealth = async () => {
try {
const response = await axios.get('/api/health')
health.value = response.data
} catch (error) {
console.error('加载健康状态失败:', error)
throw error
}
}
const loadMonitorStatus = async () => {
try {
const response = await axios.get('/api/monitor/status')
monitorStatus.value = response.data
} catch (error) {
console.error('加载监控状态失败:', error)
throw error
}
}
const startMonitoring = async () => {
try {
const response = await axios.post('/api/monitor/start')
if (response.data.success) {
await loadMonitorStatus()
return true
}
return false
} catch (error) {
console.error('启动监控失败:', error)
throw error
}
}
const stopMonitoring = async () => {
try {
const response = await axios.post('/api/monitor/stop')
if (response.data.success) {
await loadMonitorStatus()
return true
}
return false
} catch (error) {
console.error('停止监控失败:', error)
throw error
}
}
const checkAlerts = async () => {
try {
const response = await axios.post('/api/check-alerts')
if (response.data.success) {
await loadAlerts()
return response.data.count
}
return 0
} catch (error) {
console.error('检查预警失败:', error)
throw error
}
}
const resolveAlert = async (alertId: number) => {
try {
const response = await axios.post(`/api/alerts/${alertId}/resolve`)
if (response.data.success) {
await loadAlerts()
return true
}
return false
} catch (error) {
console.error('解决预警失败:', error)
throw error
}
}
const createRule = async (rule: Omit<AlertRule, 'name'> & { name: string }) => {
try {
const response = await axios.post('/api/rules', rule)
if (response.data.success) {
await loadRules()
return true
}
return false
} catch (error) {
console.error('创建规则失败:', error)
throw error
}
}
const updateRule = async (originalName: string, rule: AlertRule) => {
try {
const response = await axios.put(`/api/rules/${originalName}`, rule)
if (response.data.success) {
await loadRules()
return true
}
return false
} catch (error) {
console.error('更新规则失败:', error)
throw error
}
}
const deleteRule = async (ruleName: string) => {
try {
const response = await axios.delete(`/api/rules/${ruleName}`)
if (response.data.success) {
await loadRules()
return true
}
return false
} catch (error) {
console.error('删除规则失败:', error)
throw error
}
}
const setAlertFilter = (filter: string) => {
alertFilter.value = filter
}
const setAlertSort = (sort: string) => {
alertSort.value = sort
}
const loadInitialData = async () => {
await Promise.all([
loadHealth(),
loadAlerts(),
loadRules(),
loadMonitorStatus()
])
}
return {
// 状态
alerts,
rules,
health,
monitorStatus,
loading,
alertFilter,
alertSort,
// 计算属性
filteredAlerts,
alertStatistics,
// 动作
loadAlerts,
loadRules,
loadHealth,
loadMonitorStatus,
startMonitoring,
stopMonitoring,
checkAlerts,
resolveAlert,
createRule,
updateRule,
deleteRule,
setAlertFilter,
setAlertSort,
loadInitialData
}
})

View File

@@ -1,60 +0,0 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useAppStore = defineStore('app', () => {
// 状态
const loading = ref(false)
const theme = ref<'light' | 'dark'>('light')
const language = ref<'zh' | 'en'>('zh')
const sidebarCollapsed = ref(false)
// 计算属性
const isDark = computed(() => theme.value === 'dark')
// 动作
const setLoading = (value: boolean) => {
loading.value = value
}
const setTheme = (value: 'light' | 'dark') => {
theme.value = value
document.documentElement.classList.toggle('dark', value === 'dark')
localStorage.setItem('theme', value)
}
const setLanguage = (value: 'zh' | 'en') => {
language.value = value
localStorage.setItem('language', value)
}
const toggleSidebar = () => {
sidebarCollapsed.value = !sidebarCollapsed.value
}
const init = () => {
// 恢复主题设置
const savedTheme = localStorage.getItem('theme') as 'light' | 'dark'
if (savedTheme) {
setTheme(savedTheme)
}
// 恢复语言设置
const savedLanguage = localStorage.getItem('language') as 'zh' | 'en'
if (savedLanguage) {
setLanguage(savedLanguage)
}
}
return {
loading,
theme,
language,
sidebarCollapsed,
isDark,
setLoading,
setTheme,
setLanguage,
toggleSidebar,
init
}
})

View File

@@ -1,297 +0,0 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { io, type Socket } from 'socket.io-client'
export interface ChatMessage {
id: string
role: 'user' | 'assistant' | 'system'
content: string
timestamp: Date
metadata?: {
knowledge_used?: string[]
confidence_score?: number
work_order_id?: string
}
}
export interface ChatSession {
id: string
userId: string
workOrderId?: string
messages: ChatMessage[]
status: 'active' | 'ended'
createdAt: Date
}
export const useChatStore = defineStore('chat', () => {
// 状态
const socket = ref<Socket | null>(null)
const isConnected = ref(false)
const currentSession = ref<ChatSession | null>(null)
const messages = ref<ChatMessage[]>([])
const isTyping = ref(false)
const userId = ref('user_001')
const workOrderId = ref<string>('')
// 计算属性
const hasActiveSession = computed(() =>
currentSession.value && currentSession.value.status === 'active'
)
const messageCount = computed(() => messages.value.length)
// 动作
const connectWebSocket = () => {
return new Promise<void>((resolve, reject) => {
try {
socket.value = io('ws://localhost:8765', {
transports: ['websocket']
})
socket.value.on('connect', () => {
isConnected.value = true
resolve()
})
socket.value.on('disconnect', () => {
isConnected.value = false
})
socket.value.on('error', (error) => {
console.error('WebSocket error:', error)
reject(error)
})
socket.value.on('message_response', (data) => {
handleMessageResponse(data)
})
socket.value.on('typing_start', () => {
isTyping.value = true
})
socket.value.on('typing_end', () => {
isTyping.value = false
})
} catch (error) {
reject(error)
}
})
}
const disconnectWebSocket = () => {
if (socket.value) {
socket.value.disconnect()
socket.value = null
isConnected.value = false
}
}
const startChat = async () => {
try {
if (!socket.value) {
await connectWebSocket()
}
const response = await sendSocketMessage({
type: 'create_session',
user_id: userId.value,
work_order_id: workOrderId.value ? parseInt(workOrderId.value) : null
})
if (response.type === 'session_created') {
currentSession.value = {
id: response.session_id,
userId: userId.value,
workOrderId: workOrderId.value,
messages: [],
status: 'active',
createdAt: new Date()
}
addMessage('system', '对话已开始,请描述您的问题。')
return true
}
return false
} catch (error) {
console.error('启动对话失败:', error)
throw error
}
}
const endChat = async () => {
try {
if (currentSession.value) {
await sendSocketMessage({
type: 'end_session',
session_id: currentSession.value.id
})
currentSession.value.status = 'ended'
addMessage('system', '对话已结束。')
}
} catch (error) {
console.error('结束对话失败:', error)
}
}
const sendMessage = async (content: string) => {
if (!currentSession.value || !content.trim()) {
return
}
// 添加用户消息
addMessage('user', content)
try {
const response = await sendSocketMessage({
type: 'send_message',
session_id: currentSession.value.id,
message: content
})
if (response.type === 'message_response' && response.result.success) {
const result = response.result
// 添加助手回复
addMessage('assistant', result.content, {
knowledge_used: result.knowledge_used,
confidence_score: result.confidence_score,
work_order_id: result.work_order_id
})
// 更新工单ID
if (result.work_order_id) {
workOrderId.value = result.work_order_id.toString()
}
} else {
addMessage('assistant', '抱歉,我暂时无法处理您的问题。请稍后再试。')
}
} catch (error) {
console.error('发送消息失败:', error)
addMessage('assistant', '发送消息失败,请检查网络连接。')
}
}
const createWorkOrder = async (data: {
title: string
description: string
category: string
priority: string
}) => {
if (!currentSession.value) {
throw new Error('没有活跃的会话')
}
try {
const response = await sendSocketMessage({
type: 'create_work_order',
session_id: currentSession.value.id,
...data
})
if (response.type === 'work_order_created' && response.result.success) {
workOrderId.value = response.result.work_order_id.toString()
addMessage('system', `工单创建成功!工单号: ${response.result.order_id}`)
return response.result
} else {
throw new Error(response.result.error || '创建工单失败')
}
} catch (error) {
console.error('创建工单失败:', error)
throw error
}
}
const sendSocketMessage = (message: any): Promise<any> => {
return new Promise((resolve, reject) => {
if (!socket.value) {
reject(new Error('WebSocket未连接'))
return
}
const messageId = 'msg_' + Date.now()
message.messageId = messageId
const timeout = setTimeout(() => {
reject(new Error('请求超时'))
}, 10000)
const handleResponse = (data: any) => {
if (data.messageId === messageId) {
clearTimeout(timeout)
socket.value?.off('message_response', handleResponse)
resolve(data)
}
}
socket.value.on('message_response', handleResponse)
socket.value.emit('message', message)
})
}
const addMessage = (role: 'user' | 'assistant' | 'system', content: string, metadata?: any) => {
const message: ChatMessage = {
id: 'msg_' + Date.now() + '_' + Math.random(),
role,
content,
timestamp: new Date(),
metadata
}
messages.value.push(message)
if (currentSession.value) {
currentSession.value.messages.push(message)
}
}
const clearMessages = () => {
messages.value = []
if (currentSession.value) {
currentSession.value.messages = []
}
}
const setUserId = (id: string) => {
userId.value = id
}
const setWorkOrderId = (id: string) => {
workOrderId.value = id
}
const handleMessageResponse = (data: any) => {
// 处理WebSocket消息响应
console.log('收到消息响应:', data)
}
return {
// 状态
socket,
isConnected,
currentSession,
messages,
isTyping,
userId,
workOrderId,
// 计算属性
hasActiveSession,
messageCount,
// 动作
connectWebSocket,
disconnectWebSocket,
startChat,
endChat,
sendMessage,
createWorkOrder,
addMessage,
clearMessages,
setUserId,
setWorkOrderId
}
})

View File

@@ -1,561 +0,0 @@
<template>
<div class="alert-rules-page">
<!-- 页面头部 -->
<div class="page-header">
<h2>{{ $t('alerts.rules.title') }}</h2>
<div class="header-actions">
<el-button type="primary" @click="showAddRuleModal = true">
<el-icon><Plus /></el-icon>
{{ $t('alerts.rules.addRule') }}
</el-button>
<el-button type="success" @click="showPresetModal = true">
<el-icon><Magic /></el-icon>
{{ $t('alerts.rules.presetTemplates') }}
</el-button>
</div>
</div>
<!-- 规则列表 -->
<el-card>
<template #header>
<div class="card-header">
<el-icon><Setting /></el-icon>
<span>预警规则管理</span>
</div>
</template>
<div v-if="loading" class="loading-container">
<el-skeleton :rows="5" animated />
</div>
<div v-else-if="rules.length === 0" class="empty-state">
<el-empty description="暂无规则">
<el-button type="primary" @click="showAddRuleModal = true">
{{ $t('alerts.rules.addRule') }}
</el-button>
</el-empty>
</div>
<div v-else class="rules-table">
<el-table :data="rules" stripe>
<el-table-column prop="name" :label="$t('alerts.rules.ruleName')" />
<el-table-column prop="alert_type" :label="$t('alerts.rules.ruleType')">
<template #default="{ row }">
{{ $t(`alerts.rules.types.${row.alert_type}`) }}
</template>
</el-table-column>
<el-table-column prop="level" :label="$t('alerts.rules.ruleLevel')">
<template #default="{ row }">
<el-tag :type="getAlertTagType(row.level)">
{{ $t(`alerts.rules.levels.${row.level}`) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="threshold" :label="$t('alerts.rules.threshold')" />
<el-table-column prop="enabled" :label="$t('common.status')">
<template #default="{ row }">
<el-tag :type="row.enabled ? 'success' : 'info'">
{{ row.enabled ? $t('common.enable') : $t('common.disable') }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('common.action')" width="150">
<template #default="{ row }">
<el-button
type="primary"
size="small"
@click="handleEditRule(row)"
>
<el-icon><Edit /></el-icon>
{{ $t('common.edit') }}
</el-button>
<el-button
type="danger"
size="small"
@click="handleDeleteRule(row.name)"
>
<el-icon><Delete /></el-icon>
{{ $t('common.delete') }}
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-card>
<!-- 添加/编辑规则模态框 -->
<el-dialog
v-model="showAddRuleModal"
:title="editingRule ? $t('alerts.rules.editRule') : $t('alerts.rules.addRule')"
width="600px"
>
<el-form :model="ruleForm" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="$t('alerts.rules.ruleName')" required>
<el-input v-model="ruleForm.name" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('alerts.rules.ruleType')" required>
<el-select v-model="ruleForm.alert_type">
<el-option
v-for="(label, key) in alertTypes"
:key="key"
:label="label"
:value="key"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="$t('alerts.rules.ruleLevel')" required>
<el-select v-model="ruleForm.level">
<el-option
v-for="(label, key) in alertLevels"
:key="key"
:label="label"
:value="key"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('alerts.rules.threshold')" required>
<el-input-number v-model="ruleForm.threshold" :precision="2" />
</el-form-item>
</el-col>
</el-row>
<el-form-item :label="$t('common.description')">
<el-input
v-model="ruleForm.description"
type="textarea"
:rows="2"
/>
</el-form-item>
<el-form-item :label="$t('alerts.rules.condition')" required>
<el-input
v-model="ruleForm.condition"
placeholder="例如: satisfaction_avg < threshold"
/>
</el-form-item>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item :label="$t('alerts.rules.checkInterval')">
<el-input-number v-model="ruleForm.check_interval" :min="60" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :label="$t('alerts.rules.cooldown')">
<el-input-number v-model="ruleForm.cooldown" :min="60" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :label="$t('alerts.rules.enabled')">
<el-switch v-model="ruleForm.enabled" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="showAddRuleModal = false">
{{ $t('common.cancel') }}
</el-button>
<el-button
type="primary"
:loading="loading"
@click="handleSaveRule"
>
{{ $t('common.save') }}
</el-button>
</template>
</el-dialog>
<!-- 预设模板模态框 -->
<el-dialog
v-model="showPresetModal"
:title="$t('alerts.rules.presetTemplates')"
width="800px"
>
<div class="preset-templates">
<div
v-for="category in presetCategories"
:key="category.key"
class="preset-category"
>
<h4>{{ category.title }}</h4>
<div class="preset-grid">
<div
v-for="template in category.templates"
:key="template.key"
class="preset-card"
@click="handleSelectPreset(template)"
>
<div class="preset-icon">
<el-icon><component :is="template.icon" /></el-icon>
</div>
<div class="preset-content">
<h5>{{ template.name }}</h5>
<p>{{ template.description }}</p>
<div class="preset-tags">
<el-tag :type="getAlertTagType(template.level)" size="small">
{{ $t(`alerts.rules.levels.${template.level}`) }}
</el-tag>
<el-tag type="info" size="small">
{{ $t(`alerts.rules.types.${template.type}`) }}
</el-tag>
</div>
</div>
</div>
</div>
</div>
</div>
<template #footer>
<el-button @click="showPresetModal = false">
{{ $t('common.close') }}
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAlertStore } from '@/stores/useAlertStore'
import { ElMessage, ElMessageBox } from 'element-plus'
const { t } = useI18n()
const alertStore = useAlertStore()
// 状态
const loading = ref(false)
const showAddRuleModal = ref(false)
const showPresetModal = ref(false)
const editingRule = ref<any>(null)
// 规则表单
const ruleForm = ref({
name: '',
description: '',
alert_type: 'performance',
level: 'warning',
threshold: 0,
condition: '',
enabled: true,
check_interval: 300,
cooldown: 3600
})
// 计算属性
const rules = computed(() => alertStore.rules)
// 选项
const alertTypes = computed(() => ({
performance: t('alerts.rules.types.performance'),
quality: t('alerts.rules.types.quality'),
volume: t('alerts.rules.types.volume'),
system: t('alerts.rules.types.system'),
business: t('alerts.rules.types.business')
}))
const alertLevels = computed(() => ({
info: t('alerts.rules.levels.info'),
warning: t('alerts.rules.levels.warning'),
error: t('alerts.rules.levels.error'),
critical: t('alerts.rules.levels.critical')
}))
// 预设模板
const presetCategories = computed(() => [
{
key: 'performance',
title: t('alerts.rules.presetCategories.performance'),
templates: [
{
key: 'response_time',
name: '响应时间预警',
description: 'API响应时间超过阈值',
icon: 'Clock',
level: 'warning',
type: 'performance',
data: {
alert_type: 'performance',
level: 'warning',
threshold: 2.0,
condition: 'response_time > threshold',
check_interval: 300,
cooldown: 3600
}
},
{
key: 'cpu_usage',
name: 'CPU使用率预警',
description: 'CPU使用率过高',
icon: 'Cpu',
level: 'critical',
type: 'performance',
data: {
alert_type: 'performance',
level: 'critical',
threshold: 80,
condition: 'cpu_usage > threshold',
check_interval: 300,
cooldown: 3600
}
}
]
},
{
key: 'business',
title: t('alerts.rules.presetCategories.business'),
templates: [
{
key: 'satisfaction_low',
name: '满意度预警',
description: '用户满意度低于阈值',
icon: 'Smile',
level: 'warning',
type: 'business',
data: {
alert_type: 'business',
level: 'warning',
threshold: 3.0,
condition: 'satisfaction_avg < threshold',
check_interval: 300,
cooldown: 3600
}
}
]
}
])
// 方法
const loadRules = async () => {
loading.value = true
try {
await alertStore.loadRules()
} catch (error) {
ElMessage.error('加载规则失败')
} finally {
loading.value = false
}
}
const handleSaveRule = async () => {
if (!ruleForm.value.name || !ruleForm.value.condition) {
ElMessage.warning('请填写规则名称和条件表达式')
return
}
loading.value = true
try {
let success = false
if (editingRule.value) {
success = await alertStore.updateRule(editingRule.value.name, ruleForm.value)
} else {
success = await alertStore.createRule(ruleForm.value)
}
if (success) {
ElMessage.success(editingRule.value ? '规则更新成功' : '规则创建成功')
showAddRuleModal.value = false
resetForm()
} else {
ElMessage.error(editingRule.value ? '规则更新失败' : '规则创建失败')
}
} catch (error) {
ElMessage.error(editingRule.value ? '规则更新失败' : '规则创建失败')
} finally {
loading.value = false
}
}
const handleEditRule = (rule: any) => {
editingRule.value = rule
ruleForm.value = { ...rule }
showAddRuleModal.value = true
}
const handleDeleteRule = async (ruleName: string) => {
try {
await ElMessageBox.confirm(
`确定要删除规则 "${ruleName}" 吗?`,
'确认删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
const success = await alertStore.deleteRule(ruleName)
if (success) {
ElMessage.success('规则删除成功')
} else {
ElMessage.error('规则删除失败')
}
} catch (error) {
// 用户取消删除
}
}
const handleSelectPreset = (template: any) => {
ruleForm.value = {
name: template.name,
description: template.description,
...template.data
}
showPresetModal.value = false
showAddRuleModal.value = true
}
const resetForm = () => {
ruleForm.value = {
name: '',
description: '',
alert_type: 'performance',
level: 'warning',
threshold: 0,
condition: '',
enabled: true,
check_interval: 300,
cooldown: 3600
}
editingRule.value = null
}
const getAlertTagType = (level: string) => {
const types = {
critical: 'danger',
error: 'danger',
warning: 'warning',
info: 'info'
}
return types[level as keyof typeof types] || 'info'
}
// 生命周期
onMounted(() => {
loadRules()
})
</script>
<style scoped lang="scss">
.alert-rules-page {
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h2 {
margin: 0;
color: var(--el-text-color-primary);
}
.header-actions {
display: flex;
gap: 12px;
}
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
}
.loading-container {
padding: 20px;
}
.empty-state {
padding: 40px 20px;
}
.rules-table {
.el-table {
.el-button {
margin-right: 8px;
}
}
}
.preset-templates {
.preset-category {
margin-bottom: 32px;
h4 {
margin: 0 0 16px 0;
color: var(--el-text-color-primary);
font-size: 16px;
font-weight: 600;
}
.preset-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
.preset-card {
border: 1px solid var(--el-border-color);
border-radius: 8px;
padding: 16px;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: var(--el-color-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.preset-icon {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
margin-bottom: 12px;
}
.preset-content {
h5 {
margin: 0 0 8px 0;
color: var(--el-text-color-primary);
font-size: 14px;
font-weight: 600;
}
p {
margin: 0 0 12px 0;
color: var(--el-text-color-secondary);
font-size: 12px;
line-height: 1.4;
}
.preset-tags {
display: flex;
gap: 8px;
}
}
}
}
}
}
}
</style>

View File

@@ -1,463 +0,0 @@
<template>
<div class="alerts-page">
<!-- 预警统计 -->
<el-row :gutter="20" class="alert-stats">
<el-col :span="6">
<el-card class="stat-card stat-card--critical">
<div class="stat-content">
<div class="stat-icon">
<el-icon><WarningFilled /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ alertStatistics.critical }}</div>
<div class="stat-label">{{ $t('alerts.statistics.critical') }}</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card stat-card--warning">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Warning /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ alertStatistics.warning }}</div>
<div class="stat-label">{{ $t('alerts.statistics.warning') }}</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card stat-card--info">
<div class="stat-content">
<div class="stat-icon">
<el-icon><InfoFilled /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ alertStatistics.info }}</div>
<div class="stat-label">{{ $t('alerts.statistics.info') }}</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card stat-card--total">
<div class="stat-content">
<div class="stat-icon">
<el-icon><TrendCharts /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ alertStatistics.total }}</div>
<div class="stat-label">{{ $t('alerts.statistics.total') }}</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 预警列表 -->
<el-card class="alerts-list-card">
<template #header>
<div class="card-header">
<el-icon><Bell /></el-icon>
<span>{{ $t('alerts.title') }}</span>
<div class="header-actions">
<el-select
v-model="alertFilter"
placeholder="过滤预警"
style="width: 120px; margin-right: 12px;"
@change="handleFilterChange"
>
<el-option
v-for="filter in alertFilters"
:key="filter.value"
:label="filter.label"
:value="filter.value"
/>
</el-select>
<el-select
v-model="alertSort"
placeholder="排序方式"
style="width: 120px; margin-right: 12px;"
@change="handleSortChange"
>
<el-option
v-for="sort in alertSorts"
:key="sort.value"
:label="sort.label"
:value="sort.value"
/>
</el-select>
<el-button
type="primary"
size="small"
:loading="loading"
@click="refreshAlerts"
>
<el-icon><Refresh /></el-icon>
{{ $t('common.refresh') }}
</el-button>
</div>
</div>
</template>
<div v-if="loading" class="loading-container">
<el-skeleton :rows="5" animated />
</div>
<div v-else-if="filteredAlerts.length === 0" class="empty-state">
<el-empty :description="$t('alerts.empty.description')">
<template #image>
<el-icon size="64"><Check /></el-icon>
</template>
</el-empty>
</div>
<div v-else class="alerts-list">
<div
v-for="alert in filteredAlerts"
:key="alert.id"
class="alert-item"
:class="`alert-item--${alert.level}`"
>
<div class="alert-content">
<div class="alert-header">
<el-tag :type="getAlertTagType(alert.level)" size="small">
{{ $t(`alerts.rules.levels.${alert.level}`) }}
</el-tag>
<span class="alert-rule">{{ alert.rule_name || '未知规则' }}</span>
<span class="alert-time">{{ formatTime(alert.created_at) }}</span>
</div>
<div class="alert-message">{{ alert.message }}</div>
<div class="alert-meta">
{{ $t('common.type') }}: {{ $t(`alerts.rules.types.${alert.alert_type}`) }} |
{{ $t('common.level') }}: {{ $t(`alerts.rules.levels.${alert.level}`) }}
</div>
<div v-if="alert.data" class="alert-data">
<el-collapse>
<el-collapse-item title="详细信息" name="data">
<pre>{{ JSON.stringify(alert.data, null, 2) }}</pre>
</el-collapse-item>
</el-collapse>
</div>
</div>
<div class="alert-actions">
<el-button
type="success"
size="small"
@click="handleResolveAlert(alert.id)"
>
<el-icon><Check /></el-icon>
{{ $t('alerts.actions.resolve') }}
</el-button>
</div>
</div>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAlertStore } from '@/stores/useAlertStore'
import { ElMessage } from 'element-plus'
const { t } = useI18n()
const alertStore = useAlertStore()
// 状态
const loading = ref(false)
const alertFilter = ref('all')
const alertSort = ref('time-desc')
// 计算属性
const alertStatistics = computed(() => alertStore.alertStatistics)
const filteredAlerts = computed(() => alertStore.filteredAlerts)
// 过滤选项
const alertFilters = computed(() => [
{ value: 'all', label: t('alerts.filters.all') },
{ value: 'critical', label: t('alerts.filters.critical') },
{ value: 'error', label: t('alerts.filters.error') },
{ value: 'warning', label: t('alerts.filters.warning') },
{ value: 'info', label: t('alerts.filters.info') }
])
// 排序选项
const alertSorts = computed(() => [
{ value: 'time-desc', label: t('alerts.sort.timeDesc') },
{ value: 'time-asc', label: t('alerts.sort.timeAsc') },
{ value: 'level-desc', label: t('alerts.sort.levelDesc') },
{ value: 'level-asc', label: t('alerts.sort.levelAsc') }
])
// 方法
const refreshAlerts = async () => {
loading.value = true
try {
await alertStore.loadAlerts()
ElMessage.success('预警数据刷新成功')
} catch (error) {
ElMessage.error('预警数据刷新失败')
} finally {
loading.value = false
}
}
const handleResolveAlert = async (alertId: number) => {
try {
const success = await alertStore.resolveAlert(alertId)
if (success) {
ElMessage.success(t('notifications.alertResolved'))
} else {
ElMessage.error(t('notifications.error.resolveAlert'))
}
} catch (error) {
ElMessage.error(t('notifications.error.resolveAlert'))
}
}
const handleFilterChange = (value: string) => {
alertStore.setAlertFilter(value)
}
const handleSortChange = (value: string) => {
alertStore.setAlertSort(value)
}
const getAlertTagType = (level: string) => {
const types = {
critical: 'danger',
error: 'danger',
warning: 'warning',
info: 'info'
}
return types[level as keyof typeof types] || 'info'
}
const formatTime = (timestamp: string) => {
const date = new Date(timestamp)
const now = new Date()
const diff = now.getTime() - date.getTime()
if (diff < 60000) { // 1分钟内
return '刚刚'
} else if (diff < 3600000) { // 1小时内
return `${Math.floor(diff / 60000)}分钟前`
} else if (diff < 86400000) { // 1天内
return `${Math.floor(diff / 3600000)}小时前`
} else {
return date.toLocaleDateString()
}
}
// 生命周期
onMounted(() => {
refreshAlerts()
})
</script>
<style scoped lang="scss">
.alerts-page {
.alert-stats {
margin-bottom: 20px;
}
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
.header-actions {
margin-left: auto;
display: flex;
align-items: center;
}
}
.stat-card {
&--critical {
border-left: 4px solid #f56c6c;
}
&--warning {
border-left: 4px solid #e6a23c;
}
&--info {
border-left: 4px solid #409eff;
}
&--total {
border-left: 4px solid #67c23a;
}
.stat-content {
display: flex;
align-items: center;
gap: 16px;
.stat-icon {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: white;
.stat-card--critical & {
background: #f56c6c;
}
.stat-card--warning & {
background: #e6a23c;
}
.stat-card--info & {
background: #409eff;
}
.stat-card--total & {
background: #67c23a;
}
}
.stat-info {
.stat-number {
font-size: 24px;
font-weight: bold;
color: var(--el-text-color-primary);
}
.stat-label {
font-size: 14px;
color: var(--el-text-color-secondary);
margin-top: 4px;
}
}
}
}
.alerts-list-card {
.loading-container {
padding: 20px;
}
.empty-state {
padding: 40px 20px;
}
.alerts-list {
.alert-item {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 20px;
border: 1px solid var(--el-border-color);
border-radius: 8px;
margin-bottom: 16px;
transition: all 0.3s;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
&--critical {
border-left: 4px solid #f56c6c;
background: #fef0f0;
}
&--error {
border-left: 4px solid #f56c6c;
background: #fef0f0;
}
&--warning {
border-left: 4px solid #e6a23c;
background: #fdf6ec;
}
&--info {
border-left: 4px solid #409eff;
background: #ecf5ff;
}
.alert-content {
flex: 1;
.alert-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
.alert-rule {
font-weight: 600;
color: var(--el-text-color-primary);
}
.alert-time {
margin-left: auto;
font-size: 12px;
color: var(--el-text-color-secondary);
}
}
.alert-message {
margin-bottom: 12px;
line-height: 1.5;
font-size: 14px;
}
.alert-meta {
font-size: 12px;
color: var(--el-text-color-secondary);
margin-bottom: 12px;
}
.alert-data {
pre {
background: var(--el-bg-color-page);
padding: 12px;
border-radius: 4px;
font-size: 12px;
overflow-x: auto;
margin: 0;
}
}
}
.alert-actions {
margin-left: 16px;
flex-shrink: 0;
}
}
}
}
// 暗色主题适配
:global(.dark) {
.alert-item {
&--critical {
background: rgba(245, 108, 108, 0.1);
}
&--warning {
background: rgba(230, 162, 60, 0.1);
}
&--info {
background: rgba(64, 158, 255, 0.1);
}
}
}
</style>

View File

@@ -1,753 +0,0 @@
<template>
<div class="chat-page">
<el-row :gutter="20">
<!-- 左侧控制面板 -->
<el-col :span="6">
<el-card class="control-panel">
<template #header>
<div class="card-header">
<el-icon><Setting /></el-icon>
<span>{{ $t('chat.title') }}</span>
</div>
</template>
<div class="control-content">
<!-- 用户设置 -->
<div class="control-section">
<h4>{{ $t('chat.userId') }}</h4>
<el-input
v-model="userId"
:placeholder="$t('chat.userId')"
@change="handleUserIdChange"
/>
</div>
<div class="control-section">
<h4>{{ $t('chat.workOrderId') }}</h4>
<el-input
v-model="workOrderId"
:placeholder="$t('chat.workOrderIdPlaceholder')"
type="number"
@change="handleWorkOrderIdChange"
/>
</div>
<!-- 控制按钮 -->
<div class="control-section">
<div class="control-buttons">
<el-button
type="primary"
:loading="loading"
:disabled="hasActiveSession"
@click="handleStartChat"
class="control-btn"
>
<el-icon><VideoPlay /></el-icon>
{{ $t('chat.startChat') }}
</el-button>
<el-button
type="danger"
:loading="loading"
:disabled="!hasActiveSession"
@click="handleEndChat"
class="control-btn"
>
<el-icon><VideoPause /></el-icon>
{{ $t('chat.endChat') }}
</el-button>
<el-button
type="success"
:loading="loading"
:disabled="!hasActiveSession"
@click="showWorkOrderModal = true"
class="control-btn"
>
<el-icon><Plus /></el-icon>
{{ $t('chat.createWorkOrder') }}
</el-button>
</div>
</div>
<!-- 快速操作 -->
<div class="control-section">
<h4>{{ $t('chat.quickActions') }}</h4>
<div class="quick-actions">
<el-button
v-for="action in quickActions"
:key="action.key"
size="small"
@click="handleQuickAction(action.message)"
:disabled="!hasActiveSession"
class="quick-action-btn"
>
{{ action.label }}
</el-button>
</div>
</div>
<!-- 会话信息 -->
<div class="control-section">
<h4>{{ $t('chat.sessionInfo') }}</h4>
<div class="session-info">
<div v-if="currentSession" class="session-details">
<div class="session-item">
<span class="session-label">{{ $t('common.status') }}:</span>
<el-tag :type="hasActiveSession ? 'success' : 'info'">
{{ hasActiveSession ? '活跃' : '已结束' }}
</el-tag>
</div>
<div class="session-item">
<span class="session-label">{{ $t('common.time') }}:</span>
<span>{{ formatTime(currentSession.createdAt) }}</span>
</div>
<div class="session-item">
<span class="session-label">{{ $t('common.message') }}:</span>
<span>{{ messageCount }}</span>
</div>
</div>
<div v-else class="session-empty">
{{ $t('chat.welcomeDesc') }}
</div>
</div>
</div>
<!-- 连接状态 -->
<div class="control-section">
<h4>{{ $t('chat.connectionStatus') }}</h4>
<div class="connection-status">
<el-tag :type="isConnected ? 'success' : 'danger'">
<el-icon><CircleCheck v-if="isConnected" /><CircleClose v-else /></el-icon>
{{ isConnected ? $t('chat.connected') : $t('chat.disconnected') }}
</el-tag>
</div>
</div>
</div>
</el-card>
</el-col>
<!-- 右侧聊天区域 -->
<el-col :span="18">
<el-card class="chat-area">
<template #header>
<div class="card-header">
<el-icon><Robot /></el-icon>
<span>TSP智能助手</span>
<div class="header-subtitle">基于知识库的智能客服系统</div>
</div>
</template>
<div class="chat-container">
<!-- 消息列表 -->
<div class="chat-messages" ref="messagesContainer">
<div v-if="messages.length === 0" class="chat-empty">
<el-icon size="64"><ChatDotRound /></el-icon>
<h3>{{ $t('chat.welcome') }}</h3>
<p>{{ $t('chat.welcomeDesc') }}</p>
</div>
<div
v-for="message in messages"
:key="message.id"
class="chat-message"
:class="`chat-message--${message.role}`"
>
<div class="message-avatar">
<el-icon v-if="message.role === 'user'"><User /></el-icon>
<el-icon v-else-if="message.role === 'assistant'"><Robot /></el-icon>
<el-icon v-else><InfoFilled /></el-icon>
</div>
<div class="message-content">
<div class="message-text" v-html="message.content"></div>
<div class="message-time">{{ formatTime(message.timestamp) }}</div>
<!-- 元数据 -->
<div v-if="message.metadata" class="message-metadata">
<div v-if="message.metadata.knowledge_used?.length" class="knowledge-info">
<el-icon><Lightbulb /></el-icon>
<span>基于 {{ message.metadata.knowledge_used.length }} 条知识库信息生成</span>
</div>
<div v-if="message.metadata.confidence_score" class="confidence-score">
置信度: {{ (message.metadata.confidence_score * 100).toFixed(1) }}%
</div>
<div v-if="message.metadata.work_order_id" class="work-order-info">
<el-icon><Ticket /></el-icon>
<span>关联工单: {{ message.metadata.work_order_id }}</span>
</div>
</div>
</div>
</div>
<!-- 打字指示器 -->
<div v-if="isTyping" class="typing-indicator">
<div class="message-avatar">
<el-icon><Robot /></el-icon>
</div>
<div class="message-content">
<div class="typing-dots">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
</div>
<!-- 输入区域 -->
<div class="chat-input">
<div class="input-group">
<el-input
v-model="inputMessage"
:placeholder="$t('chat.inputPlaceholder')"
:disabled="!hasActiveSession"
@keyup.enter="handleSendMessage"
class="message-input"
type="textarea"
:rows="3"
resize="none"
/>
<el-button
type="primary"
:disabled="!hasActiveSession || !inputMessage.trim()"
@click="handleSendMessage"
class="send-btn"
>
<el-icon><Position /></el-icon>
{{ $t('chat.sendMessage') }}
</el-button>
</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 创建工单模态框 -->
<el-dialog
v-model="showWorkOrderModal"
:title="$t('chat.workOrder.title')"
width="500px"
>
<el-form :model="workOrderForm" label-width="100px">
<el-form-item :label="$t('chat.workOrder.titleLabel')" required>
<el-input v-model="workOrderForm.title" />
</el-form-item>
<el-form-item :label="$t('chat.workOrder.descriptionLabel')" required>
<el-input
v-model="workOrderForm.description"
type="textarea"
:rows="3"
/>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="$t('chat.workOrder.categoryLabel')">
<el-select v-model="workOrderForm.category">
<el-option
v-for="(label, key) in workOrderCategories"
:key="key"
:label="label"
:value="key"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('chat.workOrder.priorityLabel')">
<el-select v-model="workOrderForm.priority">
<el-option
v-for="(label, key) in workOrderPriorities"
:key="key"
:label="label"
:value="key"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="showWorkOrderModal = false">
{{ $t('common.cancel') }}
</el-button>
<el-button
type="primary"
:loading="loading"
@click="handleCreateWorkOrder"
>
{{ $t('chat.createWorkOrder') }}
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed, nextTick, watch, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useChatStore } from '@/stores/useChatStore'
import { ElMessage } from 'element-plus'
const { t } = useI18n()
const chatStore = useChatStore()
// 状态
const loading = ref(false)
const inputMessage = ref('')
const showWorkOrderModal = ref(false)
const messagesContainer = ref<HTMLElement>()
// 工单表单
const workOrderForm = ref({
title: '',
description: '',
category: 'technical',
priority: 'medium'
})
// 计算属性
const userId = computed({
get: () => chatStore.userId,
set: (value) => chatStore.setUserId(value)
})
const workOrderId = computed({
get: () => chatStore.workOrderId,
set: (value) => chatStore.setWorkOrderId(value)
})
const messages = computed(() => chatStore.messages)
const isTyping = computed(() => chatStore.isTyping)
const isConnected = computed(() => chatStore.isConnected)
const hasActiveSession = computed(() => chatStore.hasActiveSession)
const currentSession = computed(() => chatStore.currentSession)
const messageCount = computed(() => chatStore.messageCount)
// 快速操作
const quickActions = computed(() => [
{ key: 'remoteStart', label: t('chat.quickActions.remoteStart'), message: '我的车辆无法远程启动' },
{ key: 'appDisplay', label: t('chat.quickActions.appDisplay'), message: 'APP显示车辆信息错误' },
{ key: 'bluetoothAuth', label: t('chat.quickActions.bluetoothAuth'), message: '蓝牙授权失败' },
{ key: 'unbindVehicle', label: t('chat.quickActions.unbindVehicle'), message: '如何解绑车辆' }
])
// 工单选项
const workOrderCategories = computed(() => ({
technical: t('chat.workOrder.categories.technical'),
app: t('chat.workOrder.categories.app'),
remoteControl: t('chat.workOrder.categories.remoteControl'),
vehicleBinding: t('chat.workOrder.categories.vehicleBinding'),
other: t('chat.workOrder.categories.other')
}))
const workOrderPriorities = computed(() => ({
low: t('chat.workOrder.priorities.low'),
medium: t('chat.workOrder.priorities.medium'),
high: t('chat.workOrder.priorities.high'),
urgent: t('chat.workOrder.priorities.urgent')
}))
// 方法
const handleStartChat = async () => {
loading.value = true
try {
const success = await chatStore.startChat()
if (success) {
ElMessage.success('对话已开始')
} else {
ElMessage.error('启动对话失败')
}
} catch (error) {
ElMessage.error('启动对话失败: ' + (error as Error).message)
} finally {
loading.value = false
}
}
const handleEndChat = async () => {
loading.value = true
try {
await chatStore.endChat()
ElMessage.success('对话已结束')
} catch (error) {
ElMessage.error('结束对话失败')
} finally {
loading.value = false
}
}
const handleSendMessage = async () => {
if (!inputMessage.value.trim() || !hasActiveSession.value) {
return
}
const message = inputMessage.value.trim()
inputMessage.value = ''
try {
await chatStore.sendMessage(message)
} catch (error) {
ElMessage.error('发送消息失败')
console.error('发送消息失败:', error)
}
}
const handleQuickAction = async (message: string) => {
inputMessage.value = message
await handleSendMessage()
}
const handleCreateWorkOrder = async () => {
if (!workOrderForm.value.title || !workOrderForm.value.description) {
ElMessage.warning('请填写工单标题和描述')
return
}
loading.value = true
try {
await chatStore.createWorkOrder(workOrderForm.value)
showWorkOrderModal.value = false
workOrderForm.value = {
title: '',
description: '',
category: 'technical',
priority: 'medium'
}
} catch (error) {
ElMessage.error('创建工单失败: ' + (error as Error).message)
} finally {
loading.value = false
}
}
const handleUserIdChange = (value: string) => {
chatStore.setUserId(value)
}
const handleWorkOrderIdChange = (value: string) => {
chatStore.setWorkOrderId(value)
}
const formatTime = (timestamp: Date) => {
const now = new Date()
const diff = now.getTime() - timestamp.getTime()
if (diff < 60000) { // 1分钟内
return '刚刚'
} else if (diff < 3600000) { // 1小时内
return `${Math.floor(diff / 60000)}分钟前`
} else if (diff < 86400000) { // 1天内
return `${Math.floor(diff / 3600000)}小时前`
} else {
return timestamp.toLocaleDateString()
}
}
const scrollToBottom = () => {
nextTick(() => {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
}
})
}
// 监听消息变化,自动滚动到底部
watch(messages, () => {
scrollToBottom()
}, { deep: true })
// 监听打字状态变化
watch(isTyping, () => {
scrollToBottom()
})
// 生命周期
onMounted(() => {
// 尝试连接WebSocket
chatStore.connectWebSocket().catch(error => {
console.error('WebSocket连接失败:', error)
})
})
</script>
<style scoped lang="scss">
.chat-page {
.control-panel {
height: calc(100vh - 120px);
.control-content {
height: calc(100% - 60px);
overflow-y: auto;
.control-section {
margin-bottom: 24px;
h4 {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.control-buttons {
display: flex;
flex-direction: column;
gap: 8px;
.control-btn {
width: 100%;
justify-content: flex-start;
}
}
.quick-actions {
display: flex;
flex-direction: column;
gap: 8px;
.quick-action-btn {
width: 100%;
justify-content: flex-start;
font-size: 12px;
}
}
.session-info {
.session-details {
.session-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-size: 14px;
.session-label {
color: var(--el-text-color-secondary);
}
}
}
.session-empty {
color: var(--el-text-color-placeholder);
font-size: 14px;
text-align: center;
padding: 20px 0;
}
}
.connection-status {
.el-tag {
width: 100%;
justify-content: center;
}
}
}
}
}
.chat-area {
height: calc(100vh - 120px);
.card-header {
.header-subtitle {
font-size: 12px;
color: var(--el-text-color-secondary);
margin-left: 8px;
}
}
.chat-container {
height: calc(100% - 60px);
display: flex;
flex-direction: column;
.chat-messages {
flex: 1;
padding: 20px;
overflow-y: auto;
background: var(--el-bg-color-page);
border-radius: 8px;
margin-bottom: 16px;
}
.chat-empty {
text-align: center;
padding: 60px 20px;
color: var(--el-text-color-secondary);
.el-icon {
margin-bottom: 16px;
}
h3 {
margin: 16px 0 8px 0;
color: var(--el-text-color-primary);
}
p {
margin: 0;
font-size: 14px;
}
}
.chat-message {
display: flex;
margin-bottom: 20px;
align-items: flex-start;
gap: 12px;
&--user {
flex-direction: row-reverse;
.message-content {
background: var(--el-color-primary);
color: white;
border-radius: 18px 18px 4px 18px;
}
}
&--assistant {
.message-content {
background: var(--el-bg-color);
color: var(--el-text-color-primary);
border: 1px solid var(--el-border-color);
border-radius: 18px 18px 18px 4px;
}
}
&--system {
justify-content: center;
.message-content {
background: var(--el-color-info-light-9);
color: var(--el-color-info);
border-radius: 12px;
font-size: 14px;
text-align: center;
}
}
}
.message-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--el-color-primary);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
.message-content {
max-width: 70%;
padding: 16px 20px;
position: relative;
.message-text {
line-height: 1.6;
word-wrap: break-word;
}
.message-time {
font-size: 12px;
opacity: 0.7;
margin-top: 8px;
}
.message-metadata {
margin-top: 12px;
.knowledge-info,
.confidence-score,
.work-order-info {
font-size: 12px;
opacity: 0.8;
margin-top: 6px;
display: flex;
align-items: center;
gap: 6px;
}
.knowledge-info {
color: var(--el-color-info);
}
.confidence-score {
color: var(--el-color-success);
}
.work-order-info {
color: var(--el-color-warning);
}
}
}
.typing-indicator {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 20px;
}
.typing-dots {
display: flex;
gap: 6px;
padding: 16px 20px;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color);
border-radius: 18px 18px 18px 4px;
span {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--el-color-primary);
animation: typing 1.4s infinite ease-in-out;
&:nth-child(1) { animation-delay: -0.32s; }
&:nth-child(2) { animation-delay: -0.16s; }
}
}
@keyframes typing {
0%, 80%, 100% {
transform: scale(0.8);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
.chat-input {
.input-group {
display: flex;
gap: 12px;
.message-input {
flex: 1;
}
.send-btn {
flex-shrink: 0;
height: auto;
padding: 12px 20px;
}
}
}
}
}
}
</style>

View File

@@ -1,667 +0,0 @@
<template>
<div class="dashboard">
<!-- 系统健康状态 -->
<el-row :gutter="20" class="dashboard-header">
<el-col :span="6">
<el-card class="health-card">
<template #header>
<div class="card-header">
<el-icon><Monitor /></el-icon>
<span>{{ $t('dashboard.systemHealth') }}</span>
</div>
</template>
<div class="health-content">
<div class="health-score">
<el-progress
:percentage="Math.round(health.health_score)"
:color="getHealthColor(health.status)"
:stroke-width="8"
type="circle"
:width="80"
/>
</div>
<div class="health-status">
<el-tag :type="getHealthTagType(health.status)">
{{ $t(`dashboard.${health.status}`) }}
</el-tag>
</div>
</div>
</el-card>
</el-col>
<el-col :span="18">
<el-card class="monitor-card">
<template #header>
<div class="card-header">
<el-icon><Setting /></el-icon>
<span>{{ $t('dashboard.monitoring') }}</span>
</div>
</template>
<div class="monitor-controls">
<el-button
type="success"
:loading="loading"
@click="handleStartMonitoring"
>
<el-icon><VideoPlay /></el-icon>
{{ $t('dashboard.startMonitoring') }}
</el-button>
<el-button
type="danger"
:loading="loading"
@click="handleStopMonitoring"
>
<el-icon><VideoPause /></el-icon>
{{ $t('dashboard.stopMonitoring') }}
</el-button>
<el-button
type="info"
:loading="loading"
@click="handleCheckAlerts"
>
<el-icon><Search /></el-icon>
{{ $t('dashboard.checkAlerts') }}
</el-button>
<el-button
type="primary"
:loading="loading"
@click="refreshData"
>
<el-icon><Refresh /></el-icon>
{{ $t('common.refresh') }}
</el-button>
</div>
<div class="monitor-status">
<el-tag :type="getMonitorStatusType(monitorStatus.monitor_status)">
<el-icon><CircleCheck v-if="monitorStatus.monitor_status === 'running'" />
<CircleClose v-else-if="monitorStatus.monitor_status === 'stopped'" />
<QuestionFilled v-else /></el-icon>
{{ getMonitorStatusText(monitorStatus.monitor_status) }}
</el-tag>
</div>
</el-card>
</el-col>
</el-row>
<!-- 预警统计 -->
<el-row :gutter="20" class="alert-stats">
<el-col :span="6">
<el-card class="stat-card stat-card--critical">
<div class="stat-content">
<div class="stat-icon">
<el-icon><WarningFilled /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ alertStatistics.critical }}</div>
<div class="stat-label">{{ $t('alerts.statistics.critical') }}</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card stat-card--warning">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Warning /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ alertStatistics.warning }}</div>
<div class="stat-label">{{ $t('alerts.statistics.warning') }}</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card stat-card--info">
<div class="stat-content">
<div class="stat-icon">
<el-icon><InfoFilled /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ alertStatistics.info }}</div>
<div class="stat-label">{{ $t('alerts.statistics.info') }}</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card stat-card--total">
<div class="stat-content">
<div class="stat-icon">
<el-icon><TrendCharts /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ alertStatistics.total }}</div>
<div class="stat-label">{{ $t('alerts.statistics.total') }}</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 活跃预警列表 -->
<el-card class="alerts-card">
<template #header>
<div class="card-header">
<el-icon><Bell /></el-icon>
<span>{{ $t('dashboard.activeAlerts') }}</span>
<div class="header-actions">
<el-select
v-model="alertFilter"
placeholder="过滤预警"
style="width: 120px; margin-right: 12px;"
@change="handleFilterChange"
>
<el-option
v-for="filter in alertFilters"
:key="filter.value"
:label="filter.label"
:value="filter.value"
/>
</el-select>
<el-select
v-model="alertSort"
placeholder="排序方式"
style="width: 120px; margin-right: 12px;"
@change="handleSortChange"
>
<el-option
v-for="sort in alertSorts"
:key="sort.value"
:label="sort.label"
:value="sort.value"
/>
</el-select>
<el-button
type="primary"
size="small"
:loading="loading"
@click="refreshAlerts"
>
<el-icon><Refresh /></el-icon>
{{ $t('common.refresh') }}
</el-button>
</div>
</div>
</template>
<div v-if="loading" class="loading-container">
<el-skeleton :rows="3" animated />
</div>
<div v-else-if="filteredAlerts.length === 0" class="empty-state">
<el-empty :description="$t('alerts.empty.description')">
<template #image>
<el-icon size="64"><Check /></el-icon>
</template>
</el-empty>
</div>
<div v-else class="alerts-list">
<div
v-for="alert in filteredAlerts.slice(0, 10)"
:key="alert.id"
class="alert-item"
:class="`alert-item--${alert.level}`"
>
<div class="alert-content">
<div class="alert-header">
<el-tag :type="getAlertTagType(alert.level)" size="small">
{{ $t(`alerts.rules.levels.${alert.level}`) }}
</el-tag>
<span class="alert-rule">{{ alert.rule_name || '未知规则' }}</span>
<span class="alert-time">{{ formatTime(alert.created_at) }}</span>
</div>
<div class="alert-message">{{ alert.message }}</div>
<div class="alert-meta">
{{ $t('common.type') }}: {{ $t(`alerts.rules.types.${alert.alert_type}`) }} |
{{ $t('common.level') }}: {{ $t(`alerts.rules.levels.${alert.level}`) }}
</div>
</div>
<div class="alert-actions">
<el-button
type="success"
size="small"
@click="handleResolveAlert(alert.id)"
>
<el-icon><Check /></el-icon>
{{ $t('alerts.actions.resolve') }}
</el-button>
</div>
</div>
</div>
</el-card>
<!-- 聊天组件 -->
<ChatWidget />
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAlertStore } from '@/stores/useAlertStore'
import { ElMessage } from 'element-plus'
import ChatWidget from '@/components/ChatWidget.vue'
const { t } = useI18n()
const alertStore = useAlertStore()
// 状态
const loading = ref(false)
const alertFilter = ref('all')
const alertSort = ref('time-desc')
// 计算属性
const health = computed(() => alertStore.health)
const monitorStatus = computed(() => alertStore.monitorStatus)
const alertStatistics = computed(() => alertStore.alertStatistics)
const filteredAlerts = computed(() => alertStore.filteredAlerts)
// 过滤选项
const alertFilters = computed(() => [
{ value: 'all', label: t('alerts.filters.all') },
{ value: 'critical', label: t('alerts.filters.critical') },
{ value: 'error', label: t('alerts.filters.error') },
{ value: 'warning', label: t('alerts.filters.warning') },
{ value: 'info', label: t('alerts.filters.info') }
])
// 排序选项
const alertSorts = computed(() => [
{ value: 'time-desc', label: t('alerts.sort.timeDesc') },
{ value: 'time-asc', label: t('alerts.sort.timeAsc') },
{ value: 'level-desc', label: t('alerts.sort.levelDesc') },
{ value: 'level-asc', label: t('alerts.sort.levelAsc') }
])
// 方法
const refreshData = async () => {
loading.value = true
try {
await alertStore.loadInitialData()
ElMessage.success('数据刷新成功')
} catch (error) {
ElMessage.error('数据刷新失败')
} finally {
loading.value = false
}
}
const refreshAlerts = async () => {
loading.value = true
try {
await alertStore.loadAlerts()
ElMessage.success('预警数据刷新成功')
} catch (error) {
ElMessage.error('预警数据刷新失败')
} finally {
loading.value = false
}
}
const handleStartMonitoring = async () => {
loading.value = true
try {
const success = await alertStore.startMonitoring()
if (success) {
ElMessage.success(t('notifications.monitoringStarted'))
} else {
ElMessage.error(t('notifications.error.startMonitoring'))
}
} catch (error) {
ElMessage.error(t('notifications.error.startMonitoring'))
} finally {
loading.value = false
}
}
const handleStopMonitoring = async () => {
loading.value = true
try {
const success = await alertStore.stopMonitoring()
if (success) {
ElMessage.success(t('notifications.monitoringStopped'))
} else {
ElMessage.error(t('notifications.error.stopMonitoring'))
}
} catch (error) {
ElMessage.error(t('notifications.error.stopMonitoring'))
} finally {
loading.value = false
}
}
const handleCheckAlerts = async () => {
loading.value = true
try {
const count = await alertStore.checkAlerts()
ElMessage.success(t('notifications.alertsChecked', { count }))
} catch (error) {
ElMessage.error(t('notifications.error.checkAlerts'))
} finally {
loading.value = false
}
}
const handleResolveAlert = async (alertId: number) => {
try {
const success = await alertStore.resolveAlert(alertId)
if (success) {
ElMessage.success(t('notifications.alertResolved'))
} else {
ElMessage.error(t('notifications.error.resolveAlert'))
}
} catch (error) {
ElMessage.error(t('notifications.error.resolveAlert'))
}
}
const handleFilterChange = (value: string) => {
alertStore.setAlertFilter(value)
}
const handleSortChange = (value: string) => {
alertStore.setAlertSort(value)
}
const getHealthColor = (status: string) => {
const colors = {
excellent: '#67c23a',
good: '#85ce61',
fair: '#e6a23c',
poor: '#f56c6c',
critical: '#f56c6c',
unknown: '#909399'
}
return colors[status as keyof typeof colors] || '#909399'
}
const getHealthTagType = (status: string) => {
const types = {
excellent: 'success',
good: 'success',
fair: 'warning',
poor: 'danger',
critical: 'danger',
unknown: 'info'
}
return types[status as keyof typeof types] || 'info'
}
const getMonitorStatusType = (status: string) => {
const types = {
running: 'success',
stopped: 'danger',
unknown: 'warning'
}
return types[status as keyof typeof types] || 'info'
}
const getMonitorStatusText = (status: string) => {
const texts = {
running: '监控运行中',
stopped: '监控已停止',
unknown: '监控状态未知'
}
return texts[status as keyof typeof texts] || '未知'
}
const getAlertTagType = (level: string) => {
const types = {
critical: 'danger',
error: 'danger',
warning: 'warning',
info: 'info'
}
return types[level as keyof typeof types] || 'info'
}
const formatTime = (timestamp: string) => {
const date = new Date(timestamp)
const now = new Date()
const diff = now.getTime() - date.getTime()
if (diff < 60000) { // 1分钟内
return '刚刚'
} else if (diff < 3600000) { // 1小时内
return `${Math.floor(diff / 60000)}分钟前`
} else if (diff < 86400000) { // 1天内
return `${Math.floor(diff / 3600000)}小时前`
} else {
return date.toLocaleDateString()
}
}
// 生命周期
onMounted(() => {
refreshData()
})
</script>
<style scoped lang="scss">
.dashboard {
.dashboard-header {
margin-bottom: 20px;
}
.alert-stats {
margin-bottom: 20px;
}
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
.header-actions {
margin-left: auto;
display: flex;
align-items: center;
}
}
.health-card {
.health-content {
text-align: center;
.health-score {
margin-bottom: 16px;
}
.health-status {
.el-tag {
font-size: 14px;
padding: 8px 16px;
}
}
}
}
.monitor-card {
.monitor-controls {
display: flex;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.monitor-status {
.el-tag {
font-size: 14px;
padding: 8px 16px;
}
}
}
.stat-card {
&--critical {
border-left: 4px solid #f56c6c;
}
&--warning {
border-left: 4px solid #e6a23c;
}
&--info {
border-left: 4px solid #409eff;
}
&--total {
border-left: 4px solid #67c23a;
}
.stat-content {
display: flex;
align-items: center;
gap: 16px;
.stat-icon {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: white;
.stat-card--critical & {
background: #f56c6c;
}
.stat-card--warning & {
background: #e6a23c;
}
.stat-card--info & {
background: #409eff;
}
.stat-card--total & {
background: #67c23a;
}
}
.stat-info {
.stat-number {
font-size: 24px;
font-weight: bold;
color: var(--el-text-color-primary);
}
.stat-label {
font-size: 14px;
color: var(--el-text-color-secondary);
margin-top: 4px;
}
}
}
}
.alerts-card {
.loading-container {
padding: 20px;
}
.empty-state {
padding: 40px 20px;
}
.alerts-list {
.alert-item {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 16px;
border: 1px solid var(--el-border-color);
border-radius: 8px;
margin-bottom: 12px;
transition: all 0.3s;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
&--critical {
border-left: 4px solid #f56c6c;
background: #fef0f0;
}
&--error {
border-left: 4px solid #f56c6c;
background: #fef0f0;
}
&--warning {
border-left: 4px solid #e6a23c;
background: #fdf6ec;
}
&--info {
border-left: 4px solid #409eff;
background: #ecf5ff;
}
.alert-content {
flex: 1;
.alert-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
.alert-rule {
font-weight: 600;
color: var(--el-text-color-primary);
}
.alert-time {
margin-left: auto;
font-size: 12px;
color: var(--el-text-color-secondary);
}
}
.alert-message {
margin-bottom: 8px;
line-height: 1.5;
}
.alert-meta {
font-size: 12px;
color: var(--el-text-color-secondary);
}
}
.alert-actions {
margin-left: 16px;
flex-shrink: 0;
}
}
}
}
// 暗色主题适配
:global(.dark) {
.alert-item {
&--critical {
background: rgba(245, 108, 108, 0.1);
}
&--warning {
background: rgba(230, 162, 60, 0.1);
}
&--info {
background: rgba(64, 158, 255, 0.1);
}
}
}
</style>

View File

@@ -1,366 +0,0 @@
<template>
<div class="field-mapping-page">
<div class="page-header">
<h2>{{ $t('fieldMapping.title') }}</h2>
<div class="header-actions">
<el-button type="primary" @click="showAddModal = true">
<el-icon><Plus /></el-icon>
{{ $t('fieldMapping.addMapping') }}
</el-button>
<el-button type="success" @click="handleImport">
<el-icon><Upload /></el-icon>
{{ $t('fieldMapping.importMapping') }}
</el-button>
<el-button type="info" @click="handleExport">
<el-icon><Download /></el-icon>
{{ $t('fieldMapping.exportMapping') }}
</el-button>
</div>
</div>
<el-card>
<div v-if="loading" class="loading-container">
<el-skeleton :rows="5" animated />
</div>
<div v-else-if="mappings.length === 0" class="empty-state">
<el-empty description="暂无字段映射">
<el-button type="primary" @click="showAddModal = true">
{{ $t('fieldMapping.addMapping') }}
</el-button>
</el-empty>
</div>
<div v-else class="mappings-table">
<el-table :data="mappings" stripe>
<el-table-column prop="sourceField" :label="$t('fieldMapping.sourceField')" />
<el-table-column prop="targetField" :label="$t('fieldMapping.targetField')" />
<el-table-column prop="mappingType" :label="$t('fieldMapping.mappingType')">
<template #default="{ row }">
<el-tag :type="getMappingTypeTag(row.mappingType)">
{{ row.mappingType }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="transformation" :label="$t('fieldMapping.transformation')" />
<el-table-column :label="$t('common.action')" width="200">
<template #default="{ row }">
<el-button size="small" @click="handleEdit(row)">
<el-icon><Edit /></el-icon>
{{ $t('common.edit') }}
</el-button>
<el-button size="small" type="success" @click="handleTest(row)">
<el-icon><View /></el-icon>
{{ $t('fieldMapping.testMapping') }}
</el-button>
<el-button size="small" type="danger" @click="handleDelete(row.id)">
<el-icon><Delete /></el-icon>
{{ $t('common.delete') }}
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-card>
<!-- 添加/编辑模态框 -->
<el-dialog
v-model="showAddModal"
:title="editingItem ? $t('fieldMapping.editMapping') : $t('fieldMapping.addMapping')"
width="600px"
>
<el-form :model="mappingForm" label-width="120px">
<el-form-item :label="$t('fieldMapping.sourceField')" required>
<el-input v-model="mappingForm.sourceField" />
</el-form-item>
<el-form-item :label="$t('fieldMapping.targetField')" required>
<el-input v-model="mappingForm.targetField" />
</el-form-item>
<el-form-item :label="$t('fieldMapping.mappingType')" required>
<el-select v-model="mappingForm.mappingType">
<el-option label="直接映射" value="direct" />
<el-option label="转换映射" value="transform" />
<el-option label="条件映射" value="conditional" />
<el-option label="计算映射" value="calculated" />
</el-select>
</el-form-item>
<el-form-item :label="$t('fieldMapping.transformation')">
<el-input
v-model="mappingForm.transformation"
type="textarea"
:rows="3"
placeholder="例如: value * 100 或 value.toUpperCase()"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showAddModal = false">{{ $t('common.cancel') }}</el-button>
<el-button type="primary" @click="handleSave">
{{ $t('common.save') }}
</el-button>
</template>
</el-dialog>
<!-- 测试映射模态框 -->
<el-dialog
v-model="showTestModal"
title="测试字段映射"
width="500px"
>
<el-form :model="testForm" label-width="100px">
<el-form-item label="测试数据">
<el-input
v-model="testForm.testData"
type="textarea"
:rows="3"
placeholder="输入测试数据"
/>
</el-form-item>
<el-form-item label="映射结果">
<el-input
v-model="testForm.result"
type="textarea"
:rows="3"
readonly
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showTestModal = false">{{ $t('common.close') }}</el-button>
<el-button type="primary" @click="runTest">
运行测试
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage, ElMessageBox } from 'element-plus'
const { t } = useI18n()
// 状态
const loading = ref(false)
const showAddModal = ref(false)
const showTestModal = ref(false)
const editingItem = ref<any>(null)
// 字段映射数据
const mappings = ref([
{
id: 1,
sourceField: 'user_name',
targetField: 'customerName',
mappingType: 'direct',
transformation: '',
status: 'active'
},
{
id: 2,
sourceField: 'order_amount',
targetField: 'totalAmount',
mappingType: 'transform',
transformation: 'value * 100',
status: 'active'
},
{
id: 3,
sourceField: 'order_status',
targetField: 'status',
mappingType: 'conditional',
transformation: 'value === "completed" ? "已完成" : "进行中"',
status: 'active'
}
])
// 映射表单
const mappingForm = ref({
sourceField: '',
targetField: '',
mappingType: 'direct',
transformation: ''
})
// 测试表单
const testForm = ref({
testData: '',
result: ''
})
// 方法
const handleEdit = (item: any) => {
editingItem.value = item
mappingForm.value = { ...item }
showAddModal.value = true
}
const handleDelete = async (id: number) => {
try {
await ElMessageBox.confirm('确定要删除这个字段映射吗?', '确认删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const index = mappings.value.findIndex(item => item.id === id)
if (index > -1) {
mappings.value.splice(index, 1)
ElMessage.success('删除成功')
}
} catch (error) {
// 用户取消删除
}
}
const handleTest = (item: any) => {
editingItem.value = item
testForm.value = {
testData: '',
result: ''
}
showTestModal.value = true
}
const handleSave = () => {
if (!mappingForm.value.sourceField || !mappingForm.value.targetField) {
ElMessage.warning('请填写源字段和目标字段')
return
}
if (editingItem.value) {
// 编辑
const index = mappings.value.findIndex(item => item.id === editingItem.value.id)
if (index > -1) {
mappings.value[index] = {
...mappings.value[index],
...mappingForm.value
}
}
ElMessage.success('更新成功')
} else {
// 新增
const newItem = {
id: Date.now(),
...mappingForm.value,
status: 'active'
}
mappings.value.unshift(newItem)
ElMessage.success('添加成功')
}
showAddModal.value = false
resetForm()
}
const runTest = () => {
if (!testForm.value.testData) {
ElMessage.warning('请输入测试数据')
return
}
// 模拟测试逻辑
try {
const { mappingType, transformation } = editingItem.value
let result = testForm.value.testData
if (mappingType === 'transform' && transformation) {
// 简单的转换逻辑示例
if (transformation.includes('*')) {
const multiplier = parseInt(transformation.split('*')[1].trim())
result = (parseFloat(result) * multiplier).toString()
} else if (transformation.includes('toUpperCase')) {
result = result.toUpperCase()
}
} else if (mappingType === 'conditional' && transformation) {
// 简单的条件逻辑示例
if (transformation.includes('completed')) {
result = result === 'completed' ? '已完成' : '进行中'
}
}
testForm.value.result = result
ElMessage.success('测试完成')
} catch (error) {
testForm.value.result = '测试失败: ' + (error as Error).message
ElMessage.error('测试失败')
}
}
const handleImport = () => {
ElMessage.info('导入功能开发中...')
}
const handleExport = () => {
const data = JSON.stringify(mappings.value, null, 2)
const blob = new Blob([data], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'field-mappings.json'
a.click()
URL.revokeObjectURL(url)
ElMessage.success('导出成功')
}
const resetForm = () => {
mappingForm.value = {
sourceField: '',
targetField: '',
mappingType: 'direct',
transformation: ''
}
editingItem.value = null
}
const getMappingTypeTag = (type: string) => {
const types = {
direct: 'success',
transform: 'warning',
conditional: 'info',
calculated: 'danger'
}
return types[type as keyof typeof types] || 'info'
}
</script>
<style scoped lang="scss">
.field-mapping-page {
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h2 {
margin: 0;
color: var(--el-text-color-primary);
}
.header-actions {
display: flex;
gap: 12px;
}
}
.loading-container {
padding: 20px;
}
.empty-state {
padding: 40px 20px;
}
.mappings-table {
.el-table {
.el-button {
margin-right: 8px;
}
}
}
}
</style>

View File

@@ -1,330 +0,0 @@
<template>
<div class="knowledge-page">
<div class="page-header">
<h2>{{ $t('knowledge.title') }}</h2>
<el-button type="primary" @click="showAddModal = true">
<el-icon><Plus /></el-icon>
{{ $t('knowledge.add') }}
</el-button>
</div>
<el-card>
<div class="search-bar">
<el-input
v-model="searchKeyword"
:placeholder="$t('knowledge.search')"
@input="handleSearch"
clearable
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
<div v-if="loading" class="loading-container">
<el-skeleton :rows="5" animated />
</div>
<div v-else-if="filteredKnowledge.length === 0" class="empty-state">
<el-empty :description="searchKeyword ? '没有找到相关知识' : '暂无知识库内容'">
<el-button type="primary" @click="showAddModal = true">
{{ $t('knowledge.add') }}
</el-button>
</el-empty>
</div>
<div v-else class="knowledge-list">
<div
v-for="item in filteredKnowledge"
:key="item.id"
class="knowledge-item"
>
<div class="knowledge-content">
<h3 class="knowledge-title">{{ item.title }}</h3>
<p class="knowledge-desc">{{ item.content }}</p>
<div class="knowledge-meta">
<el-tag size="small">{{ item.category }}</el-tag>
<span class="knowledge-tags">
<el-tag
v-for="tag in item.tags"
:key="tag"
size="small"
type="info"
>
{{ tag }}
</el-tag>
</span>
</div>
</div>
<div class="knowledge-actions">
<el-button size="small" @click="handleEdit(item)">
<el-icon><Edit /></el-icon>
{{ $t('common.edit') }}
</el-button>
<el-button size="small" type="danger" @click="handleDelete(item.id)">
<el-icon><Delete /></el-icon>
{{ $t('common.delete') }}
</el-button>
</div>
</div>
</div>
</el-card>
<!-- 添加/编辑模态框 -->
<el-dialog
v-model="showAddModal"
:title="editingItem ? $t('knowledge.edit') : $t('knowledge.add')"
width="600px"
>
<el-form :model="knowledgeForm" label-width="80px">
<el-form-item label="标题" required>
<el-input v-model="knowledgeForm.title" />
</el-form-item>
<el-form-item label="内容" required>
<el-input
v-model="knowledgeForm.content"
type="textarea"
:rows="4"
/>
</el-form-item>
<el-form-item label="分类">
<el-input v-model="knowledgeForm.category" />
</el-form-item>
<el-form-item label="标签">
<el-input v-model="knowledgeForm.tags" placeholder="多个标签用逗号分隔" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showAddModal = false">{{ $t('common.cancel') }}</el-button>
<el-button type="primary" @click="handleSave">
{{ $t('common.save') }}
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage, ElMessageBox } from 'element-plus'
const { t } = useI18n()
// 状态
const loading = ref(false)
const searchKeyword = ref('')
const showAddModal = ref(false)
const editingItem = ref<any>(null)
// 知识库数据
const knowledgeList = ref([
{
id: 1,
title: '车辆远程启动功能',
content: '用户可以通过手机APP远程启动车辆需要确保车辆处于安全状态且用户已通过身份验证。',
category: '远程控制',
tags: ['远程启动', 'APP功能', '安全验证'],
status: 'active'
},
{
id: 2,
title: '蓝牙连接问题排查',
content: '当车辆蓝牙连接失败时,请检查手机蓝牙是否开启,车辆是否在蓝牙范围内,以及是否需要重新配对。',
category: '蓝牙连接',
tags: ['蓝牙', '连接问题', '配对'],
status: 'active'
}
])
// 知识库表单
const knowledgeForm = ref({
title: '',
content: '',
category: '',
tags: ''
})
// 计算属性
const filteredKnowledge = computed(() => {
if (!searchKeyword.value) {
return knowledgeList.value
}
const keyword = searchKeyword.value.toLowerCase()
return knowledgeList.value.filter(item =>
item.title.toLowerCase().includes(keyword) ||
item.content.toLowerCase().includes(keyword) ||
item.category.toLowerCase().includes(keyword) ||
item.tags.some(tag => tag.toLowerCase().includes(keyword))
)
})
// 方法
const handleSearch = () => {
// 搜索逻辑已在计算属性中处理
}
const handleEdit = (item: any) => {
editingItem.value = item
knowledgeForm.value = {
title: item.title,
content: item.content,
category: item.category,
tags: item.tags.join(', ')
}
showAddModal.value = true
}
const handleDelete = async (id: number) => {
try {
await ElMessageBox.confirm('确定要删除这条知识吗?', '确认删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const index = knowledgeList.value.findIndex(item => item.id === id)
if (index > -1) {
knowledgeList.value.splice(index, 1)
ElMessage.success('删除成功')
}
} catch (error) {
// 用户取消删除
}
}
const handleSave = () => {
if (!knowledgeForm.value.title || !knowledgeForm.value.content) {
ElMessage.warning('请填写标题和内容')
return
}
const tags = knowledgeForm.value.tags
.split(',')
.map(tag => tag.trim())
.filter(tag => tag)
if (editingItem.value) {
// 编辑
const index = knowledgeList.value.findIndex(item => item.id === editingItem.value.id)
if (index > -1) {
knowledgeList.value[index] = {
...knowledgeList.value[index],
title: knowledgeForm.value.title,
content: knowledgeForm.value.content,
category: knowledgeForm.value.category,
tags
}
}
ElMessage.success('更新成功')
} else {
// 新增
const newItem = {
id: Date.now(),
title: knowledgeForm.value.title,
content: knowledgeForm.value.content,
category: knowledgeForm.value.category,
tags,
status: 'active'
}
knowledgeList.value.unshift(newItem)
ElMessage.success('添加成功')
}
showAddModal.value = false
resetForm()
}
const resetForm = () => {
knowledgeForm.value = {
title: '',
content: '',
category: '',
tags: ''
}
editingItem.value = null
}
</script>
<style scoped lang="scss">
.knowledge-page {
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h2 {
margin: 0;
color: var(--el-text-color-primary);
}
}
.search-bar {
margin-bottom: 20px;
}
.loading-container {
padding: 20px;
}
.empty-state {
padding: 40px 20px;
}
.knowledge-list {
.knowledge-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 20px;
border: 1px solid var(--el-border-color);
border-radius: 8px;
margin-bottom: 16px;
transition: all 0.3s;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.knowledge-content {
flex: 1;
.knowledge-title {
margin: 0 0 8px 0;
color: var(--el-text-color-primary);
font-size: 16px;
font-weight: 600;
}
.knowledge-desc {
margin: 0 0 12px 0;
color: var(--el-text-color-secondary);
line-height: 1.5;
}
.knowledge-meta {
display: flex;
align-items: center;
gap: 12px;
.knowledge-tags {
display: flex;
gap: 6px;
}
}
}
.knowledge-actions {
margin-left: 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
}
}
}
</style>

View File

@@ -1,409 +0,0 @@
<template>
<div class="system-page">
<div class="page-header">
<h2>{{ $t('system.title') }}</h2>
</div>
<el-row :gutter="20">
<!-- 系统概览 -->
<el-col :span="12">
<el-card class="system-card">
<template #header>
<div class="card-header">
<el-icon><Monitor /></el-icon>
<span>{{ $t('system.general') }}</span>
</div>
</template>
<div class="system-info">
<div class="info-item">
<span class="info-label">系统版本:</span>
<span class="info-value">v1.4.0</span>
</div>
<div class="info-item">
<span class="info-label">运行时间:</span>
<span class="info-value">{{ systemUptime }}</span>
</div>
<div class="info-item">
<span class="info-label">Python版本:</span>
<span class="info-value">3.9.7</span>
</div>
<div class="info-item">
<span class="info-label">数据库:</span>
<span class="info-value">SQLite 3.36.0</span>
</div>
</div>
</el-card>
</el-col>
<!-- 性能监控 -->
<el-col :span="12">
<el-card class="system-card">
<template #header>
<div class="card-header">
<el-icon><TrendCharts /></el-icon>
<span>{{ $t('system.performance') }}</span>
</div>
</template>
<div class="performance-metrics">
<div class="metric-item">
<div class="metric-label">CPU使用率</div>
<el-progress :percentage="cpuUsage" :color="getProgressColor(cpuUsage)" />
</div>
<div class="metric-item">
<div class="metric-label">内存使用率</div>
<el-progress :percentage="memoryUsage" :color="getProgressColor(memoryUsage)" />
</div>
<div class="metric-item">
<div class="metric-label">磁盘使用率</div>
<el-progress :percentage="diskUsage" :color="getProgressColor(diskUsage)" />
</div>
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 20px;">
<!-- 监控设置 -->
<el-col :span="8">
<el-card class="system-card">
<template #header>
<div class="card-header">
<el-icon><Setting /></el-icon>
<span>{{ $t('system.monitoring') }}</span>
</div>
</template>
<div class="setting-section">
<div class="setting-item">
<span class="setting-label">自动监控</span>
<el-switch v-model="settings.autoMonitoring" />
</div>
<div class="setting-item">
<span class="setting-label">监控间隔</span>
<el-input-number v-model="settings.monitorInterval" :min="60" :max="3600" />
<span class="setting-unit"></span>
</div>
<div class="setting-item">
<span class="setting-label">日志级别</span>
<el-select v-model="settings.logLevel">
<el-option label="DEBUG" value="DEBUG" />
<el-option label="INFO" value="INFO" />
<el-option label="WARNING" value="WARNING" />
<el-option label="ERROR" value="ERROR" />
</el-select>
</div>
</div>
</el-card>
</el-col>
<!-- 预警设置 -->
<el-col :span="8">
<el-card class="system-card">
<template #header>
<div class="card-header">
<el-icon><Bell /></el-icon>
<span>{{ $t('system.alerts') }}</span>
</div>
</template>
<div class="setting-section">
<div class="setting-item">
<span class="setting-label">邮件通知</span>
<el-switch v-model="settings.emailNotification" />
</div>
<div class="setting-item">
<span class="setting-label">短信通知</span>
<el-switch v-model="settings.smsNotification" />
</div>
<div class="setting-item">
<span class="setting-label">通知阈值</span>
<el-input-number v-model="settings.notificationThreshold" :min="1" :max="100" />
<span class="setting-unit">%</span>
</div>
</div>
</el-card>
</el-col>
<!-- 集成设置 -->
<el-col :span="8">
<el-card class="system-card">
<template #header>
<div class="card-header">
<el-icon><Connection /></el-icon>
<span>{{ $t('system.integrations') }}</span>
</div>
</template>
<div class="integration-list">
<div class="integration-item">
<div class="integration-info">
<span class="integration-name">飞书集成</span>
<el-tag :type="integrations.feishu ? 'success' : 'danger'" size="small">
{{ integrations.feishu ? '已连接' : '未连接' }}
</el-tag>
</div>
<el-button size="small" @click="handleIntegration('feishu')">
{{ integrations.feishu ? '配置' : '连接' }}
</el-button>
</div>
<div class="integration-item">
<div class="integration-info">
<span class="integration-name">数据库</span>
<el-tag :type="integrations.database ? 'success' : 'danger'" size="small">
{{ integrations.database ? '已连接' : '未连接' }}
</el-tag>
</div>
<el-button size="small" @click="handleIntegration('database')">
{{ integrations.database ? '配置' : '连接' }}
</el-button>
</div>
<div class="integration-item">
<div class="integration-info">
<span class="integration-name">Redis缓存</span>
<el-tag :type="integrations.redis ? 'success' : 'danger'" size="small">
{{ integrations.redis ? '已连接' : '未连接' }}
</el-tag>
</div>
<el-button size="small" @click="handleIntegration('redis')">
{{ integrations.redis ? '配置' : '连接' }}
</el-button>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 操作按钮 -->
<div class="action-buttons">
<el-button type="primary" @click="saveSettings">
<el-icon><Check /></el-icon>
保存设置
</el-button>
<el-button type="success" @click="restartSystem">
<el-icon><Refresh /></el-icon>
重启系统
</el-button>
<el-button type="warning" @click="backupSystem">
<el-icon><Download /></el-icon>
备份系统
</el-button>
<el-button type="danger" @click="clearLogs">
<el-icon><Delete /></el-icon>
清理日志
</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage, ElMessageBox } from 'element-plus'
const { t } = useI18n()
// 状态
const systemUptime = ref('2天 14小时 32分钟')
const cpuUsage = ref(45)
const memoryUsage = ref(68)
const diskUsage = ref(23)
// 设置
const settings = ref({
autoMonitoring: true,
monitorInterval: 300,
logLevel: 'INFO',
emailNotification: true,
smsNotification: false,
notificationThreshold: 80
})
// 集成状态
const integrations = ref({
feishu: true,
database: true,
database: false
})
// 计算属性
const getProgressColor = (percentage: number) => {
if (percentage < 50) return '#67c23a'
if (percentage < 80) return '#e6a23c'
return '#f56c6c'
}
// 方法
const saveSettings = () => {
ElMessage.success('设置保存成功')
}
const restartSystem = async () => {
try {
await ElMessageBox.confirm('确定要重启系统吗?这将中断当前的所有服务。', '确认重启', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
ElMessage.success('系统重启中...')
} catch (error) {
// 用户取消重启
}
}
const backupSystem = () => {
ElMessage.info('备份功能开发中...')
}
const clearLogs = async () => {
try {
await ElMessageBox.confirm('确定要清理所有日志吗?此操作不可恢复。', '确认清理', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
ElMessage.success('日志清理完成')
} catch (error) {
// 用户取消清理
}
}
const handleIntegration = (type: string) => {
ElMessage.info(`${type} 集成配置功能开发中...`)
}
// 生命周期
onMounted(() => {
// 模拟数据更新
setInterval(() => {
cpuUsage.value = Math.floor(Math.random() * 100)
memoryUsage.value = Math.floor(Math.random() * 100)
diskUsage.value = Math.floor(Math.random() * 100)
}, 5000)
})
</script>
<style scoped lang="scss">
.system-page {
.page-header {
margin-bottom: 20px;
h2 {
margin: 0;
color: var(--el-text-color-primary);
}
}
.system-card {
height: 100%;
.card-header {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
}
.system-info {
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid var(--el-border-color-lighter);
&:last-child {
border-bottom: none;
}
.info-label {
color: var(--el-text-color-secondary);
}
.info-value {
color: var(--el-text-color-primary);
font-weight: 500;
}
}
}
.performance-metrics {
.metric-item {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
.metric-label {
margin-bottom: 8px;
color: var(--el-text-color-secondary);
font-size: 14px;
}
}
}
.setting-section {
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid var(--el-border-color-lighter);
&:last-child {
border-bottom: none;
}
.setting-label {
color: var(--el-text-color-secondary);
font-size: 14px;
}
.setting-unit {
margin-left: 8px;
color: var(--el-text-color-secondary);
font-size: 12px;
}
}
}
.integration-list {
.integration-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid var(--el-border-color-lighter);
&:last-child {
border-bottom: none;
}
.integration-info {
display: flex;
align-items: center;
gap: 12px;
.integration-name {
color: var(--el-text-color-primary);
font-size: 14px;
}
}
}
}
}
.action-buttons {
margin-top: 30px;
display: flex;
gap: 12px;
justify-content: center;
}
}
</style>

View File

@@ -1,25 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1,43 +0,0 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
imports: ['vue', 'vue-router', 'pinia'],
dts: true
}),
Components({
resolvers: [ElementPlusResolver()],
dts: true
})
],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true
},
'/ws': {
target: 'ws://localhost:8765',
ws: true
}
}
},
build: {
outDir: '../src/web/static/dist',
emptyOutDir: true
}
})

View File

@@ -1,133 +0,0 @@
#!/bin/bash
echo "TSP智能助手依赖安装脚本"
echo "=========================="
echo
# 检测操作系统
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
OS="linux"
elif [[ "$OSTYPE" == "darwin"* ]]; then
OS="macos"
else
OS="unknown"
fi
echo "检测到操作系统: $OS"
echo
# 安装Node.js和npm
install_nodejs() {
echo "安装Node.js和npm..."
if command -v node &> /dev/null; then
echo "Node.js已安装: $(node --version)"
return 0
fi
case $OS in
"linux")
# Ubuntu/Debian
if command -v apt &> /dev/null; then
echo "使用apt安装Node.js..."
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
# CentOS/RHEL
elif command -v yum &> /dev/null; then
echo "使用yum安装Node.js..."
curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -
sudo yum install -y nodejs
# Arch Linux
elif command -v pacman &> /dev/null; then
echo "使用pacman安装Node.js..."
sudo pacman -S nodejs npm
else
echo "请手动安装Node.js: https://nodejs.org/"
return 1
fi
;;
"macos")
if command -v brew &> /dev/null; then
echo "使用Homebrew安装Node.js..."
brew install node
else
echo "请安装Homebrew或手动安装Node.js: https://nodejs.org/"
return 1
fi
;;
*)
echo "请手动安装Node.js: https://nodejs.org/"
return 1
;;
esac
}
# 安装Python依赖
install_python_deps() {
echo "安装Python依赖..."
if [ -f "requirements.txt" ]; then
if command -v python3 &> /dev/null; then
python3 -m pip install -r requirements.txt
elif command -v python &> /dev/null; then
python -m pip install -r requirements.txt
else
echo "警告: 未找到Python"
fi
else
echo "警告: 未找到requirements.txt文件"
fi
}
# 安装前端依赖
install_frontend_deps() {
echo "安装前端依赖..."
if [ -d "frontend" ]; then
cd frontend
if [ -f "package.json" ]; then
npm install
else
echo "警告: 未找到package.json文件"
fi
cd ..
else
echo "警告: 未找到frontend目录"
fi
}
# 主安装流程
main() {
echo "开始安装依赖..."
echo
# 安装Node.js
install_nodejs
if [ $? -eq 0 ]; then
echo "Node.js安装成功: $(node --version)"
echo "npm版本: $(npm --version)"
else
echo "Node.js安装失败请手动安装"
fi
echo
# 安装Python依赖
install_python_deps
echo
# 安装前端依赖
install_frontend_deps
echo
echo "依赖安装完成!"
echo
echo "使用方法:"
echo " 启动传统版本: ./start_traditional.sh"
echo " 启动前端开发: ./start_frontend.sh"
echo " 构建前端: ./build_frontend.sh"
}
# 执行主函数
main

View File

@@ -130,50 +130,69 @@ class KnowledgeManager:
entries = session.query(KnowledgeEntry).filter(KnowledgeEntry.is_active == True).all()
if not entries:
logger.warning("知识库中没有活跃条目")
return []
# 计算相似度
texts = [entry.question + " " + entry.answer for entry in entries]
# 确保向量器已训练
try:
vocab_ok = hasattr(self.vectorizer, 'vocabulary_') and bool(self.vectorizer.vocabulary_)
if not vocab_ok:
self.vectorizer.fit(texts)
query_vector = self.vectorizer.transform([query])
entry_vectors = self.vectorizer.transform(texts)
similarities = cosine_similarity(query_vector, entry_vectors)[0]
except Exception as vec_err:
logger.warning(f"TF-IDF搜索失败回退到子串匹配: {vec_err}")
# 回退:子串匹配评分
similarities = []
q = query.strip()
for t in texts:
if not q:
similarities.append(0.0)
else:
score = 1.0 if q in t else 0.0
similarities.append(score)
similarities = np.array(similarities, dtype=float)
# 获取top_k个最相似的条目
top_indices = np.argsort(similarities)[-top_k:][::-1]
# 如果查询为空,返回所有条目
if not query.strip():
logger.info("查询为空,返回所有条目")
return [{
"id": entry.id,
"question": entry.question,
"answer": entry.answer,
"category": entry.category,
"confidence_score": entry.confidence_score,
"similarity_score": 1.0,
"usage_count": entry.usage_count,
"is_verified": entry.is_verified
} for entry in entries[:top_k]]
# 使用简化的关键词匹配搜索
q = query.strip().lower()
results = []
for idx in top_indices:
if similarities[idx] > 0.1: # 最小相似度阈值
entry = entries[idx]
for entry in entries:
# 组合问题和答案进行搜索
search_text = (entry.question + " " + entry.answer).lower()
# 计算匹配分数
score = 0.0
# 完全匹配
if q in search_text:
score = 1.0
else:
# 分词匹配
query_words = q.split()
text_words = search_text.split()
# 计算单词匹配度
matched_words = 0
for word in query_words:
if word in text_words:
matched_words += 1
if matched_words > 0:
score = matched_words / len(query_words) * 0.8
# 如果分数大于0添加到结果中
if score > 0:
results.append({
"id": entry.id,
"question": entry.question,
"answer": entry.answer,
"category": entry.category,
"confidence_score": entry.confidence_score,
"similarity_score": float(similarities[idx]),
"similarity_score": score,
"usage_count": entry.usage_count,
"is_verified": entry.is_verified
})
# 按相似度排序并返回top_k个结果
results.sort(key=lambda x: x['similarity_score'], reverse=True)
results = results[:top_k]
logger.info(f"搜索查询 '{query}' 返回 {len(results)} 个结果")
return results
except Exception as e:

View File

@@ -9,6 +9,7 @@ import sys
import os
import logging
from datetime import datetime, timedelta
from typing import Dict, Any
from flask import Flask, render_template, request, jsonify, send_from_directory
from flask_cors import CORS
@@ -16,14 +17,11 @@ from flask_cors import CORS
# 添加项目根目录到Python路径
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# 延迟导入,避免启动时重复初始化
# from src.main import TSPAssistant
# from src.agent_assistant import TSPAgentAssistant
# from src.dialogue.realtime_chat import RealtimeChatManager
# from src.vehicle.vehicle_data_manager import VehicleDataManager
# 导入核心模块
from src.core.database import db_manager
from src.core.models import Conversation, Alert, WorkOrder
from src.core.query_optimizer import query_optimizer
from src.web.service_manager import service_manager
# 导入蓝图
from src.web.blueprints.alerts import alerts_bp
@@ -33,6 +31,7 @@ from src.web.blueprints.knowledge import knowledge_bp
from src.web.blueprints.monitoring import monitoring_bp
from src.web.blueprints.system import system_bp
from src.web.blueprints.feishu_sync import feishu_sync_bp
from src.web.blueprints.core import core_bp
# 配置日志
logger = logging.getLogger(__name__)
@@ -59,43 +58,7 @@ UPLOAD_FOLDER = 'uploads'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max file size
# 延迟初始化TSP助手和Agent助手避免启动时重复初始化
assistant = None
agent_assistant = None
chat_manager = None
vehicle_manager = None
def get_assistant():
"""获取TSP助手实例懒加载"""
global assistant
if assistant is None:
from src.main import TSPAssistant
assistant = TSPAssistant()
return assistant
def get_agent_assistant():
"""获取Agent助手实例懒加载"""
global agent_assistant
if agent_assistant is None:
from src.agent_assistant import TSPAgentAssistant
agent_assistant = TSPAgentAssistant()
return agent_assistant
def get_chat_manager():
"""获取聊天管理器实例(懒加载)"""
global chat_manager
if chat_manager is None:
from src.dialogue.realtime_chat import RealtimeChatManager
chat_manager = RealtimeChatManager()
return chat_manager
def get_vehicle_manager():
"""获取车辆数据管理器实例(懒加载)"""
global vehicle_manager
if vehicle_manager is None:
from src.vehicle.vehicle_data_manager import VehicleDataManager
vehicle_manager = VehicleDataManager()
return vehicle_manager
# 使用统一的服务管理器
# 注册蓝图
app.register_blueprint(alerts_bp)
@@ -105,6 +68,7 @@ app.register_blueprint(knowledge_bp)
app.register_blueprint(monitoring_bp)
app.register_blueprint(system_bp)
app.register_blueprint(feishu_sync_bp)
app.register_blueprint(core_bp)
# 页面路由
@app.route('/')
@@ -132,157 +96,15 @@ def uploaded_file(filename):
"""提供上传文件的下载服务"""
return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
# 核心API路由
@app.route('/api/health')
def get_health():
"""获取系统健康状态附加1小时业务指标"""
try:
base = get_assistant().get_system_health() or {}
# 追加数据库近1小时指标
with db_manager.get_session() as session:
since = datetime.now() - timedelta(hours=1)
conv_count = session.query(Conversation).filter(Conversation.timestamp >= since).count()
resp_times = [c.response_time for c in session.query(Conversation).filter(Conversation.timestamp >= since).all() if c.response_time]
avg_resp = round(sum(resp_times)/len(resp_times), 2) if resp_times else 0
open_wos = session.query(WorkOrder).filter(WorkOrder.status == 'open').count()
levels = session.query(Alert.level).filter(Alert.is_active == True).all()
level_map = {}
for (lvl,) in levels:
level_map[lvl] = level_map.get(lvl, 0) + 1
base.update({
"throughput_1h": conv_count,
"avg_response_time_1h": avg_resp,
"open_workorders": open_wos,
"active_alerts_by_level": level_map
})
return jsonify(base)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/rules')
def get_rules():
"""获取预警规则列表"""
try:
rules = get_assistant().alert_system.rules
rules_data = []
for name, rule in rules.items():
rules_data.append({
"name": rule.name,
"description": rule.description,
"alert_type": rule.alert_type.value,
"level": rule.level.value,
"threshold": rule.threshold,
"condition": rule.condition,
"enabled": rule.enabled,
"check_interval": rule.check_interval,
"cooldown": rule.cooldown
})
return jsonify(rules_data)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/rules', methods=['POST'])
def create_rule():
"""创建预警规则"""
try:
from src.analytics.alert_system import AlertRule, AlertLevel, AlertType
data = request.get_json()
rule = AlertRule(
name=data['name'],
description=data['description'],
alert_type=AlertType(data['alert_type']),
level=AlertLevel(data['level']),
threshold=float(data['threshold']),
condition=data['condition'],
enabled=data.get('enabled', True),
check_interval=int(data.get('check_interval', 300)),
cooldown=int(data.get('cooldown', 3600))
)
success = get_assistant().alert_system.add_custom_rule(rule)
if success:
return jsonify({"success": True, "message": "规则创建成功"})
else:
return jsonify({"success": False, "message": "规则创建失败"}), 400
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/rules/<rule_name>', methods=['PUT'])
def update_rule(rule_name):
"""更新预警规则"""
try:
data = request.get_json()
success = get_assistant().alert_system.update_rule(rule_name, **data)
if success:
return jsonify({"success": True, "message": "规则更新成功"})
else:
return jsonify({"success": False, "message": "规则更新失败"}), 400
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/rules/<rule_name>', methods=['DELETE'])
def delete_rule(rule_name):
"""删除预警规则"""
try:
success = get_assistant().alert_system.delete_rule(rule_name)
if success:
return jsonify({"success": True, "message": "规则删除成功"})
else:
return jsonify({"success": False, "message": "规则删除失败"}), 400
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/monitor/start', methods=['POST'])
def start_monitoring():
"""启动监控服务"""
try:
success = get_assistant().start_monitoring()
if success:
return jsonify({"success": True, "message": "监控服务已启动"})
else:
return jsonify({"success": False, "message": "启动监控服务失败"}), 400
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/monitor/stop', methods=['POST'])
def stop_monitoring():
"""停止监控服务"""
try:
success = get_assistant().stop_monitoring()
if success:
return jsonify({"success": True, "message": "监控服务已停止"})
else:
return jsonify({"success": False, "message": "停止监控服务失败"}), 400
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/monitor/status')
def get_monitor_status():
"""获取监控服务状态"""
try:
health = get_assistant().get_system_health()
return jsonify({
"monitor_status": health.get("monitor_status", "unknown"),
"health_score": health.get("health_score", 0),
"active_alerts": health.get("active_alerts", 0)
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/check-alerts', methods=['POST'])
def check_alerts():
"""手动检查预警"""
try:
alerts = get_assistant().check_alerts()
return jsonify({
"success": True,
"alerts": alerts,
"count": len(alerts)
})
except Exception as e:
return jsonify({"error": str(e)}), 500
# ============================================================================
# 核心API路由 - 已迁移到蓝图
# ============================================================================
# 健康检查、预警规则、监控状态等核心功能已迁移到 core 蓝图
# 分析数据相关功能也已迁移到 core 蓝图
# ============================================================================
# 实时对话相关路由
# ============================================================================
@app.route('/api/chat/session', methods=['POST'])
def create_chat_session():
"""创建对话会话"""
@@ -291,7 +113,7 @@ def create_chat_session():
user_id = data.get('user_id', 'anonymous')
work_order_id = data.get('work_order_id')
session_id = get_chat_manager().create_session(user_id, work_order_id)
session_id = service_manager.get_chat_manager().create_session(user_id, work_order_id)
return jsonify({
"success": True,
@@ -312,7 +134,7 @@ def send_chat_message():
if not session_id or not message:
return jsonify({"error": "缺少必要参数"}), 400
result = get_chat_manager().process_message(session_id, message)
result = service_manager.get_chat_manager().process_message(session_id, message)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -321,7 +143,7 @@ def send_chat_message():
def get_chat_history(session_id):
"""获取对话历史"""
try:
history = get_chat_manager().get_session_history(session_id)
history = service_manager.get_chat_manager().get_session_history(session_id)
return jsonify({
"success": True,
"history": history
@@ -343,7 +165,7 @@ def create_work_order():
if not session_id or not title or not description:
return jsonify({"error": "缺少必要参数"}), 400
result = get_chat_manager().create_work_order(session_id, title, description, category, priority)
result = service_manager.get_chat_manager().create_work_order(session_id, title, description, category, priority)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -352,7 +174,7 @@ def create_work_order():
def get_work_order_status(work_order_id):
"""获取工单状态"""
try:
result = get_chat_manager().get_work_order_status(work_order_id)
result = service_manager.get_chat_manager().get_work_order_status(work_order_id)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -361,7 +183,7 @@ def get_work_order_status(work_order_id):
def end_chat_session(session_id):
"""结束对话会话"""
try:
success = get_chat_manager().end_session(session_id)
success = service_manager.get_chat_manager().end_session(session_id)
return jsonify({
"success": success,
"message": "会话已结束" if success else "结束会话失败"
@@ -374,7 +196,7 @@ def get_active_sessions():
"""获取活跃会话列表"""
try:
# 确保chat_manager已初始化
manager = get_chat_manager()
manager = service_manager.get_chat_manager()
sessions = manager.get_active_sessions()
return jsonify({
"success": True,
@@ -384,12 +206,14 @@ def get_active_sessions():
logger.error(f"获取活跃会话失败: {e}")
return jsonify({"error": str(e)}), 500
# ============================================================================
# Agent相关API
# ============================================================================
@app.route('/api/agent/status')
def get_agent_status():
"""获取Agent状态"""
try:
status = get_agent_assistant().get_agent_status()
status = service_manager.get_agent_assistant().get_agent_status()
return jsonify(status)
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -399,7 +223,7 @@ def get_agent_action_history():
"""获取Agent动作执行历史"""
try:
limit = request.args.get('limit', 50, type=int)
history = get_agent_assistant().get_action_history(limit)
history = service_manager.get_agent_assistant().get_action_history(limit)
return jsonify({
"success": True,
"history": history,
@@ -413,7 +237,7 @@ def trigger_sample_action():
"""触发示例动作"""
try:
import asyncio
result = asyncio.run(get_agent_assistant().trigger_sample_actions())
result = asyncio.run(service_manager.get_agent_assistant().trigger_sample_actions())
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -422,7 +246,7 @@ def trigger_sample_action():
def clear_agent_history():
"""清空Agent执行历史"""
try:
result = get_agent_assistant().clear_execution_history()
result = service_manager.get_agent_assistant().clear_execution_history()
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -431,7 +255,7 @@ def clear_agent_history():
def get_llm_stats():
"""获取LLM使用统计"""
try:
stats = get_agent_assistant().get_llm_usage_stats()
stats = service_manager.get_agent_assistant().get_llm_usage_stats()
return jsonify({
"success": True,
"stats": stats
@@ -445,7 +269,7 @@ def toggle_agent_mode():
try:
data = request.get_json()
enabled = data.get('enabled', True)
success = get_agent_assistant().toggle_agent_mode(enabled)
success = service_manager.get_agent_assistant().toggle_agent_mode(enabled)
return jsonify({
"success": success,
"message": f"Agent模式已{'启用' if enabled else '禁用'}"
@@ -457,7 +281,7 @@ def toggle_agent_mode():
def start_agent_monitoring():
"""启动Agent监控"""
try:
success = get_agent_assistant().start_proactive_monitoring()
success = service_manager.get_agent_assistant().start_proactive_monitoring()
return jsonify({
"success": success,
"message": "Agent监控已启动" if success else "启动失败"
@@ -469,7 +293,7 @@ def start_agent_monitoring():
def stop_agent_monitoring():
"""停止Agent监控"""
try:
success = get_agent_assistant().stop_proactive_monitoring()
success = service_manager.get_agent_assistant().stop_proactive_monitoring()
return jsonify({
"success": success,
"message": "Agent监控已停止" if success else "停止失败"
@@ -481,7 +305,7 @@ def stop_agent_monitoring():
def proactive_monitoring():
"""主动监控检查"""
try:
result = get_agent_assistant().run_proactive_monitoring()
result = service_manager.get_agent_assistant().run_proactive_monitoring()
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -490,7 +314,7 @@ def proactive_monitoring():
def intelligent_analysis():
"""智能分析"""
try:
analysis = get_agent_assistant().run_intelligent_analysis()
analysis = service_manager.get_agent_assistant().run_intelligent_analysis()
return jsonify({"success": True, "analysis": analysis})
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -507,7 +331,7 @@ def agent_chat():
return jsonify({"error": "消息不能为空"}), 400
# 使用Agent助手处理消息
agent_assistant = get_agent_assistant()
agent_assistant = service_manager.get_agent_assistant()
# 模拟Agent处理实际应该调用真正的Agent处理逻辑
import asyncio
@@ -527,12 +351,15 @@ def agent_chat():
except Exception as e:
return jsonify({"error": str(e)}), 500
# ============================================================================
# Agent 工具统计与自定义工具
# ============================================================================
@app.route('/api/agent/tools/stats')
def get_agent_tools_stats():
try:
tools = get_agent_assistant().agent_core.tool_manager.get_available_tools()
performance = get_agent_assistant().agent_core.tool_manager.get_tool_performance_report()
agent_assistant = service_manager.get_agent_assistant()
tools = agent_assistant.agent_core.tool_manager.get_available_tools()
performance = agent_assistant.agent_core.tool_manager.get_tool_performance_report()
return jsonify({
"success": True,
"tools": tools,
@@ -552,7 +379,7 @@ def execute_agent_tool():
return jsonify({"error": "缺少工具名称tool"}), 400
import asyncio
result = asyncio.run(get_agent_assistant().agent_core.tool_manager.execute_tool(tool_name, parameters))
result = asyncio.run(service_manager.get_agent_assistant().agent_core.tool_manager.execute_tool(tool_name, parameters))
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -570,7 +397,7 @@ def register_custom_tool():
def _placeholder_tool(**kwargs):
return {"message": f"自定义工具 {name} 已登记(占位),当前不可执行", "params": kwargs}
get_agent_assistant().agent_core.tool_manager.register_tool(
service_manager.get_agent_assistant().agent_core.tool_manager.register_tool(
name,
_placeholder_tool,
metadata={"description": description, "custom": True}
@@ -582,55 +409,21 @@ def register_custom_tool():
@app.route('/api/agent/tools/unregister/<name>', methods=['DELETE'])
def unregister_custom_tool(name):
try:
success = get_agent_assistant().agent_core.tool_manager.unregister_tool(name)
success = service_manager.get_agent_assistant().agent_core.tool_manager.unregister_tool(name)
return jsonify({"success": success})
except Exception as e:
return jsonify({"error": str(e)}), 500
# 分析相关API
@app.route('/api/analytics')
def get_analytics():
"""获取分析数据"""
try:
# 支持多种参数
time_range = request.args.get('timeRange', request.args.get('days', '30'))
dimension = request.args.get('dimension', 'workorders')
# 参数验证
try:
days = int(time_range)
if days <= 0 or days > 365:
days = 30
except (ValueError, TypeError):
days = 30
analytics = generate_db_analytics(days, dimension)
# 确保返回的数据结构完整
if not analytics:
analytics = {
"workorders": {"total": 0, "open": 0, "resolved": 0, "trend": []},
"alerts": {"total": 0, "critical": 0, "warning": 0, "trend": []},
"conversations": {"total": 0, "avg_confidence": 0, "trend": []},
"performance": {"avg_response_time": 0, "success_rate": 0}
}
return jsonify(analytics)
except Exception as e:
logger.error(f"获取分析数据失败: {e}")
return jsonify({"error": str(e)}), 500
def generate_db_analytics(days: int, dimension: str) -> dict:
"""基于数据库生成真实分析数据(优化版)"""
# 使用优化后的查询
return query_optimizer.get_analytics_optimized(days)
# ============================================================================
# 分析相关API - 已迁移到 core 蓝图
# ============================================================================
@app.route('/api/analytics/export')
def export_analytics():
"""导出分析报告"""
try:
# 生成Excel报告使用数据库真实数据
analytics = generate_db_analytics(30, 'workorders')
analytics = query_optimizer.get_analytics_optimized(30)
# 创建工作簿
from openpyxl import Workbook
@@ -664,7 +457,9 @@ def export_analytics():
except Exception as e:
return jsonify({"error": str(e)}), 500
# ============================================================================
# 车辆数据相关API
# ============================================================================
@app.route('/api/vehicle/data')
def get_vehicle_data():
"""获取车辆数据"""
@@ -674,12 +469,13 @@ def get_vehicle_data():
data_type = request.args.get('data_type')
limit = request.args.get('limit', 10, type=int)
vehicle_mgr = service_manager.get_vehicle_manager()
if vehicle_vin:
data = vehicle_manager.get_vehicle_data_by_vin(vehicle_vin, data_type, limit)
data = vehicle_mgr.get_vehicle_data_by_vin(vehicle_vin, data_type, limit)
elif vehicle_id:
data = vehicle_manager.get_vehicle_data(vehicle_id, data_type, limit)
data = vehicle_mgr.get_vehicle_data(vehicle_id, data_type, limit)
else:
data = vehicle_manager.search_vehicle_data(limit=limit)
data = vehicle_mgr.search_vehicle_data(limit=limit)
return jsonify(data)
except Exception as e:
@@ -689,7 +485,7 @@ def get_vehicle_data():
def get_latest_vehicle_data_by_vin(vehicle_vin):
"""按VIN获取车辆最新数据"""
try:
data = vehicle_manager.get_latest_vehicle_data_by_vin(vehicle_vin)
data = service_manager.get_vehicle_manager().get_latest_vehicle_data_by_vin(vehicle_vin)
return jsonify(data)
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -698,7 +494,7 @@ def get_latest_vehicle_data_by_vin(vehicle_vin):
def get_latest_vehicle_data(vehicle_id):
"""获取车辆最新数据"""
try:
data = vehicle_manager.get_latest_vehicle_data(vehicle_id)
data = service_manager.get_vehicle_manager().get_latest_vehicle_data(vehicle_id)
return jsonify(data)
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -707,7 +503,7 @@ def get_latest_vehicle_data(vehicle_id):
def get_vehicle_summary(vehicle_id):
"""获取车辆数据摘要"""
try:
summary = vehicle_manager.get_vehicle_summary(vehicle_id)
summary = service_manager.get_vehicle_manager().get_vehicle_summary(vehicle_id)
return jsonify(summary)
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -717,7 +513,7 @@ def add_vehicle_data():
"""添加车辆数据"""
try:
data = request.get_json()
success = vehicle_manager.add_vehicle_data(
success = service_manager.get_vehicle_manager().add_vehicle_data(
vehicle_id=data['vehicle_id'],
data_type=data['data_type'],
data_value=data['data_value'],
@@ -731,12 +527,14 @@ def add_vehicle_data():
def init_sample_vehicle_data():
"""初始化示例车辆数据"""
try:
success = vehicle_manager.add_sample_vehicle_data()
success = service_manager.get_vehicle_manager().add_sample_vehicle_data()
return jsonify({"success": success, "message": "示例数据初始化成功" if success else "初始化失败"})
except Exception as e:
return jsonify({"error": str(e)}), 500
# ============================================================================
# API测试相关接口
# ============================================================================
@app.route('/api/test/connection', methods=['POST'])
def test_api_connection():
"""测试API连接"""
@@ -778,6 +576,9 @@ def test_model_response():
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
# ============================================================================
# 应用启动配置
# ============================================================================
# 飞书同步功能已合并到主页面,不再需要单独的路由
if __name__ == '__main__':

File diff suppressed because it is too large Load Diff

View File

@@ -1,740 +0,0 @@
# -*- coding: utf-8 -*-
"""
TSP助手预警管理Web应用
提供预警系统的Web界面和API接口
重构版本 - 使用蓝图架构
"""
import sys
import os
import logging
from datetime import datetime, timedelta
from flask import Flask, render_template, request, jsonify, send_from_directory
from flask_cors import CORS
# 添加项目根目录到Python路径
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from src.main import TSPAssistant
from src.agent_assistant import TSPAgentAssistant
from src.dialogue.realtime_chat import RealtimeChatManager
from src.vehicle.vehicle_data_manager import VehicleDataManager
from src.core.database import db_manager
from src.core.models import Conversation, Alert, WorkOrder
from src.core.query_optimizer import query_optimizer
# 导入蓝图
from src.web.blueprints.alerts import alerts_bp
from src.web.blueprints.workorders import workorders_bp
from src.web.blueprints.conversations import conversations_bp
from src.web.blueprints.knowledge import knowledge_bp
from src.web.blueprints.monitoring import monitoring_bp
from src.web.blueprints.system import system_bp
# 配置日志
logger = logging.getLogger(__name__)
# 抑制 /api/health 的访问日志
werkzeug_logger = logging.getLogger('werkzeug')
class HealthLogFilter(logging.Filter):
def filter(self, record):
try:
msg = record.getMessage()
return '/api/health' not in msg
except Exception:
return True
werkzeug_logger.addFilter(HealthLogFilter())
# 创建Flask应用
app = Flask(__name__)
CORS(app)
# 配置上传文件夹
UPLOAD_FOLDER = 'uploads'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max file size
# 延迟初始化TSP助手和Agent助手避免启动时重复初始化
assistant = None
agent_assistant = None
chat_manager = None
vehicle_manager = None
def get_assistant():
"""获取TSP助手实例懒加载"""
global assistant
if assistant is None:
assistant = TSPAssistant()
return assistant
def get_agent_assistant():
"""获取Agent助手实例懒加载"""
global agent_assistant
if agent_assistant is None:
agent_assistant = TSPAgentAssistant()
return agent_assistant
def get_chat_manager():
"""获取聊天管理器实例(懒加载)"""
global chat_manager
if chat_manager is None:
chat_manager = RealtimeChatManager()
return chat_manager
def get_vehicle_manager():
"""获取车辆数据管理器实例(懒加载)"""
global vehicle_manager
if vehicle_manager is None:
vehicle_manager = VehicleDataManager()
return vehicle_manager
# 注册蓝图
app.register_blueprint(alerts_bp)
app.register_blueprint(workorders_bp)
app.register_blueprint(conversations_bp)
app.register_blueprint(knowledge_bp)
app.register_blueprint(monitoring_bp)
app.register_blueprint(system_bp)
# 页面路由
@app.route('/')
def index():
"""主页 - 综合管理平台"""
return render_template('dashboard.html')
@app.route('/alerts')
def alerts():
"""预警管理页面"""
return render_template('index.html')
@app.route('/chat')
def chat():
"""实时对话页面 (WebSocket版本)"""
return render_template('chat.html')
@app.route('/chat-http')
def chat_http():
"""实时对话页面 (HTTP版本)"""
return render_template('chat_http.html')
@app.route('/uploads/<filename>')
def uploaded_file(filename):
"""提供上传文件的下载服务"""
return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
# 核心API路由
@app.route('/api/health')
def get_health():
"""获取系统健康状态附加1小时业务指标"""
try:
base = get_assistant().get_system_health() or {}
# 追加数据库近1小时指标
with db_manager.get_session() as session:
since = datetime.now() - timedelta(hours=1)
conv_count = session.query(Conversation).filter(Conversation.timestamp >= since).count()
resp_times = [c.response_time for c in session.query(Conversation).filter(Conversation.timestamp >= since).all() if c.response_time]
avg_resp = round(sum(resp_times)/len(resp_times), 2) if resp_times else 0
open_wos = session.query(WorkOrder).filter(WorkOrder.status == 'open').count()
levels = session.query(Alert.level).filter(Alert.is_active == True).all()
level_map = {}
for (lvl,) in levels:
level_map[lvl] = level_map.get(lvl, 0) + 1
base.update({
"throughput_1h": conv_count,
"avg_response_time_1h": avg_resp,
"open_workorders": open_wos,
"active_alerts_by_level": level_map
})
return jsonify(base)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/rules')
def get_rules():
"""获取预警规则列表"""
try:
rules = get_assistant().alert_system.rules
rules_data = []
for name, rule in rules.items():
rules_data.append({
"name": rule.name,
"description": rule.description,
"alert_type": rule.alert_type.value,
"level": rule.level.value,
"threshold": rule.threshold,
"condition": rule.condition,
"enabled": rule.enabled,
"check_interval": rule.check_interval,
"cooldown": rule.cooldown
})
return jsonify(rules_data)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/rules', methods=['POST'])
def create_rule():
"""创建预警规则"""
try:
from src.analytics.alert_system import AlertRule, AlertLevel, AlertType
data = request.get_json()
rule = AlertRule(
name=data['name'],
description=data['description'],
alert_type=AlertType(data['alert_type']),
level=AlertLevel(data['level']),
threshold=float(data['threshold']),
condition=data['condition'],
enabled=data.get('enabled', True),
check_interval=int(data.get('check_interval', 300)),
cooldown=int(data.get('cooldown', 3600))
)
success = get_assistant().alert_system.add_custom_rule(rule)
if success:
return jsonify({"success": True, "message": "规则创建成功"})
else:
return jsonify({"success": False, "message": "规则创建失败"}), 400
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/rules/<rule_name>', methods=['PUT'])
def update_rule(rule_name):
"""更新预警规则"""
try:
data = request.get_json()
success = get_assistant().alert_system.update_rule(rule_name, **data)
if success:
return jsonify({"success": True, "message": "规则更新成功"})
else:
return jsonify({"success": False, "message": "规则更新失败"}), 400
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/rules/<rule_name>', methods=['DELETE'])
def delete_rule(rule_name):
"""删除预警规则"""
try:
success = get_assistant().alert_system.delete_rule(rule_name)
if success:
return jsonify({"success": True, "message": "规则删除成功"})
else:
return jsonify({"success": False, "message": "规则删除失败"}), 400
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/monitor/start', methods=['POST'])
def start_monitoring():
"""启动监控服务"""
try:
success = get_assistant().start_monitoring()
if success:
return jsonify({"success": True, "message": "监控服务已启动"})
else:
return jsonify({"success": False, "message": "启动监控服务失败"}), 400
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/monitor/stop', methods=['POST'])
def stop_monitoring():
"""停止监控服务"""
try:
success = get_assistant().stop_monitoring()
if success:
return jsonify({"success": True, "message": "监控服务已停止"})
else:
return jsonify({"success": False, "message": "停止监控服务失败"}), 400
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/monitor/status')
def get_monitor_status():
"""获取监控服务状态"""
try:
health = get_assistant().get_system_health()
return jsonify({
"monitor_status": health.get("monitor_status", "unknown"),
"health_score": health.get("health_score", 0),
"active_alerts": health.get("active_alerts", 0)
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/check-alerts', methods=['POST'])
def check_alerts():
"""手动检查预警"""
try:
alerts = get_assistant().check_alerts()
return jsonify({
"success": True,
"alerts": alerts,
"count": len(alerts)
})
except Exception as e:
return jsonify({"error": str(e)}), 500
# 实时对话相关路由
@app.route('/api/chat/session', methods=['POST'])
def create_chat_session():
"""创建对话会话"""
try:
data = request.get_json()
user_id = data.get('user_id', 'anonymous')
work_order_id = data.get('work_order_id')
session_id = get_chat_manager().create_session(user_id, work_order_id)
return jsonify({
"success": True,
"session_id": session_id,
"message": "会话创建成功"
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/chat/message', methods=['POST'])
def send_chat_message():
"""发送聊天消息"""
try:
data = request.get_json()
session_id = data.get('session_id')
message = data.get('message')
if not session_id or not message:
return jsonify({"error": "缺少必要参数"}), 400
result = get_chat_manager().process_message(session_id, message)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/chat/history/<session_id>')
def get_chat_history(session_id):
"""获取对话历史"""
try:
history = get_chat_manager().get_session_history(session_id)
return jsonify({
"success": True,
"history": history
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/chat/work-order', methods=['POST'])
def create_work_order():
"""创建工单"""
try:
data = request.get_json()
session_id = data.get('session_id')
title = data.get('title')
description = data.get('description')
category = data.get('category', '技术问题')
priority = data.get('priority', 'medium')
if not session_id or not title or not description:
return jsonify({"error": "缺少必要参数"}), 400
result = get_chat_manager().create_work_order(session_id, title, description, category, priority)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/chat/work-order/<int:work_order_id>')
def get_work_order_status(work_order_id):
"""获取工单状态"""
try:
result = get_chat_manager().get_work_order_status(work_order_id)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/chat/session/<session_id>', methods=['DELETE'])
def end_chat_session(session_id):
"""结束对话会话"""
try:
success = get_chat_manager().end_session(session_id)
return jsonify({
"success": success,
"message": "会话已结束" if success else "结束会话失败"
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/chat/sessions')
def get_active_sessions():
"""获取活跃会话列表"""
try:
sessions = chat_manager.get_active_sessions()
return jsonify({
"success": True,
"sessions": sessions
})
except Exception as e:
return jsonify({"error": str(e)}), 500
# Agent相关API
@app.route('/api/agent/status')
def get_agent_status():
"""获取Agent状态"""
try:
status = agent_assistant.get_agent_status()
return jsonify(status)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/agent/action-history')
def get_agent_action_history():
"""获取Agent动作执行历史"""
try:
limit = request.args.get('limit', 50, type=int)
history = agent_assistant.get_action_history(limit)
return jsonify({
"success": True,
"history": history,
"count": len(history)
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/agent/trigger-sample', methods=['POST'])
def trigger_sample_action():
"""触发示例动作"""
try:
import asyncio
result = asyncio.run(agent_assistant.trigger_sample_actions())
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/agent/clear-history', methods=['POST'])
def clear_agent_history():
"""清空Agent执行历史"""
try:
result = agent_assistant.clear_execution_history()
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/agent/llm-stats')
def get_llm_stats():
"""获取LLM使用统计"""
try:
stats = agent_assistant.get_llm_usage_stats()
return jsonify({
"success": True,
"stats": stats
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/agent/toggle', methods=['POST'])
def toggle_agent_mode():
"""切换Agent模式"""
try:
data = request.get_json()
enabled = data.get('enabled', True)
success = agent_assistant.toggle_agent_mode(enabled)
return jsonify({
"success": success,
"message": f"Agent模式已{'启用' if enabled else '禁用'}"
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/agent/monitoring/start', methods=['POST'])
def start_agent_monitoring():
"""启动Agent监控"""
try:
success = agent_assistant.start_proactive_monitoring()
return jsonify({
"success": success,
"message": "Agent监控已启动" if success else "启动失败"
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/agent/monitoring/stop', methods=['POST'])
def stop_agent_monitoring():
"""停止Agent监控"""
try:
success = agent_assistant.stop_proactive_monitoring()
return jsonify({
"success": success,
"message": "Agent监控已停止" if success else "停止失败"
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/agent/proactive-monitoring', methods=['POST'])
def proactive_monitoring():
"""主动监控检查"""
try:
result = agent_assistant.run_proactive_monitoring()
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/agent/intelligent-analysis', methods=['POST'])
def intelligent_analysis():
"""智能分析"""
try:
analysis = get_agent_assistant().run_intelligent_analysis()
return jsonify({"success": True, "analysis": analysis})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/agent/chat', methods=['POST'])
def agent_chat():
"""Agent对话接口"""
try:
data = request.get_json()
message = data.get('message', '')
context = data.get('context', {})
if not message:
return jsonify({"error": "消息不能为空"}), 400
# 使用Agent助手处理消息
agent_assistant = get_agent_assistant()
# 模拟Agent处理实际应该调用真正的Agent处理逻辑
import asyncio
result = asyncio.run(agent_assistant.process_message_agent(
message=message,
user_id=context.get('user_id', 'admin'),
work_order_id=None,
enable_proactive=True
))
return jsonify({
"success": True,
"response": result.get('response', 'Agent已处理您的请求'),
"actions": result.get('actions', []),
"status": result.get('status', 'completed')
})
except Exception as e:
return jsonify({"error": str(e)}), 500
# Agent 工具统计与自定义工具
@app.route('/api/agent/tools/stats')
def get_agent_tools_stats():
try:
tools = agent_assistant.agent_core.tool_manager.get_available_tools()
performance = agent_assistant.agent_core.tool_manager.get_tool_performance_report()
return jsonify({
"success": True,
"tools": tools,
"performance": performance
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/agent/tools/register', methods=['POST'])
def register_custom_tool():
"""注册自定义工具(仅登记元数据,函数为占位符)"""
try:
data = request.get_json() or {}
name = data.get('name')
description = data.get('description', '')
if not name:
return jsonify({"error": "缺少工具名称"}), 400
def _placeholder_tool(**kwargs):
return {"message": f"自定义工具 {name} 已登记(占位),当前不可执行", "params": kwargs}
agent_assistant.agent_core.tool_manager.register_tool(
name,
_placeholder_tool,
metadata={"description": description, "custom": True}
)
return jsonify({"success": True, "message": "工具已注册"})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/agent/tools/unregister/<name>', methods=['DELETE'])
def unregister_custom_tool(name):
try:
success = agent_assistant.agent_core.tool_manager.unregister_tool(name)
return jsonify({"success": success})
except Exception as e:
return jsonify({"error": str(e)}), 500
# 分析相关API
@app.route('/api/analytics')
def get_analytics():
"""获取分析数据"""
try:
# 支持多种参数
time_range = request.args.get('timeRange', request.args.get('days', '30'))
dimension = request.args.get('dimension', 'workorders')
analytics = generate_db_analytics(int(time_range), dimension)
return jsonify(analytics)
except Exception as e:
return jsonify({"error": str(e)}), 500
def generate_db_analytics(days: int, dimension: str) -> dict:
"""基于数据库生成真实分析数据(优化版)"""
# 使用优化后的查询
return query_optimizer.get_analytics_optimized(days)
@app.route('/api/analytics/export')
def export_analytics():
"""导出分析报告"""
try:
# 生成Excel报告使用数据库真实数据
analytics = generate_db_analytics(30, 'workorders')
# 创建工作簿
from openpyxl import Workbook
from openpyxl.styles import Font
wb = Workbook()
ws = wb.active
ws.title = "分析报告"
# 添加标题
ws['A1'] = 'TSP智能助手分析报告'
ws['A1'].font = Font(size=16, bold=True)
# 添加工单统计
ws['A3'] = '工单统计'
ws['A3'].font = Font(bold=True)
ws['A4'] = '总工单数'
ws['B4'] = analytics['workorders']['total']
ws['A5'] = '待处理'
ws['B5'] = analytics['workorders']['open']
ws['A6'] = '已解决'
ws['B6'] = analytics['workorders']['resolved']
# 保存文件
report_path = 'uploads/analytics_report.xlsx'
os.makedirs('uploads', exist_ok=True)
wb.save(report_path)
from flask import send_file
return send_file(report_path, as_attachment=True, download_name='analytics_report.xlsx')
except Exception as e:
return jsonify({"error": str(e)}), 500
# 车辆数据相关API
@app.route('/api/vehicle/data')
def get_vehicle_data():
"""获取车辆数据"""
try:
vehicle_id = request.args.get('vehicle_id')
vehicle_vin = request.args.get('vehicle_vin')
data_type = request.args.get('data_type')
limit = request.args.get('limit', 10, type=int)
if vehicle_vin:
data = vehicle_manager.get_vehicle_data_by_vin(vehicle_vin, data_type, limit)
elif vehicle_id:
data = vehicle_manager.get_vehicle_data(vehicle_id, data_type, limit)
else:
data = vehicle_manager.search_vehicle_data(limit=limit)
return jsonify(data)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/vehicle/data/vin/<vehicle_vin>/latest')
def get_latest_vehicle_data_by_vin(vehicle_vin):
"""按VIN获取车辆最新数据"""
try:
data = vehicle_manager.get_latest_vehicle_data_by_vin(vehicle_vin)
return jsonify(data)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/vehicle/data/<vehicle_id>/latest')
def get_latest_vehicle_data(vehicle_id):
"""获取车辆最新数据"""
try:
data = vehicle_manager.get_latest_vehicle_data(vehicle_id)
return jsonify(data)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/vehicle/data/<vehicle_id>/summary')
def get_vehicle_summary(vehicle_id):
"""获取车辆数据摘要"""
try:
summary = vehicle_manager.get_vehicle_summary(vehicle_id)
return jsonify(summary)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/vehicle/data', methods=['POST'])
def add_vehicle_data():
"""添加车辆数据"""
try:
data = request.get_json()
success = vehicle_manager.add_vehicle_data(
vehicle_id=data['vehicle_id'],
data_type=data['data_type'],
data_value=data['data_value'],
vehicle_vin=data.get('vehicle_vin')
)
return jsonify({"success": success, "message": "数据添加成功" if success else "添加失败"})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/vehicle/init-sample-data', methods=['POST'])
def init_sample_vehicle_data():
"""初始化示例车辆数据"""
try:
success = vehicle_manager.add_sample_vehicle_data()
return jsonify({"success": success, "message": "示例数据初始化成功" if success else "初始化失败"})
except Exception as e:
return jsonify({"error": str(e)}), 500
# API测试相关接口
@app.route('/api/test/connection', methods=['POST'])
def test_api_connection():
"""测试API连接"""
try:
data = request.get_json()
api_provider = data.get('api_provider', 'openai')
api_base_url = data.get('api_base_url', '')
api_key = data.get('api_key', '')
model_name = data.get('model_name', 'qwen-turbo')
# 这里可以调用LLM客户端进行连接测试
# 暂时返回模拟结果
return jsonify({
"success": True,
"message": f"API连接测试成功 - {api_provider}",
"response_time": "150ms",
"model_status": "可用"
})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/test/model', methods=['POST'])
def test_model_response():
"""测试模型回答"""
try:
data = request.get_json()
test_message = data.get('test_message', '你好,请简单介绍一下你自己')
# 这里可以调用LLM客户端进行回答测试
# 暂时返回模拟结果
return jsonify({
"success": True,
"test_message": test_message,
"response": "你好我是TSP智能助手基于大语言模型构建的智能客服系统。我可以帮助您解决车辆相关问题提供技术支持和服务。",
"response_time": "1.2s",
"tokens_used": 45
})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
if __name__ == '__main__':
import time
app.config['START_TIME'] = time.time()
app.config['SERVER_PORT'] = 5000
app.config['WEBSOCKET_PORT'] = 8765
app.run(debug=True, host='0.0.0.0', port=5000)

View File

@@ -1,218 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
TSP智能助手 - 新版Web应用
支持Vue 3前端和传统HTML页面
"""
import os
import sys
import json
import logging
from datetime import datetime
from flask import Flask, render_template, jsonify, request, send_from_directory
from flask_cors import CORS
from werkzeug.exceptions import NotFound
# 添加项目根目录到Python路径
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(os.path.dirname(current_dir))
sys.path.insert(0, project_root)
from src.core.database import get_db_connection
from src.analytics.monitor_service import MonitorService
from src.analytics.alert_system import AlertSystem
from src.dialogue.dialogue_manager import DialogueManager
from src.knowledge_base.knowledge_manager import KnowledgeManager
from src.integrations.workorder_sync import WorkOrderSync
from src.integrations.flexible_field_mapper import FlexibleFieldMapper
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def create_app():
"""创建Flask应用"""
app = Flask(__name__)
# 配置
app.config['SECRET_KEY'] = 'tsp-assistant-secret-key'
app.config['JSON_AS_ASCII'] = False
# 启用CORS
CORS(app, resources={
r"/api/*": {"origins": ["http://localhost:3000", "http://127.0.0.1:3000"]},
r"/ws/*": {"origins": ["http://localhost:3000", "http://127.0.0.1:3000"]}
})
# 初始化服务
monitor_service = MonitorService()
alert_system = AlertSystem()
dialogue_manager = DialogueManager()
knowledge_manager = KnowledgeManager()
workorder_sync = WorkOrderSync()
field_mapper = FlexibleFieldMapper()
# 注册蓝图
from src.web.blueprints.alerts import alerts_bp
from src.web.blueprints.conversations import conversations_bp
from src.web.blueprints.knowledge import knowledge_bp
from src.web.blueprints.monitoring import monitoring_bp
from src.web.blueprints.system import system_bp
from src.web.blueprints.feishu_sync import feishu_sync_bp
from src.web.blueprints.workorders import workorders_bp
app.register_blueprint(alerts_bp, url_prefix='/api/alerts')
app.register_blueprint(conversations_bp, url_prefix='/api/conversations')
app.register_blueprint(knowledge_bp, url_prefix='/api/knowledge')
app.register_blueprint(monitoring_bp, url_prefix='/api/monitor')
app.register_blueprint(system_bp, url_prefix='/api/system')
app.register_blueprint(feishu_sync_bp, url_prefix='/api/feishu')
app.register_blueprint(workorders_bp, url_prefix='/api/workorders')
# 静态文件路由
@app.route('/static/dist/<path:filename>')
def serve_frontend_dist(filename):
"""提供Vue前端构建文件"""
dist_path = os.path.join(current_dir, 'static', 'dist')
return send_from_directory(dist_path, filename)
@app.route('/')
def index():
"""首页 - 重定向到Vue应用"""
return render_template('index_new.html')
@app.route('/chat')
def chat():
"""聊天页面 - 重定向到Vue应用"""
return render_template('index_new.html')
@app.route('/alerts')
def alerts():
"""预警页面 - 重定向到Vue应用"""
return render_template('index_new.html')
@app.route('/knowledge')
def knowledge():
"""知识库页面 - 重定向到Vue应用"""
return render_template('index_new.html')
@app.route('/field-mapping')
def field_mapping():
"""字段映射页面 - 重定向到Vue应用"""
return render_template('index_new.html')
@app.route('/system')
def system():
"""系统设置页面 - 重定向到Vue应用"""
return render_template('index_new.html')
# API路由
@app.route('/api/health')
def health():
"""系统健康检查"""
try:
# 获取系统健康状态
health_data = {
'status': 'healthy',
'timestamp': datetime.now().isoformat(),
'health_score': 85.5,
'services': {
'database': 'healthy',
'monitor': 'healthy',
'alerts': 'healthy',
'knowledge': 'healthy'
}
}
return jsonify(health_data)
except Exception as e:
logger.error(f"健康检查失败: {e}")
return jsonify({
'status': 'unhealthy',
'error': str(e),
'timestamp': datetime.now().isoformat()
}), 500
@app.route('/api/status')
def status():
"""系统状态"""
try:
status_data = {
'version': '1.4.0',
'uptime': '2天 14小时 32分钟',
'python_version': '3.9.7',
'database': 'SQLite 3.36.0',
'timestamp': datetime.now().isoformat()
}
return jsonify(status_data)
except Exception as e:
logger.error(f"获取状态失败: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/api/performance')
def performance():
"""性能指标"""
try:
import psutil
performance_data = {
'cpu_usage': psutil.cpu_percent(),
'memory_usage': psutil.virtual_memory().percent,
'disk_usage': psutil.disk_usage('/').percent,
'timestamp': datetime.now().isoformat()
}
return jsonify(performance_data)
except ImportError:
# 如果没有psutil返回模拟数据
performance_data = {
'cpu_usage': 45.2,
'memory_usage': 68.5,
'disk_usage': 23.1,
'timestamp': datetime.now().isoformat()
}
return jsonify(performance_data)
except Exception as e:
logger.error(f"获取性能指标失败: {e}")
return jsonify({'error': str(e)}), 500
# 错误处理
@app.errorhandler(404)
def not_found(error):
"""404错误处理"""
if request.path.startswith('/api/'):
return jsonify({'error': 'API endpoint not found'}), 404
return render_template('index_new.html')
@app.errorhandler(500)
def internal_error(error):
"""500错误处理"""
logger.error(f"内部服务器错误: {error}")
if request.path.startswith('/api/'):
return jsonify({'error': 'Internal server error'}), 500
return render_template('index_new.html')
return app
if __name__ == '__main__':
app = create_app()
# 检查前端构建文件
dist_path = os.path.join(os.path.dirname(__file__), 'static', 'dist')
if not os.path.exists(dist_path):
print("警告: 前端构建文件不存在,请先运行 build_frontend.bat")
print("或者访问 http://localhost:5000/legacy 使用传统页面")
print("TSP智能助手Web服务器启动中...")
print("前端地址: http://localhost:5000")
print("API地址: http://localhost:5000/api")
print("WebSocket: ws://localhost:8765")
app.run(
host='0.0.0.0',
port=5000,
debug=True,
threaded=True
)

View File

@@ -5,26 +5,18 @@
"""
from flask import Blueprint, request, jsonify
from src.main import TSPAssistant
from src.web.service_manager import service_manager
from src.web.error_handlers import handle_api_errors, create_error_response, create_success_response
from src.analytics.alert_system import AlertRule, AlertLevel, AlertType
alerts_bp = Blueprint('alerts', __name__, url_prefix='/api/alerts')
def get_assistant():
"""获取TSP助手实例懒加载"""
global _assistant
if '_assistant' not in globals():
_assistant = TSPAssistant()
return _assistant
@alerts_bp.route('')
@handle_api_errors
def get_alerts():
"""获取预警列表"""
try:
alerts = get_assistant().get_active_alerts()
return jsonify(alerts)
except Exception as e:
return jsonify({"error": str(e)}), 500
alerts = service_manager.get_assistant().get_active_alerts()
return jsonify(alerts)
@alerts_bp.route('', methods=['POST'])
def create_alert():

293
src/web/blueprints/core.py Normal file
View File

@@ -0,0 +1,293 @@
# -*- coding: utf-8 -*-
"""
核心功能蓝图
处理系统核心功能的API路由
"""
from flask import Blueprint, request, jsonify
from typing import Dict, Any
from datetime import datetime, timedelta
from src.web.service_manager import service_manager
from src.web.error_handlers import handle_api_errors, create_error_response, create_success_response
from src.core.database import db_manager
from src.core.models import Conversation, Alert, WorkOrder
from src.core.query_optimizer import query_optimizer
core_bp = Blueprint('core', __name__, url_prefix='/api')
@core_bp.route('/health')
@handle_api_errors
def get_health() -> Dict[str, Any]:
"""获取系统健康状态附加1小时业务指标"""
base = service_manager.get_assistant().get_system_health() or {}
# 追加数据库近1小时指标
with db_manager.get_session() as session:
since = datetime.now() - timedelta(hours=1)
conv_count = session.query(Conversation).filter(Conversation.timestamp >= since).count()
resp_times = [c.response_time for c in session.query(Conversation).filter(Conversation.timestamp >= since).all() if c.response_time]
avg_resp = round(sum(resp_times)/len(resp_times), 2) if resp_times else 0
open_wos = session.query(WorkOrder).filter(WorkOrder.status == 'open').count()
levels = session.query(Alert.level).filter(Alert.is_active == True).all()
level_map = {}
for (lvl,) in levels:
level_map[lvl] = level_map.get(lvl, 0) + 1
base.update({
"throughput_1h": conv_count,
"avg_response_time_1h": avg_resp,
"open_workorders": open_wos,
"active_alerts_by_level": level_map
})
return jsonify(base)
@core_bp.route('/rules')
@handle_api_errors
def get_rules() -> Dict[str, Any]:
"""获取预警规则列表"""
rules = service_manager.get_assistant().alert_system.rules
rules_data = []
for name, rule in rules.items():
rules_data.append({
"name": rule.name,
"description": rule.description,
"alert_type": rule.alert_type.value,
"level": rule.level.value,
"threshold": rule.threshold,
"condition": rule.condition,
"enabled": rule.enabled,
"check_interval": rule.check_interval,
"cooldown": rule.cooldown
})
return jsonify(rules_data)
@core_bp.route('/rules', methods=['POST'])
@handle_api_errors
def create_rule() -> Dict[str, Any]:
"""创建预警规则"""
from src.analytics.alert_system import AlertRule, AlertLevel, AlertType
data = request.get_json()
rule = AlertRule(
name=data['name'],
description=data['description'],
alert_type=AlertType(data['alert_type']),
level=AlertLevel(data['level']),
threshold=float(data['threshold']),
condition=data['condition'],
enabled=data.get('enabled', True),
check_interval=int(data.get('check_interval', 300)),
cooldown=int(data.get('cooldown', 3600))
)
success = service_manager.get_assistant().alert_system.add_custom_rule(rule)
if success:
return jsonify(create_success_response(message="规则创建成功"))
else:
return create_error_response("规则创建失败", 400)
@core_bp.route('/rules/<rule_name>', methods=['PUT'])
@handle_api_errors
def update_rule(rule_name: str) -> Dict[str, Any]:
"""更新预警规则"""
data = request.get_json()
success = service_manager.get_assistant().alert_system.update_rule(rule_name, **data)
if success:
return jsonify(create_success_response(message="规则更新成功"))
else:
return create_error_response("规则更新失败", 400)
@core_bp.route('/rules/<rule_name>', methods=['DELETE'])
@handle_api_errors
def delete_rule(rule_name: str) -> Dict[str, Any]:
"""删除预警规则"""
success = service_manager.get_assistant().alert_system.delete_rule(rule_name)
if success:
return jsonify(create_success_response(message="规则删除成功"))
else:
return create_error_response("规则删除失败", 400)
@core_bp.route('/monitor/start', methods=['POST'])
@handle_api_errors
def start_monitoring() -> Dict[str, Any]:
"""启动监控服务"""
success = service_manager.get_assistant().start_monitoring()
if success:
return jsonify(create_success_response(message="监控服务已启动"))
else:
return create_error_response("启动监控服务失败", 400)
@core_bp.route('/monitor/stop', methods=['POST'])
@handle_api_errors
def stop_monitoring() -> Dict[str, Any]:
"""停止监控服务"""
success = service_manager.get_assistant().stop_monitoring()
if success:
return jsonify(create_success_response(message="监控服务已停止"))
else:
return create_error_response("停止监控服务失败", 400)
@core_bp.route('/monitor/status')
@handle_api_errors
def get_monitor_status() -> Dict[str, Any]:
"""获取监控服务状态"""
health = service_manager.get_assistant().get_system_health()
return jsonify({
"monitor_status": health.get("monitor_status", "unknown"),
"health_score": health.get("health_score", 0),
"active_alerts": health.get("active_alerts", 0)
})
@core_bp.route('/check-alerts', methods=['POST'])
@handle_api_errors
def check_alerts() -> Dict[str, Any]:
"""手动检查预警"""
alerts = service_manager.get_assistant().check_alerts()
return jsonify({
"success": True,
"alerts": alerts,
"count": len(alerts)
})
@core_bp.route('/analytics')
@handle_api_errors
def get_analytics() -> Dict[str, Any]:
"""获取分析数据"""
# 支持多种参数
time_range = request.args.get('timeRange', request.args.get('days', '30'))
dimension = request.args.get('dimension', 'workorders')
# 参数验证
try:
days = int(time_range)
if days <= 0 or days > 365:
days = 30
except (ValueError, TypeError):
days = 30
analytics = query_optimizer.get_analytics_optimized(days)
# 确保返回的数据结构完整
if not analytics:
analytics = {
"workorders": {"total": 0, "open": 0, "resolved": 0, "trend": []},
"alerts": {"total": 0, "critical": 0, "warning": 0, "trend": []},
"conversations": {"total": 0, "avg_confidence": 0, "trend": []},
"performance": {"avg_response_time": 0, "success_rate": 0}
}
return jsonify(analytics)
@core_bp.route('/batch-delete/workorders', methods=['POST'])
@handle_api_errors
def batch_delete_workorders() -> Dict[str, Any]:
"""批量删除工单"""
data = request.get_json()
workorder_ids = data.get('ids', [])
if not workorder_ids:
return create_error_response("请选择要删除的工单", 400)
try:
with db_manager.get_session() as session:
# 验证工单是否存在
existing_workorders = session.query(WorkOrder).filter(WorkOrder.id.in_(workorder_ids)).all()
existing_ids = [wo.id for wo in existing_workorders]
if len(existing_ids) != len(workorder_ids):
missing_ids = set(workorder_ids) - set(existing_ids)
return create_error_response(f"工单不存在: {list(missing_ids)}", 404)
# 先删除相关的工单建议记录
from src.core.models import WorkOrderSuggestion
session.query(WorkOrderSuggestion).filter(WorkOrderSuggestion.work_order_id.in_(workorder_ids)).delete(synchronize_session=False)
# 再删除工单
deleted_count = session.query(WorkOrder).filter(WorkOrder.id.in_(workorder_ids)).delete(synchronize_session=False)
session.commit()
return jsonify(create_success_response(
data={"deleted_count": deleted_count},
message=f"成功删除 {deleted_count} 个工单"
))
except Exception as e:
return create_error_response(f"批量删除工单失败: {str(e)}", 500)
@core_bp.route('/batch-delete/alerts', methods=['POST'])
@handle_api_errors
def batch_delete_alerts() -> Dict[str, Any]:
"""批量删除预警"""
data = request.get_json()
alert_ids = data.get('ids', [])
if not alert_ids:
return create_error_response("请选择要删除的预警", 400)
try:
with db_manager.get_session() as session:
# 验证预警是否存在
existing_alerts = session.query(Alert).filter(Alert.id.in_(alert_ids)).all()
existing_ids = [alert.id for alert in existing_alerts]
if len(existing_ids) != len(alert_ids):
missing_ids = set(alert_ids) - set(existing_ids)
return create_error_response(f"预警不存在: {list(missing_ids)}", 404)
# 删除预警
deleted_count = session.query(Alert).filter(Alert.id.in_(alert_ids)).delete(synchronize_session=False)
session.commit()
return jsonify(create_success_response(
data={"deleted_count": deleted_count},
message=f"成功删除 {deleted_count} 个预警"
))
except Exception as e:
return create_error_response(f"批量删除预警失败: {str(e)}", 500)
@core_bp.route('/batch-delete/knowledge', methods=['POST'])
@handle_api_errors
def batch_delete_knowledge() -> Dict[str, Any]:
"""批量删除知识库条目"""
data = request.get_json()
knowledge_ids = data.get('ids', [])
if not knowledge_ids:
return create_error_response("请选择要删除的知识库条目", 400)
try:
with db_manager.get_session() as session:
# 验证知识库条目是否存在
existing_knowledge = session.query(KnowledgeEntry).filter(KnowledgeEntry.id.in_(knowledge_ids)).all()
existing_ids = [kb.id for kb in existing_knowledge]
if len(existing_ids) != len(knowledge_ids):
missing_ids = set(knowledge_ids) - set(existing_ids)
return create_error_response(f"知识库条目不存在: {list(missing_ids)}", 404)
# 删除知识库条目
deleted_count = session.query(KnowledgeEntry).filter(KnowledgeEntry.id.in_(knowledge_ids)).delete(synchronize_session=False)
session.commit()
return jsonify(create_success_response(
data={"deleted_count": deleted_count},
message=f"成功删除 {deleted_count} 个知识库条目"
))
except Exception as e:
return create_error_response(f"批量删除知识库条目失败: {str(e)}", 500)

View File

@@ -49,10 +49,23 @@ def search_knowledge():
"""搜索知识库"""
try:
query = request.args.get('q', '')
# 这里应该调用知识库管理器的搜索方法
results = get_assistant().search_knowledge(query, top_k=5)
return jsonify(results.get('results', []))
import logging
logger = logging.getLogger(__name__)
logger.info(f"搜索查询: '{query}'")
if not query.strip():
logger.info("查询为空,返回空结果")
return jsonify([])
# 直接调用知识库管理器的搜索方法
assistant = get_assistant()
results = assistant.knowledge_manager.search_knowledge(query, top_k=5)
logger.info(f"搜索结果数量: {len(results)}")
return jsonify(results)
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f"搜索知识库失败: {e}")
return jsonify({"error": str(e)}), 500
@knowledge_bp.route('', methods=['POST'])

74
src/web/error_handlers.py Normal file
View File

@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
"""
错误处理装饰器和工具
提供统一的错误处理模式
"""
import logging
from functools import wraps
from typing import Callable, Any, Dict
from flask import jsonify
logger = logging.getLogger(__name__)
def handle_api_errors(func: Callable) -> Callable:
"""API错误处理装饰器"""
@wraps(func)
def wrapper(*args, **kwargs) -> Dict[str, Any]:
try:
return func(*args, **kwargs)
except ValueError as e:
logger.warning(f"参数错误 {func.__name__}: {e}")
return jsonify({"error": f"参数错误: {str(e)}"}), 400
except PermissionError as e:
logger.warning(f"权限错误 {func.__name__}: {e}")
return jsonify({"error": f"权限不足: {str(e)}"}), 403
except FileNotFoundError as e:
logger.warning(f"文件未找到 {func.__name__}: {e}")
return jsonify({"error": f"文件未找到: {str(e)}"}), 404
except Exception as e:
logger.error(f"未处理错误 {func.__name__}: {e}")
return jsonify({"error": f"服务器内部错误: {str(e)}"}), 500
return wrapper
def handle_database_errors(func: Callable) -> Callable:
"""数据库错误处理装饰器"""
@wraps(func)
def wrapper(*args, **kwargs) -> Dict[str, Any]:
try:
return func(*args, **kwargs)
except Exception as e:
logger.error(f"数据库错误 {func.__name__}: {e}")
return jsonify({"error": "数据库操作失败"}), 500
return wrapper
def handle_service_errors(func: Callable) -> Callable:
"""服务错误处理装饰器"""
@wraps(func)
def wrapper(*args, **kwargs) -> Dict[str, Any]:
try:
return func(*args, **kwargs)
except Exception as e:
logger.error(f"服务错误 {func.__name__}: {e}")
return jsonify({"error": "服务暂时不可用"}), 503
return wrapper
def create_error_response(message: str, status_code: int = 500, details: str = None) -> tuple:
"""创建标准错误响应"""
response = {"error": message}
if details:
response["details"] = details
logger.error(f"错误响应: {message} - {details}")
return jsonify(response), status_code
def create_success_response(data: Any = None, message: str = "操作成功") -> Dict[str, Any]:
"""创建标准成功响应"""
response = {"success": True, "message": message}
if data is not None:
response["data"] = data
return response

View File

@@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
"""
服务管理器
统一管理各种服务的懒加载实例
"""
from typing import Optional, Dict, Any
import logging
logger = logging.getLogger(__name__)
class ServiceManager:
"""服务管理器 - 统一管理各种服务的懒加载实例"""
def __init__(self):
self._services: Dict[str, Any] = {}
def get_service(self, service_name: str, factory_func):
"""获取服务实例(懒加载)"""
if service_name not in self._services:
try:
self._services[service_name] = factory_func()
logger.info(f"服务 {service_name} 已初始化")
except Exception as e:
logger.error(f"初始化服务 {service_name} 失败: {e}")
raise
return self._services[service_name]
def get_assistant(self):
"""获取TSP助手实例"""
def factory():
from src.main import TSPAssistant
return TSPAssistant()
return self.get_service('assistant', factory)
def get_agent_assistant(self):
"""获取Agent助手实例"""
def factory():
from src.agent_assistant import TSPAgentAssistant
return TSPAgentAssistant()
return self.get_service('agent_assistant', factory)
def get_chat_manager(self):
"""获取聊天管理器实例"""
def factory():
from src.dialogue.realtime_chat import RealtimeChatManager
return RealtimeChatManager()
return self.get_service('chat_manager', factory)
def get_vehicle_manager(self):
"""获取车辆数据管理器实例"""
def factory():
from src.vehicle.vehicle_data_manager import VehicleDataManager
return VehicleDataManager()
return self.get_service('vehicle_manager', factory)
def clear_service(self, service_name: str):
"""清除指定服务实例"""
if service_name in self._services:
del self._services[service_name]
logger.info(f"服务 {service_name} 已清除")
def clear_all_services(self):
"""清除所有服务实例"""
self._services.clear()
logger.info("所有服务实例已清除")
# 全局服务管理器实例
service_manager = ServiceManager()

View File

@@ -1373,19 +1373,37 @@ class TSPDashboard {
return;
}
// 添加预警列表的批量操作头部
const headerHtml = `
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center">
<input type="checkbox" id="select-all-alerts" class="form-check-input me-2" onchange="dashboard.toggleSelectAllAlerts()">
<label for="select-all-alerts" class="form-check-label">全选</label>
</div>
<div class="btn-group">
<button class="btn btn-sm btn-danger" id="batch-delete-alerts" onclick="dashboard.batchDeleteAlerts()" disabled>
<i class="fas fa-trash me-1"></i>批量删除
</button>
</div>
</div>
`;
const alertsHtml = alerts.map(alert => `
<div class="alert-item ${alert.level}">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<div class="d-flex align-items-center mb-2">
<span class="badge bg-${this.getAlertColor(alert.level)} me-2">${this.getLevelText(alert.level)}</span>
<span class="fw-bold">${alert.rule_name || '未知规则'}</span>
<span class="ms-auto text-muted small">${this.formatTime(alert.created_at)}</span>
</div>
<div class="alert-message mb-2">${alert.message}</div>
<div class="alert-meta text-muted small">
类型: ${this.getTypeText(alert.alert_type)} |
级别: ${this.getLevelText(alert.level)}
<div class="d-flex align-items-start">
<input type="checkbox" class="form-check-input me-2 alert-checkbox" value="${alert.id}" onchange="dashboard.updateBatchDeleteAlertsButton()">
<div class="flex-grow-1">
<div class="d-flex align-items-center mb-2">
<span class="badge bg-${this.getAlertColor(alert.level)} me-2">${this.getLevelText(alert.level)}</span>
<span class="fw-bold">${alert.rule_name || '未知规则'}</span>
<span class="ms-auto text-muted small">${this.formatTime(alert.created_at)}</span>
</div>
<div class="alert-message mb-2">${alert.message}</div>
<div class="alert-meta text-muted small">
类型: ${this.getTypeText(alert.alert_type)} |
级别: ${this.getLevelText(alert.level)}
</div>
</div>
</div>
<div class="ms-3">
@@ -1397,7 +1415,7 @@ class TSPDashboard {
</div>
`).join('');
container.innerHTML = alertsHtml;
container.innerHTML = headerHtml + alertsHtml;
}
updateAlertStatistics(alerts) {
@@ -1413,6 +1431,72 @@ class TSPDashboard {
document.getElementById('total-alerts-count').textContent = stats.total || 0;
}
// 预警批量删除功能
toggleSelectAllAlerts() {
const selectAllCheckbox = document.getElementById('select-all-alerts');
const alertCheckboxes = document.querySelectorAll('.alert-checkbox');
alertCheckboxes.forEach(checkbox => {
checkbox.checked = selectAllCheckbox.checked;
});
this.updateBatchDeleteAlertsButton();
}
updateBatchDeleteAlertsButton() {
const selectedCheckboxes = document.querySelectorAll('.alert-checkbox:checked');
const batchDeleteBtn = document.getElementById('batch-delete-alerts');
if (batchDeleteBtn) {
batchDeleteBtn.disabled = selectedCheckboxes.length === 0;
batchDeleteBtn.textContent = selectedCheckboxes.length > 0
? `批量删除 (${selectedCheckboxes.length})`
: '批量删除';
}
}
async batchDeleteAlerts() {
const selectedCheckboxes = document.querySelectorAll('.alert-checkbox:checked');
const selectedIds = Array.from(selectedCheckboxes).map(cb => parseInt(cb.value));
if (selectedIds.length === 0) {
this.showNotification('请选择要删除的预警', 'warning');
return;
}
if (!confirm(`确定要删除选中的 ${selectedIds.length} 个预警吗?此操作不可撤销。`)) {
return;
}
try {
const response = await fetch('/api/batch-delete/alerts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ ids: selectedIds })
});
const data = await response.json();
if (data.success) {
this.showNotification(data.message, 'success');
// 清除缓存并强制刷新
this.cache.delete('alerts');
await this.loadAlerts();
// 重置批量删除按钮状态
this.updateBatchDeleteAlertsButton();
} else {
this.showNotification(data.error || '批量删除失败', 'error');
}
} catch (error) {
console.error('批量删除预警失败:', error);
this.showNotification('批量删除预警失败', 'error');
}
}
async resolveAlert(alertId) {
try {
const response = await fetch(`/api/alerts/${alertId}/resolve`, { method: 'POST' });
@@ -1456,19 +1540,37 @@ class TSPDashboard {
return;
}
// 添加知识库列表的批量操作头部
const headerHtml = `
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center">
<input type="checkbox" id="select-all-knowledge" class="form-check-input me-2" onchange="dashboard.toggleSelectAllKnowledge()">
<label for="select-all-knowledge" class="form-check-label">全选</label>
</div>
<div class="btn-group">
<button class="btn btn-sm btn-danger" id="batch-delete-knowledge" onclick="dashboard.batchDeleteKnowledge()" disabled>
<i class="fas fa-trash me-1"></i>批量删除
</button>
</div>
</div>
`;
const knowledgeHtml = knowledge.map(item => `
<div class="knowledge-item">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<h6 class="mb-1">${item.question}</h6>
<p class="text-muted mb-2">${item.answer}</p>
<div class="d-flex gap-3">
<small class="text-muted">分类: ${item.category}</small>
<small class="text-muted">置信度: ${Math.round(item.confidence_score * 100)}%</small>
<small class="text-muted">使用次数: ${item.usage_count || 0}</small>
<span class="badge ${item.is_verified ? 'bg-success' : 'bg-warning'}">
${item.is_verified ? '已验证' : '未验证'}
</span>
<div class="d-flex align-items-start">
<input type="checkbox" class="form-check-input me-2 knowledge-checkbox" value="${item.id}" onchange="dashboard.updateBatchDeleteKnowledgeButton()">
<div class="flex-grow-1">
<h6 class="mb-1">${item.question}</h6>
<p class="text-muted mb-2">${item.answer}</p>
<div class="d-flex gap-3">
<small class="text-muted">分类: ${item.category}</small>
<small class="text-muted">置信度: ${Math.round(item.confidence_score * 100)}%</small>
<small class="text-muted">使用次数: ${item.usage_count || 0}</small>
<span class="badge ${item.is_verified ? 'bg-success' : 'bg-warning'}">
${item.is_verified ? '已验证' : '未验证'}
</span>
</div>
</div>
</div>
<div class="ms-3">
@@ -1490,7 +1592,7 @@ class TSPDashboard {
</div>
`).join('');
container.innerHTML = knowledgeHtml;
container.innerHTML = headerHtml + knowledgeHtml;
}
updateKnowledgePagination(data) {
@@ -1750,24 +1852,115 @@ class TSPDashboard {
}
}
// 工单管理
async loadWorkOrders() {
// 知识库批量删除功能
toggleSelectAllKnowledge() {
const selectAllCheckbox = document.getElementById('select-all-knowledge');
const knowledgeCheckboxes = document.querySelectorAll('.knowledge-checkbox');
knowledgeCheckboxes.forEach(checkbox => {
checkbox.checked = selectAllCheckbox.checked;
});
this.updateBatchDeleteKnowledgeButton();
}
updateBatchDeleteKnowledgeButton() {
const selectedCheckboxes = document.querySelectorAll('.knowledge-checkbox:checked');
const batchDeleteBtn = document.getElementById('batch-delete-knowledge');
if (batchDeleteBtn) {
batchDeleteBtn.disabled = selectedCheckboxes.length === 0;
batchDeleteBtn.textContent = selectedCheckboxes.length > 0
? `批量删除 (${selectedCheckboxes.length})`
: '批量删除';
}
}
async batchDeleteKnowledge() {
const selectedCheckboxes = document.querySelectorAll('.knowledge-checkbox:checked');
const selectedIds = Array.from(selectedCheckboxes).map(cb => parseInt(cb.value));
if (selectedIds.length === 0) {
this.showNotification('请选择要删除的知识库条目', 'warning');
return;
}
if (!confirm(`确定要删除选中的 ${selectedIds.length} 个知识库条目吗?此操作不可撤销。`)) {
return;
}
try {
const statusFilter = document.getElementById('workorder-status-filter').value;
const priorityFilter = document.getElementById('workorder-priority-filter').value;
const response = await fetch('/api/batch-delete/knowledge', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ ids: selectedIds })
});
const data = await response.json();
if (data.success) {
this.showNotification(data.message, 'success');
// 清除缓存并强制刷新
this.cache.delete('knowledge');
await this.loadKnowledge();
// 重置批量删除按钮状态
this.updateBatchDeleteKnowledgeButton();
} else {
this.showNotification(data.error || '批量删除失败', 'error');
}
} catch (error) {
console.error('批量删除知识库条目失败:', error);
this.showNotification('批量删除知识库条目失败', 'error');
}
}
// 工单管理
async loadWorkOrders(forceRefresh = false) {
try {
const statusFilter = document.getElementById('workorder-status-filter')?.value || 'all';
const priorityFilter = document.getElementById('workorder-priority-filter')?.value || 'all';
let url = '/api/workorders';
const params = new URLSearchParams();
if (statusFilter !== 'all') params.append('status', statusFilter);
if (priorityFilter !== 'all') params.append('priority', priorityFilter);
// 添加强制刷新参数
if (forceRefresh) {
params.append('_t', Date.now().toString());
}
if (params.toString()) url += '?' + params.toString();
const response = await fetch(url);
const response = await fetch(url, {
cache: forceRefresh ? 'no-cache' : 'default',
headers: forceRefresh ? {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
} : {}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const workorders = await response.json();
this.updateWorkOrdersDisplay(workorders);
this.updateWorkOrderStatistics(workorders);
// 更新缓存
this.cache.set('workorders', {
data: workorders,
timestamp: Date.now()
});
} catch (error) {
console.error('加载工单失败:', error);
this.showNotification('加载工单失败: ' + error.message, 'error');
}
}
@@ -1779,17 +1972,35 @@ class TSPDashboard {
return;
}
// 添加工单列表的批量操作头部
const headerHtml = `
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center">
<input type="checkbox" id="select-all-workorders" class="form-check-input me-2" onchange="dashboard.toggleSelectAllWorkorders()">
<label for="select-all-workorders" class="form-check-label">全选</label>
</div>
<div class="btn-group">
<button class="btn btn-sm btn-danger" id="batch-delete-workorders" onclick="dashboard.batchDeleteWorkorders()" disabled>
<i class="fas fa-trash me-1"></i>批量删除
</button>
</div>
</div>
`;
const workordersHtml = workorders.map(workorder => `
<div class="work-order-item">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<h6 class="mb-1">${workorder.title}</h6>
<p class="text-muted mb-2">${workorder.description ? workorder.description.substring(0, 100) + (workorder.description.length > 100 ? '...' : '') : '无处理过程'}</p>
<div class="d-flex gap-3">
<span class="badge bg-${this.getPriorityColor(workorder.priority)}">${this.getPriorityText(workorder.priority)}</span>
<span class="badge bg-${this.getStatusColor(workorder.status)}">${this.getStatusText(workorder.status)}</span>
<small class="text-muted">分类: ${workorder.category}</small>
<small class="text-muted">创建时间: ${new Date(workorder.created_at).toLocaleString()}</small>
<div class="d-flex align-items-start">
<input type="checkbox" class="form-check-input me-2 workorder-checkbox" value="${workorder.id}" onchange="dashboard.updateBatchDeleteButton()">
<div class="flex-grow-1">
<h6 class="mb-1">${workorder.title}</h6>
<p class="text-muted mb-2">${workorder.description ? workorder.description.substring(0, 100) + (workorder.description.length > 100 ? '...' : '') : '无处理过程'}</p>
<div class="d-flex gap-3">
<span class="badge bg-${this.getPriorityColor(workorder.priority)}">${this.getPriorityText(workorder.priority)}</span>
<span class="badge bg-${this.getStatusColor(workorder.status)}">${this.getStatusText(workorder.status)}</span>
<small class="text-muted">分类: ${workorder.category}</small>
<small class="text-muted">创建时间: ${new Date(workorder.created_at).toLocaleString()}</small>
</div>
</div>
</div>
<div class="ms-3">
@@ -1809,7 +2020,7 @@ class TSPDashboard {
</div>
`).join('');
container.innerHTML = workordersHtml;
container.innerHTML = headerHtml + workordersHtml;
}
updateWorkOrderStatistics(workorders) {
@@ -1825,6 +2036,73 @@ class TSPDashboard {
document.getElementById('workorders-resolved').textContent = stats.resolved || 0;
}
// 工单批量删除功能
toggleSelectAllWorkorders() {
const selectAllCheckbox = document.getElementById('select-all-workorders');
const workorderCheckboxes = document.querySelectorAll('.workorder-checkbox');
workorderCheckboxes.forEach(checkbox => {
checkbox.checked = selectAllCheckbox.checked;
});
this.updateBatchDeleteButton();
}
updateBatchDeleteButton() {
const selectedCheckboxes = document.querySelectorAll('.workorder-checkbox:checked');
const batchDeleteBtn = document.getElementById('batch-delete-workorders');
if (batchDeleteBtn) {
batchDeleteBtn.disabled = selectedCheckboxes.length === 0;
batchDeleteBtn.textContent = selectedCheckboxes.length > 0
? `批量删除 (${selectedCheckboxes.length})`
: '批量删除';
}
}
async batchDeleteWorkorders() {
const selectedCheckboxes = document.querySelectorAll('.workorder-checkbox:checked');
const selectedIds = Array.from(selectedCheckboxes).map(cb => parseInt(cb.value));
if (selectedIds.length === 0) {
this.showNotification('请选择要删除的工单', 'warning');
return;
}
if (!confirm(`确定要删除选中的 ${selectedIds.length} 个工单吗?此操作不可撤销。`)) {
return;
}
try {
const response = await fetch('/api/batch-delete/workorders', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ ids: selectedIds })
});
const data = await response.json();
if (data.success) {
this.showNotification(data.message, 'success');
// 清除缓存并强制刷新
this.cache.delete('workorders');
await this.loadWorkOrders(true); // 强制刷新
await this.loadAnalytics();
// 重置批量删除按钮状态
this.updateBatchDeleteButton();
} else {
this.showNotification(data.error || '批量删除失败', 'error');
}
} catch (error) {
console.error('批量删除工单失败:', error);
this.showNotification('批量删除工单失败', 'error');
}
}
async createWorkOrder() {
const title = document.getElementById('wo-title').value.trim();
const description = document.getElementById('wo-description').value.trim();
@@ -3407,6 +3685,7 @@ class TSPDashboard {
// 更新统计卡片
updateStatisticsCards(data) {
// 更新工单统计
const total = data.workorders?.total || 0;
const open = data.workorders?.open || 0;
const resolved = data.workorders?.resolved || 0;
@@ -3417,6 +3696,49 @@ class TSPDashboard {
document.getElementById('resolvedWorkorders').textContent = resolved;
document.getElementById('avgSatisfaction').textContent = avgSatisfaction.toFixed(1);
// 更新预警统计
const alertTotal = data.alerts?.total || 0;
const alertActive = data.alerts?.active || 0;
const alertCritical = data.alerts?.by_level?.critical || 0;
const alertWarning = data.alerts?.by_level?.warning || 0;
const alertError = data.alerts?.by_level?.error || 0;
// 更新预警统计显示
if (document.getElementById('critical-alerts')) {
document.getElementById('critical-alerts').textContent = alertCritical;
}
if (document.getElementById('warning-alerts')) {
document.getElementById('warning-alerts').textContent = alertWarning;
}
if (document.getElementById('error-alerts')) {
document.getElementById('error-alerts').textContent = alertError;
}
if (document.getElementById('total-alerts-count')) {
document.getElementById('total-alerts-count').textContent = alertTotal;
}
// 更新性能统计
const performanceScore = data.performance?.score || 0;
const performanceTrend = data.performance?.trend || 'stable';
if (document.getElementById('performance-score')) {
document.getElementById('performance-score').textContent = performanceScore.toFixed(1);
}
if (document.getElementById('performance-trend')) {
document.getElementById('performance-trend').textContent = this.getPerformanceTrendText(performanceTrend);
}
// 更新满意度统计
const satisfactionAvg = data.satisfaction?.average || 0;
const satisfactionCount = data.satisfaction?.count || 0;
if (document.getElementById('satisfaction-avg')) {
document.getElementById('satisfaction-avg').textContent = satisfactionAvg.toFixed(1);
}
if (document.getElementById('satisfaction-count')) {
document.getElementById('satisfaction-count').textContent = satisfactionCount;
}
// 更新进度条
if (total > 0) {
document.getElementById('openProgress').style.width = `${(open / total) * 100}%`;
@@ -3469,9 +3791,62 @@ class TSPDashboard {
// 更新分布图表
updateDistributionChart(data) {
const categories = data.workorders?.by_category || {};
const labels = Object.keys(categories);
const values = Object.values(categories);
const currentDimension = document.getElementById('dataDimension')?.value || 'workorders';
let labels, values, title, backgroundColor;
if (currentDimension === 'alerts') {
// 预警级别分布
const alertLevels = data.alerts?.by_level || {};
labels = Object.keys(alertLevels);
values = Object.values(alertLevels);
title = '预警级别分布';
backgroundColor = [
'#FF6384', // critical - 红色
'#FFCE56', // warning - 黄色
'#36A2EB', // error - 蓝色
'#4BC0C0', // info - 青色
'#9966FF' // 其他
];
} else if (currentDimension === 'performance') {
// 性能指标分布
const performanceMetrics = data.performance?.by_level || {};
labels = Object.keys(performanceMetrics);
values = Object.values(performanceMetrics);
title = '性能指标分布';
backgroundColor = [
'#28a745', // 优秀 - 绿色
'#ffc107', // 良好 - 黄色
'#fd7e14', // 一般 - 橙色
'#dc3545' // 差 - 红色
];
} else if (currentDimension === 'satisfaction') {
// 满意度分布
const satisfactionLevels = data.satisfaction?.by_level || {};
labels = Object.keys(satisfactionLevels);
values = Object.values(satisfactionLevels);
title = '满意度分布';
backgroundColor = [
'#28a745', // 非常满意 - 绿色
'#ffc107', // 满意 - 黄色
'#fd7e14', // 一般 - 橙色
'#dc3545' // 不满意 - 红色
];
} else {
// 工单分类分布
const categories = data.workorders?.by_category || {};
labels = Object.keys(categories);
values = Object.values(categories);
title = '工单分类分布';
backgroundColor = [
'#FF6384',
'#36A2EB',
'#FFCE56',
'#4BC0C0',
'#9966FF',
'#FF9F40'
];
}
const chartConfig = {
type: 'doughnut',
@@ -3479,14 +3854,7 @@ class TSPDashboard {
labels: labels,
datasets: [{
data: values,
backgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56',
'#4BC0C0',
'#9966FF',
'#FF9F40'
]
backgroundColor: backgroundColor
}]
},
options: {
@@ -3495,7 +3863,7 @@ class TSPDashboard {
plugins: {
title: {
display: true,
text: '工单分类分布'
text: title
},
legend: {
display: true,
@@ -3576,23 +3944,72 @@ class TSPDashboard {
this.charts.priorityChart.destroy();
}
const priorities = data.workorders?.by_priority || {};
const labels = Object.keys(priorities).map(p => this.getPriorityText(p));
const values = Object.values(priorities);
const currentDimension = document.getElementById('dataDimension')?.value || 'workorders';
let labels, values, title, backgroundColor, label;
if (currentDimension === 'alerts') {
// 预警严重程度分布
const alertSeverities = data.alerts?.by_severity || {};
labels = Object.keys(alertSeverities).map(s => this.getSeverityText(s));
values = Object.values(alertSeverities);
title = '预警严重程度分布';
label = '预警数量';
backgroundColor = [
'#28a745', // low - 绿色
'#ffc107', // medium - 黄色
'#fd7e14', // high - 橙色
'#dc3545' // critical - 红色
];
} else if (currentDimension === 'performance') {
// 性能指标分布
const performanceMetrics = data.performance?.by_metric || {};
labels = Object.keys(performanceMetrics);
values = Object.values(performanceMetrics);
title = '性能指标分布';
label = '性能值';
backgroundColor = [
'#28a745', // 优秀 - 绿色
'#ffc107', // 良好 - 黄色
'#fd7e14', // 一般 - 橙色
'#dc3545' // 差 - 红色
];
} else if (currentDimension === 'satisfaction') {
// 满意度分布
const satisfactionLevels = data.satisfaction?.by_level || {};
labels = Object.keys(satisfactionLevels).map(s => this.getSatisfactionText(s));
values = Object.values(satisfactionLevels);
title = '满意度分布';
label = '满意度数量';
backgroundColor = [
'#28a745', // 非常满意 - 绿色
'#ffc107', // 满意 - 黄色
'#fd7e14', // 一般 - 橙色
'#dc3545' // 不满意 - 红色
];
} else {
// 工单优先级分布
const priorities = data.workorders?.by_priority || {};
labels = Object.keys(priorities).map(p => this.getPriorityText(p));
values = Object.values(priorities);
title = '工单优先级分布';
label = '工单数量';
backgroundColor = [
'#28a745', // 低 - 绿色
'#ffc107', // 中 - 黄色
'#fd7e14', // 高 - 橙色
'#dc3545' // 紧急 - 红色
];
}
this.charts.priorityChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: '工单数量',
label: label,
data: values,
backgroundColor: [
'#28a745', // 低 - 绿色
'#ffc107', // 中 - 黄色
'#fd7e14', // 高 - 橙色
'#dc3545' // 紧急 - 红色
]
backgroundColor: backgroundColor
}]
},
options: {
@@ -3601,7 +4018,7 @@ class TSPDashboard {
plugins: {
title: {
display: true,
text: '优先级分布'
text: title
}
},
scales: {
@@ -3618,33 +4035,124 @@ class TSPDashboard {
const trendData = data.trend || [];
const labels = trendData.map(item => item.date);
const workorders = trendData.map(item => item.workorders);
const alerts = trendData.map(item => item.alerts);
const performance = trendData.map(item => item.performance || 0);
const satisfaction = trendData.map(item => item.satisfaction || 0);
if (chartType === 'pie' || chartType === 'doughnut') {
const categories = data.workorders?.by_category || {};
return {
labels: Object.keys(categories),
datasets: [{
data: Object.values(categories),
backgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56',
'#4BC0C0',
'#9966FF',
'#FF9F40'
]
}]
};
// 根据数据维度选择显示内容
const currentDimension = document.getElementById('dataDimension')?.value || 'workorders';
if (currentDimension === 'alerts') {
const alertLevels = data.alerts?.by_level || {};
return {
labels: Object.keys(alertLevels),
datasets: [{
data: Object.values(alertLevels),
backgroundColor: [
'#FF6384', // critical - 红色
'#FFCE56', // warning - 黄色
'#36A2EB', // error - 蓝色
'#4BC0C0', // info - 青色
'#9966FF' // 其他
]
}]
};
} else if (currentDimension === 'performance') {
// 性能指标分布
const performanceMetrics = data.performance || {};
return {
labels: Object.keys(performanceMetrics),
datasets: [{
data: Object.values(performanceMetrics),
backgroundColor: [
'#28a745', // 优秀 - 绿色
'#ffc107', // 良好 - 黄色
'#fd7e14', // 一般 - 橙色
'#dc3545' // 差 - 红色
]
}]
};
} else if (currentDimension === 'satisfaction') {
// 满意度分布
const satisfactionLevels = data.satisfaction?.by_level || {};
return {
labels: Object.keys(satisfactionLevels),
datasets: [{
data: Object.values(satisfactionLevels),
backgroundColor: [
'#28a745', // 非常满意 - 绿色
'#ffc107', // 满意 - 黄色
'#fd7e14', // 一般 - 橙色
'#dc3545' // 不满意 - 红色
]
}]
};
} else {
const categories = data.workorders?.by_category || {};
return {
labels: Object.keys(categories),
datasets: [{
data: Object.values(categories),
backgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56',
'#4BC0C0',
'#9966FF',
'#FF9F40'
]
}]
};
}
} else {
return {
labels: labels,
datasets: [{
// 线图和柱状图根据数据维度显示不同内容
const currentDimension = document.getElementById('dataDimension')?.value || 'workorders';
const datasets = [];
if (currentDimension === 'performance') {
// 性能指标图表
datasets.push({
label: '性能指标',
data: performance,
borderColor: '#28a745',
backgroundColor: 'rgba(40, 167, 69, 0.1)',
tension: chartType === 'line' ? 0.4 : 0
});
} else if (currentDimension === 'satisfaction') {
// 满意度图表
datasets.push({
label: '满意度评分',
data: satisfaction,
borderColor: '#ffc107',
backgroundColor: 'rgba(255, 193, 7, 0.1)',
tension: chartType === 'line' ? 0.4 : 0
});
} else {
// 默认显示工单和预警数据
datasets.push({
label: '工单数量',
data: workorders,
borderColor: '#36A2EB',
backgroundColor: 'rgba(54, 162, 235, 0.1)',
tension: chartType === 'line' ? 0.4 : 0
}]
});
// 如果有预警数据,添加预警数据集
if (alerts.some(alert => alert > 0)) {
datasets.push({
label: '预警数量',
data: alerts,
borderColor: '#FF6384',
backgroundColor: 'rgba(255, 99, 132, 0.1)',
tension: chartType === 'line' ? 0.4 : 0
});
}
}
return {
labels: labels,
datasets: datasets
};
}
}
@@ -4281,6 +4789,35 @@ class TSPDashboard {
return priorityMap[priority] || priority;
}
getSeverityText(severity) {
const severityMap = {
'low': '低',
'medium': '中',
'high': '高',
'critical': '严重'
};
return severityMap[severity] || severity;
}
getSatisfactionText(level) {
const satisfactionMap = {
'very_satisfied': '非常满意',
'satisfied': '满意',
'neutral': '一般',
'dissatisfied': '不满意'
};
return satisfactionMap[level] || level;
}
getPerformanceTrendText(trend) {
const trendMap = {
'up': '上升',
'down': '下降',
'stable': '稳定'
};
return trendMap[trend] || trend;
}
getPriorityColor(priority) {
const colorMap = {
'low': 'secondary',

View File

@@ -1,133 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TSP智能助手</title>
<link rel="icon" type="image/svg+xml" href="/static/dist/vite.svg">
<!-- 预加载关键资源 -->
<link rel="preload" href="/static/dist/assets/index.css" as="style">
<link rel="preload" href="/static/dist/assets/index.js" as="script">
<!-- 样式 -->
<link rel="stylesheet" href="/static/dist/assets/index.css">
<!-- 如果构建文件不存在,显示提示信息 -->
<style>
.build-missing {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #f5f5f5;
z-index: 9999;
padding: 20px;
text-align: center;
}
.build-missing.show {
display: block;
}
.build-missing h1 {
color: #333;
margin-bottom: 20px;
}
.build-missing p {
color: #666;
margin-bottom: 20px;
line-height: 1.6;
}
.build-missing .actions {
display: flex;
gap: 12px;
justify-content: center;
flex-wrap: wrap;
}
.build-missing .btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
text-decoration: none;
font-size: 14px;
transition: all 0.3s;
}
.build-missing .btn-primary {
background: #409eff;
color: white;
}
.build-missing .btn-primary:hover {
background: #337ecc;
}
.build-missing .btn-secondary {
background: #909399;
color: white;
}
.build-missing .btn-secondary:hover {
background: #73767a;
}
</style>
</head>
<body>
<div id="app"></div>
<!-- 构建文件缺失提示 -->
<div id="build-missing" class="build-missing">
<h1>🚀 TSP智能助手</h1>
<p>
前端构建文件不存在,请先构建前端应用。<br>
或者使用传统HTML页面。
</p>
<div class="actions">
<button class="btn btn-primary" onclick="buildFrontend()">
📦 构建前端
</button>
<a href="/legacy" class="btn btn-secondary">
📄 使用传统页面
</a>
</div>
</div>
<!-- 脚本 -->
<script>
// 检查构建文件是否存在
function checkBuildFiles() {
const cssLink = document.querySelector('link[href="/static/dist/assets/index.css"]');
const jsScript = document.createElement('script');
jsScript.src = '/static/dist/assets/index.js';
jsScript.onerror = function() {
document.getElementById('build-missing').classList.add('show');
};
jsScript.onload = function() {
document.getElementById('build-missing').classList.remove('show');
};
document.head.appendChild(jsScript);
}
// 构建前端
function buildFrontend() {
alert('请运行 build_frontend.bat 脚本构建前端应用');
}
// 页面加载完成后检查
document.addEventListener('DOMContentLoaded', checkBuildFiles);
</script>
<!-- 尝试加载Vue应用 -->
<script type="module" src="/static/dist/assets/index.js"></script>
</body>
</html>

View File

@@ -19,7 +19,7 @@ def setup_logging():
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('logs/dashboard.log'),
logging.FileHandler('logs/dashboard.log', encoding='utf-8'),
logging.StreamHandler()
]
)

View File

@@ -1,45 +0,0 @@
@echo off
echo 启动TSP智能助手前端开发服务器...
echo.
cd frontend
echo 检查Node.js环境...
node --version >nul 2>&1
if %errorlevel% neq 0 (
echo 错误: 未找到Node.js请先安装Node.js
echo 下载地址: https://nodejs.org/
pause
exit /b 1
)
echo 检查npm环境...
npm --version >nul 2>&1
if %errorlevel% neq 0 (
echo 错误: 未找到npm请检查Node.js安装
pause
exit /b 1
)
echo 检查依赖包...
if not exist "node_modules" (
echo 安装依赖包...
npm install
if %errorlevel% neq 0 (
echo 错误: 依赖包安装失败
pause
exit /b 1
)
)
echo 启动开发服务器...
echo 前端地址: http://localhost:3000
echo 后端API: http://localhost:5000
echo WebSocket: ws://localhost:8765
echo.
echo 按 Ctrl+C 停止服务器
echo.
npm run dev
pause

View File

@@ -1,58 +0,0 @@
#!/bin/bash
echo "启动TSP智能助手前端开发服务器..."
echo
# 检查Node.js环境
echo "检查Node.js环境..."
if ! command -v node &> /dev/null; then
echo "错误: 未找到Node.js请先安装Node.js"
echo "安装命令:"
echo " Ubuntu/Debian: sudo apt update && sudo apt install nodejs npm"
echo " CentOS/RHEL: sudo yum install nodejs npm"
echo " 或访问: https://nodejs.org/"
exit 1
fi
# 检查npm环境
echo "检查npm环境..."
if ! command -v npm &> /dev/null; then
echo "错误: 未找到npm请检查Node.js安装"
exit 1
fi
echo "Node.js版本: $(node --version)"
echo "npm版本: $(npm --version)"
echo
# 检查Node.js版本兼容性
NODE_VERSION=$(node --version | cut -d'v' -f2 | cut -d'.' -f1)
if [ "$NODE_VERSION" -ge 22 ]; then
echo "检测到Node.js v22+,使用兼容性模式..."
echo "注意: 将跳过TypeScript类型检查以避免兼容性问题"
fi
# 进入前端目录
cd frontend
# 检查依赖包
echo "检查依赖包..."
if [ ! -d "node_modules" ]; then
echo "安装依赖包..."
npm install
if [ $? -ne 0 ]; then
echo "错误: 依赖包安装失败"
exit 1
fi
fi
echo
echo "启动开发服务器..."
echo "前端地址: http://localhost:3000"
echo "后端API: http://localhost:5000"
echo "WebSocket: ws://localhost:8765"
echo
echo "按 Ctrl+C 停止服务器"
echo
npm run dev

View File

@@ -1,51 +0,0 @@
@echo off
echo 启动TSP智能助手前端开发服务器便携版...
echo.
set NODEJS_PATH=frontend\nodejs
set NODE_EXE=%NODEJS_PATH%\node.exe
set NPM_EXE=%NODEJS_PATH%\npm.cmd
echo 检查便携版Node.js...
if not exist "%NODE_EXE%" (
echo 错误: 未找到便携版Node.js
echo 请先运行 download_nodejs.bat 下载Node.js便携版
echo 或手动安装Node.js到系统
pause
exit /b 1
)
echo 使用便携版Node.js: %NODE_EXE%
echo Node.js版本:
"%NODE_EXE%" --version
echo.
echo 检查npm版本:
"%NPM_EXE%" --version
cd frontend
echo.
echo 检查依赖包...
if not exist "node_modules" (
echo 安装依赖包...
"%NPM_EXE%" install
if %errorlevel% neq 0 (
echo 错误: 依赖包安装失败
pause
exit /b 1
)
)
echo.
echo 启动开发服务器...
echo 前端地址: http://localhost:3000
echo 后端API: http://localhost:5000
echo WebSocket: ws://localhost:8765
echo.
echo 按 Ctrl+C 停止服务器
echo.
"%NPM_EXE%" run dev
pause

View File

@@ -1,22 +0,0 @@
@echo off
echo 启动TSP智能助手传统版本...
echo.
echo 检查Python环境...
python --version >nul 2>&1
if %errorlevel% neq 0 (
echo 错误: 未找到Python请先安装Python
echo 下载地址: https://www.python.org/downloads/
pause
exit /b 1
)
echo 启动Web服务器...
echo 访问地址: http://localhost:5000
echo.
echo 按 Ctrl+C 停止服务器
echo.
python src/web/app.py
pause

View File

@@ -1,32 +0,0 @@
@echo off
echo 启动TSP智能助手传统版本...
echo.
echo 检查Python环境...
python --version >nul 2>&1
if %errorlevel% neq 0 (
echo 错误: 未找到Python请先安装Python
echo 下载地址: https://www.python.org/downloads/
pause
exit /b 1
)
echo Python环境正常
echo.
echo 启动Web服务器...
echo 访问地址: http://localhost:5000
echo.
echo 功能包括:
echo - 仪表板: http://localhost:5000
echo - 聊天页面: http://localhost:5000/chat
echo - 预警管理: http://localhost:5000/alerts
echo - 知识库: http://localhost:5000/knowledge
echo - 字段映射: http://localhost:5000/field-mapping
echo - 系统设置: http://localhost:5000/system
echo.
echo 按 Ctrl+C 停止服务器
echo.
python src/web/app.py
pause

View File

@@ -1,50 +0,0 @@
#!/bin/bash
echo "启动TSP智能助手传统版本..."
echo
# 检查Python环境
echo "检查Python环境..."
if ! command -v python3 &> /dev/null; then
if ! command -v python &> /dev/null; then
echo "错误: 未找到Python请先安装Python"
echo "安装命令:"
echo " Ubuntu/Debian: sudo apt update && sudo apt install python3 python3-pip"
echo " CentOS/RHEL: sudo yum install python3 python3-pip"
echo " 或访问: https://www.python.org/downloads/"
exit 1
else
PYTHON_CMD="python"
fi
else
PYTHON_CMD="python3"
fi
echo "Python环境正常: $($PYTHON_CMD --version)"
echo
# 检查依赖
echo "检查Python依赖..."
if [ ! -f "requirements.txt" ]; then
echo "警告: 未找到requirements.txt文件"
else
echo "安装Python依赖..."
$PYTHON_CMD -m pip install -r requirements.txt
fi
echo
echo "启动Web服务器..."
echo "访问地址: http://localhost:5000"
echo
echo "功能包括:"
echo "- 仪表板: http://localhost:5000"
echo "- 聊天页面: http://localhost:5000/chat"
echo "- 预警管理: http://localhost:5000/alerts"
echo "- 知识库: http://localhost:5000/knowledge"
echo "- 字段映射: http://localhost:5000/field-mapping"
echo "- 系统设置: http://localhost:5000/system"
echo
echo "按 Ctrl+C 停止服务器"
echo
$PYTHON_CMD src/web/app.py

View File

@@ -1,265 +0,0 @@
# TSP智能助手前端架构优化总结
## 🎯 优化目标
解决原有前端架构的问题:
1. **单文件过大** - HTML文件几千行难以维护
2. **功能重复** - 聊天功能在多个页面重复实现
3. **缺乏国际化** - 硬编码中文文本,无法多语言支持
4. **架构混乱** - HTML、CSS、JS混合缺乏模块化
## 🚀 新架构设计
### 技术栈选择
- **Vue 3** + **TypeScript** - 现代化组件化开发
- **Vite** - 快速构建工具,支持热重载
- **Element Plus** - 企业级UI组件库
- **Vue Router** - 单页应用路由管理
- **Pinia** - 轻量级状态管理
- **Vue I18n** - 完整的国际化解决方案
- **Socket.IO** - 实时通信支持
### 项目结构
```
frontend/
├── src/
│ ├── components/ # 公共组件
│ │ └── ChatWidget.vue # 统一聊天组件
│ ├── views/ # 页面组件
│ │ ├── Dashboard.vue # 仪表板
│ │ ├── Chat.vue # 聊天页面
│ │ ├── Alerts.vue # 预警管理
│ │ ├── AlertRules.vue # 预警规则
│ │ ├── Knowledge.vue # 知识库
│ │ ├── FieldMapping.vue # 字段映射
│ │ └── System.vue # 系统设置
│ ├── stores/ # 状态管理
│ │ ├── useAppStore.ts # 应用状态
│ │ ├── useChatStore.ts # 聊天状态
│ │ └── useAlertStore.ts # 预警状态
│ ├── router/ # 路由配置
│ ├── i18n/ # 国际化
│ │ └── locales/
│ │ ├── zh.json # 中文
│ │ └── en.json # 英文
│ ├── layout/ # 布局组件
│ ├── App.vue # 根组件
│ └── main.ts # 入口文件
├── package.json
├── vite.config.ts
├── tsconfig.json
└── README.md
```
## ✨ 核心功能实现
### 1. 统一聊天系统
**问题解决**
- 原来首页和chat页面有重复的聊天功能
- 实现方式不同,维护困难
**解决方案**
- 创建 `ChatWidget.vue` 统一聊天组件
- 支持悬浮窗和全屏两种模式
- 统一的状态管理 `useChatStore`
- 统一的WebSocket连接管理
**特性**
- 实时消息收发
- 打字指示器
- 消息元数据显示(知识库、置信度、工单关联)
- 快速操作按钮
- 工单创建功能
### 2. 预警管理系统
**问题解决**
- 预警和预警管理页面功能分散
- 缺乏统一的预警状态管理
**解决方案**
- `Alerts.vue` - 预警列表和统计
- `AlertRules.vue` - 预警规则管理
- `useAlertStore` - 统一预警状态管理
- 预设模板系统
**特性**
- 实时预警统计
- 多维度过滤和排序
- 规则CRUD操作
- 预设模板快速创建
- 预警解决功能
### 3. 国际化系统
**问题解决**
- 硬编码中文文本
- 无法支持多语言
**解决方案**
- Vue I18n 完整集成
- 中英文双语支持
- Element Plus 组件国际化
- 动态语言切换
**覆盖范围**
- 所有页面文本
- 组件标签和提示
- 错误消息
- 表单验证信息
### 4. 现代化UI设计
**设计原则**
- 响应式设计,支持移动端
- 暗色/亮色主题切换
- 统一的视觉风格
- 优雅的动画效果
**组件化**
- 可复用的UI组件
- 统一的样式规范
- 主题变量支持
- 无障碍访问支持
## 🔧 开发体验优化
### TypeScript支持
- 完整的类型定义
- 组件Props和Emits类型安全
- API接口类型定义
- 自动类型检查
### 开发工具
- Vite热重载
- 组件自动导入
- ESLint代码规范
- 自动类型生成
### 状态管理
- Pinia轻量级状态管理
- 模块化状态设计
- 类型安全的状态操作
- 持久化存储支持
## 📦 构建和部署
### 开发环境
```bash
cd frontend
npm install
npm run dev
```
### 生产构建
```bash
npm run build
```
### 集成方式
- 构建文件输出到 `src/web/static/dist`
- Flask应用自动提供静态文件服务
- 支持SPA路由回退
## 🎨 用户体验提升
### 1. 统一导航
- 侧边栏导航
- 面包屑导航
- 响应式菜单
- 折叠/展开功能
### 2. 实时更新
- WebSocket实时通信
- 自动数据刷新
- 状态同步
- 离线检测
### 3. 交互优化
- 加载状态指示
- 错误处理
- 操作确认
- 成功反馈
### 4. 性能优化
- 组件懒加载
- 代码分割
- 资源预加载
- 缓存策略
## 🔄 迁移策略
### 渐进式迁移
1. **第一阶段**:新架构开发完成
2. **第二阶段**:并行运行,逐步切换
3. **第三阶段**:完全切换到新架构
### 兼容性保证
- 保留原有API接口
- 支持传统页面访问
- 平滑过渡方案
## 📊 优化效果
### 代码质量
- **文件大小**:从单个文件几千行 → 模块化组件
- **可维护性**:大幅提升,组件化开发
- **可复用性**:组件可在多个页面复用
- **类型安全**TypeScript提供完整类型检查
### 开发效率
- **热重载**:开发时实时预览
- **自动导入**:减少重复代码
- **组件库**Element Plus提供丰富组件
- **状态管理**:统一的数据流管理
### 用户体验
- **响应速度**SPA应用页面切换更快
- **交互体验**现代化UI设计
- **多语言**:支持中英文切换
- **主题切换**:支持暗色/亮色主题
## 🚀 后续规划
### 功能扩展
- 更多页面组件
- 数据可视化图表
- 文件上传下载
- 用户权限管理
### 技术优化
- PWA支持
- 服务端渲染(SSR)
- 微前端架构
- 性能监控
### 部署优化
- Docker容器化
- CI/CD流水线
- 自动化测试
- 监控告警
## 📝 使用指南
### 快速开始
1. 运行 `start_frontend.bat` 启动开发服务器
2. 访问 http://localhost:3000
3. 运行 `build_frontend.bat` 构建生产版本
### 开发规范
- 使用TypeScript编写组件
- 遵循Vue 3 Composition API
- 使用Pinia进行状态管理
- 添加国际化文本到i18n文件
### 部署说明
- 构建文件自动输出到后端静态目录
- Flask应用自动提供前端服务
- 支持API代理和WebSocket转发
---
**总结**通过现代化的前端架构重构我们成功解决了原有架构的所有问题提供了更好的开发体验和用户体验。新架构具有更好的可维护性、可扩展性和性能表现为TSP智能助手的长期发展奠定了坚实基础。

View File

@@ -1,136 +0,0 @@
# 前端页面优化总结
## 🎯 优化目标
根据用户需求,对前端页面进行以下优化:
1. **删除无效的系统设置**移除服务端口配置、API与模型配置等无效功能
2. **添加英文支持**:实现中英文语言切换功能
## ✅ 完成的优化
### 1. **删除无效的系统设置**
#### 🔧 移除的功能
- **API与模型配置**删除了前端页面中的API提供商、API基础URL、API密钥、模型名称、温度参数、最大令牌数等配置项
- **服务端口配置**删除了Web服务端口、WebSocket端口等端口配置项
- **重启服务按钮**:移除了无效的服务重启功能
#### 🔄 替换为有效功能
- **系统信息显示**:将端口配置替换为只读的系统信息显示
- **日志配置**:保留了有效的日志级别配置功能
- **基础设置**保留了API超时时间、最大历史记录数等有效配置
#### 📊 优化效果
- ✅ 移除了用户无法修改的无效配置项
- ✅ 避免了用户困惑和无效操作
- ✅ 简化了设置页面,提高用户体验
### 2. **添加英文支持**
#### 🌐 语言切换功能
- **导航栏语言切换**:在页面顶部添加了中英文切换按钮
- **本地存储**使用localStorage保存用户的语言偏好
- **实时切换**:点击按钮即可实时切换页面语言
#### 📝 翻译内容
**中文翻译键**
- 导航栏TSP智能助手、检查中...、系统正常等
- 侧边栏仪表板、工单管理、智能对话、Agent管理、预警管理、知识库、数据分析、飞书同步、系统设置
- 设置页面系统设置、基础设置、系统信息、日志配置、当前服务端口、WebSocket端口、日志级别、保存设置等
**英文翻译键**
- Navigation: TSP Intelligent Assistant, Checking..., System Normal, etc.
- Sidebar: Dashboard, Work Orders, Smart Chat, Agent Management, Alert Management, Knowledge Base, Analytics, Feishu Sync, System Settings
- Settings: System Settings, Basic Settings, System Information, Log Configuration, Current Service Port, WebSocket Port, Log Level, Save Settings, etc.
#### 🔧 技术实现
- **JavaScript翻译系统**:使用对象存储翻译内容
- **HTML国际化属性**:使用`data-i18n`属性标记需要翻译的元素
- **动态更新**通过JavaScript动态更新页面文本内容
- **状态管理**:维护当前语言状态和按钮激活状态
## 📊 优化前后对比
### 设置页面结构变化
**优化前**
```
系统设置
├── 基础系统配置
├── API与模型配置 (无效)
├── 服务端口配置 (无效)
└── 系统信息与状态
```
**优化后**
```
系统设置
├── 基础系统配置
├── 系统信息显示 (只读)
├── 日志配置
└── 系统信息与状态
```
### 用户体验改进
**优化前**
- ❌ 用户看到无法修改的配置项
- ❌ 点击无效按钮无响应
- ❌ 只有中文界面
**优化后**
- ✅ 只显示有效的配置项
- ✅ 所有功能都有明确的作用
- ✅ 支持中英文切换
- ✅ 更好的用户引导和说明
## 🚀 技术特性
### 1. **智能配置管理**
- 自动检测无效配置项
- 只显示用户可操作的设置
- 提供清晰的配置说明
### 2. **多语言支持**
- 完整的中英文翻译
- 本地存储语言偏好
- 实时语言切换
- 响应式设计适配
### 3. **用户体验优化**
- 简化设置页面结构
- 提供清晰的功能说明
- 避免用户困惑和无效操作
## 📋 使用说明
### 语言切换
1. 点击页面右上角的"中文"或"English"按钮
2. 页面会立即切换到对应语言
3. 语言偏好会自动保存,下次访问时保持选择
### 系统设置
1. **基础设置**可以修改API超时时间、最大历史记录数等
2. **系统信息**显示当前服务端口和WebSocket端口只读
3. **日志配置**:可以调整系统日志级别
## 🔄 后续优化建议
### 1. **扩展翻译内容**
- 添加更多页面的翻译支持
- 完善错误消息和提示的翻译
- 支持更多语言(如日语、韩语等)
### 2. **配置管理优化**
- 添加配置验证功能
- 提供配置导入/导出功能
- 添加配置历史记录
### 3. **用户体验改进**
- 添加设置向导功能
- 提供配置建议和最佳实践
- 添加配置变更通知
---
**总结**:成功删除了前端页面中的无效系统设置,并添加了完整的中英文语言切换功能。优化后的页面更加简洁、实用,用户体验得到显著提升。🎉

View File

@@ -1,255 +0,0 @@
# TSP智能助手 - 推送脚本使用说明
## 📁 脚本文件说明
### 1. `auto_push.bat` - 智能自动推送脚本
**功能**: 完整的Git推送流程包含状态检查、确认、提交和推送
**特点**:
- 显示详细的Git状态
- **智能分析markdown文件修改**
- **自动生成语义化提交信息**
- 用户确认机制(支持编辑提交信息)
- 错误处理和状态反馈
**智能提交信息生成**:
- `fix:` - 修复问题(检测到"修复"、"解决"、"问题"、"错误"
- `feat:` - 新增功能(检测到"功能"、"新增"、"添加"、"实现"
- `perf:` - 性能优化(检测到"优化"、"性能"、"改进"、"提升"
- `docs:` - 文档更新(默认类型)
**使用方法**:
```bash
# 直接运行(智能生成提交信息)
auto_push.bat
# 交互选项:
# y - 使用生成的提交信息
# n - 手动输入提交信息
# e - 编辑生成的提交信息
```
### 2. `auto_push.ps1` - PowerShell高级版本
**功能**: 功能最全面的推送脚本,支持参数和高级功能
**特点**:
- 彩色输出和美观的界面
- 支持命令行参数
- 智能提交信息生成
- 详细的统计信息
- 错误处理和回滚
**使用方法**:
```powershell
# 基本使用
.\auto_push.ps1
# 指定提交信息
.\auto_push.ps1 "feat: 添加新功能"
# 强制推送(跳过确认)
.\auto_push.ps1 -NoConfirm
# 强制推送并指定信息
.\auto_push.ps1 "紧急修复" -Force -NoConfirm
```
### 3. `quick_push.bat` - 智能快速推送脚本
**功能**: 最简单的推送方式,适合日常快速提交
**特点**:
- 一键推送
- **智能分析markdown文件并生成提交信息**
- **自动识别提交类型**
- 最小化交互
- 支持自定义提交信息
**使用方法**:
```bash
# 智能生成提交信息(推荐)
quick_push.bat
# 指定自定义提交信息
quick_push.bat "修复bug"
```
## 🧠 智能功能详解
### 自动内容分析
脚本会自动检测修改的markdown文件并提取以下信息
- **文档标题**:提取 `# 标题` 格式的内容
- **问题描述**:识别包含"问题"、"错误"等关键词的内容
- **解决方案**:识别包含"解决"、"修复"等关键词的内容
- **功能描述**:识别包含"功能"、"新增"等关键词的内容
### 提交类型识别
根据markdown文件内容自动识别提交类型
| 关键词 | 提交类型 | 示例 |
|--------|----------|------|
| 修复、解决、问题、错误 | `fix:` | `fix: 飞书权限问题修复` |
| 功能、新增、添加、实现 | `feat:` | `feat: 新增AI建议功能` |
| 优化、性能、改进、提升 | `perf:` | `perf: 优化图表渲染性能` |
| 其他 | `docs:` | `docs: 更新文档记录` |
### 示例工作流
```markdown
# 飞书权限问题修复
## 问题描述
AI建议无法写入飞书表格出现403权限错误。
## 解决方案
1. 检查飞书应用权限配置
2. 添加必要的读写权限
```
**生成的提交信息**: `fix: 飞书权限问题修复`
## 🚀 推荐使用场景
### 日常开发
```bash
# 快速提交日常更改
quick_push.bat "日常更新"
```
### 功能开发
```bash
# 使用PowerShell版本获得最佳体验
.\auto_push.ps1 "feat: 添加用户管理功能"
```
### 紧急修复
```bash
# 快速修复
quick_push.bat "hotfix: 修复登录问题"
```
### 团队协作
```bash
# 使用标准版本,确保流程规范
auto_push.bat
```
## ⚙️ 脚本特性对比
| 特性 | quick_push.bat | auto_push.bat | auto_push.ps1 |
|------|----------------|---------------|---------------|
| 执行速度 | ⚡ 最快 | 🐌 中等 | 🐌 中等 |
| 用户交互 | 最少 | 中等 | 最多 |
| 错误处理 | 基础 | 完整 | 完整 |
| 状态显示 | 基础 | 详细 | 最详细 |
| 参数支持 | 基础 | 无 | 完整 |
| 彩色输出 | 无 | 无 | ✅ |
| 统计信息 | 无 | 基础 | 详细 |
| **智能提交信息** | ✅ | ✅ | ❌ |
| **Markdown分析** | ✅ | ✅ | ❌ |
| **提交类型识别** | ✅ | ✅ | ❌ |
## 🔧 自定义配置
### 修改默认提交信息格式
编辑 `auto_push.bat` 第25行
```batch
set commit_msg=feat: 自动提交 - %date% %time%
```
### 修改远程分支
编辑所有脚本中的 `origin main` 为你的分支:
```batch
git push origin your-branch
```
### 添加预提交检查
`auto_push.ps1` 中添加检查函数:
```powershell
function Test-PreCommit {
# 运行测试
python -m pytest
# 代码格式化检查
python -m black --check .
# 类型检查
python -m mypy .
}
```
## 🛠️ 故障排除
### 常见问题
1. **"nothing to commit, working tree clean" 错误**
```
原因: 工作区没有未提交的更改
解决: 脚本已自动检测并跳过推送,这是正常行为
```
2. **推送失败 (Push failed)**
```
可能原因:
- 网络连接问题
- 远程仓库权限不足
- 分支冲突
- 需要先拉取远程更改
解决方案:
1. 检查网络连接
2. 运行: git pull origin main
3. 重新运行推送脚本
```
3. **PowerShell执行策略错误**
```powershell
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
```
4. **Git认证失败**
```bash
# 检查远程仓库配置
git remote -v
# 重新设置认证
git config --global credential.helper store
```
5. **编码问题**
```bash
# 确保控制台支持UTF-8
chcp 65001
```
6. **重复使用脚本报错**
```
原因: 工作区已干净,无需再次推送
解决: 脚本已优化,会自动检测并跳过
```
### 错误代码说明
- `退出代码 0`: 成功
- `退出代码 1`: Git操作失败
- `退出代码 2`: 用户取消操作
## 📝 最佳实践
1. **提交前检查**: 使用 `auto_push.ps1` 查看详细状态
2. **提交信息规范**: 使用 `feat:`, `fix:`, `docs:` 等前缀
3. **定期推送**: 避免长时间不推送导致冲突
4. **分支管理**: 在功能分支开发,合并到主分支
## 🎯 示例工作流
```bash
# 1. 开发功能
# ... 编写代码 ...
# 2. 快速推送
quick_push.bat "feat: 添加AI建议功能"
# 3. 或者详细推送
.\auto_push.ps1 "feat: 添加AI建议功能
- 实现语义相似度计算
- 优化前端UI显示
- 添加配置化阈值"
```
---
**提示**: 建议将脚本文件添加到项目根目录,并设置适当的执行权限。对于团队使用,建议统一使用 `auto_push.ps1` 以确保流程一致性。

View File

@@ -1,210 +0,0 @@
# TSP智能助手新功能说明
## 概述
本次更新为TSP智能助手添加了以下核心功能模块
## 1. 对话历史模块
### 功能特性
- **对话管理**: 完整的对话记录管理
- **对话记忆**: 支持多轮对话上下文保持
- **Redis缓存**: 使用Redis提升对话历史查询性能
- **分页查询**: 支持游标分页查询对话历史
- **删除功能**: 支持删除单个或批量删除对话记录
### 核心文件
- `src/dialogue/conversation_history.py`: 对话历史管理器
- 集成到 `src/dialogue/dialogue_manager.py`
### 使用方法
```python
# 获取用户对话历史
history = assistant.get_user_conversation_history(user_id, limit=10, offset=0)
# 删除对话记录
assistant.delete_conversation(conversation_id)
# 删除用户所有对话记录
assistant.delete_user_conversations(user_id)
```
## 2. Token消耗监控
### 功能特性
- **实时监控**: 实时记录和监控Token使用情况
- **成本计算**: 自动计算AI调用成本
- **阈值预警**: 设置成本阈值并触发预警
- **统计分析**: 提供详细的Token使用统计
- **趋势分析**: 成本趋势分析
### 核心文件
- `src/analytics/token_monitor.py`: Token监控器
### 使用方法
```python
# 获取Token使用统计
token_stats = assistant.get_token_usage_stats(user_id, days=7)
# 获取成本趋势
cost_trend = assistant.get_cost_trend(days=30)
```
## 3. AI调用成功率监控
### 功能特性
- **成功率监控**: 监控AI API调用成功率
- **性能分析**: 分析响应时间和错误率
- **模型对比**: 不同模型的性能对比
- **预警机制**: 连续失败和错误率预警
- **趋势分析**: 性能趋势分析
### 核心文件
- `src/analytics/ai_success_monitor.py`: AI成功率监控器
### 使用方法
```python
# 获取AI性能统计
ai_stats = assistant.get_ai_performance_stats(hours=24)
# 获取性能趋势
performance_trend = assistant.get_performance_trend(days=7)
```
## 4. 系统优化模块
### 功能特性
- **性能优化**: CPU、内存、磁盘使用率监控
- **安全优化**: 输入安全检查、频率限制
- **流量保护**: 请求频率限制和并发控制
- **成本优化**: 成本限制和预算控制
- **稳定性优化**: 系统健康状态监控
### 核心文件
- `src/core/system_optimizer.py`: 系统优化器
### 使用方法
```python
# 检查频率限制
can_proceed = assistant.check_rate_limit(user_id)
# 检查输入安全性
security_check = assistant.check_input_security(user_input)
# 检查成本限制
can_proceed = assistant.check_cost_limit(estimated_cost)
# 获取系统状态
system_status = assistant.get_system_optimization_status()
```
## 5. Redis集成
### 配置信息
- **主机**: 43.134.68.207
- **端口**: 6379
- **密码**: 123456
### 使用场景
- 对话历史缓存
- Token使用数据存储
- AI调用记录缓存
- 系统性能指标存储
- 频率限制计数
- 成本限制计数
## 6. 启动脚本整合
### 更新内容
- 整合所有新功能到 `start_dashboard.py`
- 添加系统初始化检查
- 显示完整功能列表
- 错误处理和日志记录
### 启动方式
```bash
python start_dashboard.py
```
## 7. 测试脚本
### 测试文件
- `test_new_features.py`: 新功能测试脚本
### 测试内容
- 对话历史功能测试
- Token监控功能测试
- AI性能监控测试
- 系统优化功能测试
- 对话删除功能测试
- 数据清理功能测试
### 运行测试
```bash
python test_new_features.py
```
## 8. 依赖更新
### 新增依赖
- `redis>=4.5.0`: Redis客户端库
### 更新文件
- `requirements.txt`: 添加Redis依赖
## 9. 性能优化特性
### 缓存策略
- Redis缓存对话历史提升查询性能
- 内存缓存最近对话,减少数据库访问
- 分层缓存策略,平衡性能和存储
### 监控指标
- 实时系统资源监控
- API调用性能监控
- Token使用成本监控
- 用户行为分析
### 安全特性
- 输入内容安全检查
- 请求频率限制
- 成本预算控制
- 异常情况预警
## 10. 使用建议
### 生产环境部署
1. 确保Redis服务正常运行
2. 配置合适的监控阈值
3. 定期清理历史数据
4. 监控系统性能指标
### 开发环境测试
1. 运行测试脚本验证功能
2. 检查Redis连接状态
3. 观察日志输出
4. 测试各种边界情况
## 11. 故障排除
### 常见问题
1. **Redis连接失败**: 检查Redis服务状态和网络连接
2. **数据库连接问题**: 检查MySQL服务状态
3. **性能问题**: 检查系统资源使用情况
4. **成本超限**: 调整成本限制阈值
### 日志位置
- 主日志: `logs/tsp_assistant.log`
- 启动日志: `logs/dashboard.log`
## 12. 未来扩展
### 计划功能
- 更细粒度的权限控制
- 更丰富的监控指标
- 自动化运维功能
- 机器学习优化建议
---
**注意**: 所有新功能都已集成到现有系统中,不会影响原有功能的正常使用。建议在生产环境部署前先在测试环境验证所有功能。

View File

@@ -1,243 +0,0 @@
# TSP智能助手 v1.4.0 新功能说明
## 🎉 版本概述
TSP智能助手 v1.4.0 是一个重要的功能更新版本,主要包含飞书集成、页面功能合并、数据库架构优化和代码重构等重要改进。
## 🚀 主要新功能
### 1. 飞书多维表格集成 📱
#### 功能描述
- **数据同步**: 支持从飞书多维表格自动同步工单数据
- **字段映射**: 智能映射飞书字段到本地数据库结构
- **实时更新**: 支持增量同步和全量同步
- **数据预览**: 同步前可预览数据,确保准确性
#### 支持的飞书字段
| 飞书字段 | 本地字段 | 类型 | 说明 |
|---------|---------|------|------|
| TR Number | order_id | String | 工单编号 |
| TR Description | description | Text | 工单描述 |
| Type of problem | category | String | 问题类型 |
| TR Level | priority | String | 优先级 |
| TR Status | status | String | 工单状态 |
| Source | source | String | 来源 |
| Created by | created_by | String | 创建人 |
| Module模块 | module | String | 模块 |
| Wilfulness责任人 | wilfulness | String | 责任人 |
| Date of close TR | date_of_close | DateTime | 关闭日期 |
| Vehicle Type01 | vehicle_type | String | 车型 |
| VIN\|sim | vin_sim | String | 车架号/SIM |
| App remote control version | app_remote_control_version | String | 应用远程控制版本 |
| HMI SW | hmi_sw | String | HMI软件版本 |
| 父记录 | parent_record | String | 父记录 |
| Has it been updated on the same day | has_updated_same_day | String | 是否同日更新 |
| Operating time | operating_time | String | 操作时间 |
#### 使用方法
1. 在飞书开放平台创建企业自建应用
2. 配置 `config/integrations_config.json` 文件
3. 在主仪表板的"飞书同步"标签页进行数据同步
4. 支持测试连接、预览数据、执行同步等操作
### 2. 页面功能合并 🎨
#### 改进内容
- **统一界面**: 飞书同步功能已合并到主仪表板
- **标签页设计**: 使用标签页组织不同功能模块
- **用户体验**: 所有功能现在都在一个统一的界面中
- **代码优化**: 删除了冗余的独立页面和蓝图
#### 界面变化
- **原独立页面**: `http://localhost:5000/feishu-sync` (已删除)
- **现集成位置**: 主仪表板的"飞书同步"标签页
- **访问方式**: 访问 `http://localhost:5000` 即可使用所有功能
### 3. 数据库架构优化 🗄️
#### 工单表扩展
`work_orders` 表新增了12个飞书相关字段
```sql
-- 飞书集成字段
source VARCHAR(50) -- 来源
module VARCHAR(100) -- 模块
created_by VARCHAR(100) -- 创建人
wilfulness VARCHAR(100) -- 责任人
date_of_close DATETIME -- 关闭日期
vehicle_type VARCHAR(100) -- 车型
vin_sim VARCHAR(50) -- 车架号/SIM
app_remote_control_version VARCHAR(100) -- 应用远程控制版本
hmi_sw VARCHAR(100) -- HMI软件版本
parent_record VARCHAR(100) -- 父记录
has_updated_same_day VARCHAR(50) -- 是否同日更新
operating_time VARCHAR(100) -- 操作时间
```
#### 数据库初始化改进
- **自动迁移**: 字段迁移已集成到数据库初始化流程
- **智能检测**: 自动检测缺失字段并添加
- **错误处理**: 改进的错误处理和日志记录
- **兼容性**: 保持与现有数据的兼容性
### 4. 代码重构优化 🔧
#### 文件结构优化
- **大文件拆分**: 将 `src/agent_assistant.py` 拆分为多个模块
- **模块化设计**: 创建 `agent_assistant_core.py``agent_message_handler.py``agent_sample_actions.py`
- **降低风险**: 减少单文件代码行数,降低运行风险
- **维护性**: 提高代码的可维护性和可读性
#### 前端架构改进
- **JavaScript类**: 使用类组织前端逻辑
- **模块化**: `TSPDashboard``FeishuSyncManager` 等独立模块
- **异步处理**: 改进的异步API调用处理
- **错误处理**: 更好的错误处理和用户反馈
## 📋 配置说明
### 飞书集成配置
编辑 `config/integrations_config.json` 文件:
```json
{
"feishu": {
"app_id": "cli_a8b50ec0eed1500d",
"app_secret": "ccxkE7ZCFQZcwkkM1rLy0ccZRXYsT2xK",
"app_token": "XXnEbiCmEaMblSs6FDJcFCqsnIg",
"table_id": "tblnl3vJPpgMTSiP",
"last_updated": "2025-09-19T18:27:40.579958",
"status": "active"
},
"system": {
"sync_limit": 10,
"ai_suggestions_enabled": true,
"auto_sync_interval": 0,
"last_sync_time": null
}
}
```
### 环境变量支持
```bash
# 飞书配置
export FEISHU_APP_ID="your-app-id"
export FEISHU_APP_SECRET="your-app-secret"
export FEISHU_APP_TOKEN="your-app-token"
export FEISHU_TABLE_ID="your-table-id"
```
## 🚀 部署指南
### 部署前准备
1. **配置飞书应用**
- 在飞书开放平台创建企业自建应用
- 获取应用凭证和权限
2. **更新配置文件**
- 配置 `config/integrations_config.json`
- 设置正确的飞书应用信息
3. **初始化数据库**
```bash
python init_database.py
```
4. **测试连接**
- 启动服务后访问主仪表板
- 在"飞书同步"标签页测试连接
### 部署步骤
```bash
# 1. 备份当前版本
python scripts/update_manager.py create-backup --environment production
# 2. 部署新版本
python scripts/update_manager.py auto-update --source . --environment production
# 3. 验证功能
# 访问 http://localhost:5000
# 测试飞书同步功能
```
## 🔍 使用指南
### 飞书数据同步
1. **访问功能**
- 打开浏览器访问 `http://localhost:5000`
- 点击"飞书同步"标签页
2. **测试连接**
- 点击"测试连接"按钮
- 验证飞书应用配置是否正确
3. **预览数据**
- 点击"预览数据"按钮
- 查看将要同步的数据内容
4. **执行同步**
- 点击"同步数据"按钮
- 等待同步完成
5. **查看结果**
- 在工单管理页面查看同步的数据
- 验证字段映射是否正确
### 工单管理增强
1. **查看飞书字段**
- 在工单详情页面可以看到新的飞书字段
- 包括来源、模块、责任人等信息
2. **数据关联**
- 飞书数据与本地工单数据关联
- 支持双向数据同步
## 🐛 故障排除
### 常见问题
1. **飞书连接失败**
- 检查app_id和app_secret是否正确
- 验证应用权限配置
- 确认网络连接正常
2. **字段映射错误**
- 检查飞书表格字段名称
- 验证字段映射配置
- 查看同步日志
3. **数据库迁移失败**
- 检查数据库连接状态
- 验证数据库权限
- 查看初始化日志
4. **页面功能异常**
- 清除浏览器缓存
- 检查JavaScript控制台错误
- 验证API接口状态
### 日志位置
- **应用日志**: `logs/tsp_assistant.log`
- **数据库日志**: 数据库初始化输出
- **飞书同步日志**: 在同步界面显示
## 📞 技术支持
如有问题,请:
1. 查看相关日志文件
2. 检查配置文件设置
3. 验证网络连接状态
4. 联系技术支持团队
---
**TSP智能助手 v1.4.0** - 让车辆服务更智能,让数据管理更便捷! 🚗✨

View File

@@ -1,130 +0,0 @@
# TSP助手项目重构总结
## 问题描述
1. **app.py文件损坏**: 原文件有乱码和语法错误
2. **文件过长**: 原app.py文件有1953行难以维护
3. **架构不合理**: 所有功能都集中在一个文件中
4. **前端响应问题**: 工单和对话历史的删除/新增操作前端无响应
## 解决方案
### 1. 修复乱码和错误
- 识别并修复了所有乱码字符
- 统一了API响应格式
- 修复了前端响应检查逻辑
### 2. 架构重构
采用Flask蓝图Blueprint架构将单一文件拆分为多个模块
#### 重构前
- **app.py**: 1953行包含所有功能
- 代码混乱,难以维护
- 单点故障风险高
#### 重构后
- **app.py**: 674行只包含核心路由
- **blueprints/**: 6个功能模块
- `alerts.py`: 预警管理 (100行)
- `workorders.py`: 工单管理 (400行)
- `conversations.py`: 对话管理 (80行)
- `knowledge.py`: 知识库管理 (150行)
- `monitoring.py`: 监控管理 (300行)
- `system.py`: 系统管理 (200行)
### 3. 前端响应问题修复
统一了前端JavaScript中的响应检查逻辑
- 删除操作: 检查 `data.success` 而不是 `response.ok`
- 新增操作: 统一使用 `data.success` 检查
- 修改操作: 修复了响应状态检查
## 技术改进
### 1. 模块化设计
- 每个功能模块独立
- 便于团队协作开发
- 降低代码耦合度
### 2. 错误隔离
- 单个模块错误不影响整体
- 独立的错误处理机制
- 更好的调试体验
### 3. 可扩展性
- 新增功能只需创建新蓝图
- 蓝图可复用
- 支持插件式开发
### 4. 维护性提升
- 代码结构清晰
- 功能职责明确
- 便于代码审查
## 文件结构对比
### 重构前
```
src/web/
├── app.py (1953行) - 所有功能
├── static/
└── templates/
```
### 重构后
```
src/web/
├── app.py (674行) - 核心路由
├── app_backup.py - 原文件备份
├── blueprints/ - 蓝图模块
│ ├── __init__.py
│ ├── alerts.py
│ ├── workorders.py
│ ├── conversations.py
│ ├── knowledge.py
│ ├── monitoring.py
│ ├── system.py
│ └── README.md
├── static/
└── templates/
```
## 性能优化
1. **懒加载**: 避免启动时重复初始化
2. **模块化**: 按需加载功能模块
3. **缓存优化**: 保持原有的查询优化
4. **错误处理**: 统一的异常处理机制
## 兼容性保证
1. **API接口**: 保持原有接口不变
2. **前端调用**: 无需修改前端代码
3. **数据库**: 保持原有数据模型
4. **功能**: 所有功能正常工作
## 测试验证
- ✅ 应用导入成功
- ✅ 蓝图注册正常
- ✅ API路由正确
- ✅ 前端响应修复
- ✅ 代码语法检查通过
## 后续建议
1. **单元测试**: 为每个蓝图编写单元测试
2. **文档完善**: 补充API文档和使用说明
3. **性能监控**: 添加性能监控和日志
4. **持续集成**: 建立CI/CD流程
5. **代码规范**: 制定代码规范和审查流程
## 总结
通过这次重构,我们成功解决了:
- ✅ 修复了app.py的乱码和错误问题
- ✅ 将1953行的单文件拆分为多个模块
- ✅ 修复了前端响应问题
- ✅ 提升了代码的可维护性和可扩展性
- ✅ 保持了功能的完整性和兼容性
项目现在具有更好的架构设计,便于后续的开发和维护。

View File

@@ -1,231 +0,0 @@
# 飞书同步灵活字段映射系统总结
## 问题背景
原有的飞书同步系统存在以下问题:
- **字段映射过于呆板**:只能处理预定义的字段映射
- **缺乏灵活性**:无法适应字段调整、新增字段等情况
- **维护困难**:需要修改代码才能添加新的字段映射
- **用户体验差**:字段不存在时只能看到"不存在于数据中"的日志
## 解决方案
开发了一套**灵活字段映射系统**,具备以下特性:
### 1. 动态字段发现
- 自动分析飞书表格中的字段
- 识别已映射和未映射的字段
- 为未映射字段提供智能建议
### 2. 多种映射方式
- **直接映射**:字段名完全匹配
- **别名映射**:支持多个别名
- **模式匹配**:使用正则表达式匹配
- **优先级管理**:支持字段优先级设置
### 3. 智能建议算法
- **相似度匹配**:基于字符串相似度计算
- **模式匹配**:使用正则表达式模式
- **优先级排序**:按相似度和优先级排序建议
### 4. 灵活配置管理
- **Web界面管理**:提供友好的管理界面
- **API接口**:支持程序化管理
- **配置文件**支持JSON配置文件
- **实时更新**:配置变更立即生效
## 技术实现
### 核心组件
#### 1. FlexibleFieldMapper 类
```python
class FlexibleFieldMapper:
- discover_fields() # 字段发现
- map_field() # 字段映射
- convert_fields() # 字段转换
- add_field_mapping() # 添加映射
- remove_field_mapping() # 删除映射
```
#### 2. 配置文件结构
```json
{
"field_mapping": {}, #
"field_aliases": {}, #
"field_patterns": {}, #
"field_priorities": {}, #
"auto_mapping_enabled": true,
"similarity_threshold": 0.6
}
```
#### 3. API接口
- `GET /api/feishu-sync/field-mapping/status` - 获取映射状态
- `POST /api/feishu-sync/field-mapping/discover` - 发现字段
- `POST /api/feishu-sync/field-mapping/add` - 添加映射
- `POST /api/feishu-sync/field-mapping/remove` - 删除映射
### 集成方式
#### 1. 向后兼容
- 保留原有字段映射配置
- 自动将原有映射添加到新系统中
- 不影响现有功能
#### 2. 无缝集成
- 更新 `WorkOrderSyncService`
- 使用新的字段转换方法
- 提供详细的转换统计信息
## 功能特性
### 1. 智能字段发现
```
输入:飞书字段数据
输出:
- 已映射字段列表
- 未映射字段列表
- 建议映射列表(包含置信度)
```
### 2. 多种匹配策略
- **精确匹配**:字段名完全相同
- **别名匹配**:字段名在别名列表中
- **相似度匹配**:字符串相似度超过阈值
- **模式匹配**:正则表达式匹配
### 3. 优先级管理
- **优先级 1**:核心字段(工单号、描述、状态等)
- **优先级 2**:重要字段(来源、解决方案等)
- **优先级 3**:扩展字段(版本信息、操作时间等)
### 4. 自动学习能力
- 根据使用情况优化映射规则
- 支持动态调整相似度阈值
- 可启用/禁用自动映射功能
## 使用效果
### 测试结果
```
测试数据18个字段
- 已映射16个字段88.9%
- 未映射2个字段11.1%
- 智能建议高置信度建议5个字段
映射状态:
- 直接映射23个
- 别名映射107个
- 模式匹配83个
- 自动映射:启用
```
### 实际应用场景
#### 场景1字段名调整
**之前**:需要修改代码,重新部署
**现在**在Web界面一键添加映射
#### 场景2新增字段
**之前**:字段被忽略,数据丢失
**现在**:自动识别并提供建议映射
#### 场景3字段顺序调整
**之前**:可能影响映射结果
**现在**:基于字段名映射,不受顺序影响
## 用户界面
### Web管理界面
- **字段发现**:一键分析飞书字段
- **映射管理**:可视化添加/删除映射
- **状态监控**:实时查看映射状态
- **建议应用**:一键应用智能建议
### 操作流程
1. 访问 `/api/feishu-sync/field-mapping`
2. 点击"发现字段"分析当前字段
3. 查看建议映射并一键应用
4. 手动添加特殊字段映射
5. 监控映射状态和效果
## 技术优势
### 1. 高可扩展性
- 支持无限添加字段映射
- 支持多种映射策略
- 支持自定义匹配规则
### 2. 高可维护性
- 配置文件管理
- Web界面操作
- API接口支持
### 3. 高智能化
- 自动字段发现
- 智能映射建议
- 自适应学习
### 4. 高兼容性
- 向后兼容
- 无缝集成
- 渐进式升级
## 部署说明
### 1. 文件结构
```
src/integrations/
├── flexible_field_mapper.py # 核心映射器
├── workorder_sync.py # 更新的同步服务
└── ...
config/
└── field_mapping_config.json # 映射配置文件
src/web/
├── templates/
│ └── field_mapping.html # 管理界面
└── blueprints/
└── feishu_sync.py # 更新的API接口
```
### 2. 配置要求
- Python 3.7+
- Flask
- 现有飞书同步功能
### 3. 启动方式
- 无需额外配置
- 自动加载默认映射
- 支持热更新
## 未来规划
### 短期目标
- 优化相似度算法
- 增加更多匹配模式
- 完善Web界面功能
### 中期目标
- 支持批量字段映射
- 增加映射历史记录
- 提供映射效果分析
### 长期目标
- 机器学习优化映射
- 支持多语言字段映射
- 集成更多数据源
## 总结
灵活字段映射系统成功解决了飞书同步的字段映射问题,提供了:
**智能化**:自动发现字段并提供建议
**灵活性**:支持多种映射方式和策略
**易用性**Web界面操作一键应用建议
**可维护性**配置文件管理API接口支持
**兼容性**:向后兼容,无缝集成
该系统大大提升了飞书同步的灵活性和用户体验,为后续的功能扩展奠定了坚实基础。