|
|
@@ -0,0 +1,1186 @@
|
|
|
+<template>
|
|
|
+ <div :class="prefixCls" class="case-upload-page">
|
|
|
+ <div class="page-header">
|
|
|
+ <div class="page-header__left">
|
|
|
+ <Button type="link" class="back-btn" @click="handleBack">
|
|
|
+ <template #icon><LeftOutlined /></template>
|
|
|
+ 返回
|
|
|
+ </Button>
|
|
|
+ <div>
|
|
|
+ <div class="page-title">文件上传</div>
|
|
|
+ <div class="page-subtitle">上传并解析 xls / xlsx / csv 文件,解析结果实时展示在右侧</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="page-header__actions">
|
|
|
+ <Button type="primary" class="next-btn" :disabled="!hasSuccessResult" @click="handleGoMatch">
|
|
|
+ 下一步:数据匹配
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="upload-layout">
|
|
|
+ <aside class="left-panel">
|
|
|
+ <div class="upload-picker">
|
|
|
+ <div class="upload-mode-switch" role="tablist" aria-label="上传类型切换">
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ :class="['upload-mode-switch__item', { 'is-active': uploadMode === 'single' }]"
|
|
|
+ :disabled="isUploading"
|
|
|
+ @click="handleUploadModeChange('single')"
|
|
|
+ >
|
|
|
+ 选择文件
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ :class="['upload-mode-switch__item', { 'is-active': uploadMode === 'folder' }]"
|
|
|
+ :disabled="isUploading"
|
|
|
+ @click="handleUploadModeChange('folder')"
|
|
|
+ >
|
|
|
+ 选择文件夹
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 隐藏的文件输入 -->
|
|
|
+ <input
|
|
|
+ ref="fileInputRef"
|
|
|
+ type="file"
|
|
|
+ multiple
|
|
|
+ accept=".xls,.xlsx,.csv"
|
|
|
+ style="display: none"
|
|
|
+ @change="handleFileInputChange"
|
|
|
+ />
|
|
|
+ <input
|
|
|
+ ref="folderInputRef"
|
|
|
+ type="file"
|
|
|
+ webkitdirectory
|
|
|
+ directory
|
|
|
+ style="display: none"
|
|
|
+ @change="handleFolderInputChange"
|
|
|
+ />
|
|
|
+
|
|
|
+ <div
|
|
|
+ class="upload-dropzone"
|
|
|
+ :class="{ 'is-dragover': dragOver }"
|
|
|
+ @dragover.prevent="handleDragOver"
|
|
|
+ @dragleave="handleDragLeave"
|
|
|
+ @drop.prevent="handleDrop"
|
|
|
+ >
|
|
|
+ <div class="upload-dragger-content">
|
|
|
+ <div class="upload-dragger-icon">⬆</div>
|
|
|
+ <div class="upload-dragger-title">
|
|
|
+ {{ uploadMode === 'single' ? '拖拽文件到这里,或点击选择文件' : '拖拽文件夹到这里,或点击选择文件夹' }}
|
|
|
+ </div>
|
|
|
+ <div class="upload-dragger-desc">
|
|
|
+ {{
|
|
|
+ uploadMode === 'single'
|
|
|
+ ? '支持批量选择 xls / xlsx / csv 文件,同一批次统一解析'
|
|
|
+ : '支持选择整个文件夹,系统会自动读取符合条件的文件'
|
|
|
+ }}
|
|
|
+ </div>
|
|
|
+ <Button type="primary" class="upload-action-btn" :disabled="isUploading" @click="handleTriggerFileSelect">
|
|
|
+ {{ uploadMode === 'single' ? '选择文件' : '选择文件夹' }}
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 已选文件列表(加入后自动上传,成功即移除) -->
|
|
|
+ <div v-if="selectedFiles.length" class="upload-file-list">
|
|
|
+ <div class="upload-file-list__header">
|
|
|
+ <span>待上传 {{ selectedFiles.length }} 个文件</span>
|
|
|
+ <Button type="link" size="small" :disabled="isUploading" @click="clearSelectedFiles"> 清空 </Button>
|
|
|
+ </div>
|
|
|
+ <div class="upload-file-list__body">
|
|
|
+ <div
|
|
|
+ v-for="file in selectedFiles"
|
|
|
+ :key="file.name + file.size + file.lastModified"
|
|
|
+ class="upload-file-item"
|
|
|
+ >
|
|
|
+ <div class="upload-file-item__info">
|
|
|
+ <span class="upload-file-item__name" :title="file.name">{{ file.name }}</span>
|
|
|
+ <span class="upload-file-item__size">{{ formatFileSize(file.size) }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="upload-file-item__status">
|
|
|
+ <template v-if="uploadStatuses.get(file) === 'error'">
|
|
|
+ <span class="upload-file-item__error">✗</span>
|
|
|
+ </template>
|
|
|
+ <template v-else-if="uploadStatuses.get(file) === 'uploading'">
|
|
|
+ <span class="upload-file-item__progress-text">{{ uploadProgressMap.get(file) || 0 }}%</span>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <span class="upload-file-item__pending">待上传</span>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ <button
|
|
|
+ v-if="!isUploading && (uploadStatuses.get(file) || 'pending') === 'pending'"
|
|
|
+ type="button"
|
|
|
+ class="upload-file-item__remove"
|
|
|
+ :title="`移除 ${file.name}`"
|
|
|
+ aria-label="移除文件"
|
|
|
+ @click="removeSelectedFile(file)"
|
|
|
+ >
|
|
|
+ <CloseOutlined />
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!-- 整体进度 -->
|
|
|
+ <div v-if="isUploading" class="upload-overall-progress">
|
|
|
+ <div class="upload-overall-progress__bar">
|
|
|
+ <div class="upload-overall-progress__fill" :style="{ width: overallProgress + '%' }"></div>
|
|
|
+ </div>
|
|
|
+ <span class="upload-overall-progress__text">总进度 {{ overallProgress }}%</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-else class="upload-file-list upload-file-list--empty">
|
|
|
+ <div class="upload-empty">暂无待上传文件,选择文件后将自动上传解析</div>
|
|
|
+ </div>
|
|
|
+ </aside>
|
|
|
+
|
|
|
+ <section class="right-panel">
|
|
|
+ <div class="panel-header">
|
|
|
+ <div>
|
|
|
+ <div class="panel-title">解析结果(实时)</div>
|
|
|
+ <div class="panel-desc">每完成一份文件解析,结果树会实时追加到下方</div>
|
|
|
+ </div>
|
|
|
+ <div class="panel-stats">已解析 {{ resultTrees.length }} 个文件</div>
|
|
|
+ </div>
|
|
|
+ <div class="table-wrapper">
|
|
|
+ <Table
|
|
|
+ class="result-tree-table"
|
|
|
+ :columns="resultTableColumns"
|
|
|
+ :data-source="resultTrees"
|
|
|
+ :loading="tableLoading"
|
|
|
+ :pagination="false"
|
|
|
+ row-key="id"
|
|
|
+ :children-column-name="'children'"
|
|
|
+ size="small"
|
|
|
+ :scroll="{ x: 760 }"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script lang="ts" setup name="ViewsDataUpload">
|
|
|
+ import { computed, h, ref } from 'vue';
|
|
|
+ import { onBeforeRouteLeave } from 'vue-router';
|
|
|
+ import { CloseOutlined, LeftOutlined } from '@ant-design/icons-vue';
|
|
|
+ import { Button, Modal, Table, type TableColumnsType } from 'ant-design-vue';
|
|
|
+ import { useDesign } from '@/core/hooks/web/useDesign';
|
|
|
+ import { useGo, useQuery } from '@/core/hooks/web/usePage';
|
|
|
+ import { PageEnum } from '@/core/enums/pageEnum';
|
|
|
+ import { useMessage } from '@/core/hooks/web/useMessage';
|
|
|
+ import { useEmitter } from '@/core/store/modules/case';
|
|
|
+ import { clearUploadBatch, preFileUpload } from '@/case/api/govern/governApi';
|
|
|
+ import { FileStatusEnum } from '@/case/types/enum';
|
|
|
+ import type { FileInfo } from '@/types/api';
|
|
|
+
|
|
|
+ const { prefixCls } = useDesign('case-upload');
|
|
|
+ const go = useGo();
|
|
|
+ const pageQuery = useQuery();
|
|
|
+ const emitter = useEmitter();
|
|
|
+ const { createMessage } = useMessage();
|
|
|
+ const FILE_INFO_STORAGE_KEY = 'case_clean_file_infos';
|
|
|
+ const CASE_DATA_REFRESH_EVENT = 'case-data:refresh';
|
|
|
+ const CASE_IMPORT_FILE_REFRESH_EVENT = 'case-import-file:refresh';
|
|
|
+ const VALID_UPLOAD_EXTS = ['.xls', '.xlsx', '.csv'];
|
|
|
+
|
|
|
+ type UploadMode = 'single' | 'folder';
|
|
|
+
|
|
|
+ /** 进入上传页即生成批次 id,本页所有文件统一使用该批次(Long 型,上限约 1.7e18,安全) */
|
|
|
+ const createUploadBatchId = () => Date.now() * 1000000 + Math.floor(Math.random() * 1000000);
|
|
|
+
|
|
|
+ const uploadBatchId = ref(createUploadBatchId());
|
|
|
+ const modeFromQuery = computed<UploadMode>(() => (pageQuery.value?.mode === 'folder' ? 'folder' : 'single'));
|
|
|
+ const uploadMode = ref<UploadMode>(modeFromQuery.value);
|
|
|
+
|
|
|
+ // 文件选择与上传状态(以 File 对象为键,上传成功移除文件时状态映射不会错乱)
|
|
|
+ const fileInputRef = ref<HTMLInputElement | null>(null);
|
|
|
+ const folderInputRef = ref<HTMLInputElement | null>(null);
|
|
|
+ const selectedFiles = ref<File[]>([]);
|
|
|
+ const uploadProgressMap = ref<Map<File, number>>(new Map());
|
|
|
+ const uploadStatuses = ref<Map<File, 'pending' | 'uploading' | 'done' | 'error'>>(new Map());
|
|
|
+ const isUploading = ref(false);
|
|
|
+ const overallProgress = ref(0);
|
|
|
+ const dragOver = ref(false);
|
|
|
+
|
|
|
+ // 解析结果(右侧 treetable 数据源,实时追加)
|
|
|
+ const resultTrees = ref<FileInfo[]>([]);
|
|
|
+ const allFileInfos = ref<Recordable[]>([]);
|
|
|
+ const tableLoading = ref(false);
|
|
|
+
|
|
|
+ // 离开放行标记:点「下一步」或已确认清理后的程序化跳转置为 true
|
|
|
+ const allowedToLeave = ref(false);
|
|
|
+ // 放弃标记:确认离开后中断上传循环,避免组件卸载后继续写结果
|
|
|
+ const isAbandoned = ref(false);
|
|
|
+
|
|
|
+ const batchIdReady = computed(() => !!uploadBatchId.value);
|
|
|
+ const hasActivity = computed(
|
|
|
+ () => isUploading.value || selectedFiles.value.length > 0 || resultTrees.value.length > 0,
|
|
|
+ );
|
|
|
+ const hasSuccessResult = computed(() =>
|
|
|
+ resultTrees.value.some(
|
|
|
+ (root) => isSuccessStatus(root.fileStatus) || (root.children || []).some((s) => isSuccessStatus(s.fileStatus)),
|
|
|
+ ),
|
|
|
+ );
|
|
|
+
|
|
|
+ const formatFileSize = (bytes?: number | null) => {
|
|
|
+ if (bytes == null || bytes === 0) return '0 B';
|
|
|
+ const k = 1024;
|
|
|
+ const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
|
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
|
+ };
|
|
|
+
|
|
|
+ const isValidUploadFile = (file: File) => {
|
|
|
+ const name = file.name.toLowerCase();
|
|
|
+ return VALID_UPLOAD_EXTS.some((ext) => name.endsWith(ext));
|
|
|
+ };
|
|
|
+
|
|
|
+ const filterValidFiles = (files: File[]) => {
|
|
|
+ const valid = files.filter(isValidUploadFile);
|
|
|
+ const invalid = files.filter((f) => !isValidUploadFile(f));
|
|
|
+ if (invalid.length) {
|
|
|
+ createMessage.warning(`已过滤 ${invalid.length} 个不支持的文件,仅支持 xls / xlsx / csv`);
|
|
|
+ }
|
|
|
+ return valid;
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleDragOver = () => {
|
|
|
+ dragOver.value = true;
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleDragLeave = () => {
|
|
|
+ dragOver.value = false;
|
|
|
+ };
|
|
|
+
|
|
|
+ const traverseDirectory = async (entry: FileSystemDirectoryEntry): Promise<File[]> => {
|
|
|
+ const files: File[] = [];
|
|
|
+ const reader = entry.createReader();
|
|
|
+ const getEntries = (): Promise<FileSystemEntry[]> => new Promise((resolve) => reader.readEntries(resolve));
|
|
|
+ let entries = await getEntries();
|
|
|
+ while (entries.length) {
|
|
|
+ for (const e of entries) {
|
|
|
+ if (e.isFile) {
|
|
|
+ const file = await new Promise<File>((resolve) => (e as FileSystemFileEntry).file(resolve));
|
|
|
+ files.push(file);
|
|
|
+ } else if (e.isDirectory) {
|
|
|
+ files.push(...(await traverseDirectory(e as FileSystemDirectoryEntry)));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ entries = await getEntries();
|
|
|
+ }
|
|
|
+ return files;
|
|
|
+ };
|
|
|
+
|
|
|
+ const collectFilesFromDataTransfer = async (items: DataTransferItemList): Promise<File[]> => {
|
|
|
+ const files: File[] = [];
|
|
|
+ for (let i = 0; i < items.length; i++) {
|
|
|
+ const item = items[i];
|
|
|
+ const entry = item.webkitGetAsEntry?.();
|
|
|
+ if (!entry) {
|
|
|
+ const file = item.getAsFile();
|
|
|
+ if (file) files.push(file);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (entry.isFile) {
|
|
|
+ const file = await new Promise<File>((resolve) => (entry as FileSystemFileEntry).file(resolve));
|
|
|
+ files.push(file);
|
|
|
+ } else if (entry.isDirectory) {
|
|
|
+ files.push(...(await traverseDirectory(entry as FileSystemDirectoryEntry)));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return files;
|
|
|
+ };
|
|
|
+
|
|
|
+ /** 文件加入上传列表并自动开始上传 */
|
|
|
+ const addFiles = (files: File[]) => {
|
|
|
+ if (!files.length || isAbandoned.value) return;
|
|
|
+ const valid = filterValidFiles(files);
|
|
|
+ if (!valid.length) return;
|
|
|
+ selectedFiles.value = [...selectedFiles.value, ...valid];
|
|
|
+ void startUpload();
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleDrop = async (event: DragEvent) => {
|
|
|
+ dragOver.value = false;
|
|
|
+ if (!event.dataTransfer) return;
|
|
|
+ const files = await collectFilesFromDataTransfer(event.dataTransfer.items);
|
|
|
+ addFiles(files);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleFileInputChange = (event: Event) => {
|
|
|
+ const target = event.target as HTMLInputElement;
|
|
|
+ if (!target.files?.length) return;
|
|
|
+ addFiles(Array.from(target.files));
|
|
|
+ target.value = '';
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleFolderInputChange = (event: Event) => {
|
|
|
+ const target = event.target as HTMLInputElement;
|
|
|
+ if (!target.files?.length) return;
|
|
|
+ const files = Array.from(target.files).filter((f) => f.webkitRelativePath);
|
|
|
+ addFiles(files);
|
|
|
+ target.value = '';
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleTriggerFileSelect = () => {
|
|
|
+ if (uploadMode.value === 'folder') {
|
|
|
+ folderInputRef.value?.click();
|
|
|
+ } else {
|
|
|
+ fileInputRef.value?.click();
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleUploadModeChange = (mode: UploadMode) => {
|
|
|
+ if (uploadMode.value === mode || isUploading.value) return;
|
|
|
+ uploadMode.value = mode;
|
|
|
+ clearSelectedFiles();
|
|
|
+ };
|
|
|
+
|
|
|
+ const clearSelectedFiles = () => {
|
|
|
+ selectedFiles.value = [];
|
|
|
+ uploadProgressMap.value = new Map();
|
|
|
+ uploadStatuses.value = new Map();
|
|
|
+ overallProgress.value = 0;
|
|
|
+ };
|
|
|
+
|
|
|
+ /** 移除单个待上传文件(上传解析中不允许移除) */
|
|
|
+ const removeSelectedFile = (target: File) => {
|
|
|
+ if (isUploading.value) return;
|
|
|
+ const index = selectedFiles.value.indexOf(target);
|
|
|
+ if (index === -1) return;
|
|
|
+ // 仅允许移除待上传文件
|
|
|
+ if ((uploadStatuses.value.get(target) || 'pending') !== 'pending') {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ selectedFiles.value.splice(index, 1);
|
|
|
+ uploadProgressMap.value.delete(target);
|
|
|
+ uploadStatuses.value.delete(target);
|
|
|
+ if (!selectedFiles.value.length) {
|
|
|
+ overallProgress.value = 0;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const persistFileInfos = (infos: Recordable[]) => {
|
|
|
+ sessionStorage.setItem(FILE_INFO_STORAGE_KEY, JSON.stringify(infos));
|
|
|
+ };
|
|
|
+
|
|
|
+ const isOwnBatchStorage = () => {
|
|
|
+ const raw = sessionStorage.getItem(FILE_INFO_STORAGE_KEY);
|
|
|
+ if (!raw) return true;
|
|
|
+ try {
|
|
|
+ const parsed = JSON.parse(raw);
|
|
|
+ if (!Array.isArray(parsed) || !parsed.length) return true;
|
|
|
+ return parsed.every((item) => String(item?.batchId ?? '') === String(uploadBatchId.value));
|
|
|
+ } catch {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const clearStoredFileInfos = () => {
|
|
|
+ if (isOwnBatchStorage()) {
|
|
|
+ sessionStorage.removeItem(FILE_INFO_STORAGE_KEY);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 自动上传队列:选择文件后自动触发,按序消费列表中的待上传文件;
|
|
|
+ * 上传解析成功即从列表移除,失败保留(标记错误,可手动移除)。
|
|
|
+ */
|
|
|
+ const startUpload = async () => {
|
|
|
+ if (isUploading.value || !batchIdReady.value) return;
|
|
|
+ const currentBatchId = uploadBatchId.value;
|
|
|
+ isUploading.value = true;
|
|
|
+ isAbandoned.value = false;
|
|
|
+ tableLoading.value = true;
|
|
|
+
|
|
|
+ try {
|
|
|
+ while (true) {
|
|
|
+ if (isAbandoned.value) break;
|
|
|
+ // 取当前第一个待上传文件
|
|
|
+ const index = selectedFiles.value.findIndex((f) => (uploadStatuses.value.get(f) || 'pending') === 'pending');
|
|
|
+ if (index === -1) break;
|
|
|
+ const file = selectedFiles.value[index];
|
|
|
+ uploadStatuses.value.set(file, 'uploading');
|
|
|
+
|
|
|
+ const onProgress = (percent: number) => {
|
|
|
+ uploadProgressMap.value.set(file, percent);
|
|
|
+ // 总进度:全量待处理文件按(pending=0 / uploading=进度 / error=100)取平均
|
|
|
+ const list = selectedFiles.value;
|
|
|
+ const total = list.length || 1;
|
|
|
+ const sum = list.reduce((acc, f) => {
|
|
|
+ const st = uploadStatuses.value.get(f);
|
|
|
+ if (st === 'uploading') return acc + (uploadProgressMap.value.get(f) || 0);
|
|
|
+ if (st === 'error') return acc + 100;
|
|
|
+ return acc;
|
|
|
+ }, 0);
|
|
|
+ overallProgress.value = Math.round(sum / total);
|
|
|
+ };
|
|
|
+
|
|
|
+ try {
|
|
|
+ // defHttp.uploadFile 返回原始 AxiosResponse,业务数据在 res.data.data
|
|
|
+ // 后端返回单个文件节点:文件根(含 children sheet 列表)
|
|
|
+ const res = await preFileUpload(file, onProgress, { batchId: currentBatchId });
|
|
|
+ if (isAbandoned.value) break;
|
|
|
+ const fileNode: FileInfo | null = res?.data?.data ?? null;
|
|
|
+ if (fileNode) {
|
|
|
+ // 右侧 treetable 直接渲染树(文件根 → sheet 子节点)
|
|
|
+ resultTrees.value = [...resultTrees.value, fileNode];
|
|
|
+ // 持久化给 match/清洗流程的数据保持平铺(根 + 各 sheet 同层,pid 关联),与历史契约一致
|
|
|
+ const flatList: FileInfo[] = [fileNode, ...(fileNode.children ?? [])];
|
|
|
+ allFileInfos.value = [...allFileInfos.value, ...flatList];
|
|
|
+ persistFileInfos(allFileInfos.value);
|
|
|
+ }
|
|
|
+ // 上传成功:从列表移除该文件
|
|
|
+ uploadProgressMap.value.delete(file);
|
|
|
+ uploadStatuses.value.delete(file);
|
|
|
+ const idx = selectedFiles.value.indexOf(file);
|
|
|
+ if (idx !== -1) {
|
|
|
+ selectedFiles.value.splice(idx, 1);
|
|
|
+ }
|
|
|
+ onProgress(100);
|
|
|
+ } catch (error: any) {
|
|
|
+ // 失败保留在列表,标记错误供用户查看/移除
|
|
|
+ uploadStatuses.value.set(file, 'error');
|
|
|
+ uploadProgressMap.value.set(file, 100);
|
|
|
+ createMessage.error(`「${file.name}」上传解析失败:${error?.message || '未知错误'}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ overallProgress.value = 100;
|
|
|
+ if (resultTrees.value.length && !isAbandoned.value) {
|
|
|
+ createMessage.success(`成功解析 ${resultTrees.value.length} 个文件,可在右侧查看结果`);
|
|
|
+ } else if (!isAbandoned.value && !selectedFiles.value.length) {
|
|
|
+ createMessage.warning('未识别到可处理文件,请检查文件格式是否正确');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ createMessage.error('上传解析过程中发生错误');
|
|
|
+ } finally {
|
|
|
+ isUploading.value = false;
|
|
|
+ tableLoading.value = false;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /** 文件/工作表状态 → 中文标签(与 importList、cleaning 页保持一致) */
|
|
|
+ const fileStatusLabelMap = Object.entries(FileStatusEnum).reduce<Record<string, string>>((map, [key, label]) => {
|
|
|
+ map[key] = label;
|
|
|
+ return map;
|
|
|
+ }, {});
|
|
|
+ /** 读取/处理成功,可进入匹配、清洗流程的状态 */
|
|
|
+ const SUCCESS_FILE_STATUSES = new Set<string>([
|
|
|
+ 'FILE_READ_COMPLETE',
|
|
|
+ 'SHEET_TEMPLATE_MATCHED_SUCCESS',
|
|
|
+ 'FILE_CLEAN_COMPLETE',
|
|
|
+ 'FILE_GOVERN_COMPLETE',
|
|
|
+ ]);
|
|
|
+ /** 明确失败的状态(加密文件等中性状态不算失败) */
|
|
|
+ const FAIL_FILE_STATUSES = new Set<string>([
|
|
|
+ 'FILE_READ_FAIL',
|
|
|
+ 'FILE_UNSUPPORTED_FAIL',
|
|
|
+ 'SHEET_READ_FAIL',
|
|
|
+ 'SHEET_TEMPLATE_MATCHED_FAIL',
|
|
|
+ ]);
|
|
|
+
|
|
|
+ const isSuccessStatus = (status?: string) => {
|
|
|
+ const normalized = String(status || '').trim();
|
|
|
+ return !!normalized && SUCCESS_FILE_STATUSES.has(normalized);
|
|
|
+ };
|
|
|
+
|
|
|
+ const mapStatus = (status?: string) => {
|
|
|
+ const normalized = String(status || '').trim();
|
|
|
+ if (!normalized) return { success: false, fail: false, label: '待处理' };
|
|
|
+ return {
|
|
|
+ success: SUCCESS_FILE_STATUSES.has(normalized),
|
|
|
+ fail: FAIL_FILE_STATUSES.has(normalized),
|
|
|
+ label: fileStatusLabelMap[normalized] || normalized,
|
|
|
+ };
|
|
|
+ };
|
|
|
+
|
|
|
+ const isRootNode = (record: Recordable) => !record.pid || Number(record.pid) === 0;
|
|
|
+
|
|
|
+ const resultTableColumns: TableColumnsType<Recordable> = [
|
|
|
+ {
|
|
|
+ title: '名称',
|
|
|
+ key: 'name',
|
|
|
+ width: 360,
|
|
|
+ ellipsis: true,
|
|
|
+ customRender: ({ record }: any) => {
|
|
|
+ const root = isRootNode(record);
|
|
|
+ const name = root ? record.fileName || '未命名文件' : record.sheetName || record.tableName || '未命名工作表';
|
|
|
+ const extra = root && record.fileType ? ` · ${record.fileType}` : '';
|
|
|
+ return h('span', { class: ['cell-name', { 'is-child': !root }], title: String(name) + extra }, [name + extra]);
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '大小',
|
|
|
+ key: 'fileSize',
|
|
|
+ width: 110,
|
|
|
+ customRender: ({ record }: any) => (isRootNode(record) ? formatFileSize(record.fileSize) : '—'),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '数据量',
|
|
|
+ key: 'dataNum',
|
|
|
+ width: 90,
|
|
|
+ customRender: ({ record }: any) => record.dataNum ?? '—',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '成功',
|
|
|
+ key: 'successNum',
|
|
|
+ width: 90,
|
|
|
+ customRender: ({ record }: any) =>
|
|
|
+ h('span', { class: 'cell-num is-success' }, [String(record.successNum ?? '—')]),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '失败',
|
|
|
+ key: 'failNum',
|
|
|
+ width: 90,
|
|
|
+ customRender: ({ record }: any) => h('span', { class: 'cell-num is-fail' }, [String(record.failNum ?? '—')]),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '状态',
|
|
|
+ key: 'status',
|
|
|
+ width: 130,
|
|
|
+ customRender: ({ record }: any) => {
|
|
|
+ const mapped = mapStatus(record.fileStatus);
|
|
|
+ const cls = mapped.success ? 'is-success' : mapped.fail ? 'is-fail' : 'is-other';
|
|
|
+ return h('span', { class: ['status-tag', cls] }, [mapped.label]);
|
|
|
+ },
|
|
|
+ },
|
|
|
+ ];
|
|
|
+
|
|
|
+ const cleanupAndLeave = async () => {
|
|
|
+ isAbandoned.value = true;
|
|
|
+ allowedToLeave.value = true;
|
|
|
+ try {
|
|
|
+ await clearUploadBatch({ batchId: uploadBatchId.value });
|
|
|
+ } catch (error: any) {
|
|
|
+ createMessage.warning(error?.message || '清理本批次数据失败,可稍后在导入文件管理中删除');
|
|
|
+ } finally {
|
|
|
+ clearStoredFileInfos();
|
|
|
+ await go(PageEnum.BASE_HOME, true);
|
|
|
+ emitter.emit(CASE_DATA_REFRESH_EVENT);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleBack = () => {
|
|
|
+ if (!hasActivity.value) {
|
|
|
+ allowedToLeave.value = true;
|
|
|
+ go(PageEnum.BASE_HOME, true);
|
|
|
+ emitter.emit(CASE_DATA_REFRESH_EVENT);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ Modal.confirm({
|
|
|
+ title: isUploading.value ? '上传解析进行中' : '存在未处理文件',
|
|
|
+ content: isUploading.value
|
|
|
+ ? '当前仍有文件在上传解析,离开将中断本次上传,并清理本批次已上传文件与解析结果,是否继续?'
|
|
|
+ : '离开将清理本批次已上传文件与解析结果(未进入清洗流程),是否继续?',
|
|
|
+ okText: '离开并清理',
|
|
|
+ cancelText: '留在本页',
|
|
|
+ onOk: cleanupAndLeave,
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleGoMatch = () => {
|
|
|
+ if (!hasSuccessResult.value) return;
|
|
|
+ allowedToLeave.value = true;
|
|
|
+ persistFileInfos(allFileInfos.value);
|
|
|
+ emitter.emit(CASE_IMPORT_FILE_REFRESH_EVENT);
|
|
|
+ go(PageEnum.BASE_MATCH);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 兜底浏览器返回 / 标签页关闭等非按钮离开
|
|
|
+ onBeforeRouteLeave((_to, _from, next) => {
|
|
|
+ if (allowedToLeave.value || !hasActivity.value) {
|
|
|
+ next();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ Modal.confirm({
|
|
|
+ title: isUploading.value ? '上传解析进行中' : '存在未处理文件',
|
|
|
+ content: '离开将清理本批次已上传文件与解析结果(未进入清洗流程),是否继续?',
|
|
|
+ okText: '离开并清理',
|
|
|
+ cancelText: '留在本页',
|
|
|
+ onOk: cleanupAndLeave,
|
|
|
+ });
|
|
|
+ next(false);
|
|
|
+ });
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="less">
|
|
|
+ @prefix-cls: ~'jeesite-case-upload';
|
|
|
+
|
|
|
+ .@{prefix-cls}.case-upload-page {
|
|
|
+ width: 100%;
|
|
|
+ height: 100vh;
|
|
|
+ padding: 8px 12px;
|
|
|
+ background: #f0f2f5;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+ box-sizing: border-box;
|
|
|
+ overflow: hidden;
|
|
|
+
|
|
|
+ .page-header {
|
|
|
+ flex-shrink: 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 12px;
|
|
|
+ padding: 2px 2px 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .page-header__left {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ min-width: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .back-btn {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ padding: 0 4px 0 0;
|
|
|
+ height: 30px;
|
|
|
+ border-radius: 6px;
|
|
|
+ color: #2563eb;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ line-height: 1;
|
|
|
+ flex-shrink: 0;
|
|
|
+
|
|
|
+ :deep(.ant-btn-icon) {
|
|
|
+ font-size: 15px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ color: #1d4ed8;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .page-title {
|
|
|
+ font-size: 15px;
|
|
|
+ line-height: 1.3;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #111827;
|
|
|
+ }
|
|
|
+
|
|
|
+ .page-subtitle {
|
|
|
+ margin-top: 2px;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.5;
|
|
|
+ color: #6b7280;
|
|
|
+ }
|
|
|
+
|
|
|
+ .page-header__actions {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .next-btn {
|
|
|
+ height: 32px;
|
|
|
+ padding: 0 14px;
|
|
|
+ border-radius: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-layout {
|
|
|
+ flex: 1 1 auto;
|
|
|
+ min-height: 0;
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ align-items: stretch;
|
|
|
+ }
|
|
|
+
|
|
|
+ .left-panel,
|
|
|
+ .right-panel {
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+ border-radius: 10px;
|
|
|
+ background: #fff;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ .left-panel {
|
|
|
+ flex: 0 0 460px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 10px;
|
|
|
+ padding: 12px;
|
|
|
+ box-sizing: border-box;
|
|
|
+ overflow-y: auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ .right-panel {
|
|
|
+ flex: 1 1 auto;
|
|
|
+ min-width: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+
|
|
|
+ .panel-header {
|
|
|
+ flex-shrink: 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 12px;
|
|
|
+ padding: 10px 14px 8px;
|
|
|
+ border-bottom: 1px solid #eef2f7;
|
|
|
+ }
|
|
|
+
|
|
|
+ .panel-title {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #111827;
|
|
|
+ }
|
|
|
+
|
|
|
+ .panel-desc {
|
|
|
+ margin-top: 2px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #6b7280;
|
|
|
+ line-height: 1.5;
|
|
|
+ }
|
|
|
+
|
|
|
+ .panel-stats {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #64748b;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-wrapper {
|
|
|
+ flex: 1 1 auto;
|
|
|
+ min-height: 0;
|
|
|
+ padding: 2px 10px 10px;
|
|
|
+ overflow: auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ .result-tree-table {
|
|
|
+ :deep(.ant-table-thead > tr > th) {
|
|
|
+ padding: 6px 8px;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #6b7280;
|
|
|
+ background: #fafbfc;
|
|
|
+ border-bottom: 1px solid #eef2f7;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.ant-table-tbody > tr > td) {
|
|
|
+ padding: 6px 8px;
|
|
|
+ font-size: 13px;
|
|
|
+ border-bottom: 1px solid #f6f8fa;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.ant-table-tbody > tr:hover > td) {
|
|
|
+ background: #f8fafc;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.ant-table-row-expand-icon) {
|
|
|
+ margin-right: 6px;
|
|
|
+ border-radius: 3px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .cell-name {
|
|
|
+ color: #1e293b;
|
|
|
+ font-size: 13px;
|
|
|
+
|
|
|
+ &.is-child {
|
|
|
+ color: #475569;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .cell-num {
|
|
|
+ font-size: 13px;
|
|
|
+
|
|
|
+ &.is-success {
|
|
|
+ color: #16a34a;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.is-fail {
|
|
|
+ color: #dc2626;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .status-tag {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 2px 10px;
|
|
|
+ border-radius: 999px;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ white-space: nowrap;
|
|
|
+
|
|
|
+ &.is-success {
|
|
|
+ background: #ecfdf3;
|
|
|
+ color: #15803d;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.is-fail {
|
|
|
+ background: #fef2f2;
|
|
|
+ color: #dc2626;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.is-other {
|
|
|
+ background: #f1f5f9;
|
|
|
+ color: #64748b;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-picker {
|
|
|
+ flex: 0 0 auto;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-mode-switch {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ width: fit-content;
|
|
|
+ padding: 3px;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #f1f5f9;
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-mode-switch__item {
|
|
|
+ min-width: 92px;
|
|
|
+ height: 28px;
|
|
|
+ padding: 0 12px;
|
|
|
+ border: none;
|
|
|
+ border-radius: 6px;
|
|
|
+ background: transparent;
|
|
|
+ color: #64748b;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ cursor: pointer;
|
|
|
+ transition:
|
|
|
+ background-color 0.2s ease,
|
|
|
+ color 0.2s ease;
|
|
|
+
|
|
|
+ &.is-active {
|
|
|
+ background: #2563eb;
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+ &:disabled {
|
|
|
+ cursor: not-allowed;
|
|
|
+ opacity: 0.6;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-dropzone {
|
|
|
+ flex: 0 0 auto;
|
|
|
+ min-height: 96px;
|
|
|
+ border-radius: 8px;
|
|
|
+ border: 1px dashed #cbd5e1;
|
|
|
+ background: #fafbfc;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ cursor: pointer;
|
|
|
+
|
|
|
+ &:hover,
|
|
|
+ &.is-dragover {
|
|
|
+ border-color: #3b82f6;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-dragger-content {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 4px;
|
|
|
+ padding: 12px 0;
|
|
|
+ color: #475569;
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-dragger-icon {
|
|
|
+ width: 28px;
|
|
|
+ height: 28px;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: rgba(59, 130, 246, 0.1);
|
|
|
+ color: #2563eb;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 14px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-dragger-title {
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #334155;
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-dragger-desc {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #64748b;
|
|
|
+ max-width: 420px;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-action-btn {
|
|
|
+ margin-top: 2px;
|
|
|
+ height: 26px;
|
|
|
+ padding: 0 14px;
|
|
|
+ border-radius: 6px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-file-list {
|
|
|
+ flex: 1 1 auto;
|
|
|
+ min-height: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+ border-radius: 12px;
|
|
|
+ background: #ffffff;
|
|
|
+ overflow: hidden;
|
|
|
+
|
|
|
+ &__header {
|
|
|
+ flex-shrink: 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 8px 12px;
|
|
|
+ background: #f8fafc;
|
|
|
+ border-bottom: 1px solid #e2e8f0;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #475569;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__body {
|
|
|
+ flex: 1 1 auto;
|
|
|
+ min-height: 0;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 4px 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ &--empty {
|
|
|
+ flex: 1 1 auto;
|
|
|
+ border-style: dashed;
|
|
|
+ background: #fafafa;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-empty {
|
|
|
+ margin: auto;
|
|
|
+ padding: 24px 12px;
|
|
|
+ color: #9ca3af;
|
|
|
+ font-size: 13px;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-file-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 6px 12px;
|
|
|
+ gap: 12px;
|
|
|
+ transition: background 0.15s ease;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: #f8fafc;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__info {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ min-width: 0;
|
|
|
+ flex: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__name {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #1e293b;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ max-width: 300px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__size {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #94a3b8;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__status {
|
|
|
+ flex-shrink: 0;
|
|
|
+ font-size: 12px;
|
|
|
+ min-width: 48px;
|
|
|
+ text-align: right;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__remove {
|
|
|
+ flex-shrink: 0;
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ width: 22px;
|
|
|
+ height: 22px;
|
|
|
+ padding: 0;
|
|
|
+ border: none;
|
|
|
+ border-radius: 6px;
|
|
|
+ background: transparent;
|
|
|
+ color: #9ca3af;
|
|
|
+ font-size: 11px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition:
|
|
|
+ background-color 0.15s ease,
|
|
|
+ color 0.15s ease;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: #fef2f2;
|
|
|
+ color: #dc2626;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &__error {
|
|
|
+ color: #dc2626;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__progress-text {
|
|
|
+ color: #2563eb;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__pending {
|
|
|
+ color: #94a3b8;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-overall-progress {
|
|
|
+ flex-shrink: 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ padding: 8px 12px;
|
|
|
+ border-top: 1px solid #e2e8f0;
|
|
|
+
|
|
|
+ &__bar {
|
|
|
+ flex: 1;
|
|
|
+ height: 5px;
|
|
|
+ border-radius: 3px;
|
|
|
+ background: #e2e8f0;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__fill {
|
|
|
+ height: 100%;
|
|
|
+ border-radius: 3px;
|
|
|
+ background: #2563eb;
|
|
|
+ transition: width 0.3s ease;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__text {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #475569;
|
|
|
+ font-weight: 500;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @media (max-width: 1080px) {
|
|
|
+ .upload-layout {
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+
|
|
|
+ .left-panel {
|
|
|
+ flex: 0 0 auto;
|
|
|
+ width: auto;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ html[data-theme='dark'] {
|
|
|
+ .@{prefix-cls}.case-upload-page {
|
|
|
+ background: #0f172a;
|
|
|
+
|
|
|
+ .page-title,
|
|
|
+ .panel-title,
|
|
|
+ .upload-dragger-title,
|
|
|
+ .upload-file-item__name,
|
|
|
+ .cell-name {
|
|
|
+ color: #e5e7eb;
|
|
|
+ }
|
|
|
+
|
|
|
+ .page-subtitle,
|
|
|
+ .panel-desc,
|
|
|
+ .panel-stats,
|
|
|
+ .upload-dragger-desc,
|
|
|
+ .upload-file-item__size,
|
|
|
+ .upload-file-item__pending {
|
|
|
+ color: #9ca3af;
|
|
|
+ }
|
|
|
+
|
|
|
+ .left-panel,
|
|
|
+ .right-panel,
|
|
|
+ .upload-file-list {
|
|
|
+ background: #111827;
|
|
|
+ border-color: #1f2937;
|
|
|
+ }
|
|
|
+
|
|
|
+ .panel-header,
|
|
|
+ .upload-file-list__header,
|
|
|
+ .upload-overall-progress {
|
|
|
+ border-color: #1f2937;
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-file-list__header {
|
|
|
+ background: #0f172a;
|
|
|
+ color: #cbd5e1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-file-item:hover {
|
|
|
+ background: #1f2937;
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-dropzone {
|
|
|
+ border-color: #334155;
|
|
|
+ background: #0f172a;
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-mode-switch {
|
|
|
+ background: #1e293b;
|
|
|
+ border-color: #334155;
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-mode-switch__item {
|
|
|
+ color: #94a3b8;
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-overall-progress__bar {
|
|
|
+ background: #1f2937;
|
|
|
+ }
|
|
|
+
|
|
|
+ .status-tag.is-other {
|
|
|
+ background: #1e293b;
|
|
|
+ color: #94a3b8;
|
|
|
+ }
|
|
|
+
|
|
|
+ .cell-name.is-child {
|
|
|
+ color: #cbd5e1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .result-tree-table {
|
|
|
+ :deep(.ant-table-thead > tr > th) {
|
|
|
+ background: #0f172a;
|
|
|
+ color: #94a3b8;
|
|
|
+ border-bottom-color: #1f2937;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.ant-table-tbody > tr > td) {
|
|
|
+ border-bottom-color: #111827;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.ant-table-tbody > tr:hover > td) {
|
|
|
+ background: #1f2937;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+</style>
|