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 @@
+
+
+
+
+
+
+ {{ t('dialog.subscribeMode.title') }}
+
+
+
+
+
+
+
+
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,使用、分隔