钉钉审批配置优化
This commit is contained in:
@@ -9,6 +9,9 @@
|
||||
<!-- <j-upload-button type="primary" preIcon="ant-design:import-outlined" @click="onImportXls" v-auth="'system:user:import'">导入</j-upload-button>-->
|
||||
<import-excel-progress :upload-url="getImportUrl" @success="reload"></import-excel-progress>
|
||||
<a-button type="primary" @click="openModal(true, {})" preIcon="ant-design:hdd-outlined"> 回收站</a-button>
|
||||
<!--update-begin---author:GHT ---date:2026-06-08 for:【XSLMES-20260608】同步钉钉ID按钮-->
|
||||
<a-button type="default" preIcon="ant-design:sync-outlined" :loading="syncDingLoading" @click="handleSyncDingUserId"> 同步钉钉ID</a-button>
|
||||
<!--update-end---author:GHT ---date:2026-06-08 for:【XSLMES-20260608】同步钉钉ID按钮-->
|
||||
<a-dropdown v-if="selectedRowKeys.length > 0">
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
@@ -66,12 +69,33 @@
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { columns, searchFormSchema } from './user.data';
|
||||
import { listNoCareTenant, deleteUser, batchDeleteUser, getImportUrl, getExportUrl, frozenBatch, resetPassword } from './user.api';
|
||||
import { listNoCareTenant, deleteUser, batchDeleteUser, getImportUrl, getExportUrl, frozenBatch, resetPassword, syncDingUserId } from './user.api';
|
||||
import { usePermission } from '/@/hooks/web/usePermission';
|
||||
import ImportExcelProgress from './components/ImportExcelProgress.vue';
|
||||
|
||||
const { createMessage, createConfirm } = useMessage();
|
||||
const { isDisabledAuth, hasPermission } = usePermission();
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-08 for:【XSLMES-20260608】同步钉钉ID-----------
|
||||
const syncDingLoading = ref(false);
|
||||
async function handleSyncDingUserId() {
|
||||
syncDingLoading.value = true;
|
||||
try {
|
||||
const res: any = await syncDingUserId();
|
||||
const { successCount, failCount, failDetails } = res;
|
||||
let content = `同步完成:成功 ${successCount} 人,未匹配 ${failCount} 人`;
|
||||
if (failDetails && failDetails.length > 0) {
|
||||
content += `\n\n未匹配用户:${failDetails.join('、')}`;
|
||||
}
|
||||
createMessage.info(content);
|
||||
reload();
|
||||
} catch (e) {
|
||||
createMessage.error('同步钉钉ID失败');
|
||||
} finally {
|
||||
syncDingLoading.value = false;
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-06-08 for:【XSLMES-20260608】同步钉钉ID-----------
|
||||
|
||||
//注册drawer
|
||||
const [registerDrawer, { openDrawer }] = useDrawer();
|
||||
|
||||
@@ -6,6 +6,9 @@ enum Api {
|
||||
list = '/sys/user/list',
|
||||
save = '/sys/user/add',
|
||||
edit = '/sys/user/edit',
|
||||
//update-begin---author:GHT ---date:2026-06-08 for:【XSLMES-20260608】同步钉钉ID-----------
|
||||
syncDingUserId = '/sys/user/syncDingUserId',
|
||||
//update-end---author:GHT ---date:2026-06-08 for:【XSLMES-20260608】同步钉钉ID-----------
|
||||
getUserRole = '/sys/user/queryUserRole',
|
||||
duplicateCheck = '/sys/duplicate/check',
|
||||
deleteUser = '/sys/user/delete',
|
||||
@@ -241,9 +244,16 @@ export const updateUserTenantStatus = (params) => {
|
||||
|
||||
/**
|
||||
* 根据部门id和已选中的部门岗位id获取部门下的岗位id
|
||||
*
|
||||
*
|
||||
* @param params
|
||||
*/
|
||||
export const getDepPostIdByDepId = (params) => {
|
||||
return defHttp.get({ url: Api.getDepPostIdByDepId, params },{ isTransformResponse: false });
|
||||
};
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-08 for:【XSLMES-20260608】同步钉钉ID接口-----------
|
||||
/**
|
||||
* 批量同步钉钉ID(通过手机号查询钉钉userId回写到用户表)
|
||||
*/
|
||||
export const syncDingUserId = () => defHttp.post({ url: Api.syncDingUserId });
|
||||
//update-end---author:GHT ---date:2026-06-08 for:【XSLMES-20260608】同步钉钉ID接口-----------
|
||||
|
||||
@@ -104,6 +104,14 @@ export const columns: BasicColumn[] = [
|
||||
width: 80,
|
||||
resizable: true,
|
||||
},
|
||||
//update-begin---author:GHT ---date:2026-06-08 for:【XSLMES-20260608】新增钉钉ID列-----------
|
||||
{
|
||||
title: '钉钉ID',
|
||||
dataIndex: 'dingUserId',
|
||||
width: 120,
|
||||
resizable: true,
|
||||
},
|
||||
//update-end---author:GHT ---date:2026-06-08 for:【XSLMES-20260608】新增钉钉ID列-----------
|
||||
];
|
||||
|
||||
export const recycleColumns: BasicColumn[] = [
|
||||
|
||||
@@ -4,6 +4,9 @@ enum Api {
|
||||
list = '/xslmes/mesXslApprovalTrace/list',
|
||||
queryById = '/xslmes/mesXslApprovalTrace/queryById',
|
||||
queryByBiz = '/xslmes/mesXslApprovalTrace/queryByBiz',
|
||||
dingFlowRecords = '/xslmes/mesXslApprovalTrace/dingFlowRecords',
|
||||
dingProcessForecast = '/xslmes/mesXslApprovalTrace/dingProcessForecast',
|
||||
dingProcessInstance = '/xslmes/mesXslApprovalTrace/dingProcessInstance',
|
||||
}
|
||||
|
||||
export const list = (params) => defHttp.get({ url: Api.list, params });
|
||||
@@ -13,3 +16,24 @@ export const queryById = (params: { id: string }) => defHttp.get({ url: Api.quer
|
||||
/** 按业务表 + 单据ID 查询痕迹(供业务页关联展示) */
|
||||
export const queryByBiz = (params: { bizTable: string; bizDataId: string }) =>
|
||||
defHttp.get({ url: Api.queryByBiz, params });
|
||||
|
||||
/** 拉取钉钉审批实例操作记录(时间轴) */
|
||||
export const getDingFlowRecords = (params: {
|
||||
bizTable?: string;
|
||||
bizDataId?: string;
|
||||
processInstanceId?: string;
|
||||
}) => defHttp.get({ url: Api.dingFlowRecords, params });
|
||||
|
||||
/** 从审批实例 tasks 解析审批节点 */
|
||||
export const getDingProcessForecast = (params: {
|
||||
bizTable?: string;
|
||||
bizDataId?: string;
|
||||
processInstanceId?: string;
|
||||
}) => defHttp.get({ url: Api.dingProcessForecast, params });
|
||||
|
||||
/** 拉取钉钉审批实例接口原始 JSON 响应 */
|
||||
export const getDingProcessInstance = (params: {
|
||||
bizTable?: string;
|
||||
bizDataId?: string;
|
||||
processInstanceId?: string;
|
||||
}) => defHttp.get({ url: Api.dingProcessInstance, params });
|
||||
|
||||
@@ -17,8 +17,27 @@ export const searchFormSchema: FormSchema[] = [
|
||||
{ label: '单据ID', field: 'bizDataId', component: 'JInput', colProps: { span: 8 } },
|
||||
];
|
||||
|
||||
const externalInstanceIdColumn: BasicColumn = {
|
||||
title: '钉钉审批流ID',
|
||||
dataIndex: 'externalInstanceId',
|
||||
width: 220,
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
};
|
||||
|
||||
/** 注册中心抽屉内明细列表(已按业务表过滤,不重复展示业务表列) */
|
||||
export const drawerColumns: BasicColumn[] = columns.filter((col) => col.dataIndex !== 'bizTable');
|
||||
export const drawerColumns: BasicColumn[] = [
|
||||
{
|
||||
title: '审批操作',
|
||||
dataIndex: 'flowRecord',
|
||||
width: 380,
|
||||
fixed: 'left',
|
||||
slots: { customRender: 'flowRecord' },
|
||||
},
|
||||
...columns
|
||||
.filter((col) => col.dataIndex !== 'bizTable')
|
||||
.flatMap((col) => (col.dataIndex === 'bizDataId' ? [col, externalInstanceIdColumn] : [col])),
|
||||
];
|
||||
|
||||
export const drawerSearchFormSchema: FormSchema[] = [
|
||||
{ label: '单据ID', field: 'bizDataId', component: 'JInput', colProps: { span: 12 } },
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
<template>
|
||||
<BasicModal
|
||||
v-bind="$attrs"
|
||||
@register="registerModal"
|
||||
:title="modalTitle"
|
||||
width="720px"
|
||||
:showOkBtn="false"
|
||||
cancelText="关闭"
|
||||
destroyOnClose
|
||||
>
|
||||
<a-spin :spinning="loading">
|
||||
<div class="ding-flow-timeline">
|
||||
<a-descriptions v-if="flowInfo.processInstanceId" :column="2" size="small" bordered class="ding-flow-head">
|
||||
<a-descriptions-item label="钉钉审批流ID" :span="2">{{ flowInfo.processInstanceId }}</a-descriptions-item>
|
||||
<a-descriptions-item label="审批标题" :span="2">{{ flowInfo.title || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="单据ID">{{ flowInfo.bizDataId || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="实例状态">
|
||||
<a-tag :color="statusColor">{{ statusText }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<div class="ding-flow-section-title">审批流转记录</div>
|
||||
<a-empty v-if="!loading && !records.length" description="暂无操作记录" />
|
||||
<a-timeline v-else class="ding-flow-timeline-list">
|
||||
<a-timeline-item v-for="(item, index) in records" :key="index" :color="timelineColor(item)">
|
||||
<div class="ding-flow-line">
|
||||
<b>{{ operatorText(item) }}</b>
|
||||
<span class="ding-flow-role">{{ nodeTitle(item) }}</span>
|
||||
<a-tag v-if="resultText(item.result)" :color="resultColor(item.result)" size="small">
|
||||
{{ resultText(item.result) }}
|
||||
</a-tag>
|
||||
<span class="ding-flow-time">{{ formatDate(item.date) }}</span>
|
||||
</div>
|
||||
<div v-if="item.remark" class="ding-flow-remark">意见:{{ item.remark }}</div>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
</div>
|
||||
</a-spin>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { getDingFlowRecords } from '../MesXslApprovalTrace.api';
|
||||
|
||||
defineOptions({ name: 'DingApprovalFlowTimelineModal' });
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const loading = ref(false);
|
||||
const flowInfo = ref<Recordable>({});
|
||||
const records = ref<Recordable[]>([]);
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
const title = flowInfo.value?.title;
|
||||
return title ? `审批流转记录 — ${title}` : '审批流转记录';
|
||||
});
|
||||
|
||||
const statusColor = computed(() => {
|
||||
const status = String(flowInfo.value?.status || '').toUpperCase();
|
||||
if (status === 'COMPLETED') return 'green';
|
||||
if (status === 'TERMINATED') return 'red';
|
||||
if (status === 'RUNNING') return 'blue';
|
||||
return 'default';
|
||||
});
|
||||
|
||||
const statusText = computed(() => {
|
||||
const status = String(flowInfo.value?.status || '').toUpperCase();
|
||||
const map: Record<string, string> = {
|
||||
RUNNING: '审批中',
|
||||
COMPLETED: '已完成',
|
||||
TERMINATED: '已终止',
|
||||
};
|
||||
return map[status] || flowInfo.value?.status || '-';
|
||||
});
|
||||
|
||||
const [registerModal, { setModalProps }] = useModalInner(async (data) => {
|
||||
flowInfo.value = {};
|
||||
records.value = [];
|
||||
const record = data?.record || {};
|
||||
const bizTable = record.bizTable || data?.bizTable;
|
||||
const bizDataId = record.bizDataId;
|
||||
const processInstanceId = record.externalInstanceId;
|
||||
if (!processInstanceId && (!bizTable || !bizDataId)) {
|
||||
createMessage.warning('缺少单据ID或钉钉审批流ID,无法查询流转记录');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
loading.value = true;
|
||||
setModalProps({ confirmLoading: false });
|
||||
const res = await getDingFlowRecords({ bizTable, bizDataId, processInstanceId });
|
||||
flowInfo.value = res || {};
|
||||
records.value = res?.operationRecords || [];
|
||||
} catch (e: any) {
|
||||
createMessage.error(e?.message || '获取审批流转记录失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
function nodeTitle(item: Recordable) {
|
||||
const showName = String(item?.showName || '').trim();
|
||||
if (showName && showName.toUpperCase() !== 'UNKNOWN') {
|
||||
return showName;
|
||||
}
|
||||
return typeText(item?.type);
|
||||
}
|
||||
|
||||
function operatorText(item: Recordable) {
|
||||
return item?.userName || item?.userId || '-';
|
||||
}
|
||||
|
||||
function formatDate(value?: string) {
|
||||
if (!value) return '-';
|
||||
const text = String(value).replace('T', ' ').replace('Z', '');
|
||||
return text.length > 19 ? text.substring(0, 19) : text;
|
||||
}
|
||||
|
||||
function typeText(type?: string) {
|
||||
const map: Record<string, string> = {
|
||||
START_PROCESS: '发起审批',
|
||||
EXECUTE_TASK_NORMAL: '审批',
|
||||
EXECUTE_TASK_AGENT: '代办审批',
|
||||
REDIRECT_PROCESS: '退回',
|
||||
PROCESS_CC: '抄送',
|
||||
ADD_REMARK: '评论',
|
||||
TERMINATE_PROCESS_INSTANCE: '撤销',
|
||||
FINISH_PROCESS_INSTANCE: '结束审批',
|
||||
};
|
||||
return map[String(type || '')] || type || '操作';
|
||||
}
|
||||
|
||||
function resultText(result?: string) {
|
||||
if (!result) return '';
|
||||
const map: Record<string, string> = {
|
||||
AGREE: '同意',
|
||||
REFUSE: '拒绝',
|
||||
REDIRECTED: '转交',
|
||||
NONE: '',
|
||||
};
|
||||
const text = map[String(result).toUpperCase()] ?? result;
|
||||
return text || '';
|
||||
}
|
||||
|
||||
function resultColor(result?: string) {
|
||||
const val = String(result || '').toUpperCase();
|
||||
if (val === 'AGREE') return 'green';
|
||||
if (val === 'REFUSE') return 'red';
|
||||
if (val === 'REDIRECTED') return 'orange';
|
||||
return 'default';
|
||||
}
|
||||
|
||||
function timelineColor(item: Recordable) {
|
||||
const result = String(item?.result || '').toUpperCase();
|
||||
if (result === 'REFUSE') return 'red';
|
||||
if (result === 'AGREE') return 'green';
|
||||
if (String(item?.type || '') === 'REDIRECT_PROCESS') return 'orange';
|
||||
return 'blue';
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ding-flow-head {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.ding-flow-section-title {
|
||||
margin: 12px 0 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
border-left: 3px solid #1677ff;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.ding-flow-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.ding-flow-role {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.ding-flow-time {
|
||||
margin-left: auto;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ding-flow-remark,
|
||||
.ding-flow-type {
|
||||
margin-top: 4px;
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<BasicModal
|
||||
v-bind="$attrs"
|
||||
@register="registerModal"
|
||||
title="审批实例节点"
|
||||
width="960px"
|
||||
:showOkBtn="false"
|
||||
cancelText="关闭"
|
||||
destroyOnClose
|
||||
>
|
||||
<a-spin :spinning="loading">
|
||||
<a-descriptions v-if="forecastInfo.processInstanceId" :column="2" size="small" bordered class="ding-forecast-head">
|
||||
<a-descriptions-item label="MES审批流">{{ forecastInfo.mesFlowName || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="节点来源">{{ forecastInfo.nodeSource || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="钉钉模板" :span="2">{{ forecastInfo.templateName || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="processCode" :span="2">{{ forecastInfo.processCode }}</a-descriptions-item>
|
||||
<a-descriptions-item label="审批实例ID" :span="2">{{ forecastInfo.processInstanceId || '-' }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="nodes"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
bordered
|
||||
row-key="stepNo"
|
||||
:locale="{ emptyText: '暂无审批节点' }"
|
||||
/>
|
||||
</a-spin>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { getDingProcessForecast } from '../MesXslApprovalTrace.api';
|
||||
|
||||
defineOptions({ name: 'DingApprovalForecastModal' });
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const loading = ref(false);
|
||||
const forecastInfo = ref<Recordable>({});
|
||||
const nodes = ref<Recordable[]>([]);
|
||||
|
||||
const columns = [
|
||||
{ title: '序号', dataIndex: 'stepNo', width: 70, align: 'center' },
|
||||
{ title: 'activityId', dataIndex: 'activityId', width: 120, ellipsis: true },
|
||||
{ title: 'MES节点', dataIndex: 'mesNodeName', width: 120, ellipsis: true },
|
||||
{ title: '钉钉节点', dataIndex: 'activityName', width: 120, ellipsis: true },
|
||||
{ title: '节点状态', dataIndex: 'nodeStatusText', width: 90, align: 'center' },
|
||||
{ title: '审批方式', dataIndex: 'approvalMethodText', width: 100, align: 'center' },
|
||||
{
|
||||
title: '审批人',
|
||||
dataIndex: 'actionerNames',
|
||||
customRender: ({ record }) => formatActioners(record),
|
||||
},
|
||||
];
|
||||
|
||||
const [registerModal, { setModalProps }] = useModalInner(async (data) => {
|
||||
forecastInfo.value = {};
|
||||
nodes.value = [];
|
||||
const record = data?.record || {};
|
||||
const bizTable = record.bizTable || data?.bizTable;
|
||||
const bizDataId = record.bizDataId;
|
||||
const processInstanceId = record.externalInstanceId;
|
||||
if (!processInstanceId && (!bizTable || !bizDataId)) {
|
||||
createMessage.warning('缺少单据ID或钉钉审批流ID,无法获取审批节点');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
loading.value = true;
|
||||
setModalProps({ confirmLoading: false });
|
||||
const res = await getDingProcessForecast({ bizTable, bizDataId, processInstanceId });
|
||||
forecastInfo.value = res || {};
|
||||
nodes.value = res?.nodes || [];
|
||||
} catch (e: any) {
|
||||
createMessage.error(e?.message || '获取审批节点失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
function formatActioners(record: Recordable) {
|
||||
const names = record?.actionerNames;
|
||||
if (Array.isArray(names) && names.length) {
|
||||
return names.join('、');
|
||||
}
|
||||
const ids = record?.actionerUserIds;
|
||||
if (Array.isArray(ids) && ids.length) {
|
||||
return ids.join('、');
|
||||
}
|
||||
return '-';
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ding-forecast-head {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<BasicModal
|
||||
v-bind="$attrs"
|
||||
@register="registerModal"
|
||||
title="钉钉审批实例原始JSON"
|
||||
width="960px"
|
||||
:defaultFullscreen="true"
|
||||
:showOkBtn="false"
|
||||
cancelText="关闭"
|
||||
destroyOnClose
|
||||
>
|
||||
<a-spin :spinning="loading">
|
||||
<div v-if="instanceId" class="ding-instance-head">
|
||||
<span>审批实例ID:</span>
|
||||
<span class="ding-instance-id">{{ instanceId }}</span>
|
||||
</div>
|
||||
<JCodeEditor
|
||||
v-if="jsonText"
|
||||
v-model:value="jsonText"
|
||||
language="json"
|
||||
theme="idea"
|
||||
:fullScreen="false"
|
||||
:lineNumbers="true"
|
||||
:disabled="true"
|
||||
:language-change="false"
|
||||
height="calc(100vh - 220px)"
|
||||
/>
|
||||
<a-empty v-else-if="!loading" description="暂无实例数据" />
|
||||
</a-spin>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { JCodeEditor } from '/@/components/Form';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { getDingProcessInstance } from '../MesXslApprovalTrace.api';
|
||||
import 'codemirror/theme/idea.css';
|
||||
|
||||
defineOptions({ name: 'DingApprovalInstanceJsonModal' });
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const loading = ref(false);
|
||||
const instanceId = ref('');
|
||||
const jsonText = ref('');
|
||||
|
||||
const [registerModal, { setModalProps }] = useModalInner(async (data) => {
|
||||
instanceId.value = '';
|
||||
jsonText.value = '';
|
||||
const record = data?.record || {};
|
||||
const bizTable = record.bizTable || data?.bizTable;
|
||||
const bizDataId = record.bizDataId;
|
||||
const processInstanceId = record.externalInstanceId;
|
||||
if (!processInstanceId && (!bizTable || !bizDataId)) {
|
||||
createMessage.warning('缺少单据ID或钉钉审批流ID,无法查看审批实例');
|
||||
return;
|
||||
}
|
||||
instanceId.value = processInstanceId || '';
|
||||
try {
|
||||
loading.value = true;
|
||||
setModalProps({ confirmLoading: false });
|
||||
const res = await getDingProcessInstance({ bizTable, bizDataId, processInstanceId });
|
||||
jsonText.value = JSON.stringify(res ?? {}, null, 2);
|
||||
if (!instanceId.value && res?.result?.processInstanceId) {
|
||||
instanceId.value = res.result.processInstanceId;
|
||||
}
|
||||
} catch (e: any) {
|
||||
createMessage.error(e?.message || '拉取钉钉审批实例失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ding-instance-head {
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.ding-instance-id {
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,21 @@
|
||||
<template>
|
||||
<BasicDrawer @register="registerDrawer" :title="drawerTitle" width="960" destroyOnClose>
|
||||
<BasicTable @register="registerTable" />
|
||||
<BasicDrawer @register="registerDrawer" :title="drawerTitle" width="1100" destroyOnClose>
|
||||
<BasicTable @register="registerTable">
|
||||
<template #flowRecord="{ record }">
|
||||
<a-button type="link" size="small" :disabled="!canViewFlow(record)" @click="handleViewInstance(record)">
|
||||
查看审批实例
|
||||
</a-button>
|
||||
<a-button type="link" size="small" :disabled="!canViewFlow(record)" @click="handleViewFlow(record)">
|
||||
查看审批流转记录
|
||||
</a-button>
|
||||
<a-button type="link" size="small" :disabled="!canViewFlow(record)" @click="handleViewForecast(record)">
|
||||
查看MES审批节点
|
||||
</a-button>
|
||||
</template>
|
||||
</BasicTable>
|
||||
<DingApprovalInstanceJsonModal @register="registerInstanceModal" />
|
||||
<DingApprovalFlowTimelineModal @register="registerFlowModal" />
|
||||
<DingApprovalForecastModal @register="registerForecastModal" />
|
||||
</BasicDrawer>
|
||||
</template>
|
||||
|
||||
@@ -8,10 +23,17 @@
|
||||
import { ref, computed } from 'vue';
|
||||
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
|
||||
import { BasicTable, useTable } from '/@/components/Table';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { drawerColumns, drawerSearchFormSchema } from '../MesXslApprovalTrace.data';
|
||||
import { list } from '../MesXslApprovalTrace.api';
|
||||
import DingApprovalInstanceJsonModal from './DingApprovalInstanceJsonModal.vue';
|
||||
import DingApprovalFlowTimelineModal from './DingApprovalFlowTimelineModal.vue';
|
||||
import DingApprovalForecastModal from './DingApprovalForecastModal.vue';
|
||||
|
||||
const registryRecord = ref<Recordable>({});
|
||||
const [registerInstanceModal, { openModal: openInstanceModal }] = useModal();
|
||||
const [registerFlowModal, { openModal: openFlowModal }] = useModal();
|
||||
const [registerForecastModal, { openModal: openForecastModal }] = useModal();
|
||||
|
||||
const drawerTitle = computed(() => {
|
||||
const record = registryRecord.value;
|
||||
@@ -46,4 +68,38 @@
|
||||
setDrawerProps({ confirmLoading: false });
|
||||
await reload();
|
||||
});
|
||||
|
||||
function canViewFlow(record: Recordable) {
|
||||
return !!(record?.externalInstanceId || (record?.bizTable && record?.bizDataId));
|
||||
}
|
||||
|
||||
function handleViewInstance(record: Recordable) {
|
||||
openInstanceModal(true, {
|
||||
record: {
|
||||
...record,
|
||||
bizTable: record.bizTable || registryRecord.value.tableName,
|
||||
},
|
||||
bizTable: registryRecord.value.tableName,
|
||||
});
|
||||
}
|
||||
|
||||
function handleViewFlow(record: Recordable) {
|
||||
openFlowModal(true, {
|
||||
record: {
|
||||
...record,
|
||||
bizTable: record.bizTable || registryRecord.value.tableName,
|
||||
},
|
||||
bizTable: registryRecord.value.tableName,
|
||||
});
|
||||
}
|
||||
|
||||
function handleViewForecast(record: Recordable) {
|
||||
openForecastModal(true, {
|
||||
record: {
|
||||
...record,
|
||||
bizTable: record.bizTable || registryRecord.value.tableName,
|
||||
},
|
||||
bizTable: registryRecord.value.tableName,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -4,11 +4,17 @@
|
||||
:title="isUpdate ? '编辑动作' : '添加动作'"
|
||||
width="860px"
|
||||
:confirm-loading="saving"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
@ok="handleConfirm"
|
||||
@cancel="visible = false"
|
||||
>
|
||||
<template #footer>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; width: 100%">
|
||||
<a-button @click="handleReset">清空配置</a-button>
|
||||
<a-space>
|
||||
<a-button @click="visible = false">取消</a-button>
|
||||
<a-button type="primary" :loading="saving" @click="handleConfirm">确认</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
<a-form ref="formRef" :model="form" :label-col="{ span: 5 }" :wrapper-col="{ span: 18 }" style="margin-top: 12px">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="15">
|
||||
@@ -150,43 +156,12 @@
|
||||
<!-- ============ 状态修改(全新设计) ============ -->
|
||||
<template v-if="vc.visualType === 'STATUS_MODIFY'">
|
||||
|
||||
<!-- ① 触发表节点识别(可选) -->
|
||||
<div style="background: #fffbe6; border: 1px solid #ffe58f; border-radius: 6px; padding: 12px 14px; margin-bottom: 14px">
|
||||
<div style="font-size: 12px; font-weight: 600; color: #d48806; margin-bottom: 8px">
|
||||
🎯 触发表节点识别(可选)
|
||||
<span style="font-weight: 400; color: #888; margin-left: 6px">仅在 onNodeApprove 时需要,用于区分哪个节点触发了此动作</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: flex-end; gap: 8px">
|
||||
<span style="font-size: 12px; color: #888; padding-bottom: 5px; white-space: nowrap">当 {{ sourceTable }} 的</span>
|
||||
<div style="flex: 1">
|
||||
<a-select
|
||||
v-model:value="vc.statusConfig!.srcConditionField"
|
||||
:options="sourceFieldOpts"
|
||||
placeholder="字段名,如 status"
|
||||
allow-clear
|
||||
show-search
|
||||
style="width: 100%"
|
||||
@change="onSrcConditionFieldChange"
|
||||
/>
|
||||
</div>
|
||||
<span style="font-size: 14px; color: #d48806; font-weight: 600; padding-bottom: 5px">=</span>
|
||||
<div style="flex: 1">
|
||||
<a-select
|
||||
v-if="srcConditionDictCode && vc.statusConfig!.srcConditionField"
|
||||
v-model:value="vc.statusConfig!.srcConditionValue"
|
||||
:options="srcConditionDictItems"
|
||||
placeholder="选择状态值"
|
||||
allow-clear
|
||||
show-search
|
||||
style="width: 100%"
|
||||
/>
|
||||
<a-input
|
||||
v-else
|
||||
v-model:value="vc.statusConfig!.srcConditionValue"
|
||||
:placeholder="vc.statusConfig!.srcConditionField ? '如 compile' : '选择左侧字段后填写'"
|
||||
:disabled="!vc.statusConfig!.srcConditionField"
|
||||
/>
|
||||
</div>
|
||||
<!-- ① 节点触发说明 -->
|
||||
<div style="background: #e6f4ff; border: 1px solid #91caff; border-radius: 6px; padding: 12px 14px; margin-bottom: 14px">
|
||||
<div style="font-size: 12px; font-weight: 600; color: #0958d9; margin-bottom: 6px">节点触发说明</div>
|
||||
<div style="font-size: 12px; color: #555; line-height: 1.6">
|
||||
本动作何时执行,由集成方案的<strong>触发时机</strong>与<strong>绑定环节</strong>控制(如 onNodeApprove + proofread),无需在 SQL 中判断源单 status。
|
||||
源单状态推进请使用「环节同步」动作;本动作仅负责更新<strong>关联目标表</strong>。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -313,6 +288,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { Modal } from 'ant-design-vue';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { getTableColumns, getDictItems } from '../MesXslIntegrationPlan.api';
|
||||
|
||||
@@ -351,6 +327,13 @@
|
||||
mes_xsl_formula_spec: 'xslmes_formula_spec_status',
|
||||
};
|
||||
|
||||
/** 目标表 status 字段未带字典注释时的兜底映射 */
|
||||
const TARGET_TABLE_STATUS_DICT: Record<string, string> = {
|
||||
mes_xsl_mixer_ps_compile: 'xslmes_mixer_ps_status',
|
||||
mes_xsl_formula_spec: 'xslmes_formula_spec_status',
|
||||
mes_xsl_mixing_spec: 'xslmes_formula_spec_status',
|
||||
};
|
||||
|
||||
const ACTION_TYPES = [
|
||||
{ value: 'REGISTRY_STAGE_SYNC', icon: '✅', label: '审批环节同步', desc: '按注册中心更新源单状态+操作人+痕迹', disabled: false },
|
||||
{ value: 'REGISTRY_STAGE_REVERT', icon: '↩️', label: '审批环节回退', desc: '驳回时回退源单并清空痕迹', disabled: false },
|
||||
@@ -474,11 +457,8 @@
|
||||
watch(
|
||||
() => vc.value.statusConfig?.targetField,
|
||||
async (field) => {
|
||||
if (!field) { targetStatusDictCode.value = ''; targetStatusDictItems.value = []; return; }
|
||||
const dc = extractDictCode(targetColumns.value.find((c) => c.columnName === field)?.comment || '');
|
||||
targetStatusDictCode.value = dc || '';
|
||||
targetStatusDictItems.value = dc ? await loadDict(dc) : [];
|
||||
}
|
||||
await loadTargetStatusDict(field);
|
||||
},
|
||||
);
|
||||
|
||||
// 审批环节变化时,前置状态留空则填入默认推断值
|
||||
@@ -505,11 +485,28 @@
|
||||
|
||||
function extractDictCode(comment: string): string | null {
|
||||
if (!comment) return null;
|
||||
// 匹配 "字典xslmes_xxx" 或 "字典:xslmes_xxx" 或 "字典 xslmes_xxx"
|
||||
const m = comment.match(/字典[:\s]?([a-zA-Z][a-zA-Z0-9_]*)/);
|
||||
// 匹配 "字典xslmes_xxx" / "字典:xslmes_xxx" / "字典 xslmes_xxx" / 全角括号包裹
|
||||
const m = comment.match(/字典[:\s:]?([a-zA-Z][a-zA-Z0-9_]*)/);
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
/** 按目标表字段注释(或表名兜底)加载 status 字典,供前置状态/新状态下拉 */
|
||||
async function loadTargetStatusDict(field?: string) {
|
||||
const f = field || vc.value.statusConfig?.targetField;
|
||||
if (!f) {
|
||||
targetStatusDictCode.value = '';
|
||||
targetStatusDictItems.value = [];
|
||||
return;
|
||||
}
|
||||
const col = targetColumns.value.find((c) => c.columnName === f);
|
||||
let dictCode = extractDictCode(col?.comment || '');
|
||||
if (!dictCode && vc.value.targetTable) {
|
||||
dictCode = TARGET_TABLE_STATUS_DICT[vc.value.targetTable] || '';
|
||||
}
|
||||
targetStatusDictCode.value = dictCode || '';
|
||||
targetStatusDictItems.value = dictCode ? await loadDict(dictCode) : [];
|
||||
}
|
||||
|
||||
async function loadDict(dictCode: string): Promise<any[]> {
|
||||
if (dictCache.value[dictCode]) return dictCache.value[dictCode];
|
||||
try {
|
||||
@@ -561,6 +558,17 @@
|
||||
return nonStage?.value || items[0].value;
|
||||
}
|
||||
|
||||
/** 保存时扁平化 registryStage,兼容引擎解析 stage/expectedFrom/targetStage */
|
||||
function serializeActionConfig(config: VisualConfig): string {
|
||||
const payload: Record<string, any> = { ...config };
|
||||
if (config.registryStage?.stage) payload.stage = config.registryStage.stage;
|
||||
if (config.registryStage?.expectedFrom !== undefined && config.registryStage?.expectedFrom !== null) {
|
||||
payload.expectedFrom = config.registryStage.expectedFrom;
|
||||
}
|
||||
if (config.registryStage?.targetStage) payload.targetStage = config.registryStage.targetStage;
|
||||
return JSON.stringify(payload);
|
||||
}
|
||||
|
||||
function buildSql(config: VisualConfig): string {
|
||||
const { targetTable, linkCondition, visualType, statusConfig, fieldMappings } = config;
|
||||
if (!targetTable || !linkCondition.sourceField || !linkCondition.targetField) return '';
|
||||
@@ -573,9 +581,6 @@
|
||||
if (s.addUpdateTime) sets.push('update_time=NOW()');
|
||||
const conditions = [baseWhere];
|
||||
if (s.fromValue) conditions.push(`${s.targetField}='${s.fromValue}'`);
|
||||
if (s.srcConditionField && s.srcConditionValue) {
|
||||
conditions.push(`#{source.${s.srcConditionField}}='${s.srcConditionValue}'`);
|
||||
}
|
||||
return `UPDATE ${targetTable} SET ${sets.join(', ')} WHERE ${conditions.join(' AND ')}`;
|
||||
}
|
||||
|
||||
@@ -624,21 +629,22 @@
|
||||
targetColumns.value = (cols as any) || [];
|
||||
const doc = bizDocList.value.find((d) => d.tableName === tableName);
|
||||
vc.value.targetTableLabel = getDocLabel(doc);
|
||||
// 重置状态字段的字典缓存
|
||||
targetStatusDictCode.value = '';
|
||||
targetStatusDictItems.value = [];
|
||||
// 列元数据就绪后重载字典(编辑场景:targetField 已存在但列尚未加载)
|
||||
await loadTargetStatusDict();
|
||||
} catch {
|
||||
targetColumns.value = [];
|
||||
targetStatusDictCode.value = '';
|
||||
targetStatusDictItems.value = [];
|
||||
} finally {
|
||||
loadingTargetCols.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function onTargetStatusFieldChange(field: string) {
|
||||
if (!field) { targetStatusDictCode.value = ''; targetStatusDictItems.value = []; return; }
|
||||
// 清空旧值,防止值不匹配新字典
|
||||
vc.value.statusConfig!.fromValue = '';
|
||||
vc.value.statusConfig!.newValue = '';
|
||||
await loadTargetStatusDict(field);
|
||||
}
|
||||
|
||||
async function onSrcConditionFieldChange(field: string) {
|
||||
@@ -655,6 +661,40 @@
|
||||
vc.value.fieldMappings.push({ targetField: '', sourceType: 'source_field', sourceValue: '' });
|
||||
}
|
||||
|
||||
/** 清空当前可视化配置,保留执行顺序/失败策略/启用状态(编辑时不删动作记录) */
|
||||
function handleReset() {
|
||||
Modal.confirm({
|
||||
title: '清空配置',
|
||||
content:
|
||||
'将清空动作名称、目标表、关联条件、状态变更等可视化配置,可重新填写。执行顺序、失败策略、启用状态会保留。确认清空?',
|
||||
okText: '确认清空',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
const keep = { ...form.value };
|
||||
form.value = {
|
||||
id: keep.id || '',
|
||||
actionName: '',
|
||||
execOrder: keep.execOrder ?? 0,
|
||||
onFail: keep.onFail || 'stop',
|
||||
enabled: keep.enabled !== false && keep.enabled !== 0,
|
||||
};
|
||||
vc.value = defaultVc();
|
||||
targetColumns.value = [];
|
||||
targetStatusDictCode.value = '';
|
||||
targetStatusDictItems.value = [];
|
||||
srcConditionDictCode.value = '';
|
||||
srcConditionDictItems.value = [];
|
||||
if (registryStageOptions.value.length) {
|
||||
vc.value.registryStage!.stage = registryStageOptions.value[0].value;
|
||||
vc.value.registryStage!.expectedFrom = defaultExpectedFromForStage(vc.value.registryStage!.stage);
|
||||
}
|
||||
vc.value.registryStage!.targetStage = defaultRevertTargetStage();
|
||||
formRef.value?.clearValidate?.();
|
||||
createMessage.success('已清空,请重新配置后点确认保存');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleConfirm() {
|
||||
try { await formRef.value?.validate(); } catch { return; }
|
||||
if (vc.value.visualType === 'REGISTRY_STAGE_SYNC') {
|
||||
@@ -666,7 +706,7 @@
|
||||
...form.value,
|
||||
actionType: 'REGISTRY_STAGE_SYNC',
|
||||
sqlTemplate: null,
|
||||
actionConfig: JSON.stringify(vc.value),
|
||||
actionConfig: serializeActionConfig(vc.value),
|
||||
});
|
||||
visible.value = false;
|
||||
return;
|
||||
@@ -676,7 +716,7 @@
|
||||
...form.value,
|
||||
actionType: 'REGISTRY_STAGE_REVERT',
|
||||
sqlTemplate: null,
|
||||
actionConfig: JSON.stringify(vc.value),
|
||||
actionConfig: serializeActionConfig(vc.value),
|
||||
});
|
||||
visible.value = false;
|
||||
return;
|
||||
@@ -689,7 +729,7 @@
|
||||
...form.value,
|
||||
actionType: 'SQL_UPDATE',
|
||||
sqlTemplate: sql,
|
||||
actionConfig: JSON.stringify(vc.value),
|
||||
actionConfig: serializeActionConfig(vc.value),
|
||||
});
|
||||
visible.value = false;
|
||||
}
|
||||
@@ -722,7 +762,7 @@
|
||||
const parsed = JSON.parse(a.actionConfig);
|
||||
vc.value = normalizeParsedConfig(parsed, a.actionType);
|
||||
if (vc.value.targetTable) {
|
||||
onTargetTableChange(vc.value.targetTable);
|
||||
await onTargetTableChange(vc.value.targetTable);
|
||||
}
|
||||
} catch {
|
||||
vc.value = normalizeParsedConfig(null, a.actionType);
|
||||
|
||||
@@ -37,6 +37,9 @@ export const columns: BasicColumn[] = [
|
||||
{ title: '机台', align: 'center', dataIndex: 'machineName', width: 120 },
|
||||
{ title: '制作日期', align: 'center', dataIndex: 'makeDate', width: 120 },
|
||||
{ title: '发行编号', align: 'center', dataIndex: 'issueNumber', width: 150 },
|
||||
//update-begin---author:cursor ---date:20260608 for:【XSLMES-20260608-A01】混炼示方列表新增状态列-----------
|
||||
{ title: '状态', align: 'center', dataIndex: 'status_dictText', width: 120 },
|
||||
//update-end---author:cursor ---date:20260608 for:【XSLMES-20260608-A01】混炼示方列表新增状态列-----------
|
||||
{ title: '段数', align: 'center', dataIndex: 'stageCount', width: 88 },
|
||||
{ title: '纯混炼时间(秒)', align: 'center', dataIndex: 'pureMixSec', width: 130 },
|
||||
{ title: '变更日期', align: 'center', dataIndex: 'changeDate', width: 120 },
|
||||
@@ -45,6 +48,15 @@ export const columns: BasicColumn[] = [
|
||||
|
||||
export const searchFormSchema: FormSchema[] = [
|
||||
{ label: '关键字', field: 'keyword', component: 'Input', colProps: { span: 8 }, componentProps: { placeholder: '规格/用途/发行编号/机台' } },
|
||||
//update-begin---author:cursor ---date:20260608 for:【XSLMES-20260608-A01】混炼示方查询新增状态条件-----------
|
||||
{
|
||||
label: '状态',
|
||||
field: 'status',
|
||||
component: 'JDictSelectTag',
|
||||
componentProps: { dictCode: 'xslmes_formula_spec_status', placeholder: '请选择状态' },
|
||||
colProps: { span: 8 },
|
||||
},
|
||||
//update-end---author:cursor ---date:20260608 for:【XSLMES-20260608-A01】混炼示方查询新增状态条件-----------
|
||||
{ label: '制作日期起', field: 'makeDate_begin', component: 'DatePicker', colProps: { span: 8 }, componentProps: { valueFormat: 'YYYY-MM-DD' } },
|
||||
{ label: '制作日期止', field: 'makeDate_end', component: 'DatePicker', colProps: { span: 8 }, componentProps: { valueFormat: 'YYYY-MM-DD' } },
|
||||
];
|
||||
@@ -1327,9 +1339,11 @@ export function resolveMixingSpecFormulaStatus(record: Recordable = {}): string
|
||||
return '编制中';
|
||||
}
|
||||
|
||||
/** 混炼示方是否允许编辑/删除(与配合示方一致:密炼PS校对后锁定) */
|
||||
/** 混炼示方是否允许编辑/删除(与配合示方一致:仅编制状态可改) */
|
||||
export function isMixingSpecEditable(record: Recordable = {}): boolean {
|
||||
return !record?.proofreadBy && !record?.proofreadTime;
|
||||
//update-begin---author:cursor ---date:20260608 for:【XSLMES-20260608-A01】混炼示方编辑权限按状态字段判断-----------
|
||||
return !record?.status || record.status === 'compile';
|
||||
//update-end---author:cursor ---date:20260608 for:【XSLMES-20260608-A01】混炼示方编辑权限按状态字段判断-----------
|
||||
}
|
||||
|
||||
/** 参照历史混合步骤:混炼示方选择列表列 */
|
||||
@@ -1365,6 +1379,9 @@ export const MIXING_SPEC_MAIN_STRIP_FIELDS = [
|
||||
'approveBy',
|
||||
'approveTime',
|
||||
'changeDate',
|
||||
//update-begin---author:cursor ---date:20260608 for:【XSLMES-20260608-A01】参照新增时重置状态-----------
|
||||
'status',
|
||||
//update-end---author:cursor ---date:20260608 for:【XSLMES-20260608-A01】参照新增时重置状态-----------
|
||||
'delFlag',
|
||||
'tenantId',
|
||||
'sysOrgCode',
|
||||
|
||||
Reference in New Issue
Block a user