refactor: mock清理+安全配置+通用组件+菜单优化

P0安全: SECRET_KEY/ADMIN_PASSWORD/CORS环境变量化, seed仅admin

P1Mock: mock.ts 301->88行, 删8数据mock+5generateMock, net-1300行

P2组件: useToast/useFetchList+ConfirmDialog/FormDialog

P3菜单: menuSections/menuItems->constants/menu.ts

P4API: NotificationsPage/WorkflowPage重写+RolesPage演示

测试: 1050passed, tsc clean
This commit is contained in:
logan committed 2026-05-24 02:42:29 +08:00
1 parent ec3a03cf38
commit 8b921c6c83
33 files changed
+1734 -2006

No files matched your search

+209
View File
@@ -0,0 +1,209 @@
import { useState, useEffect, useCallback } from 'react';
import api from '../api/request';
/* ── 类型 ── */
interface SettingItem {
key: string;
value: string;
value_type: string;
group: string;
label: string;
description: string | null;
default_value: string | null;
sort_order: number;
}
type SettingsMap = Record<string, SettingItem[]>;
/* ── 分组元信息 ── */
const GROUP_META: Record<string, { label: string; icon: string; desc: string }> = {
query: { label: '查询控制', icon: '🔍', desc: '查询权限、LIMIT 上限、超时设置' },
security: { label: '安全策略', icon: '🔒', desc: '登录安全、密码策略、审计日志' },
ticket: { label: '工单流程', icon: '📋', desc: '审批流程、回滚控制' },
};
export default function PlatformConfigPage() {
const [settings, setSettings] = useState<SettingsMap>({});
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [changed, setChanged] = useState<Record<string, string>>({});
const [toast, setToast] = useState('');
const fetchSettings = useCallback(async () => {
try {
const res = await api.get('/settings');
setSettings(res as SettingsMap);
} catch {
setToast('加载设置失败');
} finally {
setLoading(false);
}
}, []);
useEffect(() => { fetchSettings(); }, [fetchSettings]);
const handleChange = (key: string, value: string) => {
setChanged(prev => ({ ...prev, [key]: value }));
};
const handleSave = async () => {
if (Object.keys(changed).length === 0) return;
setSaving(true);
try {
const items = Object.entries(changed).map(([key, value]) => ({ key, value }));
await api.put('/settings', items);
setChanged({});
setToast('保存成功');
await fetchSettings();
} catch (e: any) {
setToast(e?.response?.data?.detail || '保存失败');
} finally {
setSaving(false);
setTimeout(() => setToast(''), 3000);
}
};
const handleReset = (key: string, defaultValue: string | null) => {
if (defaultValue) handleChange(key, defaultValue);
};
const displayValue = (item: SettingItem) => changed[item.key] ?? item.value;
if (loading) {
return <div className="text-center py-20 text-[var(--dg-text-muted)]">加载中...</div>;
}
const groupKeys = Object.keys(settings);
const hasChanges = Object.keys(changed).length > 0;
return (
<div className="space-y-6">
{/* Toast */}
{toast && (
<div className={`fixed top-6 right-6 z-50 px-4 py-3 rounded-lg text-sm font-medium shadow-lg ${
toast === '保存成功'
? 'bg-emerald-500/90 text-white'
: 'bg-red-500/90 text-white'
}`}>
{toast}
</div>
)}
{/* 各分组 */}
{groupKeys.map(group => {
const meta = GROUP_META[group] || { label: group, icon: '⚙️', desc: '' };
const items = settings[group] || [];
return (
<div key={group} className="bg-[var(--dg-bg-secondary)] rounded-xl overflow-hidden">
{/* 分组头 */}
<div className="px-5 py-3 flex items-center gap-3">
<span className="text-lg">{meta.icon}</span>
<div>
<h3 className="text-sm font-semibold text-[var(--dg-text-primary)]">{meta.label}</h3>
<p className="text-xs text-[var(--dg-text-muted)]">{meta.desc}</p>
</div>
</div>
{/* 设置项列表 */}
<div className="px-5 pb-4 space-y-3">
{items.map(item => {
const val = displayValue(item);
const isChanged = changed[item.key] !== undefined;
return (
<div key={item.key} className="flex items-center justify-between gap-4 py-2">
<div className="flex-1 min-w-0">
<div className="text-sm text-[var(--dg-text-primary)]">{item.label}</div>
{item.description && (
<div className="text-xs text-[var(--dg-text-muted)] mt-0.5">{item.description}</div>
)}
</div>
<div className="flex items-center gap-2 shrink-0">
{/* 重置按钮 */}
{isChanged && item.default_value && (
<button
onClick={() => handleReset(item.key, item.default_value)}
className="text-xs text-[var(--dg-text-muted)] hover:text-[var(--dg-text-secondary)]"
title="恢复默认值"
>
↩
</button>
)}
{/* 布尔 → 开关 */}
{item.value_type === 'boolean' && (
<button
onClick={() => handleChange(item.key, val === 'true' ? 'false' : 'true')}
className={`relative w-10 h-[22px] rounded-full transition-colors ${
val === 'true' ? 'bg-emerald-500' : 'bg-[var(--dg-bg-tertiary)]'
}`}
>
<span className={`absolute top-[2px] w-[18px] h-[18px] bg-white rounded-full shadow transition-transform ${
val === 'true' ? 'translate-x-[20px]' : 'translate-x-[2px]'
}`} />
</button>
)}
{/* 整数 → 数字输入 */}
{item.value_type === 'integer' && (
<input
type="number"
value={parseInt(val) || 0}
onChange={e => handleChange(item.key, e.target.value)}
className={`w-24 px-2 py-1 rounded-lg text-sm text-right bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-primary)] outline-none ${
isChanged ? 'ring-1 ring-emerald-500/50' : ''
}`}
/>
)}
{/* 字符串 → 文本 */}
{item.value_type === 'string' && (
<input
type="text"
value={val}
onChange={e => handleChange(item.key, e.target.value)}
className={`w-48 px-2 py-1 rounded-lg text-sm bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-primary)] outline-none ${
isChanged ? 'ring-1 ring-emerald-500/50' : ''
}`}
/>
)}
{/* 变更指示 */}
{isChanged && (
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
)}
</div>
</div>
);
})}
</div>
</div>
);
})}
{/* 保存按钮 */}
{hasChanges && (
<div className="flex items-center gap-3 pt-2">
<button
onClick={handleSave}
disabled={saving}
className="px-5 py-2 rounded-lg text-sm font-medium bg-emerald-500 hover:bg-emerald-600 text-white disabled:opacity-50 transition-colors"
>
{saving ? '保存中...' : '保存更改'}
</button>
<button
onClick={() => setChanged({})}
className="px-4 py-2 rounded-lg text-sm text-[var(--dg-text-muted)] hover:text-[var(--dg-text-secondary)]"
>
取消
</button>
<span className="text-xs text-[var(--dg-text-muted)]">
已修改 {Object.keys(changed).length} 项
</span>
</div>
)}
</div>
);
}