|
@@ -0,0 +1,419 @@
|
|
|
+import { NModal, NUpload, UploadFileInfo, useMessage } from 'naive-ui';
|
|
|
+import { defineComponent, watch, PropType, reactive, ref } from 'vue';
|
|
|
+import { policy } from '@/components/upload-file/api';
|
|
|
+import Copper from '@/components/upload-file/copper';
|
|
|
+import axios from 'axios';
|
|
|
+import styles from './index.module.less';
|
|
|
+import iconUploadAdd from '../../../images/icon-upload-add.svg';
|
|
|
+import { NaturalTypeEnum, PageEnum } from '@/enums/pageEnum';
|
|
|
+import { formatUrlType } from '.';
|
|
|
+
|
|
|
+/**
|
|
|
+ * 1. 图片上传可以进行裁剪
|
|
|
+ * 2. 视频上传可以选择某一帧做为封面
|
|
|
+ * 3. 音频只用限制某一种格式
|
|
|
+ * 4. 只支持单个上传,因为多个上传没有办法去处理,即有视频,图片等
|
|
|
+ */
|
|
|
+export default defineComponent({
|
|
|
+ name: 'upload-file',
|
|
|
+ props: {
|
|
|
+ fileList: {
|
|
|
+ type: String,
|
|
|
+ default: ''
|
|
|
+ },
|
|
|
+ imageList: {
|
|
|
+ type: Array,
|
|
|
+ default: () => []
|
|
|
+ },
|
|
|
+ accept: {
|
|
|
+ // 支持类型
|
|
|
+ type: String,
|
|
|
+ default: '.jpg,.png,.jpeg,.gif'
|
|
|
+ },
|
|
|
+ showType: {
|
|
|
+ type: String as PropType<'default' | 'custom'>,
|
|
|
+ default: 'default'
|
|
|
+ },
|
|
|
+ showFileList: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true
|
|
|
+ },
|
|
|
+ max: {
|
|
|
+ type: Number as PropType<number>,
|
|
|
+ default: 1
|
|
|
+ },
|
|
|
+ multiple: {
|
|
|
+ type: Boolean as PropType<boolean>,
|
|
|
+ default: false
|
|
|
+ },
|
|
|
+ disabled: {
|
|
|
+ type: Boolean as PropType<boolean>,
|
|
|
+ default: false
|
|
|
+ },
|
|
|
+ bucketName: {
|
|
|
+ type: String,
|
|
|
+ default: 'gyt'
|
|
|
+ },
|
|
|
+ path: {
|
|
|
+ type: String,
|
|
|
+ default: ''
|
|
|
+ },
|
|
|
+ fileName: {
|
|
|
+ type: String,
|
|
|
+ default: ''
|
|
|
+ },
|
|
|
+ cropper: {
|
|
|
+ // 是否裁切, 只有图片才支持
|
|
|
+ type: Boolean as PropType<boolean>,
|
|
|
+ default: false
|
|
|
+ },
|
|
|
+ options: {
|
|
|
+ type: Object,
|
|
|
+ default: () => {
|
|
|
+ return {
|
|
|
+ viewMode: 0,
|
|
|
+ autoCrop: true, //是否默认生成截图框
|
|
|
+ enlarge: 1, // 图片放大倍数
|
|
|
+ autoCropWidth: 200, //默认生成截图框宽度
|
|
|
+ autoCropHeight: 200, //默认生成截图框高度
|
|
|
+ fixedBox: false, //是否固定截图框大小 不允许改变
|
|
|
+ previewsCircle: true, //预览图是否是原图形
|
|
|
+ title: '上传图片'
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ emits: [
|
|
|
+ 'update:fileList',
|
|
|
+ 'close',
|
|
|
+ 'readFileInputEventAsArrayBuffer',
|
|
|
+ 'remove',
|
|
|
+ 'finished'
|
|
|
+ ],
|
|
|
+ setup(props, { emit, expose, slots }) {
|
|
|
+ const ossUploadUrl = `https://${props.bucketName}.ks3-cn-beijing.ksyuncs.com/`;
|
|
|
+ const message = useMessage();
|
|
|
+ const visiable = ref<boolean>(false);
|
|
|
+ const btnLoading = ref<boolean>(false);
|
|
|
+ const tempFiileBuffer = ref();
|
|
|
+ const uploadRef = ref();
|
|
|
+ const state = reactive({
|
|
|
+ policy: '',
|
|
|
+ signature: '',
|
|
|
+ key: '',
|
|
|
+ KSSAccessKeyId: '',
|
|
|
+ acl: 'public-read',
|
|
|
+ name: ''
|
|
|
+ }) as any;
|
|
|
+
|
|
|
+ const fileListRef = ref<UploadFileInfo[]>([]);
|
|
|
+ const initFileList = () => {
|
|
|
+ if (props.fileList) {
|
|
|
+ const splitName = props.fileList.split('/');
|
|
|
+ fileListRef.value = [
|
|
|
+ {
|
|
|
+ id: new Date().getTime().toString(),
|
|
|
+ name: splitName[splitName.length - 1],
|
|
|
+ status: 'finished',
|
|
|
+ url: props.fileList
|
|
|
+ }
|
|
|
+ ];
|
|
|
+ } else {
|
|
|
+ fileListRef.value = [];
|
|
|
+ }
|
|
|
+ };
|
|
|
+ initFileList();
|
|
|
+ watch(
|
|
|
+ () => props.imageList,
|
|
|
+ () => {
|
|
|
+ initFileList();
|
|
|
+ }
|
|
|
+ );
|
|
|
+ watch(
|
|
|
+ () => props.fileList,
|
|
|
+ () => {
|
|
|
+ // console.log('list');
|
|
|
+ initFileList();
|
|
|
+ }
|
|
|
+ );
|
|
|
+ const handleClearFile = () => {
|
|
|
+ console.log(uploadRef.value, 'uploadRef.value');
|
|
|
+ uploadRef.value?.clear();
|
|
|
+ };
|
|
|
+ expose({
|
|
|
+ handleClearFile
|
|
|
+ });
|
|
|
+
|
|
|
+ const getVideoMsg = (file: any) => {
|
|
|
+ return new Promise(resolve => {
|
|
|
+ let dataURL = '';
|
|
|
+ const videoElement = document.createElement('video');
|
|
|
+ videoElement.src = URL.createObjectURL(file);
|
|
|
+ videoElement.addEventListener('loadedmetadata', function () {
|
|
|
+ const canvas: any = document.createElement('canvas'),
|
|
|
+ width = videoElement.videoWidth, //canvas的尺寸和图片一样
|
|
|
+ height = videoElement.videoHeight;
|
|
|
+ canvas.width = width;
|
|
|
+ canvas.height = height;
|
|
|
+ canvas.getContext('2d').drawImage(videoElement, 0, 0, width, height); //绘制canvas
|
|
|
+ dataURL = canvas.toDataURL('image/jpeg'); //转换为base64
|
|
|
+ resolve(dataURL);
|
|
|
+ // resolve({
|
|
|
+ // duration: videoElement.duration,
|
|
|
+ // height: videoElement.videoHeight,
|
|
|
+ // width: videoElement.videoWidth
|
|
|
+ // });
|
|
|
+ });
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const CropperModal = ref();
|
|
|
+ const onBeforeUpload = async (options: any) => {
|
|
|
+ const file = options.file;
|
|
|
+ // 文件大小
|
|
|
+ let isLt2M = true;
|
|
|
+ console.log(file, 'file');
|
|
|
+
|
|
|
+ const type = file.type.includes('image')
|
|
|
+ ? NaturalTypeEnum.IMG
|
|
|
+ : file.type.includes('audio')
|
|
|
+ ? NaturalTypeEnum.SONG
|
|
|
+ : NaturalTypeEnum.VIDEO;
|
|
|
+
|
|
|
+ const size = type === 'IMG' ? 2 : type === 'SONG' ? 20 : 500;
|
|
|
+ if (size) {
|
|
|
+ isLt2M = file.file.size / 1024 / 1024 < size;
|
|
|
+ if (!isLt2M) {
|
|
|
+ message.error(`文件大小不能超过${size}M`);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!isLt2M) {
|
|
|
+ return isLt2M;
|
|
|
+ }
|
|
|
+ // 是否裁切
|
|
|
+ if (props.cropper && type === 'IMG') {
|
|
|
+ getBase64(file.file, (imageUrl: any) => {
|
|
|
+ const target = Object.assign({}, props.options, {
|
|
|
+ img: imageUrl,
|
|
|
+ name: file.file.name // 上传文件名
|
|
|
+ });
|
|
|
+ visiable.value = true;
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ CropperModal.value?.edit(target);
|
|
|
+ // console.log(CropperModal.value, 'cropper');
|
|
|
+ }, 100);
|
|
|
+ });
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ btnLoading.value = true;
|
|
|
+ const name = file.file.name;
|
|
|
+ const suffix = name.slice(name.lastIndexOf('.'));
|
|
|
+ const fileName = `${props.path}${
|
|
|
+ props.fileName || Date.now() + suffix
|
|
|
+ }`;
|
|
|
+ const obj = {
|
|
|
+ filename: fileName,
|
|
|
+ bucketName: props.bucketName,
|
|
|
+ postData: {
|
|
|
+ filename: fileName,
|
|
|
+ acl: 'public-read',
|
|
|
+ key: fileName,
|
|
|
+ unknowValueField: []
|
|
|
+ }
|
|
|
+ };
|
|
|
+ const { data } = await policy(obj);
|
|
|
+
|
|
|
+ state.policy = data.policy;
|
|
|
+ state.signature = data.signature;
|
|
|
+ state.key = fileName;
|
|
|
+ state.KSSAccessKeyId = data.kssAccessKeyId;
|
|
|
+ state.name = fileName;
|
|
|
+
|
|
|
+ tempFiileBuffer.value = file.file;
|
|
|
+ } catch {
|
|
|
+ //
|
|
|
+ // message.error('上传失败')
|
|
|
+ btnLoading.value = false;
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ };
|
|
|
+ const getBase64 = async (img: any, callback: any) => {
|
|
|
+ const reader = new FileReader();
|
|
|
+ reader.addEventListener('load', () => callback(reader.result));
|
|
|
+ reader.readAsDataURL(img);
|
|
|
+ };
|
|
|
+ const onFinish = async (options: any) => {
|
|
|
+ const url = ossUploadUrl + state.key;
|
|
|
+ const type = formatUrlType(url);
|
|
|
+ let coverImg = '';
|
|
|
+ if (type === 'IMG') {
|
|
|
+ coverImg = url;
|
|
|
+ } else if (type === 'SONG') {
|
|
|
+ coverImg = PageEnum.SONG_DEFAULT_COVER;
|
|
|
+ } else if (type === 'VIDEO') {
|
|
|
+ // 获取视频封面图
|
|
|
+ await getVideoCoverImg();
|
|
|
+ }
|
|
|
+ emit('finished', {
|
|
|
+ cover: coverImg,
|
|
|
+ content: url
|
|
|
+ });
|
|
|
+ emit('update:fileList', url);
|
|
|
+ emit('readFileInputEventAsArrayBuffer', tempFiileBuffer.value);
|
|
|
+ options.file.url = url;
|
|
|
+ visiable.value = false;
|
|
|
+ btnLoading.value = false;
|
|
|
+ };
|
|
|
+
|
|
|
+ const getVideoCoverImg = async () => {
|
|
|
+ try {
|
|
|
+ btnLoading.value = true;
|
|
|
+ const imgBlob: any = await getVideoMsg(tempFiileBuffer.value);
|
|
|
+ const fileName = `${props.path}${Date.now() + '.png'}`;
|
|
|
+ const obj = {
|
|
|
+ filename: fileName,
|
|
|
+ bucketName: props.bucketName,
|
|
|
+ postData: {
|
|
|
+ filename: fileName,
|
|
|
+ acl: 'public-read',
|
|
|
+ key: fileName,
|
|
|
+ unknowValueField: []
|
|
|
+ }
|
|
|
+ };
|
|
|
+ const { data } = await policy(obj);
|
|
|
+
|
|
|
+ const fileParams = {
|
|
|
+ policy: data.policy,
|
|
|
+ signature: data.signature,
|
|
|
+ key: fileName,
|
|
|
+ KSSAccessKeyId: data.KSSAccessKeyId,
|
|
|
+ name: fileName
|
|
|
+ } as any;
|
|
|
+ const formData = new FormData();
|
|
|
+ for (const key in fileParams) {
|
|
|
+ formData.append(key, fileParams[key]);
|
|
|
+ }
|
|
|
+ formData.append('file', imgBlob);
|
|
|
+
|
|
|
+ await axios.post(ossUploadUrl, formData);
|
|
|
+
|
|
|
+ const url = ossUploadUrl + state.key;
|
|
|
+ console.log(url, 'url');
|
|
|
+ return url;
|
|
|
+ } catch {
|
|
|
+ //
|
|
|
+ // message.error('上传失败')
|
|
|
+ // btnLoading.value = false;
|
|
|
+ // return false;
|
|
|
+ } finally {
|
|
|
+ btnLoading.value = false;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const onRemove = async (options: any) => {
|
|
|
+ emit('update:fileList', '');
|
|
|
+ emit('remove');
|
|
|
+ btnLoading.value = false;
|
|
|
+ };
|
|
|
+
|
|
|
+ // 裁切失败
|
|
|
+ // const cropperNo = () => {}
|
|
|
+ // 裁切成功
|
|
|
+ const cropperOk = async (blob: any) => {
|
|
|
+ try {
|
|
|
+ const fileName = `${props.path}${
|
|
|
+ props.fileName || new Date().getTime() + '.png'
|
|
|
+ }`;
|
|
|
+ const obj = {
|
|
|
+ filename: fileName,
|
|
|
+ bucketName: props.bucketName,
|
|
|
+ postData: {
|
|
|
+ filename: fileName,
|
|
|
+ acl: 'public-read',
|
|
|
+ key: fileName,
|
|
|
+ unknowValueField: []
|
|
|
+ }
|
|
|
+ };
|
|
|
+ const { data } = await policy(obj);
|
|
|
+
|
|
|
+ state.policy = data.policy;
|
|
|
+ state.signature = data.signature;
|
|
|
+ state.key = fileName;
|
|
|
+ state.KSSAccessKeyId = data.kssAccessKeyId;
|
|
|
+ state.name = fileName;
|
|
|
+
|
|
|
+ const formData = new FormData();
|
|
|
+ for (const key in state) {
|
|
|
+ formData.append(key, state[key]);
|
|
|
+ }
|
|
|
+ formData.append('file', blob);
|
|
|
+
|
|
|
+ await axios.post(ossUploadUrl, formData).then(() => {
|
|
|
+ const url = ossUploadUrl + state.key;
|
|
|
+ console.log(url, 'url');
|
|
|
+ const splitName = url.split('/');
|
|
|
+ fileListRef.value = [
|
|
|
+ {
|
|
|
+ id: new Date().getTime().toString(),
|
|
|
+ name: splitName[splitName.length - 1],
|
|
|
+ status: 'finished',
|
|
|
+ url: url
|
|
|
+ }
|
|
|
+ ];
|
|
|
+ emit('update:fileList', url);
|
|
|
+ visiable.value = false;
|
|
|
+ });
|
|
|
+ } catch {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ return () => (
|
|
|
+ <div class={styles.uploadFile}>
|
|
|
+ <NUpload
|
|
|
+ ref={uploadRef}
|
|
|
+ action={ossUploadUrl}
|
|
|
+ data={state}
|
|
|
+ v-model:fileList={fileListRef.value}
|
|
|
+ accept={props.accept}
|
|
|
+ multiple={props.multiple}
|
|
|
+ max={props.max}
|
|
|
+ disabled={props.disabled}
|
|
|
+ showFileList={props.showFileList}
|
|
|
+ showPreviewButton
|
|
|
+ onBeforeUpload={(options: any) => onBeforeUpload(options)}
|
|
|
+ onFinish={(options: any) => onFinish(options)}
|
|
|
+ onRemove={(options: any) => onRemove(options)}>
|
|
|
+ {props.showType === 'default' && (
|
|
|
+ <div class={styles.uploadBtn}>
|
|
|
+ <img src={iconUploadAdd} class={styles.iconUploadAdd} />
|
|
|
+ <p>上传</p>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {props.showType === 'custom' && slots.custom && slots.custom()}
|
|
|
+ </NUpload>
|
|
|
+
|
|
|
+ <NModal
|
|
|
+ v-model:show={visiable.value}
|
|
|
+ preset="dialog"
|
|
|
+ showIcon={false}
|
|
|
+ class={['modalTitle background']}
|
|
|
+ title="上传图片"
|
|
|
+ style={{ width: '800px' }}>
|
|
|
+ {/* @cropper-no="error" @cropper-ok="success" */}
|
|
|
+ <Copper
|
|
|
+ ref={CropperModal}
|
|
|
+ onClose={() => (visiable.value = false)}
|
|
|
+ onCropperOk={cropperOk}
|
|
|
+ />
|
|
|
+ </NModal>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+});
|