Merge pull request #281 from wintsa123/v2

This commit is contained in:
jxxghp
2025-01-03 22:19:26 +08:00
committed by GitHub
6 changed files with 84 additions and 91 deletions
+28 -59
View File
@@ -6,7 +6,7 @@ import { formatSeason } from '@/@core/utils/formatters'
import api from '@/api' import api from '@/api'
import { doneNProgress, startNProgress } from '@/api/nprogress' import { doneNProgress, startNProgress } from '@/api/nprogress'
import type { MediaInfo, NotExistMediaInfo, Subscribe, TmdbSeason } from '@/api/types' import type { MediaInfo, NotExistMediaInfo, Subscribe, TmdbSeason } from '@/api/types'
import router from '@/router' import router, { registerAbortController } from '@/router'
import noImage from '@images/no-image.jpeg' import noImage from '@images/no-image.jpeg'
import tmdbImage from '@images/logos/tmdb.png' import tmdbImage from '@images/logos/tmdb.png'
import doubanImage from '@images/logos/douban-black.png' import doubanImage from '@images/logos/douban-black.png'
@@ -59,7 +59,11 @@ const seasonInfos = ref<TmdbSeason[]>([])
// 选中的订阅季 // 选中的订阅季
const seasonsSelected = ref<TmdbSeason[]>([]) const seasonsSelected = ref<TmdbSeason[]>([])
let abortController: AbortController | null = null;
abortController = new AbortController();
registerAbortController(abortController);
const { signal } = abortController;
// 来源角标字典 // 来源角标字典
const sourceIconDict: { [key: string]: any } = { const sourceIconDict: { [key: string]: any } = {
themoviedb: tmdbImage, themoviedb: tmdbImage,
@@ -215,6 +219,7 @@ async function removeSubscribe() {
// 查询当前媒体是否已订阅 // 查询当前媒体是否已订阅
async function handleCheckSubscribe() { async function handleCheckSubscribe() {
try { try {
const result = await checkSubscribe(props.media?.season) const result = await checkSubscribe(props.media?.season)
if (result) isSubscribed.value = true if (result) isSubscribed.value = true
} catch (error) { } catch (error) {
@@ -225,6 +230,7 @@ async function handleCheckSubscribe() {
// 查询当前媒体是否已入库 // 查询当前媒体是否已入库
async function handleCheckExists() { async function handleCheckExists() {
try { try {
const result: { [key: string]: any } = await api.get('mediaserver/exists', { const result: { [key: string]: any } = await api.get('mediaserver/exists', {
params: { params: {
tmdbid: props.media?.tmdb_id, tmdbid: props.media?.tmdb_id,
@@ -233,6 +239,7 @@ async function handleCheckExists() {
season: props.media?.season, season: props.media?.season,
mtype: props.media?.type, mtype: props.media?.type,
}, },
signal
}) })
if (result.success) isExists.value = true if (result.success) isExists.value = true
@@ -244,6 +251,7 @@ async function handleCheckExists() {
// 调用API检查是否已订阅,电视剧需要指定季 // 调用API检查是否已订阅,电视剧需要指定季
async function checkSubscribe(season = 0) { async function checkSubscribe(season = 0) {
try { try {
const mediaid = getMediaId() const mediaid = getMediaId()
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, { const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
@@ -251,6 +259,7 @@ async function checkSubscribe(season = 0) {
season, season,
title: props.media?.title, title: props.media?.title,
}, },
signal
}) })
return result.id || null return result.id || null
@@ -444,25 +453,13 @@ function onRemoveSubscribe() {
<VHover> <VHover>
<template #default="hover"> <template #default="hover">
<div ref="mediaCardRef"> <div ref="mediaCardRef">
<VCard <VCard v-bind="hover.props" :height="props.height" :width="props.width"
v-bind="hover.props" class="outline-none shadow ring-gray-500 rounded-lg" :class="{
:height="props.height"
:width="props.width"
class="outline-none shadow ring-gray-500 rounded-lg"
:class="{
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering, 'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
'ring-1': isImageLoaded, 'ring-1': isImageLoaded,
}" }" @click.stop="goMediaDetail(hover.isHovering ?? false)">
@click.stop="goMediaDetail(hover.isHovering ?? false)" <VImg aspect-ratio="2/3" :src="getImgUrl" class="object-cover aspect-w-2 aspect-h-3" cover
> @load="isImageLoaded = true" @error="imageLoadError = true">
<VImg
aspect-ratio="2/3"
:src="getImgUrl"
class="object-cover aspect-w-2 aspect-h-3"
cover
@load="isImageLoaded = true"
@error="imageLoadError = true"
>
<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" />
@@ -470,11 +467,9 @@ function onRemoveSubscribe() {
</template> </template>
</VImg> </VImg>
<!-- 详情 --> <!-- 详情 -->
<VCardText <VCardText v-show="hover.isHovering || imageLoadError"
v-show="hover.isHovering || imageLoadError"
class="w-full h-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2" class="w-full h-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
style="background: linear-gradient(rgba(45, 55, 72, 40%) 0%, rgba(45, 55, 72, 90%) 100%)" style="background: linear-gradient(rgba(45, 55, 72, 40%) 0%, rgba(45, 55, 72, 90%) 100%)">
>
<span class="font-bold">{{ props.media?.year }}</span> <span class="font-bold">{{ props.media?.year }}</span>
<h1 class="mb-1 text-white font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ..."> <h1 class="mb-1 text-white font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
{{ props.media?.title }} {{ props.media?.title }}
@@ -488,35 +483,21 @@ function onRemoveSubscribe() {
</div> </div>
</VCardText> </VCardText>
<!-- 类型角标 --> <!-- 类型角标 -->
<VChip <VChip v-show="isImageLoaded" variant="elevated" size="small" :class="getChipColor(props.media?.type || '')"
v-show="isImageLoaded" class="absolute left-2 top-2 bg-opacity-80 shadow-md text-white font-bold">
variant="elevated"
size="small"
:class="getChipColor(props.media?.type || '')"
class="absolute left-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
>
{{ props.media?.type }} {{ props.media?.type }}
</VChip> </VChip>
<!-- 本地存在标识 --> <!-- 本地存在标识 -->
<ExistIcon v-if="isExists && !hover.isHovering" /> <ExistIcon v-if="isExists && !hover.isHovering" />
<!-- 评分角标 --> <!-- 评分角标 -->
<VChip <VChip v-if="isImageLoaded && props.media?.vote_average && !(isExists && !hover.isHovering)"
v-if="isImageLoaded && props.media?.vote_average && !(isExists && !hover.isHovering)" variant="elevated" size="small" :class="getChipColor('rating')"
variant="elevated" class="absolute right-2 top-2 bg-opacity-80 shadow-md text-white font-bold">
size="small"
:class="getChipColor('rating')"
class="absolute right-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
>
{{ props.media?.vote_average }} {{ props.media?.vote_average }}
</VChip> </VChip>
<!--来源图标--> <!--来源图标-->
<VAvatar <VAvatar size="24" density="compact" class="absolute bottom-1 right-1" tile
size="24" v-if="!hover.isHovering && isImageLoaded && props.media?.source">
density="compact"
class="absolute bottom-1 right-1"
tile
v-if="!hover.isHovering && isImageLoaded && props.media?.source"
>
<VImg cover :src="sourceIconDict[props.media?.source]" class="shadow-lg" /> <VImg cover :src="sourceIconDict[props.media?.source]" class="shadow-lg" />
</VAvatar> </VAvatar>
</VCard> </VCard>
@@ -535,14 +516,8 @@ function onRemoveSubscribe() {
<VList v-model:selected="seasonsSelected" lines="three" select-strategy="classic"> <VList v-model:selected="seasonsSelected" lines="three" select-strategy="classic">
<VListItem v-for="(item, i) in seasonInfos" :key="i" :value="item"> <VListItem v-for="(item, i) in seasonInfos" :key="i" :value="item">
<template #prepend> <template #prepend>
<VImg <VImg height="90" width="60" :src="getSeasonPoster(item.poster_path || '')" aspect-ratio="2/3"
height="90" class="object-cover rounded shadow ring-gray-500 me-3" cover>
width="60"
:src="getSeasonPoster(item.poster_path || '')"
aspect-ratio="2/3"
class="object-cover rounded shadow ring-gray-500 me-3"
cover
>
<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" />
@@ -581,12 +556,6 @@ function onRemoveSubscribe() {
</VCard> </VCard>
</VBottomSheet> </VBottomSheet>
<!-- 订阅编辑弹窗 --> <!-- 订阅编辑弹窗 -->
<SubscribeEditDialog <SubscribeEditDialog v-if="subscribeEditDialog" v-model="subscribeEditDialog" :subid="subscribeId"
v-if="subscribeEditDialog" @close="subscribeEditDialog = false" @save="subscribeEditDialog = false" @remove="onRemoveSubscribe" />
v-model="subscribeEditDialog"
:subid="subscribeId"
@close="subscribeEditDialog = false"
@save="subscribeEditDialog = false"
@remove="onRemoveSubscribe"
/>
</template> </template>
+1
View File
@@ -68,6 +68,7 @@ const viewList = reactive<{ apipath: string; linkurl: string; title: string }[]>
title: '豆瓣全球剧集榜', title: '豆瓣全球剧集榜',
}, },
]) ])
</script> </script>
<template> <template>
+15 -1
View File
@@ -189,7 +189,18 @@ const router = createRouter({
}, },
], ],
}) })
const abortControllers = new Set<AbortController>()
function registerAbortController(controller: AbortController) {
abortControllers.add(controller)
}
function abortAllControllers() {
for (const controller of abortControllers) {
controller.abort()
}
abortControllers.clear()
}
// 路由导航守卫 // 路由导航守卫
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
// 总是记录非login路由 // 总是记录非login路由
@@ -198,8 +209,11 @@ router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !isAuthenticated) { if (to.meta.requiresAuth && !isAuthenticated) {
next('/login') next('/login')
} else { } else {
abortAllControllers() // 中止所有组件的任务
next() next()
} }
}) })
export default router export default router // 导出默认对象
export { registerAbortController } // 另行导出其他功能
+3 -11
View File
@@ -27,7 +27,6 @@ const isRefreshed = ref(false)
// 数据列表 // 数据列表
const dataList = ref<MediaInfo[]>([]) const dataList = ref<MediaInfo[]>([])
const currData = ref<MediaInfo[]>([]) const currData = ref<MediaInfo[]>([])
// 拼装参数 // 拼装参数
function getParams() { function getParams() {
let params = { let params = {
@@ -78,6 +77,7 @@ async function fetchData({ done }: { done: any }) {
} else { } else {
// 加载一次 // 加载一次
// 设置加载中 // 设置加载中
loading.value = true loading.value = true
// 请求API // 请求API
currData.value = await api.get(props.apipath, { currData.value = await api.get(props.apipath, {
@@ -115,15 +115,7 @@ async function fetchData({ done }: { done: any }) {
<div v-if="dataList.length > 0" class="grid gap-4 grid-media-card mx-3" tabindex="0"> <div v-if="dataList.length > 0" class="grid gap-4 grid-media-card mx-3" tabindex="0">
<MediaCard v-for="data in dataList" :key="data.tmdb_id || data.douban_id" :media="data" /> <MediaCard v-for="data in dataList" :key="data.tmdb_id || data.douban_id" :media="data" />
</div> </div>
<NoDataFound <NoDataFound v-if="dataList.length === 0 && isRefreshed" error-code="404" error-title="没有数据"
v-if="dataList.length === 0 && isRefreshed" error-description="无法获取到媒体信息" />
error-code="404"
error-title="没有数据"
error-description="无法获取到媒体信息"
/>
</VInfiniteScroll> </VInfiniteScroll>
</template> </template>
<style lang="scss">
</style>
+19 -16
View File
@@ -3,6 +3,7 @@ import api from '@/api'
import type { MediaInfo } from '@/api/types' import type { MediaInfo } from '@/api/types'
import MediaCard from '@/components/cards/MediaCard.vue' import MediaCard from '@/components/cards/MediaCard.vue'
import SlideView from '@/components/slide/SlideView.vue' import SlideView from '@/components/slide/SlideView.vue'
import { registerAbortController } from "@/router";
// 输入参数 // 输入参数
const props = defineProps({ const props = defineProps({
@@ -10,8 +11,9 @@ const props = defineProps({
linkurl: String, linkurl: String,
title: String, title: String,
}) })
let abortController: AbortController | null = null;
provide('rankingPropsKey', reactive({...props})) provide('rankingPropsKey', reactive({ ...props }))
// 组件加载完成 // 组件加载完成
const componentLoaded = ref(false) const componentLoaded = ref(false)
@@ -24,8 +26,10 @@ async function fetchData() {
try { try {
if (!props.apipath) if (!props.apipath)
return return
abortController = new AbortController();
dataList.value = await api.get(props.apipath) registerAbortController(abortController);
const { signal } = abortController;
dataList.value = await api.get(props.apipath, { signal })
if (dataList.value.length > 0) if (dataList.value.length > 0)
componentLoaded.value = true componentLoaded.value = true
} }
@@ -35,23 +39,22 @@ async function fetchData() {
} }
// 加载时获取数据 // 加载时获取数据
onMounted(fetchData) onMounted(() => {
fetchData();
});
onActivated(() => {
if (dataList.value.length == 0) {
fetchData();
}
});
</script> </script>
<template> <template>
<SlideView <SlideView v-if="componentLoaded">
v-if="componentLoaded"
>
<template #content> <template #content>
<template <template v-for="data in dataList" :key="data.tmdb_id || data.douban_id || data.bangumi_id">
v-for="data in dataList" <MediaCard :media="data" height="15rem" width="10rem" />
:key="data.tmdb_id || data.douban_id || data.bangumi_id"
>
<MediaCard
:media="data"
height="15rem"
width="10rem"
/>
</template> </template>
</template> </template>
</SlideView> </SlideView>
+18 -4
View File
@@ -6618,8 +6618,16 @@ stop-iteration-iterator@^1.0.0:
dependencies: dependencies:
internal-slot "^1.0.4" internal-slot "^1.0.4"
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.3: "string-width-cjs@npm:string-width@^4.2.0":
name string-width-cjs version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.3:
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -6692,8 +6700,14 @@ stringify-object@^3.3.0:
is-obj "^1.0.1" is-obj "^1.0.1"
is-regexp "^1.0.0" is-regexp "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: "strip-ansi-cjs@npm:strip-ansi@^6.0.1":
name strip-ansi-cjs version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==