|
@@ -0,0 +1,431 @@
|
|
|
|
+<template>
|
|
|
|
+ <el-dialog
|
|
|
|
+ v-model="visible"
|
|
|
|
+ :width="422"
|
|
|
|
+ :title="title"
|
|
|
|
+ :close-on-click-modal="false"
|
|
|
|
+ :close-on-press-escape="false"
|
|
|
|
+ @open="modalBeforeOpen"
|
|
|
|
+ >
|
|
|
|
+ <slot></slot>
|
|
|
|
+ <div class="import-box">
|
|
|
|
+ <div class="import-temp">
|
|
|
|
+ <span>模板下载:</span>
|
|
|
|
+ <div class="temp-btn">
|
|
|
|
+ <a v-if="downloadUrl" :href="downloadUrl" :download="dfilename">{{
|
|
|
|
+ dfilename
|
|
|
|
+ }}</a>
|
|
|
|
+ <el-button
|
|
|
|
+ v-else-if="downloadHandle"
|
|
|
|
+ type="primary" link
|
|
|
|
+ @click="downloadHandle"
|
|
|
|
+ >{{ dfilename }}</el-button
|
|
|
|
+ >
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="import-body">
|
|
|
|
+ <el-upload
|
|
|
|
+ v-if="visible"
|
|
|
|
+ ref="uploadRef"
|
|
|
|
+ drag
|
|
|
|
+ :action="uploadUrl"
|
|
|
|
+ :headers="headers"
|
|
|
|
+ :accept="accept"
|
|
|
|
+ :data="uploadDataDict"
|
|
|
|
+ :show-file-list="true"
|
|
|
|
+ :auto-upload="autoUpload"
|
|
|
|
+ :http-request="customRequest"
|
|
|
|
+ :disabled="disabled"
|
|
|
|
+ :before-upload="handleBeforeUpload"
|
|
|
|
+ :file-list="customFileList"
|
|
|
|
+ :on-change="handleFileChange"
|
|
|
|
+ :on-error="handleError"
|
|
|
|
+ :on-success="handleSuccess"
|
|
|
|
+ :limit="1"
|
|
|
|
+ >
|
|
|
|
+ <el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
|
|
|
+ <div class="el-upload__text">
|
|
|
|
+ 将文件拖到此处,或<em>点击上传</em>
|
|
|
|
+ </div>
|
|
|
|
+ </el-upload>
|
|
|
|
+ <p
|
|
|
|
+ v-if="result.message && !result.success"
|
|
|
|
+ class="tips-info tips-error"
|
|
|
|
+ >
|
|
|
|
+ {{ result.message }}
|
|
|
|
+ </p>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <template v-if="!autoUpload" #footer>
|
|
|
|
+ <el-button @click="close">取消</el-button>
|
|
|
|
+ <el-button
|
|
|
|
+ type="primary"
|
|
|
|
+ :disabled="loading || !canUpload"
|
|
|
|
+ @click="confirm"
|
|
|
|
+ >确认</el-button
|
|
|
|
+ >
|
|
|
|
+ </template>
|
|
|
|
+ </el-dialog>
|
|
|
|
+</template>
|
|
|
|
+
|
|
|
|
+<script setup lang="ts">
|
|
|
|
+ import { computed, ref } from 'vue';
|
|
|
|
+ import { fileMD5 } from '@/utils/md5';
|
|
|
|
+ import { ElMessage, ElMessageBox } from 'element-plus';
|
|
|
|
+ import type { UploadProps, UploadRequestOptions, UploadFile, UploadFiles } from 'element-plus';
|
|
|
|
+ import { UploadFilled } from '@element-plus/icons-vue';
|
|
|
|
+ import axios, { AxiosError } from 'axios';
|
|
|
|
+
|
|
|
|
+ import useModal from '@/hooks/modal';
|
|
|
|
+
|
|
|
|
+ defineOptions({
|
|
|
|
+ name: 'ImportDialog',
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ /* modal */
|
|
|
|
+ const { visible, open, close } = useModal();
|
|
|
|
+ defineExpose({ open, close });
|
|
|
|
+
|
|
|
|
+ interface PromiseFunc {
|
|
|
|
+ (): Promise<void>;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const props = withDefaults(
|
|
|
|
+ defineProps<{
|
|
|
|
+ title: string;
|
|
|
|
+ uploadUrl: string;
|
|
|
|
+ format?: string[];
|
|
|
|
+ uploadData?: Record<string, any>;
|
|
|
|
+ maxSize?: number;
|
|
|
|
+ uploadFilename?: string;
|
|
|
|
+ autoUpload?: boolean;
|
|
|
|
+ disabled?: boolean;
|
|
|
|
+ uploadFileAlias?: string;
|
|
|
|
+ downloadUrl?: string;
|
|
|
|
+ downloadFilename?: string;
|
|
|
|
+ downloadHandle?: PromiseFunc;
|
|
|
|
+ beforeSubmitHandle?: PromiseFunc;
|
|
|
|
+ successMessage?: string;
|
|
|
|
+ }>(),
|
|
|
|
+ {
|
|
|
|
+ title: '文件上传',
|
|
|
|
+ uploadUrl: '',
|
|
|
|
+ format: () => ['xls', 'xlsx'],
|
|
|
|
+ uploadData: () => {
|
|
|
|
+ return {};
|
|
|
|
+ },
|
|
|
|
+ maxSize: 20 * 1024 * 1024,
|
|
|
|
+ uploadFileAlias: 'file',
|
|
|
|
+ autoUpload: true,
|
|
|
|
+ disabled: false,
|
|
|
|
+ successMessage: '上传成功',
|
|
|
|
+ }
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ const emit = defineEmits([
|
|
|
|
+ 'uploading',
|
|
|
|
+ 'uploadError',
|
|
|
|
+ 'uploadSuccess',
|
|
|
|
+ 'validError',
|
|
|
|
+ ]);
|
|
|
|
+
|
|
|
|
+ const uploadRef = ref();
|
|
|
|
+ const canUpload = ref(false);
|
|
|
|
+ const uploadDataDict = ref({});
|
|
|
|
+ const headers = ref({ md5: '' });
|
|
|
|
+ const result = ref({ success: true, message: '' });
|
|
|
|
+ const loading = ref(false);
|
|
|
|
+ const customFileList = ref<UploadFile[]>([]);
|
|
|
|
+
|
|
|
|
+ const dfilename = computed(() => {
|
|
|
|
+ if (props.downloadFilename) return props.downloadFilename;
|
|
|
|
+ return props.downloadUrl ? props.downloadUrl.split('/').pop() : '';
|
|
|
|
+ });
|
|
|
|
+ const accept = computed(() => {
|
|
|
|
+ return props.format.map((el) => `.${el}`).join();
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ async function confirm() {
|
|
|
|
+ loading.value = true;
|
|
|
|
+ if (props.beforeSubmitHandle) {
|
|
|
|
+ let handleResult = true;
|
|
|
|
+ await props.beforeSubmitHandle().catch(() => {
|
|
|
|
+ handleResult = false;
|
|
|
|
+ });
|
|
|
|
+ if (!handleResult) return;
|
|
|
|
+ }
|
|
|
|
+ uploadRef.value?.submit();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ function checkFileFormat(fileType: string) {
|
|
|
|
+ const fileFormat = fileType.split('.').pop()?.toLocaleLowerCase();
|
|
|
|
+ return props.format.some((item) => item.toLocaleLowerCase() === fileFormat);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ function handleFileChange(uploadFile: UploadFile, uploadFiles: UploadFiles) {
|
|
|
|
+ if (uploadFiles.length) {
|
|
|
|
+ // Element Plus 的 onChange 会在文件状态改变时都触发,包括移除
|
|
|
|
+ // 这里只处理添加新文件的场景,移除由 el-upload 内部处理
|
|
|
|
+ if (uploadFile.status === 'ready') {
|
|
|
|
+ customFileList.value = [uploadFile];
|
|
|
|
+ } else if (uploadFile.status === 'success' || uploadFile.status === 'fail') {
|
|
|
|
+ // 如果是上传成功或失败,也更新列表以显示状态
|
|
|
|
+ const index = customFileList.value.findIndex(f => f.uid === uploadFile.uid);
|
|
|
|
+ if (index !== -1) {
|
|
|
|
+ customFileList.value.splice(index, 1, uploadFile);
|
|
|
|
+ } else {
|
|
|
|
+ customFileList.value = [uploadFile]; // 如果之前列表为空
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ // 清理旧的 result message
|
|
|
|
+ const currentFileInList = customFileList.value.find(f => f.uid === uploadFile.uid);
|
|
|
|
+ if (currentFileInList && currentFileInList.uid !== uploadFile.uid) { // 理论上这里 customFileList 只有一个文件
|
|
|
|
+ result.value = {
|
|
|
|
+ success: true,
|
|
|
|
+ message: '',
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ customFileList.value = [];
|
|
|
|
+ result.value = {
|
|
|
|
+ success: true,
|
|
|
|
+ message: '',
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+ canUpload.value = customFileList.value[0]?.status === 'ready';
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ async function handleBeforeUpload(file: File): Promise<boolean | File> {
|
|
|
|
+ uploadDataDict.value = {
|
|
|
|
+ ...props.uploadData,
|
|
|
|
+ filename: file.name,
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ if (file.size > props.maxSize) {
|
|
|
|
+ handleExceededSize();
|
|
|
|
+ return Promise.reject(result.value);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (!checkFileFormat(file.name)) {
|
|
|
|
+ handleFormatError();
|
|
|
|
+ return Promise.reject(result.value);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const md5 = await fileMD5(file);
|
|
|
|
+ headers.value.md5 = md5;
|
|
|
|
+
|
|
|
|
+ if (props.autoUpload) loading.value = true;
|
|
|
|
+
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ interface UploadResultType {
|
|
|
|
+ hasError: boolean;
|
|
|
|
+ failRecords: Array<{
|
|
|
|
+ msg: string;
|
|
|
|
+ lineNum: number;
|
|
|
|
+ }>;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ function customRequest(options: UploadRequestOptions): XMLHttpRequest {
|
|
|
|
+ const { onProgress, onError, onSuccess, file, data } = options;
|
|
|
|
+
|
|
|
|
+ const formData = new FormData();
|
|
|
|
+ const paramData: Record<string, any> = data || {};
|
|
|
|
+ Object.entries(paramData).forEach(([k, v]) => {
|
|
|
|
+ formData.append(k, v);
|
|
|
|
+ });
|
|
|
|
+ formData.append(props.uploadFileAlias, file as File);
|
|
|
|
+ emit('uploading');
|
|
|
|
+ const uploadController = new AbortController();
|
|
|
|
+
|
|
|
|
+ (
|
|
|
|
+ axios.post(option.action as string, formData, {
|
|
|
|
+ headers: option.headers,
|
|
|
|
+ signal: uploadController.signal,
|
|
|
|
+ onUploadProgress: ({ loaded, total }) => {
|
|
|
|
+ onProgress({ percent: Math.floor((100 * loaded) / (total || 0)) });
|
|
|
|
+ },
|
|
|
|
+ }) as Promise<UploadResultType>
|
|
|
|
+ )
|
|
|
|
+ .then((res) => {
|
|
|
|
+ // 所有excel导入的特殊处理
|
|
|
|
+ if (res.hasError) {
|
|
|
|
+ const failRecords = res.failRecords;
|
|
|
|
+ const message = failRecords
|
|
|
|
+ .map((item) =>
|
|
|
|
+ item.lineNum ? `第${item.lineNum}行:${item.msg}` : item.msg
|
|
|
|
+ )
|
|
|
|
+ .join('。');
|
|
|
|
+
|
|
|
|
+ onError({ data: { message } });
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ onSuccess(res);
|
|
|
|
+ })
|
|
|
|
+ .catch((error: AxiosError) => {
|
|
|
|
+ onError(error.response);
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ return {
|
|
|
|
+ abort: uploadController.abort,
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ function handleError(fileItem: FileItem) {
|
|
|
|
+ canUpload.value = false;
|
|
|
|
+ loading.value = false;
|
|
|
|
+ result.value = {
|
|
|
|
+ success: false,
|
|
|
|
+ message: fileItem.response.data?.message || '上传错误',
|
|
|
|
+ };
|
|
|
|
+ emit('uploadError', result.value);
|
|
|
|
+ }
|
|
|
|
+ function handleSuccess(response: any, uploadFile: UploadFile) {
|
|
|
|
+ canUpload.value = false;
|
|
|
|
+ loading.value = false;
|
|
|
|
+ result.value = {
|
|
|
|
+ success: true,
|
|
|
|
+ message: '上传成功!',
|
|
|
|
+ };
|
|
|
|
+ ElMessage.success(props.successMessage);
|
|
|
|
+ emit('uploadSuccess', {
|
|
|
|
+ ...result.value,
|
|
|
|
+ filename: uploadFile.name,
|
|
|
|
+ response: response,
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ function handleFormatError() {
|
|
|
|
+ const content = `只支持文件格式为${props.format.join('/')}`;
|
|
|
|
+ result.value = {
|
|
|
|
+ success: false,
|
|
|
|
+ message: content,
|
|
|
|
+ };
|
|
|
|
+ loading.value = false;
|
|
|
|
+ ElElMessage.error(content);
|
|
|
|
+ emit('validError', result.value);
|
|
|
|
+ }
|
|
|
|
+ function handleExceededSize() {
|
|
|
|
+ const content = `文件大小不能超过${Math.floor(
|
|
|
|
+ props.maxSize / (1024 * 1024)
|
|
|
|
+ )}M`;
|
|
|
|
+ result.value = {
|
|
|
|
+ success: false,
|
|
|
|
+ message: content,
|
|
|
|
+ };
|
|
|
|
+ loading.value = false;
|
|
|
|
+ ElElMessage.error(content);
|
|
|
|
+ emit('validError', result.value);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ function modalBeforeOpen() {
|
|
|
|
+ canUpload.value = false;
|
|
|
|
+ result.value = {
|
|
|
|
+ success: true,
|
|
|
|
+ message: '',
|
|
|
|
+ };
|
|
|
|
+ headers.value = { md5: '' };
|
|
|
|
+ loading.value = false;
|
|
|
|
+ uploadDataDict.value = {};
|
|
|
|
+ customFileList.value = [];
|
|
|
|
+ }
|
|
|
|
+</script>
|
|
|
|
+
|
|
|
|
+<style lang="less">
|
|
|
|
+ .import-box {
|
|
|
|
+ .import-temp {
|
|
|
|
+ display: flex;
|
|
|
|
+ justify-content: space-between;
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
+
|
|
|
|
+ > span {
|
|
|
|
+ flex-grow: 0;
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
+ height: 20px;
|
|
|
|
+ line-height: 20px;
|
|
|
|
+ display: block;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .temp-btn {
|
|
|
|
+ flex-grow: 2;
|
|
|
|
+ text-align: left;
|
|
|
|
+
|
|
|
|
+ > a {
|
|
|
|
+ flex-grow: 2;
|
|
|
|
+ line-height: 20px;
|
|
|
|
+ color: var(--color-primary);
|
|
|
|
+
|
|
|
|
+ &:hover {
|
|
|
|
+ text-decoration: underline;
|
|
|
|
+ opacity: 0.8;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ .arco-btn {
|
|
|
|
+ line-height: 20px;
|
|
|
|
+ height: auto;
|
|
|
|
+ padding: 0;
|
|
|
|
+ background: transparent;
|
|
|
|
+ border: none;
|
|
|
|
+
|
|
|
|
+ &:hover {
|
|
|
|
+ text-decoration: underline;
|
|
|
|
+ opacity: 0.8;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ .arco-upload-drag {
|
|
|
|
+ padding: 40px 0;
|
|
|
|
+ > div:first-child {
|
|
|
|
+ height: 54px;
|
|
|
|
+ background-image: url(assets/images/upload-icon.png);
|
|
|
|
+ background-size: auto 100%;
|
|
|
|
+ background-repeat: no-repeat;
|
|
|
|
+ background-position: center;
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
+ }
|
|
|
|
+ svg {
|
|
|
|
+ display: none;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .arco-upload-list-item {
|
|
|
|
+ margin-top: 8px !important;
|
|
|
|
+ background-color: var(--color-fill-1);
|
|
|
|
+ border-radius: var(--border-radius-small);
|
|
|
|
+ .arco-upload-list-item-operation {
|
|
|
|
+ margin: 0 12px;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .svg-icon {
|
|
|
|
+ vertical-align: -2px;
|
|
|
|
+ }
|
|
|
|
+ .arco-upload-list-item-file-icon {
|
|
|
|
+ margin-right: 6px;
|
|
|
|
+ color: inherit;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ &.arco-upload-list-item-error {
|
|
|
|
+ .arco-upload-list-item-file-icon {
|
|
|
|
+ color: var(--color-danger);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ .arco-upload-progress {
|
|
|
|
+ > * {
|
|
|
|
+ display: none;
|
|
|
|
+ }
|
|
|
|
+ .arco-upload-icon-success {
|
|
|
|
+ display: block;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .tips-info {
|
|
|
|
+ max-height: 100px;
|
|
|
|
+ overflow: hidden;
|
|
|
|
+ margin-top: 5px;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+</style>
|