mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-21 08:10:16 +08:00
471 lines
14 KiB
Vue
471 lines
14 KiB
Vue
<script lang="ts" setup>
|
|
import { useToast } from 'vue-toastification'
|
|
import { useConfirm } from '@/composables/useConfirm'
|
|
import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
|
|
import SubscribeFilesDialog from '../dialog/SubscribeFilesDialog.vue'
|
|
import SubscribeShareDialog from '../dialog/SubscribeShareDialog.vue'
|
|
import { formatDateDifference, formatSeason } from '@/@core/utils/formatters'
|
|
import api from '@/api'
|
|
import type { Subscribe } from '@/api/types'
|
|
import router from '@/router'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useDisplay } from 'vuetify'
|
|
import { useGlobalSettingsStore } from '@/stores'
|
|
|
|
// 显示器宽度
|
|
const display = useDisplay()
|
|
|
|
// 国际化
|
|
const { t } = useI18n()
|
|
|
|
// 输入参数
|
|
const props = defineProps({
|
|
media: Object as PropType<Subscribe>,
|
|
batchMode: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
selected: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
})
|
|
|
|
// 从 provide 中获取全局设置
|
|
// 全局设置
|
|
const globalSettingsStore = useGlobalSettingsStore()
|
|
const globalSettings = globalSettingsStore.globalSettings
|
|
|
|
// 定义触发的自定义事件
|
|
const emit = defineEmits(['remove', 'save', 'select'])
|
|
|
|
// 确认框
|
|
const createConfirm = useConfirm()
|
|
|
|
// 提示框
|
|
const $toast = useToast()
|
|
|
|
// 图片是否加载完成
|
|
const imageLoaded = ref(false)
|
|
|
|
// 订阅弹窗
|
|
const subscribeEditDialog = ref(false)
|
|
|
|
// 订阅文件信息弹窗
|
|
const subscribeFilesDialog = ref(false)
|
|
|
|
// 分享订阅弹窗
|
|
const subscribeShareDialog = ref(false)
|
|
|
|
// 当前的订阅状态
|
|
const subscribeState = ref<string>(props.media?.state ?? 'P')
|
|
|
|
// 上一次更新时间
|
|
const lastUpdateText = computed(() => (props.media?.last_update ? formatDateDifference(props.media.last_update) : ''))
|
|
|
|
// 图片加载完成响应
|
|
function imageLoadHandler() {
|
|
imageLoaded.value = true
|
|
}
|
|
|
|
// 计算百分比
|
|
function getPercentage() {
|
|
if (props.media?.total_episode === 0) return 0
|
|
|
|
return Math.round(
|
|
(((props.media?.total_episode ?? 0) - (props.media?.lack_episode ?? 0)) / (props.media?.total_episode ?? 1)) * 100,
|
|
)
|
|
}
|
|
|
|
// 删除订阅
|
|
async function removeSubscribe() {
|
|
try {
|
|
const result: { [key: string]: any } = await api.delete(`subscribe/${props.media?.id}`)
|
|
|
|
if (result.success) {
|
|
// 通知父组件刷新
|
|
emit('remove')
|
|
}
|
|
} catch (e) {
|
|
console.log(e)
|
|
}
|
|
}
|
|
|
|
// 搜索订阅
|
|
async function searchSubscribe() {
|
|
try {
|
|
const result: { [key: string]: any } = await api.get(`subscribe/search/${props.media?.id}`)
|
|
|
|
// 提示
|
|
if (result.success) $toast.success(`${props.media?.name} 提交搜索请求成功!`)
|
|
} catch (e) {
|
|
console.log(e)
|
|
}
|
|
}
|
|
|
|
// 切换订阅状态
|
|
async function toggleSubscribeStatus(state: 'R' | 'S') {
|
|
try {
|
|
// 根据传入的 state 判断对应的操作文字
|
|
const action = state === 'S' ? t('common.pause') : t('common.enable')
|
|
// 弹出确认框
|
|
const isConfirmed = await createConfirm({
|
|
title: t('common.confirmAction', { action }),
|
|
content: t('subscribe.confirmToggle', { action, name: props.media?.name }),
|
|
})
|
|
if (!isConfirmed) return
|
|
// 调用 API 更新订阅状态
|
|
const result: { [key: string]: any } = await api.put(`subscribe/status/${props.media?.id}?state=${state}`)
|
|
// 提示
|
|
if (result.success) {
|
|
$toast.success(t('subscribe.toggleSuccess', { name: props.media?.name, action }))
|
|
subscribeState.value = state
|
|
emit('save')
|
|
} else {
|
|
$toast.error(t('subscribe.toggleFailed', { action, message: result.message }))
|
|
}
|
|
} catch (e) {
|
|
console.log(e)
|
|
}
|
|
}
|
|
|
|
// 重置订阅
|
|
async function resetSubscribe() {
|
|
// 确认
|
|
try {
|
|
const isConfirmed = await createConfirm({
|
|
title: t('common.confirm'),
|
|
content: t('subscribe.resetConfirm', { name: props.media?.name }),
|
|
})
|
|
if (!isConfirmed) return
|
|
// 重置
|
|
const result: { [key: string]: any } = await api.get(`subscribe/reset/${props.media?.id}`)
|
|
// 提示
|
|
if (result.success) {
|
|
$toast.success(t('subscribe.resetSuccess', { name: props.media?.name }))
|
|
subscribeState.value = 'R'
|
|
emit('save')
|
|
} else $toast.error(t('subscribe.resetFailed', { name: props.media?.name, message: result.message }))
|
|
} catch (e) {
|
|
console.log(e)
|
|
}
|
|
}
|
|
|
|
// 分享订阅
|
|
async function shareSubscribe() {
|
|
subscribeShareDialog.value = true
|
|
}
|
|
|
|
// 编辑订阅响应
|
|
async function editSubscribeDialog() {
|
|
subscribeEditDialog.value = true
|
|
}
|
|
|
|
// 获得mediaid
|
|
function getMediaId() {
|
|
if (props.media?.tmdbid) return `tmdb:${props.media?.tmdbid}`
|
|
else if (props.media?.doubanid) return `douban:${props.media?.doubanid}`
|
|
else if (props.media?.bangumiid) return `bangumi:${props.media?.bangumiid}`
|
|
else return props.media?.mediaid
|
|
}
|
|
|
|
// 查看媒体详情
|
|
async function viewMediaDetail() {
|
|
router.push({
|
|
path: '/media',
|
|
query: {
|
|
mediaid: getMediaId(),
|
|
title: props.media?.name,
|
|
year: props.media?.year,
|
|
type: props.media?.type,
|
|
},
|
|
})
|
|
}
|
|
|
|
// 查看文件详情
|
|
async function viewSubscribeFiles() {
|
|
subscribeFilesDialog.value = true
|
|
}
|
|
|
|
// 弹出菜单
|
|
const dropdownItems = computed(() => [
|
|
{
|
|
title: t('common.edit'),
|
|
value: 1,
|
|
props: {
|
|
prependIcon: 'mdi-file-edit-outline',
|
|
click: editSubscribeDialog,
|
|
},
|
|
},
|
|
{
|
|
title: t('common.search'),
|
|
value: 2,
|
|
props: {
|
|
prependIcon: 'mdi-magnify',
|
|
click: searchSubscribe,
|
|
},
|
|
},
|
|
{
|
|
title: t('common.details'),
|
|
value: 3,
|
|
props: {
|
|
prependIcon: 'mdi-information-outline',
|
|
click: viewMediaDetail,
|
|
},
|
|
},
|
|
{
|
|
title: t('common.files'),
|
|
value: 4,
|
|
props: {
|
|
prependIcon: 'mdi-file-document-outline',
|
|
click: viewSubscribeFiles,
|
|
},
|
|
},
|
|
{
|
|
title: subscribeState.value === 'S' ? t('common.enable') : t('common.pause'),
|
|
value: 5,
|
|
props: {
|
|
prependIcon: subscribeState.value === 'S' ? 'mdi-play' : 'mdi-pause',
|
|
click: () => toggleSubscribeStatus(subscribeState.value === 'S' ? 'R' : 'S'),
|
|
color: subscribeState.value === 'S' ? 'success' : 'info',
|
|
},
|
|
},
|
|
{
|
|
title: t('common.reset'),
|
|
value: 6,
|
|
props: {
|
|
prependIcon: 'mdi-restore-alert',
|
|
click: resetSubscribe,
|
|
color: 'warning',
|
|
},
|
|
},
|
|
{
|
|
title: t('common.share'),
|
|
value: 7,
|
|
props: {
|
|
prependIcon: 'mdi-share',
|
|
click: shareSubscribe,
|
|
color: 'success',
|
|
},
|
|
show: props.media?.type === '电视剧',
|
|
},
|
|
{
|
|
title: t('common.unsubscribe'),
|
|
value: 8,
|
|
props: {
|
|
prependIcon: 'mdi-trash-can-outline',
|
|
color: 'error',
|
|
click: removeSubscribe,
|
|
},
|
|
},
|
|
])
|
|
|
|
// 监听插件窗口状态变化
|
|
watch(
|
|
() => props.media?.page_open,
|
|
(newOpenState, _) => {
|
|
if (newOpenState) editSubscribeDialog()
|
|
},
|
|
)
|
|
|
|
// 监听订阅状态
|
|
watch(
|
|
() => props.media?.state,
|
|
newState => {
|
|
subscribeState.value = newState ?? 'P'
|
|
},
|
|
)
|
|
|
|
// 计算backdrop图片地址
|
|
const backdropUrl = computed(() => {
|
|
const url = props.media?.backdrop || props.media?.poster
|
|
// 使用图片缓存
|
|
if (globalSettings.GLOBAL_IMAGE_CACHE && url)
|
|
return `${import.meta.env.VITE_API_BASE_URL}system/cache/image?url=${encodeURIComponent(url)}`
|
|
return url
|
|
})
|
|
|
|
// 计算海报图片地址
|
|
const posterUrl = computed(() => {
|
|
const url = props.media?.poster
|
|
// 使用图片缓存
|
|
if (globalSettings.GLOBAL_IMAGE_CACHE && url)
|
|
return `${import.meta.env.VITE_API_BASE_URL}system/cache/image?url=${encodeURIComponent(url)}`
|
|
return url
|
|
})
|
|
|
|
// 订阅编辑保存
|
|
function onSubscribeEditSave() {
|
|
subscribeEditDialog.value = false
|
|
emit('save')
|
|
}
|
|
|
|
// 订阅编辑取消
|
|
function onSubscribeEditRemove() {
|
|
subscribeEditDialog.value = false
|
|
emit('remove')
|
|
}
|
|
|
|
// 处理卡片点击事件
|
|
function handleCardClick() {
|
|
if (props.batchMode) {
|
|
// 批量模式下触发选择事件
|
|
emit('select')
|
|
} else {
|
|
// 非批量模式下打开编辑弹窗
|
|
editSubscribeDialog()
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<VHover>
|
|
<template #default="hover">
|
|
<div
|
|
class="w-full h-full rounded-lg overflow-hidden"
|
|
:class="{
|
|
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
|
|
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
|
|
'outline-dotted outline-pink-500 outline-2': props.batchMode && props.selected,
|
|
}"
|
|
>
|
|
<VCard
|
|
v-bind="hover.props"
|
|
:key="props.media?.id"
|
|
class="flex flex-col h-full"
|
|
:class="{
|
|
'opacity-70': subscribeState === 'S',
|
|
}"
|
|
rounded="0"
|
|
min-height="150"
|
|
@click="handleCardClick"
|
|
:ripple="!props.batchMode"
|
|
>
|
|
<div class="me-n3 absolute top-1 right-4">
|
|
<IconBtn>
|
|
<VIcon icon="mdi-dots-vertical" color="white" />
|
|
<VMenu activator="parent" close-on-content-click>
|
|
<VList>
|
|
<template v-for="(item, i) in dropdownItems" :key="i">
|
|
<VListItem v-if="item.show !== false" :base-color="item.props.color" @click="item.props.click">
|
|
<template #prepend>
|
|
<VIcon :icon="item.props.prependIcon" />
|
|
</template>
|
|
<VListItemTitle v-text="item.title" />
|
|
</VListItem>
|
|
</template>
|
|
</VList>
|
|
</VMenu>
|
|
</IconBtn>
|
|
</div>
|
|
<template #image>
|
|
<VImg :src="backdropUrl || posterUrl" aspect-ratio="3/2" cover @load="imageLoadHandler" position="top">
|
|
<template #placeholder>
|
|
<div class="w-full h-full">
|
|
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
|
</div>
|
|
</template>
|
|
<template #default>
|
|
<div class="absolute inset-0 outline-none subscribe-card-background"></div>
|
|
</template>
|
|
</VImg>
|
|
<div
|
|
v-if="subscribeState === 'P'"
|
|
class="absolute inset-0 bg-yellow-900 opacity-80 pointer-events-none"
|
|
/>
|
|
</template>
|
|
<div>
|
|
<VCardText class="flex items-center pt-3 pb-2">
|
|
<div
|
|
class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md"
|
|
v-if="imageLoaded"
|
|
:class="{ 'cursor-move': display.mdAndUp.value }"
|
|
>
|
|
<VImg :src="posterUrl" aspect-ratio="2/3" cover>
|
|
<template #placeholder>
|
|
<div class="w-full h-full">
|
|
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
|
</div>
|
|
</template>
|
|
</VImg>
|
|
</div>
|
|
<div class="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
|
|
<div class="text-sm font-medium text-white sm:pt-1">{{ props.media?.year }}</div>
|
|
<div class="mr-2 min-w-0 text-lg font-bold text-white text-ellipsis overflow-hidden line-clamp-2 ...">
|
|
{{ props.media?.name }}
|
|
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
|
|
</div>
|
|
</div>
|
|
</VCardText>
|
|
<VCardText class="flex justify-space-between align-center flex-wrap px-3">
|
|
<div class="flex align-center">
|
|
<IconBtn
|
|
v-if="props.media?.total_episode"
|
|
size="small"
|
|
v-bind="props"
|
|
icon="mdi-progress-download"
|
|
color="white"
|
|
/>
|
|
<div v-if="props.media?.season" class="text-subtitle-2 me-2 text-white">
|
|
{{ (props.media?.total_episode || 0) - (props.media?.lack_episode || 0) }} /
|
|
{{ props.media?.total_episode }}
|
|
</div>
|
|
<IconBtn v-if="props.media?.username" icon="mdi-account" size="small" color="white" />
|
|
<span v-if="props.media?.username" class="text-subtitle-2 text-white">
|
|
{{ props.media?.username }}
|
|
</span>
|
|
</div>
|
|
</VCardText>
|
|
<VCardText
|
|
v-if="lastUpdateText"
|
|
class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300 text-xs"
|
|
>
|
|
<VIcon icon="mdi-download" class="me-1" />
|
|
{{ lastUpdateText }}
|
|
</VCardText>
|
|
<div class="w-full absolute bottom-0">
|
|
<VProgressLinear
|
|
v-if="getPercentage() > 0"
|
|
:model-value="getPercentage()"
|
|
bg-color="success"
|
|
color="success"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</VCard>
|
|
</div>
|
|
</template>
|
|
</VHover>
|
|
<!-- 订阅编辑弹窗 -->
|
|
<SubscribeEditDialog
|
|
v-if="subscribeEditDialog"
|
|
v-model="subscribeEditDialog"
|
|
:subid="props.media?.id"
|
|
@remove="onSubscribeEditRemove"
|
|
@save="onSubscribeEditSave"
|
|
@close="subscribeEditDialog = false"
|
|
/>
|
|
|
|
<!-- 订阅文件信息弹窗 -->
|
|
<SubscribeFilesDialog
|
|
v-if="subscribeFilesDialog"
|
|
v-model="subscribeFilesDialog"
|
|
:subid="props.media?.id"
|
|
@close="subscribeFilesDialog = false"
|
|
/>
|
|
<!-- 分享订阅弹窗 -->
|
|
<SubscribeShareDialog
|
|
v-if="subscribeShareDialog"
|
|
v-model="subscribeShareDialog"
|
|
:sub="props.media"
|
|
@close="subscribeShareDialog = false"
|
|
/>
|
|
</div>
|
|
</template>
|
|
<style lang="scss" scoped>
|
|
.subscribe-card-background {
|
|
background-image: linear-gradient(180deg, rgba(31, 41, 55, 47%) 0%, rgb(31, 41, 55) 100%);
|
|
}
|
|
</style>
|