Files
shaguabijia-admin-web/src/app/(main)/config/page.tsx
T
guke a392656d44 fix(config-page): 支持 json 配置类型的序列化/反序列化(同 dict_str_int)
llm_token_price 新 type=json(嵌套 JSON 含浮点数,非 str→int 映射)在配置页
需 JSON.stringify/JSON.parse,与 dict_str_int 共用编辑框。
2026-07-13 18:12:47 +08:00

172 lines
5.5 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import { App, Button, Card, Input, InputNumber, Space, Spin, Switch, Tabs, Tag, Tooltip } from 'antd';
import { api, errMsg } from '@/lib/api';
import HomeMarqueeSeeds from './HomeMarqueeSeeds';
import HomeStatsConfig from './HomeStatsConfig';
import FeedbackQrConfig from './FeedbackQrConfig';
interface ConfigItem {
key: string;
label: string;
group: string;
type: string; // int / int_list / dict_str_int / bool / json
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' || item.type === 'json') 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' || type === 'json') 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);
}
};
const groups = [...new Set(items.map((i) => i.group))];
const welfareConfig = loading ? (
<Spin style={{ display: 'block', marginTop: 24 }} />
) : (
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>
))
);
return (
<div>
<h2></h2>
<p style={{ color: '#999' }}>
() / ,
</p>
<Tabs
items={[
{
key: 'home',
label: '首页',
children: (
<>
<HomeStatsConfig />
<HomeMarqueeSeeds />
</>
),
},
{ key: 'welfare', label: '福利页', children: welfareConfig },
{ key: 'feedback-qr', label: '反馈二维码', children: <FeedbackQrConfig /> },
]}
/>
</div>
);
}