kindergarten_java/reading-platform-frontend/src/views/office/player.vue
2026-03-18 11:11:57 +08:00

251 lines
5.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>
</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';
const props = defineProps<{
url: string;
cover?: string;
noPage?: boolean;
/** 视频标题(参考 VideoPlayer */
title?: string;
}>();
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)}`);
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;
// @ts-ignore
playerInstance.value = new Aliplayer(
{
id: playerId.value,
width: '100%',
height: '100%',
source: source,
cover: cover || '/long/long.svg',
skinLayout: [
{ name: 'bigPlayButton', align: 'cc', x: 0, y: 0 },
{ name: 'H5Loading', align: 'cc' },
{
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) => {
player.on('ended', () => {
isPlaying.value = false;
emit('ended');
});
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;
});
});
}
function dispose() {
if (playerInstance.value) {
try {
playerInstance.value.dispose();
} catch (_) {}
playerInstance.value = null;
}
}
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');
}
}
// 键盘快捷键(参考 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;
}
}
watch(() => props.url, (newVal) => {
if (newVal) {
dispose();
loading.value = true;
nextTick(() => createPlayer(newVal, props.cover || '/long/long.svg'));
}
});
onMounted(() => {
nextTick(() => {
initPlayer();
playerWrapperRef.value?.focus?.();
});
});
onBeforeUnmount(() => {
dispose();
});
</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>