2026-05-13 15:49:51 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<BasicTable @register="registerTable">
|
|
|
|
|
|
<template #tableTitle>
|
2026-05-13 17:25:13 +08:00
|
|
|
|
<a-space>
|
|
|
|
|
|
<a-button type="primary" @click="openCreate" v-auth="'print:bizBind:add'">新增绑定</a-button>
|
|
|
|
|
|
<a-button @click="openPrintBizWhitelist" :loading="whitelistLoading" v-auth="'print:bizBind:whitelist'">
|
|
|
|
|
|
打印业务白名单
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</a-space>
|
2026-05-13 15:49:51 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
<template #action="{ record }">
|
|
|
|
|
|
<TableAction
|
|
|
|
|
|
:actions="[
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '编辑',
|
|
|
|
|
|
onClick: () => openEdit(record),
|
|
|
|
|
|
auth: 'print:bizBind:edit',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '删除',
|
|
|
|
|
|
color: 'error',
|
|
|
|
|
|
popConfirm: {
|
|
|
|
|
|
title: '确认删除该绑定?',
|
|
|
|
|
|
confirm: () => handleDelete(record),
|
|
|
|
|
|
},
|
|
|
|
|
|
auth: 'print:bizBind:delete',
|
|
|
|
|
|
},
|
|
|
|
|
|
]"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</BasicTable>
|
|
|
|
|
|
|
|
|
|
|
|
<BasicModal
|
|
|
|
|
|
@register="registerModal"
|
|
|
|
|
|
:title="modalTitle"
|
2026-05-13 17:25:13 +08:00
|
|
|
|
width="1000px"
|
2026-05-13 15:49:51 +08:00
|
|
|
|
@ok="submitModal"
|
|
|
|
|
|
:confirm-loading="modalSubmitLoading"
|
|
|
|
|
|
destroy-on-close
|
2026-05-13 17:25:13 +08:00
|
|
|
|
wrap-class-name="biz-bind-modal-wrap"
|
2026-05-13 15:49:51 +08:00
|
|
|
|
>
|
2026-05-13 17:25:13 +08:00
|
|
|
|
<a-spin :spinning="modalDataLoading || parseLoading">
|
|
|
|
|
|
<div class="biz-bind-form">
|
2026-05-13 15:49:51 +08:00
|
|
|
|
<a-alert
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
show-icon
|
2026-05-13 17:25:13 +08:00
|
|
|
|
class="bind-alert"
|
|
|
|
|
|
message="配置说明"
|
2026-05-14 17:11:15 +08:00
|
|
|
|
description="按卡片顺序操作:先选业务与模板 → 若模板含明细占位,在「明细数据来源」中选择主实体上的集合/嵌套对象 → 点击「解析模板占位字段」→ 在下方「主表参数」「明细与表格」中分别为每个占位选择业务字段;业务字段下拉第一项为「空占位符」,表示不参与业务 JSON 取值(等同输出空)。主表参数一般映射主实体字段;明细占位可选带「明细前缀」的路径(如 lines.qty)。支持 lines.qty(首行)或 lines.0.qty。"
|
2026-05-13 15:49:51 +08:00
|
|
|
|
/>
|
|
|
|
|
|
|
2026-05-13 17:25:13 +08:00
|
|
|
|
<a-card title="基础信息" size="small" :bordered="true" class="bind-card">
|
|
|
|
|
|
<a-form layout="vertical" class="bind-card-form">
|
|
|
|
|
|
<a-row :gutter="16">
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item
|
|
|
|
|
|
label="业务"
|
|
|
|
|
|
required
|
2026-05-14 17:11:15 +08:00
|
|
|
|
extra="业务编码为菜单 id;业务字段优先从缓存表读取(启动任务根据 print_biz_perm_entity 异步写入 mes_xsl_biz_entity_field_*),无缓存时再反射实体类。"
|
2026-05-13 17:25:13 +08:00
|
|
|
|
>
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="form.bizCode"
|
|
|
|
|
|
:options="bizSelectOptions"
|
|
|
|
|
|
placeholder="选择业务"
|
|
|
|
|
|
show-search
|
|
|
|
|
|
option-filter-prop="label"
|
|
|
|
|
|
:disabled="isEditMode"
|
|
|
|
|
|
@change="onBizCodeChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="打印模板" required extra="请选择已发布的原生打印模板">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="form.templateId"
|
|
|
|
|
|
:options="tplSelectOptions"
|
|
|
|
|
|
placeholder="选择模板"
|
|
|
|
|
|
show-search
|
|
|
|
|
|
option-filter-prop="label"
|
|
|
|
|
|
@change="onTemplateChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
|
|
|
|
|
<a-form-item label="备注">
|
|
|
|
|
|
<a-input v-model:value="form.remark" placeholder="可选" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</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>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template #extra>
|
|
|
|
|
|
<a-space wrap>
|
|
|
|
|
|
<a-button type="primary" ghost size="small" @click="reloadTemplateFields" :loading="parseLoading">
|
|
|
|
|
|
解析模板占位字段
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
@click="autoMatchFields"
|
|
|
|
|
|
:disabled="(!bizFields.length && !detailBizFields.length) || !tplFields.length"
|
|
|
|
|
|
>
|
|
|
|
|
|
同名自动匹配
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</a-space>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="!form.templateId" class="bind-placeholder">请先在上方的「基础信息」中选择打印模板</div>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-else-if="tplFields.length">
|
|
|
|
|
|
<div class="bind-map-section">
|
|
|
|
|
|
<div class="bind-section-bar">
|
|
|
|
|
|
<span class="bind-section-title">① 主表参数</span>
|
|
|
|
|
|
<span class="bind-section-hint">对应模板 dataBinding.params / 画布参数;请选择主实体 JSON 字段</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-table
|
|
|
|
|
|
v-if="mappingRowsParam.length"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
row-key="templateField"
|
|
|
|
|
|
:pagination="false"
|
|
|
|
|
|
:columns="mapTableColumnsParam"
|
|
|
|
|
|
:data-source="mappingRowsParam"
|
|
|
|
|
|
bordered
|
|
|
|
|
|
class="bind-map-table"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #bodyCell="{ column, record }">
|
|
|
|
|
|
<template v-if="column.key === 'bizField'">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="record.bizField"
|
|
|
|
|
|
:options="bizFieldOptionsMain"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
show-search
|
|
|
|
|
|
option-filter-prop="label"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
placeholder="选择主表业务字段"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-table>
|
|
|
|
|
|
<a-empty v-else class="bind-empty" description="本模板未解析到「参数」类占位" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
|
</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'">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="record.bizField"
|
|
|
|
|
|
:options="bizFieldOptions"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
show-search
|
|
|
|
|
|
option-filter-prop="label"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
placeholder="选择业务字段"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-table>
|
|
|
|
|
|
<a-empty v-else class="bind-empty" description="本模板未解析到明细/表格列占位" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<a-empty
|
|
|
|
|
|
v-else-if="form.templateId && !parseLoading"
|
|
|
|
|
|
class="bind-empty"
|
|
|
|
|
|
description="请点击右上角「解析模板占位字段」或切换模板"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-card>
|
|
|
|
|
|
|
|
|
|
|
|
<a-card title="映射预览(可选)" size="small" :bordered="true" class="bind-card">
|
|
|
|
|
|
<a-textarea
|
|
|
|
|
|
v-model:value="previewBizJson"
|
|
|
|
|
|
placeholder='粘贴业务 JSON,例如:{"barcode":"TEST001","materialName":"胶料A"}'
|
|
|
|
|
|
:rows="4"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div style="margin-top: 10px">
|
|
|
|
|
|
<a-button type="dashed" @click="runPreview" :loading="previewLoading">生成打印数据预览</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<pre v-if="previewResult" class="preview-pre">{{ previewResult }}</pre>
|
|
|
|
|
|
</a-card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-spin>
|
|
|
|
|
|
</BasicModal>
|
|
|
|
|
|
|
|
|
|
|
|
<BasicModal
|
|
|
|
|
|
@register="registerWhitelistModal"
|
|
|
|
|
|
title="打印业务白名单"
|
|
|
|
|
|
width="760px"
|
|
|
|
|
|
@ok="submitWhitelistModal"
|
|
|
|
|
|
:confirm-loading="whitelistSubmitLoading"
|
|
|
|
|
|
destroy-on-close
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-spin :spinning="whitelistLoading">
|
|
|
|
|
|
<a-alert
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
show-icon
|
|
|
|
|
|
style="margin-bottom: 12px"
|
|
|
|
|
|
message="说明"
|
|
|
|
|
|
description="勾选允许的菜单 id,保存时会写入 print_biz_perm_entity(能推断出实体类的菜单)。打开弹窗已优化为不再加载全量业务目录。白名单为空时「新增绑定」下拉仅展示表里已有映射;白名单非空时展示勾选中且能解析的菜单。"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<a-space style="margin-bottom: 8px">
|
|
|
|
|
|
<a-button size="small" @click="expandWhitelistTree(true)">展开全部</a-button>
|
|
|
|
|
|
<a-button size="small" @click="expandWhitelistTree(false)">折叠全部</a-button>
|
|
|
|
|
|
<a-button size="small" @click="checkedKeysWhitelist = []">清空勾选</a-button>
|
2026-05-13 15:49:51 +08:00
|
|
|
|
</a-space>
|
2026-05-13 17:25:13 +08:00
|
|
|
|
<BasicTree
|
|
|
|
|
|
ref="whitelistTreeRef"
|
|
|
|
|
|
checkable
|
|
|
|
|
|
:treeData="whitelistTreeData"
|
|
|
|
|
|
:checkedKeys="checkedKeysWhitelist"
|
|
|
|
|
|
:expandedKeys="expandedKeysWhitelist"
|
|
|
|
|
|
:selectedKeys="selectedKeysWhitelist"
|
|
|
|
|
|
:clickRowToExpand="false"
|
|
|
|
|
|
:checkStrictly="true"
|
|
|
|
|
|
title="系统菜单(权限树)"
|
|
|
|
|
|
@check="onWhitelistCheck"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #title="{ slotTitle, ruleFlag }">
|
|
|
|
|
|
{{ slotTitle }}
|
|
|
|
|
|
<Icon v-if="ruleFlag" icon="ant-design:align-left-outlined" style="margin-left: 5px; color: red" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</BasicTree>
|
2026-05-13 15:49:51 +08:00
|
|
|
|
</a-spin>
|
|
|
|
|
|
</BasicModal>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
|
|
|
|
|
import { computed, ref, unref } from 'vue';
|
|
|
|
|
|
import { BasicTable, TableAction, useTable } from '/@/components/Table';
|
|
|
|
|
|
import { BasicModal, useModal } from '/@/components/Modal';
|
2026-05-13 17:25:13 +08:00
|
|
|
|
import { BasicTree, TreeItem } from '/@/components/Tree';
|
|
|
|
|
|
import { Icon } from '/@/components/Icon';
|
2026-05-13 15:49:51 +08:00
|
|
|
|
import { useMessage } from '/@/hooks/web/useMessage';
|
2026-05-13 17:25:13 +08:00
|
|
|
|
import { useI18n } from '/@/hooks/web/useI18n';
|
2026-05-13 15:49:51 +08:00
|
|
|
|
import { columns } from './bizTemplateBind.data';
|
|
|
|
|
|
import * as Api from './bizTemplateBind.api';
|
|
|
|
|
|
import { list as tplList } from '../template/printTemplate.api';
|
2026-05-13 17:25:13 +08:00
|
|
|
|
import { queryTreeListForRole } from '/@/views/system/role/role.api';
|
2026-05-13 15:49:51 +08:00
|
|
|
|
|
|
|
|
|
|
const { createMessage } = useMessage();
|
2026-05-13 17:25:13 +08:00
|
|
|
|
const { t } = useI18n();
|
2026-05-13 15:49:51 +08:00
|
|
|
|
|
2026-05-14 17:11:15 +08:00
|
|
|
|
/** 下拉「空占位符」选项值(落库 fieldMappingJson 的 bizField 转为 '') */
|
|
|
|
|
|
const EMPTY_BIZ_FIELD_SENTINEL = '__PRINT_BIND_EMPTY_BIZ__';
|
|
|
|
|
|
|
2026-05-13 15:49:51 +08:00
|
|
|
|
interface BizTypeItem {
|
|
|
|
|
|
bizCode: string;
|
|
|
|
|
|
bizName: string;
|
|
|
|
|
|
fields: { fieldKey: string; label: string; description?: string }[];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface TplFieldItem {
|
|
|
|
|
|
bindField: string;
|
|
|
|
|
|
elementType?: string;
|
|
|
|
|
|
titleHint?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface MappingRow {
|
|
|
|
|
|
templateField: string;
|
|
|
|
|
|
bizField?: string;
|
|
|
|
|
|
elementType?: string;
|
|
|
|
|
|
titleHint?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 17:25:13 +08:00
|
|
|
|
interface DetailSlotItem {
|
|
|
|
|
|
propertyName: string;
|
|
|
|
|
|
itemEntityClassFqn: string;
|
|
|
|
|
|
slotKind: string;
|
|
|
|
|
|
label: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 15:49:51 +08:00
|
|
|
|
const bizTypesRef = ref<BizTypeItem[]>([]);
|
|
|
|
|
|
const tplListRef = ref<{ id: string; templateCode: string; templateName: string }[]>([]);
|
2026-05-13 17:25:13 +08:00
|
|
|
|
/** 弹窗内:业务列表 + 模板下拉并行加载中(不再阻塞 openModal,避免点按钮好几秒才出框) */
|
|
|
|
|
|
const modalDataLoading = ref(false);
|
2026-05-13 15:49:51 +08:00
|
|
|
|
const parseLoading = ref(false);
|
|
|
|
|
|
const modalSubmitLoading = ref(false);
|
|
|
|
|
|
const previewLoading = ref(false);
|
|
|
|
|
|
const previewBizJson = ref('');
|
|
|
|
|
|
const previewResult = ref('');
|
|
|
|
|
|
|
|
|
|
|
|
const form = ref({
|
|
|
|
|
|
id: '' as string | undefined,
|
|
|
|
|
|
bizCode: undefined as string | undefined,
|
|
|
|
|
|
bizName: '' as string | undefined,
|
|
|
|
|
|
templateId: undefined as string | undefined,
|
|
|
|
|
|
remark: '' as string | undefined,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const tplFields = ref<TplFieldItem[]>([]);
|
|
|
|
|
|
const bizFields = ref<BizTypeItem['fields']>([]);
|
|
|
|
|
|
const mappingRows = ref<MappingRow[]>([]);
|
2026-05-13 17:25:13 +08:00
|
|
|
|
const detailSlots = ref<DetailSlotItem[]>([]);
|
|
|
|
|
|
const selectedDetailProperty = ref<string | undefined>(undefined);
|
|
|
|
|
|
const detailBizFields = ref<BizTypeItem['fields']>([]);
|
|
|
|
|
|
const detailFieldsLoading = ref(false);
|
2026-05-13 15:49:51 +08:00
|
|
|
|
|
|
|
|
|
|
const isEditMode = ref(false);
|
|
|
|
|
|
const modalTitle = computed(() => (unref(isEditMode) ? '编辑业务打印绑定' : '新增业务打印绑定'));
|
|
|
|
|
|
|
|
|
|
|
|
const bizSelectOptions = computed(() =>
|
|
|
|
|
|
unref(bizTypesRef).map((b) => ({
|
|
|
|
|
|
label: `${b.bizName}(${b.bizCode})`,
|
|
|
|
|
|
value: b.bizCode,
|
|
|
|
|
|
})),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const tplSelectOptions = computed(() =>
|
|
|
|
|
|
unref(tplListRef).map((t) => ({
|
|
|
|
|
|
label: `${t.templateName}(${t.templateCode})`,
|
|
|
|
|
|
value: t.id,
|
|
|
|
|
|
})),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-05-13 17:25:13 +08:00
|
|
|
|
const detailSlotSelectOptions = computed(() =>
|
|
|
|
|
|
unref(detailSlots).map((s) => ({
|
|
|
|
|
|
label: `${s.label}(${s.propertyName} · ${s.slotKind})`,
|
|
|
|
|
|
value: s.propertyName,
|
|
|
|
|
|
})),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-05-14 17:11:15 +08:00
|
|
|
|
const bizFieldOptionsMain = computed(() => {
|
|
|
|
|
|
const head = [{ label: '— 空占位符(不参与业务 JSON)—', value: EMPTY_BIZ_FIELD_SENTINEL }];
|
|
|
|
|
|
const rest = unref(bizFields).map((f) => ({
|
2026-05-13 15:49:51 +08:00
|
|
|
|
label: f.label ? `${f.label}(${f.fieldKey})` : f.fieldKey,
|
|
|
|
|
|
value: f.fieldKey,
|
2026-05-14 17:11:15 +08:00
|
|
|
|
}));
|
|
|
|
|
|
return [...head, ...rest];
|
|
|
|
|
|
});
|
2026-05-13 15:49:51 +08:00
|
|
|
|
|
2026-05-13 17:25:13 +08:00
|
|
|
|
/** 主表 + 明细前缀字段(用于明细/表格占位) */
|
|
|
|
|
|
const bizFieldOptions = computed(() => {
|
2026-05-14 17:11:15 +08:00
|
|
|
|
const head = [{ label: '— 空占位符(不参与业务 JSON)—', value: EMPTY_BIZ_FIELD_SENTINEL }];
|
2026-05-13 17:25:13 +08:00
|
|
|
|
const main = unref(bizFields).map((f) => ({
|
|
|
|
|
|
label: f.label ? `${f.label}(${f.fieldKey})` : f.fieldKey,
|
|
|
|
|
|
value: f.fieldKey,
|
|
|
|
|
|
}));
|
|
|
|
|
|
const detail = unref(detailBizFields).map((f) => ({
|
|
|
|
|
|
label: f.label ? `${f.label}(${f.fieldKey})` : f.fieldKey,
|
|
|
|
|
|
value: f.fieldKey,
|
|
|
|
|
|
}));
|
2026-05-14 17:11:15 +08:00
|
|
|
|
return [...head, ...main, ...detail];
|
2026-05-13 17:25:13 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-14 17:11:15 +08:00
|
|
|
|
/** 已保存的空字符串映射为下拉哨兵,便于展示「空占位符」项 */
|
|
|
|
|
|
function normalizeBizFieldForUi(raw?: string) {
|
|
|
|
|
|
if (raw === undefined || raw === null || raw === '') {
|
|
|
|
|
|
return EMPTY_BIZ_FIELD_SENTINEL;
|
|
|
|
|
|
}
|
|
|
|
|
|
return raw;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 提交前哨兵还原为空字符串 */
|
|
|
|
|
|
function denormalizeBizFieldForSave(v?: string) {
|
|
|
|
|
|
if (v === EMPTY_BIZ_FIELD_SENTINEL || v === undefined || v === null || v === '') {
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
return v;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 17:25:13 +08:00
|
|
|
|
/** 主表参数行:模板 elementType 为 param */
|
|
|
|
|
|
const mappingRowsParam = computed(() =>
|
|
|
|
|
|
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: '主表·参数',
|
|
|
|
|
|
detailField: '模板·明细',
|
|
|
|
|
|
column: '表格列',
|
|
|
|
|
|
};
|
|
|
|
|
|
return m[t || ''] || (t || '—');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const mapTableColumnsParam = [
|
|
|
|
|
|
{ title: '模板参数占位(bindField)', dataIndex: 'templateField', width: 220 },
|
|
|
|
|
|
{ title: '标题/提示', dataIndex: 'titleHint', ellipsis: true },
|
|
|
|
|
|
{ title: '业务字段(主表)', key: 'bizField', width: 280 },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const mapTableColumnsDetail = [
|
|
|
|
|
|
{ title: '模板类型', key: 'tplKind', width: 104, ellipsis: true },
|
|
|
|
|
|
{ title: '模板占位(bindField)', dataIndex: 'templateField', width: 188 },
|
2026-05-13 15:49:51 +08:00
|
|
|
|
{ title: '标题/提示', dataIndex: 'titleHint', ellipsis: true },
|
|
|
|
|
|
{ title: '业务字段', key: 'bizField', width: 260 },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const [registerTable, { reload }] = useTable({
|
|
|
|
|
|
title: '业务打印绑定',
|
|
|
|
|
|
api: Api.list,
|
|
|
|
|
|
columns,
|
|
|
|
|
|
useSearchForm: false,
|
|
|
|
|
|
showTableSetting: true,
|
|
|
|
|
|
bordered: true,
|
|
|
|
|
|
showIndexColumn: true,
|
|
|
|
|
|
// 必须与模板插槽 #action 对应,否则操作列不会渲染(useTable 无 useListPage 的默认 slots)
|
|
|
|
|
|
actionColumn: {
|
|
|
|
|
|
width: 160,
|
|
|
|
|
|
title: '操作',
|
|
|
|
|
|
fixed: 'right',
|
|
|
|
|
|
dataIndex: 'action',
|
|
|
|
|
|
slots: { customRender: 'action' },
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const [registerModal, { openModal, closeModal }] = useModal();
|
2026-05-13 17:25:13 +08:00
|
|
|
|
const [registerWhitelistModal, { openModal: openWhitelistDlg, closeModal: closeWhitelistDlg }] = useModal();
|
|
|
|
|
|
|
|
|
|
|
|
const whitelistLoading = ref(false);
|
|
|
|
|
|
const whitelistSubmitLoading = ref(false);
|
|
|
|
|
|
const whitelistTreeRef = ref<InstanceType<typeof BasicTree> | null>(null);
|
|
|
|
|
|
const whitelistTreeData = ref<TreeItem[]>([]);
|
|
|
|
|
|
const allWhitelistTreeKeys = ref<string[]>([]);
|
|
|
|
|
|
const checkedKeysWhitelist = ref<any>([]);
|
|
|
|
|
|
const expandedKeysWhitelist = ref<any>([]);
|
|
|
|
|
|
const selectedKeysWhitelist = ref<any>([]);
|
|
|
|
|
|
|
|
|
|
|
|
/** 将菜单树标题中的 t('...') 表达式转为文案(与角色授权树一致) */
|
|
|
|
|
|
function translateWhitelistTitle(data: TreeItem[] | undefined): TreeItem[] {
|
|
|
|
|
|
if (data?.length) {
|
|
|
|
|
|
data.forEach((item) => {
|
|
|
|
|
|
if (item.slotTitle && typeof item.slotTitle === 'string' && item.slotTitle.includes("t('")) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
item.slotTitle = new Function('t', `return ${item.slotTitle}`)(t) as string;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
/* 忽略解析失败 */
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (item.children?.length) {
|
|
|
|
|
|
translateWhitelistTitle(item.children as TreeItem[]);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
return data ?? [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function expandWhitelistTree(expand: boolean) {
|
|
|
|
|
|
expandedKeysWhitelist.value = expand ? [...unref(allWhitelistTreeKeys)] : [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onWhitelistCheck(o: { checked?: any } | any) {
|
|
|
|
|
|
checkedKeysWhitelist.value = o?.checked !== undefined ? o.checked : o;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 树勾选键转为字符串数组(兼容 checkStrictly) */
|
|
|
|
|
|
function normalizeCheckedKeys(keys: unknown): string[] {
|
|
|
|
|
|
if (Array.isArray(keys)) {
|
|
|
|
|
|
return keys.map((k) => String(k));
|
|
|
|
|
|
}
|
|
|
|
|
|
if (keys && typeof keys === 'object' && 'checked' in (keys as object)) {
|
|
|
|
|
|
const c = (keys as { checked?: unknown }).checked;
|
|
|
|
|
|
if (Array.isArray(c)) {
|
|
|
|
|
|
return c.map((k) => String(k));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function openPrintBizWhitelist() {
|
|
|
|
|
|
whitelistLoading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const [treeResult, wl] = await Promise.all([queryTreeListForRole(), Api.getPermWhitelist()]);
|
|
|
|
|
|
whitelistTreeData.value = translateWhitelistTitle(treeResult.treeList);
|
|
|
|
|
|
allWhitelistTreeKeys.value = treeResult.ids || [];
|
|
|
|
|
|
expandedKeysWhitelist.value = treeResult.ids || [];
|
|
|
|
|
|
checkedKeysWhitelist.value = wl?.permIds?.length ? [...wl.permIds] : [];
|
|
|
|
|
|
openWhitelistDlg(true);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
whitelistLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function submitWhitelistModal() {
|
|
|
|
|
|
whitelistSubmitLoading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const tree = unref(whitelistTreeRef) as any;
|
|
|
|
|
|
let raw = tree?.getCheckedKeys?.() ?? unref(checkedKeysWhitelist);
|
|
|
|
|
|
const permIds = normalizeCheckedKeys(raw);
|
|
|
|
|
|
await Api.savePermWhitelist({ permIds });
|
|
|
|
|
|
createMessage.success('保存成功');
|
|
|
|
|
|
closeWhitelistDlg();
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
whitelistSubmitLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-13 15:49:51 +08:00
|
|
|
|
|
|
|
|
|
|
async function loadBizTypes() {
|
2026-05-13 17:25:13 +08:00
|
|
|
|
const res = await Api.bizTypesForBinding();
|
2026-05-13 15:49:51 +08:00
|
|
|
|
bizTypesRef.value = res || [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadAllTemplates() {
|
2026-05-13 17:25:13 +08:00
|
|
|
|
const res = await tplList({ pageNo: 1, pageSize: 500 });
|
|
|
|
|
|
tplListRef.value = res?.records ?? [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function refreshDetailSlots(code: string | undefined) {
|
|
|
|
|
|
detailSlots.value = [];
|
|
|
|
|
|
selectedDetailProperty.value = undefined;
|
|
|
|
|
|
detailBizFields.value = [];
|
|
|
|
|
|
if (!code) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
detailSlots.value = (await Api.detailSlots(code)) || [];
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
detailSlots.value = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
2026-05-13 15:49:51 +08:00
|
|
|
|
try {
|
2026-05-13 17:25:13 +08:00
|
|
|
|
const list = await Api.bizFieldsForDetailSlot({
|
|
|
|
|
|
bizCode: form.value.bizCode,
|
|
|
|
|
|
detailProperty: propertyName,
|
|
|
|
|
|
slotKind: kind,
|
|
|
|
|
|
});
|
|
|
|
|
|
detailBizFields.value = (list || []) as BizTypeItem['fields'];
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
detailBizFields.value = [];
|
2026-05-13 15:49:51 +08:00
|
|
|
|
} finally {
|
2026-05-13 17:25:13 +08:00
|
|
|
|
detailFieldsLoading.value = false;
|
2026-05-13 15:49:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 17:25:13 +08:00
|
|
|
|
async function onBizCodeChange(code: string) {
|
2026-05-13 15:49:51 +08:00
|
|
|
|
const hit = unref(bizTypesRef).find((b) => b.bizCode === code);
|
|
|
|
|
|
bizFields.value = hit?.fields ?? [];
|
|
|
|
|
|
form.value.bizName = hit?.bizName;
|
2026-05-13 17:25:13 +08:00
|
|
|
|
await refreshDetailSlots(code);
|
2026-05-13 15:49:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function onTemplateChange() {
|
|
|
|
|
|
tplFields.value = [];
|
|
|
|
|
|
mappingRows.value = [];
|
|
|
|
|
|
await reloadTemplateFields();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function reloadTemplateFields() {
|
|
|
|
|
|
const tid = form.value.templateId;
|
|
|
|
|
|
if (!tid) {
|
|
|
|
|
|
tplFields.value = [];
|
|
|
|
|
|
mappingRows.value = [];
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
parseLoading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const list = (await Api.parseTemplateFields(tid)) as TplFieldItem[];
|
|
|
|
|
|
tplFields.value = list || [];
|
|
|
|
|
|
rebuildMappingRows();
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
parseLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function rebuildMappingRows() {
|
|
|
|
|
|
const saved = unref(savedMappingRef);
|
2026-05-14 10:43:51 +08:00
|
|
|
|
const tplList = unref(tplFields);
|
|
|
|
|
|
const tplByField = new Map(tplList.map((t) => [t.bindField, t]));
|
|
|
|
|
|
|
|
|
|
|
|
// 先按已保存 JSON 的顺序收录键(含「模板未再声明」的占位如 Parameter3 + 空 bizField),避免仅依赖解析结果而丢行
|
|
|
|
|
|
const orderedKeys: string[] = [];
|
|
|
|
|
|
const seen = new Set<string>();
|
|
|
|
|
|
for (const s of saved) {
|
|
|
|
|
|
const k = (s.templateField || '').trim();
|
|
|
|
|
|
if (!k || seen.has(k)) continue;
|
|
|
|
|
|
seen.add(k);
|
|
|
|
|
|
orderedKeys.push(k);
|
|
|
|
|
|
}
|
|
|
|
|
|
for (const t of tplList) {
|
|
|
|
|
|
const k = (t.bindField || '').trim();
|
|
|
|
|
|
if (!k || seen.has(k)) continue;
|
|
|
|
|
|
seen.add(k);
|
|
|
|
|
|
orderedKeys.push(k);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
mappingRows.value = orderedKeys.map((templateField) => {
|
|
|
|
|
|
const t = tplByField.get(templateField);
|
2026-05-13 15:49:51 +08:00
|
|
|
|
const hit = saved.find((x) => x.templateField === templateField);
|
|
|
|
|
|
return {
|
|
|
|
|
|
templateField,
|
2026-05-14 17:11:15 +08:00
|
|
|
|
bizField: hit !== undefined ? normalizeBizFieldForUi(hit.bizField) : undefined,
|
2026-05-14 10:43:51 +08:00
|
|
|
|
elementType: t?.elementType || 'param',
|
|
|
|
|
|
titleHint:
|
|
|
|
|
|
t?.titleHint ||
|
|
|
|
|
|
(!t ? '已保存映射(当前模板 JSON 未声明该占位,仍可按空业务字段输出)' : ''),
|
2026-05-13 15:49:51 +08:00
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const savedMappingRef = ref<{ templateField: string; bizField?: string }[]>([]);
|
|
|
|
|
|
|
|
|
|
|
|
function autoMatchFields() {
|
2026-05-13 17:25:13 +08:00
|
|
|
|
const merged = [...unref(bizFields), ...unref(detailBizFields)];
|
|
|
|
|
|
const set = new Map(merged.map((f) => [f.fieldKey, f.fieldKey]));
|
2026-05-13 15:49:51 +08:00
|
|
|
|
for (const row of unref(mappingRows)) {
|
|
|
|
|
|
if (set.has(row.templateField)) {
|
|
|
|
|
|
row.bizField = row.templateField;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
mappingRows.value = [...unref(mappingRows)];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildFieldMappingJson() {
|
|
|
|
|
|
const arr = unref(mappingRows)
|
2026-05-14 10:43:51 +08:00
|
|
|
|
.filter((r) => r.templateField)
|
2026-05-14 17:11:15 +08:00
|
|
|
|
.map((r) => ({
|
|
|
|
|
|
templateField: r.templateField,
|
|
|
|
|
|
bizField: denormalizeBizFieldForSave(r.bizField),
|
|
|
|
|
|
}));
|
2026-05-13 15:49:51 +08:00
|
|
|
|
return JSON.stringify(arr);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function openCreate() {
|
|
|
|
|
|
isEditMode.value = false;
|
|
|
|
|
|
savedMappingRef.value = [];
|
|
|
|
|
|
form.value = { id: undefined, bizCode: undefined, bizName: undefined, templateId: undefined, remark: undefined };
|
|
|
|
|
|
tplFields.value = [];
|
|
|
|
|
|
bizFields.value = [];
|
|
|
|
|
|
mappingRows.value = [];
|
2026-05-13 17:25:13 +08:00
|
|
|
|
detailSlots.value = [];
|
|
|
|
|
|
selectedDetailProperty.value = undefined;
|
|
|
|
|
|
detailBizFields.value = [];
|
2026-05-13 15:49:51 +08:00
|
|
|
|
previewBizJson.value = '';
|
|
|
|
|
|
previewResult.value = '';
|
|
|
|
|
|
openModal(true);
|
2026-05-13 17:25:13 +08:00
|
|
|
|
modalDataLoading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
await Promise.all([loadBizTypes(), loadAllTemplates()]);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
modalDataLoading.value = false;
|
|
|
|
|
|
}
|
2026-05-13 15:49:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function openEdit(record: Recordable) {
|
|
|
|
|
|
isEditMode.value = true;
|
2026-05-13 17:25:13 +08:00
|
|
|
|
try {
|
|
|
|
|
|
savedMappingRef.value = JSON.parse(record.fieldMappingJson || '[]');
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
savedMappingRef.value = [];
|
|
|
|
|
|
}
|
2026-05-13 15:49:51 +08:00
|
|
|
|
form.value = {
|
|
|
|
|
|
id: record.id,
|
|
|
|
|
|
bizCode: record.bizCode,
|
|
|
|
|
|
bizName: record.bizName,
|
|
|
|
|
|
templateId: record.templateId,
|
|
|
|
|
|
remark: record.remark,
|
|
|
|
|
|
};
|
|
|
|
|
|
previewBizJson.value = '';
|
|
|
|
|
|
previewResult.value = '';
|
2026-05-13 17:25:13 +08:00
|
|
|
|
tplFields.value = [];
|
|
|
|
|
|
mappingRows.value = [];
|
|
|
|
|
|
bizFields.value = [];
|
|
|
|
|
|
detailSlots.value = [];
|
|
|
|
|
|
selectedDetailProperty.value = undefined;
|
|
|
|
|
|
detailBizFields.value = [];
|
2026-05-13 15:49:51 +08:00
|
|
|
|
openModal(true);
|
2026-05-13 17:25:13 +08:00
|
|
|
|
modalDataLoading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
await Promise.all([loadBizTypes(), loadAllTemplates()]);
|
|
|
|
|
|
await onBizCodeChange(record.bizCode as string);
|
|
|
|
|
|
await reloadTemplateFields();
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
modalDataLoading.value = false;
|
|
|
|
|
|
}
|
2026-05-13 15:49:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function submitModal() {
|
|
|
|
|
|
if (!form.value.bizCode) {
|
|
|
|
|
|
createMessage.warning('请选择业务');
|
|
|
|
|
|
return Promise.reject();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!form.value.templateId) {
|
|
|
|
|
|
createMessage.warning('请选择打印模板');
|
|
|
|
|
|
return Promise.reject();
|
|
|
|
|
|
}
|
|
|
|
|
|
modalSubmitLoading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const payload = {
|
|
|
|
|
|
id: form.value.id,
|
|
|
|
|
|
bizCode: form.value.bizCode,
|
|
|
|
|
|
bizName: form.value.bizName,
|
|
|
|
|
|
templateId: form.value.templateId,
|
|
|
|
|
|
remark: form.value.remark,
|
|
|
|
|
|
fieldMappingJson: buildFieldMappingJson(),
|
|
|
|
|
|
};
|
|
|
|
|
|
if (unref(isEditMode)) {
|
|
|
|
|
|
await Api.edit(payload);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await Api.add(payload);
|
|
|
|
|
|
}
|
|
|
|
|
|
closeModal();
|
|
|
|
|
|
reload();
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
modalSubmitLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleDelete(record: Recordable) {
|
|
|
|
|
|
await Api.deleteOne({ id: record.id }, () => reload());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function runPreview() {
|
|
|
|
|
|
if (!form.value.bizCode) {
|
|
|
|
|
|
createMessage.warning('请先选择业务;预览读取的是服务器已保存的绑定配置');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
let obj: Record<string, unknown> = {};
|
|
|
|
|
|
try {
|
|
|
|
|
|
obj = previewBizJson.value ? (JSON.parse(previewBizJson.value) as Record<string, unknown>) : {};
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
previewResult.value = '业务 JSON 格式不正确';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
previewLoading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await Api.previewMappedData({ bizCode: form.value.bizCode, bizDataJson: obj });
|
|
|
|
|
|
previewResult.value = JSON.stringify(res, null, 2);
|
|
|
|
|
|
} catch (e: unknown) {
|
|
|
|
|
|
previewResult.value = e instanceof Error ? e.message : String(e);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
previewLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-05-13 17:25:13 +08:00
|
|
|
|
.biz-bind-form {
|
|
|
|
|
|
max-height: calc(100vh - 220px);
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
padding-right: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bind-alert {
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bind-card {
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bind-card :deep(.ant-card-head) {
|
|
|
|
|
|
min-height: 42px;
|
|
|
|
|
|
padding: 0 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bind-card :deep(.ant-card-body) {
|
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bind-card-form :deep(.ant-form-item) {
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bind-card-head {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bind-card-head-extra {
|
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: rgba(0, 0, 0, 0.45);
|
|
|
|
|
|
margin-left: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bind-card-sub {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: rgba(0, 0, 0, 0.45);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bind-card-desc {
|
|
|
|
|
|
margin: 0 0 10px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: rgba(0, 0, 0, 0.65);
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bind-placeholder {
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
color: rgba(0, 0, 0, 0.45);
|
|
|
|
|
|
background: #fafafa;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bind-map-section {
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bind-map-section--detail {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
padding-top: 8px;
|
|
|
|
|
|
border-top: 1px dashed #f0f0f0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bind-section-bar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
|
gap: 8px 12px;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bind-section-title {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: rgba(0, 0, 0, 0.85);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bind-section-hint {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: rgba(0, 0, 0, 0.45);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bind-map-table {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bind-empty {
|
|
|
|
|
|
margin: 8px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 15:49:51 +08:00
|
|
|
|
.preview-pre {
|
|
|
|
|
|
background: #f5f5f5;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
max-height: 240px;
|
|
|
|
|
|
overflow: auto;
|
|
|
|
|
|
font-size: 12px;
|
2026-05-13 17:25:13 +08:00
|
|
|
|
margin-top: 10px;
|
2026-05-13 15:49:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|