kindergarten_java/reading-platform-frontend/src/views/office/player.vue

251 lines
5.6 KiB
Vue
Raw Normal View History

2026-03-17 14:17:21 +08:00
<template>
<div
ref="playerWrapperRef"
class="player-wrapper w-full h-full bg-#000 z-50 top-0 left-0"
:class="noPage ? 'absolute' : 'fixed'"
tabindex="0"
@keydown="handleKeydown"
>
<!-- 视频标题遮罩暂停时显示参考 VideoPlayer -->
<div v-if="title && !isPlaying && !loading" class="title-overlay">
<div class="title-text">{{ title }}</div>
</div>
<!-- 加载中遮罩 -->
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner"></div>
</div>
<!-- Aliplayer 挂载点 -->
<div :id="playerId" class="w-full h-full z-1"></div>
2026-03-17 14:17:21 +08:00
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
import { useRoute } from 'vue-router';
import { getTemItem, type TemObj } from './temObjs';
2026-03-17 14:17:21 +08:00
const props = defineProps<{
url: string;
cover?: string;
noPage?: boolean;
/** 视频标题(参考 VideoPlayer */
title?: string;
2026-03-17 14:17:21 +08:00
}>();
const emit = defineEmits<{
(e: 'ended'): void;
(e: 'play'): void;
(e: 'pause'): void;
}>();
const route = useRoute();
const playerWrapperRef = ref<HTMLElement | null>(null);
const playerInstance = ref<any>(null);
const isPlaying = ref(false);
const loading = ref(true);
// 唯一 ID 避免多实例冲突
const playerId = ref(`playerView-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`);
2026-03-17 14:17:21 +08:00
const _temObj = ref<TemObj>({
id: '',
name: '',
isEdit: false,
url: '',
});
function createPlayer(source?: string, cover?: string) {
if (!source) return;
const mountEl = document.getElementById(playerId.value);
if (!mountEl) return;
2026-03-17 14:17:21 +08:00
// @ts-ignore
playerInstance.value = new Aliplayer(
2026-03-17 14:17:21 +08:00
{
id: playerId.value,
2026-03-17 14:17:21 +08:00
width: '100%',
height: '100%',
source: source,
cover: cover || '/long/long.svg',
2026-03-17 14:17:21 +08:00
skinLayout: [
{ name: 'bigPlayButton', align: 'blabs', x: 30, y: 80 },
{ name: 'H5Loading', align: 'cc' },
2026-03-17 14:17:21 +08:00
{
name: 'controlBar',
align: 'blabs',
x: 0,
y: 0,
children: [
{ name: 'progress', align: 'tlabs', x: 0, y: 0 },
{ name: 'playButton', align: 'tl', x: 15, y: 10 },
{ name: 'timeDisplay', align: 'tl', x: 10, y: 2 },
{ name: 'fullScreenButton', align: 'tr', x: 20, y: 12 },
{ name: 'setting', align: 'tr', x: 20, y: 11 },
{ name: 'volume', align: 'tr', x: 20, y: 10 },
],
},
],
},
(player: any) => {
2026-03-17 14:17:21 +08:00
player.on('ended', () => {
isPlaying.value = false;
emit('ended');
2026-03-17 14:17:21 +08:00
});
player.on('play', () => {
isPlaying.value = true;
loading.value = false;
emit('play');
});
player.on('pause', () => {
isPlaying.value = false;
emit('pause');
});
player.on('canplay', () => {
loading.value = false;
});
player.on('waiting', () => {
loading.value = true;
});
});
}
2026-03-17 14:17:21 +08:00
function dispose() {
if (playerInstance.value) {
try {
playerInstance.value.dispose();
} catch (_) {}
playerInstance.value = null;
}
}
2026-03-17 14:17:21 +08:00
function initPlayer() {
if (!props.noPage) {
const temObj = getTemItem(route.query._t as string);
if (!temObj) return;
_temObj.value = temObj;
createPlayer(_temObj.value.url, '/long/long.svg');
} else if (props.url) {
createPlayer(props.url, props.cover || '/long/long.svg');
}
2026-03-17 14:17:21 +08:00
}
// 键盘快捷键(参考 VideoPlayer
function handleKeydown(e: KeyboardEvent) {
const p = playerInstance.value;
if (!p) return;
switch (e.key) {
case ' ':
e.preventDefault();
if (isPlaying.value) p.pause(); else p.play();
break;
case 'ArrowLeft':
e.preventDefault();
p.seek?.(Math.max(0, (p.getCurrentTime?.() ?? 0) - 10));
break;
case 'ArrowRight':
e.preventDefault();
const duration = p.getDuration?.() ?? 0;
p.seek?.(Math.min(duration, (p.getCurrentTime?.() ?? 0) + 10));
break;
case 'ArrowUp':
e.preventDefault();
p.setVolume?.(Math.min(100, (p.getVolume?.() ?? 100) + 10));
break;
case 'ArrowDown':
e.preventDefault();
p.setVolume?.(Math.max(0, (p.getVolume?.() ?? 100) - 10));
break;
case 'f':
case 'F':
e.preventDefault();
p.requestFullScreen?.();
break;
case 'm':
case 'M':
e.preventDefault();
p.mute?.();
break;
2026-03-17 14:17:21 +08:00
}
}
watch(() => props.url, (newVal) => {
if (newVal) {
2026-03-17 14:17:21 +08:00
dispose();
loading.value = true;
nextTick(() => createPlayer(newVal, props.cover || '/long/long.svg'));
}
});
onMounted(() => {
nextTick(() => {
initPlayer();
playerWrapperRef.value?.focus?.();
});
});
onBeforeUnmount(() => {
dispose();
2026-03-17 14:17:21 +08:00
});
</script>
<style scoped lang="scss">
.player-wrapper {
position: relative;
outline: none;
&:focus {
outline: none;
}
}
.title-overlay {
position: absolute;
bottom: 60px;
left: 50%;
transform: translateX(-50%);
z-index: 15;
pointer-events: none;
.title-text {
padding: 8px 20px;
background: rgba(0, 0, 0, 0.6);
color: white;
font-size: 16px;
border-radius: 8px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.5);
z-index: 10;
}
.loading-spinner {
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-top-color: #ff8c42;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>