修正了处理进度展示
parent
b315eaaf3b
commit
9d85301a50
Binary file not shown.
|
|
@ -41,7 +41,14 @@ const API_CONFIG = {
|
||||||
USER_STATS: '/api/admin/user-stats',
|
USER_STATS: '/api/admin/user-stats',
|
||||||
KICK_USER: (userId) => `/api/admin/kick-user/${userId}`,
|
KICK_USER: (userId) => `/api/admin/kick-user/${userId}`,
|
||||||
TASKS_MONITOR: '/api/admin/tasks/monitor',
|
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: {
|
TAGS: {
|
||||||
LIST: '/api/tags'
|
LIST: '/api/tags'
|
||||||
|
|
|
||||||
|
|
@ -1,55 +1,56 @@
|
||||||
import React from 'react';
|
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 { Tabs } from 'antd';
|
||||||
import UserManagement from './admin/UserManagement';
|
import UserManagement from './admin/UserManagement';
|
||||||
import SystemConfiguration from './admin/SystemConfiguration';
|
|
||||||
import ClientManagement from './ClientManagement';
|
import ClientManagement from './ClientManagement';
|
||||||
import PermissionManagement from './admin/PermissionManagement';
|
import PermissionManagement from './admin/PermissionManagement';
|
||||||
import DictManagement from './admin/DictManagement';
|
import DictManagement from './admin/DictManagement';
|
||||||
|
import HotWordManagement from './admin/HotWordManagement';
|
||||||
import Breadcrumb from '../components/Breadcrumb';
|
import Breadcrumb from '../components/Breadcrumb';
|
||||||
import './AdminManagement.css';
|
import './AdminManagement.css';
|
||||||
|
|
||||||
const { TabPane } = Tabs;
|
const { TabPane } = Tabs;
|
||||||
|
|
||||||
const AdminManagement = () => {
|
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 (
|
return (
|
||||||
<div className="admin-management-page">
|
<div className="admin-management-page">
|
||||||
<Breadcrumb currentPage="平台管理" icon={Shield} />
|
<Breadcrumb currentPage="平台管理" icon={Shield} />
|
||||||
|
|
||||||
<div className="admin-content">
|
<div className="admin-content">
|
||||||
<div className="admin-wrapper">
|
<div className="admin-wrapper">
|
||||||
<Tabs defaultActiveKey="userManagement" className="admin-tabs">
|
<Tabs
|
||||||
<TabPane
|
defaultActiveKey="userManagement"
|
||||||
tab={<span><Users size={16} /> 用户管理</span>}
|
className="admin-tabs"
|
||||||
key="userManagement"
|
items={items}
|
||||||
>
|
/>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,8 @@ const CreateMeeting = ({ user }) => {
|
||||||
|
|
||||||
const fetchUsers = async () => {
|
const fetchUsers = async () => {
|
||||||
try {
|
try {
|
||||||
// 获取所有用户,设置较大的size参数
|
// 获取所有普通用户(role_id=2),设置较大的size参数
|
||||||
const response = await apiClient.get(buildApiUrl(`${API_ENDPOINTS.USERS.LIST}?page=1&size=1000`));
|
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));
|
setAvailableUsers(response.data.users.filter(u => u.user_id !== user.user_id));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching users:', err);
|
console.error('Error fetching users:', err);
|
||||||
|
|
|
||||||
|
|
@ -73,8 +73,8 @@ const EditMeeting = ({ user }) => {
|
||||||
|
|
||||||
const fetchUsers = async () => {
|
const fetchUsers = async () => {
|
||||||
try {
|
try {
|
||||||
// 获取所有用户,设置较大的size参数
|
// 获取所有普通用户(role_id=2),设置较大的size参数
|
||||||
const response = await apiClient.get(buildApiUrl(`${API_ENDPOINTS.USERS.LIST}?page=1&size=1000`));
|
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));
|
setAvailableUsers(response.data.users.filter(u => u.user_id !== user.user_id));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching users:', err);
|
console.error('Error fetching users:', err);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
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 { BookText, Plus, Save, Trash2, FolderTree, FileText, X } from 'lucide-react';
|
||||||
import apiClient from '../../utils/apiClient';
|
import apiClient from '../../utils/apiClient';
|
||||||
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
||||||
|
import Toast from '../../components/Toast';
|
||||||
import './DictManagement.css';
|
import './DictManagement.css';
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
@ -16,8 +17,19 @@ const DictManagement = () => {
|
||||||
const [treeData, setTreeData] = useState([]); // 树形结构数据
|
const [treeData, setTreeData] = useState([]); // 树形结构数据
|
||||||
const [selectedNode, setSelectedNode] = useState(null); // 当前选中的节点
|
const [selectedNode, setSelectedNode] = useState(null); // 当前选中的节点
|
||||||
const [isEditing, setIsEditing] = useState(false); // 是否处于编辑状态
|
const [isEditing, setIsEditing] = useState(false); // 是否处于编辑状态
|
||||||
|
const [toasts, setToasts] = useState([]);
|
||||||
const [form] = Form.useForm();
|
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 () => {
|
const fetchDictTypes = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -26,7 +38,7 @@ const DictManagement = () => {
|
||||||
setDictTypes(response.data.types);
|
setDictTypes(response.data.types);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('获取字典类型失败');
|
showToast('获取字典类型失败', 'error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -48,7 +60,7 @@ const DictManagement = () => {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('获取字典数据失败');
|
showToast('获取字典数据失败', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -126,7 +138,7 @@ const DictManagement = () => {
|
||||||
try {
|
try {
|
||||||
values.extension_attr = JSON.parse(values.extension_attr);
|
values.extension_attr = JSON.parse(values.extension_attr);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
message.error('扩展属性 JSON 格式错误');
|
showToast('扩展属性 JSON 格式错误:无法解析JSON', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -136,25 +148,39 @@ const DictManagement = () => {
|
||||||
|
|
||||||
if (selectedNode) {
|
if (selectedNode) {
|
||||||
// 更新
|
// 更新
|
||||||
await apiClient.put(
|
const response = await apiClient.put(
|
||||||
buildApiUrl(API_ENDPOINTS.DICT_DATA.UPDATE(selectedNode.id)),
|
buildApiUrl(API_ENDPOINTS.DICT_DATA.UPDATE(selectedNode.id)),
|
||||||
values
|
values
|
||||||
);
|
);
|
||||||
message.success('更新成功');
|
if (response.code === '200') {
|
||||||
} else {
|
showToast('更新成功', 'success');
|
||||||
// 新增
|
|
||||||
await apiClient.post(buildApiUrl(API_ENDPOINTS.DICT_DATA.CREATE), values);
|
|
||||||
message.success('创建成功');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重新加载数据
|
// 重新加载数据
|
||||||
fetchDictData(selectedDictType);
|
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) {
|
} catch (error) {
|
||||||
if (error.errorFields) {
|
if (error.errorFields) {
|
||||||
// 表单验证错误
|
// 表单验证错误
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
message.error(selectedNode ? '更新失败' : '创建失败');
|
// 显示后端返回的具体错误信息
|
||||||
|
const errorMsg = error.response?.data?.message || error.message || '操作失败';
|
||||||
|
showToast(errorMsg, 'error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -164,7 +190,7 @@ const DictManagement = () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiClient.delete(buildApiUrl(API_ENDPOINTS.DICT_DATA.DELETE(selectedNode.id)));
|
await apiClient.delete(buildApiUrl(API_ENDPOINTS.DICT_DATA.DELETE(selectedNode.id)));
|
||||||
message.success('删除成功');
|
showToast('删除成功', 'success');
|
||||||
|
|
||||||
// 重新加载数据
|
// 重新加载数据
|
||||||
setSelectedNode(null);
|
setSelectedNode(null);
|
||||||
|
|
@ -172,7 +198,7 @@ const DictManagement = () => {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
fetchDictData(selectedDictType);
|
fetchDictData(selectedDictType);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('删除失败:' + (error.message || '未知错误'));
|
showToast('删除失败:' + (error.message || '未知错误'), 'error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -400,6 +426,16 @@ const DictManagement = () => {
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Toast notifications */}
|
||||||
|
{toasts.map(toast => (
|
||||||
|
<Toast
|
||||||
|
key={toast.id}
|
||||||
|
message={toast.message}
|
||||||
|
type={toast.type}
|
||||||
|
onClose={() => removeToast(toast.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</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 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 { Shield, Save } from 'lucide-react';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import Toast from '../../components/Toast';
|
||||||
import './PermissionManagement.css';
|
import './PermissionManagement.css';
|
||||||
|
|
||||||
const PermissionManagement = () => {
|
const PermissionManagement = () => {
|
||||||
|
|
@ -10,6 +11,17 @@ const PermissionManagement = () => {
|
||||||
const [permissions, setPermissions] = useState({}); // {roleId: [menuId1, menuId2, ...]}
|
const [permissions, setPermissions] = useState({}); // {roleId: [menuId1, menuId2, ...]}
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [saving, setSaving] = 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 () => {
|
const fetchData = async () => {
|
||||||
|
|
@ -18,7 +30,7 @@ const PermissionManagement = () => {
|
||||||
// 获取token - 与apiClient保持一致
|
// 获取token - 与apiClient保持一致
|
||||||
const savedUser = localStorage.getItem('iMeetingUser');
|
const savedUser = localStorage.getItem('iMeetingUser');
|
||||||
if (!savedUser) {
|
if (!savedUser) {
|
||||||
message.error('未找到登录信息,请重新登录');
|
showToast('未找到登录信息,请重新登录', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const user = JSON.parse(savedUser);
|
const user = JSON.parse(savedUser);
|
||||||
|
|
@ -56,10 +68,10 @@ const PermissionManagement = () => {
|
||||||
|
|
||||||
setPermissions(permsMap);
|
setPermissions(permsMap);
|
||||||
} else {
|
} else {
|
||||||
message.error('获取数据失败: ' + (rolesRes.data.message || menusRes.data.message));
|
showToast('获取数据失败: ' + (rolesRes.data.message || menusRes.data.message), 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('获取数据失败: ' + (error.response?.data?.message || error.message));
|
showToast('获取数据失败: ' + (error.response?.data?.message || error.message), 'error');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +99,7 @@ const PermissionManagement = () => {
|
||||||
// 获取token - 与apiClient保持一致
|
// 获取token - 与apiClient保持一致
|
||||||
const savedUser = localStorage.getItem('iMeetingUser');
|
const savedUser = localStorage.getItem('iMeetingUser');
|
||||||
if (!savedUser) {
|
if (!savedUser) {
|
||||||
message.error('未找到登录信息,请重新登录');
|
showToast('未找到登录信息,请重新登录', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const user = JSON.parse(savedUser);
|
const user = JSON.parse(savedUser);
|
||||||
|
|
@ -102,11 +114,11 @@ const PermissionManagement = () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
message.success('权限保存成功');
|
showToast('权限保存成功', 'success');
|
||||||
fetchData(); // 重新加载数据
|
fetchData(); // 重新加载数据
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving permissions:', error);
|
console.error('Error saving permissions:', error);
|
||||||
message.error('保存权限失败');
|
showToast('保存权限失败', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
|
|
@ -193,6 +205,16 @@ const PermissionManagement = () => {
|
||||||
/>
|
/>
|
||||||
</Spin>
|
</Spin>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Toast notifications */}
|
||||||
|
{toasts.map(toast => (
|
||||||
|
<Toast
|
||||||
|
key={toast.id}
|
||||||
|
message={toast.message}
|
||||||
|
type={toast.type}
|
||||||
|
onClose={() => removeToast(toast.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</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