|
|
@@ -1,172 +1,178 @@
|
|
|
<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 会自动解析,其余格式仅登记为「不支持解析」。 「智能」会同时跑既有模板清洗与 AI
|
|
|
- 识别:未匹配到模板的文件交给 AI 判定,判不出来的标为待人工确认。
|
|
|
+ <!-- 清洗流程公共顶栏:品牌 / 案件 / 模板库 / 下载管理 / 窗口控制 -->
|
|
|
+ <CaseFlowTopBar />
|
|
|
+
|
|
|
+ <div class="page-body">
|
|
|
+ <div class="page-header">
|
|
|
+ <div class="page-header__left">
|
|
|
+ <Tooltip title="返回" placement="bottom" :mouseEnterDelay="0.5">
|
|
|
+ <button type="button" class="back-btn" aria-label="返回" @click="handleBack">
|
|
|
+ <LeftOutlined />
|
|
|
+ </button>
|
|
|
+ </Tooltip>
|
|
|
+ <span class="page-header__divider" aria-hidden="true"></span>
|
|
|
+ <div class="page-title-block">
|
|
|
+ <div class="page-title">文件上传</div>
|
|
|
+ <div class="page-subtitle">
|
|
|
+ 支持 xls / xlsx / csv 自动解析;智能清洗结合模板与 AI 识别,未能匹配的文件待人工确认。
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+ <div class="page-header__actions">
|
|
|
+ <Button
|
|
|
+ type="primary"
|
|
|
+ class="next-btn"
|
|
|
+ :loading="smartCleaning"
|
|
|
+ :disabled="!hasCleanableSheet"
|
|
|
+ @click="handleSmartClean"
|
|
|
+ >
|
|
|
+ 智能清洗
|
|
|
+ </Button>
|
|
|
+ <Button class="next-btn" :disabled="!hasCleanableSheet" @click="handleManualClean">手动清洗</Button>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- <div class="page-header__actions">
|
|
|
- <Button
|
|
|
- type="primary"
|
|
|
- class="next-btn"
|
|
|
- :loading="smartCleaning"
|
|
|
- :disabled="!hasCleanableSheet"
|
|
|
- @click="handleSmartClean"
|
|
|
- >
|
|
|
- 智能
|
|
|
- </Button>
|
|
|
- <Button class="next-btn" :disabled="!hasCleanableSheet" @click="handleManualClean">手动清洗</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 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>
|
|
|
- </div>
|
|
|
|
|
|
- <!-- 隐藏的文件输入 -->
|
|
|
- <input ref="fileInputRef" type="file" multiple 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 会自动解析,其余格式仅登记为「不支持解析」'
|
|
|
- : '支持选择整个文件夹,系统会自动读取符合条件的文件'
|
|
|
- }}
|
|
|
+ <!-- 隐藏的文件输入 -->
|
|
|
+ <input ref="fileInputRef" type="file" multiple 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>
|
|
|
- <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">{{ file.name }}</span>
|
|
|
- <span class="upload-file-item__size">{{ formatFileSize(file.size) }}</span>
|
|
|
+ <!-- 已选文件列表(加入后自动上传,成功即移除) -->
|
|
|
+ <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">{{ 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"
|
|
|
+ aria-label="移除文件"
|
|
|
+ @click="removeSelectedFile(file)"
|
|
|
+ >
|
|
|
+ <CloseOutlined />
|
|
|
+ </button>
|
|
|
</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>
|
|
|
+ <!-- 整体进度 -->
|
|
|
+ <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>
|
|
|
- <button
|
|
|
- v-if="!isUploading && (uploadStatuses.get(file) || 'pending') === 'pending'"
|
|
|
- type="button"
|
|
|
- class="upload-file-item__remove"
|
|
|
- aria-label="移除文件"
|
|
|
- @click="removeSelectedFile(file)"
|
|
|
- >
|
|
|
- <CloseOutlined />
|
|
|
- </button>
|
|
|
+ <span class="upload-overall-progress__text">总进度 {{ overallProgress }}%</span>
|
|
|
</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 v-else class="upload-file-list upload-file-list--empty">
|
|
|
+ <div class="upload-empty">暂无待上传文件,选择文件后将自动上传解析</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- <div v-else class="upload-file-list upload-file-list--empty">
|
|
|
- <div class="upload-empty">暂无待上传文件,选择文件后将自动上传解析</div>
|
|
|
- </div>
|
|
|
- </aside>
|
|
|
+ </aside>
|
|
|
|
|
|
- <section class="right-panel">
|
|
|
- <div class="panel-header">
|
|
|
- <div>
|
|
|
- <div class="panel-title">解析结果(实时)</div>
|
|
|
- <div class="panel-desc">每完成一份文件解析,结果树会实时追加到下方</div>
|
|
|
+ <section class="right-panel">
|
|
|
+ <div class="panel-header">
|
|
|
+ <div>
|
|
|
+ <div class="panel-title">解析结果(实时)</div>
|
|
|
+ <div class="panel-desc">每完成一份文件解析,结果树会实时追加到下方</div>
|
|
|
+ </div>
|
|
|
+ <div class="panel-header__actions">
|
|
|
+ <Button size="small" :disabled="!expandableCount" @click="toggleExpandAllRows">
|
|
|
+ {{ allExpanded ? '全部收起' : '全部展开' }}
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ <div class="panel-stats">已解析 {{ resultTrees.length }} 个文件</div>
|
|
|
</div>
|
|
|
- <div class="panel-header__actions">
|
|
|
- <Button size="small" :disabled="!expandableCount" @click="toggleExpandAllRows">
|
|
|
- {{ allExpanded ? '全部收起' : '全部展开' }}
|
|
|
- </Button>
|
|
|
+ <div class="table-wrapper">
|
|
|
+ <Table
|
|
|
+ class="result-tree-table"
|
|
|
+ :columns="resultTableColumns"
|
|
|
+ :data-source="tableDataSource"
|
|
|
+ :loading="tableLoading"
|
|
|
+ :pagination="false"
|
|
|
+ row-key="id"
|
|
|
+ :children-column-name="'children'"
|
|
|
+ :expanded-row-keys="expandedRowKeys"
|
|
|
+ size="small"
|
|
|
+ :scroll="{ x: 840 }"
|
|
|
+ @expandedRowsChange="onExpandedRowsChange"
|
|
|
+ />
|
|
|
</div>
|
|
|
- <div class="panel-stats">已解析 {{ resultTrees.length }} 个文件</div>
|
|
|
- </div>
|
|
|
- <div class="table-wrapper">
|
|
|
- <Table
|
|
|
- class="result-tree-table"
|
|
|
- :columns="resultTableColumns"
|
|
|
- :data-source="tableDataSource"
|
|
|
- :loading="tableLoading"
|
|
|
- :pagination="false"
|
|
|
- row-key="id"
|
|
|
- :children-column-name="'children'"
|
|
|
- :expanded-row-keys="expandedRowKeys"
|
|
|
- size="small"
|
|
|
- :scroll="{ x: 840 }"
|
|
|
- @expandedRowsChange="onExpandedRowsChange"
|
|
|
- />
|
|
|
- </div>
|
|
|
- </section>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
@@ -175,7 +181,7 @@
|
|
|
import { computed, h, onMounted, 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 { Button, Modal, Table, Tooltip, 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';
|
|
|
@@ -183,6 +189,7 @@
|
|
|
import { useEmitter } from '@/core/store/modules/case';
|
|
|
import { clearUploadBatch, deletedFileList, preFileUpload, smartClean } from '@/case/api/govern/governApi';
|
|
|
import { FileStatusEnum } from '@/case/types/enum';
|
|
|
+ import CaseFlowTopBar from './components/CaseFlowTopBar.vue';
|
|
|
import { resetImportProgressSession, setCleanFlowSource } from './importProgressSession';
|
|
|
import type { CleanFlowSource } from './importProgressSession';
|
|
|
import type { FileInfo } from '@/types/api';
|
|
|
@@ -911,64 +918,101 @@
|
|
|
.@{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;
|
|
|
|
|
|
+ // 公共顶栏由 AppTopBar 自带高度,内容区在剩余高度里滚动
|
|
|
+ .page-body {
|
|
|
+ flex: 1 1 auto;
|
|
|
+ min-height: 0;
|
|
|
+ padding: 8px 12px;
|
|
|
+ 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;
|
|
|
+ gap: 16px;
|
|
|
+ padding: 10px 14px;
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+ border-radius: 10px;
|
|
|
+ background: #fff;
|
|
|
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03);
|
|
|
}
|
|
|
|
|
|
.page-header__left {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- gap: 10px;
|
|
|
+ gap: 12px;
|
|
|
min-width: 0;
|
|
|
}
|
|
|
|
|
|
+ .page-header__divider {
|
|
|
+ flex: 0 0 auto;
|
|
|
+ width: 1px;
|
|
|
+ height: 22px;
|
|
|
+ background: #e2e8f0;
|
|
|
+ }
|
|
|
+
|
|
|
.back-btn {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
- gap: 4px;
|
|
|
- padding: 0 4px 0 0;
|
|
|
- height: 30px;
|
|
|
- border-radius: 6px;
|
|
|
- color: #2563eb;
|
|
|
+ justify-content: center;
|
|
|
+ flex: 0 0 auto;
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ padding: 0;
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #f8fafc;
|
|
|
+ color: #475569;
|
|
|
font-size: 14px;
|
|
|
- font-weight: 600;
|
|
|
line-height: 1;
|
|
|
- flex-shrink: 0;
|
|
|
+ cursor: pointer;
|
|
|
+ transition:
|
|
|
+ color 0.2s ease,
|
|
|
+ border-color 0.2s ease,
|
|
|
+ background-color 0.2s ease;
|
|
|
|
|
|
- :deep(.ant-btn-icon) {
|
|
|
- font-size: 15px;
|
|
|
+ &:hover,
|
|
|
+ &:focus,
|
|
|
+ &:focus-visible {
|
|
|
+ color: #2563eb;
|
|
|
+ border-color: #bfdbfe;
|
|
|
+ background: #eff6ff;
|
|
|
+ outline: none;
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- &:hover {
|
|
|
- color: #1d4ed8;
|
|
|
- }
|
|
|
+ .page-title-block {
|
|
|
+ min-width: 0;
|
|
|
+ overflow: hidden;
|
|
|
}
|
|
|
|
|
|
.page-title {
|
|
|
- font-size: 15px;
|
|
|
- line-height: 1.3;
|
|
|
+ font-size: 16px;
|
|
|
+ line-height: 1.4;
|
|
|
font-weight: 600;
|
|
|
color: #111827;
|
|
|
}
|
|
|
|
|
|
.page-subtitle {
|
|
|
- margin-top: 2px;
|
|
|
+ margin-top: 1px;
|
|
|
+ overflow: hidden;
|
|
|
font-size: 12px;
|
|
|
- line-height: 1.5;
|
|
|
- color: #6b7280;
|
|
|
+ line-height: 1.6;
|
|
|
+ color: #64748b;
|
|
|
+ white-space: nowrap;
|
|
|
+ text-overflow: ellipsis;
|
|
|
}
|
|
|
|
|
|
.page-header__actions {
|
|
|
@@ -980,8 +1024,9 @@
|
|
|
|
|
|
.next-btn {
|
|
|
height: 32px;
|
|
|
- padding: 0 14px;
|
|
|
+ padding: 0 16px;
|
|
|
border-radius: 8px;
|
|
|
+ font-weight: 500;
|
|
|
}
|
|
|
|
|
|
.upload-layout {
|
|
|
@@ -1423,6 +1468,30 @@
|
|
|
.@{prefix-cls}.case-upload-page {
|
|
|
background: #0f172a;
|
|
|
|
|
|
+ .page-header {
|
|
|
+ border-color: #1f2937;
|
|
|
+ background: #111827;
|
|
|
+ box-shadow: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .page-header__divider {
|
|
|
+ background: #1f2937;
|
|
|
+ }
|
|
|
+
|
|
|
+ .back-btn {
|
|
|
+ border-color: #334155;
|
|
|
+ background: #0f172a;
|
|
|
+ color: #cbd5e1;
|
|
|
+
|
|
|
+ &:hover,
|
|
|
+ &:focus,
|
|
|
+ &:focus-visible {
|
|
|
+ border-color: #3b82f6;
|
|
|
+ background: #1e293b;
|
|
|
+ color: #93c5fd;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
.page-title,
|
|
|
.panel-title,
|
|
|
.upload-dragger-title,
|