Files
shanxia2.0/web-admin/src/utils/pl-vehicle-cert.ts
T
2026-08-21 09:35:11 +08:00

378 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { CertRow } from '@/api/pl'
export const PL_VEHICLE_CERT_PART_HEAD = 'head'
export const PL_VEHICLE_CERT_PART_TRAILER = 'trailer'
export const PL_VEHICLE_CERT_MISSING_SUFFIX = '(证书缺失)'
/** @deprecated 保存不再阻断,仅保留供历史引用 */
export const PL_VEHICLE_REQUIRED_CERT_NAMES = ['行驶证', '营运证'] as const
export const PL_SELF_VEHICLE_HEAD_REQUIRED_CERT_NAMES = [
'行驶证',
'营运证',
'保险单'
] as const
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 const PL_HUOLALA_TRAILER_NO = '货拉拉'
export type PlVehicleCertCheckType = 'self' | 'carrier'
export function isHuolalaTrailerNo(trailerNo?: string | null): boolean {
return (trailerNo || '').trim() === PL_HUOLALA_TRAILER_NO
}
/** 有挂车号且非货拉拉时展示挂车证件区 */
export function shouldShowVehicleTrailerCertSlots(trailerNo?: string | null): boolean {
const text = (trailerNo || '').trim()
return Boolean(text) && !isHuolalaTrailerNo(text)
}
/** 仅山峡自有 + 正常挂车号时强制挂车证件 */
export function shouldRequireVehicleTrailerCerts(
vehicleType: PlVehicleCertCheckType,
trailerNo?: string | null
): boolean {
return vehicleType === 'self' && shouldShowVehicleTrailerCertSlots(trailerNo)
}
export const PL_VEHICLE_CERT_NAME_OPTIONS = [
'行驶证',
'营运证',
'道路运输证',
'驾驶证',
'押运员证',
'危化品从业资格证',
'保险单',
'罐检报告'
]
export type VehicleCertPart = typeof PL_VEHICLE_CERT_PART_HEAD | typeof PL_VEHICLE_CERT_PART_TRAILER
export function normalizeVehicleCertPart(part?: string | null): VehicleCertPart {
return part === PL_VEHICLE_CERT_PART_TRAILER ? PL_VEHICLE_CERT_PART_TRAILER : PL_VEHICLE_CERT_PART_HEAD
}
export function splitVehicleCertsByPart<T extends CertRow>(certList: T[] = []) {
const headCerts: T[] = []
const trailerCerts: T[] = []
for (const cert of certList) {
if (normalizeVehicleCertPart(cert.vehiclePart) === PL_VEHICLE_CERT_PART_TRAILER) {
trailerCerts.push(cert)
} else {
headCerts.push(cert)
}
}
return { headCerts, trailerCerts }
}
export function mergeVehicleCertsByPart<T extends CertRow>(
headCerts: T[],
trailerCerts: T[],
hasTrailer: boolean
) {
const normalizedHead = headCerts.map(cert => ({
...cert,
vehiclePart: PL_VEHICLE_CERT_PART_HEAD
}))
if (!hasTrailer) {
return normalizedHead
}
const normalizedTrailer = trailerCerts.map(cert => ({
...cert,
vehiclePart: PL_VEHICLE_CERT_PART_TRAILER
}))
return [...normalizedHead, ...normalizedTrailer]
}
export function isVehicleCertUploaded(cert?: Pick<CertRow, 'certName' | 'certNo' | 'expireDate' | 'attachmentUrls'>) {
return Boolean(cert?.certName?.trim() && cert?.expireDate)
}
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,
vehicleType: PlVehicleCertCheckType
): string[] {
const requiredNames =
part === PL_VEHICLE_CERT_PART_TRAILER
? getVehicleTrailerRequiredCertNames(vehicleType)
: getVehicleHeadRequiredCertNames(vehicleType)
return requiredNames.filter(requiredName => {
const matched = certList.find(cert => cert.certName?.trim() === requiredName)
return !isVehicleCertUploaded(matched)
})
}
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 { vehicleType, certList = [], trailerNo } = options
const { headCerts, trailerCerts } = splitVehicleCertsByPart(certList)
// 三方车辆证件不强制,列表缺失后缀仅对自有生效
if (vehicleType === 'carrier') {
return false
}
if (
getMissingVehiclePartCertNames(headCerts, PL_VEHICLE_CERT_PART_HEAD, vehicleType).length > 0
) {
return true
}
if (
shouldRequireVehicleTrailerCerts(vehicleType, trailerNo) &&
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 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 getVehiclePartCertMissingSuffix(certList, part, 'self')
}
/** @deprecated 保存不再阻断证书完整性,仅校验证书名称非空等基础项 */
export function validateVehicleRequiredCerts(options: {
headCerts: CertRow[]
trailerCerts: CertRow[]
hasTrailer: boolean
licensePlate: string
trailerNo: string
onError: (message: string) => void
}) {
const { headCerts, trailerCerts, hasTrailer } = options
for (const certs of [headCerts, ...(hasTrailer ? [trailerCerts] : [])]) {
for (const cert of certs) {
if (!cert.certName?.trim()) {
options.onError('证书名称不能为空')
return false
}
}
}
return true
}
export function filterVehicleCertsForSubmit<T extends CertRow>(
headCerts: T[],
trailerCerts: T[],
hasTrailer: boolean
) {
return mergeVehicleCertsByPart(headCerts, trailerCerts, hasTrailer)
}
export function validateVehicleCertNamesNotEmpty(
certLists: CertRow[][],
onError: (message: string) => void
): boolean {
for (const certs of certLists) {
for (const cert of certs) {
if (!cert.certName?.trim()) {
onError('证书名称不能为空')
return false
}
}
}
return true
}
export const PL_VEHICLE_CERT_SHORT_LABELS: Record<string, string> = {
行驶证: '行',
营运证: '营',
保险单: '保',
罐检报告: '罐'
}
export type VehicleCertBadgeStatus = 'missing' | 'valid' | 'expiring' | 'expired'
export interface VehicleCertBadgeItem {
requiredName: string
shortLabel: string
status: VehicleCertBadgeStatus
title: string
}
function matchVehicleRequiredCertName(
actualName: string | undefined,
requiredName: string
): boolean {
return (actualName?.trim() || '') === requiredName
}
function getVehicleCertMissingFieldLabels(
cert: Pick<CertRow, 'certName' | 'certNo' | 'expireDate' | 'attachmentUrls'> | undefined,
requiredName: string
): string[] {
if (!cert || !matchVehicleRequiredCertName(cert.certName, requiredName)) {
return ['有效期']
}
const missing: string[] = []
if (!cert.expireDate) missing.push('有效期')
// 证书附件非必填
return missing
}
function formatVehicleCertBadgeTooltip(
partLabel: string,
requiredName: string,
status: VehicleCertBadgeStatus,
cert?: Pick<CertRow, 'certName' | 'certNo' | 'expireDate' | 'attachmentUrls' | 'remindDays'>
): string {
const prefix = partLabel ? `${partLabel} · ` : ''
if (status === 'missing') {
if (!cert || !matchVehicleRequiredCertName(cert.certName, requiredName)) {
return `${prefix}${requiredName}:缺失(未登记该证书)`
}
const missing = getVehicleCertMissingFieldLabels(cert, requiredName)
return `${prefix}${requiredName}:缺失(缺少${missing.join('、')})`
}
const expireDate = cert?.expireDate || '—'
const expDate = new Date(expireDate)
const today = new Date()
today.setHours(0, 0, 0, 0)
expDate.setHours(0, 0, 0, 0)
const diffDays = Math.ceil((expDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))
const certNo = cert?.certNo?.trim()
const certNoText = certNo ? `,证号 ${certNo}` : ''
if (status === 'expired') {
return `${prefix}${requiredName}:已过期,有效期 ${expireDate},已过期 ${Math.abs(diffDays)} 天${certNoText}`
}
if (status === 'expiring') {
return `${prefix}${requiredName}:临期,有效期 ${expireDate},剩余 ${diffDays} 天${certNoText}`
}
return `${prefix}${requiredName}:正常,有效期 ${expireDate}${certNoText}`
}
export function getVehicleCertBadgeStatus(
cert: Pick<CertRow, 'certName' | 'certNo' | 'expireDate' | 'attachmentUrls' | 'remindDays'> | undefined,
requiredName: string
): VehicleCertBadgeStatus {
if (!isVehicleCertUploaded(cert) || !matchVehicleRequiredCertName(cert?.certName, requiredName)) {
return 'missing'
}
const expireDate = cert?.expireDate
if (!expireDate) return 'missing'
const remindDays = cert?.remindDays ?? 30
const expDate = new Date(expireDate)
const today = new Date()
today.setHours(0, 0, 0, 0)
expDate.setHours(0, 0, 0, 0)
const diffDays = Math.ceil((expDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))
if (diffDays <= 0) return 'expired'
if (diffDays <= remindDays) return 'expiring'
return 'valid'
}
export function getVehiclePartRequiredCertBadges(options: {
vehicleType: PlVehicleCertCheckType
certList?: CertRow[]
part: VehicleCertPart
partLabel?: string
}): VehicleCertBadgeItem[] {
const { vehicleType, certList = [], part, partLabel = '' } = options
const partCerts =
part === PL_VEHICLE_CERT_PART_TRAILER
? splitVehicleCertsByPart(certList).trailerCerts
: splitVehicleCertsByPart(certList).headCerts
const requiredNames =
part === PL_VEHICLE_CERT_PART_TRAILER
? getVehicleTrailerRequiredCertNames(vehicleType)
: getVehicleHeadRequiredCertNames(vehicleType)
return requiredNames.map(requiredName => {
const matched = partCerts.find(cert => matchVehicleRequiredCertName(cert.certName, requiredName))
const status = getVehicleCertBadgeStatus(matched, requiredName)
const shortLabel = PL_VEHICLE_CERT_SHORT_LABELS[requiredName] || requiredName.slice(0, 1)
return {
requiredName,
shortLabel,
status,
title: formatVehicleCertBadgeTooltip(partLabel, requiredName, status, matched)
}
})
}