钉钉审批功能完善、混炼示方新增是否附加料
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user