Refine episode group subscriptions and mobile carousel

This commit is contained in:
jxxghp
2026-07-02 16:34:38 +08:00
parent 98e6481812
commit 1094d80b9c
4 changed files with 57 additions and 11 deletions

View File

@@ -32,6 +32,7 @@ const emit = defineEmits(['subscribe', 'close'])
const props = defineProps({
media: Object as PropType<MediaInfo>,
selectedSeason: Number,
initialEpisodeGroup: String,
subscribedSeasons: Array as PropType<number[]>,
subscribedSeasonModes: Object as PropType<SeasonSubscribeModes>,
defaultSubscribeMode: String as PropType<SubscribeMode>,
@@ -64,7 +65,7 @@ const isRefreshed = ref(false)
const episodeGroups = ref<{ [key: string]: any }[]>([])
// 当前选择剧集组
const episodeGroup = ref('')
const episodeGroup = ref(props.initialEpisodeGroup ?? '')
const subscribeModeOptions = computed<SubscribeModeOption[]>(() => [
{
@@ -84,12 +85,14 @@ const subscribeModeOptions = computed<SubscribeModeOption[]>(() => [
},
])
// 获取订阅模式的主题色。
function getSubscribeModeColor(mode: SubscribeMode) {
if (mode === 'normal') return 'primary'
if (mode === 'best_version') return 'warning'
return 'success'
}
// 校验弹窗输入是否为支持的订阅模式。
function isSubscribeMode(value: unknown): value is SubscribeMode {
return value === 'normal' || value === 'best_version' || value === 'best_version_full'
}
@@ -265,6 +268,7 @@ function getYear(airDate: string) {
return date.getFullYear()
}
// 切换当前剧集组并清空上一组的派生数据。
function setEpisodeGroup(value: string) {
if (episodeGroup.value === value) return
@@ -273,6 +277,7 @@ function setEpisodeGroup(value: string) {
episodeGroup.value = value
}
// 提交当前剧集组下选中的季及其订阅模式。
function subscribeSeasons() {
const selectedSeasons = seasonInfos.value.filter(item => {
const seasonNumber = item.season_number ?? null
@@ -289,6 +294,7 @@ function subscribeSeasons() {
)
}
// 写入指定季的订阅模式。
function setSeasonMode(season: number, mode: SubscribeMode) {
seasonModes.value = {
...seasonModes.value,
@@ -296,22 +302,26 @@ function setSeasonMode(season: number, mode: SubscribeMode) {
}
}
// 处理用户手动切换指定季的订阅模式。
function updateSeasonMode(season: number, mode: unknown) {
if (!isSubscribeMode(mode)) return
manuallySelectedModeSeasons.value.add(season)
setSeasonMode(season, mode)
}
// 根据入库状态和系统配置计算指定季的默认订阅模式。
function getDefaultSeasonMode(season: number) {
if (!seasonsNotExisted.value[season]) return 'best_version_full'
return props.defaultSubscribeMode ?? 'normal'
}
// 确保指定季已初始化订阅模式。
function ensureSeasonMode(season: number) {
if (!seasonModes.value[season]) setSeasonMode(season, props.subscribedSeasonModes?.[season] ?? getDefaultSeasonMode(season))
}
// 在入库状态刷新后同步尚未手动修改的默认模式。
function syncDefaultSeasonModes() {
seasonsSelected.value.forEach(season => {
if (subscribedSeasonSet.value.has(season) || manuallySelectedModeSeasons.value.has(season)) return
@@ -319,14 +329,17 @@ function syncDefaultSeasonModes() {
})
}
// 判断指定季是否已有订阅。
function isSeasonSubscribed(season: number) {
return subscribedSeasonSet.value.has(season)
}
// 判断指定季是否在本次提交选择中。
function isSeasonSelected(season: number) {
return selectedSeasonSet.value.has(season)
}
// 设置指定季的选择状态并同步其订阅模式。
function setSeasonSelected(season: number, selected: boolean | null) {
const nextSeasons = new Set(seasonsSelected.value)
if (selected) {
@@ -338,10 +351,12 @@ function setSeasonSelected(season: number, selected: boolean | null) {
seasonsSelected.value = [...nextSeasons].sort((a, b) => a - b)
}
// 切换指定季的选择状态。
function toggleSeasonSelected(season: number) {
setSeasonSelected(season, !isSeasonSelected(season))
}
// 将入口预选季和已有订阅同步到当前剧集组的可见季列表。
function syncSelectedSeason() {
if (!seasonInfos.value.length) return
@@ -381,7 +396,8 @@ watch(() => props.subscribedSeasons, syncSelectedSeason)
watch(() => props.subscribedSeasonModes, syncSelectedSeason)
onMounted(async () => {
getMediaSeasons()
// 自定义剧集组由 watchEffect 首次加载,避免默认季数据异步覆盖它。
if (!episodeGroup.value) getMediaSeasons()
getEpisodeGroups()
checkSeasonsNotExists()
})

View File

@@ -49,6 +49,7 @@ const SubscribeSeasonDialog = defineAsyncComponent(() => import('@/components/di
export type SeasonSubscribeModes = Record<number, SubscribeMode>
// 生成跨媒体源稳定的订阅媒体标识。
export function getMediaSubscribeId(media?: MediaInfo) {
if (media?.tmdb_id) return `tmdb:${media.tmdb_id}`
if (media?.douban_id) return `douban:${media.douban_id}`
@@ -56,6 +57,7 @@ export function getMediaSubscribeId(media?: MediaInfo) {
return `${media?.mediaid_prefix}:${media?.media_id}`
}
// 将订阅模式转换为后端订阅字段。
function getSubscribePayload(mode: SubscribeMode): SubscribePayload {
return {
best_version: mode === 'normal' ? 0 : 1,
@@ -63,16 +65,19 @@ function getSubscribePayload(mode: SubscribeMode): SubscribePayload {
}
}
// 兼容布尔值和数字、字符串形式的开关值。
function isEnabledFlag(value: unknown) {
return value === true || value === 1 || value === '1'
}
// 从订阅字段解析统一的订阅模式。
export function getSubscribeMode(subscribe: { best_version?: unknown; best_version_full?: unknown }): SubscribeMode {
if (!isEnabledFlag(subscribe.best_version)) return 'normal'
return isEnabledFlag(subscribe.best_version_full) ? 'best_version_full' : 'best_version'
}
// 从默认订阅配置解析订阅模式。
function getSubscribeConfigMode(config?: SubscribeConfig): SubscribeMode {
return getSubscribeMode({
best_version: config?.best_version,
@@ -80,30 +85,36 @@ function getSubscribeConfigMode(config?: SubscribeConfig): SubscribeMode {
})
}
// 获取订阅模式的本地化名称。
function getModeName(t: ReturnType<typeof useI18n>['t'], mode: SubscribeMode) {
if (mode === 'normal') return t('dialog.subscribeMode.normal')
if (mode === 'best_version') return t('dialog.subscribeMode.bestVersionEpisode')
return t('dialog.subscribeMode.bestVersionFull')
}
// 封装媒体卡片与详情页共用的订阅交互。
export function useMediaSubscribe(options: UseMediaSubscribeOptions) {
const { t } = useI18n()
const $toast = useToast()
const createConfirm = useConfirm()
const episodeGroup = ref('')
// 获取调用方当前媒体,避免在异步流程中持有旧对象。
function currentMedia() {
return options.media()
}
// 获取当前媒体的统一订阅标识。
function getMediaId() {
return getMediaSubscribeId(currentMedia())
}
// 获取主订阅入口默认对应的季号。
function getPrimarySeason() {
return options.primarySeason?.() ?? currentMedia()?.season ?? null
}
// 同步调用方状态和订阅状态缓存。
function updateSubscribeStatus(season: number | null, subscribed: boolean, mode: SubscribeMode = 'normal') {
const media = currentMedia()
@@ -143,6 +154,7 @@ export function useMediaSubscribe(options: UseMediaSubscribeOptions) {
}
}
// 打开已创建订阅的编辑弹窗。
function openSubscribeEditDialog(subid: number) {
openSharedDialog(
SubscribeEditDialog,
@@ -160,6 +172,7 @@ export function useMediaSubscribe(options: UseMediaSubscribeOptions) {
)
}
// 打开订阅模式选择弹窗并转换选择结果。
function openSubscribeModeDialog(
modes: SubscribeMode[],
choose: (payload: SubscribePayload, mode: SubscribeMode) => void,
@@ -174,7 +187,8 @@ export function useMediaSubscribe(options: UseMediaSubscribeOptions) {
)
}
async function openSubscribeSeasonDialog(selectedSeason?: number | null) {
// 打开季订阅弹窗,并保留发起入口当前使用的剧集组。
async function openSubscribeSeasonDialog(selectedSeason?: number | null, initialEpisodeGroup = '') {
const media = currentMedia()
if (!media) return
const defaultSubscribeConfig = await queryDefaultSubscribeConfig()
@@ -184,6 +198,7 @@ export function useMediaSubscribe(options: UseMediaSubscribeOptions) {
{
media,
selectedSeason,
initialEpisodeGroup,
subscribedSeasons: options.subscribedSeasons?.value ?? [],
subscribedSeasonModes: options.subscribedSeasonModes?.value ?? {},
defaultSubscribeMode: getSubscribeConfigMode(defaultSubscribeConfig),
@@ -195,6 +210,7 @@ export function useMediaSubscribe(options: UseMediaSubscribeOptions) {
)
}
// 查询系统默认订阅配置。
async function queryDefaultSubscribeConfig(): Promise<SubscribeConfig | undefined> {
if (!options.canSubscribe()) return undefined
@@ -214,6 +230,7 @@ export function useMediaSubscribe(options: UseMediaSubscribeOptions) {
return undefined
}
// 展示订阅新增结果通知。
function showSubscribeAddToast(
result: boolean,
title: string,
@@ -229,6 +246,7 @@ export function useMediaSubscribe(options: UseMediaSubscribeOptions) {
else $toast.error(`${title} ${t('subscribe.addFailed', { name: subname, message })}`)
}
// 创建指定季和模式的订阅。
async function addSubscribe(
season: number | null = null,
payload: SubscribePayload = {},
@@ -267,6 +285,7 @@ export function useMediaSubscribe(options: UseMediaSubscribeOptions) {
}
}
// 删除指定季的订阅。
async function removeSubscribe(season: number | null = null, removeOptions: RemoveSubscribeOptions = {}) {
if (removeOptions.confirm ?? true) {
const confirmed = await createConfirm({
@@ -302,6 +321,7 @@ export function useMediaSubscribe(options: UseMediaSubscribeOptions) {
}
}
// 检查当前媒体指定季是否已订阅。
async function checkSubscribe(season: number | null = null) {
try {
const result: Subscribe = await api.get(`subscribe/media/${getMediaId()}`, {
@@ -319,6 +339,7 @@ export function useMediaSubscribe(options: UseMediaSubscribeOptions) {
}
}
// 查询当前媒体指定季的订阅记录。
async function querySubscribe(season: number | null = null) {
try {
const result: Subscribe = await api.get(`subscribe/media/${getMediaId()}`, {
@@ -336,6 +357,7 @@ export function useMediaSubscribe(options: UseMediaSubscribeOptions) {
}
}
// 更新已有单季订阅的模式。
async function updateSubscribeMode(season: number, mode: SubscribeMode) {
const media = currentMedia()
if (!media) return
@@ -368,15 +390,17 @@ export function useMediaSubscribe(options: UseMediaSubscribeOptions) {
}
}
function handleSeasonSubscribe(season: number) {
// 处理单季订阅入口,未订阅时将当前剧集组带入季选择弹窗。
function handleSeasonSubscribe(season: number, initialEpisodeGroup = '') {
if (options.seasonsSubscribed?.value[season]) {
removeSubscribe(season)
return
}
openSubscribeSeasonDialog(season)
openSubscribeSeasonDialog(season, initialEpisodeGroup)
}
// 处理媒体主订阅入口,电视剧统一进入季选择弹窗。
function handlePrimarySubscribe() {
const media = currentMedia()
if (!media) return
@@ -401,15 +425,17 @@ export function useMediaSubscribe(options: UseMediaSubscribeOptions) {
addSubscribe(null)
}
function handleSubscribe(season?: number | null) {
// 根据是否指定季号分发主订阅或单季订阅操作。
function handleSubscribe(season?: number | null, initialEpisodeGroup = '') {
if (season !== undefined && season !== null) {
handleSeasonSubscribe(season)
handleSeasonSubscribe(season, initialEpisodeGroup)
return
}
handlePrimarySubscribe()
}
// 批量对齐弹窗中选择的季、订阅模式和当前订阅状态。
function subscribeSeasons(
seasons: MediaSeason[] = [],
seasonExistsStates: { [key: number]: number } = {},

View File

@@ -550,6 +550,10 @@ onBeforeUnmount(stopAutoplay)
@media (max-width: 740px) {
.dashboard-recommend {
aspect-ratio: auto;
inline-size: 100%;
max-inline-size: 100%;
min-inline-size: 0;
min-block-size: 460px;
}

View File

@@ -484,9 +484,9 @@ const isAllSeasonsSubscribed = computed(
subscribedSeasonNumbers.value.length >= subscribeSeasonTotal.value,
)
// 订阅按钮响应
function handleSubscribe(season: number | null = null) {
subscribeActions.handleSubscribe(season)
// 订阅按钮响应;单季入口同时传递详情页当前选择的剧集组。
function handleSubscribe(season: number | null = null, episodeGroup = '') {
subscribeActions.handleSubscribe(season, episodeGroup)
}
// 从genres中获取name使用、分隔
@@ -1017,7 +1017,7 @@ onUnmounted(() => {
class="ms-1"
:color="seasonsSubscribed[season.season_number || 0] ? 'error' : 'warning'"
variant="text"
@click.stop="handleSubscribe(season.season_number ?? null)"
@click.stop="handleSubscribe(season.season_number ?? null, selectedEpisodeGroup)"
>
<VIcon
:icon="seasonsSubscribed[season.season_number || 0] ? 'mdi-heart' : 'mdi-heart-outline'"