diff --git a/src/api/transportCapacity/certification-audit.js b/src/api/transportCapacity/certification-audit.js new file mode 100644 index 0000000..2501a8c --- /dev/null +++ b/src/api/transportCapacity/certification-audit.js @@ -0,0 +1,49 @@ +import request from '@/axios'; + +/** + * 认证记录列表:按审核状态分页,按提交时间倒序 + * 审核对象是**一条认证记录**,不是一辆车或一名司机,故必须带 bizType 区分 + * status 为审核状态(0审核中 1已通过 2已驳回 3已作废);不传时后端按 0 处理 + */ +export const getPendingPage = (bizType, current, size, params) => { + return request({ + url: '/blade-transport/certification-audit/pending-page', + method: 'get', + params: { + ...params, + bizType, + current, + size, + }, + }); +}; + +/** + * 审核详情:同时返回本次待审快照与该对象当前生效快照,供前端做字段级对比 + * 从未认证通过时 effectiveSnapshotJson 为空串 + */ +export const getAuditDetail = id => { + return request({ + url: '/blade-transport/certification-audit/detail', + method: 'get', + params: { + id, + }, + }); +}; + +/** + * 审核一条认证记录 + * 通过时不传驳回原因;驳回时必须传,且原因不超过 200 字 + */ +export const auditCertification = (id, pass, rejectReason) => { + return request({ + url: '/blade-transport/certification-audit/audit', + method: 'post', + params: { + id, + pass, + ...(rejectReason ? { rejectReason } : {}), + }, + }); +}; diff --git a/src/api/transportCapacity/transport-vehicle.js b/src/api/transportCapacity/transport-vehicle.js index f2bfb67..0eed04c 100644 --- a/src/api/transportCapacity/transport-vehicle.js +++ b/src/api/transportCapacity/transport-vehicle.js @@ -51,14 +51,6 @@ export const changeStatus = (id, status) => { }); }; -export const auditCertification = (id, certificationStatus, rejectReason) => { - return request({ - url: '/blade-transport/transport-vehicle/audit-certification', - method: 'post', - params: { id, certificationStatus, rejectReason }, - }); -}; - export const getExpiryStat = params => { return request({ url: '/blade-transport/transport-vehicle/expiry-stat', diff --git a/src/option/transportCapacity/cert-audit-driver.js b/src/option/transportCapacity/cert-audit-driver.js new file mode 100644 index 0000000..71b9856 --- /dev/null +++ b/src/option/transportCapacity/cert-audit-driver.js @@ -0,0 +1,51 @@ +export const option = { + height: 'auto', + calcHeight: 32, + tip: false, + searchShow: true, + searchMenuSpan: 24, + searchIcon: true, + searchIndex: 4, + searchMenuPosition: 'right', + border: true, + index: true, + indexLabel: '序号', + indexWidth: 70, + selection: false, + addBtn: false, + editBtn: false, + delBtn: false, + viewBtn: false, + columnBtn: false, + menuWidth: 110, + column: [ + { + label: '司机姓名', + prop: 'driverName', + search: true, + searchOrder: 3, + minWidth: 120, + }, + { + label: '司机手机号', + prop: 'driverMobile', + search: true, + searchOrder: 2, + minWidth: 130, + }, + { + label: '审核状态', + prop: 'statusText', + slot: true, + width: 110, + align: 'center', + }, + { + label: '提交时间', + prop: 'submitTime', + slot: true, + minWidth: 170, + align: 'center', + }, + ], +}; diff --git a/src/option/transportCapacity/cert-audit-vehicle.js b/src/option/transportCapacity/cert-audit-vehicle.js new file mode 100644 index 0000000..5e93caf --- /dev/null +++ b/src/option/transportCapacity/cert-audit-vehicle.js @@ -0,0 +1,58 @@ +export const option = { + height: 'auto', + calcHeight: 32, + tip: false, + searchShow: true, + searchMenuSpan: 24, + searchIcon: true, + searchIndex: 4, + searchMenuPosition: 'right', + border: true, + index: true, + indexLabel: '序号', + indexWidth: 70, + selection: false, + addBtn: false, + editBtn: false, + delBtn: false, + viewBtn: false, + columnBtn: false, + menuWidth: 110, + column: [ + { + label: '车牌号', + prop: 'bizKey', + search: true, + searchOrder: 3, + minWidth: 140, + }, + { + label: '司机姓名', + prop: 'driverName', + search: true, + searchOrder: 2, + minWidth: 120, + }, + { + label: '司机手机号', + prop: 'driverMobile', + search: true, + searchOrder: 1, + minWidth: 130, + }, + { + label: '审核状态', + prop: 'statusText', + slot: true, + width: 110, + align: 'center', + }, + { + label: '提交时间', + prop: 'submitTime', + slot: true, + minWidth: 170, + align: 'center', + }, + ], +}; diff --git a/src/option/transportCapacity/transport-vehicle.js b/src/option/transportCapacity/transport-vehicle.js index df05326..ffe1bae 100644 --- a/src/option/transportCapacity/transport-vehicle.js +++ b/src/option/transportCapacity/transport-vehicle.js @@ -95,6 +95,8 @@ export const option = { align: 'center', }, { + // 这里是「最新一次提交的状态」缓存,不等于该车当前是否已认证: + // 已认证由生效快照指针决定,曾审核通过即永久保持,重新提交不会让它退回审核中 label: '认证状态', prop: 'certificationStatus', slot: true, @@ -104,9 +106,10 @@ export const option = { type: 'select', dicData: [ { label: '全部', value: '' }, - { label: '认证中', value: 0 }, - { label: '已认证', value: 1 }, + { label: '审核中', value: 0 }, + { label: '已通过', value: 1 }, { label: '已驳回', value: 2 }, + { label: '已作废', value: 3 }, ], minWidth: 110, align: 'center', diff --git a/src/utils/cert-audit.js b/src/utils/cert-audit.js new file mode 100644 index 0000000..9044ab4 --- /dev/null +++ b/src/utils/cert-audit.js @@ -0,0 +1,403 @@ +/** + * 认证审核通用工具 + * + * 认证对象(司机 / 车辆)每次提交产生一条不可变快照记录,「审核」针对的是**一条记录**。 + * 记录分两侧:本次**待审快照**与该对象**当前生效快照**(最后一次审核通过的那条)。 + * 待审内容在审核通过前不影响生效值,所以审核页必须逐字段对比两侧,而不是只看一条当前值。 + * + * 本文件提供三件事: + * 1. 审核状态文案与标签(与后端 statusText 同一套词表); + * 2. 快照字段的展示元数据(中文名、控件类型、分组); + * 3. 两侧快照的字段级 diff 计算,含「从未认证通过、无对比基准」的处理。 + * + * 两侧快照字段名完全一致(同一套结构),故可逐键比较。 + */ + +const EMPTY_TEXT = '—'; + +const TYPE_IMAGE = 'image'; +const TYPE_DATE = 'date'; +const TYPE_LONG_TERM = 'longTerm'; + +/** 审核状态取值 */ +export const CERT_RECORD_STATUS = { + PENDING: 0, + APPROVED: 1, + REJECTED: 2, + VOIDED: 3, +}; + +/** 状态文案统一为:审核中 / 已通过 / 已驳回 / 已作废 */ +const CERT_STATUS_TAG = { + 0: { label: '审核中', type: 'warning' }, + 1: { label: '已通过', type: 'success' }, + 2: { label: '已驳回', type: 'danger' }, + 3: { label: '已作废', type: 'info' }, +}; + +/** 变更类型 → 差异标签 */ +export const CERT_CHANGE_TAG = { + added: { label: '新增', type: 'success' }, + modified: { label: '已修改', type: 'warning' }, + cleared: { label: '已清空', type: 'danger' }, +}; + +/** + * 「最新提交状态」展示缓存 → 标签 + * + * 缓存为空表示该对象从未提交过认证(即没有生效快照)。 + * 注意它只是最近一次提交的状态,不等于「是否已认证」——后者由生效快照指针决定。 + */ +export const certStatusTag = status => { + if (status === null || status === undefined || status === '') { + return { label: '未认证', type: 'info' }; + } + return CERT_STATUS_TAG[status] || { label: EMPTY_TEXT, type: 'info' }; +}; + +const pad = value => String(value).padStart(2, '0'); + +/** 后端若改用时间戳毫秒序列化日期,也能落成日期文案 */ +const EPOCH_PATTERN = /^\d{13}$/; + +const isEpochText = text => EPOCH_PATTERN.test(text); + +const toDate = text => { + const date = new Date(Number(text)); + return Number.isNaN(date.getTime()) ? null : date; +}; + +/** 提交时间等日期时间展示,兼容 `yyyy-MM-dd HH:mm:ss`、ISO 串与时间戳毫秒 */ +export const formatCertDateTime = value => { + if (value === null || value === undefined || value === '') return EMPTY_TEXT; + const text = String(value).trim(); + const matched = text.match( + /^(\d{4})[-/](\d{1,2})[-/](\d{1,2})[T ](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?/ + ); + if (matched) { + return ( + `${matched[1]}-${pad(matched[2])}-${pad(matched[3])} ` + + `${pad(matched[4])}:${pad(matched[5])}:${pad(matched[6] || 0)}` + ); + } + const date = isEpochText(text) ? toDate(text) : null; + if (!date) return text; + return ( + `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` + + `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}` + ); +}; + +/** + * 快照字段元数据 + * + * 字段名与后端提交 DTO 的 JSON 键一致(司机 PersonCertSubmitRequest / 车辆 VehicleCertSubmitRequest), + * 两侧快照共用同一套结构,故同时用于标签与比较规则。 + * + * `longTermKey` 只挂在**被长期有效作废掉的那个字段**上,也就是各组的「有效期止」与强制报废日期: + * 勾了长期有效,它们显示「长期有效」、比较时按同一个值算(避免每次审核都报「已变更」)。 + * 「有效期起」不挂:起期是一个独立的日期,长期有效并不会让它失效, + * 挂上去等于把它自己的值藏起来、还让审核员看不到起期的改动。 + */ +const CERT_SNAPSHOT_GROUPS = { + driver: [ + { + title: '身份信息', + fields: [ + { key: 'realName', label: '真实姓名' }, + { key: 'idCardNo', label: '身份证号' }, + { key: 'idCardFrontUrl', label: '身份证正面照', type: TYPE_IMAGE }, + { key: 'idCardBackUrl', label: '身份证反面照', type: TYPE_IMAGE }, + { key: 'idCardValidStart', label: '身份证有效期起', type: TYPE_DATE }, + { + key: 'idCardValidEnd', + label: '身份证有效期止', + type: TYPE_DATE, + longTermKey: 'idCardLongTerm', + }, + { key: 'idCardLongTerm', label: '身份证长期有效', type: TYPE_LONG_TERM }, + { key: 'headPhoto', label: '大头照', type: TYPE_IMAGE }, + ], + }, + { + title: '驾驶证信息', + fields: [ + { key: 'driverFileNo', label: '驾驶证档案编号' }, + { key: 'driverClass', label: '准驾车型' }, + { key: 'driverLicenseFrontUrl', label: '驾驶证主页照片', type: TYPE_IMAGE }, + { key: 'driverLicenseBackUrl', label: '驾驶证副页照片', type: TYPE_IMAGE }, + { key: 'driverValidStart', label: '驾驶证有效期起', type: TYPE_DATE }, + { + key: 'driverValidEnd', + label: '驾驶证有效期止', + type: TYPE_DATE, + longTermKey: 'driverLongTerm', + }, + { key: 'driverLongTerm', label: '驾驶证长期有效', type: TYPE_LONG_TERM }, + ], + }, + { + title: '从业资格证信息', + fields: [ + { key: 'qualificationNo', label: '从业资格证号' }, + { key: 'qualificationUrl', label: '从业资格证照片', type: TYPE_IMAGE }, + { key: 'qualificationValidEnd', label: '从业资格证有效期止', type: TYPE_DATE }, + ], + }, + ], + /* + * ⚠️ 车辆字段名与 PersonCertSubmitRequest 不同:这一组曾经整组用着改造前的旧键名 + * (licenseNo / licenseValidEnd / roadTransportNo …),而快照早已改名成 drivingLicense* / + * roadTransportCert*。键名对不上**不报错**——每个字段都算「没有值」, + * 于是车辆审核详情里行驶证与道路运输证两组全显示「—」,真值全落到未翻译的「其他字段」组, + * 审核员既看不到内容、也 diff 不出变更。字段名一律以 VehicleCertSubmitRequest 为准。 + */ + vehicle: [ + { + title: '车辆基本信息', + fields: [ + { key: 'plateNo', label: '车牌号' }, + { key: 'vehicleType', label: '车辆类型' }, + { key: 'plateColor', label: '车牌颜色' }, + { key: 'ownerName', label: '所有人' }, + { key: 'vin', label: '车辆识别代号(车架号)' }, + { key: 'outerLength', label: '外廓长度(mm)' }, + { key: 'outerWidth', label: '外廓宽度(mm)' }, + { key: 'outerHeight', label: '外廓高度(mm)' }, + { key: 'approvedLoadKg', label: '核定载质量(KG)' }, + { key: 'tractionMassKg', label: '准牵引总质量(KG)' }, + { key: 'energyType', label: '能源类型' }, + { key: 'businessRelation', label: '业务关系' }, + ], + }, + { + title: '行驶证信息', + fields: [ + { key: 'compulsoryScrapDate', label: '强制报废日期', type: TYPE_DATE, longTermKey: 'compulsoryScrapLongTerm' }, + { key: 'compulsoryScrapLongTerm', label: '强制报废长期有效', type: TYPE_LONG_TERM }, + { key: 'drivingLicenseNo', label: '行驶证档案编号' }, + { key: 'drivingLicenseImage', label: '行驶证主页正面', type: TYPE_IMAGE }, + { key: 'drivingLicenseMainBack', label: '行驶证主页反面', type: TYPE_IMAGE }, + { key: 'drivingLicenseViceFront', label: '行驶证副页正面', type: TYPE_IMAGE }, + { key: 'drivingLicenseViceBack', label: '行驶证副页反面', type: TYPE_IMAGE }, + { key: 'drivingLicenseStartDate', label: '行驶证有效期起', type: TYPE_DATE }, + { + key: 'drivingLicenseEndDate', + label: '行驶证有效期止', + type: TYPE_DATE, + longTermKey: 'drivingLicenseLongTerm', + }, + { key: 'drivingLicenseLongTerm', label: '行驶证长期有效', type: TYPE_LONG_TERM }, + ], + }, + { + title: '道路运输证信息', + fields: [ + { key: 'roadTransportCertNo', label: '道路运输证号' }, + { key: 'roadTransportCertImage', label: '道路运输证照片', type: TYPE_IMAGE }, + { key: 'roadTransportCertStartDate', label: '道路运输证有效期起', type: TYPE_DATE }, + { + key: 'roadTransportCertEndDate', + label: '道路运输证有效期止', + type: TYPE_DATE, + longTermKey: 'roadTransportCertLongTerm', + }, + { key: 'roadTransportCertLongTerm', label: '道路运输证长期有效', type: TYPE_LONG_TERM }, + ], + }, + { + title: '机动车登记本信息', + fields: [ + { key: 'registrationNo', label: '机动车登记编号' }, + { key: 'registrationImage', label: '机动车登记本照片', type: TYPE_IMAGE }, + ], + }, + ], +}; + +/** 提交元信息与主键不参与对比(两侧必相同,展示出来只是噪音) */ +const IGNORED_KEYS = [ + 'id', + 'bizType', + 'bizId', + 'bizKey', + 'certificationStatus', + 'snapshotJson', + 'pendingSnapshotJson', + 'effectiveSnapshotJson', +]; + +/** + * 解析快照 JSON + * + * 后端把 null 序列化为空串,故空串 / 'null' 都按「没有快照」处理,不算解析失败。 + */ +const readSnapshot = json => { + if (json && typeof json === 'object') { + return { value: Object.keys(json).length ? json : null, invalid: false }; + } + const text = String(json === null || json === undefined ? '' : json).trim(); + if (!text || text === 'null') { + return { value: null, invalid: false }; + } + try { + const parsed = JSON.parse(text); + if (parsed && typeof parsed === 'object') { + return { value: parsed, invalid: false }; + } + return { value: null, invalid: true }; + } catch (error) { + return { value: null, invalid: true }; + } +}; + +/** 解析快照 JSON,无快照或解析失败时返回 null */ +export const parseSnapshot = json => readSnapshot(json).value; + +const isEmptyValue = value => value === null || value === undefined || value === ''; + +const isLongTermOn = value => value === 1 || value === '1' || value === true; + +/** 日期归一化到 yyyy-MM-dd,便于 `2026-01-01` 与 `2026-01-01T00:00:00` 判等 */ +const normalizeDate = value => { + if (isEmptyValue(value)) return ''; + const text = String(value).trim(); + const matched = text.match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})/); + if (matched) return `${matched[1]}-${pad(matched[2])}-${pad(matched[3])}`; + const date = isEpochText(text) ? toDate(text) : null; + if (date) { + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`; + } + return text; +}; + +/** 比较用的归一化取值:展示相同即视为未变更 */ +const canonicalOf = (snapshot, field) => { + const value = snapshot ? snapshot[field.key] : undefined; + if (field.type === TYPE_LONG_TERM) { + if (isEmptyValue(value)) return ''; + return isLongTermOn(value) ? '1' : '0'; + } + if (field.type === TYPE_DATE) { + if (field.longTermKey && isLongTermOn(snapshot && snapshot[field.longTermKey])) { + return '长期有效'; + } + return normalizeDate(value); + } + return isEmptyValue(value) ? '' : String(value).trim(); +}; + +/** 展示文案:日期型受「长期有效」影响,图片型返回 URL 由组件渲染缩略图 */ +const textOf = (snapshot, field) => { + const value = snapshot ? snapshot[field.key] : undefined; + if (field.type === TYPE_IMAGE) { + return isEmptyValue(value) ? '' : String(value); + } + if (field.type === TYPE_LONG_TERM) { + if (isEmptyValue(value)) return EMPTY_TEXT; + return isLongTermOn(value) ? '是' : '否'; + } + if (field.type === TYPE_DATE) { + if (field.longTermKey && isLongTermOn(snapshot && snapshot[field.longTermKey])) { + return '长期有效'; + } + return normalizeDate(value) || EMPTY_TEXT; + } + return isEmptyValue(value) ? EMPTY_TEXT : String(value); +}; + +const buildRow = (field, pendingSnapshot, effectiveSnapshot, hasBaseline) => { + const type = field.type || 'text'; + const target = { ...field, type }; + const pendingCanonical = canonicalOf(pendingSnapshot, target); + const effectiveCanonical = canonicalOf(effectiveSnapshot, target); + let changeType = ''; + if (hasBaseline && pendingCanonical !== effectiveCanonical) { + if (!effectiveCanonical) { + changeType = 'added'; + } else if (!pendingCanonical) { + changeType = 'cleared'; + } else { + changeType = 'modified'; + } + } + return { + key: field.key, + label: field.label, + type, + effective: { + raw: effectiveSnapshot ? effectiveSnapshot[field.key] : undefined, + text: textOf(effectiveSnapshot, target), + }, + pending: { + raw: pendingSnapshot ? pendingSnapshot[field.key] : undefined, + text: textOf(pendingSnapshot, target), + }, + changed: Boolean(changeType), + changeType, + }; +}; + +const unionKeys = (pendingSnapshot, effectiveSnapshot) => { + const keys = []; + [pendingSnapshot, effectiveSnapshot].forEach(snapshot => { + Object.keys(snapshot || {}).forEach(key => { + if (!keys.includes(key)) keys.push(key); + }); + }); + return keys; +}; + +/** + * 构造「待审快照 vs 当前生效值」的字段级 diff + * + * @param {string} bizType 认证对象类型:driver / vehicle + * @param {string} pendingJson 本次待审快照 JSON + * @param {string} effectiveJson 该对象当前生效快照 JSON,从未认证通过时为空 + * @returns {Object} { hasBaseline, groups, totalCount, changedCount, ... } + * hasBaseline 为 false 表示无对比基准(从未认证通过),此时不产出任何变更标记 + */ +export const buildSnapshotDiff = (bizType, pendingJson, effectiveJson) => { + const pending = readSnapshot(pendingJson); + const effective = readSnapshot(effectiveJson); + const hasBaseline = effective.value !== null; + const groupsConfig = CERT_SNAPSHOT_GROUPS[bizType] || []; + const knownKeys = []; + + const groups = groupsConfig.map(group => { + const rows = group.fields.map(field => { + knownKeys.push(field.key); + return buildRow(field, pending.value, effective.value, hasBaseline); + }); + return { + title: group.title, + rows, + changedCount: rows.filter(row => row.changed).length, + }; + }); + + // 元数据未登记的字段照样展示,避免后端新增字段时审核员看不到变化 + const extraKeys = unionKeys(pending.value, effective.value).filter( + key => !knownKeys.includes(key) && !IGNORED_KEYS.includes(key) + ); + if (extraKeys.length) { + const rows = extraKeys.map(key => + buildRow({ key, label: key }, pending.value, effective.value, hasBaseline) + ); + groups.push({ + title: '其他字段', + rows, + changedCount: rows.filter(row => row.changed).length, + }); + } + + const rows = groups.reduce((all, group) => all.concat(group.rows), []); + return { + hasBaseline, + pendingInvalid: pending.invalid, + effectiveInvalid: effective.invalid, + groups, + totalCount: rows.length, + changedCount: rows.filter(row => row.changed).length, + }; +}; diff --git a/src/views/transportCapacity/cert-audit-driver.vue b/src/views/transportCapacity/cert-audit-driver.vue new file mode 100644 index 0000000..c746b2b --- /dev/null +++ b/src/views/transportCapacity/cert-audit-driver.vue @@ -0,0 +1,30 @@ + + + diff --git a/src/views/transportCapacity/cert-audit-vehicle.vue b/src/views/transportCapacity/cert-audit-vehicle.vue new file mode 100644 index 0000000..3c3786a --- /dev/null +++ b/src/views/transportCapacity/cert-audit-vehicle.vue @@ -0,0 +1,31 @@ + + + diff --git a/src/views/transportCapacity/components/cert-audit-detail/main.vue b/src/views/transportCapacity/components/cert-audit-detail/main.vue new file mode 100644 index 0000000..825f665 --- /dev/null +++ b/src/views/transportCapacity/components/cert-audit-detail/main.vue @@ -0,0 +1,279 @@ + + + + + diff --git a/src/views/transportCapacity/components/cert-audit-diff/main.vue b/src/views/transportCapacity/components/cert-audit-diff/main.vue new file mode 100644 index 0000000..1137418 --- /dev/null +++ b/src/views/transportCapacity/components/cert-audit-diff/main.vue @@ -0,0 +1,246 @@ + + + + + diff --git a/src/views/transportCapacity/components/cert-audit-list/main.vue b/src/views/transportCapacity/components/cert-audit-list/main.vue new file mode 100644 index 0000000..4c66cdc --- /dev/null +++ b/src/views/transportCapacity/components/cert-audit-list/main.vue @@ -0,0 +1,196 @@ + + + + + diff --git a/src/views/transportCapacity/driver.vue b/src/views/transportCapacity/driver.vue index 1066ad6..dad42da 100644 --- a/src/views/transportCapacity/driver.vue +++ b/src/views/transportCapacity/driver.vue @@ -649,6 +649,7 @@ import { getDictionary } from '@/api/system/dictbiz'; import { getLazyTree } from '@/api/base/region'; import { exportBlob } from '@/api/common'; import { getToken, isAdministrator } from '@/utils/auth'; +import { certStatusTag } from '@/utils/cert-audit'; import { getUploadHeaders } from '@/utils/upload'; import { downloadXls } from '@/utils/util'; import ImageUploadField from '@/components/image-upload-field/main.vue'; @@ -1233,13 +1234,9 @@ export default { this.driverForm.drivingVehicle = row.plateNo || ''; this.vehicleSelectorVisible = false; }, + // 车辆认证状态展示的是「最新一次提交的状态」缓存,与审核页使用同一套状态词表 getVehicleCertificationStatus(row) { - const statusMap = { - 0: { label: '认证中', type: 'warning' }, - 1: { label: '已认证', type: 'success' }, - 2: { label: '已驳回', type: 'danger' }, - }; - return statusMap[row.certificationStatus] || { label: '-', type: 'info' }; + return certStatusTag(row.certificationStatus); }, setImage(prop, url) { this.driverForm[prop] = url; diff --git a/src/views/transportCapacity/vehicle.vue b/src/views/transportCapacity/vehicle.vue index 1bfa225..42553a2 100644 --- a/src/views/transportCapacity/vehicle.vue +++ b/src/views/transportCapacity/vehicle.vue @@ -105,9 +105,6 @@ 修改 综合台账 - - 审核 - - - - - - - - {{ auditValue('plateNo') }} - - - - - {{ auditValue('vehicleType') }} - - - - -
-
- 长 - - {{ auditValueWithUnit('outerLength', 'mm') }} - -
-
- 宽 - - {{ auditValueWithUnit('outerWidth', 'mm') }} - -
-
- 高 - - {{ auditValueWithUnit('outerHeight', 'mm') }} - -
-
-
-
-
- - - - - {{ auditValueWithUnit('approvedLoadKg', 'KG') }} - - - - - - - {{ auditValueWithUnit('tractionMassKg', 'KG') }} - - - - - - {{ auditValue('businessRelation') }} - - - - - {{ auditValue('energyType') }} - - - - - - - - {{ auditValue('compulsoryScrapDate') }} - - - - - - {{ auditValue('customsRecordNo') }} - - - - - - {{ auditValue('organizationName') }} - - - - - - {{ auditValue('useDepartment') }} - - - -
- - - - - - - {{ auditValue('drivingLicenseEndDate') }} - - - - - - - {{ auditValue('roadTransportCertNo') }} - - - - - - - {{ auditValue('roadTransportCertEndDate') }} - - - - - - - {{ auditValue('annualReviewEndDate') }} - - - - - - - - {{ auditValue('registrationNo') }} - - - - - {{ auditValue('registrationDate') }} - - - - - {{ auditValue('drivingLicenseNo') }} - - - - - - {{ auditValue('drivingLicenseStartDate') }} - - - - - - - - - {{ auditValue('roadTransportCertStartDate') }} - - - - - - - - {{ auditValue('remark') }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - -
- + + + + + + + + + +
@@ -809,7 +559,6 @@ import { submit, remove, changeStatus, - auditCertification, getExpiryStat, recognitionTransportCertificates, recognizeBaiduOcr, @@ -818,6 +567,7 @@ import { getDeptTree } from '@/api/system/dept'; import organizationSearch from '@/mixins/organization-search'; import { exportBlob } from '@/api/common'; import { getToken, isAdministrator } from '@/utils/auth'; +import { certStatusTag } from '@/utils/cert-audit'; import { getUploadHeaders } from '@/utils/upload'; import { downloadXls } from '@/utils/util'; import ImageUploadField from '@/components/image-upload-field/main.vue'; @@ -828,8 +578,10 @@ const emptyForm = () => ({ organizationName: '', useDepartment: '', plateNo: '', + ownerName: '', plateColor: '', vehicleType: '', + vin: '', outerLength: undefined, outerWidth: undefined, outerHeight: undefined, @@ -895,10 +647,6 @@ export default { vehicleBox: false, vehicleLedgerBox: false, ledgerVehicle: {}, - certificationAuditBox: false, - certificationAuditForm: {}, - certificationRejectReason: '', - showRejectReason: false, readonly: false, vehicleForm: emptyForm(), vehicleCertificateUploads: {}, @@ -922,6 +670,8 @@ export default { { validator: this.validatePlateNo, trigger: 'change' }, ], vehicleType: [{ required: true, message: '请选择车辆类型', trigger: 'change' }], + ownerName: [{ required: true, message: '请输入所有人', trigger: 'blur' }], + vin: [{ validator: this.validateVin, trigger: 'blur' }], approvedLoadKg: [{ required: true, message: '请输入核定载质量', trigger: 'blur' }], businessRelation: [{ required: true, message: '请选择业务关系', trigger: 'change' }], energyType: [{ required: true, message: '请选择能源类型', trigger: 'change' }], @@ -1055,6 +805,24 @@ export default { } callback(); }, + // 车辆识别代号(车架号)为选填:留空放行,填了就必须是 17 位 + validateVin(rule, value, callback) { + const vin = String(value || '') + .trim() + .toUpperCase(); + if (!vin) { + callback(); + return; + } + if (!/^[A-HJ-NPR-Z0-9]{17}$/.test(vin)) { + callback(new Error('车架号必须是17位字母数字(不含 I、O、Q)')); + return; + } + if (this.vehicleForm.vin !== vin) { + this.vehicleForm.vin = vin; + } + callback(); + }, validateAnnualReviewEndDate(rule, value, callback) { if (this.vehicleForm.annualReviewLongTerm !== 1 && !value) { callback(new Error('请选择道路运输年审有效期')); @@ -1107,49 +875,6 @@ export default { this.ledgerVehicle = { ...row }; this.vehicleLedgerBox = true; }, - openCertificationAudit(row) { - getDetail(row.id).then(res => { - this.certificationAuditForm = { ...(res.data.data || {}) }; - this.certificationRejectReason = ''; - this.showRejectReason = false; - this.certificationAuditBox = true; - }); - }, - auditValue(prop) { - // 长期有效字段:勾选后显示「长期有效」,避免与未填写的「-」混淆 - const longTermMap = { - drivingLicenseEndDate: 'drivingLicenseLongTerm', - roadTransportCertEndDate: 'roadTransportCertLongTerm', - annualReviewEndDate: 'annualReviewLongTerm', - compulsoryScrapDate: 'compulsoryScrapLongTerm', - }; - const longTermProp = longTermMap[prop]; - if (longTermProp && this.certificationAuditForm[longTermProp] === 1) { - return '长期有效'; - } - const value = this.certificationAuditForm[prop]; - return value === undefined || value === null || value === '' ? '-' : value; - }, - auditValueWithUnit(prop, unit) { - const value = this.auditValue(prop); - return value === '-' ? value : `${value}${unit}`; - }, - handleCertificationApprove() { - auditCertification(this.certificationAuditForm.id, 1).then(() => { - this.$message.success('认证已通过'); - this.certificationAuditBox = false; - this.onLoad(this.page); - }); - }, - handleCertificationReject() { - if (!this.showRejectReason) { this.showRejectReason = true; return; } - if (!this.certificationRejectReason.trim()) { this.$message.warning('请输入认证驳回原因'); return; } - auditCertification(this.certificationAuditForm.id, 2, this.certificationRejectReason).then(() => { - this.$message.success('认证已驳回'); - this.certificationAuditBox = false; - this.onLoad(this.page); - }); - }, resetVehicle() { this.vehicleForm = emptyForm(); this.vehicleCertificateUploads = {}; @@ -1648,26 +1373,11 @@ export default { } return ''; }, + // 认证状态展示的是「最新一次提交的状态」缓存,不代表该车当前是否已认证 + // (是否已认证由生效快照指针决定,即曾审核通过即永久保持)。 + // 证书是否临近到期另有列表顶部的到期标签与日期列标色展示,不在这里推断。 getCertificationStatus(row) { - if (row.certificationStatus === 0) return { label: '认证中', type: 'warning' }; - if (row.certificationStatus === 1) return { label: '已认证', type: 'success' }; - if (row.certificationStatus === 2) return { label: '已驳回', type: 'danger' }; - const today = this.$dayjs().startOf('day'); - const certificateDates = [ - [row.drivingLicenseEndDate, row.drivingLicenseLongTerm], - [row.roadTransportCertEndDate, row.roadTransportCertLongTerm], - ]; - if (certificateDates.some(([date, longTerm]) => longTerm !== 1 && !date)) { - return { label: '未认证', type: 'info' }; - } - if ( - certificateDates.some( - ([date, longTerm]) => longTerm !== 1 && this.$dayjs(date).isBefore(today, 'day') - ) - ) { - return { label: '已过期', type: 'danger' }; - } - return { label: '已认证', type: 'success' }; + return certStatusTag(row.certificationStatus); }, }, }; @@ -1695,9 +1405,7 @@ export default { } } -// 车辆认证审核弹窗:与编辑弹窗一致,灰底 + section-card 白卡分组 -.certification-audit, -.certification-audit__reason, +// 编辑弹窗:灰底 + section-card 白卡分组 .vehicle-form { :deep(.el-form-item__label) { height: auto; @@ -1723,60 +1431,10 @@ export default { line-height: 18px; margin-right: 4px; } -} -.certification-audit { - :deep(.el-form-item__label) { - flex: 0 0 calc(4em + 12px) !important; - width: calc(4em + 12px) !important; - max-width: calc(4em + 12px); - } -} - -.certification-audit__reason { :deep(.el-form-item__label) { flex: 0 0 calc(4em + 24px) !important; width: calc(4em + 24px) !important; - max-width: calc(4em + 24px); - } -} - -.vehicle-form { - :deep(.el-form-item__label) { - flex: 0 0 calc(4em + 24px) !important; - width: calc(4em + 24px) !important; - } -} - -.certification-audit { - &__value { - display: block; - color: #303133; - word-break: break-all; - } - - // 外廓尺寸长/宽/高组合,排版复用编辑弹窗的 dimension-group - .dimension-group { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 8px; - align-items: center; - } - - .dimension-group__item { - display: flex; - align-items: center; - gap: 8px; - min-width: 0; - } - - .dimension-group__label { - flex-shrink: 0; - color: #303133; - } - - &__reason { - margin-top: 12px; } }