|
@@ -0,0 +1,427 @@
|
|
|
+<template>
|
|
|
+ <a-modal
|
|
|
+ v-model:open="visible"
|
|
|
+ :width="422"
|
|
|
+ :title="title"
|
|
|
+ :mask-closable="false"
|
|
|
+ :esc-to-close="false"
|
|
|
+ >
|
|
|
+ <slot></slot>
|
|
|
+ <a-form :label-col="{ style: { width: '80px' } }" label-align="left">
|
|
|
+ <a-form-item label="选择文件">
|
|
|
+ <a-upload
|
|
|
+ v-if="visible"
|
|
|
+ ref="uploadRef"
|
|
|
+ :action="uploadUrl"
|
|
|
+ :headers="headers"
|
|
|
+ :accept="accept"
|
|
|
+ :data="uploadDataDict"
|
|
|
+ :show-upload-list="false"
|
|
|
+ :custom-request="customRequest"
|
|
|
+ :disabled="disabled || loading"
|
|
|
+ :before-upload="handleBeforeUpload"
|
|
|
+ @change="handleFileChange"
|
|
|
+ >
|
|
|
+ <a-button>
|
|
|
+ <template #icon><ImportOutlined /></template>点击导入
|
|
|
+ </a-button>
|
|
|
+ </a-upload>
|
|
|
+
|
|
|
+ <a-progress v-if="loading" :percent="uploadProgress" />
|
|
|
+
|
|
|
+ <template #help>
|
|
|
+ <p
|
|
|
+ v-if="result.message && !result.success"
|
|
|
+ class="tips-info tips-error"
|
|
|
+ >
|
|
|
+ {{ result.message }}
|
|
|
+ </p>
|
|
|
+ </template>
|
|
|
+ </a-form-item>
|
|
|
+ <a-form-item label="下载模板">
|
|
|
+ <a-button :loading="downloading" @click="onDownloadTemplate">
|
|
|
+ <template #icon><DownloadOutlined /></template>{{ downloadBtnTitle }}
|
|
|
+ </a-button>
|
|
|
+ </a-form-item>
|
|
|
+ </a-form>
|
|
|
+
|
|
|
+ <template v-if="!autoUpload" #footer>
|
|
|
+ <a-button @click="close">取消</a-button>
|
|
|
+ <a-button
|
|
|
+ type="primary"
|
|
|
+ :disabled="loading || !canUpload"
|
|
|
+ @click="confirm"
|
|
|
+ >确认</a-button
|
|
|
+ >
|
|
|
+ </template>
|
|
|
+ </a-modal>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { computed, ref, watch } from "vue";
|
|
|
+import { DownloadOutlined, ImportOutlined } from "@ant-design/icons-vue";
|
|
|
+import { message } from "ant-design-vue";
|
|
|
+import type { UploadProps } from "ant-design-vue";
|
|
|
+
|
|
|
+import type { AxiosError, AxiosProgressEvent } from "axios";
|
|
|
+import { request } from "@/utils/request";
|
|
|
+import { getFileMD5 } from "@/utils/crypto";
|
|
|
+
|
|
|
+import useModal from "@/hooks/useModal";
|
|
|
+import { downloadByUrl } from "@/utils/tool";
|
|
|
+
|
|
|
+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;
|
|
|
+ autoUpload?: boolean;
|
|
|
+ disabled?: boolean;
|
|
|
+ uploadFileAlias?: string;
|
|
|
+ downloadBtnTitle?: string;
|
|
|
+ downloadUrl?: string;
|
|
|
+ downloadFilename?: string;
|
|
|
+ downloadHandle?: PromiseFunc;
|
|
|
+ beforeSubmitHandle?: PromiseFunc;
|
|
|
+ }>(),
|
|
|
+ {
|
|
|
+ title: "文件上传",
|
|
|
+ uploadUrl: "",
|
|
|
+ format: () => ["xls", "xlsx"],
|
|
|
+ uploadData: () => {
|
|
|
+ return {};
|
|
|
+ },
|
|
|
+ maxSize: 20 * 1024 * 1024,
|
|
|
+ uploadFileAlias: "file",
|
|
|
+ autoUpload: true,
|
|
|
+ disabled: false,
|
|
|
+ downloadBtnTitle: "导入模板",
|
|
|
+ }
|
|
|
+);
|
|
|
+
|
|
|
+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 uploadProgress = ref(0);
|
|
|
+const curFileUid = ref("");
|
|
|
+
|
|
|
+const accept = computed(() => {
|
|
|
+ return props.format.map((el) => `.${el}`).join();
|
|
|
+});
|
|
|
+
|
|
|
+const downloading = ref(false);
|
|
|
+async function onDownloadTemplate() {
|
|
|
+ if (props.downloadHandle) {
|
|
|
+ if (downloading.value) return;
|
|
|
+ downloading.value = true;
|
|
|
+ await props.downloadHandle().catch(() => false);
|
|
|
+ downloading.value = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (props.downloadUrl) {
|
|
|
+ downloadByUrl(props.downloadUrl, props.downloadFilename || "");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+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);
|
|
|
+}
|
|
|
+
|
|
|
+const handleFileChange: UploadProps["onChange"] = ({ file, fileList }) => {
|
|
|
+ if (!fileList.length) {
|
|
|
+ curFileUid.value = "";
|
|
|
+ result.value = {
|
|
|
+ success: true,
|
|
|
+ message: "",
|
|
|
+ };
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (curFileUid.value !== file.uid) {
|
|
|
+ result.value = {
|
|
|
+ success: true,
|
|
|
+ message: "",
|
|
|
+ };
|
|
|
+ }
|
|
|
+ curFileUid.value = file.uid;
|
|
|
+ canUpload.value = file.status === "uploading";
|
|
|
+};
|
|
|
+
|
|
|
+const handleBeforeUpload: UploadProps["beforeUpload"] = async (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 getFileMD5(file);
|
|
|
+ headers.value.md5 = md5;
|
|
|
+
|
|
|
+ if (props.autoUpload) {
|
|
|
+ loading.value = true;
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+};
|
|
|
+
|
|
|
+interface UploadResultType {
|
|
|
+ hasError: boolean;
|
|
|
+ failRecords: Array<{
|
|
|
+ msg: string;
|
|
|
+ lineNum: number;
|
|
|
+ }>;
|
|
|
+}
|
|
|
+
|
|
|
+const customRequest: UploadProps["customRequest"] = (option) => {
|
|
|
+ const { file, data } = option;
|
|
|
+
|
|
|
+ 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");
|
|
|
+
|
|
|
+ (
|
|
|
+ request({
|
|
|
+ url: option.action as string,
|
|
|
+ data: formData,
|
|
|
+ headers: option.headers,
|
|
|
+ onUploadProgress: (data: AxiosProgressEvent) => {
|
|
|
+ uploadProgress.value = data.total
|
|
|
+ ? Math.floor((100 * data.loaded) / data.total)
|
|
|
+ : 0;
|
|
|
+ },
|
|
|
+ }) as Promise<UploadResultType>
|
|
|
+ )
|
|
|
+ .then((res) => {
|
|
|
+ // 所有excel导入的特殊处理
|
|
|
+ if (res.hasError) {
|
|
|
+ const failRecords = res.failRecords;
|
|
|
+ const message = failRecords
|
|
|
+ .map((item) => `第${item.lineNum}行:${item.msg}`)
|
|
|
+ .join("。");
|
|
|
+
|
|
|
+ handleError(message);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ handleSuccess(res);
|
|
|
+ })
|
|
|
+ .catch((error: AxiosError<{ message: string }> | null) => {
|
|
|
+ handleError(error?.response?.data?.message);
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+function handleError(message: string | undefined) {
|
|
|
+ canUpload.value = false;
|
|
|
+ loading.value = false;
|
|
|
+ result.value = {
|
|
|
+ success: false,
|
|
|
+ message: message || "上传错误",
|
|
|
+ };
|
|
|
+ emit("uploadError", result.value);
|
|
|
+}
|
|
|
+function handleSuccess(data: UploadResultType) {
|
|
|
+ canUpload.value = false;
|
|
|
+ loading.value = false;
|
|
|
+ result.value = {
|
|
|
+ success: true,
|
|
|
+ message: "上传成功!",
|
|
|
+ };
|
|
|
+ emit("uploadSuccess", {
|
|
|
+ ...result.value,
|
|
|
+ data,
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function handleFormatError() {
|
|
|
+ const content = `只支持文件格式为${props.format.join("/")}`;
|
|
|
+ result.value = {
|
|
|
+ success: false,
|
|
|
+ message: content,
|
|
|
+ };
|
|
|
+ loading.value = false;
|
|
|
+ emit("validError", result.value);
|
|
|
+}
|
|
|
+function handleExceededSize() {
|
|
|
+ const content = `文件大小不能超过${Math.floor(props.maxSize / 1024)}M`;
|
|
|
+ result.value = {
|
|
|
+ success: false,
|
|
|
+ message: content,
|
|
|
+ };
|
|
|
+ loading.value = false;
|
|
|
+ emit("validError", result.value);
|
|
|
+}
|
|
|
+
|
|
|
+/* init modal */
|
|
|
+watch(
|
|
|
+ () => visible.value,
|
|
|
+ (val) => {
|
|
|
+ if (val) {
|
|
|
+ modalOpenHandle();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ immediate: true,
|
|
|
+ }
|
|
|
+);
|
|
|
+
|
|
|
+function modalOpenHandle() {
|
|
|
+ canUpload.value = false;
|
|
|
+ result.value = {
|
|
|
+ success: true,
|
|
|
+ message: "",
|
|
|
+ };
|
|
|
+ headers.value = { md5: "" };
|
|
|
+ loading.value = false;
|
|
|
+ uploadDataDict.value = {};
|
|
|
+ curFileUid.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>
|