Merge branch 'main' of http://27.223.88.102:33000/chenx/qhmes
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
全局「审批流程设计」悬浮按钮
|
||||
拥有 approval:flow:design 权限的用户,在任意功能页点击即可:
|
||||
1)后端按当前页路由反查绑定的业务表;
|
||||
2)解析该表字段,识别「校对/审核/审批/分发/抄送」等阶段字段(不存在不报错);
|
||||
3)进入可视化设计器,可点选识别到的阶段字段按顺序生成审批流程并保存发布。
|
||||
2)从审批注册中心读取该单据已启用的审批环节;
|
||||
3)进入可视化设计器,可点选启用环节按顺序生成审批节点,也支持手动添加节点。
|
||||
@author GHT
|
||||
@date 2026-05-29 for:【QH-MES审批流设计】全局审批流程设计悬浮按钮
|
||||
-->
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<!--
|
||||
全局「发起审批」悬浮按钮
|
||||
仅在「设计并发布了审批流、且能匹配到对应功能页路由」的页面显示。
|
||||
与「钉钉审批」按钮一致:当前页存在 mes_xsl_ding_tpl_bind 绑定且钉钉模板启用时显示;
|
||||
弹窗内可选该页业务表下已发布的 MES 审批流。
|
||||
支持两种发起方式:
|
||||
1)列表多选联动:在列表勾选数据后点击,弹窗自动带入选中单据并可批量发起;
|
||||
2)手动选择:未勾选时,在弹窗内搜索选择单条单据发起。
|
||||
@@ -57,11 +58,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { getPublishedFlows, getBizRecords, launchApproval, launchApprovalBatch } from '/@/views/approval/flow/launch.api';
|
||||
import { getBindingByRoute } from '/@/views/xslmes/dingtalk/dingTplBind/dingTplBind.api';
|
||||
import { useApprovalSelection } from './useApprovalSelection';
|
||||
|
||||
defineOptions({ name: 'ApprovalLaunchFloat' });
|
||||
@@ -78,6 +80,7 @@
|
||||
const bizDataId = ref<string>();
|
||||
const bizTitle = ref<string>('');
|
||||
|
||||
const binding = ref<any>(null);
|
||||
const flowList = ref<any[]>([]);
|
||||
const recordList = ref<any[]>([]);
|
||||
// 打开弹窗时快照的列表勾选行(批量模式数据源)
|
||||
@@ -86,18 +89,10 @@
|
||||
// 悬浮位置(右下角)
|
||||
const floatStyle = reactive({ right: '24px', bottom: '120px' });
|
||||
|
||||
function normalizePath(p?: string) {
|
||||
return (p || '').trim().replace(/\/+$/, '');
|
||||
}
|
||||
// 与钉钉审批按钮一致:按 mes_xsl_ding_tpl_bind + 路由解析是否显示
|
||||
const show = computed(() => !!binding.value);
|
||||
|
||||
// 当前路由匹配到的已发布审批流
|
||||
const matchedFlows = computed(() => {
|
||||
const cur = normalizePath(currentRoute.value?.path);
|
||||
if (!cur) return [];
|
||||
return flowList.value.filter((f) => f.routePath && normalizePath(f.routePath) === cur);
|
||||
});
|
||||
|
||||
const show = computed(() => matchedFlows.value.length > 0);
|
||||
const matchedFlows = computed(() => flowList.value);
|
||||
|
||||
const flowOptions = computed(() =>
|
||||
matchedFlows.value.map((f) => ({
|
||||
@@ -130,15 +125,27 @@
|
||||
}))
|
||||
);
|
||||
|
||||
onMounted(loadFlows);
|
||||
|
||||
async function loadFlows() {
|
||||
try {
|
||||
flowList.value = (await getPublishedFlows()) || [];
|
||||
} catch {
|
||||
watch(
|
||||
() => currentRoute.value?.path,
|
||||
async (path) => {
|
||||
binding.value = null;
|
||||
flowList.value = [];
|
||||
}
|
||||
}
|
||||
if (!path || path === '/' || path.startsWith('/login')) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const bind = await getBindingByRoute(path);
|
||||
binding.value = bind || null;
|
||||
if (binding.value) {
|
||||
flowList.value = (await getPublishedFlows(path)) || [];
|
||||
}
|
||||
} catch {
|
||||
binding.value = null;
|
||||
flowList.value = [];
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function resetSelection() {
|
||||
flowId.value = undefined;
|
||||
@@ -149,6 +156,10 @@
|
||||
}
|
||||
|
||||
async function openModal() {
|
||||
if (!matchedFlows.value.length) {
|
||||
createMessage.warning('当前页面暂无已发布的 MES 审批流,请先在审批流设计中发布');
|
||||
return;
|
||||
}
|
||||
visible.value = true;
|
||||
resetSelection();
|
||||
// 读取当前列表页的勾选行
|
||||
|
||||
@@ -254,6 +254,12 @@
|
||||
previewFlowApprovers,
|
||||
} from '/@/views/xslmes/dingtalk/mesXslDingProcessTpl/MesXslDingProcessTpl.api';
|
||||
import { checkCanLaunch } from '/@/views/approval/gate/approvalGate.api';
|
||||
import { resolveFieldValues } from '/@/views/xslmes/dingtalk/dingTplBind/dingTplBind.api';
|
||||
import {
|
||||
type ValueMode,
|
||||
getNestedValue,
|
||||
resolveFieldValueLocal,
|
||||
} from '/@/views/xslmes/dingtalk/dingTplBind/dingTplFieldValue';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const { createMessage } = useMessage();
|
||||
@@ -330,7 +336,7 @@
|
||||
if (f.componentName === 'TableField') tableValues[f.label] = [];
|
||||
}
|
||||
|
||||
applyPrefillForRow(currentRowIndex.value);
|
||||
applyPrefillForRowAsync(currentRowIndex.value);
|
||||
|
||||
const presetFlowId = tplRecord?.flowId;
|
||||
if (presetFlowId) {
|
||||
@@ -357,7 +363,7 @@
|
||||
if (idx === currentRowIndex.value) return;
|
||||
currentRowIndex.value = idx;
|
||||
resetFieldValues();
|
||||
applyPrefillForRow(idx);
|
||||
applyPrefillForRowAsync(idx);
|
||||
activeTab.value = 'form';
|
||||
}
|
||||
|
||||
@@ -370,10 +376,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
function applyPrefillForRow(idx: number) {
|
||||
async function applyPrefillForRowAsync(idx: number) {
|
||||
const rowData = allRows.value[idx];
|
||||
if (rowData && tplData.value?.fieldMappingJson) {
|
||||
applyPrefill(dingFields.value, tplData.value.fieldMappingJson, rowData);
|
||||
await applyPrefill(dingFields.value, tplData.value.fieldMappingJson, rowData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,13 +407,48 @@
|
||||
componentName: string;
|
||||
parentId?: string;
|
||||
bizField?: string;
|
||||
valueMode?: ValueMode;
|
||||
}
|
||||
|
||||
function applyPrefill(fields: any[], mappingJson: string, rowData: any) {
|
||||
async function applyPrefill(fields: any[], mappingJson: string, rowData: any) {
|
||||
let mapping: MappingItem[] = [];
|
||||
try { mapping = JSON.parse(mappingJson); } catch { return; }
|
||||
try {
|
||||
mapping = JSON.parse(mappingJson);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const byId = new Map(mapping.map(m => [m.componentId, m]));
|
||||
const byId = new Map(mapping.map((m) => [m.componentId, m]));
|
||||
|
||||
// 主表 text 模式字段批量解析(明细列在各行上本地解析)
|
||||
const resolveItems = mapping
|
||||
.filter((m) => m.valueMode === 'text' && m.bizField && !m.parentId)
|
||||
.map((m) => ({
|
||||
mapKey: m.componentId,
|
||||
bizField: m.bizField!,
|
||||
valueMode: 'text',
|
||||
}));
|
||||
|
||||
let resolvedMap: Record<string, any> = {};
|
||||
if (resolveItems.length && tplData.value?.bizCode) {
|
||||
try {
|
||||
resolvedMap = (await resolveFieldValues({
|
||||
bizCode: tplData.value.bizCode,
|
||||
rowData,
|
||||
items: resolveItems,
|
||||
})) || {};
|
||||
} catch {
|
||||
resolvedMap = {};
|
||||
}
|
||||
}
|
||||
|
||||
function pickValue(m: MappingItem, bizField: string): any {
|
||||
if (resolvedMap[m.componentId] !== undefined) {
|
||||
return resolvedMap[m.componentId];
|
||||
}
|
||||
const mode = (m.valueMode || 'raw') as ValueMode;
|
||||
return resolveFieldValueLocal(rowData, bizField, mode);
|
||||
}
|
||||
|
||||
for (const field of fields) {
|
||||
if (field.componentName === 'TextNote') continue;
|
||||
@@ -420,34 +461,30 @@
|
||||
const arr: any[] = getNestedValue(rowData, slotName);
|
||||
if (!Array.isArray(arr) || !arr.length) continue;
|
||||
|
||||
const childMappings = mapping.filter(x => x.parentId === cid && x.bizField);
|
||||
tableValues[field.label] = arr.map(element => {
|
||||
const childMappings = mapping.filter((x) => x.parentId === cid && x.bizField);
|
||||
tableValues[field.label] = arr.map((element) => {
|
||||
const row: Record<string, string> = {};
|
||||
for (const child of childMappings) {
|
||||
const parts = (child.bizField || '').split('.');
|
||||
const colKey = parts.slice(1).join('.');
|
||||
const val = colKey ? getNestedValue(element, colKey) : undefined;
|
||||
const mode = (child.valueMode || 'raw') as ValueMode;
|
||||
const val = colKey ? resolveFieldValueLocal(element, colKey, mode) : undefined;
|
||||
row[child.componentLabel] = val !== undefined && val !== null ? String(val) : '';
|
||||
}
|
||||
for (const child of (field.children || [])) {
|
||||
for (const child of field.children || []) {
|
||||
if (!(child.label in row)) row[child.label] = '';
|
||||
}
|
||||
return row;
|
||||
});
|
||||
} else {
|
||||
if (!m?.bizField) continue;
|
||||
const val = getNestedValue(rowData, m.bizField);
|
||||
const val = pickValue(m, m.bizField);
|
||||
if (val === undefined || val === null) continue;
|
||||
formValues[field.label] = formatForDisplay(val, field.componentName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getNestedValue(obj: any, path: string): any {
|
||||
if (!obj || !path) return undefined;
|
||||
return path.split('.').reduce((acc: any, k: string) => acc?.[k], obj);
|
||||
}
|
||||
|
||||
function formatForDisplay(v: any, componentName: string): any {
|
||||
if (v === null || v === undefined) return '';
|
||||
if (['NumberField', 'MoneyField'].includes(componentName)) {
|
||||
|
||||
@@ -11,6 +11,9 @@ import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { filterObj } from '/@/utils/common/compUtils';
|
||||
import { isFunction } from '@/utils/is';
|
||||
import { registerImPageListProvider } from '/@/views/system/im/imPageListRegistry';
|
||||
//update-begin---author:GHT ---date:20260609 for:【审批注册中心】列表页静态注入审批痕迹列(默认隐藏)-----------
|
||||
import { traceColumns } from '/@/views/xslmes/approval/integration/traceColumns';
|
||||
//update-end---author:GHT ---date:20260609 for:【审批注册中心】列表页静态注入审批痕迹列(默认隐藏)-----------
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】列表选中行同步到审批发起上下文-----
|
||||
import { useApprovalSelection } from '/@/components/ApprovalLaunch/useApprovalSelection';
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】列表选中行同步到审批发起上下文-----
|
||||
@@ -573,6 +576,14 @@ export function useListTable(tableProps: TableProps): [
|
||||
}
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260609 for:【审批注册中心】静态追加审批痕迹列(默认隐藏),无需注册中心判断-----------
|
||||
const existingCols: any[] = (defaultTableProps.columns as any[]) ?? [];
|
||||
const hasTrace = existingCols.some((c) => String(c.dataIndex ?? '').startsWith('trace'));
|
||||
if (!hasTrace) {
|
||||
defaultTableProps.columns = [...existingCols, ...traceColumns];
|
||||
}
|
||||
//update-end---author:GHT ---date:20260609 for:【审批注册中心】静态追加审批痕迹列(默认隐藏),无需注册中心判断-----------
|
||||
|
||||
return [
|
||||
...useTable(defaultTableProps),
|
||||
{
|
||||
|
||||
@@ -36,7 +36,13 @@
|
||||
import { useListPage } from '/@/hooks/system/useListPage';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { columns, searchFormSchema } from './approvalFlow.data';
|
||||
import { getApprovalFlowList, deleteApprovalFlow, batchDeleteApprovalFlow, updateApprovalFlowStatus } from './approvalFlow.api';
|
||||
import {
|
||||
getApprovalFlowList,
|
||||
deleteApprovalFlow,
|
||||
batchDeleteApprovalFlow,
|
||||
updateApprovalFlowStatus,
|
||||
getApprovalRegistryStages,
|
||||
} from './approvalFlow.api';
|
||||
import ApprovalFlowModal from './ApprovalFlowModal.vue';
|
||||
import FlowDesign from './components/FlowDesign.vue';
|
||||
|
||||
@@ -73,9 +79,17 @@
|
||||
openModal(true, { isUpdate: true, record });
|
||||
}
|
||||
|
||||
// 打开可视化设计器
|
||||
function handleDesign(record, readonly = false) {
|
||||
openDesign(true, { record, readonly });
|
||||
// 打开可视化设计器(加载审批注册中心启用环节)
|
||||
async function handleDesign(record, readonly = false) {
|
||||
let paletteStages: any[] = [];
|
||||
if (record?.bizTable) {
|
||||
try {
|
||||
paletteStages = (await getApprovalRegistryStages(record.bizTable)) || [];
|
||||
} catch {
|
||||
paletteStages = [];
|
||||
}
|
||||
}
|
||||
openDesign(true, { record, readonly, paletteStages });
|
||||
}
|
||||
|
||||
function handleDelete(record) {
|
||||
|
||||
@@ -16,6 +16,7 @@ enum Api {
|
||||
deleteBatch = '/xslmes/approvalFlow/deleteBatch',
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页设计上下文-----
|
||||
designContext = '/xslmes/approvalFlow/designContext',
|
||||
registryStages = '/xslmes/approvalFlow/registryStages',
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页设计上下文-----
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】业务表可选回调动作-----
|
||||
bizActions = '/xslmes/approvalFlow/bizActions',
|
||||
@@ -72,9 +73,12 @@ export const batchDeleteApprovalFlow = (params, handleSuccess) => {
|
||||
/**
|
||||
* 获取当前功能页的审批流设计上下文:
|
||||
* 返回 { routePath, bizTable, bizTableName, stages[], flow },
|
||||
* stages 为识别到的阶段字段(校对/审核/审批/分发/抄送),flow 为可直接进入设计器的流程记录。
|
||||
* stages 为审批注册中心已启用环节,flow 为可直接进入设计器的流程记录。
|
||||
*/
|
||||
export const getApprovalDesignContext = (routePath: string) => defHttp.get({ url: Api.designContext, params: { routePath } });
|
||||
|
||||
/** 按业务表查询审批注册中心启用环节(列表页「设计」入口用) */
|
||||
export const getApprovalRegistryStages = (bizTable: string) => defHttp.get<any[]>({ url: Api.registryStages, params: { bizTable } });
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页设计上下文(解析阶段字段+取/建草稿流程)-----
|
||||
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】业务表可选回调动作(后端@ApprovalBizAction注解扫描)-----
|
||||
@@ -84,3 +88,12 @@ export const getApprovalDesignContext = (routePath: string) => defHttp.get({ url
|
||||
*/
|
||||
export const getApprovalBizActions = (table: string) => defHttp.get({ url: Api.bizActions, params: { table } });
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】业务表可选回调动作-----
|
||||
|
||||
// update-begin---author:GHT ---date:2026-06-05 for:【XSLMES-20260605-K8R2】流程节点绑定已发布集成方案-----
|
||||
/** 查询某业务表、某触发时机下已发布的集成方案,供节点配置下拉 */
|
||||
export const listPublishedIntegrationPlans = (params: { sourceTable: string; triggerPhase: string }) =>
|
||||
defHttp.get({
|
||||
url: '/xslmes/mesXslIntegrationPlan/list',
|
||||
params: { ...params, status: '1', pageNo: 1, pageSize: 200 },
|
||||
});
|
||||
// update-end---author:GHT ---date:2026-06-05 for:【XSLMES-20260605-K8R2】流程节点绑定已发布集成方案-----
|
||||
|
||||
@@ -13,6 +13,9 @@ enum Api {
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||
cancel = '/xslmes/approvalHandle/cancel',
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||
// update-begin---author:GHT ---date:2026-06-10 for:【IM审批通用化】补发IM审批卡片-----
|
||||
resendCard = '/xslmes/approvalHandle/resendCard',
|
||||
// update-end---author:GHT ---date:2026-06-10 for:【IM审批通用化】补发IM审批卡片-----
|
||||
}
|
||||
|
||||
/** 查看单据全部字段 + 审批进度/历史 */
|
||||
@@ -31,3 +34,9 @@ export const rejectApproval = (params: { instanceId: string; reason: string }) =
|
||||
/** 撤销(仅发起人本人,审批中可撤回,业务单据恢复到发起时状态) */
|
||||
export const cancelApproval = (params: { instanceId: string; reason?: string }) => defHttp.post({ url: Api.cancel, data: params });
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||
|
||||
// update-begin---author:GHT ---date:2026-06-10 for:【IM审批通用化】补发IM审批卡片-----
|
||||
/** 补发当前节点 IM 审批卡片(instanceId 与 bizTable+bizDataId 二选一) */
|
||||
export const resendApprovalCard = (params: { instanceId?: string; bizTable?: string; bizDataId?: string }) =>
|
||||
defHttp.post({ url: Api.resendCard, data: params });
|
||||
// update-end---author:GHT ---date:2026-06-10 for:【IM审批通用化】补发IM审批卡片-----
|
||||
|
||||
@@ -11,12 +11,17 @@
|
||||
<span class="fd-tb-item">审批流:<b>{{ record.flowName }}</b></span>
|
||||
<span class="fd-tb-tip">点击节点可配置,点击节点间的「+」可插入审批人 / 抄送人 / 条件分支</span>
|
||||
</div>
|
||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页识别到的阶段字段作为候选,点选追加为流程节点----- -->
|
||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批注册中心启用环节作为候选,点选追加为流程节点----- -->
|
||||
<div class="fd-body">
|
||||
<div class="fd-palette" v-if="!readonly && paletteStages.length">
|
||||
<div class="fd-palette-title">当前页识别到的审批阶段</div>
|
||||
<div class="fd-palette-tip">点击下方阶段,按顺序追加到流程末尾;处理人将取自单据对应字段。</div>
|
||||
<div class="fd-palette-list">
|
||||
<div class="fd-palette" v-if="!readonly">
|
||||
<div class="fd-palette-title">审批注册中心启用环节</div>
|
||||
<div class="fd-palette-tip" v-if="paletteStages.length">
|
||||
点击下方环节按顺序追加到流程末尾,处理人取自注册中心配置的人员字段。
|
||||
</div>
|
||||
<div class="fd-palette-tip fd-palette-empty" v-else>
|
||||
该单据未在「审批注册中心」配置启用环节。您仍可通过节点间「+」手动添加审批人节点(仅发起审批时生效)。
|
||||
</div>
|
||||
<div class="fd-palette-list" v-if="paletteStages.length">
|
||||
<div v-for="s in paletteStages" :key="s.stageKey" class="fd-palette-item" :class="'fd-palette-' + s.nodeType" @click="appendStageNode(s)">
|
||||
<div class="fd-palette-item-name">{{ s.stageName }}</div>
|
||||
<div class="fd-palette-item-field">{{ s.fieldComment || s.field }}</div>
|
||||
@@ -34,7 +39,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页识别到的阶段字段作为候选,点选追加为流程节点----- -->
|
||||
<!-- update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批注册中心启用环节作为候选,点选追加为流程节点----- -->
|
||||
</div>
|
||||
<NodeConfigDrawer ref="drawerRef" :readonly="readonly" />
|
||||
</BasicModal>
|
||||
@@ -70,10 +75,16 @@
|
||||
const drawerRef = ref();
|
||||
// 当前审批流绑定的业务表,供节点配置按表查可选回调动作
|
||||
const bizTableRef = ref('');
|
||||
const flowIdRef = ref('');
|
||||
provide('approvalBizTable', bizTableRef);
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页识别到的候选阶段字段----- -->
|
||||
provide('approvalFlowId', flowIdRef);
|
||||
//update-begin---author:GHT ---date:2026-06-10 for:【审批流设计】向节点配置传递流程ID与实时flowConfig-----------
|
||||
provide('approvalFlowConfig', () => (root.value ? JSON.stringify(root.value) : ''));
|
||||
//update-end---author:GHT ---date:2026-06-10 for:【审批流设计】向节点配置传递流程ID与实时flowConfig-----------
|
||||
provide('approvalFlowRoot', root);
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批注册中心候选环节----- -->
|
||||
const paletteStages = ref<StageField[]>([]);
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页识别到的候选阶段字段----- -->
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批注册中心候选环节----- -->
|
||||
|
||||
const modalTitle = computed(() => (readonly.value ? '查看审批流' : '设计审批流'));
|
||||
|
||||
@@ -108,9 +119,10 @@
|
||||
readonly.value = !!data?.readonly;
|
||||
flowCtx.readonly = readonly.value;
|
||||
bizTableRef.value = data?.record?.bizTable || '';
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】接收当前页解析出的候选阶段字段----- -->
|
||||
flowIdRef.value = data?.record?.id || '';
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】接收审批注册中心候选环节----- -->
|
||||
paletteStages.value = Array.isArray(data?.paletteStages) ? data.paletteStages : [];
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】接收当前页解析出的候选阶段字段----- -->
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】接收审批注册中心候选环节----- -->
|
||||
const id = data?.record?.id || '';
|
||||
Object.assign(record, {
|
||||
id,
|
||||
|
||||
@@ -87,48 +87,100 @@
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】节点回调接口可视化配置(自动识别页面按钮)----- -->
|
||||
<a-divider style="margin: 16px 0 12px">回调接口(审批联动业务)</a-divider>
|
||||
<!-- update-begin---author:GHT ---date:2026-06-05 for:【XSLMES-20260605-K8R3】新增绑定审批环节配置,区分关键节点与纯过路审批节点----- -->
|
||||
<a-divider style="margin: 16px 0 12px">审批环节绑定</a-divider>
|
||||
<a-form-item label="绑定审批环节">
|
||||
<a-select
|
||||
v-model:value="form.props.stageKey"
|
||||
:disabled="readonly"
|
||||
allow-clear
|
||||
placeholder="未设置(按节点名/单据状态自动推断)"
|
||||
style="width: 260px"
|
||||
@change="onStageKeyChange"
|
||||
>
|
||||
<a-select-option value="">不绑定(纯过路审批,不改变单据状态)</a-select-option>
|
||||
<a-select-option value="proofread">校对</a-select-option>
|
||||
<a-select-option value="audit">审核</a-select-option>
|
||||
<a-select-option value="approve">批准</a-select-option>
|
||||
</a-select>
|
||||
<div style="font-size: 12px; color: #888; margin-top: 4px; line-height: 1.5">
|
||||
<span v-if="form.props.stageKey == null || form.props.stageKey === undefined">
|
||||
未设置:钉钉回调时按节点名称或单据状态自动匹配(旧版兼容模式)
|
||||
</span>
|
||||
<span v-else-if="form.props.stageKey === ''" style="color: #ff7a00">
|
||||
纯过路审批:此节点通过后不触发任何集成动作,不改变单据状态
|
||||
</span>
|
||||
<span v-else style="color: #389e0d">
|
||||
关键节点:此节点通过后触发「{{ stageKeyLabel(form.props.stageKey) }}」环节的集成方案
|
||||
</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<!-- update-end---author:GHT ---date:2026-06-05 for:【XSLMES-20260605-K8R3】新增绑定审批环节配置,区分关键节点与纯过路审批节点----- -->
|
||||
|
||||
<!-- update-begin---author:GHT ---date:2026-06-05 for:【XSLMES-20260605-K8R2】节点改绑集成方案,停用 HTTP 回调接口配置----- -->
|
||||
<a-divider style="margin: 16px 0 12px">集成方案(审批联动业务)</a-divider>
|
||||
<a-alert
|
||||
type="info"
|
||||
show-icon
|
||||
style="margin-bottom: 12px"
|
||||
:message="
|
||||
pageActionOptions.length
|
||||
? '以下为该业务已标注(@ApprovalBizAction)的可选接口。审批到对应时机时,系统会以「当前处理人」身份调用所选接口(自动带上单据ID)。'
|
||||
: '该业务暂未标注可选接口(在后端 Controller 方法上加 @ApprovalBizAction 注解即可出现在此),也可手动填写接口路径。'
|
||||
"
|
||||
message="审批到对应时机时,由「集成方案管理」中已发布方案自动执行业务效果(如环节状态同步)。此处选择便于设计与核对;实际执行按 source_table + trigger_phase + trigger_stage 匹配。"
|
||||
/>
|
||||
<div v-for="phase in callbackPhases" :key="phase.key" class="fd-cb-block">
|
||||
<div class="fd-cb-title">{{ phase.label }}</div>
|
||||
<div v-for="(a, i) in form.props.callbackActions[phase.key]" :key="i" class="fd-cb-row">
|
||||
<a-input v-model:value="a.name" placeholder="动作名" :disabled="readonly" style="width: 88px" />
|
||||
<a-select v-model:value="a.method" :disabled="readonly" style="width: 82px" :options="methodOptions" />
|
||||
<a-input v-model:value="a.url" placeholder="接口路径 /xxx" :disabled="readonly" style="flex: 1; min-width: 120px" />
|
||||
<Icon v-if="!readonly" icon="ant-design:minus-circle-outlined" class="fd-cb-del" @click="removeAction(phase.key, i)" />
|
||||
<a-alert
|
||||
v-if="isLastApproverNode"
|
||||
type="warning"
|
||||
show-icon
|
||||
style="margin-bottom: 12px"
|
||||
message="当前为流程最后一个审批节点:「批准」类方案触发时机为「审批通过」,已合并到下方下拉(带 [流程最终通过] 前缀)。"
|
||||
/>
|
||||
<div class="fd-ip-block">
|
||||
<div class="fd-ip-title-row">
|
||||
<div class="fd-ip-title">{{ isLastApproverNode ? '本节点通过 / 流程最终通过时执行' : '本节点通过时执行' }}</div>
|
||||
<div v-if="!readonly" class="fd-ip-title-actions">
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
class="fd-ip-gen-btn"
|
||||
:loading="generatingPlan"
|
||||
:disabled="!canGeneratePlan"
|
||||
@click="handleGenerateIntegrationPlan"
|
||||
>
|
||||
生成集成方案
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="selectedPlanId"
|
||||
type="link"
|
||||
size="small"
|
||||
class="fd-ip-gen-btn"
|
||||
:loading="editingPlan"
|
||||
@click="handleEditIntegrationPlan"
|
||||
>
|
||||
编辑方案
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<a-space v-if="!readonly" style="margin-top: 4px" :size="6" wrap>
|
||||
<a-select
|
||||
v-if="pageActionOptions.length"
|
||||
style="width: 240px"
|
||||
placeholder="从页面按钮中选择…"
|
||||
:options="pageActionOptions"
|
||||
v-model:value="actionPicker[phase.key]"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
@select="(v) => addFromButton(phase.key, v)"
|
||||
/>
|
||||
<a-button size="small" @click="addAction(phase.key)">手动添加</a-button>
|
||||
</a-space>
|
||||
<a-select
|
||||
:value="primaryPlanValue"
|
||||
:disabled="readonly"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
placeholder="选择已发布的集成方案(可留空,由引擎自动匹配)"
|
||||
style="width: 100%"
|
||||
:options="primaryPlanOptions"
|
||||
@change="onPrimaryPlanChange"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!isLastApproverNode && planOptions.onApprove.length" class="fd-ip-block">
|
||||
<div class="fd-ip-title">流程最终通过时执行</div>
|
||||
<a-alert type="info" show-icon style="margin-bottom: 8px" message="仅流程最后一个审批节点需要配置;中间节点请只配置「本节点通过」。" />
|
||||
</div>
|
||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回统一回退,无需逐节点配置----- -->
|
||||
<a-alert
|
||||
type="success"
|
||||
show-icon
|
||||
style="margin-top: 4px"
|
||||
message="驳回 / 撤销 已全局统一:系统会自动执行该业务标注为「驳回时执行(@ApprovalBizAction onReject)」的接口完成回退,无需在此逐节点、逐流程配置。"
|
||||
style="margin-top: 8px"
|
||||
message="驳回 / 撤销:在「集成方案管理」配置 trigger_phase=onReject 的方案(如 REGISTRY_STAGE_REVERT),无需在节点单独配置。"
|
||||
/>
|
||||
<!-- update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回统一回退,无需逐节点配置----- -->
|
||||
<!-- update-end---author:GHT ---date:2026-06-05 for:【XSLMES-20260605-K8R2】节点改绑集成方案,停用 HTTP 回调接口配置----- -->
|
||||
</template>
|
||||
|
||||
<!-- 抄送人 -->
|
||||
@@ -196,59 +248,230 @@
|
||||
<a-button type="primary" @click="onConfirm">确定</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<MesXslIntegrationActionDrawer ref="actionDrawerRef" />
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, inject, watch, nextTick } from 'vue';
|
||||
import { computed, ref, inject, watch, nextTick, reactive } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { ApiSelect } from '/@/components/Form';
|
||||
import JSelectUser from '/@/components/Form/src/jeecg/components/JSelectUser.vue';
|
||||
import { OPERATOR_OPTIONS } from './flowTypes';
|
||||
import { OPERATOR_OPTIONS, collectApproverNodes } from './flowTypes';
|
||||
import type { FlowNode } from './flowTypes';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { getApprovalBizActions } from '../approvalFlow.api';
|
||||
import { listPublishedIntegrationPlans } from '../approvalFlow.api';
|
||||
import { generateForNode, queryPlanById } from '/@/views/xslmes/approval/integration/MesXslIntegrationPlan.api';
|
||||
import MesXslIntegrationActionDrawer from '/@/views/xslmes/approval/integration/components/MesXslIntegrationActionDrawer.vue';
|
||||
|
||||
const props = defineProps<{ readonly?: boolean }>();
|
||||
const emit = defineEmits(['confirm']);
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
// 当前审批流绑定的业务表(由 FlowDesign 注入),据此向后端查可选回调动作
|
||||
const bizTable = inject<Ref<string>>('approvalBizTable', ref(''));
|
||||
// 后端 @ApprovalBizAction 标注的可选业务动作
|
||||
const bizActions = ref<any[]>([]);
|
||||
const bizActionsTable = ref('');
|
||||
const flowId = inject<Ref<string>>('approvalFlowId', ref(''));
|
||||
const getFlowConfig = inject<(() => string) | null>('approvalFlowConfig', null);
|
||||
const flowRoot = inject<Ref<FlowNode | null>>('approvalFlowRoot', ref(null));
|
||||
const integrationPlansCacheKey = ref('');
|
||||
const generatingPlan = ref(false);
|
||||
const editingPlan = ref(false);
|
||||
const actionDrawerRef = ref();
|
||||
|
||||
async function loadBizActions() {
|
||||
const table = bizTable.value || '';
|
||||
if (!table || bizActionsTable.value === table) {
|
||||
const STAGE_LABELS: Record<string, string> = { proofread: '校对', audit: '审核', approve: '批准' };
|
||||
|
||||
const integrationPhases = [
|
||||
{ key: 'onNodeApprove', label: '本节点通过时执行' },
|
||||
{ key: 'onApprove', label: '流程最终通过时执行' },
|
||||
];
|
||||
|
||||
const planOptions = reactive<Record<string, { label: string; value: string }[]>>({
|
||||
onNodeApprove: [],
|
||||
onApprove: [],
|
||||
});
|
||||
|
||||
const isLastApproverNode = computed(() => {
|
||||
if (!node.value || node.value.type !== 'approver') return false;
|
||||
const approvers = collectApproverNodes(flowRoot.value);
|
||||
return approvers.length > 0 && approvers[approvers.length - 1].id === node.value.id;
|
||||
});
|
||||
|
||||
const primaryPlanOptions = computed(() => {
|
||||
const opts = (planOptions.onNodeApprove || []).map((o) => ({
|
||||
label: o.label,
|
||||
value: `onNodeApprove:${o.value}`,
|
||||
}));
|
||||
if (isLastApproverNode.value) {
|
||||
(planOptions.onApprove || []).forEach((o) => {
|
||||
opts.push({
|
||||
label: `[流程最终通过] ${o.label}`,
|
||||
value: `onApprove:${o.value}`,
|
||||
});
|
||||
});
|
||||
}
|
||||
return opts;
|
||||
});
|
||||
|
||||
const primaryPlanValue = computed(() => {
|
||||
const ip = form.value?.props?.integrationPlans;
|
||||
if (!ip) return undefined;
|
||||
if (ip.onNodeApprove) return `onNodeApprove:${ip.onNodeApprove}`;
|
||||
if (isLastApproverNode.value && ip.onApprove) return `onApprove:${ip.onApprove}`;
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const canGeneratePlan = computed(() => {
|
||||
const sk = form.value?.props?.stageKey;
|
||||
return !!sk && sk !== '' && !!bizTable.value && !!flowId.value && !!node.value?.id;
|
||||
});
|
||||
|
||||
/** 当前节点已绑定的集成方案 ID(含草稿,下拉未列出也可编辑) */
|
||||
const selectedPlanId = computed(() => {
|
||||
const ip = form.value?.props?.integrationPlans;
|
||||
if (!ip) return undefined;
|
||||
if (ip.onNodeApprove) return ip.onNodeApprove;
|
||||
if (ip.onApprove) return ip.onApprove;
|
||||
return undefined;
|
||||
});
|
||||
|
||||
// update-begin---author:GHT ---date:2026-06-05 for:【XSLMES-20260605-K8R3】绑定审批环节辅助函数-----
|
||||
function stageKeyLabel(key: string): string {
|
||||
const map: Record<string, string> = { proofread: '校对', audit: '审核', approve: '批准' };
|
||||
return map[key] || key;
|
||||
}
|
||||
|
||||
function onStageKeyChange(val: string | undefined) {
|
||||
if (!form.value) return;
|
||||
// allow-clear 触发时 val=undefined,表示「未设置」(null → 启发式兼容)
|
||||
// 选了空串选项表示「明确不绑定」("" → 纯过路审批)
|
||||
form.value.props.stageKey = val === undefined ? undefined : val;
|
||||
}
|
||||
// update-end---author:GHT ---date:2026-06-05 for:【XSLMES-20260605-K8R3】绑定审批环节辅助函数-----
|
||||
|
||||
function onPrimaryPlanChange(val?: string) {
|
||||
if (!form.value) return;
|
||||
form.value.props.integrationPlans.onNodeApprove = undefined;
|
||||
form.value.props.integrationPlans.onApprove = undefined;
|
||||
if (!val) return;
|
||||
const sep = val.indexOf(':');
|
||||
if (sep <= 0) return;
|
||||
const phase = val.slice(0, sep);
|
||||
const planId = val.slice(sep + 1);
|
||||
form.value.props.integrationPlans[phase] = planId;
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-10 for:【审批流设计】节点内生成集成方案并打开动作配置-----------
|
||||
async function handleGenerateIntegrationPlan() {
|
||||
if (readonly.value || !form.value || !node.value) return;
|
||||
const stageKey = form.value.props?.stageKey;
|
||||
if (!stageKey) {
|
||||
createMessage.warning('请先在「绑定审批环节」中选择校对、审核或批准');
|
||||
return;
|
||||
}
|
||||
if (stageKey === '') {
|
||||
createMessage.warning('纯过路审批节点无需生成集成方案');
|
||||
return;
|
||||
}
|
||||
if (!bizTable.value || !flowId.value) {
|
||||
createMessage.warning('缺少业务表或审批流信息');
|
||||
return;
|
||||
}
|
||||
generatingPlan.value = true;
|
||||
try {
|
||||
const res = await getApprovalBizActions(table);
|
||||
bizActions.value = Array.isArray(res) ? res : [];
|
||||
bizActionsTable.value = table;
|
||||
} catch (e) {
|
||||
bizActions.value = [];
|
||||
const data = await generateForNode({
|
||||
sourceTable: bizTable.value,
|
||||
flowId: flowId.value,
|
||||
nodeId: node.value.id,
|
||||
stageKey,
|
||||
flowConfig: getFlowConfig?.() || undefined,
|
||||
overwriteDraft: true,
|
||||
});
|
||||
integrationPlansCacheKey.value = '';
|
||||
await loadIntegrationPlans();
|
||||
|
||||
const phase = data?.triggerPhase || 'onNodeApprove';
|
||||
if (!form.value.props.integrationPlans) {
|
||||
form.value.props.integrationPlans = {};
|
||||
}
|
||||
form.value.props.integrationPlans.onNodeApprove = undefined;
|
||||
form.value.props.integrationPlans.onApprove = undefined;
|
||||
form.value.props.integrationPlans[phase] = data.planId;
|
||||
|
||||
createMessage.success(data?.created ? '已生成集成方案,请配置动作' : '已加载已有集成方案,请配置动作');
|
||||
|
||||
const plan = await queryPlanById(data.planId);
|
||||
if (plan) {
|
||||
await actionDrawerRef.value?.openAndEditFirstAction(plan);
|
||||
}
|
||||
} catch (e: any) {
|
||||
createMessage.error(e?.message || '生成集成方案失败');
|
||||
} finally {
|
||||
generatingPlan.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 可选动作下拉项(含真实 url/method)
|
||||
const pageActionOptions = computed(() =>
|
||||
(bizActions.value || []).map((a) => ({
|
||||
label: `${a.name}(${a.method} ${a.url})`,
|
||||
value: a.url,
|
||||
raw: a,
|
||||
})),
|
||||
);
|
||||
async function handleEditIntegrationPlan() {
|
||||
const planId = selectedPlanId.value;
|
||||
if (!planId) {
|
||||
createMessage.warning('请先选择或生成集成方案');
|
||||
return;
|
||||
}
|
||||
editingPlan.value = true;
|
||||
try {
|
||||
const plan = await queryPlanById(planId);
|
||||
if (!plan) {
|
||||
createMessage.error('未找到该集成方案');
|
||||
return;
|
||||
}
|
||||
await actionDrawerRef.value?.open(plan);
|
||||
} catch (e: any) {
|
||||
createMessage.error(e?.message || '加载集成方案失败');
|
||||
} finally {
|
||||
editingPlan.value = false;
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-06-10 for:【审批流设计】节点内生成集成方案并打开动作配置-----------
|
||||
|
||||
async function loadIntegrationPlans() {
|
||||
const table = bizTable.value || '';
|
||||
if (!table) {
|
||||
planOptions.onNodeApprove = [];
|
||||
planOptions.onApprove = [];
|
||||
return;
|
||||
}
|
||||
const cacheKey = table;
|
||||
if (integrationPlansCacheKey.value === cacheKey) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
for (const phase of integrationPhases) {
|
||||
const res = await listPublishedIntegrationPlans({ sourceTable: table, triggerPhase: phase.key });
|
||||
const records = res?.records ?? [];
|
||||
planOptions[phase.key] = records.map((p: any) => ({
|
||||
label: formatPlanLabel(p),
|
||||
value: p.id,
|
||||
}));
|
||||
}
|
||||
integrationPlansCacheKey.value = cacheKey;
|
||||
} catch (_e) {
|
||||
planOptions.onNodeApprove = [];
|
||||
planOptions.onApprove = [];
|
||||
}
|
||||
}
|
||||
|
||||
function formatPlanLabel(plan: any) {
|
||||
const stage = plan?.triggerStage ? STAGE_LABELS[plan.triggerStage] || plan.triggerStage : '';
|
||||
const code = plan?.planCode ? ` [${plan.planCode}]` : '';
|
||||
return `${plan.planName || plan.planCode || plan.id}${stage ? '(' + stage + ')' : ''}${code}`;
|
||||
}
|
||||
|
||||
watch(
|
||||
bizTable,
|
||||
() => {
|
||||
loadBizActions();
|
||||
integrationPlansCacheKey.value = '';
|
||||
loadIntegrationPlans();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
@@ -256,24 +479,10 @@
|
||||
const open = ref(false);
|
||||
const node = ref<FlowNode | null>(null);
|
||||
const form = ref<any>(null);
|
||||
// 「从页面按钮中选择」下拉的临时选中值(仅作选择器,选完即清空,避免跨节点残留上次的选项)
|
||||
const actionPicker = ref<Record<string, any>>({ onNodeApprove: undefined, onApprove: undefined, onReject: undefined });
|
||||
|
||||
const operatorOptions = OPERATOR_OPTIONS;
|
||||
const readonly = computed(() => !!props.readonly);
|
||||
|
||||
// 回调接口配置:通过类时机需按节点配置;驳回类(onReject)已全局统一(后端按 @ApprovalBizAction 自动执行),无需在此逐节点维护
|
||||
const callbackPhases = [
|
||||
{ key: 'onNodeApprove', label: '本节点通过时执行' },
|
||||
{ key: 'onApprove', label: '流程最终通过时执行' },
|
||||
];
|
||||
const methodOptions = [
|
||||
{ label: 'POST', value: 'POST' },
|
||||
{ label: 'GET', value: 'GET' },
|
||||
{ label: 'PUT', value: 'PUT' },
|
||||
{ label: 'DELETE', value: 'DELETE' },
|
||||
];
|
||||
|
||||
const title = computed(() => {
|
||||
const map: Record<string, string> = { start: '发起人设置', approver: '审批人设置', cc: '抄送人设置', branch: '条件设置' };
|
||||
return map[node.value?.type || ''] || '节点设置';
|
||||
@@ -283,48 +492,23 @@
|
||||
|
||||
function openDrawer(n: FlowNode) {
|
||||
node.value = n;
|
||||
// 切换节点时清空下拉选择器的残留值
|
||||
actionPicker.value = { onNodeApprove: undefined, onApprove: undefined, onReject: undefined };
|
||||
// 编辑副本,确定时回写,避免取消后脏数据
|
||||
form.value = { name: n.name, props: cloneDeep(n.props) };
|
||||
// 审批人节点确保回调接口配置结构存在
|
||||
if (n.type === 'approver') {
|
||||
const cb = form.value.props.callbackActions || {};
|
||||
form.value.props.callbackActions = {
|
||||
onNodeApprove: Array.isArray(cb.onNodeApprove) ? cb.onNodeApprove : [],
|
||||
onApprove: Array.isArray(cb.onApprove) ? cb.onApprove : [],
|
||||
onReject: Array.isArray(cb.onReject) ? cb.onReject : [],
|
||||
const ip = form.value.props.integrationPlans || {};
|
||||
form.value.props.integrationPlans = {
|
||||
onNodeApprove: ip.onNodeApprove || undefined,
|
||||
onApprove: ip.onApprove || undefined,
|
||||
};
|
||||
// 清空历史 HTTP 回调配置,避免与集成方案双写
|
||||
form.value.props.callbackActions = { onNodeApprove: [], onApprove: [], onReject: [] };
|
||||
// update-begin---author:GHT ---date:2026-06-09 for:【审批流设计】打开节点配置时强制刷新集成方案下拉,避免先开设计器后生成方案导致缓存空值-----
|
||||
integrationPlansCacheKey.value = '';
|
||||
loadIntegrationPlans();
|
||||
// update-end---author:GHT ---date:2026-06-09 for:【审批流设计】打开节点配置时强制刷新集成方案下拉,避免先开设计器后生成方案导致缓存空值-----
|
||||
}
|
||||
open.value = true;
|
||||
}
|
||||
|
||||
function addAction(phaseKey: string) {
|
||||
form.value.props.callbackActions[phaseKey].push({ name: '', method: 'POST', url: '' });
|
||||
}
|
||||
|
||||
function removeAction(phaseKey: string, i: number) {
|
||||
form.value.props.callbackActions[phaseKey].splice(i, 1);
|
||||
}
|
||||
|
||||
/** 从「已标注的业务动作」中选择一个接口加入回调(按 url 值查回原始动作) */
|
||||
function addFromButton(phaseKey: string, url: string) {
|
||||
const opt = pageActionOptions.value.find((o) => o.value === url);
|
||||
const raw = opt?.raw;
|
||||
if (!raw || !raw.url) {
|
||||
return;
|
||||
}
|
||||
const list = form.value.props.callbackActions[phaseKey];
|
||||
// 选完即清空下拉,使其回到 placeholder(仅作选择器用)
|
||||
actionPicker.value[phaseKey] = undefined;
|
||||
if (list.some((a) => a.url === raw.url)) {
|
||||
createMessage.info('该接口已添加');
|
||||
return;
|
||||
}
|
||||
list.push({ name: raw.name, method: raw.method || 'POST', url: raw.url });
|
||||
}
|
||||
|
||||
/** 切换审批方式时,若切为单人则自动裁剪 userText 至第一个 */
|
||||
function onMultiModeChange() {
|
||||
if (!form.value) return;
|
||||
if (form.value.props.multiMode === 'none') {
|
||||
@@ -332,7 +516,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/** userText 变化时,若当前是单人模式则裁剪 */
|
||||
function onUserTextChange() {
|
||||
if (!form.value) return;
|
||||
if (form.value.props.multiMode === 'none') {
|
||||
@@ -355,7 +538,6 @@
|
||||
|
||||
function onConfirm() {
|
||||
if (node.value && form.value) {
|
||||
// 单人审批最终兜底校验
|
||||
if (
|
||||
node.value.type === 'approver' &&
|
||||
form.value.props.multiMode === 'none' &&
|
||||
@@ -397,29 +579,40 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 回调接口配置 */
|
||||
.fd-cb-block {
|
||||
.fd-ip-block {
|
||||
margin-bottom: 14px;
|
||||
padding: 10px;
|
||||
background: #fafafa;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.fd-cb-title {
|
||||
.fd-ip-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #595959;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.fd-cb-row {
|
||||
.fd-ip-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
.fd-ip-title {
|
||||
margin-bottom: 0;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
.fd-cb-del {
|
||||
color: #ff4d4f;
|
||||
cursor: pointer;
|
||||
.fd-ip-gen-btn {
|
||||
padding: 0 4px;
|
||||
height: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.fd-ip-title-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -56,6 +56,14 @@
|
||||
color: #999;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&.fd-palette-empty {
|
||||
color: #d48806;
|
||||
background: #fffbe6;
|
||||
border: 1px solid #ffe58f;
|
||||
border-radius: 6px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.fd-palette-list {
|
||||
|
||||
@@ -81,8 +81,8 @@ export function createCcNode(): FlowNode {
|
||||
};
|
||||
}
|
||||
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】按当前页解析的字段生成审批阶段节点-----
|
||||
/** 解析出的页面阶段字段 */
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】按审批注册中心启用环节生成审批阶段节点-----
|
||||
/** 审批注册中心启用环节 */
|
||||
export interface StageField {
|
||||
stageKey: string;
|
||||
stageName: string;
|
||||
@@ -92,9 +92,8 @@ export interface StageField {
|
||||
}
|
||||
|
||||
/**
|
||||
* 由"当前页字段"生成阶段节点:
|
||||
* 审批阶段(校对/审核/审批/分发) -> 审批人节点,处理人=取单据该字段中的人员;
|
||||
* 抄送阶段 -> 抄送节点,抄送人=取单据该字段中的人员。
|
||||
* 由审批注册中心启用环节生成阶段节点:
|
||||
* 审批环节(校对/审核/批准) -> 审批人节点,处理人=取注册中心配置的人员字段。
|
||||
*/
|
||||
export function createStageNode(stage: StageField): FlowNode {
|
||||
const fieldLabel = stage.fieldComment || stage.field;
|
||||
@@ -111,9 +110,10 @@ export function createStageNode(stage: StageField): FlowNode {
|
||||
node.props.approverType = 'field';
|
||||
node.props.fieldName = stage.field;
|
||||
node.props.fieldLabel = fieldLabel;
|
||||
node.props.stageKey = stage.stageKey;
|
||||
return node;
|
||||
}
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】按当前页解析的字段生成审批阶段节点-----
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】按审批注册中心启用环节生成审批阶段节点-----
|
||||
|
||||
/** 条件分支节点(默认两条分支:条件 + 其它情况) */
|
||||
export function createConditionNode(): FlowNode {
|
||||
@@ -202,6 +202,25 @@ function operatorText(op: string): string {
|
||||
return OPERATOR_OPTIONS.find((o) => o.value === op)?.label || op;
|
||||
}
|
||||
|
||||
/** 按主流程顺序收集审批人节点(不含条件分支内的审批人) */
|
||||
export function collectApproverNodes(node: FlowNode | null | undefined): FlowNode[] {
|
||||
const result: FlowNode[] = [];
|
||||
let current = node;
|
||||
while (current) {
|
||||
if (current.type === 'approver') {
|
||||
result.push(current);
|
||||
}
|
||||
if (current.type === 'condition' && current.conditionNodes?.length) {
|
||||
const firstBranch = current.conditionNodes[0];
|
||||
if (firstBranch?.childNode) {
|
||||
result.push(...collectApproverNodes(firstBranch.childNode));
|
||||
}
|
||||
}
|
||||
current = current.childNode || null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 深度遍历每个节点(含分支) */
|
||||
export function eachNode(node: FlowNode | null | undefined, cb: (n: FlowNode) => void) {
|
||||
if (!node) return;
|
||||
|
||||
@@ -14,8 +14,10 @@ enum Api {
|
||||
|
||||
/**
|
||||
* 已发布审批流列表(可发起的单据类型)
|
||||
* @param routePath 当前功能页路由;传入时与钉钉审批按钮一致,按 mes_xsl_ding_tpl_bind 绑定页过滤
|
||||
*/
|
||||
export const getPublishedFlows = () => defHttp.get({ url: Api.publishedList });
|
||||
export const getPublishedFlows = (routePath?: string) =>
|
||||
defHttp.get({ url: Api.publishedList, params: routePath ? { routePath } : undefined });
|
||||
|
||||
/**
|
||||
* 根据审批流查询其绑定单据的记录列表
|
||||
|
||||
@@ -12,6 +12,7 @@ enum Api {
|
||||
deleteThirdAccount = '/sys/thirdApp/deleteThirdAccount',
|
||||
deleteThirdAppConfig = '/sys/thirdApp/deleteThirdAppConfig',
|
||||
testKingdeeConnect = '/sys/thirdApp/testKingdeeConnect',
|
||||
getDingTalkStreamNodeInfo = '/xslmes/dingtalk/stream/nodeInfo',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,4 +87,11 @@ export const deleteThirdAppConfig = (params, handleSuccess) => {
|
||||
*/
|
||||
export const testKingdeeConnect = (params) => {
|
||||
return defHttp.get({ url: Api.testKingdeeConnect, params }, { isTransformResponse: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取本机钉钉 Stream 节点信息(主机名、IP、是否接收)
|
||||
*/
|
||||
export const getDingTalkStreamNodeInfo = () => {
|
||||
return defHttp.get({ url: Api.getDingTalkStreamNodeInfo });
|
||||
};
|
||||
@@ -77,7 +77,57 @@ export const thirdAppFormSchema: FormSchema[] = [
|
||||
},
|
||||
helpMessage: '开启后,此应用的 AppKey/AppSecret 将用于接收钉钉 Stream 事件推送(审批结果等)。同一企业只需开启一个',
|
||||
defaultValue: 0,
|
||||
},{
|
||||
},
|
||||
{
|
||||
label: '限制接收节点',
|
||||
field: 'streamReceiverEnabled',
|
||||
component: 'Switch',
|
||||
ifShow: ({ values }) => values.thirdType === 'dingtalk' && values.streamEnabled === 1,
|
||||
componentProps: {
|
||||
checkedChildren: '已限制',
|
||||
checkedValue: 1,
|
||||
unCheckedChildren: '不限制',
|
||||
unCheckedValue: 0,
|
||||
},
|
||||
helpMessage: '开启后,仅下方白名单内的服务器会建立 Stream 连接并处理审批回调(推荐用 IP)',
|
||||
defaultValue: 0,
|
||||
},
|
||||
{
|
||||
label: '允许接收的IP',
|
||||
field: 'streamDesignatedIps',
|
||||
component: 'InputTextArea',
|
||||
ifShow: ({ values }) =>
|
||||
values.thirdType === 'dingtalk' && values.streamEnabled === 1 && values.streamReceiverEnabled === 1,
|
||||
componentProps: {
|
||||
rows: 2,
|
||||
placeholder: '多个 IP 用逗号分隔,例如 192.168.1.74(可参考页面展示的本机 IP)',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '允许接收的主机名',
|
||||
field: 'streamDesignatedHosts',
|
||||
component: 'Input',
|
||||
ifShow: ({ values }) =>
|
||||
values.thirdType === 'dingtalk' && values.streamEnabled === 1 && values.streamReceiverEnabled === 1,
|
||||
componentProps: {
|
||||
placeholder: '可选,多个用逗号分隔(不如 IP 稳定)',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '集群Redis选主',
|
||||
field: 'streamClusterMode',
|
||||
component: 'Switch',
|
||||
ifShow: ({ values }) => values.thirdType === 'dingtalk' && values.streamEnabled === 1,
|
||||
componentProps: {
|
||||
checkedChildren: '开启',
|
||||
checkedValue: 1,
|
||||
unCheckedChildren: '关闭',
|
||||
unCheckedValue: 0,
|
||||
},
|
||||
helpMessage: '多实例共用同一 AppKey 时务必开启,避免多台服务器同时建连抢消息',
|
||||
defaultValue: 1,
|
||||
},
|
||||
{
|
||||
label: '租户id',
|
||||
field: 'tenantId',
|
||||
component: 'Input',
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
<template>
|
||||
<BasicModal @register="registerModal" :width="800" :title="title" @ok="handleSubmit">
|
||||
<BasicModal @register="registerModal" :width="860" :title="title" @ok="handleSubmit">
|
||||
<a-alert
|
||||
v-if="showStreamNodeHint"
|
||||
type="info"
|
||||
show-icon
|
||||
style="margin-bottom: 12px"
|
||||
:message="streamNodeHintTitle"
|
||||
:description="streamNodeHintDesc"
|
||||
/>
|
||||
<BasicForm @register="registerForm" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
@@ -9,12 +17,15 @@
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { useForm, BasicForm } from '/@/components/Form';
|
||||
import { thirdAppFormSchema } from './ThirdApp.data';
|
||||
import { getThirdConfigByTenantId, saveOrUpdateThirdConfig } from './ThirdApp.api';
|
||||
import { getThirdConfigByTenantId, saveOrUpdateThirdConfig, getDingTalkStreamNodeInfo } from './ThirdApp.api';
|
||||
export default defineComponent({
|
||||
name: 'ThirdAppConfigModal',
|
||||
components: { BasicModal, BasicForm },
|
||||
setup(props, { emit }) {
|
||||
const title = ref<string>('钉钉配置');
|
||||
const showStreamNodeHint = ref(false);
|
||||
const streamNodeHintTitle = ref('本机 Stream 节点信息');
|
||||
const streamNodeHintDesc = ref('');
|
||||
//表单配置
|
||||
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
|
||||
schemas: thirdAppFormSchema,
|
||||
@@ -25,8 +36,23 @@
|
||||
//表单赋值
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
setModalProps({ confirmLoading: true });
|
||||
showStreamNodeHint.value = false;
|
||||
streamNodeHintDesc.value = '';
|
||||
if (data.thirdType == 'dingtalk') {
|
||||
title.value = '钉钉配置';
|
||||
try {
|
||||
const nodeInfo = await getDingTalkStreamNodeInfo();
|
||||
if (nodeInfo) {
|
||||
showStreamNodeHint.value = true;
|
||||
const ips = Array.isArray(nodeInfo.localIps) ? nodeInfo.localIps.join(', ') : '';
|
||||
const receiverText = nodeInfo.thisNodeReceiver ? '是(本机将接收回调)' : '否(本机不接收回调)';
|
||||
streamNodeHintDesc.value =
|
||||
`主机名:${nodeInfo.hostName || '-'}\n本机IP:${ips || '-'}\n当前是否接收:${receiverText}\n` +
|
||||
`提示:开启「限制接收节点」后,请将本机局域网 IP 填入「允许接收的IP」`;
|
||||
}
|
||||
} catch (e) {
|
||||
// 接口不可用时忽略
|
||||
}
|
||||
} else if (data.thirdType == 'kingdee') {
|
||||
title.value = '金蝶配置';
|
||||
} else {
|
||||
@@ -60,6 +86,9 @@
|
||||
|
||||
return {
|
||||
title,
|
||||
showStreamNodeHint,
|
||||
streamNodeHintTitle,
|
||||
streamNodeHintDesc,
|
||||
registerForm,
|
||||
registerModal,
|
||||
handleSubmit,
|
||||
|
||||
@@ -53,6 +53,49 @@
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="appConfigData.streamEnabled === 1">
|
||||
<div class="flex-flow">
|
||||
<div class="base-title">接收限制</div>
|
||||
<div class="base-message" style="display:flex;align-items:center;min-height:50px;">
|
||||
<a-tag :color="appConfigData.streamReceiverEnabled === 1 ? 'orange' : 'default'">
|
||||
{{ appConfigData.streamReceiverEnabled === 1 ? '仅白名单节点' : '不限制节点' }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-flow" v-if="appConfigData.streamReceiverEnabled === 1">
|
||||
<div class="base-title">允许IP</div>
|
||||
<div class="base-message" style="min-height:50px;line-height:1.6;padding-top:12px;">
|
||||
{{ appConfigData.streamDesignatedIps || '(未配置,所有节点均可接收)' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-flow">
|
||||
<div class="base-title">集群选主</div>
|
||||
<div class="base-message" style="display:flex;align-items:center;height:50px;">
|
||||
<a-tag :color="appConfigData.streamClusterMode === 1 ? 'blue' : 'default'">
|
||||
{{
|
||||
appConfigData.streamClusterMode === 1
|
||||
? 'Redis选主已开启'
|
||||
: appConfigData.streamClusterMode === 0
|
||||
? '单节点直连'
|
||||
: '沿用YAML默认'
|
||||
}}
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-flow" v-if="streamNodeInfo.hostName">
|
||||
<div class="base-title">本机信息</div>
|
||||
<div class="base-message stream-node-info">
|
||||
<div>主机名:{{ streamNodeInfo.hostName }}</div>
|
||||
<div>本机IP:{{ (streamNodeInfo.localIps || []).join(', ') }}</div>
|
||||
<div>
|
||||
本机是否接收:
|
||||
<a-tag :color="streamNodeInfo.thisNodeReceiver ? 'green' : 'default'" style="margin-left:4px">
|
||||
{{ streamNodeInfo.thisNodeReceiver ? '是' : '否' }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div style="margin-top: 20px; width: 100%; text-align: right">
|
||||
<a-button @click="dingEditClick">编辑</a-button>
|
||||
<a-button v-if="appConfigData.id" @click="cancelBindClick" danger style="margin-left: 10px">取消绑定</a-button>
|
||||
@@ -85,7 +128,12 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, h, inject, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { getThirdConfigByTenantId, syncDingTalkDepartUserToLocal, deleteThirdAppConfig } from './ThirdApp.api';
|
||||
import {
|
||||
getThirdConfigByTenantId,
|
||||
syncDingTalkDepartUserToLocal,
|
||||
deleteThirdAppConfig,
|
||||
getDingTalkStreamNodeInfo,
|
||||
} from './ThirdApp.api';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import ThirdAppConfigModal from './ThirdAppConfigModal.vue';
|
||||
import { Modal } from 'ant-design-vue';
|
||||
@@ -109,6 +157,7 @@
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
});
|
||||
const streamNodeInfo = ref<any>({});
|
||||
|
||||
//企业微信钉钉配置modal
|
||||
const [registerAppConfigModal, { openModal }] = useModal();
|
||||
@@ -132,7 +181,12 @@
|
||||
if (values) {
|
||||
appConfigData.value = values;
|
||||
} else {
|
||||
appConfigData.value = "";
|
||||
appConfigData.value = '';
|
||||
}
|
||||
try {
|
||||
streamNodeInfo.value = (await getDingTalkStreamNodeInfo()) || {};
|
||||
} catch (e) {
|
||||
streamNodeInfo.value = {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,6 +306,7 @@
|
||||
|
||||
return {
|
||||
appConfigData,
|
||||
streamNodeInfo,
|
||||
collapseActiveKey,
|
||||
registerAppConfigModal,
|
||||
dingEditClick,
|
||||
@@ -331,4 +386,10 @@
|
||||
top: 2px
|
||||
}
|
||||
:deep(.ant-collapse-borderless >.ant-collapse-item:last-child) {border-bottom-width:1px;}
|
||||
.stream-node-info {
|
||||
line-height: 1.8;
|
||||
padding-top: 10px;
|
||||
font-size: 13px;
|
||||
color: @text-color-secondary;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,13 @@
|
||||
<template v-else>
|
||||
<!-- 单条:详情表 -->
|
||||
<template v-if="isSingleItem">
|
||||
<div class="im-biz-record-item">
|
||||
<div class="im-biz-record-item" :class="{ 'im-biz-record-item--ding': isDingStyleCard }">
|
||||
<!-- update-begin---author:GHT ---date:2026-06-10 for:【IM审批通用化】钉钉模板样式卡片头----------- -->
|
||||
<div v-if="isDingStyleCard" class="im-biz-record-ding-header">
|
||||
<span class="im-biz-record-ding-badge">审批</span>
|
||||
<span class="im-biz-record-ding-title">{{ dingCardTitle }}</span>
|
||||
</div>
|
||||
<!-- update-end---author:GHT ---date:2026-06-10 for:【IM审批通用化】钉钉模板样式卡片头----------- -->
|
||||
<div class="im-biz-record-table-wrap">
|
||||
<table class="im-biz-record-table im-biz-record-table--detail">
|
||||
<tbody>
|
||||
@@ -167,6 +173,15 @@
|
||||
const singleItem = computed(() => props.payload.items[0]);
|
||||
const listColumnLabels = computed(() => resolveImBizRecordListColumnLabels(props.payload.items));
|
||||
|
||||
// update-begin---author:GHT ---date:2026-06-10 for:【IM审批通用化】钉钉模板样式卡片-----------
|
||||
const isDingStyleCard = computed(
|
||||
() => props.payload.cardStyle === 'ding' || !!props.payload.templateName,
|
||||
);
|
||||
const dingCardTitle = computed(
|
||||
() => props.payload.templateName || props.payload.pageTitle || '审批单',
|
||||
);
|
||||
// update-end---author:GHT ---date:2026-06-10 for:【IM审批通用化】钉钉模板样式卡片-----------
|
||||
|
||||
// 审批卡片:单条且带审批实例ID
|
||||
const isApprovalCard = computed(() => isSingleItem.value && !!singleItem.value?.instanceId);
|
||||
|
||||
@@ -425,6 +440,50 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
&--ding {
|
||||
.im-biz-record-table-wrap {
|
||||
border-color: #ffe7ba;
|
||||
}
|
||||
|
||||
.im-biz-record-table--detail th {
|
||||
background: #fff7e6;
|
||||
color: #873800;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.im-biz-record-ding-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
background: linear-gradient(90deg, #fff7e6 0%, #fff 100%);
|
||||
border: 1px solid #ffe7ba;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.im-biz-record-ding-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 36px;
|
||||
padding: 0 6px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
background: #ff6900;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.im-biz-record-ding-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 审批办理按钮栏 */
|
||||
|
||||
@@ -59,21 +59,32 @@
|
||||
placement="right"
|
||||
>
|
||||
<div
|
||||
:class="['conv-item', activeTargetUserId === item.id ? 'active' : '']"
|
||||
:class="[
|
||||
'conv-item',
|
||||
{ active: activeTargetUserId === item.id, 'conv-item--work-notify': isWorkNotifyContact(item) },
|
||||
]"
|
||||
@click="selectMember(item)"
|
||||
>
|
||||
<a-badge :count="shouldShowUnread(item) ? item.unreadCount : 0" :offset="[-2, 2]">
|
||||
<a-avatar :size="leftCollapsed ? 36 : 40" :src="getAvatarUrl(item.avatar)">
|
||||
<a-avatar
|
||||
v-if="isWorkNotifyContact(item)"
|
||||
:size="leftCollapsed ? 36 : 40"
|
||||
:style="{ backgroundColor: '#fa8c16' }"
|
||||
>
|
||||
<Icon icon="ant-design:notification-outlined" />
|
||||
</a-avatar>
|
||||
<a-avatar v-else :size="leftCollapsed ? 36 : 40" :src="getAvatarUrl(item.avatar)">
|
||||
{{ (item.realname || item.username || '?').slice(0, 1) }}
|
||||
</a-avatar>
|
||||
</a-badge>
|
||||
<div v-show="!leftCollapsed" class="conv-meta">
|
||||
<div class="conv-top">
|
||||
<span class="conv-name">{{ item.realname || item.username }}</span>
|
||||
<span v-if="isWorkNotifyContact(item)" class="conv-tag">公众号</span>
|
||||
<span class="conv-time">{{ formatTime(item.lastTime) }}</span>
|
||||
</div>
|
||||
<div class="conv-bottom">
|
||||
<span class="conv-preview">{{ formatConvPreview(item.lastContent) }}</span>
|
||||
<span class="conv-preview">{{ formatConvPreview(item.lastContent, isWorkNotifyContact(item)) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -203,7 +214,9 @@
|
||||
<Icon v-if="embeddedPageContextClickable" icon="ant-design:select-outlined" class="im-page-context-action" />
|
||||
</div>
|
||||
<!--update-end---author:xsl ---date:20260528 for:【IM聊天-OA】弹窗输入框上方展示背后功能页名称------------->
|
||||
<div v-if="isWorkNotifyChat" class="im-work-notify-readonly-tip">工作通知为系统消息通道,仅接收审批等工作消息</div>
|
||||
<ImChatInput
|
||||
v-else
|
||||
v-model="draft"
|
||||
:disabled="!activeConversationId"
|
||||
:sending="sending"
|
||||
@@ -261,6 +274,7 @@
|
||||
import { syncImUnreadFromMembers } from './useImUnread';
|
||||
import {
|
||||
type ImMemberItem,
|
||||
IM_CONTACT_TYPE_WORK_NOTIFY,
|
||||
type ImMessageItem,
|
||||
getCachedMembers,
|
||||
isMembersCacheStale,
|
||||
@@ -539,9 +553,15 @@
|
||||
return parseImBizRecordPayload(content);
|
||||
}
|
||||
|
||||
function formatConvPreview(content?: string) {
|
||||
function isWorkNotifyContact(item?: DeptMemberItem | null) {
|
||||
return item?.contactType === IM_CONTACT_TYPE_WORK_NOTIFY || item?.username === 'im_work_notify';
|
||||
}
|
||||
|
||||
const isWorkNotifyChat = computed(() => isWorkNotifyContact(activeMember.value));
|
||||
|
||||
function formatConvPreview(content?: string, isWorkNotify = false) {
|
||||
if (!content) {
|
||||
return '点击开始聊天';
|
||||
return isWorkNotify ? '暂无系统通知' : '点击开始聊天';
|
||||
}
|
||||
return formatImMessagePreview(content);
|
||||
}
|
||||
@@ -948,7 +968,9 @@
|
||||
function handleOpenCreateGroupModal() {
|
||||
// useModalInner 必须传入 data 才会触发打开回调,否则不会加载成员列表
|
||||
openCreateGroupModal(true, {
|
||||
members: deptMembers.value.filter((item) => item.id !== currentUserId.value),
|
||||
members: deptMembers.value.filter(
|
||||
(item) => item.id !== currentUserId.value && !isWorkNotifyContact(item),
|
||||
),
|
||||
});
|
||||
}
|
||||
//update-end---author:xsl ---date:20260528 for:【IM聊天-OA】发起群聊弹窗需传 data 触发成员加载-----------
|
||||
@@ -1917,6 +1939,36 @@
|
||||
&.active {
|
||||
background: #eef4ff;
|
||||
}
|
||||
|
||||
&.conv-item--work-notify {
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
background: #fff7e6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.conv-tag {
|
||||
flex-shrink: 0;
|
||||
margin-left: 4px;
|
||||
padding: 0 6px;
|
||||
border-radius: 10px;
|
||||
background: #fff7e6;
|
||||
color: #fa8c16;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.im-work-notify-readonly-tip {
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
background: #fafafa;
|
||||
color: #8c8c8c;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.conv-meta {
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
import { createGroupConversation, fetchDeptMembers } from './im.api';
|
||||
import { getCachedMembers } from './imCache';
|
||||
import { getCachedMembers, IM_CONTACT_TYPE_WORK_NOTIFY } from './imCache';
|
||||
|
||||
defineOptions({ name: 'ImCreateGroupModal' });
|
||||
|
||||
@@ -73,7 +73,13 @@
|
||||
function resolveSelectableMembers(source?: Recordable[]) {
|
||||
const currentUserId = userStore.getUserInfo?.id || '';
|
||||
const list = source?.length ? source : getCachedMembers() || [];
|
||||
return list.filter((item) => item.id && item.id !== currentUserId);
|
||||
return list.filter(
|
||||
(item) =>
|
||||
item.id &&
|
||||
item.id !== currentUserId &&
|
||||
item.contactType !== IM_CONTACT_TYPE_WORK_NOTIFY &&
|
||||
item.username !== 'im_work_notify',
|
||||
);
|
||||
}
|
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data?: { members?: Recordable[] }) => {
|
||||
|
||||
@@ -34,6 +34,12 @@ export interface ImBizRecordPayload {
|
||||
pagePath: string;
|
||||
rowKey: string;
|
||||
items: ImBizRecordItem[];
|
||||
// update-begin---author:GHT ---date:2026-06-10 for:【IM审批通用化】钉钉模板样式卡片扩展-----------
|
||||
/** ding=与钉钉审批模板字段对齐的卡片样式 */
|
||||
cardStyle?: 'ding' | string;
|
||||
templateId?: string;
|
||||
templateName?: string;
|
||||
// update-end---author:GHT ---date:2026-06-10 for:【IM审批通用化】钉钉模板样式卡片扩展-----------
|
||||
}
|
||||
|
||||
/** 构建带跳转链接的业务明细消息体 */
|
||||
|
||||
@@ -8,6 +8,8 @@ import { formatImMessagePreview } from './imMessageUtil';
|
||||
import { syncImUnreadFromMembers } from './useImUnread';
|
||||
import { isImChatPageActive } from './imSession';
|
||||
|
||||
export const IM_CONTACT_TYPE_WORK_NOTIFY = 'work_notify';
|
||||
|
||||
export interface ImMemberItem {
|
||||
id: string;
|
||||
username: string;
|
||||
@@ -18,6 +20,8 @@ export interface ImMemberItem {
|
||||
lastContent?: string;
|
||||
lastTime?: string;
|
||||
unreadCount?: number;
|
||||
/** user=同事 work_notify=工作通知公众号 */
|
||||
contactType?: string;
|
||||
}
|
||||
|
||||
export interface ImMessageItem {
|
||||
|
||||
@@ -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[] = [
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
|
||||
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 });
|
||||
|
||||
export const queryById = (params: { id: string }) => defHttp.get({ url: Api.queryById, params });
|
||||
|
||||
/** 按业务表 + 单据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 });
|
||||
@@ -0,0 +1,44 @@
|
||||
import { BasicColumn, FormSchema } from '/@/components/Table';
|
||||
|
||||
export const columns: BasicColumn[] = [
|
||||
{ title: '业务表', dataIndex: 'bizTable', width: 200, align: 'left', ellipsis: true },
|
||||
{ title: '单据ID', dataIndex: 'bizDataId', width: 200, align: 'left', ellipsis: true },
|
||||
{ title: '校对人', dataIndex: 'proofreadBy', width: 100 },
|
||||
{ title: '校对时间', dataIndex: 'proofreadTime', width: 165 },
|
||||
{ title: '审核人', dataIndex: 'auditBy', width: 100 },
|
||||
{ title: '审核时间', dataIndex: 'auditTime', width: 165 },
|
||||
{ title: '批准人', dataIndex: 'approveBy', width: 100 },
|
||||
{ title: '批准时间', dataIndex: 'approveTime', width: 165 },
|
||||
{ title: '更新时间', dataIndex: 'updateTime', width: 165 },
|
||||
];
|
||||
|
||||
export const searchFormSchema: FormSchema[] = [
|
||||
{ label: '业务表', field: 'bizTable', component: 'JInput', colProps: { span: 8 } },
|
||||
{ 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[] = [
|
||||
{
|
||||
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,42 @@
|
||||
<template>
|
||||
<div>
|
||||
<BasicTable @register="registerTable" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="xslmes-mesXslApprovalTrace" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { BasicTable } from '/@/components/Table';
|
||||
import { useListPage } from '/@/hooks/system/useListPage';
|
||||
import { columns, searchFormSchema } from './MesXslApprovalTrace.data';
|
||||
import { list } from './MesXslApprovalTrace.api';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const { tableContext } = useListPage({
|
||||
tableProps: {
|
||||
title: '审批痕迹',
|
||||
api: list,
|
||||
columns,
|
||||
canResize: true,
|
||||
formConfig: {
|
||||
schemas: searchFormSchema,
|
||||
autoSubmitOnEnter: true,
|
||||
showAdvancedButton: false,
|
||||
},
|
||||
actionColumn: { width: 0, ifShow: false },
|
||||
},
|
||||
});
|
||||
|
||||
const [registerTable, { getForm, reload }] = tableContext;
|
||||
|
||||
onMounted(async () => {
|
||||
const bizTable = route.query.bizTable as string;
|
||||
if (bizTable) {
|
||||
const form = await getForm();
|
||||
await form.setFieldsValue({ bizTable });
|
||||
reload();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,40 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
|
||||
const { createConfirm } = useMessage();
|
||||
|
||||
enum Api {
|
||||
list = '/xslmes/mesXslBizDocRegistry/list',
|
||||
save = '/xslmes/mesXslBizDocRegistry/add',
|
||||
edit = '/xslmes/mesXslBizDocRegistry/edit',
|
||||
deleteOne = '/xslmes/mesXslBizDocRegistry/delete',
|
||||
deleteBatch = '/xslmes/mesXslBizDocRegistry/deleteBatch',
|
||||
dbTables = '/xslmes/mesXslBizDocRegistry/dbTables',
|
||||
}
|
||||
|
||||
export const list = (params) => defHttp.get({ url: Api.list, params });
|
||||
|
||||
/** 当前库物理表(审批注册中心下拉) */
|
||||
export const listDbTables = (keyword?: string) =>
|
||||
defHttp.get<{ value: string; label: string; comment?: string }[]>({
|
||||
url: Api.dbTables,
|
||||
params: keyword ? { keyword } : {},
|
||||
});
|
||||
|
||||
export const saveOrUpdate = (params, isUpdate) =>
|
||||
isUpdate ? defHttp.put({ url: Api.edit, params }) : defHttp.post({ url: Api.save, params });
|
||||
|
||||
export const deleteOne = (params, handleSuccess) =>
|
||||
defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => handleSuccess());
|
||||
|
||||
export const batchDelete = (params, handleSuccess) => {
|
||||
createConfirm({
|
||||
iconType: 'warning',
|
||||
title: '确认删除',
|
||||
content: '是否删除选中数据',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: () =>
|
||||
defHttp.delete({ url: Api.deleteBatch, params }, { joinParamsToUrl: true }).then(() => handleSuccess()),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
import { BasicColumn, FormSchema } from '/@/components/Table';
|
||||
import { listDbTables } from './MesXslBizDocRegistry.api';
|
||||
|
||||
const STAGE_DICT = 'mes_xsl_approval_stage';
|
||||
|
||||
export const columns: BasicColumn[] = [
|
||||
{ title: '业务编码', dataIndex: 'docCode', width: 140, align: 'left' },
|
||||
{ title: '物理表名', dataIndex: 'tableName', width: 200, align: 'left' },
|
||||
{ title: '中文名称', dataIndex: 'displayName', width: 140 },
|
||||
{ title: '启用环节', dataIndex: 'enabledStages_dictText', width: 180, ellipsis: true },
|
||||
{ title: '启用', dataIndex: 'enabled_dictText', width: 70 },
|
||||
{ title: '备注', dataIndex: 'remark', ellipsis: true },
|
||||
{ title: '创建时间', dataIndex: 'createTime', width: 160 },
|
||||
];
|
||||
|
||||
export const searchFormSchema: FormSchema[] = [
|
||||
{ label: '业务编码', field: 'docCode', component: 'JInput', colProps: { span: 6 } },
|
||||
{ label: '表名/中文名', field: 'displayName', component: 'JInput', colProps: { span: 6 } },
|
||||
{
|
||||
label: '启用',
|
||||
field: 'enabled',
|
||||
component: 'JDictSelectTag',
|
||||
componentProps: { dictCode: 'yn' },
|
||||
colProps: { span: 6 },
|
||||
},
|
||||
];
|
||||
|
||||
export const formSchema: FormSchema[] = [
|
||||
{ label: '', field: 'id', component: 'Input', show: false },
|
||||
{
|
||||
label: '业务编码',
|
||||
field: 'docCode',
|
||||
component: 'Input',
|
||||
componentProps: { placeholder: '唯一标识,如 mixer_ps_compile' },
|
||||
dynamicRules: () => [{ required: true, message: '请输入业务编码!' }],
|
||||
},
|
||||
{
|
||||
label: '物理表名',
|
||||
field: 'tableName',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: listDbTables,
|
||||
showSearch: true,
|
||||
placeholder: '请选择数据库物理表,可输入关键字筛选',
|
||||
labelField: 'label',
|
||||
valueField: 'value',
|
||||
immediate: true,
|
||||
},
|
||||
dynamicRules: () => [{ required: true, message: '请选择物理表名!' }],
|
||||
},
|
||||
{
|
||||
label: '中文名称',
|
||||
field: 'displayName',
|
||||
component: 'Input',
|
||||
componentProps: { placeholder: '如 密炼PS编制' },
|
||||
},
|
||||
{
|
||||
label: '启用',
|
||||
field: 'enabled',
|
||||
component: 'Switch',
|
||||
defaultValue: 1,
|
||||
componentProps: {
|
||||
checkedValue: 1,
|
||||
unCheckedValue: 0,
|
||||
checkedChildren: '是',
|
||||
unCheckedChildren: '否',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '启用环节',
|
||||
field: 'enabledStages',
|
||||
component: 'JDictSelectTag',
|
||||
componentProps: {
|
||||
dictCode: STAGE_DICT,
|
||||
mode: 'multiple',
|
||||
placeholder: '多选:校对 / 审核 / 批准',
|
||||
},
|
||||
helpMessage: '勾选后该业务表才允许执行对应环节,并写入审批痕迹明细',
|
||||
},
|
||||
{
|
||||
label: '状态字段',
|
||||
field: 'statusField',
|
||||
component: 'Input',
|
||||
defaultValue: 'status',
|
||||
componentProps: { placeholder: '默认 status' },
|
||||
},
|
||||
{
|
||||
label: '列表接口路径',
|
||||
field: 'listApiPath',
|
||||
component: 'Input',
|
||||
slot: 'listApiPath',
|
||||
helpMessage: '从二级菜单选取后自动填入路径;配置后列表响应自动追加 traceProofreadBy / traceAuditBy / traceApproveBy 等6个字段',
|
||||
},
|
||||
{
|
||||
label: '备注',
|
||||
field: 'remark',
|
||||
component: 'InputTextArea',
|
||||
componentProps: { rows: 3 },
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div>
|
||||
<BasicTable @register="registerTable" :rowSelection="rowSelection">
|
||||
<template #tableTitle>
|
||||
<a-button type="primary" v-auth="'xslmes:mes_xsl_biz_doc_registry:add'" preIcon="ant-design:plus-outlined" @click="handleAdd">
|
||||
新增
|
||||
</a-button>
|
||||
<a-dropdown v-if="selectedRowKeys.length > 0">
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="1" @click="batchHandleDelete">
|
||||
<Icon icon="ant-design:delete-outlined" />
|
||||
删除
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<a-button v-auth="'xslmes:mes_xsl_biz_doc_registry:delete'">
|
||||
批量操作
|
||||
<Icon icon="mdi:chevron-down" />
|
||||
</a-button>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{ label: '编辑', onClick: handleEdit.bind(null, record), auth: 'xslmes:mes_xsl_biz_doc_registry:edit' },
|
||||
{
|
||||
label: '查看明细',
|
||||
onClick: handleViewDetail.bind(null, record),
|
||||
auth: 'xslmes:mes_xsl_biz_doc_registry:trace',
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
auth: 'xslmes:mes_xsl_biz_doc_registry:delete',
|
||||
popConfirm: {
|
||||
title: '确认删除该审批注册?',
|
||||
confirm: handleDelete.bind(null, record),
|
||||
placement: 'topLeft',
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</BasicTable>
|
||||
<MesXslBizDocRegistryModal @register="registerModal" @success="handleSuccess" />
|
||||
<MesXslApprovalTraceDrawer @register="registerTraceDrawer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="xslmes-mesXslBizDocRegistry" setup>
|
||||
import { BasicTable, TableAction } from '/@/components/Table';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { useDrawer } from '/@/components/Drawer';
|
||||
import { useListPage } from '/@/hooks/system/useListPage';
|
||||
import Icon from '/@/components/Icon';
|
||||
import MesXslBizDocRegistryModal from './components/MesXslBizDocRegistryModal.vue';
|
||||
import MesXslApprovalTraceDrawer from './components/MesXslApprovalTraceDrawer.vue';
|
||||
import { columns, searchFormSchema } from './MesXslBizDocRegistry.data';
|
||||
import { list, deleteOne, batchDelete } from './MesXslBizDocRegistry.api';
|
||||
|
||||
const [registerModal, { openModal }] = useModal();
|
||||
const [registerTraceDrawer, { openDrawer: openTraceDrawer }] = useDrawer();
|
||||
|
||||
const { tableContext } = useListPage({
|
||||
tableProps: {
|
||||
title: '审批注册中心',
|
||||
api: list,
|
||||
columns,
|
||||
canResize: true,
|
||||
formConfig: {
|
||||
schemas: searchFormSchema,
|
||||
autoSubmitOnEnter: true,
|
||||
showAdvancedButton: false,
|
||||
},
|
||||
actionColumn: { width: 220, fixed: 'right' },
|
||||
},
|
||||
});
|
||||
|
||||
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
|
||||
|
||||
function handleAdd() {
|
||||
openModal(true, { isUpdate: false });
|
||||
}
|
||||
|
||||
function handleEdit(record: Recordable) {
|
||||
openModal(true, { record, isUpdate: true });
|
||||
}
|
||||
|
||||
function handleViewDetail(record: Recordable) {
|
||||
openTraceDrawer(true, { record });
|
||||
}
|
||||
|
||||
async function handleDelete(record: Recordable) {
|
||||
await deleteOne({ id: record.id }, handleSuccess);
|
||||
}
|
||||
|
||||
async function batchHandleDelete() {
|
||||
await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
(selectedRowKeys.value = []) && reload();
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
|
||||
enum Api {
|
||||
list = '/xslmes/mesXslIntegrationLog/list',
|
||||
retry = '/xslmes/mesXslIntegrationLog/retry',
|
||||
}
|
||||
|
||||
export const list = (params) => defHttp.get({ url: Api.list, params });
|
||||
export const retry = (id) => defHttp.post({ url: Api.retry, params: { id } }, { joinParamsToUrl: true });
|
||||
@@ -0,0 +1,30 @@
|
||||
import { BasicColumn, FormSchema } from '/@/components/Table';
|
||||
|
||||
export const columns: BasicColumn[] = [
|
||||
{ title: '源单表', dataIndex: 'sourceBizTable', width: 180, align: 'left' },
|
||||
{ title: '源单ID', dataIndex: 'sourceBizId', width: 120 },
|
||||
{ title: '状态', dataIndex: 'status_dictText', width: 90 },
|
||||
{ title: '耗时(ms)', dataIndex: 'execTimeMs', width: 90 },
|
||||
{ title: '重试次数', dataIndex: 'retryCount', width: 80 },
|
||||
{ title: '错误信息', dataIndex: 'errorMessage', ellipsis: true },
|
||||
{ title: '幂等键', dataIndex: 'idempotentKey', width: 200, ellipsis: true },
|
||||
{ title: '创建时间', dataIndex: 'createTime', width: 160 },
|
||||
];
|
||||
|
||||
export const searchFormSchema: FormSchema[] = [
|
||||
{ label: '源单表', field: 'sourceBizTable', component: 'JInput', colProps: { span: 6 } },
|
||||
{ label: '源单ID', field: 'sourceBizId', component: 'JInput', colProps: { span: 6 } },
|
||||
{
|
||||
label: '执行状态',
|
||||
field: 'status',
|
||||
component: 'JDictSelectTag',
|
||||
componentProps: { dictCode: 'mes_xsl_integration_log_status' },
|
||||
colProps: { span: 6 },
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
field: 'createTime',
|
||||
component: 'RangePicker',
|
||||
colProps: { span: 8 },
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div>
|
||||
<BasicTable @register="registerTable">
|
||||
<template #action="{ record }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '重试',
|
||||
icon: 'ant-design:redo-outlined',
|
||||
auth: 'xslmes:mes_xsl_integration_log:retry',
|
||||
disabled: record.status === 'success',
|
||||
tooltip: record.status === 'success' ? '已成功,无需重试' : '重新执行该动作',
|
||||
popConfirm: {
|
||||
title: '确认重试该集成动作?',
|
||||
confirm: handleRetry.bind(null, record),
|
||||
placement: 'topLeft',
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<!-- 详情展开 -->
|
||||
<template #expandedRowRender="{ record }">
|
||||
<a-descriptions :column="2" size="small" bordered>
|
||||
<a-descriptions-item label="方案ID">{{ record.planId || '—' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="动作ID">{{ record.actionId || '—' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="台账ID">{{ record.recordId || '—' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="幂等键">{{ record.idempotentKey || '—' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="错误信息" :span="2">
|
||||
<span style="color: #f5222d; white-space: pre-wrap">{{ record.errorMessage || '—' }}</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="请求快照" :span="2">
|
||||
<pre style="margin: 0; font-size: 12px; max-height: 100px; overflow: auto">{{ record.requestSnapshot || '—' }}</pre>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="响应快照" :span="2">
|
||||
<pre style="margin: 0; font-size: 12px; max-height: 100px; overflow: auto">{{ record.responseSnapshot || '—' }}</pre>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="xslmes-mesXslIntegrationLog" setup>
|
||||
import { BasicTable, TableAction } from '/@/components/Table';
|
||||
import { useListPage } from '/@/hooks/system/useListPage';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { columns, searchFormSchema } from './MesXslIntegrationLog.data';
|
||||
import { list, retry } from './MesXslIntegrationLog.api';
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const { tableContext } = useListPage({
|
||||
tableProps: {
|
||||
title: '集成执行日志',
|
||||
api: list,
|
||||
columns,
|
||||
canResize: true,
|
||||
expandRowByClick: true,
|
||||
formConfig: {
|
||||
schemas: searchFormSchema,
|
||||
autoSubmitOnEnter: true,
|
||||
showAdvancedButton: true,
|
||||
labelWidth: 80,
|
||||
},
|
||||
actionColumn: { width: 90, fixed: 'right' },
|
||||
},
|
||||
});
|
||||
|
||||
const [registerTable, { reload }] = tableContext;
|
||||
|
||||
async function handleRetry(record: Recordable) {
|
||||
try {
|
||||
await retry(record.id);
|
||||
createMessage.success('重试任务已提交');
|
||||
reload();
|
||||
} catch (e: any) {
|
||||
createMessage.error(e?.message || '重试失败');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.ant-picker-range) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
|
||||
const { createConfirm } = useMessage();
|
||||
|
||||
enum Api {
|
||||
list = '/xslmes/mesXslIntegrationPlan/list',
|
||||
save = '/xslmes/mesXslIntegrationPlan/add',
|
||||
edit = '/xslmes/mesXslIntegrationPlan/edit',
|
||||
deleteOne = '/xslmes/mesXslIntegrationPlan/delete',
|
||||
publish = '/xslmes/mesXslIntegrationPlan/publish',
|
||||
disable = '/xslmes/mesXslIntegrationPlan/disable',
|
||||
tableColumns = '/xslmes/mesXslIntegrationPlan/tableColumns',
|
||||
actionList = '/xslmes/mesXslIntegrationPlan/action/listByPlanId',
|
||||
actionAdd = '/xslmes/mesXslIntegrationPlan/action/add',
|
||||
actionEdit = '/xslmes/mesXslIntegrationPlan/action/edit',
|
||||
actionDelete = '/xslmes/mesXslIntegrationPlan/action/delete',
|
||||
bizDocList = '/xslmes/mesXslBizDocRegistry/list',
|
||||
registryByTable = '/xslmes/mesXslIntegrationPlan/registryByTable',
|
||||
previewDefaultFromFlow = '/xslmes/mesXslIntegrationPlan/previewDefaultFromFlow',
|
||||
generateDefaultFromFlow = '/xslmes/mesXslIntegrationPlan/generateDefaultFromFlow',
|
||||
generateForNode = '/xslmes/mesXslIntegrationPlan/generateForNode',
|
||||
queryById = '/xslmes/mesXslIntegrationPlan/queryById',
|
||||
}
|
||||
|
||||
export const list = (params) => defHttp.get({ url: Api.list, params });
|
||||
|
||||
export const saveOrUpdate = (params, isUpdate) =>
|
||||
isUpdate ? defHttp.put({ url: Api.edit, params }) : defHttp.post({ url: Api.save, params });
|
||||
|
||||
export const deleteOne = (params, handleSuccess) =>
|
||||
defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => handleSuccess());
|
||||
|
||||
export const publishPlan = (id) => defHttp.post({ url: Api.publish, params: { id } }, { joinParamsToUrl: true });
|
||||
export const disablePlan = (id) => defHttp.post({ url: Api.disable, params: { id } }, { joinParamsToUrl: true });
|
||||
|
||||
// 可视化向导专用
|
||||
export const savePlan = (params) => defHttp.post<any>({ url: Api.save, params });
|
||||
export const getTableColumns = (tableName: string) => defHttp.get<any[]>({ url: Api.tableColumns, params: { tableName } });
|
||||
export const listBizDocRegistry = () => defHttp.get<any>({ url: Api.bizDocList, params: { pageNo: 1, pageSize: 200 } });
|
||||
export const getRegistryByTable = (tableName: string) => defHttp.get<any>({ url: Api.registryByTable, params: { tableName } });
|
||||
export const getDictItems = (dictCode: string) => defHttp.get<any[]>({ url: `/sys/dict/getDictItems/${dictCode}` });
|
||||
|
||||
export const previewDefaultFromFlow = (params: { sourceTable: string; flowId?: string }) =>
|
||||
defHttp.get<any>({ url: Api.previewDefaultFromFlow, params });
|
||||
|
||||
export const generateDefaultFromFlow = (params: {
|
||||
sourceTable: string;
|
||||
flowId?: string;
|
||||
overwriteDraft?: boolean;
|
||||
nodeBindings?: Array<{ nodeId: string; stage?: string | null }>;
|
||||
}) => defHttp.post<any>({ url: Api.generateDefaultFromFlow, params });
|
||||
|
||||
/** 流程设计器:为单个节点生成集成方案 */
|
||||
export const generateForNode = (params: {
|
||||
sourceTable: string;
|
||||
flowId: string;
|
||||
nodeId: string;
|
||||
stageKey: string;
|
||||
flowConfig?: string;
|
||||
overwriteDraft?: boolean;
|
||||
}) => defHttp.post<any>({ url: Api.generateForNode, params });
|
||||
|
||||
export const queryPlanById = (id: string) => defHttp.get<any>({ url: Api.queryById, params: { id } });
|
||||
|
||||
// 动作管理
|
||||
export const listActions = (planId) => defHttp.get({ url: Api.actionList, params: { planId } });
|
||||
export const saveAction = (params) => defHttp.post({ url: Api.actionAdd, params });
|
||||
export const editAction = (params) => defHttp.put({ url: Api.actionEdit, params });
|
||||
export const deleteAction = (params, handleSuccess) =>
|
||||
defHttp.delete({ url: Api.actionDelete, params }, { joinParamsToUrl: true }).then(() => handleSuccess());
|
||||
@@ -0,0 +1,156 @@
|
||||
import { BasicColumn, FormSchema } from '/@/components/Table';
|
||||
|
||||
export const columns: BasicColumn[] = [
|
||||
{ title: '方案编码', dataIndex: 'planCode', width: 150, align: 'left' },
|
||||
{ title: '方案名称', dataIndex: 'planName', width: 180, align: 'left' },
|
||||
{ title: '源单表', dataIndex: 'sourceTable', width: 200, align: 'left' },
|
||||
{ title: '触发时机', dataIndex: 'triggerPhase_dictText', width: 110 },
|
||||
{ title: '绑定环节', dataIndex: 'triggerStage_dictText', width: 90 },
|
||||
{ title: '执行模式', dataIndex: 'execMode_dictText', width: 100 },
|
||||
{ title: '状态', dataIndex: 'status_dictText', width: 90 },
|
||||
{ title: '创建时间', dataIndex: 'createTime', width: 160 },
|
||||
];
|
||||
|
||||
export const searchFormSchema: FormSchema[] = [
|
||||
{ label: '方案编码', field: 'planCode', component: 'JInput', colProps: { span: 6 } },
|
||||
{ label: '方案名称', field: 'planName', component: 'JInput', colProps: { span: 6 } },
|
||||
{
|
||||
label: '状态',
|
||||
field: 'status',
|
||||
component: 'JDictSelectTag',
|
||||
componentProps: { dictCode: 'mes_xsl_integration_plan_status' },
|
||||
colProps: { span: 6 },
|
||||
},
|
||||
{
|
||||
label: '触发时机',
|
||||
field: 'triggerPhase',
|
||||
component: 'JDictSelectTag',
|
||||
componentProps: { dictCode: 'mes_xsl_integration_trigger_phase' },
|
||||
colProps: { span: 6 },
|
||||
},
|
||||
];
|
||||
|
||||
export const formSchema: FormSchema[] = [
|
||||
{ label: '', field: 'id', component: 'Input', show: false },
|
||||
{
|
||||
label: '方案编码',
|
||||
field: 'planCode',
|
||||
component: 'Input',
|
||||
componentProps: { placeholder: '唯一编码,如 formula_approve_sync' },
|
||||
dynamicRules: () => [{ required: true, message: '请输入方案编码!' }],
|
||||
},
|
||||
{
|
||||
label: '方案名称',
|
||||
field: 'planName',
|
||||
component: 'Input',
|
||||
componentProps: { placeholder: '如 配合示方审批通过同步ERP' },
|
||||
dynamicRules: () => [{ required: true, message: '请输入方案名称!' }],
|
||||
},
|
||||
{
|
||||
label: '源单表名',
|
||||
field: 'sourceTable',
|
||||
component: 'Input',
|
||||
componentProps: { placeholder: '触发的业务表,如 mes_xsl_formula_spec' },
|
||||
dynamicRules: () => [{ required: true, message: '请输入源单表名!' }],
|
||||
},
|
||||
{
|
||||
label: '触发时机',
|
||||
field: 'triggerPhase',
|
||||
component: 'JDictSelectTag',
|
||||
componentProps: { dictCode: 'mes_xsl_integration_trigger_phase', type: 'select' },
|
||||
dynamicRules: () => [{ required: true, message: '请选择触发时机!' }],
|
||||
},
|
||||
{
|
||||
label: '绑定环节',
|
||||
field: 'triggerStage',
|
||||
component: 'JDictSelectTag',
|
||||
componentProps: { dictCode: 'mes_xsl_approval_stage', type: 'select', placeholder: '节点通过时必选' },
|
||||
helpMessage: '须为审批注册中心已启用的环节;节点通过时必选,全流程通过默认批准',
|
||||
},
|
||||
{
|
||||
label: '执行模式',
|
||||
field: 'execMode',
|
||||
component: 'JDictSelectTag',
|
||||
componentProps: { dictCode: 'mes_xsl_integration_exec_mode', type: 'select' },
|
||||
defaultValue: 'async',
|
||||
},
|
||||
{
|
||||
label: '匹配条件',
|
||||
field: 'matchCondition',
|
||||
component: 'InputTextArea',
|
||||
componentProps: { rows: 2, placeholder: '可选,留空表示无条件匹配。如:status = \'approved\'' },
|
||||
},
|
||||
{
|
||||
label: '备注',
|
||||
field: 'remark',
|
||||
component: 'InputTextArea',
|
||||
componentProps: { rows: 2 },
|
||||
},
|
||||
];
|
||||
|
||||
// 动作表格列
|
||||
export const actionColumns: BasicColumn[] = [
|
||||
{ title: '动作名称', dataIndex: 'actionName', width: 140, align: 'left' },
|
||||
{ title: '动作类型', dataIndex: 'actionType_dictText', width: 110 },
|
||||
{ title: '失败策略', dataIndex: 'onFail_dictText', width: 90 },
|
||||
{ title: '执行顺序', dataIndex: 'execOrder', width: 80 },
|
||||
{ title: '启用', dataIndex: 'enabled_dictText', width: 70 },
|
||||
{ title: 'SQL 模板', dataIndex: 'sqlTemplate', ellipsis: true },
|
||||
];
|
||||
|
||||
// 动作表单
|
||||
export const actionFormSchema: FormSchema[] = [
|
||||
{ label: '', field: 'id', component: 'Input', show: false },
|
||||
{ label: '', field: 'planId', component: 'Input', show: false },
|
||||
{
|
||||
label: '动作名称',
|
||||
field: 'actionName',
|
||||
component: 'Input',
|
||||
componentProps: { placeholder: '如 更新ERP状态' },
|
||||
dynamicRules: () => [{ required: true, message: '请输入动作名称!' }],
|
||||
},
|
||||
{
|
||||
label: '动作类型',
|
||||
field: 'actionType',
|
||||
component: 'JDictSelectTag',
|
||||
componentProps: { dictCode: 'mes_xsl_integration_action_type', type: 'select' },
|
||||
defaultValue: 'SQL_UPDATE',
|
||||
dynamicRules: () => [{ required: true, message: '请选择动作类型!' }],
|
||||
},
|
||||
{
|
||||
label: 'SQL 模板',
|
||||
field: 'sqlTemplate',
|
||||
component: 'InputTextArea',
|
||||
componentProps: {
|
||||
rows: 5,
|
||||
placeholder: 'UPDATE mes_xsl_xxx SET status=\'approved\' WHERE id=#{source.id}',
|
||||
},
|
||||
ifShow: ({ values }) => values.actionType === 'SQL_UPDATE',
|
||||
},
|
||||
{
|
||||
label: '执行顺序',
|
||||
field: 'execOrder',
|
||||
component: 'InputNumber',
|
||||
componentProps: { min: 0, max: 999 },
|
||||
defaultValue: 0,
|
||||
},
|
||||
{
|
||||
label: '失败策略',
|
||||
field: 'onFail',
|
||||
component: 'JDictSelectTag',
|
||||
componentProps: { dictCode: 'mes_xsl_integration_on_fail', type: 'select' },
|
||||
defaultValue: 'stop',
|
||||
},
|
||||
{
|
||||
label: '幂等键',
|
||||
field: 'idempotentKey',
|
||||
component: 'Input',
|
||||
componentProps: { placeholder: '留空默认使用 台账ID_动作ID' },
|
||||
},
|
||||
{
|
||||
label: '启用',
|
||||
field: 'enabled',
|
||||
component: 'Switch',
|
||||
defaultValue: true,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<div>
|
||||
<BasicTable @register="registerTable">
|
||||
<template #tableTitle>
|
||||
<a-button type="primary" v-auth="'xslmes:mes_xsl_integration_plan:add'" preIcon="ant-design:plus-outlined" @click="handleAdd">
|
||||
新增方案
|
||||
</a-button>
|
||||
<a-button
|
||||
v-auth="'xslmes:mes_xsl_integration_plan:edit'"
|
||||
preIcon="ant-design:thunderbolt-outlined"
|
||||
style="margin-left: 8px"
|
||||
@click="handleGenerateDefault"
|
||||
>
|
||||
按流程生成默认方案
|
||||
</a-button>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<TableAction :actions="getTableActions(record)" :dropDownActions="getDropDownActions(record)" />
|
||||
</template>
|
||||
</BasicTable>
|
||||
<MesXslIntegrationPlanModal @register="registerModal" @success="handleSuccess" />
|
||||
<MesXslIntegrationActionDrawer ref="actionDrawerRef" />
|
||||
<MesXslIntegrationPlanWizard ref="wizardRef" @success="handleSuccess" />
|
||||
<GenerateDefaultPlanModal ref="generateModalRef" @success="handleSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="xslmes-mesXslIntegrationPlan" setup>
|
||||
import { ref } from 'vue';
|
||||
import { BasicTable, TableAction } from '/@/components/Table';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { useListPage } from '/@/hooks/system/useListPage';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import MesXslIntegrationPlanModal from './components/MesXslIntegrationPlanModal.vue';
|
||||
import MesXslIntegrationActionDrawer from './components/MesXslIntegrationActionDrawer.vue';
|
||||
import MesXslIntegrationPlanWizard from './MesXslIntegrationPlanWizard.vue';
|
||||
import GenerateDefaultPlanModal from './components/GenerateDefaultPlanModal.vue';
|
||||
import { columns, searchFormSchema } from './MesXslIntegrationPlan.data';
|
||||
import { list, deleteOne, publishPlan, disablePlan } from './MesXslIntegrationPlan.api';
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const [registerModal, { openModal }] = useModal();
|
||||
const actionDrawerRef = ref();
|
||||
const wizardRef = ref();
|
||||
const generateModalRef = ref();
|
||||
|
||||
const { tableContext } = useListPage({
|
||||
tableProps: {
|
||||
title: '集成方案管理',
|
||||
api: list,
|
||||
columns: [
|
||||
...columns,
|
||||
// 状态列用 tag 渲染覆盖默认
|
||||
],
|
||||
canResize: true,
|
||||
formConfig: {
|
||||
schemas: searchFormSchema,
|
||||
autoSubmitOnEnter: true,
|
||||
showAdvancedButton: false,
|
||||
},
|
||||
actionColumn: { width: 200, fixed: 'right' },
|
||||
customRow: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const [registerTable, { reload }] = tableContext;
|
||||
|
||||
function handleAdd() {
|
||||
wizardRef.value?.open();
|
||||
}
|
||||
|
||||
function handleGenerateDefault() {
|
||||
generateModalRef.value?.open();
|
||||
}
|
||||
|
||||
function handleEdit(record: Recordable) {
|
||||
openModal(true, { record, isUpdate: true });
|
||||
}
|
||||
|
||||
function handleManageActions(record: Recordable) {
|
||||
actionDrawerRef.value?.open(record);
|
||||
}
|
||||
|
||||
async function handlePublish(record: Recordable) {
|
||||
try {
|
||||
await publishPlan(record.id);
|
||||
createMessage.success('发布成功');
|
||||
reload();
|
||||
} catch (e: any) {
|
||||
createMessage.error(e?.message || '发布失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDisable(record: Recordable) {
|
||||
try {
|
||||
await disablePlan(record.id);
|
||||
createMessage.success('已停用');
|
||||
reload();
|
||||
} catch (e: any) {
|
||||
createMessage.error(e?.message || '停用失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(record: Recordable) {
|
||||
await deleteOne({ id: record.id }, handleSuccess);
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
reload();
|
||||
}
|
||||
|
||||
function getTableActions(record: Recordable) {
|
||||
const actions: any[] = [
|
||||
{ label: '编辑', onClick: handleEdit.bind(null, record), auth: 'xslmes:mes_xsl_integration_plan:edit', disabled: record.status === '1' },
|
||||
{ label: '管理动作', icon: 'ant-design:setting-outlined', onClick: handleManageActions.bind(null, record) },
|
||||
];
|
||||
if (record.status === '0' || record.status === '2') {
|
||||
actions.push({
|
||||
label: '发布',
|
||||
color: 'success',
|
||||
auth: 'xslmes:mes_xsl_integration_plan:publish',
|
||||
onClick: handlePublish.bind(null, record),
|
||||
});
|
||||
}
|
||||
if (record.status === '1') {
|
||||
actions.push({
|
||||
label: '停用',
|
||||
color: 'error',
|
||||
auth: 'xslmes:mes_xsl_integration_plan:publish',
|
||||
onClick: handleDisable.bind(null, record),
|
||||
});
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
function getDropDownActions(record: Recordable) {
|
||||
return [
|
||||
{
|
||||
label: '删除',
|
||||
auth: 'xslmes:mes_xsl_integration_plan:delete',
|
||||
disabled: record.status === '1',
|
||||
popConfirm: {
|
||||
title: '确认删除该方案(同时删除所有动作)?',
|
||||
confirm: handleDelete.bind(null, record),
|
||||
placement: 'topLeft',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,466 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
v-model:open="visible"
|
||||
title="新建集成方案"
|
||||
width="1120"
|
||||
:body-style="{ padding: '16px 20px', display: 'flex', flexDirection: 'column', height: 'calc(100vh - 55px)' }"
|
||||
@close="visible = false"
|
||||
>
|
||||
<!-- 基本信息 -->
|
||||
<a-card size="small" title="基本信息" style="margin-bottom: 14px; flex-shrink: 0">
|
||||
<a-form ref="planFormRef" :model="planForm" :label-col="{ span: 5 }" :wrapper-col="{ span: 18 }">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="方案名称" name="planName" :rules="[{ required: true, message: '请输入方案名称' }]">
|
||||
<a-input v-model:value="planForm.planName" placeholder="如 密炼PS审批通过→同步配合示方" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="方案编码" name="planCode" :rules="[{ required: true, message: '请输入方案编码' }]">
|
||||
<a-input v-model:value="planForm.planCode" placeholder="如 mixer_ps_on_approve(英文下划线)" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="触发时机" name="triggerPhase" :rules="[{ required: true }]">
|
||||
<a-radio-group v-model:value="planForm.triggerPhase" @change="onTriggerPhaseChange">
|
||||
<a-radio value="onApprove">审批通过</a-radio>
|
||||
<a-radio value="onReject">审批驳回</a-radio>
|
||||
<a-radio value="onNodeApprove">节点通过</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="执行模式">
|
||||
<a-radio-group v-model:value="planForm.execMode">
|
||||
<a-radio value="async">异步(推荐)</a-radio>
|
||||
<a-radio value="sync">同步</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16" v-if="planForm.sourceTable">
|
||||
<a-col :span="24">
|
||||
<a-form-item
|
||||
label="绑定环节"
|
||||
name="triggerStage"
|
||||
:rules="stageRules"
|
||||
:help="stageHelpText"
|
||||
>
|
||||
<a-radio-group v-model:value="planForm.triggerStage" :disabled="!enabledStageOptions.length">
|
||||
<a-radio v-if="planForm.triggerPhase === 'onReject'" :value="''">任意环节</a-radio>
|
||||
<a-radio v-for="opt in enabledStageOptions" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
<div v-if="!enabledStageOptions.length" style="color: #fa8c16; font-size: 12px; margin-top: 4px">
|
||||
该单据在审批注册中心未配置启用环节,请先到「审批注册中心」勾选环节
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<!-- 主体:左侧选表 + 右侧动作 -->
|
||||
<a-row :gutter="14" style="flex: 1; min-height: 0">
|
||||
<!-- 左侧:触发业务表 -->
|
||||
<a-col :span="7" style="height: 100%">
|
||||
<a-card
|
||||
size="small"
|
||||
title="触发业务表"
|
||||
:body-style="{ padding: '10px', overflowY: 'auto', maxHeight: 'calc(100% - 46px)' }"
|
||||
style="height: 100%"
|
||||
:loading="loadingBizDocs"
|
||||
>
|
||||
<div style="color: #888; font-size: 12px; margin-bottom: 10px; line-height: 1.5">
|
||||
选择当哪个业务表的记录完成审批时,触发右侧的动作列表
|
||||
</div>
|
||||
<div
|
||||
v-for="doc in bizDocList"
|
||||
:key="doc.tableName"
|
||||
:style="{
|
||||
padding: '10px 12px',
|
||||
border: '2px solid',
|
||||
borderColor: planForm.sourceTable === doc.tableName ? '#1677ff' : '#e8e8e8',
|
||||
borderRadius: '6px',
|
||||
marginBottom: '8px',
|
||||
cursor: 'pointer',
|
||||
background: planForm.sourceTable === doc.tableName ? '#e6f4ff' : 'white',
|
||||
transition: 'all 0.2s',
|
||||
}"
|
||||
@click="selectSourceTable(doc)"
|
||||
>
|
||||
<div style="font-weight: 500; color: rgba(0, 0, 0, 0.85); margin-bottom: 2px">
|
||||
{{ getDocLabel(doc) || doc.tableName }}
|
||||
</div>
|
||||
<div style="font-size: 11px; color: #999; font-family: monospace">{{ doc.tableName }}</div>
|
||||
<div v-if="planForm.sourceTable === doc.tableName && sourceColumns.length" style="font-size: 11px; color: #1677ff; margin-top: 3px">
|
||||
{{ sourceColumns.length }} 个字段 ✓
|
||||
</div>
|
||||
<div
|
||||
v-if="doc.enabledStages_dictText || doc.enabledStages"
|
||||
style="font-size: 11px; color: #52c41a; margin-top: 3px"
|
||||
>
|
||||
环节:{{ doc.enabledStages_dictText || formatStages(doc.enabledStages) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!bizDocList.length && !loadingBizDocs" style="text-align: center; padding: 20px 0; color: #bbb">
|
||||
<div style="font-size: 24px">📭</div>
|
||||
<div style="font-size: 12px; margin-top: 6px">暂无注册的业务表<br />请先在「审批注册中心」中添加</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<!-- 右侧:集成动作列表 -->
|
||||
<a-col :span="17" style="height: 100%">
|
||||
<a-card
|
||||
size="small"
|
||||
:title="`集成动作(触发表:${planForm.sourceTable || '—'})`"
|
||||
:body-style="{ padding: '12px', overflowY: 'auto', maxHeight: 'calc(100% - 46px)' }"
|
||||
style="height: 100%"
|
||||
>
|
||||
<template #extra>
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="!planForm.sourceTable"
|
||||
:title="planForm.sourceTable ? '添加动作' : '请先选择左侧触发业务表'"
|
||||
@click="openActionEditor()"
|
||||
>
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加动作
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="!actions.length" style="text-align: center; padding: 50px 0; color: #bbb">
|
||||
<div style="font-size: 40px; margin-bottom: 10px">📋</div>
|
||||
<div style="font-size: 14px">{{ planForm.sourceTable ? '点击右上角「添加动作」开始配置' : '请先在左侧选择触发业务表' }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 动作卡片列表 -->
|
||||
<div v-for="(action, idx) in actions" :key="idx" style="border: 1px solid #e8e8e8; border-radius: 8px; padding: 12px 14px; margin-bottom: 10px; background: white">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start">
|
||||
<div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap">
|
||||
<a-tag color="blue" style="font-size: 13px; font-weight: 600">{{ idx + 1 }}</a-tag>
|
||||
<span style="font-size: 13px; font-weight: 600">{{ getActionIcon(action) }} {{ action.actionName }}</span>
|
||||
<a-tag :color="action.onFail === 'stop' ? 'orange' : 'default'" size="small">
|
||||
{{ action.onFail === 'stop' ? '失败终止' : '失败继续' }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-button size="small" @click="openActionEditor(action, idx)">编辑</a-button>
|
||||
<a-popconfirm title="确认删除该动作?" @confirm="actions.splice(idx, 1)">
|
||||
<a-button size="small" danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 可视化配置摘要 -->
|
||||
<div v-if="action.actionConfig" style="margin-top: 8px; font-size: 12px">
|
||||
{{ getVisualSummary(action) }}
|
||||
</div>
|
||||
|
||||
<!-- SQL 预览 -->
|
||||
<div v-if="action.sqlTemplate" style="margin-top: 8px">
|
||||
<pre style="font-size: 11px; color: #666; margin: 0; white-space: pre-wrap; background: #f5f7fa; padding: 6px 8px; border-radius: 4px; max-height: 56px; overflow: hidden; font-family: monospace; line-height: 1.5">{{ action.sqlTemplate }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<div
|
||||
style="flex-shrink: 0; margin-top: 14px; padding: 12px 0; border-top: 1px solid #e8e8e8; display: flex; justify-content: flex-end; gap: 8px"
|
||||
>
|
||||
<a-button @click="visible = false">取消</a-button>
|
||||
<a-button :loading="saving" @click="handleSave(false)">保存为草稿</a-button>
|
||||
<a-button type="primary" :loading="saving" @click="handleSave(true)">保存并发布</a-button>
|
||||
</div>
|
||||
|
||||
<VisualActionEditor ref="actionEditorRef" @success="handleActionSaved" />
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import {
|
||||
savePlan,
|
||||
publishPlan,
|
||||
getTableColumns,
|
||||
listBizDocRegistry,
|
||||
getRegistryByTable,
|
||||
getDictItems,
|
||||
saveAction,
|
||||
} from './MesXslIntegrationPlan.api';
|
||||
import VisualActionEditor from './components/VisualActionEditor.vue';
|
||||
|
||||
const STAGE_DICT = 'mes_xsl_approval_stage';
|
||||
const STAGE_LABELS: Record<string, string> = { proofread: '校对', audit: '审核', approve: '批准' };
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const visible = ref(false);
|
||||
const saving = ref(false);
|
||||
const loadingBizDocs = ref(false);
|
||||
const planFormRef = ref();
|
||||
const actionEditorRef = ref();
|
||||
|
||||
const bizDocList = ref<any[]>([]);
|
||||
const sourceColumns = ref<any[]>([]);
|
||||
const actions = ref<any[]>([]);
|
||||
const editingIdx = ref(-1);
|
||||
|
||||
const planForm = ref({
|
||||
planCode: '',
|
||||
planName: '',
|
||||
sourceTable: '',
|
||||
registryId: '',
|
||||
triggerPhase: 'onApprove',
|
||||
triggerStage: 'approve',
|
||||
execMode: 'async',
|
||||
remark: '',
|
||||
});
|
||||
|
||||
const stageDictItems = ref<{ label: string; value: string }[]>([]);
|
||||
const selectedRegistry = ref<any>(null);
|
||||
|
||||
const enabledStageOptions = computed(() => {
|
||||
const raw = selectedRegistry.value?.enabledStages;
|
||||
if (!raw) return [];
|
||||
const enabled = String(raw)
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
const dictMap = new Map(stageDictItems.value.map((d) => [d.value, d.label]));
|
||||
return enabled.map((v) => ({ value: v, label: dictMap.get(v) || STAGE_LABELS[v] || v }));
|
||||
});
|
||||
|
||||
const stageRules = computed(() => {
|
||||
if (planForm.value.triggerPhase === 'onNodeApprove') {
|
||||
return [{ required: true, message: '节点通过时必须选择绑定环节' }];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const stageHelpText = computed(() => {
|
||||
if (!planForm.value.sourceTable) return '请先选择左侧触发业务表';
|
||||
if (planForm.value.triggerPhase === 'onApprove') return '全流程最终通过时触发,默认绑定「批准」环节';
|
||||
if (planForm.value.triggerPhase === 'onReject') return '驳回时触发;选「任意环节」表示任一节点驳回均执行';
|
||||
return '仅当该审批环节通过时触发集成动作';
|
||||
});
|
||||
|
||||
async function loadBizDocs() {
|
||||
loadingBizDocs.value = true;
|
||||
try {
|
||||
const [res, dictRes] = await Promise.all([listBizDocRegistry(), getDictItems(STAGE_DICT)]);
|
||||
bizDocList.value = (res as any)?.records || (Array.isArray(res) ? res : []);
|
||||
stageDictItems.value = (dictRes || []).map((d: any) => ({
|
||||
label: d.text || d.label || d.itemText,
|
||||
value: d.value || d.itemValue,
|
||||
}));
|
||||
} catch {
|
||||
bizDocList.value = [];
|
||||
stageDictItems.value = Object.entries(STAGE_LABELS).map(([value, label]) => ({ value, label }));
|
||||
} finally {
|
||||
loadingBizDocs.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function selectSourceTable(doc: any) {
|
||||
planForm.value.sourceTable = doc.tableName;
|
||||
planForm.value.registryId = doc.id || '';
|
||||
sourceColumns.value = [];
|
||||
selectedRegistry.value = doc;
|
||||
try {
|
||||
const [cols, registry] = await Promise.all([
|
||||
getTableColumns(doc.tableName),
|
||||
getRegistryByTable(doc.tableName).catch(() => doc),
|
||||
]);
|
||||
sourceColumns.value = (cols as any) || [];
|
||||
if (registry) {
|
||||
selectedRegistry.value = registry;
|
||||
planForm.value.registryId = registry.id || doc.id || '';
|
||||
}
|
||||
} catch {
|
||||
sourceColumns.value = [];
|
||||
}
|
||||
syncDefaultTriggerStage();
|
||||
}
|
||||
|
||||
function formatStages(stages: string) {
|
||||
return String(stages)
|
||||
.split(',')
|
||||
.map((s) => STAGE_LABELS[s.trim()] || s.trim())
|
||||
.join(' / ');
|
||||
}
|
||||
|
||||
function onTriggerPhaseChange() {
|
||||
syncDefaultTriggerStage();
|
||||
}
|
||||
|
||||
function syncDefaultTriggerStage() {
|
||||
const opts = enabledStageOptions.value;
|
||||
if (!opts.length) {
|
||||
planForm.value.triggerStage = '';
|
||||
return;
|
||||
}
|
||||
if (planForm.value.triggerPhase === 'onApprove') {
|
||||
planForm.value.triggerStage = opts.some((o) => o.value === 'approve') ? 'approve' : opts[opts.length - 1].value;
|
||||
} else if (planForm.value.triggerPhase === 'onReject') {
|
||||
if (!opts.some((o) => o.value === planForm.value.triggerStage)) {
|
||||
planForm.value.triggerStage = '';
|
||||
}
|
||||
} else if (planForm.value.triggerPhase === 'onNodeApprove') {
|
||||
if (!opts.some((o) => o.value === planForm.value.triggerStage)) {
|
||||
planForm.value.triggerStage = opts[0].value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getDocLabel(doc: any): string {
|
||||
return doc?.displayName || doc?.docName || doc?.bizName || '';
|
||||
}
|
||||
|
||||
function getActionIcon(action: any): string {
|
||||
if (action.actionType === 'REGISTRY_STAGE_SYNC') return '✅';
|
||||
if (action.actionType === 'REGISTRY_STAGE_REVERT') return '↩️';
|
||||
if (!action.actionConfig) return '🔧';
|
||||
try {
|
||||
const cfg = JSON.parse(action.actionConfig);
|
||||
if (cfg.visualType === 'REGISTRY_STAGE_SYNC') return '✅';
|
||||
if (cfg.visualType === 'REGISTRY_STAGE_REVERT') return '↩️';
|
||||
return cfg.visualType === 'STATUS_MODIFY' ? '📋' : '🔄';
|
||||
} catch {
|
||||
return '🔧';
|
||||
}
|
||||
}
|
||||
|
||||
function getVisualSummary(action: any): string {
|
||||
if (action.actionType === 'REGISTRY_STAGE_SYNC') {
|
||||
return '审批注册中心环节同步(自动写状态/操作人/痕迹)';
|
||||
}
|
||||
if (action.actionType === 'REGISTRY_STAGE_REVERT') {
|
||||
return '审批注册中心环节回退(驳回回编制态)';
|
||||
}
|
||||
if (!action.actionConfig) return '';
|
||||
try {
|
||||
const cfg = JSON.parse(action.actionConfig);
|
||||
if (cfg.visualType === 'REGISTRY_STAGE_SYNC') {
|
||||
const stage = cfg.registryStage?.stage || cfg.stage || '?';
|
||||
return `环节同步 → ${stage},前置=${cfg.registryStage?.expectedFrom || cfg.expectedFrom || '自动'}`;
|
||||
}
|
||||
if (cfg.visualType === 'REGISTRY_STAGE_REVERT') {
|
||||
return `环节回退 → ${cfg.registryStage?.targetStage || cfg.targetStage || 'compile'}`;
|
||||
}
|
||||
const lbl = cfg.targetTableLabel || cfg.targetTable || '目标表';
|
||||
if (cfg.visualType === 'STATUS_MODIFY') {
|
||||
return `修改【${lbl}】.${cfg.statusConfig?.targetField || '?'} → '${cfg.statusConfig?.newValue || '?'}',关联:${cfg.targetTable}.${cfg.linkCondition?.targetField} = ${planForm.value.sourceTable}.${cfg.linkCondition?.sourceField}`;
|
||||
}
|
||||
if (cfg.visualType === 'DATA_SYNC') {
|
||||
return `数据带入【${lbl}】,${cfg.fieldMappings?.length || 0} 个字段映射,关联:${cfg.targetTable}.${cfg.linkCondition?.targetField} = ${planForm.value.sourceTable}.${cfg.linkCondition?.sourceField}`;
|
||||
}
|
||||
} catch {/**/ }
|
||||
return '';
|
||||
}
|
||||
|
||||
function openActionEditor(action?: any, idx?: number) {
|
||||
editingIdx.value = idx ?? -1;
|
||||
actionEditorRef.value?.open({
|
||||
sourceTable: planForm.value.sourceTable,
|
||||
sourceColumns: sourceColumns.value,
|
||||
bizDocList: bizDocList.value,
|
||||
sourceRegistry: selectedRegistry.value,
|
||||
action,
|
||||
execOrder: idx === undefined ? actions.value.length + 1 : action?.execOrder,
|
||||
});
|
||||
}
|
||||
|
||||
function handleActionSaved(actionData: any) {
|
||||
if (editingIdx.value >= 0) {
|
||||
actions.value[editingIdx.value] = actionData;
|
||||
} else {
|
||||
actions.value.push(actionData);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave(publish: boolean) {
|
||||
try {
|
||||
await planFormRef.value?.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (!planForm.value.sourceTable) {
|
||||
createMessage.warning('请在左侧选择触发业务表');
|
||||
return;
|
||||
}
|
||||
if (!enabledStageOptions.value.length) {
|
||||
createMessage.warning('该单据未在审批注册中心配置启用环节');
|
||||
return;
|
||||
}
|
||||
if (planForm.value.triggerPhase === 'onNodeApprove' && !planForm.value.triggerStage) {
|
||||
createMessage.warning('节点通过时必须选择绑定环节');
|
||||
return;
|
||||
}
|
||||
if (!actions.value.length) {
|
||||
createMessage.warning('请至少添加一个集成动作');
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
// 1. 保存方案,返回含 id 的实体
|
||||
const savedPlan = await savePlan({ ...planForm.value });
|
||||
const planId = (savedPlan as any)?.id;
|
||||
if (!planId) {
|
||||
createMessage.error('保存方案失败,无法获取方案ID,请重试');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 逐个保存动作(enabled 须转 0/1,后端字段为 Integer)
|
||||
for (let i = 0; i < actions.value.length; i++) {
|
||||
const a = actions.value[i];
|
||||
await saveAction({ ...a, planId, execOrder: i + 1, enabled: a.enabled ? 1 : 0 });
|
||||
}
|
||||
|
||||
// 3. 发布(可选)
|
||||
if (publish) {
|
||||
await publishPlan(planId);
|
||||
}
|
||||
|
||||
createMessage.success(publish ? '方案已保存并发布' : '方案已保存为草稿');
|
||||
emit('success');
|
||||
visible.value = false;
|
||||
} catch (e: any) {
|
||||
createMessage.error(e?.message || '保存失败,请检查配置后重试');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function open() {
|
||||
planForm.value = {
|
||||
planCode: '',
|
||||
planName: '',
|
||||
sourceTable: '',
|
||||
registryId: '',
|
||||
triggerPhase: 'onApprove',
|
||||
triggerStage: 'approve',
|
||||
execMode: 'async',
|
||||
remark: '',
|
||||
};
|
||||
actions.value = [];
|
||||
sourceColumns.value = [];
|
||||
selectedRegistry.value = null;
|
||||
visible.value = true;
|
||||
loadBizDocs();
|
||||
}
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,344 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
title="按审批流程生成默认方案"
|
||||
width="900px"
|
||||
:confirm-loading="generating"
|
||||
ok-text="确认生成"
|
||||
cancel-text="取消"
|
||||
@ok="handleGenerate"
|
||||
@cancel="visible = false"
|
||||
>
|
||||
<a-form :label-col="{ span: 5 }" :wrapper-col="{ span: 18 }" style="margin-top: 12px">
|
||||
<a-form-item label="业务单据" required>
|
||||
<a-select
|
||||
v-model:value="form.sourceTable"
|
||||
placeholder="选择审批注册中心已启用的业务表"
|
||||
show-search
|
||||
:filter-option="filterDoc"
|
||||
:options="docOptions"
|
||||
@change="onTableChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="审批流程" required>
|
||||
<a-select
|
||||
v-model:value="form.flowId"
|
||||
placeholder="选择该业务表对应的审批流"
|
||||
:loading="loadingFlows"
|
||||
:options="flowOptions"
|
||||
@change="loadPreview"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="覆盖草稿">
|
||||
<a-checkbox v-model:checked="form.overwriteDraft">
|
||||
删除同前缀的草稿方案后重新生成(已发布方案不受影响)
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<a-spin :spinning="loadingPreview">
|
||||
<template v-if="preview">
|
||||
<a-alert
|
||||
type="info"
|
||||
show-icon
|
||||
style="margin-bottom: 12px"
|
||||
:message="`流程节点 ${preview.flowNodeCount || 0} 个;已配置环节 ${preview.configuredNodeCount || 0} 个;未配置 ${preview.unconfiguredNodeCount || 0} 个`"
|
||||
:description="`状态字典:${preview.statusDictCode || '-'};驳回回退至:${preview.initialStatusLabel}(${preview.initialStatus})`"
|
||||
/>
|
||||
<a-alert
|
||||
v-if="(preview.unconfiguredNodeCount || 0) > 0"
|
||||
type="warning"
|
||||
show-icon
|
||||
style="margin-bottom: 12px"
|
||||
message="存在未配置或未选择环节的流程节点,这些节点不会生成集成方案。"
|
||||
/>
|
||||
<a-table
|
||||
size="small"
|
||||
:pagination="false"
|
||||
:data-source="preview.nodeBindings || []"
|
||||
:columns="previewColumns"
|
||||
row-key="nodeId"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'nodeName'">
|
||||
<span>{{ record.nodeName }}</span>
|
||||
<a-tag :color="record.stageConfigured ? 'success' : 'warning'" style="margin-left: 6px">
|
||||
{{ record.configuredText }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'stage'">
|
||||
<a-select
|
||||
v-model:value="record.stage"
|
||||
allow-clear
|
||||
placeholder="可不选"
|
||||
style="width: 110px"
|
||||
:options="stageOptions"
|
||||
@change="() => onStageChange(record)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'willGenerate'">
|
||||
<a-tag v-if="record.willGenerate" color="processing">将生成</a-tag>
|
||||
<span v-else style="color: #bbb">-</span>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'unconfiguredReason'">
|
||||
<span style="color: #fa8c16; font-size: 12px">{{ record.unconfiguredReason || '-' }}</span>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<div style="margin-top: 8px; font-size: 12px; color: #888">
|
||||
将生成 {{ planCount }} 个方案(含 1 个驳回回退方案),默认状态为草稿,生成后请核对并发布。
|
||||
</div>
|
||||
</template>
|
||||
<a-empty v-else-if="!loadingPreview" description="请选择业务表与审批流程后预览" />
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { listBizDocRegistry, previewDefaultFromFlow, generateDefaultFromFlow } from '../MesXslIntegrationPlan.api';
|
||||
import { getApprovalFlowList } from '/@/views/approval/flow/approvalFlow.api';
|
||||
|
||||
const emit = defineEmits<{ success: [] }>();
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const visible = ref(false);
|
||||
const generating = ref(false);
|
||||
const loadingFlows = ref(false);
|
||||
const loadingPreview = ref(false);
|
||||
const preview = ref<any>(null);
|
||||
const docList = ref<any[]>([]);
|
||||
const flowList = ref<any[]>([]);
|
||||
|
||||
const form = ref({
|
||||
sourceTable: undefined as string | undefined,
|
||||
flowId: undefined as string | undefined,
|
||||
overwriteDraft: true,
|
||||
});
|
||||
|
||||
const previewColumns = [
|
||||
{ title: '序号', dataIndex: 'nodeIndex', width: 52 },
|
||||
{ title: '流程节点', dataIndex: 'nodeName', width: 200 },
|
||||
{ title: '识别环节', dataIndex: 'stage', width: 130 },
|
||||
{ title: '前置状态', dataIndex: 'expectedFromLabel', width: 90 },
|
||||
{ title: '通过后状态', dataIndex: 'statusAfterLabel', width: 100 },
|
||||
{ title: '生成方案', dataIndex: 'willGenerate', width: 88 },
|
||||
{ title: '未配置原因', dataIndex: 'unconfiguredReason', ellipsis: true },
|
||||
];
|
||||
|
||||
const stageOptions = computed(() =>
|
||||
(preview.value?.stageOptions || []).map((opt: any) => ({
|
||||
value: opt.value,
|
||||
label: opt.label,
|
||||
})),
|
||||
);
|
||||
|
||||
const planCount = computed(() => {
|
||||
const bindings = preview.value?.nodeBindings || [];
|
||||
const configured = bindings.filter((b: any) => b.willGenerate).length;
|
||||
return configured > 0 ? configured + 1 : 0;
|
||||
});
|
||||
|
||||
const docOptions = computed(() =>
|
||||
docList.value
|
||||
.filter((d) => d.enabled === 1)
|
||||
.map((d) => ({
|
||||
value: d.tableName,
|
||||
label: `${d.displayName || d.tableName}(${d.tableName})`,
|
||||
})),
|
||||
);
|
||||
|
||||
const flowOptions = computed(() =>
|
||||
flowList.value.map((f) => ({
|
||||
value: f.id,
|
||||
label: `${f.flowName || f.id}${f.status === '1' ? ' [已发布]' : ''}`,
|
||||
})),
|
||||
);
|
||||
|
||||
function filterDoc(input: string, option: any) {
|
||||
return (option?.label || '').toLowerCase().includes(input.toLowerCase());
|
||||
}
|
||||
|
||||
function labelOfStatusChain(chain: any[], value?: string) {
|
||||
if (!value) return '-';
|
||||
const hit = (chain || []).find((item) => item.value === value);
|
||||
return hit?.label || value;
|
||||
}
|
||||
|
||||
function buildUnconfiguredReason(stageMeta: Record<string, any>, stage?: string) {
|
||||
if (!stage) {
|
||||
return '未选择审批环节';
|
||||
}
|
||||
const meta = stageMeta?.[stage];
|
||||
if (!meta) {
|
||||
return '未选择审批环节';
|
||||
}
|
||||
if (!meta.enabled) {
|
||||
return `环节「${meta.label || stage}」未在注册中心启用`;
|
||||
}
|
||||
if (!meta.configured) {
|
||||
return `环节「${meta.label || stage}」未配置操作人字段`;
|
||||
}
|
||||
return '环节未完整配置';
|
||||
}
|
||||
|
||||
function resolveStatusAfter(stage?: string, statusChain?: any[]) {
|
||||
if (!stage) return null;
|
||||
const hit = (statusChain || []).find((item) => item.value === stage);
|
||||
return hit ? stage : null;
|
||||
}
|
||||
|
||||
function resolveExpectedFrom(bindings: any[], index: number, statusChain: any[], initialStatus: string) {
|
||||
const current = bindings[index];
|
||||
if (!current?.stage) {
|
||||
return initialStatus;
|
||||
}
|
||||
const stageIdx = (statusChain || []).findIndex((item) => item.value === current.stage);
|
||||
if (stageIdx > 0) {
|
||||
return statusChain[stageIdx - 1].value;
|
||||
}
|
||||
for (let j = index - 1; j >= 0; j--) {
|
||||
const prev = bindings[j];
|
||||
if (prev.stageConfigured && prev.stage) {
|
||||
return prev.stage;
|
||||
}
|
||||
}
|
||||
return initialStatus;
|
||||
}
|
||||
|
||||
function recalcBindings() {
|
||||
if (!preview.value?.nodeBindings) {
|
||||
return;
|
||||
}
|
||||
const bindings = preview.value.nodeBindings;
|
||||
const stageMeta = preview.value.stageMeta || {};
|
||||
const statusChain = preview.value.statusChain || [];
|
||||
const initialStatus = preview.value.initialStatus;
|
||||
|
||||
bindings.forEach((record: any) => {
|
||||
const stage = record.stage || undefined;
|
||||
const meta = stage ? stageMeta[stage] : null;
|
||||
const configured = !!(stage && meta?.enabled && meta?.configured);
|
||||
record.stageConfigured = configured;
|
||||
record.configuredText = configured ? '已配置该环节' : '未配置该环节';
|
||||
record.stageLabel = stage ? labelOfStatusChain(statusChain, stage) : '-';
|
||||
record.unconfiguredReason = configured ? undefined : buildUnconfiguredReason(stageMeta, stage);
|
||||
});
|
||||
|
||||
const configuredBindings = bindings.filter((b: any) => b.stageConfigured);
|
||||
bindings.forEach((record: any) => {
|
||||
if (!record.stageConfigured) {
|
||||
record.willGenerate = false;
|
||||
record.triggerPhase = null;
|
||||
record.expectedFrom = null;
|
||||
record.expectedFromLabel = '-';
|
||||
record.statusAfter = null;
|
||||
record.statusAfterLabel = '-';
|
||||
return;
|
||||
}
|
||||
const cfgIdx = configuredBindings.indexOf(record);
|
||||
record.willGenerate = cfgIdx >= 0;
|
||||
record.triggerPhase = 'onNodeApprove';
|
||||
const expectedFrom = resolveExpectedFrom(bindings, bindings.indexOf(record), statusChain, initialStatus);
|
||||
record.expectedFrom = expectedFrom;
|
||||
record.expectedFromLabel = labelOfStatusChain(statusChain, expectedFrom);
|
||||
const statusAfter = resolveStatusAfter(record.stage, statusChain);
|
||||
record.statusAfter = statusAfter;
|
||||
record.statusAfterLabel = statusAfter ? labelOfStatusChain(statusChain, statusAfter) : '需手配';
|
||||
});
|
||||
|
||||
preview.value.configuredNodeCount = configuredBindings.length;
|
||||
preview.value.unconfiguredNodeCount = bindings.length - configuredBindings.length;
|
||||
}
|
||||
|
||||
function onStageChange(record: any) {
|
||||
if (record.stage === undefined || record.stage === null || record.stage === '') {
|
||||
record.stage = undefined;
|
||||
}
|
||||
recalcBindings();
|
||||
}
|
||||
|
||||
async function open() {
|
||||
visible.value = true;
|
||||
preview.value = null;
|
||||
form.value = { sourceTable: undefined, flowId: undefined, overwriteDraft: true };
|
||||
const res = await listBizDocRegistry();
|
||||
docList.value = res?.records || res || [];
|
||||
}
|
||||
|
||||
async function onTableChange(table: string) {
|
||||
form.value.flowId = undefined;
|
||||
preview.value = null;
|
||||
flowList.value = [];
|
||||
if (!table) return;
|
||||
loadingFlows.value = true;
|
||||
try {
|
||||
const res = await getApprovalFlowList({ bizTable: table, pageNo: 1, pageSize: 50 });
|
||||
flowList.value = res?.records || [];
|
||||
if (flowList.value.length === 1) {
|
||||
form.value.flowId = flowList.value[0].id;
|
||||
await loadPreview();
|
||||
}
|
||||
} finally {
|
||||
loadingFlows.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPreview() {
|
||||
if (!form.value.sourceTable || !form.value.flowId) {
|
||||
preview.value = null;
|
||||
return;
|
||||
}
|
||||
loadingPreview.value = true;
|
||||
try {
|
||||
preview.value = await previewDefaultFromFlow({
|
||||
sourceTable: form.value.sourceTable,
|
||||
flowId: form.value.flowId,
|
||||
});
|
||||
recalcBindings();
|
||||
} catch (e: any) {
|
||||
preview.value = null;
|
||||
createMessage.error(e?.message || '预览失败');
|
||||
} finally {
|
||||
loadingPreview.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function buildNodeBindingsPayload() {
|
||||
return (preview.value?.nodeBindings || []).map((item: any) => ({
|
||||
nodeId: item.nodeId,
|
||||
stage: item.stage || null,
|
||||
}));
|
||||
}
|
||||
|
||||
async function handleGenerate() {
|
||||
if (!form.value.sourceTable || !form.value.flowId) {
|
||||
createMessage.warning('请选择业务表和审批流程');
|
||||
return;
|
||||
}
|
||||
recalcBindings();
|
||||
if ((preview.value?.configuredNodeCount || 0) === 0) {
|
||||
createMessage.warning('没有已配置环节的流程节点,无法生成方案');
|
||||
return;
|
||||
}
|
||||
generating.value = true;
|
||||
try {
|
||||
const res = await generateDefaultFromFlow({
|
||||
sourceTable: form.value.sourceTable,
|
||||
flowId: form.value.flowId,
|
||||
overwriteDraft: form.value.overwriteDraft,
|
||||
nodeBindings: buildNodeBindingsPayload(),
|
||||
});
|
||||
createMessage.success(res?.message || '生成成功');
|
||||
visible.value = false;
|
||||
emit('success');
|
||||
} catch (e: any) {
|
||||
createMessage.error(e?.message || '生成失败');
|
||||
} finally {
|
||||
generating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<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>
|
||||
|
||||
<script lang="ts" setup>
|
||||
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;
|
||||
const name = record.displayName || record.docCode || record.tableName || '';
|
||||
return name ? `审批明细 — ${name}` : '审批明细';
|
||||
});
|
||||
|
||||
const [registerTable, { reload, setProps }] = useTable({
|
||||
title: '审批痕迹明细',
|
||||
api: list,
|
||||
columns: drawerColumns,
|
||||
canResize: true,
|
||||
useSearchForm: true,
|
||||
formConfig: {
|
||||
schemas: drawerSearchFormSchema,
|
||||
autoSubmitOnEnter: true,
|
||||
showAdvancedButton: false,
|
||||
},
|
||||
showTableSetting: true,
|
||||
bordered: true,
|
||||
showIndexColumn: true,
|
||||
immediate: false,
|
||||
actionColumn: { width: 0, ifShow: false },
|
||||
});
|
||||
|
||||
const [registerDrawer, { setDrawerProps }] = useDrawerInner(async (data) => {
|
||||
registryRecord.value = data?.record || {};
|
||||
const tableName = registryRecord.value.tableName;
|
||||
setProps({
|
||||
searchInfo: tableName ? { bizTable: tableName } : {},
|
||||
});
|
||||
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>
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" :width="640" @ok="handleSubmit">
|
||||
<BasicForm @register="registerForm">
|
||||
<template #listApiPath="{ model, field }">
|
||||
<RegistryMenuSelect :value="model[field]" @change="(val) => (model[field] = val)" />
|
||||
</template>
|
||||
</BasicForm>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, unref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { BasicForm, useForm } from '/@/components/Form/index';
|
||||
import { formSchema } from '../MesXslBizDocRegistry.data';
|
||||
import { saveOrUpdate } from '../MesXslBizDocRegistry.api';
|
||||
import RegistryMenuSelect from './RegistryMenuSelect.vue';
|
||||
|
||||
const emit = defineEmits(['register', 'success']);
|
||||
const isUpdate = ref(false);
|
||||
|
||||
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
|
||||
labelWidth: 100,
|
||||
schemas: formSchema,
|
||||
showActionButtonGroup: false,
|
||||
baseColProps: { span: 24 },
|
||||
});
|
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
await resetFields();
|
||||
setModalProps({ confirmLoading: false });
|
||||
isUpdate.value = !!data?.isUpdate;
|
||||
if (unref(isUpdate) && data.record) {
|
||||
const record = { ...data.record };
|
||||
// 启用开关:后端为 0/1 整数,Switch 需 checkedValue/unCheckedValue 对齐
|
||||
if (record.enabled === true || record.enabled === '1') {
|
||||
record.enabled = 1;
|
||||
} else if (record.enabled === false || record.enabled === '0') {
|
||||
record.enabled = 0;
|
||||
}
|
||||
if (typeof record.enabledStages === 'string') {
|
||||
record.enabledStages = record.enabledStages.split(',').filter(Boolean);
|
||||
} else if (!Array.isArray(record.enabledStages)) {
|
||||
record.enabledStages = [];
|
||||
}
|
||||
await setFieldsValue(record);
|
||||
}
|
||||
});
|
||||
|
||||
const title = computed(() => (unref(isUpdate) ? '编辑审批注册' : '新增审批注册'));
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const values = await validate();
|
||||
// 空数组须显式传空串,后端才能清空 enabled_stages
|
||||
if (Array.isArray(values.enabledStages)) {
|
||||
values.enabledStages = values.enabledStages.join(',');
|
||||
} else if (values.enabledStages == null) {
|
||||
values.enabledStages = '';
|
||||
}
|
||||
if (typeof values.enabled === 'boolean') {
|
||||
values.enabled = values.enabled ? 1 : 0;
|
||||
} else if (values.enabled !== 0 && values.enabled !== 1) {
|
||||
values.enabled = values.enabled ? 1 : 0;
|
||||
}
|
||||
setModalProps({ confirmLoading: true });
|
||||
await saveOrUpdate(values, unref(isUpdate));
|
||||
closeModal();
|
||||
emit('success');
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<a-drawer v-model:open="visible" :title="`动作管理 — ${planName}`" width="900" destroy-on-close @close="visible = false">
|
||||
<div style="margin-bottom: 12px">
|
||||
<a-button type="primary" size="small" @click="handleAddAction">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加动作
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-table :dataSource="actions" :columns="tableColumns" :loading="loading" :pagination="false" row-key="id" size="small" bordered>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'enabled'">
|
||||
<a-tag :color="record.enabled ? 'green' : 'default'">{{ record.enabled ? '启用' : '停用' }}</a-tag>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'actionType'">
|
||||
<a-tag color="blue">{{ actionTypeLabel(record.actionType) }}</a-tag>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'onFail'">
|
||||
<a-tag :color="record.onFail === 'stop' ? 'orange' : 'default'">{{ record.onFail === 'stop' ? '终止' : '继续' }}</a-tag>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'summary'">
|
||||
<span style="font-size: 12px; color: #666">{{ formatActionSummary(record) }}</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'operation'">
|
||||
<a-space>
|
||||
<a-button size="small" @click="handleEditAction(record)">编辑</a-button>
|
||||
<a-popconfirm title="确认删除该动作?" @confirm="handleDeleteAction(record)">
|
||||
<a-button size="small" danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- update-begin---author:GHT ---date:2026-06-05 for:【XSLMES-20260605-K8R2】动作管理复用可视化编辑器,与向导添加动作一致 -->
|
||||
<VisualActionEditor ref="visualEditorRef" @success="handleActionSaved" />
|
||||
<!-- update-end---author:GHT ---date:2026-06-05 for:【XSLMES-20260605-K8R2】动作管理复用可视化编辑器 -->
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import {
|
||||
listActions,
|
||||
saveAction,
|
||||
editAction,
|
||||
deleteAction,
|
||||
getTableColumns,
|
||||
listBizDocRegistry,
|
||||
getRegistryByTable,
|
||||
} from '../MesXslIntegrationPlan.api';
|
||||
import VisualActionEditor from './VisualActionEditor.vue';
|
||||
|
||||
const STAGE_LABELS: Record<string, string> = { proofread: '校对', audit: '审核', approve: '批准' };
|
||||
|
||||
const ACTION_TYPE_LABELS: Record<string, string> = {
|
||||
REGISTRY_STAGE_SYNC: '审批环节同步',
|
||||
REGISTRY_STAGE_REVERT: '审批环节回退',
|
||||
SQL_UPDATE: 'SQL更新',
|
||||
};
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const planId = ref('');
|
||||
const planName = ref('');
|
||||
const planRecord = ref<Recordable>({});
|
||||
const actions = ref<any[]>([]);
|
||||
|
||||
const visualEditorRef = ref();
|
||||
const bizDocList = ref<any[]>([]);
|
||||
const sourceColumns = ref<any[]>([]);
|
||||
const sourceRegistry = ref<any>(null);
|
||||
|
||||
const tableColumns = [
|
||||
{ title: '顺序', dataIndex: 'execOrder', width: 60, align: 'center' },
|
||||
{ title: '动作名称', dataIndex: 'actionName', width: 140 },
|
||||
{ title: '类型', dataIndex: 'actionType', width: 120 },
|
||||
{ title: '配置摘要', dataIndex: 'summary', ellipsis: true },
|
||||
{ title: '失败策略', dataIndex: 'onFail', width: 90 },
|
||||
{ title: '启用', dataIndex: 'enabled', width: 70 },
|
||||
{ title: '操作', key: 'operation', width: 130, align: 'center' },
|
||||
];
|
||||
|
||||
function actionTypeLabel(type: string) {
|
||||
return ACTION_TYPE_LABELS[type] || type || '-';
|
||||
}
|
||||
|
||||
function formatActionSummary(record: Recordable) {
|
||||
if (record.actionType === 'REGISTRY_STAGE_SYNC' || record.actionType === 'REGISTRY_STAGE_REVERT') {
|
||||
try {
|
||||
const cfg = JSON.parse(record.actionConfig || '{}');
|
||||
if (record.actionType === 'REGISTRY_STAGE_SYNC') {
|
||||
const stage = cfg.registryStage?.stage || cfg.stage;
|
||||
const from = cfg.registryStage?.expectedFrom || cfg.expectedFrom;
|
||||
const after = cfg.registryStage?.statusAfter || cfg.statusAfter || stage;
|
||||
return `环节→${STAGE_LABELS[stage] || stage || '?'}${from ? `,前置=${from}` : ''},通过后=${after}`;
|
||||
}
|
||||
const target = cfg.registryStage?.targetStage ?? cfg.targetStage;
|
||||
return target !== undefined && target !== null && target !== '' ? `回退→${target}` : '回退→未配置';
|
||||
} catch {
|
||||
return record.actionConfig || '-';
|
||||
}
|
||||
}
|
||||
if (record.actionType === 'SQL_UPDATE') {
|
||||
try {
|
||||
const cfg = JSON.parse(record.actionConfig || '{}');
|
||||
const base = record.sqlTemplate || '-';
|
||||
return cfg.syncTrace ? `${base};痕迹同步:是` : base;
|
||||
} catch {
|
||||
return record.sqlTemplate || '-';
|
||||
}
|
||||
}
|
||||
return record.sqlTemplate || '-';
|
||||
}
|
||||
|
||||
async function loadEditorContext() {
|
||||
const sourceTable = planRecord.value.sourceTable;
|
||||
if (!sourceTable) return;
|
||||
try {
|
||||
const [cols, registryRes, docsRes] = await Promise.all([
|
||||
getTableColumns(sourceTable),
|
||||
getRegistryByTable(sourceTable),
|
||||
listBizDocRegistry(),
|
||||
]);
|
||||
sourceColumns.value = (cols as any) || [];
|
||||
sourceRegistry.value = registryRes || null;
|
||||
bizDocList.value = (docsRes as any)?.records || (Array.isArray(docsRes) ? docsRes : []);
|
||||
} catch {
|
||||
sourceColumns.value = [];
|
||||
sourceRegistry.value = null;
|
||||
bizDocList.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function open(plan: Recordable) {
|
||||
planId.value = plan.id;
|
||||
planName.value = plan.planName;
|
||||
planRecord.value = plan;
|
||||
visible.value = true;
|
||||
await Promise.all([loadActions(), loadEditorContext()]);
|
||||
}
|
||||
|
||||
async function loadActions() {
|
||||
loading.value = true;
|
||||
try {
|
||||
actions.value = (await listActions(planId.value)) || [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openVisualEditor(action?: Recordable) {
|
||||
if (!planRecord.value.sourceTable) {
|
||||
createMessage.warning('方案未配置触发业务表');
|
||||
return;
|
||||
}
|
||||
visualEditorRef.value?.open({
|
||||
sourceTable: planRecord.value.sourceTable,
|
||||
sourceColumns: sourceColumns.value,
|
||||
bizDocList: bizDocList.value,
|
||||
sourceRegistry: sourceRegistry.value,
|
||||
action: action
|
||||
? { ...action, planId: planId.value }
|
||||
: { planId: planId.value, execOrder: actions.value.length + 1 },
|
||||
execOrder: action?.execOrder ?? actions.value.length + 1,
|
||||
});
|
||||
}
|
||||
|
||||
function handleAddAction() {
|
||||
openVisualEditor();
|
||||
}
|
||||
|
||||
function handleEditAction(record: Recordable) {
|
||||
openVisualEditor(record);
|
||||
}
|
||||
|
||||
async function handleDeleteAction(record: Recordable) {
|
||||
await deleteAction({ id: record.id }, loadActions);
|
||||
}
|
||||
|
||||
async function handleActionSaved(actionData: Recordable) {
|
||||
const payload = {
|
||||
...actionData,
|
||||
planId: planId.value,
|
||||
enabled: actionData.enabled !== false && actionData.enabled !== 0 ? 1 : 0,
|
||||
};
|
||||
try {
|
||||
if (actionData.id) {
|
||||
await editAction(payload);
|
||||
} else {
|
||||
await saveAction(payload);
|
||||
}
|
||||
createMessage.success('保存成功');
|
||||
await loadActions();
|
||||
} catch (e: any) {
|
||||
createMessage.error(e?.message || '保存失败');
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open, openAndEditFirstAction });
|
||||
|
||||
async function openAndEditFirstAction(plan: Recordable) {
|
||||
await open(plan);
|
||||
if (actions.value.length > 0) {
|
||||
openVisualEditor(actions.value[0]);
|
||||
} else {
|
||||
openVisualEditor();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" :width="720" @ok="handleSubmit">
|
||||
<BasicForm @register="registerForm" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, unref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { BasicForm, useForm } from '/@/components/Form/index';
|
||||
import { formSchema } from '../MesXslIntegrationPlan.data';
|
||||
import { saveOrUpdate } from '../MesXslIntegrationPlan.api';
|
||||
|
||||
const emit = defineEmits(['register', 'success']);
|
||||
const isUpdate = ref(false);
|
||||
|
||||
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
|
||||
labelWidth: 100,
|
||||
schemas: formSchema,
|
||||
showActionButtonGroup: false,
|
||||
baseColProps: { span: 24 },
|
||||
});
|
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
await resetFields();
|
||||
setModalProps({ confirmLoading: false });
|
||||
isUpdate.value = !!data?.isUpdate;
|
||||
if (unref(isUpdate) && data.record) {
|
||||
await setFieldsValue({ ...data.record });
|
||||
}
|
||||
});
|
||||
|
||||
const title = computed(() => (unref(isUpdate) ? '编辑集成方案' : '新增集成方案'));
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const values = await validate();
|
||||
setModalProps({ confirmLoading: true });
|
||||
await saveOrUpdate(values, unref(isUpdate));
|
||||
closeModal();
|
||||
emit('success');
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<div>
|
||||
<a-select
|
||||
:value="selectedPaths"
|
||||
mode="multiple"
|
||||
allow-clear
|
||||
show-search
|
||||
placeholder="从二级菜单选取,自动填入列表接口路径;也可在下方直接编辑"
|
||||
:filter-option="filterOption"
|
||||
style="width: 100%"
|
||||
@change="handleSelectChange"
|
||||
>
|
||||
<a-select-opt-group v-for="group in menuGroups" :key="group.path" :label="group.title">
|
||||
<a-select-option
|
||||
v-for="item in group.children"
|
||||
:key="item.apiPath"
|
||||
:value="item.apiPath"
|
||||
:title="item.apiPath"
|
||||
>
|
||||
<span>{{ item.title }}</span>
|
||||
<span style="margin-left: 8px; color: #8c8c8c; font-size: 11px">{{ item.apiPath }}</span>
|
||||
</a-select-option>
|
||||
</a-select-opt-group>
|
||||
</a-select>
|
||||
|
||||
<a-input
|
||||
:value="inputVal"
|
||||
style="margin-top: 6px"
|
||||
placeholder="列表接口路径(多个逗号分隔,可手动编辑)"
|
||||
@change="handleInputChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { usePermissionStoreWithOut } from '/@/store/modules/permission';
|
||||
import type { Menu } from '/@/router/types';
|
||||
|
||||
interface MenuGroup {
|
||||
path: string;
|
||||
title: string;
|
||||
children: { apiPath: string; title: string; path: string }[];
|
||||
}
|
||||
|
||||
const props = defineProps<{ value?: string }>();
|
||||
const emit = defineEmits(['change', 'update:value']);
|
||||
|
||||
const permStore = usePermissionStoreWithOut();
|
||||
|
||||
const menuGroups = computed<MenuGroup[]>(() => {
|
||||
const list = permStore.getBackMenuList as Menu[];
|
||||
if (!list || list.length === 0) return [];
|
||||
const groups: MenuGroup[] = [];
|
||||
for (const parent of list) {
|
||||
if (!parent.children || parent.children.length === 0) continue;
|
||||
const leafChildren = parent.children.filter((c) => !c.hideMenu);
|
||||
if (leafChildren.length === 0) continue;
|
||||
groups.push({
|
||||
path: parent.path,
|
||||
title: (parent.meta?.title as string) || parent.name,
|
||||
children: leafChildren.map((c) => ({
|
||||
path: c.path,
|
||||
apiPath: c.path + '/list',
|
||||
title: (c.meta?.title as string) || c.name,
|
||||
})),
|
||||
});
|
||||
}
|
||||
return groups;
|
||||
});
|
||||
|
||||
const allApiPaths = computed<string[]>(() =>
|
||||
menuGroups.value.flatMap((g) => g.children.map((c) => c.apiPath)),
|
||||
);
|
||||
|
||||
// 当前文本框的值(原始逗号分隔串)
|
||||
const inputVal = ref(props.value || '');
|
||||
|
||||
// 从文本框值解析出在 menuGroups 里的路径(用于 Select 的高亮选中)
|
||||
const selectedPaths = computed<string[]>(() => {
|
||||
if (!inputVal.value) return [];
|
||||
return inputVal.value
|
||||
.split(',')
|
||||
.map((p) => p.trim())
|
||||
.filter((p) => allApiPaths.value.includes(p));
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(val) => {
|
||||
inputVal.value = val || '';
|
||||
},
|
||||
);
|
||||
|
||||
function handleSelectChange(vals: string[]) {
|
||||
// 将已有的手动路径(不在菜单里的)保留,把菜单选取结果合并进去
|
||||
const manualPaths = inputVal.value
|
||||
.split(',')
|
||||
.map((p) => p.trim())
|
||||
.filter((p) => p && !allApiPaths.value.includes(p));
|
||||
const merged = [...manualPaths, ...vals].filter(Boolean).join(',');
|
||||
inputVal.value = merged;
|
||||
emit('change', merged);
|
||||
emit('update:value', merged);
|
||||
}
|
||||
|
||||
function handleInputChange(e: Event) {
|
||||
const val = (e.target as HTMLInputElement).value;
|
||||
inputVal.value = val;
|
||||
emit('change', val);
|
||||
emit('update:value', val);
|
||||
}
|
||||
|
||||
function filterOption(input: string, option: any) {
|
||||
const title = option?.children?.[0]?.children || '';
|
||||
const path = option.value || '';
|
||||
const lc = input.toLowerCase();
|
||||
return String(title).toLowerCase().includes(lc) || String(path).toLowerCase().includes(lc);
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,887 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
:title="isUpdate ? '编辑动作' : '添加动作'"
|
||||
width="860px"
|
||||
:confirm-loading="saving"
|
||||
@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">
|
||||
<a-form-item label="动作名称" name="actionName" :rules="[{ required: true, message: '请输入动作名称' }]">
|
||||
<a-input v-model:value="form.actionName" placeholder="如 配合示方→认定通过" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="9">
|
||||
<a-form-item label="执行顺序" :label-col="{ span: 7 }" :wrapper-col="{ span: 17 }">
|
||||
<a-input-number v-model:value="form.execOrder" :min="0" style="width: 90px" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 动作类型卡片 -->
|
||||
<div style="margin-bottom: 20px">
|
||||
<div style="font-weight: 500; margin-bottom: 10px; color: rgba(0, 0, 0, 0.85)">动作类型</div>
|
||||
<div style="display: flex; gap: 12px">
|
||||
<div
|
||||
v-for="t in ACTION_TYPES"
|
||||
:key="t.value"
|
||||
:style="{
|
||||
flex: '1', border: '2px solid',
|
||||
borderColor: vc.visualType === t.value ? '#1677ff' : t.disabled ? '#f0f0f0' : '#d9d9d9',
|
||||
borderRadius: '8px', padding: '14px 12px', cursor: t.disabled ? 'not-allowed' : 'pointer',
|
||||
background: vc.visualType === t.value ? '#e6f4ff' : t.disabled ? '#fafafa' : 'white',
|
||||
opacity: t.disabled ? 0.5 : 1, transition: 'all 0.2s', userSelect: 'none',
|
||||
}"
|
||||
@click="!t.disabled && selectVisualType(t.value)"
|
||||
>
|
||||
<div style="font-size: 22px; margin-bottom: 6px">{{ t.icon }}</div>
|
||||
<div style="font-weight: 600; margin-bottom: 4px; color: rgba(0, 0, 0, 0.85)">{{ t.label }}</div>
|
||||
<div style="font-size: 12px; color: #888; line-height: 1.4">{{ t.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 审批注册中心环节同步(无需选目标表、无需写 SQL) -->
|
||||
<template v-if="vc.visualType === 'REGISTRY_STAGE_SYNC' || vc.visualType === 'REGISTRY_STAGE_REVERT'">
|
||||
<a-alert
|
||||
type="info"
|
||||
show-icon
|
||||
style="margin-bottom: 14px"
|
||||
message="审批环节仅用于匹配审批流与写入痕迹;业务表 status 由「通过后状态」控制。操作人/时间写入痕迹表,无需绑定 Java 接口。"
|
||||
/>
|
||||
<template v-if="vc.registryStage">
|
||||
<a-form-item v-if="vc.visualType === 'REGISTRY_STAGE_SYNC'" label="审批环节" required>
|
||||
<a-select
|
||||
v-model:value="vc.registryStage.stage"
|
||||
:options="registryStageOptions"
|
||||
placeholder="从注册中心启用环节选择"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div v-if="!registryStageOptions.length" style="font-size: 12px; color: #faad14; margin-top: 4px">
|
||||
未配置启用环节,请先在审批注册中心配置 enabled_stages
|
||||
</div>
|
||||
<div v-else style="font-size: 12px; color: #888; margin-top: 4px">
|
||||
仅参与审批流匹配与痕迹同步(proofread/audit/approve),不会直接写入业务表 status。
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="vc.visualType === 'REGISTRY_STAGE_SYNC'" label="前置状态">
|
||||
<a-select
|
||||
v-if="sourceStatusDictItems.length"
|
||||
v-model:value="vc.registryStage.expectedFrom"
|
||||
:options="sourceStatusDictItems"
|
||||
placeholder="留空则按环节自动推断"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<a-input
|
||||
v-else
|
||||
v-model:value="vc.registryStage.expectedFrom"
|
||||
placeholder="未解析到状态字典,可手填状态字典值"
|
||||
/>
|
||||
<div style="font-size: 12px; color: #888; margin-top: 4px">
|
||||
取自触发表「{{ sourceStatusFieldName }}」字段字典{{ sourceStatusDictCode ? `(${sourceStatusDictCode})` : '' }};留空则自动推断。仅当前状态等于此前置值时才执行。
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="vc.visualType === 'REGISTRY_STAGE_SYNC'" label="通过后状态" required>
|
||||
<a-select
|
||||
v-if="sourceStatusDictItems.length"
|
||||
v-model:value="vc.registryStage.statusAfter"
|
||||
:options="sourceStatusDictItems"
|
||||
placeholder="请选择本环节通过后业务表应变为的状态"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<a-input
|
||||
v-else
|
||||
v-model:value="vc.registryStage.statusAfter"
|
||||
placeholder="未解析到状态字典,可手填业务状态值"
|
||||
/>
|
||||
<div style="font-size: 12px; color: #888; margin-top: 4px">
|
||||
本环节审批通过后,将触发表「{{ sourceStatusFieldName }}」更新为此值(与审批环节码无关,按各单据自己的状态字典配置)。
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="vc.visualType === 'REGISTRY_STAGE_REVERT'" label="回退目标" required>
|
||||
<a-select
|
||||
v-if="sourceStatusDictItems.length"
|
||||
v-model:value="vc.registryStage.targetStage"
|
||||
:options="sourceStatusDictItems"
|
||||
placeholder="请选择驳回后回退到的业务状态"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<a-input v-else v-model:value="vc.registryStage.targetStage" placeholder="未解析到状态字典,可手填字典键值(item_value)" />
|
||||
<div style="font-size: 12px; color: #888; margin-top: 4px">
|
||||
保存为字典键值(item_value),执行时原样写入触发表「{{ sourceStatusFieldName }}」,与界面显示文字无关。
|
||||
</div>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 操作目标表 -->
|
||||
<a-form-item v-if="vc.visualType === 'STATUS_MODIFY' || vc.visualType === 'DATA_SYNC'" label="操作目标表">
|
||||
<div style="display: flex; gap: 8px; align-items: center">
|
||||
<a-select
|
||||
v-model:value="vc.targetTable"
|
||||
placeholder="选择要被操作的业务表"
|
||||
show-search
|
||||
:filter-option="filterBizDoc"
|
||||
@change="onTargetTableChange"
|
||||
style="flex: 1"
|
||||
>
|
||||
<a-select-option v-for="doc in bizDocList" :key="doc.tableName" :value="doc.tableName">
|
||||
<span>{{ getDocLabel(doc) }}</span>
|
||||
<span style="color: #bbb; margin-left: 8px; font-size: 12px">{{ doc.tableName }}</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-spin v-if="loadingTargetCols" size="small" />
|
||||
<span v-else-if="targetColumns.length" style="color: #888; font-size: 12px; white-space: nowrap">
|
||||
{{ targetColumns.length }} 个字段
|
||||
</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<template v-if="vc.targetTable">
|
||||
<!-- 关联条件 -->
|
||||
<div style="background: #f9f9f9; border: 1px solid #e8e8e8; border-radius: 6px; padding: 14px; margin-bottom: 16px">
|
||||
<div style="font-weight: 500; margin-bottom: 10px; font-size: 13px">
|
||||
关联条件
|
||||
<span style="font-weight: 400; color: #888; font-size: 12px; margin-left: 6px">通过哪个字段定位目标表记录</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: flex-end; gap: 12px">
|
||||
<div style="flex: 1">
|
||||
<div style="font-size: 12px; color: #888; margin-bottom: 4px">触发表字段({{ sourceTable }})</div>
|
||||
<a-select v-model:value="vc.linkCondition.sourceField" :options="sourceFieldOpts" placeholder="触发表关联字段" show-search style="width: 100%" />
|
||||
</div>
|
||||
<div style="font-size: 15px; color: #1677ff; padding-bottom: 6px; flex-shrink: 0; font-weight: 500">→ 等于 →</div>
|
||||
<div style="flex: 1">
|
||||
<div style="font-size: 12px; color: #888; margin-bottom: 4px">目标表字段({{ vc.targetTable }})</div>
|
||||
<a-select v-model:value="vc.linkCondition.targetField" :options="targetFieldOpts" placeholder="目标表关联字段" show-search style="width: 100%" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- update-begin---author:GHT ---date:2026-06-10 for:【关联表痕迹同步】关联表动作可选同步主表审批痕迹 -->
|
||||
<div
|
||||
v-if="vc.visualType === 'STATUS_MODIFY' || vc.visualType === 'DATA_SYNC'"
|
||||
style="background: #f6ffed; border: 1px solid #b7eb8f; border-radius: 6px; padding: 12px 14px; margin-bottom: 16px"
|
||||
>
|
||||
<a-checkbox v-model:checked="vc.syncTrace">同步主表审批痕迹到目标表</a-checkbox>
|
||||
<div style="font-size: 12px; color: #666; margin-top: 8px; line-height: 1.6">
|
||||
开启后:环节通过时将主表当前审批人/时间写入目标表 <code>mes_xsl_approval_trace</code>;
|
||||
驳回时将按「新状态」清空对应环节痕迹。
|
||||
<span v-if="vc.visualType === 'DATA_SYNC'" style="color: #fa8c16">(驳回清空仅对「状态修改」动作生效)</span>
|
||||
目标表须已在审批注册中心启用对应环节。
|
||||
</div>
|
||||
</div>
|
||||
<!-- update-end---author:GHT ---date:2026-06-10 for:【关联表痕迹同步】关联表动作可选同步主表审批痕迹 -->
|
||||
|
||||
<!-- ============ 状态修改(全新设计) ============ -->
|
||||
<template v-if="vc.visualType === 'STATUS_MODIFY'">
|
||||
|
||||
<!-- ① 节点触发说明 -->
|
||||
<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>
|
||||
|
||||
<!-- ② 目标表状态变更:字段选择 + from→to 箭头 -->
|
||||
<div style="font-weight: 500; margin-bottom: 10px; font-size: 13px">
|
||||
目标表状态变更
|
||||
</div>
|
||||
|
||||
<a-form-item label="修改字段" :label-col="{ span: 5 }" :wrapper-col="{ span: 14 }">
|
||||
<a-select
|
||||
v-model:value="vc.statusConfig!.targetField"
|
||||
:options="targetFieldOpts"
|
||||
placeholder="选择要更新的字段(如 status)"
|
||||
show-search
|
||||
@change="onTargetStatusFieldChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- from → to 箭头区 -->
|
||||
<div v-if="vc.statusConfig!.targetField"
|
||||
style="display: flex; align-items: stretch; gap: 0; background: #f5f5f5; border: 1px solid #e0e0e0; border-radius: 8px; overflow: hidden; margin-bottom: 12px; margin-left: 20%">
|
||||
|
||||
<!-- 前置状态 -->
|
||||
<div style="flex: 1; padding: 14px 16px; text-align: center">
|
||||
<div style="font-size: 11px; color: #888; margin-bottom: 6px; line-height: 1.4">
|
||||
前置状态(可选)<br>
|
||||
<span style="color: #aaa">仅当字段当前值为此值时才执行</span>
|
||||
</div>
|
||||
<a-select
|
||||
v-if="targetStatusDictCode"
|
||||
v-model:value="vc.statusConfig!.fromValue"
|
||||
:options="[{ value: '', label: '不限(留空)' }, ...targetStatusDictItems]"
|
||||
placeholder="不限(留空)"
|
||||
allow-clear
|
||||
show-search
|
||||
style="width: 100%"
|
||||
/>
|
||||
<a-input
|
||||
v-else
|
||||
v-model:value="vc.statusConfig!.fromValue"
|
||||
placeholder="不限(留空则不检查)"
|
||||
allow-clear
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 箭头分隔 -->
|
||||
<div style="display: flex; align-items: center; padding: 0 10px; background: #e8e8e8; flex-shrink: 0">
|
||||
<div style="text-align: center; color: #1677ff">
|
||||
<div style="font-size: 22px; line-height: 1">→</div>
|
||||
<div style="font-size: 10px; color: #888; margin-top: 2px">改为</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新状态 -->
|
||||
<div style="flex: 1; padding: 14px 16px; text-align: center; background: #e6f4ff">
|
||||
<div style="font-size: 11px; color: #1677ff; margin-bottom: 6px; line-height: 1.4">
|
||||
新状态 *<br>
|
||||
<span style="color: #888">执行后字段将被改为此值</span>
|
||||
</div>
|
||||
<a-select
|
||||
v-if="targetStatusDictCode"
|
||||
v-model:value="vc.statusConfig!.newValue"
|
||||
:options="targetStatusDictItems"
|
||||
placeholder="请选择新状态"
|
||||
show-search
|
||||
style="width: 100%"
|
||||
/>
|
||||
<a-input
|
||||
v-else
|
||||
v-model:value="vc.statusConfig!.newValue"
|
||||
placeholder="如 approve"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="vc.statusConfig!.targetField" style="margin-left: 20%; margin-bottom: 12px">
|
||||
<a-checkbox v-model:checked="vc.statusConfig!.addUpdateTime">同时更新 update_time = NOW()</a-checkbox>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ============ 数据带入 ============ -->
|
||||
<template v-if="vc.visualType === 'DATA_SYNC'">
|
||||
<div style="font-weight: 500; margin-bottom: 10px; font-size: 13px">
|
||||
字段映射
|
||||
<span style="font-weight: 400; color: #888; font-size: 12px; margin-left: 6px">目标字段 ← 数据来源</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="(m, idx) in vc.fieldMappings"
|
||||
:key="idx"
|
||||
style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px"
|
||||
>
|
||||
<a-select v-model:value="m.targetField" :options="targetFieldOpts" placeholder="目标字段" show-search style="width: 160px" />
|
||||
<span style="color: #1677ff; flex-shrink: 0; font-weight: 600">←</span>
|
||||
<a-select v-model:value="m.sourceType" style="width: 110px">
|
||||
<a-select-option value="source_field">触发表字段</a-select-option>
|
||||
<a-select-option value="constant">固定值</a-select-option>
|
||||
<a-select-option value="expression">SQL表达式</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-if="m.sourceType === 'source_field'" v-model:value="m.sourceValue" :options="sourceFieldOpts" placeholder="来源字段" show-search style="flex: 1" />
|
||||
<a-input v-else v-model:value="m.sourceValue" :placeholder="getMappingPlaceholder(m.sourceType)" style="flex: 1" />
|
||||
<a-button size="small" danger type="text" @click="vc.fieldMappings!.splice(idx, 1)">删除</a-button>
|
||||
</div>
|
||||
<a-button type="dashed" size="small" @click="addMappingRow" style="width: 100%; margin-top: 4px">
|
||||
+ 添加字段映射
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- SQL 预览 -->
|
||||
<div v-if="previewSql" style="margin-top: 16px; background: #f0f5ff; border: 1px solid #d6e4ff; padding: 12px; border-radius: 6px">
|
||||
<div style="font-size: 12px; color: #1677ff; font-weight: 500; margin-bottom: 6px">生成的 SQL(预览)</div>
|
||||
<pre style="margin: 0; font-size: 12px; font-family: monospace; white-space: pre-wrap; word-break: break-all; color: #333; line-height: 1.6">{{ previewSql }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<a-form-item label="失败策略" style="margin-top: 16px">
|
||||
<a-radio-group v-model:value="form.onFail">
|
||||
<a-radio value="stop">失败后终止后续动作</a-radio>
|
||||
<a-radio value="continue">失败后继续执行</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<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';
|
||||
|
||||
interface ColMeta { columnName: string; comment: string; dataType: string; columnKey: string; }
|
||||
interface FieldMapping { targetField: string; sourceType: 'source_field' | 'constant' | 'expression'; sourceValue: string; }
|
||||
interface StatusConfig {
|
||||
targetField: string;
|
||||
fromValue: string;
|
||||
newValue: string;
|
||||
addUpdateTime: boolean;
|
||||
srcConditionField: string;
|
||||
srcConditionValue: string;
|
||||
}
|
||||
interface RegistryStageConfig {
|
||||
stage?: string;
|
||||
expectedFrom?: string;
|
||||
statusAfter?: string;
|
||||
targetStage?: string;
|
||||
}
|
||||
|
||||
interface VisualConfig {
|
||||
visualType: 'STATUS_MODIFY' | 'DATA_SYNC' | 'REGISTRY_STAGE_SYNC' | 'REGISTRY_STAGE_REVERT';
|
||||
targetTable: string;
|
||||
targetTableLabel: string;
|
||||
linkCondition: { sourceField: string; targetField: string };
|
||||
statusConfig: StatusConfig;
|
||||
fieldMappings: FieldMapping[];
|
||||
registryStage?: RegistryStageConfig;
|
||||
/** 是否将主表审批痕迹同步到目标表 */
|
||||
syncTrace?: boolean;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{ success: [action: any] }>();
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
/** 审批环节码固定中文名(与业务 status 字典无关) */
|
||||
const APPROVAL_STAGE_LABELS: Record<string, string> = {
|
||||
proofread: '校对',
|
||||
audit: '审核',
|
||||
approve: '批准',
|
||||
};
|
||||
|
||||
/** 触发表 status 字段未带字典注释时的兜底映射 */
|
||||
const SOURCE_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_raw_material_entry: 'xslmes_entry_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 },
|
||||
{ value: 'STATUS_MODIFY', icon: '📋', label: '状态修改', desc: '手写SQL修改目标表状态', disabled: false },
|
||||
{ value: 'DATA_SYNC', icon: '🔄', label: '数据带入', desc: '将触发表字段映射到目标表', disabled: false },
|
||||
{ value: 'CREATE_DOC', icon: '➕', label: '下推生成单据', desc: '在目标表创建新记录(Phase 1)', disabled: true },
|
||||
];
|
||||
|
||||
const visible = ref(false);
|
||||
const saving = ref(false);
|
||||
const isUpdate = ref(false);
|
||||
const formRef = ref();
|
||||
const loadingTargetCols = ref(false);
|
||||
|
||||
const sourceTable = ref('');
|
||||
const sourceColumns = ref<ColMeta[]>([]);
|
||||
const bizDocList = ref<any[]>([]);
|
||||
const sourceRegistry = ref<any>(null);
|
||||
|
||||
/** 审批环节:注册中心 enabled_stages + 业务 status 字典标签(不写死校对/审核/批准) */
|
||||
const registryStageOptions = computed(() => {
|
||||
const raw = sourceRegistry.value?.enabledStages;
|
||||
if (!raw) return [];
|
||||
const dictLabelMap = Object.fromEntries(sourceStatusDictItems.value.map((i) => [i.value, i.label]));
|
||||
return String(raw)
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
.map((v) => ({ value: v, label: APPROVAL_STAGE_LABELS[v] || dictLabelMap[v] || v }));
|
||||
});
|
||||
const targetColumns = ref<ColMeta[]>([]);
|
||||
|
||||
// 字典相关
|
||||
const dictCache = ref<Record<string, any[]>>({});
|
||||
const targetStatusDictCode = ref('');
|
||||
const targetStatusDictItems = ref<any[]>([]);
|
||||
const srcConditionDictCode = ref('');
|
||||
const srcConditionDictItems = ref<any[]>([]);
|
||||
const sourceStatusDictCode = ref('');
|
||||
const sourceStatusDictItems = ref<any[]>([]);
|
||||
|
||||
const sourceStatusFieldName = computed(
|
||||
() => sourceRegistry.value?.statusField || 'status',
|
||||
);
|
||||
|
||||
const defaultStatusConfig = (): StatusConfig => ({
|
||||
targetField: '', fromValue: '', newValue: '', addUpdateTime: true,
|
||||
srcConditionField: '', srcConditionValue: '',
|
||||
});
|
||||
|
||||
const defaultRegistryStage = (): RegistryStageConfig => ({
|
||||
stage: '',
|
||||
expectedFrom: '',
|
||||
statusAfter: '',
|
||||
targetStage: '',
|
||||
});
|
||||
|
||||
const defaultVc = (): VisualConfig => ({
|
||||
visualType: 'REGISTRY_STAGE_SYNC',
|
||||
targetTable: '', targetTableLabel: '',
|
||||
linkCondition: { sourceField: '', targetField: '' },
|
||||
statusConfig: defaultStatusConfig(),
|
||||
fieldMappings: [],
|
||||
registryStage: defaultRegistryStage(),
|
||||
syncTrace: false,
|
||||
});
|
||||
|
||||
/** 兼容 Flyway 扁平格式(stage/expectedFrom 在顶层)与向导嵌套格式(registryStage 对象) */
|
||||
function normalizeParsedConfig(parsed: any, actionType?: string): VisualConfig {
|
||||
const base = defaultVc();
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
if (actionType === 'REGISTRY_STAGE_REVERT') {
|
||||
base.visualType = 'REGISTRY_STAGE_REVERT';
|
||||
} else if (actionType === 'REGISTRY_STAGE_SYNC') {
|
||||
base.visualType = 'REGISTRY_STAGE_SYNC';
|
||||
}
|
||||
return base;
|
||||
}
|
||||
const visualType = (parsed.visualType || actionType || base.visualType) as VisualConfig['visualType'];
|
||||
const merged: VisualConfig = {
|
||||
...base,
|
||||
...parsed,
|
||||
visualType,
|
||||
linkCondition: { ...base.linkCondition, ...(parsed.linkCondition || {}) },
|
||||
statusConfig: { ...defaultStatusConfig(), ...(parsed.statusConfig || {}) },
|
||||
fieldMappings: Array.isArray(parsed.fieldMappings) ? parsed.fieldMappings : [],
|
||||
registryStage: {
|
||||
...defaultRegistryStage(),
|
||||
...(parsed.registryStage || {}),
|
||||
},
|
||||
};
|
||||
// Flyway 预置:{"visualType":"REGISTRY_STAGE_SYNC","stage":"proofread","expectedFrom":"compile"}
|
||||
if (parsed.stage) merged.registryStage!.stage = parsed.stage;
|
||||
if (parsed.expectedFrom !== undefined && parsed.expectedFrom !== null) {
|
||||
merged.registryStage!.expectedFrom = parsed.expectedFrom;
|
||||
}
|
||||
if (parsed.statusAfter !== undefined && parsed.statusAfter !== null) {
|
||||
merged.registryStage!.statusAfter = parsed.statusAfter;
|
||||
}
|
||||
if (parsed.targetStage !== undefined && parsed.targetStage !== null) {
|
||||
merged.registryStage!.targetStage = parsed.targetStage;
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
const defaultForm = () => ({ id: '', actionName: '', execOrder: 0, onFail: 'stop', enabled: true });
|
||||
|
||||
const form = ref(defaultForm());
|
||||
const vc = ref(defaultVc());
|
||||
|
||||
const sourceFieldOpts = computed(() =>
|
||||
sourceColumns.value.map((c) => ({
|
||||
value: c.columnName,
|
||||
label: c.comment ? `${c.columnName}(${c.comment})` : c.columnName,
|
||||
}))
|
||||
);
|
||||
|
||||
const targetFieldOpts = computed(() =>
|
||||
targetColumns.value.map((c) => ({
|
||||
value: c.columnName,
|
||||
label: c.comment ? `${c.columnName}(${c.comment})` : c.columnName,
|
||||
}))
|
||||
);
|
||||
|
||||
const previewSql = computed(() => buildSql(vc.value));
|
||||
|
||||
// 监听目标状态字段变化 → 加载字典
|
||||
watch(
|
||||
() => vc.value.statusConfig?.targetField,
|
||||
async (field) => {
|
||||
await loadTargetStatusDict(field);
|
||||
},
|
||||
);
|
||||
|
||||
// 审批环节变化时,前置/通过后状态留空则填入默认推断值
|
||||
watch(
|
||||
() => vc.value.registryStage?.stage,
|
||||
(stage) => {
|
||||
if (!vc.value.registryStage || !stage) return;
|
||||
if (!vc.value.registryStage.expectedFrom) {
|
||||
vc.value.registryStage.expectedFrom = defaultExpectedFromForStage(stage);
|
||||
}
|
||||
if (!vc.value.registryStage.statusAfter) {
|
||||
vc.value.registryStage.statusAfter = defaultStatusAfterForStage(stage);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 监听触发表节点字段变化 → 加载字典
|
||||
watch(
|
||||
() => vc.value.statusConfig?.srcConditionField,
|
||||
async (field) => {
|
||||
if (!field) { srcConditionDictCode.value = ''; srcConditionDictItems.value = []; return; }
|
||||
const dc = extractDictCode(sourceColumns.value.find((c) => c.columnName === field)?.comment || '');
|
||||
srcConditionDictCode.value = dc || '';
|
||||
srcConditionDictItems.value = dc ? await loadDict(dc) : [];
|
||||
}
|
||||
);
|
||||
|
||||
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_]*)/);
|
||||
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 {
|
||||
const items = await getDictItems(dictCode);
|
||||
const opts = ((items as any) || []).map((it: any) => ({
|
||||
value: it.value,
|
||||
label: it.title || it.text || it.value,
|
||||
}));
|
||||
dictCache.value[dictCode] = opts;
|
||||
return opts;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** 从触发表 status 字段解析字典,供「前置状态」「回退目标」下拉 */
|
||||
async function loadSourceStatusDict() {
|
||||
const field = sourceStatusFieldName.value;
|
||||
const col = sourceColumns.value.find((c) => c.columnName === field);
|
||||
let dictCode = extractDictCode(col?.comment || '');
|
||||
if (!dictCode && sourceTable.value) {
|
||||
dictCode = SOURCE_TABLE_STATUS_DICT[sourceTable.value] || '';
|
||||
}
|
||||
sourceStatusDictCode.value = dictCode || '';
|
||||
sourceStatusDictItems.value = dictCode ? await loadDict(dictCode) : [];
|
||||
}
|
||||
|
||||
/** 按 status 字典顺序推断前置状态(字典第一项为驳回回退初始态) */
|
||||
function defaultExpectedFromForStage(stage?: string): string {
|
||||
if (!stage) return '';
|
||||
const items = sourceStatusDictItems.value;
|
||||
if (items.length) {
|
||||
const idx = items.findIndex((i) => i.value === stage);
|
||||
if (idx > 0) return items[idx - 1].value;
|
||||
}
|
||||
const opts = registryStageOptions.value;
|
||||
const sidx = opts.findIndex((o) => o.value === stage);
|
||||
if (sidx > 0) return opts[sidx - 1].value;
|
||||
return items.length ? items[0].value : '';
|
||||
}
|
||||
|
||||
/** 推断通过后业务状态:字典含环节码时用环节码,否则不自动填充(需用户手选) */
|
||||
function defaultStatusAfterForStage(stage?: string): string {
|
||||
if (!stage) return '';
|
||||
const items = sourceStatusDictItems.value;
|
||||
if (items.some((i) => i.value === stage)) {
|
||||
return stage;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function defaultRevertTargetStage(): string {
|
||||
const items = sourceStatusDictItems.value;
|
||||
if (!items.length) return 'compile';
|
||||
const enabled = new Set(registryStageOptions.value.map((o) => o.value));
|
||||
const firstStageIdx = items.findIndex((i) => enabled.has(i.value));
|
||||
if (firstStageIdx > 0) return items[firstStageIdx - 1].value;
|
||||
const nonStage = items.find((i) => !enabled.has(i.value));
|
||||
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?.statusAfter !== undefined && config.registryStage?.statusAfter !== null) {
|
||||
payload.statusAfter = config.registryStage.statusAfter;
|
||||
}
|
||||
if (config.registryStage?.targetStage !== undefined && config.registryStage?.targetStage !== null) {
|
||||
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 '';
|
||||
const baseWhere = `${linkCondition.targetField}=#{source.${linkCondition.sourceField}}`;
|
||||
|
||||
if (visualType === 'STATUS_MODIFY') {
|
||||
const s = statusConfig;
|
||||
if (!s.targetField || !s.newValue) return '';
|
||||
const sets = [`${s.targetField}='${s.newValue}'`];
|
||||
if (s.addUpdateTime) sets.push('update_time=NOW()');
|
||||
const conditions = [baseWhere];
|
||||
if (s.fromValue) conditions.push(`${s.targetField}='${s.fromValue}'`);
|
||||
return `UPDATE ${targetTable} SET ${sets.join(', ')} WHERE ${conditions.join(' AND ')}`;
|
||||
}
|
||||
|
||||
if (visualType === 'DATA_SYNC') {
|
||||
const filled = fieldMappings.filter((m) => m.targetField && m.sourceValue);
|
||||
if (!filled.length) return '';
|
||||
const sets = filled.map((m) => {
|
||||
if (m.sourceType === 'source_field') return `${m.targetField}=#{source.${m.sourceValue}}`;
|
||||
if (m.sourceType === 'expression') return `${m.targetField}=${m.sourceValue}`;
|
||||
return `${m.targetField}='${m.sourceValue}'`;
|
||||
});
|
||||
sets.push('update_time=NOW()');
|
||||
return `UPDATE ${targetTable} SET ${sets.join(', ')} WHERE ${baseWhere}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function selectVisualType(type: VisualConfig['visualType']) {
|
||||
vc.value.visualType = type;
|
||||
if ((type === 'REGISTRY_STAGE_SYNC' || type === 'REGISTRY_STAGE_REVERT') && !vc.value.registryStage) {
|
||||
vc.value.registryStage = defaultRegistryStage();
|
||||
}
|
||||
if (type === 'REGISTRY_STAGE_SYNC' && registryStageOptions.value.length && !vc.value.registryStage?.stage) {
|
||||
vc.value.registryStage!.stage = registryStageOptions.value[0].value;
|
||||
vc.value.registryStage!.expectedFrom = defaultExpectedFromForStage(vc.value.registryStage!.stage);
|
||||
vc.value.registryStage!.statusAfter = defaultStatusAfterForStage(vc.value.registryStage!.stage);
|
||||
}
|
||||
if (type === 'REGISTRY_STAGE_REVERT' && !vc.value.registryStage?.targetStage) {
|
||||
vc.value.registryStage!.targetStage = defaultRevertTargetStage();
|
||||
}
|
||||
}
|
||||
|
||||
function getDocLabel(doc: any): string {
|
||||
return doc?.displayName || doc?.docName || doc?.bizName || doc?.tableName || '';
|
||||
}
|
||||
|
||||
function filterBizDoc(input: string, option: any): boolean {
|
||||
return (option.value?.toLowerCase() || '').includes(input.toLowerCase());
|
||||
}
|
||||
|
||||
async function onTargetTableChange(tableName: string) {
|
||||
if (!tableName) { targetColumns.value = []; return; }
|
||||
loadingTargetCols.value = true;
|
||||
try {
|
||||
const cols = await getTableColumns(tableName);
|
||||
targetColumns.value = (cols as any) || [];
|
||||
const doc = bizDocList.value.find((d) => d.tableName === tableName);
|
||||
vc.value.targetTableLabel = getDocLabel(doc);
|
||||
// 列元数据就绪后重载字典(编辑场景:targetField 已存在但列尚未加载)
|
||||
await loadTargetStatusDict();
|
||||
} catch {
|
||||
targetColumns.value = [];
|
||||
targetStatusDictCode.value = '';
|
||||
targetStatusDictItems.value = [];
|
||||
} finally {
|
||||
loadingTargetCols.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function onTargetStatusFieldChange(field: string) {
|
||||
// 清空旧值,防止值不匹配新字典
|
||||
vc.value.statusConfig!.fromValue = '';
|
||||
vc.value.statusConfig!.newValue = '';
|
||||
await loadTargetStatusDict(field);
|
||||
}
|
||||
|
||||
async function onSrcConditionFieldChange(field: string) {
|
||||
if (!field) { srcConditionDictCode.value = ''; srcConditionDictItems.value = []; return; }
|
||||
vc.value.statusConfig!.srcConditionValue = '';
|
||||
}
|
||||
|
||||
function getMappingPlaceholder(sourceType: string): string {
|
||||
return sourceType === 'expression' ? 'NOW()、REPLACE(UUID(),\'-\',\'\') 等SQL表达式' : '固定值字符串';
|
||||
}
|
||||
|
||||
function addMappingRow() {
|
||||
if (!vc.value.fieldMappings) vc.value.fieldMappings = [];
|
||||
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!.statusAfter = defaultStatusAfterForStage(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') {
|
||||
if (!vc.value.registryStage?.stage) {
|
||||
createMessage.warning('请选择审批环节');
|
||||
return;
|
||||
}
|
||||
if (!vc.value.registryStage?.statusAfter) {
|
||||
createMessage.warning('请选择通过后状态');
|
||||
return;
|
||||
}
|
||||
emit('success', {
|
||||
...form.value,
|
||||
actionType: 'REGISTRY_STAGE_SYNC',
|
||||
sqlTemplate: null,
|
||||
actionConfig: serializeActionConfig(vc.value),
|
||||
});
|
||||
visible.value = false;
|
||||
return;
|
||||
}
|
||||
if (vc.value.visualType === 'REGISTRY_STAGE_REVERT') {
|
||||
if (vc.value.registryStage?.targetStage === undefined || vc.value.registryStage?.targetStage === null || vc.value.registryStage?.targetStage === '') {
|
||||
createMessage.warning('请选择回退目标(业务状态字典项)');
|
||||
return;
|
||||
}
|
||||
emit('success', {
|
||||
...form.value,
|
||||
actionType: 'REGISTRY_STAGE_REVERT',
|
||||
sqlTemplate: null,
|
||||
actionConfig: serializeActionConfig(vc.value),
|
||||
});
|
||||
visible.value = false;
|
||||
return;
|
||||
}
|
||||
if (!vc.value.targetTable) { createMessage.warning('请选择操作目标表'); return; }
|
||||
if (!vc.value.linkCondition.sourceField || !vc.value.linkCondition.targetField) { createMessage.warning('请配置关联条件'); return; }
|
||||
const sql = buildSql(vc.value);
|
||||
if (!sql) { createMessage.warning('配置不完整,请检查字段设置'); return; }
|
||||
emit('success', {
|
||||
...form.value,
|
||||
actionType: 'SQL_UPDATE',
|
||||
sqlTemplate: sql,
|
||||
actionConfig: serializeActionConfig(vc.value),
|
||||
});
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
async function open(opts: { sourceTable: string; sourceColumns: ColMeta[]; bizDocList: any[]; sourceRegistry?: any; action?: any; execOrder?: number }) {
|
||||
sourceTable.value = opts.sourceTable;
|
||||
sourceColumns.value = opts.sourceColumns || [];
|
||||
bizDocList.value = opts.bizDocList || [];
|
||||
sourceRegistry.value = opts.sourceRegistry || null;
|
||||
targetColumns.value = [];
|
||||
targetStatusDictCode.value = '';
|
||||
targetStatusDictItems.value = [];
|
||||
srcConditionDictCode.value = '';
|
||||
srcConditionDictItems.value = [];
|
||||
sourceStatusDictCode.value = '';
|
||||
sourceStatusDictItems.value = [];
|
||||
|
||||
if (opts.action) {
|
||||
isUpdate.value = true;
|
||||
const a = opts.action;
|
||||
form.value = {
|
||||
id: a.id || '',
|
||||
actionName: a.actionName || '',
|
||||
execOrder: a.execOrder ?? 0,
|
||||
onFail: a.onFail || 'stop',
|
||||
enabled: a.enabled !== false && a.enabled !== 0,
|
||||
};
|
||||
if (a.actionConfig) {
|
||||
try {
|
||||
const parsed = JSON.parse(a.actionConfig);
|
||||
vc.value = normalizeParsedConfig(parsed, a.actionType);
|
||||
if (vc.value.targetTable) {
|
||||
await onTargetTableChange(vc.value.targetTable);
|
||||
}
|
||||
} catch {
|
||||
vc.value = normalizeParsedConfig(null, a.actionType);
|
||||
}
|
||||
} else {
|
||||
vc.value = normalizeParsedConfig(null, a.actionType);
|
||||
}
|
||||
} else {
|
||||
isUpdate.value = false;
|
||||
form.value = { ...defaultForm(), execOrder: opts.execOrder ?? 0 };
|
||||
vc.value = defaultVc();
|
||||
}
|
||||
|
||||
await loadSourceStatusDict();
|
||||
// 旧数据兼容:未配置 statusAfter 时,若字典含环节码则回填,否则保持空由用户手选
|
||||
if (isUpdate.value && vc.value.registryStage?.stage && !vc.value.registryStage?.statusAfter) {
|
||||
const legacy = defaultStatusAfterForStage(vc.value.registryStage.stage);
|
||||
if (legacy) {
|
||||
vc.value.registryStage.statusAfter = legacy;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isUpdate.value) {
|
||||
if (registryStageOptions.value.length && !vc.value.registryStage?.stage) {
|
||||
vc.value.registryStage!.stage = registryStageOptions.value[0].value;
|
||||
vc.value.registryStage!.expectedFrom = defaultExpectedFromForStage(vc.value.registryStage!.stage);
|
||||
vc.value.registryStage!.statusAfter = defaultStatusAfterForStage(vc.value.registryStage!.stage);
|
||||
}
|
||||
if (!vc.value.registryStage?.targetStage) {
|
||||
vc.value.registryStage!.targetStage = defaultRevertTargetStage();
|
||||
}
|
||||
}
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
@@ -0,0 +1,24 @@
|
||||
import { BasicColumn } from '/@/components/Table';
|
||||
|
||||
/**
|
||||
* 按 sentinel key 分组的审批痕迹列。
|
||||
* key = 后端注入的字段名(traceProofreadBy / traceAuditBy / traceApproveBy)
|
||||
* 只要响应里出现该 key,就注入对应两列。
|
||||
*/
|
||||
export const traceColumnsByStage: Record<string, BasicColumn[]> = {
|
||||
traceProofreadBy: [
|
||||
{ title: '校对人', dataIndex: 'traceProofreadBy', width: 100, align: 'center', defaultHidden: true },
|
||||
{ title: '校对时间', dataIndex: 'traceProofreadTime', width: 165, align: 'center', defaultHidden: true },
|
||||
],
|
||||
traceAuditBy: [
|
||||
{ title: '审核人', dataIndex: 'traceAuditBy', width: 100, align: 'center', defaultHidden: true },
|
||||
{ title: '审核时间', dataIndex: 'traceAuditTime', width: 165, align: 'center', defaultHidden: true },
|
||||
],
|
||||
traceApproveBy: [
|
||||
{ title: '批准人', dataIndex: 'traceApproveBy', width: 100, align: 'center', defaultHidden: true },
|
||||
{ title: '批准时间', dataIndex: 'traceApproveTime', width: 165, align: 'center', defaultHidden: true },
|
||||
],
|
||||
};
|
||||
|
||||
/** 全量痕迹列(密炼PS等已知需要全部的场景直接引用) */
|
||||
export const traceColumns: BasicColumn[] = Object.values(traceColumnsByStage).flat();
|
||||
@@ -0,0 +1,82 @@
|
||||
import { queryByBiz } from './MesXslApprovalTrace.api';
|
||||
|
||||
/** 配合示方业务表(与审批注册中心 table_name 一致) */
|
||||
export const FORMULA_SPEC_BIZ_TABLE = 'mes_xsl_formula_spec';
|
||||
|
||||
/** 混炼示方业务表 */
|
||||
export const MIXING_SPEC_BIZ_TABLE = 'mes_xsl_mixing_spec';
|
||||
|
||||
const TRACE_FIELD_KEYS = [
|
||||
'traceProofreadBy',
|
||||
'traceProofreadTime',
|
||||
'traceAuditBy',
|
||||
'traceAuditTime',
|
||||
'traceApproveBy',
|
||||
'traceApproveTime',
|
||||
] as const;
|
||||
|
||||
/** 从列表/详情记录中提取已注入的痕迹字段 */
|
||||
export function pickTraceFields(record?: Recordable | null): Recordable {
|
||||
const out: Recordable = {};
|
||||
if (!record) return out;
|
||||
for (const key of TRACE_FIELD_KEYS) {
|
||||
if (record[key] != null && record[key] !== '') {
|
||||
out[key] = record[key];
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 将痕迹表实体字段映射为列表注入格式 traceProofreadBy 等 */
|
||||
export function applyTraceEntityToRecord(record: Recordable, trace?: Recordable | null): Recordable {
|
||||
if (!record) return record;
|
||||
const merged = { ...record, ...pickTraceFields(record) };
|
||||
if (!trace) return merged;
|
||||
return {
|
||||
...merged,
|
||||
traceProofreadBy: trace.proofreadBy ?? merged.traceProofreadBy,
|
||||
traceProofreadTime: trace.proofreadTime ?? merged.traceProofreadTime,
|
||||
traceAuditBy: trace.auditBy ?? merged.traceAuditBy,
|
||||
traceAuditTime: trace.auditTime ?? merged.traceAuditTime,
|
||||
traceApproveBy: trace.approveBy ?? merged.traceApproveBy,
|
||||
traceApproveTime: trace.approveTime ?? merged.traceApproveTime,
|
||||
};
|
||||
}
|
||||
|
||||
export function hasTraceWorkflowInfo(record?: Recordable | null): boolean {
|
||||
return !!(
|
||||
record?.traceProofreadBy ||
|
||||
record?.traceProofreadTime ||
|
||||
record?.traceAuditBy ||
|
||||
record?.traceAuditTime ||
|
||||
record?.traceApproveBy ||
|
||||
record?.traceApproveTime
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeApiRecord(mainRaw: unknown): Recordable {
|
||||
const raw = mainRaw as Recordable;
|
||||
if (raw?.id != null) return raw;
|
||||
return (raw as any)?.result ?? raw ?? {};
|
||||
}
|
||||
|
||||
/** 加载业务主表并合并痕迹(列表注入 / queryById 增强 / queryByBiz 兜底) */
|
||||
export async function loadRecordWithTrace(
|
||||
id: string,
|
||||
bizTable: string,
|
||||
fetchById: (params: { id: string }) => Promise<unknown>,
|
||||
listRecord?: Recordable,
|
||||
): Promise<Recordable> {
|
||||
const mainRaw = await fetchById({ id });
|
||||
let record = normalizeApiRecord(mainRaw);
|
||||
record = applyTraceEntityToRecord(record, pickTraceFields(listRecord));
|
||||
if (!hasTraceWorkflowInfo(record)) {
|
||||
try {
|
||||
const trace = await queryByBiz({ bizTable, bizDataId: id });
|
||||
record = applyTraceEntityToRecord(record, trace);
|
||||
} catch {
|
||||
// 无痕迹或无权查询时忽略
|
||||
}
|
||||
}
|
||||
return record;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useTable } from '/@/components/Table';
|
||||
import type { BasicTableProps } from '/@/components/Table';
|
||||
import { traceColumns } from './traceColumns';
|
||||
|
||||
/**
|
||||
* 替换 useTable(不经过 useListPage 的特殊场景):自动追加审批痕迹列(默认隐藏)。
|
||||
* 普通列表页已由 useListPage 统一注入,无需使用本函数。
|
||||
*/
|
||||
export function useTraceTable(tableProps: BasicTableProps) {
|
||||
const columns = tableProps.columns as any[] | undefined;
|
||||
const alreadyHasTrace = columns?.some((c) => c.dataIndex === 'traceProofreadBy');
|
||||
return useTable({
|
||||
...tableProps,
|
||||
columns: alreadyHasTrace ? columns : [...(columns ?? []), ...traceColumns],
|
||||
});
|
||||
}
|
||||
@@ -19,6 +19,16 @@
|
||||
onClick: handleDetail.bind(null, record),
|
||||
auth: 'xslmes:mes_xsl_approval_record:list',
|
||||
},
|
||||
{
|
||||
label: '补发IM卡片',
|
||||
color: 'warning',
|
||||
ifShow: record.status === '0' && record.channel === 'MES' && !!record.externalInstanceId,
|
||||
popConfirm: {
|
||||
title: '向当前处理人重新发送 IM 审批卡片?',
|
||||
confirm: handleResendCard.bind(null, record),
|
||||
},
|
||||
auth: 'xslmes:mes_xsl_approval_record:list',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
@@ -34,7 +44,10 @@
|
||||
import MesXslApprovalRecordDetailModal from './components/MesXslApprovalRecordDetailModal.vue';
|
||||
import { columns, searchFormSchema } from './MesXslApprovalRecord.data';
|
||||
import { list, getExportUrl } from './MesXslApprovalRecord.api';
|
||||
import { resendApprovalCard } from '/@/views/approval/flow/approvalHandle.api';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const [registerModal, { openModal }] = useModal();
|
||||
|
||||
const { tableContext, onExportXls } = useListPage({
|
||||
@@ -50,7 +63,7 @@
|
||||
showAdvancedButton: true,
|
||||
},
|
||||
actionColumn: {
|
||||
width: 100,
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
},
|
||||
},
|
||||
@@ -65,4 +78,13 @@
|
||||
function handleDetail(record: Recordable) {
|
||||
openModal(true, { record });
|
||||
}
|
||||
|
||||
async function handleResendCard(record: Recordable) {
|
||||
try {
|
||||
const msg = await resendApprovalCard({ instanceId: record.externalInstanceId });
|
||||
createMessage.success(msg || '补发成功');
|
||||
} catch (e: any) {
|
||||
createMessage.error(e?.message || '补发失败');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
|
||||
const { createConfirm } = useMessage();
|
||||
|
||||
enum Api {
|
||||
list = '/xslmes/mesXslDingCallbackLog/list',
|
||||
save = '/xslmes/mesXslDingCallbackLog/add',
|
||||
edit = '/xslmes/mesXslDingCallbackLog/edit',
|
||||
deleteOne = '/xslmes/mesXslDingCallbackLog/delete',
|
||||
deleteBatch = '/xslmes/mesXslDingCallbackLog/deleteBatch',
|
||||
exportXlsUrl = '/xslmes/mesXslDingCallbackLog/exportXls',
|
||||
importExcelUrl = '/xslmes/mesXslDingCallbackLog/importExcel',
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表分页查询
|
||||
*/
|
||||
export const list = (params) => defHttp.get({ url: Api.list, params });
|
||||
|
||||
/**
|
||||
* 删除单条
|
||||
*/
|
||||
export const deleteOne = (params, handleSuccess) => {
|
||||
return defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => {
|
||||
handleSuccess();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
*/
|
||||
export const batchDelete = (params, handleSuccess) => {
|
||||
createConfirm({
|
||||
iconType: 'warning',
|
||||
title: '确认删除',
|
||||
content: '是否删除选中数据',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => {
|
||||
handleSuccess();
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存或新增
|
||||
*/
|
||||
export const saveOrUpdate = (params, isUpdate) => {
|
||||
const url = isUpdate ? Api.edit : Api.save;
|
||||
return defHttp.post({ url, params });
|
||||
};
|
||||
|
||||
/**
|
||||
* 导出 XLS
|
||||
*/
|
||||
export const getExportUrl = Api.exportXlsUrl;
|
||||
|
||||
/**
|
||||
* 导入 XLS
|
||||
*/
|
||||
export const getImportUrl = Api.importExcelUrl;
|
||||
@@ -0,0 +1,158 @@
|
||||
import { BasicColumn } from '/@/components/Table';
|
||||
import { FormSchema } from '/@/components/Form';
|
||||
import { rules } from '/@/utils/helper/validator';
|
||||
|
||||
export const columns: BasicColumn[] = [
|
||||
{
|
||||
title: '钉钉事件ID',
|
||||
align: 'center',
|
||||
dataIndex: 'eventId',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '事件类型',
|
||||
align: 'center',
|
||||
dataIndex: 'eventType',
|
||||
width: 160,
|
||||
},
|
||||
{
|
||||
title: '审批实例ID',
|
||||
align: 'center',
|
||||
dataIndex: 'processInstanceId',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '接收时间',
|
||||
align: 'center',
|
||||
dataIndex: 'receivedTime',
|
||||
width: 160,
|
||||
},
|
||||
{
|
||||
title: '是否已处理',
|
||||
align: 'center',
|
||||
dataIndex: 'processed_dictText',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '关联业务表',
|
||||
align: 'center',
|
||||
dataIndex: 'bizTable',
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: '关联业务记录ID',
|
||||
align: 'center',
|
||||
dataIndex: 'bizDataId',
|
||||
width: 160,
|
||||
},
|
||||
{
|
||||
title: '关联审批台账ID',
|
||||
align: 'center',
|
||||
dataIndex: 'recordId',
|
||||
width: 160,
|
||||
},
|
||||
];
|
||||
|
||||
export const searchFormSchema: FormSchema[] = [
|
||||
{
|
||||
label: '事件类型',
|
||||
field: 'eventType',
|
||||
component: 'Input',
|
||||
colProps: { span: 6 },
|
||||
},
|
||||
{
|
||||
label: '审批实例ID',
|
||||
field: 'processInstanceId',
|
||||
component: 'Input',
|
||||
colProps: { span: 6 },
|
||||
},
|
||||
{
|
||||
label: '是否已处理',
|
||||
field: 'processed',
|
||||
component: 'JDictSelectTag',
|
||||
componentProps: { dictCode: 'yn' },
|
||||
colProps: { span: 6 },
|
||||
},
|
||||
{
|
||||
label: '接收时间',
|
||||
field: 'receivedTime',
|
||||
component: 'RangePicker',
|
||||
componentProps: { valueType: 'Date', showTime: true, format: 'YYYY-MM-DD HH:mm:ss' },
|
||||
colProps: { span: 12 },
|
||||
},
|
||||
{
|
||||
label: '关联业务表',
|
||||
field: 'bizTable',
|
||||
component: 'Input',
|
||||
colProps: { span: 6 },
|
||||
},
|
||||
];
|
||||
|
||||
export const formSchema: FormSchema[] = [
|
||||
{ label: '', field: 'id', component: 'Input', show: false },
|
||||
{
|
||||
label: '钉钉事件ID',
|
||||
field: 'eventId',
|
||||
component: 'Input',
|
||||
colProps: { span: 12 },
|
||||
},
|
||||
{
|
||||
label: '事件类型',
|
||||
field: 'eventType',
|
||||
component: 'Input',
|
||||
componentProps: { placeholder: '如 bpms_instance_change' },
|
||||
colProps: { span: 12 },
|
||||
},
|
||||
{
|
||||
label: '审批实例ID',
|
||||
field: 'processInstanceId',
|
||||
component: 'Input',
|
||||
colProps: { span: 12 },
|
||||
},
|
||||
{
|
||||
label: '接收时间',
|
||||
field: 'receivedTime',
|
||||
component: 'DatePicker',
|
||||
componentProps: { showTime: true, format: 'YYYY-MM-DD HH:mm:ss', valueFormat: 'YYYY-MM-DD HH:mm:ss', style: { width: '100%' } },
|
||||
colProps: { span: 12 },
|
||||
},
|
||||
{
|
||||
label: '是否已处理',
|
||||
field: 'processed',
|
||||
component: 'JDictSelectTag',
|
||||
componentProps: { dictCode: 'yn', placeholder: '请选择' },
|
||||
colProps: { span: 12 },
|
||||
},
|
||||
{
|
||||
label: '关联业务表',
|
||||
field: 'bizTable',
|
||||
component: 'Input',
|
||||
colProps: { span: 12 },
|
||||
},
|
||||
{
|
||||
label: '关联业务记录ID',
|
||||
field: 'bizDataId',
|
||||
component: 'Input',
|
||||
colProps: { span: 12 },
|
||||
},
|
||||
{
|
||||
label: '关联审批台账ID',
|
||||
field: 'recordId',
|
||||
component: 'Input',
|
||||
colProps: { span: 12 },
|
||||
},
|
||||
{
|
||||
label: '原始推送数据',
|
||||
field: 'rawData',
|
||||
component: 'InputTextArea',
|
||||
componentProps: { rows: 6, placeholder: 'JSON 原始推送内容' },
|
||||
colProps: { span: 24 },
|
||||
},
|
||||
{
|
||||
label: '处理备注',
|
||||
field: 'processRemark',
|
||||
component: 'InputTextArea',
|
||||
componentProps: { rows: 3, placeholder: '处理结果或失败原因' },
|
||||
colProps: { span: 24 },
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div>
|
||||
<BasicTable @register="registerTable" :rowSelection="rowSelection">
|
||||
<template #tableTitle>
|
||||
<a-button type="primary" @click="handleAdd" preIcon="ant-design:plus-outlined">新增</a-button>
|
||||
<a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls">导出</a-button>
|
||||
<j-upload-button type="primary" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
|
||||
<a-button type="primary" danger preIcon="ant-design:delete-outlined" @click="batchHandleDelete">批量删除</a-button>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
|
||||
</template>
|
||||
</BasicTable>
|
||||
<MesXslDingCallbackLogModal @register="registerModal" @success="handleSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="xslmes-mesXslDingCallbackLog" setup>
|
||||
import { BasicTable, useTable, TableAction } from '/@/components/Table';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { useListPage } from '/@/hooks/system/useListPage';
|
||||
import MesXslDingCallbackLogModal from './components/MesXslDingCallbackLogModal.vue';
|
||||
import { columns, searchFormSchema } from './MesXslDingCallbackLog.data';
|
||||
import { list, deleteOne, batchDelete, getExportUrl, getImportUrl } from './MesXslDingCallbackLog.api';
|
||||
|
||||
const [registerModal, { openModal }] = useModal();
|
||||
|
||||
const { tableContext, onExportXls, onImportXls } = useListPage({
|
||||
tableProps: {
|
||||
title: '钉钉回调日志',
|
||||
api: list,
|
||||
columns,
|
||||
canResize: false,
|
||||
formConfig: {
|
||||
labelWidth: 90,
|
||||
schemas: searchFormSchema,
|
||||
autoSubmitOnEnter: true,
|
||||
showAdvancedButton: true,
|
||||
fieldMapToTime: [['receivedTime', ['receivedTime_begin', 'receivedTime_end'], 'YYYY-MM-DD HH:mm:ss']],
|
||||
},
|
||||
actionColumn: {
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
},
|
||||
},
|
||||
exportConfig: {
|
||||
name: '钉钉回调日志',
|
||||
url: getExportUrl,
|
||||
},
|
||||
importConfig: {
|
||||
url: getImportUrl,
|
||||
success: handleSuccess,
|
||||
},
|
||||
});
|
||||
|
||||
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
|
||||
|
||||
function handleAdd() {
|
||||
openModal(true, { isUpdate: false, showFooter: true });
|
||||
}
|
||||
|
||||
function handleEdit(record: Recordable) {
|
||||
openModal(true, { record, isUpdate: true, showFooter: true });
|
||||
}
|
||||
|
||||
function handleDetail(record: Recordable) {
|
||||
openModal(true, { record, isUpdate: true, showFooter: false });
|
||||
}
|
||||
|
||||
function handleDelete(record: Recordable) {
|
||||
deleteOne({ id: record.id }, handleSuccess);
|
||||
}
|
||||
|
||||
function batchHandleDelete() {
|
||||
batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
reload();
|
||||
}
|
||||
|
||||
function getTableAction(record: Recordable) {
|
||||
return [
|
||||
{
|
||||
label: '编辑',
|
||||
onClick: handleEdit.bind(null, record),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getDropDownAction(record: Recordable) {
|
||||
return [
|
||||
{
|
||||
label: '详情',
|
||||
onClick: handleDetail.bind(null, record),
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
popConfirm: {
|
||||
title: '是否确认删除',
|
||||
confirm: handleDelete.bind(null, record),
|
||||
placement: 'topLeft',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" @register="registerModal" :title="title" :width="1000" @ok="handleSubmit">
|
||||
<BasicForm @register="registerForm" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, unref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { BasicForm, useForm } from '/@/components/Form';
|
||||
import { formSchema } from '../MesXslDingCallbackLog.data';
|
||||
import { saveOrUpdate } from '../MesXslDingCallbackLog.api';
|
||||
|
||||
const emit = defineEmits(['success', 'register']);
|
||||
|
||||
const isUpdate = ref(true);
|
||||
const isDetail = ref(false);
|
||||
|
||||
const [registerForm, { setProps, resetFields, setFieldsValue, validate }] = useForm({
|
||||
labelWidth: 110,
|
||||
schemas: formSchema,
|
||||
showActionButtonGroup: false,
|
||||
baseColProps: { span: 12 },
|
||||
});
|
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
resetFields();
|
||||
setModalProps({ confirmLoading: false, showOkBtn: !!data?.showFooter });
|
||||
isUpdate.value = !!data?.isUpdate;
|
||||
isDetail.value = !data?.showFooter;
|
||||
|
||||
setProps({ disabled: isDetail.value });
|
||||
|
||||
if (unref(isUpdate) && data?.record) {
|
||||
setFieldsValue({ ...data.record });
|
||||
}
|
||||
});
|
||||
|
||||
const title = computed(() => (!unref(isUpdate) ? '新增' : unref(isDetail) ? '详情' : '编辑'));
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const values = await validate();
|
||||
setModalProps({ confirmLoading: true });
|
||||
await saveOrUpdate(values, unref(isUpdate));
|
||||
closeModal();
|
||||
emit('success');
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -31,7 +31,14 @@ export const saveBind = (data: {
|
||||
}) => defHttp.post({ url: `${BASE}/save`, data });
|
||||
|
||||
export const deleteBind = (id: string) =>
|
||||
defHttp.delete({ url: `${BASE}/delete`, params: { id } });
|
||||
defHttp.delete({ url: `${BASE}/delete`, params: { id } }, { joinParamsToUrl: true });
|
||||
|
||||
/** 批量解析绑定字段取值(字典/表字典显示文本) */
|
||||
export const resolveFieldValues = (data: {
|
||||
bizCode: string;
|
||||
rowData: Record<string, any>;
|
||||
items: { mapKey: string; bizField: string; valueMode: string }[];
|
||||
}) => defHttp.post<Record<string, any>>({ url: `${BASE}/resolveFieldValues`, data });
|
||||
|
||||
/** 复用现有接口:拉取钉钉模板表单字段(含 dingFields) */
|
||||
export const getTemplateDetail = (id: string) =>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/** 审批模板绑定字段取值:原值 / 显示文本 */
|
||||
|
||||
export type ValueMode = 'raw' | 'text';
|
||||
|
||||
export interface FieldTranslateMeta {
|
||||
fieldKey: string;
|
||||
label?: string;
|
||||
translateKind?: string;
|
||||
dictCode?: string;
|
||||
dictTable?: string;
|
||||
dictText?: string;
|
||||
dictCodeField?: string;
|
||||
}
|
||||
|
||||
export const VALUE_MODE_OPTIONS = [
|
||||
{ label: '原值(ID/Code)', value: 'raw' as ValueMode },
|
||||
{ label: '显示文本', value: 'text' as ValueMode },
|
||||
];
|
||||
|
||||
const DD_SELECT_TYPES = new Set([
|
||||
'DDSelectField',
|
||||
'DDMultiSelectField',
|
||||
'DepartmentField',
|
||||
'InnerContactField',
|
||||
]);
|
||||
|
||||
export function isTranslatableMeta(meta?: FieldTranslateMeta | null): boolean {
|
||||
return !!meta?.translateKind && meta.translateKind !== 'NONE';
|
||||
}
|
||||
|
||||
export function getNestedValue(obj: any, path: string): any {
|
||||
if (!obj || !path) return undefined;
|
||||
return path.split('.').reduce((acc: any, k: string) => acc?.[k], obj);
|
||||
}
|
||||
|
||||
export function getDictTextFromRow(rowData: any, bizField: string): any {
|
||||
if (!rowData || !bizField) return undefined;
|
||||
const parts = bizField.split('.');
|
||||
if (parts.length === 1) {
|
||||
return rowData[`${parts[0]}_dictText`];
|
||||
}
|
||||
const parentPath = parts.slice(0, -1).join('.');
|
||||
const leaf = parts[parts.length - 1];
|
||||
const parent = getNestedValue(rowData, parentPath);
|
||||
if (parent && typeof parent === 'object') {
|
||||
return parent[`${leaf}_dictText`];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** 前端本地解析(优先 _dictText) */
|
||||
export function resolveFieldValueLocal(
|
||||
rowData: any,
|
||||
bizField: string,
|
||||
valueMode: ValueMode = 'raw',
|
||||
): any {
|
||||
const raw = getNestedValue(rowData, bizField);
|
||||
if (valueMode !== 'text') return raw;
|
||||
const textVal = getDictTextFromRow(rowData, bizField);
|
||||
if (textVal !== undefined && textVal !== null && textVal !== '') {
|
||||
return textVal;
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
/** 绑定页默认取值方式:下拉类控件用原值,其余字典字段用显示文本 */
|
||||
export function defaultValueMode(
|
||||
componentName: string,
|
||||
meta?: FieldTranslateMeta | null,
|
||||
): ValueMode {
|
||||
if (!isTranslatableMeta(meta)) return 'raw';
|
||||
return DD_SELECT_TYPES.has(componentName) ? 'raw' : 'text';
|
||||
}
|
||||
@@ -138,7 +138,7 @@
|
||||
type="info"
|
||||
show-icon
|
||||
style="margin-bottom:16px"
|
||||
message="绑定说明:将钉钉审批模板的表单控件与实体字段一一对应,系统自动发起审批时会从业务数据中读取字段值填入表单。TextNote 说明文字类控件无需绑定。明细表(TableField)需先指定对应的业务明细集合,再绑定各列字段。"
|
||||
message="绑定说明:将钉钉审批模板的表单控件与实体字段一一对应。带 @Dict 或表字典的字段可配置「原值/显示文本」:下拉类控件建议原值,文本类控件建议显示文本。明细表需先指定业务明细集合再绑定各列。"
|
||||
/>
|
||||
|
||||
<!-- ① 主表字段 -->
|
||||
@@ -169,8 +169,19 @@
|
||||
option-filter-prop="label"
|
||||
allow-clear
|
||||
size="small"
|
||||
@change="() => onMainBizFieldChange(record)"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="column.key === 'valueMode'">
|
||||
<a-select
|
||||
v-if="record.bizField && isTranslatableMainField(record.bizField)"
|
||||
v-model:value="record.valueMode"
|
||||
style="width:100%"
|
||||
:options="VALUE_MODE_OPTIONS"
|
||||
size="small"
|
||||
/>
|
||||
<span v-else class="dtb-value-mode-hint">—</span>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
@@ -229,8 +240,19 @@
|
||||
option-filter-prop="label"
|
||||
allow-clear
|
||||
size="small"
|
||||
@change="() => onDetailBizFieldChange(record, tf.componentId)"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="column.key === 'valueMode'">
|
||||
<a-select
|
||||
v-if="record.bizField && isTranslatableDetailField(record.bizField, tf.componentId)"
|
||||
v-model:value="record.valueMode"
|
||||
style="width:100%"
|
||||
:options="VALUE_MODE_OPTIONS"
|
||||
size="small"
|
||||
/>
|
||||
<span v-else class="dtb-value-mode-hint">—</span>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-empty v-else description="该明细表无子控件" class="dtb-empty-sm" />
|
||||
@@ -288,6 +310,13 @@
|
||||
import { ref, computed, reactive, onMounted } from 'vue';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import * as Api from './dingTplBind.api';
|
||||
import {
|
||||
type FieldTranslateMeta,
|
||||
type ValueMode,
|
||||
VALUE_MODE_OPTIONS,
|
||||
defaultValueMode,
|
||||
isTranslatableMeta,
|
||||
} from './dingTplFieldValue';
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
@@ -317,6 +346,7 @@
|
||||
componentName: string;
|
||||
parentId?: string;
|
||||
bizField?: string;
|
||||
valueMode?: ValueMode;
|
||||
children?: FieldRow[]; // 仅 TableField 使用
|
||||
}
|
||||
|
||||
@@ -349,14 +379,14 @@
|
||||
const mainFieldRows = ref<FieldRow[]>([]);
|
||||
const tableFieldRows = ref<FieldRow[]>([]);
|
||||
|
||||
const bizFields = ref<{ fieldKey: string; label: string }[]>([]);
|
||||
const bizFields = ref<FieldTranslateMeta[]>([]);
|
||||
const bizFieldsLoading = ref(false);
|
||||
|
||||
const detailSlots = ref<DetailSlot[]>([]);
|
||||
const detailSlotsLoading = ref(false);
|
||||
|
||||
// key = tableField's componentId, value = loaded detail field options
|
||||
const detailFieldsMap = reactive<Record<string, { fieldKey: string; label: string }[]>>({});
|
||||
const detailFieldsMap = reactive<Record<string, FieldTranslateMeta[]>>({});
|
||||
const detailFieldsLoadingMap = reactive<Record<string, boolean>>({});
|
||||
|
||||
const saving = ref(false);
|
||||
@@ -404,15 +434,17 @@
|
||||
// ══ 表格列定义 ══
|
||||
|
||||
const mainColumns = [
|
||||
{ title: '控件类型', key: 'componentType', width: 140 },
|
||||
{ title: '钉钉字段名', dataIndex: 'componentLabel', width: 160 },
|
||||
{ title: '控件类型', key: 'componentType', width: 130 },
|
||||
{ title: '钉钉字段名', dataIndex: 'componentLabel', width: 140 },
|
||||
{ title: '绑定实体字段', key: 'bizField' },
|
||||
{ title: '取值方式', key: 'valueMode', width: 140 },
|
||||
];
|
||||
|
||||
const detailColumns = [
|
||||
{ title: '控件类型', key: 'componentType', width: 140 },
|
||||
{ title: '字段名', dataIndex: 'componentLabel', width: 160 },
|
||||
{ title: '控件类型', key: 'componentType', width: 130 },
|
||||
{ title: '字段名', dataIndex: 'componentLabel', width: 140 },
|
||||
{ title: '绑定明细字段', key: 'bizField' },
|
||||
{ title: '取值方式', key: 'valueMode', width: 140 },
|
||||
];
|
||||
|
||||
// ══ 菜单树操作 ══
|
||||
@@ -482,7 +514,7 @@
|
||||
bizFieldsLoading.value = true;
|
||||
try {
|
||||
const list = await Api.getBizFields(bizCode);
|
||||
bizFields.value = (list || []) as { fieldKey: string; label: string }[];
|
||||
bizFields.value = (list || []) as FieldTranslateMeta[];
|
||||
} catch {
|
||||
bizFields.value = [];
|
||||
} finally {
|
||||
@@ -551,6 +583,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
interface SavedMappingEntry {
|
||||
bizField?: string;
|
||||
valueMode?: ValueMode;
|
||||
}
|
||||
|
||||
/** 从 dingFields 构建 mainFieldRows / tableFieldRows */
|
||||
function buildFieldRows(fields: DingField[], savedMappingJson: string | null) {
|
||||
const savedMap = parseSavedMapping(savedMappingJson);
|
||||
@@ -563,30 +600,46 @@
|
||||
const cid = f.id || f.label;
|
||||
|
||||
if (f.componentName === 'TableField') {
|
||||
const saved = savedMap.get(cid);
|
||||
const tfRow: FieldRow = {
|
||||
componentId: cid,
|
||||
componentLabel: f.label,
|
||||
componentName: f.componentName,
|
||||
bizField: savedMap.get(cid),
|
||||
bizField: saved?.bizField,
|
||||
children: (f.children || []).map((child) => {
|
||||
const childCid = `${cid}.${child.id || child.label}`;
|
||||
return {
|
||||
const childSaved = savedMap.get(childCid);
|
||||
const row: FieldRow = {
|
||||
componentId: childCid,
|
||||
componentLabel: child.label,
|
||||
componentName: child.componentName,
|
||||
parentId: cid,
|
||||
bizField: savedMap.get(childCid),
|
||||
} as FieldRow;
|
||||
bizField: childSaved?.bizField,
|
||||
valueMode: childSaved?.valueMode,
|
||||
};
|
||||
if (row.bizField && !row.valueMode) {
|
||||
row.valueMode = defaultValueMode(
|
||||
row.componentName,
|
||||
findDetailFieldMeta(row.bizField, cid),
|
||||
);
|
||||
}
|
||||
return row;
|
||||
}),
|
||||
};
|
||||
tables.push(tfRow);
|
||||
} else {
|
||||
mains.push({
|
||||
const saved = savedMap.get(cid);
|
||||
const row: FieldRow = {
|
||||
componentId: cid,
|
||||
componentLabel: f.label,
|
||||
componentName: f.componentName,
|
||||
bizField: savedMap.get(cid),
|
||||
});
|
||||
bizField: saved?.bizField,
|
||||
valueMode: saved?.valueMode,
|
||||
};
|
||||
if (row.bizField && !row.valueMode) {
|
||||
row.valueMode = defaultValueMode(row.componentName, findMainFieldMeta(row.bizField));
|
||||
}
|
||||
mains.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,14 +647,23 @@
|
||||
tableFieldRows.value = tables;
|
||||
}
|
||||
|
||||
/** 解析已保存的 fieldMappingJson → Map<componentId, bizField> */
|
||||
function parseSavedMapping(json: string | null): Map<string, string | undefined> {
|
||||
const m = new Map<string, string | undefined>();
|
||||
/** 解析已保存的 fieldMappingJson */
|
||||
function parseSavedMapping(json: string | null): Map<string, SavedMappingEntry> {
|
||||
const m = new Map<string, SavedMappingEntry>();
|
||||
if (!json) return m;
|
||||
try {
|
||||
const arr = JSON.parse(json) as { componentId: string; bizField?: string }[];
|
||||
const arr = JSON.parse(json) as {
|
||||
componentId: string;
|
||||
bizField?: string;
|
||||
valueMode?: ValueMode;
|
||||
}[];
|
||||
for (const item of arr) {
|
||||
if (item.componentId) m.set(item.componentId, item.bizField || undefined);
|
||||
if (item.componentId) {
|
||||
m.set(item.componentId, {
|
||||
bizField: item.bizField || undefined,
|
||||
valueMode: item.valueMode,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* 解析失败忽略 */
|
||||
@@ -638,7 +700,7 @@
|
||||
detailFieldsLoadingMap[tableComponentId] = true;
|
||||
try {
|
||||
const list = await Api.getDetailFields(selectedBizCode.value, slotName, kind);
|
||||
detailFieldsMap[tableComponentId] = (list || []) as { fieldKey: string; label: string }[];
|
||||
detailFieldsMap[tableComponentId] = (list || []) as FieldTranslateMeta[];
|
||||
} catch {
|
||||
detailFieldsMap[tableComponentId] = [];
|
||||
} finally {
|
||||
@@ -653,6 +715,43 @@
|
||||
}));
|
||||
}
|
||||
|
||||
function findMainFieldMeta(fieldKey?: string): FieldTranslateMeta | undefined {
|
||||
if (!fieldKey) return undefined;
|
||||
return bizFields.value.find((f) => f.fieldKey === fieldKey);
|
||||
}
|
||||
|
||||
function findDetailFieldMeta(fieldKey?: string, tableId?: string): FieldTranslateMeta | undefined {
|
||||
if (!fieldKey || !tableId) return undefined;
|
||||
return (detailFieldsMap[tableId] || []).find((f) => f.fieldKey === fieldKey);
|
||||
}
|
||||
|
||||
function isTranslatableMainField(fieldKey?: string): boolean {
|
||||
return isTranslatableMeta(findMainFieldMeta(fieldKey));
|
||||
}
|
||||
|
||||
function isTranslatableDetailField(fieldKey?: string, tableId?: string): boolean {
|
||||
return isTranslatableMeta(findDetailFieldMeta(fieldKey, tableId));
|
||||
}
|
||||
|
||||
function onMainBizFieldChange(record: FieldRow) {
|
||||
if (!record.bizField) {
|
||||
record.valueMode = undefined;
|
||||
return;
|
||||
}
|
||||
record.valueMode = defaultValueMode(record.componentName, findMainFieldMeta(record.bizField));
|
||||
}
|
||||
|
||||
function onDetailBizFieldChange(record: FieldRow, tableId: string) {
|
||||
if (!record.bizField) {
|
||||
record.valueMode = undefined;
|
||||
return;
|
||||
}
|
||||
record.valueMode = defaultValueMode(
|
||||
record.componentName,
|
||||
findDetailFieldMeta(record.bizField, tableId),
|
||||
);
|
||||
}
|
||||
|
||||
// ══ 自动匹配 ══
|
||||
|
||||
function autoMatchFields() {
|
||||
@@ -727,15 +826,20 @@
|
||||
componentName: string;
|
||||
parentId?: string;
|
||||
bizField?: string;
|
||||
valueMode?: ValueMode;
|
||||
}[] = [];
|
||||
|
||||
for (const row of mainFieldRows.value) {
|
||||
items.push({
|
||||
const item: (typeof items)[0] = {
|
||||
componentId: row.componentId,
|
||||
componentLabel: row.componentLabel,
|
||||
componentName: row.componentName,
|
||||
bizField: row.bizField || '',
|
||||
});
|
||||
};
|
||||
if (row.bizField && isTranslatableMainField(row.bizField) && row.valueMode) {
|
||||
item.valueMode = row.valueMode;
|
||||
}
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
for (const tf of tableFieldRows.value) {
|
||||
@@ -746,13 +850,21 @@
|
||||
bizField: tf.bizField || '',
|
||||
});
|
||||
for (const child of tf.children || []) {
|
||||
items.push({
|
||||
const childItem: (typeof items)[0] = {
|
||||
componentId: child.componentId,
|
||||
componentLabel: child.componentLabel,
|
||||
componentName: child.componentName,
|
||||
parentId: tf.componentId,
|
||||
bizField: child.bizField || '',
|
||||
});
|
||||
};
|
||||
if (
|
||||
child.bizField &&
|
||||
isTranslatableDetailField(child.bizField, tf.componentId) &&
|
||||
child.valueMode
|
||||
) {
|
||||
childItem.valueMode = child.valueMode;
|
||||
}
|
||||
items.push(childItem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1063,7 +1175,12 @@
|
||||
}
|
||||
|
||||
.dtb-bind-table {
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.dtb-value-mode-hint {
|
||||
color: #bbb;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dtb-bind-table :deep(.ant-table-cell) {
|
||||
|
||||
@@ -22,6 +22,7 @@ enum Api {
|
||||
bindFlow = '/xslmes/mesXslDingProcessTpl/bindFlow',
|
||||
approvalFlowList = '/xslmes/approvalFlow/list',
|
||||
previewFlowApprovers = '/xslmes/mesXslDingProcessTpl/previewFlowApprovers',
|
||||
toggleStatus = '/xslmes/mesXslDingProcessTpl/toggleStatus',
|
||||
}
|
||||
|
||||
export const getExportUrl = Api.exportXls;
|
||||
@@ -45,7 +46,7 @@ export const batchDelete = (params, handleSuccess) => {
|
||||
};
|
||||
|
||||
export const saveOrUpdate = (params, isUpdate) =>
|
||||
defHttp.post({ url: isUpdate ? Api.edit : Api.save, params }, { successMessageMode: 'none' });
|
||||
defHttp.post({ url: isUpdate ? Api.edit : Api.save, params }, { successMessageMode: isUpdate ? 'message' : 'none' });
|
||||
|
||||
/** 新增审批模板草稿(返回含 id 的完整记录) */
|
||||
export const addNewTemplate = (params) =>
|
||||
@@ -81,3 +82,7 @@ export const getApprovalFlowList = (params?) =>
|
||||
|
||||
export const previewFlowApprovers = (flowId: string) =>
|
||||
defHttp.get({ url: Api.previewFlowApprovers, params: { flowId } }, { successMessageMode: 'none' });
|
||||
|
||||
/** 切换模板启用/停用(停用后绑定的业务页不再显示钉钉审批按钮) */
|
||||
export const toggleTplStatus = (id: string) =>
|
||||
defHttp.post({ url: Api.toggleStatus, params: { id } }, { joinParamsToUrl: true });
|
||||
|
||||
@@ -105,6 +105,10 @@
|
||||
<DingApprovalLaunchModal ref="launchModalRef" @success="handleLaunchSuccess" />
|
||||
<!--update-end---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】手动填表发起钉钉审批-->
|
||||
|
||||
<!--update-begin---author:GHT ---date:20260610 for:【MESToDing审批配置】操作列绑定审批流程-->
|
||||
<BindApprovalFlowModal ref="bindFlowModalRef" @success="handleSuccess" />
|
||||
<!--update-end---author:GHT ---date:20260610 for:【MESToDing审批配置】操作列绑定审批流程-->
|
||||
|
||||
<!--update-begin---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】钉钉同步结果弹窗-->
|
||||
<a-modal
|
||||
v-model:open="syncVisible"
|
||||
@@ -154,8 +158,11 @@
|
||||
//update-begin---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】手动填表发起钉钉审批
|
||||
import DingApprovalLaunchModal from './components/DingApprovalLaunchModal.vue';
|
||||
//update-end---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】手动填表发起钉钉审批
|
||||
//update-begin---author:GHT ---date:20260610 for:【MESToDing审批配置】操作列绑定审批流程
|
||||
import BindApprovalFlowModal from './components/BindApprovalFlowModal.vue';
|
||||
//update-end---author:GHT ---date:20260610 for:【MESToDing审批配置】操作列绑定审批流程
|
||||
import { columns, searchFormSchema, superQuerySchema } from './MesXslDingProcessTpl.data';
|
||||
import { list, deleteOne, batchDelete, getImportUrl, getExportUrl, syncFromDingtalk, batchImport, getTemplateDetail } from './MesXslDingProcessTpl.api';
|
||||
import { list, deleteOne, batchDelete, getImportUrl, getExportUrl, syncFromDingtalk, batchImport, getTemplateDetail, toggleTplStatus } from './MesXslDingProcessTpl.api';
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const queryParam = reactive<any>({});
|
||||
@@ -167,6 +174,9 @@
|
||||
api: list,
|
||||
columns,
|
||||
canResize: true,
|
||||
//update-begin---author:GHT ---date:2026-06-10 for:【钉钉审批模板】操作列加宽避免按钮挤压-----------
|
||||
scroll: { x: 1700 },
|
||||
//update-end---author:GHT ---date:2026-06-10 for:【钉钉审批模板】操作列加宽避免按钮挤压-----------
|
||||
formConfig: {
|
||||
schemas: searchFormSchema,
|
||||
autoSubmitOnEnter: true,
|
||||
@@ -175,7 +185,9 @@
|
||||
actionColumn: {
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
width: 220,
|
||||
//update-begin---author:GHT ---date:2026-06-10 for:【钉钉审批模板】操作列加宽避免按钮挤压-----------
|
||||
width: 540,
|
||||
//update-end---author:GHT ---date:2026-06-10 for:【钉钉审批模板】操作列加宽避免按钮挤压-----------
|
||||
fixed: 'right',
|
||||
slots: { customRender: 'action' },
|
||||
},
|
||||
@@ -230,27 +242,72 @@
|
||||
(selectedRowKeys.value = []) && reload();
|
||||
}
|
||||
|
||||
function isTplEnabled(record: Recordable) {
|
||||
return record.status === '1' || record.status === 1;
|
||||
}
|
||||
|
||||
async function handleToggleStatus(record: Recordable) {
|
||||
try {
|
||||
const msg = await toggleTplStatus(record.id);
|
||||
createMessage.success(typeof msg === 'string' ? msg : isTplEnabled(record) ? '已停用' : '已启用');
|
||||
reload();
|
||||
} catch (e: any) {
|
||||
createMessage.error(e?.message || '操作失败');
|
||||
}
|
||||
}
|
||||
|
||||
function getTableAction(record) {
|
||||
const enabled = isTplEnabled(record);
|
||||
return [
|
||||
{ label: '编辑', onClick: handleEdit.bind(null, record), auth: 'xslmes:mes_xsl_ding_process_tpl:edit' },
|
||||
//update-begin---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】操作列新增发起审批按钮
|
||||
//update-begin---author:GHT ---date:20260610 for:【钉钉审批模板】操作列停用/启用-----------
|
||||
{
|
||||
label: '发起审批',
|
||||
label: enabled ? '停用' : '启用',
|
||||
color: enabled ? 'warning' : 'success',
|
||||
auth: 'xslmes:mes_xsl_ding_process_tpl:edit',
|
||||
popConfirm: {
|
||||
title: enabled
|
||||
? '停用后,已绑定该模板的业务页面将不再显示「钉钉审批」按钮,确认停用?'
|
||||
: '确认启用该审批模板?启用后业务页将恢复显示钉钉审批按钮。',
|
||||
confirm: handleToggleStatus.bind(null, record),
|
||||
placement: 'topLeft',
|
||||
},
|
||||
},
|
||||
//update-end---author:GHT ---date:20260610 for:【钉钉审批模板】操作列停用/启用-----------
|
||||
//update-begin---author:GHT ---date:20260610 for:【MESToDing审批配置】操作列绑定审批流程
|
||||
{
|
||||
label: '绑定审批流程',
|
||||
icon: 'ant-design:apartment-outlined',
|
||||
auth: 'xslmes:mes_xsl_ding_process_tpl:edit',
|
||||
onClick: handleBindApprovalFlow.bind(null, record),
|
||||
},
|
||||
//update-end---author:GHT ---date:20260610 for:【MESToDing审批配置】操作列绑定审批流程
|
||||
//update-begin---author:GHT ---date:2026-06-10 for:【MESToDing审批配置】设计模板移至操作列、发起审批改名为测试审批
|
||||
{
|
||||
label: '设计模板',
|
||||
icon: 'ant-design:layout-outlined',
|
||||
auth: 'xslmes:mes_xsl_ding_process_tpl:edit',
|
||||
onClick: handleDesignTemplate.bind(null, record),
|
||||
},
|
||||
{
|
||||
label: '测试审批',
|
||||
icon: 'ant-design:send-outlined',
|
||||
color: 'success',
|
||||
disabled: !record.processCode,
|
||||
tooltip: record.processCode ? '手动填表后发起钉钉审批' : '请先配置 processCode',
|
||||
disabled: !enabled || !record.processCode,
|
||||
tooltip: !enabled
|
||||
? '模板已停用'
|
||||
: record.processCode
|
||||
? '手动填表后测试发起钉钉审批'
|
||||
: '请先配置 processCode',
|
||||
onClick: handleLaunchApproval.bind(null, record),
|
||||
},
|
||||
//update-end---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】操作列新增发起审批按钮
|
||||
//update-end---author:GHT ---date:2026-06-10 for:【MESToDing审批配置】设计模板移至操作列、发起审批改名为测试审批
|
||||
];
|
||||
}
|
||||
|
||||
function getDropDownAction(record) {
|
||||
const actions: any[] = [
|
||||
{ label: '详情', onClick: handleDetail.bind(null, record) },
|
||||
//update-begin---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】新增设计模板入口
|
||||
{ label: '设计模板', onClick: handleDesignTemplate.bind(null, record), icon: 'ant-design:layout-outlined' },
|
||||
];
|
||||
if (!record.processCode) {
|
||||
actions.push({
|
||||
@@ -262,7 +319,6 @@
|
||||
}
|
||||
actions.push(
|
||||
{ label: '查看钉钉字段', onClick: handleShowDingSchema.bind(null, record), icon: 'ant-design:dingtalk-outlined' },
|
||||
//update-end---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】新增设计模板入口
|
||||
{
|
||||
label: '删除',
|
||||
popConfirm: { title: '是否确认删除', confirm: handleDelete.bind(null, record), placement: 'topLeft' },
|
||||
@@ -272,11 +328,24 @@
|
||||
return actions;
|
||||
}
|
||||
|
||||
// ===== 绑定审批流程 =====
|
||||
//update-begin---author:GHT ---date:20260610 for:【MESToDing审批配置】操作列绑定审批流程
|
||||
const bindFlowModalRef = ref();
|
||||
|
||||
function handleBindApprovalFlow(record: Recordable) {
|
||||
bindFlowModalRef.value?.open(record);
|
||||
}
|
||||
//update-end---author:GHT ---date:20260610 for:【MESToDing审批配置】操作列绑定审批流程
|
||||
|
||||
// ===== 手动填表发起钉钉审批 =====
|
||||
//update-begin---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】手动填表发起钉钉审批
|
||||
const launchModalRef = ref();
|
||||
|
||||
function handleLaunchApproval(record: Recordable) {
|
||||
if (!isTplEnabled(record)) {
|
||||
createMessage.warning('该模板已停用,请先启用后再发起审批');
|
||||
return;
|
||||
}
|
||||
if (!record.processCode) {
|
||||
createMessage.warning('该模板尚未配置 processCode,请先完成模板配置');
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
<!--
|
||||
钉钉审批模板 - 绑定 MES 审批流弹窗
|
||||
@author GHT
|
||||
@date 2026-06-10 for:【MESToDing审批配置】操作列绑定审批流程
|
||||
-->
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
title="绑定审批流程"
|
||||
width="660px"
|
||||
wrap-class-name="baf-modal-wrap"
|
||||
:body-style="{ padding: '8px 28px 20px' }"
|
||||
:confirm-loading="submitting"
|
||||
ok-text="确认绑定"
|
||||
cancel-text="取消"
|
||||
destroy-on-close
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleClose"
|
||||
>
|
||||
<a-spin :spinning="loading">
|
||||
<div class="baf-body">
|
||||
<div v-if="tplRecord" class="baf-info-card">
|
||||
<a-descriptions :column="1" bordered size="small" class="baf-descriptions">
|
||||
<a-descriptions-item label="模板名称">{{ tplRecord.tplName || '—' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="processCode">
|
||||
<a-typography-text v-if="tplRecord.processCode" code copyable>{{ tplRecord.processCode }}</a-typography-text>
|
||||
<a-tag v-else color="orange">未创建</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="当前绑定">
|
||||
<span v-if="currentFlowName" class="baf-bound-name">{{ currentFlowName }}</span>
|
||||
<span v-else class="baf-unbound">未绑定</span>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
|
||||
<div class="baf-section">
|
||||
<a-form layout="vertical" class="baf-form">
|
||||
<a-form-item label="选择审批流程" required>
|
||||
<div class="baf-select-row">
|
||||
<a-select
|
||||
v-model:value="selectedFlowId"
|
||||
class="baf-flow-select"
|
||||
placeholder="请选择 MES 审批流"
|
||||
:loading="flowLoading"
|
||||
:options="flowSelectOptions"
|
||||
show-search
|
||||
:filter-option="filterFlowOption"
|
||||
allow-clear
|
||||
@change="handleFlowChange"
|
||||
>
|
||||
<template #option="{ label, status, remark, bizTableName }">
|
||||
<div class="baf-opt-item">
|
||||
<span class="baf-opt-name">{{ label }}</span>
|
||||
<span class="baf-opt-meta">
|
||||
<span v-if="bizTableName" class="baf-opt-remark">{{ bizTableName }}</span>
|
||||
<span v-if="remark" class="baf-opt-remark">{{ remark }}</span>
|
||||
<a-tag
|
||||
:color="status === '1' ? 'green' : status === '2' ? 'default' : 'orange'"
|
||||
class="baf-opt-tag"
|
||||
>
|
||||
{{ status === '1' ? '已发布' : status === '2' ? '已停用' : '草稿' }}
|
||||
</a-tag>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-select>
|
||||
<a-button v-if="selectedFlowId" type="link" class="baf-design-btn" @click="handleDesignFlow">设计</a-button>
|
||||
</div>
|
||||
<div class="baf-hint">发起钉钉审批时,将按所选审批流解析各节点审批人</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedFlowId" class="baf-preview-card">
|
||||
<div class="baf-preview-title">
|
||||
审批节点预览
|
||||
<a-spin :spinning="previewLoading" size="small" />
|
||||
</div>
|
||||
<div v-if="!previewLoading && approverPreview.length === 0" class="baf-preview-empty">
|
||||
该审批流暂无审批人节点,请先在流程设计器中配置
|
||||
</div>
|
||||
<div v-else class="baf-preview-list">
|
||||
<div v-for="(node, ni) in approverPreview" :key="node.nodeId || ni" class="baf-preview-node">
|
||||
<a-tag :color="node.nodeType === 'cc' ? 'blue' : 'orange'" class="baf-node-tag">
|
||||
{{ node.nodeType === 'cc' ? '抄送' : '审批' }}
|
||||
</a-tag>
|
||||
<span class="baf-preview-name">{{ node.nodeName }}</span>
|
||||
<span v-if="node.users?.length" class="baf-preview-users">
|
||||
{{ node.users.map((u) => u.realname || u.username).join('、') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<a-alert
|
||||
v-if="selectedFlowStatus && selectedFlowStatus !== '1'"
|
||||
type="warning"
|
||||
show-icon
|
||||
class="baf-warn-alert"
|
||||
message="所选审批流尚未发布,发起审批前请先发布流程"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
|
||||
<FlowDesign @register="registerFlowDesign" @success="handleDesignSuccess" />
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { bindApprovalFlow, getApprovalFlowList, previewFlowApprovers } from '../MesXslDingProcessTpl.api';
|
||||
import FlowDesign from '/@/views/approval/flow/components/FlowDesign.vue';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const tplRecord = ref<Recordable | null>(null);
|
||||
|
||||
const flowLoading = ref(false);
|
||||
const flowList = ref<any[]>([]);
|
||||
const selectedFlowId = ref('');
|
||||
const previewLoading = ref(false);
|
||||
const approverPreview = ref<any[]>([]);
|
||||
|
||||
const [registerFlowDesign, { openModal: openFlowDesign }] = useModal();
|
||||
|
||||
const flowSelectOptions = computed(() =>
|
||||
flowList.value.map((f) => ({
|
||||
value: f.id,
|
||||
label: f.flowName,
|
||||
status: f.status,
|
||||
remark: f.remark || '',
|
||||
bizTableName: f.bizTableName || '',
|
||||
})),
|
||||
);
|
||||
|
||||
const currentFlowName = computed(() => {
|
||||
if (!tplRecord.value?.flowId) return '';
|
||||
const flow = flowList.value.find((f) => f.id === tplRecord.value?.flowId);
|
||||
return flow?.flowName || tplRecord.value?.flowId;
|
||||
});
|
||||
|
||||
const selectedFlowStatus = computed(() => {
|
||||
if (!selectedFlowId.value) return '';
|
||||
return flowList.value.find((f) => f.id === selectedFlowId.value)?.status || '';
|
||||
});
|
||||
|
||||
function filterFlowOption(input: string, option: any) {
|
||||
return (option?.label ?? '').toLowerCase().includes(input.toLowerCase());
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
selectedFlowId.value = '';
|
||||
approverPreview.value = [];
|
||||
tplRecord.value = null;
|
||||
}
|
||||
|
||||
async function open(record: Recordable) {
|
||||
resetState();
|
||||
tplRecord.value = record;
|
||||
visible.value = true;
|
||||
loading.value = true;
|
||||
try {
|
||||
await loadFlowList();
|
||||
if (record.flowId) {
|
||||
selectedFlowId.value = record.flowId;
|
||||
await loadPreview(record.flowId);
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
async function loadFlowList() {
|
||||
flowLoading.value = true;
|
||||
try {
|
||||
const res = await getApprovalFlowList({ pageSize: 500 });
|
||||
flowList.value = res?.records || res || [];
|
||||
} catch {
|
||||
flowList.value = [];
|
||||
} finally {
|
||||
flowLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFlowChange() {
|
||||
approverPreview.value = [];
|
||||
if (selectedFlowId.value) {
|
||||
await loadPreview(selectedFlowId.value);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPreview(flowId: string) {
|
||||
previewLoading.value = true;
|
||||
try {
|
||||
const res = await previewFlowApprovers(flowId);
|
||||
approverPreview.value = Array.isArray(res) ? res : [];
|
||||
} catch {
|
||||
approverPreview.value = [];
|
||||
} finally {
|
||||
previewLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleDesignFlow() {
|
||||
const flow = flowList.value.find((f) => f.id === selectedFlowId.value);
|
||||
if (flow) {
|
||||
openFlowDesign(true, { record: flow, readonly: false });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDesignSuccess() {
|
||||
if (selectedFlowId.value) {
|
||||
await loadPreview(selectedFlowId.value);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!tplRecord.value?.id) {
|
||||
createMessage.warning('模板信息无效');
|
||||
return;
|
||||
}
|
||||
if (!selectedFlowId.value) {
|
||||
createMessage.warning('请选择要绑定的审批流程');
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
await bindApprovalFlow({ id: tplRecord.value.id, flowId: selectedFlowId.value });
|
||||
createMessage.success('审批流程绑定成功');
|
||||
visible.value = false;
|
||||
emit('success');
|
||||
} catch (e: any) {
|
||||
createMessage.error(e?.message || '绑定失败');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.baf-body {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.baf-info-card {
|
||||
margin-bottom: 20px;
|
||||
|
||||
:deep(.baf-descriptions) {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-descriptions-item-label {
|
||||
width: 110px;
|
||||
background: #fafafa;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.ant-descriptions-item-content {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.baf-bound-name {
|
||||
color: #1677ff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.baf-unbound {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.baf-section {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.baf-form {
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-label > label) {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.baf-select-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.baf-flow-select {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.baf-design-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.baf-hint {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
line-height: 1.6;
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.baf-opt-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.baf-opt-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.baf-opt-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.baf-opt-remark {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.baf-opt-tag {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.baf-preview-card {
|
||||
margin-top: 20px;
|
||||
padding: 14px 16px;
|
||||
background: #fafafa;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.baf-preview-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.baf-preview-empty {
|
||||
color: #bbb;
|
||||
font-size: 12px;
|
||||
padding: 8px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.baf-preview-list {
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
padding: 4px 12px;
|
||||
}
|
||||
|
||||
.baf-preview-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px dashed #f0f0f0;
|
||||
font-size: 13px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.baf-node-tag {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.baf-preview-name {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.baf-preview-users {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.baf-warn-alert {
|
||||
margin-top: 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.baf-modal-wrap {
|
||||
.ant-modal-footer {
|
||||
padding: 12px 28px 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -676,6 +676,9 @@
|
||||
try {
|
||||
const detail = await getTemplateDetail(record.id);
|
||||
tplData.value = detail;
|
||||
if (detail?.dingNameSynced) {
|
||||
createMessage.info('已从钉钉同步最新模板名称');
|
||||
}
|
||||
|
||||
if (detail?.dingFields?.length) {
|
||||
// 以钉钉最新 schema 为准(保证结构与钉钉同步)
|
||||
|
||||
@@ -445,7 +445,14 @@ function sectionTitle(label: string, field: string): FormSchema {
|
||||
}
|
||||
|
||||
const hasWorkflowInfo = ({ values }) =>
|
||||
!!(values.proofreadBy || values.proofreadTime || values.auditBy || values.auditTime || values.approveBy || values.approveTime);
|
||||
!!(
|
||||
values.traceProofreadBy ||
|
||||
values.traceProofreadTime ||
|
||||
values.traceAuditBy ||
|
||||
values.traceAuditTime ||
|
||||
values.traceApproveBy ||
|
||||
values.traceApproveTime
|
||||
);
|
||||
|
||||
export const columns: BasicColumn[] = [
|
||||
{ title: '示方编号', align: 'center', dataIndex: 'specCode', width: 150, fixed: 'left' },
|
||||
@@ -468,8 +475,7 @@ export const columns: BasicColumn[] = [
|
||||
width: 100,
|
||||
customRender: ({ record }) => record?.createBy_dictText || record?.createBy || '',
|
||||
},
|
||||
{ title: '审核人', align: 'center', dataIndex: 'auditBy', width: 100, defaultHidden: true },
|
||||
{ title: '批准人', align: 'center', dataIndex: 'approveBy', width: 100, defaultHidden: true },
|
||||
// 审批痕迹 6 列(校对人/校对时间/审核人/审核时间/批准人/批准时间)由 useListPage 统一从 traceColumns 追加,勿在此手写 trace* 列
|
||||
{ title: '状态', align: 'center', dataIndex: 'status_dictText', width: 120 },
|
||||
{ title: '混合段数', align: 'center', dataIndex: 'mixingStages', width: 90, defaultHidden: true },
|
||||
{ title: 'TOTAL PHR', align: 'center', dataIndex: 'totalPhr', width: 100, defaultHidden: true },
|
||||
@@ -617,51 +623,51 @@ export const workflowFormSchema: FormSchema[] = [
|
||||
sectionTitle('审批记录', 'dividerWorkflow'),
|
||||
{
|
||||
label: '校对人',
|
||||
field: 'proofreadBy',
|
||||
field: 'traceProofreadBy',
|
||||
component: 'Input',
|
||||
componentProps: { disabled: true, bordered: false },
|
||||
colProps: colHalf,
|
||||
ifShow: ({ values }) => !!values.proofreadBy,
|
||||
ifShow: ({ values }) => !!values.traceProofreadBy,
|
||||
},
|
||||
{
|
||||
label: '校对时间',
|
||||
field: 'proofreadTime',
|
||||
field: 'traceProofreadTime',
|
||||
component: 'Input',
|
||||
componentProps: { disabled: true, bordered: false },
|
||||
colProps: colHalf,
|
||||
ifShow: ({ values }) => !!values.proofreadTime,
|
||||
ifShow: ({ values }) => !!values.traceProofreadTime,
|
||||
},
|
||||
{
|
||||
label: '审核人',
|
||||
field: 'auditBy',
|
||||
field: 'traceAuditBy',
|
||||
component: 'Input',
|
||||
componentProps: { disabled: true, bordered: false },
|
||||
colProps: colHalf,
|
||||
ifShow: ({ values }) => !!values.auditBy,
|
||||
ifShow: ({ values }) => !!values.traceAuditBy,
|
||||
},
|
||||
{
|
||||
label: '审核时间',
|
||||
field: 'auditTime',
|
||||
field: 'traceAuditTime',
|
||||
component: 'Input',
|
||||
componentProps: { disabled: true, bordered: false },
|
||||
colProps: colHalf,
|
||||
ifShow: ({ values }) => !!values.auditTime,
|
||||
ifShow: ({ values }) => !!values.traceAuditTime,
|
||||
},
|
||||
{
|
||||
label: '批准人',
|
||||
field: 'approveBy',
|
||||
field: 'traceApproveBy',
|
||||
component: 'Input',
|
||||
componentProps: { disabled: true, bordered: false },
|
||||
colProps: colHalf,
|
||||
ifShow: ({ values }) => !!values.approveBy,
|
||||
ifShow: ({ values }) => !!values.traceApproveBy,
|
||||
},
|
||||
{
|
||||
label: '批准时间',
|
||||
field: 'approveTime',
|
||||
field: 'traceApproveTime',
|
||||
component: 'Input',
|
||||
componentProps: { disabled: true, bordered: false },
|
||||
colProps: colHalf,
|
||||
ifShow: ({ values }) => !!values.approveTime,
|
||||
ifShow: ({ values }) => !!values.traceApproveTime,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -450,6 +450,11 @@
|
||||
workflowFormSchema,
|
||||
} from '../MesXslFormulaSpec.data';
|
||||
import { saveOrUpdate, queryById, generateRubberCode as generateRubberCodeApi, getRubberContentSetting } from '../MesXslFormulaSpec.api';
|
||||
import {
|
||||
FORMULA_SPEC_BIZ_TABLE,
|
||||
hasTraceWorkflowInfo,
|
||||
loadRecordWithTrace,
|
||||
} from '/@/views/xslmes/approval/integration/traceRecordHelper';
|
||||
import MesXslFormulaRubberContentSettingModal from './MesXslFormulaRubberContentSettingModal.vue';
|
||||
import MesXslFormulaGenerateMixingModal from './MesXslFormulaGenerateMixingModal.vue';
|
||||
import MesXslFormulaLineColumnSetting from './MesXslFormulaLineColumnSetting.vue';
|
||||
@@ -464,9 +469,9 @@
|
||||
const CATEGORY_DICT_CODE = 'xslmes_formula_spec_category';
|
||||
const WORKFLOW_HEADER_DEFS = [
|
||||
{ key: 'compile', label: '编制', operatorField: 'createBy', operatorTextField: 'createBy_dictText' },
|
||||
{ key: 'proofread', label: '校对', operatorField: 'proofreadBy', operatorTextField: 'proofreadBy_dictText' },
|
||||
{ key: 'audit', label: '审核', operatorField: 'auditBy', operatorTextField: 'auditBy_dictText' },
|
||||
{ key: 'approve', label: '批准', operatorField: 'approveBy', operatorTextField: 'approveBy_dictText' },
|
||||
{ key: 'proofread', label: '校对', operatorField: 'traceProofreadBy', operatorTextField: 'traceProofreadBy_dictText' },
|
||||
{ key: 'audit', label: '审核', operatorField: 'traceAuditBy', operatorTextField: 'traceAuditBy_dictText' },
|
||||
{ key: 'approve', label: '批准', operatorField: 'traceApproveBy', operatorTextField: 'traceApproveBy_dictText' },
|
||||
] as const;
|
||||
|
||||
const modalWidth = '96%';
|
||||
@@ -681,6 +686,7 @@
|
||||
const userInfo = userStore.getUserInfo || {};
|
||||
const text = record?.[step.operatorTextField];
|
||||
const raw = record?.[step.operatorField];
|
||||
// 编制:无 createBy 时回退当前登录人;校对/审核/批准仅展示痕迹表数据,无则留空
|
||||
if (step.key === 'compile') {
|
||||
return resolveFormulaSpecUserDisplayName(raw, text, userInfo);
|
||||
}
|
||||
@@ -688,11 +694,15 @@
|
||||
return String(text);
|
||||
}
|
||||
if (raw != null && raw !== '') {
|
||||
return String(raw);
|
||||
return resolveFormulaSpecUserDisplayName(raw, null, userInfo);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function loadMainRecordWithTrace(id: string, listRecord?: Recordable) {
|
||||
return loadRecordWithTrace(id, FORMULA_SPEC_BIZ_TABLE, queryById, listRecord);
|
||||
}
|
||||
|
||||
function formatCategoryShortLabel(text?: string) {
|
||||
if (!text) {
|
||||
return '';
|
||||
@@ -930,14 +940,7 @@
|
||||
}
|
||||
|
||||
function hasWorkflowData(record: Recordable) {
|
||||
return !!(
|
||||
record?.proofreadBy ||
|
||||
record?.proofreadTime ||
|
||||
record?.auditBy ||
|
||||
record?.auditTime ||
|
||||
record?.approveBy ||
|
||||
record?.approveTime
|
||||
);
|
||||
return hasTraceWorkflowInfo(record);
|
||||
}
|
||||
|
||||
function resetFooterValues() {
|
||||
@@ -1133,8 +1136,7 @@
|
||||
if (unref(isUpdate) && data?.record?.id) {
|
||||
lineLoading.value = true;
|
||||
try {
|
||||
const mainRaw = await queryById({ id: data.record.id });
|
||||
const m = (mainRaw as any)?.id != null ? mainRaw : (mainRaw as any)?.result ?? mainRaw;
|
||||
const m = await loadMainRecordWithTrace(data.record.id, data.record);
|
||||
applyMixingStages(m?.mixingStages);
|
||||
currentStatus.value = m?.status || 'compile';
|
||||
const lines = m?.lineList?.length ? normalizeLineRows(m.lineList) : createEmptyLineRows();
|
||||
|
||||
@@ -9,9 +9,11 @@ enum Api {
|
||||
importExcel = '/xslmes/mesXslMixerPsCompile/importExcel',
|
||||
exportXls = '/xslmes/mesXslMixerPsCompile/exportXls',
|
||||
queryById = '/xslmes/mesXslMixerPsCompile/queryById',
|
||||
proofread = '/xslmes/mesXslMixerPsCompile/proofread',
|
||||
audit = '/xslmes/mesXslMixerPsCompile/audit',
|
||||
approve = '/xslmes/mesXslMixerPsCompile/approve',
|
||||
// update-begin---author:GHT ---date:2026-06-05 for:【XSLMES-20260605-K8R2】校对/审核/批准接口停用,改由审批流+集成方案驱动
|
||||
// proofread = '/xslmes/mesXslMixerPsCompile/proofread',
|
||||
// audit = '/xslmes/mesXslMixerPsCompile/audit',
|
||||
// approve = '/xslmes/mesXslMixerPsCompile/approve',
|
||||
// update-end---author:GHT ---date:2026-06-05 for:【XSLMES-20260605-K8R2】
|
||||
}
|
||||
|
||||
export const list = (params) => defHttp.get({ url: Api.list, params });
|
||||
@@ -32,8 +34,8 @@ export const saveOrUpdate = (params, isUpdate) => {
|
||||
export const getExportUrl = Api.exportXls;
|
||||
export const getImportUrl = Api.importExcel;
|
||||
|
||||
export const proofread = (params: { ids: string }) => defHttp.post({ url: Api.proofread, params }, { joinParamsToUrl: true });
|
||||
|
||||
export const audit = (params: { ids: string }) => defHttp.post({ url: Api.audit, params }, { joinParamsToUrl: true });
|
||||
|
||||
export const approve = (params: { ids: string }) => defHttp.post({ url: Api.approve, params }, { joinParamsToUrl: true });
|
||||
// update-begin---author:GHT ---date:2026-06-05 for:【XSLMES-20260605-K8R2】校对/审核/批准接口停用,保留代码备后期恢复
|
||||
// export const proofread = (params: { ids: string }) => defHttp.post({ url: Api.proofread, params }, { joinParamsToUrl: true });
|
||||
// export const audit = (params: { ids: string }) => defHttp.post({ url: Api.audit, params }, { joinParamsToUrl: true });
|
||||
// export const approve = (params: { ids: string }) => defHttp.post({ url: Api.approve, params }, { joinParamsToUrl: true });
|
||||
// update-end---author:GHT ---date:2026-06-05 for:【XSLMES-20260605-K8R2】
|
||||
|
||||
@@ -19,7 +19,7 @@ const deptSelectProps = {
|
||||
};
|
||||
|
||||
const hasWorkflowInfo = ({ values }) =>
|
||||
!!(values.proofreadBy || values.proofreadTime || values.auditBy || values.auditTime || values.approveBy || values.approveTime);
|
||||
!!(values.traceProofreadBy || values.traceProofreadTime || values.traceAuditBy || values.traceAuditTime || values.traceApproveBy || values.traceApproveTime);
|
||||
|
||||
function sectionDivider(label: string, field: string, ifShow?: FormSchema['ifShow']): FormSchema {
|
||||
return {
|
||||
@@ -51,12 +51,6 @@ export const columns: BasicColumn[] = [
|
||||
width: 100,
|
||||
customRender: ({ record }) => record?.createBy_dictText || record?.createBy || '',
|
||||
},
|
||||
{ title: '校对人', align: 'center', dataIndex: 'proofreadBy', width: 100, defaultHidden: true },
|
||||
{ title: '校对时间', align: 'center', dataIndex: 'proofreadTime', width: 165, defaultHidden: true },
|
||||
{ title: '审核人', align: 'center', dataIndex: 'auditBy', width: 100, defaultHidden: true },
|
||||
{ title: '审核时间', align: 'center', dataIndex: 'auditTime', width: 165, defaultHidden: true },
|
||||
{ title: '批准人', align: 'center', dataIndex: 'approveBy', width: 100, defaultHidden: true },
|
||||
{ title: '批准时间', align: 'center', dataIndex: 'approveTime', width: 165, defaultHidden: true },
|
||||
{ title: '所属工厂', align: 'center', dataIndex: 'factoryName', width: 120, defaultHidden: true },
|
||||
{ title: '施工代号', align: 'center', dataIndex: 'constructionCode_dictText', width: 110, defaultHidden: true },
|
||||
{ title: '创建人', align: 'center', dataIndex: 'createBy', width: 100, defaultHidden: true },
|
||||
@@ -222,51 +216,51 @@ export const formSchema: FormSchema[] = [
|
||||
sectionDivider('审批记录', 'dividerWorkflow', hasWorkflowInfo),
|
||||
{
|
||||
label: '校对人',
|
||||
field: 'proofreadBy',
|
||||
field: 'traceProofreadBy',
|
||||
component: 'Input',
|
||||
componentProps: { disabled: true, bordered: false },
|
||||
colProps: colHalf,
|
||||
ifShow: ({ values }) => !!values.proofreadBy,
|
||||
ifShow: ({ values }) => !!values.traceProofreadBy,
|
||||
},
|
||||
{
|
||||
label: '校对时间',
|
||||
field: 'proofreadTime',
|
||||
field: 'traceProofreadTime',
|
||||
component: 'Input',
|
||||
componentProps: { disabled: true, bordered: false },
|
||||
colProps: colHalf,
|
||||
ifShow: ({ values }) => !!values.proofreadTime,
|
||||
ifShow: ({ values }) => !!values.traceProofreadTime,
|
||||
},
|
||||
{
|
||||
label: '审核人',
|
||||
field: 'auditBy',
|
||||
field: 'traceAuditBy',
|
||||
component: 'Input',
|
||||
componentProps: { disabled: true, bordered: false },
|
||||
colProps: colHalf,
|
||||
ifShow: ({ values }) => !!values.auditBy,
|
||||
ifShow: ({ values }) => !!values.traceAuditBy,
|
||||
},
|
||||
{
|
||||
label: '审核时间',
|
||||
field: 'auditTime',
|
||||
field: 'traceAuditTime',
|
||||
component: 'Input',
|
||||
componentProps: { disabled: true, bordered: false },
|
||||
colProps: colHalf,
|
||||
ifShow: ({ values }) => !!values.auditTime,
|
||||
ifShow: ({ values }) => !!values.traceAuditTime,
|
||||
},
|
||||
{
|
||||
label: '批准人',
|
||||
field: 'approveBy',
|
||||
field: 'traceApproveBy',
|
||||
component: 'Input',
|
||||
componentProps: { disabled: true, bordered: false },
|
||||
colProps: colHalf,
|
||||
ifShow: ({ values }) => !!values.approveBy,
|
||||
ifShow: ({ values }) => !!values.traceApproveBy,
|
||||
},
|
||||
{
|
||||
label: '批准时间',
|
||||
field: 'approveTime',
|
||||
field: 'traceApproveTime',
|
||||
component: 'Input',
|
||||
componentProps: { disabled: true, bordered: false },
|
||||
colProps: colHalf,
|
||||
ifShow: ({ values }) => !!values.approveTime,
|
||||
ifShow: ({ values }) => !!values.traceApproveTime,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -10,30 +10,8 @@
|
||||
>
|
||||
新增
|
||||
</a-button>
|
||||
<a-button
|
||||
v-auth="'xslmes:mes_xsl_mixer_ps_compile:proofread'"
|
||||
:disabled="selectedRowKeys.length === 0"
|
||||
preIcon="ant-design:check-circle-outlined"
|
||||
@click="handleProofread"
|
||||
>
|
||||
校对
|
||||
</a-button>
|
||||
<a-button
|
||||
v-auth="'xslmes:mes_xsl_mixer_ps_compile:audit'"
|
||||
:disabled="selectedRowKeys.length === 0"
|
||||
preIcon="ant-design:audit-outlined"
|
||||
@click="handleAudit"
|
||||
>
|
||||
审核
|
||||
</a-button>
|
||||
<a-button
|
||||
v-auth="'xslmes:mes_xsl_mixer_ps_compile:approve'"
|
||||
:disabled="selectedRowKeys.length === 0"
|
||||
preIcon="ant-design:safety-certificate-outlined"
|
||||
@click="handleApprove"
|
||||
>
|
||||
批准
|
||||
</a-button>
|
||||
<!-- update-begin---author:GHT ---date:2026-06-05 for:【XSLMES-20260605-K8R2】校对/审核/批准改由审批流+集成方案驱动,列表不再提供手工操作入口 -->
|
||||
<!-- update-end---author:GHT ---date:2026-06-05 for:【XSLMES-20260605-K8R2】校对/审核/批准改由审批流+集成方案驱动 -->
|
||||
<a-button
|
||||
type="primary"
|
||||
v-auth="'xslmes:mes_xsl_mixer_ps_compile:exportXls'"
|
||||
@@ -86,7 +64,6 @@
|
||||
|
||||
<script lang="ts" name="xslmes-mesXslMixerPsCompile" setup>
|
||||
import { computed, reactive } from 'vue';
|
||||
import { Modal } from 'ant-design-vue';
|
||||
import { BasicTable, TableAction } from '/@/components/Table';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { useListPage } from '/@/hooks/system/useListPage';
|
||||
@@ -100,9 +77,6 @@
|
||||
batchDelete,
|
||||
getExportUrl,
|
||||
getImportUrl,
|
||||
proofread,
|
||||
audit,
|
||||
approve,
|
||||
} from './MesXslMixerPsCompile.api';
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
@@ -171,38 +145,6 @@
|
||||
openModal(true, { record, isUpdate: true, showFooter: true });
|
||||
}
|
||||
|
||||
function handleStatusAction(action: 'proofread' | 'audit' | 'approve', label: string) {
|
||||
if (selectedRowKeys.value.length === 0) {
|
||||
createMessage.warning('请先选择要' + label + '的记录');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '确认' + label,
|
||||
content: `确定对选中的 ${selectedRowKeys.value.length} 条记录执行${label}吗?`,
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
const ids = selectedRowKeys.value.join(',');
|
||||
const fn = action === 'proofread' ? proofread : action === 'audit' ? audit : approve;
|
||||
await fn({ ids });
|
||||
createMessage.success(label + '成功');
|
||||
handleSuccess();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleProofread() {
|
||||
handleStatusAction('proofread', '校对');
|
||||
}
|
||||
|
||||
function handleAudit() {
|
||||
handleStatusAction('audit', '审核');
|
||||
}
|
||||
|
||||
function handleApprove() {
|
||||
handleStatusAction('approve', '批准');
|
||||
}
|
||||
|
||||
function handleDetail(record: Recordable) {
|
||||
openModal(true, { record, isUpdate: true, showFooter: false });
|
||||
}
|
||||
|
||||
@@ -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' } },
|
||||
];
|
||||
@@ -682,11 +694,32 @@ export const downStepColumns: JVxeColumn[] = [...stepColumns];
|
||||
|
||||
//update-begin---author:cursor ---date:20260522 for:【XSLMES-20260522-A19】TCU温度条件表列宽可调且表头换行-----------
|
||||
/** TCU 温度条件明细列宽偏好 localStorage 键 */
|
||||
export const MIXING_TCU_COLUMN_WIDTH_CACHE_KEY = 'mes_xsl_mixing_spec_tcu_column_widths_v2';
|
||||
export const MIXING_TCU_COLUMN_WIDTH_CACHE_KEY = 'mes_xsl_mixing_spec_tcu_column_widths_v3';
|
||||
|
||||
/** TCU 温度条件明细列可缩小到的最小宽度 */
|
||||
export const MIXING_TCU_MIN_COLUMN_WIDTH = 48;
|
||||
|
||||
/** TCU 是否附加:否 */
|
||||
export const MIXING_TCU_ATTACH_NO = '0';
|
||||
|
||||
/** TCU 是否附加:是 */
|
||||
export const MIXING_TCU_ATTACH_YES = '1';
|
||||
|
||||
/** 判断 TCU 行是否允许维护附加重量 */
|
||||
export function isMixingTcuAttachEnabled(value: unknown): boolean {
|
||||
return value === 1 || value === '1' || value === true;
|
||||
}
|
||||
|
||||
/** 规范化 TCU 行是否附加/重量联动 */
|
||||
export function normalizeMixingTcuAttachRow(row: Recordable) {
|
||||
if (row.isAttach == null || row.isAttach === '') {
|
||||
row.isAttach = MIXING_TCU_ATTACH_NO;
|
||||
}
|
||||
if (!isMixingTcuAttachEnabled(row.isAttach)) {
|
||||
row.attachWeight = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export const tcuColumns: JVxeColumn[] = [
|
||||
//update-begin---author:cursor ---date:20260522 for:【XSLMES-20260522-A33】TCU区分固定上/下密炼机-----------
|
||||
{ title: '区分', key: 'sectionType', type: JVxeTypes.select, dictCode: 'xslmes_mixing_tcu_section', disabled: true, width: 96, minWidth: MIXING_TCU_MIN_COLUMN_WIDTH, align: 'center' },
|
||||
@@ -697,6 +730,29 @@ export const tcuColumns: JVxeColumn[] = [
|
||||
{ title: '后混炼室温度', key: 'rearChamberTemp', type: JVxeTypes.inputNumber, width: 76, minWidth: MIXING_TCU_MIN_COLUMN_WIDTH, align: 'center' },
|
||||
{ title: '上下顶栓温度', key: 'topPlugTemp', type: JVxeTypes.inputNumber, width: 76, minWidth: MIXING_TCU_MIN_COLUMN_WIDTH, align: 'center' },
|
||||
{ title: '药品称量位置', key: 'drugWeighPos', type: JVxeTypes.select, dictCode: 'xslmes_mixing_drug_weigh_pos', width: 76, minWidth: MIXING_TCU_MIN_COLUMN_WIDTH, align: 'center' },
|
||||
//update-begin---author:GHT ---date:2026-06-10 for:【混炼示方】TCU温度条件新增是否附加/重量-----------
|
||||
{
|
||||
title: '是否附加',
|
||||
key: 'isAttach',
|
||||
type: JVxeTypes.select,
|
||||
dictCode: 'yn',
|
||||
defaultValue: MIXING_TCU_ATTACH_NO,
|
||||
width: 76,
|
||||
minWidth: MIXING_TCU_MIN_COLUMN_WIDTH,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '重量',
|
||||
key: 'attachWeight',
|
||||
type: JVxeTypes.inputNumber,
|
||||
width: 76,
|
||||
minWidth: MIXING_TCU_MIN_COLUMN_WIDTH,
|
||||
align: 'center',
|
||||
props: {
|
||||
isDisabledCell: ({ row }: { row?: Recordable }) => !isMixingTcuAttachEnabled(row?.isAttach),
|
||||
},
|
||||
},
|
||||
//update-end---author:GHT ---date:2026-06-10 for:【混炼示方】TCU温度条件新增是否附加/重量-----------
|
||||
];
|
||||
|
||||
/** 读取已保存的 TCU 温度条件明细列宽 */
|
||||
@@ -1000,11 +1056,15 @@ export const MIXING_TCU_UP_MIXER_DRUG_WEIGH_POS = 'drug_scale';
|
||||
export function buildDefaultMixingTcuRows(rows: Recordable[] = []): Recordable[] {
|
||||
const up =
|
||||
rows.find((r) => r.sectionType === 'up_mixer') ||
|
||||
({ sectionType: 'up_mixer', drugWeighPos: MIXING_TCU_UP_MIXER_DRUG_WEIGH_POS } as Recordable);
|
||||
({ sectionType: 'up_mixer', drugWeighPos: MIXING_TCU_UP_MIXER_DRUG_WEIGH_POS, isAttach: MIXING_TCU_ATTACH_NO } as Recordable);
|
||||
if (up.sectionType === 'up_mixer' && !up.drugWeighPos) {
|
||||
up.drugWeighPos = MIXING_TCU_UP_MIXER_DRUG_WEIGH_POS;
|
||||
}
|
||||
const down = rows.find((r) => r.sectionType === 'down_mixer') || ({ sectionType: 'down_mixer', drugWeighPos: undefined } as Recordable);
|
||||
const down =
|
||||
rows.find((r) => r.sectionType === 'down_mixer') ||
|
||||
({ sectionType: 'down_mixer', drugWeighPos: undefined, isAttach: MIXING_TCU_ATTACH_NO } as Recordable);
|
||||
normalizeMixingTcuAttachRow(up);
|
||||
normalizeMixingTcuAttachRow(down);
|
||||
return [up, down];
|
||||
}
|
||||
//update-end---author:cursor ---date:20260522 for:【XSLMES-20260522-A33】TCU默认两行及上密炼机药品称默认值-----------
|
||||
@@ -1327,9 +1387,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 +1427,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',
|
||||
|
||||
@@ -510,6 +510,8 @@ import {
|
||||
DEFAULT_MIXING_STEP_ROW_COUNT,
|
||||
DEFAULT_MIXING_DOWN_STEP_ROW_COUNT,
|
||||
buildDefaultMixingTcuRows,
|
||||
isMixingTcuAttachEnabled,
|
||||
MIXING_TCU_ATTACH_NO,
|
||||
applyMixingMaterialFromSelection,
|
||||
fillMixingMaterialAccumWeight,
|
||||
calcMixingMaterialUnitWeightTotal,
|
||||
@@ -534,6 +536,8 @@ import {
|
||||
MIXING_STEP_MIN_COLUMN_WIDTH,
|
||||
} from '../MesXslMixingSpec.data';
|
||||
import { saveOrUpdate, queryById } from '../MesXslMixingSpec.api';
|
||||
import { resolveFormulaSpecUserDisplayName } from '/@/views/xslmes/mesXslFormulaSpec/MesXslFormulaSpec.data';
|
||||
import { MIXING_SPEC_BIZ_TABLE, loadRecordWithTrace } from '/@/views/xslmes/approval/integration/traceRecordHelper';
|
||||
import MesXslMixingMaterialColumnSetting from './MesXslMixingMaterialColumnSetting.vue';
|
||||
import MesXslMixingTableRowHeightSetting from './MesXslMixingTableRowHeightSetting.vue';
|
||||
import MesXslMixingStepSelectCell from './MesXslMixingStepSelectCell.vue';
|
||||
@@ -949,15 +953,23 @@ function formatSignDate(value?: string) {
|
||||
}
|
||||
|
||||
function refreshSignDisplay(row: Recordable = {}) {
|
||||
signDisplay.draftBy = row.draftBy || row.createBy_dictText || row.createBy || '';
|
||||
const userInfo = userStore.getUserInfo || {};
|
||||
//update-begin---author:GHT ---date:2026-06-10 for:【混炼示方】页脚起草人/变更人展示姓名-----------
|
||||
signDisplay.draftBy = resolveFormulaSpecUserDisplayName(
|
||||
row.draftBy || row.createBy,
|
||||
row.draftBy_dictText || row.createBy_dictText,
|
||||
userInfo,
|
||||
);
|
||||
signDisplay.changeBy = resolveFormulaSpecUserDisplayName(row.updateBy, row.updateBy_dictText, userInfo);
|
||||
//update-end---author:GHT ---date:2026-06-10 for:【混炼示方】页脚起草人/变更人展示姓名-----------
|
||||
signDisplay.draftTime = formatSignDateTime(row.draftTime || row.createTime);
|
||||
signDisplay.proofreadBy = row.proofreadBy || row.proofreadBy_dictText || '';
|
||||
signDisplay.proofreadTime = formatSignDateTime(row.proofreadTime);
|
||||
signDisplay.auditBy = row.auditBy || row.auditBy_dictText || '';
|
||||
signDisplay.auditTime = formatSignDateTime(row.auditTime);
|
||||
signDisplay.approveBy = row.approveBy || row.approveBy_dictText || '';
|
||||
signDisplay.approveTime = formatSignDateTime(row.approveTime);
|
||||
signDisplay.changeBy = row.updateBy_dictText || row.updateBy || '';
|
||||
// 校对/审核/批准:优先展示痕迹表注入字段
|
||||
signDisplay.proofreadBy = row.traceProofreadBy || '';
|
||||
signDisplay.proofreadTime = formatSignDateTime(row.traceProofreadTime);
|
||||
signDisplay.auditBy = row.traceAuditBy || '';
|
||||
signDisplay.auditTime = formatSignDateTime(row.traceAuditTime);
|
||||
signDisplay.approveBy = row.traceApproveBy || '';
|
||||
signDisplay.approveTime = formatSignDateTime(row.traceApproveTime);
|
||||
signDisplay.changeDate = formatSignDate(row.changeDate || row.updateTime);
|
||||
}
|
||||
//update-end---author:cursor ---date:20260522 for:【XSLMES-20260522-A17】页脚签章区只读展示-----------
|
||||
@@ -1057,8 +1069,7 @@ async function onSpecPickerEdit(payload: Recordable | null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const raw = await queryById({ id: payload.mixingSpecId });
|
||||
const row = (raw as Recordable)?.specName != null ? raw : (raw as Recordable)?.result;
|
||||
const row = await loadRecordWithTrace(payload.mixingSpecId, MIXING_SPEC_BIZ_TABLE, queryById);
|
||||
if (!row?.id) {
|
||||
createMessage.warning('未找到混炼示方数据');
|
||||
return;
|
||||
@@ -1159,17 +1170,25 @@ function ensureTcuDefaultRows(rows: Recordable[] = []) {
|
||||
}
|
||||
|
||||
function handleTcuValueChange(event) {
|
||||
//update-begin---author:cursor ---date:20260522 for:【XSLMES-20260522-A17】下密炼机禁用药品称量位置-----------
|
||||
const row = event?.row;
|
||||
const key = event?.column?.key;
|
||||
if (!row || key !== 'drugWeighPos') {
|
||||
if (!row || !key) {
|
||||
return;
|
||||
}
|
||||
if (row.sectionType === 'down_mixer') {
|
||||
row.drugWeighPos = undefined;
|
||||
createMessage.warning('下密炼机不允许选择药品称量位置');
|
||||
//update-begin---author:cursor ---date:20260522 for:【XSLMES-20260522-A17】下密炼机禁用药品称量位置-----------
|
||||
if (key === 'drugWeighPos') {
|
||||
if (row.sectionType === 'down_mixer') {
|
||||
row.drugWeighPos = undefined;
|
||||
createMessage.warning('下密炼机不允许选择药品称量位置');
|
||||
}
|
||||
return;
|
||||
}
|
||||
//update-end---author:cursor ---date:20260522 for:【XSLMES-20260522-A17】下密炼机禁用药品称量位置-----------
|
||||
//update-begin---author:GHT ---date:2026-06-10 for:【混炼示方】TCU是否附加为否时清空重量-----------
|
||||
if (key === 'isAttach' && !isMixingTcuAttachEnabled(row.isAttach)) {
|
||||
row.attachWeight = undefined;
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-06-10 for:【混炼示方】TCU是否附加为否时清空重量-----------
|
||||
}
|
||||
|
||||
function resetSheetForm() {
|
||||
@@ -1231,8 +1250,7 @@ const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data
|
||||
await setProps({ disabled: !showFooter.value });
|
||||
setModalProps({ showOkBtn: showFooter.value, showCancelBtn: showFooter.value, confirmLoading: false });
|
||||
if (isUpdate.value && data?.record?.id) {
|
||||
const raw = await queryById({ id: data.record.id });
|
||||
const row = raw?.result || raw;
|
||||
const row = await loadRecordWithTrace(data.record.id, MIXING_SPEC_BIZ_TABLE, queryById, data.record);
|
||||
await applyMixingSpecPageData(row, 'edit');
|
||||
} else {
|
||||
const userInfo = userStore.getUserInfo || {};
|
||||
@@ -1271,7 +1289,9 @@ async function handleSubmit() {
|
||||
downStepList: cleanRows(downStepList),
|
||||
tcuList: tcuList.map((row) => ({
|
||||
...row,
|
||||
isAttach: row.isAttach ?? MIXING_TCU_ATTACH_NO,
|
||||
drugWeighPos: row.sectionType === 'down_mixer' ? undefined : row.drugWeighPos,
|
||||
attachWeight: isMixingTcuAttachEnabled(row.isAttach) ? row.attachWeight : undefined,
|
||||
})),
|
||||
};
|
||||
setModalProps({ confirmLoading: true });
|
||||
|
||||
Reference in New Issue
Block a user