a393b5c0e3
- 系统配置页支持 bool 类型(Switch 控件),供「提现自动对账」「比价期广告」等运营开关使用 - message / Modal.confirm 静态调用迁移到 App.useApp() 的 message / modal,消除 React 19 "Static function can not consume context" 告警;providers 用 <App> 容器包裹全站 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: OuYingJun1024 <1034284404@qq.com> Reviewed-on: #12 Co-authored-by: ouzhou <ouzhou@wonderable.ai> Co-committed-by: ouzhou <ouzhou@wonderable.ai>
154 lines
5.0 KiB
TypeScript
154 lines
5.0 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { App, Button, Card, Input, InputNumber, Space, Spin, Switch, Tag, Tooltip } from 'antd';
|
|
import { api, errMsg } from '@/lib/api';
|
|
|
|
interface ConfigItem {
|
|
key: string;
|
|
label: string;
|
|
group: string;
|
|
type: string; // int / int_list / dict_str_int / bool
|
|
help: string | null;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
default: any;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
value: any;
|
|
overridden: boolean;
|
|
updated_at: string | null;
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
function toEdit(item: ConfigItem): any {
|
|
if (item.type === 'int_list') return (item.value as number[]).join(', ');
|
|
if (item.type === 'dict_str_int') return JSON.stringify(item.value);
|
|
return item.value;
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
function fromEdit(type: string, raw: any): any {
|
|
if (type === 'int') return Number(raw);
|
|
if (type === 'bool') return Boolean(raw);
|
|
if (type === 'int_list') {
|
|
return String(raw)
|
|
.split(',')
|
|
.map((s) => parseInt(s.trim(), 10));
|
|
}
|
|
if (type === 'dict_str_int') return JSON.parse(raw);
|
|
return raw;
|
|
}
|
|
|
|
export default function ConfigPage() {
|
|
const { message } = App.useApp();
|
|
const [items, setItems] = useState<ConfigItem[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const [edits, setEdits] = useState<Record<string, any>>({});
|
|
const [saving, setSaving] = useState<string | null>(null);
|
|
|
|
const load = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const { data } = await api.get<ConfigItem[]>('/admin/api/config');
|
|
setItems(data);
|
|
setEdits(Object.fromEntries(data.map((i) => [i.key, toEdit(i)])));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
useEffect(() => {
|
|
load();
|
|
}, []);
|
|
|
|
const save = async (item: ConfigItem) => {
|
|
let value;
|
|
try {
|
|
value = fromEdit(item.type, edits[item.key]);
|
|
} catch {
|
|
message.error('格式不对(列表用逗号分隔、映射用 JSON)');
|
|
return;
|
|
}
|
|
setSaving(item.key);
|
|
try {
|
|
await api.patch(`/admin/api/config/${item.key}`, { value });
|
|
message.success('已保存,即时生效');
|
|
load();
|
|
} catch (e) {
|
|
message.error(errMsg(e));
|
|
} finally {
|
|
setSaving(null);
|
|
}
|
|
};
|
|
|
|
if (loading) return <Spin style={{ display: 'block', marginTop: 80 }} />;
|
|
|
|
const groups = [...new Set(items.map((i) => i.group))];
|
|
|
|
return (
|
|
<div>
|
|
<h2>系统配置</h2>
|
|
<p style={{ color: '#999' }}>
|
|
改完即生效(业务下次读取用新值)。涉及金额 / 上限,改前请确认。每次改动都进审计日志。
|
|
</p>
|
|
{groups.map((g) => (
|
|
<Card key={g} title={g} size="small" style={{ marginBottom: 16 }}>
|
|
{items
|
|
.filter((i) => i.group === g)
|
|
.map((item) => (
|
|
<div
|
|
key={item.key}
|
|
style={{ marginBottom: 12, paddingBottom: 12, borderBottom: '1px solid #f0f0f0' }}
|
|
>
|
|
<div style={{ marginBottom: 4 }}>
|
|
<b>{item.label}</b>{' '}
|
|
{item.overridden ? <Tag color="blue">已改</Tag> : <Tag>默认</Tag>}
|
|
{item.help && (
|
|
<Tooltip title={item.help}>
|
|
<span style={{ color: '#999', marginLeft: 6, fontSize: 12, cursor: 'help' }}>
|
|
ⓘ
|
|
</span>
|
|
</Tooltip>
|
|
)}
|
|
</div>
|
|
<Space wrap>
|
|
{item.type === 'bool' ? (
|
|
<Switch
|
|
checked={!!edits[item.key]}
|
|
onChange={(v) => setEdits({ ...edits, [item.key]: v })}
|
|
/>
|
|
) : item.type === 'int' ? (
|
|
<InputNumber
|
|
value={edits[item.key]}
|
|
onChange={(v) => setEdits({ ...edits, [item.key]: v })}
|
|
style={{ width: 180 }}
|
|
/>
|
|
) : (
|
|
<Input
|
|
value={edits[item.key]}
|
|
onChange={(e) => setEdits({ ...edits, [item.key]: e.target.value })}
|
|
style={{ width: 380 }}
|
|
placeholder={
|
|
item.type === 'int_list' ? '逗号分隔,如 10, 20, 30' : 'JSON,如 {"key": 100}'
|
|
}
|
|
/>
|
|
)}
|
|
<Button
|
|
type="primary"
|
|
size="small"
|
|
loading={saving === item.key}
|
|
onClick={() => save(item)}
|
|
>
|
|
保存
|
|
</Button>
|
|
<span style={{ color: '#bbb', fontSize: 12 }}>
|
|
默认 {JSON.stringify(item.default)}
|
|
</span>
|
|
</Space>
|
|
</div>
|
|
))}
|
|
</Card>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|