feat: KidsMode 文档/视频预览调整,player 增强
- KidsMode: 视频对接 player 组件,文档对接 WebOffice 组件 - WebOffice: 新增 noPage 嵌入模式,支持 props 传入 url/fileName - player: 参考 VideoPlayer 增强功能(title、emit、键盘快捷键、加载遮罩、唯一ID) Made-with: Cursor
This commit is contained in:
parent
155f5f230b
commit
459fa434ac
@ -1,9 +1,10 @@
|
||||
<template>
|
||||
|
||||
<div v-if="!expire" ref="containerRef" class="!w-full !h-full pos-fixed top-0 left-0 z-999"></div>
|
||||
<div v-else class="flex justify-center">
|
||||
<div class="my-60px">
|
||||
链接已失效!<span class=" cursor-pointer color-#0085FF ml-10px" @click="home">返回首页</span>
|
||||
<div v-if="!expire" ref="containerRef" class="!w-full !h-full z-999" :class="noPage ? 'absolute top-0 left-0' : 'pos-fixed top-0 left-0'"></div>
|
||||
<div v-else class="flex justify-center items-center w-full h-full">
|
||||
<div class="my-60px text-center">
|
||||
链接已失效!
|
||||
<span v-if="!noPage" class="cursor-pointer color-#0085FF ml-10px" @click="home">返回首页</span>
|
||||
<span v-else class="block mt-10px color-#999">文档预览加载失败</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <Modal ref="modalRef" class="max-w-80%" width="1340px" v-model:open="open" :footer="null" title="在线资源">
|
||||
@ -38,7 +39,7 @@
|
||||
</template>
|
||||
<!-- 阿里云IMM weboffice -->
|
||||
<script lang="ts" name="WebOffice" setup>
|
||||
import { onMounted, ref, nextTick, reactive, watch, onUnmounted, onBeforeUnmount } from 'vue';
|
||||
import { onMounted, ref, nextTick, onUnmounted, onBeforeUnmount } from 'vue';
|
||||
|
||||
import {
|
||||
generateWebofficeToken,
|
||||
@ -48,16 +49,39 @@ import {
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { getTemItem, TemObj } from './temObjs';
|
||||
|
||||
const props = defineProps<{
|
||||
/** 嵌入模式:与页面共存,使用 absolute 定位 */
|
||||
noPage?: boolean;
|
||||
/** 文档 URL(嵌入模式必传) */
|
||||
url?: string;
|
||||
/** 文件名(嵌入模式,用于 IMM) */
|
||||
fileName?: string;
|
||||
/** 文件 ID(嵌入模式可选) */
|
||||
fileId?: string;
|
||||
}>();
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null);
|
||||
const route = useRoute();
|
||||
const expire = ref(false);
|
||||
const router = useRouter();
|
||||
let updateSizeInterval: any;
|
||||
// const { hasPermission } = usePermission();
|
||||
|
||||
function getTemObjFromProps(): TemObj | null {
|
||||
if (!props.url) return null;
|
||||
const ext = props.url.split('.').pop()?.split('?')[0] || 'pdf';
|
||||
const name = props.fileName?.includes('.') ? props.fileName : `${props.fileName || 'document'}.${ext}`;
|
||||
return {
|
||||
id: props.fileId || '',
|
||||
isEdit: false,
|
||||
name,
|
||||
url: encodeURIComponent(props.url),
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
init(containerRef.value);
|
||||
})
|
||||
});
|
||||
});
|
||||
const _temObj = ref<TemObj>({
|
||||
id: '',
|
||||
@ -98,12 +122,16 @@ const baseInstance = ref<any>(null);
|
||||
async function init(mount: HTMLElement | null) {
|
||||
if (!mount) {
|
||||
console.error('确保挂载节点元素存在。 一般在 onMounted 钩子中调用。');
|
||||
return;
|
||||
}
|
||||
|
||||
let temObj: TemObj | null;
|
||||
if (props.noPage && props.url) {
|
||||
temObj = getTemObjFromProps();
|
||||
} else {
|
||||
temObj = getTemItem(route.query._t as string);
|
||||
}
|
||||
// IMM vue3接入文档 https://help.aliyun.com/zh/imm/user-guide/vue3-usage?spm=a2c4g.11186623.0.0.3a0244142zAkss
|
||||
// 获取 token
|
||||
// let tokenInfo = await props.getTokenFun(props.teachingMaterialsImmUrl);
|
||||
|
||||
const temObj = getTemItem(route.query._t as string);
|
||||
if (!temObj) {
|
||||
expire.value = true;
|
||||
return;
|
||||
@ -161,7 +189,7 @@ async function init(mount: HTMLElement | null) {
|
||||
instance.on('fileStatus', () => {
|
||||
debouncedFn(5000);
|
||||
});
|
||||
instance.ApiEvent.AddApiEventListener('error', (err) => {
|
||||
instance.ApiEvent.AddApiEventListener('error', (err: unknown) => {
|
||||
console.log('发生错误:', err);
|
||||
})
|
||||
|
||||
|
||||
@ -1,70 +1,78 @@
|
||||
<template>
|
||||
<div class="w-full h-full bg-#000 z-50 top-0 left-0" :class="noPage ? 'absolute' : 'fixed '">
|
||||
<!-- <PressDrag @click="toBreak"
|
||||
class="pos-absolute bg-#00000033 cursor-pointer w-30px h-30px flex-center z-99999 p-10px top-20px right-20px rounded-50%">
|
||||
<CloseCircleOutlined class="text-30px color-#ffffff" />
|
||||
</PressDrag> -->
|
||||
<div class="w-full h-full z-1" id="playerView"></div>
|
||||
<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 { CloseCircleOutlined } from '@ant-design/icons-vue';
|
||||
import PressDrag from '@/components/PressDrag.vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { getTemItem, TemObj } from './temObjs';
|
||||
// @ts-ignore
|
||||
const player = ref<typeof Aliplayer>(null); //播放器
|
||||
const route = useRoute();
|
||||
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: '',
|
||||
});
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
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');
|
||||
}
|
||||
})
|
||||
function createPlayer(source?: string, cover?: string) {
|
||||
if (!source) return;
|
||||
|
||||
});
|
||||
watch(() => props.url, (newVal) => {
|
||||
if (!!newVal) {
|
||||
createPlayer(newVal, props.cover || '/long/long.svg');
|
||||
}
|
||||
});
|
||||
const createPlayer = (source?: string, cover?: string) => {
|
||||
// 更多使用方法请参考接入文档:https://help.aliyun.com/zh/vod/developer-reference/integration
|
||||
const mountEl = document.getElementById(playerId.value);
|
||||
if (!mountEl) return;
|
||||
|
||||
// @ts-ignore
|
||||
player.value = new Aliplayer(
|
||||
playerInstance.value = new Aliplayer(
|
||||
{
|
||||
id: 'playerView',
|
||||
id: playerId.value,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
source: source, // 如果是私有加密播放请传入 vid/playauth/encryptType
|
||||
cover: cover,
|
||||
source: source,
|
||||
cover: cover || '/long/long.svg',
|
||||
skinLayout: [
|
||||
{ name: 'bigPlayButton', align: 'blabs', x: 30, y: 80 },
|
||||
{
|
||||
name: 'H5Loading',
|
||||
align: 'cc',
|
||||
},
|
||||
{ name: 'H5Loading', align: 'cc' },
|
||||
{
|
||||
name: 'controlBar',
|
||||
align: 'blabs',
|
||||
@ -81,43 +89,162 @@ const createPlayer = (source?: string, cover?: string) => {
|
||||
},
|
||||
],
|
||||
},
|
||||
// @ts-ignore
|
||||
(player: typeof Aliplayer) => {
|
||||
//播放下一个视频
|
||||
(player: any) => {
|
||||
player.on('ended', () => {
|
||||
// update(videoList[index + 1]);
|
||||
isPlaying.value = false;
|
||||
emit('ended');
|
||||
});
|
||||
},
|
||||
);
|
||||
console.log('player', player.value);
|
||||
};
|
||||
|
||||
function toBreak() {
|
||||
|
||||
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;
|
||||
});
|
||||
});
|
||||
}
|
||||
//点击右侧列表视频切换
|
||||
// const update = (video: PlayInfo) => {
|
||||
// // playObj.value = video;
|
||||
// player.value.dispose(); //销毁
|
||||
// createPlayer(video.Source, video.CoverURL); //创建
|
||||
// };
|
||||
|
||||
function dispose() {
|
||||
if (player.value) {
|
||||
player.value.dispose(); //销毁
|
||||
if (playerInstance.value) {
|
||||
try {
|
||||
playerInstance.value.dispose();
|
||||
} catch (_) {}
|
||||
playerInstance.value = null;
|
||||
}
|
||||
}
|
||||
onBeforeUnmount(() => {
|
||||
try {
|
||||
|
||||
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();
|
||||
} catch (error) { }
|
||||
loading.value = true;
|
||||
nextTick(() => createPlayer(newVal, props.cover || '/long/long.svg'));
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initPlayer();
|
||||
playerWrapperRef.value?.focus?.();
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
dispose();
|
||||
});
|
||||
// // 存储当前播放时间
|
||||
// const saveTime = function (memoryVideo: string, currentTime: string) {
|
||||
// localStorage.setItem(memoryVideo, currentTime);
|
||||
// };
|
||||
// // 获取此视频上次播放时间
|
||||
// const getTime = function (memoryVideo: string): string | null {
|
||||
// return localStorage.getItem(memoryVideo);
|
||||
// };
|
||||
</script>
|
||||
<style scoped></style>
|
||||
|
||||
<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>
|
||||
|
||||
@ -11,21 +11,18 @@
|
||||
<!-- 绘本展示 -->
|
||||
<Transition name="content-fade" mode="out-in">
|
||||
<div v-if="currentResourceType === 'ebook'" key="ebook" class="content-viewer">
|
||||
<EbookViewer
|
||||
:pages="ebookPages"
|
||||
:current-page="currentEbookPage"
|
||||
:audio-url="syncAudioUrl"
|
||||
:auto-play="autoPlayAudio"
|
||||
@page-change="handlePageChange"
|
||||
@toggle-audio-sync="toggleAudioSync"
|
||||
/>
|
||||
<EbookViewer :pages="ebookPages" :current-page="currentEbookPage" :audio-url="syncAudioUrl"
|
||||
:auto-play="autoPlayAudio" @page-change="handlePageChange" @toggle-audio-sync="toggleAudioSync" />
|
||||
</div>
|
||||
|
||||
<!-- 视频播放 -->
|
||||
<div v-else-if="currentResourceType === 'video'" key="video" class="content-viewer">
|
||||
<VideoPlayer
|
||||
:src="currentResourceUrl"
|
||||
<Player
|
||||
v-if="currentResourceUrl"
|
||||
:key="currentResourceUrl"
|
||||
:url="currentResourceUrl"
|
||||
:title="currentResourceName"
|
||||
:no-page="true"
|
||||
@ended="handleMediaEnded"
|
||||
/>
|
||||
</div>
|
||||
@ -40,40 +37,36 @@
|
||||
|
||||
<!-- 音频播放 -->
|
||||
<div v-else-if="currentResourceType === 'audio'" key="audio" class="content-viewer">
|
||||
<AudioPlayer
|
||||
:src="currentResourceUrl"
|
||||
:title="currentResourceName"
|
||||
:background-image="backgroundImageUrl"
|
||||
@ended="handleMediaEnded"
|
||||
/>
|
||||
<AudioPlayer :src="currentResourceUrl" :title="currentResourceName" :background-image="backgroundImageUrl"
|
||||
@ended="handleMediaEnded" />
|
||||
</div>
|
||||
|
||||
<!-- PPT/挂图展示 -->
|
||||
<div v-else-if="currentResourceType === 'ppt' || currentResourceType === 'poster'" key="slides" class="content-viewer">
|
||||
<SlidesViewer
|
||||
<div v-else-if="currentResourceType === 'ppt'" key="ppt" class="content-viewer">
|
||||
<!-- <SlidesViewer
|
||||
:pages="slidesPages"
|
||||
:current-page="currentSlidePage"
|
||||
:type="currentResourceType"
|
||||
@page-change="handleSlideChange"
|
||||
/>
|
||||
/> -->
|
||||
<WebOffice v-if="currentResourceUrl" :key="currentResourceUrl" :url="currentResourceUrl"
|
||||
:file-name="currentResourceName" :no-page="true" />
|
||||
</div>
|
||||
|
||||
<!-- 文档展示 (PDF等) -->
|
||||
<!-- PPT/挂图展示 -->
|
||||
<div v-else-if="currentResourceType === 'poster'" key="slides" class="content-viewer">
|
||||
<SlidesViewer :pages="slidesPages" :current-page="currentSlidePage" :type="currentResourceType"
|
||||
@page-change="handleSlideChange" />
|
||||
</div>
|
||||
<!-- 文档展示 (PDF/Word/Excel 等,使用 WebOffice) -->
|
||||
<div v-else-if="currentResourceType === 'document'" key="document" class="content-viewer">
|
||||
<SlidesViewer
|
||||
:pages="[currentResourceUrl]"
|
||||
:current-page="0"
|
||||
type="pdf"
|
||||
/>
|
||||
<WebOffice v-if="currentResourceUrl" :key="currentResourceUrl" :url="currentResourceUrl"
|
||||
:file-name="currentResourceName" :no-page="true" />
|
||||
</div>
|
||||
|
||||
<!-- 课程资源展示 (从底部点击的PPT/挂图) -->
|
||||
<div v-else-if="showingResource" key="resource" class="content-viewer">
|
||||
<SlidesViewer
|
||||
:pages="[showingResource.url]"
|
||||
:current-page="0"
|
||||
:type="(showingResource.type as 'ppt' | 'poster')"
|
||||
/>
|
||||
<SlidesViewer :pages="[showingResource.url]" :current-page="0"
|
||||
:type="(showingResource.type as 'ppt' | 'poster')" />
|
||||
<div class="resource-header">
|
||||
<span class="resource-title">{{ showingResource.name }}</span>
|
||||
<button class="close-resource-btn" @click="showingResource = null">
|
||||
@ -102,13 +95,9 @@
|
||||
<span>教学资源</span>
|
||||
</div>
|
||||
<div class="resources-track">
|
||||
<button
|
||||
v-for="resource in allLessonResources"
|
||||
:key="resource.id"
|
||||
class="resource-chip"
|
||||
<button v-for="resource in allLessonResources" :key="resource.id" class="resource-chip"
|
||||
:class="{ active: currentResourceIndex === resource.index }"
|
||||
@click.stop="handleResourceByIndex(resource.index)"
|
||||
>
|
||||
@click.stop="handleResourceByIndex(resource.index)">
|
||||
<component :is="getResourceIcon(resource.type)" :size="24" :stroke-width="2.5" />
|
||||
<span>{{ resource.name }}</span>
|
||||
</button>
|
||||
@ -133,12 +122,8 @@
|
||||
<span>延伸活动</span>
|
||||
</div>
|
||||
<div class="activities-track">
|
||||
<button
|
||||
v-for="activity in activities"
|
||||
:key="activity.id"
|
||||
class="activity-chip"
|
||||
@click.stop="handleActivityClick(activity)"
|
||||
>
|
||||
<button v-for="activity in activities" :key="activity.id" class="activity-chip"
|
||||
@click.stop="handleActivityClick(activity)">
|
||||
<component :is="getActivityIcon(activity.activityType)" :size="22" :stroke-width="2.5" />
|
||||
<span>{{ activity.name }}</span>
|
||||
</button>
|
||||
@ -151,7 +136,8 @@
|
||||
<ChevronLeft :size="28" :stroke-width="3" />
|
||||
<span>上一个</span>
|
||||
</button>
|
||||
<button class="ctrl-btn primary" :disabled="currentResourceIndex >= allLessonResources.length - 1" @click.stop="nextResource">
|
||||
<button class="ctrl-btn primary" :disabled="currentResourceIndex >= allLessonResources.length - 1"
|
||||
@click.stop="nextResource">
|
||||
<span>下一个</span>
|
||||
<ChevronRight :size="28" :stroke-width="3" />
|
||||
</button>
|
||||
@ -163,18 +149,14 @@
|
||||
</div>
|
||||
|
||||
<!-- 延伸活动弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="activityModalVisible"
|
||||
:title="selectedActivity?.name"
|
||||
:footer="null"
|
||||
width="90%"
|
||||
class="activity-modal"
|
||||
@cancel="activityModalVisible = false"
|
||||
>
|
||||
<a-modal v-model:open="activityModalVisible" :title="selectedActivity?.name" :footer="null" width="90%"
|
||||
class="activity-modal" @cancel="activityModalVisible = false">
|
||||
<div v-if="selectedActivity" class="activity-content">
|
||||
<div v-if="activityResourceUrl" class="activity-media">
|
||||
<video v-if="activityResourceType === 'video'" :src="activityResourceUrl" controls style="width: 100%; max-height: 50vh;"></video>
|
||||
<img v-else-if="activityResourceType === 'image'" :src="activityResourceUrl" style="width: 100%; max-height: 50vh; object-fit: contain;">
|
||||
<video v-if="activityResourceType === 'video'" :src="activityResourceUrl" controls
|
||||
style="width: 100%; max-height: 50vh;"></video>
|
||||
<img v-else-if="activityResourceType === 'image'" :src="activityResourceUrl"
|
||||
style="width: 100%; max-height: 50vh; object-fit: contain;">
|
||||
</div>
|
||||
<div class="activity-info">
|
||||
<div class="info-item" v-if="selectedActivity.objectives">
|
||||
@ -224,6 +206,8 @@ import {
|
||||
Lightbulb,
|
||||
} from 'lucide-vue-next';
|
||||
import EbookViewer from './viewers/EbookViewer.vue';
|
||||
import Player from '@/views/office/player.vue';
|
||||
import WebOffice from '@/views/office/WebOffice.vue';
|
||||
import VideoPlayer from './viewers/VideoPlayer.vue';
|
||||
import AudioPlayer from './viewers/AudioPlayer.vue';
|
||||
import SlidesViewer from './viewers/SlidesViewer.vue';
|
||||
@ -734,10 +718,10 @@ const loadCurrentStepResources = () => {
|
||||
|
||||
if (resource) {
|
||||
currentResourceType.value = resource.type === '电子绘本' ? 'ebook' :
|
||||
resource.type === '音频' ? 'audio' :
|
||||
resource.type === '视频' ? 'video' :
|
||||
resource.type === 'PPT课件' ? 'ppt' :
|
||||
resource.type === '教学挂图' ? 'poster' : '';
|
||||
resource.type === '音频' ? 'audio' :
|
||||
resource.type === '视频' ? 'video' :
|
||||
resource.type === 'PPT课件' ? 'ppt' :
|
||||
resource.type === '教学挂图' ? 'poster' : '';
|
||||
currentResourceUrl.value = resource.url ? getFileUrl(resource.url) : '';
|
||||
currentResourceName.value = resource.name;
|
||||
|
||||
@ -889,7 +873,7 @@ const handlePageChange = (page: number) => {
|
||||
currentEbookPage.value = page;
|
||||
};
|
||||
const handleSlideChange = (page: number) => { currentSlidePage.value = page; };
|
||||
const handleMediaEnded = () => {};
|
||||
const handleMediaEnded = () => { };
|
||||
const toggleAudioSync = () => { autoPlayAudio.value = !autoPlayAudio.value; };
|
||||
|
||||
const handleDocumentPageChange = (page: number) => {
|
||||
@ -1109,8 +1093,15 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0) translateX(0); }
|
||||
50% { transform: translateY(-20px) translateX(10px); }
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0) translateX(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-20px) translateX(10px);
|
||||
}
|
||||
}
|
||||
|
||||
// 内容切换动画
|
||||
@ -1135,12 +1126,15 @@ onUnmounted(() => {
|
||||
opacity: 0;
|
||||
transform: scale(0.3);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
@ -1153,6 +1147,7 @@ onUnmounted(() => {
|
||||
transform: scale(0);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(2.5);
|
||||
opacity: 0;
|
||||
@ -1161,15 +1156,32 @@ onUnmounted(() => {
|
||||
|
||||
// 摇晃动画
|
||||
@keyframes wiggle {
|
||||
0%, 100% { transform: rotate(0deg); }
|
||||
25% { transform: rotate(-5deg); }
|
||||
75% { transform: rotate(5deg); }
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: rotate(-5deg);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: rotate(5deg);
|
||||
}
|
||||
}
|
||||
|
||||
// 闪烁动画
|
||||
@keyframes sparkle {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.kids-content {
|
||||
@ -1184,6 +1196,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.content-viewer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
@ -1232,7 +1245,7 @@ onUnmounted(() => {
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
color: #FF8C42;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.1);
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.empty-hint {
|
||||
@ -1283,7 +1296,9 @@ onUnmounted(() => {
|
||||
overflow-x: auto;
|
||||
padding-bottom: 8px;
|
||||
|
||||
&::-webkit-scrollbar { display: none; }
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.step-group {
|
||||
@ -1320,7 +1335,7 @@ onUnmounted(() => {
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, #FFF3E0 0%, #FFE0B2 100%);
|
||||
@ -1369,8 +1384,15 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { box-shadow: 0 4px 16px rgba(255, 111, 0, 0.4); }
|
||||
50% { box-shadow: 0 6px 24px rgba(255, 111, 0, 0.6); }
|
||||
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 4px 16px rgba(255, 111, 0, 0.4);
|
||||
}
|
||||
|
||||
50% {
|
||||
box-shadow: 0 6px 24px rgba(255, 111, 0, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.step-resources {
|
||||
@ -1395,7 +1417,7 @@ onUnmounted(() => {
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, #BBDEFB 0%, #90CAF9 100%);
|
||||
@ -1421,7 +1443,7 @@ onUnmounted(() => {
|
||||
background: linear-gradient(90deg, #FFE0B2 0%, #FFCCBC 100%);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
@ -1545,7 +1567,9 @@ onUnmounted(() => {
|
||||
padding-bottom: 8px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&::-webkit-scrollbar { display: none; }
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.resource-chip {
|
||||
@ -1642,13 +1666,13 @@ onUnmounted(() => {
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
min-height: 60px;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #F5F5F5 0%, #EEEEEE 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,0.15);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
@ -1693,7 +1717,7 @@ onUnmounted(() => {
|
||||
:deep(.ant-modal-content) {
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.2);
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
:deep(.ant-modal-header) {
|
||||
@ -1741,7 +1765,7 @@ onUnmounted(() => {
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||
border: 3px solid #FFE0B2;
|
||||
}
|
||||
|
||||
@ -1751,7 +1775,7 @@ onUnmounted(() => {
|
||||
padding: 20px 24px;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
border: 2px solid #FFE0B2;
|
||||
|
||||
.info-label {
|
||||
|
||||
@ -1,30 +1,14 @@
|
||||
<template>
|
||||
<div
|
||||
class="video-player"
|
||||
ref="playerRef"
|
||||
:class="{
|
||||
'is-fullscreen': isFullscreen,
|
||||
'is-web-fullscreen': isWebFullscreen,
|
||||
'controls-visible': showControls || !isPlaying
|
||||
}"
|
||||
@mousemove="handleMouseMove"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<div class="video-player" ref="playerRef" :class="{
|
||||
'is-fullscreen': isFullscreen,
|
||||
'is-web-fullscreen': isWebFullscreen,
|
||||
'controls-visible': showControls || !isPlaying
|
||||
}" @mousemove="handleMouseMove" @mouseleave="handleMouseLeave">
|
||||
<!-- 视频容器 -->
|
||||
<div class="video-container" @click="togglePlay">
|
||||
<video
|
||||
ref="videoRef"
|
||||
:src="src"
|
||||
:poster="posterUrl"
|
||||
class="video-element"
|
||||
@loadedmetadata="onLoaded"
|
||||
@timeupdate="onTimeUpdate"
|
||||
@ended="onEnded"
|
||||
@play="isPlaying = true"
|
||||
@pause="isPlaying = false"
|
||||
@waiting="loading = true"
|
||||
@canplay="loading = false"
|
||||
/>
|
||||
<video ref="videoRef" :src="src" :poster="posterUrl" class="video-element" @loadedmetadata="onLoaded"
|
||||
@timeupdate="onTimeUpdate" @ended="onEnded" @play="isPlaying = true" @pause="isPlaying = false"
|
||||
@waiting="loading = true" @canplay="loading = false" />
|
||||
</div>
|
||||
|
||||
<!-- 加载中 -->
|
||||
@ -86,13 +70,8 @@
|
||||
{{ playbackRate }}x
|
||||
</button>
|
||||
<div class="speed-menu" v-if="showSpeedMenu">
|
||||
<div
|
||||
v-for="speed in speedOptions"
|
||||
:key="speed"
|
||||
class="speed-option"
|
||||
:class="{ active: playbackRate === speed }"
|
||||
@click="setSpeed(speed)"
|
||||
>
|
||||
<div v-for="speed in speedOptions" :key="speed" class="speed-option"
|
||||
:class="{ active: playbackRate === speed }" @click="setSpeed(speed)">
|
||||
{{ speed }}x
|
||||
</div>
|
||||
</div>
|
||||
@ -106,35 +85,18 @@
|
||||
<VolumeX v-else :size="20" />
|
||||
</button>
|
||||
<div class="volume-slider-wrapper">
|
||||
<input
|
||||
type="range"
|
||||
class="volume-slider"
|
||||
:value="volume"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
@input="changeVolume"
|
||||
/>
|
||||
<input type="range" class="volume-slider" :value="volume" min="0" max="1" step="0.05"
|
||||
@input="changeVolume" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 循环 -->
|
||||
<button
|
||||
class="ctrl-btn"
|
||||
:class="{ active: isLooping }"
|
||||
@click="toggleLoop"
|
||||
title="循环播放"
|
||||
>
|
||||
<button class="ctrl-btn" :class="{ active: isLooping }" @click="toggleLoop" title="循环播放">
|
||||
<Repeat :size="20" />
|
||||
</button>
|
||||
|
||||
<!-- 画中画 -->
|
||||
<button
|
||||
v-if="supportsPiP"
|
||||
class="ctrl-btn"
|
||||
@click="togglePiP"
|
||||
title="画中画"
|
||||
>
|
||||
<button v-if="supportsPiP" class="ctrl-btn" @click="togglePiP" title="画中画">
|
||||
<PictureInPicture2 :size="20" />
|
||||
</button>
|
||||
|
||||
@ -563,7 +525,9 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.play-overlay {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user