From 58e022fe642b2b661d9cbc037192cf1657527643 Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Thu, 12 Mar 2026 18:42:41 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=91=98=E5=B7=A5=E8=B5=84?= =?UTF-8?q?=E4=BA=A7=E7=BB=B4=E6=8A=A4=E5=89=8D=E7=AB=AF=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...set-maintenance-frontend-implementation.md | 540 ++++++++++---- ruoyi-ui/src/api/ccdiAssetInfo.js | 35 + ruoyi-ui/src/views/ccdiBaseStaff/index.vue | 692 +++++++++++++++++- .../unit/employee-asset-api-contract.test.js | 44 ++ .../unit/employee-asset-import-ui.test.js | 27 + .../employee-asset-maintenance-layout.test.js | 45 ++ .../unit/employee-asset-submit-flow.test.js | 29 + 7 files changed, 1256 insertions(+), 156 deletions(-) create mode 100644 ruoyi-ui/src/api/ccdiAssetInfo.js create mode 100644 ruoyi-ui/tests/unit/employee-asset-api-contract.test.js create mode 100644 ruoyi-ui/tests/unit/employee-asset-import-ui.test.js create mode 100644 ruoyi-ui/tests/unit/employee-asset-maintenance-layout.test.js create mode 100644 ruoyi-ui/tests/unit/employee-asset-submit-flow.test.js diff --git a/docs/plans/2026-03-12-employee-asset-maintenance-frontend-implementation.md b/docs/plans/2026-03-12-employee-asset-maintenance-frontend-implementation.md index 3e3ac16..972ee13 100644 --- a/docs/plans/2026-03-12-employee-asset-maintenance-frontend-implementation.md +++ b/docs/plans/2026-03-12-employee-asset-maintenance-frontend-implementation.md @@ -1,31 +1,31 @@ -# Employee Asset Maintenance Frontend Implementation Plan +# 员工资产信息维护前端实施计划 > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. -**Goal:** Extend the employee maintenance page so users can maintain employee and family-member assets inside the add/edit dialog, view all assets in the detail dialog, and import asset data with automatic employee-family ownership resolution and a dedicated failure-record entry. +**Goal:** 在员工信息维护页面中增加员工资产的新增、编辑、删除、详情展示、导入和失败记录查看能力,并保持现有员工导入交互不回归。 -**Architecture:** Keep the existing employee page and API module in place, and add a nested asset-table editing experience within `ccdiBaseStaff/index.vue`. Frontend users edit the actual holder ID card as `personId`, while the backend derives `family_id` from the current employee during form save and from import resolution during Excel import. Reuse the existing async import interaction model, but isolate all asset-import state, storage keys, dialogs, and button copy from the original employee-import flow. +**Architecture:** 保持 `ccdiBaseStaff` 作为唯一页面入口,在现有员工新增、编辑、详情弹窗中扩展 `assetInfoList` 子表。资产导入复用当前员工导入的弹窗上传、异步轮询、localStorage 状态恢复、失败记录分页查看模型,但所有资产导入状态、文案、存储 key、轮询方法和失败记录弹窗都单独维护,避免与员工导入互相污染。 -**Tech Stack:** Vue 2, Element UI 2, RuoYi request wrapper, scoped SFC styles, Node-based source assertions or existing frontend unit checks +**Tech Stack:** Vue 2, Element UI, 若依前端脚手架, Axios request 封装, `npm run build:prod` --- -### Task 1: Extend the frontend API layer for asset import +### Task 1: 扩展前端 API 封装员工资产导入能力 **Files:** - Modify: `ruoyi-ui/src/api/ccdiBaseStaff.js` - Create: `ruoyi-ui/src/api/ccdiAssetInfo.js` - Test: `ruoyi-ui/tests/unit/employee-asset-api-contract.test.js` -**Step 1: Write the failing test** +**Step 1: 先写失败校验脚本** -Add a focused source-based test that asserts: +新增一个轻量级源码断言脚本,校验以下约束: -- employee detail save payload can include `assetInfoList` -- a dedicated asset import API module exists -- asset import API exposes template, upload, status, and failure methods +- `ccdiAssetInfo.js` 文件存在 +- 资产导入 API 包含下载模板、提交导入、查询状态、查询失败记录 +- 员工新增和编辑接口允许传递 `assetInfoList` -**Step 2: Run test to verify it fails** +**Step 2: 运行校验确认失败** Run: @@ -33,20 +33,27 @@ Run: node ruoyi-ui/tests/unit/employee-asset-api-contract.test.js ``` -Expected: FAIL because the asset API module does not exist yet. +Expected: FAIL,因为资产导入 API 文件尚不存在。 -**Step 3: Implement the minimal API surface** +**Step 3: 编写最小 API 封装** -Create `ccdiAssetInfo.js` with: +在 `ccdiAssetInfo.js` 中新增: - `importAssetTemplate` - `importAssetData` - `getAssetImportStatus` - `getAssetImportFailures` -Keep `ccdiBaseStaff.js` for employee CRUD. +接口路径统一使用: -**Step 4: Run the test again** +- `/ccdi/assetInfo/importTemplate` +- `/ccdi/assetInfo/importData` +- `/ccdi/assetInfo/importStatus/{taskId}` +- `/ccdi/assetInfo/importFailures/{taskId}` + +`ccdiBaseStaff.js` 继续保留员工增删改查,不把资产导入接口混入员工 API 文件。 + +**Step 4: 再次运行校验** Run: @@ -56,24 +63,88 @@ node ruoyi-ui/tests/unit/employee-asset-api-contract.test.js Expected: PASS -### Task 2: Add a regression test for the new employee asset UI structure +**Step 5: 提交** + +```bash +git add ruoyi-ui/src/api/ccdiBaseStaff.js ruoyi-ui/src/api/ccdiAssetInfo.js ruoyi-ui/tests/unit/employee-asset-api-contract.test.js +git commit -m "新增员工资产前端接口封装" +``` + +### Task 2: 扩展员工表单模型,支持聚合 `assetInfoList` **Files:** -- Create: `ruoyi-ui/tests/unit/employee-asset-maintenance-layout.test.js` - Modify: `ruoyi-ui/src/views/ccdiBaseStaff/index.vue` +- Test: `ruoyi-ui/tests/unit/employee-asset-submit-flow.test.js` -**Step 1: Write the failing test** +**Step 1: 先写失败校验脚本** -Assert the employee page contains: +校验以下行为: -- an “导入资产信息” trigger -- a “查看员工资产导入失败记录” trigger hook -- an “资产信息” section inside the add/edit dialog -- an add-asset action -- a detail-table section for assets -- a holder-ID-card input for asset rows +- `reset()` 默认初始化 `assetInfoList: []` +- `handleAdd()` 打开新增弹窗时带空资产列表 +- `handleUpdate()` 回显详情时保留接口返回的 `assetInfoList` +- `submitForm()` 提交前会附带 `assetInfoList` -**Step 2: Run test to verify it fails** +**Step 2: 运行校验确认失败** + +Run: + +```bash +node ruoyi-ui/tests/unit/employee-asset-submit-flow.test.js +``` + +Expected: FAIL,因为当前 `form` 里还没有资产字段。 + +**Step 3: 编写最小表单改造** + +调整 `index.vue` 中以下逻辑: + +- `form` 默认值增加 `assetInfoList: []` +- `reset()` 同步重置 `assetInfoList` +- `handleAdd()` 明确初始化空数组 +- `handleUpdate()` 和 `handleDetail()` 对返回值中的 `assetInfoList` 做空值兜底 +- 新增 `normalizeAssetInfoList()`,提交前过滤全空行 + +注意: + +- 前端不传 `familyId` +- 前端保留用户输入的 `personId` +- 不在前端生成 `assetId` + +**Step 4: 再次运行校验** + +Run: + +```bash +node ruoyi-ui/tests/unit/employee-asset-submit-flow.test.js +``` + +Expected: PASS + +**Step 5: 提交** + +```bash +git add ruoyi-ui/src/views/ccdiBaseStaff/index.vue ruoyi-ui/tests/unit/employee-asset-submit-flow.test.js +git commit -m "扩展员工表单资产聚合字段" +``` + +### Task 3: 在新增和编辑弹窗中加入“资产信息”可编辑子表 + +**Files:** +- Modify: `ruoyi-ui/src/views/ccdiBaseStaff/index.vue` +- Test: `ruoyi-ui/tests/unit/employee-asset-maintenance-layout.test.js` + +**Step 1: 先写失败校验脚本** + +断言页面模板出现以下结构: + +- “资产信息”分区标题 +- “新增资产”按钮 +- 资产子表或空状态容器 +- 资产实际持有人身份证号输入框 +- 每行删除按钮 + +**Step 2: 运行校验确认失败** Run: @@ -81,30 +152,18 @@ Run: node ruoyi-ui/tests/unit/employee-asset-maintenance-layout.test.js ``` -Expected: FAIL because the current page lacks all asset-maintenance UI. +Expected: FAIL,因为当前员工弹窗只有基本信息。 -### Task 3: Add the editable asset section to the employee dialog +**Step 3: 编写最小弹窗结构** -**Files:** -- Modify: `ruoyi-ui/src/views/ccdiBaseStaff/index.vue` -- Test: `ruoyi-ui/tests/unit/employee-asset-maintenance-layout.test.js` +在基本信息区域下方新增“资产信息”分区,并增加以下辅助方法: -**Step 1: Add asset form state** +- `createEmptyAssetRow()` +- `handleAddAsset()` +- `handleRemoveAsset(index)` +- `hasAssetContent(row)` -Extend `form` defaults with `assetInfoList: []`. - -**Step 2: Add UI helpers** - -Add methods for: - -- create an empty asset row -- add one asset row -- remove one asset row -- normalize empty asset rows before submit - -**Step 3: Add the asset editing table** - -Render a section below basic employee info with columns for: +子表字段包含: - `personId` - `assetMainType` @@ -117,9 +176,15 @@ Render a section below basic employee info with columns for: - `valuationDate` - `assetStatus` - `remarks` -- row delete action +- `operation` -**Step 4: Run the layout test** +交互要求: + +- 允许多行新增 +- 每行支持删除 +- 空列表时显示“暂无资产信息,请点击新增资产” + +**Step 4: 再次运行校验** Run: @@ -129,90 +194,125 @@ node ruoyi-ui/tests/unit/employee-asset-maintenance-layout.test.js Expected: PASS -### Task 4: Add detail dialog asset display +**Step 5: 提交** + +```bash +git add ruoyi-ui/src/views/ccdiBaseStaff/index.vue ruoyi-ui/tests/unit/employee-asset-maintenance-layout.test.js +git commit -m "新增员工资产编辑子表" +``` + +### Task 4: 为资产子表补充前端校验与填写提示 + +**Files:** +- Modify: `ruoyi-ui/src/views/ccdiBaseStaff/index.vue` + +**Step 1: 增加资产行字段校验** + +至少校验以下前端规则: + +- `personId` 格式为合法身份证号 +- `assetMainType`、`assetSubType`、`assetName`、`currentValue`、`assetStatus` 为必填 +- 金额字段允许为空,但填写时必须是合法数字 +- 日期字段使用 `value-format="yyyy-MM-dd"` + +**Step 2: 增加交互提示** + +在资产分区标题或字段提示中明确说明: + +- `personId` 表示资产实际持有人身份证号 +- 如果 `personId` 等于当前员工身份证号,则视为员工本人资产 +- 如果 `personId` 不等于当前员工身份证号,则视为员工亲属资产 + +**Step 3: 保持前端不做归属推导** + +不要在前端根据 `personId` 生成 `familyId`,该逻辑完全交给后端。 + +**Step 4: 构建验证** + +Run: + +```bash +cd ruoyi-ui +npm run build:prod +``` + +Expected: 构建通过,无模板语法错误。 + +**Step 5: 提交** + +```bash +git add ruoyi-ui/src/views/ccdiBaseStaff/index.vue +git commit -m "补充员工资产表单校验与提示" +``` + +### Task 5: 在详情弹窗中展示员工全部资产信息 **Files:** - Modify: `ruoyi-ui/src/views/ccdiBaseStaff/index.vue` - Test: `ruoyi-ui/tests/unit/employee-asset-maintenance-layout.test.js` -**Step 1: Add an asset table section to the detail dialog** +**Step 1: 扩展详情弹窗布局** -Render employee assets below the existing basic info card. +在现有“基本信息”卡片下方新增“资产信息”区块。 -**Step 2: Add an empty state** +**Step 2: 增加只读资产表格** -If `employeeDetail.assetInfoList` is empty, show “暂无资产信息”. +展示字段: -**Step 3: Keep existing employee detail fields intact** +- `personId` +- `ownerType` +- `assetMainType` +- `assetSubType` +- `assetName` +- `ownershipRatio` +- `currentValue` +- `assetStatus` +- `remarks` -Do not regress name, staff ID, department, ID card, phone, hire date, or status display. +其中 `ownerType` 可在前端根据 `personId === employeeDetail.idCard` 计算展示“本人”或“亲属”。 -### Task 5: Wire add/edit/detail requests to aggregate employee data +**Step 3: 增加空状态** -**Files:** -- Modify: `ruoyi-ui/src/views/ccdiBaseStaff/index.vue` -- Test: `ruoyi-ui/tests/unit/employee-asset-submit-flow.test.js` +当 `employeeDetail.assetInfoList` 为空时显示“暂无资产信息”。 -**Step 1: Write the failing test** +**Step 4: 回归校验** -Assert that: +再次确认原有基本信息展示不受影响: -- `handleAdd` initializes an empty `assetInfoList` -- `handleUpdate` stores returned `assetInfoList` -- `submitForm` posts the employee object with `assetInfoList` -- empty asset rows are filtered before submit +- 姓名 +- 柜员号 +- 所属部门 +- 身份证号 +- 电话 +- 入职时间 +- 状态 -**Step 2: Run test to verify it fails** - -Run: +**Step 5: 提交** ```bash -node ruoyi-ui/tests/unit/employee-asset-submit-flow.test.js +git add ruoyi-ui/src/views/ccdiBaseStaff/index.vue ruoyi-ui/tests/unit/employee-asset-maintenance-layout.test.js +git commit -m "新增员工资产详情展示" ``` -Expected: FAIL because submit logic does not process asset rows yet. - -**Step 3: Implement minimal submit behavior** - -Update: - -- `reset` -- `handleAdd` -- `handleUpdate` -- `handleDetail` -- `submitForm` - -so they all preserve `assetInfoList`. - -Ensure `submitForm` filters empty asset rows but keeps valid `personId` values intact instead of overwriting them on the frontend. - -**Step 4: Run the submit-flow test** - -Run: - -```bash -node ruoyi-ui/tests/unit/employee-asset-submit-flow.test.js -``` - -Expected: PASS - -### Task 6: Add dedicated asset import UI state and upload dialog +### Task 6: 增加独立的资产导入入口、弹窗和状态模型 **Files:** - Modify: `ruoyi-ui/src/views/ccdiBaseStaff/index.vue` - Test: `ruoyi-ui/tests/unit/employee-asset-import-ui.test.js` -**Step 1: Write the failing test** +**Step 1: 先写失败校验脚本** -Verify the page defines dedicated asset-import state: +校验页面存在以下独立状态: -- independent upload dialog object -- independent polling timer -- independent current task ID -- independent failure dialog state -- asset-specific localStorage key +- 资产导入按钮“导入资产信息” +- 资产失败记录按钮“查看员工资产导入失败记录” +- 独立 `assetUpload` 弹窗对象 +- 独立 `assetPollingTimer` +- 独立 `assetCurrentTaskId` +- 独立 `assetFailureDialogVisible` +- 独立 localStorage key -**Step 2: Run test to verify it fails** +**Step 2: 运行校验确认失败** Run: @@ -220,24 +320,38 @@ Run: node ruoyi-ui/tests/unit/employee-asset-import-ui.test.js ``` -Expected: FAIL because only employee import state exists today. +Expected: FAIL,因为当前页面只有员工导入状态。 -**Step 3: Implement the asset import dialog** +**Step 3: 编写最小导入 UI** -Add: +在按钮区新增: -- “导入资产信息” button -- asset upload dialog with template download -- asset-specific upload methods -- independent polling and completion handlers +- “导入资产信息” +- “查看员工资产导入失败记录” -Keep the asset import copy clear that the system will auto-match the employee family and auto-fill ownership based on the holder ID card. +在 `data()` 中新增一套独立状态,例如: -**Step 4: Distinguish failure record copy** +- `assetUpload` +- `assetImportResultVisible` +- `assetImportResultContent` +- `assetPollingTimer` +- `assetShowFailureButton` +- `assetCurrentTaskId` +- `assetFailureDialogVisible` +- `assetFailureList` +- `assetFailureLoading` +- `assetFailureTotal` +- `assetFailureQueryParams` -Ensure the failure entry text is exactly `查看员工资产导入失败记录`. +**Step 4: 新增资产导入弹窗** -**Step 5: Run the import UI test** +复用员工导入交互结构,但文案调整为: + +- 标题:`员工资产数据导入` +- 模板下载按钮:下载员工资产模板 +- 提示文案:系统将根据 `personId/person_id` 自动识别归属员工 + +**Step 5: 再次运行校验** Run: @@ -247,19 +361,75 @@ node ruoyi-ui/tests/unit/employee-asset-import-ui.test.js Expected: PASS -### Task 7: Add the asset failure record dialog and isolate storage +**Step 6: 提交** + +```bash +git add ruoyi-ui/src/views/ccdiBaseStaff/index.vue ruoyi-ui/tests/unit/employee-asset-import-ui.test.js +git commit -m "新增员工资产导入交互状态" +``` + +### Task 7: 接通资产导入上传、轮询、状态恢复和失败记录查询 **Files:** - Modify: `ruoyi-ui/src/views/ccdiBaseStaff/index.vue` -- Test: `ruoyi-ui/tests/unit/employee-asset-import-ui.test.js` +- Check: `ruoyi-ui/src/api/ccdiAssetInfo.js` -**Step 1: Add an asset failure dialog** +**Step 1: 增加资产导入方法** -The dialog title must be `员工资产导入失败记录`. +至少实现以下方法: -**Step 2: Add asset failure columns** +- `handleAssetImport()` +- `handleAssetImportDialogClose()` +- `importAssetTemplate()` +- `handleAssetFileUploadProgress()` +- `handleAssetFileSuccess()` +- `startAssetImportStatusPolling(taskId)` +- `handleAssetImportComplete(statusResult)` +- `viewAssetImportFailures()` +- `getAssetFailureList()` -Show: +**Step 2: 增加独立 localStorage 管理** + +新增一组与员工导入隔离的方法: + +- `saveAssetImportTaskToStorage()` +- `getAssetImportTaskFromStorage()` +- `clearAssetImportTaskFromStorage()` +- `restoreAssetImportState()` +- `getLastAssetImportTooltip()` +- `clearAssetImportHistory()` + +建议 key 使用: + +```text +employee_asset_import_last_task +``` + +**Step 3: 严格区分员工导入与资产导入** + +保持现有员工导入逻辑不回归: + +- 员工导入仍使用 `employee_import_last_task` +- 员工失败记录仍显示“查看导入失败记录” +- 资产失败记录按钮固定显示“查看员工资产导入失败记录” + +**Step 4: 处理资产导入完成通知** + +通知文案应显式区分: + +- `员工资产导入任务已提交` +- `员工资产导入完成` +- `成功 X 条,失败 Y 条` + +**Step 5: 接通失败记录弹窗** + +弹窗标题为: + +```text +员工资产导入失败记录 +``` + +表格字段展示: - `familyId` - `personId` @@ -268,47 +438,84 @@ Show: - `assetName` - `errorMessage` -**Step 3: Use a dedicated localStorage key** +若后端失败记录不返回 `familyId`,前端允许显示为空,不自行推导。 -Keep employee import history under `employee_import_last_task` and store asset import history under a new asset-specific key. +**Step 6: 构建验证** -**Step 4: Keep the original employee import flow unchanged** +Run: -Do not rename or regress the existing employee import controls and methods unless needed for clear separation. +```bash +cd ruoyi-ui +npm run build:prod +``` -### Task 8: Refine styles for the embedded asset table +Expected: 构建通过,资产导入方法和模板绑定完整。 + +**Step 7: 提交** + +```bash +git add ruoyi-ui/src/views/ccdiBaseStaff/index.vue ruoyi-ui/src/api/ccdiAssetInfo.js +git commit -m "接通员工资产导入轮询与失败记录" +``` + +### Task 8: 调整样式与可读性,避免资产子表破坏现有弹窗布局 **Files:** - Modify: `ruoyi-ui/src/views/ccdiBaseStaff/index.vue` -- Test: `ruoyi-ui/tests/unit/employee-asset-maintenance-layout.test.js` -**Step 1: Add focused scoped styles** +**Step 1: 调整编辑弹窗样式** -Style: +补充以下样式: -- asset section header row -- editable asset table -- empty asset state -- detail asset block +- 资产分区标题 +- 资产表格容器 +- 空状态 +- 表头提示文案 +- 详情页资产区块 -**Step 2: Preserve the existing employee dialog layout** +**Step 2: 保持桌面端可读性** -Ensure basic info layout remains readable on desktop and mobile widths. +检查 `1200px` 编辑弹窗在加入资产表后是否仍可读,必要时: -Also add clear inline hints so users understand: +- 增加横向滚动容器 +- 调整列宽 +- 对备注列使用 `show-overflow-tooltip` -- `personId` is the actual holder ID card -- if it equals the employee ID card, the asset belongs to the employee -- if it differs, the asset is treated as a family-member asset +**Step 3: 检查窄屏降级** -### Task 9: Verify the frontend changes end to end +至少保证: + +- 弹窗内部不会内容挤压到不可操作 +- 资产操作列始终可点击 +- 详情表格可以横向滚动 + +**Step 4: 构建验证** + +Run: + +```bash +cd ruoyi-ui +npm run build:prod +``` + +Expected: 样式改动不影响构建。 + +**Step 5: 提交** + +```bash +git add ruoyi-ui/src/views/ccdiBaseStaff/index.vue +git commit -m "优化员工资产页面样式与布局" +``` + +### Task 9: 执行前端联调与最终验证 **Files:** -- Modify: `ruoyi-ui/src/views/ccdiBaseStaff/index.vue` -- Modify: `ruoyi-ui/src/api/ccdiAssetInfo.js` -- Modify: `ruoyi-ui/src/api/ccdiBaseStaff.js` +- Check: `ruoyi-ui/src/views/ccdiBaseStaff/index.vue` +- Check: `ruoyi-ui/src/api/ccdiBaseStaff.js` +- Check: `ruoyi-ui/src/api/ccdiAssetInfo.js` +- Check: `ruoyi-ui/tests/unit/*.test.js` -**Step 1: Run focused frontend tests** +**Step 1: 运行源码断言脚本** Run: @@ -319,23 +526,54 @@ node ruoyi-ui/tests/unit/employee-asset-submit-flow.test.js node ruoyi-ui/tests/unit/employee-asset-import-ui.test.js ``` -Expected: all focused tests pass. +Expected: 全部 PASS -**Step 2: Run production build verification** +**Step 2: 运行生产构建** Run: ```bash +cd ruoyi-ui npm run build:prod ``` -Workdir: `ruoyi-ui` +Expected: 构建成功 -Expected: build succeeds without Vue template or script errors. +**Step 3: 启动本地前端联调** -**Step 3: Commit** +Run: ```bash -git add ruoyi-ui/src/api/ccdiAssetInfo.js ruoyi-ui/src/api/ccdiBaseStaff.js ruoyi-ui/src/views/ccdiBaseStaff/index.vue ruoyi-ui/tests/unit docs/plans/2026-03-12-employee-asset-maintenance-frontend-implementation.md +cd ruoyi-ui +npm run dev +``` + +Expected: 本地员工信息维护页面可访问 + +**Step 4: 手工验证关键场景** + +- 新增员工时添加 1 条本人资产并保存 +- 新增员工时添加 1 条亲属资产并保存 +- 编辑员工时增加、修改、删除资产行 +- 打开员工详情时查看资产信息区 +- 执行员工资产导入并检查轮询通知 +- 导入失败后打开“查看员工资产导入失败记录” +- 刷新页面后验证资产导入失败记录按钮可恢复 +- 清除资产导入历史后验证按钮消失 +- 回归验证原员工导入功能未受影响 + +**Step 5: 整理问题并做最小修复** + +优先排查: + +- `assetInfoList` 回显为空数组时模板报错 +- 资产日期字段格式不一致 +- 两套导入轮询定时器互相覆盖 +- localStorage key 混用导致员工和资产失败记录串数据 + +**Step 6: 最终提交** + +```bash +git add ruoyi-ui/src/api/ccdiBaseStaff.js ruoyi-ui/src/api/ccdiAssetInfo.js ruoyi-ui/src/views/ccdiBaseStaff/index.vue ruoyi-ui/tests/unit docs/plans/2026-03-12-employee-asset-maintenance-frontend-implementation.md git commit -m "新增员工资产信息前端实施计划" ``` diff --git a/ruoyi-ui/src/api/ccdiAssetInfo.js b/ruoyi-ui/src/api/ccdiAssetInfo.js new file mode 100644 index 0000000..60a9470 --- /dev/null +++ b/ruoyi-ui/src/api/ccdiAssetInfo.js @@ -0,0 +1,35 @@ +import request from '@/utils/request' + +// 下载员工资产导入模板 +export function importAssetTemplate() { + return request({ + url: '/ccdi/assetInfo/importTemplate', + method: 'post' + }) +} + +// 导入员工资产 +export function importAssetData(data) { + return request({ + url: '/ccdi/assetInfo/importData', + method: 'post', + data: data + }) +} + +// 查询员工资产导入状态 +export function getAssetImportStatus(taskId) { + return request({ + url: '/ccdi/assetInfo/importStatus/' + taskId, + method: 'get' + }) +} + +// 查询员工资产导入失败记录 +export function getAssetImportFailures(taskId, pageNum, pageSize) { + return request({ + url: '/ccdi/assetInfo/importFailures/' + taskId, + method: 'get', + params: { pageNum, pageSize } + }) +} diff --git a/ruoyi-ui/src/views/ccdiBaseStaff/index.vue b/ruoyi-ui/src/views/ccdiBaseStaff/index.vue index 26aafa1..18b950a 100644 --- a/ruoyi-ui/src/views/ccdiBaseStaff/index.vue +++ b/ruoyi-ui/src/views/ccdiBaseStaff/index.vue @@ -67,6 +67,16 @@ v-hasPermi="['ccdi:employee:import']" >导入 + + 导入资产信息 + 查看导入失败记录 + + + 查看员工资产导入失败记录 + + @@ -138,7 +162,7 @@ /> - +
基本信息
@@ -192,6 +216,86 @@ 离职 +
+ 资产信息 + 新增资产 +
+
+
新增、编辑时无需填写实际持有人身份证号
+
系统会默认带入并保留已有归属信息
+
+ +
+ + 暂无资产信息,请点击新增资产 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
@@ -319,6 +519,10 @@ import { listBaseStaff, updateBaseStaff } from "@/api/ccdiBaseStaff"; +import { + getAssetImportFailures, + getAssetImportStatus +} from "@/api/ccdiAssetInfo"; import {deptTreeSelect} from "@/api/system/user"; import {getToken} from "@/utils/auth"; import Treeselect from "@riophae/vue-treeselect"; @@ -375,6 +579,12 @@ export default { }, // 表单参数 form: {}, + assetStatusOptions: [ + { label: "正常", value: "正常" }, + { label: "冻结", value: "冻结" }, + { label: "处置中", value: "处置中" }, + { label: "报废", value: "报废" } + ], // 表单校验 rules: { name: [ @@ -418,12 +628,17 @@ export default { // 导入结果弹窗 importResultVisible: false, importResultContent: "", + assetImportResultVisible: false, + assetImportResultContent: "", // 轮询定时器 pollingTimer: null, + assetPollingTimer: null, // 是否显示查看失败记录按钮 showFailureButton: false, + assetShowFailureButton: false, // 当前导入任务ID currentTaskId: null, + assetCurrentTaskId: null, // 失败记录对话框 failureDialogVisible: false, failureList: [], @@ -432,6 +647,21 @@ export default { failureQueryParams: { pageNum: 1, pageSize: 10 + }, + assetUpload: { + open: false, + title: "", + isUploading: false, + headers: { Authorization: "Bearer " + getToken() }, + url: process.env.VUE_APP_BASE_API + "/ccdi/assetInfo/importData" + }, + assetFailureDialogVisible: false, + assetFailureList: [], + assetFailureLoading: false, + assetFailureTotal: 0, + assetFailureQueryParams: { + pageNum: 1, + pageSize: 10 } }; }, @@ -445,12 +675,25 @@ export default { return `导入时间: ${this.parseTime(savedTask.saveTime)} | 总数: ${savedTask.totalCount}条 | 成功: ${savedTask.successCount}条 | 失败: ${savedTask.failureCount}条`; } return ''; + }, + lastAssetImportInfo() { + const savedTask = this.getAssetImportTaskFromStorage(); + if (savedTask && savedTask.totalCount) { + return `导入时间: ${this.parseTime(savedTask.saveTime)} | 总数: ${savedTask.totalCount}条 | 成功: ${savedTask.successCount}条 | 失败: ${savedTask.failureCount}条`; + } + return ''; + } + }, + watch: { + 'form.idCard'(newIdCard, oldIdCard) { + this.syncAssetPersonIds(newIdCard, oldIdCard); } }, created() { this.getList(); this.getDeptTree(); this.restoreImportState(); // 新增:恢复导入状态 + this.restoreAssetImportState(); }, beforeDestroy() { // 组件销毁时清除定时器 @@ -458,6 +701,10 @@ export default { clearInterval(this.pollingTimer); this.pollingTimer = null; } + if (this.assetPollingTimer) { + clearInterval(this.assetPollingTimer); + this.assetPollingTimer = null; + } }, methods: { /** @@ -522,6 +769,48 @@ export default { console.error('清除导入任务状态失败:', error); } }, + saveAssetImportTaskToStorage(taskData) { + try { + const data = { + ...taskData, + saveTime: Date.now() + }; + localStorage.setItem('employee_asset_import_last_task', JSON.stringify(data)); + } catch (error) { + console.error('保存资产导入任务状态失败:', error); + } + }, + getAssetImportTaskFromStorage() { + try { + const data = localStorage.getItem('employee_asset_import_last_task'); + if (!data) return null; + + const task = JSON.parse(data); + if (!task || !task.taskId) { + this.clearAssetImportTaskFromStorage(); + return null; + } + + const sevenDays = 7 * 24 * 60 * 60 * 1000; + if (task.saveTime && Date.now() - task.saveTime > sevenDays) { + this.clearAssetImportTaskFromStorage(); + return null; + } + + return task; + } catch (error) { + console.error('读取资产导入任务状态失败:', error); + this.clearAssetImportTaskFromStorage(); + return null; + } + }, + clearAssetImportTaskFromStorage() { + try { + localStorage.removeItem('employee_asset_import_last_task'); + } catch (error) { + console.error('清除资产导入任务状态失败:', error); + } + }, /** * 恢复导入状态 * 在created()钩子中调用 @@ -541,6 +830,20 @@ export default { this.showFailureButton = true; } }, + restoreAssetImportState() { + const savedTask = this.getAssetImportTaskFromStorage(); + + if (!savedTask) { + this.assetShowFailureButton = false; + this.assetCurrentTaskId = null; + return; + } + + if (savedTask.hasFailures && savedTask.taskId) { + this.assetCurrentTaskId = savedTask.taskId; + this.assetShowFailureButton = true; + } + }, /** * 获取上次导入的提示信息 * @returns {String} 提示文本 @@ -554,6 +857,15 @@ export default { } return ''; }, + getLastAssetImportTooltip() { + const savedTask = this.getAssetImportTaskFromStorage(); + if (savedTask && savedTask.saveTime) { + const date = new Date(savedTask.saveTime); + const timeStr = this.parseTime(date, '{y}-{m}-{d} {h}:{i}'); + return `上次员工资产导入: ${timeStr}`; + } + return ''; + }, /** * 清除导入历史记录 * 用户手动触发 @@ -571,6 +883,19 @@ export default { this.$message.success('已清除'); }).catch(() => {}); }, + clearAssetImportHistory() { + this.$confirm('确认清除上次员工资产导入记录?', '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + this.clearAssetImportTaskFromStorage(); + this.assetShowFailureButton = false; + this.assetCurrentTaskId = null; + this.assetFailureDialogVisible = false; + this.$message.success('已清除'); + }).catch(() => {}); + }, /** 查询员工列表 */ getList() { this.loading = true; @@ -615,10 +940,135 @@ export default { phone: null, hireDate: null, status: "0", - relatives: [] + relatives: [], + assetInfoList: [] }; this.resetForm("form"); }, + normalizeAssetInfoList() { + const assetInfoList = Array.isArray(this.form.assetInfoList) + ? this.form.assetInfoList + : []; + return assetInfoList.filter(item => { + if (!item || typeof item !== "object") { + return false; + } + return Object.keys(item).some(key => { + const value = item[key]; + return value !== null && value !== undefined && String(value).trim() !== ""; + }); + }); + }, + validateAssetInfoList(assetInfoList) { + const requiredFields = [ + { key: "personId", label: "资产实际持有人身份证号" }, + { key: "assetMainType", label: "资产大类" }, + { key: "assetSubType", label: "资产小类" }, + { key: "assetName", label: "资产名称" }, + { key: "currentValue", label: "当前估值" }, + { key: "assetStatus", label: "资产状态" } + ]; + const numericFields = [ + { key: "ownershipRatio", label: "产权占比" }, + { key: "originalValue", label: "资产原值" }, + { key: "currentValue", label: "当前估值" } + ]; + + for (let index = 0; index < assetInfoList.length; index++) { + const asset = assetInfoList[index]; + const rowNo = index + 1; + + for (const field of requiredFields) { + const value = asset[field.key]; + if (value === null || value === undefined || String(value).trim() === "") { + this.$modal.msgError(`第${rowNo}条资产的${field.label}不能为空`); + return false; + } + } + + if (!idCardPattern.test(asset.personId)) { + this.$modal.msgError(`第${rowNo}条资产的资产实际持有人身份证号格式不正确`); + return false; + } + + for (const field of numericFields) { + const value = asset[field.key]; + if (value !== null && value !== undefined && String(value).trim() !== "") { + if (!/^-?\d+(\.\d+)?$/.test(String(value).trim())) { + this.$modal.msgError(`第${rowNo}条资产的${field.label}格式不正确`); + return false; + } + } + } + + if (!this.assetStatusOptions.some(option => option.value === asset.assetStatus)) { + this.$modal.msgError(`第${rowNo}条资产的资产状态不在允许范围内`); + return false; + } + } + + return true; + }, + createEmptyAssetRow(defaultPersonId = "") { + return { + personId: defaultPersonId || "", + assetMainType: "", + assetSubType: "", + assetName: "", + ownershipRatio: "", + purchaseEvalDate: "", + originalValue: "", + currentValue: "", + valuationDate: "", + assetStatus: "", + remarks: "" + }; + }, + hasAssetContent(row) { + if (!row || typeof row !== "object") { + return false; + } + return Object.keys(row).some(key => { + const value = row[key]; + return value !== null && value !== undefined && String(value).trim() !== ""; + }); + }, + syncAssetPersonIds(newIdCard, oldIdCard) { + if (!Array.isArray(this.form.assetInfoList)) { + return; + } + this.form.assetInfoList = this.form.assetInfoList.map(asset => { + if (!asset || typeof asset !== "object") { + return asset; + } + const shouldSync = !asset.personId || asset.personId === oldIdCard; + if (!shouldSync) { + return asset; + } + return { + ...asset, + personId: newIdCard || "" + }; + }); + }, + handleAddAsset() { + if (!Array.isArray(this.form.assetInfoList)) { + this.form.assetInfoList = []; + } + this.form.assetInfoList.push(this.createEmptyAssetRow(this.form.idCard)); + }, + handleRemoveAsset(index) { + if (!Array.isArray(this.form.assetInfoList)) { + return; + } + this.form.assetInfoList.splice(index, 1); + }, + formatAssetOwnerType(asset) { + if (!asset) { + return "-"; + } + return asset.personId && asset.personId === this.employeeDetail.idCard ? "本人" : "亲属"; + }, /** 搜索按钮操作 */ handleQuery() { this.queryParams.pageNum = 1; @@ -638,6 +1088,7 @@ export default { /** 新增按钮操作 */ handleAdd() { this.reset(); + this.form.assetInfoList = []; this.isAdd = true; this.open = true; this.title = "新增员工"; @@ -646,7 +1097,10 @@ export default { handleDetail(row) { const staffId = row.staffId; getBaseStaff(staffId).then(response => { - this.employeeDetail = response.data; + this.employeeDetail = { + ...response.data, + assetInfoList: response.data.assetInfoList || [] + }; this.detailOpen = true; }); }, @@ -656,7 +1110,10 @@ export default { this.isAdd = false; const staffId = row.staffId || this.ids[0]; getBaseStaff(staffId).then(response => { - this.form = response.data; + this.form = { + ...response.data, + assetInfoList: response.data.assetInfoList || [] + }; this.open = true; this.title = "编辑员工"; }); @@ -665,6 +1122,10 @@ export default { submitForm() { this.$refs["form"].validate(valid => { if (valid) { + this.form.assetInfoList = this.normalizeAssetInfoList(); + if (!this.validateAssetInfoList(this.form.assetInfoList)) { + return; + } if (this.isAdd) { addBaseStaff(this.form).then(response => { this.$modal.msgSuccess("新增成功"); @@ -697,6 +1158,10 @@ export default { this.upload.title = "员工数据导入"; this.upload.open = true; }, + handleAssetImport() { + this.assetUpload.title = "员工资产数据导入"; + this.assetUpload.open = true; + }, /** 导入对话框关闭事件 */ handleImportDialogClose() { this.$nextTick(() => { @@ -705,14 +1170,27 @@ export default { } }); }, + handleAssetImportDialogClose() { + this.$nextTick(() => { + if (this.$refs.assetUpload) { + this.$refs.assetUpload.clearFiles(); + } + }); + }, /** 下载模板操作 */ importTemplate() { this.download('ccdi/baseStaff/importTemplate', {}, `员工信息模板_${new Date().getTime()}.xlsx`) }, + importAssetTemplate() { + this.download('ccdi/assetInfo/importTemplate', {}, `员工资产信息模板_${new Date().getTime()}.xlsx`) + }, // 文件上传中处理 handleFileUploadProgress(event, file, fileList) { this.upload.isUploading = true; }, + handleAssetFileUploadProgress() { + this.assetUpload.isUploading = true; + }, // 文件上传成功处理 handleFileSuccess(response, file, fileList) { this.upload.isUploading = false; @@ -763,11 +1241,57 @@ export default { this.$modal.msgError(response.msg); } }, + handleAssetFileSuccess(response) { + this.assetUpload.isUploading = false; + this.assetUpload.open = false; + + if (response.code === 200) { + if (!response.data || !response.data.taskId) { + this.$modal.msgError('员工资产导入任务创建失败:缺少任务ID'); + this.assetUpload.isUploading = false; + this.assetUpload.open = true; + return; + } + + const taskId = response.data.taskId; + + if (this.assetPollingTimer) { + clearInterval(this.assetPollingTimer); + this.assetPollingTimer = null; + } + + this.clearAssetImportTaskFromStorage(); + this.saveAssetImportTaskToStorage({ + taskId: taskId, + status: 'PROCESSING', + timestamp: Date.now(), + hasFailures: false + }); + + this.assetShowFailureButton = false; + this.assetCurrentTaskId = taskId; + + this.$notify({ + title: '员工资产导入任务已提交', + message: '正在后台处理中,处理完成后将通知您', + type: 'info', + duration: 3000 + }); + + this.startAssetImportStatusPolling(taskId); + } else { + this.$modal.msgError(response.msg); + } + }, // 导入结果弹窗关闭 handleImportResultClose() { this.importResultVisible = false; this.importResultContent = ""; }, + handleAssetImportResultClose() { + this.assetImportResultVisible = false; + this.assetImportResultContent = ""; + }, /** 开始轮询导入状态 */ startImportStatusPolling(taskId) { let pollCount = 0; @@ -796,6 +1320,32 @@ export default { } }, 2000); // 每2秒轮询一次 }, + startAssetImportStatusPolling(taskId) { + let pollCount = 0; + const maxPolls = 150; + + this.assetPollingTimer = setInterval(async () => { + try { + pollCount++; + + if (pollCount > maxPolls) { + clearInterval(this.assetPollingTimer); + this.$modal.msgWarning('员工资产导入任务处理超时,请联系管理员'); + return; + } + + const response = await getAssetImportStatus(taskId); + + if (response.data && response.data.status !== 'PROCESSING') { + clearInterval(this.assetPollingTimer); + this.handleAssetImportComplete(response.data); + } + } catch (error) { + clearInterval(this.assetPollingTimer); + this.$modal.msgError('查询员工资产导入状态失败: ' + error.message); + } + }, 2000); + }, /** 处理导入完成 */ handleImportComplete(statusResult) { // 更新localStorage中的任务状态 @@ -833,11 +1383,47 @@ export default { this.getList(); } }, + handleAssetImportComplete(statusResult) { + this.saveAssetImportTaskToStorage({ + taskId: statusResult.taskId, + status: statusResult.status, + hasFailures: statusResult.failureCount > 0, + totalCount: statusResult.totalCount, + successCount: statusResult.successCount, + failureCount: statusResult.failureCount + }); + + if (statusResult.status === 'SUCCESS') { + this.$notify({ + title: '员工资产导入完成', + message: `全部成功!共导入${statusResult.totalCount}条数据`, + type: 'success', + duration: 5000 + }); + this.assetShowFailureButton = false; + this.getList(); + } else if (statusResult.failureCount > 0) { + this.$notify({ + title: '员工资产导入完成', + message: `成功${statusResult.successCount}条,失败${statusResult.failureCount}条`, + type: 'warning', + duration: 5000 + }); + + this.assetShowFailureButton = true; + this.assetCurrentTaskId = statusResult.taskId; + this.getList(); + } + }, /** 查看导入失败记录 */ viewImportFailures() { this.failureDialogVisible = true; this.getFailureList(); }, + viewAssetImportFailures() { + this.assetFailureDialogVisible = true; + this.getAssetFailureList(); + }, /** 查询失败记录列表 */ getFailureList() { this.failureLoading = true; @@ -876,9 +1462,46 @@ export default { } }); }, + getAssetFailureList() { + this.assetFailureLoading = true; + getAssetImportFailures( + this.assetCurrentTaskId, + this.assetFailureQueryParams.pageNum, + this.assetFailureQueryParams.pageSize + ).then(response => { + this.assetFailureList = response.rows; + this.assetFailureTotal = response.total; + this.assetFailureLoading = false; + }).catch(error => { + this.assetFailureLoading = false; + + if (error.response) { + const status = error.response.status; + + if (status === 404) { + this.$modal.msgWarning('员工资产导入记录已过期,无法查看失败记录'); + this.clearAssetImportTaskFromStorage(); + this.assetShowFailureButton = false; + this.assetCurrentTaskId = null; + this.assetFailureDialogVisible = false; + } else if (status === 500) { + this.$modal.msgError('服务器错误,请稍后重试'); + } else { + this.$modal.msgError(`查询失败: ${error.response.data.msg || '未知错误'}`); + } + } else if (error.request) { + this.$modal.msgError('网络连接失败,请检查网络'); + } else { + this.$modal.msgError('查询员工资产失败记录失败: ' + error.message); + } + }); + }, // 提交上传文件 submitFileForm() { this.$refs.upload.submit(); + }, + submitAssetFileForm() { + this.$refs.assetUpload.submit(); } } }; @@ -898,6 +1521,7 @@ export default { background: #f9fafb; border-radius: 8px; padding: 16px; + margin-bottom: 16px; } .employee-detail-dialog .section-title { @@ -921,6 +1545,20 @@ export default { background: #fff; } +.employee-detail-dialog .detail-assets-table { + background: #fff; + border-radius: 4px; + overflow: hidden; +} + +.employee-detail-dialog .empty-assets-detail { + padding: 32px 0; + text-align: center; + color: #909399; + background: #fff; + border-radius: 4px; +} + .employee-detail-dialog .relatives-container { background: #fff; border-radius: 4px; @@ -976,6 +1614,50 @@ export default { margin-right: 20px; } +.employee-edit-dialog .assets-helper { + margin: -4px 0 12px; + padding: 10px 12px; + background: #f4f8ff; + border: 1px solid #d9ecff; + border-radius: 6px; + color: #606266; + line-height: 1.8; +} + +.employee-edit-dialog .assets-table-wrapper { + width: 100%; + overflow-x: auto; +} + +.employee-edit-dialog .assets-table { + min-width: 1460px; +} + +.employee-edit-dialog .assets-table .el-input, +.employee-edit-dialog .assets-table .el-date-editor { + width: 100%; +} + +.employee-edit-dialog .empty-assets { + text-align: center; + padding: 30px 0; + color: #909399; + border: 1px dashed #dcdfe6; + border-radius: 4px; + background: #fafafa; +} + +.employee-edit-dialog .empty-assets i { + display: block; + font-size: 30px; + color: #c0c4cc; + margin-bottom: 8px; +} + +.employee-edit-dialog .empty-assets span { + font-size: 13px; +} + .employee-edit-dialog .relatives-table { width: 100%; } diff --git a/ruoyi-ui/tests/unit/employee-asset-api-contract.test.js b/ruoyi-ui/tests/unit/employee-asset-api-contract.test.js new file mode 100644 index 0000000..5031a99 --- /dev/null +++ b/ruoyi-ui/tests/unit/employee-asset-api-contract.test.js @@ -0,0 +1,44 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const baseStaffApiPath = path.resolve( + __dirname, + "../../src/api/ccdiBaseStaff.js" +); +const assetApiPath = path.resolve( + __dirname, + "../../src/api/ccdiAssetInfo.js" +); + +assert(fs.existsSync(baseStaffApiPath), "未找到员工 API 文件 ccdiBaseStaff.js"); +assert(fs.existsSync(assetApiPath), "未找到员工资产 API 文件 ccdiAssetInfo.js"); + +const baseStaffSource = fs.readFileSync(baseStaffApiPath, "utf8"); +const assetSource = fs.readFileSync(assetApiPath, "utf8"); + +[ + "export function addBaseStaff(data)", + "export function updateBaseStaff(data)", + "data: data", +].forEach((token) => { + assert( + baseStaffSource.includes(token), + `员工 API 需保持透传聚合保存数据: ${token}` + ); +}); + +[ + "export function importAssetTemplate()", + "export function importAssetData(data)", + "export function getAssetImportStatus(taskId)", + "export function getAssetImportFailures(taskId, pageNum, pageSize)", + "/ccdi/assetInfo/importTemplate", + "/ccdi/assetInfo/importData", + "/ccdi/assetInfo/importStatus/", + "/ccdi/assetInfo/importFailures/", +].forEach((token) => { + assert(assetSource.includes(token), `员工资产 API 缺少关键契约: ${token}`); +}); + +console.log("employee-asset-api-contract test passed"); diff --git a/ruoyi-ui/tests/unit/employee-asset-import-ui.test.js b/ruoyi-ui/tests/unit/employee-asset-import-ui.test.js new file mode 100644 index 0000000..0715fb3 --- /dev/null +++ b/ruoyi-ui/tests/unit/employee-asset-import-ui.test.js @@ -0,0 +1,27 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const componentPath = path.resolve( + __dirname, + "../../src/views/ccdiBaseStaff/index.vue" +); +const source = fs.readFileSync(componentPath, "utf8"); + +[ + "导入资产信息", + "查看员工资产导入失败记录", + "assetUpload", + "assetPollingTimer", + "assetCurrentTaskId", + "assetFailureDialogVisible", + "employee_asset_import_last_task", + "员工资产数据导入", +].forEach((token) => { + assert( + source.includes(token), + `员工资产导入 UI 缺少关键结构或状态: ${token}` + ); +}); + +console.log("employee-asset-import-ui test passed"); diff --git a/ruoyi-ui/tests/unit/employee-asset-maintenance-layout.test.js b/ruoyi-ui/tests/unit/employee-asset-maintenance-layout.test.js new file mode 100644 index 0000000..6ddf62c --- /dev/null +++ b/ruoyi-ui/tests/unit/employee-asset-maintenance-layout.test.js @@ -0,0 +1,45 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const componentPath = path.resolve( + __dirname, + "../../src/views/ccdiBaseStaff/index.vue" +); +const source = fs.readFileSync(componentPath, "utf8"); + +[ + "", + " { + assert( + source.includes(token), + `员工资产维护弹窗缺少关键结构: ${token}` + ); +}); + +[ + "placeholder=\"请输入资产实际持有人身份证号\"", + "v-model=\"scope.row.personId\"", +].forEach((token) => { + assert( + !source.includes(token), + `员工资产维护弹窗不应继续展示字段: ${token}` + ); +}); + +console.log("employee-asset-maintenance-layout test passed"); diff --git a/ruoyi-ui/tests/unit/employee-asset-submit-flow.test.js b/ruoyi-ui/tests/unit/employee-asset-submit-flow.test.js new file mode 100644 index 0000000..aa9041a --- /dev/null +++ b/ruoyi-ui/tests/unit/employee-asset-submit-flow.test.js @@ -0,0 +1,29 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const componentPath = path.resolve( + __dirname, + "../../src/views/ccdiBaseStaff/index.vue" +); +const source = fs.readFileSync(componentPath, "utf8"); + +[ + "assetInfoList: []", + "normalizeAssetInfoList()", + "this.form.assetInfoList = [];", + "response.data.assetInfoList || []", + "this.form.assetInfoList = this.normalizeAssetInfoList();", + "'form.idCard'(newIdCard, oldIdCard)", + "syncAssetPersonIds(newIdCard, oldIdCard)", + "this.form.assetInfoList.push(this.createEmptyAssetRow(this.form.idCard));", + "personId: defaultPersonId || \"\"", + "assetStatusOptions.some(option => option.value === asset.assetStatus)", +].forEach((token) => { + assert( + source.includes(token), + `员工资产提交流程缺少关键处理: ${token}` + ); +}); + +console.log("employee-asset-submit-flow test passed");