fix(pl): 调整证件建档弹窗布局并按所选类型解析单个附件。
This commit is contained in:
1 parent
c16681a16f
commit
4dbd48d827
4 files changed
+478
-32
No files matched your search
@@ -35,3 +35,18 @@ Files: `web-admin/src/views/pl/{plan-quota,quota-cert,legal-use-document}/index.
|
||||
- 最新Web生产构建通过46.61秒(既有chunk体积警告),日志 `/tmp/shanxia-issue8-build.log`。全库vue-tsc仍失败:主目录和本任务均499条错误,归一化行号与目录后新增错误0;不声称全库类型通过。日志 `/tmp/shanxia-issue8-typecheck{,-baseline}.log`。
|
||||
- 本工单未读取运行密钥、连接真实业务数据库或执行SQL,无需新增迁移。H5保留原移动检索、卡片、分页分支。完整应用及真实数据验收仍待用户。
|
||||
- 复用本会话既有隔离目录,任务分支 `codex/gitea-issue-8`,#4分支保留原交付提交。用户已确认三个Web页面范围并免除新Figma;延续本会话本地合入V3.0供用户测试的方式,不推送远端、不关闭工单。保留任务分支与工作区用于反馈修复。
|
||||
|
||||
## 用户转测补充:新增证件弹窗(2026-11-01)
|
||||
|
||||
用户明确要求加宽、每行三个字段、单附件解析、弹窗内单选购买证/运输证并与建档方式同行、区块标题增加绿色竖条。沿用本任务已确认的Web范围及直接修复方式。
|
||||
|
||||
视觉依据:Fresh Mint §9.3.1、全局 `form-grid-3` 与运输计划的区块标题;保留 `AttachmentUploadField`。宽度1200px且限制在视口内,窄屏按全局栅格降列。建档方式与证件类型同行,证面信息每行三项,附件及备注整行。绿色竖条使用现有 section-bar Token。
|
||||
|
||||
- [x] 在 `web-admin/scripts/quota-cert-form.test.ts` 验证真实SFC setup:弹窗类型独立于列表、类型切换安全、单附件自动选择、解析/保存使用所选类型、失效响应不回填;先运行观察失败。
|
||||
- [x] 修改 `web-admin/src/views/pl/quota-cert/index.vue`:新增 `formKind` 驱动表单字段/校验/解析/保存;新增上传限制1个,已有档案保留多附件;切换有内容的类型前提示,保留建档方式/附件/备注,避免错证回填。
|
||||
- [x] 附件组件已支持 `limit` 并覆盖上传/拖拽/粘贴路径,只将文件选择器的 multiple 与 limit 关联,不改全局默认15个。
|
||||
- [x] 执行 Node 专项、Web构建、合成API的实际弹窗交互与截图;审查后延续本地V3.0合入,不推送/关闭工单。无需数据库修改。
|
||||
|
||||
补充验证:新增7项真实setup回归(先失败后通过),连同列表、移动状态、附件预览/分类及证件model共56项通过。真实组件Playwright验证1200px宽度、建档方式/证件类型同行、3px绿色标题竖条,1440/1100/780/500宽度分别3/3/2/1列;单选文件、重复上传拦截(仅1次上传请求)、transport解析及保存请求、取消类型切换恢复选中项均通过,页面错误0。全程合成API,未连接真实上传或模型服务;截图位于忽略目录 `web-admin/.local/cert-dialog/`。
|
||||
|
||||
浏览器检查修正了弹窗根节点scoped样式失效、异步取消类型切换时原生radio选中态未恢复、规则变化提前显示必填错误;日期框已撑满列宽,弹窗高度遵守现有viewport inset变量。最终构建52.97秒通过;vue-tsc仍有历史499项错误,归一化行号比较新增0项。日志 `/tmp/shanxia-cert-dialog-{tests,build,typecheck,browser}.log`。
|
||||
@@ -0,0 +1,266 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
import { parse } from '@vue/compiler-sfc'
|
||||
import ts from 'typescript'
|
||||
import * as vue from 'vue'
|
||||
import * as model from '../src/views/pl/quota-cert/model'
|
||||
import { useClientColumnFilters } from '../src/composables/useClientColumnFilters'
|
||||
import { usePersistedListColumns } from '../src/composables/usePersistedListColumns'
|
||||
|
||||
function deferred() {
|
||||
let resolve!: (value: any) => void
|
||||
const promise = new Promise<any>((r) => {
|
||||
resolve = r
|
||||
})
|
||||
return { promise, resolve }
|
||||
}
|
||||
function harness() {
|
||||
const reads: string[] = [],
|
||||
writes: any[] = [],
|
||||
parses: any[] = [],
|
||||
messages: string[] = []
|
||||
let confirmSwitch = true
|
||||
let pendingOptions: ReturnType<typeof deferred> | undefined
|
||||
const env = {
|
||||
...Object.fromEntries(
|
||||
Object.entries(vue).filter(([key]) => /^[a-zA-Z_$][\w$]*$/.test(key) && key !== 'default')
|
||||
),
|
||||
...model,
|
||||
useClientColumnFilters,
|
||||
usePersistedListColumns,
|
||||
useListTableViewportHeight: () => ({ tableHeight: vue.ref(400) }),
|
||||
useRoute: () => ({ query: {} }),
|
||||
useUserStore: () => ({ accessToken: 'synthetic', getUserInfo: {} }),
|
||||
onMounted: () => {},
|
||||
onBeforeUnmount: () => {},
|
||||
fetchGetMenuCodes: async () => ({ all: true }),
|
||||
ElMessage: {
|
||||
warning: (s: string) => messages.push(s),
|
||||
success: (s: string) => messages.push(s)
|
||||
},
|
||||
ElMessageBox: {
|
||||
confirm: async () => {
|
||||
if (!confirmSwitch) throw new Error('cancel')
|
||||
}
|
||||
},
|
||||
quotaCertApi: {
|
||||
list: async (kind: string) => {
|
||||
reads.push(kind)
|
||||
return pendingOptions ? pendingOptions.promise : []
|
||||
},
|
||||
get: async (kind: string, id: string) => ({
|
||||
...model.newCert(kind as any),
|
||||
id,
|
||||
certNo: 'EXISTING',
|
||||
attachmentUrls: 'a.png,b.png'
|
||||
}),
|
||||
save: async (kind: string, body: any, id?: string) => {
|
||||
writes.push({ kind, body, id })
|
||||
},
|
||||
recognize: (kind: string, url: string) => {
|
||||
const pending = deferred()
|
||||
parses.push({ kind, url, ...pending })
|
||||
return pending.promise
|
||||
}
|
||||
}
|
||||
}
|
||||
const script = parse(
|
||||
readFileSync(new URL('../src/views/pl/quota-cert/index.vue', import.meta.url), 'utf8')
|
||||
).descriptor.scriptSetup!.content
|
||||
const source = ts.createSourceFile('page.ts', script, ts.ScriptTarget.Latest, true)
|
||||
const statements = source.statements.filter((s) => !ts.isImportDeclaration(s))
|
||||
const names = statements.flatMap((s) =>
|
||||
ts.isVariableStatement(s)
|
||||
? s.declarationList.declarations
|
||||
.filter((d) => ts.isIdentifier(d.name))
|
||||
.map((d) => d.name.getText(source))
|
||||
: ts.isFunctionDeclaration(s) && s.name
|
||||
? [s.name.text]
|
||||
: []
|
||||
)
|
||||
const code = ts.transpile(statements.map((s) => s.getText(source)).join('\n'), {
|
||||
target: ts.ScriptTarget.ESNext
|
||||
})
|
||||
const scope = vue.effectScope()
|
||||
const state = scope.run(() =>
|
||||
new Function(...Object.keys(env), `${code}; return {${names.join(',')}}`)(...Object.values(env))
|
||||
)
|
||||
state.formRef.value = { validate: async () => true, clearValidate: () => {} }
|
||||
return {
|
||||
state,
|
||||
reads,
|
||||
writes,
|
||||
parses,
|
||||
messages,
|
||||
deferOptions: () => {
|
||||
pendingOptions = deferred()
|
||||
return pendingOptions
|
||||
},
|
||||
cancelSwitch: () => {
|
||||
confirmSwitch = false
|
||||
},
|
||||
stop: () => scope.stop()
|
||||
}
|
||||
}
|
||||
|
||||
test('type changes immediately while options load and stale options cannot enter a later form', async () => {
|
||||
const h = harness(),
|
||||
s = h.state
|
||||
try {
|
||||
await s.edit()
|
||||
const options = h.deferOptions()
|
||||
const switching = s.changeFormKind('transport')
|
||||
assert.equal(s.formKind.value, 'transport')
|
||||
await s.changeFormKind('purchase')
|
||||
options.resolve([{ id: 'old-option' }])
|
||||
await switching
|
||||
assert.equal(s.formKind.value, 'purchase')
|
||||
assert.deepEqual(s.purchases.value, [])
|
||||
} finally {
|
||||
h.stop()
|
||||
}
|
||||
})
|
||||
|
||||
test('new form type changes without closing dialog or changing the list, and drives save', async () => {
|
||||
const h = harness(),
|
||||
s = h.state
|
||||
try {
|
||||
await s.edit()
|
||||
assert.equal(s.formKind?.value, 'purchase')
|
||||
await s.changeFormKind('transport')
|
||||
assert.equal(s.formKind.value, 'transport')
|
||||
assert.equal(s.kind.value, 'purchase')
|
||||
assert.equal(s.formVisible.value, true)
|
||||
assert.ok(s.formFields.value.some(([key]: string[]) => key === 'shipperName'))
|
||||
assert.ok(!s.formFields.value.some(([key]: string[]) => key === 'buyerName'))
|
||||
s.form.value.shipperName = '合成托运单位'
|
||||
await s.save()
|
||||
assert.equal(h.writes[0].kind, 'transport')
|
||||
assert.equal(h.writes[0].body.shipperName, '合成托运单位')
|
||||
assert.equal(h.writes[0].body.buyerName, undefined)
|
||||
assert.equal(s.kind.value, 'transport')
|
||||
} finally {
|
||||
h.stop()
|
||||
}
|
||||
})
|
||||
|
||||
test('switch clears old certificate data after confirmation, preserving attachments and mode', async () => {
|
||||
const h = harness(),
|
||||
s = h.state
|
||||
try {
|
||||
await s.edit()
|
||||
s.form.value.certNo = 'PURCHASE-1'
|
||||
s.form.value.entryMode = 'image_parse'
|
||||
s.form.value.attachmentUrls = 'one.png'
|
||||
s.form.value.remark = '保留备注'
|
||||
await s.changeFormKind('transport')
|
||||
assert.equal(s.form.value.certNo, '')
|
||||
assert.equal(s.form.value.attachmentUrls, 'one.png')
|
||||
assert.equal(s.form.value.entryMode, 'image_parse')
|
||||
assert.equal(s.form.value.remark, '保留备注')
|
||||
assert.equal(s.form.value.buyerName, undefined)
|
||||
} finally {
|
||||
h.stop()
|
||||
}
|
||||
})
|
||||
|
||||
test('cancel type switch and existing certificate edit preserve data and original type', async () => {
|
||||
const h = harness(),
|
||||
s = h.state
|
||||
try {
|
||||
await s.edit()
|
||||
s.form.value.certNo = 'KEEP'
|
||||
h.cancelSwitch()
|
||||
s.selectedFormKind.value = 'transport'
|
||||
await s.changeFormKind('transport')
|
||||
assert.equal(s.formKind.value, 'purchase')
|
||||
assert.equal(s.selectedFormKind.value, 'purchase')
|
||||
assert.equal(s.form.value.certNo, 'KEEP')
|
||||
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')
|
||||
} finally {
|
||||
h.stop()
|
||||
}
|
||||
})
|
||||
|
||||
test('single attachment is automatically selected, parsing uses form type and preserves manual edits', async () => {
|
||||
const h = harness(),
|
||||
s = h.state
|
||||
try {
|
||||
await s.edit()
|
||||
await s.changeFormKind('transport')
|
||||
s.form.value.attachmentUrls = 'one.png'
|
||||
await vue.nextTick()
|
||||
assert.equal(s.recognitionImage.value, 'one.png')
|
||||
s.form.value.certNo = 'MANUAL'
|
||||
s.edited.add('certNo')
|
||||
const parsing = s.recognize()
|
||||
assert.equal(h.parses.length, 1)
|
||||
assert.equal(h.parses[0].kind, 'transport')
|
||||
assert.equal(h.parses[0].url, 'one.png')
|
||||
h.parses[0].resolve({
|
||||
candidates: { certNo: 'OCR', shipperName: '合成托运单位' },
|
||||
invalidFields: {}
|
||||
})
|
||||
await parsing
|
||||
assert.equal(s.form.value.certNo, 'MANUAL')
|
||||
assert.equal(s.form.value.shipperName, '合成托运单位')
|
||||
} finally {
|
||||
h.stop()
|
||||
}
|
||||
})
|
||||
|
||||
test('missing, removed, multiple new attachments and uploads in progress never start parsing', async () => {
|
||||
const h = harness(),
|
||||
s = h.state
|
||||
try {
|
||||
await s.edit()
|
||||
const empty = s.recognize()
|
||||
assert.equal(h.parses.length, 0)
|
||||
await empty
|
||||
s.form.value.attachmentUrls = 'one.png'
|
||||
await vue.nextTick()
|
||||
s.form.value.attachmentUrls = ''
|
||||
await vue.nextTick()
|
||||
assert.equal(s.recognitionImage.value, '')
|
||||
await s.recognize()
|
||||
s.form.value.attachmentUrls = 'one.png,two.png'
|
||||
s.recognitionImage.value = 'one.png'
|
||||
await s.recognize()
|
||||
s.form.value.attachmentUrls = 'one.png'
|
||||
s.certAttachments.value = { hasUploading: () => true }
|
||||
await s.recognize()
|
||||
assert.equal(h.parses.length, 0)
|
||||
await s.save()
|
||||
assert.equal(h.writes.length, 0)
|
||||
} finally {
|
||||
h.stop()
|
||||
}
|
||||
})
|
||||
|
||||
test('closed or switched forms reject delayed recognition results', async () => {
|
||||
for (const action of ['close', 'switch']) {
|
||||
const h = harness(),
|
||||
s = h.state
|
||||
try {
|
||||
await s.edit()
|
||||
s.form.value.attachmentUrls = 'one.png'
|
||||
await vue.nextTick()
|
||||
const parsing = s.recognize()
|
||||
assert.equal(h.parses.length, 1)
|
||||
if (action === 'close') {
|
||||
s.formVisible.value = false
|
||||
await s.edit()
|
||||
} else await s.changeFormKind('transport')
|
||||
h.parses[0].resolve({ candidates: { certNo: 'OLD' }, invalidFields: {} })
|
||||
await parsing
|
||||
assert.equal(s.form.value.certNo, '')
|
||||
assert.equal(s.recognizing.value, false)
|
||||
} finally {
|
||||
h.stop()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -25,7 +25,7 @@
|
||||
class="att-upload"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:multiple="true"
|
||||
:multiple="limit > 1"
|
||||
:accept="accept"
|
||||
@change="handleAttachmentChange"
|
||||
>
|
||||
@@ -85,7 +85,7 @@
|
||||
ref="uploadRef"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:multiple="true"
|
||||
:multiple="limit > 1"
|
||||
:accept="accept"
|
||||
@change="handleAttachmentChange"
|
||||
>
|
||||
|
||||
@@ -104,10 +104,10 @@
|
||||
</section>
|
||||
<el-dialog
|
||||
v-model="formVisible"
|
||||
:title="`${editingId ? '编辑' : '新增'}${kind === 'purchase' ? '购买证' : '运输证'}`"
|
||||
width="960px"
|
||||
:title="editingId ? `编辑${formKind === 'purchase' ? '购买证' : '运输证'}` : '新增证件'"
|
||||
width="1200px"
|
||||
align-center
|
||||
class="dialog-form-layout"
|
||||
class="dialog-form-layout quota-cert-form-dialog"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="!saving"
|
||||
:show-close="!saving"
|
||||
@@ -117,47 +117,77 @@
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:validate-on-rule-change="false"
|
||||
label-position="top"
|
||||
scroll-to-error
|
||||
>
|
||||
<section class="view-section">
|
||||
<el-form-item label="建档方式"
|
||||
><el-radio-group v-model="form.entryMode" :disabled="!!editingId"
|
||||
<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 label="证件附件"
|
||||
><AttachmentUploadField
|
||||
<el-form-item>
|
||||
<template #label><span class="dialog-section-title">证件类型</span></template>
|
||||
<el-radio-group
|
||||
v-model="selectedFormKind"
|
||||
:disabled="!!editingId"
|
||||
@change="changeFormKind"
|
||||
>
|
||||
<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"
|
||||
: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">
|
||||
<el-select v-model="recognitionImage" placeholder="选择一张已上传的证件图片"
|
||||
><el-option
|
||||
v-for="url in (form.attachmentUrls || '').split(',').filter(Boolean)"
|
||||
:key="url"
|
||||
:value="url"
|
||||
:label="url.split('/').pop()"
|
||||
/></el-select>
|
||||
<el-button :loading="recognizing" :disabled="!recognitionImage" @click="recognize"
|
||||
><el-icon><Document /></el-icon>解析为候选字段</el-button
|
||||
>
|
||||
<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
|
||||
>
|
||||
</div>
|
||||
<p class="form-tip"
|
||||
>请核对证号、数量、单位、有效次数和引用关系。解析只补充未编辑的空字段。</p
|
||||
>
|
||||
</template>
|
||||
</section>
|
||||
<section class="view-section"
|
||||
><h3>证面信息</h3
|
||||
><div class="form-grid-2">
|
||||
><h3 class="dialog-section-title">证面信息</h3
|
||||
><div class="form-grid-3">
|
||||
<el-form-item
|
||||
v-for="field in fields"
|
||||
v-for="field in formFields"
|
||||
:key="field[0]"
|
||||
:label="field[1]"
|
||||
:prop="field[0]"
|
||||
@@ -179,7 +209,7 @@
|
||||
/>
|
||||
</el-form-item> </div
|
||||
></section>
|
||||
<section v-if="kind === 'transport'" class="view-section">
|
||||
<section v-if="formKind === 'transport'" class="view-section">
|
||||
<el-form-item label="车牌号(多车共用额度)"
|
||||
><el-select
|
||||
v-model="form.vehicleNumbersJson"
|
||||
@@ -189,7 +219,7 @@
|
||||
default-first-option
|
||||
@change="edited.add('vehicleNumbersJson')"
|
||||
/></el-form-item>
|
||||
<h3>证面引用</h3>
|
||||
<h3 class="dialog-section-title">证面引用</h3>
|
||||
<el-table :data="form.purchaseRefs" row-key="id" stripe>
|
||||
<el-table-column label="引用类型" min-width="160"
|
||||
><template #default="{ row }"
|
||||
@@ -397,6 +427,8 @@
|
||||
} from './model'
|
||||
const route = useRoute()
|
||||
const kind = ref<CertType>('purchase')
|
||||
const formKind = ref<CertType>('purchase')
|
||||
const selectedFormKind = ref<CertType>('purchase')
|
||||
const keyword = ref(''),
|
||||
page = ref(1)
|
||||
const rows = ref<QuotaCert[]>([]),
|
||||
@@ -430,6 +462,24 @@
|
||||
...commonFields,
|
||||
...(kind.value === 'purchase' ? purchaseFields : transportFields)
|
||||
])
|
||||
const formFields = computed(() => [
|
||||
...commonFields,
|
||||
...(formKind.value === 'purchase' ? purchaseFields : transportFields)
|
||||
])
|
||||
const attachmentOptions = computed<string[]>(() =>
|
||||
(form.value.attachmentUrls || '')
|
||||
.split(',')
|
||||
.map((url: string) => url.trim())
|
||||
.filter(Boolean)
|
||||
)
|
||||
watch(
|
||||
attachmentOptions,
|
||||
(urls) => {
|
||||
if (urls.length === 1) recognitionImage.value = urls[0]
|
||||
else if (!urls.includes(recognitionImage.value)) recognitionImage.value = ''
|
||||
},
|
||||
{ flush: 'sync' }
|
||||
)
|
||||
|
||||
const statusLabels = { draft: '待核对', active: '已启用', suspended: '已停用', void: '已作废' }
|
||||
const refLabels = {
|
||||
@@ -455,7 +505,7 @@
|
||||
'totalQty',
|
||||
'quantityUnit',
|
||||
'validUseText',
|
||||
...(kind.value === 'purchase' ? ['buyerName', 'sellerName'] : ['shipperName'])
|
||||
...(formKind.value === 'purchase' ? ['buyerName', 'sellerName'] : ['shipperName'])
|
||||
].map((k) => [k, [{ required: true, message: '请填写此项', trigger: 'blur' }]])
|
||||
)
|
||||
)
|
||||
@@ -507,6 +557,8 @@
|
||||
const options = targetKind === 'transport' ? await quotaCertApi.list('purchase') : []
|
||||
if (session !== editSession || kind.value !== targetKind) return
|
||||
editingId.value = row?.id || ''
|
||||
formKind.value = targetKind
|
||||
selectedFormKind.value = targetKind
|
||||
edited.clear()
|
||||
recognitionImage.value = ''
|
||||
form.value = value
|
||||
@@ -514,6 +566,54 @@
|
||||
recognizing.value = false
|
||||
formVisible.value = true
|
||||
}
|
||||
async function changeFormKind(value: string | number | boolean | undefined) {
|
||||
try {
|
||||
if (
|
||||
(value !== 'purchase' && value !== 'transport') ||
|
||||
value === formKind.value ||
|
||||
editingId.value ||
|
||||
saving.value
|
||||
)
|
||||
return
|
||||
if (certAttachments.value?.hasUploading()) {
|
||||
ElMessage.warning('请等待附件上传完成')
|
||||
return
|
||||
}
|
||||
const session = editSession
|
||||
const hasContent =
|
||||
formFields.value.some(([key]) => form.value[key] != null && form.value[key] !== '') ||
|
||||
form.value.purchaseRefs?.length ||
|
||||
form.value.vehicleNumbersJson?.length
|
||||
if (hasContent) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'切换证件类型将清空证面信息,保留建档方式、附件和系统备注。是否继续?',
|
||||
'切换证件类型',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
if (session !== editSession || !formVisible.value || saving.value) return
|
||||
const nextSession = ++editSession
|
||||
recognizing.value = false
|
||||
const { entryMode, attachmentUrls, remark, orderNum } = form.value
|
||||
formKind.value = value
|
||||
selectedFormKind.value = value
|
||||
form.value = { ...newCert(value), entryMode, attachmentUrls, remark, orderNum }
|
||||
purchases.value = []
|
||||
edited.clear()
|
||||
formRef.value?.clearValidate()
|
||||
if (value === 'transport') {
|
||||
const options = await quotaCertApi.list('purchase')
|
||||
if (nextSession === editSession && formVisible.value && formKind.value === value)
|
||||
purchases.value = options
|
||||
}
|
||||
} finally {
|
||||
selectedFormKind.value = formKind.value
|
||||
}
|
||||
}
|
||||
async function save() {
|
||||
if (saving.value) return
|
||||
if (certAttachments.value?.hasUploading()) {
|
||||
@@ -521,6 +621,7 @@
|
||||
return
|
||||
}
|
||||
const session = editSession
|
||||
const targetKind = formKind.value
|
||||
saving.value = true
|
||||
if (!(await formRef.value.validate().catch(() => false)) || session !== editSession) {
|
||||
saving.value = false
|
||||
@@ -528,21 +629,34 @@
|
||||
}
|
||||
try {
|
||||
await quotaCertApi.save(
|
||||
kind.value,
|
||||
certPayload(kind.value, form.value),
|
||||
targetKind,
|
||||
certPayload(targetKind, form.value),
|
||||
editingId.value || undefined
|
||||
)
|
||||
formVisible.value = false
|
||||
ElMessage.success('证件已保存')
|
||||
await load()
|
||||
if (kind.value !== targetKind) kind.value = targetKind
|
||||
else await load()
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
async function recognize() {
|
||||
if (recognizing.value || saving.value) return
|
||||
if (certAttachments.value?.hasUploading()) {
|
||||
ElMessage.warning('请等待附件上传完成')
|
||||
return
|
||||
}
|
||||
if (
|
||||
!recognitionImage.value ||
|
||||
!attachmentOptions.value.includes(recognitionImage.value) ||
|
||||
(!editingId.value && attachmentOptions.value.length !== 1)
|
||||
) {
|
||||
ElMessage.warning('请选择一个已上传的证件附件')
|
||||
return
|
||||
}
|
||||
const session = editSession,
|
||||
targetKind = kind.value,
|
||||
targetKind = formKind.value,
|
||||
targetImage = recognitionImage.value
|
||||
recognizing.value = true
|
||||
try {
|
||||
@@ -550,12 +664,15 @@
|
||||
if (
|
||||
session !== editSession ||
|
||||
!formVisible.value ||
|
||||
kind.value !== targetKind ||
|
||||
formKind.value !== targetKind ||
|
||||
recognitionImage.value !== targetImage ||
|
||||
!(form.value.attachmentUrls || '').split(',').includes(targetImage)
|
||||
)
|
||||
return
|
||||
mergeCandidates(form.value as any, result.candidates, edited)
|
||||
await nextTick()
|
||||
if (session !== editSession || !formVisible.value) return
|
||||
formRef.value?.clearValidate()
|
||||
ElMessage.success(
|
||||
Object.keys(result.invalidFields).length
|
||||
? '部分字段未识别,请手工核对补全'
|
||||
@@ -727,3 +844,51 @@
|
||||
tableRef
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.quota-cert-form-dialog {
|
||||
max-width: calc(100vw - 40px);
|
||||
--dialog-viewport-inset: 50px;
|
||||
--dialog-max-height: calc(100vh - 100px);
|
||||
|
||||
.view-section {
|
||||
padding: 0 0 20px;
|
||||
}
|
||||
|
||||
.dialog-section-title {
|
||||
margin: 0 0 14px;
|
||||
padding-left: 8px;
|
||||
border-left: 3px solid var(--section-bar-border-color);
|
||||
color: var(--section-bar-title-color);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
span.dialog-section-title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-grid-2,
|
||||
.form-grid-3 {
|
||||
.el-form-item {
|
||||
min-width: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.el-date-editor.el-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.recognition-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
|
||||
.el-select {
|
||||
width: 320px;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user