2026-07-04 17:58:19 +08:00
|
|
|
|
<template>
|
2026-07-30 14:11:26 +08:00
|
|
|
|
<section class="page-section block w-full">
|
|
|
|
|
|
<div class="max-w-[90rem] mx-auto w-full pt-20 md:pt-32 px-4 md:px-6 pb-12 md:pb-20">
|
2026-07-14 12:47:53 +08:00
|
|
|
|
<button @click="$router.back()" class="mb-8 text-art-muted hover:text-art-text flex items-center gap-2 text-sm">
|
2026-07-04 17:58:19 +08:00
|
|
|
|
<Icon name="arrow-left" :size="16" /> 返回
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
2026-07-30 14:11:26 +08:00
|
|
|
|
<div v-if="loading" class="flex justify-center py-20">
|
|
|
|
|
|
<div class="animate-spin h-10 w-10 border-t-2 border-art-accent rounded-full" />
|
|
|
|
|
|
</div>
|
2026-07-04 17:58:19 +08:00
|
|
|
|
<div v-else-if="error" class="text-red-400 text-center py-12">
|
|
|
|
|
|
<p class="mb-4">{{ error }}</p>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
2026-07-14 12:47:53 +08:00
|
|
|
|
class="px-4 py-2 border border-art-border rounded-full text-sm text-art-text hover:border-art-accent transition-colors"
|
2026-07-04 17:58:19 +08:00
|
|
|
|
@click="load"
|
2026-07-30 14:11:26 +08:00
|
|
|
|
>
|
|
|
|
|
|
重试
|
|
|
|
|
|
</button>
|
2026-07-04 17:58:19 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-else-if="video">
|
2026-07-14 12:47:53 +08:00
|
|
|
|
<h1 class="font-serif text-3xl md:text-4xl text-art-text mb-6">{{ video.title }}</h1>
|
2026-07-04 17:58:19 +08:00
|
|
|
|
|
2026-07-30 14:11:26 +08:00
|
|
|
|
<!-- 播放区:未挂载前封面;挂载后 DPlayer + 暂停时大按钮遮罩 -->
|
2026-07-04 17:58:19 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="video.videoUrl"
|
2026-07-30 14:11:26 +08:00
|
|
|
|
class="w-full aspect-video rounded-xl overflow-hidden bg-black mb-6 relative"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- DPlayer 容器:playerReady 后始终保留,暂停不销毁 -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-show="playerReady"
|
|
|
|
|
|
ref="playerRef"
|
|
|
|
|
|
class="absolute inset-0 w-full h-full"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 大播放按钮:初始 / 暂停时显示;点击开始或继续播 -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="showPlayOverlay"
|
|
|
|
|
|
class="absolute inset-0 z-20 cursor-pointer group"
|
|
|
|
|
|
role="button"
|
|
|
|
|
|
tabindex="0"
|
|
|
|
|
|
aria-label="播放视频"
|
|
|
|
|
|
@click="onOverlayPlay"
|
|
|
|
|
|
@keydown.enter.prevent="onOverlayPlay"
|
|
|
|
|
|
@keydown.space.prevent="onOverlayPlay"
|
|
|
|
|
|
>
|
|
|
|
|
|
<img
|
|
|
|
|
|
v-if="!playerReady && coverSrc"
|
|
|
|
|
|
:src="coverSrc"
|
|
|
|
|
|
class="w-full h-full object-cover"
|
|
|
|
|
|
alt=""
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-else-if="!playerReady"
|
|
|
|
|
|
class="w-full h-full bg-black/60"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="absolute inset-0 transition-colors"
|
|
|
|
|
|
:class="playerReady ? 'bg-black/40' : 'bg-black/35 group-hover:bg-black/25'"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-20 h-20 md:w-24 md:h-24 rounded-full border border-white/40 bg-black/40 backdrop-blur-md flex items-center justify-center transition-transform group-hover:scale-105 group-active:scale-95 z-10"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Icon name="play" :size="32" class="text-white ml-1" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-07-04 17:58:19 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-else
|
2026-07-14 12:47:53 +08:00
|
|
|
|
class="w-full aspect-video rounded-xl overflow-hidden bg-black/40 border border-art-border flex items-center justify-center mb-6"
|
2026-07-04 17:58:19 +08:00
|
|
|
|
>
|
|
|
|
|
|
<p class="text-art-muted text-sm">暂无视频地址</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-06 08:19:47 +08:00
|
|
|
|
<!-- 专辑内前后导航:前 2 + 后 2 -->
|
|
|
|
|
|
<div v-if="video.albumContext" class="mb-8">
|
|
|
|
|
|
<div class="flex items-center justify-between mb-4">
|
|
|
|
|
|
<p class="text-sm text-art-muted">
|
2026-07-14 12:47:53 +08:00
|
|
|
|
来自专辑 <span class="text-art-text">{{ video.albumContext.albumName }}</span>
|
2026-07-06 08:19:47 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
<router-link
|
|
|
|
|
|
:to="`/videos/albums/${video.albumContext.albumId}`"
|
|
|
|
|
|
class="text-xs text-art-accent hover:underline"
|
2026-07-30 14:11:26 +08:00
|
|
|
|
>
|
|
|
|
|
|
返回专辑
|
|
|
|
|
|
</router-link>
|
2026-07-06 08:19:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex gap-3 overflow-x-auto pb-2">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="pv in video.albumContext.prevVideos"
|
|
|
|
|
|
:key="'prev-' + pv.id"
|
|
|
|
|
|
class="shrink-0 w-36 cursor-pointer group"
|
|
|
|
|
|
@click="router.push(`/videos/${pv.id}`)"
|
|
|
|
|
|
>
|
2026-07-14 12:47:53 +08:00
|
|
|
|
<div class="aspect-video rounded-lg overflow-hidden border border-art-border group-hover:border-art-accent/50 transition-colors bg-black/40">
|
2026-07-06 08:19:47 +08:00
|
|
|
|
<img v-if="pv.cover" :src="pv.cover" class="w-full h-full object-cover opacity-80 group-hover:opacity-100" alt="" />
|
|
|
|
|
|
</div>
|
2026-07-14 12:47:53 +08:00
|
|
|
|
<p class="text-xs text-art-muted mt-1 line-clamp-2 group-hover:text-art-text">{{ pv.title }}</p>
|
2026-07-06 08:19:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="shrink-0 w-36">
|
|
|
|
|
|
<div class="aspect-video rounded-lg overflow-hidden border-2 border-art-accent bg-black/40">
|
|
|
|
|
|
<img v-if="video.cover" :src="video.cover" class="w-full h-full object-cover" alt="" />
|
|
|
|
|
|
</div>
|
2026-07-30 14:11:26 +08:00
|
|
|
|
<p class="text-xs text-art-accent mt-1 line-clamp-2 font-medium">
|
|
|
|
|
|
{{ playerReady && !showPlayOverlay ? '正在播放' : '当前视频' }}
|
|
|
|
|
|
</p>
|
2026-07-06 08:19:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="nv in video.albumContext.nextVideos"
|
|
|
|
|
|
:key="'next-' + nv.id"
|
|
|
|
|
|
class="shrink-0 w-36 cursor-pointer group"
|
|
|
|
|
|
@click="router.push(`/videos/${nv.id}`)"
|
|
|
|
|
|
>
|
2026-07-14 12:47:53 +08:00
|
|
|
|
<div class="aspect-video rounded-lg overflow-hidden border border-art-border group-hover:border-art-accent/50 transition-colors bg-black/40">
|
2026-07-06 08:19:47 +08:00
|
|
|
|
<img v-if="nv.cover" :src="nv.cover" class="w-full h-full object-cover opacity-80 group-hover:opacity-100" alt="" />
|
|
|
|
|
|
</div>
|
2026-07-14 12:47:53 +08:00
|
|
|
|
<p class="text-xs text-art-muted mt-1 line-clamp-2 group-hover:text-art-text">{{ nv.title }}</p>
|
2026-07-06 08:19:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-04 17:58:19 +08:00
|
|
|
|
<div class="max-w-3xl">
|
|
|
|
|
|
<div class="flex gap-4 text-sm text-art-muted mb-4">
|
|
|
|
|
|
<span v-if="video.categoryName">{{ video.categoryName }}</span>
|
2026-07-06 08:19:47 +08:00
|
|
|
|
<span v-if="video.createdAt">发布于 {{ video.createdAt }}</span>
|
2026-07-04 17:58:19 +08:00
|
|
|
|
</div>
|
2026-07-14 12:47:53 +08:00
|
|
|
|
<p v-if="video.description" class="text-art-text/80 leading-relaxed whitespace-pre-wrap">{{ video.description }}</p>
|
2026-07-04 17:58:19 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-07-30 14:11:26 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 视频详情:封面大按钮点播;暂停时再次显示大按钮(不销毁播放器)。
|
|
|
|
|
|
* 恢复 rounded-xl / rounded-lg 与 w-full,避免 PC 端观感变窄变生硬。
|
|
|
|
|
|
*/
|
|
|
|
|
|
import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
|
2026-07-06 08:19:47 +08:00
|
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
2026-07-04 17:58:19 +08:00
|
|
|
|
import DPlayer from 'dplayer'
|
|
|
|
|
|
import 'dplayer/dist/DPlayer.min.css'
|
2026-07-06 08:19:47 +08:00
|
|
|
|
import { fetchVideo, type VideoDetailItem } from '../services/api'
|
2026-07-04 17:58:19 +08:00
|
|
|
|
import Icon from '../components/Icon.vue'
|
|
|
|
|
|
|
|
|
|
|
|
const route = useRoute()
|
2026-07-06 08:19:47 +08:00
|
|
|
|
const router = useRouter()
|
2026-07-04 17:58:19 +08:00
|
|
|
|
const loading = ref(true)
|
|
|
|
|
|
const error = ref('')
|
2026-07-06 08:19:47 +08:00
|
|
|
|
const video = ref<VideoDetailItem | null>(null)
|
2026-07-30 14:11:26 +08:00
|
|
|
|
/** 已挂载 DPlayer */
|
|
|
|
|
|
const playerReady = ref(false)
|
|
|
|
|
|
/** 初始或暂停时显示大播放按钮 */
|
|
|
|
|
|
const showPlayOverlay = ref(true)
|
2026-07-04 17:58:19 +08:00
|
|
|
|
const playerRef = ref<HTMLElement | null>(null)
|
|
|
|
|
|
let dp: DPlayer | null = null
|
|
|
|
|
|
|
2026-07-30 14:11:26 +08:00
|
|
|
|
const coverSrc = computed(() => video.value?.poster || video.value?.cover || '')
|
|
|
|
|
|
|
|
|
|
|
|
/** 销毁 DPlayer,释放资源 */
|
|
|
|
|
|
function destroyPlayer() {
|
|
|
|
|
|
if (dp) {
|
|
|
|
|
|
dp.destroy()
|
|
|
|
|
|
dp = null
|
|
|
|
|
|
}
|
|
|
|
|
|
playerReady.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 绑定 play/pause,驱动大按钮显隐 */
|
|
|
|
|
|
function bindPlayerEvents(player: DPlayer) {
|
|
|
|
|
|
player.on('play', () => {
|
|
|
|
|
|
showPlayOverlay.value = false
|
|
|
|
|
|
})
|
|
|
|
|
|
player.on('pause', () => {
|
|
|
|
|
|
showPlayOverlay.value = true
|
|
|
|
|
|
})
|
2026-07-04 17:58:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-30 14:11:26 +08:00
|
|
|
|
/** 首次创建播放器并播放 */
|
|
|
|
|
|
async function initPlayer() {
|
2026-07-04 17:58:19 +08:00
|
|
|
|
destroyPlayer()
|
2026-07-30 14:11:26 +08:00
|
|
|
|
playerReady.value = true
|
2026-07-04 17:58:19 +08:00
|
|
|
|
await nextTick()
|
2026-07-30 14:11:26 +08:00
|
|
|
|
if (!playerRef.value || !video.value?.videoUrl) {
|
|
|
|
|
|
playerReady.value = false
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-04 17:58:19 +08:00
|
|
|
|
dp = new DPlayer({
|
|
|
|
|
|
container: playerRef.value,
|
2026-07-30 14:11:26 +08:00
|
|
|
|
video: {
|
|
|
|
|
|
url: video.value.videoUrl,
|
|
|
|
|
|
pic: video.value.poster || video.value.cover,
|
|
|
|
|
|
},
|
|
|
|
|
|
autoplay: false,
|
2026-07-04 17:58:19 +08:00
|
|
|
|
theme: '#d4b383',
|
|
|
|
|
|
lang: 'zh-cn',
|
|
|
|
|
|
})
|
2026-07-30 14:11:26 +08:00
|
|
|
|
bindPlayerEvents(dp)
|
|
|
|
|
|
try {
|
|
|
|
|
|
dp.play()
|
|
|
|
|
|
showPlayOverlay.value = false
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
showPlayOverlay.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 点击大按钮:未就绪则初始化;已就绪则继续 play。
|
|
|
|
|
|
* 用户手势触发,利于移动端绕过自动播限制。
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function onOverlayPlay() {
|
|
|
|
|
|
if (!video.value?.videoUrl) return
|
|
|
|
|
|
if (!playerReady.value || !dp) {
|
|
|
|
|
|
showPlayOverlay.value = false
|
|
|
|
|
|
await initPlayer()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
dp.play()
|
|
|
|
|
|
showPlayOverlay.value = false
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
showPlayOverlay.value = true
|
|
|
|
|
|
}
|
2026-07-04 17:58:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-30 14:11:26 +08:00
|
|
|
|
/** 加载视频详情;切换条目时重置播放态 */
|
|
|
|
|
|
async function load() {
|
2026-07-04 17:58:19 +08:00
|
|
|
|
loading.value = true
|
|
|
|
|
|
error.value = ''
|
2026-07-30 14:11:26 +08:00
|
|
|
|
showPlayOverlay.value = true
|
2026-07-04 17:58:19 +08:00
|
|
|
|
destroyPlayer()
|
|
|
|
|
|
try {
|
|
|
|
|
|
video.value = await fetchVideo(route.params.id as string)
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
error.value = '加载视频失败'
|
|
|
|
|
|
video.value = null
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(load)
|
|
|
|
|
|
watch(() => route.params.id, load)
|
2026-07-30 14:11:26 +08:00
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
|
destroyPlayer()
|
|
|
|
|
|
showPlayOverlay.value = true
|
|
|
|
|
|
})
|
2026-07-04 17:58:19 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-07-30 14:11:26 +08:00
|
|
|
|
:deep(.dplayer) {
|
|
|
|
|
|
width: 100% !important;
|
|
|
|
|
|
height: 100% !important;
|
|
|
|
|
|
}
|
2026-07-04 17:58:19 +08:00
|
|
|
|
</style>
|