Files
MoviePilot-Frontend/src/views/subscribe/SubscribeListView.vue
2025-07-21 09:57:10 +08:00

267 lines
6.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts" setup>
import draggable from 'vuedraggable'
import api from '@/api'
import type { Subscribe } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue'
import SubscribeCard from '@/components/cards/SubscribeCard.vue'
import SubscribeHistoryDialog from '@/components/dialog/SubscribeHistoryDialog.vue'
import { useUserStore } from '@/stores'
import { useDynamicButton } from '@/composables/useDynamicButton'
import { useI18n } from 'vue-i18n'
import { usePWA } from '@/composables/usePWA'
// 国际化
const { t } = useI18n()
// 路由
const route = useRoute()
// PWA模式检测
const { appMode } = usePWA()
// 用户 Store
const userStore = useUserStore()
// 从 Store 中获取用户信息
const superUser = userStore.superUser
const userName = userStore.userName
// 输入参数
const props = defineProps({
type: String,
subid: String,
keyword: String,
statusFilter: String,
})
// 是否刷新过
let isRefreshed = ref(false)
// 刷新状态
const loading = ref(false)
// 数据列表
const dataList = ref<Subscribe[]>([])
// 历史记录弹窗
const historyDialog = ref(false)
// 订阅顺序配置
const orderConfig = ref<{ id: number }[]>([])
// 显示的订阅列表
const displayList = ref<Subscribe[]>([])
// 根据订阅数据判断订阅状态
function getSubscribeStatus(subscribe: Subscribe) {
// 洗版中
if (subscribe.best_version) {
return 'best_version'
}
// 根据订阅状态判断
if (subscribe.state === 'P') {
return 'pending' // 待定
} else if (subscribe.state === 'S') {
return 'paused' // 暂停
}
// 如果是电影,只有洗版和状态
if (subscribe.type === '电影') {
return 'all'
}
// 电视剧根据集数情况判断
if (subscribe.total_episode && subscribe.total_episode > 0) {
const lackEpisode = subscribe.lack_episode || 0
const completedEpisode = subscribe.total_episode - lackEpisode
if (lackEpisode === 0) {
return 'completed' // 订阅完成
} else if (completedEpisode > 0) {
return 'subscribing' // 订阅中
} else {
return 'not_started' // 未开始
}
}
return 'not_started' // 默认未开始
}
// API请求键值计算属性
const orderRequestKey = computed(() => (props.type === '电影' ? 'SubscribeMovieOrder' : 'SubscribeTvOrder'))
// 监听dataList变化同步更新displayList
watch([dataList, () => props.keyword, () => props.statusFilter], () => {
if (superUser)
displayList.value = dataList.value.filter(
data =>
data.type === props.type &&
(!props.keyword || data.name.toLowerCase().includes(props.keyword.toLowerCase())) &&
(!props.statusFilter || props.statusFilter === 'all' || getSubscribeStatus(data) === props.statusFilter),
)
else
displayList.value = dataList.value.filter(
data =>
data.type === props.type &&
data.username === userName &&
(!props.keyword || data.name.toLowerCase().includes(props.keyword.toLowerCase())) &&
(!props.statusFilter || props.statusFilter === 'all' || getSubscribeStatus(data) === props.statusFilter),
)
// 排序
sortSubscribeOrder()
})
// 加载顺序
async function loadSubscribeOrderConfig() {
try {
const response = await api.get(`/user/config/${orderRequestKey.value}`)
if (response && response.data && response.data.value) {
orderConfig.value = response.data.value
}
} catch (error) {
console.error('Failed to load subscribe order config:', error)
orderConfig.value = []
}
}
// 按order的顺序排序
async function sortSubscribeOrder() {
if (!orderConfig.value) {
return
}
if (displayList.value.length === 0) {
return
}
await loadSubscribeOrderConfig()
displayList.value.sort((a, b) => {
const aIndex = orderConfig.value.findIndex((item: { id: number }) => item.id === a.id)
const bIndex = orderConfig.value.findIndex((item: { id: number }) => item.id === b.id)
return (aIndex === -1 ? 999 : aIndex) - (bIndex === -1 ? 999 : bIndex)
})
}
// 保存顺序设置
async function saveSubscribeOrder() {
// 顺序配置
const orderObj = displayList.value.map(item => ({ id: item.id }))
orderConfig.value = orderObj
// 保存到服务端
try {
await api.post(`/user/config/${orderRequestKey.value}`, orderObj)
} catch (error) {
console.error(error)
}
}
// 获取订阅列表数据
async function fetchData() {
try {
loading.value = true
dataList.value = await api.get('subscribe/')
loading.value = false
isRefreshed.value = true
} catch (error) {
console.error(error)
}
}
// 历史记录窗口完成
function historyDone() {
historyDialog.value = false
fetchData()
}
// 错误描述
const errorDescription = computed(() => {
if ((props.statusFilter && props.statusFilter !== 'all') || props.keyword) {
return t('common.tryChangingFilters')
}
return t('subscribe.noSubscribeData')
})
// 错误标题
const errorTitle = computed(() => {
if ((props.statusFilter && props.statusFilter !== 'all') || props.keyword) {
return t('common.noMatchingData')
}
return t('common.noData')
})
onMounted(async () => {
await fetchData()
if (props.subid) {
// 找到这个订阅
const sub = dataList.value.find(sub => sub.id.toString() == props.subid?.toString())
if (sub) {
// 打开编辑弹窗
sub.page_open = true
}
}
})
onActivated(async () => {
if (!loading.value) {
fetchData()
}
})
// 使用动态按钮钩子
useDynamicButton({
icon: 'mdi-history',
onClick: () => {
historyDialog.value = true
},
})
</script>
<template>
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
<draggable
v-if="displayList.length > 0"
v-model="displayList"
@end="saveSubscribeOrder"
handle=".cursor-move"
item-key="id"
tag="div"
:component-data="{ class: 'grid gap-4 grid-subscribe-card px-2' }"
:disabled="props.keyword || (props.statusFilter && props.statusFilter !== 'all')"
>
<template #item="{ element }">
<SubscribeCard :key="element.id" :media="element" @remove="fetchData" @save="fetchData" />
</template>
</draggable>
<NoDataFound
v-if="displayList.length === 0 && isRefreshed"
error-code="404"
:error-title="errorTitle"
:error-description="errorDescription"
/>
<!-- 底部操作按钮 -->
<Teleport to="body" v-if="route.path.startsWith(`/subscribe/${props.type === '电影' ? 'movie' : 'tv'}`)">
<div v-if="isRefreshed">
<VFab
v-if="userStore.superUser && !appMode"
icon="mdi-history"
color="info"
location="bottom"
:class="{ 'mb-12': appMode }"
size="x-large"
fixed
app
appear
@click="historyDialog = true"
/>
</div>
</Teleport>
<!-- 历史记录弹窗 -->
<SubscribeHistoryDialog
v-if="historyDialog"
v-model="historyDialog"
:type="props.type"
@close="historyDialog = false"
@save="historyDone"
/>
</template>