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 @@
+
+
+
+
+
+ 新增
+
+
+ 密码重置
+
+
+ 批量删除
+
+
+
+ {{ row.identitiesName || formatIdentities(row.identities) }}
+
+
+
+ {{ row.status === 1 ? '启用' : '停用' }}
+
+
+
+ {{ formatUpdateUserName(row) }}
+
+
+
+ 查看
+
+
+ 编辑
+
+
+ {{ row.status === 1 ? '停用' : '启用' }}
+
+
+ 删除
+
+
+
+
+
+
+
+
+
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;
},