release: bump version to 0.26.0
This commit is contained in:
@@ -2315,6 +2315,32 @@ body {
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.settings-tv-toolbar {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.settings-tv-toolbar__controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.settings-tv-toolbar__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.settings-tv-field {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -55,6 +55,32 @@ interface CollectorSettings {
|
||||
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,
|
||||
@@ -78,6 +104,8 @@ function Settings() {
|
||||
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 collectorTableRegionRef = useRef<HTMLDivElement | null>(null)
|
||||
const [collectorTableHeight, setCollectorTableHeight] = useState(360)
|
||||
const [systemForm] = Form.useForm<SystemSettings>()
|
||||
@@ -91,6 +119,7 @@ function 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('获取系统配置失败')
|
||||
@@ -175,6 +204,100 @@ function Settings() {
|
||||
}
|
||||
}
|
||||
|
||||
const updateTvSetting = <K extends keyof TVSettings>(field: K, value: TVSettings[K]) => {
|
||||
setTvSettings((prev) => (prev ? { ...prev, [field]: value } : prev))
|
||||
}
|
||||
|
||||
const updateTvSourceField = <K extends keyof TVStreamSource>(
|
||||
sourceId: string,
|
||||
field: K,
|
||||
value: TVStreamSource[K]
|
||||
) => {
|
||||
setTvSettings((prev) => {
|
||||
if (!prev) return prev
|
||||
const nextSources = prev.sources.map((source) => {
|
||||
if (field === 'is_fallback' && value === true) {
|
||||
return { ...source, is_fallback: source.id === sourceId }
|
||||
}
|
||||
if (source.id === sourceId) {
|
||||
return { ...source, [field]: value }
|
||||
}
|
||||
return source
|
||||
})
|
||||
|
||||
const nextDefaultSourceId =
|
||||
field === 'is_enabled' && value === false && prev.default_source_id === sourceId
|
||||
? nextSources.find((source) => source.id !== sourceId && source.is_enabled)?.id || ''
|
||||
: prev.default_source_id
|
||||
|
||||
return {
|
||||
...prev,
|
||||
default_source_id: nextDefaultSourceId,
|
||||
sources: nextSources,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const addTvSource = () => {
|
||||
setTvSettings((prev) => {
|
||||
if (!prev) return prev
|
||||
const nextIndex = prev.sources.length + 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: '',
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
sources: [...prev.sources, newSource],
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const removeTvSource = (sourceId: string) => {
|
||||
setTvSettings((prev) => {
|
||||
if (!prev) return prev
|
||||
const nextSources = prev.sources.filter((source) => source.id !== sourceId)
|
||||
const nextDefaultSourceId =
|
||||
prev.default_source_id === sourceId ? nextSources[0]?.id || '' : prev.default_source_id
|
||||
return {
|
||||
...prev,
|
||||
default_source_id: nextDefaultSourceId,
|
||||
sources: nextSources,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const saveTvSettings = async () => {
|
||||
if (!tvSettings) return
|
||||
try {
|
||||
setSavingTvSettings(true)
|
||||
await axios.put('/api/v1/settings/tv', tvSettings)
|
||||
message.success('电视直播配置已保存')
|
||||
await fetchSettings()
|
||||
} catch (error) {
|
||||
message.error('电视直播配置保存失败')
|
||||
console.error(error)
|
||||
} finally {
|
||||
setSavingTvSettings(false)
|
||||
}
|
||||
}
|
||||
|
||||
const collectorColumns = [
|
||||
{
|
||||
title: '数据源',
|
||||
@@ -274,6 +397,133 @@ function Settings() {
|
||||
},
|
||||
]
|
||||
|
||||
const tvSourceColumns = [
|
||||
{
|
||||
title: '频道',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 220,
|
||||
render: (_: string, record: TVStreamSource) => (
|
||||
<div style={{ display: 'grid', gap: 8 }}>
|
||||
<Input value={record.name} onChange={(event) => updateTvSourceField(record.id, 'name', event.target.value)} />
|
||||
<Input
|
||||
value={record.provider}
|
||||
placeholder="提供方"
|
||||
onChange={(event) => updateTvSourceField(record.id, 'provider', event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '区域 / 语言',
|
||||
key: 'locale',
|
||||
width: 160,
|
||||
render: (_: unknown, record: TVStreamSource) => (
|
||||
<div style={{ display: 'grid', gap: 8 }}>
|
||||
<Input
|
||||
value={record.region}
|
||||
placeholder="区域"
|
||||
onChange={(event) => updateTvSourceField(record.id, 'region', event.target.value)}
|
||||
/>
|
||||
<Input
|
||||
value={record.language}
|
||||
placeholder="语言"
|
||||
onChange={(event) => updateTvSourceField(record.id, 'language', event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'source_type',
|
||||
key: 'source_type',
|
||||
width: 120,
|
||||
render: (value: TVStreamSource['source_type'], record: TVStreamSource) => (
|
||||
<Select
|
||||
value={value}
|
||||
style={{ width: '100%' }}
|
||||
onChange={(nextValue) => updateTvSourceField(record.id, 'source_type', nextValue)}
|
||||
options={[
|
||||
{ value: 'iframe', label: 'iframe' },
|
||||
{ value: 'hls', label: 'hls' },
|
||||
{ value: 'video', label: 'video' },
|
||||
{ value: 'youtube', label: 'youtube' },
|
||||
{ value: 'external', label: 'external' },
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '播放地址',
|
||||
key: 'urls',
|
||||
width: 320,
|
||||
render: (_: unknown, record: TVStreamSource) => (
|
||||
<div style={{ display: 'grid', gap: 8 }}>
|
||||
<Input
|
||||
value={record.embed_url}
|
||||
placeholder="嵌入地址 / iframe 地址"
|
||||
onChange={(event) => updateTvSourceField(record.id, 'embed_url', event.target.value)}
|
||||
/>
|
||||
<Input
|
||||
value={record.stream_url}
|
||||
placeholder="流地址 / HLS 地址"
|
||||
onChange={(event) => updateTvSourceField(record.id, 'stream_url', event.target.value)}
|
||||
/>
|
||||
<Input
|
||||
value={record.youtube_video_id}
|
||||
placeholder="YouTube 视频 ID(可选)"
|
||||
onChange={(event) => updateTvSourceField(record.id, 'youtube_video_id', event.target.value)}
|
||||
/>
|
||||
<Input
|
||||
value={record.youtube_channel}
|
||||
placeholder="YouTube 频道 Handle / URL(可选)"
|
||||
onChange={(event) => updateTvSourceField(record.id, 'youtube_channel', event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '官网',
|
||||
dataIndex: 'homepage_url',
|
||||
key: 'homepage_url',
|
||||
width: 220,
|
||||
render: (value: string, record: TVStreamSource) => (
|
||||
<Input value={value} onChange={(event) => updateTvSourceField(record.id, 'homepage_url', event.target.value)} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 110,
|
||||
render: (_: unknown, record: TVStreamSource) => (
|
||||
<div style={{ display: 'grid', gap: 8 }}>
|
||||
<Switch checked={record.is_enabled} onChange={(checked) => updateTvSourceField(record.id, 'is_enabled', checked)} />
|
||||
<Switch checked={record.is_fallback} onChange={(checked) => updateTvSourceField(record.id, 'is_fallback', checked)} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'notes',
|
||||
key: 'notes',
|
||||
width: 220,
|
||||
render: (value: string, record: TVStreamSource) => (
|
||||
<Input value={value} onChange={(event) => updateTvSourceField(record.id, 'notes', event.target.value)} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 90,
|
||||
fixed: 'right' as const,
|
||||
render: (_: unknown, record: TVStreamSource) => (
|
||||
<Button danger onClick={() => removeTvSource(record.id)} disabled={record.id === tvSettings?.default_source_id}>
|
||||
删除
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const tabItems = [
|
||||
{
|
||||
key: 'system',
|
||||
@@ -356,6 +606,58 @@ function Settings() {
|
||||
</SettingsPanel>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'tv',
|
||||
label: '电视直播',
|
||||
children: (
|
||||
<div className="settings-pane">
|
||||
<Card className="settings-panel-card settings-panel-card--table" loading={loading}>
|
||||
<div className="settings-panel-scroll" style={{ display: 'grid', gap: 16 }}>
|
||||
<div className="settings-tv-toolbar">
|
||||
<div className="settings-tv-toolbar__controls">
|
||||
<div className="settings-tv-field">
|
||||
<Text type="secondary">默认直播源</Text>
|
||||
<Select
|
||||
value={tvSettings?.default_source_id}
|
||||
style={{ minWidth: 260 }}
|
||||
options={(tvSettings?.sources || []).map((source) => ({
|
||||
value: source.id,
|
||||
label: source.name,
|
||||
}))}
|
||||
onChange={(value) => updateTvSetting('default_source_id', value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="settings-tv-field">
|
||||
<Text type="secondary">自动回退</Text>
|
||||
<Switch
|
||||
checked={tvSettings?.auto_fallback || false}
|
||||
onChange={(checked) => updateTvSetting('auto_fallback', checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-tv-toolbar__actions">
|
||||
<Button onClick={addTvSource}>新增直播源</Button>
|
||||
<Button type="primary" loading={savingTvSettings} onClick={saveTvSettings}>
|
||||
保存电视直播配置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="table-scroll-region data-source-table-region">
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={tvSourceColumns}
|
||||
dataSource={tvSettings?.sources || []}
|
||||
pagination={false}
|
||||
scroll={{ x: 1500, y: 420 }}
|
||||
tableLayout="fixed"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'collectors',
|
||||
label: '采集调度',
|
||||
|
||||
Reference in New Issue
Block a user