From 64a7f78e1b82eb52217dcd776691a4f366f38cc7 Mon Sep 17 00:00:00 2001 From: ShukeBta <272197458+ShukeBta@users.noreply.github.com> Date: Thu, 25 Jun 2026 04:57:33 +0800 Subject: [PATCH] refactor: split discover detail modal sections --- web/src/pages/DiscoverDetailModal.tsx | 207 +++--------------- web/src/pages/DiscoverDetailModalSections.tsx | 172 +++++++++++++++ web/src/pages/discoverDetailModalModel.ts | 92 ++++++++ 3 files changed, 289 insertions(+), 182 deletions(-) create mode 100644 web/src/pages/DiscoverDetailModalSections.tsx create mode 100644 web/src/pages/discoverDetailModalModel.ts diff --git a/web/src/pages/DiscoverDetailModal.tsx b/web/src/pages/DiscoverDetailModal.tsx index 773338f..fc29f62 100644 --- a/web/src/pages/DiscoverDetailModal.tsx +++ b/web/src/pages/DiscoverDetailModal.tsx @@ -1,64 +1,30 @@ import { useState } from 'react' import toast from 'react-hot-toast' -import { Download, Rss, X } from 'lucide-react' import type { DiscoverItem } from '../api/discover' -import { imageURL } from '../api/client' -import { buildSiteSearchFeedURL, buildSubscriptionAliases, subscriptionsAPI } from '../api/subscriptions' -import { buildSubscribeKeyword, discoverItemSource } from './discoverPageModel' +import { subscriptionsAPI } from '../api/subscriptions' +import { discoverItemSource } from './discoverPageModel' +import { + DiscoverArtworkPanel, + DiscoverModalHeader, + DiscoverOverviewPanel, + DiscoverSubscriptionRules, +} from './DiscoverDetailModalSections' +import { + apiErrorMessage, + buildDiscoverSubscriptionInput, + initialDiscoverSubscriptionForm, +} from './discoverDetailModalModel' export function DiscoverDetailModal({ item, onClose }: { item: DiscoverItem; onClose: () => void }) { const source = discoverItemSource(item) - const keyword = item.subscribe_keyword || buildSubscribeKeyword(item) - const [form, setForm] = useState({ - keyword, - search_mode: 'keyword', - imdb_id: '', - media_type: item.media_type || '', - resolution: 'best', - quality: '', - effects: '', - release_groups: '', - exclude_words: 'cam,ts,tc,枪版', - wash_enabled: false, - wash_priority: 'balanced', - save_path: '', - media_category: '', - priority: 50, - run_now: true, - }) + const [form, setForm] = useState(() => initialDiscoverSubscriptionForm(item)) const [busy, setBusy] = useState(false) const submit = async () => { - const finalKeyword = form.keyword.trim() || keyword - const feed = buildSiteSearchFeedURL(finalKeyword, source, buildSubscriptionAliases(item)) setBusy(true) try { - const sub = await subscriptionsAPI.create({ - name: `${item.title} 自动订阅`, - feed_url: feed, - filter: finalKeyword, - media_type: form.media_type || undefined, - media_category: form.media_category || undefined, - save_path: form.save_path || undefined, - search_mode: form.search_mode, - imdb_id: form.imdb_id || undefined, - source, - poster_url: item.poster_url || undefined, - backdrop_url: item.backdrop_url || undefined, - overview: item.overview || undefined, - original_name: item.original_name || undefined, - year: item.year || undefined, - resolution: form.resolution === 'best' ? 'best' : form.resolution, - quality: form.quality || undefined, - effects: form.effects || undefined, - release_groups: form.release_groups || undefined, - exclude_words: form.exclude_words || undefined, - wash_enabled: form.wash_enabled, - wash_priority: form.wash_priority, - priority: form.priority, - enabled: true, - }) + const sub = await subscriptionsAPI.create(buildDiscoverSubscriptionInput(item, form, source)) if (form.run_now) { const run = await subscriptionsAPI.runNow(sub.id) toast.success(run.queued > 0 ? `已订阅并加入 ${run.queued} 个下载` : '已订阅,暂未命中可下载资源') @@ -67,10 +33,7 @@ export function DiscoverDetailModal({ item, onClose }: { item: DiscoverItem; onC } onClose() } catch (err) { - const msg = - (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? - '订阅失败' - toast.error(msg) + toast.error(apiErrorMessage(err, '订阅失败')) } finally { setBusy(false) } @@ -79,137 +42,17 @@ export function DiscoverDetailModal({ item, onClose }: { item: DiscoverItem; onC return (
-
-
-

{source}

-

{item.title}

-

- {[item.media_type, item.year && item.year > 0 ? item.year : '', item.rating ? `★ ${item.rating.toFixed(1)}` : ''] - .filter(Boolean) - .join(' · ')} -

-
- -
- +
-
-
- {item.poster_url ? ( - {item.title} - ) : ( -
无海报
- )} -
- {item.backdrop_url && ( - - )} -
- +
-
-

简介

-

{item.overview || '当前数据源没有返回简介。'}

-
- -
-

- - 订阅下载规则 -

-
- - - - - - - - - - - - - -
-
- - -
-
+ + setForm((current) => ({ ...current, ...patch }))} + onSubmit={submit} + />
diff --git a/web/src/pages/DiscoverDetailModalSections.tsx b/web/src/pages/DiscoverDetailModalSections.tsx new file mode 100644 index 0000000..6221828 --- /dev/null +++ b/web/src/pages/DiscoverDetailModalSections.tsx @@ -0,0 +1,172 @@ +import { Download, Rss, X } from 'lucide-react' + +import type { DiscoverItem } from '../api/discover' +import { imageURL } from '../api/client' +import { discoverItemMetaText, type DiscoverSubscriptionForm } from './discoverDetailModalModel' + +type FormPatch = Partial + +type FieldOption = { + label: string + value: string +} + +const searchModeOptions: FieldOption[] = [ + { value: 'keyword', label: '标题关键词' }, + { value: 'imdb', label: 'IMDB ID' }, +] + +const mediaTypeOptions: FieldOption[] = [ + { value: '', label: '自动识别' }, + { value: 'movie', label: '电影' }, + { value: 'tv', label: '电视剧' }, + { value: 'anime', label: '动漫' }, + { value: 'variety', label: '综艺' }, +] + +const resolutionOptions: FieldOption[] = [ + { value: 'best', label: '自动择优' }, + { value: '2160p', label: '2160p / 4K' }, + { value: '1080p', label: '1080p' }, + { value: '720p', label: '720p' }, +] + +const qualityOptions: FieldOption[] = [ + { value: '', label: '不限' }, + { value: 'remux', label: 'REMUX' }, + { value: 'bluray', label: 'BluRay' }, + { value: 'web-dl', label: 'WEB-DL' }, + { value: 'hdtv', label: 'HDTV' }, +] + +const washPriorityOptions: FieldOption[] = [ + { value: 'balanced', label: '均衡' }, + { value: 'resolution', label: '分辨率优先' }, + { value: 'quality', label: '片源质量优先' }, + { value: 'effects', label: 'HDR/DV/Atmos 优先' }, + { value: 'seeders', label: '做种数优先' }, +] + +export function DiscoverModalHeader({ item, source, onClose }: { item: DiscoverItem; source: string; onClose: () => void }) { + return ( +
+
+

{source}

+

{item.title}

+

{discoverItemMetaText(item)}

+
+ +
+ ) +} + +export function DiscoverArtworkPanel({ item }: { item: DiscoverItem }) { + return ( +
+
+ {item.poster_url ? ( + {item.title} + ) : ( +
无海报
+ )} +
+ {item.backdrop_url && ( + + )} +
+ ) +} + +export function DiscoverOverviewPanel({ overview }: { overview?: string }) { + return ( +
+

简介

+

{overview || '当前数据源没有返回简介。'}

+
+ ) +} + +export function DiscoverSubscriptionRules({ + form, + busy, + onChange, + onSubmit, +}: { + form: DiscoverSubscriptionForm + busy: boolean + onChange: (patch: FormPatch) => void + onSubmit: () => void +}) { + return ( +
+

+ + 订阅下载规则 +

+
+ onChange({ keyword })} className="md:col-span-2" /> + onChange({ search_mode })} /> + onChange({ imdb_id })} /> + onChange({ media_type })} /> + onChange({ resolution })} /> + onChange({ quality })} /> + onChange({ effects })} /> + onChange({ wash_priority })} /> + onChange({ wash_enabled })} /> + onChange({ release_groups })} /> + onChange({ exclude_words })} /> + onChange({ media_category })} /> + onChange({ save_path })} /> +
+ onChange({ run_now })} onSubmit={onSubmit} /> +
+ ) +} + +function TextField({ label, value, placeholder, className = '', onChange }: { label: string; value: string; placeholder?: string; className?: string; onChange: (value: string) => void }) { + return ( + + ) +} + +function SelectField({ label, value, options, disabled, onChange }: { label: string; value: string; options: FieldOption[]; disabled?: boolean; onChange: (value: string) => void }) { + return ( + + ) +} + +function CheckboxField({ label, checked, onChange }: { label: string; checked: boolean; onChange: (checked: boolean) => void }) { + return ( + + ) +} + +function DiscoverSubmitBar({ busy, runNow, onRunNowChange, onSubmit }: { busy: boolean; runNow: boolean; onRunNowChange: (value: boolean) => void; onSubmit: () => void }) { + return ( +
+ + +
+ ) +} diff --git a/web/src/pages/discoverDetailModalModel.ts b/web/src/pages/discoverDetailModalModel.ts new file mode 100644 index 0000000..88d9b6a --- /dev/null +++ b/web/src/pages/discoverDetailModalModel.ts @@ -0,0 +1,92 @@ +import type { DiscoverItem } from '../api/discover' +import { buildSiteSearchFeedURL, buildSubscriptionAliases } from '../api/subscriptions' +import { buildSubscribeKeyword } from './discoverPageModel' + +export type DiscoverSubscriptionForm = { + keyword: string + search_mode: string + imdb_id: string + media_type: string + resolution: string + quality: string + effects: string + release_groups: string + exclude_words: string + wash_enabled: boolean + wash_priority: string + save_path: string + media_category: string + priority: number + run_now: boolean +} + +export function discoverSubscriptionKeyword(item: DiscoverItem): string { + return item.subscribe_keyword || buildSubscribeKeyword(item) +} + +export function initialDiscoverSubscriptionForm(item: DiscoverItem): DiscoverSubscriptionForm { + return { + keyword: discoverSubscriptionKeyword(item), + search_mode: 'keyword', + imdb_id: '', + media_type: item.media_type || '', + resolution: 'best', + quality: '', + effects: '', + release_groups: '', + exclude_words: 'cam,ts,tc,枪版', + wash_enabled: false, + wash_priority: 'balanced', + save_path: '', + media_category: '', + priority: 50, + run_now: true, + } +} + +export function discoverItemMetaText(item: DiscoverItem): string { + return [ + item.media_type, + item.year && item.year > 0 ? item.year : '', + item.rating ? `★ ${item.rating.toFixed(1)}` : '', + ] + .filter(Boolean) + .join(' · ') +} + +export function buildDiscoverSubscriptionInput( + item: DiscoverItem, + form: DiscoverSubscriptionForm, + source: string, +) { + const finalKeyword = form.keyword.trim() || discoverSubscriptionKeyword(item) + return { + name: `${item.title} 自动订阅`, + feed_url: buildSiteSearchFeedURL(finalKeyword, source, buildSubscriptionAliases(item)), + filter: finalKeyword, + media_type: form.media_type || undefined, + media_category: form.media_category || undefined, + save_path: form.save_path || undefined, + search_mode: form.search_mode, + imdb_id: form.imdb_id || undefined, + source, + poster_url: item.poster_url || undefined, + backdrop_url: item.backdrop_url || undefined, + overview: item.overview || undefined, + original_name: item.original_name || undefined, + year: item.year || undefined, + resolution: form.resolution === 'best' ? 'best' : form.resolution, + quality: form.quality || undefined, + effects: form.effects || undefined, + release_groups: form.release_groups || undefined, + exclude_words: form.exclude_words || undefined, + wash_enabled: form.wash_enabled, + wash_priority: form.wash_priority, + priority: form.priority, + enabled: true, + } +} + +export function apiErrorMessage(err: unknown, fallback: string): string { + return (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? fallback +}