diff --git a/src/api/types.ts b/src/api/types.ts index 603ad696..55be9d1a 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -100,6 +100,8 @@ export interface ManualScrapeOptions { // 订阅 export interface Subscribe { + /** 独立定时搜索间隔(小时);null 或缺省跟随系统。 */ + search_interval?: number | null // 订阅ID id: number // 订阅名称 diff --git a/src/components/dialog/SubscribeEditDialog.vue b/src/components/dialog/SubscribeEditDialog.vue index 6ea2a009..40ad20e5 100644 --- a/src/components/dialog/SubscribeEditDialog.vue +++ b/src/components/dialog/SubscribeEditDialog.vue @@ -78,6 +78,7 @@ const subscribeForm = ref({ media_source: undefined, media_id: undefined, state: '', + search_interval: null, last_update: '', username: '', sites: [], @@ -90,6 +91,20 @@ const subscribeForm = ref({ episode_group: '', }) +// 切换为系统周期时显式提交 null,确保覆盖已保存的独立周期。 +const customSearchInterval = computed({ + get: () => subscribeForm.value.search_interval != null, + set: value => { + subscribeForm.value.search_interval = value ? 24 : null + }, +}) + +// 空输入和非整数不能保存;接口采用同样的小时范围。 +const searchIntervalValid = computed(() => { + const interval = subscribeForm.value.search_interval + return interval == null || (Number.isInteger(Number(interval)) && Number(interval) >= 1 && Number(interval) <= 8760) +}) + // 提示框 const $toast = useToast() @@ -190,6 +205,7 @@ const filterRuleGroupOptions = computed(() => { // 调用API修改订阅 async function updateSubscribeInfo() { + if (!searchIntervalValid.value) return const displayName = getSubscribeDisplayName() try { await api.put('subscribe/', subscribeForm.value, { feedback: 'silent' }) @@ -209,7 +225,7 @@ async function updateSubscribeInfo() { // 设置用户设置的默认订阅规则 async function saveDefaultSubscribeConfig() { - if (!canAdmin.value) return + if (!canAdmin.value || !searchIntervalValid.value) return const typeName = getDefaultSubscribeTypeName() try { @@ -523,6 +539,32 @@ onMounted(() => { /> + + + + + + + + { { expect(screen.getByText('2 季 • 24 集')).toBeInTheDocument() }) + it('saves a custom search interval and explicitly restores the system setting', async () => { + const updated = vi.fn() + server.use( + subscribeDetailsHandler( + 820, + createSubscribe({ id: 820, name: '搜索周期测试', search_interval: null, type: '电影' }), + ), + updateSubscribeHandler({ success: true }, 200, updated), + ) + useDialogOptions() + const user = userEvent.setup() + await renderDialog({ subid: 820 }) + await screen.findByText('搜索周期测试') + await user.click(screen.getByLabelText('搜索周期')) + await user.click(await screen.findByRole('option', { name: '自定义' })) + await fireEvent.update(screen.getByLabelText('搜索间隔(小时)'), '6') + await user.click(screen.getByRole('button', { name: '保存' })) + await waitFor(() => expect(updated).toHaveBeenCalledOnce()) + expect(updated.mock.calls[0][0]).toMatchObject({ search_interval: 6 }) + + await user.click(screen.getByLabelText('搜索周期')) + await user.click(await screen.findByRole('option', { name: '跟随系统' })) + expect(screen.queryByLabelText('搜索间隔(小时)')).not.toBeInTheDocument() + await user.click(screen.getByRole('button', { name: '保存' })) + await waitFor(() => expect(updated).toHaveBeenCalledTimes(2)) + expect(updated.mock.calls[1][0]).toMatchObject({ search_interval: null }) + }) + + it('loads a saved custom interval and prevents invalid values from being submitted', async () => { + const updated = vi.fn() + server.use( + subscribeDetailsHandler( + 821, + createSubscribe({ id: 821, name: '周期校验测试', search_interval: 48, type: '电影' }), + ), + updateSubscribeHandler({ success: true }, 200, updated), + ) + useDialogOptions() + await renderDialog({ subid: 821 }) + expect(await screen.findByLabelText('搜索间隔(小时)')).toHaveValue(48) + for (const value of ['', '0', '-1', '1.5', '8761']) { + await fireEvent.update(screen.getByLabelText('搜索间隔(小时)'), value) + expect(screen.getByRole('button', { name: '保存' })).toBeDisabled() + } + expect(updated).not.toHaveBeenCalled() + await fireEvent.update(screen.getByLabelText('搜索间隔(小时)'), '24') + expect(screen.getByRole('button', { name: '保存' })).toBeEnabled() + }) + it('keeps movie titles free of season suffixes and skips episode groups', async () => { const record = createSubscribe({ id: 802, media_id: '8020', name: '电影测试项', season: undefined, type: '电影' }) const episodeGroupsRequested = vi.fn() diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index 2188b523..03b84e10 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -4158,6 +4158,13 @@ export default { bestVersionFull: 'Full', }, subscribeEdit: { + searchInterval: 'Search schedule', + searchIntervalSystem: 'Use system setting', + searchIntervalCustom: 'Custom', + searchIntervalHours: 'Search interval (hours)', + searchIntervalHint: + 'Requires scheduled subscription search to be enabled. Manual searches and RSS refreshes are unaffected.', + searchIntervalInvalid: 'Enter a whole number of hours from 1 to 8760', titleDefault: 'Default Subscription Rules', titleEdit: 'Edit Subscription', seasonFormat: 'Season {number}', diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index ecaf6700..3a5a932d 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -4067,6 +4067,12 @@ export default { bestVersionFull: '全集洗版', }, subscribeEdit: { + searchInterval: '搜索周期', + searchIntervalSystem: '跟随系统', + searchIntervalCustom: '自定义', + searchIntervalHours: '搜索间隔(小时)', + searchIntervalHint: '仅在开启订阅定时搜索时生效;手动搜索和 RSS 刷新不受影响。', + searchIntervalInvalid: '请输入 1–8760 的整数小时数', titleDefault: '默认订阅规则', titleEdit: '编辑订阅', seasonFormat: '第 {number} 季', diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts index a68639d3..c8cb9a9c 100644 --- a/src/locales/zh-TW.ts +++ b/src/locales/zh-TW.ts @@ -4036,6 +4036,12 @@ export default { bestVersionFull: '全集洗版', }, subscribeEdit: { + searchInterval: '搜尋週期', + searchIntervalSystem: '跟隨系統', + searchIntervalCustom: '自訂', + searchIntervalHours: '搜尋間隔(小時)', + searchIntervalHint: '僅在開啟訂閱定時搜尋時生效;手動搜尋與 RSS 更新不受影響。', + searchIntervalInvalid: '請輸入 1–8760 的整數小時數', titleDefault: '默認訂閱規則', titleEdit: '編輯訂閱', seasonFormat: '第 {number} 季',