Phase A (P1 production blockers): - A1: Apply IP rate limiting to public routes (login/refresh) - A2: Publish domain events for workflow instance state transitions (completed/suspended/resumed/terminated) via outbox pattern - A3: Replace hardcoded nil UUID default tenant with dynamic DB lookup - A4: Add GET /api/v1/audit-logs query endpoint with pagination - A5: Enhance CORS wildcard warning for production environments Phase B (P2 functional gaps): - B1: Remove dead erp-common crate (zero references in codebase) - B2: Refactor 5 settings pages to use typed API modules instead of direct client calls; create api/themes.ts; delete dead errors.ts - B3: Add resume/suspend buttons to InstanceMonitor page - B4: Remove unused EventHandler trait from erp-core - B5: Handle task.completed events in message module (send notifications) - B6: Wire TimeoutChecker as 60s background task - B7: Auto-skip ServiceTask nodes instead of crashing the process - B8: Remove empty register_routes() from ErpModule trait and modules
100 lines
2.7 KiB
TypeScript
100 lines
2.7 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
import { Form, Input, Select, Button, ColorPicker, message, Typography } from 'antd';
|
|
import {
|
|
getTheme,
|
|
updateTheme,
|
|
type ThemeConfig,
|
|
} from '../../api/themes';
|
|
|
|
// --- Component ---
|
|
|
|
export default function ThemeSettings() {
|
|
const [form] = Form.useForm();
|
|
const [loading, setLoading] = useState(false);
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
const fetchTheme = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const theme = await getTheme();
|
|
form.setFieldsValue({
|
|
primary_color: theme.primary_color || '#1677ff',
|
|
logo_url: theme.logo_url || '',
|
|
sidebar_style: theme.sidebar_style || 'light',
|
|
});
|
|
} catch {
|
|
// Theme may not exist yet; use defaults
|
|
form.setFieldsValue({
|
|
primary_color: '#1677ff',
|
|
logo_url: '',
|
|
sidebar_style: 'light',
|
|
});
|
|
}
|
|
setLoading(false);
|
|
}, [form]);
|
|
|
|
useEffect(() => {
|
|
fetchTheme();
|
|
}, [fetchTheme]);
|
|
|
|
const handleSave = async (values: {
|
|
primary_color: string;
|
|
logo_url: string;
|
|
sidebar_style: 'light' | 'dark';
|
|
}) => {
|
|
setSaving(true);
|
|
try {
|
|
await updateTheme({
|
|
primary_color:
|
|
typeof values.primary_color === 'string'
|
|
? values.primary_color
|
|
: (values.primary_color as { toHexString?: () => string }).toHexString?.() ?? String(values.primary_color),
|
|
logo_url: values.logo_url,
|
|
sidebar_style: values.sidebar_style,
|
|
});
|
|
message.success('主题设置已保存');
|
|
} catch (err: unknown) {
|
|
const errorMsg =
|
|
(err as { response?: { data?: { message?: string } } })?.response?.data
|
|
?.message || '保存失败';
|
|
message.error(errorMsg);
|
|
}
|
|
setSaving(false);
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<Typography.Title level={5} style={{ marginBottom: 16 }}>
|
|
主题设置
|
|
</Typography.Title>
|
|
|
|
<Form
|
|
form={form}
|
|
onFinish={handleSave}
|
|
layout="vertical"
|
|
style={{ maxWidth: 480 }}
|
|
>
|
|
<Form.Item name="primary_color" label="主色调">
|
|
<ColorPicker format="hex" />
|
|
</Form.Item>
|
|
<Form.Item name="logo_url" label="Logo URL">
|
|
<Input placeholder="https://example.com/logo.png" />
|
|
</Form.Item>
|
|
<Form.Item name="sidebar_style" label="侧边栏风格">
|
|
<Select
|
|
options={[
|
|
{ label: '亮色', value: 'light' },
|
|
{ label: '暗色', value: 'dark' },
|
|
]}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item>
|
|
<Button type="primary" htmlType="submit" loading={saving}>
|
|
保存
|
|
</Button>
|
|
</Form.Item>
|
|
</Form>
|
|
</div>
|
|
);
|
|
}
|