🔀 合并认证审核页面分支到 master
新增认证审核-司机/车辆页(按字段级 diff 对比待审值与生效值)、 审核列表待审核/已通过/已驳回页签、司机审核补手机号、 车辆表单补所有人与人车辆识别代号,并移除旧认证审核入口。
This commit is contained in:
@@ -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 } : {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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 => {
|
export const getExpiryStat = params => {
|
||||||
return request({
|
return request({
|
||||||
url: '/blade-transport/transport-vehicle/expiry-stat',
|
url: '/blade-transport/transport-vehicle/expiry-stat',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -95,6 +95,8 @@ export const option = {
|
|||||||
align: 'center',
|
align: 'center',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
// 这里是「最新一次提交的状态」缓存,不等于该车当前是否已认证:
|
||||||
|
// 已认证由生效快照指针决定,曾审核通过即永久保持,重新提交不会让它退回审核中
|
||||||
label: '认证状态',
|
label: '认证状态',
|
||||||
prop: 'certificationStatus',
|
prop: 'certificationStatus',
|
||||||
slot: true,
|
slot: true,
|
||||||
@@ -104,9 +106,10 @@ export const option = {
|
|||||||
type: 'select',
|
type: 'select',
|
||||||
dicData: [
|
dicData: [
|
||||||
{ label: '全部', value: '' },
|
{ label: '全部', value: '' },
|
||||||
{ label: '认证中', value: 0 },
|
{ label: '审核中', value: 0 },
|
||||||
{ label: '已认证', value: 1 },
|
{ label: '已通过', value: 1 },
|
||||||
{ label: '已驳回', value: 2 },
|
{ label: '已驳回', value: 2 },
|
||||||
|
{ label: '已作废', value: 3 },
|
||||||
],
|
],
|
||||||
minWidth: 110,
|
minWidth: 110,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<cert-audit-list
|
||||||
|
:option="option"
|
||||||
|
biz-type="driver"
|
||||||
|
audit-permission-code="cert_certification_audit_driver_audit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { option } from '@/option/transportCapacity/cert-audit-driver';
|
||||||
|
import CertAuditList from './components/cert-audit-list/main.vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证审核-司机
|
||||||
|
*
|
||||||
|
* 菜单挂在运力管理下(path: /transportCapacity/cert-audit-driver),
|
||||||
|
* 列表与审核详情由通用组件承接,本页只固定认证对象类型与审核权限点。
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
name: 'CertAuditDriver',
|
||||||
|
components: {
|
||||||
|
CertAuditList,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
option,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<cert-audit-list
|
||||||
|
:option="option"
|
||||||
|
biz-type="vehicle"
|
||||||
|
audit-permission-code="cert_certification_audit_vehicle_audit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { option } from '@/option/transportCapacity/cert-audit-vehicle';
|
||||||
|
import CertAuditList from './components/cert-audit-list/main.vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证审核-车辆
|
||||||
|
*
|
||||||
|
* 菜单挂在运力管理下(path: /transportCapacity/cert-audit-vehicle),
|
||||||
|
* 车辆认证审核已从「车辆管理」页迁到本页:审核对象由「车辆行」改为「认证记录」,
|
||||||
|
* 生效值与待审值分离后,原「看当前值直接通过/驳回」的模型不再成立。
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
name: 'CertAuditVehicle',
|
||||||
|
components: {
|
||||||
|
CertAuditList,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
option,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visible"
|
||||||
|
:title="dialogTitle"
|
||||||
|
width="92%"
|
||||||
|
top="4vh"
|
||||||
|
append-to-body
|
||||||
|
class="cert-audit-detail-dialog"
|
||||||
|
@closed="handleClosed"
|
||||||
|
>
|
||||||
|
<div v-loading="loading" class="cert-audit-detail">
|
||||||
|
<el-alert
|
||||||
|
v-if="loadFailed"
|
||||||
|
type="error"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
title="认证记录详情加载失败,请刷新后重试"
|
||||||
|
class="cert-audit-detail__notice"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<section-card title="提交信息">
|
||||||
|
<el-descriptions :column="3" border size="small">
|
||||||
|
<el-descriptions-item label="认证对象">
|
||||||
|
{{ detail.bizKey || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="审核状态">
|
||||||
|
<el-tag :type="statusTag.type" size="small">
|
||||||
|
{{ detail.statusText || statusTag.label }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="该对象当前是否已认证">
|
||||||
|
<el-tag :type="detail.certified ? 'success' : 'info'" size="small">
|
||||||
|
{{ detail.certified ? '已认证' : '未认证' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="提交人">
|
||||||
|
{{ detail.submitUserName || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="提交时间">
|
||||||
|
{{ formatCertDateTime(detail.submitTime) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="认证类型">
|
||||||
|
{{ bizTypeLabel }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</section-card>
|
||||||
|
|
||||||
|
<section-card v-if="!loading && !loadFailed" title="待审内容 vs 当前生效值">
|
||||||
|
<cert-audit-diff
|
||||||
|
:key="diffKey"
|
||||||
|
:biz-type="bizType"
|
||||||
|
:pending-json="detail.pendingSnapshotJson"
|
||||||
|
:effective-json="detail.effectiveSnapshotJson"
|
||||||
|
/>
|
||||||
|
</section-card>
|
||||||
|
|
||||||
|
<section-card title="审核意见">
|
||||||
|
<el-form label-position="right" label-width="90px" class="cert-audit-detail__form">
|
||||||
|
<el-form-item label="驳回原因">
|
||||||
|
<el-input
|
||||||
|
v-model="rejectReason"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
maxlength="200"
|
||||||
|
show-word-limit
|
||||||
|
:disabled="!auditable"
|
||||||
|
placeholder="驳回时必填,不超过 200 字;通过时无需填写"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</section-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="visible = false">取消</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
:disabled="!auditable || loadFailed"
|
||||||
|
:loading="rejectLoading"
|
||||||
|
@click="handleReject"
|
||||||
|
>
|
||||||
|
驳回
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:disabled="!auditable || loadFailed"
|
||||||
|
:loading="approveLoading"
|
||||||
|
@click="handleApprove"
|
||||||
|
>
|
||||||
|
通过
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getAuditDetail, auditCertification } from '@/api/transportCapacity/certification-audit';
|
||||||
|
import { CERT_RECORD_STATUS, certStatusTag, formatCertDateTime } from '@/utils/cert-audit';
|
||||||
|
import CertAuditDiff from '../cert-audit-diff/main.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CertAuditDetail',
|
||||||
|
components: {
|
||||||
|
CertAuditDiff,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
// 认证记录主键
|
||||||
|
recordId: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
// 认证对象类型:driver / vehicle
|
||||||
|
bizType: {
|
||||||
|
type: String,
|
||||||
|
default: 'driver',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue', 'success'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
loadFailed: false,
|
||||||
|
detail: {},
|
||||||
|
rejectReason: '',
|
||||||
|
approveLoading: false,
|
||||||
|
rejectLoading: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
visible: {
|
||||||
|
get() {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit('update:modelValue', value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bizTypeLabel() {
|
||||||
|
return this.bizType === 'vehicle' ? '车辆认证' : '司机认证';
|
||||||
|
},
|
||||||
|
dialogTitle() {
|
||||||
|
const bizKey = this.detail.bizKey ? `:${this.detail.bizKey}` : '';
|
||||||
|
return `${this.bizTypeLabel}审核${bizKey}`;
|
||||||
|
},
|
||||||
|
statusTag() {
|
||||||
|
return certStatusTag(this.detail.status);
|
||||||
|
},
|
||||||
|
// 只有「审核中」的记录可审;已通过/已驳回/已作废都是终态
|
||||||
|
auditable() {
|
||||||
|
return String(this.detail.status) === String(CERT_RECORD_STATUS.PENDING);
|
||||||
|
},
|
||||||
|
diffKey() {
|
||||||
|
return `${this.detail.id || this.recordId || ''}-${this.detail.submitTime || ''}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
modelValue(value) {
|
||||||
|
if (value) {
|
||||||
|
this.resetState();
|
||||||
|
this.loadDetail();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatCertDateTime,
|
||||||
|
resetState() {
|
||||||
|
this.detail = {};
|
||||||
|
this.rejectReason = '';
|
||||||
|
this.loadFailed = false;
|
||||||
|
this.approveLoading = false;
|
||||||
|
this.rejectLoading = false;
|
||||||
|
},
|
||||||
|
loadDetail() {
|
||||||
|
if (!this.recordId) {
|
||||||
|
this.loadFailed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
getAuditDetail(this.recordId)
|
||||||
|
.then(res => {
|
||||||
|
this.detail = res.data.data || {};
|
||||||
|
this.loadFailed = false;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.detail = {};
|
||||||
|
this.loadFailed = true;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleClosed() {
|
||||||
|
this.resetState();
|
||||||
|
},
|
||||||
|
// 通过:只提交是否通过,不带驳回原因
|
||||||
|
handleApprove() {
|
||||||
|
const bizKey = this.detail.bizKey || '该认证对象';
|
||||||
|
this.$confirm(`通过后本次提交将立即成为「${bizKey}」的当前生效值,是否确认通过?`, {
|
||||||
|
confirmButtonText: '确认通过',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.approveLoading = true;
|
||||||
|
return auditCertification(this.detail.id, true);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.$message.success('认证已通过');
|
||||||
|
this.visible = false;
|
||||||
|
this.$emit('success');
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => {
|
||||||
|
this.approveLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// 驳回:原因必填且不超过 200 字,前端先拦一道
|
||||||
|
handleReject() {
|
||||||
|
const rejectReason = String(this.rejectReason || '').trim();
|
||||||
|
if (!rejectReason) {
|
||||||
|
this.$message.warning('请输入驳回原因');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (rejectReason.length > 200) {
|
||||||
|
this.$message.warning('驳回原因不能超过 200 字');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bizKey = this.detail.bizKey || '该认证对象';
|
||||||
|
this.$confirm(
|
||||||
|
`驳回后本次提交的内容不会生效,「${bizKey}」的当前生效值保持不变,是否确认驳回?`,
|
||||||
|
{
|
||||||
|
confirmButtonText: '确认驳回',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
this.rejectLoading = true;
|
||||||
|
return auditCertification(this.detail.id, false, rejectReason);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.$message.success('认证已驳回');
|
||||||
|
this.visible = false;
|
||||||
|
this.$emit('success');
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => {
|
||||||
|
this.rejectLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.cert-audit-detail {
|
||||||
|
&__notice {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.section-card) {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-descriptions__label) {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__form {
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
<template>
|
||||||
|
<div class="cert-audit-diff">
|
||||||
|
<el-alert
|
||||||
|
v-if="diff.effectiveInvalid"
|
||||||
|
type="error"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
title="当前生效快照内容无法解析,本次不展示对比结果"
|
||||||
|
class="cert-audit-diff__notice"
|
||||||
|
/>
|
||||||
|
<el-alert
|
||||||
|
v-else-if="!diff.hasBaseline"
|
||||||
|
type="warning"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
title="该认证对象从未审核通过,没有生效快照可对比(无对比基准)"
|
||||||
|
class="cert-audit-diff__notice"
|
||||||
|
>
|
||||||
|
下方为本次提交的全部内容,审核通过后即成为该对象的当前生效值。
|
||||||
|
</el-alert>
|
||||||
|
<el-alert
|
||||||
|
v-else-if="diff.changedCount === 0"
|
||||||
|
type="success"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
title="本次提交内容与当前生效值完全一致,没有任何字段发生变化"
|
||||||
|
class="cert-audit-diff__notice"
|
||||||
|
/>
|
||||||
|
<el-alert
|
||||||
|
v-else
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
:title="`共 ${diff.changedCount} / ${diff.totalCount} 项内容与当前生效值不同,已在下方高亮标记`"
|
||||||
|
class="cert-audit-diff__notice"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="cert-audit-diff__toolbar">
|
||||||
|
<el-switch
|
||||||
|
v-model="onlyChanged"
|
||||||
|
:disabled="!diff.hasBaseline"
|
||||||
|
active-text="仅看变更项"
|
||||||
|
inactive-text="展示全部"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section-card
|
||||||
|
v-for="group in visibleGroups"
|
||||||
|
:key="group.title"
|
||||||
|
:title="group.title"
|
||||||
|
class="cert-audit-diff__group"
|
||||||
|
>
|
||||||
|
<template #extra>
|
||||||
|
<span v-if="diff.hasBaseline" class="cert-audit-diff__extra">
|
||||||
|
变更 {{ group.changedCount }} 项
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<el-table
|
||||||
|
:data="group.rows"
|
||||||
|
border
|
||||||
|
size="small"
|
||||||
|
:row-class-name="rowClassName"
|
||||||
|
class="cert-audit-diff__table"
|
||||||
|
>
|
||||||
|
<el-table-column label="字段" prop="label" width="190" />
|
||||||
|
<el-table-column :label="effectiveColumnLabel" min-width="240">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-image
|
||||||
|
v-if="diff.hasBaseline && row.type === 'image' && row.effective.raw"
|
||||||
|
:src="row.effective.raw"
|
||||||
|
:preview-src-list="[row.effective.raw]"
|
||||||
|
preview-teleported
|
||||||
|
:z-index="3000"
|
||||||
|
fit="contain"
|
||||||
|
class="cert-audit-diff__image"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-else-if="diff.hasBaseline && row.type !== 'image'"
|
||||||
|
class="cert-audit-diff__value"
|
||||||
|
>
|
||||||
|
{{ row.effective.text }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="cert-audit-diff__absent">—</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="待审提交值" min-width="240">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-image
|
||||||
|
v-if="row.type === 'image' && row.pending.raw"
|
||||||
|
:src="row.pending.raw"
|
||||||
|
:preview-src-list="[row.pending.raw]"
|
||||||
|
preview-teleported
|
||||||
|
:z-index="3000"
|
||||||
|
fit="contain"
|
||||||
|
class="cert-audit-diff__image"
|
||||||
|
/>
|
||||||
|
<span v-else-if="row.type === 'image'" class="cert-audit-diff__absent">—</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="cert-audit-diff__value"
|
||||||
|
:class="{ 'cert-audit-diff__value--changed': row.changed }"
|
||||||
|
>
|
||||||
|
{{ row.pending.text }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="差异" width="96" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.changeType" :type="changeTag(row.changeType).type" size="small">
|
||||||
|
{{ changeTag(row.changeType).label }}
|
||||||
|
</el-tag>
|
||||||
|
<span v-else class="cert-audit-diff__absent">—</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</section-card>
|
||||||
|
|
||||||
|
<el-empty v-if="!visibleGroups.length" :description="emptyText" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { buildSnapshotDiff, CERT_CHANGE_TAG } from '@/utils/cert-audit';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CertAuditDiff',
|
||||||
|
props: {
|
||||||
|
// 认证对象类型:driver / vehicle
|
||||||
|
bizType: {
|
||||||
|
type: String,
|
||||||
|
default: 'driver',
|
||||||
|
},
|
||||||
|
// 本次待审快照 JSON
|
||||||
|
pendingJson: {
|
||||||
|
type: [String, Object],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
// 该对象当前生效快照 JSON,从未认证通过时为空
|
||||||
|
effectiveJson: {
|
||||||
|
type: [String, Object],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// 默认并排展示两侧全部字段(差异行高亮),审核员可切换为只看变更项
|
||||||
|
onlyChanged: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
diff() {
|
||||||
|
return buildSnapshotDiff(this.bizType, this.pendingJson, this.effectiveJson);
|
||||||
|
},
|
||||||
|
visibleGroups() {
|
||||||
|
// 无对比基准时不存在「变更项」,强制展示全部内容
|
||||||
|
const onlyChanged = this.onlyChanged && this.diff.hasBaseline;
|
||||||
|
return this.diff.groups
|
||||||
|
.map(group => ({
|
||||||
|
...group,
|
||||||
|
rows: onlyChanged ? group.rows.filter(row => row.changed) : group.rows,
|
||||||
|
}))
|
||||||
|
.filter(group => group.rows.length);
|
||||||
|
},
|
||||||
|
emptyText() {
|
||||||
|
if (this.diff.hasBaseline && this.onlyChanged && this.diff.changedCount === 0) {
|
||||||
|
return '本次提交与当前生效值完全一致,切换为「展示全部」可查看明细';
|
||||||
|
}
|
||||||
|
return '本次提交没有可对比的内容';
|
||||||
|
},
|
||||||
|
// 无对比基准时在列头直接写明,避免逐行重复「无对比基准」
|
||||||
|
effectiveColumnLabel() {
|
||||||
|
return this.diff.hasBaseline ? '当前生效值' : '当前生效值(无对比基准)';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
pendingJson() {
|
||||||
|
this.onlyChanged = false;
|
||||||
|
},
|
||||||
|
effectiveJson() {
|
||||||
|
this.onlyChanged = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
changeTag(changeType) {
|
||||||
|
return CERT_CHANGE_TAG[changeType] || { label: changeType, type: 'info' };
|
||||||
|
},
|
||||||
|
rowClassName({ row }) {
|
||||||
|
return row.changed ? 'cert-audit-diff__row--changed' : '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.cert-audit-diff {
|
||||||
|
&__notice {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 0 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__group {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__extra {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__value {
|
||||||
|
word-break: break-all;
|
||||||
|
|
||||||
|
&--changed {
|
||||||
|
color: #e6a23c;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__absent {
|
||||||
|
color: #c0c4cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__image {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border: 1px solid #eff1f7;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__table {
|
||||||
|
:deep(.el-table__row.cert-audit-diff__row--changed) {
|
||||||
|
background-color: #fdf6ec;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__row.cert-audit-diff__row--changed td.el-table__cell) {
|
||||||
|
background-color: #fdf6ec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
<template>
|
||||||
|
<basic-container class="cert-audit-list-page">
|
||||||
|
<avue-crud
|
||||||
|
:option="option"
|
||||||
|
:table-loading="loading"
|
||||||
|
:data="data"
|
||||||
|
v-model:page="page"
|
||||||
|
ref="crud"
|
||||||
|
@search-change="searchChange"
|
||||||
|
@search-reset="searchReset"
|
||||||
|
@current-change="currentChange"
|
||||||
|
@size-change="sizeChange"
|
||||||
|
@refresh-change="refreshChange"
|
||||||
|
@on-load="onLoad"
|
||||||
|
>
|
||||||
|
<!-- 审核状态分页签:审核对象每次提交都产生一条记录,故按记录状态分类 -->
|
||||||
|
<template #header>
|
||||||
|
<el-tabs
|
||||||
|
v-model="activeStatus"
|
||||||
|
class="cert-audit-list-page__tabs"
|
||||||
|
@tab-change="handleStatusChange"
|
||||||
|
>
|
||||||
|
<el-tab-pane
|
||||||
|
v-for="tab in statusTabs"
|
||||||
|
:key="tab.value"
|
||||||
|
:label="tab.label"
|
||||||
|
:name="tab.value"
|
||||||
|
/>
|
||||||
|
</el-tabs>
|
||||||
|
</template>
|
||||||
|
<template #statusText="{ row }">
|
||||||
|
<el-tag :type="certStatusTag(row.status).type" class="status-text">
|
||||||
|
{{ row.statusText || certStatusTag(row.status).label }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
<template #submitTime="{ row }">
|
||||||
|
{{ formatCertDateTime(row.submitTime) }}
|
||||||
|
</template>
|
||||||
|
<template #menu="{ row }">
|
||||||
|
<!-- 只有审核中的记录能审;已通过/已驳回只能查看,弹窗内也会禁用审核按钮 -->
|
||||||
|
<el-link type="primary" v-if="canAudit" @click="openDetail(row)">
|
||||||
|
{{ isPending(row) ? '审核' : '查看' }}
|
||||||
|
</el-link>
|
||||||
|
</template>
|
||||||
|
</avue-crud>
|
||||||
|
|
||||||
|
<cert-audit-detail
|
||||||
|
v-model="detailVisible"
|
||||||
|
:record-id="detailRecordId"
|
||||||
|
:biz-type="bizType"
|
||||||
|
@success="handleAuditSuccess"
|
||||||
|
/>
|
||||||
|
</basic-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { getPendingPage } from '@/api/transportCapacity/certification-audit';
|
||||||
|
import { CERT_RECORD_STATUS, certStatusTag, formatCertDateTime } from '@/utils/cert-audit';
|
||||||
|
import { isAdministrator } from '@/utils/auth';
|
||||||
|
import CertAuditDetail from '../cert-audit-detail/main.vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证记录列表(司机 / 车辆共用)
|
||||||
|
*
|
||||||
|
* 审核对象是「一条认证记录」:每次提交产生一条,按审核状态分为
|
||||||
|
* 待审核 / 已通过 / 已驳回三个页签,默认停在待审核。
|
||||||
|
* 列表只负责挑选记录,具体改了什么由审核详情弹窗做字段级对比。
|
||||||
|
*
|
||||||
|
* 列表上的司机姓名与手机号取的是**司机档案当前值**(认证记录里的姓名是提交时的历史值,
|
||||||
|
* 手机号则从来不在快照里)。审核员靠这两列认人、联系核实,故不脱敏。
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
name: 'CertAuditList',
|
||||||
|
components: {
|
||||||
|
CertAuditDetail,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
// Avue 列表配置,按认证对象类型分别放在 src/option/transportCapacity/ 下
|
||||||
|
option: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
// 认证对象类型:driver / vehicle,由列表页固定传入
|
||||||
|
bizType: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
// 审核动作权限点,如 cert_certification_audit_driver_audit
|
||||||
|
auditPermissionCode: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: true,
|
||||||
|
data: [],
|
||||||
|
query: {},
|
||||||
|
page: {
|
||||||
|
pageSize: 10,
|
||||||
|
currentPage: 1,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
// 当前页签对应的审核状态,默认待审核
|
||||||
|
activeStatus: CERT_RECORD_STATUS.PENDING,
|
||||||
|
statusTabs: [
|
||||||
|
{ label: '待审核', value: CERT_RECORD_STATUS.PENDING },
|
||||||
|
{ label: '已通过', value: CERT_RECORD_STATUS.APPROVED },
|
||||||
|
{ label: '已驳回', value: CERT_RECORD_STATUS.REJECTED },
|
||||||
|
],
|
||||||
|
detailVisible: false,
|
||||||
|
detailRecordId: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['permission', 'userInfo']),
|
||||||
|
isAdmin() {
|
||||||
|
return isAdministrator(this.userInfo);
|
||||||
|
},
|
||||||
|
canAudit() {
|
||||||
|
return this.isAdmin || this.permission?.[this.auditPermissionCode] === true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
certStatusTag,
|
||||||
|
formatCertDateTime,
|
||||||
|
isPending(row) {
|
||||||
|
return String(row.status) === String(CERT_RECORD_STATUS.PENDING);
|
||||||
|
},
|
||||||
|
onLoad(page, params = {}) {
|
||||||
|
this.loading = true;
|
||||||
|
getPendingPage(this.bizType, page.currentPage, page.pageSize, {
|
||||||
|
...this.query,
|
||||||
|
...params,
|
||||||
|
status: this.activeStatus,
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
const data = res.data.data || {};
|
||||||
|
this.page.total = data.total || 0;
|
||||||
|
this.data = data.records || [];
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// 切页签等于换一批数据,必须回到第一页,否则会停在上一页签的页码上
|
||||||
|
handleStatusChange() {
|
||||||
|
this.page.currentPage = 1;
|
||||||
|
this.onLoad(this.page, this.query);
|
||||||
|
},
|
||||||
|
searchChange(params, done) {
|
||||||
|
this.query = params;
|
||||||
|
this.page.currentPage = 1;
|
||||||
|
this.onLoad(this.page, params);
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
searchReset() {
|
||||||
|
this.query = {};
|
||||||
|
this.page.currentPage = 1;
|
||||||
|
this.onLoad(this.page);
|
||||||
|
},
|
||||||
|
currentChange(currentPage) {
|
||||||
|
this.page.currentPage = currentPage;
|
||||||
|
},
|
||||||
|
sizeChange(pageSize) {
|
||||||
|
this.page.pageSize = pageSize;
|
||||||
|
},
|
||||||
|
refreshChange() {
|
||||||
|
this.onLoad(this.page, this.query);
|
||||||
|
},
|
||||||
|
openDetail(row) {
|
||||||
|
this.detailRecordId = row.id;
|
||||||
|
this.detailVisible = true;
|
||||||
|
},
|
||||||
|
// 审核后该记录已是终态,重新拉取当前页签的列表
|
||||||
|
handleAuditSuccess() {
|
||||||
|
this.onLoad(this.page, this.query);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.cert-audit-list-page {
|
||||||
|
&__tabs {
|
||||||
|
:deep(.el-tabs__header) {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tabs__nav-wrap::after) {
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -649,6 +649,7 @@ import { getDictionary } from '@/api/system/dictbiz';
|
|||||||
import { getLazyTree } from '@/api/base/region';
|
import { getLazyTree } from '@/api/base/region';
|
||||||
import { exportBlob } from '@/api/common';
|
import { exportBlob } from '@/api/common';
|
||||||
import { getToken, isAdministrator } from '@/utils/auth';
|
import { getToken, isAdministrator } from '@/utils/auth';
|
||||||
|
import { certStatusTag } from '@/utils/cert-audit';
|
||||||
import { getUploadHeaders } from '@/utils/upload';
|
import { getUploadHeaders } from '@/utils/upload';
|
||||||
import { downloadXls } from '@/utils/util';
|
import { downloadXls } from '@/utils/util';
|
||||||
import ImageUploadField from '@/components/image-upload-field/main.vue';
|
import ImageUploadField from '@/components/image-upload-field/main.vue';
|
||||||
@@ -1233,13 +1234,9 @@ export default {
|
|||||||
this.driverForm.drivingVehicle = row.plateNo || '';
|
this.driverForm.drivingVehicle = row.plateNo || '';
|
||||||
this.vehicleSelectorVisible = false;
|
this.vehicleSelectorVisible = false;
|
||||||
},
|
},
|
||||||
|
// 车辆认证状态展示的是「最新一次提交的状态」缓存,与审核页使用同一套状态词表
|
||||||
getVehicleCertificationStatus(row) {
|
getVehicleCertificationStatus(row) {
|
||||||
const statusMap = {
|
return certStatusTag(row.certificationStatus);
|
||||||
0: { label: '认证中', type: 'warning' },
|
|
||||||
1: { label: '已认证', type: 'success' },
|
|
||||||
2: { label: '已驳回', type: 'danger' },
|
|
||||||
};
|
|
||||||
return statusMap[row.certificationStatus] || { label: '-', type: 'info' };
|
|
||||||
},
|
},
|
||||||
setImage(prop, url) {
|
setImage(prop, url) {
|
||||||
this.driverForm[prop] = url;
|
this.driverForm[prop] = url;
|
||||||
|
|||||||
@@ -105,9 +105,6 @@
|
|||||||
修改
|
修改
|
||||||
</el-link>
|
</el-link>
|
||||||
<el-link type="primary" @click="openVehicleLedger(row)">综合台账</el-link>
|
<el-link type="primary" @click="openVehicleLedger(row)">综合台账</el-link>
|
||||||
<el-link type="primary" v-if="row.certificationStatus === 0" @click="openCertificationAudit(row)">
|
|
||||||
审核
|
|
||||||
</el-link>
|
|
||||||
<el-link
|
<el-link
|
||||||
type="primary"
|
type="primary"
|
||||||
v-if="hasPermission('transport_vehicle_status')"
|
v-if="hasPermission('transport_vehicle_status')"
|
||||||
@@ -120,271 +117,6 @@
|
|||||||
|
|
||||||
<vehicle-ledger v-model="vehicleLedgerBox" :vehicle="ledgerVehicle" />
|
<vehicle-ledger v-model="vehicleLedgerBox" :vehicle="ledgerVehicle" />
|
||||||
|
|
||||||
<el-dialog
|
|
||||||
v-model="certificationAuditBox"
|
|
||||||
title="车辆认证审核"
|
|
||||||
width="92%"
|
|
||||||
top="4vh"
|
|
||||||
class="certification-audit-dialog"
|
|
||||||
>
|
|
||||||
<el-form
|
|
||||||
label-position="right"
|
|
||||||
label-width="calc(4em + 12px)"
|
|
||||||
class="certification-audit archive-form"
|
|
||||||
>
|
|
||||||
<section-card title="基础信息">
|
|
||||||
<el-row :gutter="18">
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-form-item label="车牌号">
|
|
||||||
<span class="certification-audit__value">{{ auditValue('plateNo') }}</span>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-form-item label="车辆类型">
|
|
||||||
<span class="certification-audit__value">{{ auditValue('vehicleType') }}</span>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-form-item label="外廓尺寸">
|
|
||||||
<div class="dimension-group">
|
|
||||||
<div class="dimension-group__item">
|
|
||||||
<span class="dimension-group__label">长</span>
|
|
||||||
<span class="certification-audit__value">
|
|
||||||
{{ auditValueWithUnit('outerLength', 'mm') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="dimension-group__item">
|
|
||||||
<span class="dimension-group__label">宽</span>
|
|
||||||
<span class="certification-audit__value">
|
|
||||||
{{ auditValueWithUnit('outerWidth', 'mm') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="dimension-group__item">
|
|
||||||
<span class="dimension-group__label">高</span>
|
|
||||||
<span class="certification-audit__value">
|
|
||||||
{{ auditValueWithUnit('outerHeight', 'mm') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
<el-row :gutter="18">
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-form-item label="核定载质量">
|
|
||||||
<span class="certification-audit__value">
|
|
||||||
{{ auditValueWithUnit('approvedLoadKg', 'KG') }}
|
|
||||||
</span>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-form-item label="准牵引总质量">
|
|
||||||
<span class="certification-audit__value">
|
|
||||||
{{ auditValueWithUnit('tractionMassKg', 'KG') }}
|
|
||||||
</span>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-form-item label="业务关系">
|
|
||||||
<span class="certification-audit__value">{{ auditValue('businessRelation') }}</span>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-form-item label="能源类型">
|
|
||||||
<span class="certification-audit__value">{{ auditValue('energyType') }}</span>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
<el-row :gutter="18">
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-form-item label="强制报废日期">
|
|
||||||
<span class="certification-audit__value">
|
|
||||||
{{ auditValue('compulsoryScrapDate') }}
|
|
||||||
</span>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-form-item label="海关备案号">
|
|
||||||
<span class="certification-audit__value">{{ auditValue('customsRecordNo') }}</span>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-form-item label="所有组织">
|
|
||||||
<span class="certification-audit__value">
|
|
||||||
{{ auditValue('organizationName') }}
|
|
||||||
</span>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-form-item label="使用部门">
|
|
||||||
<span class="certification-audit__value">{{ auditValue('useDepartment') }}</span>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</section-card>
|
|
||||||
|
|
||||||
<section-card title="证件信息">
|
|
||||||
<el-row :gutter="18">
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-form-item label="行驶证有效期止">
|
|
||||||
<span class="certification-audit__value">
|
|
||||||
{{ auditValue('drivingLicenseEndDate') }}
|
|
||||||
</span>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-form-item label="道路运输证号">
|
|
||||||
<span class="certification-audit__value">
|
|
||||||
{{ auditValue('roadTransportCertNo') }}
|
|
||||||
</span>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-form-item label="道路运输证有效期止">
|
|
||||||
<span class="certification-audit__value">
|
|
||||||
{{ auditValue('roadTransportCertEndDate') }}
|
|
||||||
</span>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-form-item label="道路运输年审有效期">
|
|
||||||
<span class="certification-audit__value">
|
|
||||||
{{ auditValue('annualReviewEndDate') }}
|
|
||||||
</span>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
<el-row :gutter="18">
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-form-item label="机动车登记编号">
|
|
||||||
<span class="certification-audit__value">{{ auditValue('registrationNo') }}</span>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-form-item label="机动车登记日期">
|
|
||||||
<span class="certification-audit__value">{{ auditValue('registrationDate') }}</span>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-form-item label="行驶证档案编号">
|
|
||||||
<span class="certification-audit__value">{{ auditValue('drivingLicenseNo') }}</span>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-form-item label="行驶证有效期起">
|
|
||||||
<span class="certification-audit__value">
|
|
||||||
{{ auditValue('drivingLicenseStartDate') }}
|
|
||||||
</span>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
<el-row :gutter="18">
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-form-item label="道路运输证有效期起">
|
|
||||||
<span class="certification-audit__value">
|
|
||||||
{{ auditValue('roadTransportCertStartDate') }}
|
|
||||||
</span>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
<el-row :gutter="18">
|
|
||||||
<el-col :span="24">
|
|
||||||
<el-form-item label="备注">
|
|
||||||
<span class="certification-audit__value">{{ auditValue('remark') }}</span>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</section-card>
|
|
||||||
|
|
||||||
<section-card title="证件图片">
|
|
||||||
<el-row :gutter="18">
|
|
||||||
<el-col :span="6">
|
|
||||||
<image-upload-field
|
|
||||||
label="行驶证主页正面"
|
|
||||||
prop="drivingLicenseImage"
|
|
||||||
:value="certificationAuditForm.drivingLicenseImage"
|
|
||||||
readonly
|
|
||||||
large
|
|
||||||
class-prefix="vehicle"
|
|
||||||
/>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<image-upload-field
|
|
||||||
label="行驶证主页反面"
|
|
||||||
prop="drivingLicenseMainBack"
|
|
||||||
:value="certificationAuditForm.drivingLicenseMainBack"
|
|
||||||
readonly
|
|
||||||
large
|
|
||||||
class-prefix="vehicle"
|
|
||||||
/>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<image-upload-field
|
|
||||||
label="行驶证副页正面"
|
|
||||||
prop="drivingLicenseViceFront"
|
|
||||||
:value="certificationAuditForm.drivingLicenseViceFront"
|
|
||||||
readonly
|
|
||||||
large
|
|
||||||
class-prefix="vehicle"
|
|
||||||
/>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
<el-row :gutter="18">
|
|
||||||
<el-col :span="6">
|
|
||||||
<image-upload-field
|
|
||||||
label="行驶证副页反面"
|
|
||||||
prop="drivingLicenseViceBack"
|
|
||||||
:value="certificationAuditForm.drivingLicenseViceBack"
|
|
||||||
readonly
|
|
||||||
large
|
|
||||||
class-prefix="vehicle"
|
|
||||||
/>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<image-upload-field
|
|
||||||
label="道路运输证图片"
|
|
||||||
prop="roadTransportCertImage"
|
|
||||||
:value="certificationAuditForm.roadTransportCertImage"
|
|
||||||
readonly
|
|
||||||
large
|
|
||||||
class-prefix="vehicle"
|
|
||||||
/>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<image-upload-field
|
|
||||||
label="机动车登记本"
|
|
||||||
prop="registrationImage"
|
|
||||||
:value="certificationAuditForm.registrationImage"
|
|
||||||
readonly
|
|
||||||
large
|
|
||||||
class-prefix="vehicle"
|
|
||||||
/>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</section-card>
|
|
||||||
</el-form>
|
|
||||||
<el-form
|
|
||||||
v-if="showRejectReason"
|
|
||||||
class="certification-audit__reason"
|
|
||||||
label-position="right"
|
|
||||||
label-width="calc(4em + 24px)"
|
|
||||||
>
|
|
||||||
<el-form-item label="驳回原因" required>
|
|
||||||
<el-input
|
|
||||||
v-model="certificationRejectReason"
|
|
||||||
type="textarea"
|
|
||||||
maxlength="200"
|
|
||||||
show-word-limit
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="certificationAuditBox = false">取消</el-button>
|
|
||||||
<el-button type="danger" @click="handleCertificationReject">认证驳回</el-button>
|
|
||||||
<el-button type="primary" @click="handleCertificationApprove">认证通过</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
:title="dialogTitle"
|
:title="dialogTitle"
|
||||||
append-to-body
|
append-to-body
|
||||||
@@ -436,6 +168,24 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-form-item label="所有人" prop="ownerName">
|
||||||
|
<el-input
|
||||||
|
v-model="vehicleForm.ownerName"
|
||||||
|
maxlength="100"
|
||||||
|
placeholder="行驶证登记的单位名称"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-form-item label="车辆识别代号" prop="vin">
|
||||||
|
<el-input
|
||||||
|
v-model="vehicleForm.vin"
|
||||||
|
maxlength="17"
|
||||||
|
placeholder="选填,17位车架号"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="外廓尺寸">
|
<el-form-item label="外廓尺寸">
|
||||||
<div class="dimension-group">
|
<div class="dimension-group">
|
||||||
@@ -809,7 +559,6 @@ import {
|
|||||||
submit,
|
submit,
|
||||||
remove,
|
remove,
|
||||||
changeStatus,
|
changeStatus,
|
||||||
auditCertification,
|
|
||||||
getExpiryStat,
|
getExpiryStat,
|
||||||
recognitionTransportCertificates,
|
recognitionTransportCertificates,
|
||||||
recognizeBaiduOcr,
|
recognizeBaiduOcr,
|
||||||
@@ -818,6 +567,7 @@ import { getDeptTree } from '@/api/system/dept';
|
|||||||
import organizationSearch from '@/mixins/organization-search';
|
import organizationSearch from '@/mixins/organization-search';
|
||||||
import { exportBlob } from '@/api/common';
|
import { exportBlob } from '@/api/common';
|
||||||
import { getToken, isAdministrator } from '@/utils/auth';
|
import { getToken, isAdministrator } from '@/utils/auth';
|
||||||
|
import { certStatusTag } from '@/utils/cert-audit';
|
||||||
import { getUploadHeaders } from '@/utils/upload';
|
import { getUploadHeaders } from '@/utils/upload';
|
||||||
import { downloadXls } from '@/utils/util';
|
import { downloadXls } from '@/utils/util';
|
||||||
import ImageUploadField from '@/components/image-upload-field/main.vue';
|
import ImageUploadField from '@/components/image-upload-field/main.vue';
|
||||||
@@ -828,8 +578,10 @@ const emptyForm = () => ({
|
|||||||
organizationName: '',
|
organizationName: '',
|
||||||
useDepartment: '',
|
useDepartment: '',
|
||||||
plateNo: '',
|
plateNo: '',
|
||||||
|
ownerName: '',
|
||||||
plateColor: '',
|
plateColor: '',
|
||||||
vehicleType: '',
|
vehicleType: '',
|
||||||
|
vin: '',
|
||||||
outerLength: undefined,
|
outerLength: undefined,
|
||||||
outerWidth: undefined,
|
outerWidth: undefined,
|
||||||
outerHeight: undefined,
|
outerHeight: undefined,
|
||||||
@@ -895,10 +647,6 @@ export default {
|
|||||||
vehicleBox: false,
|
vehicleBox: false,
|
||||||
vehicleLedgerBox: false,
|
vehicleLedgerBox: false,
|
||||||
ledgerVehicle: {},
|
ledgerVehicle: {},
|
||||||
certificationAuditBox: false,
|
|
||||||
certificationAuditForm: {},
|
|
||||||
certificationRejectReason: '',
|
|
||||||
showRejectReason: false,
|
|
||||||
readonly: false,
|
readonly: false,
|
||||||
vehicleForm: emptyForm(),
|
vehicleForm: emptyForm(),
|
||||||
vehicleCertificateUploads: {},
|
vehicleCertificateUploads: {},
|
||||||
@@ -922,6 +670,8 @@ export default {
|
|||||||
{ validator: this.validatePlateNo, trigger: 'change' },
|
{ validator: this.validatePlateNo, trigger: 'change' },
|
||||||
],
|
],
|
||||||
vehicleType: [{ required: true, message: '请选择车辆类型', 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' }],
|
approvedLoadKg: [{ required: true, message: '请输入核定载质量', trigger: 'blur' }],
|
||||||
businessRelation: [{ required: true, message: '请选择业务关系', trigger: 'change' }],
|
businessRelation: [{ required: true, message: '请选择业务关系', trigger: 'change' }],
|
||||||
energyType: [{ required: true, message: '请选择能源类型', trigger: 'change' }],
|
energyType: [{ required: true, message: '请选择能源类型', trigger: 'change' }],
|
||||||
@@ -1055,6 +805,24 @@ export default {
|
|||||||
}
|
}
|
||||||
callback();
|
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) {
|
validateAnnualReviewEndDate(rule, value, callback) {
|
||||||
if (this.vehicleForm.annualReviewLongTerm !== 1 && !value) {
|
if (this.vehicleForm.annualReviewLongTerm !== 1 && !value) {
|
||||||
callback(new Error('请选择道路运输年审有效期'));
|
callback(new Error('请选择道路运输年审有效期'));
|
||||||
@@ -1107,49 +875,6 @@ export default {
|
|||||||
this.ledgerVehicle = { ...row };
|
this.ledgerVehicle = { ...row };
|
||||||
this.vehicleLedgerBox = true;
|
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() {
|
resetVehicle() {
|
||||||
this.vehicleForm = emptyForm();
|
this.vehicleForm = emptyForm();
|
||||||
this.vehicleCertificateUploads = {};
|
this.vehicleCertificateUploads = {};
|
||||||
@@ -1648,26 +1373,11 @@ export default {
|
|||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
|
// 认证状态展示的是「最新一次提交的状态」缓存,不代表该车当前是否已认证
|
||||||
|
// (是否已认证由生效快照指针决定,即曾审核通过即永久保持)。
|
||||||
|
// 证书是否临近到期另有列表顶部的到期标签与日期列标色展示,不在这里推断。
|
||||||
getCertificationStatus(row) {
|
getCertificationStatus(row) {
|
||||||
if (row.certificationStatus === 0) return { label: '认证中', type: 'warning' };
|
return certStatusTag(row.certificationStatus);
|
||||||
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' };
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -1695,9 +1405,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 车辆认证审核弹窗:与编辑弹窗一致,灰底 + section-card 白卡分组
|
// 编辑弹窗:灰底 + section-card 白卡分组
|
||||||
.certification-audit,
|
|
||||||
.certification-audit__reason,
|
|
||||||
.vehicle-form {
|
.vehicle-form {
|
||||||
:deep(.el-form-item__label) {
|
:deep(.el-form-item__label) {
|
||||||
height: auto;
|
height: auto;
|
||||||
@@ -1723,60 +1431,10 @@ export default {
|
|||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
margin-right: 4px;
|
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) {
|
:deep(.el-form-item__label) {
|
||||||
flex: 0 0 calc(4em + 24px) !important;
|
flex: 0 0 calc(4em + 24px) !important;
|
||||||
width: 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user