✨ 角色权限联动与临时额度/项目备案/司机车辆审核MK

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-11 01:09:35 +08:00
co-authored by Cursor
parent f081ccd3f0
commit 0da0c6f493
14 changed files with 656 additions and 75 deletions
+2 -1
View File
@@ -7,7 +7,8 @@ import {
export const changeTypeOptions = [
{ label: '项目变更', value: '项目变更' },
{ label: '项目备案调整', value: '项目备案调整' },
{ label: '项目备案流程(本部流程)', value: '项目备案流程(本部流程)' },
{ label: '项目备案流程(运输部流程)', value: '项目备案流程(运输部流程)' },
];
export const config = {
+10 -7
View File
@@ -85,13 +85,6 @@ export const config = {
status: ['draft', 'withdrawn', 'rejected'],
permission: 'temporary_credit_limit_submit',
},
{
action: 'flow',
label: '流程',
statusProp: 'approvalStatus',
status: ['reviewing'],
permission: 'temporary_credit_limit_view',
},
{
action: 'withdraw',
label: '撤回',
@@ -99,6 +92,14 @@ export const config = {
status: ['reviewing'],
permission: 'temporary_credit_limit_withdraw',
},
{
action: 'mkAudit',
label: '流程',
statusProp: 'approvalStatus',
status: ['reviewing'],
permission: 'temporary_credit_limit_approve',
mkOnly: true,
},
{
action: 'approve',
label: '审批通过',
@@ -106,6 +107,7 @@ export const config = {
statusProp: 'approvalStatus',
status: ['reviewing'],
permission: 'temporary_credit_limit_approve',
localOnly: true,
},
{
action: 'reject',
@@ -114,6 +116,7 @@ export const config = {
statusProp: 'approvalStatus',
status: ['reviewing'],
permission: 'temporary_credit_limit_reject',
localOnly: true,
},
],
};
+44
View File
@@ -106,6 +106,17 @@ export default [
bizType: 'project-change',
},
},
{
path: '/business/project-record-change/public-view',
name: '查看项目备案信息',
component: () => import('@/views/business/project-apply-public-view.vue'),
meta: {
keepAlive: false,
isTab: false,
isAuth: false,
bizType: 'project-record-change',
},
},
{
path: '/business/contract-manage/public-view',
name: '查看合同信息',
@@ -194,6 +205,39 @@ export default [
bizType: 'bill-payment',
},
},
{
path: '/business/temporary-credit-limit/public-view',
name: '查看临时额度申请信息',
component: () => import('@/views/business/temporary-credit-limit-public-view.vue'),
meta: {
keepAlive: false,
isTab: false,
isAuth: false,
bizType: 'temporary-credit-limit',
},
},
{
path: '/transportCapacity/cert-audit-driver/public-view',
name: '查看司机认证审核信息',
component: () => import('@/views/transportCapacity/cert-audit-driver-public-view.vue'),
meta: {
keepAlive: false,
isTab: false,
isAuth: false,
bizType: 'cert-audit-driver',
},
},
{
path: '/transportCapacity/cert-audit-vehicle/public-view',
name: '查看车辆认证审核信息',
component: () => import('@/views/transportCapacity/cert-audit-vehicle-public-view.vue'),
meta: {
keepAlive: false,
isTab: false,
isAuth: false,
bizType: 'cert-audit-vehicle',
},
},
{
path: '/',
name: '主页',
+45
View File
@@ -21,6 +21,19 @@ export const MK_BIZ = {
rejected: ['rejected', 'withdrawn'],
approved: ['approved'],
},
/** 项目备案流程(本部/运输部)共用 */
'project-record-change': {
dictName: '提交项目备案审核流',
subjectPrefix: '项目备案审批',
rejected: ['rejected', 'withdrawn'],
approved: ['approved'],
},
'temporary-credit-limit': {
dictName: '提交临时额度审核流',
subjectPrefix: '临时额度审批',
rejected: ['rejected', 'withdrawn'],
approved: ['approved'],
},
'contract-manage': {
dictName: '提交合同审核流',
subjectPrefix: '合同审批',
@@ -69,10 +82,42 @@ export const MK_BIZ = {
rejected: ['returned'],
approved: ['approved'],
},
'cert-audit-driver': {
dictName: '提交司机审核流',
subjectPrefix: '司机认证审批',
// 认证记录 status:0审核中 1已通过 2已驳回
rejected: ['2'],
approved: ['1'],
},
'cert-audit-vehicle': {
dictName: '提交车辆审核流',
subjectPrefix: '车辆认证审批',
rejected: ['2'],
approved: ['1'],
},
};
/** 按认证对象类型解析 MK 业务类型 */
export function resolveCertAuditMkBizType(bizType) {
return String(bizType || '') === 'vehicle' ? 'cert-audit-vehicle' : 'cert-audit-driver';
}
const MK_SWITCH_NAME = '开启MK';
/** 项目备案类变更类型(含历史「项目备案调整」) */
export const PROJECT_RECORD_CHANGE_TYPES = [
'项目备案流程(本部流程)',
'项目备案流程(运输部流程)',
'项目备案调整',
];
/** 按变更类型选择项目变更 / 项目备案 MK 业务类型 */
export function resolveProjectChangeMkBizType(changeType) {
return PROJECT_RECORD_CHANGE_TYPES.includes(String(changeType || '').trim())
? 'project-record-change'
: 'project-change';
}
/** 进程内缓存,避免每次提交都打字典;登录后同一会话开关一般不变 */
let mkEnabledCache = null;
let mkEnabledPromise = null;
+64 -41
View File
@@ -92,6 +92,7 @@
ref="treeMenu"
:default-checked-keys="menuTreeObj"
:props="props"
@check="handleMenuCheck"
>
</el-tree>
</el-tab-pane>
@@ -102,7 +103,7 @@
style="margin-bottom: 12px; background: #f5f7fa; padding: 6px 10px; border-radius: 4px"
>
<span style="color: #606266; font-size: 13px"
>仅展示菜单权限已勾选的菜单,已配置的数据权限会保留。勾选多个菜单后点击「设置」可批量配置</span
>菜单权限勾选/反选会自动同步到此页;仅展示已勾选菜单,已配置的数据权限会保留。可再调整勾选后点击「设置」批量配置</span
>
<el-button type="primary" size="small" @click="openDataScopeSetting">设置</el-button>
</el-row>
@@ -191,20 +192,8 @@
<el-radio :value="1">所在部门及下级</el-radio>
<el-radio :value="2">全部组织</el-radio>
<el-radio :value="3">本人创建</el-radio>
<el-radio :value="4">自定义组织</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="Number(dataScopeSetting.scopeType) === 4" label="组织">
<el-tree
ref="treeCustomDept"
:data="deptTree"
show-checkbox
node-key="id"
default-expand-all
:props="{ label: 'title', children: 'children' }"
style="width: 100%; max-height: 320px; overflow: auto; border: 1px solid #ebeef5; padding: 8px"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
@@ -228,7 +217,6 @@ import {
update,
} from '@/api/system/role';
import { getRoleMenuDataScopeTree, submitRoleMenuDataScope } from '@/api/system/roleDataScope';
import { getDeptTree } from '@/api/system/dept';
import { mapGetters } from 'vuex';
import website from '@/config/website';
@@ -253,7 +241,6 @@ export default {
currentRoleName: '',
roleDataScopeMenuTree: [],
roleDataScopeMap: {},
deptTree: [],
dataScopeSetting: {
roleName: '',
scopeType: 2,
@@ -430,6 +417,11 @@ export default {
if (!tree) return;
const allKeys = this.getAllNodeKeys(dataList);
tree.setCheckedKeys(allKeys);
if (treeRef === 'treeMenu') {
this.$nextTick(() => {
this.syncDataScopeTreeFromMenu();
});
}
},
handleInvertSelect(treeRef, dataList, isLinked) {
const tree = this.$refs[treeRef];
@@ -444,6 +436,11 @@ export default {
const invertedKeys = allKeys.filter(key => !checkedKeys.has(key));
tree.setCheckedKeys(invertedKeys);
}
if (treeRef === 'treeMenu') {
this.$nextTick(() => {
this.syncDataScopeTreeFromMenu();
});
}
},
handleLinkedChange(treeRef) {
const tree = this.$refs[treeRef];
@@ -452,6 +449,9 @@ export default {
const halfCheckedKeys = tree.getHalfCheckedKeys();
this.$nextTick(() => {
tree.setCheckedKeys([...checkedKeys, ...halfCheckedKeys]);
if (treeRef === 'treeMenu') {
this.syncDataScopeTreeFromMenu();
}
});
},
// el-tree 会把 setCheckedKeys 的入参记录为默认勾选集并在树数据重载时回放,
@@ -461,6 +461,50 @@ export default {
if (!tree) return;
tree.setCheckedKeys(keys);
},
handleMenuCheck() {
this.syncDataScopeTreeFromMenu();
},
/**
* 按当前菜单权限勾选结果过滤数据权限树,并同步勾选状态。
* 取消勾选的菜单从数据权限树移除;已配置的数据权限标签仍保留在 roleDataScopeMap。
*/
syncDataScopeTreeFromMenu() {
const treeMenu = this.$refs.treeMenu;
if (!treeMenu) {
return;
}
const checkedKeys = treeMenu.getCheckedKeys() || [];
const checkedKeySet = new Set(checkedKeys);
this.roleDataScopeMenuTree = this.filterMenuTreeByChecked(this.menuGrantList, checkedKeySet);
this.$nextTick(() => {
const treeDataScope = this.$refs.treeRoleDataScope;
if (!treeDataScope) {
return;
}
const visibleKeys = new Set(this.getAllNodeKeys(this.roleDataScopeMenuTree));
const syncKeys = checkedKeys.filter(key => visibleKeys.has(key));
treeDataScope.setCheckedKeys(syncKeys);
});
},
/**
* 仅保留已勾选菜单及其祖先节点,保持树层级。
*/
filterMenuTreeByChecked(nodes, checkedKeySet) {
if (!nodes || nodes.length === 0) {
return [];
}
const result = [];
nodes.forEach(node => {
const children = this.filterMenuTreeByChecked(node.children || [], checkedKeySet);
if (checkedKeySet.has(node.id) || children.length > 0) {
result.push({
...node,
children,
});
}
});
return result;
},
submit() {
const menuList = this.$refs.treeMenu.getCheckedKeys();
const apiScopeList = this.$refs.treeApiScope.getCheckedKeys();
@@ -484,7 +528,7 @@ export default {
return getRoleMenuDataScopeTree(roleId).then(res => {
const data = res.data.data || {};
this.currentRoleName = data.roleName || '';
this.roleDataScopeMenuTree = data.menuTree || [];
// 菜单树由菜单权限勾选实时联动生成;这里只加载已配置的数据权限标签
const scopeMap = {};
(data.scopes || []).forEach(item => {
scopeMap[String(item.menuId)] = item;
@@ -507,20 +551,7 @@ export default {
scopeType: 2,
menuIds,
};
const loadDept =
this.deptTree && this.deptTree.length
? Promise.resolve()
: getDeptTree(this.userInfo.tenantId).then(res => {
this.deptTree = res.data.data || [];
});
loadDept.then(() => {
this.dataScopeSettingBox = true;
this.$nextTick(() => {
if (this.$refs.treeCustomDept) {
this.$refs.treeCustomDept.setCheckedKeys([]);
}
});
});
},
submitDataScopeSetting() {
const roleId = this.currentRoleIds && this.currentRoleIds[0];
@@ -529,27 +560,17 @@ export default {
return;
}
const scopeType = Number(this.dataScopeSetting.scopeType || 2);
let deptIds = [];
if (scopeType === 4) {
deptIds = this.$refs.treeCustomDept ? this.$refs.treeCustomDept.getCheckedKeys() : [];
if (!deptIds.length) {
this.$message.warning('自定义组织时请勾选组织');
return;
}
}
submitRoleMenuDataScope({
roleId,
menuIds: this.dataScopeSetting.menuIds,
scopeType,
deptIds,
deptIds: [],
}).then(() => {
this.$message.success('数据权限设置成功');
this.dataScopeSettingBox = false;
this.loadRoleDataScopeTree(roleId).then(() => {
this.$nextTick(() => {
if (this.$refs.treeRoleDataScope) {
this.$refs.treeRoleDataScope.setCheckedKeys([]);
}
this.syncDataScopeTreeFromMenu();
});
});
});
@@ -663,6 +684,7 @@ export default {
this.$nextTick(() => {
this.applyCheckedKeys('treeMenu', this.menuTreeObj);
this.applyCheckedKeys('treeApiScope', this.apiScopeTreeObj);
this.syncDataScopeTreeFromMenu();
});
});
},
@@ -682,6 +704,7 @@ export default {
this.$nextTick(() => {
this.applyCheckedKeys('treeMenu', this.menuTreeObj);
this.applyCheckedKeys('treeApiScope', this.apiScopeTreeObj);
this.syncDataScopeTreeFromMenu();
});
});
},
+36 -9
View File
@@ -1248,7 +1248,11 @@ import { getList as getUserList } from '@/api/system/user';
import { getToken, isAdministrator } from '@/utils/auth';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { getMKApprovalUrl } from '@/api/system/business-process';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
import {
loadMkEnabled,
resolveProjectChangeMkBizType,
submitMkApprovalFlow,
} from '@/utils/mk-approval';
import { ElImageViewer } from 'element-plus';
import { OpenFileViewer } from '@open-file-viewer/vue';
import PdfPreview from '@/components/pdf-preview/main.vue';
@@ -1353,7 +1357,8 @@ const optionalSentinelFields = [
const changeTypeOptions = [
{ label: '项目变更', value: '项目变更' },
{ label: '项目备案调整', value: '项目备案调整' },
{ label: '项目备案流程(本部流程)', value: '项目备案流程(本部流程)' },
{ label: '项目备案流程(运输部流程)', value: '项目备案流程(运输部流程)' },
];
const attachmentViewerPlugins = [
@@ -1756,12 +1761,16 @@ export default {
isPublicViewPage() {
return (
this.$route.path === '/business/project-apply/public-view' ||
this.$route.path === '/business/project-change/public-view'
this.$route.path === '/business/project-change/public-view' ||
this.$route.path === '/business/project-record-change/public-view'
);
},
publicBizType() {
return this.$route.meta?.bizType || 'project-apply';
},
isPublicProjectChangeView() {
return ['project-change', 'project-record-change'].includes(this.publicBizType);
},
projectFormContainer() {
// 表单页实例始终用页面容器,避免 keep-alive 失活时误切弹窗
return this.isFormPageInstance ? 'div' : 'el-dialog';
@@ -1900,7 +1909,7 @@ export default {
},
openPublicProjectForm() {
const id = this.$route.query.id;
this.dialogType = this.publicBizType === 'project-change' ? 'change' : 'view';
this.dialogType = this.isPublicProjectChangeView ? 'change' : 'view';
this.dialogReadonly = true;
this.projectBox = true;
if (!id) {
@@ -2535,10 +2544,20 @@ export default {
},
resolveChangeType(row = {}) {
if (row.changeType) return row.changeType;
const node = row.currentNode || '';
if (String(node).includes('备案')) return '项目备案调整';
const node = String(row.currentNode || '');
if (node.includes('运输部')) return '项目备案流程(运输部流程)';
if (node.includes('本部') || node.includes('备案')) return '项目备案流程(本部流程)';
return '项目变更';
},
buildProjectChangeMkTempVarData(row = {}) {
const changeType = this.resolveChangeType(row);
return {
projectType: row.projectType || this.form.projectType || '',
// MK 临时变量:变更类型(本部/运输部备案分流)
changeType,
变更类型: changeType,
};
},
submitProject() {
this.$refs.projectForm.validate(valid => {
if (!valid) {
@@ -2681,6 +2700,7 @@ export default {
if (this.isProjectChangeModule) {
const payload = this.normalizeSubmitForm({ includeChangeType: true });
const approvalStatus = this.form.approvalStatus || '';
const changeType = this.resolveChangeType(this.form);
projectChangeApi
.submit(payload)
.then(res => {
@@ -2690,12 +2710,15 @@ export default {
}
const id = this.form.id;
return submitMkApprovalFlow({
bizType: 'project-change',
bizType: resolveProjectChangeMkBizType(changeType),
bizId: id,
subjectName: this.form.projectName || '',
approvalStatus,
mkEnabled: this.mkEnabled,
tempVarData: { projectType: this.form.projectType || '' },
tempVarData: this.buildProjectChangeMkTempVarData({
...this.form,
changeType,
}),
}).then(() => {
this.$message.success('提交成功!');
this.closeProjectForm();
@@ -2715,13 +2738,17 @@ export default {
return;
}
const id = this.form.id;
const changeType = this.resolveChangeType(this.form);
return submitMkApprovalFlow({
bizType: 'project-apply',
bizId: id,
subjectName: this.form.projectName || '',
approvalStatus: this.form.approvalStatus || 'change_rejected',
mkEnabled: this.mkEnabled,
tempVarData: { projectType: this.form.projectType || '' },
tempVarData: this.buildProjectChangeMkTempVarData({
...this.form,
changeType,
}),
}).then(() => {
this.$message.success('提交成功!');
this.closeProjectForm();
+15 -6
View File
@@ -75,7 +75,11 @@ import * as api from '@/api/business/project-change';
import { getMKApprovalUrl } from '@/api/system/business-process';
import { config, option } from '@/option/business/project-change';
import { approvalStatusOptions } from '@/option/business/common';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
import {
loadMkEnabled,
resolveProjectChangeMkBizType,
submitMkApprovalFlow,
} from '@/utils/mk-approval';
import { isAdministrator } from '@/utils/auth';
import { mapGetters } from 'vuex';
@@ -217,16 +221,21 @@ export default {
// 先本地提交再调 MK,避免 MK 回写 reviewing 后本地 submitApproval 被编辑校验拦住。
const afterMk =
operation.action === 'submitApproval'
? request().then(() =>
submitMkApprovalFlow({
bizType: 'project-change',
? request().then(() => {
const changeType = row.changeType || '项目变更';
return submitMkApprovalFlow({
bizType: resolveProjectChangeMkBizType(changeType),
bizId: row.id,
subjectName: row.projectName || '',
approvalStatus: row.approvalStatus || '',
mkEnabled: this.mkEnabled,
tempVarData: { projectType: row.projectType || '' },
tempVarData: {
projectType: row.projectType || '',
changeType,
变更类型: changeType,
},
});
})
)
: request();
const loading =
operation.action === 'approve'
@@ -0,0 +1,113 @@
<template>
<mk-public-shell biz-type="temporary-credit-limit" :get-form="getForm">
<div class="temporary-credit-limit-public-view">
<div class="temporary-credit-limit-public-view__title">查看临时额度申请</div>
<template v-if="detail">
<div class="temporary-credit-limit-public-view__section-title">项目额度信息</div>
<el-descriptions :column="2" border>
<el-descriptions-item label="项目名称">{{ detail.projectName || '-' }}</el-descriptions-item>
<el-descriptions-item label="项目编号">{{ detail.projectCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="承办部门">{{ detail.undertakeDeptName || '-' }}</el-descriptions-item>
<el-descriptions-item label="项目资金使用额度(万元)">
{{ formatAmount(detail.projectFundLimit) }}
</el-descriptions-item>
<el-descriptions-item label="已使用项目资金使用额度(万元)">
{{ formatAmount(detail.usedFundLimit) }}
</el-descriptions-item>
<el-descriptions-item label="剩余项目资金使用额度(万元)">
{{ formatAmount(detail.remainingFundLimit) }}
</el-descriptions-item>
</el-descriptions>
<div class="temporary-credit-limit-public-view__section-title">临时额度信息</div>
<el-descriptions :column="2" border>
<el-descriptions-item label="申请单号">{{ detail.applicationNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="申请临时额度(万元)">
{{ formatAmount(detail.applyLimit) }}
</el-descriptions-item>
<el-descriptions-item label="申请有效期至">{{ detail.validUntil || '-' }}</el-descriptions-item>
<el-descriptions-item label="审批状态">
{{ detail.approvalStatusName || detail.approvalStatus || '-' }}
</el-descriptions-item>
<el-descriptions-item label="申请人">{{ detail.applicantName || '-' }}</el-descriptions-item>
<el-descriptions-item label="申请部门">{{ detail.applyDeptName || '-' }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ detail.remark || '-' }}</el-descriptions-item>
</el-descriptions>
</template>
<el-empty v-else description="暂无数据" />
</div>
</mk-public-shell>
</template>
<script>
import { getMkPublicDetail } from '@/api/mk-process';
import MkPublicShell from '@/views/mk/mk-public-shell.vue';
export default {
name: 'TemporaryCreditLimitPublicView',
components: { MkPublicShell },
data() {
return {
detail: null,
};
},
mounted() {
this.loadDetail();
},
methods: {
loadDetail() {
const id = this.$route.query.id;
if (!id) return;
getMkPublicDetail('temporary-credit-limit', id).then(res => {
this.detail = res.data?.data || null;
});
},
formatAmount(value) {
if (value === undefined || value === null || value === '' || Number(value) === -1) {
return '-';
}
return value;
},
getForm() {
const detail = this.detail || {};
return {
...detail,
subject: detail.projectName || detail.applicationNo || '',
};
},
},
};
</script>
<style lang="scss" scoped>
.temporary-credit-limit-public-view {
padding: 16px 20px 24px;
&__title {
margin-bottom: 16px;
font-size: 18px;
font-weight: 600;
color: #303133;
}
&__section-title {
position: relative;
margin: 16px 0 12px;
padding-left: 12px;
font-size: 15px;
font-weight: 600;
color: #303133;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
width: 4px;
height: 14px;
margin-top: -7px;
background: #409eff;
}
}
}
</style>
+62 -3
View File
@@ -329,8 +329,10 @@ import {
getDetail as getProjectDetail,
getList as getProjectList,
} from '@/api/business/project-apply';
import { getMKApprovalUrl } from '@/api/system/business-process';
import { config, option } from '@/option/business/temporary-credit-limit';
import { getToken, isAdministrator } from '@/utils/auth';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { ElImageViewer } from 'element-plus';
import { OpenFileViewer } from '@open-file-viewer/vue';
@@ -426,6 +428,7 @@ export default {
flowBox: false,
flowUrl: '',
processInstanceId: '',
mkEnabled: false,
};
},
computed: {
@@ -455,6 +458,13 @@ export default {
},
created() {
this.loadProjectOptions();
loadMkEnabled()
.then(enabled => {
this.mkEnabled = !!enabled;
})
.catch(() => {
this.mkEnabled = false;
});
},
methods: {
buildTableOption() {
@@ -543,11 +553,19 @@ export default {
},
customOperations(row) {
return (this.config.operations || []).filter(operation => {
if (operation.mkOnly && !this.mkEnabled) return false;
if (operation.localOnly && this.mkEnabled) return false;
const permission = operation.permission || `${this.config.permission}_${operation.action}`;
const statusProp = operation.statusProp || this.config.statusProp || 'status';
// MK「流程」入口允许只读行查看审核页;本地审批/提交/撤回仍禁止只读
if (
row.readonly &&
['submitApproval', 'withdraw', 'approve', 'reject'].includes(operation.action)
) {
return false;
}
return (
this.hasPermission(permission) &&
!row.readonly &&
(!operation.status?.length || operation.status.includes(row[statusProp]))
);
});
@@ -736,6 +754,10 @@ export default {
});
},
handleOperation(operation, row) {
if (operation.action === 'mkAudit') {
this.handleMkAudit(row);
return;
}
if (operation.action === 'flow') {
this.openFlow(row);
return;
@@ -744,12 +766,49 @@ export default {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
const previousApprovalStatus = row.approvalStatus || '';
const request = () => this.api[operation.action](row.id);
// 先本地提交再调 MK,避免 MK 回写 reviewing 后本地 submitApproval 被编辑校验拦住
const afterMk =
operation.action === 'submitApproval'
? request().then(() =>
submitMkApprovalFlow({
bizType: 'temporary-credit-limit',
bizId: row.id,
subjectName: row.projectName || row.applicationNo || '',
approvalStatus: previousApprovalStatus,
mkEnabled: this.mkEnabled,
})
.then(() => this.api[operation.action](row.id))
.then(() => {
)
: request();
return afterMk.then(res => {
if (res?.data?.success === false) {
this.$message.error(res.data?.msg || `${operation.label}失败,请联系管理员`);
return;
}
this.$message.success(`${operation.label}成功`);
this.onLoad(this.page, this.query);
});
});
},
handleMkAudit(row) {
if (!row?.id) {
this.$message.warning('临时额度申请ID为空,无法打开审核页');
return;
}
getMKApprovalUrl({ bizId: String(row.id) })
.then(res => {
const url = res?.data?.data;
if (!url) {
this.$message.error('获取审核跳转地址失败');
return;
}
window.open(url, '_blank');
})
.catch(() => {
this.$message.error('获取审核跳转地址失败');
});
},
openFlow(row, loaded = false) {
const processInstanceId = row.processInstanceId || row.processInstanceID || row.procInstId;
@@ -0,0 +1,20 @@
<template>
<mk-public-shell biz-type="cert-audit-driver" :get-form="getForm">
<cert-audit-public-content ref="page" biz-type="driver" />
</mk-public-shell>
</template>
<script>
import MkPublicShell from '@/views/mk/mk-public-shell.vue';
import CertAuditPublicContent from './components/cert-audit-public-content/main.vue';
export default {
name: 'CertAuditDriverPublicView',
components: { MkPublicShell, CertAuditPublicContent },
methods: {
getForm() {
return this.$refs.page?.getForm?.() || {};
},
},
};
</script>
@@ -0,0 +1,20 @@
<template>
<mk-public-shell biz-type="cert-audit-vehicle" :get-form="getForm">
<cert-audit-public-content ref="page" biz-type="vehicle" />
</mk-public-shell>
</template>
<script>
import MkPublicShell from '@/views/mk/mk-public-shell.vue';
import CertAuditPublicContent from './components/cert-audit-public-content/main.vue';
export default {
name: 'CertAuditVehiclePublicView',
components: { MkPublicShell, CertAuditPublicContent },
methods: {
getForm() {
return this.$refs.page?.getForm?.() || {};
},
},
};
</script>
@@ -61,6 +61,14 @@
</section-card>
<section-card title="审核意见">
<el-alert
v-if="mkEnabled && auditable"
type="info"
:closable="false"
show-icon
title="已开启 MK 审核流:请在列表点击「提交 / 流程」完成审批,此处仅可查看"
class="cert-audit-detail__notice"
/>
<el-form label-position="right" label-width="90px" class="cert-audit-detail__form">
<el-form-item label="驳回原因">
<el-input
@@ -69,7 +77,7 @@
:rows="3"
maxlength="200"
show-word-limit
:disabled="!auditable"
:disabled="!localAuditable"
placeholder="驳回时必填,不超过 200 字;通过时无需填写"
/>
</el-form-item>
@@ -78,18 +86,20 @@
</div>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button @click="visible = false">{{ mkEnabled ? '关闭' : '取消' }}</el-button>
<el-button
v-if="!mkEnabled"
type="danger"
:disabled="!auditable || loadFailed"
:disabled="!localAuditable || loadFailed"
:loading="rejectLoading"
@click="handleReject"
>
驳回
</el-button>
<el-button
v-if="!mkEnabled"
type="primary"
:disabled="!auditable || loadFailed"
:disabled="!localAuditable || loadFailed"
:loading="approveLoading"
@click="handleApprove"
>
@@ -124,6 +134,11 @@ export default {
type: String,
default: 'driver',
},
// 开启 MK 后详情页仅查看,本地通过/驳回隐藏
mkEnabled: {
type: Boolean,
default: false,
},
},
emits: ['update:modelValue', 'success'],
data() {
@@ -166,6 +181,9 @@ export default {
auditable() {
return String(this.detail.status) === String(CERT_RECORD_STATUS.PENDING);
},
localAuditable() {
return this.auditable && !this.mkEnabled;
},
diffKey() {
return `${this.detail.id || this.recordId || ''}-${this.detail.submitTime || ''}`;
},
@@ -39,7 +39,21 @@
<template #menu="{ row }">
<!-- 只有审核中的记录能审;已通过/已驳回只能查看,弹窗内也会禁用审核按钮 -->
<el-link type="primary" v-if="canAudit" @click="openDetail(row)">
{{ isPending(row) ? '审核' : '查看' }}
{{ isPending(row) ? (mkEnabled ? '查看' : '审核') : '查看' }}
</el-link>
<el-link
v-if="canAudit && mkEnabled && isPending(row)"
type="primary"
@click="handleMkSubmit(row)"
>
提交
</el-link>
<el-link
v-if="canAudit && mkEnabled && isPending(row)"
type="primary"
@click="handleMkAudit(row)"
>
流程
</el-link>
</template>
</avue-crud>
@@ -48,6 +62,7 @@
v-model="detailVisible"
:record-id="detailRecordId"
:biz-type="bizType"
:mk-enabled="mkEnabled"
@success="handleAuditSuccess"
/>
</basic-container>
@@ -56,8 +71,14 @@
<script>
import { mapGetters } from 'vuex';
import { getPendingPage } from '@/api/transportCapacity/certification-audit';
import { getMKApprovalUrl } from '@/api/system/business-process';
import { CERT_RECORD_STATUS, certStatusTag, formatCertDateTime } from '@/utils/cert-audit';
import { isAdministrator } from '@/utils/auth';
import {
loadMkEnabled,
resolveCertAuditMkBizType,
submitMkApprovalFlow,
} from '@/utils/mk-approval';
import CertAuditDetail from '../cert-audit-detail/main.vue';
/**
@@ -111,6 +132,7 @@ export default {
],
detailVisible: false,
detailRecordId: '',
mkEnabled: false,
};
},
computed: {
@@ -122,12 +144,65 @@ export default {
return this.isAdmin || this.permission?.[this.auditPermissionCode] === true;
},
},
created() {
loadMkEnabled()
.then(enabled => {
this.mkEnabled = !!enabled;
})
.catch(() => {
this.mkEnabled = false;
});
},
methods: {
certStatusTag,
formatCertDateTime,
isPending(row) {
return String(row.status) === String(CERT_RECORD_STATUS.PENDING);
},
handleMkSubmit(row) {
if (!row?.id) {
this.$message.warning('认证记录ID为空,无法提交审核流');
return;
}
this.$confirm('确定提交该认证记录到 MK 审核流?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() =>
submitMkApprovalFlow({
bizType: resolveCertAuditMkBizType(this.bizType),
bizId: row.id,
subjectName: row.bizKey || row.driverName || '',
approvalStatus: String(row.status ?? ''),
mkEnabled: this.mkEnabled,
tempVarData: {
bizType: this.bizType,
bizKey: row.bizKey || '',
},
}).then(() => {
this.$message.success('提交成功');
this.onLoad(this.page, this.query);
})
);
},
handleMkAudit(row) {
if (!row?.id) {
this.$message.warning('认证记录ID为空,无法打开审核页');
return;
}
getMKApprovalUrl({ bizId: String(row.id) })
.then(res => {
const url = res?.data?.data;
if (!url) {
this.$message.error('获取审核跳转地址失败,请先提交审核流');
return;
}
window.open(url, '_blank');
})
.catch(() => {
this.$message.error('获取审核跳转地址失败');
});
},
onLoad(page, params = {}) {
this.loading = true;
getPendingPage(this.bizType, page.currentPage, page.pageSize, {
@@ -0,0 +1,124 @@
<template>
<div class="cert-audit-public-content">
<div class="cert-audit-public-content__title">{{ pageTitle }}</div>
<template v-if="detail">
<el-descriptions :column="2" border>
<el-descriptions-item label="认证对象">{{ detail.bizKey || '-' }}</el-descriptions-item>
<el-descriptions-item label="审核状态">
{{ detail.statusText || certStatusTag(detail.status).label }}
</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-item label="是否已认证">
{{ detail.certified ? '已认证' : '未认证' }}
</el-descriptions-item>
<el-descriptions-item v-if="detail.rejectReason" label="驳回原因" :span="2">
{{ detail.rejectReason }}
</el-descriptions-item>
</el-descriptions>
<div class="cert-audit-public-content__section-title">待审内容 vs 当前生效值</div>
<cert-audit-diff
:key="diffKey"
:biz-type="bizType"
:pending-json="detail.pendingSnapshotJson"
:effective-json="detail.effectiveSnapshotJson"
/>
</template>
<el-empty v-else description="暂无数据" />
</div>
</template>
<script>
import { getMkPublicDetail } from '@/api/mk-process';
import { certStatusTag, formatCertDateTime } from '@/utils/cert-audit';
import CertAuditDiff from '../cert-audit-diff/main.vue';
export default {
name: 'CertAuditPublicContent',
components: { CertAuditDiff },
props: {
bizType: {
type: String,
default: 'driver',
},
},
data() {
return {
detail: null,
};
},
computed: {
bizTypeLabel() {
return this.bizType === 'vehicle' ? '车辆认证' : '司机认证';
},
pageTitle() {
const key = this.detail?.bizKey ? `:${this.detail.bizKey}` : '';
return `${this.bizTypeLabel}审核${key}`;
},
mkBizType() {
return this.bizType === 'vehicle' ? 'cert-audit-vehicle' : 'cert-audit-driver';
},
diffKey() {
return `${this.detail?.id || ''}-${this.detail?.submitTime || ''}`;
},
},
mounted() {
this.loadDetail();
},
methods: {
certStatusTag,
formatCertDateTime,
loadDetail() {
const id = this.$route.query.id;
if (!id) return;
getMkPublicDetail(this.mkBizType, id).then(res => {
this.detail = res.data?.data || null;
});
},
getForm() {
const detail = this.detail || {};
return {
...detail,
subject: detail.bizKey || '',
};
},
},
};
</script>
<style lang="scss" scoped>
.cert-audit-public-content {
padding: 16px 20px 24px;
&__title {
margin-bottom: 16px;
font-size: 18px;
font-weight: 600;
color: #303133;
}
&__section-title {
position: relative;
margin: 16px 0 12px;
padding-left: 12px;
font-size: 15px;
font-weight: 600;
color: #303133;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
width: 4px;
height: 14px;
margin-top: -7px;
background: #409eff;
}
}
}
</style>