提交
This commit is contained in:
1 parent
ffc89d9b57
commit
c2cba3e668
53 files changed
+11794
-6264
No files matched your search
@@ -147,6 +147,8 @@ export interface Personnel {
|
||||
phoneNo?: string
|
||||
age?: number
|
||||
idCardNo?: string
|
||||
personnelType: string
|
||||
personnelAffiliation: string
|
||||
status: string
|
||||
remark?: string
|
||||
nearestExpireDate?: string
|
||||
@@ -162,6 +164,8 @@ export interface PersonnelPayload {
|
||||
phoneNo?: string
|
||||
age?: number | null
|
||||
idCardNo?: string
|
||||
personnelType: string
|
||||
personnelAffiliation: string
|
||||
status: string
|
||||
remark?: string
|
||||
certList?: CertRow[]
|
||||
@@ -170,6 +174,7 @@ export interface PersonnelPayload {
|
||||
export interface PersonnelQuery {
|
||||
q?: string
|
||||
status?: string
|
||||
personnelAffiliation?: string
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
<div class="cert-form-header">
|
||||
<span class="cert-form-title">{{ title }}</span>
|
||||
<el-button type="primary" size="small" @click="handleAddCert">
|
||||
<el-icon><Plus /></el-icon>添加证书
|
||||
<el-icon><Plus /></el-icon>{{ addButtonLabel }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div v-if="!certList.length" class="cert-empty">暂无证书,点击「添加证书」</div>
|
||||
<div v-if="!certList.length" class="cert-empty">{{ emptyHint }}</div>
|
||||
|
||||
<el-table v-else-if="layout === 'table'" :data="certList" stripe class="list-table cert-form-table">
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
|
||||
@@ -20,7 +20,7 @@
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-if="hasTrailerPlateOption"
|
||||
v-if="hasTrailerPlateOption && !isRowRequired(row)"
|
||||
:model-value="normalizeVehicleCertPart(row.vehiclePart)"
|
||||
placeholder="请选择车牌号"
|
||||
style="width: 100%"
|
||||
@@ -33,12 +33,17 @@
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
<span v-else>{{ headPlateLabel }}</span>
|
||||
<span v-else>{{ resolvePlateLabel(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="证书名称" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<span v-if="isRowRequired(row)" class="cert-name-locked">
|
||||
{{ row.certName }}
|
||||
<el-tag type="info" size="small">必备</el-tag>
|
||||
</span>
|
||||
<el-select
|
||||
v-else
|
||||
v-model="row.certName"
|
||||
placeholder="请选择或输入"
|
||||
filterable
|
||||
@@ -87,8 +92,8 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="60" align="center" header-align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button link type="danger" @click="handleRemoveCert($index)">
|
||||
<template #default="{ row, $index }">
|
||||
<el-button v-if="!isRowRequired(row)" link type="danger" @click="handleRemoveCert($index)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -98,15 +103,19 @@
|
||||
<div v-else class="cert-card-list">
|
||||
<div v-for="(cert, index) in certList" :key="cert._key" class="cert-card">
|
||||
<div class="cert-card-head">
|
||||
<span class="cert-card-index">证书 {{ index + 1 }}</span>
|
||||
<el-button link type="danger" @click="handleRemoveCert(index)">
|
||||
<span class="cert-card-index">
|
||||
{{ isRowRequired(cert) ? `必备 · ${cert.certName}` : `证书 ${index + 1}` }}
|
||||
</span>
|
||||
<el-button v-if="!isRowRequired(cert)" link type="danger" @click="handleRemoveCert(index)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="form-grid-4 cert-card-grid">
|
||||
<el-form-item label="证书名称" required>
|
||||
<span v-if="isRowRequired(cert)" class="cert-name-locked">{{ cert.certName }}</span>
|
||||
<el-select
|
||||
v-else
|
||||
v-model="cert.certName"
|
||||
placeholder="请选择或输入证书名称"
|
||||
filterable
|
||||
@@ -155,12 +164,23 @@ import { Delete, Plus } from '@element-plus/icons-vue'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import type { CertRow } from '@/api/pl'
|
||||
import {
|
||||
ensureCarrierVehicleCertSlots,
|
||||
ensureSelfVehicleCertSlots,
|
||||
mergeCertListWithRequiredSlots,
|
||||
type CertRowWithMeta
|
||||
} from '@/utils/pl-cert-slots'
|
||||
import { matchRequiredCertName } from '@/utils/pl-personnel-cert'
|
||||
import {
|
||||
PL_CARRIER_VEHICLE_HEAD_REQUIRED_CERT_NAMES,
|
||||
PL_CARRIER_VEHICLE_TRAILER_REQUIRED_CERT_NAMES,
|
||||
PL_SELF_VEHICLE_HEAD_REQUIRED_CERT_NAMES,
|
||||
PL_SELF_VEHICLE_TRAILER_REQUIRED_CERT_NAMES,
|
||||
PL_VEHICLE_CERT_PART_HEAD,
|
||||
PL_VEHICLE_CERT_PART_TRAILER,
|
||||
normalizeVehicleCertPart
|
||||
} from '@/utils/pl-vehicle-cert'
|
||||
|
||||
type CertRowWithKey = CertRow & { _key: string }
|
||||
type CertRowWithKey = CertRowWithMeta & { _key: string }
|
||||
type CertUploadRef = InstanceType<typeof AttachmentUploadField>
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -169,10 +189,16 @@ const props = withDefaults(
|
||||
uploadFolder: string
|
||||
certNameOptions?: string[]
|
||||
layout?: 'card' | 'table'
|
||||
/** 列表模式:展示车牌号列,可从主车/挂车车牌中选择 */
|
||||
showVehiclePartColumn?: boolean
|
||||
licensePlate?: string
|
||||
trailerNo?: string
|
||||
/** 人员/单段车辆:固定必备证书名称 */
|
||||
requiredCertNames?: readonly string[]
|
||||
requiredVehiclePart?: string
|
||||
/** 山峡自有车统一表格:按主车/挂车自动注入必备槽位 */
|
||||
enforceSelfVehicleRequired?: boolean
|
||||
/** 承运商车辆:按主车/挂车注入必备槽位(行驶证、营运证) */
|
||||
enforceCarrierVehicleRequired?: boolean
|
||||
}>(),
|
||||
{
|
||||
title: '证书',
|
||||
@@ -180,10 +206,27 @@ const props = withDefaults(
|
||||
layout: 'card',
|
||||
showVehiclePartColumn: false,
|
||||
licensePlate: '',
|
||||
trailerNo: ''
|
||||
trailerNo: '',
|
||||
requiredCertNames: () => [],
|
||||
requiredVehiclePart: PL_VEHICLE_CERT_PART_HEAD,
|
||||
enforceSelfVehicleRequired: false,
|
||||
enforceCarrierVehicleRequired: false
|
||||
}
|
||||
)
|
||||
|
||||
const hasRequiredMode = computed(
|
||||
() =>
|
||||
props.enforceSelfVehicleRequired ||
|
||||
props.enforceCarrierVehicleRequired ||
|
||||
(props.requiredCertNames?.length ?? 0) > 0
|
||||
)
|
||||
|
||||
const addButtonLabel = computed(() => (hasRequiredMode.value ? '添加其他证书' : '添加证书'))
|
||||
|
||||
const emptyHint = computed(() =>
|
||||
hasRequiredMode.value ? '暂无证书' : '暂无证书,点击「添加证书」'
|
||||
)
|
||||
|
||||
const headPlateLabel = computed(() => props.licensePlate?.trim() || '—')
|
||||
const hasTrailerPlateOption = computed(() => Boolean(props.trailerNo?.trim()))
|
||||
|
||||
@@ -203,23 +246,88 @@ const vehiclePlateOptions = computed(() => {
|
||||
return options
|
||||
})
|
||||
|
||||
function resolvePlateLabel(row: CertRowWithKey) {
|
||||
const part = normalizeVehicleCertPart(row.vehiclePart)
|
||||
if (part === PL_VEHICLE_CERT_PART_TRAILER) {
|
||||
return props.trailerNo?.trim() || '挂车'
|
||||
}
|
||||
return headPlateLabel.value
|
||||
}
|
||||
|
||||
function requiredNamesForRow(row: CertRowWithKey): readonly string[] {
|
||||
if (props.requiredCertNames?.length) {
|
||||
return props.requiredCertNames
|
||||
}
|
||||
if (!props.enforceSelfVehicleRequired && !props.enforceCarrierVehicleRequired) return []
|
||||
const part = normalizeVehicleCertPart(row.vehiclePart)
|
||||
if (props.enforceCarrierVehicleRequired) {
|
||||
return part === PL_VEHICLE_CERT_PART_TRAILER
|
||||
? PL_CARRIER_VEHICLE_TRAILER_REQUIRED_CERT_NAMES
|
||||
: PL_CARRIER_VEHICLE_HEAD_REQUIRED_CERT_NAMES
|
||||
}
|
||||
return part === PL_VEHICLE_CERT_PART_TRAILER
|
||||
? PL_SELF_VEHICLE_TRAILER_REQUIRED_CERT_NAMES
|
||||
: PL_SELF_VEHICLE_HEAD_REQUIRED_CERT_NAMES
|
||||
}
|
||||
|
||||
function isRowRequired(row: CertRowWithKey) {
|
||||
if (row._required) return true
|
||||
const requiredNames = requiredNamesForRow(row)
|
||||
if (!requiredNames.length) return false
|
||||
return requiredNames.some(name => matchRequiredCertName(row.certName, name))
|
||||
}
|
||||
|
||||
function syncRequiredSlots() {
|
||||
if (props.enforceSelfVehicleRequired) {
|
||||
certList.value = ensureSelfVehicleCertSlots(certList.value, props.trailerNo)
|
||||
return
|
||||
}
|
||||
if (props.enforceCarrierVehicleRequired) {
|
||||
certList.value = ensureCarrierVehicleCertSlots(certList.value, props.trailerNo)
|
||||
return
|
||||
}
|
||||
if (props.requiredCertNames?.length) {
|
||||
certList.value = mergeCertListWithRequiredSlots(certList.value, props.requiredCertNames, {
|
||||
vehiclePart: props.requiredVehiclePart
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function handleVehiclePartChange(row: CertRowWithKey, value: string) {
|
||||
if (isRowRequired(row)) return
|
||||
row.vehiclePart = normalizeVehicleCertPart(value)
|
||||
}
|
||||
|
||||
const certList = defineModel<CertRowWithKey[]>('modelValue', { default: () => [] })
|
||||
|
||||
watch(
|
||||
() =>
|
||||
[
|
||||
props.enforceSelfVehicleRequired,
|
||||
props.enforceCarrierVehicleRequired,
|
||||
props.requiredCertNames,
|
||||
props.trailerNo
|
||||
] as const,
|
||||
() => {
|
||||
if (hasRequiredMode.value) {
|
||||
syncRequiredSlots()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const certUploadRefs = ref<Record<number, CertUploadRef | null>>({})
|
||||
|
||||
function createEmptyCert(): CertRowWithKey {
|
||||
return {
|
||||
_key: `cert-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
vehiclePart: PL_VEHICLE_CERT_PART_HEAD,
|
||||
vehiclePart: props.requiredVehiclePart || PL_VEHICLE_CERT_PART_HEAD,
|
||||
certName: '',
|
||||
certNo: '',
|
||||
expireDate: '',
|
||||
remindDays: 30,
|
||||
attachmentUrls: ''
|
||||
attachmentUrls: '',
|
||||
_required: false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,6 +336,8 @@ function handleAddCert() {
|
||||
}
|
||||
|
||||
function handleRemoveCert(index: number) {
|
||||
const row = certList.value[index]
|
||||
if (row && isRowRequired(row)) return
|
||||
const next = [...certList.value]
|
||||
next.splice(index, 1)
|
||||
certList.value = next
|
||||
@@ -250,7 +360,11 @@ function clearUploadRefs() {
|
||||
certUploadRefs.value = {}
|
||||
}
|
||||
|
||||
defineExpose({ hasUploading, clearUploadRefs })
|
||||
function refreshRequiredSlots() {
|
||||
syncRequiredSlots()
|
||||
}
|
||||
|
||||
defineExpose({ hasUploading, clearUploadRefs, refreshRequiredSlots })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -282,6 +396,14 @@ defineExpose({ hasUploading, clearUploadRefs })
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.cert-name-locked {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 500;
|
||||
color: var(--text, #1e293b);
|
||||
}
|
||||
|
||||
.cert-form-table {
|
||||
border-top: 1px solid var(--mint-border, #d9e8e2);
|
||||
|
||||
|
||||
@@ -26,61 +26,67 @@
|
||||
</section>
|
||||
|
||||
<section class="pl-plan-form-section pl-plan-freight-section">
|
||||
<h4 class="pl-plan-subtitle">运费信息</h4>
|
||||
<div class="pl-plan-freight-section-head">
|
||||
<h4 class="pl-plan-subtitle">运费信息</h4>
|
||||
<label class="pl-plan-freight-switch">
|
||||
<span class="pl-plan-freight-switch-label">是否有运输费用</span>
|
||||
<el-switch v-model="formData.hasFreight" :active-value="1" :inactive-value="0" />
|
||||
</label>
|
||||
</div>
|
||||
<el-form
|
||||
v-if="formData.hasFreight === 1"
|
||||
:model="formData"
|
||||
label-width="140px"
|
||||
label-position="right"
|
||||
class="pl-plan-freight-form"
|
||||
>
|
||||
<div class="form-grid-4">
|
||||
<el-form-item label="是否有运输费用">
|
||||
<el-switch v-model="formData.hasFreight" :active-value="1" :inactive-value="0" />
|
||||
<div class="form-grid-4 pl-plan-archive-freight-grid">
|
||||
<el-form-item label="运费计算方式">
|
||||
<el-select v-model="formData.freightType">
|
||||
<el-option label="按重量" value="by_weight" />
|
||||
<el-option label="按趟" value="by_trip" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="运输公里数">
|
||||
<el-input-number
|
||||
v-model="formData.distanceKm"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="formData.freightType === 'by_weight'"
|
||||
:label="`运输量(${freightUnitLabel})`"
|
||||
>
|
||||
<el-input-number
|
||||
v-model="freightCalc.transportQty.value"
|
||||
:min="0"
|
||||
:precision="3"
|
||||
@change="freightCalc.markTransportQtyTouched()"
|
||||
/>
|
||||
</el-form-item>
|
||||
<div v-else class="pl-plan-archive-freight-grid-cell" aria-hidden="true" />
|
||||
|
||||
<div class="pl-plan-archive-freight-grid-cell" aria-hidden="true" />
|
||||
<el-form-item
|
||||
v-if="formData.freightType === 'by_weight'"
|
||||
:label="`运输单价(元/${freightUnitLabel})`"
|
||||
>
|
||||
<el-input-number
|
||||
v-model="freightCalc.freightPrice.value"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
/>
|
||||
</el-form-item>
|
||||
<div v-else class="pl-plan-archive-freight-grid-cell" aria-hidden="true" />
|
||||
<el-form-item label="运费金额(元)">
|
||||
<el-input-number
|
||||
v-model="freightCalc.freightAmount.value"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
@change="freightCalc.markFreightAmountTouched()"
|
||||
/>
|
||||
</el-form-item>
|
||||
<template v-if="formData.hasFreight === 1">
|
||||
<el-form-item label="运输公里数">
|
||||
<el-input-number
|
||||
v-model="formData.distanceKm"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="运费计算方式">
|
||||
<el-select v-model="formData.freightType">
|
||||
<el-option label="按重量" value="by_weight" />
|
||||
<el-option label="按趟" value="by_trip" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="formData.freightType === 'by_weight'"
|
||||
:label="`运输量(${freightUnitLabel})`"
|
||||
>
|
||||
<el-input-number
|
||||
v-model="freightCalc.transportQty.value"
|
||||
:min="0"
|
||||
:precision="3"
|
||||
@change="freightCalc.markTransportQtyTouched()"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="formData.freightType === 'by_weight'"
|
||||
:label="`运输单价(元/${freightUnitLabel})`"
|
||||
>
|
||||
<el-input-number
|
||||
v-model="freightCalc.freightPrice.value"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="运费金额(元)">
|
||||
<el-input-number
|
||||
v-model="freightCalc.freightAmount.value"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
@change="freightCalc.markFreightAmountTouched()"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</div>
|
||||
</el-form>
|
||||
</section>
|
||||
@@ -119,7 +125,10 @@ import {
|
||||
import PlPlanFormProductTable from '@/components/business/pl-plan-form-dialog/PlPlanFormProductTable.vue'
|
||||
import PlPlanViewTabs from '@/components/business/pl-plan-view-tabs/index.vue'
|
||||
import type { PlanFormProductRow } from '@/composables/usePlContractToPlan'
|
||||
import { enrichPlanProductsHazardFromCrm } from '@/composables/usePlToxicProductCertCheck'
|
||||
import {
|
||||
buildMissingHazardCertReadyMessage,
|
||||
enrichPlanProductsHazardFromCrm
|
||||
} from '@/composables/usePlToxicProductCertCheck'
|
||||
import { usePlFreightCalc, resolveFreightUnitLabel } from '@/composables/usePlFreightCalc'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { pickBxData } from '@/utils/http/bx'
|
||||
@@ -302,6 +311,16 @@ async function handleSubmit() {
|
||||
return
|
||||
}
|
||||
|
||||
if (!isAdjustMode.value) {
|
||||
const certMessage = buildMissingHazardCertReadyMessage(plan.value, productRows.value)
|
||||
if (certMessage) {
|
||||
await ElMessageBox.alert(`${certMessage},请至【危化品证上传】页面操作。`, '无法归档', {
|
||||
type: 'warning'
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const body: PlanArchiveBody = {
|
||||
@@ -392,6 +411,7 @@ async function handleSubmit() {
|
||||
.pl-plan-freight-section {
|
||||
.pl-plan-subtitle {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
@@ -400,6 +420,28 @@ async function handleSubmit() {
|
||||
}
|
||||
}
|
||||
|
||||
.pl-plan-freight-section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 16px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.pl-plan-freight-switch {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pl-plan-freight-switch-label {
|
||||
font-size: 13px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pl-plan-freight-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
@@ -418,8 +460,13 @@ async function handleSubmit() {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.form-grid-4 {
|
||||
.pl-plan-archive-freight-grid {
|
||||
align-items: center;
|
||||
row-gap: 12px;
|
||||
}
|
||||
|
||||
.pl-plan-archive-freight-grid-cell {
|
||||
min-height: 1px;
|
||||
}
|
||||
|
||||
:deep(.el-select),
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
show-vehicle-part-column
|
||||
:license-plate="licensePlate"
|
||||
:trailer-no="trailerNo"
|
||||
:enforce-self-vehicle-required="enforceSelfVehicleRequired"
|
||||
:enforce-carrier-vehicle-required="enforceCarrierVehicleRequired"
|
||||
/>
|
||||
|
||||
<el-tabs
|
||||
@@ -73,13 +75,19 @@ const props = withDefaults(
|
||||
certNameOptions?: string[]
|
||||
title?: string
|
||||
layout?: 'card' | 'table'
|
||||
/** 山峡自有车:注入必备证书槽位并校验 */
|
||||
enforceSelfVehicleRequired?: boolean
|
||||
/** 承运商车辆:注入必备证书槽位并校验 */
|
||||
enforceCarrierVehicleRequired?: boolean
|
||||
}>(),
|
||||
{
|
||||
licensePlate: '',
|
||||
trailerNo: '',
|
||||
certNameOptions: () => [],
|
||||
title: '车辆证书',
|
||||
layout: 'card'
|
||||
layout: 'card',
|
||||
enforceSelfVehicleRequired: false,
|
||||
enforceCarrierVehicleRequired: false
|
||||
}
|
||||
)
|
||||
|
||||
@@ -149,9 +157,16 @@ function clearUploadRefs() {
|
||||
trailerCertFormRef.value?.clearUploadRefs()
|
||||
}
|
||||
|
||||
function refreshRequiredSlots() {
|
||||
unifiedCertFormRef.value?.refreshRequiredSlots?.()
|
||||
headCertFormRef.value?.refreshRequiredSlots?.()
|
||||
trailerCertFormRef.value?.refreshRequiredSlots?.()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
hasUploading,
|
||||
clearUploadRefs
|
||||
clearUploadRefs,
|
||||
refreshRequiredSlots
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -46,7 +46,8 @@ import type { ViewTableMode } from '@/components/business/system-view-dialog/typ
|
||||
import {
|
||||
PL_VEHICLE_CERT_PART_HEAD,
|
||||
PL_VEHICLE_CERT_PART_TRAILER,
|
||||
getSelfVehiclePartCertMissingSuffix,
|
||||
getVehiclePartCertMissingSuffix,
|
||||
type PlVehicleCertCheckType,
|
||||
splitVehicleCertsByPart
|
||||
} from '@/utils/pl-vehicle-cert'
|
||||
|
||||
@@ -56,6 +57,8 @@ const props = withDefaults(
|
||||
licensePlate?: string
|
||||
trailerNo?: string
|
||||
vehicleId?: string
|
||||
vehicleCertCheckType?: PlVehicleCertCheckType
|
||||
/** @deprecated 使用 vehicleCertCheckType */
|
||||
checkSelfVehicleCerts?: boolean
|
||||
tableMode?: ViewTableMode
|
||||
certLayout?: 'card' | 'table'
|
||||
@@ -65,6 +68,7 @@ const props = withDefaults(
|
||||
licensePlate: '',
|
||||
trailerNo: '',
|
||||
vehicleId: '',
|
||||
vehicleCertCheckType: undefined,
|
||||
checkSelfVehicleCerts: false,
|
||||
tableMode: 'triple',
|
||||
certLayout: 'card'
|
||||
@@ -98,16 +102,26 @@ const scopedCertList = computed(() => {
|
||||
const headCertList = computed(() => splitVehicleCertsByPart(scopedCertList.value).headCerts)
|
||||
const trailerCertList = computed(() => splitVehicleCertsByPart(scopedCertList.value).trailerCerts)
|
||||
|
||||
const resolvedVehicleCertCheckType = computed<PlVehicleCertCheckType | undefined>(() => {
|
||||
if (props.vehicleCertCheckType) return props.vehicleCertCheckType
|
||||
return props.checkSelfVehicleCerts ? 'self' : undefined
|
||||
})
|
||||
|
||||
const headTabLabelWithHint = computed(() => {
|
||||
if (!props.checkSelfVehicleCerts) return headTabLabel.value
|
||||
return headTabLabel.value + getSelfVehiclePartCertMissingSuffix(headCertList.value, PL_VEHICLE_CERT_PART_HEAD)
|
||||
const checkType = resolvedVehicleCertCheckType.value
|
||||
if (!checkType) return headTabLabel.value
|
||||
return (
|
||||
headTabLabel.value +
|
||||
getVehiclePartCertMissingSuffix(headCertList.value, PL_VEHICLE_CERT_PART_HEAD, checkType)
|
||||
)
|
||||
})
|
||||
|
||||
const trailerTabLabelWithHint = computed(() => {
|
||||
if (!props.checkSelfVehicleCerts) return trailerTabLabel.value
|
||||
const checkType = resolvedVehicleCertCheckType.value
|
||||
if (!checkType) return trailerTabLabel.value
|
||||
return (
|
||||
trailerTabLabel.value +
|
||||
getSelfVehiclePartCertMissingSuffix(trailerCertList.value, PL_VEHICLE_CERT_PART_TRAILER)
|
||||
getVehiclePartCertMissingSuffix(trailerCertList.value, PL_VEHICLE_CERT_PART_TRAILER, checkType)
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
:license-plate="licensePlate"
|
||||
:trailer-no="trailerNo"
|
||||
:vehicle-id="vehicleId"
|
||||
:check-self-vehicle-certs="checkSelfVehicleCerts"
|
||||
:vehicle-cert-check-type="vehicleCertCheckType"
|
||||
:table-mode="tableMode"
|
||||
:cert-layout="certLayout"
|
||||
/>
|
||||
@@ -20,7 +20,7 @@
|
||||
>
|
||||
<PersonnelViewPanel
|
||||
:person="person"
|
||||
:check-self-vehicle-certs="checkSelfVehicleCerts"
|
||||
:check-linked-personnel-certs="showLinkedPersonnelCertCheck"
|
||||
:table-mode="tableMode"
|
||||
:cert-layout="certLayout"
|
||||
/>
|
||||
@@ -34,7 +34,7 @@ import PersonnelViewPanel from '@/components/business/pl-vehicle-personnel-view-
|
||||
import type { CertRead } from '@/api/pl'
|
||||
import type { VehiclePersonnelViewRow } from '@/components/business/pl-vehicle-personnel-view-list/index.vue'
|
||||
import type { ViewTableMode } from '@/components/business/system-view-dialog/types'
|
||||
import { getSelfVehicleCertMissingSuffix } from '@/utils/pl-vehicle-cert'
|
||||
import { getVehicleCertMissingSuffix, type PlVehicleCertCheckType } from '@/utils/pl-vehicle-cert'
|
||||
import { getPersonnelCertMissingSuffix } from '@/utils/pl-personnel-cert'
|
||||
import { formatPlPersonnelNameWithRole } from '@/utils/pl-display'
|
||||
|
||||
@@ -45,6 +45,10 @@ const props = withDefaults(
|
||||
trailerNo?: string
|
||||
vehicleId?: string
|
||||
personnelList?: VehiclePersonnelViewRow[]
|
||||
vehicleCertCheckType?: PlVehicleCertCheckType
|
||||
/** 山峡自有车:关联人员证书缺失提示 */
|
||||
checkLinkedPersonnelCerts?: boolean
|
||||
/** @deprecated 使用 vehicleCertCheckType + checkLinkedPersonnelCerts */
|
||||
checkSelfVehicleCerts?: boolean
|
||||
tableMode?: ViewTableMode
|
||||
certLayout?: 'card' | 'table'
|
||||
@@ -55,6 +59,8 @@ const props = withDefaults(
|
||||
trailerNo: '',
|
||||
vehicleId: '',
|
||||
personnelList: () => [],
|
||||
vehicleCertCheckType: undefined,
|
||||
checkLinkedPersonnelCerts: false,
|
||||
checkSelfVehicleCerts: false,
|
||||
tableMode: 'triple',
|
||||
certLayout: 'card'
|
||||
@@ -73,23 +79,29 @@ const tabsInstanceKey = computed(
|
||||
|
||||
const vehicleCertTabLabel = computed(() => {
|
||||
const label = '车辆证书'
|
||||
if (!props.checkSelfVehicleCerts) return label
|
||||
const checkType = props.vehicleCertCheckType
|
||||
if (!checkType) return label
|
||||
return (
|
||||
label +
|
||||
getSelfVehicleCertMissingSuffix({
|
||||
getVehicleCertMissingSuffix({
|
||||
vehicleType: checkType,
|
||||
certList: props.certList,
|
||||
trailerNo: props.trailerNo
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
const showLinkedPersonnelCertCheck = computed(
|
||||
() => props.checkLinkedPersonnelCerts || props.checkSelfVehicleCerts
|
||||
)
|
||||
|
||||
function getPersonKey(person: VehiclePersonnelViewRow, index: number) {
|
||||
return `person-${props.vehicleId || props.licensePlate || 'vehicle'}-${person.personnelId || `${person.personnelName}-${index}`}`
|
||||
}
|
||||
|
||||
function getPersonTabLabel(person: VehiclePersonnelViewRow) {
|
||||
const suffix =
|
||||
props.checkSelfVehicleCerts && person.role
|
||||
showLinkedPersonnelCertCheck.value && person.role
|
||||
? getPersonnelCertMissingSuffix(person.certList || [], person.role as 'driver' | 'escort')
|
||||
: ''
|
||||
return `${formatPlPersonnelNameWithRole(person.personnelName, person.role)}${suffix}`
|
||||
|
||||
+8
-1
@@ -38,19 +38,26 @@ const props = withDefaults(
|
||||
person: VehiclePersonnelViewRow
|
||||
showTitle?: boolean
|
||||
checkSelfVehicleCerts?: boolean
|
||||
/** @deprecated 使用 checkLinkedPersonnelCerts */
|
||||
checkLinkedPersonnelCerts?: boolean
|
||||
tableMode?: ViewTableMode
|
||||
certLayout?: 'card' | 'table'
|
||||
}>(),
|
||||
{
|
||||
showTitle: false,
|
||||
checkSelfVehicleCerts: false,
|
||||
checkLinkedPersonnelCerts: false,
|
||||
tableMode: 'triple',
|
||||
certLayout: 'card'
|
||||
}
|
||||
)
|
||||
|
||||
const showPersonnelCertCheck = computed(
|
||||
() => props.checkLinkedPersonnelCerts || props.checkSelfVehicleCerts
|
||||
)
|
||||
|
||||
const personnelCertMissingSuffix = computed(() => {
|
||||
if (!props.checkSelfVehicleCerts || !props.person.role) return ''
|
||||
if (!showPersonnelCertCheck.value || !props.person.role) return ''
|
||||
return getPersonnelCertMissingSuffix(
|
||||
props.person.certList || [],
|
||||
props.person.role as 'driver' | 'escort'
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
:trailer-no="detail?.trailerNo"
|
||||
:vehicle-id="detail?.id"
|
||||
:personnel-list="personnelList"
|
||||
:check-self-vehicle-certs="detail?.vehicleType === 'self'"
|
||||
:vehicle-cert-check-type="detail?.vehicleType === 'self' || detail?.vehicleType === 'carrier' ? detail.vehicleType : undefined"
|
||||
:check-linked-personnel-certs="detail?.vehicleType === 'self'"
|
||||
cert-layout="table"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
import type { CertRow } from '@/api/pl'
|
||||
import { matchRequiredCertName } from '@/utils/pl-personnel-cert'
|
||||
import {
|
||||
PL_CARRIER_VEHICLE_HEAD_REQUIRED_CERT_NAMES,
|
||||
PL_CARRIER_VEHICLE_TRAILER_REQUIRED_CERT_NAMES,
|
||||
PL_SELF_VEHICLE_HEAD_REQUIRED_CERT_NAMES,
|
||||
PL_SELF_VEHICLE_TRAILER_REQUIRED_CERT_NAMES,
|
||||
PL_VEHICLE_CERT_PART_HEAD,
|
||||
PL_VEHICLE_CERT_PART_TRAILER,
|
||||
type PlVehicleCertCheckType,
|
||||
getVehicleHeadRequiredCertNames,
|
||||
getVehicleTrailerRequiredCertNames,
|
||||
normalizeVehicleCertPart
|
||||
} from '@/utils/pl-vehicle-cert'
|
||||
|
||||
export type CertRowWithMeta = CertRow & { _key?: string; _required?: boolean }
|
||||
|
||||
export function isRequiredCertSlotComplete(
|
||||
cert: CertRow | undefined,
|
||||
requiredName: string
|
||||
): boolean {
|
||||
if (!cert) return false
|
||||
if (!matchRequiredCertName(cert.certName, requiredName)) return false
|
||||
if (!cert.certNo?.trim()) return false
|
||||
if (!cert.expireDate) return false
|
||||
if (!cert.attachmentUrls?.trim()) return false
|
||||
return true
|
||||
}
|
||||
|
||||
export function getMissingRequiredCertFieldLabels(
|
||||
cert: CertRow | undefined,
|
||||
requiredName: string
|
||||
): string[] {
|
||||
if (!cert || !matchRequiredCertName(cert.certName, requiredName)) {
|
||||
return ['证号', '有效期', '附件']
|
||||
}
|
||||
const missing: string[] = []
|
||||
if (!cert.certNo?.trim()) missing.push('证号')
|
||||
if (!cert.expireDate) missing.push('有效期')
|
||||
if (!cert.attachmentUrls?.trim()) missing.push('附件')
|
||||
return missing
|
||||
}
|
||||
|
||||
export function collectRequiredCertSlotErrors(
|
||||
certList: CertRow[],
|
||||
requiredNames: readonly string[],
|
||||
partLabel = ''
|
||||
): string[] {
|
||||
const errors: string[] = []
|
||||
for (const requiredName of requiredNames) {
|
||||
const matched = certList.find(c => matchRequiredCertName(c.certName, requiredName))
|
||||
const missing = getMissingRequiredCertFieldLabels(matched, requiredName)
|
||||
if (missing.length) {
|
||||
const prefix = partLabel ? `${partLabel}「${requiredName}」` : `「${requiredName}」`
|
||||
errors.push(`${prefix}缺少:${missing.join('、')}`)
|
||||
}
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
export function collectSelfVehicleRequiredCertErrors(
|
||||
certList: CertRow[],
|
||||
trailerNo?: string | null
|
||||
): string[] {
|
||||
return collectVehicleRequiredCertErrors(certList, trailerNo, 'self')
|
||||
}
|
||||
|
||||
export function collectCarrierVehicleRequiredCertErrors(
|
||||
certList: CertRow[],
|
||||
trailerNo?: string | null
|
||||
): string[] {
|
||||
return collectVehicleRequiredCertErrors(certList, trailerNo, 'carrier')
|
||||
}
|
||||
|
||||
export function collectVehicleRequiredCertErrors(
|
||||
certList: CertRow[],
|
||||
trailerNo: string | null | undefined,
|
||||
vehicleType: PlVehicleCertCheckType
|
||||
): string[] {
|
||||
const headCerts = certList.filter(
|
||||
c => normalizeVehicleCertPart(c.vehiclePart) === PL_VEHICLE_CERT_PART_HEAD
|
||||
)
|
||||
const trailerCerts = certList.filter(
|
||||
c => normalizeVehicleCertPart(c.vehiclePart) === PL_VEHICLE_CERT_PART_TRAILER
|
||||
)
|
||||
const errors = [
|
||||
...collectRequiredCertSlotErrors(
|
||||
headCerts,
|
||||
getVehicleHeadRequiredCertNames(vehicleType),
|
||||
'主车'
|
||||
),
|
||||
...(trailerNo?.trim()
|
||||
? collectRequiredCertSlotErrors(
|
||||
trailerCerts,
|
||||
getVehicleTrailerRequiredCertNames(vehicleType),
|
||||
'挂车'
|
||||
)
|
||||
: [])
|
||||
]
|
||||
return errors
|
||||
}
|
||||
|
||||
function createEmptyCertRow(
|
||||
certName: string,
|
||||
vehiclePart?: string,
|
||||
required = false
|
||||
): CertRowWithMeta {
|
||||
return {
|
||||
_key: `cert-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
vehiclePart,
|
||||
certName,
|
||||
certNo: '',
|
||||
expireDate: '',
|
||||
remindDays: 30,
|
||||
attachmentUrls: '',
|
||||
_required: required
|
||||
}
|
||||
}
|
||||
|
||||
function findCertIndexForRequired(
|
||||
certs: CertRowWithMeta[],
|
||||
requiredName: string,
|
||||
vehiclePart?: string
|
||||
): number {
|
||||
return certs.findIndex(cert => {
|
||||
if (vehiclePart && normalizeVehicleCertPart(cert.vehiclePart) !== vehiclePart) {
|
||||
return false
|
||||
}
|
||||
return matchRequiredCertName(cert.certName, requiredName)
|
||||
})
|
||||
}
|
||||
|
||||
export function mergeCertListWithRequiredSlots(
|
||||
existing: CertRowWithMeta[],
|
||||
requiredNames: readonly string[],
|
||||
options?: { vehiclePart?: string }
|
||||
): CertRowWithMeta[] {
|
||||
const vehiclePart = options?.vehiclePart
|
||||
const next = existing.map(cert => ({ ...cert }))
|
||||
const normalizedPart = vehiclePart ? normalizeVehicleCertPart(vehiclePart) : null
|
||||
|
||||
const extras = next.filter(cert => {
|
||||
if (normalizedPart && normalizeVehicleCertPart(cert.vehiclePart) !== normalizedPart) {
|
||||
return false
|
||||
}
|
||||
return !requiredNames.some(name => matchRequiredCertName(cert.certName, name))
|
||||
})
|
||||
|
||||
const requiredRows: CertRowWithMeta[] = requiredNames.map(requiredName => {
|
||||
const idx = findCertIndexForRequired(next, requiredName, normalizedPart || undefined)
|
||||
if (idx >= 0) {
|
||||
return {
|
||||
...next[idx],
|
||||
certName: requiredName,
|
||||
vehiclePart: normalizedPart || next[idx].vehiclePart || PL_VEHICLE_CERT_PART_HEAD,
|
||||
_required: true
|
||||
}
|
||||
}
|
||||
return createEmptyCertRow(
|
||||
requiredName,
|
||||
normalizedPart || PL_VEHICLE_CERT_PART_HEAD,
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
const otherPartRows = normalizedPart
|
||||
? next.filter(cert => normalizeVehicleCertPart(cert.vehiclePart) !== normalizedPart)
|
||||
: []
|
||||
|
||||
return [...otherPartRows, ...requiredRows, ...extras.map(c => ({ ...c, _required: false }))]
|
||||
}
|
||||
|
||||
function ensureVehicleCertSlotsByNames(
|
||||
certList: CertRowWithMeta[],
|
||||
trailerNo: string | null | undefined,
|
||||
headRequired: readonly string[],
|
||||
trailerRequired: readonly string[]
|
||||
): CertRowWithMeta[] {
|
||||
let merged = mergeCertListWithRequiredSlots(certList, headRequired, {
|
||||
vehiclePart: PL_VEHICLE_CERT_PART_HEAD
|
||||
})
|
||||
if (trailerNo?.trim()) {
|
||||
merged = mergeCertListWithRequiredSlots(merged, trailerRequired, {
|
||||
vehiclePart: PL_VEHICLE_CERT_PART_TRAILER
|
||||
})
|
||||
} else {
|
||||
merged = stripTrailerRequiredSlots(merged, trailerRequired)
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
export function ensureSelfVehicleCertSlots(
|
||||
certList: CertRowWithMeta[],
|
||||
trailerNo?: string | null
|
||||
): CertRowWithMeta[] {
|
||||
return ensureVehicleCertSlotsByNames(
|
||||
certList,
|
||||
trailerNo,
|
||||
PL_SELF_VEHICLE_HEAD_REQUIRED_CERT_NAMES,
|
||||
PL_SELF_VEHICLE_TRAILER_REQUIRED_CERT_NAMES
|
||||
)
|
||||
}
|
||||
|
||||
export function ensureCarrierVehicleCertSlots(
|
||||
certList: CertRowWithMeta[],
|
||||
trailerNo?: string | null
|
||||
): CertRowWithMeta[] {
|
||||
return ensureVehicleCertSlotsByNames(
|
||||
certList,
|
||||
trailerNo,
|
||||
PL_CARRIER_VEHICLE_HEAD_REQUIRED_CERT_NAMES,
|
||||
PL_CARRIER_VEHICLE_TRAILER_REQUIRED_CERT_NAMES
|
||||
)
|
||||
}
|
||||
|
||||
export function ensureVehicleCertSlots(
|
||||
certList: CertRowWithMeta[],
|
||||
trailerNo: string | null | undefined,
|
||||
vehicleType: PlVehicleCertCheckType
|
||||
): CertRowWithMeta[] {
|
||||
return vehicleType === 'carrier'
|
||||
? ensureCarrierVehicleCertSlots(certList, trailerNo)
|
||||
: ensureSelfVehicleCertSlots(certList, trailerNo)
|
||||
}
|
||||
|
||||
export function stripTrailerRequiredSlots(
|
||||
certList: CertRowWithMeta[],
|
||||
trailerRequiredNames: readonly string[] = PL_SELF_VEHICLE_TRAILER_REQUIRED_CERT_NAMES
|
||||
): CertRowWithMeta[] {
|
||||
return certList.filter(cert => {
|
||||
if (normalizeVehicleCertPart(cert.vehiclePart) !== PL_VEHICLE_CERT_PART_TRAILER) {
|
||||
return true
|
||||
}
|
||||
return !trailerRequiredNames.some(name => matchRequiredCertName(cert.certName, name))
|
||||
})
|
||||
}
|
||||
|
||||
export function formatRequiredCertErrors(errors: string[]): string {
|
||||
if (!errors.length) return ''
|
||||
return `必备证书未齐全:${errors.join(';')}`
|
||||
}
|
||||
@@ -225,7 +225,7 @@ export function getPlPlanFollowStatusLabel(status?: string) {
|
||||
}
|
||||
|
||||
export function getPlPlanArchiveStatusTagType(status?: string) {
|
||||
return status === 'archived' ? 'info' : 'success'
|
||||
return status === 'archived' ? 'info' : 'danger'
|
||||
}
|
||||
|
||||
export function getPlPlanArchiveStatusLabel(status?: string) {
|
||||
|
||||
@@ -11,6 +11,95 @@ export const PL_PERSONNEL_DRIVER_REQUIRED_CERT_NAMES = [
|
||||
export const PL_PERSONNEL_ESCORT_REQUIRED_CERT_NAMES = ['身份证', '从业资格证'] as const
|
||||
|
||||
export type PersonnelCertRole = 'driver' | 'escort'
|
||||
export type PersonnelType = 'driver' | 'escort' | 'both'
|
||||
|
||||
export const PL_PERSONNEL_TYPE_OPTIONS = [
|
||||
{ value: 'driver', label: '驾驶员' },
|
||||
{ value: 'escort', label: '押运员' },
|
||||
{ value: 'both', label: '驾驶员兼押运员' }
|
||||
] as const
|
||||
|
||||
export type PersonnelAffiliation = 'self' | 'carrier'
|
||||
|
||||
export const PL_PERSONNEL_AFFILIATION_OPTIONS = [
|
||||
{ value: 'self', label: '山峡' },
|
||||
{ value: 'carrier', label: '非山峡' }
|
||||
] as const
|
||||
|
||||
export function getPersonnelAffiliationLabel(affiliation?: string | null): string {
|
||||
const found = PL_PERSONNEL_AFFILIATION_OPTIONS.find(item => item.value === affiliation)
|
||||
return found?.label || affiliation || '—'
|
||||
}
|
||||
|
||||
export function getPersonnelAffiliationTagType(
|
||||
affiliation?: string | null
|
||||
): 'success' | 'warning' | 'info' {
|
||||
if (affiliation === 'self') return 'success'
|
||||
if (affiliation === 'carrier') return 'warning'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
export function validatePersonnelAffiliationVehicleCompat(
|
||||
personnelAffiliation: string | undefined,
|
||||
vehicleType: string | undefined,
|
||||
onError: (message: string) => void
|
||||
): boolean {
|
||||
if (!personnelAffiliation || !vehicleType) return true
|
||||
if (personnelAffiliation === vehicleType) return true
|
||||
const vehicleLabel = vehicleType === 'self' ? '山峡' : '非山峡'
|
||||
onError(`${vehicleLabel}车辆仅可关联${vehicleLabel}人员`)
|
||||
return false
|
||||
}
|
||||
|
||||
export function getPersonnelTypeLabel(type?: string | null): string {
|
||||
const found = PL_PERSONNEL_TYPE_OPTIONS.find(item => item.value === type)
|
||||
return found?.label || type || '—'
|
||||
}
|
||||
|
||||
export function getPersonnelTypeTagType(type?: string | null): 'success' | 'warning' | 'primary' | 'info' {
|
||||
if (type === 'driver') return 'primary'
|
||||
if (type === 'escort') return 'warning'
|
||||
if (type === 'both') return 'success'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
export function getRequiredPersonnelCertNamesByType(
|
||||
personnelType?: string | null
|
||||
): readonly string[] {
|
||||
if (personnelType === 'escort') {
|
||||
return PL_PERSONNEL_ESCORT_REQUIRED_CERT_NAMES
|
||||
}
|
||||
return PL_PERSONNEL_DRIVER_REQUIRED_CERT_NAMES
|
||||
}
|
||||
|
||||
export function validatePersonnelTypeRoleCompat(
|
||||
personnelType: string | undefined,
|
||||
role: string | undefined,
|
||||
onError: (message: string) => void
|
||||
): boolean {
|
||||
if (!personnelType || !role) return true
|
||||
if (personnelType === 'driver' && role === 'escort') {
|
||||
onError('驾驶员类型人员不可关联为押运员')
|
||||
return false
|
||||
}
|
||||
if (personnelType === 'escort' && role === 'driver') {
|
||||
onError('押运员类型人员不可关联为驾驶员')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function isPersonnelCertStrictComplete(
|
||||
cert: Pick<CertRow, 'certName' | 'certNo' | 'expireDate' | 'attachmentUrls'> | undefined,
|
||||
requiredName: string
|
||||
): boolean {
|
||||
if (!cert?.certName?.trim()) return false
|
||||
if (!matchRequiredCertName(cert.certName, requiredName)) return false
|
||||
if (!cert.certNo?.trim()) return false
|
||||
if (!cert.expireDate) return false
|
||||
if (!cert.attachmentUrls?.trim()) return false
|
||||
return true
|
||||
}
|
||||
|
||||
export function matchRequiredCertName(
|
||||
actualName: string | undefined,
|
||||
@@ -28,8 +117,13 @@ export function matchRequiredCertName(
|
||||
return name === requiredName
|
||||
}
|
||||
|
||||
export function isPersonnelCertUploaded(cert?: Pick<CertRow, 'certName' | 'attachmentUrls'>) {
|
||||
return Boolean(cert?.certName?.trim() && cert?.attachmentUrls?.trim())
|
||||
export function isPersonnelCertUploaded(cert?: Pick<CertRow, 'certName' | 'certNo' | 'expireDate' | 'attachmentUrls'>) {
|
||||
return Boolean(
|
||||
cert?.certName?.trim() &&
|
||||
cert?.certNo?.trim() &&
|
||||
cert?.expireDate &&
|
||||
cert?.attachmentUrls?.trim()
|
||||
)
|
||||
}
|
||||
|
||||
export function getRequiredPersonnelCertNames(role: PersonnelCertRole): readonly string[] {
|
||||
@@ -55,15 +149,28 @@ export function hasPersonnelCertMissing(
|
||||
return getMissingPersonnelCertNames(role, certList).length > 0
|
||||
}
|
||||
|
||||
/** 人员主数据页:至少满足押运员必备证书(身份证 + 从业资格证) */
|
||||
export function hasPersonnelMasterCertMissing(certList: CertRow[] = []): boolean {
|
||||
return hasPersonnelCertMissing('escort', certList)
|
||||
/** 人员主数据页:按人员类型检查必备证书 */
|
||||
export function hasPersonnelMasterCertMissing(
|
||||
certList: CertRow[] = [],
|
||||
personnelType?: string | null
|
||||
): boolean {
|
||||
const required = getRequiredPersonnelCertNamesByType(personnelType || 'both')
|
||||
return required.some(requiredName => {
|
||||
const matched = certList.find(cert => matchRequiredCertName(cert.certName, requiredName))
|
||||
return !isPersonnelCertStrictComplete(matched, requiredName)
|
||||
})
|
||||
}
|
||||
|
||||
export function getPersonnelCertMissingSuffix(
|
||||
certList: CertRow[] = [],
|
||||
role?: PersonnelCertRole
|
||||
role?: PersonnelCertRole,
|
||||
personnelType?: string | null
|
||||
): string {
|
||||
if (personnelType) {
|
||||
return hasPersonnelMasterCertMissing(certList, personnelType)
|
||||
? PL_PERSONNEL_CERT_MISSING_SUFFIX
|
||||
: ''
|
||||
}
|
||||
if (role) {
|
||||
return hasPersonnelCertMissing(role, certList) ? PL_PERSONNEL_CERT_MISSING_SUFFIX : ''
|
||||
}
|
||||
|
||||
@@ -21,6 +21,12 @@ export const PL_SELF_VEHICLE_TRAILER_REQUIRED_CERT_NAMES = [
|
||||
'罐检报告'
|
||||
] as const
|
||||
|
||||
export const PL_CARRIER_VEHICLE_HEAD_REQUIRED_CERT_NAMES = ['行驶证', '营运证'] as const
|
||||
|
||||
export const PL_CARRIER_VEHICLE_TRAILER_REQUIRED_CERT_NAMES = ['行驶证', '营运证'] as const
|
||||
|
||||
export type PlVehicleCertCheckType = 'self' | 'carrier'
|
||||
|
||||
export const PL_VEHICLE_CERT_NAME_OPTIONS = [
|
||||
'行驶证',
|
||||
'营运证',
|
||||
@@ -70,18 +76,40 @@ export function mergeVehicleCertsByPart<T extends CertRow>(
|
||||
return [...normalizedHead, ...normalizedTrailer]
|
||||
}
|
||||
|
||||
export function isVehicleCertUploaded(cert?: Pick<CertRow, 'certName' | 'attachmentUrls'>) {
|
||||
return Boolean(cert?.certName?.trim() && cert?.attachmentUrls?.trim())
|
||||
export function isVehicleCertUploaded(cert?: Pick<CertRow, 'certName' | 'certNo' | 'expireDate' | 'attachmentUrls'>) {
|
||||
return Boolean(
|
||||
cert?.certName?.trim() &&
|
||||
cert?.certNo?.trim() &&
|
||||
cert?.expireDate &&
|
||||
cert?.attachmentUrls?.trim()
|
||||
)
|
||||
}
|
||||
|
||||
export function getMissingSelfVehiclePartCertNames(
|
||||
export function getVehicleHeadRequiredCertNames(
|
||||
vehicleType: PlVehicleCertCheckType
|
||||
): readonly string[] {
|
||||
return vehicleType === 'carrier'
|
||||
? PL_CARRIER_VEHICLE_HEAD_REQUIRED_CERT_NAMES
|
||||
: PL_SELF_VEHICLE_HEAD_REQUIRED_CERT_NAMES
|
||||
}
|
||||
|
||||
export function getVehicleTrailerRequiredCertNames(
|
||||
vehicleType: PlVehicleCertCheckType
|
||||
): readonly string[] {
|
||||
return vehicleType === 'carrier'
|
||||
? PL_CARRIER_VEHICLE_TRAILER_REQUIRED_CERT_NAMES
|
||||
: PL_SELF_VEHICLE_TRAILER_REQUIRED_CERT_NAMES
|
||||
}
|
||||
|
||||
export function getMissingVehiclePartCertNames(
|
||||
certList: CertRow[] = [],
|
||||
part: VehicleCertPart
|
||||
part: VehicleCertPart,
|
||||
vehicleType: PlVehicleCertCheckType
|
||||
): string[] {
|
||||
const requiredNames =
|
||||
part === PL_VEHICLE_CERT_PART_TRAILER
|
||||
? PL_SELF_VEHICLE_TRAILER_REQUIRED_CERT_NAMES
|
||||
: PL_SELF_VEHICLE_HEAD_REQUIRED_CERT_NAMES
|
||||
? getVehicleTrailerRequiredCertNames(vehicleType)
|
||||
: getVehicleHeadRequiredCertNames(vehicleType)
|
||||
|
||||
return requiredNames.filter(requiredName => {
|
||||
const matched = certList.find(cert => cert.certName?.trim() === requiredName)
|
||||
@@ -89,39 +117,77 @@ export function getMissingSelfVehiclePartCertNames(
|
||||
})
|
||||
}
|
||||
|
||||
export function hasSelfVehicleCertMissing(options: {
|
||||
export function getMissingSelfVehiclePartCertNames(
|
||||
certList: CertRow[] = [],
|
||||
part: VehicleCertPart
|
||||
): string[] {
|
||||
return getMissingVehiclePartCertNames(certList, part, 'self')
|
||||
}
|
||||
|
||||
export function hasVehicleCertMissing(options: {
|
||||
vehicleType: PlVehicleCertCheckType
|
||||
certList?: CertRow[]
|
||||
trailerNo?: string | null
|
||||
}): boolean {
|
||||
const { certList = [], trailerNo } = options
|
||||
const { vehicleType, certList = [], trailerNo } = options
|
||||
const { headCerts, trailerCerts } = splitVehicleCertsByPart(certList)
|
||||
const hasTrailer = Boolean(trailerNo?.trim())
|
||||
|
||||
if (getMissingSelfVehiclePartCertNames(headCerts, PL_VEHICLE_CERT_PART_HEAD).length > 0) {
|
||||
if (
|
||||
getMissingVehiclePartCertNames(headCerts, PL_VEHICLE_CERT_PART_HEAD, vehicleType).length > 0
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (hasTrailer && getMissingSelfVehiclePartCertNames(trailerCerts, PL_VEHICLE_CERT_PART_TRAILER).length > 0) {
|
||||
if (
|
||||
hasTrailer &&
|
||||
getMissingVehiclePartCertNames(trailerCerts, PL_VEHICLE_CERT_PART_TRAILER, vehicleType)
|
||||
.length > 0
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function hasSelfVehicleCertMissing(options: {
|
||||
certList?: CertRow[]
|
||||
trailerNo?: string | null
|
||||
}): boolean {
|
||||
return hasVehicleCertMissing({ ...options, vehicleType: 'self' })
|
||||
}
|
||||
|
||||
export function getVehicleCertMissingSuffix(options: {
|
||||
vehicleType: PlVehicleCertCheckType
|
||||
certList?: CertRow[]
|
||||
trailerNo?: string | null
|
||||
}): string {
|
||||
return hasVehicleCertMissing(options) ? PL_VEHICLE_CERT_MISSING_SUFFIX : ''
|
||||
}
|
||||
|
||||
export function getSelfVehicleCertMissingSuffix(options: {
|
||||
certList?: CertRow[]
|
||||
trailerNo?: string | null
|
||||
}): string {
|
||||
return hasSelfVehicleCertMissing(options) ? PL_VEHICLE_CERT_MISSING_SUFFIX : ''
|
||||
return getVehicleCertMissingSuffix({ ...options, vehicleType: 'self' })
|
||||
}
|
||||
|
||||
export function getVehiclePartCertMissingSuffix(
|
||||
certList: CertRow[] = [],
|
||||
part: VehicleCertPart,
|
||||
vehicleType?: PlVehicleCertCheckType | null
|
||||
): string {
|
||||
if (!vehicleType) return ''
|
||||
return getMissingVehiclePartCertNames(certList, part, vehicleType).length > 0
|
||||
? PL_VEHICLE_CERT_MISSING_SUFFIX
|
||||
: ''
|
||||
}
|
||||
|
||||
export function getSelfVehiclePartCertMissingSuffix(
|
||||
certList: CertRow[] = [],
|
||||
part: VehicleCertPart
|
||||
): string {
|
||||
return getMissingSelfVehiclePartCertNames(certList, part).length > 0
|
||||
? PL_VEHICLE_CERT_MISSING_SUFFIX
|
||||
: ''
|
||||
return getVehiclePartCertMissingSuffix(certList, part, 'self')
|
||||
}
|
||||
|
||||
/** @deprecated 保存不再阻断证书完整性,仅校验证书名称非空等基础项 */
|
||||
|
||||
@@ -426,6 +426,7 @@ const formData = reactive({
|
||||
const createQualificationRules = {
|
||||
businessLicenseUrls: [
|
||||
{
|
||||
required: true,
|
||||
validator: (_rule: unknown, value: string, callback: (error?: Error) => void) => {
|
||||
if (!value || !String(value).trim()) {
|
||||
callback(new Error('请上传营业执照'))
|
||||
@@ -438,6 +439,7 @@ const createQualificationRules = {
|
||||
],
|
||||
roadTransportLicenseUrls: [
|
||||
{
|
||||
required: true,
|
||||
validator: (_rule: unknown, value: string, callback: (error?: Error) => void) => {
|
||||
if (!value || !String(value).trim()) {
|
||||
callback(new Error('请上传道路经营许可证'))
|
||||
@@ -450,6 +452,7 @@ const createQualificationRules = {
|
||||
],
|
||||
transportAgreementUrls: [
|
||||
{
|
||||
required: true,
|
||||
validator: (_rule: unknown, value: string, callback: (error?: Error) => void) => {
|
||||
if (!value || !String(value).trim()) {
|
||||
callback(new Error('请上传运输协议'))
|
||||
@@ -462,10 +465,12 @@ const createQualificationRules = {
|
||||
]
|
||||
}
|
||||
|
||||
const formRules = computed(() => ({
|
||||
const formRules = {
|
||||
carrierName: [{ required: true, message: '请输入承运商名称', trigger: 'blur' }],
|
||||
...(isEdit.value ? {} : createQualificationRules)
|
||||
}))
|
||||
contactName: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
|
||||
contactPhone: [{ required: true, message: '请输入联系电话', trigger: 'blur' }],
|
||||
...createQualificationRules
|
||||
}
|
||||
|
||||
function handleCertFieldChange(
|
||||
field: 'businessLicenseUrls' | 'roadTransportLicenseUrls' | 'transportAgreementUrls'
|
||||
@@ -658,29 +663,12 @@ async function handleSubmit() {
|
||||
contactPhone: formData.contactPhone,
|
||||
status: formData.status,
|
||||
remark: formData.remark,
|
||||
...(isEdit.value
|
||||
? {
|
||||
...(formData.businessLicenseUrls
|
||||
? { businessLicenseUrls: formData.businessLicenseUrls }
|
||||
: {}),
|
||||
...(formData.roadTransportLicenseUrls
|
||||
? { roadTransportLicenseUrls: formData.roadTransportLicenseUrls }
|
||||
: {}),
|
||||
...(formData.transportAgreementUrls
|
||||
? { transportAgreementUrls: formData.transportAgreementUrls }
|
||||
: {}),
|
||||
...(formData.transportAgreementExpireDate
|
||||
? { transportAgreementExpireDate: formData.transportAgreementExpireDate }
|
||||
: {})
|
||||
}
|
||||
: {
|
||||
businessLicenseUrls: formData.businessLicenseUrls,
|
||||
roadTransportLicenseUrls: formData.roadTransportLicenseUrls,
|
||||
transportAgreementUrls: formData.transportAgreementUrls,
|
||||
...(formData.transportAgreementExpireDate
|
||||
? { transportAgreementExpireDate: formData.transportAgreementExpireDate }
|
||||
: {})
|
||||
})
|
||||
businessLicenseUrls: formData.businessLicenseUrls,
|
||||
roadTransportLicenseUrls: formData.roadTransportLicenseUrls,
|
||||
transportAgreementUrls: formData.transportAgreementUrls,
|
||||
...(formData.transportAgreementExpireDate
|
||||
? { transportAgreementExpireDate: formData.transportAgreementExpireDate }
|
||||
: {})
|
||||
}
|
||||
if (isEdit.value) {
|
||||
await carrierApi.update(formData.id, payload)
|
||||
|
||||
@@ -24,6 +24,16 @@
|
||||
<el-option label="停用" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="人员归属">
|
||||
<el-select v-model="searchForm.personnelAffiliation" placeholder="全部" clearable>
|
||||
<el-option
|
||||
v-for="option in PL_PERSONNEL_AFFILIATION_OPTIONS"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<div class="filter-actions-end">
|
||||
@@ -112,6 +122,32 @@
|
||||
<el-link type="primary" @click="handleView(row)">{{ row.personnelName }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('personnelAffiliation')"
|
||||
label="人员归属"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPersonnelAffiliationTagType(row.personnelAffiliation)" size="small">
|
||||
{{ getPersonnelAffiliationLabel(row.personnelAffiliation) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('personnelType')"
|
||||
label="人员类型"
|
||||
min-width="130"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPersonnelTypeTagType(row.personnelType)" size="small">
|
||||
{{ getPersonnelTypeLabel(row.personnelType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('phoneNo')"
|
||||
prop="phoneNo"
|
||||
@@ -224,6 +260,26 @@
|
||||
<el-form-item label="身份证号" prop="idCardNo">
|
||||
<el-input v-model="formData.idCardNo" placeholder="请输入身份证号" maxlength="32" />
|
||||
</el-form-item>
|
||||
<el-form-item label="人员归属" prop="personnelAffiliation">
|
||||
<el-select v-model="formData.personnelAffiliation" placeholder="请选择人员归属">
|
||||
<el-option
|
||||
v-for="option in PL_PERSONNEL_AFFILIATION_OPTIONS"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="人员类型" prop="personnelType">
|
||||
<el-select v-model="formData.personnelType" placeholder="请选择人员类型">
|
||||
<el-option
|
||||
v-for="option in PL_PERSONNEL_TYPE_OPTIONS"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="formData.status" placeholder="请选择">
|
||||
<el-option label="在职" value="1" />
|
||||
@@ -248,6 +304,7 @@
|
||||
title="人员证书"
|
||||
upload-folder="pl/personnel-cert"
|
||||
:cert-name-options="certNameOptions"
|
||||
:required-cert-names="requiredPersonnelCertNames"
|
||||
layout="table"
|
||||
/>
|
||||
</el-form>
|
||||
@@ -311,7 +368,21 @@ import { pickBxPageResult } from '@/utils/http/bx'
|
||||
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { getPersonnelCertMissingSuffix } from '@/utils/pl-personnel-cert'
|
||||
import {
|
||||
collectRequiredCertSlotErrors,
|
||||
formatRequiredCertErrors,
|
||||
mergeCertListWithRequiredSlots
|
||||
} from '@/utils/pl-cert-slots'
|
||||
import {
|
||||
getPersonnelCertMissingSuffix,
|
||||
getRequiredPersonnelCertNamesByType,
|
||||
getPersonnelTypeLabel,
|
||||
getPersonnelTypeTagType,
|
||||
getPersonnelAffiliationLabel,
|
||||
getPersonnelAffiliationTagType,
|
||||
PL_PERSONNEL_AFFILIATION_OPTIONS,
|
||||
PL_PERSONNEL_TYPE_OPTIONS
|
||||
} from '@/utils/pl-personnel-cert'
|
||||
|
||||
type CertListItem = CertRow & { _key?: string }
|
||||
type CertFormListRef = InstanceType<typeof PlCertFormList>
|
||||
@@ -319,6 +390,8 @@ type CertFormListRef = InstanceType<typeof PlCertFormList>
|
||||
const loading = ref(false)
|
||||
|
||||
const listColumnDefs = [
|
||||
{ prop: 'personnelAffiliation', label: '人员归属' },
|
||||
{ prop: 'personnelType', label: '人员类型' },
|
||||
{ prop: 'phoneNo', label: '手机号' },
|
||||
{ prop: 'age', label: '年龄' },
|
||||
{ prop: 'idCardNo', label: '身份证号' },
|
||||
@@ -359,7 +432,8 @@ const certNameOptions = [
|
||||
|
||||
const searchForm = reactive({
|
||||
q: '',
|
||||
status: ''
|
||||
status: '',
|
||||
personnelAffiliation: ''
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
@@ -374,12 +448,20 @@ const formData = reactive({
|
||||
phoneNo: '',
|
||||
age: null as number | null,
|
||||
idCardNo: '',
|
||||
personnelType: '',
|
||||
personnelAffiliation: 'self',
|
||||
status: '1',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const requiredPersonnelCertNames = computed(() =>
|
||||
getRequiredPersonnelCertNamesByType(formData.personnelType)
|
||||
)
|
||||
|
||||
const formRules = {
|
||||
personnelName: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
personnelAffiliation: [{ required: true, message: '请选择人员归属', trigger: 'change' }],
|
||||
personnelType: [{ required: true, message: '请选择人员类型', trigger: 'change' }],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
|
||||
}
|
||||
|
||||
@@ -410,7 +492,7 @@ const viewHeroMeta = computed(() => {
|
||||
const viewDialogTitle = computed(() => {
|
||||
const name = viewDetail.value?.personnelName || '人员详情'
|
||||
if (!viewDetail.value) return name
|
||||
return name + getPersonnelCertMissingSuffix(viewDetail.value.certList || [])
|
||||
return name + getPersonnelCertMissingSuffix(viewDetail.value.certList || [], undefined, viewDetail.value.personnelType)
|
||||
})
|
||||
|
||||
const viewSections = computed<ViewSection[]>(() => {
|
||||
@@ -423,6 +505,16 @@ const viewSections = computed<ViewSection[]>(() => {
|
||||
fields: [
|
||||
{ label: '姓名', value: d.personnelName },
|
||||
{ label: '手机号', value: d.phoneNo || '—' },
|
||||
{
|
||||
label: '人员归属',
|
||||
value: getPersonnelAffiliationLabel(d.personnelAffiliation),
|
||||
tagType: getPersonnelAffiliationTagType(d.personnelAffiliation)
|
||||
},
|
||||
{
|
||||
label: '人员类型',
|
||||
value: getPersonnelTypeLabel(d.personnelType),
|
||||
tagType: getPersonnelTypeTagType(d.personnelType)
|
||||
},
|
||||
{ label: '年龄', value: d.age ?? '—' },
|
||||
{ label: '身份证号', value: maskIdCard(d.idCardNo) },
|
||||
{
|
||||
@@ -443,7 +535,7 @@ const viewSections = computed<ViewSection[]>(() => {
|
||||
tableMode: 'triple'
|
||||
},
|
||||
{
|
||||
title: `人员证书${viewDetail.value ? getPersonnelCertMissingSuffix(viewDetail.value.certList || []) : ''}`,
|
||||
title: `人员证书${viewDetail.value ? getPersonnelCertMissingSuffix(viewDetail.value.certList || [], undefined, viewDetail.value.personnelType) : ''}`,
|
||||
fields: [],
|
||||
slot: 'certs'
|
||||
}
|
||||
@@ -451,15 +543,39 @@ const viewSections = computed<ViewSection[]>(() => {
|
||||
})
|
||||
|
||||
function validateCerts(): boolean {
|
||||
if (!formData.personnelType) {
|
||||
ElMessage.warning('请选择人员类型')
|
||||
return false
|
||||
}
|
||||
const errors = collectRequiredCertSlotErrors(
|
||||
certList.value,
|
||||
requiredPersonnelCertNames.value
|
||||
)
|
||||
if (errors.length) {
|
||||
ElMessage.warning(formatRequiredCertErrors(errors))
|
||||
return false
|
||||
}
|
||||
for (const cert of certList.value) {
|
||||
if (!cert.certName?.trim()) {
|
||||
ElMessage.warning('证书名称不能为空')
|
||||
if (!cert._required && !cert.certName?.trim()) {
|
||||
ElMessage.warning('其他证书名称不能为空')
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
watch(
|
||||
() => formData.personnelType,
|
||||
() => {
|
||||
if (!formData.personnelType) return
|
||||
certList.value = mergeCertListWithRequiredSlots(
|
||||
certList.value,
|
||||
getRequiredPersonnelCertNamesByType(formData.personnelType)
|
||||
)
|
||||
certFormListRef.value?.refreshRequiredSlots?.()
|
||||
}
|
||||
)
|
||||
|
||||
function resetCertList() {
|
||||
certList.value = []
|
||||
certFormListRef.value?.clearUploadRefs()
|
||||
@@ -471,6 +587,7 @@ async function fetchData() {
|
||||
const res = await personnelApi.list({
|
||||
q: searchForm.q || undefined,
|
||||
status: searchForm.status || undefined,
|
||||
personnelAffiliation: searchForm.personnelAffiliation || undefined,
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
@@ -492,6 +609,7 @@ function handleSearch() {
|
||||
function handleReset() {
|
||||
searchForm.q = ''
|
||||
searchForm.status = ''
|
||||
searchForm.personnelAffiliation = ''
|
||||
selectedRows.value = []
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
@@ -548,6 +666,8 @@ function handleAdd() {
|
||||
phoneNo: '',
|
||||
age: null,
|
||||
idCardNo: '',
|
||||
personnelType: '',
|
||||
personnelAffiliation: 'self',
|
||||
status: '1',
|
||||
remark: ''
|
||||
})
|
||||
@@ -583,19 +703,24 @@ async function handleEdit(row: Personnel) {
|
||||
phoneNo: detail.phoneNo || '',
|
||||
age: detail.age ?? null,
|
||||
idCardNo: detail.idCardNo || '',
|
||||
personnelType: detail.personnelType || 'both',
|
||||
personnelAffiliation: detail.personnelAffiliation || 'self',
|
||||
status: detail.status,
|
||||
remark: detail.remark || ''
|
||||
})
|
||||
certList.value = (detail.certList || []).map(c => ({
|
||||
_key: c.id || c.certId || `cert-${Math.random().toString(36).slice(2, 10)}`,
|
||||
certId: c.id || c.certId,
|
||||
certName: c.certName,
|
||||
certNo: c.certNo,
|
||||
expireDate: c.expireDate,
|
||||
remindDays: c.remindDays ?? 30,
|
||||
attachmentUrls: c.attachmentUrls || ''
|
||||
}))
|
||||
certFormListRef.value?.clearUploadRefs()
|
||||
certList.value = mergeCertListWithRequiredSlots(
|
||||
(detail.certList || []).map(c => ({
|
||||
_key: c.id || c.certId || `cert-${Math.random().toString(36).slice(2, 10)}`,
|
||||
certId: c.id || c.certId,
|
||||
certName: c.certName,
|
||||
certNo: c.certNo,
|
||||
expireDate: c.expireDate,
|
||||
remindDays: c.remindDays ?? 30,
|
||||
attachmentUrls: c.attachmentUrls || ''
|
||||
})),
|
||||
getRequiredPersonnelCertNamesByType(detail.personnelType || 'both')
|
||||
)
|
||||
certFormListRef.value?.refreshRequiredSlots?.()
|
||||
} catch {
|
||||
ElMessage.error('加载人员详情失败')
|
||||
}
|
||||
@@ -616,6 +741,8 @@ async function handleSubmit() {
|
||||
phoneNo: formData.phoneNo || undefined,
|
||||
age: formData.age ?? undefined,
|
||||
idCardNo: formData.idCardNo || undefined,
|
||||
personnelType: formData.personnelType,
|
||||
personnelAffiliation: formData.personnelAffiliation,
|
||||
status: formData.status,
|
||||
remark: formData.remark || undefined,
|
||||
certList: certList.value.map(c => ({
|
||||
@@ -664,7 +791,8 @@ async function handleDelete(row: Personnel) {
|
||||
useDbInstantSearch(
|
||||
() => ({
|
||||
q: searchForm.q,
|
||||
status: searchForm.status
|
||||
status: searchForm.status,
|
||||
personnelAffiliation: searchForm.personnelAffiliation
|
||||
}),
|
||||
handleSearch
|
||||
)
|
||||
|
||||
@@ -148,36 +148,6 @@
|
||||
min-width="120"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('planType')"
|
||||
prop="planType"
|
||||
label="单据类型"
|
||||
min-width="100"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlanTypeTagType(row.planType)" size="small">
|
||||
{{ getPlanTypeLabel(row.planType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('customerName')"
|
||||
prop="customerName"
|
||||
label="客户名称"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column v-if="isColumnVisible('products')" label="产品信息" min-width="240">
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell :products="row.products" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="isColumnVisible('planAttachments')" label="计划附件" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<PlPlanListCertCell :products="row.products" compact hide-product />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('archiveStatus')"
|
||||
prop="archiveStatus"
|
||||
@@ -217,6 +187,36 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('planType')"
|
||||
prop="planType"
|
||||
label="单据类型"
|
||||
min-width="100"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlanTypeTagType(row.planType)" size="small">
|
||||
{{ getPlanTypeLabel(row.planType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('customerName')"
|
||||
prop="customerName"
|
||||
label="客户名称"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column v-if="isColumnVisible('products')" label="产品信息" min-width="240">
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell :products="row.products" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="isColumnVisible('planAttachments')" label="计划附件" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<PlPlanListCertCell :products="row.products" compact hide-product />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('licensePlate')"
|
||||
prop="licensePlate"
|
||||
@@ -313,6 +313,11 @@ import {
|
||||
Search
|
||||
} from '@element-plus/icons-vue'
|
||||
import { planApi, type Plan } from '@/api/pl'
|
||||
import {
|
||||
buildMissingHazardCertReadyMessage,
|
||||
enrichPlanProductsHazardFromCrm
|
||||
} from '@/composables/usePlToxicProductCertCheck'
|
||||
import { pickBxData } from '@/utils/http/bx'
|
||||
import PlPlanArchiveDialog from '@/components/business/pl-plan-archive-dialog/index.vue'
|
||||
import PlPlanFormDialog from '@/components/business/pl-plan-form-dialog/index.vue'
|
||||
import PlPlanListCertCell from '@/components/business/pl-plan-list-cert-cell/index.vue'
|
||||
@@ -347,13 +352,13 @@ const planList = ref<Plan[]>([])
|
||||
|
||||
const listColumnDefs = [
|
||||
{ prop: 'planDate', label: '计划日期' },
|
||||
{ prop: 'archiveStatus', label: '归档状态' },
|
||||
{ prop: 'followStatus', label: '跟进状态' },
|
||||
{ prop: 'transportStatus', label: '运输状态' },
|
||||
{ prop: 'planType', label: '单据类型' },
|
||||
{ prop: 'customerName', label: '客户名称' },
|
||||
{ prop: 'products', label: '产品信息' },
|
||||
{ prop: 'planAttachments', label: '计划附件' },
|
||||
{ prop: 'archiveStatus', label: '归档状态' },
|
||||
{ prop: 'followStatus', label: '跟进状态' },
|
||||
{ prop: 'transportStatus', label: '运输状态' },
|
||||
{ prop: 'licensePlate', label: '车牌号' },
|
||||
{ prop: 'personnelSummary', label: '人员' },
|
||||
{ prop: 'followUserName', label: '跟进人', defaultVisible: false }
|
||||
@@ -530,7 +535,34 @@ function handleEdit(row: Plan) {
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleArchive(row: Plan) {
|
||||
async function handleArchive(row: Plan) {
|
||||
if (row.archiveStatus === 'archived') {
|
||||
archivePlanId.value = row.id
|
||||
archiveDialogVisible.value = true
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await planApi.get(row.id)
|
||||
const data = pickBxData<Plan>(res)
|
||||
if (!data?.id) {
|
||||
throw new Error('运输计划数据无效')
|
||||
}
|
||||
const rawProducts = Array.isArray(data.products) ? data.products : []
|
||||
const enrichedProducts = await enrichPlanProductsHazardFromCrm(rawProducts)
|
||||
const certMessage = buildMissingHazardCertReadyMessage(data, enrichedProducts)
|
||||
if (certMessage) {
|
||||
await ElMessageBox.alert(`${certMessage},请至【危化品证上传】页面操作。`, '无法归档', {
|
||||
type: 'warning'
|
||||
})
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '加载运输计划失败'
|
||||
ElMessage.error(message.includes('加载') ? message : `加载运输计划失败:${message}`)
|
||||
return
|
||||
}
|
||||
|
||||
archivePlanId.value = row.id
|
||||
archiveDialogVisible.value = true
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.transportStatus === 'arranged' ? 'success' : 'info'" size="small">
|
||||
<el-tag :type="row.transportStatus === 'arranged' ? 'success' : 'danger'" size="small">
|
||||
{{ row.transportStatus === 'arranged' ? '已安排' : '待安排' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
@@ -316,6 +316,7 @@
|
||||
upload-folder="pl/vehicle-cert"
|
||||
:cert-name-options="certNameOptions"
|
||||
layout="table"
|
||||
enforce-self-vehicle-required
|
||||
/>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -362,7 +363,8 @@
|
||||
:trailer-no="viewDetail?.trailerNo"
|
||||
:vehicle-id="viewDetail?.id"
|
||||
:personnel-list="viewPersonnelList"
|
||||
check-self-vehicle-certs
|
||||
vehicle-cert-check-type="self"
|
||||
check-linked-personnel-certs
|
||||
cert-layout="table"
|
||||
/>
|
||||
</template>
|
||||
@@ -403,12 +405,16 @@ import {
|
||||
getPlVehicleStatusTagType,
|
||||
normalizePlPersonnelSummaryDisplay
|
||||
} from '@/utils/pl-display'
|
||||
import {
|
||||
collectSelfVehicleRequiredCertErrors,
|
||||
ensureSelfVehicleCertSlots,
|
||||
formatRequiredCertErrors
|
||||
} from '@/utils/pl-cert-slots'
|
||||
import {
|
||||
PL_VEHICLE_CERT_NAME_OPTIONS,
|
||||
splitVehicleCertsByPart,
|
||||
getSelfVehicleCertMissingSuffix,
|
||||
validateVehicleCertNamesNotEmpty
|
||||
getSelfVehicleCertMissingSuffix
|
||||
} from '@/utils/pl-vehicle-cert'
|
||||
import { validatePersonnelTypeRoleCompat, validatePersonnelAffiliationVehicleCompat } from '@/utils/pl-personnel-cert'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
|
||||
const VEHICLE_TYPE_SELF = 'self'
|
||||
@@ -628,7 +634,7 @@ function resolvePersonnelRows(rows: VehiclePersonnelRow[]) {
|
||||
}
|
||||
|
||||
async function loadPersonnelOptions() {
|
||||
const res = await personnelApi.list({ status: '1', pageSize: 100 })
|
||||
const res = await personnelApi.list({ status: '1', pageSize: 100, personnelAffiliation: 'self' })
|
||||
personnelOptions.value = pickBxList<Personnel>(res)
|
||||
}
|
||||
|
||||
@@ -682,14 +688,29 @@ function validateSubTables(): boolean {
|
||||
ElMessage.warning('请选择人员类型')
|
||||
return false
|
||||
}
|
||||
const person = personnelOptions.value.find(p => p.id === row.personnelId)
|
||||
if (
|
||||
!validatePersonnelTypeRoleCompat(person?.personnelType, row.role, message =>
|
||||
ElMessage.warning(message)
|
||||
)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
!validatePersonnelAffiliationVehicleCompat(person?.personnelAffiliation, 'self', message =>
|
||||
ElMessage.warning(message)
|
||||
)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const hasTrailer = Boolean(formData.trailerNo?.trim())
|
||||
const { headCerts, trailerCerts } = splitVehicleCertsByPart(certList.value)
|
||||
return validateVehicleCertNamesNotEmpty(
|
||||
[headCerts, ...(hasTrailer ? [trailerCerts] : [])],
|
||||
message => ElMessage.warning(message)
|
||||
)
|
||||
const certErrors = collectSelfVehicleRequiredCertErrors(certList.value, formData.trailerNo)
|
||||
if (certErrors.length) {
|
||||
ElMessage.warning(formatRequiredCertErrors(certErrors))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
@@ -794,6 +815,7 @@ function handleAdd() {
|
||||
remark: ''
|
||||
})
|
||||
resetSubTables()
|
||||
certList.value = ensureSelfVehicleCertSlots([], formData.trailerNo)
|
||||
loadPersonnelOptions()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
@@ -845,17 +867,20 @@ async function handleEdit(row: Vehicle) {
|
||||
age: p.age ?? null
|
||||
}))
|
||||
)
|
||||
certList.value = (detail.certList || []).map(c => ({
|
||||
_key: c.id || c.certId || `cert-${Math.random().toString(36).slice(2, 10)}`,
|
||||
certId: c.id || c.certId,
|
||||
vehiclePart: c.vehiclePart,
|
||||
certName: c.certName,
|
||||
certNo: c.certNo,
|
||||
expireDate: c.expireDate,
|
||||
remindDays: c.remindDays ?? 30,
|
||||
attachmentUrls: c.attachmentUrls || ''
|
||||
}))
|
||||
certFormListRef.value?.clearUploadRefs()
|
||||
certList.value = ensureSelfVehicleCertSlots(
|
||||
(detail.certList || []).map(c => ({
|
||||
_key: c.id || c.certId || `cert-${Math.random().toString(36).slice(2, 10)}`,
|
||||
certId: c.id || c.certId,
|
||||
vehiclePart: c.vehiclePart,
|
||||
certName: c.certName,
|
||||
certNo: c.certNo,
|
||||
expireDate: c.expireDate,
|
||||
remindDays: c.remindDays ?? 30,
|
||||
attachmentUrls: c.attachmentUrls || ''
|
||||
})),
|
||||
formData.trailerNo
|
||||
)
|
||||
certFormListRef.value?.refreshRequiredSlots?.()
|
||||
} catch {
|
||||
dialogVisible.value = false
|
||||
ElMessage.error('加载车辆详情失败')
|
||||
|
||||
@@ -335,6 +335,8 @@
|
||||
upload-folder="pl/vehicle-cert"
|
||||
:cert-name-options="certNameOptions"
|
||||
layout="table"
|
||||
:enforce-self-vehicle-required="formData.vehicleType === 'self'"
|
||||
:enforce-carrier-vehicle-required="formData.vehicleType === 'carrier'"
|
||||
/>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -381,7 +383,10 @@
|
||||
:trailer-no="viewDetail?.trailerNo"
|
||||
:vehicle-id="viewDetail?.id"
|
||||
:personnel-list="viewPersonnelList"
|
||||
:check-self-vehicle-certs="viewDetail?.vehicleType === 'self'"
|
||||
:vehicle-cert-check-type="viewVehicleCertCheckType"
|
||||
:check-linked-personnel-certs="
|
||||
viewDetail?.vehicleType === 'self' || viewDetail?.vehicleType === 'carrier'
|
||||
"
|
||||
cert-layout="table"
|
||||
/>
|
||||
</template>
|
||||
@@ -416,12 +421,18 @@ import { pickBxList, pickBxPageResult } from '@/utils/http/bx'
|
||||
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import { formatPlVehiclePlateDisplay, normalizePlPersonnelSummaryDisplay } from '@/utils/pl-display'
|
||||
import {
|
||||
collectCarrierVehicleRequiredCertErrors,
|
||||
collectSelfVehicleRequiredCertErrors,
|
||||
ensureVehicleCertSlots,
|
||||
formatRequiredCertErrors
|
||||
} from '@/utils/pl-cert-slots'
|
||||
import {
|
||||
PL_VEHICLE_CERT_NAME_OPTIONS,
|
||||
splitVehicleCertsByPart,
|
||||
getSelfVehicleCertMissingSuffix,
|
||||
validateVehicleCertNamesNotEmpty
|
||||
getVehicleCertMissingSuffix,
|
||||
type PlVehicleCertCheckType
|
||||
} from '@/utils/pl-vehicle-cert'
|
||||
import { validatePersonnelTypeRoleCompat, validatePersonnelAffiliationVehicleCompat } from '@/utils/pl-personnel-cert'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
|
||||
type CertListItem = CertRow & { _key?: string }
|
||||
@@ -534,12 +545,19 @@ const viewHeroMeta = computed(() => {
|
||||
return parts.join(' · ')
|
||||
})
|
||||
|
||||
const viewVehicleCertCheckType = computed<PlVehicleCertCheckType | undefined>(() => {
|
||||
const type = viewDetail.value?.vehicleType
|
||||
return type === 'self' || type === 'carrier' ? type : undefined
|
||||
})
|
||||
|
||||
const viewDialogTitle = computed(() => {
|
||||
const plate = viewDetail.value?.licensePlate || '车辆详情'
|
||||
if (!viewDetail.value || viewDetail.value.vehicleType !== 'self') return plate
|
||||
const checkType = viewVehicleCertCheckType.value
|
||||
if (!viewDetail.value || !checkType) return plate
|
||||
return (
|
||||
plate +
|
||||
getSelfVehicleCertMissingSuffix({
|
||||
getVehicleCertMissingSuffix({
|
||||
vehicleType: checkType,
|
||||
certList: viewDetail.value.certList,
|
||||
trailerNo: viewDetail.value.trailerNo
|
||||
})
|
||||
@@ -682,7 +700,13 @@ function resolvePersonnelRows(rows: VehiclePersonnelRow[]) {
|
||||
}
|
||||
|
||||
async function loadPersonnelOptions() {
|
||||
const res = await personnelApi.list({ status: '1', pageSize: 100 })
|
||||
const affiliation =
|
||||
formData.vehicleType === 'carrier' ? 'carrier' : formData.vehicleType === 'self' ? 'self' : undefined
|
||||
const res = await personnelApi.list({
|
||||
status: '1',
|
||||
pageSize: 100,
|
||||
personnelAffiliation: affiliation
|
||||
})
|
||||
personnelOptions.value = pickBxList<Personnel>(res)
|
||||
}
|
||||
|
||||
@@ -736,14 +760,41 @@ function validateSubTables(): boolean {
|
||||
ElMessage.warning('请选择人员类型')
|
||||
return false
|
||||
}
|
||||
if (formData.vehicleType === 'self' || formData.vehicleType === 'carrier') {
|
||||
const person = personnelOptions.value.find(p => p.id === row.personnelId)
|
||||
if (
|
||||
!validatePersonnelTypeRoleCompat(person?.personnelType, row.role, message =>
|
||||
ElMessage.warning(message)
|
||||
)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
!validatePersonnelAffiliationVehicleCompat(
|
||||
person?.personnelAffiliation,
|
||||
formData.vehicleType,
|
||||
message => ElMessage.warning(message)
|
||||
)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hasTrailer = Boolean(formData.trailerNo?.trim())
|
||||
const { headCerts, trailerCerts } = splitVehicleCertsByPart(certList.value)
|
||||
return validateVehicleCertNamesNotEmpty(
|
||||
[headCerts, ...(hasTrailer ? [trailerCerts] : [])],
|
||||
message => ElMessage.warning(message)
|
||||
)
|
||||
if (formData.vehicleType === 'self') {
|
||||
const certErrors = collectSelfVehicleRequiredCertErrors(certList.value, formData.trailerNo)
|
||||
if (certErrors.length) {
|
||||
ElMessage.warning(formatRequiredCertErrors(certErrors))
|
||||
return false
|
||||
}
|
||||
} else if (formData.vehicleType === 'carrier') {
|
||||
const certErrors = collectCarrierVehicleRequiredCertErrors(certList.value, formData.trailerNo)
|
||||
if (certErrors.length) {
|
||||
ElMessage.warning(formatRequiredCertErrors(certErrors))
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
@@ -850,6 +901,7 @@ function handleAdd() {
|
||||
remark: ''
|
||||
})
|
||||
resetSubTables()
|
||||
certList.value = ensureVehicleCertSlots([], formData.trailerNo, formData.vehicleType as PlVehicleCertCheckType)
|
||||
loadPersonnelOptions()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
@@ -898,7 +950,7 @@ async function handleEdit(row: Vehicle) {
|
||||
age: p.age ?? null
|
||||
}))
|
||||
)
|
||||
certList.value = (detail.certList || []).map(c => ({
|
||||
const mappedCerts = (detail.certList || []).map(c => ({
|
||||
_key: c.id || c.certId || `cert-${Math.random().toString(36).slice(2, 10)}`,
|
||||
certId: c.id || c.certId,
|
||||
vehiclePart: c.vehiclePart,
|
||||
@@ -908,7 +960,15 @@ async function handleEdit(row: Vehicle) {
|
||||
remindDays: c.remindDays ?? 30,
|
||||
attachmentUrls: c.attachmentUrls || ''
|
||||
}))
|
||||
certFormListRef.value?.clearUploadRefs()
|
||||
certList.value =
|
||||
detail.vehicleType === 'self' || detail.vehicleType === 'carrier'
|
||||
? ensureVehicleCertSlots(
|
||||
mappedCerts,
|
||||
formData.trailerNo,
|
||||
detail.vehicleType as PlVehicleCertCheckType
|
||||
)
|
||||
: mappedCerts
|
||||
certFormListRef.value?.refreshRequiredSlots?.()
|
||||
} finally {
|
||||
dialogLoading.value = false
|
||||
}
|
||||
@@ -993,6 +1053,22 @@ async function handleDelete(row: Vehicle) {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => formData.vehicleType,
|
||||
async type => {
|
||||
if (!dialogVisible.value) return
|
||||
await loadPersonnelOptions()
|
||||
if (type === 'self' || type === 'carrier') {
|
||||
certList.value = ensureVehicleCertSlots(
|
||||
certList.value,
|
||||
formData.trailerNo,
|
||||
type as PlVehicleCertCheckType
|
||||
)
|
||||
certFormListRef.value?.refreshRequiredSlots?.()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
useDbInstantSearch(
|
||||
() => ({
|
||||
q: searchForm.q,
|
||||
|
||||
Reference in new issue
Block a user