2026-03-17 14:17:21 +08:00
|
|
|
|
<template>
|
2026-03-17 14:43:08 +08:00
|
|
|
|
<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>
|
2026-03-17 14:43:08 +08:00
|
|
|
|
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;
|
2026-03-17 14:43:08 +08:00
|
|
|
|
/** 视频标题(参考 VideoPlayer) */
|
|
|
|
|
|
title?: string;
|
2026-03-17 14:17:21 +08:00
|
|
|
|
}>();
|
2026-03-17 14:43:08 +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: '',
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-17 14:43:08 +08:00
|
|
|
|
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
|
2026-03-17 14:43:08 +08:00
|
|
|
|
playerInstance.value = new Aliplayer(
|
2026-03-17 14:17:21 +08:00
|
|
|
|
{
|
2026-03-17 14:43:08 +08:00
|
|
|
|
id: playerId.value,
|
2026-03-17 14:17:21 +08:00
|
|
|
|
width: '100%',
|
|
|
|
|
|
height: '100%',
|
2026-03-17 14:43:08 +08:00
|
|
|
|
source: source,
|
|
|
|
|
|
cover: cover || '/long/long.svg',
|
2026-03-17 14:17:21 +08:00
|
|
|
|
skinLayout: [
|
|
|
|
|
|
{ name: 'bigPlayButton', align: 'blabs', x: 30, y: 80 },
|
2026-03-17 14:43:08 +08:00
|
|
|
|
{ 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 },
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
2026-03-17 14:43:08 +08:00
|
|
|
|
(player: any) => {
|
2026-03-17 14:17:21 +08:00
|
|
|
|
player.on('ended', () => {
|
2026-03-17 14:43:08 +08:00
|
|
|
|
isPlaying.value = false;
|
|
|
|
|
|
emit('ended');
|
2026-03-17 14:17:21 +08:00
|
|
|
|
});
|
2026-03-17 14:43:08 +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
|
|
|
|
|
2026-03-17 14:43:08 +08:00
|
|
|
|
function dispose() {
|
|
|
|
|
|
if (playerInstance.value) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
playerInstance.value.dispose();
|
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
playerInstance.value = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-17 14:17:21 +08:00
|
|
|
|
|
2026-03-17 14:43:08 +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
|
|
|
|
}
|
2026-03-17 14:43:08 +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
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-17 14:43:08 +08:00
|
|
|
|
|
|
|
|
|
|
watch(() => props.url, (newVal) => {
|
|
|
|
|
|
if (newVal) {
|
2026-03-17 14:17:21 +08:00
|
|
|
|
dispose();
|
2026-03-17 14:43:08 +08:00
|
|
|
|
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>
|
2026-03-17 14:43:08 +08:00
|
|
|
|
|
|
|
|
|
|
<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>
|