2026-04-08 09:29:06 +00:00
|
|
|
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
|
|
|
|
import { Alert, App, Button, Drawer, Form, Input, Popconfirm, Select, Space, Switch, Table, Tag, Tooltip, Typography } from 'antd';
|
2026-03-26 06:55:12 +00:00
|
|
|
|
import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined, SaveOutlined, SettingOutlined } from '@ant-design/icons';
|
2026-04-08 11:19:33 +00:00
|
|
|
|
import httpService from '../../services/httpService';
|
2026-03-26 06:55:12 +00:00
|
|
|
|
import { API_ENDPOINTS, buildApiUrl } from '../../config/api';
|
|
|
|
|
|
import AdminModuleShell from '../../components/AdminModuleShell';
|
2026-04-03 16:25:53 +00:00
|
|
|
|
import ActionButton from '../../components/ActionButton';
|
2026-03-26 06:55:12 +00:00
|
|
|
|
import StatusTag from '../../components/StatusTag';
|
2026-04-08 09:29:06 +00:00
|
|
|
|
import configService from '../../utils/configService';
|
|
|
|
|
|
import useSystemPageSize from '../../hooks/useSystemPageSize';
|
|
|
|
|
|
|
|
|
|
|
|
const { Text } = Typography;
|
|
|
|
|
|
|
|
|
|
|
|
const CATEGORY_OPTIONS = [
|
|
|
|
|
|
{ label: 'public', value: 'public' },
|
|
|
|
|
|
{ label: 'system', value: 'system' },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const VALUE_TYPE_OPTIONS = [
|
|
|
|
|
|
{ label: 'string', value: 'string' },
|
|
|
|
|
|
{ label: 'number', value: 'number' },
|
|
|
|
|
|
{ label: 'json', value: 'json' },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const PUBLIC_PARAM_KEYS = new Set([
|
|
|
|
|
|
'app_name',
|
|
|
|
|
|
'page_size',
|
|
|
|
|
|
'max_audio_size',
|
|
|
|
|
|
'preview_title',
|
|
|
|
|
|
'login_welcome',
|
|
|
|
|
|
'footer_text',
|
|
|
|
|
|
'console_subtitle',
|
|
|
|
|
|
]);
|
2026-03-26 06:55:12 +00:00
|
|
|
|
|
|
|
|
|
|
const ParameterManagement = () => {
|
|
|
|
|
|
const { message } = App.useApp();
|
|
|
|
|
|
const [items, setItems] = useState([]);
|
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
|
|
|
|
const [editing, setEditing] = useState(null);
|
|
|
|
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
|
|
|
const [form] = Form.useForm();
|
2026-04-08 09:29:06 +00:00
|
|
|
|
const pageSize = useSystemPageSize(10);
|
|
|
|
|
|
const currentParamKey = Form.useWatch('param_key', form);
|
|
|
|
|
|
|
|
|
|
|
|
const categorySuggestion = useMemo(() => {
|
|
|
|
|
|
const normalizedKey = String(currentParamKey || '').trim();
|
|
|
|
|
|
if (!normalizedKey) {
|
|
|
|
|
|
return '前端需要读取并缓存的参数请选择 public,仅后端内部使用的参数请选择 system。';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (PUBLIC_PARAM_KEYS.has(normalizedKey)) {
|
|
|
|
|
|
return `检测到参数键 ${normalizedKey} 更适合作为 public 参数,前端会统一初始化并缓存本地。`;
|
|
|
|
|
|
}
|
|
|
|
|
|
return `参数键 ${normalizedKey} 暂未命中公开参数白名单,如仅后端使用,建议归类为 system。`;
|
|
|
|
|
|
}, [currentParamKey]);
|
2026-03-26 06:55:12 +00:00
|
|
|
|
|
2026-04-08 09:29:06 +00:00
|
|
|
|
const fetchItems = useCallback(async () => {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
2026-04-08 11:19:33 +00:00
|
|
|
|
const res = await httpService.get(buildApiUrl(API_ENDPOINTS.ADMIN.PARAMETERS));
|
2026-03-26 06:55:12 +00:00
|
|
|
|
setItems(res.data.items || []);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
message.error('获取参数列表失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
2026-04-08 09:29:06 +00:00
|
|
|
|
}, [message]);
|
2026-03-26 06:55:12 +00:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
fetchItems();
|
2026-04-08 09:29:06 +00:00
|
|
|
|
}, [fetchItems]);
|
2026-03-26 06:55:12 +00:00
|
|
|
|
|
|
|
|
|
|
const openCreate = () => {
|
|
|
|
|
|
setEditing(null);
|
|
|
|
|
|
form.setFieldsValue({
|
|
|
|
|
|
param_key: '',
|
|
|
|
|
|
param_name: '',
|
|
|
|
|
|
param_value: '',
|
|
|
|
|
|
value_type: 'string',
|
|
|
|
|
|
category: 'system',
|
|
|
|
|
|
description: '',
|
|
|
|
|
|
is_active: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
setDrawerOpen(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const openEdit = (row) => {
|
|
|
|
|
|
setEditing(row);
|
|
|
|
|
|
form.setFieldsValue({
|
|
|
|
|
|
...row,
|
|
|
|
|
|
is_active: Boolean(row.is_active),
|
|
|
|
|
|
});
|
|
|
|
|
|
setDrawerOpen(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const submit = async () => {
|
|
|
|
|
|
const values = await form.validateFields();
|
|
|
|
|
|
setSubmitting(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (editing) {
|
2026-04-08 11:19:33 +00:00
|
|
|
|
await httpService.put(buildApiUrl(API_ENDPOINTS.ADMIN.PARAMETER_DETAIL(editing.param_key)), values);
|
2026-03-26 06:55:12 +00:00
|
|
|
|
message.success('参数更新成功');
|
|
|
|
|
|
} else {
|
2026-04-08 11:19:33 +00:00
|
|
|
|
await httpService.post(buildApiUrl(API_ENDPOINTS.ADMIN.PARAMETERS), values);
|
2026-03-26 06:55:12 +00:00
|
|
|
|
message.success('参数创建成功');
|
|
|
|
|
|
}
|
2026-04-08 09:29:06 +00:00
|
|
|
|
configService.clearCache();
|
2026-03-26 06:55:12 +00:00
|
|
|
|
setDrawerOpen(false);
|
|
|
|
|
|
fetchItems();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error(error?.response?.data?.message || '参数保存失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setSubmitting(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const columns = [
|
|
|
|
|
|
{ title: '参数键', dataIndex: 'param_key', key: 'param_key', width: 220 },
|
|
|
|
|
|
{ title: '参数名', dataIndex: 'param_name', key: 'param_name', width: 180 },
|
|
|
|
|
|
{ title: '值', dataIndex: 'param_value', key: 'param_value' },
|
|
|
|
|
|
{ title: '类型', dataIndex: 'value_type', key: 'value_type', width: 100, render: (v) => <Tag>{v}</Tag> },
|
2026-04-08 09:29:06 +00:00
|
|
|
|
{
|
|
|
|
|
|
title: '分类',
|
|
|
|
|
|
dataIndex: 'category',
|
|
|
|
|
|
key: 'category',
|
|
|
|
|
|
width: 120,
|
|
|
|
|
|
render: (value) => (
|
|
|
|
|
|
<Tag color={value === 'public' ? 'blue' : 'default'}>
|
|
|
|
|
|
{value || 'system'}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
2026-03-26 06:55:12 +00:00
|
|
|
|
{
|
|
|
|
|
|
title: '状态',
|
|
|
|
|
|
dataIndex: 'is_active',
|
|
|
|
|
|
key: 'is_active',
|
|
|
|
|
|
width: 90,
|
|
|
|
|
|
render: (v) => <StatusTag active={v} />,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '操作',
|
|
|
|
|
|
key: 'action',
|
2026-04-03 16:25:53 +00:00
|
|
|
|
width: 90,
|
2026-03-26 06:55:12 +00:00
|
|
|
|
render: (_, row) => (
|
2026-04-03 16:25:53 +00:00
|
|
|
|
<Space size={6}>
|
|
|
|
|
|
<ActionButton tone="edit" variant="iconSm" tooltip="编辑" icon={<EditOutlined />} onClick={() => openEdit(row)} />
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<Popconfirm
|
|
|
|
|
|
title="确认删除参数?"
|
|
|
|
|
|
description={`参数键:${row.param_key}`}
|
|
|
|
|
|
okText="删除"
|
|
|
|
|
|
cancelText="取消"
|
|
|
|
|
|
okButtonProps={{ danger: true }}
|
|
|
|
|
|
onConfirm={async () => {
|
|
|
|
|
|
try {
|
2026-04-08 11:19:33 +00:00
|
|
|
|
await httpService.delete(buildApiUrl(API_ENDPOINTS.ADMIN.PARAMETER_DELETE(row.param_key)));
|
2026-04-08 09:29:06 +00:00
|
|
|
|
configService.clearCache();
|
2026-03-26 06:55:12 +00:00
|
|
|
|
message.success('参数删除成功');
|
|
|
|
|
|
fetchItems();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error(error?.response?.data?.message || '参数删除失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-04-03 16:25:53 +00:00
|
|
|
|
<ActionButton tone="delete" variant="iconSm" tooltip="删除" icon={<DeleteOutlined />} />
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</Popconfirm>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<AdminModuleShell
|
|
|
|
|
|
icon={<SettingOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />}
|
|
|
|
|
|
title="参数管理"
|
|
|
|
|
|
subtitle="系统参数已从字典 system_config 迁移到专用参数表。"
|
|
|
|
|
|
rightActions={(
|
|
|
|
|
|
<Space>
|
|
|
|
|
|
<Button icon={<ReloadOutlined />} onClick={fetchItems} loading={loading}>刷新</Button>
|
|
|
|
|
|
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>新增参数</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
)}
|
|
|
|
|
|
stats={[
|
|
|
|
|
|
{ label: '参数总数', value: items.length },
|
|
|
|
|
|
{ label: '启用参数', value: items.filter((item) => item.is_active).length },
|
|
|
|
|
|
]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="console-table">
|
|
|
|
|
|
<Table
|
|
|
|
|
|
rowKey="param_key"
|
|
|
|
|
|
columns={columns}
|
|
|
|
|
|
dataSource={items}
|
|
|
|
|
|
loading={loading}
|
2026-04-08 09:29:06 +00:00
|
|
|
|
pagination={{ pageSize, showTotal: (count) => `共 ${count} 条` }}
|
2026-03-26 06:55:12 +00:00
|
|
|
|
scroll={{ x: 1100 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</AdminModuleShell>
|
|
|
|
|
|
|
|
|
|
|
|
<Drawer
|
|
|
|
|
|
title={editing ? '编辑参数' : '新增参数'}
|
|
|
|
|
|
placement="right"
|
|
|
|
|
|
width={560}
|
|
|
|
|
|
open={drawerOpen}
|
|
|
|
|
|
onClose={() => setDrawerOpen(false)}
|
|
|
|
|
|
extra={(
|
|
|
|
|
|
<Space>
|
|
|
|
|
|
<Button type="primary" icon={<SaveOutlined />} loading={submitting} onClick={submit}>保存</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Form form={form} layout="vertical">
|
2026-04-08 09:29:06 +00:00
|
|
|
|
<Alert
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
showIcon
|
|
|
|
|
|
style={{ marginBottom: 16 }}
|
|
|
|
|
|
message="参数分类说明"
|
|
|
|
|
|
description={(
|
|
|
|
|
|
<Space direction="vertical" size={4}>
|
|
|
|
|
|
<Text>`public`:前端可读取,适合页面交互参数。</Text>
|
|
|
|
|
|
<Text>`system`:仅后端内部使用,不对前端公开。</Text>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<Form.Item name="param_key" label="参数键" rules={[{ required: true, message: '请输入参数键' }]}>
|
2026-04-08 09:29:06 +00:00
|
|
|
|
<Input placeholder="page_size" disabled={Boolean(editing)} />
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
<Form.Item name="param_name" label="参数名" rules={[{ required: true, message: '请输入参数名' }]}>
|
2026-04-08 09:29:06 +00:00
|
|
|
|
<Input placeholder="通用分页数量" />
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
<Form.Item name="param_value" label="参数值" rules={[{ required: true, message: '请输入参数值' }]}>
|
|
|
|
|
|
<Input.TextArea rows={3} />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
<Form.Item name="value_type" label="值类型" rules={[{ required: true, message: '请选择值类型' }]}>
|
2026-04-08 09:29:06 +00:00
|
|
|
|
<Select options={VALUE_TYPE_OPTIONS} />
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</Form.Item>
|
2026-04-08 09:29:06 +00:00
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="category"
|
|
|
|
|
|
label="分类"
|
|
|
|
|
|
rules={[{ required: true, message: '请选择分类' }]}
|
|
|
|
|
|
extra={categorySuggestion}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Select options={CATEGORY_OPTIONS} />
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
<Form.Item name="description" label="描述">
|
|
|
|
|
|
<Input.TextArea rows={2} />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
<Form.Item name="is_active" label="启用状态" valuePropName="checked">
|
2026-03-26 09:32:31 +00:00
|
|
|
|
<Switch />
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</Drawer>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default ParameterManagement;
|