feat(pl): 调整计划用证布局并在单张证件保存后重置表单。

This commit is contained in:
wuzilong committed 2026-11-02 15:01:07 +08:00
1 parent 26c6dc3a93
commit 0e83e22847
4 files changed
+516 -281

No files matched your search

+339 -267
View File
@@ -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;