修正了处理进度展示

main
mula.liu 2026-01-09 16:05:58 +08:00
parent b315eaaf3b
commit 9d85301a50
12 changed files with 492 additions and 359 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
dist.zip

Binary file not shown.

BIN
public/.DS_Store vendored

Binary file not shown.

View File

@ -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'

View File

@ -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>

View File

@ -40,8 +40,8 @@ const CreateMeeting = ({ user }) => {
const fetchUsers = async () => { const fetchUsers = async () => {
try { try {
// size // role_id=2size
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);

View File

@ -73,8 +73,8 @@ const EditMeeting = ({ user }) => {
const fetchUsers = async () => { const fetchUsers = async () => {
try { try {
// size // role_id=2size
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);

View File

@ -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>
); );
}; };

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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>
); );
}; };

View File

@ -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;