Files
planet/frontend/src/pages/Settings/Settings.tsx
2026-03-25 17:19:10 +08:00

381 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useRef, useState, type ReactNode } from 'react'
import {
Button,
Card,
Form,
Input,
InputNumber,
message,
Select,
Space,
Switch,
Table,
Tabs,
Tag,
Typography,
} from 'antd'
import axios from 'axios'
import AppLayout from '../../components/AppLayout/AppLayout'
const { Title, Text } = Typography
interface SystemSettings {
system_name: string
refresh_interval: number
auto_refresh: boolean
data_retention_days: number
max_concurrent_tasks: number
}
interface NotificationSettings {
email_enabled: boolean
email_address: string
critical_alerts: boolean
warning_alerts: boolean
daily_summary: boolean
}
interface SecuritySettings {
session_timeout: number
max_login_attempts: number
password_policy: string
}
interface CollectorSettings {
id: number
name: string
source: string
module: string
priority: string
frequency_minutes: number
frequency: string
is_active: boolean
last_run_at: string | null
last_status: string | null
next_run_at: string | null
}
function SettingsPanel({
loading,
children,
}: {
loading: boolean
children: ReactNode
}) {
return (
<div className="settings-pane">
<Card className="settings-panel-card" loading={loading}>
<div className="settings-panel-scroll">{children}</div>
</Card>
</div>
)
}
function Settings() {
const [loading, setLoading] = useState(true)
const [savingCollectorId, setSavingCollectorId] = useState<number | null>(null)
const [collectors, setCollectors] = useState<CollectorSettings[]>([])
const collectorTableRegionRef = useRef<HTMLDivElement | null>(null)
const [collectorTableHeight, setCollectorTableHeight] = useState(360)
const [systemForm] = Form.useForm<SystemSettings>()
const [notificationForm] = Form.useForm<NotificationSettings>()
const [securityForm] = Form.useForm<SecuritySettings>()
const fetchSettings = async () => {
try {
setLoading(true)
const response = await axios.get('/api/v1/settings')
systemForm.setFieldsValue(response.data.system)
notificationForm.setFieldsValue(response.data.notifications)
securityForm.setFieldsValue(response.data.security)
setCollectors(response.data.collectors || [])
} catch (error) {
message.error('获取系统配置失败')
console.error(error)
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchSettings()
}, [])
useEffect(() => {
const updateTableHeight = () => {
const regionHeight = collectorTableRegionRef.current?.offsetHeight || 0
setCollectorTableHeight(Math.max(220, regionHeight - 56))
}
updateTableHeight()
if (typeof ResizeObserver === 'undefined') {
return undefined
}
const observer = new ResizeObserver(updateTableHeight)
if (collectorTableRegionRef.current) observer.observe(collectorTableRegionRef.current)
return () => observer.disconnect()
}, [collectors.length])
const saveSection = async (section: 'system' | 'notifications' | 'security', values: object) => {
try {
await axios.put(`/api/v1/settings/${section}`, values)
message.success('配置已保存')
await fetchSettings()
} catch (error) {
message.error('保存失败')
console.error(error)
}
}
const updateCollectorField = (id: number, field: keyof CollectorSettings, value: string | number | boolean) => {
setCollectors((prev) =>
prev.map((collector) => (collector.id === id ? { ...collector, [field]: value } : collector))
)
}
const saveCollector = async (collector: CollectorSettings) => {
try {
setSavingCollectorId(collector.id)
await axios.put(`/api/v1/settings/collectors/${collector.id}`, {
is_active: collector.is_active,
priority: collector.priority,
frequency_minutes: collector.frequency_minutes,
})
message.success(`${collector.name} 配置已更新`)
await fetchSettings()
} catch (error) {
message.error('采集调度配置保存失败')
console.error(error)
} finally {
setSavingCollectorId(null)
}
}
const collectorColumns = [
{
title: '数据源',
dataIndex: 'name',
key: 'name',
render: (_: string, record: CollectorSettings) => (
<div>
<div>{record.name}</div>
<Text type="secondary">{record.source}</Text>
</div>
),
},
{
title: '层级',
dataIndex: 'module',
key: 'module',
width: 90,
render: (module: string) => <Tag color="blue">{module}</Tag>,
},
{
title: '优先级',
dataIndex: 'priority',
key: 'priority',
width: 130,
render: (priority: string, record: CollectorSettings) => (
<Select
value={priority}
style={{ width: '100%' }}
onChange={(value) => updateCollectorField(record.id, 'priority', value)}
options={[
{ value: 'P0', label: 'P0' },
{ value: 'P1', label: 'P1' },
{ value: 'P2', label: 'P2' },
]}
/>
),
},
{
title: '频率(分钟)',
dataIndex: 'frequency_minutes',
key: 'frequency_minutes',
width: 150,
render: (value: number, record: CollectorSettings) => (
<InputNumber
min={1}
max={10080}
value={value}
style={{ width: '100%' }}
onChange={(nextValue) => updateCollectorField(record.id, 'frequency_minutes', nextValue || 1)}
/>
),
},
{
title: '启用',
dataIndex: 'is_active',
key: 'is_active',
width: 90,
render: (value: boolean, record: CollectorSettings) => (
<Switch checked={value} onChange={(checked) => updateCollectorField(record.id, 'is_active', checked)} />
),
},
{
title: '上次执行',
dataIndex: 'last_run_at',
key: 'last_run_at',
width: 180,
render: (value: string | null) => (value ? new Date(value).toLocaleString('zh-CN') : '-'),
},
{
title: '下次执行',
dataIndex: 'next_run_at',
key: 'next_run_at',
width: 180,
render: (value: string | null) => (value ? new Date(value).toLocaleString('zh-CN') : '-'),
},
{
title: '状态',
dataIndex: 'last_status',
key: 'last_status',
width: 120,
render: (value: string | null) => {
if (!value) return <Tag></Tag>
const color = value === 'success' ? 'success' : value === 'failed' ? 'error' : 'default'
return <Tag color={color}>{value}</Tag>
},
},
{
title: '操作',
key: 'action',
width: 92,
fixed: 'right' as const,
render: (_: unknown, record: CollectorSettings) => (
<Button type="primary" loading={savingCollectorId === record.id} onClick={() => saveCollector(record)}>
</Button>
),
},
]
const tabItems = [
{
key: 'system',
label: '系统显示',
children: (
<SettingsPanel loading={loading}>
<Form form={systemForm} layout="vertical" onFinish={(values) => saveSection('system', values)}>
<Form.Item name="system_name" label="系统名称" rules={[{ required: true, message: '请输入系统名称' }]}>
<Input />
</Form.Item>
<Form.Item name="refresh_interval" label="默认刷新间隔(秒)">
<InputNumber min={10} max={3600} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="data_retention_days" label="数据保留天数">
<InputNumber min={1} max={3650} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="max_concurrent_tasks" label="最大并发任务数">
<InputNumber min={1} max={50} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="auto_refresh" label="自动刷新" valuePropName="checked">
<Switch />
</Form.Item>
<Button type="primary" htmlType="submit"></Button>
</Form>
</SettingsPanel>
),
},
{
key: 'notifications',
label: '通知策略',
children: (
<SettingsPanel loading={loading}>
<Form form={notificationForm} layout="vertical" onFinish={(values) => saveSection('notifications', values)}>
<Form.Item name="email_enabled" label="启用邮件通知" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item name="email_address" label="通知邮箱">
<Input />
</Form.Item>
<Form.Item name="critical_alerts" label="严重告警通知" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item name="warning_alerts" label="警告告警通知" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item name="daily_summary" label="每日摘要" valuePropName="checked">
<Switch />
</Form.Item>
<Button type="primary" htmlType="submit"></Button>
</Form>
</SettingsPanel>
),
},
{
key: 'security',
label: '安全策略',
children: (
<SettingsPanel loading={loading}>
<Form form={securityForm} layout="vertical" onFinish={(values) => saveSection('security', values)}>
<Form.Item name="session_timeout" label="会话超时(分钟)">
<InputNumber min={5} max={1440} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="max_login_attempts" label="最大登录尝试次数">
<InputNumber min={1} max={20} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="password_policy" label="密码策略">
<Select
options={[
{ value: 'low', label: '简单' },
{ value: 'medium', label: '中等' },
{ value: 'high', label: '严格' },
]}
/>
</Form.Item>
<Button type="primary" htmlType="submit"></Button>
</Form>
</SettingsPanel>
),
},
{
key: 'collectors',
label: '采集调度',
children: (
<div className="settings-pane">
<Card
className="settings-panel-card settings-panel-card--table"
loading={loading}
styles={{ body: { padding: 0 } }}
>
<div ref={collectorTableRegionRef} className="table-scroll-region data-source-table-region">
<Table
rowKey="id"
columns={collectorColumns}
dataSource={collectors}
pagination={false}
scroll={{ x: 1200, y: collectorTableHeight }}
tableLayout="fixed"
size="small"
/>
</div>
</Card>
</div>
),
},
]
return (
<AppLayout>
<div className="page-shell settings-shell">
<div className="page-shell__header">
<div>
<Title level={3} style={{ marginBottom: 4 }}></Title>
<Text type="secondary"></Text>
</div>
</div>
<div className="page-shell__body settings-tabs-shell">
<Tabs className="settings-tabs" items={tabItems} />
</div>
</div>
</AppLayout>
)
}
export default Settings