656 lines
20 KiB
TypeScript
656 lines
20 KiB
TypeScript
import React, {useEffect, useRef, useState} from 'react';
|
|
import {
|
|
Button,
|
|
Card,
|
|
Checkbox,
|
|
Col,
|
|
Divider,
|
|
Form,
|
|
Input,
|
|
InputNumber,
|
|
Menu,
|
|
message,
|
|
Popconfirm,
|
|
Radio,
|
|
Row,
|
|
Space,
|
|
Spin,
|
|
Switch,
|
|
Typography,
|
|
} from 'antd';
|
|
import {
|
|
DeleteOutlined,
|
|
EditOutlined,
|
|
PlusOutlined,
|
|
ReloadOutlined,
|
|
SaveOutlined,
|
|
SettingOutlined,
|
|
} from '@ant-design/icons';
|
|
import XinForm from '@/components/XinForm';
|
|
import type { XinFormRef } from '@/components/XinForm/typings';
|
|
import type { FormColumn } from '@/components/XinFormField/FieldRender/typings';
|
|
import {useTranslation} from 'react-i18next';
|
|
import type {IConfigGroup} from '@/domain/iConfigGroup.ts';
|
|
import type {IConfigItem} from '@/domain/iConfigItem.ts';
|
|
import {
|
|
createConfigGroup,
|
|
createConfigItem,
|
|
deleteConfigGroup,
|
|
deleteConfigItem,
|
|
getConfigGroupList,
|
|
getConfigItemList, refreshCache,
|
|
saveConfigItems,
|
|
updateConfigGroup,
|
|
updateConfigItem,
|
|
} from '@/api/system/sys_config.ts';
|
|
|
|
const { TextArea } = Input;
|
|
const { Text, Title } = Typography;
|
|
|
|
const ConfigManagement: React.FC = () => {
|
|
const { t } = useTranslation();
|
|
|
|
/** 表单组件类型选项 */
|
|
const FORM_COMPONENT_OPTIONS = [
|
|
{ label: t('system.config.component.Input'), value: 'Input' },
|
|
{ label: t('system.config.component.TextArea'), value: 'TextArea' },
|
|
{ label: t('system.config.component.InputNumber'), value: 'InputNumber' },
|
|
{ label: t('system.config.component.Switch'), value: 'Switch' },
|
|
{ label: t('system.config.component.Radio'), value: 'Radio' },
|
|
{ label: t('system.config.component.Checkbox'), value: 'Checkbox' },
|
|
];
|
|
const [configGroups, setConfigGroups] = useState<IConfigGroup[]>([]);
|
|
const [selectedGroupId, setSelectedGroupId] = useState<number>();
|
|
const [selectedGroupKey, setSelectedGroupKey] = useState<string>();
|
|
const [configItems, setConfigItems] = useState<IConfigItem[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [itemsLoading, setItemsLoading] = useState(false);
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
// 设置组表单
|
|
const [editingGroup, setEditingGroup] = useState<IConfigGroup | null>(null);
|
|
const groupFormRef = useRef<XinFormRef>(null);
|
|
|
|
// 设置项表单
|
|
const [editingItem, setEditingItem] = useState<IConfigItem | null>(null);
|
|
const itemFormRef = useRef<XinFormRef>(null);
|
|
|
|
// 设置项值表单
|
|
const [valuesForm] = Form.useForm();
|
|
|
|
/** 加载设置组列表 */
|
|
const loadConfigGroups = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const { data } = await getConfigGroupList();
|
|
const groups = data.data || [];
|
|
setConfigGroups(groups);
|
|
|
|
// 如果当前没有选中的组,选中第一个
|
|
if (!selectedGroupId && groups.length > 0) {
|
|
setSelectedGroupId(groups[0].id);
|
|
setSelectedGroupKey(groups[0].key);
|
|
}
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
/** 加载设置项列表 */
|
|
const loadConfigItems = async (groupId?: number) => {
|
|
if (!groupId) return;
|
|
|
|
try {
|
|
setItemsLoading(true);
|
|
const { data } = await getConfigItemList(groupId);
|
|
const items = data.data || [];
|
|
setConfigItems(items);
|
|
|
|
// 初始化表单值
|
|
const initialValues: { [key: string]: any } = {};
|
|
items.forEach(item => {
|
|
if(!item.type || ['Input', 'TextArea', 'Radio'].includes(item.type)) {
|
|
initialValues[`item_${item.id}`] = String(item.values);
|
|
}
|
|
if(item.type && item.type === 'InputNumber') {
|
|
initialValues[`item_${item.id}`] = Number(item.values);
|
|
}
|
|
if(item.type && item.type === 'Switch') {
|
|
if(item.values === '0' || item.values === 'false') {
|
|
initialValues[`item_${item.id}`] = false;
|
|
}else {
|
|
initialValues[`item_${item.id}`] = Boolean(item.values);
|
|
}
|
|
}
|
|
if(item.type && item.type === 'Checkbox') {
|
|
try {
|
|
initialValues[`item_${item.id}`] = JSON.parse(item.values || '[]');
|
|
} catch {
|
|
initialValues[`item_${item.id}`] = [];
|
|
}
|
|
}
|
|
});
|
|
console.log(initialValues)
|
|
valuesForm.setFieldsValue(initialValues);
|
|
} finally {
|
|
setItemsLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
loadConfigGroups();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (selectedGroupId) {
|
|
loadConfigItems(selectedGroupId);
|
|
}
|
|
}, [selectedGroupId]);
|
|
|
|
/** 打开新增设置组对话框 */
|
|
const handleAddGroup = () => {
|
|
setEditingGroup(null);
|
|
groupFormRef.current?.resetFields();
|
|
groupFormRef.current?.open();
|
|
};
|
|
|
|
/** 打开编辑设置组对话框 */
|
|
const handleEditGroup = (group: IConfigGroup) => {
|
|
setEditingGroup(group);
|
|
groupFormRef.current?.setFieldsValue(group);
|
|
groupFormRef.current?.open();
|
|
};
|
|
|
|
/** 保存设置组 */
|
|
const handleSaveGroup = async (values: IConfigGroup) => {
|
|
try {
|
|
if (editingGroup) {
|
|
await updateConfigGroup(editingGroup.id!, values);
|
|
message.success(t('system.config.group.updateSuccess'));
|
|
} else {
|
|
await createConfigGroup(values);
|
|
message.success(t('system.config.group.createSuccess'));
|
|
}
|
|
groupFormRef.current?.close();
|
|
await loadConfigGroups();
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/** 删除设置组 */
|
|
const handleDeleteGroup = async (id: number) => {
|
|
try {
|
|
await deleteConfigGroup(id);
|
|
message.success(t('system.config.group.deleteSuccess'));
|
|
if (selectedGroupId === id) {
|
|
setSelectedGroupId(undefined);
|
|
setSelectedGroupKey(undefined);
|
|
}
|
|
await loadConfigGroups();
|
|
} catch (error) {
|
|
console.error('删除设置组失败:', error);
|
|
}
|
|
};
|
|
|
|
/** 打开新增设置项对话框 */
|
|
const handleAddItem = () => {
|
|
setEditingItem(null);
|
|
itemFormRef.current?.resetFields();
|
|
itemFormRef.current?.setFieldsValue({ group_id: selectedGroupId });
|
|
itemFormRef.current?.open();
|
|
};
|
|
|
|
/** 打开编辑设置项对话框 */
|
|
const handleEditItem = (item: IConfigItem) => {
|
|
setEditingItem(item);
|
|
itemFormRef.current?.setFieldsValue(item);
|
|
itemFormRef.current?.open();
|
|
};
|
|
|
|
/** 保存设置项 */
|
|
const handleSaveItem = async (values: IConfigItem) => {
|
|
try {
|
|
if (editingItem) {
|
|
await updateConfigItem(editingItem.id!, { ...values, group_id: selectedGroupId });
|
|
message.success(t('system.config.item.updateSuccess'));
|
|
} else {
|
|
await createConfigItem({ ...values, group_id: selectedGroupId });
|
|
message.success(t('system.config.item.createSuccess'));
|
|
}
|
|
itemFormRef.current?.close();
|
|
await loadConfigItems(selectedGroupId);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/** 删除设置项 */
|
|
const handleDeleteItem = async (id: number) => {
|
|
try {
|
|
await deleteConfigItem(id);
|
|
message.success(t('system.config.item.deleteSuccess'));
|
|
await loadConfigItems(selectedGroupId);
|
|
} catch (error) {
|
|
console.error('删除设置项失败:', error);
|
|
}
|
|
};
|
|
|
|
/** 批量保存所有设置项值 */
|
|
const handleSaveAll = async () => {
|
|
try {
|
|
setSaving(true);
|
|
const values = valuesForm.getFieldsValue();
|
|
const configs = configItems.map(item => {
|
|
const value = values[`item_${item.id}`];
|
|
if(!item.type || ['Input', 'TextArea', 'Radio'].includes(item.type)) {
|
|
return { id: item.id!, value: String(value) };
|
|
}
|
|
if(item.type && item.type === 'InputNumber') {
|
|
return { id: item.id!, value: String(value) };
|
|
}
|
|
if(item.type && item.type === 'Switch') {
|
|
if(value === '0' || value === 'false' || !!value) {
|
|
return { id: item.id!, value: 'false' };
|
|
}else {
|
|
return { id: item.id!, value: 'true' };
|
|
}
|
|
}
|
|
if(item.type && item.type === 'Checkbox') {
|
|
return { id: item.id!, value: JSON.stringify(value) };
|
|
}
|
|
return { id: item.id!, value: value };
|
|
});
|
|
await saveConfigItems(configs);
|
|
message.success(t('system.config.item.updateSuccess'));
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleRefresh = async () => {
|
|
try {
|
|
setSaving(true);
|
|
await refreshCache();
|
|
message.success(t('system.config.item.refreshCacheSuccess'))
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}
|
|
|
|
/** 渲染设置项的表单组件 */
|
|
const renderConfigItemComponent = (item: IConfigItem) => {
|
|
const fieldName = `item_${item.id}`;
|
|
const componentType = item.type || 'Input';
|
|
const usage = `site_config('${selectedGroupKey}.${item.key}')`;
|
|
|
|
// 解析options
|
|
let options: any[] = [];
|
|
if (item.options_json) {
|
|
try {
|
|
options = JSON.parse(item.options_json);
|
|
} catch {
|
|
console.error('options解析失败:', item.options_json);
|
|
}
|
|
}
|
|
|
|
// 解析props
|
|
let componentProps: any = {};
|
|
if (item.props_json) {
|
|
try {
|
|
componentProps = JSON.parse(item.props_json);
|
|
} catch {
|
|
console.error('props解析失败:', item.props_json);
|
|
}
|
|
}
|
|
|
|
// 自定义Label组件
|
|
const CustomLabel = () => (
|
|
<div style={{ marginBottom: 8 }}>
|
|
<Space>
|
|
<Text strong>{item.title}</Text>
|
|
<Button
|
|
type="text"
|
|
size="small"
|
|
icon={<EditOutlined />}
|
|
onClick={() => handleEditItem(item)}
|
|
/>
|
|
<Popconfirm
|
|
title={t('system.config.item.deleteConfirm')}
|
|
onConfirm={() => handleDeleteItem(item.id!)}
|
|
okText={t('system.config.confirm.ok')}
|
|
cancelText={t('system.config.confirm.cancel')}
|
|
>
|
|
<Button
|
|
type="text"
|
|
size="small"
|
|
danger
|
|
icon={<DeleteOutlined />}
|
|
/>
|
|
</Popconfirm>
|
|
</Space>
|
|
<div style={{ marginBottom: 4 }}>
|
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
{item.describe} <Text code copyable>{usage}</Text>
|
|
</Text>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
let component;
|
|
switch (componentType) {
|
|
case 'Input':
|
|
component = (
|
|
<Input
|
|
placeholder={item.describe || t('system.config.form.placeholder.input')}
|
|
{...componentProps}
|
|
/>
|
|
);
|
|
break;
|
|
case 'TextArea':
|
|
component = (
|
|
<TextArea
|
|
placeholder={item.describe || t('system.config.form.placeholder.input')}
|
|
rows={4}
|
|
{...componentProps}
|
|
/>
|
|
);
|
|
break;
|
|
case 'InputNumber':
|
|
component = (
|
|
<InputNumber
|
|
style={{ width: '100%' }}
|
|
placeholder={item.describe || t('system.config.form.placeholder.input')}
|
|
{...componentProps}
|
|
/>
|
|
);
|
|
break;
|
|
case 'Switch':
|
|
component = <Switch {...componentProps} />;
|
|
break;
|
|
case 'Radio':
|
|
component = <Radio.Group options={options} {...componentProps} />;
|
|
break;
|
|
case 'Checkbox':
|
|
component = <Checkbox.Group options={options} {...componentProps} />;
|
|
break;
|
|
default:
|
|
component = <Input placeholder={item.describe || t('system.config.form.placeholder.input')} {...componentProps} />;
|
|
}
|
|
|
|
return (
|
|
<div style={{ marginBottom: 24 }}>
|
|
<CustomLabel />
|
|
<Form.Item name={fieldName} noStyle>
|
|
{component}
|
|
</Form.Item>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/** 设置组表单列 */
|
|
const groupColumns: FormColumn<IConfigGroup>[] = [
|
|
{
|
|
title: t('system.config.group.field.title'),
|
|
dataIndex: 'title',
|
|
valueType: 'text',
|
|
rules: [{ required: true, message: t('system.config.group.field.title.required') }],
|
|
},
|
|
{
|
|
title: t('system.config.group.field.key'),
|
|
dataIndex: 'key',
|
|
valueType: 'text',
|
|
rules: [{ required: true, message: t('system.config.group.field.key.required') }],
|
|
},
|
|
{
|
|
title: t('system.config.group.field.remark'),
|
|
dataIndex: 'remark',
|
|
valueType: 'textarea',
|
|
colProps: {span: 24},
|
|
},
|
|
];
|
|
|
|
/** 设置项表单列 */
|
|
const itemColumns: FormColumn<IConfigItem>[] = [
|
|
{
|
|
title: t('system.config.item.field.key'),
|
|
dataIndex: 'key',
|
|
valueType: 'text',
|
|
rules: [{ required: true, message: t('system.config.item.field.key.required') }],
|
|
},
|
|
{
|
|
title: t('system.config.item.field.title'),
|
|
dataIndex: 'title',
|
|
valueType: 'text',
|
|
rules: [{ required: true, message: t('system.config.item.field.title.required') }],
|
|
},
|
|
{
|
|
title: t('system.config.item.field.type'),
|
|
dataIndex: 'type',
|
|
valueType: 'select',
|
|
fieldProps: {
|
|
options: FORM_COMPONENT_OPTIONS,
|
|
},
|
|
rules: [{ required: true, message: t('system.config.item.field.type.required') }],
|
|
},
|
|
{
|
|
title: t('system.config.item.field.sort'),
|
|
dataIndex: 'sort',
|
|
valueType: 'digit',
|
|
initialValue: 0
|
|
},
|
|
{
|
|
title: t('system.config.item.field.describe'),
|
|
dataIndex: 'describe',
|
|
valueType: 'textarea',
|
|
colProps: { span: 24 },
|
|
},
|
|
{
|
|
title: t('system.config.item.field.options'),
|
|
dataIndex: 'options',
|
|
valueType: 'textarea',
|
|
tooltip: t('system.config.item.field.options.tooltip'),
|
|
},
|
|
{
|
|
title: t('system.config.item.field.props'),
|
|
dataIndex: 'props',
|
|
valueType: 'textarea',
|
|
tooltip: t('system.config.item.field.props.tooltip'),
|
|
},
|
|
{
|
|
title: t('system.config.item.field.values'),
|
|
dataIndex: 'values',
|
|
valueType: 'text',
|
|
},
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<div className={'mb-5'}>
|
|
<Title level={3}>{t('system.config.page.title')}</Title>
|
|
<Text type="secondary">{t('system.config.page.description')}</Text>
|
|
</div>
|
|
<Row gutter={[16, 16]}>
|
|
{/* 左侧设置组菜单 */}
|
|
<Col xs={24} lg={4}>
|
|
<Card
|
|
styles={{
|
|
header: { paddingInline: 16, paddingBlock: 0, minHeight: 48 },
|
|
body: { padding: 16, minHeight: 52 },
|
|
}}
|
|
title={(
|
|
<Space>
|
|
<SettingOutlined />
|
|
{t('system.config.group.title')}
|
|
</Space>
|
|
)}
|
|
extra={
|
|
<Button
|
|
type="link"
|
|
size="small"
|
|
icon={<PlusOutlined />}
|
|
onClick={handleAddGroup}
|
|
>
|
|
{t('system.config.group.add')}
|
|
</Button>
|
|
}
|
|
loading={loading}
|
|
>
|
|
<Menu
|
|
mode="inline"
|
|
selectedKeys={selectedGroupId ? [String(selectedGroupId)] : []}
|
|
items={configGroups.map(group => ({
|
|
key: String(group.id),
|
|
label: (
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
<span>{group.title}</span>
|
|
<Space size="small">
|
|
<Button
|
|
type="text"
|
|
size="small"
|
|
icon={<EditOutlined />}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleEditGroup(group);
|
|
}}
|
|
/>
|
|
<Popconfirm
|
|
title={t('system.config.group.deleteConfirm')}
|
|
description={t('system.config.group.deleteWarning')}
|
|
onConfirm={(e) => {
|
|
e?.stopPropagation();
|
|
handleDeleteGroup(group.id!);
|
|
}}
|
|
okText={t('system.config.confirm.ok')}
|
|
cancelText={t('system.config.confirm.cancel')}
|
|
>
|
|
<Button
|
|
type="text"
|
|
size="small"
|
|
danger
|
|
icon={<DeleteOutlined />}
|
|
onClick={(e) => e.stopPropagation()}
|
|
/>
|
|
</Popconfirm>
|
|
</Space>
|
|
</div>
|
|
),
|
|
onClick: () => {
|
|
setSelectedGroupId(group.id);
|
|
setSelectedGroupKey(group.key);
|
|
},
|
|
}))}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
|
|
{/* 右侧设置项 */}
|
|
<Col xs={24} lg={20}>
|
|
<Card
|
|
styles={{
|
|
header: { paddingInline: 16, paddingBlock: 0, minHeight: 48 },
|
|
body: { padding: 16, minHeight: 52 },
|
|
}}
|
|
title={t('system.config.item.title')}
|
|
extra={
|
|
selectedGroupId && (
|
|
<Space>
|
|
<Button
|
|
type="primary"
|
|
icon={<ReloadOutlined />}
|
|
onClick={handleRefresh}
|
|
>
|
|
{t('system.config.refresh.button')}
|
|
</Button>
|
|
<Button
|
|
type="primary"
|
|
icon={<SaveOutlined />}
|
|
loading={saving}
|
|
onClick={handleSaveAll}
|
|
>
|
|
{t('system.config.save.button')}
|
|
</Button>
|
|
<Button
|
|
type="primary"
|
|
icon={<PlusOutlined />}
|
|
onClick={handleAddItem}
|
|
>
|
|
{t('system.config.item.add')}
|
|
</Button>
|
|
</Space>
|
|
)
|
|
}
|
|
>
|
|
{!selectedGroupId ? (
|
|
<div style={{ textAlign: 'center', padding: '60px 0', color: '#999' }}>
|
|
{t('system.config.item.selectGroup')}
|
|
</div>
|
|
) : (
|
|
<Spin spinning={itemsLoading}>
|
|
<Form form={valuesForm} layout="vertical">
|
|
{configItems.length === 0 ? (
|
|
<div style={{ textAlign: 'center', padding: '60px 0', color: '#999' }}>
|
|
{t('system.config.item.empty')}
|
|
</div>
|
|
) : (
|
|
configItems.map((item, index) => (
|
|
<div key={item.id} style={{ position: 'relative' }}>
|
|
<div style={{ paddingRight: 80 }}>
|
|
{renderConfigItemComponent(item)}
|
|
</div>
|
|
{index < configItems.length - 1 && <Divider />}
|
|
</div>
|
|
))
|
|
)}
|
|
</Form>
|
|
</Spin>
|
|
)}
|
|
</Card>
|
|
</Col>
|
|
|
|
{/* 设置组表单弹窗 */}
|
|
<XinForm<IConfigGroup>
|
|
formRef={groupFormRef}
|
|
layoutType="ModalForm"
|
|
columns={groupColumns}
|
|
onFinish={handleSaveGroup}
|
|
modalProps={{
|
|
title: editingGroup ? t('system.config.group.edit') : t('system.config.group.create'),
|
|
onCancel: () => groupFormRef.current?.close(),
|
|
forceRender: true,
|
|
width: 800
|
|
}}
|
|
grid
|
|
layout={'vertical'}
|
|
colProps={{span: 8}}
|
|
rowProps={{gutter: [30, 0]}}
|
|
trigger={<span style={{display: 'none'}} />}
|
|
/>
|
|
|
|
{/* 设置项表单弹窗 */}
|
|
<XinForm<IConfigItem>
|
|
formRef={itemFormRef}
|
|
layoutType="ModalForm"
|
|
columns={itemColumns}
|
|
onFinish={handleSaveItem}
|
|
grid={true}
|
|
colProps={{ span: 12 }}
|
|
rowProps={{gutter: 30}}
|
|
layout={'vertical'}
|
|
modalProps={{
|
|
title: editingItem ? t('system.config.item.edit') : t('system.config.item.create'),
|
|
onCancel: () => itemFormRef.current?.close(),
|
|
forceRender: true,
|
|
width: 800,
|
|
}}
|
|
trigger={<span style={{display: 'none'}} />}
|
|
/>
|
|
</Row>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default ConfigManagement;
|