Fix issues with MediaCard and SubscribeListView components

This commit is contained in:
jxxghp
2024-05-05 11:54:20 +08:00
parent 37e5e57d5b
commit 271d59ca51
3 changed files with 280 additions and 294 deletions

View File

@@ -19,6 +19,8 @@ const props = defineProps({
height: String, height: String,
}) })
const store = useStore()
// 提示框 // 提示框
const $toast = useToast() const $toast = useToast()
@@ -288,6 +290,8 @@ async function getMediaSeasons() {
// 查询订阅弹窗规则 // 查询订阅弹窗规则
async function queryDefaultSubscribeConfig() { async function queryDefaultSubscribeConfig() {
// 非管理员不显示
if (!store.state.auth.superUser) return false
try { try {
let subscribe_config_url = '' let subscribe_config_url = ''
if (props.media?.type === '电影') subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig' if (props.media?.type === '电影') subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'

View File

@@ -16,6 +16,8 @@ const mediaProps = defineProps({
type: String, type: String,
}) })
const store = useStore()
// 提示框 // 提示框
const $toast = useToast() const $toast = useToast()
@@ -51,8 +53,8 @@ function getMediaId() {
return mediaDetail.value?.tmdb_id return mediaDetail.value?.tmdb_id
? `tmdb:${mediaDetail.value?.tmdb_id}` ? `tmdb:${mediaDetail.value?.tmdb_id}`
: mediaDetail.value?.douban_id : mediaDetail.value?.douban_id
? `douban:${mediaDetail.value?.douban_id}` ? `douban:${mediaDetail.value?.douban_id}`
: `bangumi:${mediaDetail.value?.bangumi_id}` : `bangumi:${mediaDetail.value?.bangumi_id}`
} }
// 调用API查询详情 // 调用API查询详情
@@ -64,31 +66,25 @@ async function getMediaDetail() {
}, },
}) })
isRefreshed.value = true isRefreshed.value = true
if (!mediaDetail.value.tmdb_id && !mediaDetail.value.douban_id && !mediaDetail.value.bangumi_id) if (!mediaDetail.value.tmdb_id && !mediaDetail.value.douban_id && !mediaDetail.value.bangumi_id) return
return
// 检查存在状态 // 检查存在状态
checkExists() checkExists()
if (mediaDetail.value.type === '电视剧') if (mediaDetail.value.type === '电视剧') checkSeasonsNotExists()
checkSeasonsNotExists()
// 检查订阅状态 // 检查订阅状态
if (mediaDetail.value.type === '电影') if (mediaDetail.value.type === '电影') checkMovieSubscribed()
checkMovieSubscribed() else checkSeasonsSubscribed()
else
checkSeasonsSubscribed()
} }
} }
// 调用API加载季集信息 // 调用API加载季集信息
async function loadSeasonEpisodes(season: number) { async function loadSeasonEpisodes(season: number) {
if (seasonEpisodesInfo.value[season]) if (seasonEpisodesInfo.value[season]) return
return
try { try {
const result: TmdbEpisode[] = await api.get(`tmdb/${mediaDetail.value.tmdb_id}/${season}`) const result: TmdbEpisode[] = await api.get(`tmdb/${mediaDetail.value.tmdb_id}/${season}`)
seasonEpisodesInfo.value[season] = result || [] seasonEpisodesInfo.value[season] = result || []
} } catch (error) {
catch (error) {
console.error(error) console.error(error)
} }
} }
@@ -106,10 +102,8 @@ async function checkExists() {
}, },
}) })
if (result.success) if (result.success) existsItemId.value = result.data.item.id
existsItemId.value = result.data.item.id } catch (error) {
}
catch (error) {
console.error(error) console.error(error)
} }
} }
@@ -126,10 +120,8 @@ async function checkSubscribe(season = 0) {
}, },
}) })
if (result.id) if (result.id) return true
return true } catch (error) {
}
catch (error) {
console.error(error) console.error(error)
} }
@@ -138,31 +130,26 @@ async function checkSubscribe(season = 0) {
// 检查所有季的缺失状态 // 检查所有季的缺失状态
async function checkSeasonsNotExists() { async function checkSeasonsNotExists() {
if (mediaDetail.value.type !== '电视剧') if (mediaDetail.value.type !== '电视剧') return
return
try { try {
const result: NotExistMediaInfo[] = await api.post('mediaserver/notexists', mediaDetail.value) const result: NotExistMediaInfo[] = await api.post('mediaserver/notexists', mediaDetail.value)
if (result) { if (result) {
result.forEach((item) => { result.forEach(item => {
// 0-已入库 1-部分缺失 2-全部缺失 // 0-已入库 1-部分缺失 2-全部缺失
let state = 0 let state = 0
if (item.episodes.length === 0) if (item.episodes.length === 0) state = 2
state = 2 else if (item.episodes.length < item.total_episode) state = 1
else if (item.episodes.length < item.total_episode)
state = 1
seasonsNotExisted.value[item.season] = state seasonsNotExisted.value[item.season] = state
}) })
} }
} } catch (error) {
catch (error) {
console.error(error) console.error(error)
} }
} }
// 检查电影订阅状态 // 检查电影订阅状态
async function checkMovieSubscribed() { async function checkMovieSubscribed() {
if (mediaDetail.value.type !== '电影') if (mediaDetail.value.type !== '电影') return
return
isSubscribed.value = await checkSubscribe() isSubscribed.value = await checkSubscribe()
} }
@@ -173,14 +160,12 @@ const getMediaSeasons = computed(() => {
// 检查所有季的订阅状态 // 检查所有季的订阅状态
async function checkSeasonsSubscribed() { async function checkSeasonsSubscribed() {
if (mediaDetail.value.type !== '电视剧') if (mediaDetail.value.type !== '电视剧') return
return
try { try {
mediaDetail.value?.season_info?.forEach(async (item) => { mediaDetail.value?.season_info?.forEach(async item => {
seasonsSubscribed.value[item.season_number ?? 0] = await checkSubscribe(item.season_number) seasonsSubscribed.value[item.season_number ?? 0] = await checkSubscribe(item.season_number)
}) })
} } catch (error) {
catch (error) {
console.error(error) console.error(error)
} }
} }
@@ -211,18 +196,11 @@ async function addSubscribe(season = 0) {
if (result.success) { if (result.success) {
// 订阅成功 // 订阅成功
isSubscribed.value = true isSubscribed.value = true
if (season) if (season) seasonsSubscribed.value[season] = true
seasonsSubscribed.value[season] = true
} }
// 提示 // 提示
showSubscribeAddToast( showSubscribeAddToast(result.success, mediaDetail.value?.title ?? '', season, result.message, best_version)
result.success,
mediaDetail.value?.title ?? '',
season,
result.message,
best_version,
)
// 显示编辑弹窗 // 显示编辑弹窗
if (result.success) { if (result.success) {
@@ -232,28 +210,20 @@ async function addSubscribe(season = 0) {
subscribeEditDialog.value = true subscribeEditDialog.value = true
} }
} }
} } catch (error) {
catch (error) {
console.error(error) console.error(error)
} }
doneNProgress() doneNProgress()
} }
// 弹出添加订阅提示 // 弹出添加订阅提示
function showSubscribeAddToast(result: boolean, function showSubscribeAddToast(result: boolean, title: string, season: number, message: string, best_version: number) {
title: string, if (season) title = `${title} ${formatSeason(season.toString())}`
season: number,
message: string,
best_version: number) {
if (season)
title = `${title} ${formatSeason(season.toString())}`
let subname = '订阅' let subname = '订阅'
if (best_version > 0) if (best_version > 0) subname = '洗版订阅'
subname = '洗版订阅'
if (!result) if (!result) $toast.error(`${title} 添加${subname}失败:${message}`)
$toast.error(`${title} 添加${subname}失败:${message}`)
} }
// 调用API取消订阅 // 调用API取消订阅
@@ -263,26 +233,20 @@ async function removeSubscribe(season: number) {
try { try {
const mediaid = getMediaId() const mediaid = getMediaId()
const result: { [key: string]: any } = await api.delete( const result: { [key: string]: any } = await api.delete(`subscribe/media/${mediaid}`, {
`subscribe/media/${mediaid}`, params: {
{ season,
params: {
season,
},
}, },
) })
if (result.success) { if (result.success) {
isSubscribed.value = false isSubscribed.value = false
if (season) if (season) seasonsSubscribed.value[season] = false
seasonsSubscribed.value[season] = false
$toast.success(`${mediaDetail.value?.title} 已取消订阅!`) $toast.success(`${mediaDetail.value?.title} 已取消订阅!`)
} } else {
else {
$toast.error(`${mediaDetail.value?.title} 取消订阅失败:${result.message}`) $toast.error(`${mediaDetail.value?.title} 取消订阅失败:${result.message}`)
} }
} } catch (error) {
catch (error) {
console.error(error) console.error(error)
} }
doneNProgress() doneNProgress()
@@ -290,10 +254,8 @@ async function removeSubscribe(season: number) {
// 订阅按钮响应 // 订阅按钮响应
function handleSubscribe(season = 0) { function handleSubscribe(season = 0) {
if (isSubscribed.value) if (isSubscribed.value) removeSubscribe(season)
removeSubscribe(season) else addSubscribe(season)
else
addSubscribe(season)
} }
// 从genres中获取name使用、分隔 // 从genres中获取name使用、分隔
@@ -329,15 +291,13 @@ function getBangumiLink() {
// 拼装集图片地址 // 拼装集图片地址
function getEpisodeImage(stillPath: string) { function getEpisodeImage(stillPath: string) {
if (!stillPath) if (!stillPath) return ''
return ''
return `https://image.tmdb.org/t/p/w500${stillPath}` return `https://image.tmdb.org/t/p/w500${stillPath}`
} }
// TMDB图片转换为w500大小 // TMDB图片转换为w500大小
function getW500Image(url = '') { function getW500Image(url = '') {
if (!url) if (!url) return ''
return ''
return url.replace('original', 'w500') return url.replace('original', 'w500')
} }
@@ -354,45 +314,33 @@ const getProductionCompanies = computed(() => {
// 计算存在状态的颜色 // 计算存在状态的颜色
function getExistColor(season: number) { function getExistColor(season: number) {
const state = seasonsNotExisted.value[season] const state = seasonsNotExisted.value[season]
if (!state) if (!state) return 'success'
return 'success'
if (state === 1) if (state === 1) return 'warning'
return 'warning' else if (state === 2) return 'error'
else if (state === 2) else return 'success'
return 'error'
else
return 'success'
} }
// 计算存在状态的文本 // 计算存在状态的文本
function getExistText(season: number) { function getExistText(season: number) {
const state = seasonsNotExisted.value[season] const state = seasonsNotExisted.value[season]
if (!state) if (!state) return '已入库'
return '已入库'
if (state === 1) if (state === 1) return '部分缺失'
return '部分缺失' else if (state === 2) return '缺失'
else if (state === 2) else return '已入库'
return '缺失'
else
return '已入库'
} }
// 计算订阅图标 // 计算订阅图标
const getSubscribeIcon = computed(() => { const getSubscribeIcon = computed(() => {
if (isSubscribed.value) if (isSubscribed.value) return 'mdi-heart'
return 'mdi-heart' else return 'mdi-heart-outline'
else
return 'mdi-heart-outline'
}) })
// 计算订阅按钮颜色 // 计算订阅按钮颜色
const getSubscribeColor = computed(() => { const getSubscribeColor = computed(() => {
if (isSubscribed.value) if (isSubscribed.value) return 'error'
return 'error' else return 'warning'
else
return 'warning'
}) })
// 使用、拼装数组为字符串 // 使用、拼装数组为字符串
@@ -418,36 +366,32 @@ function handleSearch(area: string) {
async function handlePlay() { async function handlePlay() {
// 获取播放链接地址 // 获取播放链接地址
try { try {
const result: { [key: string]: any } = await api.get( const result: { [key: string]: any } = await api.get(`mediaserver/play/${existsItemId.value}`)
`mediaserver/play/${existsItemId.value}`,
)
if (result?.success) { if (result?.success) {
// 打开链接地址 // 打开链接地址
setTimeout(() => { setTimeout(() => {
window.open(result.data.url, '_blank') window.open(result.data.url, '_blank')
}, 100) }, 100)
} else {
$toast.error(`获取播放链接失败:${result.message}`)
} }
else { $toast.error(`获取播放链接失败:${result.message}`) } } catch (error) {
}
catch (error) {
console.error(error) console.error(error)
} }
} }
async function queryDefaultSubscribeConfig() { async function queryDefaultSubscribeConfig() {
// 非管理员不显示
if (!store.state.auth.superUser) return false
try { try {
let subscribe_config_url = '' let subscribe_config_url = ''
if (mediaProps.type === '电影') if (mediaProps.type === '电影') subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig' else subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
else
subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
const result: { [key: string]: any } = await api.get(subscribe_config_url) const result: { [key: string]: any } = await api.get(subscribe_config_url)
if (result.data?.value) if (result.data?.value) return result.data.value.show_edit_dialog
return result.data.value.show_edit_dialog } catch (error) {
}
catch (error) {
console.log(error) console.log(error)
} }
return false return false
@@ -459,10 +403,7 @@ onBeforeMount(() => {
</script> </script>
<template> <template>
<LoadingBanner <LoadingBanner v-if="!isRefreshed" class="mt-12" />
v-if="!isRefreshed"
class="mt-12"
/>
<div v-if="mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id" class="max-w-8xl mx-auto px-4"> <div v-if="mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id" class="max-w-8xl mx-auto px-4">
<template v-if="mediaDetail.backdrop_path || mediaDetail.poster_path"> <template v-if="mediaDetail.backdrop_path || mediaDetail.poster_path">
<div class="vue-media-back absolute left-0 top-0 w-full h-96"> <div class="vue-media-back absolute left-0 top-0 w-full h-96">
@@ -473,7 +414,11 @@ onBeforeMount(() => {
<div class="media-page"> <div class="media-page">
<div class="media-header"> <div class="media-header">
<div class="media-poster"> <div class="media-poster">
<VImg :src="getW500Image(mediaDetail.poster_path)" cover class="object-cover aspect-w-2 aspect-h-3 ring-1 ring-gray-500"> <VImg
:src="getW500Image(mediaDetail.poster_path)"
cover
class="object-cover aspect-w-2 aspect-h-3 ring-1 ring-gray-500"
>
<template #placeholder> <template #placeholder>
<div class="w-full h-full"> <div class="w-full h-full">
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" /> <VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
@@ -483,7 +428,9 @@ onBeforeMount(() => {
</div> </div>
<div class="media-title"> <div class="media-title">
<div v-if="existsItemId" class="media-status"> <div v-if="existsItemId" class="media-status">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap transition !no-underline bg-green-500 bg-opacity-80 border border-green-500 !text-green-100 hover:bg-green-500 hover:bg-opacity-100 false overflow-hidden"> <span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap transition !no-underline bg-green-500 bg-opacity-80 border border-green-500 !text-green-100 hover:bg-green-500 hover:bg-opacity-100 false overflow-hidden"
>
<div class="relative z-20 flex items-center false"><span>已入库</span></div> <div class="relative z-20 flex items-center false"><span>已入库</span></div>
</span> </span>
</div> </div>
@@ -496,45 +443,56 @@ onBeforeMount(() => {
</div> </div>
</h1> </h1>
<span class="media-attributes"> <span class="media-attributes">
<span v-if="mediaDetail.runtime || mediaDetail.episode_run_time[0]">{{ mediaDetail.runtime || mediaDetail.episode_run_time[0] }} 分钟</span> <span v-if="mediaDetail.runtime || mediaDetail.episode_run_time[0]"
<span v-if="(mediaDetail.runtime || mediaDetail.episode_run_time[0]) && mediaDetail.genres" class="mx-1"> | </span> >{{ mediaDetail.runtime || mediaDetail.episode_run_time[0] }} 分钟</span
>
<span v-if="(mediaDetail.runtime || mediaDetail.episode_run_time[0]) && mediaDetail.genres" class="mx-1">
|
</span>
<span v-if="mediaDetail.genres">{{ getGenresName(mediaDetail.genres || []) }}</span> <span v-if="mediaDetail.genres">{{ getGenresName(mediaDetail.genres || []) }}</span>
</span> </span>
</div> </div>
<div class="media-actions"> <div class="media-actions">
<VBtn v-if="(mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id) && mediaDetail.imdb_id" variant="tonal" color="info" class="mb-2"> <VBtn
v-if="(mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id) && mediaDetail.imdb_id"
variant="tonal"
color="info"
class="mb-2"
>
<template #prepend> <template #prepend>
<VIcon icon="mdi-magnify" /> <VIcon icon="mdi-magnify" />
</template> </template>
搜索资源 搜索资源
<VMenu <VMenu activator="parent" close-on-content-click>
activator="parent"
close-on-content-click
>
<VList> <VList>
<VListItem <VListItem variant="plain" @click="handleSearch('title')">
variant="plain"
@click="handleSearch('title')"
>
<VListItemTitle>标题</VListItemTitle> <VListItemTitle>标题</VListItemTitle>
</VListItem> </VListItem>
<VListItem <VListItem v-show="mediaDetail.imdb_id" variant="plain" @click="handleSearch('imdbid')">
v-show="mediaDetail.imdb_id"
variant="plain"
@click="handleSearch('imdbid')"
>
<VListItemTitle>IMDB链接</VListItemTitle> <VListItemTitle>IMDB链接</VListItemTitle>
</VListItem> </VListItem>
</VList> </VList>
</VMenu> </VMenu>
</VBtn> </VBtn>
<VBtn v-if="(mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id) && !mediaDetail.imdb_id" variant="tonal" color="info" class="mb-2" @click="handleSearch('title')"> <VBtn
v-if="(mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id) && !mediaDetail.imdb_id"
variant="tonal"
color="info"
class="mb-2"
@click="handleSearch('title')"
>
<template #prepend> <template #prepend>
<VIcon icon="mdi-magnify" /> <VIcon icon="mdi-magnify" />
</template> </template>
搜索资源 搜索资源
</VBtn> </VBtn>
<VBtn v-if="mediaDetail.type === '电影' || mediaDetail.douban_id || mediaDetail.bangumi_id" class="ms-2 mb-2" :color="getSubscribeColor" variant="tonal" @click="handleSubscribe(0)"> <VBtn
v-if="mediaDetail.type === '电影' || mediaDetail.douban_id || mediaDetail.bangumi_id"
class="ms-2 mb-2"
:color="getSubscribeColor"
variant="tonal"
@click="handleSubscribe(0)"
>
<template #prepend> <template #prepend>
<VIcon :icon="getSubscribeIcon" /> <VIcon :icon="getSubscribeIcon" />
</template> </template>
@@ -553,9 +511,7 @@ onBeforeMount(() => {
<div v-if="mediaDetail.tagline" class="tagline"> <div v-if="mediaDetail.tagline" class="tagline">
{{ mediaDetail.tagline }} {{ mediaDetail.tagline }}
</div> </div>
<h2 v-if="mediaDetail.overview"> <h2 v-if="mediaDetail.overview">简介</h2>
简介
</h2>
<p>{{ mediaDetail.overview }}</p> <p>{{ mediaDetail.overview }}</p>
<ul v-if="mediaDetail.tmdb_id" class="media-crew"> <ul v-if="mediaDetail.tmdb_id" class="media-crew">
<li v-for="director in mediaDetail.directors" :key="director.id"> <li v-for="director in mediaDetail.directors" :key="director.id">
@@ -572,40 +528,63 @@ onBeforeMount(() => {
</li> </li>
</ul> </ul>
<div class="mt-6"> <div class="mt-6">
<a v-if="mediaDetail.tmdb_id" class="mb-2 mr-2 inline-flex last:mr-0" :href="getTheMovieDbLink()" target="_blank"> <a
<div class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 py-1 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700"> v-if="mediaDetail.tmdb_id"
class="mb-2 mr-2 inline-flex last:mr-0"
:href="getTheMovieDbLink()"
target="_blank"
>
<div
class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 py-1 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700"
>
<VIcon icon="mdi-link" /> <VIcon icon="mdi-link" />
<span class="ms-1">TheMovieDb</span> <span class="ms-1">TheMovieDb</span>
</div> </div>
</a> </a>
<a v-if="mediaDetail.douban_id" class="mb-2 mr-2 inline-flex last:mr-0" :href="getDoubanLink()" target="_blank"> <a
<div class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 py-1 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700"> v-if="mediaDetail.douban_id"
class="mb-2 mr-2 inline-flex last:mr-0"
:href="getDoubanLink()"
target="_blank"
>
<div
class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 py-1 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700"
>
<VIcon icon="mdi-link" /> <VIcon icon="mdi-link" />
<span class="ms-1">豆瓣</span> <span class="ms-1">豆瓣</span>
</div> </div>
</a> </a>
<a v-if="mediaDetail.imdb_id" class="mb-2 mr-2 inline-flex last:mr-0" :href="getImdbLink()" target="_blank"> <a v-if="mediaDetail.imdb_id" class="mb-2 mr-2 inline-flex last:mr-0" :href="getImdbLink()" target="_blank">
<div class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 py-1 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700"> <div
class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 py-1 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700"
>
<VIcon icon="mdi-link" /> <VIcon icon="mdi-link" />
<span class="ms-1">IMDb</span> <span class="ms-1">IMDb</span>
</div> </div>
</a> </a>
<a v-if="mediaDetail.tvdb_id" class="mb-2 mr-2 inline-flex last:mr-0" :href="getTvdbLink()" target="_blank"> <a v-if="mediaDetail.tvdb_id" class="mb-2 mr-2 inline-flex last:mr-0" :href="getTvdbLink()" target="_blank">
<div class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 py-1 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700"> <div
class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 py-1 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700"
>
<VIcon icon="mdi-link" /> <VIcon icon="mdi-link" />
<span class="ms-1">TheTvDb</span> <span class="ms-1">TheTvDb</span>
</div> </div>
</a> </a>
<a v-if="mediaDetail.bangumi_id" class="mb-2 mr-2 inline-flex last:mr-0" :href="getBangumiLink()" target="_blank"> <a
<div class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 py-1 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700"> v-if="mediaDetail.bangumi_id"
class="mb-2 mr-2 inline-flex last:mr-0"
:href="getBangumiLink()"
target="_blank"
>
<div
class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 py-1 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700"
>
<VIcon icon="mdi-link" /> <VIcon icon="mdi-link" />
<span class="ms-1">Bangumi</span> <span class="ms-1">Bangumi</span>
</div> </div>
</a> </a>
</div> </div>
<h2 v-if="mediaDetail.type === '电视剧' && mediaDetail.tmdb_id" class="py-4"> <h2 v-if="mediaDetail.type === '电视剧' && mediaDetail.tmdb_id" class="py-4"></h2>
</h2>
<div v-if="mediaDetail.type === '电视剧' && mediaDetail.tmdb_id" class="flex w-full flex-col space-y-2"> <div v-if="mediaDetail.type === '电视剧' && mediaDetail.tmdb_id" class="flex w-full flex-col space-y-2">
<VExpansionPanels> <VExpansionPanels>
<VExpansionPanel <VExpansionPanel
@@ -617,22 +596,20 @@ onBeforeMount(() => {
<template #default> <template #default>
<div class="flex flex-row items-center justify-between"> <div class="flex flex-row items-center justify-between">
<span class="font-weight-bold"> {{ season.season_number }} </span> <span class="font-weight-bold"> {{ season.season_number }} </span>
<VChip size="small" class="ms-1"> <VChip size="small" class="ms-1"> {{ season.episode_count }} </VChip>
{{ season.episode_count }}
</VChip>
<div class="absolute right-12"> <div class="absolute right-12">
<VChip <VChip v-if="seasonsNotExisted" :color="getExistColor(season.season_number || 0)" flat>
v-if="seasonsNotExisted"
:color="getExistColor(season.season_number || 0)"
flat
>
{{ getExistText(season.season_number || 0) }} {{ getExistText(season.season_number || 0) }}
</VChip> </VChip>
<IconBtn <IconBtn
class="ms-1" :color="seasonsSubscribed[season.season_number || 0] ? 'error' : 'warning'" variant="text" class="ms-1"
:color="seasonsSubscribed[season.season_number || 0] ? 'error' : 'warning'"
variant="text"
@click.stop="handleSubscribe(season.season_number)" @click.stop="handleSubscribe(season.season_number)"
> >
<VIcon :icon="seasonsSubscribed[season.season_number || 0] ? 'mdi-heart' : 'mdi-heart-outline'" /> <VIcon
:icon="seasonsSubscribed[season.season_number || 0] ? 'mdi-heart' : 'mdi-heart-outline'"
/>
</IconBtn> </IconBtn>
</div> </div>
</div> </div>
@@ -640,26 +617,33 @@ onBeforeMount(() => {
</VExpansionPanelTitle> </VExpansionPanelTitle>
<VExpansionPanelText> <VExpansionPanelText>
<template #default> <template #default>
<LoadingBanner <LoadingBanner v-if="!seasonEpisodesInfo[season.season_number || 0]" class="mt-3" />
v-if="!seasonEpisodesInfo[season.season_number || 0]"
class="mt-3"
/>
<div class="flex flex-col justify-center divide-y divide-gray-700"> <div class="flex flex-col justify-center divide-y divide-gray-700">
<div v-for="episode in seasonEpisodesInfo[season.season_number || 0]" :key="episode.episode_number" class="flex flex-col space-y-4 py-4 xl:flex-row xl:space-y-4 xl:space-x-4"> <div
v-for="episode in seasonEpisodesInfo[season.season_number || 0]"
:key="episode.episode_number"
class="flex flex-col space-y-4 py-4 xl:flex-row xl:space-y-4 xl:space-x-4"
>
<div class="flex-1"> <div class="flex-1">
<div class="flex flex-col space-y-2 lg:flex-row lg:items-center lg:space-y-0 lg:space-x-2"> <div class="flex flex-col space-y-2 lg:flex-row lg:items-center lg:space-y-0 lg:space-x-2">
<h3 class="text-lg"> <h3 class="text-lg">{{ episode.episode_number }} - {{ episode.name }}</h3>
{{ episode.episode_number }} - {{ episode.name }}
</h3>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap cursor-default bg-gray-700 !text-gray-300"> <span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap cursor-default bg-gray-700 !text-gray-300"
>
{{ episode.air_date }} {{ episode.air_date }}
</span> </span>
</div> </div>
</div> </div>
<p>{{ episode.overview }}</p> <p>{{ episode.overview }}</p>
</div> </div>
<VImg cover class="rounded-lg" max-width="15rem" :src="getEpisodeImage(episode.still_path || '')" alt="" /> <VImg
cover
class="rounded-lg"
max-width="15rem"
:src="getEpisodeImage(episode.still_path || '')"
alt=""
/>
</div> </div>
</div> </div>
</template> </template>
@@ -671,13 +655,7 @@ onBeforeMount(() => {
<div v-if="mediaDetail.tmdb_id" class="media-overview-right"> <div v-if="mediaDetail.tmdb_id" class="media-overview-right">
<div class="media-facts"> <div class="media-facts">
<div v-if="mediaDetail.vote_average" class="media-ratings"> <div v-if="mediaDetail.vote_average" class="media-ratings">
<VRating <VRating v-model="mediaDetail.vote_average" density="compact" length="10" class="ma-2" readonly />
v-model="mediaDetail.vote_average"
density="compact"
length="10"
class="ma-2"
readonly
/>
</div> </div>
<div v-if="mediaDetail.tmdb_id" class="media-fact"> <div v-if="mediaDetail.tmdb_id" class="media-fact">
<span>ID</span> <span>ID</span>
@@ -695,7 +673,20 @@ onBeforeMount(() => {
<span>上映日期</span> <span>上映日期</span>
<span class="media-fact-value"> <span class="media-fact-value">
<span class="flex items-center justify-end"> <span class="flex items-center justify-end">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" class="h-4 w-4"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15a4.5 4.5 0 004.5 4.5H18a3.75 3.75 0 001.332-7.257 3 3 0 00-3.758-3.848 5.25 5.25 0 00-10.233 2.33A4.502 4.502 0 002.25 15z" /> <svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
aria-hidden="true"
class="h-4 w-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.25 15a4.5 4.5 0 004.5 4.5H18a3.75 3.75 0 001.332-7.257 3 3 0 00-3.758-3.848 5.25 5.25 0 00-10.233 2.33A4.502 4.502 0 002.25 15z"
/>
</svg> </svg>
<span class="ml-1.5">{{ mediaDetail.release_date || mediaDetail.first_air_date }}</span> <span class="ml-1.5">{{ mediaDetail.release_date || mediaDetail.first_air_date }}</span>
</span> </span>
@@ -708,7 +699,11 @@ onBeforeMount(() => {
<div v-if="mediaDetail.production_countries" class="media-fact"> <div v-if="mediaDetail.production_countries" class="media-fact">
<span>出品国家</span> <span>出品国家</span>
<span class="media-fact-value"> <span class="media-fact-value">
<span v-for="country in getProductionCountries" :key="country" class="flex items-center justify-end text-end"> <span
v-for="country in getProductionCountries"
:key="country"
class="flex items-center justify-end text-end"
>
{{ country }} {{ country }}
</span> </span>
</span> </span>
@@ -724,13 +719,7 @@ onBeforeMount(() => {
<div v-else-if="mediaDetail.douban_id" class="media-overview-right"> <div v-else-if="mediaDetail.douban_id" class="media-overview-right">
<div class="media-facts"> <div class="media-facts">
<div v-if="mediaDetail.vote_average" class="media-ratings"> <div v-if="mediaDetail.vote_average" class="media-ratings">
<VRating <VRating v-model="mediaDetail.vote_average" density="compact" length="10" class="ma-2" readonly />
v-model="mediaDetail.vote_average"
density="compact"
length="10"
class="ma-2"
readonly
/>
</div> </div>
<div v-if="mediaDetail.douban_id" class="media-fact"> <div v-if="mediaDetail.douban_id" class="media-fact">
<span>豆瓣ID</span> <span>豆瓣ID</span>
@@ -749,7 +738,11 @@ onBeforeMount(() => {
<div v-if="mediaDetail.production_countries" class="media-fact border-b-0"> <div v-if="mediaDetail.production_countries" class="media-fact border-b-0">
<span>出品国家</span> <span>出品国家</span>
<span class="media-fact-value"> <span class="media-fact-value">
<span v-for="country in getProductionCountries" :key="country" class="flex items-center justify-end text-end"> <span
v-for="country in getProductionCountries"
:key="country"
class="flex items-center justify-end text-end"
>
{{ country }} {{ country }}
</span> </span>
</span> </span>
@@ -759,13 +752,7 @@ onBeforeMount(() => {
<div v-else-if="mediaDetail.bangumi_id" class="media-overview-right"> <div v-else-if="mediaDetail.bangumi_id" class="media-overview-right">
<div class="media-facts"> <div class="media-facts">
<div v-if="mediaDetail.vote_average" class="media-ratings"> <div v-if="mediaDetail.vote_average" class="media-ratings">
<VRating <VRating v-model="mediaDetail.vote_average" density="compact" length="10" class="ma-2" readonly />
v-model="mediaDetail.vote_average"
density="compact"
length="10"
class="ma-2"
readonly
/>
</div> </div>
<div v-if="mediaDetail.bangumi_id" class="media-fact"> <div v-if="mediaDetail.bangumi_id" class="media-fact">
<span>ID</span> <span>ID</span>
@@ -850,20 +837,23 @@ onBeforeMount(() => {
:subid="subscribeId" :subid="subscribeId"
@close="subscribeEditDialog = false" @close="subscribeEditDialog = false"
@save="subscribeEditDialog = false" @save="subscribeEditDialog = false"
@remove="() => { @remove="
subscribeEditDialog = false; () => {
if (mediaDetail.type === '电影') subscribeEditDialog = false
checkMovieSubscribed() if (mediaDetail.type === '电影') checkMovieSubscribed()
else else checkSeasonsSubscribed()
checkSeasonsSubscribed(); }
}" "
/> />
</template> </template>
<style lang="scss"> <style lang="scss">
.vue-media-back { .vue-media-back {
background-image: background-image: linear-gradient(
linear-gradient(180deg, rgba(var(--v-theme-background), 0) 50%, rgba(var(--v-theme-background), 1) 100%), 180deg,
rgba(var(--v-theme-background), 0) 50%,
rgba(var(--v-theme-background), 1) 100%
),
linear-gradient(90deg, rgba(var(--v-theme-background), 0) 50%, rgba(var(--v-theme-background), 1) 100%), linear-gradient(90deg, rgba(var(--v-theme-background), 0) 50%, rgba(var(--v-theme-background), 1) 100%),
linear-gradient(270deg, rgba(var(--v-theme-background), 0) 50%, rgba(var(--v-theme-background), 1) 100%); linear-gradient(270deg, rgba(var(--v-theme-background), 0) 50%, rgba(var(--v-theme-background), 1) 100%);
box-shadow: 0 0 0 2px rgb(var(--v-theme-background)); box-shadow: 0 0 0 2px rgb(var(--v-theme-background));
@@ -908,7 +898,7 @@ onBeforeMount(() => {
.media-poster { .media-poster {
overflow: hidden; overflow: hidden;
border-radius: .25rem; border-radius: 0.25rem;
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
inline-size: 8rem; inline-size: 8rem;
@@ -925,7 +915,7 @@ onBeforeMount(() => {
@media (width >= 768px) { @media (width >= 768px) {
.media-poster { .media-poster {
border-radius: .5rem; border-radius: 0.5rem;
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
inline-size: 11rem; inline-size: 11rem;
@@ -950,45 +940,45 @@ onBeforeMount(() => {
} }
} }
.media-title>h1 { .media-title > h1 {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 700; font-weight: 700;
line-height: 2rem; line-height: 2rem;
} }
@media (width >= 1280px) { @media (width >= 1280px) {
.media-title>h1 { .media-title > h1 {
font-size: 2.25rem; font-size: 2.25rem;
line-height: 2.5rem; line-height: 2.5rem;
} }
} }
ul.media-crew { ul.media-crew {
display: grid; display: grid;
gap: 1.5rem; gap: 1.5rem;
grid-template-columns: repeat(2,minmax(0,1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
margin-block-start: 1.5rem; margin-block-start: 1.5rem;
} }
@media (width >= 640px) { @media (width >= 640px) {
ul.media-crew { ul.media-crew {
grid-template-columns: repeat(3,minmax(0,1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
} }
} }
ul.media-crew>li { ul.media-crew > li {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
font-weight: 700; font-weight: 700;
grid-column: span 1/span 1; grid-column: span 1 / span 1;
} }
a.crew-name { a.crew-name {
font-weight: 400; font-weight: 400;
} }
.media-status { .media-status {
margin-block-end: .5rem; margin-block-end: 0.5rem;
} }
.media-attributes { .media-attributes {
@@ -996,7 +986,7 @@ a.crew-name {
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-block-start: .25rem; margin-block-start: 0.25rem;
} }
@media (width >= 1280px) { @media (width >= 1280px) {
@@ -1010,7 +1000,7 @@ a.crew-name {
@media (width >= 640px) { @media (width >= 640px) {
.media-attributes { .media-attributes {
font-size: .875rem; font-size: 0.875rem;
line-height: 1.25rem; line-height: 1.25rem;
} }
} }
@@ -1061,69 +1051,66 @@ a.crew-name {
} }
.media-facts { .media-facts {
border-width: 1px; border-width: 1px;
border-color: rgb(55 65 81/var(--tw-border-opacity)); border-color: rgb(55 65 81 / var(--tw-border-opacity));
border-radius: 0.5rem; border-radius: 0.5rem;
font-size: .875rem; font-size: 0.875rem;
font-weight: 700; font-weight: 700;
line-height: 1.25rem; line-height: 1.25rem;
--tw-border-opacity: 1; --tw-border-opacity: 1;
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
--tw-text-opacity: 1; --tw-text-opacity: 1;
} }
.media-ratings { .media-ratings {
border-color: rgb(55 65 81/var(--tw-border-opacity)); display: flex;
border-block-end-width: 1px; align-items: center;
font-weight: 500; justify-content: center;
padding-block: 0.5rem; border-color: rgb(55 65 81 / var(--tw-border-opacity));
padding-inline: 1rem; border-block-end-width: 1px;
font-weight: 500;
padding-block: 0.5rem;
padding-inline: 1rem;
--tw-border-opacity: 1; --tw-border-opacity: 1;
}
.media-ratings {
display: flex;
align-items: center;
justify-content: center;
} }
.media-fact { .media-fact {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
border-color: rgb(55 65 81/var(--tw-border-opacity)); border-color: rgb(55 65 81 / var(--tw-border-opacity));
border-block-end-width: 1px; border-block-end-width: 1px;
padding-block: 0.5rem; padding-block: 0.5rem;
padding-inline: 1rem; padding-inline: 1rem;
--tw-border-opacity: 1; --tw-border-opacity: 1;
} }
.media-overview h2 { .media-overview h2 {
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 700; font-weight: 700;
line-height: 1.75rem; line-height: 1.75rem;
} }
@media (width >= 640px) { @media (width >= 640px) {
.media-overview h2 { .media-overview h2 {
font-size: 1.5rem; font-size: 1.5rem;
line-height: 2rem; line-height: 2rem;
} }
} }
.tagline { .tagline {
font-size: 1.25rem; font-size: 1.25rem;
font-style: italic; font-style: italic;
line-height: 1.75rem; line-height: 1.75rem;
margin-block-end: 1rem; margin-block-end: 1rem;
} }
@media (width >= 1024px) { @media (width >= 1024px) {
.tagline { .tagline {
font-size: 1.5rem; font-size: 1.5rem;
line-height: 2rem; line-height: 2rem;
} }
} }
</style> </style>

View File

@@ -30,8 +30,7 @@ async function fetchData() {
try { try {
dataList.value = await api.get('subscribe/') dataList.value = await api.get('subscribe/')
isRefreshed.value = true isRefreshed.value = true
} } catch (error) {
catch (error) {
console.error(error) console.error(error)
} }
} }
@@ -51,29 +50,18 @@ function onRefresh() {
// 过滤数据,管理员用户显示全部,非管理员只显示自己的订阅 // 过滤数据,管理员用户显示全部,非管理员只显示自己的订阅
const filteredDataList = computed(() => { const filteredDataList = computed(() => {
// 从Vuex Store中获取用户信息 // 从Vuex Store中获取用户信息
const superUser = store.state.auth.superUser const superUser = store.state.auth.superUser
const userName = store.state.auth.userName const userName = store.state.auth.userName
if (superUser) if (superUser) return dataList.value.filter(data => data.type === props.type)
return dataList.value.filter(data => data.type === props.type) else return dataList.value.filter(data => data.type === props.type && data.username === userName)
else
return dataList.value.filter(data => data.type === props.type && (data.username === userName))
}) })
</script> </script>
<template> <template>
<LoadingBanner <LoadingBanner v-if="!isRefreshed" class="mt-12" />
v-if="!isRefreshed" <PullRefresh v-model="loading" @refresh="onRefresh">
class="mt-12" <div v-if="filteredDataList.length > 0" class="grid gap-3 grid-subscribe-card p-1">
/>
<PullRefresh
v-model="loading"
@refresh="onRefresh"
>
<div
v-if="filteredDataList.length > 0"
class="grid gap-3 grid-subscribe-card p-1"
>
<SubscribeCard <SubscribeCard
v-for="data in filteredDataList" v-for="data in filteredDataList"
:key="data.id" :key="data.id"
@@ -91,7 +79,8 @@ const filteredDataList = computed(() => {
</PullRefresh> </PullRefresh>
<!-- 底部操作按钮 --> <!-- 底部操作按钮 -->
<VFab <VFab
icon="mdi-file-document-edit" v-if="store.state.auth.superUser"
icon="mdi-clipboard-edit"
location="bottom end" location="bottom end"
size="x-large" size="x-large"
fixed fixed
@@ -100,6 +89,7 @@ const filteredDataList = computed(() => {
@click="subscribeEditDialog = true" @click="subscribeEditDialog = true"
/> />
<VFab <VFab
v-if="store.state.auth.superUser"
icon="mdi-history" icon="mdi-history"
color="info" color="info"
location="bottom end" location="bottom end"
@@ -120,12 +110,17 @@ const filteredDataList = computed(() => {
@close="subscribeEditDialog = false" @close="subscribeEditDialog = false"
/> />
<!-- 历史记录弹窗 --> <!-- 历史记录弹窗 -->
<SubscribeHistoryDialog <SubscribeHistoryDialog
v-if="historyDialog" v-if="historyDialog"
v-model="historyDialog" v-model="historyDialog"
:type="props.type" :type="props.type"
@close="historyDialog = false" @close="historyDialog = false"
@save="() => {historyDialog = false; fetchData()}" @save="
() => {
historyDialog = false
fetchData()
}
"
/> />
</template> </template>