mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-06 16:16:42 +08:00
feat: dashboard
This commit is contained in:
@@ -935,3 +935,15 @@ export interface MediaServerPlayItem {
|
|||||||
link?: string
|
link?: string
|
||||||
percent?: number
|
percent?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 媒体服务器媒体库
|
||||||
|
export interface MediaServerLibrary {
|
||||||
|
server: string
|
||||||
|
id?: string | number
|
||||||
|
name: string
|
||||||
|
path?: string
|
||||||
|
type?: string
|
||||||
|
image?: string
|
||||||
|
image_list?: string[]
|
||||||
|
link?: string
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { MediaServerPlayItem } from '@/api/types'
|
||||||
|
|
||||||
|
// 输入参数
|
||||||
|
const props = defineProps({
|
||||||
|
media: Object as PropType<MediaServerPlayItem>,
|
||||||
|
width: String,
|
||||||
|
height: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 图片是否加载完成
|
||||||
|
const imageLoaded = ref(false)
|
||||||
|
|
||||||
|
// 图片加载完成响应
|
||||||
|
function imageLoadHandler() {
|
||||||
|
imageLoaded.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转播放
|
||||||
|
function goPlay() {
|
||||||
|
if (props.media?.link)
|
||||||
|
window.open(props.media?.link, '_blank')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VHover
|
||||||
|
v-bind="props"
|
||||||
|
:height="props.height"
|
||||||
|
:width="props.width"
|
||||||
|
>
|
||||||
|
<template #default="hover">
|
||||||
|
<VCard
|
||||||
|
v-bind="hover.props"
|
||||||
|
:height="props.height"
|
||||||
|
:width="props.width"
|
||||||
|
class="shadow ring-gray-500 ring-1"
|
||||||
|
:class="{
|
||||||
|
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||||
|
}"
|
||||||
|
@click="goPlay"
|
||||||
|
>
|
||||||
|
<template #image>
|
||||||
|
<VImg
|
||||||
|
:src="props.media?.image"
|
||||||
|
aspect-ratio="2/3"
|
||||||
|
cover
|
||||||
|
@load="imageLoadHandler"
|
||||||
|
>
|
||||||
|
<template #placeholder>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<VCardText
|
||||||
|
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
|
||||||
|
>
|
||||||
|
<h1 class="mb-1 text-white font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||||
|
{{ props.media?.title }}
|
||||||
|
</h1>
|
||||||
|
<span class="font-bold">{{ props.media?.subtitle }}</span>
|
||||||
|
</VCardText>
|
||||||
|
</VImg>
|
||||||
|
</template>
|
||||||
|
<div class="w-full absolute bottom-0">
|
||||||
|
<VProgressLinear
|
||||||
|
v-if="props.media?.percent"
|
||||||
|
:model-value="props.media?.percent"
|
||||||
|
bg-color="success"
|
||||||
|
color="success"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
||||||
|
</VHover>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { MediaServerLibrary } from '@/api/types'
|
||||||
|
|
||||||
|
// 输入参数
|
||||||
|
const props = defineProps({
|
||||||
|
media: Object as PropType<MediaServerLibrary>,
|
||||||
|
width: String,
|
||||||
|
height: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 图片是否加载完成
|
||||||
|
const imageLoaded = ref(false)
|
||||||
|
|
||||||
|
// 图片加载完成响应
|
||||||
|
function imageLoadHandler() {
|
||||||
|
imageLoaded.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转播放
|
||||||
|
function goPlay() {
|
||||||
|
if (props.media?.link)
|
||||||
|
window.open(props.media?.link, '_blank')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VHover
|
||||||
|
v-bind="props"
|
||||||
|
:height="props.height"
|
||||||
|
:width="props.width"
|
||||||
|
>
|
||||||
|
<template #default="hover">
|
||||||
|
<VCard
|
||||||
|
v-bind="hover.props"
|
||||||
|
:height="props.height"
|
||||||
|
:width="props.width"
|
||||||
|
:class="{
|
||||||
|
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||||
|
}"
|
||||||
|
@click="goPlay"
|
||||||
|
>
|
||||||
|
<template #image>
|
||||||
|
<VImg
|
||||||
|
:src="props.media?.image"
|
||||||
|
aspect-ratio="2/3"
|
||||||
|
cover
|
||||||
|
@load="imageLoadHandler"
|
||||||
|
>
|
||||||
|
<template #placeholder>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<VCardText
|
||||||
|
class="w-full flex flex-col flex-wrap justify-end align-center text-white absolute bottom-0 cursor-pointer pa-2"
|
||||||
|
>
|
||||||
|
<h1 class="mb-1 text-white font-bold line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||||
|
{{ props.media?.name }}
|
||||||
|
</h1>
|
||||||
|
</VCardText>
|
||||||
|
</VImg>
|
||||||
|
</template>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
||||||
|
</VHover>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { PropType } from 'vue'
|
||||||
|
import type { MediaServerPlayItem } from '@/api/types'
|
||||||
|
import noImage from '@images/no-image.jpeg'
|
||||||
|
|
||||||
|
// 输入参数
|
||||||
|
const props = defineProps({
|
||||||
|
media: Object as PropType<MediaServerPlayItem>,
|
||||||
|
width: String,
|
||||||
|
height: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 图片加载状态
|
||||||
|
const isImageLoaded = ref(false)
|
||||||
|
|
||||||
|
// 图片加载失败
|
||||||
|
const imageLoadError = ref(false)
|
||||||
|
|
||||||
|
// 角标颜色
|
||||||
|
function getChipColor(type: string) {
|
||||||
|
if (type === '电影')
|
||||||
|
return 'border-blue-500 bg-blue-600'
|
||||||
|
else if (type === '电视剧')
|
||||||
|
return ' bg-indigo-500 border-indigo-600'
|
||||||
|
else
|
||||||
|
return 'border-purple-600 bg-purple-600'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算图片地址
|
||||||
|
const getImgUrl = computed(() => {
|
||||||
|
if (imageLoadError.value)
|
||||||
|
return noImage
|
||||||
|
return props.media?.image
|
||||||
|
})
|
||||||
|
|
||||||
|
// 跳转播放
|
||||||
|
function goPlay() {
|
||||||
|
if (props.media?.link)
|
||||||
|
window.open(props.media?.link, '_blank')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VHover v-bind="props">
|
||||||
|
<template #default="hover">
|
||||||
|
<VCard
|
||||||
|
v-bind="hover.props"
|
||||||
|
: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,
|
||||||
|
'ring-1': isImageLoaded,
|
||||||
|
}"
|
||||||
|
@click.stop="goPlay"
|
||||||
|
>
|
||||||
|
<VImg
|
||||||
|
aspect-ratio="2/3"
|
||||||
|
:src="getImgUrl"
|
||||||
|
class="object-cover aspect-w-2 aspect-h-3"
|
||||||
|
:class="hover.isHovering ? 'on-hover' : ''"
|
||||||
|
cover
|
||||||
|
@load="isImageLoaded = true"
|
||||||
|
@error="imageLoadError = true"
|
||||||
|
>
|
||||||
|
<template #placeholder>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<!-- 类型角标 -->
|
||||||
|
<VChip
|
||||||
|
v-show="isImageLoaded"
|
||||||
|
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 }}
|
||||||
|
</VChip>
|
||||||
|
<!-- 详情 -->
|
||||||
|
<VCardText
|
||||||
|
v-show="hover.isHovering || imageLoadError"
|
||||||
|
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
|
||||||
|
>
|
||||||
|
<span class="font-bold">{{ props.media?.subtitle }}</span>
|
||||||
|
<h1 class="mb-1 text-white font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||||
|
{{ props.media?.title }}
|
||||||
|
</h1>
|
||||||
|
</VCardText>
|
||||||
|
</VImg>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
||||||
|
</VHover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.on-hover img {
|
||||||
|
@apply brightness-50;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { MediaServerPlayItem } from '@/api/types'
|
import type { MediaServerPlayItem } from '@/api/types'
|
||||||
|
import PosterCard from '@/components/cards/PosterCard.vue'
|
||||||
|
|
||||||
// 最近入库列表
|
// 最近入库列表
|
||||||
const latestList = ref<MediaServerPlayItem[]>([])
|
const latestList = ref<MediaServerPlayItem[]>([])
|
||||||
@@ -26,12 +27,22 @@ onMounted(() => {
|
|||||||
<VCardTitle>最近添加</VCardTitle>
|
<VCardTitle>最近添加</VCardTitle>
|
||||||
</VCardItem>
|
</VCardItem>
|
||||||
|
|
||||||
<VCardText class="pt-4" />
|
<div
|
||||||
|
v-if="latestList.length > 0"
|
||||||
|
class="grid gap-4 grid-media-card mx-3"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<PosterCard
|
||||||
|
v-for="data in latestList"
|
||||||
|
:key="data.id"
|
||||||
|
:media="data"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</VCard>
|
</VCard>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss">
|
||||||
.card-list {
|
.grid-media-card {
|
||||||
--v-card-list-gap: 1rem;
|
grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr));
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { MediaServerPlayItem } from '@/api/types'
|
import type { MediaServerPlayItem } from '@/api/types'
|
||||||
|
import LibraryCard from '@/components/cards/LibraryCard.vue'
|
||||||
|
|
||||||
// 媒体库列表
|
// 媒体库列表
|
||||||
const libraryList = ref<MediaServerPlayItem[]>([])
|
const libraryList = ref<MediaServerPlayItem[]>([])
|
||||||
@@ -26,12 +27,24 @@ onMounted(() => {
|
|||||||
<VCardTitle>我的媒体库</VCardTitle>
|
<VCardTitle>我的媒体库</VCardTitle>
|
||||||
</VCardItem>
|
</VCardItem>
|
||||||
|
|
||||||
<VCardText class="pt-4" />
|
<div
|
||||||
|
v-if="libraryList.length > 0"
|
||||||
|
class="grid gap-4 grid-backdrop-card mx-3"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<LibraryCard
|
||||||
|
v-for="data in libraryList"
|
||||||
|
:key="data.id"
|
||||||
|
:media="data"
|
||||||
|
height="10rem"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</VCard>
|
</VCard>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss">
|
||||||
.card-list {
|
.grid-backdrop-card {
|
||||||
--v-card-list-gap: 1rem;
|
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||||
|
padding-block-end: 1rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { MediaServerPlayItem } from '@/api/types'
|
import type { MediaServerPlayItem } from '@/api/types'
|
||||||
|
import BackdropCard from '@/components/cards/BackdropCard.vue'
|
||||||
|
|
||||||
// 继续播放列表
|
// 继续播放列表
|
||||||
const playingList = ref<MediaServerPlayItem[]>([])
|
const playingList = ref<MediaServerPlayItem[]>([])
|
||||||
@@ -26,12 +27,24 @@ onMounted(() => {
|
|||||||
<VCardTitle>继续观看</VCardTitle>
|
<VCardTitle>继续观看</VCardTitle>
|
||||||
</VCardItem>
|
</VCardItem>
|
||||||
|
|
||||||
<VCardText class="pt-4" />
|
<div
|
||||||
|
v-if="playingList.length > 0"
|
||||||
|
class="grid gap-4 grid-backdrop-card mx-3"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<BackdropCard
|
||||||
|
v-for="data in playingList"
|
||||||
|
:key="data.id"
|
||||||
|
:media="data"
|
||||||
|
height="10rem"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</VCard>
|
</VCard>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss">
|
||||||
.card-list {
|
.grid-backdrop-card {
|
||||||
--v-card-list-gap: 1rem;
|
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||||
|
padding-block-end: 1rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -427,7 +427,7 @@ async function handlePlay() {
|
|||||||
// 打开链接地址
|
// 打开链接地址
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.open(result.data.url, '_blank')
|
window.open(result.data.url, '_blank')
|
||||||
}, 500)
|
}, 100)
|
||||||
}
|
}
|
||||||
else { $toast.error(`获取播放链接失败:${result.message}!`) }
|
else { $toast.error(`获取播放链接失败:${result.message}!`) }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user