|
|
@@ -0,0 +1,354 @@
|
|
|
+<template>
|
|
|
+ <div>
|
|
|
+ <div class="flex upload-toolbar">
|
|
|
+ <Upload
|
|
|
+ :accept="getStringAccept"
|
|
|
+ :multiple="multiple"
|
|
|
+ :before-upload="beforeUpload"
|
|
|
+ :show-upload-list="false"
|
|
|
+ class="upload-toolbar__btn"
|
|
|
+ >
|
|
|
+ <a-button>
|
|
|
+ <upload-outlined />
|
|
|
+ 上传文件
|
|
|
+ </a-button>
|
|
|
+ </Upload>
|
|
|
+ <Alert :message="getHelpText" type="info" banner class="upload-toolbar__text" />
|
|
|
+ </div>
|
|
|
+ <div class="upload-filelist">
|
|
|
+ <!-- <div>已上传</div> -->
|
|
|
+ <div class="upload-filelist__item" v-for="item in fileList" :key="item.id">
|
|
|
+ <div>
|
|
|
+ <paper-clip-outlined class="mr-1" />
|
|
|
+ <span class="mr-2">{{ item.realName }}</span>
|
|
|
+ <span class="color-muted">({{ item.size }}kb)</span>
|
|
|
+ </div>
|
|
|
+ <div class="flex">
|
|
|
+ <!-- <span class="mr-2">30%</span> -->
|
|
|
+ <!-- <a :href="item.absolutePath" target="_blank" class="mr-2">预览</a> -->
|
|
|
+ <span
|
|
|
+ class="upload-filelist--color pointer"
|
|
|
+ @click="handlePreview(item)"
|
|
|
+ :title="'预览-' + item.realName"
|
|
|
+ >预览</span
|
|
|
+ >
|
|
|
+ <span class="upload-filelist--color pointer" @click="handleRemove(item, 1)">删除</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!-- <div v-if="fileListRef.length" @click="handleStartUpload">未上传</div> -->
|
|
|
+ <div class="upload-filelist">
|
|
|
+ <div class="upload-filelist__item" v-for="item in fileListRef" :key="item.uuid">
|
|
|
+ <div class="upload-filelist__item-info">
|
|
|
+ <paper-clip-outlined class="mr-1" />
|
|
|
+ <span class="mr-2">{{ item.name }}</span>
|
|
|
+ <span>({{ item.size }}kb)</span>
|
|
|
+ </div>
|
|
|
+ <div class="upload-filelist__item-progress">
|
|
|
+ <a-progress
|
|
|
+ :percent="item.percent"
|
|
|
+ size="small"
|
|
|
+ :status="setProgressSataus(item.status)"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="flex">
|
|
|
+ <!-- <span class="mr-2">30%</span> -->
|
|
|
+ <!-- <a :href="item.absolutePath" target="_blank" class="mr-2">预览</a> -->
|
|
|
+ <span class="upload-filelist--error" v-if="item.status == 'error'">上传失败</span>
|
|
|
+ <span class="upload-filelist--color" @click="handleRemove(item, 0)">删除</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!-- <FileList :dataSource="fileListRef" /> -->
|
|
|
+ <UploadPreviewModal @register="registerPreviewModal" />
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+<script lang="ts">
|
|
|
+ import { UploadOutlined, PaperClipOutlined } from '@ant-design/icons-vue';
|
|
|
+ import { PropType, defineComponent, reactive, ref, toRefs, unref, watch } from 'vue';
|
|
|
+ import { Upload, Alert } from 'ant-design-vue';
|
|
|
+ import { uploadContainerProps } from './props';
|
|
|
+ import { FileItem, UploadResultStatus } from './typing';
|
|
|
+ import { useModal } from '/@/components/Modal';
|
|
|
+ // hooks
|
|
|
+ import { useUploadType } from './useUpload';
|
|
|
+ import { useMessage } from '/@/hooks/web/useMessage';
|
|
|
+ // utils
|
|
|
+ import { isFunction, isArray } from '/@/utils/is';
|
|
|
+ import { warn } from '/@/utils/log';
|
|
|
+ // import FileList from './FileList.vue';
|
|
|
+ import { nanoid } from 'nanoid';
|
|
|
+ import locales from '/@/utils/locales';
|
|
|
+ import { getPreviewUrl, postFileRemoveByIds } from '/@/api/common';
|
|
|
+ import UploadPreviewModal from './UploadPreviewModal.vue';
|
|
|
+
|
|
|
+ export default defineComponent({
|
|
|
+ name: 'XTUpload',
|
|
|
+ components: {
|
|
|
+ UploadOutlined,
|
|
|
+ PaperClipOutlined,
|
|
|
+ Upload,
|
|
|
+ Alert,
|
|
|
+ UploadPreviewModal,
|
|
|
+ // FileList,
|
|
|
+ },
|
|
|
+ props: {
|
|
|
+ ...uploadContainerProps,
|
|
|
+ previewFileList: {
|
|
|
+ type: Array as PropType<string[]>,
|
|
|
+ default: () => [],
|
|
|
+ },
|
|
|
+ },
|
|
|
+ emits: ['change', 'delete'],
|
|
|
+ setup(props, { emit }) {
|
|
|
+ const state = reactive<{ fileList: FileItem[] }>({
|
|
|
+ fileList: [],
|
|
|
+ });
|
|
|
+ // 预览modal
|
|
|
+ const [registerPreviewModal, { openModal: openPreviewModal }] = useModal();
|
|
|
+
|
|
|
+ // 是否正在上传
|
|
|
+ const isUploadingRef = ref(false);
|
|
|
+ const fileList = ref<any[]>([]);
|
|
|
+ const fileListRef = ref<FileItem[]>([]);
|
|
|
+ const previewFile = ref<any>({});
|
|
|
+ const { accept, helpText, maxNumber, maxSize } = toRefs(props);
|
|
|
+
|
|
|
+ const { getStringAccept, getHelpText } = useUploadType({
|
|
|
+ acceptRef: accept,
|
|
|
+ helpTextRef: helpText,
|
|
|
+ maxNumberRef: maxNumber,
|
|
|
+ maxSizeRef: maxSize,
|
|
|
+ });
|
|
|
+
|
|
|
+ const { createMessage } = useMessage();
|
|
|
+
|
|
|
+ watch(
|
|
|
+ () => props.value,
|
|
|
+ (value = []) => {
|
|
|
+ fileList.value = isArray(value) ? value : [];
|
|
|
+ console.log('🚀 ~ file: XTUpload.vue:80 ~ setup ~ fileList.value :', fileList.value);
|
|
|
+ },
|
|
|
+ { immediate: true },
|
|
|
+ );
|
|
|
+
|
|
|
+ // 上传前校验
|
|
|
+ async function beforeUpload(file: File) {
|
|
|
+ const { size, name } = file;
|
|
|
+ const { maxSize } = props;
|
|
|
+ // 设置最大值,则判断
|
|
|
+ if (maxSize && file.size / 1024 / 1024 >= maxSize) {
|
|
|
+ createMessage.error(`只能上传不超过${maxSize}MB的文件!`);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ const commonItem = {
|
|
|
+ uuid: nanoid(5),
|
|
|
+ file,
|
|
|
+ size,
|
|
|
+ name,
|
|
|
+ percent: 0,
|
|
|
+ type: name.split('.').pop(),
|
|
|
+ };
|
|
|
+ // 生成文件列表
|
|
|
+ fileListRef.value = [...unref(fileListRef), commonItem];
|
|
|
+ console.log(
|
|
|
+ '🚀 ~ file: XTUpload.vue:114 ~ beforeUpload ~ fileListRef.value:',
|
|
|
+ fileListRef.value,
|
|
|
+ );
|
|
|
+ await handleStartUpload();
|
|
|
+ return false;
|
|
|
+ // return false 会停止自动上传
|
|
|
+ }
|
|
|
+ // 删除
|
|
|
+ async function handleRemove(record, bool) {
|
|
|
+ if (bool) {
|
|
|
+ const index = fileList.value.findIndex(item => item.id === record.id);
|
|
|
+ index !== -1 && fileList.value.splice(index, 1);
|
|
|
+ emit('delete', record);
|
|
|
+ await postFileRemoveByIds([record.id]);
|
|
|
+ } else {
|
|
|
+ const index = fileListRef.value.findIndex(item => item.uuid === record.uuid);
|
|
|
+ index !== -1 && fileListRef.value.splice(index, 1);
|
|
|
+ emit('delete', record);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 预览
|
|
|
+ async function handlePreview(record) {
|
|
|
+ // console.log('🚀 ~ file: XTUpload.vue:147 ~ handlePreview ~ record:', record);
|
|
|
+ const res = await getPreviewUrl(record.id);
|
|
|
+ console.log('预览数据', { ...record, previewUrl: res });
|
|
|
+ previewFile.value = {
|
|
|
+ ...record,
|
|
|
+ previewUrl: res,
|
|
|
+ };
|
|
|
+ openPreviewModal(true, {
|
|
|
+ ...record,
|
|
|
+ previewUrl: res,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ // 进度状态渲染
|
|
|
+ function setProgressSataus(uploadStatus) {
|
|
|
+ let status: 'normal' | 'exception' | 'active' | 'success' = 'normal';
|
|
|
+ if (uploadStatus === UploadResultStatus.ERROR) {
|
|
|
+ status = 'exception';
|
|
|
+ } else if (uploadStatus === UploadResultStatus.UPLOADING) {
|
|
|
+ status = 'active';
|
|
|
+ } else if (uploadStatus === UploadResultStatus.SUCCESS) {
|
|
|
+ status = 'success';
|
|
|
+ }
|
|
|
+ return status;
|
|
|
+ }
|
|
|
+ async function uploadApiByItem(item: FileItem) {
|
|
|
+ const { api } = props;
|
|
|
+ if (!api || !isFunction(api)) {
|
|
|
+ return warn('upload api must exist and be a function');
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ // eslint-disable-next-line no-unsafe-optional-chaining
|
|
|
+ const { data } = await props?.api?.(
|
|
|
+ {
|
|
|
+ data: {
|
|
|
+ ...(props.uploadParams || {}),
|
|
|
+ },
|
|
|
+ file: item.file,
|
|
|
+ name: props.name,
|
|
|
+ filename: props.filename,
|
|
|
+ },
|
|
|
+ function onUploadProgress(progressEvent: ProgressEvent) {
|
|
|
+ const complete = ((progressEvent.loaded / progressEvent.total) * 100) | 0;
|
|
|
+ item.percent = complete;
|
|
|
+ },
|
|
|
+ );
|
|
|
+ item.status = UploadResultStatus.SUCCESS;
|
|
|
+ item.responseData = data;
|
|
|
+ console.log(
|
|
|
+ '🚀 ~ file: XTUpload.vue:191 ~ handleStartUpload ~ fileListRef.value:',
|
|
|
+ fileListRef.value,
|
|
|
+ );
|
|
|
+ fileListRef.value.forEach(ele => {
|
|
|
+ const responseData = ele.responseData;
|
|
|
+ if (responseData.code == '00000') {
|
|
|
+ fileList.value.push(responseData.data);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ fileListRef.value = fileListRef.value.filter(ele => {
|
|
|
+ return ele.status != UploadResultStatus.SUCCESS;
|
|
|
+ });
|
|
|
+ return {
|
|
|
+ success: true,
|
|
|
+ error: null,
|
|
|
+ };
|
|
|
+ } catch (e) {
|
|
|
+ console.log(e);
|
|
|
+ item.status = UploadResultStatus.ERROR;
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ error: e,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 点击开始上传
|
|
|
+ async function handleStartUpload() {
|
|
|
+ const { maxNumber } = props;
|
|
|
+ if ((fileListRef.value.length + fileList.value.length ?? 0) > maxNumber) {
|
|
|
+ return createMessage.warning(locales.upload.maxNumber, maxNumber);
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ isUploadingRef.value = true;
|
|
|
+ // 只上传不是成功状态的
|
|
|
+ const uploadFileList =
|
|
|
+ fileListRef.value.filter(item => item.status !== UploadResultStatus.SUCCESS) || [];
|
|
|
+ const data = await Promise.all(
|
|
|
+ uploadFileList.map(item => {
|
|
|
+ const result = uploadApiByItem(item);
|
|
|
+ return result;
|
|
|
+ }),
|
|
|
+ );
|
|
|
+ isUploadingRef.value = false;
|
|
|
+ // 生产环境:抛出错误
|
|
|
+ const errorList = data.filter((item: any) => !item.success);
|
|
|
+ if (errorList.length > 0) throw errorList;
|
|
|
+ } catch (e) {
|
|
|
+ isUploadingRef.value = false;
|
|
|
+ throw e;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ getHelpText,
|
|
|
+ getStringAccept,
|
|
|
+ beforeUpload,
|
|
|
+ fileListRef,
|
|
|
+ state,
|
|
|
+ isUploadingRef,
|
|
|
+ handleStartUpload,
|
|
|
+ locales,
|
|
|
+ fileList,
|
|
|
+ handleRemove,
|
|
|
+ handlePreview,
|
|
|
+ previewFile,
|
|
|
+ registerPreviewModal,
|
|
|
+ setProgressSataus,
|
|
|
+ };
|
|
|
+ },
|
|
|
+ });
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="less" scoped>
|
|
|
+ .upload {
|
|
|
+ .ant-upload-list {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ant-table-wrapper .ant-spin-nested-loading {
|
|
|
+ padding: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ &-toolbar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 8px;
|
|
|
+
|
|
|
+ &__btn {
|
|
|
+ text-align: left;
|
|
|
+ margin-right: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__text {
|
|
|
+ height: 32px;
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &-filelist {
|
|
|
+ &__item {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ background-color: rgb(241 247 255 / 100%);
|
|
|
+ margin-bottom: 4px;
|
|
|
+ padding: 5px 8px;
|
|
|
+
|
|
|
+ &-info {
|
|
|
+ max-width: 220px;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ }
|
|
|
+
|
|
|
+ &-progress {
|
|
|
+ min-width: 180px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &--color {
|
|
|
+ color: rgb(0 117 255 / 100%);
|
|
|
+ margin-left: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &--error {
|
|
|
+ color: rgb(255 72 4 / 100%);
|
|
|
+ margin-left: 16px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+</style>
|