视频预览
This commit is contained in:
parent
bb7fb86c3b
commit
155f5f230b
@ -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>
|
||||||
|
|||||||
@ -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']
|
||||||
|
|||||||
@ -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':
|
||||||
|
|||||||
141
reading-platform-frontend/src/components/PressDrag.vue
Normal file
141
reading-platform-frontend/src/components/PressDrag.vue
Normal 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>
|
||||||
51
reading-platform-frontend/src/utils/index.ts
Normal file
51
reading-platform-frontend/src/utils/index.ts
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
123
reading-platform-frontend/src/views/office/player.vue
Normal file
123
reading-platform-frontend/src/views/office/player.vue
Normal 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>
|
||||||
13
reading-platform-frontend/typed-router.d.ts
vendored
13
reading-platform-frontend/typed-router.d.ts
vendored
@ -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'
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user