feat: 自动提交 - 周一 2025/09/22 15:12:38.91

This commit is contained in:
赵杰
2025-09-22 15:12:38 +01:00
parent 9306e7a401
commit b635c9e7d4
41 changed files with 7360 additions and 950 deletions

211
frontend/README.md Normal file
View File

@@ -0,0 +1,211 @@
# 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

13
frontend/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!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 Normal file
View File

@@ -0,0 +1,35 @@
{
"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"
}
}
}
}

33
frontend/package.json Normal file
View File

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

38
frontend/src/App.vue Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,304 @@
<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>

27
frontend/src/main.ts Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,463 @@
<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>

753
frontend/src/views/Chat.vue Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,409 @@
<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>

25
frontend/tsconfig.json Normal file
View File

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

View File

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

43
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,43 @@
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
}
})