110 lines
3.5 KiB
Vue
110 lines
3.5 KiB
Vue
|
|
<template>
|
||
|
|
<section class="page-section block">
|
||
|
|
<div class="max-w-6xl mx-auto pb-12 md:pb-20">
|
||
|
|
<button @click="$router.back()" class="mb-8 text-art-muted hover:text-white flex items-center gap-2 text-sm">
|
||
|
|
<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"
|
||
|
|
class="px-4 py-2 border border-white/20 rounded-full text-sm text-white hover:border-art-accent transition-colors"
|
||
|
|
@click="load"
|
||
|
|
>重试</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<template v-else-if="video">
|
||
|
|
<h1 class="font-serif text-3xl md:text-4xl text-white mb-6">{{ video.title }}</h1>
|
||
|
|
|
||
|
|
<!-- 有视频地址时挂载 DPlayer 容器 -->
|
||
|
|
<div
|
||
|
|
v-if="video.videoUrl"
|
||
|
|
ref="playerRef"
|
||
|
|
class="w-full aspect-video rounded-xl overflow-hidden bg-black mb-8"
|
||
|
|
></div>
|
||
|
|
<!-- 无视频地址时的占位提示 -->
|
||
|
|
<div
|
||
|
|
v-else
|
||
|
|
class="w-full aspect-video rounded-xl overflow-hidden bg-black/40 border border-white/10 flex items-center justify-center mb-8"
|
||
|
|
>
|
||
|
|
<p class="text-art-muted text-sm">暂无视频地址</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<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>
|
||
|
|
<span>{{ video.createdAt }}</span>
|
||
|
|
</div>
|
||
|
|
<p v-if="video.description" class="text-white/80 leading-relaxed whitespace-pre-wrap">{{ video.description }}</p>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup lang="ts">
|
||
|
|
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
|
||
|
|
import { useRoute } from 'vue-router'
|
||
|
|
import DPlayer from 'dplayer'
|
||
|
|
import 'dplayer/dist/DPlayer.min.css'
|
||
|
|
import { fetchVideo, type VideoItem } from '../services/api'
|
||
|
|
import Icon from '../components/Icon.vue'
|
||
|
|
|
||
|
|
const route = useRoute()
|
||
|
|
const loading = ref(true)
|
||
|
|
const error = ref('')
|
||
|
|
const video = ref<VideoItem | null>(null)
|
||
|
|
const playerRef = ref<HTMLElement | null>(null)
|
||
|
|
let dp: DPlayer | null = null
|
||
|
|
|
||
|
|
/** 销毁 DPlayer 实例,避免内存泄漏 */
|
||
|
|
const destroyPlayer = () => {
|
||
|
|
if (dp) { dp.destroy(); dp = null }
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 在容器与视频 URL 就绪后创建 DPlayer */
|
||
|
|
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',
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 加载视频详情数据 */
|
||
|
|
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
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 监听 video、容器 DOM、loading 状态,在 DOM 挂载后再初始化播放器
|
||
|
|
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>
|