!143 fix(fixcontact): 修复前端页面 QA 审查发现的问题

Merge pull request !143 from wsm/fix/fix_contact_qa
This commit is contained in:
wsm
2026-08-07 09:19:21 +00:00
committed by Gitee
4 changed files with 135 additions and 15 deletions
+102
View File
@@ -0,0 +1,102 @@
# 前端页面审查报告 — fixcontact(定点联系单)
## 审查信息
- 页面/路由:`/bg/fixcontact/FixedContact20260730List`
- 审查日期:2026-08-07
- 审查方式:浏览器实测(Playwrightadmin/123456+ 代码静态审查
- 测试环境:前端 dev `http://127.0.0.1:3100`,后端 `http://localhost:8080/jeecg-boot`,登录走 mock、其余接口走真实后端
- 相关文件:
- `src/views/bg/fixcontact/FixedContact20260730List.vue`
- `src/views/bg/fixcontact/components/FixedContact20260730Form.vue`
- `src/views/bg/fixcontact/components/FixedContact20260730Modal.vue` / `BPMForm.vue` / `BPMFeedbackModal.vue`
- `src/views/bg/fixcontact/FixedContact20260730.api.ts`
- 依赖组件:`JInput``SzSelectDept``DeptRoleUserSelectDropDown``JFormContainer``FlowScheduleStartModalForBG``BusinessProcessListModal`
- 测试证据目录:`qa-tmp/fixcontact/artifacts/<probe>/`(截图 / console.json / requests.json
## 总体结论
- **P0 × 0 P1 × 2 P2 × 4**
- 一句话总结:页面核心功能(增删改查、详情只读、批量删除、导出/模板、流程入口、反馈子表、统计标签)均可用,无 P0;存在 2 个 P1 影响体验与数据完整性(搜索逐字符发请求、`是否所办领导审批=是` 时审批人可留空),建议交付前修复。
---
## P1 - 强烈建议
| # | 维度 | 问题描述 | 证据 | 位置 |
|---|------|---------|------|------|
| 1 | 后端调用/性能 | 表单编号搜索框**逐字符触发查询**,每敲一个字符发 1 次列表请求 + 1 次统计请求,无防抖/回车确认。实测输入 5 个字符共发出 5 list + 5 statusStats 请求(每个字符 2 个接口)。查询输入框直接绑定 `@update:value="searchQuery"`,而 `JInput` 每次敲键都会 emit `update:value`。 | probe_12 A 段输出:`after typing 'QA' (2 chars): list=2 stats=2``after typing full 'QA001' (5 chars): list=3 stats=3`;截图 `12_search_flood_pagination/` | `FixedContact20260730List.vue:12``@update:value="searchQuery"`);`JInput.vue` emit `update:value` |
| 2 | 表单校验/状态一致性 | **联动校验缺位**`是否所办领导审批=是`isNeedAppro='1')时 `所办领导`supDeptleaderid)字段解除禁用,但 `validatorRules` 中**没有该字段的校验规则**,可留空提交。实测:填满其余必填、`是` + 空审批人提交 → 无校验错误,创建成功,请求体为 `"isNeedAppro":"1","supDeptleaderid":""`。该记录随后发起流程时审批人为空,审批任务可能无人办理。 | probe_14 输出:`VALIDATION ERRORS: []``TOASTS: ['添加成功!']`、add body 含 `"isNeedAppro":"1","supDeptleaderid":""`;截图 `14_isneed_submit/` | `FixedContact20260730Form.vue:91-99``:disabled="formData.isNeedAppro !== '1'"`)、`FixedContact20260730Form.vue:158-171`validatorRules 无 supDeptleaderid |
---
## P2 - 建议
| # | 维度 | 问题描述 | 证据 | 位置 |
|---|------|---------|------|------|
| 1 | 后端调用 | `handleFlowScheduleConfirm``startProcessSchedules``try/catch`、不校验返回值,成功后无条件 `createMessage.success('发起成功')`。当前依赖框架默认 transform(失败即抛错并弹框架错误提示)故不会误报成功,但属于隐性依赖,建议显式 `try/catch` + 检查 `res.success`。 | 代码审查 | `FixedContact20260730List.vue:578-626` |
| 2 | 用户体验/冗余 | 工具栏同时存在旧 `导入`onImportXls→importExcel)与新增 `模板导入`handleImportByTemplate→importExcelByEasyExcel)、`模板校验`,同一导入能力出现两个入口,易混淆。另 `handleDownloadTemplate` 复用的 `getExportXlsHeadersUrl` 命名与语义不符(实际为模板下载,非导出表头)。 | 代码审查;截图 `10_toolbar_export_expand/a_`… | `FixedContact20260730List.vue:101-105, 639` |
| 3 | 异常与边界 | `handleDownloadTemplate`:仅 `if (!data)` 提示下载失败,网络异常(请求 reject)时无兜底提示(未捕获的 Promise rejection)。 | 代码审查 | `FixedContact20260730List.vue:639-651` |
| 4 | 渲染/console | 使用 fixcontact 页面期间 console 出现框架级告警:①`Invalid prop: type check failed for prop "onClick"/"onChange"`96 次,来自 VcSelect/BaseSelect/DeptRoleUserSelectDropDown 向组件传数组型事件);②`Property "interUserSecurityLevel" was accessed during render but is not defined`(渲染时访问未定义属性);③全局头部 `Failed to resolve component: AppSearch/Notify/...`。均为框架/全局布局噪声,非本模块引入,但 ①② 建议顺手清理。 | `09_toast_detail_batch/console.json``10_toolbar_export_expand/console.json` | 表单组件渲染链路 |
---
## 通过项(做得好的地方)
- **CRUD 全链路正确**:新增(add POST 请求体字段完整)→ 列表回显 → 编辑(edit POST 带 id、字段更新)→ 详情(标题"详情")→ 单删(popconfirm 二次确认)均正常,操作后列表与统计标签同步刷新。
- **详情只读实现正确**:详情态表单整体只读(`JFormContainer` 施加 `.jeecg-form-container-disabled` + `fieldset { pointer-events: none }`),实测详情态输入框 `pointer-events: none`、无法编辑(早期"详情未禁用"怀疑不成立,实测为只读)。
- **无重复提交/重复 toast**:新增仅发 1 次 POST、仅弹 1 次"添加成功!"(早期"重复 toast"观察为前一条消息残留,已排除)。
- **批量删除**:勾选多行 → 批量操作 → 删除 → 确认弹窗 → 批量删除成功(实测 3 条一次删净)。
- **统计标签**`未开始 4 / 督办中 1(已逾期 0)/ 已完成 0` 正确渲染,由 statusStats 接口驱动。
- **导入导出/模板**`导出`GET exportXls)、`下载模板`GET exportXlsHeaders)均正常触发下载。
- **反馈子表**:行展开后按主表 id 调用 `queryFixedContactFeedback20260730ByMainId` 加载反馈记录(实测无数据时正确显示"共 0 条")。
- **流程入口**`流程操作` 下拉(发起流程 / 查询督办流程)正常;`发起流程` 弹窗(标题"发起流程",含"取消/提交"),`查询督办流程` 弹窗(标题"流程列表")正常展示已发起调度记录。
- **字段联动(显隐)**`是否所办领导审批` 切换 `是/否``所办领导` 选择器正确解除/恢复禁用(仅缺联动校验,见 P1-2)。
- **搜索筛选**:表单编号、联系单位(部门树选择器)筛选生效,列表请求正确携带 `contactDept[]` 参数;重置清空条件并刷新。
- **分页**`共 5 条数据`、页大小 12/24/36 可选、切页大小正常触发列表刷新。
- **表单必填校验**:12 个必填字段在提交时整体校验并定位错误字段(probe_08b 实测)。
- **权限指令**:新增/导出/导入/模板校验/模板导入/批量删除按钮均带 `v-auth` 权限控制。
---
## 未覆盖项(仅 1 个测试账号 admin)
- **权限差异**:非管理员/不同部门账号下按钮显隐、数据范围(后端过滤)无法用 1 个账号验证,`v-auth` 仅确认指令存在。
- **接口越权(IDOR**:改 `id` 访问他人数据是否被后端拦截未验证(前端未暴露此类入口)。
- **纯视觉细节**:当前环境模型无法直接查看截图,对齐/间距/配色/空态图等视觉项以 DOM 结构 + getComputedStyle 评估,需人工复核 `qa-tmp/fixcontact/artifacts/*/*.png`
- **多视图一致性**:本列表页无平铺/分组等其他视图,无跨视图数据同步场景。
- **深链接/浏览器前进后退**:未覆盖。
---
## 测试过程记录
测试脚本:`qa-tmp/fixcontact/scripts/probe_0{1..15}_*.py`(共享助手 `_fx_helpers.py`)。
证据根目录:`qa-tmp/fixcontact/artifacts/`
| Probe | 覆盖内容 | 关键结果 | 证据目录 |
|-------|---------|---------|---------|
| 01-05 | 路由可达、列表加载、接口契约、渲染诊断、弹窗映射 | 页面正常进入,list/statusStats 接口可用 | `01_login_route``05_modal_map` |
| 06 | 校验 + 下拉选择 | 必填校验生效、领导下拉可选 | `06_validation_selects` |
| 07 | 选择器(部门/用户/领导/日期) | 选择器均可提交值(部门树需勾选 checkbox,见下) | `07_pickers` |
| 08 | CRUD 全链路 | 增删改查+详情全部通过,add/edit POST body 正确 | `08_crud` |
| 08b/08c | 表单值诊断、部门选择器调试 | 确认 SzSelectDept 取值依赖树 checkbox 勾选(测试脚本问题,非页面 bug) | `08b_fill_diag``08c_dept_debug` |
| 09 | 重复 toast / 详情只读 / 流程菜单 / 批量删除 | 单 toast 单 POST;详情只读正确;发起流程+流程列表弹窗正常;批量删除成功 | `09_toast_detail_batch` |
| 10 | 导出/模板/反馈子表展开/是审批切换/高级查询 | 导出、下载模板、反馈子表、是/否切换均正常;高级查询面板可打开 | `10_toolbar_export_expand` |
| 11 | 是审批切换细节/反馈子表稳定/高级查询 | 所办领导禁用⇄启用切换正确;反馈子表"共 0 条";高级查询面板含"执行查询/保存查询/清空/关闭" | `11_isneed_expand_superq` |
| 12 | 搜索请求数/重置/分页 | **逐字符 1 次 list+1 次 stats**(P1-1);重置清空;分页 12/24/36 正常 | `12_search_flood_pagination` |
| 13 | 搜索区联系单位部门筛选 | 树弹窗选择后列表携带 `contactDept[]` 参数 | `13_dept_filter` |
| 14 | isNeedAppro=是 + 空审批人提交 | **校验放行,创建成功,supDeptleaderid 为空**P1-2 | `14_isneed_submit` |
| 15 | 测试数据清理 | 删除全部 `QA_TST_/QA_APPRO_` 前缀测试数据 | `15_cleanup` |
### console 告警汇总(均为框架级噪声,无本模块 app error)
- 96× `Invalid prop: type check failed for prop "onClick"/"onChange"`VcSelect/BaseSelect 系列)
- 6× `Property "interUserSecurityLevel" was accessed during render but is not defined`
- 若干 `Failed to resolve component: AppSearch/Notify/AppLocalePicker/SettingDrawer/Aide`(全局头部组件)
- 其余为应用启动日志与 `sign requestBody` 请求签名日志
### 残留/预存数据说明
- 测试过程中创建并已清理的 QA_ 数据:`QA_TEST_*``QA_TST_*``QA_APPRO_*`probe_15 确认全部删除)。
- 列表中存在一条**预存测试脏数据**:`formNo="new"``meetingPlace="w'da'w"`、创建时间 2026-08-07 15:45:38,非本次测试产生,**未删除**,建议人工确认是否需要清理。
@@ -9,7 +9,6 @@ enum Api {
edit = '/bg/fixcontact/fixedContact20260730/edit',
deleteOne = '/bg/fixcontact/fixedContact20260730/delete',
deleteBatch = '/bg/fixcontact/fixedContact20260730/deleteBatch',
importExcel = '/bg/fixcontact/fixedContact20260730/importExcel',
exportXls = '/bg/fixcontact/fixedContact20260730/exportXls',
exportXlsHeaders = '/bg/fixcontact/fixedContact20260730/exportXlsHeaders',
importExcelByEasyExcel = '/bg/fixcontact/fixedContact20260730/importExcelByEasyExcel',
@@ -20,7 +19,6 @@ enum Api {
}
export const getExportUrl = Api.exportXls;
export const getImportUrl = Api.importExcel;
export const getExportXlsHeadersUrl = Api.exportXlsHeaders;
export const getImportExcelByEasyExcelUrl = Api.importExcelByEasyExcel;
export const getCheckExcelByEasyExcelUrl = Api.checkExcelByEasyExcel;
@@ -9,7 +9,7 @@
<template #label>
<a-tooltip title="支持模糊查询">表单编号</a-tooltip>
</template>
<a-tooltip title="支持模糊查询"><JInput v-model:value="queryParam.formNo" type="like" placeholder="请输入表单编号" allow-clear trim @update:value="searchQuery" /></a-tooltip>
<a-tooltip title="支持模糊查询"><JInput v-model:value="queryParam.formNo" type="like" placeholder="请输入表单编号" allow-clear trim @update:value="debouncedSearchQuery" /></a-tooltip>
</a-form-item>
</a-col>
<a-col :xl="6" :lg="6" :md="8" :sm="12">
@@ -99,7 +99,6 @@
<template #tableTitle>
<a-button type="primary" v-auth="actionAuth.add" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button>
<a-button type="primary" v-auth="actionAuth.exportXls" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
<j-upload-button type="primary" v-auth="actionAuth.importExcel" preIcon="ant-design:import-outlined" @click="onImportXls"> 导入</j-upload-button>
<a-button type="primary" v-auth="actionAuth.exportXls" preIcon="ant-design:download-outlined" @click="handleDownloadTemplate">下载模板</a-button>
<j-upload-button type="primary" v-auth="actionAuth.importExcel" preIcon="ant-design:check-outlined" @click="handleCheckExcel">模板校验</j-upload-button>
<j-upload-button type="primary" v-auth="actionAuth.importExcel" preIcon="ant-design:import-outlined" @click="handleImportByTemplate">模板导入</j-upload-button>
@@ -202,8 +201,9 @@
import { defHttp } from '/@/utils/http/axios';
import { startProcessSchedules } from '/@/api/common/api';
import { dateUtil } from '/@/utils/dateUtil';
import { useDebounceFn } from '@vueuse/core';
import { columns, superQuerySchema, fixedContactFeedbackColumns } from './FixedContact20260730.data';
import { list, deleteOne, batchDelete, getImportUrl, getExportUrl, getExportXlsHeadersUrl, getImportExcelByEasyExcelUrl, getCheckExcelByEasyExcelUrl, fixedContactFeedbackList, saveOrUpdate, withDrawFixContact, statusStats } from './FixedContact20260730.api';
import { list, deleteOne, batchDelete, getExportUrl, getExportXlsHeadersUrl, getImportExcelByEasyExcelUrl, getCheckExcelByEasyExcelUrl, fixedContactFeedbackList, saveOrUpdate, withDrawFixContact, statusStats } from './FixedContact20260730.api';
import { showImportValidationErrors } from '/@/utils/helper/importError';
function expandIcon({ expanded, onExpand, record }: { expanded: boolean; onExpand: Function; record: Recordable }) {
@@ -252,7 +252,7 @@ import { showImportValidationErrors } from '/@/utils/helper/importError';
const feedbackExpandColumns: BasicColumn[] = [...fixedContactFeedbackColumns];
const { tableContext, onExportXls, onImportXls } = useListPage({
const { tableContext, onExportXls } = useListPage({
tableProps: {
title: '定点联系单',
api: list,
@@ -280,10 +280,6 @@ import { showImportValidationErrors } from '/@/utils/helper/importError';
name: '定点联系单',
url: getExportUrl,
},
importConfig: {
url: getImportUrl,
success: handleSuccess,
},
});
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
@@ -385,6 +381,9 @@ import { showImportValidationErrors } from '/@/utils/helper/importError';
fetchStatusStats();
}
// 输入框逐字触发的查询做防抖,查询按钮/回车仍走未防抖的 searchQuery
const debouncedSearchQuery = useDebounceFn(searchQuery, 500);
function clearQueryParams() {
Object.keys(queryParam).forEach((key) => {
delete queryParam[key];
@@ -618,7 +617,12 @@ import { showImportValidationErrors } from '/@/utils/helper/importError';
},
};
await startProcessSchedules(params);
try {
await startProcessSchedules(params);
} catch (e) {
createMessage.error('发起流程失败,请稍后重试');
return;
}
await saveOrUpdate({ id: record.id, bpmStatus: '2', superviseCount: nextSuperviseCount, isNeedAppro: options.isNeedAppro ?? record.isNeedAppro ?? '0', supDeptleaderid: options.supDeptleaderid ?? record.supDeptleaderid ?? '' }, true);
createMessage.success('发起成功');
reload();
@@ -637,10 +641,16 @@ import { showImportValidationErrors } from '/@/utils/helper/importError';
});
async function handleDownloadTemplate() {
const data = await defHttp.get(
{ url: getExportXlsHeadersUrl, params: {}, responseType: 'blob', timeout: 60000 },
{ isTransformResponse: false },
);
let data;
try {
data = await defHttp.get(
{ url: getExportXlsHeadersUrl, params: {}, responseType: 'blob', timeout: 60000 },
{ isTransformResponse: false },
);
} catch (e) {
createMessage.error('模板下载失败,请稍后重试');
return;
}
if (!data) {
createMessage.warning('文件下载失败');
return;
@@ -168,6 +168,16 @@
relatedLeader: [{ required: true, message: '请选择相关分管所领导' }],
applicant: [{ required: true, message: '请选择申请人' }],
hostDept: [{ required: true, message: '请选择主办部门' }],
supDeptleaderid: [
{
validator: async (_rule, value) => {
if (formData.isNeedAppro === '1' && !value) {
return Promise.reject('请选择所办领导');
}
return Promise.resolve();
},
},
],
});
const { resetFields, validate, validateInfos } = useForm(formData, validatorRules, { immediate: false });
const formRef = ref();