case-data-fullscreen-upload.md 15 KB

案件数据 · 选择文件 → 全屏上传页改造方案

Summary

将案件数据页(index.vue)头部「选择文件」按钮当前弹出的上传弹框(BasicModal),改为跳转一个全屏页面(/data/upload)完成文件上传,并引入批次(batch)生命周期管理:

  • 进入页面即生成批次 id:upload.vue 挂载时前端生成一个 Long 型 batchId(不依赖首次上传响应),本页所有上传文件统一携带该 batchId 上传(后端 preFileUpload 收到非空 batchId 会直接复用)。
  • 左侧区域:顶部「选择文件 / 选择文件夹」模式切换 + 选择文件按钮;中间已选文件列表(每个文件的待上传/解析中 n%/成功/失败状态);底部「开始上传」按钮与整体进度条。
  • 右侧区域:treetable(树表格),实时展示每份文件解析完成后后端返回的解析结果树(文件 → sheet,列含状态 / 数据量 / 成功 / 失败 / 大小)。
  • 离开本页:上传解析进行中(或有未处理文件)时返回 → 弹确认提示;确认离开且未进入清洗 → 调用后端清理本批次的内容和文件(已上传文件 + 解析缓存 + 本批次记录)。
  • 设计主题与现有案件数据页一致(主色 #2a50ec、白底面板 #e2e8f0 边框 12px 圆角、渐变 #316dff→#4f8bff、页面底色 #f0f2f5、暗色主题适配),UI 复用 index.vue 既有上传样式与 match.vue 全屏页头样式。

Current State Analysis

前端现状

  • 入口:index.vue 头部「选择文件」按钮 → handleUploadNavigate('single') → openUpload('single') 打开 BasicModal(模板 L141-L261)。
  • 弹框内 UI(整体移植到新页):模式切换 L153-L171、隐藏 <input type="file" multiple webkitdirectory> L175-L190、拖拽区 L192-L215、文件列表+状态/进度 L218-L253、「开始上传并解析」L254-L258。
  • 上传逻辑 startUpload(L2604-L2665):逐个 preFileUpload(file, onProgress, { batchId })(POST api/dm/preFileUpload multipart)返回 List<FileInfo> 树(根节点 pid=0=文件、children=各 sheet);全部完成后写 sessionStorage('case_clean_file_infos') 并 go(PageEnum.BASE_MATCH)。当前 batchId 只在首次上传成功后从响应里回填,后续沿用——不满足“进页即生成”。
  • 解析结果字段(FileInfo.java):id/pid/fileName/sheetName/tableName/fileStatus/dataNum/fileSize/successNum/failNum/batchId/createDateTime/children;fileStatus 含读取失败/格式不正确/读取完成/加密/模板匹配失败/模板匹配成功等。
  • 路由:routes/index.ts DataRoute.children 注册全屏页(match/cleaning 用 meta.hideMenu/hideTab/currentActiveMenu=BASE_HOME);pageEnum.ts 已有 BASE_MATCH='/data/match'。
  • 树表格:BasicTable 支持 isTreeTable,children 字段名由 childrenColumnName 指定(默认 'childList',需显式传 'children'),row-key='id'。
  • 主题 token:主色 #2a50ec;成功 #16a34a、失败 #dc2626、文字 #111827/#475569/#64748b、面板 #fff+#e2e8f0 边框+12px 圆角+0 8px 24px rgba(15,23,42,0.04) 阴影;暗色 html[data-theme='dark'](背景 #0f172a/#111827、边框 #1f2937)。

后端批次现状

  • preProcessUploadedFile(MultipartFile, Long batchId, Integer reClean)(DmService.java):batchId != null 时直接复用,否则雪花生成;上传文件落盘 PathConst.WORKSPACE/{caseId}/upload/{batchId}/(L173-L198);解析缓存 PreDataListener 写 PathConst.TMP_PATH/{batchId}/{fileId}_{sheetName}.json;解析结果先驻内存,DB file_info 入库发生在后续 doClean 清洗环节,废弃批次通常无 DB 行。
  • cancelClean(batchId)(L552-L559):仅删 TMP_PATH/{batchId} 缓存,不删上传文件、不删 DB 记录。
  • 缺口:现有接口无法“清理本批次的内容和文件”(上传文件目录 + 缓存 + 本批次 file_info 记录),需新增。

Proposed Changes

1. 后端:新增「清理废弃批次」接口

文件:e:\workspace\zsjz-ai\ai-server\src\main\java\com\zsjz\ai\module\dm\service\DmService.java

  • 新增方法:

    /** 清理废弃批次:删除本批次已上传文件、解析缓存与 file_info 记录(仅用于未进入清洗的批次) */
    public void clearUploadBatch(Long batchId) {
    if (batchId == null) {
        return;
    }
    // 1) 解析缓存 tmp/{batchId}
    try {
        FileUtil.del(PathConst.TMP_PATH.resolve(batchId.toString()));
    } catch (Exception e) {
        log.warn("清理批次解析缓存失败, batchId={}", batchId, e);
    }
    // 2) 已上传文件 workspace/{caseId}/upload/{batchId}
    try {
        if (StateManager.instance().isCaseOpened()) {
            Long caseId = StateManager.instance().getCaseId().longValue();
            FileUtil.del(PathConst.WORKSPACE.resolve(String.valueOf(caseId))
                    .resolve(UPLOAD_DIR_NAME).resolve(batchId.toString()));
        }
    } catch (Exception e) {
        log.warn("清理批次上传文件失败, batchId={}", batchId, e);
    }
    // 3) 本批次 file_info 记录(安全兜底;正常废弃批次无记录)
    fileInfoMapper.delete(Wrappers.lambdaQuery(FileInfo.class).eq(FileInfo::getBatchId, batchId));
    }
    

文件:e:\workspace\zsjz-ai\ai-server\src\main\java\com\zsjz\ai\module\dm\controller\DmController.java

  • 新增(参照既有 cancelClean GET 风格,幂等):

    /** 清理废弃批次:删除本批次已上传文件、解析缓存与记录(上传页放弃离开时调用) */
    @GetMapping("/clearUploadBatch")
    public Result<Void> clearUploadBatch(Long batchId) {
    dmService.clearUploadBatch(batchId);
    return Result.succeed();
    }
    

文件:e:\workspace\zsjz-ai\ai-frontend\src\case\api\govern\governApi.ts

  • 新增(放在 cancelClean 附近):

    /** 清理废弃批次:删除本批次已上传文件、解析缓存与记录(未进入清洗而离开上传页时调用) */
    export const clearUploadBatch = (params: { batchId?: ApiId | number }) =>
    defHttp.get<void>({ url: adminPath + '/dm/clearUploadBatch', params });
    

2. 新增全屏上传页 upload.vue

文件:新建 e:\workspace\zsjz-ai\ai-frontend\src\case\views\data\upload.vue(组件命名 useDesign('case-upload'),prefix jeesite-case-upload)

批次生命周期(核心新增逻辑):

  • 挂载时生成批次 id(“进页即生成,交给后端”):不依赖后端响应,本地生成 Long:

    const createUploadBatchId = () => Date.now() * 1000000 + Math.floor(Math.random() * 1000000); // 约 1.7e18,Long 安全
    

startUpload 中每个文件统一传 { batchId: uploadBatchId.value },保证本页全部文件同批(后端对非空 batchId 复用)。删除原“首次响应回填 batchId”逻辑。

  • 离开拦截:
    • 返回按钮:若 isUploading || selectedFiles.length > 0 || resultTrees.length > 0(本页有上传活动),用 Modal.confirm 提示「上传解析进行中/存在未处理文件,离开将清理本批次已上传文件与解析结果,是否继续?」;取消 → 留在本页;确认 → await clearUploadBatch({ batchId })(失败仅 warning 不阻塞)→ 清空 sessionStorage('case_clean_file_infos')(仅当其中结果全属本批次)→ go(PageEnum.BASE_HOME, true) + emitter.emit('case-data:refresh')。
    • onBeforeRouteLeave 守卫兜底浏览器返回/标签页关闭:仅当 allowedToLeave 为 true(点「下一步」或已确认清理后的程序化跳转)时放行,否则中止并弹同类确认。
    • 「下一步:数据匹配」(≥1 个文件解析成功才可点):设 allowedToLeave=true,写 sessionStorage('case_clean_file_infos') + emitter.emit('case-import-file:refresh') + go(PageEnum.BASE_MATCH),不调用清理接口。

页面结构:

  • 页头(仿 match.vue):左=LeftOutlined 返回按钮 + 标题「文件上传」+ 副标题「上传并解析 xls / xlsx / csv 文件,解析结果实时展示在右侧」;右上=主按钮「下一步:数据匹配」(无成功解析时 disabled)。
  • 左右布局:页面容器沿用 index.vue 全出血样式(background:#f0f2f5; width:calc(100% + 24px); margin:0 -12px; padding:12px),内部 display:flex; gap:12px; height:100%; min-height:0。
    • 左面板(flex:0 0 460px 白底卡片):
    • 顶部:胶囊切换「选择文件 / 选择文件夹」(复用 .upload-mode-switch 样式,默认取路由 query mode,缺省 single)+ 「选择文件/选择文件夹」主按钮触发隐藏 input;保留拖拽区(复用 .upload-dropzone)。
    • 中部:.upload-file-list(复用原样式)文件列表:文件名 + 大小 + 状态徽标(待上传 / 解析中 n% / ✓ / ✗)。
    • 底部:整体进度条 .upload-overall-progress + 「开始上传」主按钮(无文件或上传中禁用)+ 「清空」链接。
    • 右面板(flex:1 白底卡片):标题「解析结果(实时)」+ BasicTable:
    • :is-tree-table="true"、:children-column-name="'children'"、row-key="id"、:pagination="false"、:data-source="resultTrees"。
    • 列:名称(根行=fileName,子行=sheetName/tableName,有子节点自动显示展开箭头)、大小(根行 fileSize)、数据量 dataNum、成功 successNum、失败 failNum、状态(fileStatus 映射彩色标签:成功 #16a34a/失败 #dc2626/其他灰)。

逻辑(从 index.vue 移植并改造):

  • 状态:uploadMode、uploadBatchId、selectedFiles、uploadStatuses/uploadProgressMap/overallProgress/isUploading/dragOver、resultTrees(实时结果数组)、allowedToLeave。
  • 文件工具:移植 formatFileSize/isValidUploadFile/filterValidFiles/handleDragOver/handleDragLeave/collectFilesFromDataTransfer/traverseDirectory/handleFileInputChange/handleFolderInputChange。
  • startUpload:逐个 preFileUpload(file, onProgress, { batchId: uploadBatchId.value });每份文件返回的 FileInfo[] 追加到 resultTrees(右侧实时刷新)并同步累积写 sessionStorage('case_clean_file_infos');整体进度 = 已完成文件数占比;全部结束显示「下一步」。
  • mapStatus:从 match.vue L185 移植(SUCCESS/COMPLETE→成功、FAIL→失败、PASSWORD/FORMAT/EMPTY→对应文案)。

样式:复用 index.vue L5810-L6060 上传样式(upload-mode-switch / upload-dropzone / upload-file-list / upload-file-item / upload-overall-progress),补充双栏布局与右面板表格样式;沿用 L6076 起 html[data-theme='dark'] 暗色覆盖写法。

3. 注册路由与页面枚举

  • e:\workspace\zsjz-ai\ai-frontend\src\core\enums\pageEnum.ts:新增 BASE_UPLOAD = '/data/upload'。
  • e:\workspace\zsjz-ai\ai-frontend\src\core\router\routes\index.ts:DataRoute.children 新增:

    {
    path: 'upload',
    name: 'CaseDataUpload',
    component: () => import('@/case/views/data/upload.vue'),
    meta: { title: '文件上传', hideMenu: true, hideTab: true, currentActiveMenu: PageEnum.BASE_HOME },
    },
    

4. 改造 index.vue:去掉弹框、改为跳转

  • handleUploadNavigate(L920-L926)改为 go({ path: PageEnum.BASE_UPLOAD, query: { mode: mode || 'single' } });删除 openUpload/handleUploadModeChange/handleUploadChange/handleUploadOk/handleUploadCancel。
  • 删除模板弹框块 L141-L261(BasicModal)及 .upload-modal-body 相关样式(L5810-L6060、L6063-L6074 媒体查询)。
  • 删除脚本上传状态与函数:uploadVisible/uploadMode/uploadBizKey/uploadLoadTime/fileInputRef/folderInputRef/selectedFiles/uploadResults/uploadProgressMap/uploadStatuses/isUploading/overallProgress/dragOver/registerUploadModal、formatFileSize/isValidUploadFile/filterValidFiles/handleDragOver/handleDragLeave/traverseDirectory/collectFilesFromDataTransfer/handleDrop/handleFileInputChange/handleFolderInputChange/handleTriggerFileSelect/clearSelectedFiles/startUpload、VALID_UPLOAD_EXTS。
  • 清理因此未再使用的导入:preFileUpload(governApi)、Upload(antd)、BasicModal, useModal、FileInfo 类型。保留 buildUUID(L1604/L1674 仍用)、createMessage、Modal(模板创建弹框用)。

Assumptions & Decisions

  1. 批次生成:前端进入上传页即用时间戳+随机数生成 Long batchId 并在每个文件请求中回传;后端非空即复用,无需新增“创建批次”接口。生成值上限约 1.7e18 < Long.MAX,满足 Long 转换。
  2. 清理时机:仅当用户在本页确认离开/放弃且未进入清洗(未点「下一步」)时调用 clearUploadBatch;点「下一步」进入 /data/match 是正常流程,不清理。
  3. 保留文件夹上传:原弹框「选择文件 / 选择文件夹」能力在新页左侧顶部保留,默认 single。
  4. 不再自动跳转:解析完成后由页头「下一步:数据匹配」手动进入 match 页,右侧可先行查看解析结果(有成功解析才可点)。
  5. “实时”语义:解析为逐文件接口(无流式中间态),右侧 treetable 在每份文件解析完成瞬间追加对应结果树即满足“实时显示解析结果”;上传中进度在左侧列表实时显示。
  6. 树结构:后端已返回 pid=0 根 + children sheet 的树;前端直接追加为表格树节点(childrenColumnName='children'、row-key='id')。
  7. 暗色主题照抄 index.vue 写法;不动既有 /dm/preFileUpload 接口语义。

Verification

  1. 后端:mvn compile -f pom.xml 编译通过;GET /dm/clearUploadBatch?batchId=xxx 返回成功,且对应 workspace/{caseId}/upload/{batchId}、tmp/{batchId} 与 file_info 记录被清除(清理前后比对目录与 DB)。
  2. 前端:用 IDEA MCP 编译验证(tools.build_project 或 npm run build),无未使用导入告警。
  3. 手工验证:
    • 案件数据页点击「选择文件」→ 打开 /data/upload 全屏页(不再弹框),进入即生成批次 id(可经网络请求确认所有文件携带同一 batchId)。
    • 左侧选多个 xls/xlsx/csv → 文件列表与大小正确、状态徽标实时变化(待上传→n%→✓/✗)、整体进度到 100%;文件夹模式递归收集正常。
    • 每完成一个文件,右侧 treetable 立即出现该文件根节点与 sheet 子节点(可展开),状态/数据量/成功/失败列正确;失败文件仍展示且状态为失败。
    • 上传中(或已选未上传)点返回 → 弹确认提示;确认后调用 clearUploadBatch 并返回 /data/index,后端目录与记录被清理;取消则留在本页。
    • 全部完成后点「下一步:数据匹配」→ 进入 /data/match,结果与改动前一致,且不触发清理。
    • 亮/暗主题、≥1280px 双栏布局正常。