测试带有明细表的打印绑定是否生效
This commit is contained in:
@@ -47,7 +47,7 @@
|
||||
show-icon
|
||||
class="bind-alert"
|
||||
message="配置说明"
|
||||
description="按卡片顺序操作:先选业务与模板 → 若模板含明细占位,在「明细数据来源」中选择主实体上的集合/嵌套对象 → 点击「解析模板占位字段」→ 在下方「主表参数」「明细与表格」中分别为每个占位选择业务字段;业务字段下拉第一项为「空占位符」,表示不参与业务 JSON 取值(等同输出空)。主表参数一般映射主实体字段;明细占位可选带「明细前缀」的路径(如 lines.qty)。支持 lines.qty(首行)或 lines.0.qty。"
|
||||
description="按卡片顺序操作:先选业务与模板 → 点击「解析模板占位字段」→ 主表参数映射主实体字段 → 若模板含多个明细表,在「明细与表格」标签页中逐表选择与模板明细键对应的业务明细集合,再映射列字段。业务字段下拉第一项为「空占位符」,表示不参与业务 JSON。明细列占位多为「模板明细键.列」(如 List2.Field1),业务侧选「明细属性.列」(如 lineList.qty),打印时会按数组展开。"
|
||||
/>
|
||||
|
||||
<a-card title="基础信息" size="small" :bordered="true" class="bind-card">
|
||||
@@ -89,30 +89,6 @@
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<a-card size="small" :bordered="true" class="bind-card">
|
||||
<template #title>
|
||||
<span class="bind-card-head">明细数据来源</span>
|
||||
<span class="bind-card-head-extra">(可选)</span>
|
||||
</template>
|
||||
<template #extra>
|
||||
<span class="bind-card-sub">有明细/表格占位时需配置</span>
|
||||
</template>
|
||||
<p class="bind-card-desc">
|
||||
选择主实体类上的明细集合属性(如 List<明细实体>)或嵌套对象;系统将明细类字段并入下方「明细与表格」中的业务字段下拉。
|
||||
</p>
|
||||
<a-select
|
||||
v-model:value="selectedDetailProperty"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
placeholder="无需明细请留空"
|
||||
:options="detailSlotSelectOptions"
|
||||
:loading="detailFieldsLoading"
|
||||
style="width: 100%"
|
||||
@change="onDetailSlotChange"
|
||||
/>
|
||||
</a-card>
|
||||
|
||||
<a-card size="small" :bordered="true" class="bind-card bind-card--mapping">
|
||||
<template #title>
|
||||
<span class="bind-card-head">字段映射</span>
|
||||
@@ -125,7 +101,7 @@
|
||||
<a-button
|
||||
size="small"
|
||||
@click="autoMatchFields"
|
||||
:disabled="(!bizFields.length && !detailBizFields.length) || !tplFields.length"
|
||||
:disabled="(!bizFields.length && !hasAnyDetailBizFields()) || !tplFields.length"
|
||||
>
|
||||
同名自动匹配
|
||||
</a-button>
|
||||
@@ -170,35 +146,59 @@
|
||||
<div class="bind-map-section bind-map-section--detail">
|
||||
<div class="bind-section-bar">
|
||||
<span class="bind-section-title">② 明细与表格列</span>
|
||||
<span class="bind-section-hint">对应明细字段、表格列等;可选主表字段或上方明细来源生成的前缀字段</span>
|
||||
<span class="bind-section-hint">
|
||||
按模板明细表(tableKey)分页配置:每个标签页先选业务明细集合,再映射列;多表明细互不影响
|
||||
</span>
|
||||
</div>
|
||||
<a-table
|
||||
v-if="mappingRowsDetail.length"
|
||||
size="small"
|
||||
row-key="templateField"
|
||||
:pagination="false"
|
||||
:columns="mapTableColumnsDetail"
|
||||
:data-source="mappingRowsDetail"
|
||||
bordered
|
||||
class="bind-map-table"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'tplKind'">
|
||||
{{ templateFieldKindLabel(record.elementType) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'bizField'">
|
||||
<template v-if="detailTablesStructure.length">
|
||||
<a-tabs v-model:activeKey="detailTabKey" type="card" size="small" class="bind-detail-tabs">
|
||||
<a-tab-pane v-for="dt in detailTablesStructure" :key="dt.tableKey" :tab="detailTabTitle(dt)">
|
||||
<p class="bind-card-desc">
|
||||
模板明细键 <code>{{ dt.tableKey }}</code>
|
||||
对应画布表格等组件的数据源;请选择主实体上要绑定到该明细表的业务集合(或嵌套对象)。
|
||||
</p>
|
||||
<a-select
|
||||
v-model:value="record.bizField"
|
||||
:options="bizFieldOptions"
|
||||
v-model:value="detailSlotByTable[dt.tableKey]"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
style="width: 100%"
|
||||
placeholder="选择业务字段"
|
||||
placeholder="选择业务明细属性(如 lineList)"
|
||||
:options="detailSlotSelectOptions"
|
||||
:loading="!!detailFieldsLoadingMap[dt.tableKey]"
|
||||
style="width: 100%; margin-bottom: 12px"
|
||||
@change="(v) => onDetailSlotChangeForTable(dt.tableKey, v as string | undefined)"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-table
|
||||
v-if="mappingRowsForDetailTable(dt).length"
|
||||
size="small"
|
||||
row-key="templateField"
|
||||
:pagination="false"
|
||||
:columns="mapTableColumnsDetail"
|
||||
:data-source="mappingRowsForDetailTable(dt)"
|
||||
bordered
|
||||
class="bind-map-table"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'tplKind'">
|
||||
{{ templateFieldKindLabel(record.elementType) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'bizField'">
|
||||
<a-select
|
||||
v-model:value="record.bizField"
|
||||
:options="bizFieldOptionsForTable(dt.tableKey)"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
style="width: 100%"
|
||||
placeholder="选择业务字段"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-empty v-else class="bind-empty" description="该模板明细表下暂无占位字段(可在设计器中维护 dataBinding.detailTables)" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</template>
|
||||
<a-empty v-else class="bind-empty" description="本模板未解析到明细/表格列占位" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -269,7 +269,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, unref } from 'vue';
|
||||
import { computed, reactive, ref, unref } from 'vue';
|
||||
import { BasicTable, TableAction, useTable } from '/@/components/Table';
|
||||
import { BasicModal, useModal } from '/@/components/Modal';
|
||||
import { BasicTree, TreeItem } from '/@/components/Tree';
|
||||
@@ -313,6 +313,12 @@
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface TemplateDetailTableItem {
|
||||
tableKey: string;
|
||||
label?: string;
|
||||
fields: TplFieldItem[];
|
||||
}
|
||||
|
||||
const bizTypesRef = ref<BizTypeItem[]>([]);
|
||||
const tplListRef = ref<{ id: string; templateCode: string; templateName: string }[]>([]);
|
||||
/** 弹窗内:业务列表 + 模板下拉并行加载中(不再阻塞 openModal,避免点按钮好几秒才出框) */
|
||||
@@ -335,9 +341,12 @@
|
||||
const bizFields = ref<BizTypeItem['fields']>([]);
|
||||
const mappingRows = ref<MappingRow[]>([]);
|
||||
const detailSlots = ref<DetailSlotItem[]>([]);
|
||||
const selectedDetailProperty = ref<string | undefined>(undefined);
|
||||
const detailBizFields = ref<BizTypeItem['fields']>([]);
|
||||
const detailFieldsLoading = ref(false);
|
||||
const detailTablesStructure = ref<TemplateDetailTableItem[]>([]);
|
||||
const detailTabKey = ref('');
|
||||
/** 每个模板明细表键对应选中的业务明细属性名 */
|
||||
const detailSlotByTable = reactive<Record<string, string | undefined>>({});
|
||||
const detailBizFieldsMap = reactive<Record<string, BizTypeItem['fields']>>({});
|
||||
const detailFieldsLoadingMap = reactive<Record<string, boolean>>({});
|
||||
|
||||
const isEditMode = ref(false);
|
||||
const modalTitle = computed(() => (unref(isEditMode) ? '编辑业务打印绑定' : '新增业务打印绑定'));
|
||||
@@ -372,19 +381,125 @@
|
||||
return [...head, ...rest];
|
||||
});
|
||||
|
||||
/** 主表 + 明细前缀字段(用于明细/表格占位) */
|
||||
const bizFieldOptions = computed(() => {
|
||||
/** 某一模板明细表标签页:主表字段 + 该表选中的业务明细前缀字段 */
|
||||
function bizFieldOptionsForTable(tableKey: string) {
|
||||
const head = [{ label: '— 空占位符(不参与业务 JSON)—', value: EMPTY_BIZ_FIELD_SENTINEL }];
|
||||
const main = unref(bizFields).map((f) => ({
|
||||
label: f.label ? `${f.label}(${f.fieldKey})` : f.fieldKey,
|
||||
value: f.fieldKey,
|
||||
}));
|
||||
const detail = unref(detailBizFields).map((f) => ({
|
||||
const detail = (detailBizFieldsMap[tableKey] || []).map((f) => ({
|
||||
label: f.label ? `${f.label}(${f.fieldKey})` : f.fieldKey,
|
||||
value: f.fieldKey,
|
||||
}));
|
||||
return [...head, ...main, ...detail];
|
||||
});
|
||||
}
|
||||
|
||||
function hasAnyDetailBizFields(): boolean {
|
||||
for (const k in detailBizFieldsMap) {
|
||||
if ((detailBizFieldsMap[k] || []).length) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function detailTabTitle(dt: TemplateDetailTableItem) {
|
||||
const hint = dt.label || '';
|
||||
return hint ? `${hint}(${dt.tableKey})` : dt.tableKey;
|
||||
}
|
||||
|
||||
/** 属于某一模板明细表的映射行(与后端分组一致) */
|
||||
function mappingRowsForDetailTable(dt: TemplateDetailTableItem): MappingRow[] {
|
||||
const keys = new Set((dt.fields || []).map((f) => (f.bindField || '').trim()).filter(Boolean));
|
||||
return unref(mappingRows).filter((r) => keys.has((r.templateField || '').trim()));
|
||||
}
|
||||
|
||||
/** 根据已保存映射推断业务明细属性(编辑时用) */
|
||||
function inferDetailSlotForTable(tableKey: string): string | undefined {
|
||||
const dt = unref(detailTablesStructure).find((d) => d.tableKey === tableKey);
|
||||
if (!dt?.fields?.length) {
|
||||
return undefined;
|
||||
}
|
||||
const keySet = new Set(dt.fields.map((f) => (f.bindField || '').trim()).filter(Boolean));
|
||||
const counts = new Map<string, number>();
|
||||
for (const r of unref(mappingRows)) {
|
||||
if (!keySet.has((r.templateField || '').trim())) {
|
||||
continue;
|
||||
}
|
||||
const bf = r.bizField;
|
||||
if (!bf || bf === EMPTY_BIZ_FIELD_SENTINEL) {
|
||||
continue;
|
||||
}
|
||||
const s = String(bf);
|
||||
const dot = s.indexOf('.');
|
||||
if (dot <= 0) {
|
||||
continue;
|
||||
}
|
||||
const head = s.slice(0, dot);
|
||||
counts.set(head, (counts.get(head) || 0) + 1);
|
||||
}
|
||||
let best: string | undefined;
|
||||
let bestN = 0;
|
||||
counts.forEach((n, h) => {
|
||||
if (n > bestN) {
|
||||
bestN = n;
|
||||
best = h;
|
||||
}
|
||||
});
|
||||
return best;
|
||||
}
|
||||
|
||||
async function loadBizFieldsForTableSlot(tableKey: string, propertyName: string | undefined) {
|
||||
if (!propertyName || !form.value.bizCode) {
|
||||
detailBizFieldsMap[tableKey] = [];
|
||||
return;
|
||||
}
|
||||
const slot = unref(detailSlots).find((s) => s.propertyName === propertyName);
|
||||
const kind = slot?.slotKind || 'LIST';
|
||||
detailFieldsLoadingMap[tableKey] = true;
|
||||
try {
|
||||
const list = await Api.bizFieldsForDetailSlot({
|
||||
bizCode: form.value.bizCode,
|
||||
detailProperty: propertyName,
|
||||
slotKind: kind,
|
||||
});
|
||||
detailBizFieldsMap[tableKey] = (list || []) as BizTypeItem['fields'];
|
||||
} catch {
|
||||
detailBizFieldsMap[tableKey] = [];
|
||||
} finally {
|
||||
detailFieldsLoadingMap[tableKey] = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function onDetailSlotChangeForTable(tableKey: string, propertyName: string | undefined) {
|
||||
detailSlotByTable[tableKey] = propertyName;
|
||||
await loadBizFieldsForTableSlot(tableKey, propertyName);
|
||||
}
|
||||
|
||||
async function restoreDetailSlotsFromMapping() {
|
||||
for (const dt of unref(detailTablesStructure)) {
|
||||
const tk = dt.tableKey;
|
||||
const existing = detailSlotByTable[tk];
|
||||
if (existing) {
|
||||
await loadBizFieldsForTableSlot(tk, existing);
|
||||
continue;
|
||||
}
|
||||
const inferred = inferDetailSlotForTable(tk);
|
||||
if (inferred && unref(detailSlots).some((s) => s.propertyName === inferred)) {
|
||||
detailSlotByTable[tk] = inferred;
|
||||
await loadBizFieldsForTableSlot(tk, inferred);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetDetailTableUiState() {
|
||||
detailTablesStructure.value = [];
|
||||
detailTabKey.value = '';
|
||||
Object.keys(detailSlotByTable).forEach((k) => delete detailSlotByTable[k]);
|
||||
Object.keys(detailBizFieldsMap).forEach((k) => delete detailBizFieldsMap[k]);
|
||||
Object.keys(detailFieldsLoadingMap).forEach((k) => delete detailFieldsLoadingMap[k]);
|
||||
}
|
||||
|
||||
/** 已保存的空字符串映射为下拉哨兵,便于展示「空占位符」项 */
|
||||
function normalizeBizFieldForUi(raw?: string) {
|
||||
@@ -407,11 +522,6 @@
|
||||
unref(mappingRows).filter((r) => (r.elementType || '') === 'param'),
|
||||
);
|
||||
|
||||
/** 非参数占位(明细字段、表格列、其它画布元素) */
|
||||
const mappingRowsDetail = computed(() =>
|
||||
unref(mappingRows).filter((r) => (r.elementType || '') !== 'param'),
|
||||
);
|
||||
|
||||
function templateFieldKindLabel(t?: string) {
|
||||
const m: Record<string, string> = {
|
||||
param: '主表·参数',
|
||||
@@ -545,8 +655,6 @@
|
||||
|
||||
async function refreshDetailSlots(code: string | undefined) {
|
||||
detailSlots.value = [];
|
||||
selectedDetailProperty.value = undefined;
|
||||
detailBizFields.value = [];
|
||||
if (!code) {
|
||||
return;
|
||||
}
|
||||
@@ -557,37 +665,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function onDetailSlotChange(propertyName: string | undefined) {
|
||||
selectedDetailProperty.value = propertyName;
|
||||
if (!propertyName || !form.value.bizCode) {
|
||||
detailBizFields.value = [];
|
||||
return;
|
||||
}
|
||||
const slot = unref(detailSlots).find((s) => s.propertyName === propertyName);
|
||||
const kind = slot?.slotKind || 'LIST';
|
||||
detailFieldsLoading.value = true;
|
||||
try {
|
||||
const list = await Api.bizFieldsForDetailSlot({
|
||||
bizCode: form.value.bizCode,
|
||||
detailProperty: propertyName,
|
||||
slotKind: kind,
|
||||
});
|
||||
detailBizFields.value = (list || []) as BizTypeItem['fields'];
|
||||
} catch {
|
||||
detailBizFields.value = [];
|
||||
} finally {
|
||||
detailFieldsLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function onBizCodeChange(code: string) {
|
||||
const hit = unref(bizTypesRef).find((b) => b.bizCode === code);
|
||||
bizFields.value = hit?.fields ?? [];
|
||||
form.value.bizName = hit?.bizName;
|
||||
Object.keys(detailSlotByTable).forEach((k) => delete detailSlotByTable[k]);
|
||||
Object.keys(detailBizFieldsMap).forEach((k) => delete detailBizFieldsMap[k]);
|
||||
await refreshDetailSlots(code);
|
||||
if (form.value.templateId && unref(detailTablesStructure).length) {
|
||||
await restoreDetailSlotsFromMapping();
|
||||
}
|
||||
}
|
||||
|
||||
async function onTemplateChange() {
|
||||
resetDetailTableUiState();
|
||||
tplFields.value = [];
|
||||
mappingRows.value = [];
|
||||
await reloadTemplateFields();
|
||||
@@ -598,13 +689,20 @@
|
||||
if (!tid) {
|
||||
tplFields.value = [];
|
||||
mappingRows.value = [];
|
||||
detailTablesStructure.value = [];
|
||||
detailTabKey.value = '';
|
||||
return;
|
||||
}
|
||||
parseLoading.value = true;
|
||||
try {
|
||||
const list = (await Api.parseTemplateFields(tid)) as TplFieldItem[];
|
||||
tplFields.value = list || [];
|
||||
const structure = await Api.parseTemplateStructure(tid);
|
||||
detailTablesStructure.value = structure?.detailTables ?? [];
|
||||
const params = structure?.params ?? [];
|
||||
const flatDetail = (structure?.detailTables ?? []).flatMap((d) => d.fields ?? []);
|
||||
tplFields.value = [...params, ...flatDetail];
|
||||
detailTabKey.value = detailTablesStructure.value[0]?.tableKey ?? '';
|
||||
rebuildMappingRows();
|
||||
await restoreDetailSlotsFromMapping();
|
||||
} finally {
|
||||
parseLoading.value = false;
|
||||
}
|
||||
@@ -648,7 +746,10 @@
|
||||
const savedMappingRef = ref<{ templateField: string; bizField?: string }[]>([]);
|
||||
|
||||
function autoMatchFields() {
|
||||
const merged = [...unref(bizFields), ...unref(detailBizFields)];
|
||||
const merged = [...unref(bizFields)];
|
||||
for (const k in detailBizFieldsMap) {
|
||||
merged.push(...(detailBizFieldsMap[k] || []));
|
||||
}
|
||||
const set = new Map(merged.map((f) => [f.fieldKey, f.fieldKey]));
|
||||
for (const row of unref(mappingRows)) {
|
||||
if (set.has(row.templateField)) {
|
||||
@@ -676,8 +777,7 @@
|
||||
bizFields.value = [];
|
||||
mappingRows.value = [];
|
||||
detailSlots.value = [];
|
||||
selectedDetailProperty.value = undefined;
|
||||
detailBizFields.value = [];
|
||||
resetDetailTableUiState();
|
||||
previewBizJson.value = '';
|
||||
previewResult.value = '';
|
||||
openModal(true);
|
||||
@@ -709,8 +809,7 @@
|
||||
mappingRows.value = [];
|
||||
bizFields.value = [];
|
||||
detailSlots.value = [];
|
||||
selectedDetailProperty.value = undefined;
|
||||
detailBizFields.value = [];
|
||||
resetDetailTableUiState();
|
||||
openModal(true);
|
||||
modalDataLoading.value = true;
|
||||
try {
|
||||
@@ -853,6 +952,14 @@
|
||||
border-top: 1px dashed #f0f0f0;
|
||||
}
|
||||
|
||||
.bind-detail-tabs {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.bind-detail-tabs :deep(.ant-tabs-nav) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.bind-section-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
Reference in New Issue
Block a user