778 lines
24 KiB
TypeScript
778 lines
24 KiB
TypeScript
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
||
import { useCollapsedActions } from '../../hooks'
|
||
import { TableActions, actionCellProps } from '../../components/TableActions/TableActions'
|
||
import { CheckCircleOutlined, DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons'
|
||
import {
|
||
Button,
|
||
Card,
|
||
Form,
|
||
Input,
|
||
InputNumber,
|
||
message,
|
||
Modal,
|
||
Select,
|
||
Switch,
|
||
Table,
|
||
Tabs,
|
||
Tag,
|
||
Tooltip,
|
||
Typography,
|
||
} from 'antd'
|
||
import axios from 'axios'
|
||
import AppLayout from '../../components/AppLayout/AppLayout'
|
||
import Scrollbar from '../../components/Scrollbar/Scrollbar'
|
||
import TableScrollRegion from '../../components/Scrollbar/TableScrollRegion'
|
||
import { formatDateTimeZhCN } from '../../utils/datetime'
|
||
|
||
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
|
||
}
|
||
|
||
interface TVStreamSource {
|
||
id: string
|
||
name: string
|
||
provider: string
|
||
region: string
|
||
language: string
|
||
source_type: 'iframe' | 'hls' | 'video' | 'external' | 'youtube'
|
||
embed_url: string
|
||
stream_url: string
|
||
homepage_url: string
|
||
poster_url: string
|
||
youtube_video_id: string
|
||
youtube_channel: string
|
||
is_enabled: boolean
|
||
is_fallback: boolean
|
||
sort_order: number
|
||
collector_source: string | null
|
||
notes: string
|
||
}
|
||
|
||
interface TVSettings {
|
||
default_source_id: string
|
||
auto_fallback: boolean
|
||
sources: TVStreamSource[]
|
||
}
|
||
|
||
function SettingsPanel({
|
||
loading,
|
||
children,
|
||
}: {
|
||
loading: boolean
|
||
children: ReactNode
|
||
}) {
|
||
return (
|
||
<div className="settings-pane">
|
||
<Card className="settings-panel-card" loading={loading}>
|
||
<Scrollbar className="settings-panel-scroll">{children}</Scrollbar>
|
||
</Card>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function Settings() {
|
||
const [loading, setLoading] = useState(true)
|
||
const [savingCollectorId, setSavingCollectorId] = useState<number | null>(null)
|
||
const [collectors, setCollectors] = useState<CollectorSettings[]>([])
|
||
const [systemSettings, setSystemSettings] = useState<SystemSettings | null>(null)
|
||
const [notificationSettings, setNotificationSettings] = useState<NotificationSettings | null>(null)
|
||
const [securitySettings, setSecuritySettings] = useState<SecuritySettings | null>(null)
|
||
const [tvSettings, setTvSettings] = useState<TVSettings | null>(null)
|
||
const [savingTvSettings, setSavingTvSettings] = useState(false)
|
||
const [editingSource, setEditingSource] = useState<TVStreamSource | null>(null)
|
||
const [tvActionsCollapsed, tvTableRef] = useCollapsedActions(780)
|
||
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 [tvEditForm] = Form.useForm<TVStreamSource>()
|
||
|
||
const fetchSettings = async () => {
|
||
try {
|
||
setLoading(true)
|
||
const response = await axios.get('/api/v1/settings')
|
||
setSystemSettings(response.data.system)
|
||
setNotificationSettings(response.data.notifications)
|
||
setSecuritySettings(response.data.security)
|
||
setTvSettings(response.data.tv || null)
|
||
setCollectors(response.data.collectors || [])
|
||
} catch (error) {
|
||
message.error('获取系统配置失败')
|
||
console.error(error)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
fetchSettings()
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
if (!loading && systemSettings) {
|
||
systemForm.setFieldsValue(systemSettings)
|
||
}
|
||
}, [loading, systemForm, systemSettings])
|
||
|
||
useEffect(() => {
|
||
if (!loading && notificationSettings) {
|
||
notificationForm.setFieldsValue(notificationSettings)
|
||
}
|
||
}, [loading, notificationForm, notificationSettings])
|
||
|
||
useEffect(() => {
|
||
if (!loading && securitySettings) {
|
||
securityForm.setFieldsValue(securitySettings)
|
||
}
|
||
}, [loading, securityForm, securitySettings])
|
||
|
||
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 setDefaultSource = (sourceId: string) => {
|
||
if (!tvSettings) return
|
||
const next = { ...tvSettings, default_source_id: sourceId }
|
||
setTvSettings(next)
|
||
saveTvSettings(next)
|
||
}
|
||
|
||
const addTvSource = () => {
|
||
const nextIndex = (tvSettings?.sources.length || 0) + 1
|
||
const newSource: TVStreamSource = {
|
||
id: `manual-tv-${Date.now()}`,
|
||
name: `新闻直播源 ${nextIndex}`,
|
||
provider: 'Manual',
|
||
region: 'Global',
|
||
language: 'und',
|
||
source_type: 'iframe',
|
||
embed_url: '',
|
||
stream_url: '',
|
||
homepage_url: '',
|
||
poster_url: '',
|
||
youtube_video_id: '',
|
||
youtube_channel: '',
|
||
is_enabled: true,
|
||
is_fallback: false,
|
||
sort_order: nextIndex * 10,
|
||
collector_source: null,
|
||
notes: '',
|
||
}
|
||
setEditingSource(newSource)
|
||
tvEditForm.setFieldsValue(newSource)
|
||
}
|
||
|
||
const confirmEditSource = async () => {
|
||
if (!editingSource || !tvSettings) return
|
||
const values = tvEditForm.getFieldsValue()
|
||
const nextSources = tvSettings.sources
|
||
.map((source) => {
|
||
if (source.id === editingSource.id) return { ...source, ...values }
|
||
if (values.is_fallback) return { ...source, is_fallback: false }
|
||
return source
|
||
})
|
||
|
||
if (!tvSettings.sources.some((source) => source.id === editingSource.id)) {
|
||
nextSources.push({
|
||
...editingSource,
|
||
...values,
|
||
})
|
||
if (values.is_fallback) {
|
||
for (let index = 0; index < nextSources.length - 1; index += 1) {
|
||
nextSources[index] = { ...nextSources[index], is_fallback: false }
|
||
}
|
||
}
|
||
}
|
||
|
||
const nextDefaultSourceId =
|
||
values.is_enabled === false && tvSettings.default_source_id === editingSource.id
|
||
? nextSources.find((s) => s.id !== editingSource.id && s.is_enabled)?.id || ''
|
||
: tvSettings.default_source_id
|
||
const next = { ...tvSettings, default_source_id: nextDefaultSourceId, sources: nextSources }
|
||
setTvSettings(next)
|
||
setEditingSource(null)
|
||
await saveTvSettings(next)
|
||
}
|
||
|
||
const removeTvSource = async (sourceId: string) => {
|
||
if (!tvSettings) return
|
||
|
||
const nextSources = tvSettings.sources.filter((source) => source.id !== sourceId)
|
||
const nextDefaultSourceId =
|
||
tvSettings.default_source_id === sourceId ? nextSources[0]?.id || '' : tvSettings.default_source_id
|
||
const next = {
|
||
...tvSettings,
|
||
default_source_id: nextDefaultSourceId,
|
||
sources: nextSources,
|
||
}
|
||
|
||
setTvSettings(next)
|
||
if (editingSource?.id === sourceId) {
|
||
setEditingSource(null)
|
||
}
|
||
await saveTvSettings(next)
|
||
}
|
||
|
||
const saveTvSettings = async (next?: TVSettings) => {
|
||
const toSave = next ?? tvSettings
|
||
if (!toSave) return
|
||
try {
|
||
setSavingTvSettings(true)
|
||
await axios.put('/api/v1/settings/tv', toSave)
|
||
message.success('电视直播配置已保存')
|
||
await fetchSettings()
|
||
} catch (error) {
|
||
message.error('电视直播配置保存失败')
|
||
console.error(error)
|
||
} finally {
|
||
setSavingTvSettings(false)
|
||
}
|
||
}
|
||
|
||
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) => formatDateTimeZhCN(value),
|
||
},
|
||
{
|
||
title: '下次执行',
|
||
dataIndex: 'next_run_at',
|
||
key: 'next_run_at',
|
||
width: 180,
|
||
render: (value: string | null) => formatDateTimeZhCN(value),
|
||
},
|
||
{
|
||
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 tvSourceColumns = [
|
||
{
|
||
title: '频道',
|
||
key: 'name',
|
||
width: 180,
|
||
render: (_: unknown, record: TVStreamSource) => (
|
||
<div>
|
||
<div style={{ fontWeight: 500 }}>{record.name}</div>
|
||
<Text type="secondary" style={{ fontSize: 12 }}>{record.provider}</Text>
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
title: '区域 / 语言',
|
||
key: 'locale',
|
||
width: 130,
|
||
render: (_: unknown, record: TVStreamSource) => (
|
||
<Text type="secondary">{record.region} · {record.language}</Text>
|
||
),
|
||
},
|
||
{
|
||
title: '类型',
|
||
dataIndex: 'source_type',
|
||
key: 'source_type',
|
||
width: 90,
|
||
render: (value: string) => <Tag>{value}</Tag>,
|
||
},
|
||
{
|
||
title: '状态',
|
||
key: 'status',
|
||
width: 130,
|
||
render: (_: unknown, record: TVStreamSource) => (
|
||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' as const }}>
|
||
<Tag color={record.is_enabled ? 'success' : 'default'}>{record.is_enabled ? '启用' : '禁用'}</Tag>
|
||
{record.id === tvSettings?.default_source_id && <Tag color="gold">默认</Tag>}
|
||
{record.is_fallback && <Tag color="blue">备用</Tag>}
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
title: '备注',
|
||
dataIndex: 'notes',
|
||
key: 'notes',
|
||
width: 200,
|
||
ellipsis: true,
|
||
render: (value: string) => <Text type="secondary">{value || '—'}</Text>,
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'action',
|
||
fixed: 'right' as const,
|
||
width: tvActionsCollapsed ? 40 : 258,
|
||
onCell: () => actionCellProps,
|
||
render: (_: unknown, record: TVStreamSource) => (
|
||
<TableActions
|
||
collapsed={tvActionsCollapsed}
|
||
items={[
|
||
{
|
||
key: 'default',
|
||
label: '设为默认',
|
||
icon: <CheckCircleOutlined />,
|
||
disabled: record.id === tvSettings?.default_source_id,
|
||
onClick: () => setDefaultSource(record.id),
|
||
},
|
||
{
|
||
key: 'edit',
|
||
label: '编辑',
|
||
icon: <EditOutlined />,
|
||
onClick: () => {
|
||
setEditingSource(record)
|
||
tvEditForm.setFieldsValue(record)
|
||
},
|
||
},
|
||
{ type: 'divider' },
|
||
{
|
||
key: 'delete',
|
||
label: '删除',
|
||
icon: <DeleteOutlined />,
|
||
danger: true,
|
||
disabled: record.id === tvSettings?.default_source_id,
|
||
onClick: () => {
|
||
void removeTvSource(record.id)
|
||
},
|
||
},
|
||
]}
|
||
>
|
||
<Button
|
||
type="link"
|
||
size="small"
|
||
icon={<CheckCircleOutlined />}
|
||
disabled={record.id === tvSettings?.default_source_id}
|
||
onClick={() => setDefaultSource(record.id)}
|
||
>
|
||
设为默认
|
||
</Button>
|
||
<Button
|
||
type="link"
|
||
size="small"
|
||
icon={<EditOutlined />}
|
||
onClick={() => {
|
||
setEditingSource(record)
|
||
tvEditForm.setFieldsValue(record)
|
||
}}
|
||
>
|
||
编辑
|
||
</Button>
|
||
<Button
|
||
type="link"
|
||
size="small"
|
||
danger
|
||
icon={<DeleteOutlined />}
|
||
disabled={record.id === tvSettings?.default_source_id}
|
||
onClick={() => {
|
||
void removeTvSource(record.id)
|
||
}}
|
||
>
|
||
删除
|
||
</Button>
|
||
</TableActions>
|
||
),
|
||
},
|
||
]
|
||
|
||
const tabItems = [
|
||
{
|
||
key: 'system',
|
||
label: '系统显示',
|
||
forceRender: true,
|
||
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: '通知策略',
|
||
forceRender: true,
|
||
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: '安全策略',
|
||
forceRender: true,
|
||
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: 'tv',
|
||
label: '电视直播',
|
||
children: (
|
||
<div className="settings-pane" ref={tvTableRef}>
|
||
<Card
|
||
className="settings-panel-card settings-panel-card--table"
|
||
loading={loading}
|
||
styles={{ body: { padding: 0 } }}
|
||
>
|
||
<TableScrollRegion
|
||
className="data-source-table-region"
|
||
style={{ flex: '1 1 auto', minHeight: 0 }}
|
||
>
|
||
<Table
|
||
rowKey="id"
|
||
columns={tvSourceColumns}
|
||
dataSource={tvSettings?.sources || []}
|
||
pagination={false}
|
||
scroll={{ x: 'max-content', y: 420 }}
|
||
tableLayout="fixed"
|
||
size="small"
|
||
/>
|
||
</TableScrollRegion>
|
||
<Tooltip title="新增直播源">
|
||
<Button
|
||
type="text"
|
||
icon={<PlusOutlined />}
|
||
onClick={addTvSource}
|
||
style={{ width: '100%', borderRadius: 0, borderTop: '1px solid rgba(0,0,0,0.06)' }}
|
||
/>
|
||
</Tooltip>
|
||
</Card>
|
||
<Modal
|
||
title={editingSource?.id.startsWith('manual-tv-') ? '新增直播源' : '编辑直播源'}
|
||
open={editingSource !== null}
|
||
onOk={confirmEditSource}
|
||
onCancel={() => {
|
||
setEditingSource(null)
|
||
tvEditForm.resetFields()
|
||
}}
|
||
okText="保存"
|
||
okButtonProps={{ loading: savingTvSettings }}
|
||
cancelText="取消"
|
||
width={560}
|
||
centered
|
||
destroyOnHidden
|
||
className="settings-tv-edit-modal"
|
||
styles={{ body: { padding: 0 } }}
|
||
>
|
||
<div className="settings-tv-edit-modal__body">
|
||
<Scrollbar className="settings-tv-edit-modal__scroll">
|
||
<Form form={tvEditForm} layout="vertical" style={{ paddingBottom: 16 }}>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0 16px' }}>
|
||
<Form.Item name="name" label="频道名称" rules={[{ required: true, message: '请输入频道名称' }]}>
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item name="provider" label="提供方">
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item name="region" label="区域">
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item name="language" label="语言">
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item name="source_type" label="类型">
|
||
<Select options={[
|
||
{ value: 'iframe', label: 'iframe' },
|
||
{ value: 'hls', label: 'HLS' },
|
||
{ value: 'video', label: 'video' },
|
||
{ value: 'youtube', label: 'YouTube' },
|
||
{ value: 'external', label: 'external(仅外部打开)' },
|
||
]} />
|
||
</Form.Item>
|
||
<Form.Item name="sort_order" label="排序">
|
||
<InputNumber style={{ width: '100%' }} min={0} />
|
||
</Form.Item>
|
||
</div>
|
||
<Form.Item name="embed_url" label="嵌入地址 / iframe 地址">
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item name="stream_url" label="流地址 / HLS 地址">
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item name="youtube_video_id" label="YouTube 视频 ID">
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item name="youtube_channel" label="YouTube 频道 Handle / URL">
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item name="homepage_url" label="官网地址">
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item name="notes" label="备注">
|
||
<Input />
|
||
</Form.Item>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0 16px' }}>
|
||
<Form.Item name="is_enabled" label="启用" valuePropName="checked">
|
||
<Switch />
|
||
</Form.Item>
|
||
<Form.Item name="is_fallback" label="设为备用源" valuePropName="checked">
|
||
<Switch />
|
||
</Form.Item>
|
||
</div>
|
||
</Form>
|
||
</Scrollbar>
|
||
</div>
|
||
</Modal>
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
key: 'collectors',
|
||
label: '采集调度',
|
||
children: (
|
||
<div className="settings-pane">
|
||
<Card
|
||
className="settings-panel-card settings-panel-card--table"
|
||
loading={loading}
|
||
styles={{ body: { padding: 0 } }}
|
||
>
|
||
<TableScrollRegion ref={collectorTableRegionRef} className="data-source-table-region">
|
||
<Table
|
||
rowKey="id"
|
||
columns={collectorColumns}
|
||
dataSource={collectors}
|
||
pagination={false}
|
||
scroll={{ x: 1200, y: collectorTableHeight }}
|
||
tableLayout="fixed"
|
||
size="small"
|
||
/>
|
||
</TableScrollRegion>
|
||
</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
|