钉钉审批功能完善、混炼示方新增是否附加料

This commit is contained in:
geht
2026-06-10 15:41:02 +08:00
parent de48bd2324
commit 39a9bd83f1
37 changed files with 2461 additions and 166 deletions

View File

@@ -75,7 +75,12 @@
const drawerRef = ref();
// 当前审批流绑定的业务表,供节点配置按表查可选回调动作
const bizTableRef = ref('');
const flowIdRef = ref('');
provide('approvalBizTable', bizTableRef);
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[]>([]);
@@ -114,6 +119,7 @@
readonly.value = !!data?.readonly;
flowCtx.readonly = readonly.value;
bizTableRef.value = data?.record?.bizTable || '';
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审批流设计】接收审批注册中心候选环节----- -->

View File

@@ -133,7 +133,31 @@
message="当前为流程最后一个审批节点:「批准」类方案触发时机为「审批通过」,已合并到下方下拉(带 [流程最终通过] 前缀)。"
/>
<div class="fd-ip-block">
<div class="fd-ip-title">{{ isLastApproverNode ? '本节点通过 / 流程最终通过时执行' : '本节点通过时执行' }}</div>
<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-select
:value="primaryPlanValue"
:disabled="readonly"
@@ -224,6 +248,7 @@
<a-button type="primary" @click="onConfirm">确定</a-button>
</a-space>
</template>
<MesXslIntegrationActionDrawer ref="actionDrawerRef" />
</a-drawer>
</template>
@@ -238,6 +263,8 @@
import type { FlowNode } from './flowTypes';
import { useMessage } from '/@/hooks/web/useMessage';
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']);
@@ -245,8 +272,13 @@
const { createMessage } = useMessage();
const bizTable = inject<Ref<string>>('approvalBizTable', 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();
const STAGE_LABELS: Record<string, string> = { proofread: '校对', audit: '审核', approve: '批准' };
@@ -290,6 +322,20 @@
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: '批准' };
@@ -316,6 +362,78 @@
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 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;
}
}
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) {
@@ -474,4 +592,27 @@
color: #595959;
margin-bottom: 8px;
}
.fd-ip-title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
.fd-ip-title {
margin-bottom: 0;
flex: 1;
min-width: 0;
}
}
.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>

View File

@@ -9,10 +9,18 @@ enum Api {
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 });

View File

@@ -1,4 +1,5 @@
import { BasicColumn, FormSchema } from '/@/components/Table';
import { listDbTables } from './MesXslBizDocRegistry.api';
const STAGE_DICT = 'mes_xsl_approval_stage';
@@ -36,9 +37,16 @@ export const formSchema: FormSchema[] = [
{
label: '物理表名',
field: 'tableName',
component: 'Input',
componentProps: { placeholder: '数据库表名,如 mes_xsl_mixer_ps_compile' },
dynamicRules: () => [{ required: true, message: '请输入物理表名!' }],
component: 'ApiSelect',
componentProps: {
api: listDbTables,
showSearch: true,
placeholder: '请选择数据库物理表,可输入关键字筛选',
labelField: 'label',
valueField: 'value',
immediate: true,
},
dynamicRules: () => [{ required: true, message: '请选择物理表名!' }],
},
{
label: '中文名称',

View File

@@ -19,6 +19,8 @@ enum Api {
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 });
@@ -49,6 +51,18 @@ export const generateDefaultFromFlow = (params: {
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 });

View File

@@ -105,6 +105,15 @@
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 || '-';
}
@@ -192,5 +201,14 @@
}
}
defineExpose({ open });
defineExpose({ open, openAndEditFirstAction });
async function openAndEditFirstAction(plan: Recordable) {
await open(plan);
if (actions.value.length > 0) {
openVisualEditor(actions.value[0]);
} else {
openVisualEditor();
}
}
</script>

View File

@@ -177,6 +177,21 @@
</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'">
@@ -341,6 +356,8 @@
statusConfig: StatusConfig;
fieldMappings: FieldMapping[];
registryStage?: RegistryStageConfig;
/** 是否将主表审批痕迹同步到目标表 */
syncTrace?: boolean;
}
const emit = defineEmits<{ success: [action: any] }>();
@@ -431,6 +448,7 @@
statusConfig: defaultStatusConfig(),
fieldMappings: [],
registryStage: defaultRegistryStage(),
syncTrace: false,
});
/** 兼容 Flyway 扁平格式stage/expectedFrom 在顶层与向导嵌套格式registryStage 对象) */

View File

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

View File

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

View File

@@ -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';
}

View File

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

View File

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

View File

@@ -105,6 +105,10 @@
<DingApprovalLaunchModal ref="launchModalRef" @success="handleLaunchSuccess" />
<!--update-end---author:GHT ---date:2026-06-03 forMESToDing审批配置手动填表发起钉钉审批-->
<!--update-begin---author:GHT ---date:20260610 forMESToDing审批配置操作列绑定审批流程-->
<BindApprovalFlowModal ref="bindFlowModalRef" @success="handleSuccess" />
<!--update-end---author:GHT ---date:20260610 forMESToDing审批配置操作列绑定审批流程-->
<!--update-begin---author:GHT ---date:2026-06-03 forMESToDing审批配置钉钉同步结果弹窗-->
<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 forMESToDing审批配置】操作列新增发起审批按钮
//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;

View File

@@ -0,0 +1,433 @@
<!--
钉钉审批模板 - 绑定 MES 审批流弹窗
@author GHT
@date 2026-06-10 forMESToDing审批配置操作列绑定审批流程
-->
<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>

View File

@@ -676,6 +676,9 @@
try {
const detail = await getTemplateDetail(record.id);
tplData.value = detail;
if (detail?.dingNameSynced) {
createMessage.info('已从钉钉同步最新模板名称');
}
if (detail?.dingFields?.length) {
// 以钉钉最新 schema 为准(保证结构与钉钉同步)

View File

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

View File

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

View File

@@ -694,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' },
@@ -709,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 温度条件明细列宽 */
@@ -1012,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默认两行及上密炼机药品称默认值-----------

View File

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