- KidsMode: 视频对接 player 组件,文档对接 WebOffice 组件 - WebOffice: 新增 noPage 嵌入模式,支持 props 传入 url/fileName - player: 参考 VideoPlayer 增强功能(title、emit、键盘快捷键、加载遮罩、唯一ID) Made-with: Cursor
251 lines
5.6 KiB
Vue
251 lines
5.6 KiB
Vue
<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: 'blabs', x: 30, y: 80 },
|
||
{ 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>
|