fix: refine subscribe mode selection (#501)

* fix(subscribe): refine subscribe mode selection

* fix(subscribe): prompt mode for single existing season
This commit is contained in:
InfinityPacer
2026-06-25 11:30:24 +08:00
committed by GitHub
parent 175f610524
commit ec4eaed1df
7 changed files with 214 additions and 33 deletions

View File

@@ -12,6 +12,7 @@ import { useI18n } from 'vue-i18n'
import { mediaTypeDict } from '@/api/constants' import { mediaTypeDict } from '@/api/constants'
import { buildUserPermissionContext, hasPermission } from '@/utils/permission' import { buildUserPermissionContext, hasPermission } from '@/utils/permission'
import { openSharedDialog } from '@/composables/useSharedDialog' import { openSharedDialog } from '@/composables/useSharedDialog'
import { useConfirm } from '@/composables/useConfirm'
import { import {
getCachedMediaExistsStatus, getCachedMediaExistsStatus,
getCachedMediaSubscribeStatus, getCachedMediaSubscribeStatus,
@@ -21,6 +22,7 @@ import {
const SearchSiteDialog = defineAsyncComponent(() => import('@/components/dialog/SearchSiteDialog.vue')) const SearchSiteDialog = defineAsyncComponent(() => import('@/components/dialog/SearchSiteDialog.vue'))
const SubscribeEditDialog = defineAsyncComponent(() => import('../dialog/SubscribeEditDialog.vue')) const SubscribeEditDialog = defineAsyncComponent(() => import('../dialog/SubscribeEditDialog.vue'))
const SubscribeModeDialog = defineAsyncComponent(() => import('../dialog/SubscribeModeDialog.vue'))
const SubscribeSeasonDialog = defineAsyncComponent(() => import('../dialog/SubscribeSeasonDialog.vue')) const SubscribeSeasonDialog = defineAsyncComponent(() => import('../dialog/SubscribeSeasonDialog.vue'))
// 国际化 // 国际化
@@ -51,6 +53,7 @@ const canSubscribe = computed(() => hasPermission(userPermissions.value, 'subscr
// 提示框 // 提示框
const $toast = useToast() const $toast = useToast()
const createConfirm = useConfirm()
// 图片加载状态 // 图片加载状态
const isImageLoaded = ref(false) const isImageLoaded = ref(false)
@@ -191,18 +194,30 @@ async function handleAddSubscribe() {
seasonsSelected.value = [] seasonsSelected.value = []
openSubscribeSeasonDialog() openSubscribeSeasonDialog()
} else { } else {
// 电影 if (isExists.value) {
addSubscribe() 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添加订阅电视剧的话需要指定季 // 调用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() startNProgress()
try { try {
// 是否洗版
if (!best_version && props.media?.type == '电影') best_version = isExists.value ? 1 : 0
// 请求API // 请求API
const result: { [key: string]: any } = await api.post('subscribe/', { const result: { [key: string]: any } = await api.post('subscribe/', {
name: props.media?.title, name: props.media?.title,
@@ -213,7 +228,7 @@ async function addSubscribe(season: number | null = null, best_version: number =
bangumiid: props.media?.bangumi_id, bangumiid: props.media?.bangumi_id,
mediaid: props.media?.media_id ? `${props.media?.mediaid_prefix}:${props.media?.media_id}` : '', mediaid: props.media?.media_id ? `${props.media?.mediaid_prefix}:${props.media?.media_id}` : '',
season: props.media?.type === '电影' ? null : season, season: props.media?.type === '电影' ? null : season,
best_version, ...payload,
episode_group: episodeGroup.value, 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) { if (result.success && seasonsSelected.value.length <= 1) {
@@ -254,6 +269,12 @@ function showSubscribeAddToast(result: boolean, title: string, season: number |
// 调用API取消订阅 // 调用API取消订阅
async function removeSubscribe() { async function removeSubscribe() {
const confirmed = await createConfirm({
title: t('common.confirm'),
content: t('dialog.subscribeEdit.cancelSubscribeConfirm'),
})
if (!confirmed) return
// 开始处理 // 开始处理
startNProgress() startNProgress()
try { try {
@@ -264,13 +285,16 @@ async function removeSubscribe() {
season: props.media?.season, 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) { if (result.success) {
isSubscribed.value = false isSubscribed.value = false
setCachedMediaSubscribeStatus(getSubscribeStatusKey(props.media?.season ?? null), false) setCachedMediaSubscribeStatus(getSubscribeStatusKey(props.media?.season ?? null), false)
$toast.success(`${props.media?.title} ${t('subscribe.cancelSuccess')}`) $toast.success(`${title} ${t('subscribe.cancelSuccess')}`)
} else { } else {
$toast.error(`${props.media?.title} ${t('subscribe.cancelFailed', { message: result.message })}`) $toast.error(`${title} ${t('subscribe.cancelFailed', { message: result.message })}`)
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@@ -362,12 +386,29 @@ function handleSubscribe() {
function subscribeSeasons(seasons: MediaSeason[], seasonNoExists: { [key: number]: number }, groudId: string) { function subscribeSeasons(seasons: MediaSeason[], seasonNoExists: { [key: number]: number }, groudId: string) {
episodeGroup.value = groudId episodeGroup.value = groudId
seasonsSelected.value = seasons || [] 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 => { seasonsSelected.value.forEach(season => {
let best_version = 0 const seasonNumber = season.season_number ?? null
if (season && props.media?.tmdb_id) const payload =
// 全部存在时洗版 seasonNumber !== null && !seasonNoExists[seasonNumber] ? { best_version: 1, best_version_full: 1 } : {}
best_version = !seasonNoExists[season.season_number || 0] ? 1 : 0 addSubscribe(seasonNumber, payload)
addSubscribe(season.season_number ?? null, best_version)
}) })
} }

View File

@@ -9,6 +9,7 @@ import { useI18n } from 'vue-i18n'
import { qualityOptions, resolutionOptions, effectOptions } from '@/api/constants' import { qualityOptions, resolutionOptions, effectOptions } from '@/api/constants'
import { useUserStore } from '@/stores' import { useUserStore } from '@/stores'
import { buildUserPermissionContext, hasPermission } from '@/utils/permission' import { buildUserPermissionContext, hasPermission } from '@/utils/permission'
import { formatSeason } from '@/@core/utils/formatters'
// i18n // i18n
const { t } = useI18n() const { t } = useI18n()
const userStore = useUserStore() 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 }) { function episodeGroupItemProps(item: { title: string; subtitle: string }) {
return { return {
@@ -158,11 +166,11 @@ async function updateSubscribeInfo() {
const result: { [key: string]: any } = await api.put('subscribe/', subscribeForm.value) const result: { [key: string]: any } = await api.put('subscribe/', subscribeForm.value)
// 提示 // 提示
if (result.success) { if (result.success) {
$toast.success(`${subscribeForm.value.name} 更新成功!`) $toast.success(`${getSubscribeDisplayName()} 更新成功!`)
// 通知父组件刷新 // 通知父组件刷新
emit('save') emit('save')
} else { } else {
$toast.error(`${subscribeForm.value.name} 更新失败:${result.message}`) $toast.error(`${getSubscribeDisplayName()} 更新失败:${result.message}`)
} }
} catch (e) { } catch (e) {
console.log(e) console.log(e)
@@ -258,7 +266,7 @@ async function removeSubscribe() {
const result: { [key: string]: any } = await api.delete(`subscribe/${props.subid}`) const result: { [key: string]: any } = await api.delete(`subscribe/${props.subid}`)
if (result.success) { if (result.success) {
$toast.success(`订阅 ${subscribeForm.value.name} 已取消!`) $toast.success(`订阅 ${getSubscribeDisplayName()} 已取消!`)
// 通知父组件刷新 // 通知父组件刷新
emit('remove') emit('remove')
} }
@@ -317,10 +325,7 @@ onMounted(() => {
{{ props.default ? t('dialog.subscribeEdit.titleDefault') : t('dialog.subscribeEdit.titleEdit') }} {{ props.default ? t('dialog.subscribeEdit.titleDefault') : t('dialog.subscribeEdit.titleEdit') }}
</VCardTitle> </VCardTitle>
<VCardSubtitle v-if="!props.default"> <VCardSubtitle v-if="!props.default">
{{ subscribeForm.name }} {{ getSubscribeDisplayName() }}
<span v-if="subscribeForm.season">
{{ t('dialog.subscribeEdit.seasonFormat', { number: subscribeForm.season }) }}
</span>
</VCardSubtitle> </VCardSubtitle>
<VCardSubtitle v-else> <VCardSubtitle v-else>
{{ props.type }} {{ props.type }}

View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
type SubscribeMode = 'normal' | 'best_version' | 'best_version_full'
const props = defineProps<{
modelValue?: boolean
type?: string
modes?: SubscribeMode[]
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
(e: 'choose', mode: SubscribeMode): void
(e: 'close'): void
}>()
const { t } = useI18n()
const modeItems = computed<SubscribeMode[]>(() =>
props.modes?.length
? props.modes
: props.type === '电视剧'
? ['normal', 'best_version', 'best_version_full']
: ['normal', 'best_version'],
)
const optionMeta: Record<SubscribeMode, { icon: string; title: string }> = {
normal: {
icon: 'mdi-plus-circle-outline',
title: t('dialog.subscribeMode.normal'),
},
best_version: {
icon: 'mdi-refresh',
title: props.type === '电视剧' ? t('dialog.subscribeMode.bestVersionEpisode') : t('dialog.subscribeMode.bestVersion'),
},
best_version_full: {
icon: 'mdi-shimmer',
title: t('dialog.subscribeMode.bestVersionFull'),
},
}
</script>
<template>
<VDialog :model-value="modelValue" max-width="28rem" @update:model-value="emit('update:modelValue', $event)">
<VCard>
<VCardTitle class="text-lg font-weight-bold px-5 pt-5">
{{ t('dialog.subscribeMode.title') }}
</VCardTitle>
<VList class="py-2">
<VListItem
v-for="mode in modeItems"
:key="mode"
:prepend-icon="optionMeta[mode].icon"
:title="optionMeta[mode].title"
@click="emit('choose', mode)"
/>
</VList>
<VDialogCloseBtn @click="emit('close')" />
</VCard>
</VDialog>
</template>

View File

@@ -2814,6 +2814,13 @@ export default {
processing: 'Processing ...', processing: 'Processing ...',
successMessage: 'File {name} has been added to the organization queue!', 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: { subscribeEdit: {
titleDefault: 'Default Subscription Rules', titleDefault: 'Default Subscription Rules',
titleEdit: 'Edit Subscription', titleEdit: 'Edit Subscription',

View File

@@ -2763,6 +2763,13 @@ export default {
processing: '正在处理 ...', processing: '正在处理 ...',
successMessage: '文件 {name} 已加入整理队列!', successMessage: '文件 {name} 已加入整理队列!',
}, },
subscribeMode: {
title: '选择订阅方式',
normal: '普通订阅',
bestVersion: '洗版',
bestVersionEpisode: '分集洗版',
bestVersionFull: '全集洗版',
},
subscribeEdit: { subscribeEdit: {
titleDefault: '默认订阅规则', titleDefault: '默认订阅规则',
titleEdit: '编辑订阅', titleEdit: '编辑订阅',

View File

@@ -2764,6 +2764,13 @@ export default {
processing: '正在處理 ...', processing: '正在處理 ...',
successMessage: '文件 {name} 已加入整理隊列!', successMessage: '文件 {name} 已加入整理隊列!',
}, },
subscribeMode: {
title: '選擇訂閱方式',
normal: '普通訂閱',
bestVersion: '洗版',
bestVersionEpisode: '分集洗版',
bestVersionFull: '全集洗版',
},
subscribeEdit: { subscribeEdit: {
titleDefault: '默認訂閱規則', titleDefault: '默認訂閱規則',
titleEdit: '編輯訂閱', titleEdit: '編輯訂閱',

View File

@@ -14,6 +14,7 @@ import { useUserStore } from '@/stores'
import { useTheme } from 'vuetify' import { useTheme } from 'vuetify'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { buildUserPermissionContext, hasPermission } from '@/utils/permission' import { buildUserPermissionContext, hasPermission } from '@/utils/permission'
import { useConfirm } from '@/composables/useConfirm'
import { useGlobalSettingsStore } from '@/stores' import { useGlobalSettingsStore } from '@/stores'
import { openMediaServerItem, openDoubanApp } from '@/utils/appDeepLink' import { openMediaServerItem, openDoubanApp } from '@/utils/appDeepLink'
import { openSharedDialog } from '@/composables/useSharedDialog' import { openSharedDialog } from '@/composables/useSharedDialog'
@@ -21,6 +22,7 @@ import { getDisplayImageUrl } from '@/utils/imageUtils'
const SearchSiteDialog = defineAsyncComponent(() => import('@/components/dialog/SearchSiteDialog.vue')) const SearchSiteDialog = defineAsyncComponent(() => import('@/components/dialog/SearchSiteDialog.vue'))
const SubscribeEditDialog = defineAsyncComponent(() => import('@/components/dialog/SubscribeEditDialog.vue')) const SubscribeEditDialog = defineAsyncComponent(() => import('@/components/dialog/SubscribeEditDialog.vue'))
const SubscribeModeDialog = defineAsyncComponent(() => import('@/components/dialog/SubscribeModeDialog.vue'))
// 国际化 // 国际化
const { t } = useI18n() const { t } = useI18n()
@@ -46,6 +48,7 @@ const canSubscribe = computed(() => hasPermission(userPermissions.value, 'subscr
// 提示框 // 提示框
const $toast = useToast() const $toast = useToast()
const createConfirm = useConfirm()
// 获取主题信息 // 获取主题信息
const theme = useTheme() const theme = useTheme()
@@ -293,15 +296,10 @@ async function checkSeasonsSubscribed() {
} }
// 调用API添加订阅电视剧的话需要指定季 // 调用API添加订阅电视剧的话需要指定季
async function addSubscribe(season: number | null) { async function addSubscribe(season: number | null, payload: { best_version?: number; best_version_full?: number } = {}) {
// 开始处理 // 开始处理
startNProgress() startNProgress()
try { try {
// 是否洗版
let best_version = existsItemId.value ? 1 : 0
if (season !== null)
// 全部存在时洗版
best_version = !seasonsNotExisted.value[season] ? 1 : 0
// 请求API // 请求API
const result: { [key: string]: any } = await api.post('subscribe/', { const result: { [key: string]: any } = await api.post('subscribe/', {
name: mediaDetail.value?.title, name: mediaDetail.value?.title,
@@ -311,7 +309,7 @@ async function addSubscribe(season: number | null) {
doubanid: mediaDetail.value?.douban_id, doubanid: mediaDetail.value?.douban_id,
bangumiid: mediaDetail.value?.bangumi_id, bangumiid: mediaDetail.value?.bangumi_id,
season: mediaDetail.value?.type === '电影' ? null : season, 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) { if (result.success) {
@@ -350,6 +348,12 @@ function showSubscribeAddToast(result: boolean, title: string, season: number |
// 调用API取消订阅 // 调用API取消订阅
async function removeSubscribe(season: number | null) { async function removeSubscribe(season: number | null) {
const confirmed = await createConfirm({
title: t('common.confirm'),
content: t('dialog.subscribeEdit.cancelSubscribeConfirm'),
})
if (!confirmed) return
// 开始处理 // 开始处理
startNProgress() startNProgress()
try { try {
@@ -360,13 +364,15 @@ async function removeSubscribe(season: number | null) {
season, season,
}, },
}) })
let title = mediaDetail.value?.title ?? ''
if (season !== null) title = `${title} ${formatSeason(season.toString())}`
if (result.success) { if (result.success) {
isSubscribed.value = false isSubscribed.value = false
if (season !== null) seasonsSubscribed.value[season] = 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 { } 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) { } catch (error) {
console.error(error) console.error(error)
@@ -376,8 +382,54 @@ async function removeSubscribe(season: number | null) {
// 订阅按钮响应 // 订阅按钮响应
function handleSubscribe(season: number | null = null) { function handleSubscribe(season: number | null = null) {
if (isSubscribed.value) removeSubscribe(season) if (season !== null) {
else addSubscribe(season) 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使用、分隔 // 从genres中获取name使用、分隔