|
|
@@ -0,0 +1,257 @@
|
|
|
+<script setup lang="ts">
|
|
|
+import type { UploadFile, UploadProps } from 'ant-design-vue';
|
|
|
+
|
|
|
+import type { VideoSubmitVO } from '#/api/outcome';
|
|
|
+
|
|
|
+import { ref } from 'vue';
|
|
|
+
|
|
|
+import { PictureOutlined, UploadOutlined } from '@ant-design/icons-vue';
|
|
|
+import { message, Upload, UploadDragger } from 'ant-design-vue';
|
|
|
+
|
|
|
+import { useEditShell } from '#/adapter/shell';
|
|
|
+import { invokeMethod } from '#/adapter/vxe-table/proxy/invoke-method';
|
|
|
+import { uploadFileMethod } from '#/api/common';
|
|
|
+import { useWorkroomStore } from '#/stores';
|
|
|
+
|
|
|
+import { videoForm } from '../video.data';
|
|
|
+
|
|
|
+const workroomStore = useWorkroomStore();
|
|
|
+
|
|
|
+const VIDEO_MAX_SIZE = 50 * 1024 * 1024;
|
|
|
+
|
|
|
+const videoFileList = ref<UploadFile[]>([]);
|
|
|
+const thumbnailFileList = ref<UploadFile[]>([]);
|
|
|
+const videoUrl = ref<string>();
|
|
|
+const thumbnailUrl = ref<string>();
|
|
|
+const duration = ref<string>();
|
|
|
+const videoUploading = ref(false);
|
|
|
+const thumbnailUploading = ref(false);
|
|
|
+
|
|
|
+function createUploadFile(url: string | undefined, name: string): UploadFile[] {
|
|
|
+ if (!url) return [];
|
|
|
+ return [
|
|
|
+ {
|
|
|
+ uid: '-1',
|
|
|
+ name,
|
|
|
+ status: 'done',
|
|
|
+ url,
|
|
|
+ },
|
|
|
+ ];
|
|
|
+}
|
|
|
+
|
|
|
+function resetUploads() {
|
|
|
+ videoFileList.value = [];
|
|
|
+ thumbnailFileList.value = [];
|
|
|
+ videoUrl.value = void 0;
|
|
|
+ thumbnailUrl.value = void 0;
|
|
|
+ duration.value = void 0;
|
|
|
+ videoUploading.value = false;
|
|
|
+ thumbnailUploading.value = false;
|
|
|
+}
|
|
|
+
|
|
|
+function formatDuration(totalSeconds: number) {
|
|
|
+ const hours = Math.floor(totalSeconds / 3600);
|
|
|
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
|
+ const seconds = Math.floor(totalSeconds % 60);
|
|
|
+ if (hours > 0) {
|
|
|
+ return `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
|
|
+ }
|
|
|
+ return `${minutes}:${String(seconds).padStart(2, '0')}`;
|
|
|
+}
|
|
|
+
|
|
|
+async function readVideoDuration(file: File) {
|
|
|
+ return new Promise<string>((resolve) => {
|
|
|
+ const video = document.createElement('video');
|
|
|
+ video.preload = 'metadata';
|
|
|
+ video.addEventListener('loadedmetadata', () => {
|
|
|
+ URL.revokeObjectURL(video.src);
|
|
|
+ resolve(formatDuration(Math.floor(video.duration)));
|
|
|
+ });
|
|
|
+ video.addEventListener('error', () => {
|
|
|
+ URL.revokeObjectURL(video.src);
|
|
|
+ resolve('');
|
|
|
+ });
|
|
|
+ video.src = URL.createObjectURL(file);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+async function uploadImmediately(
|
|
|
+ file: File,
|
|
|
+ listRef: typeof videoFileList,
|
|
|
+ urlRef: typeof videoUrl,
|
|
|
+ uploadingRef: typeof videoUploading,
|
|
|
+ onSuccess?: () => Promise<void> | void,
|
|
|
+) {
|
|
|
+ const uploadFile: UploadFile = {
|
|
|
+ uid: `${Date.now()}`,
|
|
|
+ name: file.name,
|
|
|
+ status: 'uploading',
|
|
|
+ percent: 0,
|
|
|
+ };
|
|
|
+ listRef.value = [uploadFile];
|
|
|
+ uploadingRef.value = true;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const url = await invokeMethod(uploadFileMethod(file), { force: true });
|
|
|
+ if (!url) {
|
|
|
+ throw new Error('upload empty url');
|
|
|
+ }
|
|
|
+ urlRef.value = url;
|
|
|
+ listRef.value = [
|
|
|
+ {
|
|
|
+ ...uploadFile,
|
|
|
+ status: 'done',
|
|
|
+ url,
|
|
|
+ },
|
|
|
+ ];
|
|
|
+ await onSuccess?.();
|
|
|
+ } catch {
|
|
|
+ urlRef.value = void 0;
|
|
|
+ listRef.value = [
|
|
|
+ {
|
|
|
+ ...uploadFile,
|
|
|
+ status: 'error',
|
|
|
+ },
|
|
|
+ ];
|
|
|
+ message.error(`${file.name} 上传失败,请重试`);
|
|
|
+ } finally {
|
|
|
+ uploadingRef.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const { Form, Shell, api } = useEditShell<VideoSubmitVO>(videoForm, {
|
|
|
+ onLoaded(model) {
|
|
|
+ api.shell.setState({
|
|
|
+ title: model.id ? '编辑视频' : '上传视频',
|
|
|
+ });
|
|
|
+ },
|
|
|
+ handleLoad(model) {
|
|
|
+ resetUploads();
|
|
|
+ videoUrl.value = model.videoUrl;
|
|
|
+ thumbnailUrl.value = model.thumbnailUrl;
|
|
|
+ duration.value = model.duration;
|
|
|
+ videoFileList.value = createUploadFile(
|
|
|
+ model.videoUrl,
|
|
|
+ model.name || '视频',
|
|
|
+ );
|
|
|
+ thumbnailFileList.value = createUploadFile(model.thumbnailUrl, '视频首图');
|
|
|
+ return model;
|
|
|
+ },
|
|
|
+ handleSubmit(values) {
|
|
|
+ const workroomId = values.workroomId || workroomStore.workroomId;
|
|
|
+ if (!workroomId) {
|
|
|
+ message.error('请先选择工作室');
|
|
|
+ throw new Error('workroom required');
|
|
|
+ }
|
|
|
+ if (videoUploading.value || thumbnailUploading.value) {
|
|
|
+ message.warning('文件上传中,请稍候');
|
|
|
+ throw new Error('uploading');
|
|
|
+ }
|
|
|
+ if (!videoUrl.value) {
|
|
|
+ message.error('请上传视频');
|
|
|
+ throw new Error('video required');
|
|
|
+ }
|
|
|
+ if (!thumbnailUrl.value) {
|
|
|
+ message.error('请上传视频首图');
|
|
|
+ throw new Error('thumbnail required');
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ ...values,
|
|
|
+ workroomId,
|
|
|
+ videoUrl: videoUrl.value,
|
|
|
+ thumbnailUrl: thumbnailUrl.value,
|
|
|
+ duration: duration.value,
|
|
|
+ };
|
|
|
+ },
|
|
|
+ onClosed: resetUploads,
|
|
|
+});
|
|
|
+
|
|
|
+const beforeVideoUpload: UploadProps['beforeUpload'] = (file) => {
|
|
|
+ if (file.size > VIDEO_MAX_SIZE) {
|
|
|
+ message.error('上传视频大小不超过50M');
|
|
|
+ return Upload.LIST_IGNORE;
|
|
|
+ }
|
|
|
+ void uploadImmediately(
|
|
|
+ file,
|
|
|
+ videoFileList,
|
|
|
+ videoUrl,
|
|
|
+ videoUploading,
|
|
|
+ async () => {
|
|
|
+ duration.value = await readVideoDuration(file);
|
|
|
+ },
|
|
|
+ );
|
|
|
+ return false;
|
|
|
+};
|
|
|
+
|
|
|
+const beforeThumbnailUpload: UploadProps['beforeUpload'] = (file) => {
|
|
|
+ const isImage = file.type.startsWith('image/');
|
|
|
+ if (!isImage) {
|
|
|
+ message.error('请上传图片文件');
|
|
|
+ return Upload.LIST_IGNORE;
|
|
|
+ }
|
|
|
+ void uploadImmediately(
|
|
|
+ file,
|
|
|
+ thumbnailFileList,
|
|
|
+ thumbnailUrl,
|
|
|
+ thumbnailUploading,
|
|
|
+ );
|
|
|
+ return false;
|
|
|
+};
|
|
|
+
|
|
|
+function onVideoRemove() {
|
|
|
+ videoUrl.value = void 0;
|
|
|
+ duration.value = void 0;
|
|
|
+}
|
|
|
+
|
|
|
+function onThumbnailRemove() {
|
|
|
+ thumbnailUrl.value = void 0;
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <Shell>
|
|
|
+ <div class="mx-4">
|
|
|
+ <Form />
|
|
|
+
|
|
|
+ <div class="pb-4">
|
|
|
+ <div class="mb-2 text-sm">
|
|
|
+ <span class="text-destructive mr-1">*</span>
|
|
|
+ 上传视频
|
|
|
+ </div>
|
|
|
+ <UploadDragger
|
|
|
+ v-model:file-list="videoFileList"
|
|
|
+ :before-upload="beforeVideoUpload"
|
|
|
+ :max-count="1"
|
|
|
+ accept="video/*"
|
|
|
+ @remove="onVideoRemove"
|
|
|
+ >
|
|
|
+ <p class="ant-upload-drag-icon">
|
|
|
+ <UploadOutlined />
|
|
|
+ </p>
|
|
|
+ <p class="ant-upload-text">点击上传或拖拽视频到此处</p>
|
|
|
+ <p class="ant-upload-hint">上传视频大小不超过50M</p>
|
|
|
+ </UploadDragger>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="pb-2">
|
|
|
+ <div class="mb-2 text-sm">
|
|
|
+ <span class="text-destructive mr-1">*</span>
|
|
|
+ 视频首图
|
|
|
+ </div>
|
|
|
+ <Upload
|
|
|
+ v-model:file-list="thumbnailFileList"
|
|
|
+ :before-upload="beforeThumbnailUpload"
|
|
|
+ :max-count="1"
|
|
|
+ accept="image/*"
|
|
|
+ list-type="picture-card"
|
|
|
+ @remove="onThumbnailRemove"
|
|
|
+ >
|
|
|
+ <div v-if="thumbnailFileList.length === 0" class="text-gray-400">
|
|
|
+ <PictureOutlined class="text-2xl" />
|
|
|
+ </div>
|
|
|
+ </Upload>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </Shell>
|
|
|
+</template>
|