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:
1 parent
ec3a03cf38
commit
8b921c6c83
33 files changed
+1734
-2006
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user