diff --git a/.DS_Store b/.DS_Store index ec943fa..05fc10e 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/KB_TEMPLATE_SELECTION_IMPLEMENTATION.md b/KB_TEMPLATE_SELECTION_IMPLEMENTATION.md new file mode 100644 index 0000000..ca05ee3 --- /dev/null +++ b/KB_TEMPLATE_SELECTION_IMPLEMENTATION.md @@ -0,0 +1,246 @@ +# 知识库新增页面 - 模版选择功能实现总结 + +## 功能概述 +在知识库新增流程的第二步"自定义提示词"中添加了模版选择功能,用户可以选择预设的模版来生成知识库内容。 + +## 实现的功能点 + +### 1. 添加状态管理 ✅ +**文件**: `frontend/src/pages/KnowledgeBasePage.jsx` + +添加了两个新的 state: +```javascript +const [availablePrompts, setAvailablePrompts] = useState([]); // 可用的提示词模版列表 +const [selectedPromptId, setSelectedPromptId] = useState(null); // 选中的提示词模版ID +``` + +### 2. 获取模版列表 ✅ +**文件**: `frontend/src/pages/KnowledgeBasePage.jsx` + +新增方法 `fetchAvailablePrompts()`: +```javascript +const fetchAvailablePrompts = async () => { + try { + const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.ACTIVE('KNOWLEDGE_TASK'))); + const promptsList = response.data.prompts || []; + setAvailablePrompts(promptsList); + + // 自动选中默认模版 + const defaultPrompt = promptsList.find(p => p.is_default); + if (defaultPrompt) { + setSelectedPromptId(defaultPrompt.id); + } + } catch (error) { + console.error("Error fetching available prompts:", error); + setAvailablePrompts([]); + } +}; +``` + +**调用时机**: +- 在 `handleOpenCreateModal()` 中调用,当用户打开新增知识库弹窗时获取模版列表 +- 自动选中默认模版,无需用户手动选择 + +### 3. UI 实现 ✅ +**文件**: `frontend/src/pages/KnowledgeBasePage.jsx` + +在第二步(`createStep === 2`)中添加模版选择下拉框: + +```jsx +{/* 模版选择 */} +{availablePrompts.length > 0 && ( +
+ +

选择一个预设的模版来生成知识库内容,或使用默认模版。

+ +
+)} +``` + +**UI 特点**: +- 只在有可用模版时显示 +- 第一个选项为"使用默认模版"(value为空) +- 每个模版选项显示名称,默认模版标注"(默认)" +- 支持清空选择(返回默认模版) + +### 4. 数据传递 ✅ +**文件**: `frontend/src/pages/KnowledgeBasePage.jsx` + +修改 `handleGenerate()` 方法,将 `prompt_id` 传递给后端: + +```javascript +const handleGenerate = async () => { + if (!selectedMeetings || selectedMeetings.length === 0) { + showToast('请至少选择一个会议', 'warning'); + return; + } + + setGenerating(true); + try { + const response = await apiClient.post(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.CREATE), { + user_prompt: userPrompt, + source_meeting_ids: selectedMeetings.join(','), + is_shared: false, + prompt_id: selectedPromptId // 传递选中的模版ID + }); + setTaskId(response.data.task_id); + } catch (error) { + console.error("Error creating knowledge base:", error); + setGenerating(false); + } +}; +``` + +### 5. API 配置 ✅ +**文件**: `frontend/src/config/api.js` + +添加新的 API 端点配置: + +```javascript +PROMPTS: { + LIST: '/api/prompts', + CREATE: '/api/prompts', + UPDATE: (promptId) => `/api/prompts/${promptId}`, + DELETE: (promptId) => `/api/prompts/${promptId}`, + ACTIVE: (taskType) => `/api/prompts/active/${taskType}` // 获取指定任务类型的启用模版 +}, +``` + +### 6. CSS 样式 ✅ +**文件**: `frontend/src/pages/KnowledgeBasePage.css` + +添加模版选择器的样式: + +```css +.field-hint { + font-size: 0.875rem; + color: #64748b; + margin-bottom: 0.5rem; + line-height: 1.5; +} + +.template-select { + width: 100%; + padding: 0.75rem; + border: 1px solid #e2e8f0; + border-radius: 8px; + font-family: inherit; + font-size: 0.95rem; + background-color: white; + color: #1e293b; + cursor: pointer; + transition: all 0.2s ease; +} + +.template-select:hover { + border-color: #cbd5e1; +} + +.template-select:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.template-select option { + padding: 0.5rem; +} +``` + +## 用户流程 + +1. **打开新增知识库弹窗** + - 点击"新增知识条目"按钮 + - 自动获取 KNOWLEDGE_TASK 类型的启用模版列表 + +2. **第一步:选择会议** + - 选择一个或多个会议作为数据源 + - 点击"下一步" + +3. **第二步:自定义提示词** + - 【新增】选择生成模版(可选) + - 下拉框显示所有启用的知识库模版 + - 默认选中标记为"默认"的模版 + - 可以选择其他模版或清空选择 + - 输入用户提示词(可选) + - 点击"生成知识库" + +4. **生成知识库** + - 系统将 `prompt_id` 和 `user_prompt` 一起发送到后端 + - 后端根据选择的模版生成知识库内容 + +## 数据流 + +``` +用户打开弹窗 + ↓ +fetchAvailablePrompts() + ↓ +GET /api/prompts/active/KNOWLEDGE_TASK + ↓ +获取启用的模版列表 + ↓ +自动选中默认模版 + ↓ +用户可以选择其他模版 + ↓ +用户点击"生成知识库" + ↓ +POST /api/knowledge-bases +{ + user_prompt: "...", + source_meeting_ids: "1,2,3", + is_shared: false, + prompt_id: 13 // 选中的模版ID +} + ↓ +后端使用指定模版生成知识库 +``` + +## 兼容性 + +- **向后兼容**:prompt_id 为可选参数,不选择时传 null,后端使用默认模版 +- **容错处理**:如果获取模版列表失败,不影响正常流程,用户仍可生成知识库 +- **智能默认**:自动选中默认模版,减少用户操作 + +## 特点 + +1. **用户友好** + - 自动选中默认模版,无需手动选择 + - 清晰标注哪个是默认模版 + - 提供"使用默认模版"选项方便重置 + +2. **界面一致** + - 与会议总结的模版选择保持一致的设计风格 + - 使用相同的 UI 组件和样式 + +3. **灵活性** + - 支持选择任何启用的 KNOWLEDGE_TASK 模版 + - 可以与用户提示词组合使用 + - 可以随时切换模版 + +## 文件变更列表 + +1. `frontend/src/pages/KnowledgeBasePage.jsx` - 主要逻辑实现 +2. `frontend/src/pages/KnowledgeBasePage.css` - 样式文件 +3. `frontend/src/config/api.js` - API 配置 + +## 后续优化建议 + +1. 可以考虑在模版选择器中显示模版的描述或预览 +2. 可以添加"查看模版详情"的功能 +3. 可以记住用户上次选择的模版(localStorage) + +## 总结 + +知识库新增页面的模版选择功能已完全实现,用户现在可以在创建知识库时灵活选择不同的生成模版,大大提升了知识库生成的灵活性和个性化程度。 diff --git a/dist.zip b/dist.zip index 7ba799f..040f48b 100644 Binary files a/dist.zip and b/dist.zip differ diff --git a/src/App.jsx b/src/App.jsx index 3959146..4522369 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -4,6 +4,7 @@ import apiClient from './utils/apiClient'; import { buildApiUrl, API_ENDPOINTS } from './config/api'; import HomePage from './pages/HomePage'; import Dashboard from './pages/Dashboard'; +import AdminDashboard from './pages/AdminDashboard'; import MeetingDetails from './pages/MeetingDetails'; import MeetingPreview from './pages/MeetingPreview'; import CreateMeeting from './pages/CreateMeeting'; @@ -72,7 +73,11 @@ function App() { user ? : } /> : + user ? ( + user.role_id === 1 + ? + : + ) : } /> : diff --git a/src/components/ClientDownloads.jsx b/src/components/ClientDownloads.jsx index fcc67f1..db5ef46 100644 --- a/src/components/ClientDownloads.jsx +++ b/src/components/ClientDownloads.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Download, Smartphone, Monitor, Apple, ChevronRight } from 'lucide-react'; +import { Download, Smartphone, Monitor, Apple, ChevronRight, Cpu } from 'lucide-react'; import apiClient from '../utils/apiClient'; import { buildApiUrl, API_ENDPOINTS } from '../config/api'; import './ClientDownloads.css'; @@ -7,7 +7,8 @@ import './ClientDownloads.css'; const ClientDownloads = () => { const [clients, setClients] = useState({ mobile: [], - desktop: [] + desktop: [], + terminal: [] }); const [loading, setLoading] = useState(true); @@ -20,7 +21,7 @@ const ClientDownloads = () => { try { const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.LATEST)); console.log('Latest clients response:', response); - setClients(response.data || { mobile: [], desktop: [] }); + setClients(response.data || { mobile: [], desktop: [], terminal: [] }); } catch (error) { console.error('获取客户端下载失败:', error); } finally { @@ -37,12 +38,24 @@ const ClientDownloads = () => { case 'mac_intel': case 'mac_m': return ; + case 'mcu': + return ; default: return ; } }; - const getPlatformLabel = (platformName) => { + const getPlatformLabel = (client) => { + const platformName = client.platform_name; + const platformType = client.platform_type; + + // 根据平台类型和平台名称组合判断 + if (platformType === 'terminal') { + if (platformName === 'android') return 'Android终端'; + if (platformName === 'mcu') return '单片机'; + } + + // 默认标签 const labels = { ios: 'iOS', android: 'Android', @@ -99,7 +112,7 @@ const ClientDownloads = () => { {getPlatformIcon(client.platform_name)}
-

{getPlatformLabel(client.platform_name)}

+

{getPlatformLabel(client)}

v{client.version} {client.file_size && ( @@ -139,7 +152,47 @@ const ClientDownloads = () => { {getPlatformIcon(client.platform_name)}
-

{getPlatformLabel(client.platform_name)}

+

{getPlatformLabel(client)}

+
+ v{client.version} + {client.file_size && ( + {formatFileSize(client.file_size)} + )} +
+ {client.min_system_version && ( +

需要 {client.min_system_version} 或更高版本

+ )} +
+
+ +
+ + ))} +
+ + )} + + {/* 专用终端 */} + {clients.terminal && clients.terminal.length > 0 && ( +
+
+ +

专用终端

+
+
+ {clients.terminal.map(client => ( + +
+ {getPlatformIcon(client.platform_name)} +
+
+

{getPlatformLabel(client)}

v{client.version} {client.file_size && ( @@ -160,7 +213,7 @@ const ClientDownloads = () => { )}
- {!clients.mobile?.length && !clients.desktop?.length && ( + {!clients.mobile?.length && !clients.desktop?.length && !clients.terminal?.length && (
暂无可用的客户端下载
)}
diff --git a/src/config/api.js b/src/config/api.js index 9bed12c..6564c27 100644 --- a/src/config/api.js +++ b/src/config/api.js @@ -35,7 +35,12 @@ const API_CONFIG = { NAVIGATION: (meetingId) => `/api/meetings/${meetingId}/navigation` }, ADMIN: { - SYSTEM_CONFIG: '/api/admin/system-config' + SYSTEM_CONFIG: '/api/admin/system-config', + DASHBOARD_STATS: '/api/admin/dashboard/stats', + ONLINE_USERS: '/api/admin/online-users', + KICK_USER: (userId) => `/api/admin/kick-user/${userId}`, + TASKS_MONITOR: '/api/admin/tasks/monitor', + SYSTEM_RESOURCES: '/api/admin/system/resources' }, TAGS: { LIST: '/api/tags' @@ -48,7 +53,8 @@ const API_CONFIG = { LIST: '/api/prompts', CREATE: '/api/prompts', UPDATE: (promptId) => `/api/prompts/${promptId}`, - DELETE: (promptId) => `/api/prompts/${promptId}` + DELETE: (promptId) => `/api/prompts/${promptId}`, + ACTIVE: (taskType) => `/api/prompts/active/${taskType}` // 获取指定任务类型的启用模版 }, KNOWLEDGE_BASE: { LIST: '/api/knowledge-bases', @@ -59,13 +65,13 @@ const API_CONFIG = { TASK_STATUS: (taskId) => `/api/knowledge-bases/tasks/${taskId}` }, CLIENT_DOWNLOADS: { - LIST: '/api/clients/downloads', - LATEST: '/api/clients/downloads/latest', - LATEST_BY_PLATFORM: (platformName) => `/api/clients/downloads/${platformName}/latest`, - DETAIL: (id) => `/api/clients/downloads/${id}`, - CREATE: '/api/clients/downloads', - UPDATE: (id) => `/api/clients/downloads/${id}`, - DELETE: (id) => `/api/clients/downloads/${id}` + LIST: '/api/clients', + LATEST: '/api/clients/latest', + LATEST_BY_PLATFORM: '/api/clients/latest/by-platform', + DETAIL: (id) => `/api/clients/${id}`, + CREATE: '/api/clients', + UPDATE: (id) => `/api/clients/${id}`, + DELETE: (id) => `/api/clients/${id}` }, VOICEPRINT: { STATUS: (userId) => `/api/voiceprint/${userId}`, diff --git a/src/pages/AdminDashboard.css b/src/pages/AdminDashboard.css new file mode 100644 index 0000000..cacc4b7 --- /dev/null +++ b/src/pages/AdminDashboard.css @@ -0,0 +1,434 @@ +.admin-dashboard { + min-height: 100vh; + background: #f8fafc; +} + +/* 使用与普通Dashboard一致的Header样式 */ +.admin-dashboard .dashboard-header { + background: white; + border-bottom: 1px solid #e2e8f0; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.admin-dashboard .header-content { + max-width: 1400px; + margin: 0 auto; + padding: 1rem 2rem; + display: flex; + justify-content: space-between; + align-items: center; +} + +.admin-dashboard .logo { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 1.5rem; + font-weight: bold; + color: #667eea; +} + +.admin-dashboard .logo-icon { + width: 32px; + height: 32px; +} + +.admin-dashboard .user-actions { + display: flex; + align-items: center; + gap: 1rem; +} + +.auto-refresh-toggle { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + border: 1px solid #e2e8f0; + background: white; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s ease; + font-size: 0.875rem; + color: #64748b; +} + +.auto-refresh-toggle:hover { + border-color: #667eea; + color: #667eea; + transform: translateY(-1px); +} + +.auto-refresh-toggle.active { + background: #667eea; + color: white; + border-color: #667eea; +} + +.admin-dashboard .user-menu-trigger { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background: #f8fafc; + border-radius: 12px; + cursor: pointer; + transition: all 0.2s; +} + +.admin-dashboard .user-menu-trigger:hover { + background: #f1f5f9; + transform: translateY(-1px); +} + +.dashboard-content { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.stat-card { + background: white; + border-radius: 16px; + padding: 1.5rem; + display: flex; + gap: 1rem; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + transition: all 0.3s ease; +} + +.stat-card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12); +} + +.stat-icon { + width: 60px; + height: 60px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + color: white; + flex-shrink: 0; +} + +.stat-content { + flex: 1; +} + +.stat-content h3 { + margin: 0 0 0.5rem 0; + font-size: 0.875rem; + color: #64748b; + font-weight: 500; +} + +.stat-number { + font-size: 2rem; + font-weight: 700; + color: #1e293b; + margin-bottom: 0.5rem; +} + +.stat-details { + display: flex; + flex-direction: column; + gap: 0.25rem; + font-size: 0.875rem; + color: #64748b; +} + +.resource-bars { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.resource-item { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.resource-bar { + flex: 1; + height: 8px; + background: #f1f5f9; + border-radius: 4px; + overflow: hidden; +} + +.resource-fill { + height: 100%; + background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); + transition: width 0.3s ease; +} + +.tasks-overview { + margin-bottom: 2rem; +} + +.tasks-overview h2 { + color: #1e293b; + margin-bottom: 1rem; + font-size: 1.25rem; + font-weight: 600; +} + +.task-stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 1.5rem; +} + +.task-stat-card { + background: white; + border-radius: 16px; + padding: 1.5rem; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); +} + +.task-stat-card h3 { + margin: 0 0 1rem 0; + font-size: 1.125rem; + color: #1e293b; + font-weight: 600; +} + +.task-stat-numbers { + display: flex; + justify-content: space-between; + gap: 1rem; +} + +.task-stat-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; +} + +.task-stat-label { + font-size: 0.875rem; + color: #64748b; +} + +.task-stat-value { + font-size: 1.5rem; + font-weight: 700; +} + +.task-stat-value.running { + color: #3b82f6; +} + +.task-stat-value.completed { + color: #10b981; +} + +.task-stat-value.failed { + color: #ef4444; +} + +.admin-content-grid { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.admin-panel { + background: white; + border-radius: 16px; + overflow: hidden; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); +} + +.panel-header { + padding: 1.5rem; + border-bottom: 1px solid #e2e8f0; + display: flex; + justify-content: space-between; + align-items: center; +} + +.panel-header h2 { + margin: 0; + font-size: 1.25rem; + color: #1e293b; + font-weight: 600; +} + +.panel-filters { + display: flex; + gap: 0.75rem; +} + +.panel-filters select { + padding: 0.5rem 1rem; + border: 1px solid #e2e8f0; + border-radius: 8px; + font-size: 0.875rem; + background: white; + cursor: pointer; + transition: all 0.2s; +} + +.panel-filters select:hover { + border-color: #667eea; +} + +.panel-filters select:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.panel-content { + padding: 1.5rem; + max-height: 500px; + overflow-y: auto; +} + +.empty-state { + text-align: center; + padding: 3rem; + color: #94a3b8; + font-size: 0.875rem; +} + +.online-users-table, .tasks-table { + width: 100%; + border-collapse: collapse; +} + +.online-users-table th, .tasks-table th { + text-align: left; + padding: 0.75rem; + background: #f8fafc; + color: #475569; + font-weight: 600; + font-size: 0.875rem; + border-bottom: 2px solid #e2e8f0; +} + +.online-users-table td, .tasks-table td { + padding: 0.75rem; + border-bottom: 1px solid #f1f5f9; + font-size: 0.875rem; + color: #1e293b; +} + +.online-users-table tbody tr:hover, .tasks-table tbody tr:hover { + background: #f8fafc; +} + +.kick-btn { + padding: 0.375rem; + border: none; + background: #fef2f2; + color: #ef4444; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.kick-btn:hover { + background: #fee2e2; + transform: translateY(-1px); +} + +.status-badge { + display: inline-block; + padding: 0.25rem 0.75rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; +} + +.status-badge.status-pending { + background: #fef3c7; + color: #92400e; +} + +.status-badge.status-processing { + background: #dbeafe; + color: #1e40af; +} + +.status-badge.status-completed { + background: #d1fae5; + color: #065f46; +} + +.status-badge.status-failed { + background: #fee2e2; + color: #991b1b; +} + +.loading-container { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + color: white; + font-size: 1.25rem; +} + +.admin-dashboard .error-message { + background: #fee2e2; + color: #991b1b; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +/* 滚动条样式 */ +.panel-content::-webkit-scrollbar { + width: 8px; +} + +.panel-content::-webkit-scrollbar-track { + background: #f1f5f9; + border-radius: 4px; +} + +.panel-content::-webkit-scrollbar-thumb { + background: #cbd5e1; + border-radius: 4px; +} + +.panel-content::-webkit-scrollbar-thumb:hover { + background: #94a3b8; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .stats-grid { + grid-template-columns: 1fr; + } + + .task-stats-grid { + grid-template-columns: 1fr; + } + + .admin-navbar { + flex-direction: column; + gap: 1rem; + align-items: stretch; + } + + .admin-navbar-right { + justify-content: space-between; + } +} diff --git a/src/pages/AdminDashboard.jsx b/src/pages/AdminDashboard.jsx new file mode 100644 index 0000000..1be8bb3 --- /dev/null +++ b/src/pages/AdminDashboard.jsx @@ -0,0 +1,508 @@ +import React, { useState, useEffect } from 'react'; +import { LogOut, User, Users, Activity, Server, HardDrive, Cpu, MemoryStick, RefreshCw, UserX, ChevronDown, KeyRound, Shield, BookText, Waves } from 'lucide-react'; +import apiClient from '../utils/apiClient'; +import { buildApiUrl, API_ENDPOINTS } from '../config/api'; +import Dropdown from '../components/Dropdown'; +import menuService from '../services/menuService'; +import './AdminDashboard.css'; + +// 常量定义 +const AUTO_REFRESH_INTERVAL = 30; // 自动刷新间隔(秒) +const STATUS_BADGE_MAP = { + 'pending': 'status-badge status-pending', + 'processing': 'status-badge status-processing', + 'completed': 'status-badge status-completed', + 'failed': 'status-badge status-failed' +}; +const STATUS_TEXT_MAP = { + 'pending': '待处理', + 'processing': '处理中', + 'completed': '已完成', + 'failed': '失败' +}; +const TASK_TYPE_TEXT_MAP = { + 'transcription': '转录', + 'summary': '总结', + 'knowledge_base': '知识库' +}; + +// 辅助函数 +const getStatusBadgeClass = (status) => STATUS_BADGE_MAP[status] || 'status-badge'; +const getStatusText = (status) => STATUS_TEXT_MAP[status] || status; +const getTaskTypeText = (type) => TASK_TYPE_TEXT_MAP[type] || type; + +// 默认管理员菜单 +const getDefaultMenus = () => [ + { menu_code: 'change_password', menu_name: '修改密码', menu_type: 'action' }, + { menu_code: 'prompt_management', menu_name: '提示词仓库', menu_type: 'link', menu_url: '/prompts' }, + { menu_code: 'platform_admin', menu_name: '平台管理', menu_type: 'link', menu_url: '/admin' }, + { menu_code: 'logout', menu_name: '退出登录', menu_type: 'action' } +]; + +const AdminDashboard = ({ user, onLogout }) => { + // 统计数据 + const [stats, setStats] = useState(null); + const [onlineUsers, setOnlineUsers] = useState([]); + const [tasks, setTasks] = useState([]); + const [resources, setResources] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + // 菜单权限相关状态 + const [userMenus, setUserMenus] = useState([]); + + // 任务筛选 + const [taskType, setTaskType] = useState('all'); + const [taskStatus, setTaskStatus] = useState('all'); + + // 自动刷新定时器 + const [autoRefresh, setAutoRefresh] = useState(true); + const [countdown, setCountdown] = useState(AUTO_REFRESH_INTERVAL); + + // 获取菜单配置 + const getMenuItemConfig = (menu) => { + const iconMap = { + 'change_password': , + 'prompt_management': , + 'platform_admin': , + 'logout': + }; + + const actionMap = { + 'logout': onLogout + }; + + return { + icon: iconMap[menu.menu_code], + label: menu.menu_name, + onClick: menu.menu_type === 'link' && menu.menu_url + ? () => window.location.href = menu.menu_url + : actionMap[menu.menu_code] + }; + }; + + // 获取用户菜单权限 + const fetchUserMenus = async () => { + try { + const response = await menuService.getUserMenus(); + if (response.code === '200') { + setUserMenus(response.data.menus || []); + } else { + setUserMenus(getDefaultMenus()); + } + } catch (error) { + console.error('Error fetching user menus:', error); + setUserMenus(getDefaultMenus()); + } + }; + + // 初始化和自动刷新 + useEffect(() => { + fetchAllData(); + fetchUserMenus(); + }, []); + + useEffect(() => { + if (autoRefresh) { + const timer = setInterval(() => { + setCountdown(prev => { + if (prev <= 1) { + fetchAllData(); + return AUTO_REFRESH_INTERVAL; + } + return prev - 1; + }); + }, 1000); + return () => clearInterval(timer); + } + }, [autoRefresh]); + + useEffect(() => { + fetchTasks(); + }, [taskType, taskStatus]); + + const fetchAllData = async () => { + try { + setLoading(true); + await Promise.all([ + fetchStats(), + fetchOnlineUsers(), + fetchTasks(), + fetchResources() + ]); + setError(''); + } catch (err) { + console.error('获取数据失败:', err); + setError('加载数据失败,请稍后重试'); + } finally { + setLoading(false); + } + }; + + const fetchStats = async () => { + try { + const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.DASHBOARD_STATS)); + if (response.code === '200') { + setStats(response.data); + } + } catch (err) { + console.error('获取统计数据失败:', err); + } + }; + + const fetchOnlineUsers = async () => { + try { + const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.ONLINE_USERS)); + if (response.code === '200') { + setOnlineUsers(response.data.users || []); + } + } catch (err) { + console.error('获取在线用户失败:', err); + } + }; + + const fetchTasks = async () => { + try { + const params = new URLSearchParams(); + if (taskType !== 'all') params.append('task_type', taskType); + if (taskStatus !== 'all') params.append('status', taskStatus); + params.append('limit', '20'); + + const url = `${API_ENDPOINTS.ADMIN.TASKS_MONITOR}?${params.toString()}`; + const response = await apiClient.get(buildApiUrl(url)); + if (response.code === '200') { + setTasks(response.data.tasks || []); + } + } catch (err) { + console.error('获取任务列表失败:', err); + } + }; + + const fetchResources = async () => { + try { + const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.SYSTEM_RESOURCES)); + if (response.code === '200') { + setResources(response.data); + } + } catch (err) { + console.error('获取系统资源失败:', err); + } + }; + + const handleKickUser = async (userId) => { + if (!confirm('确定要踢出该用户吗?')) return; + + try { + const response = await apiClient.post(buildApiUrl(API_ENDPOINTS.ADMIN.KICK_USER(userId))); + if (response.code === '200') { + alert('用户已被踢出'); + fetchOnlineUsers(); + } else { + alert('踢出失败: ' + response.message); + } + } catch (err) { + console.error('踢出用户失败:', err); + alert('踢出用户失败'); + } + }; + + if (loading && !stats) { + return ( +
+
加载中...
+
+ ); + } + + return ( +
+ {/* 顶部导航栏 - 使用与普通Dashboard一致的样式 */} +
+
+
+ + iMeeting 管理后台 +
+
+ + + + {user.caption} + +
+ } + items={userMenus.map(menu => getMenuItemConfig(menu))} + /> +
+
+
+ + {/* Dashboard Content */} +
+ {error &&
{error}
} + + {/* 统计卡片 */} + {stats && ( +
+ {/* 用户统计 */} +
+
+ +
+
+

用户统计

+
{stats.users.total}
+
+ 今日新增: {stats.users.today_new} + 在线: {stats.users.online} +
+
+
+ + {/* 会议统计 */} +
+
+ +
+
+

会议统计

+
{stats.meetings.total}
+
+ 今日新增: {stats.meetings.today_new} +
+
+
+ + {/* 存储统计 */} +
+
+ +
+
+

存储统计

+
{stats.storage.audio_total_size_gb} GB
+
+ 音频文件: {stats.storage.audio_files_count} 个 +
+
+
+ + {/* 服务器资源 */} + {resources && ( +
+
+ +
+
+

服务器资源

+
+
+ +
+
+
+ {resources.cpu.percent}% +
+
+ +
+
+
+ {resources.memory.percent}% +
+
+ +
+
+
+ {resources.disk.percent}% +
+
+
+
+ )} +
+ )} + + {/* 任务统计 */} + {stats && ( +
+

任务概览

+
+
+

转录任务

+
+
+ 进行中 + {stats.tasks.transcription.running} +
+
+ 已完成 + {stats.tasks.transcription.completed} +
+
+ 失败 + {stats.tasks.transcription.failed} +
+
+
+ +
+

总结任务

+
+
+ 进行中 + {stats.tasks.summary.running} +
+
+ 已完成 + {stats.tasks.summary.completed} +
+
+ 失败 + {stats.tasks.summary.failed} +
+
+
+ +
+

知识库任务

+
+
+ 进行中 + {stats.tasks.knowledge_base.running} +
+
+ 已完成 + {stats.tasks.knowledge_base.completed} +
+
+ 失败 + {stats.tasks.knowledge_base.failed} +
+
+
+
+
+ )} + +
+ {/* 在线用户列表 */} +
+
+

在线用户 ({onlineUsers.length})

+
+
+ {onlineUsers.length === 0 ? ( +
暂无在线用户
+ ) : ( + + + + + + + + + + + + + {onlineUsers.map(u => ( + + + + + + + + + ))} + +
ID用户名姓名会话数剩余时间操作
{u.user_id}{u.username}{u.caption}{u.token_count}{u.ttl_hours}h + +
+ )} +
+
+ + {/* 任务监控 */} +
+
+

任务监控

+
+ + +
+
+
+ {tasks.length === 0 ? ( +
暂无任务
+ ) : ( + + + + + + + + + + + + + + {tasks.map(task => ( + + + + + + + + + + ))} + +
任务ID类型关联对象创建者状态进度创建时间
{task.task_id}{getTaskTypeText(task.task_type)}{task.meeting_title || '-'}{task.creator_name || '-'} + + {getStatusText(task.status)} + + + {task.progress !== null ? `${task.progress}%` : '-'} + {new Date(task.created_at).toLocaleString()}
+ )} +
+
+
+
+
+ ); +}; + +export default AdminDashboard; diff --git a/src/pages/AdminManagement.jsx b/src/pages/AdminManagement.jsx index 00742f7..b0f2085 100644 --- a/src/pages/AdminManagement.jsx +++ b/src/pages/AdminManagement.jsx @@ -1,10 +1,11 @@ import React from 'react'; -import { MessageSquare, Settings, Users, Smartphone } from 'lucide-react'; +import { MessageSquare, Settings, Users, Smartphone, Shield } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { Tabs } from 'antd'; import UserManagement from './admin/UserManagement'; import SystemConfiguration from './admin/SystemConfiguration'; import ClientManagement from './ClientManagement'; +import PermissionManagement from './admin/PermissionManagement'; import './AdminManagement.css'; const { TabPane } = Tabs; @@ -36,6 +37,12 @@ const AdminManagement = () => { > + 权限管理} + key="permissionManagement" + > + + 系统配置} key="systemConfiguration" diff --git a/src/pages/ClientManagement.jsx b/src/pages/ClientManagement.jsx index 600615b..d4e2027 100644 --- a/src/pages/ClientManagement.jsx +++ b/src/pages/ClientManagement.jsx @@ -15,7 +15,8 @@ import { Hash, Link, FileText, - HardDrive + HardDrive, + Cpu } from 'lucide-react'; import apiClient from '../utils/apiClient'; import { buildApiUrl, API_ENDPOINTS } from '../config/api'; @@ -59,6 +60,10 @@ const ClientManagement = ({ user }) => { { value: 'mac_intel', label: 'Mac (Intel)', icon: }, { value: 'mac_m', label: 'Mac (M系列)', icon: }, { value: 'linux', label: 'Linux', icon: } + ], + terminal: [ + { value: 'android', label: 'Android终端', icon: }, + { value: 'mcu', label: '单片机', icon: } ] }; @@ -264,7 +269,7 @@ const ClientManagement = ({ user }) => { }; const getPlatformLabel = (platformName) => { - const allOptions = [...platformOptions.mobile, ...platformOptions.desktop]; + const allOptions = [...platformOptions.mobile, ...platformOptions.desktop, ...platformOptions.terminal]; const option = allOptions.find(opt => opt.value === platformName); return option ? option.label : platformName; }; @@ -299,7 +304,8 @@ const ClientManagement = ({ user }) => { const groupedClients = { mobile: filteredClients.filter(c => c.platform_type === 'mobile'), - desktop: filteredClients.filter(c => c.platform_type === 'desktop') + desktop: filteredClients.filter(c => c.platform_type === 'desktop'), + terminal: filteredClients.filter(c => c.platform_type === 'terminal') }; if (loading) { @@ -353,21 +359,34 @@ const ClientManagement = ({ user }) => { 桌面端 +
- {['mobile', 'desktop'].map(type => { + {['mobile', 'desktop', 'terminal'].map(type => { const typeClients = groupedClients[type]; if (typeClients.length === 0 && filterPlatformType && filterPlatformType !== type) { return null; } + const typeConfig = { + mobile: { icon: , label: '移动端' }, + desktop: { icon: , label: '桌面端' }, + terminal: { icon: , label: '专用终端' } + }; + return (

- {type === 'mobile' ? : } - {type === 'mobile' ? '移动端' : '桌面端'} + {typeConfig[type].icon} + {typeConfig[type].label} ({typeClients.length})

@@ -484,6 +503,7 @@ const ClientManagement = ({ user }) => { > +
diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx index 37443d4..67a0a24 100644 --- a/src/pages/Dashboard.jsx +++ b/src/pages/Dashboard.jsx @@ -12,6 +12,7 @@ import PageLoading from '../components/PageLoading'; import ScrollToTop from '../components/ScrollToTop'; import Dropdown from '../components/Dropdown'; import meetingCacheService from '../services/meetingCacheService'; +import menuService from '../services/menuService'; import './Dashboard.css'; const Dashboard = ({ user, onLogout }) => { @@ -39,10 +40,14 @@ const Dashboard = ({ user, onLogout }) => { const [voiceprintLoading, setVoiceprintLoading] = useState(true); const [showDeleteVoiceprintDialog, setShowDeleteVoiceprintDialog] = useState(false); + // 菜单权限相关状态 + const [userMenus, setUserMenus] = useState([]); + useEffect(() => { fetchUserData(); fetchMeetingsStats(); fetchVoiceprintData(); + fetchUserMenus(); // 开发环境下,在控制台添加缓存调试命令 if (process.env.NODE_ENV === 'development') { @@ -96,6 +101,76 @@ const Dashboard = ({ user, onLogout }) => { } }; + const fetchUserMenus = async () => { + try { + console.log('[Dashboard] 开始获取用户菜单...'); + const response = await menuService.getUserMenus(); + console.log('[Dashboard] 菜单API响应:', response); + + if (response.code === '200') { + const menus = response.data.menus || []; + console.log('[Dashboard] 用户菜单获取成功,菜单数量:', menus.length, '菜单内容:', menus); + setUserMenus(menus); + } else { + console.error('[Dashboard] 获取用户菜单失败:', response.message); + // 使用默认菜单作为fallback + setUserMenus(getDefaultMenus()); + } + } catch (err) { + console.error('[Dashboard] 获取用户菜单异常:', err); + // 使用默认菜单作为fallback + setUserMenus(getDefaultMenus()); + } + }; + + // 获取默认菜单(fallback) + const getDefaultMenus = () => { + const defaultMenus = [ + { menu_code: 'change_password', menu_name: '修改密码', menu_type: 'action', sort_order: 1 }, + { menu_code: 'prompt_management', menu_name: '提示词仓库', menu_type: 'link', menu_url: '/prompt-management', sort_order: 2 }, + { menu_code: 'logout', menu_name: '退出登录', menu_type: 'action', sort_order: 99 } + ]; + + // 如果是管理员,添加平台管理菜单 + if (user.role_id === 1) { + defaultMenus.splice(2, 0, { + menu_code: 'platform_admin', + menu_name: '平台管理', + menu_type: 'link', + menu_url: '/admin/management', + sort_order: 3 + }); + } + + console.log('[Dashboard] 使用默认菜单:', defaultMenus); + return defaultMenus; + }; + + // 将菜单code映射到图标和行为 + const getMenuItemConfig = (menu) => { + const iconMap = { + 'change_password': , + 'prompt_management': , + 'platform_admin': , + 'logout': + }; + + const actionMap = { + 'change_password': () => setShowChangePasswordModal(true), + 'prompt_management': () => window.location.href = '/prompt-management', + 'platform_admin': () => window.location.href = '/admin/management', + 'logout': onLogout + }; + + return { + icon: iconMap[menu.menu_code] || null, + label: menu.menu_name, + onClick: menu.menu_type === 'link' && menu.menu_url + ? () => window.location.href = menu.menu_url + : actionMap[menu.menu_code] || (() => {}) + }; + }; + // 过滤会议 useEffect(() => { fetchMeetings(1, false); @@ -359,28 +434,7 @@ const Dashboard = ({ user, onLogout }) => {
} - items={[ - { - icon: , - label: '修改密码', - onClick: () => setShowChangePasswordModal(true) - }, - { - icon: , - label: '提示词仓库', - onClick: () => window.location.href = '/prompt-management' - }, - ...(user.role_id === 1 ? [{ - icon: , - label: '平台管理', - onClick: () => window.location.href = '/admin/management' - }] : []), - { - icon: , - label: '退出登录', - onClick: onLogout - } - ]} + items={userMenus.map(menu => getMenuItemConfig(menu))} align="right" className="user-menu-dropdown" /> diff --git a/src/pages/KnowledgeBasePage.css b/src/pages/KnowledgeBasePage.css index 6b85d4f..2bf68c8 100644 --- a/src/pages/KnowledgeBasePage.css +++ b/src/pages/KnowledgeBasePage.css @@ -661,6 +661,40 @@ color: #1e293b; } +.field-hint { + font-size: 0.875rem; + color: #64748b; + margin-bottom: 0.5rem; + line-height: 1.5; +} + +.template-select { + width: 100%; + padding: 0.75rem; + border: 1px solid #e2e8f0; + border-radius: 8px; + font-family: inherit; + font-size: 0.95rem; + background-color: white; + color: #1e293b; + cursor: pointer; + transition: all 0.2s ease; +} + +.template-select:hover { + border-color: #cbd5e1; +} + +.template-select:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.template-select option { + padding: 0.5rem; +} + .kb-prompt-input { width: 100%; padding: 0.75rem; diff --git a/src/pages/KnowledgeBasePage.jsx b/src/pages/KnowledgeBasePage.jsx index 56d2433..0612076 100644 --- a/src/pages/KnowledgeBasePage.jsx +++ b/src/pages/KnowledgeBasePage.jsx @@ -40,6 +40,8 @@ const KnowledgeBasePage = ({ user }) => { const [createStep, setCreateStep] = useState(1); // 1: 选择会议, 2: 输入提示词 const [meetingsPagination, setMeetingsPagination] = useState({ page: 1, total: 0, has_more: false }); const [loadingMeetings, setLoadingMeetings] = useState(false); + const [availablePrompts, setAvailablePrompts] = useState([]); // 可用的提示词模版列表 + const [selectedPromptId, setSelectedPromptId] = useState(null); // 选中的提示词模版ID // Toast helper functions const showToast = (message, type = 'info') => { @@ -183,6 +185,24 @@ const KnowledgeBasePage = ({ user }) => { } }; + // 获取知识库任务的启用提示词模版列表 + const fetchAvailablePrompts = async () => { + try { + const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.ACTIVE('KNOWLEDGE_TASK'))); + const promptsList = response.data.prompts || []; + setAvailablePrompts(promptsList); + + // 自动选中默认模版 + const defaultPrompt = promptsList.find(p => p.is_default); + if (defaultPrompt) { + setSelectedPromptId(defaultPrompt.id); + } + } catch (error) { + console.error("Error fetching available prompts:", error); + setAvailablePrompts([]); + } + }; + const loadKbDetail = async (kbId) => { try { const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.DETAIL(kbId))); @@ -207,7 +227,8 @@ const KnowledgeBasePage = ({ user }) => { const response = await apiClient.post(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.CREATE), { user_prompt: userPrompt, source_meeting_ids: selectedMeetings.join(','), - is_shared: false + is_shared: false, + prompt_id: selectedPromptId // 传递选中的模版ID }); setTaskId(response.data.task_id); } catch (error) { @@ -247,7 +268,10 @@ const KnowledgeBasePage = ({ user }) => { setUserPrompt(''); setSearchQuery(''); setSelectedTags([]); + setSelectedPromptId(null); setShowCreateForm(true); + // 获取可用的提示词模版 + fetchAvailablePrompts(); }; const handleCloseCreateModal = () => { @@ -800,8 +824,27 @@ const KnowledgeBasePage = ({ user }) => { {selectedMeetings.length} 个 + + {/* 模版选择 */} + {availablePrompts.length > 0 && ( +
+ + +
+ )} +
- +

您可以添加额外的要求来定制知识库生成内容,例如重点关注某个主题、提取特定信息等。如不填写,系统将使用默认提示词。