pl-vehicle-cert.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. import type { CertRow } from '@/api/pl'
  2. export const PL_VEHICLE_CERT_PART_HEAD = 'head'
  3. export const PL_VEHICLE_CERT_PART_TRAILER = 'trailer'
  4. export const PL_VEHICLE_CERT_MISSING_SUFFIX = '(证书缺失)'
  5. /** @deprecated 保存不再阻断,仅保留供历史引用 */
  6. export const PL_VEHICLE_REQUIRED_CERT_NAMES = ['行驶证', '营运证'] as const
  7. export const PL_SELF_VEHICLE_HEAD_REQUIRED_CERT_NAMES = [
  8. '行驶证',
  9. '营运证',
  10. '保险单'
  11. ] as const
  12. export const PL_SELF_VEHICLE_TRAILER_REQUIRED_CERT_NAMES = [
  13. '行驶证',
  14. '营运证',
  15. '保险单',
  16. '罐检报告'
  17. ] as const
  18. export const PL_CARRIER_VEHICLE_HEAD_REQUIRED_CERT_NAMES = ['行驶证', '营运证'] as const
  19. export const PL_CARRIER_VEHICLE_TRAILER_REQUIRED_CERT_NAMES = ['行驶证', '营运证'] as const
  20. /** 特殊挂车号:不展示挂车证件槽位、不校验挂车证件 */
  21. export const PL_HUOLALA_TRAILER_NO = '货拉拉'
  22. export type PlVehicleCertCheckType = 'self' | 'carrier'
  23. export function isHuolalaTrailerNo(trailerNo?: string | null): boolean {
  24. return (trailerNo || '').trim() === PL_HUOLALA_TRAILER_NO
  25. }
  26. /** 有挂车号且非货拉拉时展示挂车证件区 */
  27. export function shouldShowVehicleTrailerCertSlots(trailerNo?: string | null): boolean {
  28. const text = (trailerNo || '').trim()
  29. return Boolean(text) && !isHuolalaTrailerNo(text)
  30. }
  31. /** 仅山峡自有 + 正常挂车号时强制挂车证件 */
  32. export function shouldRequireVehicleTrailerCerts(
  33. vehicleType: PlVehicleCertCheckType,
  34. trailerNo?: string | null
  35. ): boolean {
  36. return vehicleType === 'self' && shouldShowVehicleTrailerCertSlots(trailerNo)
  37. }
  38. export const PL_VEHICLE_CERT_NAME_OPTIONS = [
  39. '行驶证',
  40. '营运证',
  41. '道路运输证',
  42. '驾驶证',
  43. '押运员证',
  44. '危化品从业资格证',
  45. '保险单',
  46. '罐检报告'
  47. ]
  48. export type VehicleCertPart = typeof PL_VEHICLE_CERT_PART_HEAD | typeof PL_VEHICLE_CERT_PART_TRAILER
  49. export function normalizeVehicleCertPart(part?: string | null): VehicleCertPart {
  50. return part === PL_VEHICLE_CERT_PART_TRAILER ? PL_VEHICLE_CERT_PART_TRAILER : PL_VEHICLE_CERT_PART_HEAD
  51. }
  52. export function splitVehicleCertsByPart<T extends CertRow>(certList: T[] = []) {
  53. const headCerts: T[] = []
  54. const trailerCerts: T[] = []
  55. for (const cert of certList) {
  56. if (normalizeVehicleCertPart(cert.vehiclePart) === PL_VEHICLE_CERT_PART_TRAILER) {
  57. trailerCerts.push(cert)
  58. } else {
  59. headCerts.push(cert)
  60. }
  61. }
  62. return { headCerts, trailerCerts }
  63. }
  64. export function mergeVehicleCertsByPart<T extends CertRow>(
  65. headCerts: T[],
  66. trailerCerts: T[],
  67. hasTrailer: boolean
  68. ) {
  69. const normalizedHead = headCerts.map(cert => ({
  70. ...cert,
  71. vehiclePart: PL_VEHICLE_CERT_PART_HEAD
  72. }))
  73. if (!hasTrailer) {
  74. return normalizedHead
  75. }
  76. const normalizedTrailer = trailerCerts.map(cert => ({
  77. ...cert,
  78. vehiclePart: PL_VEHICLE_CERT_PART_TRAILER
  79. }))
  80. return [...normalizedHead, ...normalizedTrailer]
  81. }
  82. export function isVehicleCertUploaded(cert?: Pick<CertRow, 'certName' | 'certNo' | 'expireDate' | 'attachmentUrls'>) {
  83. return Boolean(cert?.certName?.trim() && cert?.expireDate)
  84. }
  85. export function getVehicleHeadRequiredCertNames(
  86. vehicleType: PlVehicleCertCheckType
  87. ): readonly string[] {
  88. return vehicleType === 'carrier'
  89. ? PL_CARRIER_VEHICLE_HEAD_REQUIRED_CERT_NAMES
  90. : PL_SELF_VEHICLE_HEAD_REQUIRED_CERT_NAMES
  91. }
  92. export function getVehicleTrailerRequiredCertNames(
  93. vehicleType: PlVehicleCertCheckType
  94. ): readonly string[] {
  95. return vehicleType === 'carrier'
  96. ? PL_CARRIER_VEHICLE_TRAILER_REQUIRED_CERT_NAMES
  97. : PL_SELF_VEHICLE_TRAILER_REQUIRED_CERT_NAMES
  98. }
  99. export function getMissingVehiclePartCertNames(
  100. certList: CertRow[] = [],
  101. part: VehicleCertPart,
  102. vehicleType: PlVehicleCertCheckType
  103. ): string[] {
  104. const requiredNames =
  105. part === PL_VEHICLE_CERT_PART_TRAILER
  106. ? getVehicleTrailerRequiredCertNames(vehicleType)
  107. : getVehicleHeadRequiredCertNames(vehicleType)
  108. return requiredNames.filter(requiredName => {
  109. const matched = certList.find(cert => cert.certName?.trim() === requiredName)
  110. return !isVehicleCertUploaded(matched)
  111. })
  112. }
  113. export function getMissingSelfVehiclePartCertNames(
  114. certList: CertRow[] = [],
  115. part: VehicleCertPart
  116. ): string[] {
  117. return getMissingVehiclePartCertNames(certList, part, 'self')
  118. }
  119. export function hasVehicleCertMissing(options: {
  120. vehicleType: PlVehicleCertCheckType
  121. certList?: CertRow[]
  122. trailerNo?: string | null
  123. }): boolean {
  124. const { vehicleType, certList = [], trailerNo } = options
  125. const { headCerts, trailerCerts } = splitVehicleCertsByPart(certList)
  126. // 三方车辆证件不强制,列表缺失后缀仅对自有生效
  127. if (vehicleType === 'carrier') {
  128. return false
  129. }
  130. if (
  131. getMissingVehiclePartCertNames(headCerts, PL_VEHICLE_CERT_PART_HEAD, vehicleType).length > 0
  132. ) {
  133. return true
  134. }
  135. if (
  136. shouldRequireVehicleTrailerCerts(vehicleType, trailerNo) &&
  137. getMissingVehiclePartCertNames(trailerCerts, PL_VEHICLE_CERT_PART_TRAILER, vehicleType)
  138. .length > 0
  139. ) {
  140. return true
  141. }
  142. return false
  143. }
  144. export function hasSelfVehicleCertMissing(options: {
  145. certList?: CertRow[]
  146. trailerNo?: string | null
  147. }): boolean {
  148. return hasVehicleCertMissing({ ...options, vehicleType: 'self' })
  149. }
  150. export function getVehicleCertMissingSuffix(options: {
  151. vehicleType: PlVehicleCertCheckType
  152. certList?: CertRow[]
  153. trailerNo?: string | null
  154. }): string {
  155. return hasVehicleCertMissing(options) ? PL_VEHICLE_CERT_MISSING_SUFFIX : ''
  156. }
  157. export function getSelfVehicleCertMissingSuffix(options: {
  158. certList?: CertRow[]
  159. trailerNo?: string | null
  160. }): string {
  161. return getVehicleCertMissingSuffix({ ...options, vehicleType: 'self' })
  162. }
  163. export function getVehiclePartCertMissingSuffix(
  164. certList: CertRow[] = [],
  165. part: VehicleCertPart,
  166. vehicleType?: PlVehicleCertCheckType | null
  167. ): string {
  168. if (!vehicleType) return ''
  169. return getMissingVehiclePartCertNames(certList, part, vehicleType).length > 0
  170. ? PL_VEHICLE_CERT_MISSING_SUFFIX
  171. : ''
  172. }
  173. export function getSelfVehiclePartCertMissingSuffix(
  174. certList: CertRow[] = [],
  175. part: VehicleCertPart
  176. ): string {
  177. return getVehiclePartCertMissingSuffix(certList, part, 'self')
  178. }
  179. /** @deprecated 保存不再阻断证书完整性,仅校验证书名称非空等基础项 */
  180. export function validateVehicleRequiredCerts(options: {
  181. headCerts: CertRow[]
  182. trailerCerts: CertRow[]
  183. hasTrailer: boolean
  184. licensePlate: string
  185. trailerNo: string
  186. onError: (message: string) => void
  187. }) {
  188. const { headCerts, trailerCerts, hasTrailer } = options
  189. for (const certs of [headCerts, ...(hasTrailer ? [trailerCerts] : [])]) {
  190. for (const cert of certs) {
  191. if (!cert.certName?.trim()) {
  192. options.onError('证书名称不能为空')
  193. return false
  194. }
  195. }
  196. }
  197. return true
  198. }
  199. export function filterVehicleCertsForSubmit<T extends CertRow>(
  200. headCerts: T[],
  201. trailerCerts: T[],
  202. hasTrailer: boolean
  203. ) {
  204. return mergeVehicleCertsByPart(headCerts, trailerCerts, hasTrailer)
  205. }
  206. export function validateVehicleCertNamesNotEmpty(
  207. certLists: CertRow[][],
  208. onError: (message: string) => void
  209. ): boolean {
  210. for (const certs of certLists) {
  211. for (const cert of certs) {
  212. if (!cert.certName?.trim()) {
  213. onError('证书名称不能为空')
  214. return false
  215. }
  216. }
  217. }
  218. return true
  219. }
  220. export const PL_VEHICLE_CERT_SHORT_LABELS: Record<string, string> = {
  221. 行驶证: '行',
  222. 营运证: '营',
  223. 保险单: '保',
  224. 罐检报告: '罐'
  225. }
  226. export type VehicleCertBadgeStatus = 'missing' | 'valid' | 'expiring' | 'expired'
  227. export interface VehicleCertBadgeItem {
  228. requiredName: string
  229. shortLabel: string
  230. status: VehicleCertBadgeStatus
  231. title: string
  232. }
  233. function matchVehicleRequiredCertName(
  234. actualName: string | undefined,
  235. requiredName: string
  236. ): boolean {
  237. return (actualName?.trim() || '') === requiredName
  238. }
  239. function getVehicleCertMissingFieldLabels(
  240. cert: Pick<CertRow, 'certName' | 'certNo' | 'expireDate' | 'attachmentUrls'> | undefined,
  241. requiredName: string
  242. ): string[] {
  243. if (!cert || !matchVehicleRequiredCertName(cert.certName, requiredName)) {
  244. return ['有效期']
  245. }
  246. const missing: string[] = []
  247. if (!cert.expireDate) missing.push('有效期')
  248. // 证书附件非必填
  249. return missing
  250. }
  251. function formatVehicleCertBadgeTooltip(
  252. partLabel: string,
  253. requiredName: string,
  254. status: VehicleCertBadgeStatus,
  255. cert?: Pick<CertRow, 'certName' | 'certNo' | 'expireDate' | 'attachmentUrls' | 'remindDays'>
  256. ): string {
  257. const prefix = partLabel ? `${partLabel} · ` : ''
  258. if (status === 'missing') {
  259. if (!cert || !matchVehicleRequiredCertName(cert.certName, requiredName)) {
  260. return `${prefix}${requiredName}:缺失(未登记该证书)`
  261. }
  262. const missing = getVehicleCertMissingFieldLabels(cert, requiredName)
  263. return `${prefix}${requiredName}:缺失(缺少${missing.join('、')})`
  264. }
  265. const expireDate = cert?.expireDate || '—'
  266. const expDate = new Date(expireDate)
  267. const today = new Date()
  268. today.setHours(0, 0, 0, 0)
  269. expDate.setHours(0, 0, 0, 0)
  270. const diffDays = Math.ceil((expDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))
  271. const certNo = cert?.certNo?.trim()
  272. const certNoText = certNo ? `,证号 ${certNo}` : ''
  273. if (status === 'expired') {
  274. return `${prefix}${requiredName}:已过期,有效期 ${expireDate},已过期 ${Math.abs(diffDays)} 天${certNoText}`
  275. }
  276. if (status === 'expiring') {
  277. return `${prefix}${requiredName}:临期,有效期 ${expireDate},剩余 ${diffDays} 天${certNoText}`
  278. }
  279. return `${prefix}${requiredName}:正常,有效期 ${expireDate}${certNoText}`
  280. }
  281. export function getVehicleCertBadgeStatus(
  282. cert: Pick<CertRow, 'certName' | 'certNo' | 'expireDate' | 'attachmentUrls' | 'remindDays'> | undefined,
  283. requiredName: string
  284. ): VehicleCertBadgeStatus {
  285. if (!isVehicleCertUploaded(cert) || !matchVehicleRequiredCertName(cert?.certName, requiredName)) {
  286. return 'missing'
  287. }
  288. const expireDate = cert?.expireDate
  289. if (!expireDate) return 'missing'
  290. const remindDays = cert?.remindDays ?? 30
  291. const expDate = new Date(expireDate)
  292. const today = new Date()
  293. today.setHours(0, 0, 0, 0)
  294. expDate.setHours(0, 0, 0, 0)
  295. const diffDays = Math.ceil((expDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))
  296. if (diffDays <= 0) return 'expired'
  297. if (diffDays <= remindDays) return 'expiring'
  298. return 'valid'
  299. }
  300. export function getVehiclePartRequiredCertBadges(options: {
  301. vehicleType: PlVehicleCertCheckType
  302. certList?: CertRow[]
  303. part: VehicleCertPart
  304. partLabel?: string
  305. }): VehicleCertBadgeItem[] {
  306. const { vehicleType, certList = [], part, partLabel = '' } = options
  307. const partCerts =
  308. part === PL_VEHICLE_CERT_PART_TRAILER
  309. ? splitVehicleCertsByPart(certList).trailerCerts
  310. : splitVehicleCertsByPart(certList).headCerts
  311. const requiredNames =
  312. part === PL_VEHICLE_CERT_PART_TRAILER
  313. ? getVehicleTrailerRequiredCertNames(vehicleType)
  314. : getVehicleHeadRequiredCertNames(vehicleType)
  315. return requiredNames.map(requiredName => {
  316. const matched = partCerts.find(cert => matchVehicleRequiredCertName(cert.certName, requiredName))
  317. const status = getVehicleCertBadgeStatus(matched, requiredName)
  318. const shortLabel = PL_VEHICLE_CERT_SHORT_LABELS[requiredName] || requiredName.slice(0, 1)
  319. return {
  320. requiredName,
  321. shortLabel,
  322. status,
  323. title: formatVehicleCertBadgeTooltip(partLabel, requiredName, status, matched)
  324. }
  325. })
  326. }