diff --git a/.DS_Store b/.DS_Store index 53fdf9b..3f5d312 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/dist.zip b/dist.zip index d5163d7..87e8622 100644 Binary files a/dist.zip and b/dist.zip differ diff --git a/public/.DS_Store b/public/.DS_Store index c176d00..63e21d2 100644 Binary files a/public/.DS_Store and b/public/.DS_Store differ diff --git a/src/config/api.js b/src/config/api.js index becdf26..71c83e7 100644 --- a/src/config/api.js +++ b/src/config/api.js @@ -41,7 +41,14 @@ const API_CONFIG = { USER_STATS: '/api/admin/user-stats', KICK_USER: (userId) => `/api/admin/kick-user/${userId}`, TASKS_MONITOR: '/api/admin/tasks/monitor', - SYSTEM_RESOURCES: '/api/admin/system/resources' + SYSTEM_RESOURCES: '/api/admin/system/resources', + HOT_WORDS: { + LIST: '/api/admin/hot-words', + CREATE: '/api/admin/hot-words', + UPDATE: (id) => `/api/admin/hot-words/${id}`, + DELETE: (id) => `/api/admin/hot-words/${id}`, + SYNC: '/api/admin/hot-words/sync' + } }, TAGS: { LIST: '/api/tags' diff --git a/src/pages/AdminManagement.jsx b/src/pages/AdminManagement.jsx index 44465d2..3de1130 100644 --- a/src/pages/AdminManagement.jsx +++ b/src/pages/AdminManagement.jsx @@ -1,55 +1,56 @@ import React from 'react'; -import { Settings, Users, Smartphone, Shield, BookText } from 'lucide-react'; +import { Settings, Users, Smartphone, Shield, BookText, Type } from 'lucide-react'; import { Tabs } from 'antd'; import UserManagement from './admin/UserManagement'; -import SystemConfiguration from './admin/SystemConfiguration'; import ClientManagement from './ClientManagement'; import PermissionManagement from './admin/PermissionManagement'; import DictManagement from './admin/DictManagement'; +import HotWordManagement from './admin/HotWordManagement'; import Breadcrumb from '../components/Breadcrumb'; import './AdminManagement.css'; const { TabPane } = Tabs; const AdminManagement = () => { + const items = [ + { + key: 'userManagement', + label: 用户管理, + children: , + }, + { + key: 'permissionManagement', + label: 权限管理, + children: , + }, + { + key: 'dictManagement', + label: 字典管理, + children: , + }, + { + key: 'hotWordManagement', + label: 热词管理, + children: , + }, + { + key: 'clientManagement', + label: 客户端管理, + children: , + }, + ]; + return (
- - 用户管理} - key="userManagement" - > - - - 权限管理} - key="permissionManagement" - > - - - 字典管理} - key="dictManagement" - > - - - 系统配置} - key="systemConfiguration" - > - - - 客户端管理} - key="clientManagement" - > - - - +
diff --git a/src/pages/CreateMeeting.jsx b/src/pages/CreateMeeting.jsx index 4247ea4..5f5232c 100644 --- a/src/pages/CreateMeeting.jsx +++ b/src/pages/CreateMeeting.jsx @@ -40,8 +40,8 @@ const CreateMeeting = ({ user }) => { const fetchUsers = async () => { try { - // 获取所有用户,设置较大的size参数 - const response = await apiClient.get(buildApiUrl(`${API_ENDPOINTS.USERS.LIST}?page=1&size=1000`)); + // 获取所有普通用户(role_id=2),设置较大的size参数 + const response = await apiClient.get(buildApiUrl(`${API_ENDPOINTS.USERS.LIST}?page=1&size=1000&role_id=2`)); setAvailableUsers(response.data.users.filter(u => u.user_id !== user.user_id)); } catch (err) { console.error('Error fetching users:', err); diff --git a/src/pages/EditMeeting.jsx b/src/pages/EditMeeting.jsx index 88cb928..618c7d9 100644 --- a/src/pages/EditMeeting.jsx +++ b/src/pages/EditMeeting.jsx @@ -73,8 +73,8 @@ const EditMeeting = ({ user }) => { const fetchUsers = async () => { try { - // 获取所有用户,设置较大的size参数 - const response = await apiClient.get(buildApiUrl(`${API_ENDPOINTS.USERS.LIST}?page=1&size=1000`)); + // 获取所有普通用户(role_id=2),设置较大的size参数 + const response = await apiClient.get(buildApiUrl(`${API_ENDPOINTS.USERS.LIST}?page=1&size=1000&role_id=2`)); setAvailableUsers(response.data.users.filter(u => u.user_id !== user.user_id)); } catch (err) { console.error('Error fetching users:', err); diff --git a/src/pages/admin/DictManagement.jsx b/src/pages/admin/DictManagement.jsx index 66f11e2..d6bd4af 100644 --- a/src/pages/admin/DictManagement.jsx +++ b/src/pages/admin/DictManagement.jsx @@ -1,8 +1,9 @@ import React, { useState, useEffect } from 'react'; -import { Tree, Button, Form, Input, InputNumber, Select, Switch, Space, message, Card, Empty, Popconfirm } from 'antd'; +import { Tree, Button, Form, Input, InputNumber, Select, Switch, Space, Card, Empty, Popconfirm } from 'antd'; import { BookText, Plus, Save, Trash2, FolderTree, FileText, X } from 'lucide-react'; import apiClient from '../../utils/apiClient'; import { buildApiUrl, API_ENDPOINTS } from '../../config/api'; +import Toast from '../../components/Toast'; import './DictManagement.css'; const { Option } = Select; @@ -16,8 +17,19 @@ const DictManagement = () => { const [treeData, setTreeData] = useState([]); // 树形结构数据 const [selectedNode, setSelectedNode] = useState(null); // 当前选中的节点 const [isEditing, setIsEditing] = useState(false); // 是否处于编辑状态 + const [toasts, setToasts] = useState([]); const [form] = Form.useForm(); + // Toast helper functions + const showToast = (message, type = 'info') => { + const id = Date.now(); + setToasts(prev => [...prev, { id, message, type }]); + }; + + const removeToast = (id) => { + setToasts(prev => prev.filter(toast => toast.id !== id)); + }; + // 获取所有字典类型 const fetchDictTypes = async () => { try { @@ -26,7 +38,7 @@ const DictManagement = () => { setDictTypes(response.data.types); } } catch (error) { - message.error('获取字典类型失败'); + showToast('获取字典类型失败', 'error'); } }; @@ -48,7 +60,7 @@ const DictManagement = () => { form.resetFields(); } } catch (error) { - message.error('获取字典数据失败'); + showToast('获取字典数据失败', 'error'); } finally { setLoading(false); } @@ -126,7 +138,7 @@ const DictManagement = () => { try { values.extension_attr = JSON.parse(values.extension_attr); } catch (e) { - message.error('扩展属性 JSON 格式错误'); + showToast('扩展属性 JSON 格式错误:无法解析JSON', 'error'); return; } } @@ -136,25 +148,39 @@ const DictManagement = () => { if (selectedNode) { // 更新 - await apiClient.put( + const response = await apiClient.put( buildApiUrl(API_ENDPOINTS.DICT_DATA.UPDATE(selectedNode.id)), values ); - message.success('更新成功'); + if (response.code === '200') { + showToast('更新成功', 'success'); + // 重新加载数据 + fetchDictData(selectedDictType); + } else { + showToast(response.message || '更新失败', 'error'); + } } else { // 新增 - await apiClient.post(buildApiUrl(API_ENDPOINTS.DICT_DATA.CREATE), values); - message.success('创建成功'); + const response = await apiClient.post( + buildApiUrl(API_ENDPOINTS.DICT_DATA.CREATE), + values + ); + if (response.code === '200') { + showToast('创建成功', 'success'); + // 重新加载数据 + fetchDictData(selectedDictType); + } else { + showToast(response.message || '创建失败', 'error'); + } } - - // 重新加载数据 - fetchDictData(selectedDictType); } catch (error) { if (error.errorFields) { // 表单验证错误 return; } - message.error(selectedNode ? '更新失败' : '创建失败'); + // 显示后端返回的具体错误信息 + const errorMsg = error.response?.data?.message || error.message || '操作失败'; + showToast(errorMsg, 'error'); } }; @@ -164,7 +190,7 @@ const DictManagement = () => { try { await apiClient.delete(buildApiUrl(API_ENDPOINTS.DICT_DATA.DELETE(selectedNode.id))); - message.success('删除成功'); + showToast('删除成功', 'success'); // 重新加载数据 setSelectedNode(null); @@ -172,7 +198,7 @@ const DictManagement = () => { form.resetFields(); fetchDictData(selectedDictType); } catch (error) { - message.error('删除失败:' + (error.message || '未知错误')); + showToast('删除失败:' + (error.message || '未知错误'), 'error'); } }; @@ -400,6 +426,16 @@ const DictManagement = () => { + + {/* Toast notifications */} + {toasts.map(toast => ( + removeToast(toast.id)} + /> + ))} ); }; diff --git a/src/pages/admin/HotWordManagement.css b/src/pages/admin/HotWordManagement.css new file mode 100644 index 0000000..8c91eda --- /dev/null +++ b/src/pages/admin/HotWordManagement.css @@ -0,0 +1,38 @@ +.hot-word-management { + padding: 16px 0; +} + +.hot-word-management .status-bar { + background-color: #e6f7ff; + border: 1px solid #91d5ff; + padding: 10px 16px; + border-radius: 4px; + margin-bottom: 16px; + font-size: 14px; + color: #003a8c; +} + +.hot-word-management .table-header { + margin-bottom: 24px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.hot-word-management .helper-text { + font-size: 13px; + color: #8c8c8c; +} + +.animate-spin { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/src/pages/admin/HotWordManagement.jsx b/src/pages/admin/HotWordManagement.jsx new file mode 100644 index 0000000..06092ad --- /dev/null +++ b/src/pages/admin/HotWordManagement.jsx @@ -0,0 +1,328 @@ +import React, { useState, useEffect } from 'react'; +import { Table, Button, Modal, Form, Input, InputNumber, Switch, Space, Popconfirm, Tag } from 'antd'; +import { Plus, RefreshCw, Trash2, Edit } from 'lucide-react'; +import apiClient from '../../utils/apiClient'; +import { buildApiUrl, API_ENDPOINTS } from '../../config/api'; +import Toast from '../../components/Toast'; +import './HotWordManagement.css'; + +const HotWordManagement = () => { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [syncLoading, setSyncLoading] = useState(false); + const [modalVisible, setModalVisible] = useState(false); + const [editingItem, setEditingItem] = useState(null); + const [form] = Form.useForm(); + const [vocabInfo, setVocabInfo] = useState(null); + const [toasts, setToasts] = useState([]); + + // Toast helper functions + const showToast = (message, type = 'info') => { + const id = Date.now(); + setToasts(prev => [...prev, { id, message, type }]); + }; + + const removeToast = (id) => { + setToasts(prev => prev.filter(toast => toast.id !== id)); + }; + + const fetchHotWords = async () => { + setLoading(true); + try { + const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.LIST)); + console.log('Fetch hot words response:', response); + // apiClient unwrap the code 200 responses, so response IS {code, message, data} + if (response.code === "200") { + setData(response.data); + } else { + showToast(response.message, 'error'); + } + } catch (error) { + console.error('Fetch hot words error:', error); + showToast(error.message || '获取热词列表失败', 'error'); + } finally { + setLoading(false); + } + }; + + const fetchVocabInfo = async () => { + try { + const response = await apiClient.get( + buildApiUrl(API_ENDPOINTS.DICT_DATA.BY_CODE('system_config', 'asr_vocabulary_id')) + ); + if (response.code === "200") { + const data = response.data; + // extension_attr is already parsed by backend + const vocabId = data.extension_attr?.value || ''; + setVocabInfo({ ...data, vocabId }); + } + } catch (error) { + // Ignore error if config not found + } + }; + + useEffect(() => { + fetchHotWords(); + fetchVocabInfo(); + }, []); + + useEffect(() => { + if (modalVisible) { + form.resetFields(); + if (editingItem) { + form.setFieldsValue(editingItem); + } else { + form.setFieldsValue({ weight: 4, lang: 'zh', status: 1 }); + } + } + }, [modalVisible, editingItem, form]); + + const handleAdd = () => { + setEditingItem(null); + setModalVisible(true); + }; + + const handleEdit = (record) => { + setEditingItem(record); + setModalVisible(true); + }; + + const handleDelete = async (id) => { + try { + const response = await apiClient.delete(buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.DELETE(id))); + if (response.code === "200") { + showToast('删除成功', 'success'); + fetchHotWords(); + } else { + showToast(response.message, 'error'); + } + } catch (error) { + showToast('删除失败', 'error'); + } + }; + + const handleSync = async () => { + console.log('Starting sync...'); + setSyncLoading(true); + try { + const response = await apiClient.post(buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.SYNC)); + console.log('Sync response:', response); + if (response.code === "200") { + showToast('同步到阿里云成功', 'success'); + fetchVocabInfo(); + } else { + showToast(response.message, 'error'); + } + } catch (error) { + console.error('Sync error:', error); + showToast('同步失败', 'error'); + } finally { + setSyncLoading(false); + } + }; + + const handleModalOk = async () => { + try { + const values = await form.validateFields(); + if (editingItem) { + const response = await apiClient.put( + buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.UPDATE(editingItem.id)), + values + ); + if (response.code === "200") { + showToast('更新成功', 'success'); + setModalVisible(false); + fetchHotWords(); + } else { + showToast(response.message, 'error'); + } + } else { + const response = await apiClient.post( + buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.CREATE), + values + ); + if (response.code === "200") { + showToast('创建成功', 'success'); + setModalVisible(false); + fetchHotWords(); + } else { + showToast(response.message, 'error'); + } + } + } catch (error) { + console.error('Submit hot word error:', error); + } + }; + + const columns = [ + { + title: '热词内容', + dataIndex: 'text', + key: 'text', + }, + { + title: '权重', + dataIndex: 'weight', + key: 'weight', + render: (weight) => {weight} + }, + { + title: '语言', + dataIndex: 'lang', + key: 'lang', + render: (lang) => {lang === 'zh' ? '中文' : '英文'} + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + render: (status, record) => ( + { + try { + await apiClient.put( + buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.UPDATE(record.id)), + { status: checked ? 1 : 0 } + ); + fetchHotWords(); + } catch (error) { + showToast('更新状态失败', 'error'); + } + }} + /> + ) + }, + { + title: '更新时间', + dataIndex: 'update_time', + key: 'update_time', + render: (time) => new Date(time).toLocaleString() + }, + { + title: '操作', + key: 'action', + render: (_, record) => ( + + + + +
+ 提示:修改热词后需点击“同步到阿里云”才能在转录中生效。权重越高识别概率越大。 +
+ + + + + setModalVisible(false)} + forceRender + > +
+
+ + + + + + + + + + ({ checked: value === 1 })} + getValueFromEvent={(checked) => (checked ? 1 : 0)} + > + + + +
+
+ + {/* Toast notifications */} + {toasts.map(toast => ( + removeToast(toast.id)} + /> + ))} + + ); + }; + export default HotWordManagement; diff --git a/src/pages/admin/PermissionManagement.jsx b/src/pages/admin/PermissionManagement.jsx index 916066a..6f67a61 100644 --- a/src/pages/admin/PermissionManagement.jsx +++ b/src/pages/admin/PermissionManagement.jsx @@ -1,7 +1,8 @@ import React, { useState, useEffect } from 'react'; -import { Table, Button, Checkbox, message, Spin, Card } from 'antd'; +import { Table, Button, Checkbox, Spin, Card } from 'antd'; import { Shield, Save } from 'lucide-react'; import axios from 'axios'; +import Toast from '../../components/Toast'; import './PermissionManagement.css'; const PermissionManagement = () => { @@ -10,6 +11,17 @@ const PermissionManagement = () => { const [permissions, setPermissions] = useState({}); // {roleId: [menuId1, menuId2, ...]} const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); + const [toasts, setToasts] = useState([]); + + // Toast helper functions + const showToast = (message, type = 'info') => { + const id = Date.now(); + setToasts(prev => [...prev, { id, message, type }]); + }; + + const removeToast = (id) => { + setToasts(prev => prev.filter(toast => toast.id !== id)); + }; // 获取所有数据 const fetchData = async () => { @@ -18,7 +30,7 @@ const PermissionManagement = () => { // 获取token - 与apiClient保持一致 const savedUser = localStorage.getItem('iMeetingUser'); if (!savedUser) { - message.error('未找到登录信息,请重新登录'); + showToast('未找到登录信息,请重新登录', 'error'); return; } const user = JSON.parse(savedUser); @@ -56,10 +68,10 @@ const PermissionManagement = () => { setPermissions(permsMap); } else { - message.error('获取数据失败: ' + (rolesRes.data.message || menusRes.data.message)); + showToast('获取数据失败: ' + (rolesRes.data.message || menusRes.data.message), 'error'); } } catch (error) { - message.error('获取数据失败: ' + (error.response?.data?.message || error.message)); + showToast('获取数据失败: ' + (error.response?.data?.message || error.message), 'error'); } finally { setLoading(false); } @@ -87,7 +99,7 @@ const PermissionManagement = () => { // 获取token - 与apiClient保持一致 const savedUser = localStorage.getItem('iMeetingUser'); if (!savedUser) { - message.error('未找到登录信息,请重新登录'); + showToast('未找到登录信息,请重新登录', 'error'); return; } const user = JSON.parse(savedUser); @@ -102,11 +114,11 @@ const PermissionManagement = () => { ); } - message.success('权限保存成功'); + showToast('权限保存成功', 'success'); fetchData(); // 重新加载数据 } catch (error) { console.error('Error saving permissions:', error); - message.error('保存权限失败'); + showToast('保存权限失败', 'error'); } finally { setSaving(false); } @@ -193,6 +205,16 @@ const PermissionManagement = () => { /> + + {/* Toast notifications */} + {toasts.map(toast => ( + removeToast(toast.id)} + /> + ))} ); }; diff --git a/src/pages/admin/SystemConfiguration.jsx b/src/pages/admin/SystemConfiguration.jsx deleted file mode 100644 index b85f89a..0000000 --- a/src/pages/admin/SystemConfiguration.jsx +++ /dev/null @@ -1,299 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Settings, Brain, Shield, Save, RotateCcw, CheckCircle, AlertCircle, Loader } from 'lucide-react'; -import apiClient from '../../utils/apiClient'; -import { buildApiUrl, API_ENDPOINTS } from '../../config/api'; -import PageLoading from '../../components/PageLoading'; -import './SystemConfiguration.css'; - -const SystemConfiguration = () => { - const [configs, setConfigs] = useState({ - model_name: '', - template_text: '', - DEFAULT_RESET_PASSWORD: '', - MAX_FILE_SIZE: 0, - TIMELINE_PAGESIZE: 0 - }); - - // 用于存储输入框的显示值(MB单位) - const [displayValues, setDisplayValues] = useState({ - MAX_FILE_SIZE: 0, - TIMELINE_PAGESIZE: 0 - }); - - const [originalConfigs, setOriginalConfigs] = useState({}); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - const [message, setMessage] = useState({ type: '', text: '' }); - - // 工具函数:字节转MB - const bytesToMB = (bytes) => { - return Math.round(bytes / (1024 * 1024)); - }; - - // 工具函数:MB转字节 - const mbToBytes = (mb) => { - return mb * 1024 * 1024; - }; - - // 加载配置数据 - useEffect(() => { - fetchConfigs(); - }, []); - - const fetchConfigs = async () => { - try { - setLoading(true); - const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.SYSTEM_CONFIG)); - const configData = response.data; - setConfigs(configData); - setOriginalConfigs(configData); - // 初始化显示值 - setDisplayValues({ - MAX_FILE_SIZE: bytesToMB(configData.MAX_FILE_SIZE), - TIMELINE_PAGESIZE: configData.TIMELINE_PAGESIZE - }); - setMessage({ type: '', text: '' }); - } catch (err) { - console.error('Failed to fetch configurations:', err); - setMessage({ - type: 'error', - text: '加载配置失败:' + (err.response?.data?.message || err.message) - }); - } finally { - setLoading(false); - } - }; - - const handleInputChange = (key, value) => { - if (key === 'MAX_FILE_SIZE') { - // 直接更新显示值,不进行转换 - setDisplayValues(prev => ({ - ...prev, - MAX_FILE_SIZE: value - })); - // 转换为字节存储 - const numValue = parseInt(value) || 0; - setConfigs(prev => ({ - ...prev, - [key]: mbToBytes(numValue) - })); - } else if (key === 'TIMELINE_PAGESIZE') { - // 直接更新显示值 - setDisplayValues(prev => ({ - ...prev, - TIMELINE_PAGESIZE: value - })); - // 存储数字 - const numValue = parseInt(value) || 0; - setConfigs(prev => ({ - ...prev, - [key]: numValue - })); - } else { - setConfigs(prev => ({ - ...prev, - [key]: value - })); - } - }; - - const handleSave = async () => { - try { - setSaving(true); - setMessage({ type: 'loading', text: '保存配置中...' }); - - const saveResponse = await apiClient.put(buildApiUrl(API_ENDPOINTS.ADMIN.SYSTEM_CONFIG), configs); - - setOriginalConfigs(configs); - setMessage({ type: 'success', text: saveResponse.message }); - - // 3秒后清除成功消息 - setTimeout(() => { - setMessage({ type: '', text: '' }); - }, 3000); - } catch (err) { - console.error('Failed to save configurations:', err); - setMessage({ - type: 'error', - text: '保存配置失败:' + (err.response?.data?.message || err.message) - }); - } finally { - setSaving(false); - } - }; - - const handleReset = () => { - setConfigs(originalConfigs); - setDisplayValues({ - MAX_FILE_SIZE: bytesToMB(originalConfigs.MAX_FILE_SIZE), - TIMELINE_PAGESIZE: originalConfigs.TIMELINE_PAGESIZE - }); - setMessage({ type: '', text: '' }); - }; - - const hasChanges = JSON.stringify(configs) !== JSON.stringify(originalConfigs); - - if (loading) { - return ; - } - - return ( -
-
-

- - 系统配置 -

-
- - {message.text && ( -
- {message.type === 'success' && } - {message.type === 'error' && } - {message.type === 'loading' && } - {message.text} -
- )} - -
- {/* 模型配置块 */} -
-
-

- - 模型配置 -

-

配置AI模型相关参数

-
-
-
- - handleInputChange('model_name', e.target.value)} - placeholder="请输入模型名称" - /> -
- 指定要使用的AI模型名称,例如:gpt-4, claude-3-sonnet等 -
-
- -
- -