diff --git a/src/api/system/mpUser.js b/src/api/system/mpUser.js new file mode 100644 index 0000000..99427e2 --- /dev/null +++ b/src/api/system/mpUser.js @@ -0,0 +1,62 @@ +import request from '@/axios'; + +export const getList = (current, size, params) => { + return request({ + url: '/blade-system/mp-user/list', + method: 'get', + params: { + ...params, + current, + size, + }, + }); +}; + +export const getDetail = id => { + return request({ + url: '/blade-system/mp-user/detail', + method: 'get', + params: { + id, + }, + }); +}; + +export const submit = row => { + return request({ + url: '/blade-system/mp-user/submit', + method: 'post', + data: row, + }); +}; + +export const remove = ids => { + return request({ + url: '/blade-system/mp-user/remove', + method: 'post', + params: { + ids, + }, + }); +}; + +export const changeStatus = (id, status) => { + return request({ + url: '/blade-system/mp-user/change-status', + method: 'post', + params: { + id, + status, + }, + }); +}; + +export const resetPassword = userIds => { + return request({ + url: '/blade-system/mp-user/reset-password', + method: 'post', + params: { + userIds, + }, + }); +}; diff --git a/src/option/system/mpUser.js b/src/option/system/mpUser.js new file mode 100644 index 0000000..a5b4b9a --- /dev/null +++ b/src/option/system/mpUser.js @@ -0,0 +1,203 @@ +const identityDic = [ + { label: '司机', value: 'driver' }, + { label: '管理员', value: 'admin' }, +]; + +const statusDic = [ + { label: '启用', value: 1 }, + { label: '停用', value: 0 }, +]; + +const validatePhone = (rule, value, callback) => { + const phone = String(value || '').trim(); + if (!phone) { + callback(new Error('请输入手机号')); + return; + } + if (!/^1\d{10}$/.test(phone)) { + callback(new Error('请输入正确的手机号')); + return; + } + callback(); +}; + +const validateRemark = (rule, value, callback) => { + if (String(value || '').length > 200) { + callback(new Error('备注不能超过200字')); + } else { + callback(); + } +}; + +export default { + height: 'auto', + calcHeight: 22, + dialogWidth: 760, + labelPosition: 'right', + labelWidth: 'auto', + tip: false, + searchBtnText: '查询', + emptyBtnText: '重置', + saveBtnText: '提交', + updateBtnText: '提交', + searchShow: true, + searchIcon: true, + searchLabelWidth: 160, + searchMenuSpan: 24, + searchIndex: 4, + searchMenuPosition: 'right', + border: true, + index: true, + indexLabel: '序号', + indexWidth: 70, + viewBtn: false, + delBtn: false, + editBtn: false, + addBtn: false, + selection: true, + dialogClickModal: false, + menuWidth: 280, + column: [ + { + label: '手机号', + prop: 'phone', + minWidth: 130, + search: true, + searchOrder: 4, + searchSpan: 6, + searchPlaceholder: '请输入', + maxlength: 11, + showWordLimit: true, + order: 100, + rules: [ + { required: true, message: '请输入手机号', trigger: 'blur' }, + { validator: validatePhone, trigger: 'blur' }, + ], + }, + { + label: '账号', + prop: 'account', + minWidth: 130, + search: true, + searchOrder: 3, + searchSpan: 6, + searchPlaceholder: '请输入', + maxlength: 45, + order: 90, + placeholder: '默认与手机号相同', + }, + { + label: '姓名', + prop: 'realName', + minWidth: 100, + search: true, + searchOrder: 2, + searchSpan: 6, + searchPlaceholder: '请输入', + maxlength: 10, + showWordLimit: true, + order: 80, + }, + { + label: '昵称', + prop: 'name', + minWidth: 100, + maxlength: 20, + showWordLimit: true, + order: 70, + }, + { + label: '小程序身份', + prop: 'identities', + type: 'select', + multiple: true, + dataType: 'string', + dicData: identityDic, + minWidth: 140, + search: true, + searchOrder: 1, + searchSpan: 6, + searchPlaceholder: '请选择', + order: 60, + rules: [{ required: true, message: '请选择小程序身份', trigger: 'change' }], + }, + { + label: '性别', + prop: 'sex', + type: 'select', + dicUrl: '/blade-system/dict/dictionary?code=sex', + props: { + label: 'dictValue', + value: 'dictKey', + }, + dataType: 'number', + minWidth: 80, + hide: true, + order: 50, + }, + { + label: 'openid', + prop: 'openid', + minWidth: 180, + overHidden: true, + addDisplay: false, + editDisplay: true, + order: 40, + placeholder: '可清空以解绑微信', + }, + { + label: '状态', + prop: 'status', + type: 'select', + dicData: statusDic, + dataType: 'number', + minWidth: 90, + search: true, + searchOrder: 0, + searchSpan: 6, + addDisplay: false, + editDisplay: false, + value: 1, + order: 30, + }, + { + label: '备注', + prop: 'remark', + type: 'textarea', + span: 24, + minRows: 2, + maxlength: 200, + showWordLimit: true, + hide: true, + order: 20, + rules: [{ validator: validateRemark, trigger: 'blur' }], + }, + { + label: '创建时间', + prop: 'createTime', + minWidth: 170, + addDisplay: false, + editDisplay: false, + viewDisplay: true, + order: 10, + }, + { + label: '更新人', + prop: 'updateUserName', + minWidth: 100, + addDisplay: false, + editDisplay: false, + viewDisplay: true, + order: 5, + }, + { + label: '更新时间', + prop: 'updateTime', + minWidth: 170, + addDisplay: false, + editDisplay: false, + viewDisplay: true, + order: 1, + }, + ], +}; diff --git a/src/option/system/user.js b/src/option/system/user.js index 78401f8..f3956d7 100644 --- a/src/option/system/user.js +++ b/src/option/system/user.js @@ -39,6 +39,7 @@ export const userOption = safe => { label: '账号名', prop: 'account', slot: true, + minWidth: 180, search: true, searchOrder: 2, searchPlaceholder: '支持模糊搜索', @@ -96,7 +97,7 @@ export const userOption = safe => { { label: '账号名', prop: 'account', - span: 6, + span: 12, rules: [ { required: true, diff --git a/src/utils/cert-audit.js b/src/utils/cert-audit.js index d57cb9b..331eced 100644 --- a/src/utils/cert-audit.js +++ b/src/utils/cert-audit.js @@ -139,9 +139,23 @@ const CERT_SNAPSHOT_GROUPS = { { title: '从业资格证信息', fields: [ + { key: 'qualificationType', label: '从业资格认证类型' }, { key: 'qualificationNo', label: '从业资格证号' }, - { key: 'qualificationUrl', label: '从业资格证照片', type: TYPE_IMAGE }, - { key: 'qualificationValidEnd', label: '从业资格证有效期止', type: TYPE_DATE }, + { + key: 'qualificationFront', + label: '从业资格证封面页', + type: TYPE_IMAGE, + legacyKeys: ['qualificationUrl'], + }, + { key: 'qualificationBack', label: '从业资格证内容页', type: TYPE_IMAGE }, + { key: 'qualificationValidStart', label: '从业资格证有效期起', type: TYPE_DATE }, + { + key: 'qualificationValidEnd', + label: '从业资格证有效期止', + type: TYPE_DATE, + longTermKey: 'qualificationLongTerm', + }, + { key: 'qualificationLongTerm', label: '从业资格证长期有效', type: TYPE_LONG_TERM }, ], }, ], @@ -268,9 +282,28 @@ const normalizeDate = value => { return text; }; +/** + * 取字段值,兼容字段改名前的旧键 + * + * 后端把某字段改名后,改名之前提交的记录里存的仍是旧键(例:从业资格证封面页 + * 由 `qualificationUrl` 改为 `qualificationFront`)。只读新键会得到空, + * 审核页就把已填的封面页显示成「—」,误导审核员以为对方没传。 + * 新旧键都存在时以新键为准。 + */ +const readFieldValue = (snapshot, field) => { + if (!snapshot) return undefined; + const value = snapshot[field.key]; + if (!isEmptyValue(value)) return value; + const legacyKeys = field.legacyKeys || []; + for (let i = 0; i < legacyKeys.length; i += 1) { + if (!isEmptyValue(snapshot[legacyKeys[i]])) return snapshot[legacyKeys[i]]; + } + return value; +}; + /** 比较用的归一化取值:展示相同即视为未变更 */ const canonicalOf = (snapshot, field) => { - const value = snapshot ? snapshot[field.key] : undefined; + const value = readFieldValue(snapshot, field); if (field.type === TYPE_LONG_TERM) { if (isEmptyValue(value)) return ''; return isLongTermOn(value) ? '1' : '0'; @@ -286,7 +319,7 @@ const canonicalOf = (snapshot, field) => { /** 展示文案:日期型受「长期有效」影响,图片型返回 URL 由组件渲染缩略图 */ const textOf = (snapshot, field) => { - const value = snapshot ? snapshot[field.key] : undefined; + const value = readFieldValue(snapshot, field); if (field.type === TYPE_IMAGE) { return isEmptyValue(value) ? '' : String(value); } @@ -323,11 +356,11 @@ const buildRow = (field, pendingSnapshot, effectiveSnapshot, hasBaseline) => { label: field.label, type, effective: { - raw: effectiveSnapshot ? effectiveSnapshot[field.key] : undefined, + raw: readFieldValue(effectiveSnapshot, target), text: textOf(effectiveSnapshot, target), }, pending: { - raw: pendingSnapshot ? pendingSnapshot[field.key] : undefined, + raw: readFieldValue(pendingSnapshot, target), text: textOf(pendingSnapshot, target), }, changed: Boolean(changeType), @@ -363,7 +396,8 @@ export const buildSnapshotDiff = (bizType, pendingJson, effectiveJson) => { const groups = groupsConfig.map(group => { const rows = group.fields.map(field => { - knownKeys.push(field.key); + // 旧键已由所属字段并入展示,不能再作为独立字段出现,否则同一张封面页会出现两行 + knownKeys.push(field.key, ...(field.legacyKeys || [])); return buildRow(field, pending.value, effective.value, hasBaseline); }); return { diff --git a/src/views/system/mp-user.vue b/src/views/system/mp-user.vue new file mode 100644 index 0000000..bf66361 --- /dev/null +++ b/src/views/system/mp-user.vue @@ -0,0 +1,321 @@ + + + + + diff --git a/src/views/system/user.vue b/src/views/system/user.vue index 7aeee32..533db3e 100644 --- a/src/views/system/user.vue +++ b/src/views/system/user.vue @@ -139,7 +139,7 @@ - + {{ bizTypeLabel }} + + {{ detail.auditUserName || '-' }} + + + {{ detail.auditTime ? formatCertDateTime(detail.auditTime) : '-' }} + @@ -149,6 +155,13 @@ export default { statusTag() { return certStatusTag(this.detail.status); }, + // 审核时间一栏按审核结论换标签:还在「审核中」的记录没有结论,不能写成通过或驳回时间 + auditTimeLabel() { + const status = String(this.detail.status); + if (status === String(CERT_RECORD_STATUS.APPROVED)) return '审核通过时间'; + if (status === String(CERT_RECORD_STATUS.REJECTED)) return '审核驳回时间'; + return '审核时间'; + }, // 只有「审核中」的记录可审;已通过/已驳回/已作废都是终态 auditable() { return String(this.detail.status) === String(CERT_RECORD_STATUS.PENDING); diff --git a/src/views/transportCapacity/driver.vue b/src/views/transportCapacity/driver.vue index 2efbed7..d9ce637 100644 --- a/src/views/transportCapacity/driver.vue +++ b/src/views/transportCapacity/driver.vue @@ -1593,17 +1593,40 @@ export default { const fullNation = `${nation}族`; return this.nationOptions.includes(fullNation) ? fullNation : nation; }, + /** + * OCR / 手工录入日期归一化:对齐车辆侧与后端 FlexibleLocalDateDeserializer。 + * 支持 yyyyMMdd、yyyy-MM-dd、2018.3.9、2030年05月20日及任意非数字分隔;无法识别返回空串。 + */ normalizeBirthday(value = '') { const birthday = String(value || '').trim(); - const compactMatch = birthday.match(/^(\d{4})(\d{2})(\d{2})$/); - if (compactMatch) { - const [, year, month, day] = compactMatch; - return `${year}-${month}-${day}`; + if (!birthday) return ''; + if (/长期|永久/.test(birthday)) return ''; + const looseMatch = birthday.match(/(\d{4})\D+(\d{1,2})\D+(\d{1,2})/); + if (looseMatch) { + return this.toValidDateText(looseMatch[1], looseMatch[2], looseMatch[3]); } - const match = birthday.match(/^(\d{4})[-/.年](\d{1,2})[-/.月](\d{1,2})日?$/); - if (!match) return birthday; - const [, year, month, day] = match; - return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`; + const compact = birthday.replace(/\D/g, ''); + if (compact.length === 8) { + return this.toValidDateText(compact.slice(0, 4), compact.slice(4, 6), compact.slice(6, 8)); + } + return ''; + }, + toValidDateText(yearText, monthText, dayText) { + const year = Number(yearText); + const month = Number(monthText); + const day = Number(dayText); + if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) { + return ''; + } + const date = new Date(year, month - 1, day); + if ( + date.getFullYear() !== year || + date.getMonth() !== month - 1 || + date.getDate() !== day + ) { + return ''; + } + return `${String(year).padStart(4, '0')}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; }, parseIdCardInfo(idCardNo = '') { if (!/^\d{17}[\dX]$/.test(idCardNo)) { diff --git a/src/views/transportCapacity/vehicle.vue b/src/views/transportCapacity/vehicle.vue index 49802c9..9ec02ca 100644 --- a/src/views/transportCapacity/vehicle.vue +++ b/src/views/transportCapacity/vehicle.vue @@ -1174,37 +1174,59 @@ export default { }); }, /** - * OCR 日期常为区间(如 2022-08-17至2024-01-05)或含无效日(如 01-65)。 - * 提取可解析的合法日期;prefer=first 取登记类起始日,prefer=last 取有效期截止日。 + * OCR 日期常为区间(如 2022-08-17至2024-01-05)、无分隔符(如 20260317)、 + * 年月日/点分(2030年05月20日、2018.3.9)或任意非数字分隔。 + * 对齐司机侧 normalizeBirthday / 后端 OcrFieldSupport:年与月、月与日之间允许任意非数字。 + * prefer=first 取登记类起始日,prefer=last 取有效期截止日。 + * 返回值统一为 YYYY-MM-DD,供 el-date-picker 展示与后端 LocalDate 反序列化。 */ normalizeDate(value = '', prefer = 'first') { const dateValue = String(value || '').trim(); if (!dateValue) return ''; - const segments = dateValue.match(/\d{4}[-/.年]\d{1,2}[-/.月]\d{1,2}日?/g) || []; - const sourceList = segments.length ? segments : [dateValue]; - const candidates = sourceList - .map(item => { - const match = String(item) - .trim() - .match(/^(\d{4})[-/.年](\d{1,2})[-/.月](\d{1,2})日?$/); - if (!match) return ''; - const year = Number(match[1]); - const month = Number(match[2]); - const day = Number(match[3]); - const date = new Date(year, month - 1, day); - if ( - date.getFullYear() !== year || - date.getMonth() !== month - 1 || - date.getDate() !== day - ) { - return ''; + if (/长期|永久/.test(dateValue)) return ''; + const candidates = []; + const loosePattern = /(\d{4})\D+(\d{1,2})\D+(\d{1,2})/g; + let match = loosePattern.exec(dateValue); + while (match) { + const normalized = this.toValidDateText(match[1], match[2], match[3]); + if (normalized) { + candidates.push(normalized); + } + match = loosePattern.exec(dateValue); + } + if (!candidates.length) { + const compact = dateValue.replace(/\D/g, ''); + if (compact.length === 8) { + const normalized = this.toValidDateText( + compact.slice(0, 4), + compact.slice(4, 6), + compact.slice(6, 8) + ); + if (normalized) { + candidates.push(normalized); } - return `${String(year).padStart(4, '0')}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; - }) - .filter(Boolean); + } + } if (!candidates.length) return ''; return prefer === 'last' ? candidates[candidates.length - 1] : candidates[0]; }, + toValidDateText(yearText, monthText, dayText) { + const year = Number(yearText); + const month = Number(monthText); + const day = Number(dayText); + if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) { + return ''; + } + const date = new Date(year, month - 1, day); + if ( + date.getFullYear() !== year || + date.getMonth() !== month - 1 || + date.getDate() !== day + ) { + return ''; + } + return `${String(year).padStart(4, '0')}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; + }, normalizeVehicleDateFields(form = {}) { const dateFields = [ ['compulsoryScrapDate', 'first'], @@ -1216,8 +1238,13 @@ export default { ['registrationDate', 'first'], ]; dateFields.forEach(([prop, prefer]) => { - if (form[prop] === undefined || form[prop] === null || form[prop] === '') return; - form[prop] = this.normalizeDate(form[prop], prefer); + if (form[prop] === undefined || form[prop] === null || form[prop] === '') { + form[prop] = null; + return; + } + const normalized = this.normalizeDate(form[prop], prefer); + // 无法识别或空串统一置 null,避免把 "" / "20260317" 原样提交给 LocalDate + form[prop] = normalized || null; }); return form; },