378 lines
11 KiB
TypeScript
378 lines
11 KiB
TypeScript
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)
|
||
}
|
||
})
|
||
}
|