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

+241
View File
@@ -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(';')}`
}
+1 -1
View File
@@ -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) {
+113 -6
View File
@@ -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 : ''
}
+80 -14
View File
@@ -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 保存不再阻断证书完整性,仅校验证书名称非空等基础项 */