Files
MeBox/web/src/pages/SubscriptionsPage.tsx
T

164 lines
5.8 KiB
TypeScript

import { FormEvent, useEffect, useState } from 'react'
import toast from 'react-hot-toast'
import { subscriptionsAPI } from '../api/subscriptions'
import { confirmAction } from '../components/confirmAction'
import type { Subscription } from '../types'
import { SubscriptionCard } from './SubscriptionCard'
import { SubscriptionForm } from './SubscriptionForm'
import { SubscriptionHistorySection } from './SubscriptionHistorySection'
import { defaultSubscriptionFormValues, type SubscriptionFormValues } from './subscriptionFormModel'
export function SubscriptionsPage() {
const [items, setItems] = useState<Subscription[]>([])
const [historyItems, setHistoryItems] = useState<Subscription[]>([])
const [formValues, setFormValues] = useState<SubscriptionFormValues>(defaultSubscriptionFormValues)
const [editingId, setEditingId] = useState('')
const [loading, setLoading] = useState(true)
const refresh = () =>
Promise.all([subscriptionsAPI.list(), subscriptionsAPI.history()])
.then(([active, history]) => {
setItems(active)
setHistoryItems(history)
})
.finally(() => setLoading(false))
useEffect(() => {
refresh().catch(() => undefined)
}, [])
const onCreate = async (e: FormEvent) => {
e.preventDefault()
try {
const payload = {
name: formValues.name,
feed_url: formValues.feed,
filter: formValues.filter,
media_type: formValues.mediaType || undefined,
media_category: formValues.mediaCategory || undefined,
save_path: formValues.savePath || undefined,
search_mode: formValues.searchMode,
imdb_id: formValues.imdbID || undefined,
resolution: formValues.resolution,
// 规则类字段发送原始字符串(含空串),以便编辑时可清空/修改;create 存空串无害,update 才能真正生效。
quality: formValues.quality,
effects: formValues.effects,
release_groups: formValues.releaseGroups,
exclude_words: formValues.excludeWords,
wash_enabled: formValues.washEnabled,
wash_priority: formValues.washPriority,
priority: 50,
}
if (editingId) {
await subscriptionsAPI.update(editingId, payload)
toast.success('已更新订阅')
} else {
await subscriptionsAPI.create(payload)
toast.success('已创建订阅')
}
resetForm()
await refresh()
} catch (err: unknown) {
const msg =
(err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '创建失败'
toast.error(msg)
}
}
const resetForm = () => {
setEditingId('')
setFormValues(defaultSubscriptionFormValues)
}
const startEdit = (s: Subscription) => {
setEditingId(s.id)
setFormValues({
name: s.name,
feed: s.feed_url,
filter: s.filter || '',
mediaType: s.media_type || '',
mediaCategory: s.media_category || '',
savePath: s.save_path || '',
searchMode: s.search_mode || 'keyword',
imdbID: s.imdb_id || '',
resolution: s.resolution || 'best',
quality: s.quality || '',
effects: s.effects || '',
releaseGroups: s.release_groups || '',
excludeWords: s.exclude_words || '',
washEnabled: Boolean(s.wash_enabled),
washPriority: s.wash_priority || 'balanced',
})
window.scrollTo({ top: 0, behavior: 'smooth' })
}
const updateFormValue = <K extends keyof SubscriptionFormValues>(key: K, value: SubscriptionFormValues[K]) => {
setFormValues((current) => ({ ...current, [key]: value }))
}
const restoreHistorySubscription = async (subscription: Subscription, runAfterRestore = false) => {
try {
const restored = await subscriptionsAPI.restore(subscription.id)
if (runAfterRestore) {
const result = await subscriptionsAPI.runNow(restored.id)
toast.success(`已恢复订阅并加入 ${result.queued} 项`)
} else {
toast.success('已恢复到正在订阅')
}
await refresh()
} catch (err: unknown) {
const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error || '恢复订阅失败'
toast.error(msg)
}
}
const runSubscriptionNow = async (subscription: Subscription) => {
const result = await subscriptionsAPI.runNow(subscription.id)
toast.success(`已加入 ${result.queued} 项`)
}
const removeSubscription = async (subscription: Subscription) => {
if (!(await confirmAction({ title: '删除订阅', message: `删除订阅「${subscription.name}」?`, confirmText: '删除' }))) return
await subscriptionsAPI.remove(subscription.id)
toast.success('已删除')
await refresh()
}
return (
<div className="space-y-6">
<h1 className="font-display text-3xl font-bold text-ink-600">RSS 订阅</h1>
<p className="text-sm text-ink-50">
定期轮询 RSS 源(每 10 分钟一次),将匹配过滤器的项目自动加入下载队列;启用智能分类后会按二级分类写入下载目录。
</p>
<SubscriptionForm
values={formValues}
editing={Boolean(editingId)}
onSubmit={onCreate}
onCancelEdit={resetForm}
onChange={updateFormValue}
/>
{loading && <p className="text-sand-500">加载中…</p>}
{!loading && items.length === 0 && <p className="text-ink-50">暂无订阅。</p>}
{items.length > 0 && (
<div className="grid gap-5 sm:grid-cols-2 xl:grid-cols-3">
{items.map((subscription) => (
<SubscriptionCard
key={subscription.id}
subscription={subscription}
onEdit={startEdit}
onRunNow={runSubscriptionNow}
onRemove={removeSubscription}
/>
))}
</div>
)}
<SubscriptionHistorySection subscriptions={historyItems} onRestore={restoreHistorySubscription} />
</div>
)
}