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 { 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)
})
}

View File

@@ -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') }}
</VCardTitle>
<VCardSubtitle v-if="!props.default">
{{ subscribeForm.name }}
<span v-if="subscribeForm.season">
{{ t('dialog.subscribeEdit.seasonFormat', { number: subscribeForm.season }) }}
</span>
{{ getSubscribeDisplayName() }}
</VCardSubtitle>
<VCardSubtitle v-else>
{{ 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 ...',
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',

View File

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

View File

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

View File

@@ -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使用、分隔