钉钉回调事件处理

This commit is contained in:
geht
2026-06-09 17:52:33 +08:00
parent fd5205e33e
commit 5b8bd2797a
50 changed files with 2861 additions and 428 deletions

View File

@@ -2,13 +2,6 @@ import { BasicColumn, FormSchema } from '/@/components/Table';
const STAGE_DICT = 'mes_xsl_approval_stage';
function hasStage(values: Recordable, stage: string) {
const raw = values?.enabledStages;
if (!raw) return false;
if (Array.isArray(raw)) return raw.includes(stage);
return String(raw).split(',').includes(stage);
}
export const columns: BasicColumn[] = [
{ title: '业务编码', dataIndex: 'docCode', width: 140, align: 'left' },
{ title: '物理表名', dataIndex: 'tableName', width: 200, align: 'left' },
@@ -84,46 +77,11 @@ export const formSchema: FormSchema[] = [
componentProps: { placeholder: '默认 status' },
},
{
label: '校对人字段',
field: 'proofreadByField',
label: '列表接口路径',
field: 'listApiPath',
component: 'Input',
defaultValue: 'proofread_by',
ifShow: ({ values }) => hasStage(values, 'proofread'),
},
{
label: '校对时间字段',
field: 'proofreadTimeField',
component: 'Input',
defaultValue: 'proofread_time',
ifShow: ({ values }) => hasStage(values, 'proofread'),
},
{
label: '审核人字段',
field: 'auditByField',
component: 'Input',
defaultValue: 'audit_by',
ifShow: ({ values }) => hasStage(values, 'audit'),
},
{
label: '审核时间字段',
field: 'auditTimeField',
component: 'Input',
defaultValue: 'audit_time',
ifShow: ({ values }) => hasStage(values, 'audit'),
},
{
label: '批准人字段',
field: 'approveByField',
component: 'Input',
defaultValue: 'approve_by',
ifShow: ({ values }) => hasStage(values, 'approve'),
},
{
label: '批准时间字段',
field: 'approveTimeField',
component: 'Input',
defaultValue: 'approve_time',
ifShow: ({ values }) => hasStage(values, 'approve'),
slot: 'listApiPath',
helpMessage: '从二级菜单选取后自动填入路径;配置后列表响应自动追加 traceProofreadBy / traceAuditBy / traceApproveBy 等6个字段',
},
{
label: '备注',

View File

@@ -122,6 +122,7 @@
{ 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 },
];
@@ -182,6 +183,12 @@
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) {
@@ -226,6 +233,8 @@
record.triggerPhase = null;
record.expectedFrom = null;
record.expectedFromLabel = '-';
record.statusAfter = null;
record.statusAfterLabel = '-';
return;
}
const cfgIdx = configuredBindings.indexOf(record);
@@ -234,6 +243,9 @@
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;

View File

@@ -1,6 +1,10 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" :width="640" @ok="handleSubmit">
<BasicForm @register="registerForm" />
<BasicForm @register="registerForm">
<template #listApiPath="{ model, field }">
<RegistryMenuSelect :value="model[field]" @change="(val) => (model[field] = val)" />
</template>
</BasicForm>
</BasicModal>
</template>
@@ -10,6 +14,7 @@
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);

View File

@@ -96,10 +96,11 @@
if (record.actionType === 'REGISTRY_STAGE_SYNC') {
const stage = cfg.registryStage?.stage || cfg.stage;
const from = cfg.registryStage?.expectedFrom || cfg.expectedFrom;
return `环节→${STAGE_LABELS[stage] || stage || '?'}${from ? `,前置=${from}` : ''}`;
const after = cfg.registryStage?.statusAfter || cfg.statusAfter || stage;
return `环节→${STAGE_LABELS[stage] || stage || '?'}${from ? `,前置=${from}` : ''},通过后=${after}`;
}
const target = cfg.registryStage?.targetStage || cfg.targetStage || 'compile';
return `回退→${target}`;
const target = cfg.registryStage?.targetStage ?? cfg.targetStage;
return target !== undefined && target !== null && target !== '' ? `回退→${target}` : '回退→未配置';
} catch {
return record.actionConfig || '-';
}

View File

@@ -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>

View File

@@ -58,7 +58,7 @@
type="info"
show-icon
style="margin-bottom: 14px"
message="审批注册中心配置自动更新源单状态、操作人/时间,并双写审批痕迹明细,无需绑定 Java 校对/审核/批准接口。"
message="审批环节仅用于匹配审批流与写入痕迹;业务表 status 由「通过后状态」控制。操作人/时间写入痕迹表,无需绑定 Java 接口。"
/>
<template v-if="vc.registryStage">
<a-form-item v-if="vc.visualType === 'REGISTRY_STAGE_SYNC'" label="审批环节" required>
@@ -73,6 +73,9 @@
<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
@@ -88,24 +91,45 @@
<a-input
v-else
v-model:value="vc.registryStage.expectedFrom"
placeholder="未解析到状态字典,可手填 compile / proofread / audit"
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_REVERT'" label="回退目标">
<a-form-item v-if="vc.visualType === 'REGISTRY_STAGE_SYNC'" label="通过后状态" required>
<a-select
v-if="sourceStatusDictItems.length"
v-model:value="vc.registryStage.targetStage"
v-model:value="vc.registryStage.statusAfter"
:options="sourceStatusDictItems"
placeholder="默认 compile编制态"
allow-clear
placeholder="请选择本环节通过后业务表应变为的状态"
show-search
option-filter-prop="label"
style="width: 100%"
/>
<a-input v-else v-model:value="vc.registryStage.targetStage" placeholder="默认 compile编制态" />
<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>
@@ -305,6 +329,7 @@
interface RegistryStageConfig {
stage?: string;
expectedFrom?: string;
statusAfter?: string;
targetStage?: string;
}
@@ -321,10 +346,18 @@
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 字段未带字典注释时的兜底映射 */
@@ -362,7 +395,7 @@
.split(',')
.map((s) => s.trim())
.filter(Boolean)
.map((v) => ({ value: v, label: dictLabelMap[v] || v }));
.map((v) => ({ value: v, label: APPROVAL_STAGE_LABELS[v] || dictLabelMap[v] || v }));
});
const targetColumns = ref<ColMeta[]>([]);
@@ -387,6 +420,7 @@
const defaultRegistryStage = (): RegistryStageConfig => ({
stage: '',
expectedFrom: '',
statusAfter: '',
targetStage: '',
});
@@ -428,7 +462,12 @@
if (parsed.expectedFrom !== undefined && parsed.expectedFrom !== null) {
merged.registryStage!.expectedFrom = parsed.expectedFrom;
}
if (parsed.targetStage) merged.registryStage!.targetStage = parsed.targetStage;
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;
}
@@ -461,7 +500,7 @@
},
);
// 审批环节变化时,前置状态留空则填入默认推断值
// 审批环节变化时,前置/通过后状态留空则填入默认推断值
watch(
() => vc.value.registryStage?.stage,
(stage) => {
@@ -469,6 +508,9 @@
if (!vc.value.registryStage.expectedFrom) {
vc.value.registryStage.expectedFrom = defaultExpectedFromForStage(stage);
}
if (!vc.value.registryStage.statusAfter) {
vc.value.registryStage.statusAfter = defaultStatusAfterForStage(stage);
}
},
);
@@ -548,6 +590,16 @@
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';
@@ -565,7 +617,12 @@
if (config.registryStage?.expectedFrom !== undefined && config.registryStage?.expectedFrom !== null) {
payload.expectedFrom = config.registryStage.expectedFrom;
}
if (config.registryStage?.targetStage) payload.targetStage = config.registryStage.targetStage;
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);
}
@@ -607,6 +664,7 @@
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();
@@ -687,6 +745,7 @@
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?.();
@@ -702,6 +761,10 @@
createMessage.warning('请选择审批环节');
return;
}
if (!vc.value.registryStage?.statusAfter) {
createMessage.warning('请选择通过后状态');
return;
}
emit('success', {
...form.value,
actionType: 'REGISTRY_STAGE_SYNC',
@@ -712,6 +775,10 @@
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',
@@ -777,10 +844,19 @@
}
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();

View File

@@ -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();

View File

@@ -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],
});
}