From 01d607d76629af2bdc6aa9526882638cd519baf2 Mon Sep 17 00:00:00 2001 From: wuzilong <704541015@qq.com> Date: Thu, 12 Nov 2026 19:29:37 +0800 Subject: [PATCH] feat(web-admin): open cert maintain dialog on reminder page Keep users on the document reminder list by reusing vehicle/personnel form dialogs instead of navigating away to base-info pages. Co-authored-by: Cursor --- ...1-12-document-reminder-edit-jump-design.md | 135 +++++++----------- .../utils/pl-document-reminder-jump.test.ts | 52 ++++--- .../src/utils/pl-document-reminder-jump.ts | 38 ++++- .../views/pl/inventory/document-reminder.vue | 51 +++++-- 4 files changed, 156 insertions(+), 120 deletions(-) diff --git a/docs/superpowers/specs/2026-11-12-document-reminder-edit-jump-design.md b/docs/superpowers/specs/2026-11-12-document-reminder-edit-jump-design.md index 765d19615..c7bf80d73 100644 --- a/docs/superpowers/specs/2026-11-12-document-reminder-edit-jump-design.md +++ b/docs/superpowers/specs/2026-11-12-document-reminder-edit-jump-design.md @@ -1,117 +1,80 @@ -# 证件到期提醒跳转基础信息库编辑弹窗(Web) +# 证件到期提醒本页维护证件弹窗(Web) ## 背景与问题 -Web【证件到期提醒】(`/#/pl/document-reminder-module/document-reminder`)展示车辆、人员证件过期信息。用户处理时需要到【基础信息库】打开对应档案弹窗补录/更新证件,当前需手动找菜单与记录,路径长。 - -用户希望列表增加操作列,点击后进入基础信息库并直接打开编辑弹窗。示例链接曾指向承运商页;经核对提醒主体为 `vehicle` / `personnel`,应按主体类型跳到车辆或人员档案,而不是一律进承运商。 +Web【证件到期提醒】展示车辆、人员证件过期信息。用户需要补录/更新证件时,若整页跳到【基础信息库】再返回,连续处理多条很麻烦。 ## 已确认决策 | 项 | 结论 | | --- | --- | -| 跳转策略 | 按 `ownerType` 分流:车辆 → 车辆档案;人员 → 人员档案 | -| 车辆细分 | 根据车辆 `vehicleType`:`self` → 山峡自有车辆;否则(含 `carrier`)→ 三方车辆 | -| 人员细分 | 根据 `personnelAffiliation`:`self` → 山峡驾押人员;`carrier` → 三方驾押人员 | -| 打开方式 | 目标页自动打开**编辑**弹窗(非仅查看) | -| 实现方案 | 方案 1:提醒页点操作时先 get 主体详情判定归属,再 `router.push` 带 `editId` | -| 后端 | 本期不改提醒接口;不新增 `routeHint` 字段 | -| 范围 | 仅 Web;小程序不改 | -| 提醒状态 | 不因跳转/保存档案自动标为已处理;仍用现有「处理」按钮 | -| 证件定位 | 本期不强制 `focusCert`;打开编辑弹窗即可 | +| 交互(方案 A) | 在提醒页点「维护证件」,**本页弹出**与基础信息库同款编辑弹窗;关闭后仍停在提醒列表 | +| 车辆弹窗 | 复用 `PlVehicleFormDialog`;按 `vehicleType` 传 `self` / `carrier` | +| 人员弹窗 | 复用 `PlPersonnelFormDialog`;按 `personnelAffiliation` 传 `self` / `carrier` | +| 打开前 | `get` 主体详情以判定归属;缺 ID / 失败则提示,不弹窗 | +| 保存后 | 刷新提醒列表与菜单角标;**不**自动将提醒标为已处理 | +| 整页跳转 | 提醒页「维护证件」**不再** `router.push` 离开本页 | +| `editId` 深链 | 车辆/人员档案页已有的 `editId` 能力可保留(书签/外链),与本页弹窗无关 | +| 后端 | 不改 | +| 范围 | 仅 Web | ## 范围 **包含** -- `document-reminder.vue`:操作列与跳转编排 -- `vehicle/index.vue`、`self-vehicle/index.vue`:监听 `editId` 打开编辑 -- `personnel/index.vue`(自有与三方共用组件、靠 `meta.affiliationMode`):监听 `editId` 打开编辑 +- `document-reminder.vue`:操作列改为打开本页 `PlVehicleFormDialog` / `PlPersonnelFormDialog` +- `pl-document-reminder-jump.ts`:增加表单模式解析(或复用归属判定);路径函数可保留给深链/兼容 **不包含** -- 承运商页深链(提醒无承运商主体) -- 小程序 / 移动 H5 -- 后端提醒列表字段 enrichment -- 保存档案后自动 resolve 提醒 -- 弹窗内自动滚动到某一证件行(可后续加 `focusCert`) - -## 路由约定 - -| 条件 | 目标 path | -| --- | --- | -| vehicle + `vehicleType === 'self'` | `/vehicle-mgmt/base-info/self-vehicle` | -| vehicle + 其他 | `/vehicle-mgmt/base-info/vehicle` | -| personnel + `personnelAffiliation === 'carrier'` | `/vehicle-mgmt/base-info/personnel-carrier` | -| personnel + 其他(默认 self) | `/vehicle-mgmt/base-info/personnel` | - -Query: - -- `editId`:主体主键(`ownerId`),必填语义 -- 打开弹窗成功后 `router.replace` 去掉 `editId`(保留同页其他 query),避免刷新重复弹窗 - -深链示例: - -`/#/vehicle-mgmt/base-info/vehicle?editId=` +- 小程序 +- 保存后自动 resolve 提醒 +- 抽离档案列表页内联大表单(已有独立 dialog 组件则直接复用) ## 前端设计 -### 证件提醒页 +### 提醒页 -文件:`web-admin/src/views/pl/inventory/document-reminder.vue` +1. 模板挂载: -- 表格最右增加 `fixed="right"` 操作列,`min-width` ≥ 120(单按钮可略窄,但须符合 Web AGENTS 操作列惯例;若仅一枚按钮可用约 120–140)。 -- 按钮文案:**维护证件**;带前置图标(如 `EditPen` / `Document`)。 -- 点击 `handleMaintainCert(row)`: - 1. 校验 `row.ownerId` 与 `row.ownerType`;缺失则 `ElMessage.warning`。 - 2. `ownerType === 'vehicle'`:`vehicleApi.get(ownerId)`,取 `vehicleType`,选择 path。 - 3. `ownerType === 'personnel'`:`personnelApi.get(ownerId)`(或现有等价 API),取 `personnelAffiliation`,选择 path。 - 4. 其他 `ownerType`:提示暂不支持。 - 5. `router.push({ path, query: { editId: ownerId } })`。 - 6. get 失败(404/无权限):`ElMessage.error`,不跳转。 -- 保留工具栏现有「处理」逻辑不变。 -- loading:按钮或行级 loading,避免连点。 +```vue + + +``` -### 目标档案页共用行为 +2. `handleMaintainCert`:校验 → get → `resolveDocumentReminderFormMode` → 设置 `editingOwnerId` 与 type/affiliation → `vehicleFormVisible` 或 `personnelFormVisible = true`。 +3. `@saved`:`fetchData()` + `refreshMenuBadges()`;成功提示可选。 +4. 文案:「缺少主体信息,无法维护」等,不再提「跳转」。 -在 `vehicle`、`self-vehicle`、`personnel` 页面(personnel 覆盖两种 affiliation 路由): +### 模式解析 -1. `watch` / `onMounted` 读取 `route.query.editId`(仅当当前页 path 属于本页)。 -2. 有值则调用现有 `handleEdit` 等价逻辑:先用 `{ id: editId }` 或先 `get(editId)` 再打开编辑弹窗(与页内编辑一致,含证件列表回填)。 -3. 成功打开或明确失败后,清除 `editId` query。 -4. 若 `editId` 不属于本页数据范围(例如自有车页打开了三方车 id):提示失败并清 query;提醒页已按类型分流,此为兜底。 +```ts +type ReminderFormMode = + | { kind: 'vehicle'; vehicleType: 'self' | 'carrier' } + | { kind: 'personnel'; affiliation: 'self' | 'carrier' } + | null +``` -实现时优先抽一小段 composable(可选,非必须),例如 `useRouteEditIdOpen(handler)`,避免三处复制 watch/replace;若页内差异大则内联亦可,保持最小改动。 - -### 权限 - -- 跳转后目标菜单仍受角色菜单与数据范围约束;无菜单权限时路由/页面按现有行为处理。 -- 提醒页不新增后端权限码;操作列对所有能进提醒页的用户可见。 - -## 后端 - -无变更。列表继续返回现有 `ownerType` / `ownerId` / `documentType` 等字段。 - -## UI 规范 - -- 沿用提醒页现有列表模式与 Fresh Mint;操作列图标+文字,不新造布局。 -- 不因本需求改顶部统计或筛选结构。 +- vehicle + vehicleType self → self,否则 carrier +- personnel + affiliation carrier → carrier,否则 self ## 验收 -1. 车辆提醒「维护证件」→ 正确进入自有或三方车辆页,并弹出编辑框,可见证件字段。 -2. 人员提醒同理进入山峡/三方驾押人员页并弹出编辑框。 -3. 无 `ownerId`、主体不存在、无权限时有明确提示且不误跳。 -4. 刷新目标页或清 query 后不重复自动弹窗。 -5. 原「处理」标记已办流程与导出不受影响。 -6. 小程序未改。 - -## 风险 - -- 自有/三方分流依赖详情字段;字段异常时可能进错页,需兜底提示。 -- 档案页 `keepAlive` 若开启,需确认二次带 `editId` 进入仍触发打开(watch 全量 query,不只 onMounted)。 -- 连续从提醒跳多条时,依赖清 query + 新 `editId` 再次触发。 +1. 提醒页点「维护证件」不离开当前路由,弹出编辑框(含证件)。 +2. 关窗后列表仍在,可立刻点下一条。 +3. 保存后列表/角标刷新;提醒状态仍须手动「处理」。 +4. 车辆自有/三方、人员山峡/三方弹窗标题与校验与基础信息库一致。 ## 状态 -会话中已确认按主体类型跳转(方案 1)。实现计划:`docs/superpowers/plans/2026-11-12-document-reminder-edit-jump.md`。 +用户确认方案 A(本页弹窗)。实现于 `V3.0`。 diff --git a/web-admin/src/utils/pl-document-reminder-jump.test.ts b/web-admin/src/utils/pl-document-reminder-jump.test.ts index 49cef0c3a..03050b57c 100644 --- a/web-admin/src/utils/pl-document-reminder-jump.test.ts +++ b/web-admin/src/utils/pl-document-reminder-jump.test.ts @@ -2,11 +2,24 @@ import assert from 'node:assert/strict' import { describe, it } from 'node:test' import { resolveDocumentReminderEditPath, + resolveDocumentReminderFormMode, DOCUMENT_REMINDER_JUMP_UNSUPPORTED } from './pl-document-reminder-jump' -describe('证件提醒跳转路径', () => { - it('车辆按 vehicleType 分流自有与三方', () => { +describe('证件提醒表单模式与路径', () => { + it('车辆按 vehicleType 解析弹窗类型与路径', () => { + assert.deepEqual( + resolveDocumentReminderFormMode({ ownerType: 'vehicle', vehicleType: 'self' }), + { kind: 'vehicle', vehicleType: 'self' } + ) + assert.deepEqual( + resolveDocumentReminderFormMode({ ownerType: 'vehicle', vehicleType: 'carrier' }), + { kind: 'vehicle', vehicleType: 'carrier' } + ) + assert.deepEqual(resolveDocumentReminderFormMode({ ownerType: 'vehicle', vehicleType: '' }), { + kind: 'vehicle', + vehicleType: 'carrier' + }) assert.equal( resolveDocumentReminderEditPath({ ownerType: 'vehicle', vehicleType: 'self' }), '/vehicle-mgmt/base-info/self-vehicle' @@ -15,19 +28,29 @@ describe('证件提醒跳转路径', () => { resolveDocumentReminderEditPath({ ownerType: 'vehicle', vehicleType: 'carrier' }), '/vehicle-mgmt/base-info/vehicle' ) - assert.equal( - resolveDocumentReminderEditPath({ ownerType: 'vehicle', vehicleType: '' }), - '/vehicle-mgmt/base-info/vehicle' - ) }) - it('人员按 personnelAffiliation 分流', () => { - assert.equal( - resolveDocumentReminderEditPath({ + it('人员按 personnelAffiliation 解析弹窗归属与路径', () => { + assert.deepEqual( + resolveDocumentReminderFormMode({ ownerType: 'personnel', personnelAffiliation: 'self' }), - '/vehicle-mgmt/base-info/personnel' + { kind: 'personnel', affiliation: 'self' } + ) + assert.deepEqual( + resolveDocumentReminderFormMode({ + ownerType: 'personnel', + personnelAffiliation: 'carrier' + }), + { kind: 'personnel', affiliation: 'carrier' } + ) + assert.deepEqual( + resolveDocumentReminderFormMode({ + ownerType: 'personnel', + personnelAffiliation: '' + }), + { kind: 'personnel', affiliation: 'self' } ) assert.equal( resolveDocumentReminderEditPath({ @@ -36,17 +59,10 @@ describe('证件提醒跳转路径', () => { }), '/vehicle-mgmt/base-info/personnel-carrier' ) - assert.equal( - resolveDocumentReminderEditPath({ - ownerType: 'personnel', - personnelAffiliation: '' - }), - '/vehicle-mgmt/base-info/personnel' - ) }) it('不支持的主体返回 null', () => { - assert.equal(resolveDocumentReminderEditPath({ ownerType: 'carrier' }), null) + assert.equal(resolveDocumentReminderFormMode({ ownerType: 'carrier' }), null) assert.equal(resolveDocumentReminderEditPath({ ownerType: '' }), null) assert.equal(DOCUMENT_REMINDER_JUMP_UNSUPPORTED, '暂不支持该主体类型的证件维护跳转') }) diff --git a/web-admin/src/utils/pl-document-reminder-jump.ts b/web-admin/src/utils/pl-document-reminder-jump.ts index 3c54d8081..eeda96c4d 100644 --- a/web-admin/src/utils/pl-document-reminder-jump.ts +++ b/web-admin/src/utils/pl-document-reminder-jump.ts @@ -6,17 +6,41 @@ export type ReminderJumpInput = { personnelAffiliation?: string | null } -export function resolveDocumentReminderEditPath(input: ReminderJumpInput): string | null { +export type ReminderFormMode = + | { kind: 'vehicle'; vehicleType: 'self' | 'carrier' } + | { kind: 'personnel'; affiliation: 'self' | 'carrier' } + +/** 提醒页本页弹窗:解析车辆/人员表单锁定类型。 */ +export function resolveDocumentReminderFormMode( + input: ReminderJumpInput +): ReminderFormMode | null { const ownerType = String(input.ownerType || '').trim() if (ownerType === 'vehicle') { - return String(input.vehicleType || '').trim() === 'self' - ? '/vehicle-mgmt/base-info/self-vehicle' - : '/vehicle-mgmt/base-info/vehicle' + return { + kind: 'vehicle', + vehicleType: String(input.vehicleType || '').trim() === 'self' ? 'self' : 'carrier' + } } if (ownerType === 'personnel') { - return String(input.personnelAffiliation || '').trim() === 'carrier' - ? '/vehicle-mgmt/base-info/personnel-carrier' - : '/vehicle-mgmt/base-info/personnel' + return { + kind: 'personnel', + affiliation: + String(input.personnelAffiliation || '').trim() === 'carrier' ? 'carrier' : 'self' + } } return null } + +/** 可选深链:档案页 path(提醒主流程已改为本页弹窗)。 */ +export function resolveDocumentReminderEditPath(input: ReminderJumpInput): string | null { + const mode = resolveDocumentReminderFormMode(input) + if (!mode) return null + if (mode.kind === 'vehicle') { + return mode.vehicleType === 'self' + ? '/vehicle-mgmt/base-info/self-vehicle' + : '/vehicle-mgmt/base-info/vehicle' + } + return mode.affiliation === 'carrier' + ? '/vehicle-mgmt/base-info/personnel-carrier' + : '/vehicle-mgmt/base-info/personnel' +} diff --git a/web-admin/src/views/pl/inventory/document-reminder.vue b/web-admin/src/views/pl/inventory/document-reminder.vue index 64c4ddd12..1c51cf3c0 100644 --- a/web-admin/src/views/pl/inventory/document-reminder.vue +++ b/web-admin/src/views/pl/inventory/document-reminder.vue @@ -242,6 +242,19 @@ + + + @@ -250,6 +263,8 @@ import ListQuickSearchInput from '@/components/business/list-quick-search-input/ import ColumnHeaderFilter from '@/components/business/column-header-filter/index.vue' import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue' import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue' +import PlVehicleFormDialog from '@/components/business/pl-vehicle-form-dialog/index.vue' +import PlPersonnelFormDialog from '@/components/business/pl-personnel-form-dialog/index.vue' import { CircleCheck, Filter, EditPen } from '@element-plus/icons-vue' import { inventoryApi, @@ -259,10 +274,9 @@ import { type VehicleDetail, type PersonnelDetail } from '@/api/pl' -import { useRouter } from 'vue-router' import { pickBxData } from '@/utils/http/bx' import { - resolveDocumentReminderEditPath, + resolveDocumentReminderFormMode, DOCUMENT_REMINDER_JUMP_UNSUPPORTED } from '@/utils/pl-document-reminder-jump' import { usePersistedListColumns } from '@/composables/usePersistedListColumns' @@ -355,8 +369,12 @@ const { tableHeight, recalc: recalcTableHeight } = useListTableViewportHeight({ tableRef: docReminderTableRef }) -const router = useRouter() const maintainLoadingId = ref(null) +const vehicleFormVisible = ref(false) +const personnelFormVisible = ref(false) +const vehicleFormType = ref<'self' | 'carrier'>('carrier') +const personnelFormAffiliation = ref<'self' | 'carrier'>('self') +const editingOwnerId = ref(null) const loading = ref(false) const resolveLoading = ref(false) @@ -473,37 +491,52 @@ function handleToolbarResolve() { handleResolve(selectedRow.value) } +function handleOwnerFormSaved() { + ElMessage.success('保存成功') + fetchData() +} + async function handleMaintainCert(row: DocumentReminder) { const ownerId = String(row.ownerId || '').trim() const ownerType = String(row.ownerType || '').trim() if (!ownerId || !ownerType) { - ElMessage.warning('缺少主体信息,无法跳转维护') + ElMessage.warning('缺少主体信息,无法维护证件') return } maintainLoadingId.value = row.id try { - let path: string | null = null + let mode = null as ReturnType if (ownerType === 'vehicle') { const res = await vehicleApi.get(ownerId) const detail = pickBxData(res) || (res as any)?.data || res const vehicleType = String((detail as VehicleDetail)?.vehicleType || '') - path = resolveDocumentReminderEditPath({ ownerType, vehicleType }) + mode = resolveDocumentReminderFormMode({ ownerType, vehicleType }) } else if (ownerType === 'personnel') { const res = await personnelApi.get(ownerId) const detail = pickBxData(res) || (res as any)?.data || res const personnelAffiliation = String( (detail as PersonnelDetail)?.personnelAffiliation || '' ) - path = resolveDocumentReminderEditPath({ ownerType, personnelAffiliation }) + mode = resolveDocumentReminderFormMode({ ownerType, personnelAffiliation }) } else { ElMessage.warning(DOCUMENT_REMINDER_JUMP_UNSUPPORTED) return } - if (!path) { + if (!mode) { ElMessage.warning(DOCUMENT_REMINDER_JUMP_UNSUPPORTED) return } - await router.push({ path, query: { editId: ownerId } }) + editingOwnerId.value = ownerId + vehicleFormVisible.value = false + personnelFormVisible.value = false + await nextTick() + if (mode.kind === 'vehicle') { + vehicleFormType.value = mode.vehicleType + vehicleFormVisible.value = true + } else { + personnelFormAffiliation.value = mode.affiliation + personnelFormVisible.value = true + } } catch { ElMessage.error('无法打开主体档案,请确认数据存在且有权限') } finally {