import { useEffect, useMemo, useState, type ReactNode } from 'react';
import {
Alert,
Button,
Card,
Collapse,
Form,
Input,
InputNumber,
Space,
Switch,
Table,
Tag,
Typography,
message,
} from 'antd';
import type { MockSmsCodeItem, SystemConfigFieldMeta, SystemConfigFormResponse } from '@dukang/shared-types';
import { request } from '../lib/api';
const { TextArea } = Input;
function MockSmsCodePanel({ codes, loading }: { codes: MockSmsCodeItem[]; loading?: boolean }) {
return (
最近 Mock 验证码(写入数据库,最新 50 条)
size="small"
rowKey="id"
loading={loading}
pagination={false}
scroll={{ y: 240 }}
locale={{ emptyText: '暂无记录,触发短信发送后将显示在此' }}
columns={[
{
title: '时间',
dataIndex: 'createdAt',
width: 168,
render: (v: string) => new Date(v).toLocaleString(),
},
{ title: '手机号', dataIndex: 'phone', width: 120 },
{ title: '场景', dataIndex: 'scene', width: 160 },
{
title: '验证码',
dataIndex: 'code',
width: 88,
render: (code: string) => (
{code}
),
},
]}
dataSource={codes}
/>
);
}
function renderField(
field: SystemConfigFieldMeta,
configuredSecrets: string[],
extra?: ReactNode,
) {
const isConfiguredSecret = field.secret && configuredSecrets.includes(field.key);
if (field.type === 'boolean') {
return (
{field.label}
{field.key}
{field.requiresRestart ? 需重启 : 即时}
}
tooltip={field.description}
valuePropName="checked"
getValueFromEvent={(checked: boolean) => (checked ? 'true' : 'false')}
getValueProps={(v: string) => ({ checked: v === 'true' || v === '1' })}
>
{extra}
);
}
const input =
field.type === 'textarea' ? (
) : field.type === 'number' ? (
) : field.secret ? (
) : (
);
return (
{field.label}
{field.key}
{field.requiresRestart ? 需重启 : 即时}
{isConfiguredSecret ? 已配置 : null}
}
tooltip={field.description}
>
{input}
);
}
export default function SystemSettingsPage() {
const [form] = Form.useForm>();
const [meta, setMeta] = useState(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [syncing, setSyncing] = useState(false);
const mockSmsEnabled = Form.useWatch('MOCK_SMS', form) === 'true';
async function load(silent = false) {
if (!silent) setLoading(true);
try {
const data = await request('/admin/system-config');
if (silent) {
// 仅刷新 Mock 验证码列表,避免轮询用服务端值覆盖未保存的表单(含 MOCK_SMS 开关)
setMeta((prev) =>
prev
? { ...prev, mockSmsCodes: data.mockSmsCodes, updatedAt: data.updatedAt }
: data,
);
return;
}
setMeta(data);
form.setFieldsValue(data.values);
} catch (e) {
if (!silent) message.error(e instanceof Error ? e.message : '加载失败');
} finally {
if (!silent) setLoading(false);
}
}
useEffect(() => {
void load();
}, []);
useEffect(() => {
// 以表单开关为准展示验证码;轮询只刷列表,不回写表单
if (!mockSmsEnabled) return;
const timer = window.setInterval(() => void load(true), 5000);
return () => window.clearInterval(timer);
}, [mockSmsEnabled]);
const collapseItems = useMemo(() => {
if (!meta) return [];
return meta.groups.map((group) => ({
key: group.key,
label: group.label,
children: (
{meta.fields
.filter((f) => f.group === group.key)
.map((f) =>
renderField(
f,
meta.configuredSecrets,
f.key === 'MOCK_SMS' && mockSmsEnabled ? (
) : undefined,
),
)}
),
}));
}, [meta, mockSmsEnabled, loading]);
async function onSave() {
const values = await form.validateFields();
const payload: Record = {};
for (const [k, v] of Object.entries(values)) {
payload[k] = v === undefined || v === null ? '' : String(v);
}
setSaving(true);
try {
const res = await request<{ updatedKeys: string[]; requiresRestartKeys: string[] }>(
'/admin/system-config',
{ method: 'PUT', body: JSON.stringify({ values: payload }) },
);
message.success(`已保存 ${res.updatedKeys.length} 项`);
if (res.requiresRestartKeys.length) {
message.warning(`以下配置需重启 API 后生效:${res.requiresRestartKeys.join(', ')}`);
}
await load();
} catch (e) {
message.error(e instanceof Error ? e.message : '保存失败');
} finally {
setSaving(false);
}
}
async function onSyncEnv() {
setSyncing(true);
try {
const res = await request<{ message: string; envFilePath: string }>(
'/admin/system-config/sync-env',
{ method: 'POST' },
);
message.success(res.message || '已同步到 env 文件');
} catch (e) {
message.error(e instanceof Error ? e.message : '同步失败');
} finally {
setSyncing(false);
}
}
async function onImportEnv() {
try {
const res = await request<{ imported: number }>('/admin/system-config/import-env', {
method: 'POST',
});
message.success(`已从当前进程环境导入 ${res.imported} 项`);
await load();
} catch (e) {
message.error(e instanceof Error ? e.message : '导入失败');
}
}
return (
系统设置
配置存于 system_config 表;保存后写入进程环境。可同步到{' '}
{meta?.envFilePath ?? '.env'} 以便部署持久化。
即时:保存后写入 process.env,Mock 开关、短信模板等可立即生效。
需重启:微信/OSS 密钥等集成凭证变更后,建议重启 API 进程。
OSS 始终走阿里云配置;凭证缺失时上传接口将直接报错。
}
/>
{meta?.updatedAt ? (
最近更新:{new Date(meta.updatedAt).toLocaleString()}
) : null}
);
}