feat(pl): 调整计划用证布局并在单张证件保存后重置表单。
This commit is contained in:
1 parent
26c6dc3a93
commit
0e83e22847
4 files changed
+516
-281
No files matched your search
@@ -0,0 +1,44 @@
|
||||
# 计划用证弹窗布局 Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 完成工单 #11 的 Web 弹窗加宽、类型与附件同行、下方左右分区。
|
||||
|
||||
**Architecture:** 仅移动现有 Vue 模板分区并调整局部布局,布局最小调整;用户追加单张处理及保存成功重置需修改本页script setup与既有行为测试,不改共享组件或API。复用全局 form-grid-2;下方以2:3分区,≤1100px堆叠。
|
||||
|
||||
**Tech Stack:** Vue 3、Element Plus、SCSS、pnpm。
|
||||
|
||||
## Task 1: 调整布局并验证
|
||||
|
||||
Files: 修改 `web-admin/src/views/pl/quota-cert/index.vue`;设计依据 `docs/superpowers/specs/2026-11-02-quota-cert-dialog-layout-design.md`。
|
||||
|
||||
- [x] 将新增/编辑弹窗的 `width="1200px"` 改为 `width="1440px"`,保留 max-width 和正文滚动。
|
||||
- [x] 第一行采用局部cert-header-grid,左侧包含建档方式、证件类型,右侧包含原附件及解析模板;保留全部事件和禁用绑定。
|
||||
- [x] 下方cert-content-grid分两列:cert-info-panel保留证面信息form-grid-2及车牌、状态、备注;cert-relations-panel先放原合同产品section,再放原证面引用section。
|
||||
- [x] 局部样式:`.cert-content-grid { display:grid; grid-template-columns:minmax(0,2fr) minmax(0,3fr); gap:24px; align-items:start; }`;各直接子项min-width:0;1100px以下改1fr。首行左侧minmax(0,1fr)、右侧minmax(0,3fr),900px以下1:2,560px以下单列。复用全局form-grid-2的560px降列。
|
||||
- [x] 表格保留最小列宽、固定操作列及本身横向滚动,不改变共享组件;script setup仅按Task 3调整。
|
||||
- [x] 运行 `pnpm --config.verify-deps-before-run=false exec tsx --test scripts/quota-cert-form.test.ts`(本页21项通过)及 `pnpm --config.verify-deps-before-run=false exec eslint src/views/pl/quota-cert/index.vue`(预期无错误)。这是低影响布局调整,不新增仅镜像模板实现的单测。
|
||||
- [x] 运行 `pnpm --config.verify-deps-before-run=false build`;临时 Playwright fixture 直接编译真实弹窗与附件模板,加载真实样式,检查1600/1024/800/375视口、单附件、保存/解析禁用、错误提示。记录合成验证限制,不冒称真实业务E2E。
|
||||
- [x] 需求审查后再代码质量审查,修复阻塞项;`git diff --check` 后按 Conventional Commits 提交。
|
||||
|
||||
## Task 2: 交付
|
||||
|
||||
- [x] 更新设计与验证记录,核对工单最新评论。
|
||||
- [ ] 按现有 push_task_branch 授权推送任务分支。
|
||||
- [ ] 准备包含工单、原型、改动及验证结果的PR正文。create_pr/merge/close_issue 尚未授权,完成可评审代码后仅询问对应具体动作;不继承 #10 的单工单授权。
|
||||
|
||||
## Task 3: 用户追加的单张处理
|
||||
|
||||
- [x] 在既有script harness先写失败测试:零/多附件解析与保存被拒,保存失败保留输入和备注,新增/编辑成功进入同类型新建空表单,下一张无旧ID,旧异步响应失效。
|
||||
- [x] 本页附件limit固定1,删除多附件选择器,移除“可后补图片”矛盾文案。recognize与save统一校验附件数量。
|
||||
- [x] save在API成功且会话有效后重置表单、附件、备注、关系ID与编辑缓存,推进会话版本;保留类型/录入方式且弹窗仍打开。当前备注必须在API调用前已纳入payload。
|
||||
- [x] 修正原测试的默认多附件数据,保留历史异常多附件专用用例;跑完整本页行为回归、eslint/build及更新后的单附件视觉验证。
|
||||
- [x] 重新进行需求与质量审查,先前布局审查不冒充新增业务行为通过。
|
||||
|
||||
## 验证记录
|
||||
|
||||
- 本页真实 script setup 回归21项、共享附件组件回归2项,共23项通过。新增覆盖单附件门槛、编辑后下一张无旧ID、成功重置/失败保留备注,以及运输证候选加载与保存的竞态。
|
||||
- 改动Vue和测试文件ESLint通过;生产构建通过,仍有既有大chunk告警。ESLint读取构建生成的.auto-import.json,必须在构建后顺序执行,避免瞬态缺少自动导入声明。
|
||||
- 实际Vue弹窗/附件模板及真实SCSS的合成浏览器检查:购买证新增和运输证编辑,1600/1440/1024/800/375视口,共10场景通过;系统备注进入真实certPayload,保存/解析禁用、必填提示及固定底栏检查通过。没有真实登录、上传存储或后端写入E2E,不宣称完整环境验收。
|
||||
- 本轮未运行全仓类型检查(已有其他模块错误),未变更数据库、共享附件组件、后端或服务运行环境。
|
||||
- 需求独立复审通过;最终质量审查通过,无P0–P2阻塞项。
|
||||
@@ -1,29 +1,37 @@
|
||||
# 计划用证弹窗布局(工单 #11)
|
||||
# 计划用证弹窗布局(工单 #11,修订 v2)
|
||||
|
||||
工单:https://gitea.aichengyan.com/cykj/shanxia2.0/issues/11
|
||||
|
||||
## 范围与设计
|
||||
|
||||
用户已确认仅修改 Web 新增、编辑购买证及运输证弹窗。目标分支 V3.0,基线 4d10142bfb6c211feee8d82e52bc10f844989cfe。采用 1440px 上限的弹窗,沿用现有视口最大宽度与固定头部、底栏规则。相较全屏弹窗,保留上下文;相较仅改为四列,增加字段可用宽度。
|
||||
用户确认仅修改 Web 新增、编辑购买证及运输证弹窗。用户最新纠正需求:附件放在第一行,下方左侧为证面信息,右侧上方为绑定合同产品、下方为证面引用。此修订替代 v1 的通栏四列布局,不继续执行旧方案。
|
||||
|
||||
建档方式单独一行。下一行左侧为证件类型,右侧为证件附件及其解析操作,宽屏比例约 1:3,窄屏适当缩小类型区,手机窄视口垂直排列。附件沿用 AttachmentUploadField 的 140×96 卡片和自适应换行,不改变上传限制、已上传列表、解析与禁用条件。
|
||||
弹窗最大宽度从1200px增至1440px,保留视口最大宽度、固定头部底栏及正文滚动。第一行为建档方式/证件类型设置和附件区域,附件区域从首行开始展示,解析操作紧随附件。复用 AttachmentUploadField,上传与解析遵循下述新增单张规则,预览、下载和纠正附件能力保留。
|
||||
|
||||
证面信息复用全局 form-grid-4:宽屏四列,视口不超过 900px 时两列,不超过 560px 时一列。字段顺序、数据绑定、必填校验和错误定位不变;日期输入撑满所在列。弹窗底栏固定、正文独立滚动。
|
||||
下方采用约2:3左右布局:左侧证面信息使用现有form-grid-2两列,随证件类型显示相应字段,车牌号、状态和备注随证面信息放置;右侧合同产品分区在上、证面引用分区在下。购买证保持不显示证面引用;原字段、子数据ID和引用保护不变。两表保留水平滚动及固定操作列。
|
||||
|
||||
视口≤1100px时两区按证面信息、合同产品、证面引用顺序上下排列;≤560px时左侧字段及首行布局降为单列。用minmax(0,...)和min-width:0防止表格撑破父容器。无需后端、API或数据库改动。
|
||||
|
||||
## 视觉与复用
|
||||
|
||||
依据 Fresh Mint 规范 §9–10,模式 P12(多分区表单),参考现有 web-admin/src/views/pl/quota-cert/index.vue 与全局 src/assets/styles/core/app.scss。复用 el-dialog、el-form、AttachmentUploadField;无 API、后端或数据库改动。
|
||||
依据Fresh Mint规范§9–10,模式P12,参考现有web-admin/src/views/pl/quota-cert/index.vue及全局src/assets/styles/core/app.scss。复用el-dialog、el-form、AttachmentUploadField及现有表格。
|
||||
|
||||
可编辑原型 v1:https://www.figma.com/design/ImDcikQnCX5GiJQLwUHwDC/shanxia2.0?node-id=36-314
|
||||
可编辑原型v2:https://www.figma.com/design/ImDcikQnCX5GiJQLwUHwDC/shanxia2.0?node-id=39-431
|
||||
|
||||
原型的宽屏、窄屏均已查看;248 个可编辑后代节点,含 46 个组件实例,无完整界面图片。Inter 字体与规范一致。原型重点表达本次布局,已有证面引用、合同绑定及保存规则仍以已验收 #10 实现为准。
|
||||
v2已查看1440px原型截图:附件与首行建档设置顶部对齐;下方证面信息在左、合同产品和证面引用在右上下排列。162个可编辑后代节点、26个组件实例,无界面图片,字体Inter,附件提示使用Noto Sans SC以完整显示中文。原型表达本次分区和单张处理提示;保留已验收#10引用保护。
|
||||
|
||||
## 验收与风险
|
||||
|
||||
Agent 执行针对性验证:真实 Vue 模板与全局样式在 1600、1024、800、375 像素视口渲染;购买证、运输证新增/编辑;多附件、长文件名、解析中、保存中及必填错误;检查字段列数、同行关系、无页面级横向溢出和底栏可达。
|
||||
Agent执行实际Vue弹窗和附件模板的合成渲染,覆盖1600、1440、1024、800、375像素视口,购买证/运输证、新增/编辑、单附件、长文件名、解析中、保存中及必填错误。历史多附件阻止解析/保存由实际script harness验证。核对首行附件、左右/上下顺序、字段列数、水平滚动局限于表格、底栏可达。
|
||||
|
||||
运行既有前端 17 项行为回归、改动文件 ESLint 和生产构建。已知全仓类型检查有既存错误,不以局部检查声称全仓通过。业务 API/上传不连接真实数据,合成组件视觉验证与真实业务联调明确区分。
|
||||
运行21项本页行为回归及2项共享附件组件回归、改动文件ESLint和生产构建。已知全仓类型检查存在其他文件错误;不将组件合成验证称为真实业务E2E。重点检查表格不撑宽分区、窄屏降列和原有保存载荷/引用保护不受影响。
|
||||
|
||||
主要风险是附件区挤压和窄屏字段溢出;通过复用全局降列规则、子项 min-width:0 及真实渲染核查。不得改变 #10 已关联行保护、子数据 ID、表单载荷、图片解析逻辑。
|
||||
状态:用户在v2确认题中补充“备注字段不要丢了”;已补入原型,按v2保留系统备注继续实施。
|
||||
|
||||
状态:范围已确认;原型及本设计等待用户确认。
|
||||
## 用户追加:逐张解析与保存后重置
|
||||
|
||||
用户明确“附件有且只能一张……保存后表单重置,然后才可以继续传下一张进行解析”。新增和编辑上传上限均为1;解析和保存入口均校验恰好一个已上传附件。历史多附件不自动删减,保持内容并提示先整理为单附件。上传或解析进行中禁止保存。
|
||||
|
||||
保存成功后保持弹窗,清空当前业务字段、附件、系统备注、关系和各层旧ID,进入同类型、同建档方式的新建表单。编辑只更新当前证件一次,下一张使用新增API。备注完整提交到当前证件,下一张不继承。保存/校验失败保留原表单;异步识别或选项请求不能污染重置后的新表单。用户可删除错误附件纠正当前单,但不可累积或批量解析多张。
|
||||
|
||||
范围仍为Web;不自动修改历史记录或其他客户端/API。新增真实业务回归覆盖单附件门槛、成功重置、失败保留、旧ID隔离、旧异步响应隔离。
|
||||
@@ -78,7 +78,7 @@ function harness() {
|
||||
...model.newCert(kind as any),
|
||||
id,
|
||||
certNo: 'EXISTING',
|
||||
attachmentUrls: 'a.png,b.png',
|
||||
attachmentUrls: 'a.png',
|
||||
...certData
|
||||
},
|
||||
save: async (kind: string, body: any, id?: string) => {
|
||||
@@ -148,6 +148,7 @@ test('type changes immediately while options load and stale options cannot enter
|
||||
s = h.state
|
||||
try {
|
||||
await s.edit()
|
||||
s.form.value.attachmentUrls = 'one.png'
|
||||
const options = h.deferOptions()
|
||||
const switching = s.changeFormKind('transport')
|
||||
assert.equal(s.formKind.value, 'transport')
|
||||
@@ -166,6 +167,7 @@ test('new form type changes without closing dialog or changing the list, and dri
|
||||
s = h.state
|
||||
try {
|
||||
await s.edit()
|
||||
s.form.value.attachmentUrls = 'one.png'
|
||||
assert.equal(s.formKind?.value, 'purchase')
|
||||
await s.changeFormKind('transport')
|
||||
assert.equal(s.formKind.value, 'transport')
|
||||
@@ -189,6 +191,7 @@ test('switch clears old certificate data after confirmation, preserving attachme
|
||||
s = h.state
|
||||
try {
|
||||
await s.edit()
|
||||
s.form.value.attachmentUrls = 'one.png'
|
||||
s.form.value.certNo = 'PURCHASE-1'
|
||||
s.form.value.entryMode = 'image_parse'
|
||||
s.form.value.attachmentUrls = 'one.png'
|
||||
@@ -209,6 +212,7 @@ test('cancel type switch and existing certificate edit preserve data and origina
|
||||
s = h.state
|
||||
try {
|
||||
await s.edit()
|
||||
s.form.value.attachmentUrls = 'one.png'
|
||||
s.form.value.certNo = 'KEEP'
|
||||
h.cancelSwitch()
|
||||
s.selectedFormKind.value = 'transport'
|
||||
@@ -219,7 +223,7 @@ test('cancel type switch and existing certificate edit preserve data and origina
|
||||
await s.edit({ id: 'existing' })
|
||||
await s.changeFormKind('transport')
|
||||
assert.equal(s.formKind.value, 'purchase')
|
||||
assert.equal(s.form.value.attachmentUrls, 'a.png,b.png')
|
||||
assert.equal(s.form.value.attachmentUrls, 'a.png')
|
||||
} finally {
|
||||
h.stop()
|
||||
}
|
||||
@@ -230,6 +234,7 @@ test('single attachment is automatically selected, parsing uses form type and pr
|
||||
s = h.state
|
||||
try {
|
||||
await s.edit()
|
||||
s.form.value.attachmentUrls = 'one.png'
|
||||
await s.changeFormKind('transport')
|
||||
s.form.value.attachmentUrls = 'one.png'
|
||||
await vue.nextTick()
|
||||
@@ -257,6 +262,7 @@ test('missing, removed, multiple new attachments and uploads in progress never s
|
||||
s = h.state
|
||||
try {
|
||||
await s.edit()
|
||||
s.form.value.attachmentUrls = ''
|
||||
const empty = s.recognize()
|
||||
assert.equal(h.parses.length, 0)
|
||||
await empty
|
||||
@@ -293,6 +299,7 @@ test('closed or switched forms reject delayed recognition results', async () =>
|
||||
if (action === 'close') {
|
||||
s.formVisible.value = false
|
||||
await s.edit()
|
||||
s.form.value.attachmentUrls = 'one.png'
|
||||
} else await s.changeFormKind('transport')
|
||||
h.parses[0].resolve({ candidates: { certNo: 'OLD' }, invalidFields: {} })
|
||||
await parsing
|
||||
@@ -355,6 +362,7 @@ test('产品勾选往返保留原范围ID;关闭重开后拒绝旧合同和产
|
||||
s = h.state
|
||||
try {
|
||||
await s.edit()
|
||||
s.form.value.attachmentUrls = 'one.png'
|
||||
s.addContractBinding('c')
|
||||
h.productRequests[0].resolve([{ id: 'p' }, { id: 'p2' }])
|
||||
await vue.nextTick()
|
||||
@@ -368,6 +376,7 @@ test('产品勾选往返保留原范围ID;关闭重开后拒绝旧合同和产
|
||||
const products = s.loadBindingProducts('c')
|
||||
s.formVisible.value = false
|
||||
await s.edit()
|
||||
s.form.value.attachmentUrls = 'one.png'
|
||||
h.contractRequests.at(-1).resolve([{ id: 'stale', contractNo: '旧合同' }])
|
||||
h.productRequests.forEach((r) => r.resolve([{ id: 'stale', productName: '旧产品' }]))
|
||||
await Promise.all([search, products])
|
||||
@@ -407,6 +416,7 @@ test('后发合同搜索覆盖先发,租户切换使保存和读取旧结果
|
||||
s = h.state
|
||||
try {
|
||||
await s.edit()
|
||||
s.form.value.attachmentUrls = 'one.png'
|
||||
const first = s.searchBindingContracts('first'),
|
||||
second = s.searchBindingContracts('second')
|
||||
h.contractRequests[1].resolve([{ id: 'second', contractNo: 'SECOND', contractName: '' }])
|
||||
@@ -438,6 +448,7 @@ test('新增多合同连同证面成功提交一次,移除全部提交明确
|
||||
s = h.state
|
||||
try {
|
||||
await s.edit()
|
||||
s.form.value.attachmentUrls = 'one.png'
|
||||
s.addContractBinding('one')
|
||||
s.addContractBinding('two')
|
||||
h.productRequests[0].resolve([{ id: 'p1' }])
|
||||
@@ -453,7 +464,7 @@ test('新增多合同连同证面成功提交一次,移除全部提交明确
|
||||
h.writes[0].body.contractBindings.map((b: any) => b.contractId),
|
||||
['one', 'two']
|
||||
)
|
||||
assert.equal(s.formVisible.value, false)
|
||||
assert.equal(s.formVisible.value, true)
|
||||
h.setCert({ contractBindings: [{ id: 'b', contractId: 'one', products: [] }] })
|
||||
await s.edit({ id: 'existing' })
|
||||
s.removeContractBinding(0)
|
||||
@@ -469,6 +480,7 @@ test('同合同产品后发请求优先,移除重加使移除前请求失效',
|
||||
s = h.state
|
||||
try {
|
||||
await s.edit()
|
||||
s.form.value.attachmentUrls = 'one.png'
|
||||
s.addContractBinding('c')
|
||||
const second = s.loadBindingProducts('c')
|
||||
h.productRequests[1].resolve([{ id: 'new', productName: '新产品' }])
|
||||
@@ -496,6 +508,7 @@ test('产品加载失败可再次展开重试,加载中不能改选择', async
|
||||
s = h.state
|
||||
try {
|
||||
await s.edit()
|
||||
s.form.value.attachmentUrls = 'one.png'
|
||||
s.addContractBinding('c')
|
||||
h.productRequests[0].reject(new Error('network'))
|
||||
await vue.nextTick()
|
||||
@@ -518,6 +531,7 @@ test('inline binding adds an empty row, requires contract, and resets products o
|
||||
s = h.state
|
||||
try {
|
||||
await s.edit()
|
||||
s.form.value.attachmentUrls = 'one.png'
|
||||
s.addContractBinding()
|
||||
assert.equal(s.form.value.contractBindings.length, 1)
|
||||
assert.equal(s.form.value.contractBindings[0].contractId, '')
|
||||
@@ -589,3 +603,100 @@ test('recognition cannot populate locked empty references while other empty fiel
|
||||
h.stop()
|
||||
}
|
||||
})
|
||||
|
||||
test('successful edit clears attachment and remark and becomes a fresh open form', async () => {
|
||||
const h = harness(),
|
||||
s = h.state
|
||||
try {
|
||||
await s.edit({ id: 'existing' })
|
||||
s.form.value.attachmentUrls = 'one.png'
|
||||
s.form.value.remark = '本张备注'
|
||||
await s.save()
|
||||
assert.equal(h.writes[0].body.remark, '本张备注')
|
||||
assert.equal(s.formVisible.value, true)
|
||||
assert.equal(s.editingId.value, '')
|
||||
assert.equal(s.form.value.attachmentUrls, '')
|
||||
assert.equal(s.form.value.remark, '')
|
||||
s.form.value.attachmentUrls = 'next.png'
|
||||
await s.save()
|
||||
assert.equal(h.writes[1].id, undefined)
|
||||
} finally {
|
||||
h.stop()
|
||||
}
|
||||
})
|
||||
|
||||
test('save and recognition reject missing or historical multiple attachments', async () => {
|
||||
const h = harness(),
|
||||
s = h.state
|
||||
try {
|
||||
await s.edit({ id: 'existing' })
|
||||
for (const urls of ['', 'a.png,b.png']) {
|
||||
s.form.value.attachmentUrls = urls
|
||||
await vue.nextTick()
|
||||
s.recognitionImage.value = urls.split(',')[0]
|
||||
await s.save()
|
||||
const parsing = s.recognize()
|
||||
for (const p of h.parses) p.resolve({ candidates: {}, invalidFields: {} })
|
||||
await parsing
|
||||
}
|
||||
assert.equal(h.writes.length, 0)
|
||||
assert.equal(h.parses.length, 0)
|
||||
} finally {
|
||||
h.stop()
|
||||
}
|
||||
})
|
||||
|
||||
test('new transport save keeps its type open while failures and recognition preserve the current remark', async () => {
|
||||
const h = harness(),
|
||||
s = h.state
|
||||
try {
|
||||
await s.edit()
|
||||
await s.changeFormKind('transport')
|
||||
s.form.value.attachmentUrls = 'one.png'
|
||||
s.form.value.remark = '不得丢失'
|
||||
await s.save()
|
||||
assert.equal(s.formVisible.value, true)
|
||||
assert.equal(s.kind.value, 'transport')
|
||||
assert.equal(s.formKind.value, 'transport')
|
||||
assert.equal(s.form.value.remark, '')
|
||||
assert.deepEqual(s.form.value.purchaseRefs, [])
|
||||
assert.deepEqual(s.form.value.contractBindings, [])
|
||||
s.form.value.attachmentUrls = 'two.png'
|
||||
s.form.value.remark = '失败保留'
|
||||
s.recognizing.value = true
|
||||
await s.save()
|
||||
assert.equal(h.writes.length, 1)
|
||||
assert.equal(s.form.value.remark, '失败保留')
|
||||
s.recognizing.value = false
|
||||
h.failSave()
|
||||
await assert.rejects(s.save(), /保存失败/)
|
||||
assert.equal(s.form.value.remark, '失败保留')
|
||||
assert.equal(s.form.value.attachmentUrls, 'two.png')
|
||||
assert.equal(s.formVisible.value, true)
|
||||
} finally {
|
||||
h.stop()
|
||||
}
|
||||
})
|
||||
|
||||
test('saving while transport purchase candidates load refreshes candidates for the next session', async () => {
|
||||
const h = harness(),
|
||||
s = h.state
|
||||
try {
|
||||
await s.edit()
|
||||
const old = h.deferOptions()
|
||||
const switching = s.changeFormKind('transport')
|
||||
const fresh = h.deferOptions()
|
||||
s.form.value.attachmentUrls = 'one.png'
|
||||
await s.save()
|
||||
old.resolve([{ id: 'stale' }])
|
||||
await switching
|
||||
assert.deepEqual(s.purchases.value, [])
|
||||
fresh.resolve([{ id: 'fresh' }])
|
||||
await Promise.resolve()
|
||||
await vue.nextTick()
|
||||
assert.deepEqual(s.purchases.value, [{ id: 'fresh' }])
|
||||
assert.equal(s.formVisible.value, true)
|
||||
} finally {
|
||||
h.stop()
|
||||
}
|
||||
})
|
||||
@@ -105,7 +105,7 @@
|
||||
<el-dialog
|
||||
v-model="formVisible"
|
||||
:title="editingId ? `编辑${formKind === 'purchase' ? '购买证' : '运输证'}` : '新增证件'"
|
||||
width="1200px"
|
||||
width="1440px"
|
||||
align-center
|
||||
class="dialog-form-layout quota-cert-form-dialog"
|
||||
:close-on-click-modal="false"
|
||||
@@ -121,270 +121,270 @@
|
||||
label-position="top"
|
||||
scroll-to-error
|
||||
>
|
||||
<section class="view-section form-grid-2">
|
||||
<el-form-item>
|
||||
<template #label><span class="dialog-section-title">建档方式</span></template>
|
||||
<el-radio-group v-model="form.entryMode" :disabled="!!editingId || recognizing"
|
||||
><el-radio value="manual">手工录入(可后补图片)</el-radio
|
||||
><el-radio value="image_parse">图片解析后核对</el-radio></el-radio-group
|
||||
></el-form-item
|
||||
>
|
||||
<el-form-item>
|
||||
<template #label><span class="dialog-section-title">证件类型</span></template>
|
||||
<el-radio-group
|
||||
v-model="selectedFormKind"
|
||||
:disabled="!!editingId"
|
||||
@change="changeFormKind"
|
||||
<section class="view-section cert-header-grid">
|
||||
<div class="cert-entry-settings">
|
||||
<el-form-item>
|
||||
<template #label><span class="dialog-section-title">建档方式</span></template>
|
||||
<el-radio-group v-model="form.entryMode" :disabled="!!editingId || recognizing"
|
||||
><el-radio value="manual">手工录入</el-radio
|
||||
><el-radio value="image_parse">图片解析后核对</el-radio></el-radio-group
|
||||
></el-form-item
|
||||
>
|
||||
<el-radio value="purchase">购买证</el-radio>
|
||||
<el-radio value="transport">运输证</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</section>
|
||||
<section class="view-section">
|
||||
<el-form-item>
|
||||
<template #label><span class="dialog-section-title">证件附件</span></template>
|
||||
<AttachmentUploadField
|
||||
ref="certAttachments"
|
||||
:key="editSession"
|
||||
:disabled="saving || recognizing"
|
||||
v-model="form.attachmentUrls"
|
||||
folder="pl/quota-cert"
|
||||
:limit="editingId ? 15 : 1"
|
||||
accept="image/*,.pdf"
|
||||
:hint="
|
||||
editingId
|
||||
? '支持图片、PDF;每次仅解析一个附件'
|
||||
: '支持图片、PDF;每次仅上传并解析一个附件,更换请先删除原附件'
|
||||
"
|
||||
@update:model-value="edited.add('attachmentUrls')"
|
||||
/></el-form-item>
|
||||
<template v-if="form.entryMode === 'image_parse' || !!form.attachmentUrls">
|
||||
<div class="recognition-actions">
|
||||
<el-select
|
||||
v-if="editingId && attachmentOptions.length > 1"
|
||||
v-model="recognitionImage"
|
||||
:disabled="recognizing"
|
||||
placeholder="选择一个附件进行解析"
|
||||
><el-option
|
||||
v-for="url in attachmentOptions"
|
||||
:key="url"
|
||||
:value="url"
|
||||
:label="url.split('/').pop()"
|
||||
/></el-select>
|
||||
<el-button :loading="recognizing" :disabled="!recognitionImage" @click="recognize"
|
||||
><el-icon><Document /></el-icon>解析{{
|
||||
formKind === 'purchase' ? '购买证' : '运输证'
|
||||
}}</el-button
|
||||
<el-form-item>
|
||||
<template #label><span class="dialog-section-title">证件类型</span></template>
|
||||
<el-radio-group
|
||||
v-model="selectedFormKind"
|
||||
:disabled="!!editingId"
|
||||
@change="changeFormKind"
|
||||
>
|
||||
</div>
|
||||
<p class="form-tip"
|
||||
>请核对证号、数量、单位、有效次数和引用关系。解析只补充未编辑的空字段。</p
|
||||
>
|
||||
</template>
|
||||
</section>
|
||||
<section class="view-section"
|
||||
><h3 class="dialog-section-title">证面信息</h3
|
||||
><div class="form-grid-3">
|
||||
<el-form-item
|
||||
v-for="field in formFields"
|
||||
:key="field[0]"
|
||||
:label="field[1]"
|
||||
:prop="field[0]"
|
||||
>
|
||||
<el-date-picker
|
||||
v-if="isDate(field[0])"
|
||||
v-model="form[field[0]]"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
@change="edited.add(field[0])"
|
||||
/>
|
||||
<el-input
|
||||
v-else
|
||||
v-model="form[field[0]]"
|
||||
:placeholder="
|
||||
field[0] === 'validUseText' ? '例如:一次有效、多次有效' : `请输入${field[1]}`
|
||||
"
|
||||
@input="edited.add(field[0])"
|
||||
/>
|
||||
<el-radio value="purchase">购买证</el-radio>
|
||||
<el-radio value="transport">运输证</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item> </div
|
||||
></section>
|
||||
<section v-if="formKind === 'transport'" class="view-section">
|
||||
<el-form-item label="车牌号(多车共用额度)"
|
||||
><el-select
|
||||
v-model="form.vehicleNumbersJson"
|
||||
multiple
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
@change="edited.add('vehicleNumbersJson')"
|
||||
/></el-form-item>
|
||||
<h3 class="dialog-section-title">证面引用</h3>
|
||||
<el-button
|
||||
class="mb-3"
|
||||
type="primary"
|
||||
:disabled="saving || form.referencesEditable === false"
|
||||
@click="addReference"
|
||||
><el-icon><Plus /></el-icon>添加引用</el-button
|
||||
>
|
||||
<p v-if="form.referencesEditable === false" class="form-tip"
|
||||
>已有用证关联,不能编辑或移除证面引用,请联系管理员。</p
|
||||
>
|
||||
<el-table :data="form.purchaseRefs" row-key="id" stripe>
|
||||
<el-table-column label="引用类型" min-width="160"
|
||||
><template #default="{ row }"
|
||||
><el-select
|
||||
v-model="row.referenceType"
|
||||
:disabled="saving || form.referencesEditable === false"
|
||||
@change="edited.add('purchaseRefs')"
|
||||
><el-option
|
||||
v-for="(label, value) in refLabels"
|
||||
:key="value"
|
||||
:value="value"
|
||||
:label="label" /></el-select></template
|
||||
></el-table-column>
|
||||
<el-table-column label="引用证号" min-width="160"
|
||||
><template #default="{ row }"
|
||||
><el-input
|
||||
v-model="row.referenceNo"
|
||||
:disabled="saving || form.referencesEditable === false"
|
||||
@input="edited.add('purchaseRefs')" /></template
|
||||
></el-table-column>
|
||||
<el-table-column label="对应购买证档案" min-width="200"
|
||||
><template #default="{ row }"
|
||||
><el-select
|
||||
v-model="row.purchaseCertId"
|
||||
:disabled="saving || form.referencesEditable === false"
|
||||
filterable
|
||||
clearable
|
||||
@change="edited.add('purchaseRefs')"
|
||||
><el-option
|
||||
v-for="c in purchases"
|
||||
:key="c.id"
|
||||
:value="c.id"
|
||||
:label="c.certNo" /></el-select></template
|
||||
></el-table-column>
|
||||
<el-table-column label="操作" min-width="260" fixed="right"
|
||||
><template #default="{ $index }"
|
||||
><el-button
|
||||
:disabled="saving || form.referencesEditable === false"
|
||||
@click="removeReference($index)"
|
||||
><el-icon><Delete /></el-icon>移除引用</el-button
|
||||
></template
|
||||
></el-table-column
|
||||
>
|
||||
</el-table>
|
||||
</section>
|
||||
<section class="view-section">
|
||||
<h3 class="dialog-section-title">绑定合同产品</h3>
|
||||
<p v-if="form.bindingsEditable === false" class="form-tip">
|
||||
当前仅展示可见合同,绑定范围不可编辑;证面信息仍可保存。
|
||||
</p>
|
||||
<el-button
|
||||
v-if="form.bindingsEditable !== false"
|
||||
class="mb-3"
|
||||
type="primary"
|
||||
:disabled="saving"
|
||||
@click="addContractBinding()"
|
||||
><el-icon><Plus /></el-icon>添加合同</el-button
|
||||
>
|
||||
<el-table :data="form.contractBindings || []" stripe>
|
||||
<el-table-column label="合同" min-width="220">
|
||||
<template #default="{ row, $index }">
|
||||
<el-select
|
||||
:model-value="row.contractId"
|
||||
filterable
|
||||
remote
|
||||
placeholder="请选择合同"
|
||||
:remote-method="searchBindingContracts"
|
||||
:loading="bindingSearching"
|
||||
:disabled="saving || form.bindingsEditable === false || row.editLocked"
|
||||
@visible-change="(open: boolean) => open && searchBindingContracts('')"
|
||||
@change="(id: string) => changeBindingContract($index, id)"
|
||||
><div class="cert-attachment-section">
|
||||
<el-form-item>
|
||||
<template #label><span class="dialog-section-title">证件附件</span></template>
|
||||
<AttachmentUploadField
|
||||
ref="certAttachments"
|
||||
:key="editSession"
|
||||
:disabled="saving || recognizing"
|
||||
v-model="form.attachmentUrls"
|
||||
folder="pl/quota-cert"
|
||||
:limit="1"
|
||||
accept="image/*,.pdf"
|
||||
hint="一次仅上传并解析一张证件;保存成功后自动清空,继续处理下一张"
|
||||
@update:model-value="edited.add('attachmentUrls')"
|
||||
/></el-form-item>
|
||||
<template v-if="form.entryMode === 'image_parse' || !!form.attachmentUrls">
|
||||
<div class="recognition-actions">
|
||||
<el-button :loading="recognizing" :disabled="!recognitionImage" @click="recognize"
|
||||
><el-icon><Document /></el-icon>解析{{
|
||||
formKind === 'purchase' ? '购买证' : '运输证'
|
||||
}}</el-button
|
||||
>
|
||||
<el-option
|
||||
v-for="contract in bindingContracts"
|
||||
:key="contract.id"
|
||||
:value="contract.id"
|
||||
:label="contract.contractNo + ' · ' + contract.contractName"
|
||||
:disabled="
|
||||
form.contractBindings.some(
|
||||
(b: CertificateContractBinding, i: number) =>
|
||||
i !== $index && b.contractId === contract.id
|
||||
)
|
||||
"
|
||||
/>
|
||||
<el-option
|
||||
v-if="row.contractId && !bindingContracts.some((c) => c.id === row.contractId)"
|
||||
:value="row.contractId"
|
||||
label="合同信息不可用"
|
||||
/>
|
||||
</el-select>
|
||||
<p v-if="row.editLocked" class="form-tip" style="white-space: normal"
|
||||
>已有用证关联,不能编辑或移除,请联系管理员。</p
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="适用合同产品" min-width="360">
|
||||
<template #default="{ row, $index }">
|
||||
<el-select
|
||||
multiple
|
||||
:model-value="row.products.map((p: any) => p.contractProductId)"
|
||||
:disabled="
|
||||
saving || form.bindingsEditable === false || row.editLocked || !row.contractId
|
||||
"
|
||||
:loading="bindingProductLoading[row.contractId]"
|
||||
:placeholder="row.contractId ? '勾选适用产品' : '请先选择合同'"
|
||||
style="width: 100%"
|
||||
@change="(ids: string[]) => setBindingProducts($index, ids)"
|
||||
@visible-change="(open: boolean) => open && loadBindingProducts(row.contractId)"
|
||||
>
|
||||
<el-option
|
||||
v-for="product in bindingProductOptions(row)"
|
||||
:key="product.id"
|
||||
:value="product.id"
|
||||
:disabled="bindingProductLoading[row.contractId]"
|
||||
:label="
|
||||
product.productName +
|
||||
(product.model ? ' / ' + product.model : '') +
|
||||
(product.unit ? '(' + product.unit + ')' : '')
|
||||
"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" min-width="100">
|
||||
<template #default="{ row }"
|
||||
><el-tag :type="row.linkStatus === 'inactive' ? 'info' : 'success'" size="small">{{
|
||||
row.linkStatus === 'inactive' ? '已停用' : '启用'
|
||||
}}</el-tag></template
|
||||
</div>
|
||||
<p class="form-tip"
|
||||
>请核对证号、数量、单位、有效次数和引用关系。解析只补充未编辑的空字段。</p
|
||||
>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="form.bindingsEditable !== false"
|
||||
label="操作"
|
||||
min-width="260"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ row, $index }"
|
||||
><el-button
|
||||
:disabled="saving || row.editLocked"
|
||||
@click="removeContractBinding($index)"
|
||||
><el-icon><Delete /></el-icon>移除合同</el-button
|
||||
></template
|
||||
</template>
|
||||
</div></section
|
||||
>
|
||||
<div class="cert-content-grid"
|
||||
><div class="cert-info-panel">
|
||||
<section class="view-section"
|
||||
><h3 class="dialog-section-title">证面信息</h3
|
||||
><div class="form-grid-2">
|
||||
<el-form-item
|
||||
v-for="field in formFields"
|
||||
:key="field[0]"
|
||||
:label="field[1]"
|
||||
:prop="field[0]"
|
||||
>
|
||||
<el-date-picker
|
||||
v-if="isDate(field[0])"
|
||||
v-model="form[field[0]]"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
@change="edited.add(field[0])"
|
||||
/>
|
||||
<el-input
|
||||
v-else
|
||||
v-model="form[field[0]]"
|
||||
:placeholder="
|
||||
field[0] === 'validUseText' ? '例如:一次有效、多次有效' : `请输入${field[1]}`
|
||||
"
|
||||
@input="edited.add(field[0])"
|
||||
/>
|
||||
</el-form-item> </div
|
||||
></section>
|
||||
<el-form-item v-if="formKind === 'transport'" label="车牌号(多车共用额度)"
|
||||
><el-select
|
||||
v-model="form.vehicleNumbersJson"
|
||||
multiple
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
@change="edited.add('vehicleNumbersJson')"
|
||||
/></el-form-item>
|
||||
<el-form-item v-if="editingId" label="停止新增使用">
|
||||
<el-select v-model="form.status" clearable placeholder="保持当前状态">
|
||||
<el-option label="停用(可复核恢复)" value="suspended" />
|
||||
<el-option label="作废(不可恢复)" value="void" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="系统备注"
|
||||
><el-input v-model="form.remark" type="textarea"
|
||||
/></el-form-item> </div
|
||||
><div class="cert-relations-panel">
|
||||
<section class="view-section">
|
||||
<h3 class="dialog-section-title">绑定合同产品</h3>
|
||||
<p v-if="form.bindingsEditable === false" class="form-tip">
|
||||
当前仅展示可见合同,绑定范围不可编辑;证面信息仍可保存。
|
||||
</p>
|
||||
<el-button
|
||||
v-if="form.bindingsEditable !== false"
|
||||
class="mb-3"
|
||||
type="primary"
|
||||
:disabled="saving"
|
||||
@click="addContractBinding()"
|
||||
><el-icon><Plus /></el-icon>添加合同</el-button
|
||||
>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
<el-form-item v-if="editingId" label="停止新增使用">
|
||||
<el-select v-model="form.status" clearable placeholder="保持当前状态">
|
||||
<el-option label="停用(可复核恢复)" value="suspended" />
|
||||
<el-option label="作废(不可恢复)" value="void" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="系统备注"
|
||||
><el-input v-model="form.remark" type="textarea"
|
||||
/></el-form-item>
|
||||
<el-table :data="form.contractBindings || []" stripe>
|
||||
<el-table-column label="合同" min-width="220">
|
||||
<template #default="{ row, $index }">
|
||||
<el-select
|
||||
:model-value="row.contractId"
|
||||
filterable
|
||||
remote
|
||||
placeholder="请选择合同"
|
||||
:remote-method="searchBindingContracts"
|
||||
:loading="bindingSearching"
|
||||
:disabled="saving || form.bindingsEditable === false || row.editLocked"
|
||||
@visible-change="(open: boolean) => open && searchBindingContracts('')"
|
||||
@change="(id: string) => changeBindingContract($index, id)"
|
||||
>
|
||||
<el-option
|
||||
v-for="contract in bindingContracts"
|
||||
:key="contract.id"
|
||||
:value="contract.id"
|
||||
:label="contract.contractNo + ' · ' + contract.contractName"
|
||||
:disabled="
|
||||
form.contractBindings.some(
|
||||
(b: CertificateContractBinding, i: number) =>
|
||||
i !== $index && b.contractId === contract.id
|
||||
)
|
||||
"
|
||||
/>
|
||||
<el-option
|
||||
v-if="
|
||||
row.contractId && !bindingContracts.some((c) => c.id === row.contractId)
|
||||
"
|
||||
:value="row.contractId"
|
||||
label="合同信息不可用"
|
||||
/>
|
||||
</el-select>
|
||||
<p v-if="row.editLocked" class="form-tip" style="white-space: normal"
|
||||
>已有用证关联,不能编辑或移除,请联系管理员。</p
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="适用合同产品" min-width="360">
|
||||
<template #default="{ row, $index }">
|
||||
<el-select
|
||||
multiple
|
||||
:model-value="row.products.map((p: any) => p.contractProductId)"
|
||||
:disabled="
|
||||
saving ||
|
||||
form.bindingsEditable === false ||
|
||||
row.editLocked ||
|
||||
!row.contractId
|
||||
"
|
||||
:loading="bindingProductLoading[row.contractId]"
|
||||
:placeholder="row.contractId ? '勾选适用产品' : '请先选择合同'"
|
||||
style="width: 100%"
|
||||
@change="(ids: string[]) => setBindingProducts($index, ids)"
|
||||
@visible-change="
|
||||
(open: boolean) => open && loadBindingProducts(row.contractId)
|
||||
"
|
||||
>
|
||||
<el-option
|
||||
v-for="product in bindingProductOptions(row)"
|
||||
:key="product.id"
|
||||
:value="product.id"
|
||||
:disabled="bindingProductLoading[row.contractId]"
|
||||
:label="
|
||||
product.productName +
|
||||
(product.model ? ' / ' + product.model : '') +
|
||||
(product.unit ? '(' + product.unit + ')' : '')
|
||||
"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" min-width="100">
|
||||
<template #default="{ row }"
|
||||
><el-tag
|
||||
:type="row.linkStatus === 'inactive' ? 'info' : 'success'"
|
||||
size="small"
|
||||
>{{ row.linkStatus === 'inactive' ? '已停用' : '启用' }}</el-tag
|
||||
></template
|
||||
>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="form.bindingsEditable !== false"
|
||||
label="操作"
|
||||
min-width="260"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ row, $index }"
|
||||
><el-button
|
||||
:disabled="saving || row.editLocked"
|
||||
@click="removeContractBinding($index)"
|
||||
><el-icon><Delete /></el-icon>移除合同</el-button
|
||||
></template
|
||||
>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
<section v-if="formKind === 'transport'" class="view-section">
|
||||
<h3 class="dialog-section-title">证面引用</h3>
|
||||
<el-button
|
||||
class="mb-3"
|
||||
type="primary"
|
||||
:disabled="saving || form.referencesEditable === false"
|
||||
@click="addReference"
|
||||
><el-icon><Plus /></el-icon>添加引用</el-button
|
||||
>
|
||||
<p v-if="form.referencesEditable === false" class="form-tip"
|
||||
>已有用证关联,不能编辑或移除证面引用,请联系管理员。</p
|
||||
>
|
||||
<el-table :data="form.purchaseRefs" row-key="id" stripe>
|
||||
<el-table-column label="引用类型" min-width="160"
|
||||
><template #default="{ row }"
|
||||
><el-select
|
||||
v-model="row.referenceType"
|
||||
:disabled="saving || form.referencesEditable === false"
|
||||
@change="edited.add('purchaseRefs')"
|
||||
><el-option
|
||||
v-for="(label, value) in refLabels"
|
||||
:key="value"
|
||||
:value="value"
|
||||
:label="label" /></el-select></template
|
||||
></el-table-column>
|
||||
<el-table-column label="引用证号" min-width="160"
|
||||
><template #default="{ row }"
|
||||
><el-input
|
||||
v-model="row.referenceNo"
|
||||
:disabled="saving || form.referencesEditable === false"
|
||||
@input="edited.add('purchaseRefs')" /></template
|
||||
></el-table-column>
|
||||
<el-table-column label="对应购买证档案" min-width="200"
|
||||
><template #default="{ row }"
|
||||
><el-select
|
||||
v-model="row.purchaseCertId"
|
||||
:disabled="saving || form.referencesEditable === false"
|
||||
filterable
|
||||
clearable
|
||||
@change="edited.add('purchaseRefs')"
|
||||
><el-option
|
||||
v-for="c in purchases"
|
||||
:key="c.id"
|
||||
:value="c.id"
|
||||
:label="c.certNo" /></el-select></template
|
||||
></el-table-column>
|
||||
<el-table-column label="操作" min-width="260" fixed="right"
|
||||
><template #default="{ $index }"
|
||||
><el-button
|
||||
:disabled="saving || form.referencesEditable === false"
|
||||
@click="removeReference($index)"
|
||||
><el-icon><Delete /></el-icon>移除引用</el-button
|
||||
></template
|
||||
></el-table-column
|
||||
>
|
||||
</el-table>
|
||||
</section>
|
||||
</div></div
|
||||
>
|
||||
</el-form>
|
||||
<template #footer
|
||||
><el-button :disabled="saving" @click="formVisible = false"
|
||||
@@ -920,7 +920,11 @@
|
||||
}
|
||||
}
|
||||
async function save() {
|
||||
if (saving.value) return
|
||||
if (saving.value || recognizing.value) return
|
||||
if (attachmentOptions.value.length !== 1) {
|
||||
ElMessage.warning('请保留且仅上传一张证件附件')
|
||||
return
|
||||
}
|
||||
if (
|
||||
form.value.bindingsEditable !== false &&
|
||||
form.value.contractBindings?.some((b: CertificateContractBinding) => !b.contractId)
|
||||
@@ -946,10 +950,46 @@
|
||||
editingId.value || undefined
|
||||
)
|
||||
if (session !== editSession || disposed) return
|
||||
formVisible.value = false
|
||||
ElMessage.success('证件已保存')
|
||||
if (kind.value !== targetKind) kind.value = targetKind
|
||||
else await load()
|
||||
const entryMode = form.value.entryMode
|
||||
const switchedKind = kind.value !== targetKind
|
||||
if (switchedKind) kind.value = targetKind
|
||||
editSession++
|
||||
editingId.value = ''
|
||||
formKind.value = targetKind
|
||||
selectedFormKind.value = targetKind
|
||||
edited.clear()
|
||||
recognitionImage.value = ''
|
||||
bindingSearchEpoch++
|
||||
bindingContracts.value = []
|
||||
bindingProducts.value = {}
|
||||
bindingProductLoading.value = {}
|
||||
bindingProductEpochs.clear()
|
||||
bindingSearching.value = false
|
||||
originalScopes.clear()
|
||||
removedBindings.clear()
|
||||
form.value = {
|
||||
...newCert(targetKind),
|
||||
entryMode,
|
||||
contractBindings: [],
|
||||
bindingsEditable: true
|
||||
}
|
||||
recognizing.value = false
|
||||
formVisible.value = true
|
||||
purchases.value = []
|
||||
if (targetKind === 'transport') {
|
||||
const nextSession = editSession
|
||||
void quotaCertApi
|
||||
.list('purchase')
|
||||
.then((options) => {
|
||||
if (nextSession === editSession && formVisible.value && formKind.value === targetKind)
|
||||
purchases.value = options
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
await nextTick()
|
||||
formRef.value?.clearValidate()
|
||||
ElMessage.success('证件已保存,请上传下一张证件')
|
||||
if (!switchedKind) await load()
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
@@ -963,7 +1003,7 @@
|
||||
if (
|
||||
!recognitionImage.value ||
|
||||
!attachmentOptions.value.includes(recognitionImage.value) ||
|
||||
(!editingId.value && attachmentOptions.value.length !== 1)
|
||||
attachmentOptions.value.length !== 1
|
||||
) {
|
||||
ElMessage.warning('请选择一个已上传的证件附件')
|
||||
return
|
||||
@@ -1191,8 +1231,7 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-grid-2,
|
||||
.form-grid-3 {
|
||||
.form-grid-2 {
|
||||
.el-form-item {
|
||||
min-width: 0;
|
||||
margin-bottom: 0;
|
||||
@@ -1203,7 +1242,40 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cert-header-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 3fr);
|
||||
gap: 24px;
|
||||
}
|
||||
.cert-content-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
}
|
||||
.cert-entry-settings,
|
||||
.cert-attachment-section,
|
||||
.cert-info-panel,
|
||||
.cert-relations-panel {
|
||||
min-width: 0;
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.cert-content-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.cert-header-grid {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
|
||||
}
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
.cert-header-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
.recognition-actions {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
|
||||
Reference in new issue
Block a user