修正了处理进度展示
parent
b315eaaf3b
commit
9d85301a50
Binary file not shown.
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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: <span><Users size={16} /> 用户管理</span>,
|
||||
children: <UserManagement />,
|
||||
},
|
||||
{
|
||||
key: 'permissionManagement',
|
||||
label: <span><Shield size={16} /> 权限管理</span>,
|
||||
children: <PermissionManagement />,
|
||||
},
|
||||
{
|
||||
key: 'dictManagement',
|
||||
label: <span><BookText size={16} /> 字典管理</span>,
|
||||
children: <DictManagement />,
|
||||
},
|
||||
{
|
||||
key: 'hotWordManagement',
|
||||
label: <span><Type size={16} /> 热词管理</span>,
|
||||
children: <HotWordManagement />,
|
||||
},
|
||||
{
|
||||
key: 'clientManagement',
|
||||
label: <span><Smartphone size={16} /> 客户端管理</span>,
|
||||
children: <ClientManagement />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="admin-management-page">
|
||||
<Breadcrumb currentPage="平台管理" icon={Shield} />
|
||||
|
||||
<div className="admin-content">
|
||||
<div className="admin-wrapper">
|
||||
<Tabs defaultActiveKey="userManagement" className="admin-tabs">
|
||||
<TabPane
|
||||
tab={<span><Users size={16} /> 用户管理</span>}
|
||||
key="userManagement"
|
||||
>
|
||||
<UserManagement />
|
||||
</TabPane>
|
||||
<TabPane
|
||||
tab={<span><Shield size={16} /> 权限管理</span>}
|
||||
key="permissionManagement"
|
||||
>
|
||||
<PermissionManagement />
|
||||
</TabPane>
|
||||
<TabPane
|
||||
tab={<span><BookText size={16} /> 字典管理</span>}
|
||||
key="dictManagement"
|
||||
>
|
||||
<DictManagement />
|
||||
</TabPane>
|
||||
<TabPane
|
||||
tab={<span><Settings size={16} /> 系统配置</span>}
|
||||
key="systemConfiguration"
|
||||
>
|
||||
<SystemConfiguration />
|
||||
</TabPane>
|
||||
<TabPane
|
||||
tab={<span><Smartphone size={16} /> 客户端管理</span>}
|
||||
key="clientManagement"
|
||||
>
|
||||
<ClientManagement />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
<Tabs
|
||||
defaultActiveKey="userManagement"
|
||||
className="admin-tabs"
|
||||
items={items}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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('更新成功');
|
||||
} else {
|
||||
// 新增
|
||||
await apiClient.post(buildApiUrl(API_ENDPOINTS.DICT_DATA.CREATE), values);
|
||||
message.success('创建成功');
|
||||
}
|
||||
|
||||
if (response.code === '200') {
|
||||
showToast('更新成功', 'success');
|
||||
// 重新加载数据
|
||||
fetchDictData(selectedDictType);
|
||||
} else {
|
||||
showToast(response.message || '更新失败', 'error');
|
||||
}
|
||||
} else {
|
||||
// 新增
|
||||
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');
|
||||
}
|
||||
}
|
||||
} 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 = () => {
|
|||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toast notifications */}
|
||||
{toasts.map(toast => (
|
||||
<Toast
|
||||
key={toast.id}
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
onClose={() => removeToast(toast.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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) => <Tag color="blue">{weight}</Tag>
|
||||
},
|
||||
{
|
||||
title: '语言',
|
||||
dataIndex: 'lang',
|
||||
key: 'lang',
|
||||
render: (lang) => <Tag>{lang === 'zh' ? '中文' : '英文'}</Tag>
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status, record) => (
|
||||
<Switch
|
||||
checked={status === 1}
|
||||
onChange={async (checked) => {
|
||||
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) => (
|
||||
<Space size="middle">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<Edit size={16} />}
|
||||
onClick={() => handleEdit(record)}
|
||||
/>
|
||||
<Popconfirm
|
||||
title="确定要删除这个热词吗?"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<Trash2 size={16} />}
|
||||
/>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="hot-word-management">
|
||||
{vocabInfo && (
|
||||
<div className="status-bar">
|
||||
<Space size="large">
|
||||
<span>当前生效热词表 ID: <b>{vocabInfo.vocabId}</b></span>
|
||||
<span>上次同步时间: {new Date(vocabInfo.update_time).toLocaleString()}</span>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="table-header">
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<Plus size={16} />}
|
||||
onClick={handleAdd}
|
||||
>
|
||||
添加热词
|
||||
</Button>
|
||||
<Button
|
||||
icon={<RefreshCw size={16} className={syncLoading ? 'animate-spin' : ''} />}
|
||||
onClick={handleSync}
|
||||
loading={syncLoading}
|
||||
>
|
||||
同步到阿里云
|
||||
</Button>
|
||||
</Space>
|
||||
<div className="helper-text">
|
||||
提示:修改热词后需点击“同步到阿里云”才能在转录中生效。权重越高识别概率越大。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={editingItem ? "编辑热词" : "添加热词"}
|
||||
open={modalVisible}
|
||||
onOk={handleModalOk}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
forceRender
|
||||
>
|
||||
<div style={{ paddingTop: '10px' }}>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
preserve={false}
|
||||
>
|
||||
<Form.Item
|
||||
name="text"
|
||||
label="热词内容"
|
||||
rules={[{ required: true, message: '请输入热词内容' }]}
|
||||
>
|
||||
<Input placeholder="例如:通义千问" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="weight"
|
||||
label="权重 (1-10)"
|
||||
rules={[{ required: true, message: '请输入权重' }]}
|
||||
>
|
||||
<InputNumber min={1} max={10} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="lang"
|
||||
label="语言"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input placeholder="zh 或 en" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="status"
|
||||
label="启用状态"
|
||||
valuePropName="checked"
|
||||
getValueProps={(value) => ({ checked: value === 1 })}
|
||||
getValueFromEvent={(checked) => (checked ? 1 : 0)}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Toast notifications */}
|
||||
{toasts.map(toast => (
|
||||
<Toast
|
||||
key={toast.id}
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
onClose={() => removeToast(toast.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default HotWordManagement;
|
||||
|
|
@ -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 = () => {
|
|||
/>
|
||||
</Spin>
|
||||
</Card>
|
||||
|
||||
{/* Toast notifications */}
|
||||
{toasts.map(toast => (
|
||||
<Toast
|
||||
key={toast.id}
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
onClose={() => removeToast(toast.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 <PageLoading message="加载配置数据中..." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="system-configuration">
|
||||
<div className="config-header">
|
||||
<h2>
|
||||
<Settings />
|
||||
系统配置
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{message.text && (
|
||||
<div className={`config-message ${message.type}`}>
|
||||
{message.type === 'success' && <CheckCircle size={16} />}
|
||||
{message.type === 'error' && <AlertCircle size={16} />}
|
||||
{message.type === 'loading' && <Loader className="loading-spinner" size={16} />}
|
||||
{message.text}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="config-grid">
|
||||
{/* 模型配置块 */}
|
||||
<div className="config-block">
|
||||
<div className="config-block-header">
|
||||
<h3>
|
||||
<Brain />
|
||||
模型配置
|
||||
</h3>
|
||||
<p>配置AI模型相关参数</p>
|
||||
</div>
|
||||
<div className="config-block-content">
|
||||
<div className="config-form-group">
|
||||
<label>
|
||||
模型名称
|
||||
<span className="label-hint">(model_name)</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="config-input"
|
||||
value={configs.model_name}
|
||||
onChange={(e) => handleInputChange('model_name', e.target.value)}
|
||||
placeholder="请输入模型名称"
|
||||
/>
|
||||
<div className="config-input-hint">
|
||||
指定要使用的AI模型名称,例如:gpt-4, claude-3-sonnet等
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="config-form-group">
|
||||
<label>
|
||||
声纹提示词模板
|
||||
<span className="label-hint">(template_text)</span>
|
||||
</label>
|
||||
<textarea
|
||||
className="config-input config-textarea"
|
||||
value={configs.template_text}
|
||||
onChange={(e) => handleInputChange('template_text', e.target.value)}
|
||||
placeholder="请输入声纹采集提示词模板"
|
||||
rows={6}
|
||||
/>
|
||||
<div className="config-input-hint">
|
||||
定义声纹采集时用户需要朗读的文本内容,用于生成用户的声纹特征
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 管理配置块 */}
|
||||
<div className="config-block">
|
||||
<div className="config-block-header">
|
||||
<h3>
|
||||
<Shield />
|
||||
管理配置
|
||||
</h3>
|
||||
<p>系统管理相关设置</p>
|
||||
</div>
|
||||
<div className="config-block-content">
|
||||
<div className="config-form-group">
|
||||
<label>
|
||||
默认重置密码
|
||||
<span className="label-hint">(DEFAULT_RESET_PASSWORD)</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="config-input"
|
||||
value={configs.DEFAULT_RESET_PASSWORD}
|
||||
onChange={(e) => handleInputChange('DEFAULT_RESET_PASSWORD', e.target.value)}
|
||||
placeholder="请输入默认密码"
|
||||
/>
|
||||
<div className="config-input-hint">
|
||||
管理员重置用户密码时使用的默认密码,建议设置为安全的临时密码
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="config-form-group">
|
||||
<label>
|
||||
最大文件大小
|
||||
<span className="label-hint">(MAX_FILE_SIZE)</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
className="config-input"
|
||||
value={displayValues.MAX_FILE_SIZE}
|
||||
onChange={(e) => handleInputChange('MAX_FILE_SIZE', e.target.value)}
|
||||
placeholder="请输入文件大小限制(MB)"
|
||||
min="1"
|
||||
max="1000"
|
||||
/>
|
||||
<div className="config-input-hint">
|
||||
用户上传音频文件的大小限制,单位为MB,建议设置为50-200MB
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="config-form-group">
|
||||
<label>
|
||||
会议列表分页大小
|
||||
<span className="label-hint">(TIMELINE_PAGESIZE)</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
className="config-input"
|
||||
value={displayValues.TIMELINE_PAGESIZE}
|
||||
onChange={(e) => handleInputChange('TIMELINE_PAGESIZE', e.target.value)}
|
||||
placeholder="请输入分页大小"
|
||||
min="5"
|
||||
max="100"
|
||||
/>
|
||||
<div className="config-input-hint">
|
||||
会议时间轴每页显示的会议数量,建议设置为10-50
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="config-actions">
|
||||
<button
|
||||
className="config-btn config-btn-secondary"
|
||||
onClick={handleReset}
|
||||
disabled={!hasChanges || saving}
|
||||
>
|
||||
<RotateCcw size={16} />
|
||||
重置更改
|
||||
</button>
|
||||
<button
|
||||
className="config-btn config-btn-primary"
|
||||
onClick={handleSave}
|
||||
disabled={!hasChanges || saving}
|
||||
>
|
||||
{saving ? (
|
||||
<Loader className="loading-spinner" size={16} />
|
||||
) : (
|
||||
<Save size={16} />
|
||||
)}
|
||||
{saving ? '保存中...' : '保存配置'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemConfiguration;
|
||||
Loading…
Reference in New Issue