视频预览

This commit is contained in:
zhonghua 2026-03-17 14:17:21 +08:00
parent bb7fb86c3b
commit 155f5f230b
7 changed files with 348 additions and 11 deletions

View File

@ -9,6 +9,10 @@
<title>幼儿阅读教学服务平台</title> <title>幼儿阅读教学服务平台</title>
<!-- 阿里云IMM --> <!-- 阿里云IMM -->
<script src="https://g.alicdn.com/IMM/office-js/1.1.19/aliyun-web-office-sdk.min.js"></script> <script src="https://g.alicdn.com/IMM/office-js/1.1.19/aliyun-web-office-sdk.min.js"></script>
<link rel="stylesheet"
href="https://g.alicdn.com/apsara-media-box/imp-web-player/2.16.3/skins/default/aliplayer-min.css" />
<script charset="utf-8" type="text/javascript"
src="https://g.alicdn.com/apsara-media-box/imp-web-player/2.16.3/aliplayer-min.js"></script>
</head> </head>
<body> <body>

View File

@ -26,6 +26,7 @@ declare module 'vue' {
AForm: typeof import('ant-design-vue/es')['Form'] AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem'] AFormItem: typeof import('ant-design-vue/es')['FormItem']
AImage: typeof import('ant-design-vue/es')['Image'] AImage: typeof import('ant-design-vue/es')['Image']
AImagePreviewGroup: typeof import('ant-design-vue/es')['ImagePreviewGroup']
AInput: typeof import('ant-design-vue/es')['Input'] AInput: typeof import('ant-design-vue/es')['Input']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber'] AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword'] AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
@ -75,6 +76,7 @@ declare module 'vue' {
LessonConfigPanel: typeof import('./components/course/LessonConfigPanel.vue')['default'] LessonConfigPanel: typeof import('./components/course/LessonConfigPanel.vue')['default']
LessonStepsEditor: typeof import('./components/course/LessonStepsEditor.vue')['default'] LessonStepsEditor: typeof import('./components/course/LessonStepsEditor.vue')['default']
NotificationBell: typeof import('./components/NotificationBell.vue')['default'] NotificationBell: typeof import('./components/NotificationBell.vue')['default']
PressDrag: typeof import('./components/PressDrag.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
Step1BasicInfo: typeof import('./components/course-edit/Step1BasicInfo.vue')['default'] Step1BasicInfo: typeof import('./components/course-edit/Step1BasicInfo.vue')['default']

View File

@ -1,12 +1,16 @@
<template> <template>
<a-modal v-model:open="visible" :title="title" :width="modalWidth" :footer="null" centered @cancel="handleClose"> <a-modal v-model:open="visible" :title="title" :width="modalWidth" :footer="null" centered @cancel="handleClose">
<!-- PDF 预览 --> <!-- PDF 预览 -->
<div v-if="fileType === 'pdf'" class="preview-container pdf-container"> <div v-if="fileType === 'pdf'" class="preview-container ppt-container">
<iframe v-if="previewUrl" :src="previewUrl" class="pdf-iframe" frameborder="0"></iframe> <div class="ppt-notice">
<div v-else class="preview-error">
<FileTextOutlined style="font-size: 48px; color: #999;" /> <FileTextOutlined style="font-size: 48px; color: #999;" />
<p>无法预览此PDF文件</p> <h3>PDF 文件预览</h3>
<a-button type="primary" @click="downloadFile">下载查看</a-button> <p>使用 WebOffice 在新页面中预览</p>
<a-space>
<a-button type="primary" @click="openInWebOffice">
<EyeOutlined /> PDF 预览
</a-button>
</a-space>
</div> </div>
</div> </div>
@ -26,10 +30,11 @@
</div> </div>
<!-- 视频播放器 --> <!-- 视频播放器 -->
<div v-else-if="fileType === 'video'" class="preview-container video-container"> <div v-else-if="fileType === 'video'" class="preview-container video-container relative h-600px">
<video ref="videoRef" :src="previewUrl" controls class="video-element" @error="handleMediaError"> <!-- <video ref="videoRef" :src="previewUrl" controls class="video-element" @error="handleMediaError">
您的浏览器不支持视频播放 您的浏览器不支持视频播放
</video> </video> -->
<player v-if="visible" :url="previewUrl" :noPage="true" />
</div> </div>
<!-- 图片预览 --> <!-- 图片预览 -->
@ -107,7 +112,7 @@ import {
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { openWebOffice } from '@/views/office/webOffice'; import { openWebOffice } from '@/views/office/webOffice';
import Player from '@/views/office/player.vue';
const props = defineProps<{ const props = defineProps<{
open: boolean; open: boolean;
fileUrl: string; fileUrl: string;
@ -169,8 +174,6 @@ const title = computed(() => {
// //
const modalWidth = computed(() => { const modalWidth = computed(() => {
switch (fileType.value) { switch (fileType.value) {
case 'pdf':
return '90%';
case 'video': case 'video':
return 900; return 900;
case 'audio': case 'audio':

View File

@ -0,0 +1,141 @@
<template>
<div ref="" id="draggableElementView" class="draggable-box cursor-grab" :style="elementStyle"
@mousedown.passive="handleStart" @touchstart.passive="handleStart" @click="handleClick">
<slot></slot>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, onUnmounted } from 'vue';
import { addResizeTimeOut, removeResizeTimeOut } from '@/utils';
export default defineComponent({
name: 'PressDragComponent',
emits: ['click'],
props: {
storageKey: {
default: '',
},
},
setup(props, { emit }) {
const view = {
offsetWidth: 50,
offsetHeight: 50,
innerWidth: 750,
innerHeight: 1280,
};
const storageKeyVal = `_tem_drag_${props.storageKey}`;
const initil = () => {
const node = document.querySelector('#draggableElementView') as HTMLElement;
view.offsetWidth = node.offsetWidth;
view.offsetHeight = node.offsetHeight;
view.innerWidth =
window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
view.innerHeight =
window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
};
addResizeTimeOut(initil);
const isDragging = ref(false);
const pressTimer = ref<number | null>(null);
const startTime = ref(0);
const elementStyle = ref<any>({
userSelect: 'none',
touchAction: 'none',
});
onMounted(() => {
nextTick(() => {
initil();
// if (props.storageKey && props.storageKey.length > 0) {
// let storageVal: any = localStorage.getItem(storageKeyVal);
// if (storageVal && storageVal.length > 0) {
// storageVal = storageVal.split(',');
// setStyle(storageVal[0], storageVal[1]);
// }
// }
});
});
const handleStart = (e: MouseEvent | TouchEvent) => {
e.preventDefault();
startTime.value = Date.now();
pressTimer.value = window.setTimeout(() => {
isDragging.value = true;
document.addEventListener('mousemove', handleDrag);
document.addEventListener('touchmove', handleDrag, { passive: false });
document.addEventListener('mouseup', handleEnd);
document.addEventListener('touchend', handleEnd);
}, 200);
};
const handleDrag = (e: MouseEvent | TouchEvent) => {
if (!isDragging.value) return;
e.preventDefault();
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
let left = clientX - view.offsetWidth / 2;
let top = clientY - view.offsetHeight / 2;
left = left > view.innerWidth - view.offsetWidth ? view.innerWidth - view.offsetWidth : left;
top = top > view.innerHeight - view.offsetHeight ? view.innerHeight - view.offsetHeight : top;
setStyle(left, top);
};
const setStyle = (left: number, top: number) => {
if (props.storageKey && props.storageKey.length > 0) {
localStorage.setItem(storageKeyVal, [left, top].join(','));
}
// if (left > innerWidth - 90) {
// left = innerWidth - 90;
// } else if (left < 30) {
// left = 30;
// }
// if (top > innerHeight - 90) {
// top = innerHeight - 90;
// } else if (top < 30) {
// top = 30;
// }
left = (left / view.innerWidth) * 100;
left = left < 0 ? 0 : left;
top = (top / view.innerHeight) * 100;
top = top < 0 ? 0 : top;
elementStyle.value = {
...elementStyle.value,
left: `${left}%`,
top: `${top}%`,
};
};
const handleEnd = () => {
clearTimeout(pressTimer.value!);
document.removeEventListener('mousemove', handleDrag);
document.removeEventListener('touchmove', handleDrag);
document.removeEventListener('mouseup', handleEnd);
document.removeEventListener('touchend', handleEnd);
isDragging.value = false;
};
const handleClick = (e: MouseEvent) => {
if (Date.now() - startTime.value > 200) return;
emit('click', e);
};
onUnmounted(() => {
handleEnd();
removeResizeTimeOut(initil);
});
return { elementStyle, handleStart, handleClick };
},
});
</script>
<style scoped>
/* .draggable-box {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.1s, box-shadow 0.3s;
} */
.draggable-box:active {
transform: scale(0.98);
}
</style>

View File

@ -0,0 +1,51 @@
/**
*
* @param ms
* @returns
*/
export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/***
* resize事件封装
*/
const resizeObjs = reactive<{
time: any;
callbackList: Function[];
}>({
time: -1,
callbackList: [],
});
window.addEventListener(
"resize",
() => {
if (resizeObjs.time >= 0) {
clearTimeout(resizeObjs.time);
}
resizeObjs.time = setTimeout(() => {
resizeObjs.time = -1;
try {
resizeObjs.callbackList.forEach((callback: Function) => {
callback();
});
} catch (error) {
console.error(error);
}
}, 600);
},
false,
);
/**
* @param callback
*/
export function addResizeTimeOut(callback: Function) {
resizeObjs.callbackList.push(callback);
}
/**
* @param callback
*/
export function removeResizeTimeOut(callback: Function) {
resizeObjs.callbackList = resizeObjs.callbackList.filter((_callback) => {
return callback != _callback;
});
}

View File

@ -0,0 +1,123 @@
<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>
</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();
const props = defineProps<{
url: string;
cover?: string;
noPage?: boolean;
}>();
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');
}
})
});
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
// @ts-ignore
player.value = new Aliplayer(
{
id: 'playerView',
width: '100%',
height: '100%',
source: source, // vid/playauth/encryptType
cover: cover,
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 },
],
},
],
},
// @ts-ignore
(player: typeof Aliplayer) => {
//
player.on('ended', () => {
// update(videoList[index + 1]);
});
},
);
console.log('player', player.value);
};
function toBreak() {
}
//
// const update = (video: PlayInfo) => {
// // playObj.value = video;
// player.value.dispose(); //
// createPlayer(video.Source, video.CoverURL); //
// };
function dispose() {
if (player.value) {
player.value.dispose(); //
}
}
onBeforeUnmount(() => {
try {
dispose();
} catch (error) { }
});
// //
// 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>

View File

@ -191,6 +191,13 @@ declare module 'vue-router/auto-routes' {
Record<never, never>, Record<never, never>,
| never | never
>, >,
'/office/player': RouteRecordInfo<
'/office/player',
'/office/player',
Record<never, never>,
Record<never, never>,
| never
>,
'/office/WebOffice': RouteRecordInfo< '/office/WebOffice': RouteRecordInfo<
'/office/WebOffice', '/office/WebOffice',
'/office/WebOffice', '/office/WebOffice',
@ -852,6 +859,12 @@ declare module 'vue-router/auto-routes' {
views: views:
| never | never
} }
'src/views/office/player.vue': {
routes:
| '/office/player'
views:
| never
}
'src/views/office/WebOffice.vue': { 'src/views/office/WebOffice.vue': {
routes: routes:
| '/office/WebOffice' | '/office/WebOffice'