Files
nl-blogs/client/src/pages/VideoDetail.vue

154 lines
5.8 KiB
Vue
Raw Normal View History

2026-07-04 17:58:19 +08:00
<template>
<section class="page-section block">
<div class="max-w-6xl mx-auto 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>
<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></div>
<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"
>重试</button>
</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
<div
v-if="video.videoUrl"
ref="playerRef"
2026-07-06 08:19:47 +08:00
class="w-full aspect-video rounded-xl overflow-hidden bg-black mb-6"
2026-07-04 17:58:19 +08:00
></div>
<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"
>返回专辑</router-link>
</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>
<p class="text-xs text-art-accent mt-1 line-clamp-2 font-medium">正在播放</p>
</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">
import { ref, 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-04 17:58:19 +08:00
const playerRef = ref<HTMLElement | null>(null)
let dp: DPlayer | null = null
2026-07-06 08:19:47 +08:00
/** 销毁 DPlayer 实例 */
2026-07-04 17:58:19 +08:00
const destroyPlayer = () => {
if (dp) { dp.destroy(); dp = null }
}
2026-07-06 08:19:47 +08:00
/** 创建 DPlayer 播放器 */
2026-07-04 17:58:19 +08:00
const initPlayer = async () => {
destroyPlayer()
if (!playerRef.value || !video.value?.videoUrl) return
await nextTick()
dp = new DPlayer({
container: playerRef.value,
video: { url: video.value.videoUrl, pic: video.value.poster || video.value.cover },
theme: '#d4b383',
lang: 'zh-cn',
})
}
2026-07-06 08:19:47 +08:00
/** 加载视频详情 */
2026-07-04 17:58:19 +08:00
const load = async () => {
loading.value = true
error.value = ''
destroyPlayer()
try {
video.value = await fetchVideo(route.params.id as string)
} catch {
error.value = '加载视频失败'
video.value = null
} finally {
loading.value = false
}
}
watch([video, playerRef, loading], async ([v, el, isLoading]) => {
if (isLoading || !v?.videoUrl || !el) return
await nextTick()
initPlayer()
}, { flush: 'post' })
onMounted(load)
watch(() => route.params.id, load)
onBeforeUnmount(destroyPlayer)
</script>
<style scoped>
:deep(.dplayer) { width: 100% !important; height: 100% !important; }
</style>