mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-10 17:42:50 +08:00
add subscribe share cards
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
// 订阅
|
||||
export interface Subscribe {
|
||||
// 订阅ID
|
||||
id: number
|
||||
// 订阅名称
|
||||
name: string
|
||||
@@ -73,6 +74,64 @@ export interface Subscribe {
|
||||
filter_groups?: string[]
|
||||
}
|
||||
|
||||
// 订阅分享
|
||||
export interface SubscribeShare {
|
||||
// 分享ID
|
||||
id?: number
|
||||
// 订阅ID
|
||||
subscribe_id?: number
|
||||
// 分享标题
|
||||
share_title?: string
|
||||
// 分享说明
|
||||
share_comment?: string
|
||||
// 分享人
|
||||
share_user?: string
|
||||
// 订阅名称
|
||||
name?: string
|
||||
// 订阅年份
|
||||
year?: string
|
||||
// 订阅类型 电影/电视剧
|
||||
type?: string
|
||||
// 搜索关键字
|
||||
keyword?: string
|
||||
// TMDB ID
|
||||
tmdbid?: number
|
||||
// 豆瓣ID
|
||||
doubanid?: string
|
||||
// 季号
|
||||
season?: number
|
||||
// 海报
|
||||
poster?: string
|
||||
// 背景图
|
||||
backdrop?: string
|
||||
// 评分
|
||||
vote?: number
|
||||
// 描述
|
||||
description?: string
|
||||
// 过滤规则
|
||||
filter?: string
|
||||
// 包含
|
||||
include?: string
|
||||
// 排除
|
||||
exclude?: string
|
||||
// 质量
|
||||
quality?: string
|
||||
// 分辨率
|
||||
resolution?: string
|
||||
// 特效
|
||||
effect?: string
|
||||
// 总集数
|
||||
total_episode?: number
|
||||
// 时间
|
||||
date?: string
|
||||
// 自定义识别词
|
||||
custom_words?: string
|
||||
// 自定义媒体类别
|
||||
media_category?: string
|
||||
// 复用次数
|
||||
count?: number
|
||||
}
|
||||
|
||||
// 历史记录
|
||||
export interface TransferHistory {
|
||||
// ID
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useToast } from 'vue-toast-notification'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
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'
|
||||
@@ -34,6 +35,9 @@ const subscribeEditDialog = ref(false)
|
||||
// 订阅文件信息弹窗
|
||||
const subscribeFilesDialog = ref(false)
|
||||
|
||||
// 分享订阅弹窗
|
||||
const subscribeShareDialog = ref(false)
|
||||
|
||||
// 上一次更新时间
|
||||
const lastUpdateText = ref(props.media && props.media.last_update ? formatDateDifference(props.media.last_update) : '')
|
||||
|
||||
@@ -98,6 +102,11 @@ async function resetSubscribe() {
|
||||
}
|
||||
}
|
||||
|
||||
// 分享订阅
|
||||
async function shareSubscribe() {
|
||||
subscribeShareDialog.value = true
|
||||
}
|
||||
|
||||
// 编辑订阅响应
|
||||
async function editSubscribeDialog() {
|
||||
subscribeEditDialog.value = true
|
||||
@@ -164,8 +173,18 @@ const dropdownItems = ref([
|
||||
show: props.media?.type === '电视剧',
|
||||
},
|
||||
{
|
||||
title: '取消订阅',
|
||||
title: '分享',
|
||||
value: 6,
|
||||
props: {
|
||||
prependIcon: 'mdi-share',
|
||||
click: shareSubscribe,
|
||||
color: 'success',
|
||||
},
|
||||
show: props.media?.type === '电视剧',
|
||||
},
|
||||
{
|
||||
title: '取消订阅',
|
||||
value: 7,
|
||||
props: {
|
||||
prependIcon: 'mdi-trash-can-outline',
|
||||
color: 'error',
|
||||
@@ -330,6 +349,13 @@ function onSubscribeEditRemove() {
|
||||
:subid="props.media?.id"
|
||||
@close="subscribeFilesDialog = false"
|
||||
/>
|
||||
<!-- 分享订阅弹窗 -->
|
||||
<SubscribeShareDialog
|
||||
v-if="subscribeShareDialog"
|
||||
v-model="subscribeShareDialog"
|
||||
:sub="props.media"
|
||||
@close="subscribeShareDialog = false"
|
||||
/>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.subscribe-card-background {
|
||||
|
||||
146
src/components/cards/SubscribeShareCard.vue
Normal file
146
src/components/cards/SubscribeShareCard.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import type { SubscribeShare } from '@/api/types'
|
||||
import router from '@/router'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
media: Object as PropType<SubscribeShare>,
|
||||
})
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
// 从 provide 中获取全局设置
|
||||
const globalSettings: any = inject('globalSettings')
|
||||
|
||||
// 图片是否加载完成
|
||||
const imageLoaded = ref(false)
|
||||
|
||||
// 图片加载完成响应
|
||||
function imageLoadHandler() {
|
||||
imageLoaded.value = true
|
||||
}
|
||||
|
||||
// 计算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
|
||||
})
|
||||
|
||||
// 查看媒体详情
|
||||
async function viewMediaDetail() {
|
||||
router.push({
|
||||
path: '/media',
|
||||
query: {
|
||||
mediaid: `${props.media?.tmdbid ? `tmdb:${props.media?.tmdbid}` : `douban:${props.media?.doubanid}`}`,
|
||||
type: props.media?.type,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 复用订阅
|
||||
async function forkSubscribe() {
|
||||
// 开始处理
|
||||
startNProgress()
|
||||
try {
|
||||
// 确认
|
||||
const isConfirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: `是否确认添加来自 ${props.media?.share_user} 分享的订阅:${props.media?.share_title}?`,
|
||||
})
|
||||
if (!isConfirmed) return
|
||||
|
||||
// 请求API
|
||||
const result: { [key: string]: any } = await api.post('subscribe/fork', props.media)
|
||||
|
||||
// 订阅状态
|
||||
if (result.success) {
|
||||
$toast.success(`${props.media?.share_title} 添加订阅成功!`)
|
||||
} else {
|
||||
$toast.error(`${props.media?.share_title} 添加订阅失败:${result.message}!`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
doneNProgress()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:key="props.media?.id"
|
||||
class="flex flex-col rounded-lg"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||
}"
|
||||
min-height="170"
|
||||
@click="forkSubscribe"
|
||||
>
|
||||
<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>
|
||||
<div class="absolute inset-0 subscribe-card-background"></div>
|
||||
</VImg>
|
||||
</template>
|
||||
<div>
|
||||
<VCardText class="flex items-center">
|
||||
<div class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md shadow-lg" v-if="imageLoaded">
|
||||
<VImg :src="posterUrl" aspect-ratio="2/3" cover @click.stop="viewMediaDetail">
|
||||
<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">
|
||||
{{ props.media?.share_title }}
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText class="flex justify-space-between align-center flex-wrap">
|
||||
{{ props.media?.share_comment }}
|
||||
</VCardText>
|
||||
<VCardText class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300">
|
||||
<VIcon icon="mdi-account" class="me-1" />
|
||||
{{ props.media?.share_user }}
|
||||
</VCardText>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.subscribe-card-background {
|
||||
background-image: linear-gradient(90deg, rgba(31, 41, 55, 47%) 0%, rgb(31, 41, 55) 100%);
|
||||
}
|
||||
</style>
|
||||
92
src/components/dialog/SubscribeShareDialog.vue
Normal file
92
src/components/dialog/SubscribeShareDialog.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { requiredValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import type { Subscribe, SubscribeShare } from '@/api/types'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
sub: Object as PropType<Subscribe>,
|
||||
})
|
||||
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
// 订阅编辑表单
|
||||
const shareForm = ref<SubscribeShare>({
|
||||
subscribe_id: props.sub?.id ?? 0,
|
||||
})
|
||||
|
||||
// 分享订阅
|
||||
async function doShare() {
|
||||
if (!shareForm.value.share_title || !shareForm.value.share_comment || !shareForm.value.share_user) return
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('subscribe/share', shareForm.value)
|
||||
// 提示
|
||||
if (result.success) {
|
||||
$toast.success(`${props.sub?.name} 分享成功!`)
|
||||
// 通知父组件刷新
|
||||
emit('close')
|
||||
} else {
|
||||
$toast.error(`${props.sub?.name} 分享失败:${result.message}!`)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog scrollable max-width="50rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard
|
||||
:title="`分享订阅 - ${props.sub?.name} ${props.sub?.season ? `第 ${props.sub?.season} 季` : ''}`"
|
||||
class="rounded-t"
|
||||
>
|
||||
<VCardText>
|
||||
<DialogCloseBtn @click="emit('close')" />
|
||||
<VForm @submit.prevent="() => {}" class="pt-2">
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="shareForm.share_title"
|
||||
label="标题"
|
||||
hint="给分享取一个便于识别的名称"
|
||||
:rules="[requiredValidator]"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextarea
|
||||
v-model="shareForm.share_comment"
|
||||
label="说明"
|
||||
:rules="[requiredValidator]"
|
||||
hint="关于该订阅的说明"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="shareForm.share_user"
|
||||
label="分享用户"
|
||||
:rules="[requiredValidator]"
|
||||
hint="分享人的昵称"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardActions class="pt-3">
|
||||
<VSpacer />
|
||||
<VBtn variant="elevated" @click="doShare" prepend-icon="mdi-share" class="px-5"> 确认分享 </VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import SubscribeListView from '@/views/subscribe/SubscribeListView.vue'
|
||||
import SubscribePopularView from '@/views/subscribe/SubscribePopularView.vue'
|
||||
import SubscribeShareView from '@/views/subscribe/SubscribeShareView.vue'
|
||||
import { SubscribeMovieTabs } from '@/router/menu'
|
||||
import router from '@/router'
|
||||
|
||||
@@ -21,6 +22,9 @@ function jumpTab(tab: string) {
|
||||
<VTab v-for="item in SubscribeMovieTabs" :value="item.tab" @to="jumpTab(item.tab)">
|
||||
<span class="mx-5">{{ item.title }}</span>
|
||||
</VTab>
|
||||
<VTab v-if="subType == '电视剧'" value="share" @to="jumpTab('share')">
|
||||
<span class="mx-5">订阅分享</span>
|
||||
</VTab>
|
||||
</VTabs>
|
||||
|
||||
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
|
||||
@@ -34,6 +38,11 @@ function jumpTab(tab: string) {
|
||||
<SubscribePopularView :type="subType" />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
<VWindowItem value="share">
|
||||
<transition name="fade-slide" appear>
|
||||
<SubscribeShareView />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
123
src/views/subscribe/SubscribeShareView.vue
Normal file
123
src/views/subscribe/SubscribeShareView.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api'
|
||||
import type { SubscribeShare } from '@/api/types'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
import SubscribeShareCard from '@/components/cards/SubscribeShareCard.vue'
|
||||
|
||||
// 判断是否有滚动条
|
||||
function hasScroll() {
|
||||
return document.body.scrollHeight - (window.innerHeight || document.documentElement.clientHeight) > 2
|
||||
}
|
||||
|
||||
// API
|
||||
const apipath = 'subscribe/shares'
|
||||
|
||||
// 当前页码
|
||||
const page = ref(1)
|
||||
|
||||
// 是否加载中
|
||||
const loading = ref(false)
|
||||
|
||||
// 是否加载完成
|
||||
const isRefreshed = ref(false)
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<SubscribeShare[]>([])
|
||||
const currData = ref<SubscribeShare[]>([])
|
||||
|
||||
// 拼装参数
|
||||
function getParams() {
|
||||
let params = {
|
||||
page: page.value,
|
||||
count: 30,
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
// 获取列表数据
|
||||
async function fetchData({ done }: { done: any }) {
|
||||
try {
|
||||
// 如果正在加载中,直接返回
|
||||
if (loading.value) {
|
||||
done('ok')
|
||||
return
|
||||
}
|
||||
|
||||
// 加载到满屏或者加载出错
|
||||
if (!hasScroll()) {
|
||||
// 加载多次
|
||||
while (!hasScroll()) {
|
||||
// 设置加载中
|
||||
loading.value = true
|
||||
// 请求API
|
||||
currData.value = await api.get(apipath, {
|
||||
params: getParams(),
|
||||
})
|
||||
// 取消加载中
|
||||
loading.value = false
|
||||
// 标计为已请求完成
|
||||
isRefreshed.value = true
|
||||
if (currData.value.length === 0) {
|
||||
// 如果没有数据,跳出
|
||||
done('empty')
|
||||
return
|
||||
}
|
||||
// 合并数据
|
||||
dataList.value = [...dataList.value, ...currData.value]
|
||||
// 页码+1
|
||||
page.value++
|
||||
// 返回加载成功
|
||||
done('ok')
|
||||
}
|
||||
} else {
|
||||
// 设置加载中
|
||||
loading.value = true
|
||||
// 请求API
|
||||
currData.value = await api.get(apipath, {
|
||||
params: getParams(),
|
||||
})
|
||||
loading.value = false
|
||||
// 标计为已请求完成
|
||||
isRefreshed.value = true
|
||||
if (currData.value.length === 0) {
|
||||
// 如果没有数据,跳出
|
||||
done('empty')
|
||||
} else {
|
||||
// 合并数据
|
||||
dataList.value = [...dataList.value, ...currData.value]
|
||||
// 页码+1
|
||||
page.value++
|
||||
// 返回加载成功
|
||||
done('ok')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
// 返回加载失败
|
||||
done('error')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-hidden" @load="fetchData">
|
||||
<template #loading />
|
||||
<template #empty />
|
||||
<div v-if="dataList.length > 0" class="grid gap-4 grid-subscribe-card mx-3" tabindex="0">
|
||||
<div v-for="data in dataList" :key="data.id">
|
||||
<SubscribeShareCard :media="data" />
|
||||
<div class="mt-2 flex flex-row justify-center align-center text-subtitle-2">
|
||||
<VIcon icon="mdi-fire" color="error" />
|
||||
<span> {{ data.count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<NoDataFound
|
||||
v-if="dataList.length === 0 && isRefreshed"
|
||||
error-code="404"
|
||||
error-title="没有数据"
|
||||
error-description="未获取到共享订阅数据,未开启数据分享或服务器无法连接。"
|
||||
/>
|
||||
</VInfiniteScroll>
|
||||
</template>
|
||||
Reference in New Issue
Block a user