✨ 角色权限联动与临时额度/项目备案/司机车辆审核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 = [ export const changeTypeOptions = [
{ label: '项目变更', value: '项目变更' }, { label: '项目变更', value: '项目变更' },
{ label: '项目备案调整', value: '项目备案调整' }, { label: '项目备案流程(本部流程)', value: '项目备案流程(本部流程)' },
{ label: '项目备案流程(运输部流程)', value: '项目备案流程(运输部流程)' },
]; ];
export const config = { export const config = {
+10 -7
View File
@@ -85,13 +85,6 @@ export const config = {
status: ['draft', 'withdrawn', 'rejected'], status: ['draft', 'withdrawn', 'rejected'],
permission: 'temporary_credit_limit_submit', permission: 'temporary_credit_limit_submit',
}, },
{
action: 'flow',
label: '流程',
statusProp: 'approvalStatus',
status: ['reviewing'],
permission: 'temporary_credit_limit_view',
},
{ {
action: 'withdraw', action: 'withdraw',
label: '撤回', label: '撤回',
@@ -99,6 +92,14 @@ export const config = {
status: ['reviewing'], status: ['reviewing'],
permission: 'temporary_credit_limit_withdraw', permission: 'temporary_credit_limit_withdraw',
}, },
{
action: 'mkAudit',
label: '流程',
statusProp: 'approvalStatus',
status: ['reviewing'],
permission: 'temporary_credit_limit_approve',
mkOnly: true,
},
{ {
action: 'approve', action: 'approve',
label: '审批通过', label: '审批通过',
@@ -106,6 +107,7 @@ export const config = {
statusProp: 'approvalStatus', statusProp: 'approvalStatus',
status: ['reviewing'], status: ['reviewing'],
permission: 'temporary_credit_limit_approve', permission: 'temporary_credit_limit_approve',
localOnly: true,
}, },
{ {
action: 'reject', action: 'reject',
@@ -114,6 +116,7 @@ export const config = {
statusProp: 'approvalStatus', statusProp: 'approvalStatus',
status: ['reviewing'], status: ['reviewing'],
permission: 'temporary_credit_limit_reject', permission: 'temporary_credit_limit_reject',
localOnly: true,
}, },
], ],
}; };
+44
View File
@@ -106,6 +106,17 @@ export default [
bizType: 'project-change', 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', path: '/business/contract-manage/public-view',
name: '查看合同信息', name: '查看合同信息',
@@ -194,6 +205,39 @@ export default [
bizType: 'bill-payment', 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: '/', path: '/',
name: '主页', name: '主页',
+45
View File
@@ -21,6 +21,19 @@ export const MK_BIZ = {
rejected: ['rejected', 'withdrawn'], rejected: ['rejected', 'withdrawn'],
approved: ['approved'], approved: ['approved'],
}, },
/** 项目备案流程(本部/运输部)共用 */
'project-record-change': {
dictName: '提交项目备案审核流',
subjectPrefix: '项目备案审批',
rejected: ['rejected', 'withdrawn'],
approved: ['approved'],
},
'temporary-credit-limit': {
dictName: '提交临时额度审核流',
subjectPrefix: '临时额度审批',
rejected: ['rejected', 'withdrawn'],
approved: ['approved'],
},
'contract-manage': { 'contract-manage': {
dictName: '提交合同审核流', dictName: '提交合同审核流',
subjectPrefix: '合同审批', subjectPrefix: '合同审批',
@@ -69,10 +82,42 @@ export const MK_BIZ = {
rejected: ['returned'], rejected: ['returned'],
approved: ['approved'], 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'; 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 mkEnabledCache = null;
let mkEnabledPromise = null; let mkEnabledPromise = null;
+65 -42
View File
@@ -92,6 +92,7 @@
ref="treeMenu" ref="treeMenu"
:default-checked-keys="menuTreeObj" :default-checked-keys="menuTreeObj"
:props="props" :props="props"
@check="handleMenuCheck"
> >
</el-tree> </el-tree>
</el-tab-pane> </el-tab-pane>
@@ -102,7 +103,7 @@
style="margin-bottom: 12px; background: #f5f7fa; padding: 6px 10px; border-radius: 4px" style="margin-bottom: 12px; background: #f5f7fa; padding: 6px 10px; border-radius: 4px"
> >
<span style="color: #606266; font-size: 13px" <span style="color: #606266; font-size: 13px"
>仅展示菜单权限已勾选的菜单,已配置的数据权限会保留。勾选多个菜单后点击「设置」可批量配置</span >菜单权限勾选/反选会自动同步到此页;仅展示已勾选菜单,已配置的数据权限会保留。可再调整勾选后点击「设置」批量配置</span
> >
<el-button type="primary" size="small" @click="openDataScopeSetting">设置</el-button> <el-button type="primary" size="small" @click="openDataScopeSetting">设置</el-button>
</el-row> </el-row>
@@ -191,20 +192,8 @@
<el-radio :value="1">所在部门及下级</el-radio> <el-radio :value="1">所在部门及下级</el-radio>
<el-radio :value="2">全部组织</el-radio> <el-radio :value="2">全部组织</el-radio>
<el-radio :value="3">本人创建</el-radio> <el-radio :value="3">本人创建</el-radio>
<el-radio :value="4">自定义组织</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </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> </el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
@@ -228,7 +217,6 @@ import {
update, update,
} from '@/api/system/role'; } from '@/api/system/role';
import { getRoleMenuDataScopeTree, submitRoleMenuDataScope } from '@/api/system/roleDataScope'; import { getRoleMenuDataScopeTree, submitRoleMenuDataScope } from '@/api/system/roleDataScope';
import { getDeptTree } from '@/api/system/dept';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import website from '@/config/website'; import website from '@/config/website';
@@ -253,7 +241,6 @@ export default {
currentRoleName: '', currentRoleName: '',
roleDataScopeMenuTree: [], roleDataScopeMenuTree: [],
roleDataScopeMap: {}, roleDataScopeMap: {},
deptTree: [],
dataScopeSetting: { dataScopeSetting: {
roleName: '', roleName: '',
scopeType: 2, scopeType: 2,
@@ -430,6 +417,11 @@ export default {
if (!tree) return; if (!tree) return;
const allKeys = this.getAllNodeKeys(dataList); const allKeys = this.getAllNodeKeys(dataList);
tree.setCheckedKeys(allKeys); tree.setCheckedKeys(allKeys);
if (treeRef === 'treeMenu') {
this.$nextTick(() => {
this.syncDataScopeTreeFromMenu();
});
}
}, },
handleInvertSelect(treeRef, dataList, isLinked) { handleInvertSelect(treeRef, dataList, isLinked) {
const tree = this.$refs[treeRef]; const tree = this.$refs[treeRef];
@@ -444,6 +436,11 @@ export default {
const invertedKeys = allKeys.filter(key => !checkedKeys.has(key)); const invertedKeys = allKeys.filter(key => !checkedKeys.has(key));
tree.setCheckedKeys(invertedKeys); tree.setCheckedKeys(invertedKeys);
} }
if (treeRef === 'treeMenu') {
this.$nextTick(() => {
this.syncDataScopeTreeFromMenu();
});
}
}, },
handleLinkedChange(treeRef) { handleLinkedChange(treeRef) {
const tree = this.$refs[treeRef]; const tree = this.$refs[treeRef];
@@ -452,6 +449,9 @@ export default {
const halfCheckedKeys = tree.getHalfCheckedKeys(); const halfCheckedKeys = tree.getHalfCheckedKeys();
this.$nextTick(() => { this.$nextTick(() => {
tree.setCheckedKeys([...checkedKeys, ...halfCheckedKeys]); tree.setCheckedKeys([...checkedKeys, ...halfCheckedKeys]);
if (treeRef === 'treeMenu') {
this.syncDataScopeTreeFromMenu();
}
}); });
}, },
// el-tree 会把 setCheckedKeys 的入参记录为默认勾选集并在树数据重载时回放, // el-tree 会把 setCheckedKeys 的入参记录为默认勾选集并在树数据重载时回放,
@@ -461,6 +461,50 @@ export default {
if (!tree) return; if (!tree) return;
tree.setCheckedKeys(keys); 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() { submit() {
const menuList = this.$refs.treeMenu.getCheckedKeys(); const menuList = this.$refs.treeMenu.getCheckedKeys();
const apiScopeList = this.$refs.treeApiScope.getCheckedKeys(); const apiScopeList = this.$refs.treeApiScope.getCheckedKeys();
@@ -484,7 +528,7 @@ export default {
return getRoleMenuDataScopeTree(roleId).then(res => { return getRoleMenuDataScopeTree(roleId).then(res => {
const data = res.data.data || {}; const data = res.data.data || {};
this.currentRoleName = data.roleName || ''; this.currentRoleName = data.roleName || '';
this.roleDataScopeMenuTree = data.menuTree || []; // 菜单树由菜单权限勾选实时联动生成;这里只加载已配置的数据权限标签
const scopeMap = {}; const scopeMap = {};
(data.scopes || []).forEach(item => { (data.scopes || []).forEach(item => {
scopeMap[String(item.menuId)] = item; scopeMap[String(item.menuId)] = item;
@@ -507,20 +551,7 @@ export default {
scopeType: 2, scopeType: 2,
menuIds, menuIds,
}; };
const loadDept = this.dataScopeSettingBox = true;
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() { submitDataScopeSetting() {
const roleId = this.currentRoleIds && this.currentRoleIds[0]; const roleId = this.currentRoleIds && this.currentRoleIds[0];
@@ -529,27 +560,17 @@ export default {
return; return;
} }
const scopeType = Number(this.dataScopeSetting.scopeType || 2); 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({ submitRoleMenuDataScope({
roleId, roleId,
menuIds: this.dataScopeSetting.menuIds, menuIds: this.dataScopeSetting.menuIds,
scopeType, scopeType,
deptIds, deptIds: [],
}).then(() => { }).then(() => {
this.$message.success('数据权限设置成功'); this.$message.success('数据权限设置成功');
this.dataScopeSettingBox = false; this.dataScopeSettingBox = false;
this.loadRoleDataScopeTree(roleId).then(() => { this.loadRoleDataScopeTree(roleId).then(() => {
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.treeRoleDataScope) { this.syncDataScopeTreeFromMenu();
this.$refs.treeRoleDataScope.setCheckedKeys([]);
}
}); });
}); });
}); });
@@ -663,6 +684,7 @@ export default {
this.$nextTick(() => { this.$nextTick(() => {
this.applyCheckedKeys('treeMenu', this.menuTreeObj); this.applyCheckedKeys('treeMenu', this.menuTreeObj);
this.applyCheckedKeys('treeApiScope', this.apiScopeTreeObj); this.applyCheckedKeys('treeApiScope', this.apiScopeTreeObj);
this.syncDataScopeTreeFromMenu();
}); });
}); });
}, },
@@ -682,6 +704,7 @@ export default {
this.$nextTick(() => { this.$nextTick(() => {
this.applyCheckedKeys('treeMenu', this.menuTreeObj); this.applyCheckedKeys('treeMenu', this.menuTreeObj);
this.applyCheckedKeys('treeApiScope', this.apiScopeTreeObj); 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 { getToken, isAdministrator } from '@/utils/auth';
import { downloadFileByUrl, downloadXls } from '@/utils/util'; import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { getMKApprovalUrl } from '@/api/system/business-process'; 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 { ElImageViewer } from 'element-plus';
import { OpenFileViewer } from '@open-file-viewer/vue'; import { OpenFileViewer } from '@open-file-viewer/vue';
import PdfPreview from '@/components/pdf-preview/main.vue'; import PdfPreview from '@/components/pdf-preview/main.vue';
@@ -1353,7 +1357,8 @@ const optionalSentinelFields = [
const changeTypeOptions = [ const changeTypeOptions = [
{ label: '项目变更', value: '项目变更' }, { label: '项目变更', value: '项目变更' },
{ label: '项目备案调整', value: '项目备案调整' }, { label: '项目备案流程(本部流程)', value: '项目备案流程(本部流程)' },
{ label: '项目备案流程(运输部流程)', value: '项目备案流程(运输部流程)' },
]; ];
const attachmentViewerPlugins = [ const attachmentViewerPlugins = [
@@ -1756,12 +1761,16 @@ export default {
isPublicViewPage() { isPublicViewPage() {
return ( return (
this.$route.path === '/business/project-apply/public-view' || 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() { publicBizType() {
return this.$route.meta?.bizType || 'project-apply'; return this.$route.meta?.bizType || 'project-apply';
}, },
isPublicProjectChangeView() {
return ['project-change', 'project-record-change'].includes(this.publicBizType);
},
projectFormContainer() { projectFormContainer() {
// 表单页实例始终用页面容器,避免 keep-alive 失活时误切弹窗 // 表单页实例始终用页面容器,避免 keep-alive 失活时误切弹窗
return this.isFormPageInstance ? 'div' : 'el-dialog'; return this.isFormPageInstance ? 'div' : 'el-dialog';
@@ -1900,7 +1909,7 @@ export default {
}, },
openPublicProjectForm() { openPublicProjectForm() {
const id = this.$route.query.id; const id = this.$route.query.id;
this.dialogType = this.publicBizType === 'project-change' ? 'change' : 'view'; this.dialogType = this.isPublicProjectChangeView ? 'change' : 'view';
this.dialogReadonly = true; this.dialogReadonly = true;
this.projectBox = true; this.projectBox = true;
if (!id) { if (!id) {
@@ -2535,10 +2544,20 @@ export default {
}, },
resolveChangeType(row = {}) { resolveChangeType(row = {}) {
if (row.changeType) return row.changeType; if (row.changeType) return row.changeType;
const node = row.currentNode || ''; const node = String(row.currentNode || '');
if (String(node).includes('备案')) return '项目备案调整'; if (node.includes('运输部')) return '项目备案流程(运输部流程)';
if (node.includes('本部') || node.includes('备案')) return '项目备案流程(本部流程)';
return '项目变更'; return '项目变更';
}, },
buildProjectChangeMkTempVarData(row = {}) {
const changeType = this.resolveChangeType(row);
return {
projectType: row.projectType || this.form.projectType || '',
// MK 临时变量:变更类型(本部/运输部备案分流)
changeType,
变更类型: changeType,
};
},
submitProject() { submitProject() {
this.$refs.projectForm.validate(valid => { this.$refs.projectForm.validate(valid => {
if (!valid) { if (!valid) {
@@ -2681,6 +2700,7 @@ export default {
if (this.isProjectChangeModule) { if (this.isProjectChangeModule) {
const payload = this.normalizeSubmitForm({ includeChangeType: true }); const payload = this.normalizeSubmitForm({ includeChangeType: true });
const approvalStatus = this.form.approvalStatus || ''; const approvalStatus = this.form.approvalStatus || '';
const changeType = this.resolveChangeType(this.form);
projectChangeApi projectChangeApi
.submit(payload) .submit(payload)
.then(res => { .then(res => {
@@ -2690,12 +2710,15 @@ export default {
} }
const id = this.form.id; const id = this.form.id;
return submitMkApprovalFlow({ return submitMkApprovalFlow({
bizType: 'project-change', bizType: resolveProjectChangeMkBizType(changeType),
bizId: id, bizId: id,
subjectName: this.form.projectName || '', subjectName: this.form.projectName || '',
approvalStatus, approvalStatus,
mkEnabled: this.mkEnabled, mkEnabled: this.mkEnabled,
tempVarData: { projectType: this.form.projectType || '' }, tempVarData: this.buildProjectChangeMkTempVarData({
...this.form,
changeType,
}),
}).then(() => { }).then(() => {
this.$message.success('提交成功!'); this.$message.success('提交成功!');
this.closeProjectForm(); this.closeProjectForm();
@@ -2715,13 +2738,17 @@ export default {
return; return;
} }
const id = this.form.id; const id = this.form.id;
const changeType = this.resolveChangeType(this.form);
return submitMkApprovalFlow({ return submitMkApprovalFlow({
bizType: 'project-apply', bizType: 'project-apply',
bizId: id, bizId: id,
subjectName: this.form.projectName || '', subjectName: this.form.projectName || '',
approvalStatus: this.form.approvalStatus || 'change_rejected', approvalStatus: this.form.approvalStatus || 'change_rejected',
mkEnabled: this.mkEnabled, mkEnabled: this.mkEnabled,
tempVarData: { projectType: this.form.projectType || '' }, tempVarData: this.buildProjectChangeMkTempVarData({
...this.form,
changeType,
}),
}).then(() => { }).then(() => {
this.$message.success('提交成功!'); this.$message.success('提交成功!');
this.closeProjectForm(); this.closeProjectForm();
+16 -7
View File
@@ -75,7 +75,11 @@ import * as api from '@/api/business/project-change';
import { getMKApprovalUrl } from '@/api/system/business-process'; import { getMKApprovalUrl } from '@/api/system/business-process';
import { config, option } from '@/option/business/project-change'; import { config, option } from '@/option/business/project-change';
import { approvalStatusOptions } from '@/option/business/common'; 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 { isAdministrator } from '@/utils/auth';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
@@ -217,16 +221,21 @@ export default {
// 先本地提交再调 MK,避免 MK 回写 reviewing 后本地 submitApproval 被编辑校验拦住。 // 先本地提交再调 MK,避免 MK 回写 reviewing 后本地 submitApproval 被编辑校验拦住。
const afterMk = const afterMk =
operation.action === 'submitApproval' operation.action === 'submitApproval'
? request().then(() => ? request().then(() => {
submitMkApprovalFlow({ const changeType = row.changeType || '项目变更';
bizType: 'project-change', return submitMkApprovalFlow({
bizType: resolveProjectChangeMkBizType(changeType),
bizId: row.id, bizId: row.id,
subjectName: row.projectName || '', subjectName: row.projectName || '',
approvalStatus: row.approvalStatus || '', approvalStatus: row.approvalStatus || '',
mkEnabled: this.mkEnabled, mkEnabled: this.mkEnabled,
tempVarData: { projectType: row.projectType || '' }, tempVarData: {
}) projectType: row.projectType || '',
) changeType,
变更类型: changeType,
},
});
})
: request(); : request();
const loading = const loading =
operation.action === 'approve' 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>
+63 -4
View File
@@ -329,8 +329,10 @@ import {
getDetail as getProjectDetail, getDetail as getProjectDetail,
getList as getProjectList, getList as getProjectList,
} from '@/api/business/project-apply'; } from '@/api/business/project-apply';
import { getMKApprovalUrl } from '@/api/system/business-process';
import { config, option } from '@/option/business/temporary-credit-limit'; import { config, option } from '@/option/business/temporary-credit-limit';
import { getToken, isAdministrator } from '@/utils/auth'; import { getToken, isAdministrator } from '@/utils/auth';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
import { downloadFileByUrl, downloadXls } from '@/utils/util'; import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { ElImageViewer } from 'element-plus'; import { ElImageViewer } from 'element-plus';
import { OpenFileViewer } from '@open-file-viewer/vue'; import { OpenFileViewer } from '@open-file-viewer/vue';
@@ -426,6 +428,7 @@ export default {
flowBox: false, flowBox: false,
flowUrl: '', flowUrl: '',
processInstanceId: '', processInstanceId: '',
mkEnabled: false,
}; };
}, },
computed: { computed: {
@@ -455,6 +458,13 @@ export default {
}, },
created() { created() {
this.loadProjectOptions(); this.loadProjectOptions();
loadMkEnabled()
.then(enabled => {
this.mkEnabled = !!enabled;
})
.catch(() => {
this.mkEnabled = false;
});
}, },
methods: { methods: {
buildTableOption() { buildTableOption() {
@@ -543,11 +553,19 @@ export default {
}, },
customOperations(row) { customOperations(row) {
return (this.config.operations || []).filter(operation => { 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 permission = operation.permission || `${this.config.permission}_${operation.action}`;
const statusProp = operation.statusProp || this.config.statusProp || 'status'; const statusProp = operation.statusProp || this.config.statusProp || 'status';
// MK「流程」入口允许只读行查看审核页;本地审批/提交/撤回仍禁止只读
if (
row.readonly &&
['submitApproval', 'withdraw', 'approve', 'reject'].includes(operation.action)
) {
return false;
}
return ( return (
this.hasPermission(permission) && this.hasPermission(permission) &&
!row.readonly &&
(!operation.status?.length || operation.status.includes(row[statusProp])) (!operation.status?.length || operation.status.includes(row[statusProp]))
); );
}); });
@@ -736,6 +754,10 @@ export default {
}); });
}, },
handleOperation(operation, row) { handleOperation(operation, row) {
if (operation.action === 'mkAudit') {
this.handleMkAudit(row);
return;
}
if (operation.action === 'flow') { if (operation.action === 'flow') {
this.openFlow(row); this.openFlow(row);
return; return;
@@ -744,12 +766,49 @@ export default {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning', type: 'warning',
}) }).then(() => {
.then(() => this.api[operation.action](row.id)) const previousApprovalStatus = row.approvalStatus || '';
.then(() => { 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,
})
)
: 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.$message.success(`${operation.label}成功`);
this.onLoad(this.page, this.query); 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) { openFlow(row, loaded = false) {
const processInstanceId = row.processInstanceId || row.processInstanceID || row.procInstId; 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>
<section-card title="审核意见"> <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 label-position="right" label-width="90px" class="cert-audit-detail__form">
<el-form-item label="驳回原因"> <el-form-item label="驳回原因">
<el-input <el-input
@@ -69,7 +77,7 @@
:rows="3" :rows="3"
maxlength="200" maxlength="200"
show-word-limit show-word-limit
:disabled="!auditable" :disabled="!localAuditable"
placeholder="驳回时必填,不超过 200 字;通过时无需填写" placeholder="驳回时必填,不超过 200 字;通过时无需填写"
/> />
</el-form-item> </el-form-item>
@@ -78,18 +86,20 @@
</div> </div>
<template #footer> <template #footer>
<el-button @click="visible = false">取消</el-button> <el-button @click="visible = false">{{ mkEnabled ? '关闭' : '取消' }}</el-button>
<el-button <el-button
v-if="!mkEnabled"
type="danger" type="danger"
:disabled="!auditable || loadFailed" :disabled="!localAuditable || loadFailed"
:loading="rejectLoading" :loading="rejectLoading"
@click="handleReject" @click="handleReject"
> >
驳回 驳回
</el-button> </el-button>
<el-button <el-button
v-if="!mkEnabled"
type="primary" type="primary"
:disabled="!auditable || loadFailed" :disabled="!localAuditable || loadFailed"
:loading="approveLoading" :loading="approveLoading"
@click="handleApprove" @click="handleApprove"
> >
@@ -124,6 +134,11 @@ export default {
type: String, type: String,
default: 'driver', default: 'driver',
}, },
// 开启 MK 后详情页仅查看,本地通过/驳回隐藏
mkEnabled: {
type: Boolean,
default: false,
},
}, },
emits: ['update:modelValue', 'success'], emits: ['update:modelValue', 'success'],
data() { data() {
@@ -166,6 +181,9 @@ export default {
auditable() { auditable() {
return String(this.detail.status) === String(CERT_RECORD_STATUS.PENDING); return String(this.detail.status) === String(CERT_RECORD_STATUS.PENDING);
}, },
localAuditable() {
return this.auditable && !this.mkEnabled;
},
diffKey() { diffKey() {
return `${this.detail.id || this.recordId || ''}-${this.detail.submitTime || ''}`; return `${this.detail.id || this.recordId || ''}-${this.detail.submitTime || ''}`;
}, },
@@ -39,7 +39,21 @@
<template #menu="{ row }"> <template #menu="{ row }">
<!-- 只有审核中的记录能审;已通过/已驳回只能查看,弹窗内也会禁用审核按钮 --> <!-- 只有审核中的记录能审;已通过/已驳回只能查看,弹窗内也会禁用审核按钮 -->
<el-link type="primary" v-if="canAudit" @click="openDetail(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> </el-link>
</template> </template>
</avue-crud> </avue-crud>
@@ -48,6 +62,7 @@
v-model="detailVisible" v-model="detailVisible"
:record-id="detailRecordId" :record-id="detailRecordId"
:biz-type="bizType" :biz-type="bizType"
:mk-enabled="mkEnabled"
@success="handleAuditSuccess" @success="handleAuditSuccess"
/> />
</basic-container> </basic-container>
@@ -56,8 +71,14 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { getPendingPage } from '@/api/transportCapacity/certification-audit'; 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 { CERT_RECORD_STATUS, certStatusTag, formatCertDateTime } from '@/utils/cert-audit';
import { isAdministrator } from '@/utils/auth'; import { isAdministrator } from '@/utils/auth';
import {
loadMkEnabled,
resolveCertAuditMkBizType,
submitMkApprovalFlow,
} from '@/utils/mk-approval';
import CertAuditDetail from '../cert-audit-detail/main.vue'; import CertAuditDetail from '../cert-audit-detail/main.vue';
/** /**
@@ -111,6 +132,7 @@ export default {
], ],
detailVisible: false, detailVisible: false,
detailRecordId: '', detailRecordId: '',
mkEnabled: false,
}; };
}, },
computed: { computed: {
@@ -122,12 +144,65 @@ export default {
return this.isAdmin || this.permission?.[this.auditPermissionCode] === true; return this.isAdmin || this.permission?.[this.auditPermissionCode] === true;
}, },
}, },
created() {
loadMkEnabled()
.then(enabled => {
this.mkEnabled = !!enabled;
})
.catch(() => {
this.mkEnabled = false;
});
},
methods: { methods: {
certStatusTag, certStatusTag,
formatCertDateTime, formatCertDateTime,
isPending(row) { isPending(row) {
return String(row.status) === String(CERT_RECORD_STATUS.PENDING); 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 = {}) { onLoad(page, params = {}) {
this.loading = true; this.loading = true;
getPendingPage(this.bizType, page.currentPage, page.pageSize, { 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>