|
|
@@ -155,7 +155,7 @@
|
|
|
<button
|
|
|
type="button"
|
|
|
:class="['upload-mode-switch__item', { 'is-active': uploadMode === 'single' }]"
|
|
|
- :disabled="uploadProcessing"
|
|
|
+ :disabled="isUploading"
|
|
|
@click="handleUploadModeChange('single')"
|
|
|
>
|
|
|
选择文件
|
|
|
@@ -163,46 +163,99 @@
|
|
|
<button
|
|
|
type="button"
|
|
|
:class="['upload-mode-switch__item', { 'is-active': uploadMode === 'folder' }]"
|
|
|
- :disabled="uploadProcessing"
|
|
|
+ :disabled="isUploading"
|
|
|
@click="handleUploadModeChange('folder')"
|
|
|
>
|
|
|
选择文件夹
|
|
|
</button>
|
|
|
</div>
|
|
|
- <!-- <div class="upload-modal-title">{{ uploadMode === 'single' ? '选择文件' : '选择文件夹' }}</div> -->
|
|
|
- <!-- <div class="upload-modal-subtitle">支持拖拽文件到这里,或点击选择文件或选择文件夹,系统会自动识别处理</div> -->
|
|
|
</div>
|
|
|
- <div class="upload-dropzone">
|
|
|
- <Spin :spinning="uploadProcessing" tip="解析中">
|
|
|
- <Upload.Dragger
|
|
|
- :directory="uploadMode === 'folder'"
|
|
|
- :multiple="true"
|
|
|
- :disabled="uploadProcessing"
|
|
|
- :before-upload="uploadMode === 'folder' ? beforeUploadFolder : beforeUpload"
|
|
|
- :show-upload-list="false"
|
|
|
- >
|
|
|
- <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' ? '支持批量选择文件' : '支持选择整个文件夹,系统会自动读取文件' }}
|
|
|
- </div>
|
|
|
- <Button
|
|
|
- type="primary"
|
|
|
- class="upload-action-btn"
|
|
|
- :loading="selecting"
|
|
|
- :disabled="uploadProcessing"
|
|
|
- @click="handleSelectFile"
|
|
|
- >
|
|
|
- {{ uploadMode === 'single' ? '选择文件' : '选择文件夹' }}
|
|
|
- </Button>
|
|
|
+
|
|
|
+ <!-- 隐藏的文件输入 -->
|
|
|
+ <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, idx) in selectedFiles" :key="file.name + '-' + idx" 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.name + '-' + idx) === 'done'">
|
|
|
+ <span class="upload-file-item__success">✓</span>
|
|
|
+ </template>
|
|
|
+ <template v-else-if="uploadStatuses.get(file.name + '-' + idx) === 'error'">
|
|
|
+ <span class="upload-file-item__error">✗</span>
|
|
|
+ </template>
|
|
|
+ <template v-else-if="uploadStatuses.get(file.name + '-' + idx) === 'uploading'">
|
|
|
+ <span class="upload-file-item__progress-text"
|
|
|
+ >{{ uploadProgressMap.get(file.name + '-' + idx) || 0 }}%</span
|
|
|
+ >
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <span class="upload-file-item__pending">待上传</span>
|
|
|
+ </template>
|
|
|
</div>
|
|
|
- </Upload.Dragger>
|
|
|
- </Spin>
|
|
|
+ </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 class="upload-file-list__actions">
|
|
|
+ <Button type="primary" :loading="isUploading" :disabled="selectedFiles.length === 0" @click="startUpload">
|
|
|
+ {{ isUploading ? '上传解析中...' : '开始上传并解析' }}
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</BasicModal>
|
|
|
@@ -675,6 +728,7 @@
|
|
|
import CustomSearch from './components/CustomSearch.vue';
|
|
|
import GovernProgressModal from './components/GovernProgressModal.vue';
|
|
|
import { getActiveImportProgress } from './importProgressSession';
|
|
|
+ import type { FileInfo } from '@/types/api';
|
|
|
import CallRecordModal from '@/call/views/components/CallRecordModal.vue';
|
|
|
import TransRecordModal from '@/trans/views/components/TransRecordModal.vue';
|
|
|
import {
|
|
|
@@ -702,6 +756,7 @@
|
|
|
getTreeCallPersonPage,
|
|
|
getTreeCallOperPage,
|
|
|
preFile,
|
|
|
+ preFileUpload,
|
|
|
saveGovernConf,
|
|
|
} from '@/case/api/govern/governApi';
|
|
|
import type { GovernConfItem } from '@/case/api/govern/governApi';
|
|
|
@@ -814,8 +869,18 @@
|
|
|
const uploadMode = ref<'single' | 'folder'>('single');
|
|
|
const uploadBizKey = ref(buildUUID());
|
|
|
const uploadLoadTime = ref(Date.now());
|
|
|
- const pendingUploadPaths = ref<string[]>([]);
|
|
|
- let uploadBatchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
+
|
|
|
+ // Web 上传相关状态
|
|
|
+ const fileInputRef = ref<HTMLInputElement | null>(null);
|
|
|
+ const folderInputRef = ref<HTMLInputElement | null>(null);
|
|
|
+ const selectedFiles = ref<File[]>([]);
|
|
|
+ const uploadResults = ref<FileInfo[]>([]);
|
|
|
+ const uploadProgressMap = ref<Map<string, number>>(new Map());
|
|
|
+ const uploadStatuses = ref<Map<string, 'pending' | 'uploading' | 'done' | 'error'>>(new Map());
|
|
|
+ const isUploading = ref(false);
|
|
|
+ const overallProgress = ref(0);
|
|
|
+ const dragOver = ref(false);
|
|
|
+
|
|
|
let governConfigSaveTimer: ReturnType<typeof setTimeout> | undefined;
|
|
|
|
|
|
const { createMessage } = useMessage();
|
|
|
@@ -865,6 +930,7 @@
|
|
|
uploadMode.value = mode;
|
|
|
uploadBizKey.value = buildUUID();
|
|
|
uploadLoadTime.value = Date.now();
|
|
|
+ clearSelectedFiles();
|
|
|
};
|
|
|
|
|
|
const handleUploadChange = () => {
|
|
|
@@ -2416,128 +2482,188 @@
|
|
|
await loadTemplateList();
|
|
|
};
|
|
|
|
|
|
- const selecting = ref(false);
|
|
|
- const uploadProcessing = ref(false);
|
|
|
- const PRE_FILE_EMPTY_MESSAGE = '未识别到可处理文件,可能是文件重复、所有文件不存在,或文件格式不正确';
|
|
|
+ const VALID_UPLOAD_EXTS = ['.xls', '.xlsx', '.csv'];
|
|
|
|
|
|
- const normalizeUploadPaths = (filePaths: Array<string | undefined | null>) =>
|
|
|
- filePaths.map((item) => String(item ?? '').trim()).filter((item) => !!item);
|
|
|
+ const formatFileSize = (bytes?: number) => {
|
|
|
+ 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 extractUploadFilePath = (file: File) => {
|
|
|
- const electronApi = window.electron;
|
|
|
- const resolvedPath = electronApi?.getPathForFile?.(file) || (file as File & { path?: string })?.path;
|
|
|
- return normalizeUploadPaths([resolvedPath])[0];
|
|
|
+ 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 splitUploadPath = (value: string) => value.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
|
+ const handleDrop = async (event: DragEvent) => {
|
|
|
+ dragOver.value = false;
|
|
|
+ if (!event.dataTransfer) return;
|
|
|
+ const files = await collectFilesFromDataTransfer(event.dataTransfer.items);
|
|
|
+ const valid = filterValidFiles(files);
|
|
|
+ if (valid.length) {
|
|
|
+ selectedFiles.value = [...selectedFiles.value, ...valid];
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
- const extractUploadFolderPath = (file: File) => {
|
|
|
- const filePath = extractUploadFilePath(file);
|
|
|
- if (!filePath) return '';
|
|
|
+ const handleFileInputChange = (event: Event) => {
|
|
|
+ const target = event.target as HTMLInputElement;
|
|
|
+ if (!target.files?.length) return;
|
|
|
+ const files = Array.from(target.files);
|
|
|
+ const valid = filterValidFiles(files);
|
|
|
+ if (valid.length) {
|
|
|
+ selectedFiles.value = [...selectedFiles.value, ...valid];
|
|
|
+ }
|
|
|
+ target.value = '';
|
|
|
+ };
|
|
|
|
|
|
- const relativePath = String((file as File & { webkitRelativePath?: string }).webkitRelativePath || '').trim();
|
|
|
- const relativeSegments = splitUploadPath(relativePath);
|
|
|
- if (relativeSegments.length <= 1) return '';
|
|
|
+ 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);
|
|
|
+ const valid = filterValidFiles(files);
|
|
|
+ if (valid.length) {
|
|
|
+ selectedFiles.value = [...selectedFiles.value, ...valid];
|
|
|
+ }
|
|
|
+ target.value = '';
|
|
|
+ };
|
|
|
|
|
|
- const fileSegments = splitUploadPath(filePath);
|
|
|
- const rootSegmentCount = fileSegments.length - relativeSegments.length + 1;
|
|
|
- if (rootSegmentCount <= 0) return '';
|
|
|
+ const handleTriggerFileSelect = () => {
|
|
|
+ if (uploadMode.value === 'folder') {
|
|
|
+ folderInputRef.value?.click();
|
|
|
+ } else {
|
|
|
+ fileInputRef.value?.click();
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
- const separator = filePath.includes('\\') ? '\\' : '/';
|
|
|
- return fileSegments.slice(0, rootSegmentCount).join(separator);
|
|
|
+ const clearSelectedFiles = () => {
|
|
|
+ selectedFiles.value = [];
|
|
|
+ uploadResults.value = [];
|
|
|
+ uploadProgressMap.value = new Map();
|
|
|
+ uploadStatuses.value = new Map();
|
|
|
+ overallProgress.value = 0;
|
|
|
};
|
|
|
|
|
|
- const processUploadFilePaths = async (filePaths: string[]) => {
|
|
|
- const normalizedPaths = normalizeUploadPaths(filePaths);
|
|
|
- if (!normalizedPaths.length || uploadProcessing.value) return;
|
|
|
+ const startUpload = async () => {
|
|
|
+ if (!selectedFiles.value.length || isUploading.value) return;
|
|
|
+ isUploading.value = true;
|
|
|
+ uploadResults.value = [];
|
|
|
+ uploadProgressMap.value = new Map();
|
|
|
+ uploadStatuses.value = new Map();
|
|
|
+ overallProgress.value = 0;
|
|
|
+
|
|
|
+ const files = selectedFiles.value;
|
|
|
+ const total = files.length;
|
|
|
+ const batchId = Date.now();
|
|
|
+ const allFileInfos: FileInfo[] = [];
|
|
|
|
|
|
- uploadProcessing.value = true;
|
|
|
- selecting.value = true;
|
|
|
try {
|
|
|
- const isDir = uploadMode.value === 'folder';
|
|
|
- const response = await preFile({ filePath: normalizedPaths, isDir });
|
|
|
- const fileInfos = Array.isArray(response) ? response : ((response as any)?.data ?? []);
|
|
|
- if (Array.isArray(fileInfos) && fileInfos.length) {
|
|
|
- sessionStorage.setItem('case_clean_file_infos', JSON.stringify(fileInfos));
|
|
|
- createMessage.success('文件预处理成功');
|
|
|
+ for (let i = 0; i < files.length; i++) {
|
|
|
+ const file = files[i];
|
|
|
+ const key = file.name + '-' + i;
|
|
|
+ uploadStatuses.value.set(key, 'uploading');
|
|
|
+
|
|
|
+ const onProgress = (percent: number) => {
|
|
|
+ uploadProgressMap.value.set(key, percent);
|
|
|
+ const completedSum = files.reduce((sum, f, idx) => {
|
|
|
+ const k = f.name + '-' + idx;
|
|
|
+ const status = uploadStatuses.value.get(k);
|
|
|
+ if (status === 'done') return sum + 100;
|
|
|
+ if (status === 'uploading') return sum + (uploadProgressMap.value.get(k) || 0);
|
|
|
+ return sum;
|
|
|
+ }, 0);
|
|
|
+ overallProgress.value = Math.round(completedSum / total);
|
|
|
+ };
|
|
|
+
|
|
|
+ try {
|
|
|
+ const result = await preFileUpload(file, onProgress, { batchId });
|
|
|
+ const list = Array.isArray(result) ? result : ((result as any)?.data ?? []);
|
|
|
+ if (Array.isArray(list) && list.length) {
|
|
|
+ allFileInfos.push(...list);
|
|
|
+ }
|
|
|
+ uploadStatuses.value.set(key, 'done');
|
|
|
+ onProgress(100);
|
|
|
+ } catch (error: any) {
|
|
|
+ uploadStatuses.value.set(key, 'error');
|
|
|
+ createMessage.error(`「${file.name}」上传解析失败:${error?.message || '未知错误'}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ overallProgress.value = 100;
|
|
|
+
|
|
|
+ if (allFileInfos.length) {
|
|
|
+ sessionStorage.setItem('case_clean_file_infos', JSON.stringify(allFileInfos));
|
|
|
+ createMessage.success(`成功解析 ${allFileInfos.length} 个文件`);
|
|
|
handleUploadOk();
|
|
|
emitter.emit(CASE_IMPORT_FILE_REFRESH_EVENT);
|
|
|
go(PageEnum.BASE_MATCH);
|
|
|
} else {
|
|
|
- createMessage.warning(PRE_FILE_EMPTY_MESSAGE);
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- createMessage.error('文件预处理失败');
|
|
|
- } finally {
|
|
|
- uploadProcessing.value = false;
|
|
|
- selecting.value = false;
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- const queueUploadFilePaths = (filePaths: string[]) => {
|
|
|
- const normalizedPaths = normalizeUploadPaths(filePaths);
|
|
|
- if (!normalizedPaths.length) return;
|
|
|
- pendingUploadPaths.value.push(...normalizedPaths);
|
|
|
- if (uploadBatchTimer) clearTimeout(uploadBatchTimer);
|
|
|
- uploadBatchTimer = setTimeout(() => {
|
|
|
- const batchPaths = Array.from(new Set(pendingUploadPaths.value));
|
|
|
- pendingUploadPaths.value = [];
|
|
|
- uploadBatchTimer = null;
|
|
|
- void processUploadFilePaths(batchPaths);
|
|
|
- }, 0);
|
|
|
- };
|
|
|
-
|
|
|
- const handleSelectFile = async (event: MouseEvent) => {
|
|
|
- event.stopPropagation();
|
|
|
- if (selecting.value || uploadProcessing.value) return;
|
|
|
- selecting.value = true;
|
|
|
- const isDir = uploadMode.value === 'folder';
|
|
|
- const ipcRenderer = (window as any)?.electron?.ipcRenderer;
|
|
|
- if (!ipcRenderer?.invoke) {
|
|
|
- createMessage.error('无法获取文件路径,请在 Electron 环境下选择文件');
|
|
|
- selecting.value = false;
|
|
|
- return;
|
|
|
- }
|
|
|
- try {
|
|
|
- const channel = isDir ? 'controller/os/selectFolder' : 'controller/effect/selectFile';
|
|
|
- const resolvedPath = await ipcRenderer.invoke(channel);
|
|
|
- const filePaths = Array.isArray(resolvedPath)
|
|
|
- ? resolvedPath.filter((item: unknown): item is string => typeof item === 'string' && !!item.trim())
|
|
|
- : typeof resolvedPath === 'string' && resolvedPath.trim()
|
|
|
- ? [resolvedPath]
|
|
|
- : [];
|
|
|
- if (!filePaths.length) {
|
|
|
- selecting.value = false;
|
|
|
- return;
|
|
|
+ createMessage.warning('未识别到可处理文件,请检查文件格式是否正确');
|
|
|
}
|
|
|
- await processUploadFilePaths(filePaths);
|
|
|
} catch (error) {
|
|
|
- createMessage.error('文件预处理失败');
|
|
|
+ createMessage.error('上传解析过程中发生错误');
|
|
|
} finally {
|
|
|
- selecting.value = false;
|
|
|
+ isUploading.value = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- const beforeUpload = async (file: File) => {
|
|
|
- const filePath = extractUploadFilePath(file);
|
|
|
- if (!filePath) {
|
|
|
- createMessage.warning('当前拖拽文件未获取到绝对路径,请重新拖拽或点击选择文件');
|
|
|
- return false;
|
|
|
- }
|
|
|
- queueUploadFilePaths([filePath]);
|
|
|
- return false;
|
|
|
- };
|
|
|
-
|
|
|
- const beforeUploadFolder = async (file: File) => {
|
|
|
- const folderPath = extractUploadFolderPath(file);
|
|
|
- if (!folderPath) {
|
|
|
- createMessage.warning('当前拖拽文件夹未获取到目录路径,请重新拖拽或点击选择文件夹');
|
|
|
- return false;
|
|
|
- }
|
|
|
- queueUploadFilePaths([folderPath]);
|
|
|
- return false;
|
|
|
- };
|
|
|
-
|
|
|
const treeData = ref<GovernTreeNodeOption[]>([]);
|
|
|
const columns = ref<BasicColumn[]>([]);
|
|
|
const selectedKeys = ref<string[]>([]);
|
|
|
@@ -3638,7 +3764,6 @@
|
|
|
});
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
- if (uploadBatchTimer) clearTimeout(uploadBatchTimer);
|
|
|
emitter.off(CASE_DATA_REFRESH_EVENT, handleCaseDataRefresh, true);
|
|
|
emitter.off(HEADER_TEMPLATE_LIBRARY_OPEN_EVENT, handleTemplateOpen, true);
|
|
|
});
|
|
|
@@ -5751,32 +5876,18 @@
|
|
|
|
|
|
.upload-dropzone {
|
|
|
flex: 0 0 auto;
|
|
|
+ min-height: 148px;
|
|
|
+ border-radius: 14px;
|
|
|
+ border: 1px dashed #c7d2fe;
|
|
|
+ background: linear-gradient(180deg, #f8fafc 0%, #ffffff 100%);
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ cursor: pointer;
|
|
|
|
|
|
- .ant-spin-nested-loading,
|
|
|
- .ant-spin-container {
|
|
|
- display: block;
|
|
|
- }
|
|
|
-
|
|
|
- .ant-upload-drag {
|
|
|
- min-height: 148px;
|
|
|
- border-radius: 14px;
|
|
|
- border: 1px dashed #c7d2fe;
|
|
|
- background: linear-gradient(180deg, #f8fafc 0%, #ffffff 100%);
|
|
|
- transition: all 0.2s ease;
|
|
|
- }
|
|
|
-
|
|
|
- .ant-upload-drag:hover {
|
|
|
+ &:hover,
|
|
|
+ &.is-dragover {
|
|
|
border-color: #3b82f6;
|
|
|
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.08);
|
|
|
}
|
|
|
-
|
|
|
- .ant-upload.ant-upload-drag {
|
|
|
- padding: 0;
|
|
|
- }
|
|
|
-
|
|
|
- .ant-upload-drag-container {
|
|
|
- height: 100%;
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
.upload-dragger-content {
|
|
|
@@ -5821,6 +5932,132 @@
|
|
|
border-radius: 10px;
|
|
|
box-shadow: 0 8px 18px rgba(37, 99, 235, 0.22);
|
|
|
}
|
|
|
+
|
|
|
+ .upload-file-list {
|
|
|
+ margin-top: 16px;
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+ border-radius: 12px;
|
|
|
+ background: #ffffff;
|
|
|
+ overflow: hidden;
|
|
|
+
|
|
|
+ &__header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 10px 14px;
|
|
|
+ background: #f8fafc;
|
|
|
+ border-bottom: 1px solid #e2e8f0;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #475569;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__body {
|
|
|
+ max-height: 220px;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 4px 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__actions {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ padding: 12px 14px;
|
|
|
+ border-top: 1px solid #e2e8f0;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-file-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 8px 14px;
|
|
|
+ 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: 420px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__size {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #94a3b8;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__status {
|
|
|
+ flex-shrink: 0;
|
|
|
+ font-size: 12px;
|
|
|
+ min-width: 48px;
|
|
|
+ text-align: right;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__success {
|
|
|
+ color: #16a34a;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__error {
|
|
|
+ color: #dc2626;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__progress-text {
|
|
|
+ color: #2563eb;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__pending {
|
|
|
+ color: #94a3b8;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-overall-progress {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ padding: 10px 14px;
|
|
|
+ border-top: 1px solid #e2e8f0;
|
|
|
+
|
|
|
+ &__bar {
|
|
|
+ flex: 1;
|
|
|
+ height: 6px;
|
|
|
+ border-radius: 3px;
|
|
|
+ background: #e2e8f0;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__fill {
|
|
|
+ height: 100%;
|
|
|
+ border-radius: 3px;
|
|
|
+ background: linear-gradient(90deg, #3b82f6, #2563eb);
|
|
|
+ transition: width 0.3s ease;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__text {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #475569;
|
|
|
+ font-weight: 500;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
@media (max-width: 960px) {
|