This commit is contained in:
zlwu committed 2026-07-12 17:14:53 +08:00
1 parent ffc89d9b57
commit c2cba3e668
53 files changed
+11794 -6264

No files matched your search

+14 -26
View File
@@ -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)
+145 -17
View File
@@ -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
)
+66 -34
View File
@@ -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
}
+1 -1
View File
@@ -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>
+47 -22
View File
@@ -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('加载车辆详情失败')
+91 -15
View File
@@ -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,