新增JeecgBoot BPM流程自动生成器,包含流程创建、修改及审批人配置功能,支持自然语言描述转化为BPMN XML,并通过API与JeecgBoot系统交互。
This commit is contained in:
867
jeecgboot-vue3/src/views/print/template/PrintDesigner.vue
Normal file
867
jeecgboot-vue3/src/views/print/template/PrintDesigner.vue
Normal file
@@ -0,0 +1,867 @@
|
||||
<template>
|
||||
<PageWrapper :title="pageTitle" contentBackground dense contentFullHeight>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="goBack">返回列表</a-button>
|
||||
<a-button type="primary" v-auth="'print:template:edit'" @click="handleSave" :loading="saving">保存模板</a-button>
|
||||
<a-button v-auth="'print:template:edit'" @click="handlePreview">浏览器预览打印</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-card :bordered="false" class="hiprint-card">
|
||||
<a-alert
|
||||
type="info"
|
||||
show-icon
|
||||
message="从左侧拖拽组件到画布;选中控件后在右侧设置属性。文本中可使用变量占位(与业务传入的 JSON 字段对应)。"
|
||||
class="mb-2"
|
||||
/>
|
||||
<a-row :gutter="8" class="designer-row">
|
||||
<a-col :span="4" class="left-col">
|
||||
<div class="col-title">组件库</div>
|
||||
<a-collapse :bordered="false" defaultActiveKey="basic">
|
||||
<a-collapse-panel key="basic" header="常用组件">
|
||||
<div class="ep-draggable-item" tid="defaultModule.text">文本</div>
|
||||
<div class="ep-draggable-item" tid="defaultModule.longText">长文本</div>
|
||||
<div class="ep-draggable-item" tid="defaultModule.html">富文本(HTML)</div>
|
||||
<div class="ep-draggable-item" tid="defaultModule.image">图片</div>
|
||||
<div class="ep-draggable-item" tid="defaultModule.qrcode">二维码</div>
|
||||
<div class="ep-draggable-item" tid="defaultModule.barcode">条形码</div>
|
||||
<div class="ep-draggable-item" tid="defaultModule.hline">横线</div>
|
||||
<div class="ep-draggable-item" tid="defaultModule.vline">竖线</div>
|
||||
<div class="ep-draggable-item" tid="defaultModule.rect">矩形</div>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel key="detail" header="明细表" forceRender>
|
||||
<div class="ep-draggable-item" tid="qhmesModule.tableSimple">普通明细表(默认列)</div>
|
||||
<div class="help-tip">默认普通表头;字段/绑定在右侧属性里修改。</div>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel key="section" header="报表区块(布局)" forceRender>
|
||||
<div class="section-tip">这些是“更像 HttpPrinter”的常用组件预设(本质仍是 Hiprint 元素)。</div>
|
||||
<div class="ep-draggable-item" tid="qhmesModule.reportTitle">报表标题</div>
|
||||
<div class="ep-draggable-item" tid="qhmesModule.subTitle">副标题</div>
|
||||
<div class="ep-draggable-item" tid="qhmesModule.labelValue">标签:值</div>
|
||||
<div class="ep-draggable-item" tid="qhmesModule.pageNo">页码</div>
|
||||
<div class="ep-draggable-item" tid="qhmesModule.qrcode">二维码(预设)</div>
|
||||
<div class="ep-draggable-item" tid="qhmesModule.barcode">条形码(预设)</div>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</a-col>
|
||||
<a-col :span="13" class="center-col">
|
||||
<div class="col-title">画布</div>
|
||||
<div id="hiprint-printTemplate" class="hiprint-printTemplate"></div>
|
||||
<div class="hiprint-printPagination"></div>
|
||||
</a-col>
|
||||
<a-col :span="7" class="right-col">
|
||||
<div class="col-title">属性</div>
|
||||
<div id="PrintElementOptionSetting"></div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-divider>预览数据(JSON,供「预览打印」合并变量)</a-divider>
|
||||
<a-textarea v-model:value="printDataJson" :rows="6" placeholder='例如:{"title":"标题","orderNo":"001"}' />
|
||||
<a-divider>普通明细表列配置(用于 qhmesModule.tableSimple)</a-divider>
|
||||
<a-space direction="vertical" style="width: 100%">
|
||||
<a-textarea
|
||||
v-model:value="tableColumnsJson"
|
||||
:rows="6"
|
||||
placeholder='例如:[{"title":"物料","field":"name","width":90},{"title":"数量","field":"qty","width":45}]'
|
||||
/>
|
||||
<a-space>
|
||||
<a-button @click="buildColumnsFromSampleData">从预览数据推导列</a-button>
|
||||
<a-button type="primary" @click="applyTableColumnsConfig">应用到“普通明细表”组件</a-button>
|
||||
</a-space>
|
||||
</a-space>
|
||||
<a-divider>明细分组配置(可视化,多级)</a-divider>
|
||||
<a-space direction="vertical" style="width: 100%">
|
||||
<a-space>
|
||||
<span>启用分组</span>
|
||||
<a-switch v-model:checked="groupEnabled" />
|
||||
</a-space>
|
||||
<a-space>
|
||||
<span>保真预览(样式优先)</span>
|
||||
<a-switch v-model:checked="preserveDesignStyle" />
|
||||
</a-space>
|
||||
<a-select
|
||||
v-model:value="groupFields"
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
:options="tableFieldOptions"
|
||||
placeholder="选择分组字段(按选择顺序分层:一级→二级→三级)"
|
||||
/>
|
||||
<div class="help-tip">
|
||||
规则:按所选字段顺序做层级分组,相同值将隐藏重复内容并居中展示(视觉合并效果),无需写代码。
|
||||
</div>
|
||||
</a-space>
|
||||
</a-card>
|
||||
</PageWrapper>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, onUnmounted, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { PageWrapper } from '/@/components/Page';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import $ from 'jquery';
|
||||
import { hiprint, defaultElementTypeProvider, disAutoConnect } from 'vue-plugin-hiprint';
|
||||
import 'vue-plugin-hiprint/dist/print-lock.css';
|
||||
import { queryById, saveJson } from './printTemplate.api';
|
||||
import { createQhmesProvider } from './hiprint/qhmesProvider';
|
||||
|
||||
defineOptions({ name: 'PrintDesigner' });
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const tplName = ref('');
|
||||
const saving = ref(false);
|
||||
const printDataJson = ref(
|
||||
'{\n "title": "演示标题",\n "orderNo": "SO20260101001",\n "table": [\n { "name": "物料A", "qty": 1, "amount": 100 }\n ]\n}'
|
||||
);
|
||||
const tableColumnsJson = ref(
|
||||
'[\n { "title": "物料", "field": "name", "width": 90 },\n { "title": "数量", "field": "qty", "width": 45 },\n { "title": "金额", "field": "amount", "width": 45 }\n]'
|
||||
);
|
||||
const groupEnabled = ref(false);
|
||||
const groupFields = ref<string[]>([]);
|
||||
const preserveDesignStyle = ref(true);
|
||||
|
||||
const pageTitle = computed(() => (tplName.value ? `打印设计 - ${tplName.value}` : '打印设计器'));
|
||||
|
||||
let hiprintTemplate: any = null;
|
||||
let hiprintInited = false;
|
||||
let fieldsTimer: any = null;
|
||||
|
||||
function ensureJqueryGlobal() {
|
||||
(window as any).$ = $;
|
||||
(window as any).jQuery = $;
|
||||
}
|
||||
|
||||
function destroyDesigner() {
|
||||
try {
|
||||
hiprintTemplate = null;
|
||||
$('#hiprint-printTemplate').empty();
|
||||
$('#PrintElementOptionSetting').empty();
|
||||
$('.hiprint-printPagination').empty();
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
|
||||
function initHiprintRuntime() {
|
||||
if (hiprintInited) {
|
||||
return;
|
||||
}
|
||||
ensureJqueryGlobal();
|
||||
disAutoConnect();
|
||||
hiprint.init({
|
||||
providers: [new defaultElementTypeProvider(), createQhmesProvider()],
|
||||
lang: 'cn',
|
||||
});
|
||||
hiprintInited = true;
|
||||
}
|
||||
|
||||
function parseTemplateJson(str: string | undefined) {
|
||||
if (!str || str === '{}') {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch {
|
||||
createMessage.warning('模板 JSON 解析失败,已使用空白模板');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function parsePrintData(): Record<string, any> {
|
||||
try {
|
||||
return JSON.parse(printDataJson.value || '{}');
|
||||
} catch {
|
||||
createMessage.error('预览数据不是合法 JSON');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const tableFieldOptions = computed(() => {
|
||||
const data = parsePrintData();
|
||||
const firstRow = Array.isArray(data?.table) && data.table.length > 0 ? data.table[0] : null;
|
||||
if (!firstRow || Object.prototype.toString.call(firstRow) !== '[object Object]') {
|
||||
return [];
|
||||
}
|
||||
return Object.keys(firstRow).map((k) => ({ label: k, value: k }));
|
||||
});
|
||||
|
||||
function buildFieldsFromJson(value: any, prefix = ''): Array<{ field: string; text: string }> {
|
||||
const res: Array<{ field: string; text: string }> = [];
|
||||
if (value == null) {
|
||||
return res;
|
||||
}
|
||||
const isObj = Object.prototype.toString.call(value) === '[object Object]';
|
||||
const isArr = Array.isArray(value);
|
||||
if (!isObj && !isArr) {
|
||||
if (prefix) {
|
||||
res.push({ field: prefix, text: prefix });
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
if (isArr) {
|
||||
// 数组字段:允许绑定数组本身,也允许绑定数组元素的字段(table[0].xx 推导为 table.xx)
|
||||
if (prefix) {
|
||||
res.push({ field: prefix, text: prefix });
|
||||
}
|
||||
const first = value.length > 0 ? value[0] : null;
|
||||
if (first && Object.prototype.toString.call(first) === '[object Object]') {
|
||||
Object.keys(first).forEach((k) => {
|
||||
const f = prefix ? `${prefix}.${k}` : k;
|
||||
res.push({ field: f, text: f });
|
||||
});
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
Object.keys(value).forEach((k) => {
|
||||
const f = prefix ? `${prefix}.${k}` : k;
|
||||
const v = value[k];
|
||||
// 先把自己加进去(让属性面板可直接选择)
|
||||
res.push({ field: f, text: f });
|
||||
// 再递归对象/数组
|
||||
res.push(...buildFieldsFromJson(v, f));
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
function applyFieldsToTemplate() {
|
||||
if (!hiprintTemplate) {
|
||||
return;
|
||||
}
|
||||
const data = parsePrintData();
|
||||
const fields = buildFieldsFromJson(data);
|
||||
// 去重
|
||||
const uniq = new Map<string, { field: string; text: string }>();
|
||||
fields.forEach((it) => {
|
||||
if (it?.field && !uniq.has(it.field)) {
|
||||
uniq.set(it.field, it);
|
||||
}
|
||||
});
|
||||
try {
|
||||
hiprintTemplate.setFields(Array.from(uniq.values()));
|
||||
} catch (e) {
|
||||
// setFields 非强依赖,失败不影响设计器
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
|
||||
type SimpleColumn = {
|
||||
title: string;
|
||||
field: string;
|
||||
width?: number;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
};
|
||||
|
||||
function normalizeColumns(cols: any[]): any[] {
|
||||
return cols.map((c) => ({
|
||||
title: c.title || c.field || '列',
|
||||
field: c.field || '',
|
||||
width: Number(c.width || 60),
|
||||
align: c.align || 'left',
|
||||
colspan: 1,
|
||||
rowspan: 1,
|
||||
}));
|
||||
}
|
||||
|
||||
function getTableSimpleFormatter() {
|
||||
// 与 qhmesProvider.ts 保持一致:多级分组按 groupFields 顺序计算 rowspan
|
||||
return `
|
||||
function(t,e,printData){
|
||||
var opts = (t && t.options) ? t.options : {};
|
||||
var list = printData && Array.isArray(printData[opts.field || 'table']) ? printData[opts.field || 'table'] : [];
|
||||
var globalCols = printData && Array.isArray(printData.__qhmesTableColumns) ? printData.__qhmesTableColumns : [];
|
||||
var columns = Array.isArray(opts.columns) && opts.columns.length ? opts.columns : (globalCols.length ? globalCols : [
|
||||
{ title: '物料', field: 'name', width: 90, align: 'left' },
|
||||
{ title: '数量', field: 'qty', width: 45, align: 'right' },
|
||||
{ title: '金额', field: 'amount', width: 45, align: 'right' }
|
||||
]);
|
||||
var globalGroups = printData && Array.isArray(printData.__qhmesGroupFields) ? printData.__qhmesGroupFields : [];
|
||||
var groupFields = Array.isArray(opts.groupFields) && opts.groupFields.length ? opts.groupFields : globalGroups;
|
||||
var style = opts.__qhmesStyle || {};
|
||||
var fontSize = style.fontSize || 10;
|
||||
var borderColor = style.borderColor || '#000';
|
||||
var borderWidth = style.borderWidth || 1;
|
||||
var cellPadding = style.cellPadding || '2pt 4pt';
|
||||
var headerBg = style.headerBg || '';
|
||||
var tableWidth = style.tableWidth || '100%';
|
||||
function esc(v){
|
||||
if (v === null || v === undefined) return '';
|
||||
return String(v).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
function isGroupCol(field){ return groupFields.indexOf(field) > -1; }
|
||||
var rowspanMap = {};
|
||||
for (var c=0;c<columns.length;c++){ var f = columns[c].field; rowspanMap[f] = new Array(list.length).fill(1); }
|
||||
for (var g=0; g<groupFields.length; g++){
|
||||
var gf = groupFields[g];
|
||||
// 分组字段可能不在 columns 中,需先兜底初始化,避免 rowspanMap[gf] 未定义报错
|
||||
if (!rowspanMap[gf]) rowspanMap[gf] = new Array(list.length).fill(1);
|
||||
var i = 0;
|
||||
while(i < list.length){
|
||||
var j = i + 1;
|
||||
while(j < list.length){
|
||||
var upperOk = true;
|
||||
for (var up=0; up<g; up++){
|
||||
var uf = groupFields[up];
|
||||
if ((list[j][uf] ?? '') !== (list[i][uf] ?? '')) { upperOk = false; break; }
|
||||
}
|
||||
if (!upperOk) break;
|
||||
if ((list[j][gf] ?? '') === (list[i][gf] ?? '')) j++; else break;
|
||||
}
|
||||
var span = j - i;
|
||||
if (!rowspanMap[gf]) rowspanMap[gf] = new Array(list.length).fill(1);
|
||||
rowspanMap[gf][i] = span;
|
||||
for (var k=i+1;k<j;k++) rowspanMap[gf][k] = 0;
|
||||
i = j;
|
||||
}
|
||||
}
|
||||
var html = '<table style="width:'+tableWidth+';border-collapse:collapse;table-layout:fixed;font-size:'+fontSize+'pt;">';
|
||||
html += '<thead><tr>';
|
||||
for (var h=0;h<columns.length;h++){
|
||||
var hc = columns[h];
|
||||
var hw = hc.width ? ('width:'+hc.width+'pt;') : '';
|
||||
var hbg = headerBg ? ('background:'+headerBg+';') : '';
|
||||
html += '<th style="border:'+borderWidth+'px solid '+borderColor+';padding:'+cellPadding+';text-align:center;'+hbg+hw+'">'+esc(hc.title || hc.field || '')+'</th>';
|
||||
}
|
||||
html += '</tr></thead><tbody>';
|
||||
for (var r=0;r<list.length;r++){
|
||||
html += '<tr>';
|
||||
for (var cc=0;cc<columns.length;cc++){
|
||||
var col = columns[cc];
|
||||
var field = col.field;
|
||||
var align = col.align || 'left';
|
||||
if (isGroupCol(field)){
|
||||
var rs = rowspanMap[field][r];
|
||||
if (rs > 0){
|
||||
html += '<td rowspan="'+rs+'" style="border:'+borderWidth+'px solid '+borderColor+';padding:'+cellPadding+';text-align:center;vertical-align:middle;">'+esc(list[r][field])+'</td>';
|
||||
}
|
||||
} else {
|
||||
html += '<td style="border:'+borderWidth+'px solid '+borderColor+';padding:'+cellPadding+';text-align:'+align+';">'+esc(list[r][field])+'</td>';
|
||||
}
|
||||
}
|
||||
html += '</tr>';
|
||||
}
|
||||
html += '</tbody></table>';
|
||||
return html;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function parseTemplateObjectFromInstance(): any {
|
||||
if (!hiprintTemplate) return null;
|
||||
const json = hiprintTemplate.getJson();
|
||||
if (typeof json === 'string') {
|
||||
try {
|
||||
return JSON.parse(json);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return json || null;
|
||||
}
|
||||
|
||||
function patchTableSimpleElements(templateObj: any): number {
|
||||
if (!templateObj) return 0;
|
||||
|
||||
let parsedCols: any[] = [];
|
||||
try {
|
||||
parsedCols = JSON.parse(tableColumnsJson.value || '[]');
|
||||
} catch {
|
||||
parsedCols = [];
|
||||
}
|
||||
const mergedCols = normalizeColumns(parsedCols).map((c) => ({
|
||||
title: c.title,
|
||||
field: c.field,
|
||||
width: c.width,
|
||||
align: c.align,
|
||||
}));
|
||||
const mergedGroups = groupEnabled.value ? [...groupFields.value] : [];
|
||||
let changedCount = 0;
|
||||
let candidateCount = 0;
|
||||
|
||||
const visited = new Set<any>();
|
||||
const walk = (node: any) => {
|
||||
if (!node || typeof node !== 'object' || visited.has(node)) return;
|
||||
visited.add(node);
|
||||
|
||||
const hasOptionsObj = node.options && typeof node.options === 'object';
|
||||
// 安全策略:只处理系统托管组件,避免覆盖用户已有明细组件
|
||||
const isManagedTableElement =
|
||||
node?.tid === 'qhmesModule.tableSimple' || (hasOptionsObj && node.options.__qhmesManaged === true);
|
||||
|
||||
if (isManagedTableElement) {
|
||||
candidateCount++;
|
||||
node.type = 'html';
|
||||
node.tid = 'qhmesModule.tableSimple';
|
||||
node.options = node.options || {};
|
||||
node.options.__qhmesManaged = true;
|
||||
node.options.field = node.options.field || 'table';
|
||||
node.options.columns = mergedCols;
|
||||
node.options.groupFields = mergedGroups;
|
||||
node.options.formatter = getTableSimpleFormatter();
|
||||
changedCount++;
|
||||
}
|
||||
|
||||
// 递归所有子属性(兼容 printPanels/panels/printElements 等不同结构)
|
||||
Object.keys(node).forEach((k) => {
|
||||
const v = node[k];
|
||||
if (Array.isArray(v)) {
|
||||
v.forEach((item) => walk(item));
|
||||
} else if (v && typeof v === 'object') {
|
||||
walk(v);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
walk(templateObj);
|
||||
(templateObj as any).__qhmesPatchStat = { changedCount, candidateCount };
|
||||
return changedCount;
|
||||
}
|
||||
|
||||
function syncExistingTableSimpleElements(showTip = false) {
|
||||
if (!hiprintTemplate) return;
|
||||
const templateObj = parseTemplateObjectFromInstance();
|
||||
if (!templateObj) return;
|
||||
const changedCount = patchTableSimpleElements(templateObj);
|
||||
const stat = (templateObj as any).__qhmesPatchStat || { changedCount, candidateCount: 0 };
|
||||
if (changedCount > 0) {
|
||||
try {
|
||||
hiprintTemplate.update(templateObj);
|
||||
if (showTip) {
|
||||
createMessage.success(`已同步 ${changedCount} 个明细组件分组配置(识别候选 ${stat.candidateCount})`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
} else if (showTip) {
|
||||
createMessage.warning('未识别到系统托管的分组明细组件,请先拖入“普通明细表(默认列)”后再应用');
|
||||
}
|
||||
}
|
||||
|
||||
function buildPreviewTemplateWithGrouping() {
|
||||
const templateObj = parseTemplateObjectFromInstance();
|
||||
if (!templateObj) return null;
|
||||
// 仅用于预览的临时模板,避免污染设计器当前画布
|
||||
const previewTemplate = JSON.parse(JSON.stringify(templateObj));
|
||||
|
||||
let parsedCols: any[] = [];
|
||||
try {
|
||||
parsedCols = JSON.parse(tableColumnsJson.value || '[]');
|
||||
} catch {
|
||||
parsedCols = [];
|
||||
}
|
||||
const mergedCols = normalizeColumns(parsedCols).map((c) => ({
|
||||
title: c.title,
|
||||
field: c.field,
|
||||
width: c.width,
|
||||
align: c.align,
|
||||
}));
|
||||
const mergedGroups = groupEnabled.value ? [...groupFields.value] : [];
|
||||
|
||||
const extractColumnsFromElement = (el: any) => {
|
||||
// 1) 新结构:options.columns
|
||||
if (Array.isArray(el?.options?.columns) && el.options.columns.length) {
|
||||
return el.options.columns.map((c: any) => ({
|
||||
title: c.title || c.field || '列',
|
||||
field: c.field || '',
|
||||
width: Number(c.width || 60),
|
||||
align: c.align || 'left',
|
||||
}));
|
||||
}
|
||||
// 2) 旧 table 结构:element.columns(二维表头)
|
||||
if (Array.isArray(el?.columns) && el.columns.length) {
|
||||
const headerRow = Array.isArray(el.columns[0]) ? el.columns[0] : [];
|
||||
if (headerRow.length) {
|
||||
return headerRow.map((c: any) => ({
|
||||
title: c.title || c.field || '列',
|
||||
field: c.field || '',
|
||||
width: Number(c.width || 60),
|
||||
align: c.align || 'left',
|
||||
}));
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const visited = new Set<any>();
|
||||
const walk = (node: any) => {
|
||||
if (!node || typeof node !== 'object' || visited.has(node)) return;
|
||||
visited.add(node);
|
||||
|
||||
// 预览阶段宽松匹配:尽可能覆盖用户已有明细表组件
|
||||
const hasOptionsObj = node.options && typeof node.options === 'object';
|
||||
const maybeTableElement =
|
||||
node?.tid === 'qhmesModule.tableSimple' ||
|
||||
node?.tid === 'defaultModule.table' ||
|
||||
node?.type === 'table' ||
|
||||
(hasOptionsObj && Array.isArray(node.options.columns) && (node.options.field === 'table' || !node.options.field));
|
||||
|
||||
if (maybeTableElement) {
|
||||
const originOptions = node.options || {};
|
||||
const originColumns = extractColumnsFromElement(node);
|
||||
node.type = 'html';
|
||||
node.tid = 'qhmesModule.tableSimple';
|
||||
node.options = node.options || {};
|
||||
node.options.__qhmesManaged = true;
|
||||
node.options.field = node.options.field || 'table';
|
||||
// 优先保留用户已有列定义(包括旧 table 的 element.columns),其次才用可视化配置列
|
||||
if (originColumns.length > 0) {
|
||||
node.options.columns = originColumns;
|
||||
} else if (!Array.isArray(node.options.columns) || node.options.columns.length === 0) {
|
||||
node.options.columns = mergedCols;
|
||||
}
|
||||
node.options.groupFields = mergedGroups;
|
||||
node.options.__qhmesStyle = {
|
||||
fontSize: originOptions.fontSize,
|
||||
borderColor: originOptions.borderColor,
|
||||
borderWidth: originOptions.borderWidth,
|
||||
cellPadding: originOptions.cellPadding,
|
||||
headerBg: originOptions.headerBg,
|
||||
tableWidth: originOptions.tableWidth,
|
||||
};
|
||||
node.options.formatter = getTableSimpleFormatter();
|
||||
}
|
||||
|
||||
Object.keys(node).forEach((k) => {
|
||||
const v = node[k];
|
||||
if (Array.isArray(v)) v.forEach((it) => walk(it));
|
||||
else if (v && typeof v === 'object') walk(v);
|
||||
});
|
||||
};
|
||||
|
||||
walk(previewTemplate);
|
||||
return previewTemplate;
|
||||
}
|
||||
|
||||
function applyTableColumnsConfig(silent = false) {
|
||||
let cols: SimpleColumn[] = [];
|
||||
try {
|
||||
const parsed = JSON.parse(tableColumnsJson.value || '[]');
|
||||
if (!Array.isArray(parsed) || parsed.length === 0) {
|
||||
createMessage.warning('列配置必须是非空数组');
|
||||
return;
|
||||
}
|
||||
cols = parsed;
|
||||
} catch {
|
||||
createMessage.error('明细列配置 JSON 格式错误');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 动态更新 provider 元素定义:后续拖入的“普通明细表”会使用新列配置
|
||||
hiprint.updateElementType('qhmesModule.tableSimple', (type: any) => {
|
||||
const mergedCols = normalizeColumns(cols).map((col) => {
|
||||
if (groupEnabled.value && groupFields.value.includes(col.field)) {
|
||||
return { ...col, align: 'center' };
|
||||
}
|
||||
return col;
|
||||
});
|
||||
type.options = type.options || {};
|
||||
type.options.columns = mergedCols.map((c) => ({
|
||||
title: c.title,
|
||||
field: c.field,
|
||||
width: c.width,
|
||||
align: c.align,
|
||||
}));
|
||||
type.options.groupFields = groupEnabled.value ? [...groupFields.value] : [];
|
||||
return type;
|
||||
});
|
||||
// 同步到当前画布上已存在的“普通明细表”组件,避免只影响新拖拽组件
|
||||
syncExistingTableSimpleElements(!silent);
|
||||
if (!silent) {
|
||||
createMessage.success('已更新普通明细表列与分组配置');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
createMessage.error('更新组件列配置失败');
|
||||
}
|
||||
}
|
||||
|
||||
function buildColumnsFromSampleData() {
|
||||
const data = parsePrintData();
|
||||
const firstRow = Array.isArray(data?.table) && data.table.length > 0 ? data.table[0] : null;
|
||||
if (!firstRow || Object.prototype.toString.call(firstRow) !== '[object Object]') {
|
||||
createMessage.warning('预览数据中未找到 table[0] 对象,无法推导');
|
||||
return;
|
||||
}
|
||||
const cols = Object.keys(firstRow).map((k) => ({
|
||||
title: k,
|
||||
field: k,
|
||||
width: 60,
|
||||
align: typeof firstRow[k] === 'number' ? 'right' : 'left',
|
||||
}));
|
||||
tableColumnsJson.value = JSON.stringify(cols, null, 2);
|
||||
createMessage.success('已根据 table[0] 推导列配置');
|
||||
}
|
||||
|
||||
function buildGroupedRowsVisualMerge(rows: any[], groups: string[]) {
|
||||
if (!Array.isArray(rows) || rows.length === 0 || !Array.isArray(groups) || groups.length === 0) {
|
||||
return rows;
|
||||
}
|
||||
const out = rows.map((r) => ({ ...r }));
|
||||
for (let i = 1; i < out.length; i++) {
|
||||
for (let level = 0; level < groups.length; level++) {
|
||||
const field = groups[level];
|
||||
// 上层一致才比较当前层
|
||||
let upperSame = true;
|
||||
for (let up = 0; up < level; up++) {
|
||||
const upField = groups[up];
|
||||
if ((out[i][upField] ?? '') !== (out[i - 1][upField] ?? '')) {
|
||||
upperSame = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!upperSame) break;
|
||||
if ((out[i][field] ?? '') === (out[i - 1][field] ?? '')) {
|
||||
// 保真模式下只清空重复值,保持原组件样式不变
|
||||
out[i][field] = '';
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseTemplatePayload(str: string | undefined) {
|
||||
if (!str || str === '{}') {
|
||||
return { template: {}, ext: null as any };
|
||||
}
|
||||
try {
|
||||
const obj = JSON.parse(str);
|
||||
// 兼容历史格式:直接存 template json
|
||||
if (obj?.printPanels || obj?.panels) {
|
||||
return { template: obj, ext: null as any };
|
||||
}
|
||||
// 新格式:{ template, ext }
|
||||
if (obj?.template && (obj.template.printPanels || obj.template.panels || Object.keys(obj.template).length >= 0)) {
|
||||
return { template: obj.template, ext: obj.ext || null };
|
||||
}
|
||||
return { template: obj, ext: null as any };
|
||||
} catch {
|
||||
createMessage.warning('模板 JSON 解析失败,已使用空白模板');
|
||||
return { template: {}, ext: null as any };
|
||||
}
|
||||
}
|
||||
|
||||
async function bootDesigner() {
|
||||
const id = route.query.id as string;
|
||||
if (!id) {
|
||||
createMessage.error('缺少模板 id');
|
||||
goBack();
|
||||
return;
|
||||
}
|
||||
const record = await queryById(id);
|
||||
if (!record?.id) {
|
||||
createMessage.error('模板不存在');
|
||||
goBack();
|
||||
return;
|
||||
}
|
||||
tplName.value = record.templateName || record.templateCode || '';
|
||||
|
||||
initHiprintRuntime();
|
||||
destroyDesigner();
|
||||
await nextTick();
|
||||
|
||||
hiprint.PrintElementTypeManager.buildByHtml($('.ep-draggable-item'));
|
||||
// 兼容 a-collapse-panel 延迟渲染:初始化后再补一次,确保所有拖拽项都绑定到拖拽事件上
|
||||
setTimeout(() => {
|
||||
hiprint.PrintElementTypeManager.buildByHtml($('.ep-draggable-item'));
|
||||
}, 300);
|
||||
|
||||
const payload = parseTemplatePayload(record.templateJson);
|
||||
const template = payload.template;
|
||||
if (payload.ext) {
|
||||
if (typeof payload.ext.printDataJson === 'string' && payload.ext.printDataJson.trim()) {
|
||||
printDataJson.value = payload.ext.printDataJson;
|
||||
}
|
||||
if (typeof payload.ext.tableColumnsJson === 'string') {
|
||||
tableColumnsJson.value = payload.ext.tableColumnsJson;
|
||||
}
|
||||
groupEnabled.value = !!payload.ext.groupEnabled;
|
||||
if (Array.isArray(payload.ext.groupFields)) {
|
||||
groupFields.value = payload.ext.groupFields;
|
||||
}
|
||||
}
|
||||
hiprintTemplate = new hiprint.PrintTemplate({
|
||||
template,
|
||||
settingContainer: '#PrintElementOptionSetting',
|
||||
paginationContainer: '.hiprint-printPagination',
|
||||
history: true,
|
||||
});
|
||||
hiprintTemplate.design('#hiprint-printTemplate');
|
||||
applyFieldsToTemplate();
|
||||
applyTableColumnsConfig(true);
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.push('/print/template');
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!hiprintTemplate || !route.query.id) {
|
||||
return;
|
||||
}
|
||||
const json = hiprintTemplate.getJson();
|
||||
let templateObj: any = {};
|
||||
if (typeof json === 'string') {
|
||||
try {
|
||||
templateObj = JSON.parse(json);
|
||||
} catch {
|
||||
templateObj = {};
|
||||
}
|
||||
} else {
|
||||
templateObj = json;
|
||||
}
|
||||
const payload = {
|
||||
template: templateObj,
|
||||
ext: {
|
||||
printDataJson: printDataJson.value,
|
||||
tableColumnsJson: tableColumnsJson.value,
|
||||
groupEnabled: groupEnabled.value,
|
||||
groupFields: groupFields.value,
|
||||
},
|
||||
};
|
||||
const templateJson = JSON.stringify(payload);
|
||||
saving.value = true;
|
||||
try {
|
||||
await saveJson({ id: route.query.id as string, templateJson });
|
||||
createMessage.success('模板已保存');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handlePreview() {
|
||||
if (!hiprintTemplate) {
|
||||
return;
|
||||
}
|
||||
const data = parsePrintData();
|
||||
if (preserveDesignStyle.value) {
|
||||
// 样式优先:不改模板结构,只改分组字段的显示数据
|
||||
if (groupEnabled.value && Array.isArray(data.table) && groupFields.value.length > 0) {
|
||||
data.table = buildGroupedRowsVisualMerge(data.table, groupFields.value);
|
||||
}
|
||||
hiprintTemplate.print(data, {}, { callback: () => {} });
|
||||
return;
|
||||
}
|
||||
// 兜底:即使元素 options 未同步,预览时也从全局参数读取列和分组配置
|
||||
let parsedCols: any[] = [];
|
||||
try {
|
||||
parsedCols = JSON.parse(tableColumnsJson.value || '[]');
|
||||
} catch {
|
||||
parsedCols = [];
|
||||
}
|
||||
data.__qhmesTableColumns = normalizeColumns(parsedCols).map((c) => ({
|
||||
title: c.title,
|
||||
field: c.field,
|
||||
width: c.width,
|
||||
align: c.align,
|
||||
}));
|
||||
data.__qhmesGroupFields = groupEnabled.value ? [...groupFields.value] : [];
|
||||
const previewTemplate = buildPreviewTemplateWithGrouping();
|
||||
if (previewTemplate) {
|
||||
const previewPrintTpl = new (hiprint as any).PrintTemplate({ template: previewTemplate });
|
||||
previewPrintTpl.print(data, {}, { callback: () => {} });
|
||||
return;
|
||||
}
|
||||
hiprintTemplate.print(data, {}, { callback: () => {} });
|
||||
}
|
||||
|
||||
watch(
|
||||
() => printDataJson.value,
|
||||
() => {
|
||||
// 输入过程中做一个简单防抖
|
||||
if (fieldsTimer) {
|
||||
clearTimeout(fieldsTimer);
|
||||
}
|
||||
fieldsTimer = setTimeout(() => {
|
||||
applyFieldsToTemplate();
|
||||
}, 400);
|
||||
}
|
||||
);
|
||||
|
||||
// 分组配置不再自动覆盖画布组件,避免误改用户自定义明细
|
||||
|
||||
// 首次进入与 query.id 变化时加载(避免 onMounted + watch 重复初始化)
|
||||
watch(
|
||||
() => route.query.id as string,
|
||||
(id) => {
|
||||
if (id) {
|
||||
bootDesigner();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
if (fieldsTimer) {
|
||||
clearTimeout(fieldsTimer);
|
||||
fieldsTimer = null;
|
||||
}
|
||||
destroyDesigner();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.hiprint-card {
|
||||
min-height: calc(100vh - 120px);
|
||||
}
|
||||
|
||||
.designer-row {
|
||||
min-height: 520px;
|
||||
}
|
||||
|
||||
.col-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.left-col .ep-draggable-item {
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 6px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
cursor: move;
|
||||
background: #fafafa;
|
||||
font-size: 12px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.help-tip,
|
||||
.section-tip {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
margin: 6px 0 0;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.center-col {
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
background: #fff;
|
||||
min-height: 520px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.right-col {
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
background: #fff;
|
||||
min-height: 520px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#hiprint-printTemplate {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" @register="register" :title="title" width="640px" @ok="handleSubmit" destroyOnClose>
|
||||
<BasicForm @register="registerForm" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, unref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { BasicForm, useForm } from '/@/components/Form';
|
||||
import { formSchema } from '../printTemplate.data';
|
||||
import { add, edit } from '../printTemplate.api';
|
||||
|
||||
const emit = defineEmits(['success', 'register']);
|
||||
|
||||
const isUpdate = ref(false);
|
||||
|
||||
const title = computed(() => (!unref(isUpdate) ? '新增打印模板' : '编辑打印模板'));
|
||||
|
||||
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
|
||||
labelWidth: 110,
|
||||
schemas: formSchema,
|
||||
showActionButtonGroup: false,
|
||||
baseColProps: { span: 24 },
|
||||
});
|
||||
|
||||
const [register, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
resetFields();
|
||||
setModalProps({ confirmLoading: false });
|
||||
isUpdate.value = !!data?.isUpdate;
|
||||
if (unref(isUpdate) && data?.record) {
|
||||
setFieldsValue({
|
||||
...data.record,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const values = await validate();
|
||||
if (!unref(isUpdate)) {
|
||||
delete values.id;
|
||||
if (!values.templateJson) {
|
||||
values.templateJson = '{}';
|
||||
}
|
||||
}
|
||||
setModalProps({ confirmLoading: true });
|
||||
if (unref(isUpdate)) {
|
||||
await edit(values);
|
||||
} else {
|
||||
await add(values);
|
||||
}
|
||||
closeModal();
|
||||
emit('success');
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
187
jeecgboot-vue3/src/views/print/template/hiprint/qhmesProvider.ts
Normal file
187
jeecgboot-vue3/src/views/print/template/hiprint/qhmesProvider.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { hiprint } from 'vue-plugin-hiprint';
|
||||
|
||||
/**
|
||||
* QH-MES 自定义 provider(参考 vue-plugin-hiprint 动态 provider 机制)
|
||||
* - 新增一组“报表/套打”常用组件
|
||||
* - 提供一个默认的“普通明细表”(单行表头)
|
||||
*
|
||||
* 注意:此 provider 不替换 defaultElementTypeProvider,只做补充。
|
||||
*/
|
||||
export function createQhmesProvider() {
|
||||
const key = 'qhmesModule';
|
||||
|
||||
const addElementTypes = function (context: any) {
|
||||
// 避免重复注册
|
||||
context.removePrintElementTypes(key);
|
||||
|
||||
const commonText = (tid: string, title: string, extraOptions: Record<string, any> = {}) => {
|
||||
return {
|
||||
tid,
|
||||
title,
|
||||
type: 'text',
|
||||
options: {
|
||||
title,
|
||||
field: '',
|
||||
testData: title,
|
||||
...extraOptions,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const elements: any[] = [
|
||||
commonText(`${key}.reportTitle`, '报表标题', { fontSize: 18, fontWeight: 'bold', textAlign: 'center' }),
|
||||
commonText(`${key}.subTitle`, '副标题', { fontSize: 12, textAlign: 'center' }),
|
||||
commonText(`${key}.labelValue`, '标签:值', { fontSize: 10 }),
|
||||
commonText(`${key}.pageNo`, '页码', { field: 'pageNumber', testData: '1/1' }),
|
||||
|
||||
// 二维码/条码(用 text + textType)
|
||||
{
|
||||
tid: `${key}.qrcode`,
|
||||
title: '二维码',
|
||||
type: 'text',
|
||||
options: {
|
||||
title: '二维码',
|
||||
field: 'qrcode',
|
||||
testData: 'QRCODE_DEMO',
|
||||
textType: 'qrcode',
|
||||
width: 35,
|
||||
height: 35,
|
||||
},
|
||||
},
|
||||
{
|
||||
tid: `${key}.barcode`,
|
||||
title: '条形码',
|
||||
type: 'text',
|
||||
options: {
|
||||
title: '条形码',
|
||||
field: 'barcode',
|
||||
testData: '1234567890',
|
||||
textType: 'barcode',
|
||||
width: 80,
|
||||
height: 25,
|
||||
},
|
||||
},
|
||||
|
||||
// 普通明细表(单行表头,支持多级分组合并)
|
||||
{
|
||||
tid: `${key}.tableSimple`,
|
||||
title: '普通明细表',
|
||||
type: 'html',
|
||||
options: {
|
||||
title: '普通明细表',
|
||||
field: 'table',
|
||||
testData: '',
|
||||
width: 180,
|
||||
height: 60,
|
||||
__qhmesManaged: true,
|
||||
columns: [
|
||||
{ title: '物料', field: 'name', width: 90, align: 'left' },
|
||||
{ title: '数量', field: 'qty', width: 45, align: 'right' },
|
||||
{ title: '金额', field: 'amount', width: 45, align: 'right' },
|
||||
],
|
||||
groupFields: [],
|
||||
formatter: `
|
||||
function(t,e,printData){
|
||||
var opts = (t && t.options) ? t.options : {};
|
||||
var list = printData && Array.isArray(printData[opts.field || 'table']) ? printData[opts.field || 'table'] : [];
|
||||
var globalCols = printData && Array.isArray(printData.__qhmesTableColumns) ? printData.__qhmesTableColumns : [];
|
||||
var columns = Array.isArray(opts.columns) && opts.columns.length ? opts.columns : (globalCols.length ? globalCols : [
|
||||
{ title: '物料', field: 'name', width: 90, align: 'left' },
|
||||
{ title: '数量', field: 'qty', width: 45, align: 'right' },
|
||||
{ title: '金额', field: 'amount', width: 45, align: 'right' }
|
||||
]);
|
||||
var globalGroups = printData && Array.isArray(printData.__qhmesGroupFields) ? printData.__qhmesGroupFields : [];
|
||||
var groupFields = Array.isArray(opts.groupFields) && opts.groupFields.length ? opts.groupFields : globalGroups;
|
||||
var style = opts.__qhmesStyle || {};
|
||||
var fontSize = style.fontSize || 10;
|
||||
var borderColor = style.borderColor || '#000';
|
||||
var borderWidth = style.borderWidth || 1;
|
||||
var cellPadding = style.cellPadding || '2pt 4pt';
|
||||
var headerBg = style.headerBg || '';
|
||||
var tableWidth = style.tableWidth || '100%';
|
||||
|
||||
function esc(v){
|
||||
if (v === null || v === undefined) return '';
|
||||
return String(v).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
function isGroupCol(field){
|
||||
return groupFields.indexOf(field) > -1;
|
||||
}
|
||||
|
||||
// 计算每个分组列在每行的 rowspan(多级:上层一致前提下再判断下层)
|
||||
var rowspanMap = {};
|
||||
for (var c=0;c<columns.length;c++){
|
||||
var f = columns[c].field;
|
||||
rowspanMap[f] = new Array(list.length).fill(1);
|
||||
}
|
||||
for (var g=0; g<groupFields.length; g++){
|
||||
var gf = groupFields[g];
|
||||
// 分组字段可能不在 columns 中,需先兜底初始化,避免 rowspanMap[gf] 未定义报错
|
||||
if (!rowspanMap[gf]) rowspanMap[gf] = new Array(list.length).fill(1);
|
||||
var i = 0;
|
||||
while(i < list.length){
|
||||
var j = i + 1;
|
||||
while(j < list.length){
|
||||
var upperOk = true;
|
||||
for (var up=0; up<g; up++){
|
||||
var uf = groupFields[up];
|
||||
if ((list[j][uf] ?? '') !== (list[i][uf] ?? '')) { upperOk = false; break; }
|
||||
}
|
||||
if (!upperOk) break;
|
||||
if ((list[j][gf] ?? '') === (list[i][gf] ?? '')) j++;
|
||||
else break;
|
||||
}
|
||||
var span = j - i;
|
||||
if (!rowspanMap[gf]) rowspanMap[gf] = new Array(list.length).fill(1);
|
||||
rowspanMap[gf][i] = span;
|
||||
for (var k=i+1;k<j;k++) rowspanMap[gf][k] = 0;
|
||||
i = j;
|
||||
}
|
||||
}
|
||||
|
||||
var html = '<table style="width:'+tableWidth+';border-collapse:collapse;table-layout:fixed;font-size:'+fontSize+'pt;">';
|
||||
html += '<thead><tr>';
|
||||
for (var h=0;h<columns.length;h++){
|
||||
var hc = columns[h];
|
||||
var hw = hc.width ? ('width:'+hc.width+'pt;') : '';
|
||||
var hbg = headerBg ? ('background:'+headerBg+';') : '';
|
||||
html += '<th style="border:'+borderWidth+'px solid '+borderColor+';padding:'+cellPadding+';text-align:center;'+hbg+hw+'">'+esc(hc.title || hc.field || '')+'</th>';
|
||||
}
|
||||
html += '</tr></thead><tbody>';
|
||||
|
||||
for (var r=0;r<list.length;r++){
|
||||
html += '<tr>';
|
||||
for (var cc=0;cc<columns.length;cc++){
|
||||
var col = columns[cc];
|
||||
var field = col.field;
|
||||
var align = col.align || 'left';
|
||||
if (isGroupCol(field)){
|
||||
var rs = rowspanMap[field][r];
|
||||
if (rs > 0){
|
||||
html += '<td rowspan="'+rs+'" style="border:'+borderWidth+'px solid '+borderColor+';padding:'+cellPadding+';text-align:center;vertical-align:middle;">'+esc(list[r][field])+'</td>';
|
||||
}
|
||||
}else{
|
||||
html += '<td style="border:'+borderWidth+'px solid '+borderColor+';padding:'+cellPadding+';text-align:'+align+';">'+esc(list[r][field])+'</td>';
|
||||
}
|
||||
}
|
||||
html += '</tr>';
|
||||
}
|
||||
html += '</tbody></table>';
|
||||
return html;
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// 分组(左侧面板展示更友好)
|
||||
const groups = [
|
||||
new (hiprint as any).PrintElementTypeGroup('HttpPrinter风格组件', elements),
|
||||
];
|
||||
|
||||
context.addPrintElementTypes(key, groups);
|
||||
};
|
||||
|
||||
return { addElementTypes };
|
||||
}
|
||||
|
||||
96
jeecgboot-vue3/src/views/print/template/index.vue
Normal file
96
jeecgboot-vue3/src/views/print/template/index.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div>
|
||||
<BasicTable @register="registerTable" :rowSelection="rowSelection">
|
||||
<template #tableTitle>
|
||||
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleCreate" v-auth="'print:template:add'"> 新增</a-button>
|
||||
<a-dropdown v-if="selectedRowKeys.length > 0">
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="1" @click="batchHandleDelete" v-auth="'print:template:delete'">
|
||||
<Icon icon="ant-design:delete-outlined" />
|
||||
删除
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<a-button>
|
||||
批量操作
|
||||
<Icon icon="mdi:chevron-down" />
|
||||
</a-button>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<TableAction :actions="getTableAction(record)" />
|
||||
</template>
|
||||
</BasicTable>
|
||||
<PrintTemplateModal @register="registerModal" @success="handleSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="PrintTemplateList" setup>
|
||||
import { Icon } from '/@/components/Icon';
|
||||
import { BasicTable, TableAction } from '/@/components/Table';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { useListPage } from '/@/hooks/system/useListPage';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { columns, searchFormSchema } from './printTemplate.data';
|
||||
import { list, deleteOne, batchDelete } from './printTemplate.api';
|
||||
import PrintTemplateModal from './components/PrintTemplateModal.vue';
|
||||
|
||||
defineOptions({ name: 'PrintTemplateList' });
|
||||
|
||||
const router = useRouter();
|
||||
const [registerModal, { openModal }] = useModal();
|
||||
|
||||
const { tableContext } = useListPage({
|
||||
tableProps: {
|
||||
title: '打印模板',
|
||||
api: list,
|
||||
columns,
|
||||
rowKey: 'id',
|
||||
formConfig: { schemas: searchFormSchema },
|
||||
actionColumn: { width: 220 },
|
||||
},
|
||||
});
|
||||
|
||||
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
|
||||
|
||||
function handleCreate() {
|
||||
openModal(true, { isUpdate: false });
|
||||
}
|
||||
|
||||
function handleEdit(record: Recordable) {
|
||||
openModal(true, { isUpdate: true, record });
|
||||
}
|
||||
|
||||
function handleDesign(record: Recordable) {
|
||||
router.push({ path: '/print/designer', query: { id: record.id } });
|
||||
}
|
||||
|
||||
async function handleDelete(record: Recordable) {
|
||||
await deleteOne({ id: record.id }, reload);
|
||||
}
|
||||
|
||||
async function batchHandleDelete() {
|
||||
await batchDelete({ ids: selectedRowKeys.value.join(',') }, reload);
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
reload();
|
||||
}
|
||||
|
||||
function getTableAction(record: Recordable) {
|
||||
return [
|
||||
{ label: '设计', onClick: handleDesign.bind(null, record), auth: 'print:template:edit' },
|
||||
{ label: '编辑', onClick: handleEdit.bind(null, record), auth: 'print:template:edit' },
|
||||
{
|
||||
label: '删除',
|
||||
color: 'error',
|
||||
popConfirm: {
|
||||
title: '确认删除?',
|
||||
confirm: handleDelete.bind(null, record),
|
||||
},
|
||||
auth: 'print:template:delete',
|
||||
},
|
||||
];
|
||||
}
|
||||
</script>
|
||||
34
jeecgboot-vue3/src/views/print/template/printTemplate.api.ts
Normal file
34
jeecgboot-vue3/src/views/print/template/printTemplate.api.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
|
||||
enum Api {
|
||||
list = '/print/template/list',
|
||||
add = '/print/template/add',
|
||||
edit = '/print/template/edit',
|
||||
deleteOne = '/print/template/delete',
|
||||
deleteBatch = '/print/template/deleteBatch',
|
||||
queryById = '/print/template/queryById',
|
||||
saveJson = '/print/template/saveJson',
|
||||
}
|
||||
|
||||
export const list = (params) => defHttp.get({ url: Api.list, params });
|
||||
|
||||
export const add = (params) => defHttp.post({ url: Api.add, params });
|
||||
|
||||
export const edit = (params) => defHttp.put({ url: Api.edit, params });
|
||||
|
||||
export const deleteOne = (params, handleSuccess?) => {
|
||||
return defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => {
|
||||
handleSuccess?.();
|
||||
});
|
||||
};
|
||||
|
||||
export const batchDelete = (params, handleSuccess?) => {
|
||||
return defHttp.delete({ url: Api.deleteBatch, params }, { joinParamsToUrl: true }).then(() => {
|
||||
handleSuccess?.();
|
||||
});
|
||||
};
|
||||
|
||||
export const queryById = (id: string) => defHttp.get({ url: Api.queryById, params: { id } });
|
||||
|
||||
export const saveJson = (data: { id: string; templateJson: string }) =>
|
||||
defHttp.post({ url: Api.saveJson, data }, { successMessageMode: 'message' });
|
||||
110
jeecgboot-vue3/src/views/print/template/printTemplate.data.ts
Normal file
110
jeecgboot-vue3/src/views/print/template/printTemplate.data.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { BasicColumn, FormSchema } from '/@/components/Table';
|
||||
|
||||
export const columns: BasicColumn[] = [
|
||||
{ title: '模板编码', dataIndex: 'templateCode', width: 140 },
|
||||
{ title: '模板名称', dataIndex: 'templateName', width: 180 },
|
||||
{
|
||||
title: '分类',
|
||||
dataIndex: 'category',
|
||||
width: 100,
|
||||
customRender: ({ text }) => {
|
||||
const m = { barcode: '条码', form: '表单套打', report: '报表' };
|
||||
return m[text] || text;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '纸张(mm)',
|
||||
dataIndex: 'paperWidthMm',
|
||||
width: 130,
|
||||
customRender: ({ record }) =>
|
||||
record?.paperWidthMm != null && record?.paperHeightMm != null
|
||||
? `${record.paperWidthMm}×${record.paperHeightMm}`
|
||||
: '-',
|
||||
},
|
||||
{ title: '备注', dataIndex: 'remark', ellipsis: true },
|
||||
{ title: '创建时间', dataIndex: 'createTime', width: 165 },
|
||||
];
|
||||
|
||||
export const searchFormSchema: FormSchema[] = [
|
||||
{ label: '模板编码', field: 'templateCode', component: 'Input', colProps: { span: 6 } },
|
||||
{ label: '模板名称', field: 'templateName', component: 'Input', colProps: { span: 6 } },
|
||||
{
|
||||
label: '分类',
|
||||
field: 'category',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '条码', value: 'barcode' },
|
||||
{ label: '表单套打', value: 'form' },
|
||||
{ label: '报表', value: 'report' },
|
||||
],
|
||||
allowClear: true,
|
||||
},
|
||||
colProps: { span: 6 },
|
||||
},
|
||||
];
|
||||
|
||||
export const formSchema: FormSchema[] = [
|
||||
{
|
||||
label: '模板编码',
|
||||
field: 'templateCode',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
componentProps: { maxlength: 64 },
|
||||
dynamicDisabled: ({ values }) => !!values?.id,
|
||||
},
|
||||
{
|
||||
label: '模板名称',
|
||||
field: 'templateName',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
label: '分类',
|
||||
field: 'category',
|
||||
component: 'Select',
|
||||
defaultValue: 'form',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '条码', value: 'barcode' },
|
||||
{ label: '表单套打', value: 'form' },
|
||||
{ label: '报表', value: 'report' },
|
||||
],
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
label: '纸宽(mm)',
|
||||
field: 'paperWidthMm',
|
||||
component: 'InputNumber',
|
||||
defaultValue: 210,
|
||||
componentProps: { min: 10, max: 2000, style: { width: '100%' } },
|
||||
},
|
||||
{
|
||||
label: '纸高(mm)',
|
||||
field: 'paperHeightMm',
|
||||
component: 'InputNumber',
|
||||
defaultValue: 297,
|
||||
componentProps: { min: 10, max: 2000, style: { width: '100%' } },
|
||||
},
|
||||
{
|
||||
label: '方向',
|
||||
field: 'paperOrientation',
|
||||
component: 'Select',
|
||||
defaultValue: 'portrait',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '纵向', value: 'portrait' },
|
||||
{ label: '横向', value: 'landscape' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '备注',
|
||||
field: 'remark',
|
||||
component: 'InputTextArea',
|
||||
componentProps: { rows: 2 },
|
||||
},
|
||||
{ label: '', field: 'id', component: 'Input', show: false },
|
||||
{ label: '', field: 'templateJson', component: 'Input', show: false, defaultValue: '{}' },
|
||||
];
|
||||
Reference in New Issue
Block a user