!143 fix(fixcontact): 修复前端页面 QA 审查发现的问题
Merge pull request !143 from wsm/fix/fix_contact_qa
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
# 前端页面审查报告 — fixcontact(定点联系单)
|
||||
|
||||
## 审查信息
|
||||
|
||||
- 页面/路由:`/bg/fixcontact/FixedContact20260730List`
|
||||
- 审查日期:2026-08-07
|
||||
- 审查方式:浏览器实测(Playwright,admin/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();
|
||||
|
||||
Reference in New Issue
Block a user