feat: 自动提交 - 周一 2025/09/22 16:28:00.19
This commit is contained in:
@@ -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建议功能将可以正常工作!🎉
|
||||
259
Linux使用说明.md
259
Linux使用说明.md
@@ -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智能助手了!🎉
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
35
frontend/package-lock.json
generated
35
frontend/package-lock.json
generated
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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": "网络错误"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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')
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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" }]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
321
src/web/app.py
321
src/web/app.py
@@ -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
@@ -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)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
293
src/web/blueprints/core.py
Normal 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)
|
||||
@@ -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
74
src/web/error_handlers.py
Normal 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
|
||||
71
src/web/service_manager.py
Normal file
71
src/web/service_manager.py
Normal 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()
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
]
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
265
前端架构优化总结.md
265
前端架构优化总结.md
@@ -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智能助手的长期发展奠定了坚实基础。
|
||||
136
前端页面优化总结.md
136
前端页面优化总结.md
@@ -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. **用户体验改进**
|
||||
- 添加设置向导功能
|
||||
- 提供配置建议和最佳实践
|
||||
- 添加配置变更通知
|
||||
|
||||
---
|
||||
|
||||
**总结**:成功删除了前端页面中的无效系统设置,并添加了完整的中英文语言切换功能。优化后的页面更加简洁、实用,用户体验得到显著提升。🎉
|
||||
255
推送脚本使用说明.md
255
推送脚本使用说明.md
@@ -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` 以确保流程一致性。
|
||||
210
新功能说明.md
210
新功能说明.md
@@ -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. 未来扩展
|
||||
|
||||
### 计划功能
|
||||
- 更细粒度的权限控制
|
||||
- 更丰富的监控指标
|
||||
- 自动化运维功能
|
||||
- 机器学习优化建议
|
||||
|
||||
---
|
||||
|
||||
**注意**: 所有新功能都已集成到现有系统中,不会影响原有功能的正常使用。建议在生产环境部署前先在测试环境验证所有功能。
|
||||
243
新功能说明_v1.4.0.md
243
新功能说明_v1.4.0.md
@@ -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** - 让车辆服务更智能,让数据管理更便捷! 🚗✨
|
||||
130
重构总结.md
130
重构总结.md
@@ -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行的单文件拆分为多个模块
|
||||
- ✅ 修复了前端响应问题
|
||||
- ✅ 提升了代码的可维护性和可扩展性
|
||||
- ✅ 保持了功能的完整性和兼容性
|
||||
|
||||
项目现在具有更好的架构设计,便于后续的开发和维护。
|
||||
@@ -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接口支持
|
||||
✅ **兼容性**:向后兼容,无缝集成
|
||||
|
||||
该系统大大提升了飞书同步的灵活性和用户体验,为后续的功能扩展奠定了坚实基础。
|
||||
Reference in New Issue
Block a user