# 案件数据 · 选择文件 → 全屏上传页改造方案 ## 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](file:///e:/workspace/zsjz-ai/ai-frontend/src/case/views/data/index.vue#L28-L30) 头部「选择文件」按钮 → `handleUploadNavigate('single')` → `openUpload('single')` 打开 `BasicModal`(模板 L141-L261)。 - 弹框内 UI(整体移植到新页):模式切换 L153-L171、隐藏 `` L175-L190、拖拽区 L192-L215、文件列表+状态/进度 L218-L253、「开始上传并解析」L254-L258。 - 上传逻辑 `startUpload`(L2604-L2665):逐个 `preFileUpload(file, onProgress, { batchId })`(POST `api/dm/preFileUpload` multipart)返回 `List` **树**(根节点 pid=0=文件、children=各 sheet);全部完成后写 `sessionStorage('case_clean_file_infos')` 并 `go(PageEnum.BASE_MATCH)`。当前 batchId 只在**首次上传成功后**从响应里回填,后续沿用——不满足“进页即生成”。 - 解析结果字段([FileInfo.java](file:///e:/workspace/zsjz-ai/ai-server/src/main/java/com/zsjz/ai/common/model/dm/entity/FileInfo.java)):`id/pid/fileName/sheetName/tableName/fileStatus/dataNum/fileSize/successNum/failNum/batchId/createDateTime/children`;`fileStatus` 含读取失败/格式不正确/读取完成/加密/模板匹配失败/模板匹配成功等。 - 路由:[routes/index.ts](file:///e:/workspace/zsjz-ai/ai-frontend/src/core/router/routes/index.ts#L38-L98) `DataRoute.children` 注册全屏页(match/cleaning 用 `meta.hideMenu/hideTab/currentActiveMenu=BASE_HOME`);[pageEnum.ts](file:///e:/workspace/zsjz-ai/ai-frontend/src/core/enums/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](file:///e:/workspace/zsjz-ai/ai-server/src/main/java/com/zsjz/ai/module/dm/service/DmService.java#L140-L167)):`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` - 新增方法: ```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 风格,幂等): ```java /** 清理废弃批次:删除本批次已上传文件、解析缓存与记录(上传页放弃离开时调用) */ @GetMapping("/clearUploadBatch") public Result clearUploadBatch(Long batchId) { dmService.clearUploadBatch(batchId); return Result.succeed(); } ``` **文件**:`e:\workspace\zsjz-ai\ai-frontend\src\case\api\govern\governApi.ts` - 新增(放在 `cancelClean` 附近): ```ts /** 清理废弃批次:删除本批次已上传文件、解析缓存与记录(未进入清洗而离开上传页时调用) */ export const clearUploadBatch = (params: { batchId?: ApiId | number }) => defHttp.get({ 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: ```ts 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` 新增: ```ts { 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 双栏布局正常。