From ec4eaed1df0b105dacf7f2de8d1eed49200f8c03 Mon Sep 17 00:00:00 2001 From: InfinityPacer <160988576+InfinityPacer@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:30:24 +0800 Subject: [PATCH] fix: refine subscribe mode selection (#501) * fix(subscribe): refine subscribe mode selection * fix(subscribe): prompt mode for single existing season --- src/components/cards/MediaCard.vue | 69 +++++++++++++---- src/components/dialog/SubscribeEditDialog.vue | 19 +++-- src/components/dialog/SubscribeModeDialog.vue | 62 +++++++++++++++ src/locales/en-US.ts | 7 ++ src/locales/zh-CN.ts | 7 ++ src/locales/zh-TW.ts | 7 ++ src/views/discover/MediaDetailView.vue | 76 ++++++++++++++++--- 7 files changed, 214 insertions(+), 33 deletions(-) create mode 100644 src/components/dialog/SubscribeModeDialog.vue diff --git a/src/components/cards/MediaCard.vue b/src/components/cards/MediaCard.vue index 2aeb9fd3..4c4f268d 100644 --- a/src/components/cards/MediaCard.vue +++ b/src/components/cards/MediaCard.vue @@ -12,6 +12,7 @@ import { useI18n } from 'vue-i18n' import { mediaTypeDict } from '@/api/constants' import { buildUserPermissionContext, hasPermission } from '@/utils/permission' import { openSharedDialog } from '@/composables/useSharedDialog' +import { useConfirm } from '@/composables/useConfirm' import { getCachedMediaExistsStatus, getCachedMediaSubscribeStatus, @@ -21,6 +22,7 @@ import { const SearchSiteDialog = defineAsyncComponent(() => import('@/components/dialog/SearchSiteDialog.vue')) const SubscribeEditDialog = defineAsyncComponent(() => import('../dialog/SubscribeEditDialog.vue')) +const SubscribeModeDialog = defineAsyncComponent(() => import('../dialog/SubscribeModeDialog.vue')) const SubscribeSeasonDialog = defineAsyncComponent(() => import('../dialog/SubscribeSeasonDialog.vue')) // 国际化 @@ -51,6 +53,7 @@ const canSubscribe = computed(() => hasPermission(userPermissions.value, 'subscr // 提示框 const $toast = useToast() +const createConfirm = useConfirm() // 图片加载状态 const isImageLoaded = ref(false) @@ -191,18 +194,30 @@ async function handleAddSubscribe() { seasonsSelected.value = [] openSubscribeSeasonDialog() } else { - // 电影 - addSubscribe() + if (isExists.value) { + openSharedDialog( + SubscribeModeDialog, + { modes: ['normal', 'best_version'], type: props.media?.type }, + { + choose: (mode: string) => + addSubscribe(null, { + best_version: mode === 'normal' ? 0 : 1, + best_version_full: 0, + }), + }, + { closeOn: ['close', 'choose'] }, + ) + } else { + addSubscribe() + } } } // 调用API添加订阅,电视剧的话需要指定季 -async function addSubscribe(season: number | null = null, best_version: number = 0) { +async function addSubscribe(season: number | null = null, payload: { best_version?: number; best_version_full?: number } = {}) { // 开始处理 startNProgress() try { - // 是否洗版 - if (!best_version && props.media?.type == '电影') best_version = isExists.value ? 1 : 0 // 请求API const result: { [key: string]: any } = await api.post('subscribe/', { name: props.media?.title, @@ -213,7 +228,7 @@ async function addSubscribe(season: number | null = null, best_version: number = bangumiid: props.media?.bangumi_id, mediaid: props.media?.media_id ? `${props.media?.mediaid_prefix}:${props.media?.media_id}` : '', season: props.media?.type === '电影' ? null : season, - best_version, + ...payload, episode_group: episodeGroup.value, }) @@ -225,7 +240,7 @@ async function addSubscribe(season: number | null = null, best_version: number = } // 提示 - showSubscribeAddToast(result.success, props.media?.title ?? '', season, result.message, best_version) + showSubscribeAddToast(result.success, props.media?.title ?? '', season, result.message, payload.best_version ?? 0) // 弹出订阅编辑弹窗 if (result.success && seasonsSelected.value.length <= 1) { @@ -254,6 +269,12 @@ function showSubscribeAddToast(result: boolean, title: string, season: number | // 调用API取消订阅 async function removeSubscribe() { + const confirmed = await createConfirm({ + title: t('common.confirm'), + content: t('dialog.subscribeEdit.cancelSubscribeConfirm'), + }) + if (!confirmed) return + // 开始处理 startNProgress() try { @@ -264,13 +285,16 @@ async function removeSubscribe() { season: props.media?.season, }, }) + let title = props.media?.title ?? '' + if (props.media?.season !== null && props.media?.season !== undefined) + title = `${title} ${formatSeason(props.media.season.toString())}` if (result.success) { isSubscribed.value = false setCachedMediaSubscribeStatus(getSubscribeStatusKey(props.media?.season ?? null), false) - $toast.success(`${props.media?.title} ${t('subscribe.cancelSuccess')}`) + $toast.success(`${title} ${t('subscribe.cancelSuccess')}`) } else { - $toast.error(`${props.media?.title} ${t('subscribe.cancelFailed', { message: result.message })}`) + $toast.error(`${title} ${t('subscribe.cancelFailed', { message: result.message })}`) } } catch (error) { console.error(error) @@ -362,12 +386,29 @@ function handleSubscribe() { function subscribeSeasons(seasons: MediaSeason[], seasonNoExists: { [key: number]: number }, groudId: string) { episodeGroup.value = groudId seasonsSelected.value = seasons || [] + if (seasonsSelected.value.length === 1) { + const seasonNumber = seasonsSelected.value[0]?.season_number ?? null + if (seasonNumber !== null && !seasonNoExists[seasonNumber]) { + openSharedDialog( + SubscribeModeDialog, + { modes: ['normal', 'best_version', 'best_version_full'], type: props.media?.type }, + { + choose: (mode: string) => + addSubscribe(seasonNumber, { + best_version: mode === 'normal' ? 0 : 1, + best_version_full: mode === 'best_version_full' ? 1 : 0, + }), + }, + { closeOn: ['close', 'choose'] }, + ) + return + } + } seasonsSelected.value.forEach(season => { - let best_version = 0 - if (season && props.media?.tmdb_id) - // 全部存在时洗版 - best_version = !seasonNoExists[season.season_number || 0] ? 1 : 0 - addSubscribe(season.season_number ?? null, best_version) + const seasonNumber = season.season_number ?? null + const payload = + seasonNumber !== null && !seasonNoExists[seasonNumber] ? { best_version: 1, best_version_full: 1 } : {} + addSubscribe(seasonNumber, payload) }) } diff --git a/src/components/dialog/SubscribeEditDialog.vue b/src/components/dialog/SubscribeEditDialog.vue index 4be7a9f7..b6189a8a 100644 --- a/src/components/dialog/SubscribeEditDialog.vue +++ b/src/components/dialog/SubscribeEditDialog.vue @@ -9,6 +9,7 @@ import { useI18n } from 'vue-i18n' import { qualityOptions, resolutionOptions, effectOptions } from '@/api/constants' import { useUserStore } from '@/stores' import { buildUserPermissionContext, hasPermission } from '@/utils/permission' +import { formatSeason } from '@/@core/utils/formatters' // i18n const { t } = useI18n() const userStore = useUserStore() @@ -96,6 +97,13 @@ const seasonItems = ref( })), ) +function getSubscribeDisplayName() { + const name = subscribeForm.value.name || '' + const season = subscribeForm.value.season + if (season === null || season === undefined) return name + return `${name} ${formatSeason(season.toString())}` +} + // 剧集组选项属性 function episodeGroupItemProps(item: { title: string; subtitle: string }) { return { @@ -158,11 +166,11 @@ async function updateSubscribeInfo() { const result: { [key: string]: any } = await api.put('subscribe/', subscribeForm.value) // 提示 if (result.success) { - $toast.success(`${subscribeForm.value.name} 更新成功!`) + $toast.success(`${getSubscribeDisplayName()} 更新成功!`) // 通知父组件刷新 emit('save') } else { - $toast.error(`${subscribeForm.value.name} 更新失败:${result.message}!`) + $toast.error(`${getSubscribeDisplayName()} 更新失败:${result.message}!`) } } catch (e) { console.log(e) @@ -258,7 +266,7 @@ async function removeSubscribe() { const result: { [key: string]: any } = await api.delete(`subscribe/${props.subid}`) if (result.success) { - $toast.success(`订阅 ${subscribeForm.value.name} 已取消!`) + $toast.success(`订阅 ${getSubscribeDisplayName()} 已取消!`) // 通知父组件刷新 emit('remove') } @@ -317,10 +325,7 @@ onMounted(() => { {{ props.default ? t('dialog.subscribeEdit.titleDefault') : t('dialog.subscribeEdit.titleEdit') }} - {{ subscribeForm.name }} - - {{ t('dialog.subscribeEdit.seasonFormat', { number: subscribeForm.season }) }} - + {{ getSubscribeDisplayName() }} {{ props.type }} diff --git a/src/components/dialog/SubscribeModeDialog.vue b/src/components/dialog/SubscribeModeDialog.vue new file mode 100644 index 00000000..9b91a26e --- /dev/null +++ b/src/components/dialog/SubscribeModeDialog.vue @@ -0,0 +1,62 @@ + + + diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index 12b69ef7..92b290d5 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -2814,6 +2814,13 @@ export default { processing: 'Processing ...', successMessage: 'File {name} has been added to the organization queue!', }, + subscribeMode: { + title: 'Choose Subscription Type', + normal: 'Subscribe', + bestVersion: 'Version Upgrade', + bestVersionEpisode: 'Episode Upgrade', + bestVersionFull: 'Full Season Upgrade', + }, subscribeEdit: { titleDefault: 'Default Subscription Rules', titleEdit: 'Edit Subscription', diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index 9227bcd8..527b8e68 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -2763,6 +2763,13 @@ export default { processing: '正在处理 ...', successMessage: '文件 {name} 已加入整理队列!', }, + subscribeMode: { + title: '选择订阅方式', + normal: '普通订阅', + bestVersion: '洗版', + bestVersionEpisode: '分集洗版', + bestVersionFull: '全集洗版', + }, subscribeEdit: { titleDefault: '默认订阅规则', titleEdit: '编辑订阅', diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts index 3ac2ae96..869278ca 100644 --- a/src/locales/zh-TW.ts +++ b/src/locales/zh-TW.ts @@ -2764,6 +2764,13 @@ export default { processing: '正在處理 ...', successMessage: '文件 {name} 已加入整理隊列!', }, + subscribeMode: { + title: '選擇訂閱方式', + normal: '普通訂閱', + bestVersion: '洗版', + bestVersionEpisode: '分集洗版', + bestVersionFull: '全集洗版', + }, subscribeEdit: { titleDefault: '默認訂閱規則', titleEdit: '編輯訂閱', diff --git a/src/views/discover/MediaDetailView.vue b/src/views/discover/MediaDetailView.vue index 78cebe6d..4297f6c8 100644 --- a/src/views/discover/MediaDetailView.vue +++ b/src/views/discover/MediaDetailView.vue @@ -14,6 +14,7 @@ import { useUserStore } from '@/stores' import { useTheme } from 'vuetify' import { useI18n } from 'vue-i18n' import { buildUserPermissionContext, hasPermission } from '@/utils/permission' +import { useConfirm } from '@/composables/useConfirm' import { useGlobalSettingsStore } from '@/stores' import { openMediaServerItem, openDoubanApp } from '@/utils/appDeepLink' import { openSharedDialog } from '@/composables/useSharedDialog' @@ -21,6 +22,7 @@ import { getDisplayImageUrl } from '@/utils/imageUtils' const SearchSiteDialog = defineAsyncComponent(() => import('@/components/dialog/SearchSiteDialog.vue')) const SubscribeEditDialog = defineAsyncComponent(() => import('@/components/dialog/SubscribeEditDialog.vue')) +const SubscribeModeDialog = defineAsyncComponent(() => import('@/components/dialog/SubscribeModeDialog.vue')) // 国际化 const { t } = useI18n() @@ -46,6 +48,7 @@ const canSubscribe = computed(() => hasPermission(userPermissions.value, 'subscr // 提示框 const $toast = useToast() +const createConfirm = useConfirm() // 获取主题信息 const theme = useTheme() @@ -293,15 +296,10 @@ async function checkSeasonsSubscribed() { } // 调用API添加订阅,电视剧的话需要指定季 -async function addSubscribe(season: number | null) { +async function addSubscribe(season: number | null, payload: { best_version?: number; best_version_full?: number } = {}) { // 开始处理 startNProgress() try { - // 是否洗版 - let best_version = existsItemId.value ? 1 : 0 - if (season !== null) - // 全部存在时洗版 - best_version = !seasonsNotExisted.value[season] ? 1 : 0 // 请求API const result: { [key: string]: any } = await api.post('subscribe/', { name: mediaDetail.value?.title, @@ -311,7 +309,7 @@ async function addSubscribe(season: number | null) { doubanid: mediaDetail.value?.douban_id, bangumiid: mediaDetail.value?.bangumi_id, season: mediaDetail.value?.type === '电影' ? null : season, - best_version, + ...payload, }) // 订阅状态 @@ -322,7 +320,7 @@ async function addSubscribe(season: number | null) { } // 提示 - showSubscribeAddToast(result.success, mediaDetail.value?.title ?? '', season, result.message, best_version) + showSubscribeAddToast(result.success, mediaDetail.value?.title ?? '', season, result.message, payload.best_version ?? 0) // 显示编辑弹窗 if (result.success) { @@ -350,6 +348,12 @@ function showSubscribeAddToast(result: boolean, title: string, season: number | // 调用API取消订阅 async function removeSubscribe(season: number | null) { + const confirmed = await createConfirm({ + title: t('common.confirm'), + content: t('dialog.subscribeEdit.cancelSubscribeConfirm'), + }) + if (!confirmed) return + // 开始处理 startNProgress() try { @@ -360,13 +364,15 @@ async function removeSubscribe(season: number | null) { season, }, }) + let title = mediaDetail.value?.title ?? '' + if (season !== null) title = `${title} ${formatSeason(season.toString())}` if (result.success) { isSubscribed.value = false if (season !== null) seasonsSubscribed.value[season] = false - $toast.success(`${mediaDetail.value?.title} ${t('media.subscribe.canceled')}`) + $toast.success(`${title} ${t('media.subscribe.canceled')}`) } else { - $toast.error(`${mediaDetail.value?.title} ${t('media.subscribe.cancelFailed', { reason: result.message })}`) + $toast.error(`${title} ${t('media.subscribe.cancelFailed', { reason: result.message })}`) } } catch (error) { console.error(error) @@ -376,8 +382,54 @@ async function removeSubscribe(season: number | null) { // 订阅按钮响应 function handleSubscribe(season: number | null = null) { - if (isSubscribed.value) removeSubscribe(season) - else addSubscribe(season) + if (season !== null) { + if (seasonsSubscribed.value[season]) { + removeSubscribe(season) + return + } + + if (!seasonsNotExisted.value[season]) { + openSharedDialog( + SubscribeModeDialog, + { modes: ['normal', 'best_version', 'best_version_full'], type: mediaDetail.value?.type }, + { + choose: (mode: string) => + addSubscribe(season, { + best_version: mode === 'normal' ? 0 : 1, + best_version_full: mode === 'best_version_full' ? 1 : 0, + }), + }, + { closeOn: ['close', 'choose'] }, + ) + return + } + + addSubscribe(season) + return + } + + if (isSubscribed.value) { + removeSubscribe(season) + return + } + + if (mediaDetail.value?.type === '电影' && existsItemId.value) { + openSharedDialog( + SubscribeModeDialog, + { modes: ['normal', 'best_version'], type: mediaDetail.value?.type }, + { + choose: (mode: string) => + addSubscribe(season, { + best_version: mode === 'normal' ? 0 : 1, + best_version_full: 0, + }), + }, + { closeOn: ['close', 'choose'] }, + ) + return + } + + addSubscribe(season) } // 从genres中获取name,使用、分隔