mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-22 16:59:46 +08:00
fix ui
This commit is contained in:
@@ -35,36 +35,28 @@ function imageErrorHandler() {
|
||||
|
||||
// 默认图片
|
||||
function getDefaultImage() {
|
||||
if (props.media?.server === 'plex')
|
||||
return plex
|
||||
else if (props.media?.server === 'emby')
|
||||
return emby
|
||||
else if (props.media?.server === 'jellyfin')
|
||||
return jellyfin
|
||||
else
|
||||
return plex
|
||||
if (props.media?.server === 'plex') return plex
|
||||
else if (props.media?.server === 'emby') return emby
|
||||
else if (props.media?.server === 'jellyfin') return jellyfin
|
||||
else return plex
|
||||
}
|
||||
|
||||
// 跳转播放
|
||||
function goPlay() {
|
||||
if (props.media?.link)
|
||||
window.open(props.media?.link, '_blank')
|
||||
if (props.media?.link) window.open(props.media?.link, '_blank')
|
||||
}
|
||||
|
||||
// 生成图片代理路径
|
||||
function getImgUrl(url: string) {
|
||||
if (!url)
|
||||
return getDefaultImage()
|
||||
else
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(url)}`
|
||||
if (!url) return getDefaultImage()
|
||||
else return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(url)}`
|
||||
}
|
||||
|
||||
// 根据多张图片生成媒体库封面
|
||||
async function drawImages(imageList: string[]) {
|
||||
// 图片
|
||||
const IMAGES = imageList
|
||||
if (IMAGES.length === 0)
|
||||
return getDefaultImage()
|
||||
if (IMAGES.length === 0) return getDefaultImage()
|
||||
|
||||
// 为所有图片添加system/img前缀
|
||||
for (let i = 0; i < IMAGES.length; i++)
|
||||
@@ -72,8 +64,7 @@ async function drawImages(imageList: string[]) {
|
||||
|
||||
// canvas
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas)
|
||||
return getDefaultImage()
|
||||
if (!canvas) return getDefaultImage()
|
||||
|
||||
// 画布参数
|
||||
const POSTER_WIDTH = (canvas.width - 32) / 4
|
||||
@@ -85,8 +76,7 @@ async function drawImages(imageList: string[]) {
|
||||
|
||||
// 获取画布上下文
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx)
|
||||
return getDefaultImage()
|
||||
if (!ctx) return getDefaultImage()
|
||||
|
||||
// 设置背景色为黑色
|
||||
ctx.fillStyle = '#000000'
|
||||
@@ -94,16 +84,14 @@ async function drawImages(imageList: string[]) {
|
||||
|
||||
// 绘制图片
|
||||
async function drawImageWithReflection(imgSrc: string, index: number) {
|
||||
if (!canvas)
|
||||
return
|
||||
if (!canvas) return
|
||||
|
||||
if (!ctx)
|
||||
return
|
||||
if (!ctx) return
|
||||
|
||||
const img = new Image()
|
||||
img.setAttribute('crossorigin', 'anonymous')
|
||||
img.src = imgSrc
|
||||
await new Promise(resolve => img.onload = resolve)
|
||||
await new Promise(resolve => (img.onload = resolve))
|
||||
|
||||
const x = MARGIN_WIDTH * index + POSTER_WIDTH * (index - 1)
|
||||
const y = MARGIN_HEIGHT
|
||||
@@ -125,12 +113,7 @@ async function drawImages(imageList: string[]) {
|
||||
REFLECTION_HEIGHT,
|
||||
)
|
||||
|
||||
const gradient = ctx.createLinearGradient(
|
||||
0,
|
||||
REFLECTION_SHOW_HEIGHT - REFLECTION_HEIGHT,
|
||||
0,
|
||||
REFLECTION_HEIGHT,
|
||||
)
|
||||
const gradient = ctx.createLinearGradient(0, REFLECTION_SHOW_HEIGHT - REFLECTION_HEIGHT, 0, REFLECTION_HEIGHT)
|
||||
|
||||
gradient.addColorStop(0, 'rgba(0, 0, 0, 1)')
|
||||
gradient.addColorStop(1, 'rgba(0, 0, 0, 0.3)')
|
||||
@@ -142,8 +125,7 @@ async function drawImages(imageList: string[]) {
|
||||
|
||||
// 绘制多张图片
|
||||
const loopCount = Math.min(4, IMAGES.length)
|
||||
for (let i = 0; i < loopCount; i++)
|
||||
await drawImageWithReflection(IMAGES[i], i + 1)
|
||||
for (let i = 0; i < loopCount; i++) await drawImageWithReflection(IMAGES[i], i + 1)
|
||||
|
||||
// 转换为图片地址
|
||||
return canvas.toDataURL('image/png')
|
||||
@@ -152,17 +134,12 @@ async function drawImages(imageList: string[]) {
|
||||
onMounted(async () => {
|
||||
if (props.media?.image_list && props.media?.image_list.length > 0)
|
||||
imgUrl.value = await drawImages(props.media?.image_list || [])
|
||||
else
|
||||
imgUrl.value = getImgUrl(props.media?.image || '')
|
||||
else imgUrl.value = getImgUrl(props.media?.image || '')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VHover
|
||||
v-bind="props"
|
||||
:height="props.height"
|
||||
:width="props.width"
|
||||
>
|
||||
<VHover v-bind="props" :height="props.height" :width="props.width">
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
@@ -175,13 +152,7 @@ onMounted(async () => {
|
||||
>
|
||||
<template #image>
|
||||
<canvas ref="canvasRef" class="w-full h-full hidden" />
|
||||
<VImg
|
||||
:src="imgUrl"
|
||||
aspect-ratio="2/3"
|
||||
cover
|
||||
@load="imageLoadHandler"
|
||||
@error="imageErrorHandler"
|
||||
>
|
||||
<VImg :src="imgUrl" aspect-ratio="2/3" cover @load="imageLoadHandler" @error="imageErrorHandler">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||
@@ -190,7 +161,7 @@ onMounted(async () => {
|
||||
<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 ...">
|
||||
<h1 class="mb-1 text-white text-shadow font-bold line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.name }}
|
||||
</h1>
|
||||
</VCardText>
|
||||
@@ -200,3 +171,9 @@ onMounted(async () => {
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.text-shadow {
|
||||
text-shadow: 1px 1px #777;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -180,6 +180,7 @@ watch(
|
||||
class="flex flex-col rounded-lg"
|
||||
:class="{
|
||||
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
|
||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||
}"
|
||||
@click="editSubscribeDialog"
|
||||
>
|
||||
@@ -223,26 +224,29 @@ watch(
|
||||
</template>
|
||||
<div v-if="imageLoaded">
|
||||
<VCardText class="flex items-center">
|
||||
<div
|
||||
class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md shadow-lg"
|
||||
:class="{ 'transition transform-cpu duration-300 scale-105': hover.isHovering }"
|
||||
>
|
||||
<VImg :src="props.media?.poster" aspect-ratio="2/3" cover @click.stop="viewMediaDetail" />
|
||||
<div class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md shadow-lg">
|
||||
<VImg :src="props.media?.poster" 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 xl:text-xl">
|
||||
<div class="mr-2 min-w-0 text-lg font-bold text-white">
|
||||
{{ props.media?.name }}
|
||||
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText class="flex justify-space-between align-center flex-wrap">
|
||||
<div class="d-flex align-center">
|
||||
<div class="flex align-center">
|
||||
<IconBtn
|
||||
v-if="props.media?.total_episode"
|
||||
v-bind="props"
|
||||
icon="mdi-progress-clock"
|
||||
icon="mdi-progress-download"
|
||||
color="white"
|
||||
class="me-1"
|
||||
/>
|
||||
@@ -260,12 +264,14 @@ watch(
|
||||
<VIcon icon="mdi-download" class="me-1" />
|
||||
{{ lastUpdateText }}
|
||||
</VCardText>
|
||||
<VProgressLinear
|
||||
v-if="getPercentage() > 0"
|
||||
:model-value="getPercentage()"
|
||||
bg-color="success"
|
||||
color="success"
|
||||
/>
|
||||
<div class="w-full absolute bottom-0">
|
||||
<VProgressLinear
|
||||
v-if="getPercentage() > 0"
|
||||
:model-value="getPercentage()"
|
||||
bg-color="success"
|
||||
color="success"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
@@ -27,12 +27,10 @@ function slideNext(next: boolean) {
|
||||
if (run_to_left_px >= slideview_content.value.scrollWidth - slideview_content.value.clientWidth)
|
||||
run_to_left_px = slideview_content.value.scrollWidth - slideview_content.value.clientWidth
|
||||
// console.log(`最多显示: ${card_max} 当前起点: ${card_current} 目标起点: ${card_index} 卡片宽度: ${card_width}`)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
const card_index = card_current - card_max
|
||||
run_to_left_px = card_index * card_width
|
||||
if (run_to_left_px <= 0)
|
||||
run_to_left_px = 0
|
||||
if (run_to_left_px <= 0) run_to_left_px = 0
|
||||
// console.log(`最多显示: ${card_max} 当前起点: ${card_current} 目标起点: ${card_index} 卡片宽度: ${card_width}`)
|
||||
}
|
||||
slideview_content.value.scrollTo({
|
||||
@@ -46,7 +44,7 @@ function slideNext(next: boolean) {
|
||||
function countMaxNumber() {
|
||||
slide_card_length = slideview_content.value.children.length
|
||||
card_width = slideview_content.value.firstElementChild.getBoundingClientRect().width
|
||||
slide_gap_px = (slideview_content.value.scrollWidth / slide_card_length) - card_width
|
||||
slide_gap_px = slideview_content.value.scrollWidth / slide_card_length - card_width
|
||||
card_width += slide_gap_px
|
||||
card_max = Math.trunc(slideview_content.value.clientWidth / card_width)
|
||||
countDisabled()
|
||||
@@ -55,16 +53,18 @@ function countMaxNumber() {
|
||||
// 修改分页切换按钮状态
|
||||
function countDisabled() {
|
||||
slideview_scrollLeft.value = slideview_content.value.scrollLeft
|
||||
card_current = slideview_content.value.scrollLeft === 0 ? 0 : Math.trunc((slideview_content.value.scrollLeft + card_width / 2) / card_width)
|
||||
if (slide_card_length * card_width <= slideview_content.value.clientWidth)
|
||||
disabled.value = 3
|
||||
else if (slideview_content.value.scrollLeft === 0)
|
||||
disabled.value = 0
|
||||
else if (slideview_content.value.scrollLeft >= slideview_content.value.scrollWidth - slideview_content.value.clientWidth - 2)
|
||||
card_current =
|
||||
slideview_content.value.scrollLeft === 0
|
||||
? 0
|
||||
: Math.trunc((slideview_content.value.scrollLeft + card_width / 2) / card_width)
|
||||
if (slide_card_length * card_width <= slideview_content.value.clientWidth) disabled.value = 3
|
||||
else if (slideview_content.value.scrollLeft === 0) disabled.value = 0
|
||||
else if (
|
||||
slideview_content.value.scrollLeft >=
|
||||
slideview_content.value.scrollWidth - slideview_content.value.clientWidth - 2
|
||||
)
|
||||
disabled.value = 2
|
||||
|
||||
else
|
||||
disabled.value = 1
|
||||
else disabled.value = 1
|
||||
}
|
||||
|
||||
// 组件加载完成
|
||||
@@ -123,8 +123,7 @@ onActivated(() => {
|
||||
<style lang="scss" scoped>
|
||||
.slideview_content {
|
||||
-ms-overflow-style: none !important;
|
||||
overflow-x: scroll !important;
|
||||
overflow-y: hidden !important;
|
||||
overflow: scroll hidden !important;
|
||||
overscroll-behavior-x: contain !important;
|
||||
scrollbar-width: none !important;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
// 输入参数
|
||||
const props = inject('rankingPropsKey')
|
||||
|
||||
const props: any = inject('rankingPropsKey')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="ms-1"
|
||||
>
|
||||
<RouterLink
|
||||
:to="props.linkurl ? props.linkurl : ''"
|
||||
class="slider-title"
|
||||
>
|
||||
<span>{{ props.title }}</span>
|
||||
<VIcon
|
||||
icon="mdi-arrow-right-circle-outline"
|
||||
class="ms-1"
|
||||
/>
|
||||
<div class="ms-1">
|
||||
<RouterLink :to="props?.linkurl ? props?.linkurl : ''" class="slider-title">
|
||||
<span>{{ props?.title }}</span>
|
||||
<VIcon icon="mdi-arrow-right-circle-outline" class="ms-1" />
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -31,7 +31,7 @@ function getApiPath(paths: string[] | string) {
|
||||
<div v-if="title" class="mt-3 md:flex md:items-center md:justify-between">
|
||||
<div class="min-w-0 flex-1 mx-0">
|
||||
<h2
|
||||
class="mb-4 truncate text-2xl font-bold leading-7 text-gray-100 sm:overflow-visible sm:text-4xl sm:leading-9 md:mb-0"
|
||||
class="mb-4 ms-3 truncate text-2xl font-bold leading-7 text-gray-100 sm:overflow-visible sm:text-4xl sm:leading-9 md:mb-0"
|
||||
data-testid="page-header"
|
||||
>
|
||||
<span class="text-moviepilot">{{ title }}</span>
|
||||
|
||||
@@ -16,6 +16,9 @@ const progressDialog = ref(false)
|
||||
// 加载中
|
||||
const loading = ref(false)
|
||||
|
||||
// 已加载过
|
||||
const isLoaded = ref(false)
|
||||
|
||||
// 日历属性
|
||||
const calendarOptions: Ref<CalendarOptions> = ref({
|
||||
height: 'auto',
|
||||
@@ -103,7 +106,7 @@ async function eventsHander(subscribe: Subscribe) {
|
||||
|
||||
// 调用API查询所有订阅
|
||||
async function getSubscribes() {
|
||||
progressDialog.value = true
|
||||
if (!isLoaded.value) progressDialog.value = true
|
||||
try {
|
||||
// 订阅
|
||||
loading.value = true
|
||||
@@ -111,6 +114,7 @@ async function getSubscribes() {
|
||||
loading.value = false
|
||||
const subEvents = await Promise.all(subscribes.map(async sub => eventsHander(sub)))
|
||||
calendarOptions.value.events = subEvents.flat().filter(event => event.start) as EventSourceInput
|
||||
isLoaded.value = true
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user