1686 lines
56 KiB
Vue
1686 lines
56 KiB
Vue
|
|
<template>
|
|||
|
|
<div class="native-designer-page">
|
|||
|
|
<div class="designer-toolbar">
|
|||
|
|
<div class="toolbar-left">
|
|||
|
|
<span class="title">原生打印模板设计器</span>
|
|||
|
|
<span v-if="templateId" class="tip">ID: {{ templateId }}</span>
|
|||
|
|
</div>
|
|||
|
|
<a-space>
|
|||
|
|
<a-button @click="backToList">返回列表</a-button>
|
|||
|
|
<a-button @click="handleDelete">删除选中</a-button>
|
|||
|
|
<a-button @click="duplicateElement">复制元素</a-button>
|
|||
|
|
<a-button @click="bringForward">上移图层</a-button>
|
|||
|
|
<a-button @click="sendBackward">下移图层</a-button>
|
|||
|
|
<a-button @click="previewTemplate">即时预览</a-button>
|
|||
|
|
<a-button @click="printTemplate">打印</a-button>
|
|||
|
|
<a-button type="primary" :loading="saving" @click="saveTemplate">保存模板</a-button>
|
|||
|
|
</a-space>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="designer-meta">
|
|||
|
|
<div class="designer-meta__grid">
|
|||
|
|
<a-input v-model:value="meta.templateCode" placeholder="模板编码" allow-clear />
|
|||
|
|
<a-input v-model:value="meta.templateName" placeholder="模板名称" allow-clear />
|
|||
|
|
<a-input v-model:value="meta.category" placeholder="分类(默认form)" allow-clear />
|
|||
|
|
<a-input v-model:value="meta.remark" placeholder="备注" allow-clear />
|
|||
|
|
</div>
|
|||
|
|
<div class="designer-meta__actions">
|
|||
|
|
<a-input-number
|
|||
|
|
:value="state.scale"
|
|||
|
|
:min="0.2"
|
|||
|
|
:max="2"
|
|||
|
|
:step="0.1"
|
|||
|
|
addon-before="按比例缩放"
|
|||
|
|
class="meta-scale-input"
|
|||
|
|
@change="setScale(Number($event || 1))"
|
|||
|
|
/>
|
|||
|
|
<a-button class="meta-scale-btn" @click="pageConfigModalOpen = true">页面配置</a-button>
|
|||
|
|
<a-button class="meta-scale-btn" @click="openImageAnalyzeModal">上传图片分析模板</a-button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="designer-main">
|
|||
|
|
<div
|
|||
|
|
class="left-panel-wrap"
|
|||
|
|
:class="{ 'is-collapsed': leftPanelWidth <= 0 }"
|
|||
|
|
:style="leftPanelWrapStyle"
|
|||
|
|
>
|
|||
|
|
<div class="left-panel">
|
|||
|
|
<ToolbarPalette
|
|||
|
|
v-model:insert-region="insertRegion"
|
|||
|
|
:data-binding="state.schema.dataBinding"
|
|||
|
|
@add="handleAddElement"
|
|||
|
|
@update-data-binding="handleUpdateDataBinding"
|
|||
|
|
/>
|
|||
|
|
<a-divider />
|
|||
|
|
<a-space direction="vertical" style="width: 100%">
|
|||
|
|
<a-tabs v-model:activeKey="jsonTabKey" size="small" class="json-tabs">
|
|||
|
|
<a-tab-pane key="template" tab="模板JSON">
|
|||
|
|
<div class="json-template-pane">
|
|||
|
|
<div class="json-box-title">画布实际 JSON(模板样式)</div>
|
|||
|
|
<a-space class="json-action-row json-action-row--capsule" size="small" :wrap="true">
|
|||
|
|
<a-button size="small" class="json-capsule-btn" @click="generateCanvasJson">从画布生成</a-button>
|
|||
|
|
<a-button size="small" type="primary" ghost class="json-capsule-btn" @click="applyCanvasJsonToCanvas">应用到画布</a-button>
|
|||
|
|
</a-space>
|
|||
|
|
<a-textarea
|
|||
|
|
v-model:value="canvasJsonText"
|
|||
|
|
:rows="18"
|
|||
|
|
class="json-textarea"
|
|||
|
|
placeholder="记录画布实际JSON,可反向应用到画布并还原模板样式"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</a-tab-pane>
|
|||
|
|
<a-tab-pane key="params" tab="参数JSON">
|
|||
|
|
<a-tabs v-model:activeKey="dataSourceType" size="small" class="json-sub-tabs json-sub-tabs--segmented">
|
|||
|
|
<a-tab-pane key="manual" tab="手动JSON">
|
|||
|
|
<a-textarea
|
|||
|
|
v-model:value="previewDataText"
|
|||
|
|
:rows="16"
|
|||
|
|
class="json-textarea"
|
|||
|
|
placeholder="手动输入预览/打印数据 JSON"
|
|||
|
|
/>
|
|||
|
|
</a-tab-pane>
|
|||
|
|
<a-tab-pane key="mock" tab="模拟JSON">
|
|||
|
|
<div class="json-mock-toolbar">
|
|||
|
|
<a-button size="small" type="primary" ghost class="json-capsule-btn" @click="generateMockData">根据画布生成</a-button>
|
|||
|
|
</div>
|
|||
|
|
<a-textarea
|
|||
|
|
v-model:value="mockDataText"
|
|||
|
|
:rows="14"
|
|||
|
|
class="json-textarea"
|
|||
|
|
placeholder="点击「根据画布生成」自动填充模拟数据"
|
|||
|
|
/>
|
|||
|
|
</a-tab-pane>
|
|||
|
|
</a-tabs>
|
|||
|
|
</a-tab-pane>
|
|||
|
|
</a-tabs>
|
|||
|
|
</a-space>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div
|
|||
|
|
class="left-panel-resizer"
|
|||
|
|
role="separator"
|
|||
|
|
aria-orientation="vertical"
|
|||
|
|
:title="leftResizerTitle"
|
|||
|
|
@mousedown="startLeftPanelResize"
|
|||
|
|
@dblclick.prevent="onLeftResizerDblClick"
|
|||
|
|
/>
|
|||
|
|
<div class="center-panel">
|
|||
|
|
<DesignerCanvas
|
|||
|
|
:schema="state.schema"
|
|||
|
|
:selected-id="state.selectedId"
|
|||
|
|
:scale="state.scale"
|
|||
|
|
:preview-data="previewData"
|
|||
|
|
:selected-table-column="selectedTableColumn"
|
|||
|
|
:selected-free-table-cell="selectedFreeTableCell"
|
|||
|
|
:selected-free-table-merge-corner="selectedFreeTableRangeCorner"
|
|||
|
|
@select="handleSelectElement"
|
|||
|
|
@update="handleElementUpdate"
|
|||
|
|
@select-table-column="handleSelectTableColumn"
|
|||
|
|
@select-free-table-cell="handleSelectFreeTableCell"
|
|||
|
|
@edit-free-table-cell="handleEditFreeTableCell"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div class="right-panel">
|
|||
|
|
<PropertiesPanel
|
|||
|
|
:schema="state.schema"
|
|||
|
|
:selected-element="selectedElement"
|
|||
|
|
:selected-table-column-key="resolveSelectedTableColumnKey()"
|
|||
|
|
:selected-free-table-cell="resolveSelectedFreeTableCell()"
|
|||
|
|
:selected-free-table-merge-rect="resolveSelectedFreeTableMergeRect()"
|
|||
|
|
@update-element="handleElementUpdate"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<PageConfigModal v-model:open="pageConfigModalOpen" :schema="state.schema" @update-page="updatePage" />
|
|||
|
|
|
|||
|
|
<a-drawer v-model:open="previewVisible" title="即时预览" width="70%" placement="right">
|
|||
|
|
<iframe class="preview-frame" :srcdoc="previewHtml"></iframe>
|
|||
|
|
</a-drawer>
|
|||
|
|
|
|||
|
|
<FreeTableCellEditModal
|
|||
|
|
v-model:open="freeTableCellModalOpen"
|
|||
|
|
:element-id="freeTableCellModalTarget?.elementId || ''"
|
|||
|
|
:row="freeTableCellModalTarget?.row ?? 0"
|
|||
|
|
:col="freeTableCellModalTarget?.col ?? 0"
|
|||
|
|
:schema="state.schema"
|
|||
|
|
@save="handleFreeTableCellModalSave"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<a-modal
|
|||
|
|
v-model:open="imageAnalyzeVisible"
|
|||
|
|
title="上传图片分析模板"
|
|||
|
|
width="920px"
|
|||
|
|
destroy-on-close
|
|||
|
|
:closable="!imageAnalyzeLoading"
|
|||
|
|
:mask-closable="!imageAnalyzeLoading"
|
|||
|
|
@cancel="resetImageAnalyzeModal"
|
|||
|
|
>
|
|||
|
|
<div class="image-analyze-body">
|
|||
|
|
<a-alert type="info" show-icon style="margin-bottom: 12px">
|
|||
|
|
<template #message>
|
|||
|
|
<span>选择或拖拽图片到下方区域,系统将生成画布 JSON 与模拟数据;先预览渲染效果,确认后再应用到画布。</span>
|
|||
|
|
</template>
|
|||
|
|
</a-alert>
|
|||
|
|
<div
|
|||
|
|
class="image-analyze-drop"
|
|||
|
|
:class="{ 'is-dragover': imageAnalyzeDragover, 'is-busy': imageAnalyzeLoading }"
|
|||
|
|
@dragenter.prevent="onImageAnalyzeDragEnter"
|
|||
|
|
@dragover.prevent="onImageAnalyzeDragOver"
|
|||
|
|
@dragleave.prevent="onImageAnalyzeDragLeave"
|
|||
|
|
@drop.prevent="onImageAnalyzeDrop"
|
|||
|
|
>
|
|||
|
|
<input
|
|||
|
|
ref="imageAnalyzeFileInputRef"
|
|||
|
|
type="file"
|
|||
|
|
accept="image/*"
|
|||
|
|
class="image-analyze-file-input"
|
|||
|
|
:disabled="imageAnalyzeLoading"
|
|||
|
|
@change="onImageAnalyzeFileInputChange"
|
|||
|
|
/>
|
|||
|
|
<p class="image-analyze-drop-title">拖拽图片到此处,或</p>
|
|||
|
|
<a-button type="link" :disabled="imageAnalyzeLoading" @click="triggerImageAnalyzeFilePick">点击选择文件</a-button>
|
|||
|
|
</div>
|
|||
|
|
<div v-if="imageAnalyzeLoading" class="image-analyze-progress">
|
|||
|
|
<a-progress
|
|||
|
|
:percent="Math.min(100, Math.round(imageAnalyzeProgress))"
|
|||
|
|
:status="imageAnalyzeProgress >= 100 ? 'success' : 'active'"
|
|||
|
|
:stroke-width="10"
|
|||
|
|
/>
|
|||
|
|
<p class="image-analyze-progress-tip">{{ imageAnalyzeProgressTip }}</p>
|
|||
|
|
<p class="image-analyze-progress-sub">大模型分析可能需要数十秒至两分钟,请耐心等待,勿关闭窗口。</p>
|
|||
|
|
</div>
|
|||
|
|
<div v-if="imageAnalyzeHint" class="image-analyze-hint">{{ imageAnalyzeHint }}</div>
|
|||
|
|
<a-divider v-if="imageAnalyzePreviewHtml" plain orientation="left">效果预览(应用前)</a-divider>
|
|||
|
|
<div v-if="imageAnalyzePreviewHtml" class="image-analyze-preview-wrap">
|
|||
|
|
<div v-if="imageAnalyzeThumbUrl" class="image-analyze-thumb">
|
|||
|
|
<div class="thumb-label">原图</div>
|
|||
|
|
<img :src="imageAnalyzeThumbUrl" alt="上传原图" />
|
|||
|
|
</div>
|
|||
|
|
<div class="image-analyze-preview-frame-wrap">
|
|||
|
|
<div class="thumb-label">按生成模板渲染</div>
|
|||
|
|
<iframe class="image-analyze-preview-frame" :srcdoc="imageAnalyzePreviewHtml"></iframe>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<template #footer>
|
|||
|
|
<a-button @click="resetImageAnalyzeModal">取消</a-button>
|
|||
|
|
<a-button type="primary" :disabled="!imageAnalyzePendingSchema || imageAnalyzeLoading" @click="confirmApplyImageAnalyze">
|
|||
|
|
应用到画布
|
|||
|
|
</a-button>
|
|||
|
|
</template>
|
|||
|
|
</a-modal>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script lang="ts" setup>
|
|||
|
|
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
|
|||
|
|
import { useRoute, useRouter } from 'vue-router';
|
|||
|
|
import { useMessage } from '/@/hooks/web/useMessage';
|
|||
|
|
import { add, analyzeImageForNativeFile, edit, queryByCode, queryById } from '../printTemplate.api';
|
|||
|
|
import DesignerCanvas from './components/DesignerCanvas.vue';
|
|||
|
|
import FreeTableCellEditModal from './components/FreeTableCellEditModal.vue';
|
|||
|
|
import PageConfigModal from './components/PageConfigModal.vue';
|
|||
|
|
import PropertiesPanel from './components/PropertiesPanel.vue';
|
|||
|
|
import ToolbarPalette from './components/ToolbarPalette.vue';
|
|||
|
|
import { printHtml } from './core/printService';
|
|||
|
|
import { renderNativePrintHtml } from './core/printRenderer';
|
|||
|
|
import { generateNativeMockDataObject } from './core/nativeMockData';
|
|||
|
|
import { buildNativeTemplateStylePayload } from './core/nativeTemplateStyleSerialize';
|
|||
|
|
import { normalizeImportedNativeSchema } from './core/nativeSchemaNormalize';
|
|||
|
|
import { normalizeFreeTableAnchors } from './core/freeTableGrid';
|
|||
|
|
import { scaleFreeTableTracks } from './core/freeTableTracks';
|
|||
|
|
import { useDesignerStore } from './core/useDesignerStore';
|
|||
|
|
import type { NativeElement, NativeElementType, NativeTemplateSchema } from './core/types';
|
|||
|
|
|
|||
|
|
defineOptions({ name: 'NativePrintDesigner' });
|
|||
|
|
|
|||
|
|
const router = useRouter();
|
|||
|
|
const route = useRoute();
|
|||
|
|
const { createMessage } = useMessage();
|
|||
|
|
const {
|
|||
|
|
state,
|
|||
|
|
selectedElement,
|
|||
|
|
setSchema,
|
|||
|
|
patchDataBinding,
|
|||
|
|
addElement,
|
|||
|
|
updateElement,
|
|||
|
|
setSelected,
|
|||
|
|
removeSelected,
|
|||
|
|
duplicateSelected,
|
|||
|
|
bringForward,
|
|||
|
|
sendBackward,
|
|||
|
|
setScale,
|
|||
|
|
serialize,
|
|||
|
|
} = useDesignerStore();
|
|||
|
|
const templateId = ref('');
|
|||
|
|
const saving = ref(false);
|
|||
|
|
const previewVisible = ref(false);
|
|||
|
|
const previewHtml = ref('');
|
|||
|
|
const selectedTableColumn = ref<{ elementId: string; columnKey: string } | null>(null);
|
|||
|
|
const selectedFreeTableCell = ref<{ elementId: string; row: number; col: number } | null>(null);
|
|||
|
|
/** Shift 点选第二角,与 selectedFreeTableCell 组成合并矩形 */
|
|||
|
|
const selectedFreeTableRangeCorner = ref<{ elementId: string; row: number; col: number } | null>(null);
|
|||
|
|
/** 自由表格单元格双击编辑弹窗 */
|
|||
|
|
const freeTableCellModalOpen = ref(false);
|
|||
|
|
const freeTableCellModalTarget = ref<{ elementId: string; row: number; col: number } | null>(null);
|
|||
|
|
/** 页面尺寸、边距、网格(从第二行工具区打开) */
|
|||
|
|
const pageConfigModalOpen = ref(false);
|
|||
|
|
|
|||
|
|
/** 上传图片分析模板 */
|
|||
|
|
const imageAnalyzeVisible = ref(false);
|
|||
|
|
const imageAnalyzeLoading = ref(false);
|
|||
|
|
/** 分析过程假进度(0–100),接口无真实进度,仅用于缓解长时间无反馈 */
|
|||
|
|
const imageAnalyzeProgress = ref(0);
|
|||
|
|
const imageAnalyzeProgressTip = ref('正在准备上传…');
|
|||
|
|
let imageAnalyzeProgressTimer: ReturnType<typeof setInterval> | null = null;
|
|||
|
|
/** 关闭弹窗或发起新任务时递增,用于丢弃在途请求结果 */
|
|||
|
|
let imageAnalyzeSeq = 0;
|
|||
|
|
|
|||
|
|
const imageAnalyzeHint = ref('');
|
|||
|
|
const imageAnalyzePreviewHtml = ref('');
|
|||
|
|
const imageAnalyzeThumbUrl = ref('');
|
|||
|
|
const imageAnalyzePendingSchema = ref<NativeTemplateSchema | null>(null);
|
|||
|
|
const imageAnalyzeMockJson = ref('');
|
|||
|
|
const imageAnalyzeDragover = ref(false);
|
|||
|
|
const imageAnalyzeFileInputRef = ref<HTMLInputElement | null>(null);
|
|||
|
|
|
|||
|
|
/** 左侧工具栏宽度(px),0 表示隐藏 */
|
|||
|
|
const LS_LEFT_PANEL_KEY = 'qhmes-native-print-left-panel-w';
|
|||
|
|
const LEFT_PANEL_MIN = 260;
|
|||
|
|
const LEFT_PANEL_MAX = 640;
|
|||
|
|
const LEFT_PANEL_DEFAULT = 360;
|
|||
|
|
/** 松手时宽度小于此值则收拢为隐藏 */
|
|||
|
|
const LEFT_PANEL_COLLAPSE_SNAP = 96;
|
|||
|
|
|
|||
|
|
const leftPanelWidth = ref(LEFT_PANEL_DEFAULT);
|
|||
|
|
const lastLeftPanelWidth = ref(LEFT_PANEL_DEFAULT);
|
|||
|
|
let leftResizeActive = false;
|
|||
|
|
let leftResizeStartX = 0;
|
|||
|
|
let leftResizeStartW = 0;
|
|||
|
|
|
|||
|
|
const leftPanelWrapStyle = computed(() => ({
|
|||
|
|
width: leftPanelWidth.value <= 0 ? '0px' : `${leftPanelWidth.value}px`,
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
const leftResizerTitle = '拖动调整左侧工具栏宽度;拖至极窄可隐藏。双击收起或恢复。';
|
|||
|
|
|
|||
|
|
function startLeftPanelResize(e: MouseEvent) {
|
|||
|
|
if (e.button !== 0) return;
|
|||
|
|
e.preventDefault();
|
|||
|
|
leftResizeActive = true;
|
|||
|
|
leftResizeStartX = e.clientX;
|
|||
|
|
leftResizeStartW = leftPanelWidth.value;
|
|||
|
|
window.addEventListener('mousemove', onLeftPanelResizeMove);
|
|||
|
|
window.addEventListener('mouseup', stopLeftPanelResize);
|
|||
|
|
document.body.style.cursor = 'col-resize';
|
|||
|
|
document.body.style.userSelect = 'none';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function onLeftPanelResizeMove(e: MouseEvent) {
|
|||
|
|
if (!leftResizeActive) return;
|
|||
|
|
const delta = e.clientX - leftResizeStartX;
|
|||
|
|
let next = leftResizeStartW + delta;
|
|||
|
|
next = Math.max(0, Math.min(LEFT_PANEL_MAX, next));
|
|||
|
|
leftPanelWidth.value = next;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function stopLeftPanelResize() {
|
|||
|
|
if (!leftResizeActive) return;
|
|||
|
|
leftResizeActive = false;
|
|||
|
|
window.removeEventListener('mousemove', onLeftPanelResizeMove);
|
|||
|
|
window.removeEventListener('mouseup', stopLeftPanelResize);
|
|||
|
|
document.body.style.cursor = '';
|
|||
|
|
document.body.style.userSelect = '';
|
|||
|
|
|
|||
|
|
let w = leftPanelWidth.value;
|
|||
|
|
if (w > 0 && w < LEFT_PANEL_COLLAPSE_SNAP) {
|
|||
|
|
w = 0;
|
|||
|
|
} else if (w >= LEFT_PANEL_COLLAPSE_SNAP && w < LEFT_PANEL_MIN) {
|
|||
|
|
w = LEFT_PANEL_MIN;
|
|||
|
|
}
|
|||
|
|
if (w > 0) {
|
|||
|
|
lastLeftPanelWidth.value = w;
|
|||
|
|
}
|
|||
|
|
leftPanelWidth.value = w;
|
|||
|
|
try {
|
|||
|
|
localStorage.setItem(LS_LEFT_PANEL_KEY, String(w));
|
|||
|
|
} catch (_e) {
|
|||
|
|
/* 忽略存储异常 */
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function onLeftResizerDblClick() {
|
|||
|
|
if (leftPanelWidth.value <= 0) {
|
|||
|
|
const restore = Math.min(
|
|||
|
|
LEFT_PANEL_MAX,
|
|||
|
|
Math.max(LEFT_PANEL_MIN, lastLeftPanelWidth.value || LEFT_PANEL_DEFAULT),
|
|||
|
|
);
|
|||
|
|
leftPanelWidth.value = restore;
|
|||
|
|
} else {
|
|||
|
|
lastLeftPanelWidth.value = Math.max(leftPanelWidth.value, LEFT_PANEL_MIN);
|
|||
|
|
leftPanelWidth.value = 0;
|
|||
|
|
}
|
|||
|
|
try {
|
|||
|
|
localStorage.setItem(LS_LEFT_PANEL_KEY, String(leftPanelWidth.value));
|
|||
|
|
} catch (_e) {
|
|||
|
|
/* 忽略 */
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
watch(freeTableCellModalOpen, (v) => {
|
|||
|
|
if (!v) {
|
|||
|
|
freeTableCellModalTarget.value = null;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
const insertRegion = ref<'header' | 'body'>('body');
|
|||
|
|
const jsonTabKey = ref<'template' | 'params'>('template');
|
|||
|
|
const dataSourceType = ref<'manual' | 'mock'>('manual');
|
|||
|
|
const previewDataText = ref('{}');
|
|||
|
|
const mockDataText = ref('{}');
|
|||
|
|
const canvasJsonText = ref('{}');
|
|||
|
|
const meta = reactive({
|
|||
|
|
templateCode: `NATIVE_${Date.now()}`,
|
|||
|
|
templateName: '原生打印模板',
|
|||
|
|
category: 'form',
|
|||
|
|
remark: '原生设计器模板',
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const activeDataText = computed(() => (dataSourceType.value === 'mock' ? mockDataText.value : previewDataText.value));
|
|||
|
|
|
|||
|
|
const previewData = computed(() => {
|
|||
|
|
try {
|
|||
|
|
return JSON.parse(activeDataText.value || '{}');
|
|||
|
|
} catch (_error) {
|
|||
|
|
return {};
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
function generateMockData(options: { syncManual?: boolean; showMessage?: boolean } = {}) {
|
|||
|
|
const { syncManual = false, showMessage = true } = options;
|
|||
|
|
const mock = generateNativeMockDataObject(state.schema.elements, canvasJsonText.value);
|
|||
|
|
const nextText = JSON.stringify(mock, null, 2);
|
|||
|
|
mockDataText.value = nextText;
|
|||
|
|
if (syncManual || !String(previewDataText.value || '').trim() || previewDataText.value === '{}') {
|
|||
|
|
previewDataText.value = nextText;
|
|||
|
|
}
|
|||
|
|
if (showMessage) {
|
|||
|
|
createMessage.success('已根据画布组件生成模拟数据 JSON');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function mapTemplateStyleToElement(item: any): NativeElement {
|
|||
|
|
const type = String(item?.component || 'text') as NativeElementType;
|
|||
|
|
const rect = item?.rect || {};
|
|||
|
|
const base: any = {
|
|||
|
|
id: String(item?.id || `${type}_${Math.random().toString(36).slice(2, 8)}`),
|
|||
|
|
type,
|
|||
|
|
bindField: String(item?.bindField || ''),
|
|||
|
|
region: item?.region || '',
|
|||
|
|
bandId: String(item?.bandId || ''),
|
|||
|
|
x: Number(rect?.x || 0),
|
|||
|
|
y: Number(rect?.y || 0),
|
|||
|
|
w: Number(rect?.w || 40),
|
|||
|
|
h: Number(rect?.h || 12),
|
|||
|
|
zIndex: Number(rect?.zIndex || 1),
|
|||
|
|
style: item?.style || {},
|
|||
|
|
};
|
|||
|
|
const payload = item?.payload || {};
|
|||
|
|
if (type === 'image') {
|
|||
|
|
base.src = payload.src || '';
|
|||
|
|
base.fit = payload.fit || 'contain';
|
|||
|
|
return base as NativeElement;
|
|||
|
|
}
|
|||
|
|
if (type === 'table' || type === 'detailTable') {
|
|||
|
|
base.source = payload.source || (type === 'detailTable' ? 'detailList' : 'mainTable');
|
|||
|
|
base.mergeColumnKeys = Array.isArray(payload?.mergeColumnKeys) ? payload.mergeColumnKeys.map((item: any) => String(item || '')) : [];
|
|||
|
|
base.strictGrouping = payload?.strictGrouping !== false;
|
|||
|
|
base.enableMultiHeader = payload.enableMultiHeader === true;
|
|||
|
|
base.tableHeightMode = payload.tableHeightMode || 'autoPage';
|
|||
|
|
base.fixedRows = Number(payload.fixedRows || 5);
|
|||
|
|
base.showHeader = payload.showHeader !== false;
|
|||
|
|
base.rowHeight = Number(payload.rowHeight || 8);
|
|||
|
|
base.headerHeight = Number(payload.headerHeight || 10);
|
|||
|
|
base.headerFontSize = Number(payload.headerFontSize || 12);
|
|||
|
|
base.bodyFontSize = Number(payload.bodyFontSize || 12);
|
|||
|
|
base.headerBgColor = payload.headerBgColor || '#f5f5f5';
|
|||
|
|
base.headerTextColor = payload.headerTextColor || '#111111';
|
|||
|
|
base.footerLabelColumnKey = payload.footerLabelColumnKey || '';
|
|||
|
|
base.footerLabelText = payload.footerLabelText || '合计';
|
|||
|
|
base.footerLabelCenter = payload.footerLabelCenter !== false;
|
|||
|
|
base.footerShowTotal = payload.footerShowTotal !== false;
|
|||
|
|
base.footerTotalMode = payload.footerTotalMode === 'page' ? 'page' : 'overall';
|
|||
|
|
base.headerConfig =
|
|||
|
|
payload?.headerConfig && Array.isArray(payload.headerConfig?.cells)
|
|||
|
|
? {
|
|||
|
|
rowCount: Math.max(1, Number(payload.headerConfig.rowCount || 1)),
|
|||
|
|
colCount: Math.max(1, Number(payload.headerConfig.colCount || 1)),
|
|||
|
|
cells: payload.headerConfig.cells.map((cell: any, idx: number) => ({
|
|||
|
|
id: String(cell?.id || `h_${idx}`),
|
|||
|
|
row: Math.max(0, Number(cell?.row || 0)),
|
|||
|
|
col: Math.max(0, Number(cell?.col || 0)),
|
|||
|
|
rowspan: Math.max(1, Number(cell?.rowspan || 1)),
|
|||
|
|
colspan: Math.max(1, Number(cell?.colspan || 1)),
|
|||
|
|
title: String(cell?.title || ''),
|
|||
|
|
align: String(cell?.align || 'center'),
|
|||
|
|
})),
|
|||
|
|
}
|
|||
|
|
: undefined;
|
|||
|
|
base.columns = Array.isArray(payload.columns)
|
|||
|
|
? payload.columns.map((col: any, index: number) => ({
|
|||
|
|
key: col?.key || `col_${index + 1}`,
|
|||
|
|
title: col?.title || `列${index + 1}`,
|
|||
|
|
field: col?.field || `field${index + 1}`,
|
|||
|
|
bindField: col?.bindField || col?.field || `field${index + 1}`,
|
|||
|
|
width: Number(col?.width || 30),
|
|||
|
|
align: col?.align || 'left',
|
|||
|
|
contentType: col?.contentType || 'text',
|
|||
|
|
fontFamily: col?.fontFamily || '',
|
|||
|
|
fontSize: Number(col?.fontSize || 12),
|
|||
|
|
useCustomFontSize: !!col?.useCustomFontSize,
|
|||
|
|
fontColor: col?.fontColor || '#111111',
|
|||
|
|
autoFitFont: !!col?.autoFitFont,
|
|||
|
|
autoWrap: col?.autoWrap !== false,
|
|||
|
|
fillCell: col?.fillCell !== false,
|
|||
|
|
contentScale: Number(col?.contentScale || 100),
|
|||
|
|
imageFit: col?.imageFit || 'contain',
|
|||
|
|
qrLevel: col?.qrLevel || 'M',
|
|||
|
|
qrRenderType: col?.qrRenderType || 'image/png',
|
|||
|
|
barcodeFormat: col?.barcodeFormat || 'CODE128',
|
|||
|
|
decimalPlaces: Number(col?.decimalPlaces ?? 2),
|
|||
|
|
roundHalfUp: col?.roundHalfUp !== false,
|
|||
|
|
amountType: col?.amountType || 'CNY',
|
|||
|
|
enableFooterTotal: !!col?.enableFooterTotal,
|
|||
|
|
mergeByValue: !!col?.mergeByValue,
|
|||
|
|
}))
|
|||
|
|
: [];
|
|||
|
|
if (!base.mergeColumnKeys?.length) {
|
|||
|
|
base.mergeColumnKeys = (base.columns || []).filter((col: any) => col?.mergeByValue).map((col: any) => String(col?.key || ''));
|
|||
|
|
}
|
|||
|
|
return base as NativeElement;
|
|||
|
|
}
|
|||
|
|
if (type === 'freeTable') {
|
|||
|
|
const rowCount = Math.max(1, Number(payload?.rowCount || 3));
|
|||
|
|
const colCount = Math.max(1, Number(payload?.colCount || 3));
|
|||
|
|
base.rowCount = rowCount;
|
|||
|
|
base.colCount = colCount;
|
|||
|
|
base.borderColor = String(payload?.borderColor || '#d9d9d9');
|
|||
|
|
base.borderWidth = Math.max(1, Number(payload?.borderWidth || 1));
|
|||
|
|
base.outerBorderLineStyle = payload?.outerBorderLineStyle || 'solid';
|
|||
|
|
base.innerBorderHorizontalLineStyle = payload?.innerBorderHorizontalLineStyle || 'solid';
|
|||
|
|
base.innerBorderVerticalLineStyle = payload?.innerBorderVerticalLineStyle || 'solid';
|
|||
|
|
if (Array.isArray(payload?.colWidths)) {
|
|||
|
|
base.colWidths = payload.colWidths.map((n: any) => Number(n));
|
|||
|
|
}
|
|||
|
|
if (Array.isArray(payload?.rowHeights)) {
|
|||
|
|
base.rowHeights = payload.rowHeights.map((n: any) => Number(n));
|
|||
|
|
}
|
|||
|
|
if (payload?.outerBorder && typeof payload.outerBorder === 'object') {
|
|||
|
|
base.outerBorder = { ...payload.outerBorder };
|
|||
|
|
}
|
|||
|
|
if (payload?.innerBorder && typeof payload.innerBorder === 'object') {
|
|||
|
|
base.innerBorder = { ...payload.innerBorder };
|
|||
|
|
}
|
|||
|
|
base.cells = Array.isArray(payload?.cells)
|
|||
|
|
? payload.cells
|
|||
|
|
.map((cell: any) => ({
|
|||
|
|
row: Math.max(0, Number(cell?.row || 0)),
|
|||
|
|
col: Math.max(0, Number(cell?.col || 0)),
|
|||
|
|
rowspan: Math.max(1, Number(cell?.rowspan || 1)),
|
|||
|
|
colspan: Math.max(1, Number(cell?.colspan || 1)),
|
|||
|
|
text: String(cell?.text || ''),
|
|||
|
|
bindField: String(cell?.bindField || ''),
|
|||
|
|
contentType: cell?.contentType || 'text',
|
|||
|
|
fillCell: cell?.fillCell,
|
|||
|
|
contentScale: cell?.contentScale != null ? Number(cell.contentScale) : undefined,
|
|||
|
|
imageFit: cell?.imageFit,
|
|||
|
|
qrLevel: cell?.qrLevel,
|
|||
|
|
qrRenderType: cell?.qrRenderType,
|
|||
|
|
barcodeFormat: cell?.barcodeFormat,
|
|||
|
|
decimalPlaces: cell?.decimalPlaces != null ? Number(cell.decimalPlaces) : undefined,
|
|||
|
|
roundHalfUp: cell?.roundHalfUp,
|
|||
|
|
amountType: cell?.amountType,
|
|||
|
|
autoWrap: cell?.autoWrap,
|
|||
|
|
autoFitFont: cell?.autoFitFont,
|
|||
|
|
align: String(cell?.align || 'left'),
|
|||
|
|
verticalAlign: String(cell?.verticalAlign || 'middle'),
|
|||
|
|
fontSize: Math.max(8, Number(cell?.fontSize || 12)),
|
|||
|
|
color: String(cell?.color || '#111111'),
|
|||
|
|
backgroundColor: String(cell?.backgroundColor || '#ffffff'),
|
|||
|
|
hideBorderTop: cell?.hideBorderTop === true,
|
|||
|
|
hideBorderRight: cell?.hideBorderRight === true,
|
|||
|
|
hideBorderBottom: cell?.hideBorderBottom === true,
|
|||
|
|
hideBorderLeft: cell?.hideBorderLeft === true,
|
|||
|
|
}))
|
|||
|
|
.filter(
|
|||
|
|
(cell: any) =>
|
|||
|
|
cell.row < rowCount &&
|
|||
|
|
cell.col < colCount &&
|
|||
|
|
cell.row + cell.rowspan <= rowCount &&
|
|||
|
|
cell.col + cell.colspan <= colCount,
|
|||
|
|
)
|
|||
|
|
: [];
|
|||
|
|
return base as NativeElement;
|
|||
|
|
}
|
|||
|
|
if (type === 'qrcode' || type === 'barcode') {
|
|||
|
|
base.value = payload.value || '';
|
|||
|
|
return base as NativeElement;
|
|||
|
|
}
|
|||
|
|
if (type === 'reportHeader' || type === 'reportFooter') {
|
|||
|
|
base.region = item?.region || (type === 'reportHeader' ? 'header' : 'footer');
|
|||
|
|
base.text = payload.text || '';
|
|||
|
|
base.bookmarkText = payload.bookmarkText || '';
|
|||
|
|
base.keepTogether = payload.keepTogether !== false;
|
|||
|
|
base.centerWithDetail = payload.centerWithDetail !== false;
|
|||
|
|
base.refreshPage = payload.refreshPage || 'none';
|
|||
|
|
base.visible = payload.visible !== false;
|
|||
|
|
base.stretch = payload.stretch === true;
|
|||
|
|
base.shrink = payload.shrink === true;
|
|||
|
|
base.printRepeated = payload.printRepeated === true;
|
|||
|
|
base.printAtPageBottom = payload.printAtPageBottom === true;
|
|||
|
|
base.removeBlankWhenNoData = payload.removeBlankWhenNoData === true;
|
|||
|
|
return base as NativeElement;
|
|||
|
|
}
|
|||
|
|
base.text = payload.text || '';
|
|||
|
|
if (payload.format) {
|
|||
|
|
base.format = payload.format;
|
|||
|
|
}
|
|||
|
|
return base as NativeElement;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function defaultDataBinding() {
|
|||
|
|
return {
|
|||
|
|
fieldMap: {} as Record<string, string>,
|
|||
|
|
tableSources: ['mainTable', 'detailList'],
|
|||
|
|
params: [] as any[],
|
|||
|
|
detailTables: [] as any[],
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function generateCanvasJson() {
|
|||
|
|
const payload = buildNativeTemplateStylePayload(state.schema);
|
|||
|
|
canvasJsonText.value = JSON.stringify(payload, null, 2);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function handleUpdateDataBinding(value: Partial<NonNullable<NativeTemplateSchema['dataBinding']>>) {
|
|||
|
|
patchDataBinding(value);
|
|||
|
|
generateCanvasJson();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function applyCanvasJsonToCanvas() {
|
|||
|
|
try {
|
|||
|
|
const parsed = JSON.parse(canvasJsonText.value || '{}');
|
|||
|
|
if (!Array.isArray(parsed?.elements) || !parsed?.page) {
|
|||
|
|
createMessage.warning('实际 JSON 格式无效,缺少 page 或 elements');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const dbIn = parsed.dataBinding || {};
|
|||
|
|
const curDb = state.schema.dataBinding || defaultDataBinding();
|
|||
|
|
const schema: NativeTemplateSchema = {
|
|||
|
|
engine: 'native',
|
|||
|
|
version: '1.0.0',
|
|||
|
|
page: {
|
|||
|
|
width: Number(parsed.page?.width || 210),
|
|||
|
|
height: Number(parsed.page?.height || 297),
|
|||
|
|
unit: 'mm',
|
|||
|
|
margin: Array.isArray(parsed.page?.margin) ? parsed.page.margin : [10, 10, 10, 10],
|
|||
|
|
gridSize: Number(parsed.page?.gridSize || 2),
|
|||
|
|
},
|
|||
|
|
elements: parsed.elements.map((item: any) => mapTemplateStyleToElement(item)),
|
|||
|
|
dataBinding: {
|
|||
|
|
fieldMap: { ...(curDb.fieldMap || {}), ...(dbIn.fieldMap || {}) },
|
|||
|
|
tableSources: Array.isArray(dbIn.tableSources) && dbIn.tableSources.length
|
|||
|
|
? [...dbIn.tableSources]
|
|||
|
|
: curDb.tableSources || ['mainTable', 'detailList'],
|
|||
|
|
params: Array.isArray(dbIn.params) ? [...dbIn.params] : curDb.params || [],
|
|||
|
|
detailTables: Array.isArray(dbIn.detailTables)
|
|||
|
|
? dbIn.detailTables.map((t: any) => ({
|
|||
|
|
tableKey: String(t.tableKey || ''),
|
|||
|
|
label: t.label ? String(t.label) : undefined,
|
|||
|
|
fields: Array.isArray(t.fields)
|
|||
|
|
? t.fields.map((f: any) => ({
|
|||
|
|
key: String(f.key || ''),
|
|||
|
|
label: f.label ? String(f.label) : undefined,
|
|||
|
|
}))
|
|||
|
|
: [],
|
|||
|
|
}))
|
|||
|
|
: curDb.detailTables || [],
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
setSchema(schema);
|
|||
|
|
generateCanvasJson();
|
|||
|
|
createMessage.success('已按实际 JSON 还原画布模板样式');
|
|||
|
|
} catch (error: any) {
|
|||
|
|
createMessage.error(`应用失败:${error?.message || 'JSON格式错误'}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function loadTemplate() {
|
|||
|
|
const id = String(route.query.id || '');
|
|||
|
|
if (!id) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const record = await queryById(id);
|
|||
|
|
templateId.value = String(record.id || id);
|
|||
|
|
meta.templateCode = String(record.templateCode || meta.templateCode);
|
|||
|
|
meta.templateName = String(record.templateName || meta.templateName);
|
|||
|
|
meta.category = String(record.category || 'form');
|
|||
|
|
meta.remark = String(record.remark || '');
|
|||
|
|
if (record.templateJson) {
|
|||
|
|
const parsed = JSON.parse(record.templateJson);
|
|||
|
|
if (parsed?.engine === 'native') {
|
|||
|
|
// 列表「纸张(mm)」来自实体字段;若仅在编辑弹窗改过纸张,JSON 内 page 可能未同步,以实体为准
|
|||
|
|
const pw = Number(record.paperWidthMm);
|
|||
|
|
const ph = Number(record.paperHeightMm);
|
|||
|
|
if (pw > 0 && ph > 0) {
|
|||
|
|
if (!parsed.page || typeof parsed.page !== 'object') {
|
|||
|
|
parsed.page = { unit: 'mm', margin: [10, 10, 10, 10], gridSize: 2 };
|
|||
|
|
}
|
|||
|
|
parsed.page.width = pw;
|
|||
|
|
parsed.page.height = ph;
|
|||
|
|
}
|
|||
|
|
setSchema(parsed);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
generateCanvasJson();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function handleElementUpdate(payload: { id: string; patch: Partial<NativeElement> }) {
|
|||
|
|
const before = state.schema.elements.find((item) => item.id === payload.id) as any;
|
|||
|
|
const prevW = Number(before?.w);
|
|||
|
|
const prevH = Number(before?.h);
|
|||
|
|
const wasFreeTable = before?.type === 'freeTable';
|
|||
|
|
const patch = payload.patch || ({} as Partial<NativeElement>);
|
|||
|
|
updateElement(payload.id, patch);
|
|||
|
|
const current = state.schema.elements.find((item) => item.id === payload.id) as any;
|
|||
|
|
if (wasFreeTable && current?.type === 'freeTable') {
|
|||
|
|
const rowCount = Math.max(1, Number(current.rowCount || 1));
|
|||
|
|
const colCount = Math.max(1, Number(current.colCount || 1));
|
|||
|
|
const cw = Array.isArray(current.colWidths) ? current.colWidths.map((x: any) => Number(x)) : [];
|
|||
|
|
const rh = Array.isArray(current.rowHeights) ? current.rowHeights.map((x: any) => Number(x)) : [];
|
|||
|
|
const touchedTracks =
|
|||
|
|
Object.prototype.hasOwnProperty.call(patch, 'colWidths') || Object.prototype.hasOwnProperty.call(patch, 'rowHeights');
|
|||
|
|
const bboxChanged =
|
|||
|
|
(Object.prototype.hasOwnProperty.call(patch, 'w') || Object.prototype.hasOwnProperty.call(patch, 'h')) && !touchedTracks;
|
|||
|
|
const nw = Number(current.w);
|
|||
|
|
const nh = Number(current.h);
|
|||
|
|
if (
|
|||
|
|
bboxChanged &&
|
|||
|
|
cw.length === colCount &&
|
|||
|
|
rh.length === rowCount &&
|
|||
|
|
prevW > 0 &&
|
|||
|
|
prevH > 0 &&
|
|||
|
|
(Math.abs(nw - prevW) > 0.001 || Math.abs(nh - prevH) > 0.001)
|
|||
|
|
) {
|
|||
|
|
const scaled = scaleFreeTableTracks(cw, rh, prevW, prevH, nw, nh);
|
|||
|
|
updateElement(payload.id, { colWidths: scaled.colWidths, rowHeights: scaled.rowHeights } as any);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (current?.type === 'reportHeader') {
|
|||
|
|
updateElement(payload.id, {
|
|||
|
|
x: 0,
|
|||
|
|
y: 0,
|
|||
|
|
w: state.schema.page.width,
|
|||
|
|
region: 'header',
|
|||
|
|
bandId: payload.id,
|
|||
|
|
} as any);
|
|||
|
|
} else if (current?.type === 'reportFooter') {
|
|||
|
|
updateElement(payload.id, {
|
|||
|
|
x: 0,
|
|||
|
|
y: Math.max(0, state.schema.page.height - current.h),
|
|||
|
|
w: state.schema.page.width,
|
|||
|
|
region: 'footer',
|
|||
|
|
bandId: payload.id,
|
|||
|
|
} as any);
|
|||
|
|
}
|
|||
|
|
if (selectedTableColumn.value?.elementId === payload.id) {
|
|||
|
|
const columns = Array.isArray(current?.columns) ? current.columns : [];
|
|||
|
|
if (!columns.some((col: any) => col?.key === selectedTableColumn.value?.columnKey)) {
|
|||
|
|
selectedTableColumn.value = null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (selectedFreeTableCell.value?.elementId === payload.id) {
|
|||
|
|
const rowCount = Math.max(1, Number(current?.rowCount || 1));
|
|||
|
|
const colCount = Math.max(1, Number(current?.colCount || 1));
|
|||
|
|
const row = Number(selectedFreeTableCell.value.row || 0);
|
|||
|
|
const col = Number(selectedFreeTableCell.value.col || 0);
|
|||
|
|
if (row >= rowCount || col >= colCount) {
|
|||
|
|
selectedFreeTableCell.value = null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (payload.patch && 'cells' in payload.patch && selectedFreeTableRangeCorner.value?.elementId === payload.id) {
|
|||
|
|
selectedFreeTableRangeCorner.value = null;
|
|||
|
|
}
|
|||
|
|
generateCanvasJson();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function updatePage(patch: any) {
|
|||
|
|
state.schema.page = { ...state.schema.page, ...patch };
|
|||
|
|
state.schema.elements.forEach((item: any) => {
|
|||
|
|
if (item.type === 'reportHeader') {
|
|||
|
|
item.x = 0;
|
|||
|
|
item.y = 0;
|
|||
|
|
item.w = state.schema.page.width;
|
|||
|
|
item.region = 'header';
|
|||
|
|
item.bandId = item.id;
|
|||
|
|
} else if (item.type === 'reportFooter') {
|
|||
|
|
item.x = 0;
|
|||
|
|
item.y = Math.max(0, state.schema.page.height - item.h);
|
|||
|
|
item.w = state.schema.page.width;
|
|||
|
|
item.region = 'footer';
|
|||
|
|
item.bandId = item.id;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
generateCanvasJson();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function handleAddElement(type: NativeElementType) {
|
|||
|
|
if (type === 'reportHeader') {
|
|||
|
|
const exists = state.schema.elements.find((item) => item.type === 'reportHeader');
|
|||
|
|
if (exists) {
|
|||
|
|
setSelected(exists.id);
|
|||
|
|
createMessage.warning('报表头已存在,可直接调整其高度');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (type === 'reportFooter') {
|
|||
|
|
const exists = state.schema.elements.find((item) => item.type === 'reportFooter');
|
|||
|
|
if (exists) {
|
|||
|
|
setSelected(exists.id);
|
|||
|
|
createMessage.warning('报表尾已存在,可直接调整其高度');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
addElement(type);
|
|||
|
|
const current = selectedElement.value as any;
|
|||
|
|
if (current) {
|
|||
|
|
const layout = resolveBandLayout();
|
|||
|
|
if (type === 'reportHeader') {
|
|||
|
|
updateElement(current.id, {
|
|||
|
|
x: 0,
|
|||
|
|
y: 0,
|
|||
|
|
w: state.schema.page.width,
|
|||
|
|
region: 'header',
|
|||
|
|
bandId: current.id,
|
|||
|
|
} as any);
|
|||
|
|
} else if (type === 'reportFooter') {
|
|||
|
|
updateElement(current.id, {
|
|||
|
|
x: 0,
|
|||
|
|
y: Math.max(0, state.schema.page.height - current.h),
|
|||
|
|
w: state.schema.page.width,
|
|||
|
|
region: 'footer',
|
|||
|
|
bandId: current.id,
|
|||
|
|
} as any);
|
|||
|
|
} else {
|
|||
|
|
let targetRegion = insertRegion.value;
|
|||
|
|
if (targetRegion === 'header' && layout.headerHeight <= 0) {
|
|||
|
|
targetRegion = 'body';
|
|||
|
|
createMessage.warning('尚未设置报表头区域,已自动放入报表主体');
|
|||
|
|
}
|
|||
|
|
const minY = targetRegion === 'header' ? 0 : layout.headerHeight;
|
|||
|
|
const maxY = targetRegion === 'header' ? Math.max(0, layout.headerHeight - current.h) : Math.max(layout.headerHeight, layout.bodyBottom - current.h);
|
|||
|
|
const nextY = Math.min(maxY, Math.max(minY, current.y));
|
|||
|
|
const headerBand = state.schema.elements.find((item) => item.type === 'reportHeader') as any;
|
|||
|
|
updateElement(current.id, {
|
|||
|
|
y: nextY,
|
|||
|
|
region: targetRegion,
|
|||
|
|
bandId: targetRegion === 'header' ? String(headerBand?.id || '') : '',
|
|||
|
|
} as any);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
selectedTableColumn.value = null;
|
|||
|
|
selectedFreeTableCell.value = null;
|
|||
|
|
selectedFreeTableRangeCorner.value = null;
|
|||
|
|
generateCanvasJson();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function resolveBandLayout() {
|
|||
|
|
const pageHeight = state.schema.page.height;
|
|||
|
|
const header = state.schema.elements.find((item) => item.type === 'reportHeader') as any;
|
|||
|
|
const footer = state.schema.elements.find((item) => item.type === 'reportFooter') as any;
|
|||
|
|
const headerHeight = Number(header?.h || 0);
|
|||
|
|
const footerHeight = Number(footer?.h || 0);
|
|||
|
|
const bodyBottom = Math.max(headerHeight, pageHeight - footerHeight);
|
|||
|
|
return { headerHeight, footerHeight, bodyBottom };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function buildSavePayload() {
|
|||
|
|
const orientation = state.schema.page.width > state.schema.page.height ? 'landscape' : 'portrait';
|
|||
|
|
return {
|
|||
|
|
id: templateId.value || undefined,
|
|||
|
|
templateCode: meta.templateCode,
|
|||
|
|
templateName: meta.templateName,
|
|||
|
|
category: meta.category || 'form',
|
|||
|
|
paperWidthMm: state.schema.page.width,
|
|||
|
|
paperHeightMm: state.schema.page.height,
|
|||
|
|
paperOrientation: orientation,
|
|||
|
|
remark: meta.remark,
|
|||
|
|
templateJson: serialize(),
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function saveTemplate() {
|
|||
|
|
if (!meta.templateCode || !meta.templateName) {
|
|||
|
|
createMessage.warning('请先填写模板编码和模板名称');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
saving.value = true;
|
|||
|
|
try {
|
|||
|
|
const payload = buildSavePayload();
|
|||
|
|
if (templateId.value) {
|
|||
|
|
await edit(payload);
|
|||
|
|
} else {
|
|||
|
|
await add(payload);
|
|||
|
|
const queryResult = await queryByCode(meta.templateCode);
|
|||
|
|
if (queryResult?.id) {
|
|||
|
|
templateId.value = String(queryResult.id);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
createMessage.success('原生模板保存成功');
|
|||
|
|
} catch (error: any) {
|
|||
|
|
createMessage.error(error?.message || '保存失败');
|
|||
|
|
} finally {
|
|||
|
|
saving.value = false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function previewTemplate() {
|
|||
|
|
previewHtml.value = await renderNativePrintHtml(state.schema, previewData.value);
|
|||
|
|
previewVisible.value = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function printTemplate() {
|
|||
|
|
const html = await renderNativePrintHtml(state.schema, previewData.value);
|
|||
|
|
previewHtml.value = html;
|
|||
|
|
printHtml(html);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function handleDelete() {
|
|||
|
|
removeSelected();
|
|||
|
|
selectedTableColumn.value = null;
|
|||
|
|
selectedFreeTableCell.value = null;
|
|||
|
|
selectedFreeTableRangeCorner.value = null;
|
|||
|
|
generateCanvasJson();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function duplicateElement() {
|
|||
|
|
duplicateSelected();
|
|||
|
|
selectedTableColumn.value = null;
|
|||
|
|
selectedFreeTableCell.value = null;
|
|||
|
|
selectedFreeTableRangeCorner.value = null;
|
|||
|
|
generateCanvasJson();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function handleSelectElement(id: string) {
|
|||
|
|
setSelected(id);
|
|||
|
|
if (!id || selectedTableColumn.value?.elementId !== id) {
|
|||
|
|
selectedTableColumn.value = null;
|
|||
|
|
}
|
|||
|
|
if (!id || selectedFreeTableCell.value?.elementId !== id) {
|
|||
|
|
selectedFreeTableCell.value = null;
|
|||
|
|
}
|
|||
|
|
if (!id || selectedFreeTableRangeCorner.value?.elementId !== id) {
|
|||
|
|
selectedFreeTableRangeCorner.value = null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function handleSelectTableColumn(payload: { elementId: string; columnKey: string }) {
|
|||
|
|
setSelected(payload.elementId);
|
|||
|
|
selectedTableColumn.value = payload;
|
|||
|
|
selectedFreeTableCell.value = null;
|
|||
|
|
selectedFreeTableRangeCorner.value = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function handleSelectFreeTableCell(payload: { elementId: string; row: number; col: number; shiftKey?: boolean }) {
|
|||
|
|
setSelected(payload.elementId);
|
|||
|
|
selectedTableColumn.value = null;
|
|||
|
|
if (payload.shiftKey === true && selectedFreeTableCell.value?.elementId === payload.elementId) {
|
|||
|
|
selectedFreeTableRangeCorner.value = {
|
|||
|
|
elementId: payload.elementId,
|
|||
|
|
row: payload.row,
|
|||
|
|
col: payload.col,
|
|||
|
|
};
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
selectedFreeTableCell.value = {
|
|||
|
|
elementId: payload.elementId,
|
|||
|
|
row: payload.row,
|
|||
|
|
col: payload.col,
|
|||
|
|
};
|
|||
|
|
selectedFreeTableRangeCorner.value = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 画布内双击自由表格单元格:打开仅针对该表的编辑弹窗 */
|
|||
|
|
function handleEditFreeTableCell(payload: { elementId: string; row: number; col: number }) {
|
|||
|
|
setSelected(payload.elementId);
|
|||
|
|
selectedTableColumn.value = null;
|
|||
|
|
selectedFreeTableRangeCorner.value = null;
|
|||
|
|
selectedFreeTableCell.value = {
|
|||
|
|
elementId: payload.elementId,
|
|||
|
|
row: payload.row,
|
|||
|
|
col: payload.col,
|
|||
|
|
};
|
|||
|
|
freeTableCellModalTarget.value = {
|
|||
|
|
elementId: payload.elementId,
|
|||
|
|
row: payload.row,
|
|||
|
|
col: payload.col,
|
|||
|
|
};
|
|||
|
|
freeTableCellModalOpen.value = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function handleFreeTableCellModalSave(payload: {
|
|||
|
|
elementId: string;
|
|||
|
|
row: number;
|
|||
|
|
col: number;
|
|||
|
|
patch: Record<string, unknown>;
|
|||
|
|
}) {
|
|||
|
|
const el = state.schema.elements.find((e) => e.id === payload.elementId) as any;
|
|||
|
|
if (!el || el.type !== 'freeTable') {
|
|||
|
|
freeTableCellModalOpen.value = false;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const rowCount = Math.max(1, Number(el.rowCount || 1));
|
|||
|
|
const colCount = Math.max(1, Number(el.colCount || 1));
|
|||
|
|
const anchors = normalizeFreeTableAnchors(rowCount, colCount, el.cells || []);
|
|||
|
|
const next = anchors.map((item: any) =>
|
|||
|
|
item.row === payload.row && item.col === payload.col ? { ...item, ...payload.patch } : { ...item },
|
|||
|
|
);
|
|||
|
|
handleElementUpdate({ id: payload.elementId, patch: { cells: next } as any });
|
|||
|
|
freeTableCellModalOpen.value = false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function resolveSelectedTableColumnKey() {
|
|||
|
|
if (!selectedElement.value || (selectedElement.value.type !== 'table' && selectedElement.value.type !== 'detailTable')) {
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
if (selectedTableColumn.value?.elementId === selectedElement.value.id) {
|
|||
|
|
return selectedTableColumn.value.columnKey;
|
|||
|
|
}
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function resolveSelectedFreeTableCell() {
|
|||
|
|
if (!selectedElement.value || selectedElement.value.type !== 'freeTable') {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
if (selectedFreeTableCell.value?.elementId === selectedElement.value.id) {
|
|||
|
|
return {
|
|||
|
|
row: Number(selectedFreeTableCell.value.row || 0),
|
|||
|
|
col: Number(selectedFreeTableCell.value.col || 0),
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function resolveSelectedFreeTableMergeRect() {
|
|||
|
|
if (!selectedFreeTableCell.value || !selectedFreeTableRangeCorner.value) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
if (
|
|||
|
|
selectedFreeTableCell.value.elementId !== selectedFreeTableRangeCorner.value.elementId ||
|
|||
|
|
!selectedElement.value ||
|
|||
|
|
selectedElement.value.type !== 'freeTable' ||
|
|||
|
|
selectedFreeTableCell.value.elementId !== selectedElement.value.id
|
|||
|
|
) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
const a = selectedFreeTableCell.value;
|
|||
|
|
const b = selectedFreeTableRangeCorner.value;
|
|||
|
|
return {
|
|||
|
|
r0: Math.min(a.row, b.row),
|
|||
|
|
r1: Math.max(a.row, b.row),
|
|||
|
|
c0: Math.min(a.col, b.col),
|
|||
|
|
c1: Math.max(a.col, b.col),
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function invalidatePendingImageAnalyze() {
|
|||
|
|
imageAnalyzeSeq += 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function clearImageAnalyzeProgressTimer() {
|
|||
|
|
if (imageAnalyzeProgressTimer != null) {
|
|||
|
|
clearInterval(imageAnalyzeProgressTimer);
|
|||
|
|
imageAnalyzeProgressTimer = null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function resetImageAnalyzeProgressUi() {
|
|||
|
|
clearImageAnalyzeProgressTimer();
|
|||
|
|
imageAnalyzeProgress.value = 0;
|
|||
|
|
imageAnalyzeProgressTip.value = '正在准备上传…';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function startImageAnalyzeFakeProgress() {
|
|||
|
|
resetImageAnalyzeProgressUi();
|
|||
|
|
imageAnalyzeProgress.value = 8;
|
|||
|
|
imageAnalyzeProgressTip.value = '正在上传图片…';
|
|||
|
|
imageAnalyzeProgressTimer = setInterval(() => {
|
|||
|
|
imageAnalyzeProgress.value = Math.min(86, imageAnalyzeProgress.value + (Math.random() * 4 + 0.6));
|
|||
|
|
if (imageAnalyzeProgress.value >= 28 && imageAnalyzeProgress.value < 48) {
|
|||
|
|
imageAnalyzeProgressTip.value = '正在调用大模型识别版式与字段…';
|
|||
|
|
} else if (imageAnalyzeProgress.value >= 48) {
|
|||
|
|
imageAnalyzeProgressTip.value = '正在生成模板 JSON,请稍候…';
|
|||
|
|
}
|
|||
|
|
}, 420);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function completeImageAnalyzeProgressSuccess() {
|
|||
|
|
clearImageAnalyzeProgressTimer();
|
|||
|
|
imageAnalyzeProgress.value = 100;
|
|||
|
|
imageAnalyzeProgressTip.value = '分析完成';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function clearImageAnalyzeState() {
|
|||
|
|
if (imageAnalyzeThumbUrl.value) {
|
|||
|
|
URL.revokeObjectURL(imageAnalyzeThumbUrl.value);
|
|||
|
|
}
|
|||
|
|
imageAnalyzeThumbUrl.value = '';
|
|||
|
|
imageAnalyzePreviewHtml.value = '';
|
|||
|
|
imageAnalyzeHint.value = '';
|
|||
|
|
imageAnalyzePendingSchema.value = null;
|
|||
|
|
imageAnalyzeMockJson.value = '';
|
|||
|
|
imageAnalyzeDragover.value = false;
|
|||
|
|
imageAnalyzeLoading.value = false;
|
|||
|
|
resetImageAnalyzeProgressUi();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function resetImageAnalyzeModal() {
|
|||
|
|
invalidatePendingImageAnalyze();
|
|||
|
|
clearImageAnalyzeState();
|
|||
|
|
imageAnalyzeVisible.value = false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function openImageAnalyzeModal() {
|
|||
|
|
clearImageAnalyzeState();
|
|||
|
|
imageAnalyzeVisible.value = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function onImageAnalyzeDragEnter(e: DragEvent) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
if (imageAnalyzeLoading.value) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
imageAnalyzeDragover.value = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function onImageAnalyzeDragOver(e: DragEvent) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
if (imageAnalyzeLoading.value) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
imageAnalyzeDragover.value = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function onImageAnalyzeDragLeave(e: DragEvent) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
if (imageAnalyzeLoading.value) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
imageAnalyzeDragover.value = false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function triggerImageAnalyzeFilePick() {
|
|||
|
|
if (imageAnalyzeLoading.value) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
imageAnalyzeFileInputRef.value?.click();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function onImageAnalyzeFileInputChange(e: Event) {
|
|||
|
|
const input = e.target as HTMLInputElement;
|
|||
|
|
const file = input.files?.[0];
|
|||
|
|
input.value = '';
|
|||
|
|
if (file) {
|
|||
|
|
void processImageAnalyzeFile(file);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function onImageAnalyzeDrop(e: DragEvent) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
imageAnalyzeDragover.value = false;
|
|||
|
|
if (imageAnalyzeLoading.value) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const file = e.dataTransfer?.files?.[0];
|
|||
|
|
if (!file) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (!file.type.startsWith('image/')) {
|
|||
|
|
createMessage.warning('请拖拽图片文件');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
void processImageAnalyzeFile(file);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function processImageAnalyzeFile(file: File) {
|
|||
|
|
if (imageAnalyzeLoading.value) {
|
|||
|
|
createMessage.warning('正在分析中,请稍候');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const mySeq = ++imageAnalyzeSeq;
|
|||
|
|
imageAnalyzeLoading.value = true;
|
|||
|
|
startImageAnalyzeFakeProgress();
|
|||
|
|
try {
|
|||
|
|
const res = await analyzeImageForNativeFile(file);
|
|||
|
|
if (mySeq !== imageAnalyzeSeq) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
imageAnalyzeProgress.value = Math.max(imageAnalyzeProgress.value, 44);
|
|||
|
|
imageAnalyzeProgressTip.value = '正在解析返回结果…';
|
|||
|
|
|
|||
|
|
const parsed = normalizeImportedNativeSchema(JSON.parse(res.templateJson));
|
|||
|
|
imageAnalyzePendingSchema.value = parsed;
|
|||
|
|
imageAnalyzeMockJson.value = res.mockDataJson || '{}';
|
|||
|
|
const extra = res.aiUsed ? '(已调用视觉大模型)' : '(未调用大模型或已回退占位)';
|
|||
|
|
imageAnalyzeHint.value = `${res.hint || ''}${extra}`.trim();
|
|||
|
|
if (imageAnalyzeThumbUrl.value) {
|
|||
|
|
URL.revokeObjectURL(imageAnalyzeThumbUrl.value);
|
|||
|
|
}
|
|||
|
|
imageAnalyzeThumbUrl.value = URL.createObjectURL(file);
|
|||
|
|
let mock: Record<string, any> = {};
|
|||
|
|
try {
|
|||
|
|
mock = JSON.parse(imageAnalyzeMockJson.value || '{}');
|
|||
|
|
} catch (_e) {
|
|||
|
|
mock = {};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (mySeq !== imageAnalyzeSeq) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
imageAnalyzeProgress.value = Math.max(imageAnalyzeProgress.value, 72);
|
|||
|
|
imageAnalyzeProgressTip.value = '正在生成本地预览…';
|
|||
|
|
imageAnalyzePreviewHtml.value = await renderNativePrintHtml(parsed, mock);
|
|||
|
|
|
|||
|
|
if (mySeq !== imageAnalyzeSeq) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
completeImageAnalyzeProgressSuccess();
|
|||
|
|
await new Promise<void>((r) => setTimeout(r, 360));
|
|||
|
|
if (mySeq !== imageAnalyzeSeq) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
createMessage.success('已生成预览,请确认后应用到画布');
|
|||
|
|
} catch (err: any) {
|
|||
|
|
if (mySeq === imageAnalyzeSeq) {
|
|||
|
|
createMessage.error(err?.message || '图片分析失败');
|
|||
|
|
}
|
|||
|
|
} finally {
|
|||
|
|
if (mySeq === imageAnalyzeSeq) {
|
|||
|
|
imageAnalyzeLoading.value = false;
|
|||
|
|
resetImageAnalyzeProgressUi();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function confirmApplyImageAnalyze() {
|
|||
|
|
const pending = imageAnalyzePendingSchema.value;
|
|||
|
|
if (!pending) {
|
|||
|
|
createMessage.warning('请先生成预览');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
setSchema(pending);
|
|||
|
|
const mockText = imageAnalyzeMockJson.value || '{}';
|
|||
|
|
mockDataText.value = mockText;
|
|||
|
|
previewDataText.value = mockText;
|
|||
|
|
dataSourceType.value = 'mock';
|
|||
|
|
selectedTableColumn.value = null;
|
|||
|
|
selectedFreeTableCell.value = null;
|
|||
|
|
selectedFreeTableRangeCorner.value = null;
|
|||
|
|
setSelected('');
|
|||
|
|
generateCanvasJson();
|
|||
|
|
resetImageAnalyzeModal();
|
|||
|
|
createMessage.success('已应用到画布');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function backToList() {
|
|||
|
|
router.push('/print/template');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
onMounted(async () => {
|
|||
|
|
try {
|
|||
|
|
const raw = localStorage.getItem(LS_LEFT_PANEL_KEY);
|
|||
|
|
if (raw != null && raw !== '') {
|
|||
|
|
const n = Number.parseInt(raw, 10);
|
|||
|
|
if (!Number.isNaN(n) && n >= 0 && n <= LEFT_PANEL_MAX) {
|
|||
|
|
leftPanelWidth.value = n;
|
|||
|
|
if (n >= LEFT_PANEL_MIN) {
|
|||
|
|
lastLeftPanelWidth.value = n;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch (_e) {
|
|||
|
|
/* 忽略 */
|
|||
|
|
}
|
|||
|
|
await loadTemplate();
|
|||
|
|
generateCanvasJson();
|
|||
|
|
generateMockData({ syncManual: true, showMessage: false });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
onUnmounted(() => {
|
|||
|
|
stopLeftPanelResize();
|
|||
|
|
invalidatePendingImageAnalyze();
|
|||
|
|
clearImageAnalyzeProgressTimer();
|
|||
|
|
imageAnalyzeLoading.value = false;
|
|||
|
|
if (imageAnalyzeThumbUrl.value) {
|
|||
|
|
URL.revokeObjectURL(imageAnalyzeThumbUrl.value);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped lang="less">
|
|||
|
|
.native-designer-page {
|
|||
|
|
background: #fff;
|
|||
|
|
min-height: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.designer-toolbar {
|
|||
|
|
padding: 12px;
|
|||
|
|
border-bottom: 1px solid #f0f0f0;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
|
|||
|
|
.toolbar-left {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.title {
|
|||
|
|
font-size: 16px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tip {
|
|||
|
|
color: #666;
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.designer-meta {
|
|||
|
|
padding: 10px 12px;
|
|||
|
|
border-bottom: 1px solid #f0f0f0;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 10px;
|
|||
|
|
min-width: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 模板编码/名称等:均分列宽,避免单行 24 栅格在窄宽度下挤压重叠 */
|
|||
|
|
.designer-meta__grid {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|||
|
|
gap: 10px 12px;
|
|||
|
|
width: 100%;
|
|||
|
|
min-width: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 1200px) {
|
|||
|
|
.designer-meta__grid {
|
|||
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 560px) {
|
|||
|
|
.designer-meta__grid {
|
|||
|
|
grid-template-columns: minmax(0, 1fr);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.designer-meta__grid :deep(.ant-input) {
|
|||
|
|
min-width: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 缩放 + 按钮单独一行,保证「按比例缩放」与按钮完整展示 */
|
|||
|
|
.designer-meta__actions {
|
|||
|
|
display: flex;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 8px 10px;
|
|||
|
|
width: 100%;
|
|||
|
|
min-width: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.meta-scale-input {
|
|||
|
|
flex: 1 1 220px;
|
|||
|
|
min-width: 0;
|
|||
|
|
max-width: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.meta-scale-input :deep(.ant-input-number) {
|
|||
|
|
width: 100%;
|
|||
|
|
min-width: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.meta-scale-input :deep(.ant-input-number-group-addon) {
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.meta-scale-btn {
|
|||
|
|
flex: 0 0 auto;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.designer-main {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: row;
|
|||
|
|
align-items: stretch;
|
|||
|
|
min-height: calc(100vh - 132px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.left-panel-wrap {
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
min-height: 0;
|
|||
|
|
overflow: hidden;
|
|||
|
|
border-right: 1px solid #f0f0f0;
|
|||
|
|
background: #fafbfc;
|
|||
|
|
|
|||
|
|
&.is-collapsed {
|
|||
|
|
border-right: none;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.left-panel-resizer {
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
width: 6px;
|
|||
|
|
cursor: col-resize;
|
|||
|
|
align-self: stretch;
|
|||
|
|
position: relative;
|
|||
|
|
z-index: 2;
|
|||
|
|
background: transparent;
|
|||
|
|
user-select: none;
|
|||
|
|
|
|||
|
|
&:hover {
|
|||
|
|
background: rgba(22, 119, 255, 0.08);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&::after {
|
|||
|
|
content: '';
|
|||
|
|
position: absolute;
|
|||
|
|
left: 50%;
|
|||
|
|
top: 18%;
|
|||
|
|
bottom: 18%;
|
|||
|
|
width: 2px;
|
|||
|
|
transform: translateX(-50%);
|
|||
|
|
border-radius: 1px;
|
|||
|
|
background: #d9d9d9;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.left-panel {
|
|||
|
|
flex: 1;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
min-height: 0;
|
|||
|
|
overflow: hidden;
|
|||
|
|
padding: 12px;
|
|||
|
|
|
|||
|
|
.json-box-title {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
margin-bottom: 6px;
|
|||
|
|
font-size: 13px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: rgba(0, 0, 0, 0.75);
|
|||
|
|
line-height: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.json-box-title-space {
|
|||
|
|
margin-top: 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.json-action-row {
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 模板 JSON:纵向 flex + gap,避免 margin 合并导致按钮与文本框仍贴在一起 */
|
|||
|
|
.json-template-pane {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.json-template-pane > .json-box-title {
|
|||
|
|
margin-bottom: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.json-template-pane > .json-action-row.json-action-row--capsule {
|
|||
|
|
margin-bottom: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 模板 JSON:操作按钮胶囊形 */
|
|||
|
|
.json-action-row--capsule,
|
|||
|
|
.json-mock-toolbar {
|
|||
|
|
:deep(.json-capsule-btn.ant-btn) {
|
|||
|
|
height: 28px;
|
|||
|
|
padding: 0 16px;
|
|||
|
|
line-height: 26px;
|
|||
|
|
border-radius: 999px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.json-tabs {
|
|||
|
|
border: 1px solid #f0f0f0;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
background: #fff;
|
|||
|
|
padding: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 参数 JSON:子级「分段胶囊」;灰色轨道随内容收缩,不铺满整行 */
|
|||
|
|
.json-sub-tabs.json-sub-tabs--segmented {
|
|||
|
|
margin-top: 4px;
|
|||
|
|
|
|||
|
|
:deep(.ant-tabs-nav) {
|
|||
|
|
margin-bottom: 10px;
|
|||
|
|
width: fit-content;
|
|||
|
|
max-width: 100%;
|
|||
|
|
align-self: flex-start;
|
|||
|
|
|
|||
|
|
&::before {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(.ant-tabs-nav-wrap) {
|
|||
|
|
border-bottom: none;
|
|||
|
|
display: inline-flex;
|
|||
|
|
align-items: center;
|
|||
|
|
width: fit-content;
|
|||
|
|
max-width: 100%;
|
|||
|
|
padding: 3px 5px;
|
|||
|
|
background: #f0f2f5;
|
|||
|
|
border-radius: 999px;
|
|||
|
|
border: 1px solid #e8e8e8;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(.ant-tabs-nav-list) {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 3px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(.ant-tabs-tab) {
|
|||
|
|
margin: 0 !important;
|
|||
|
|
padding: 4px 12px !important;
|
|||
|
|
border: none !important;
|
|||
|
|
border-radius: 999px !important;
|
|||
|
|
background: transparent;
|
|||
|
|
font-size: 12px;
|
|||
|
|
line-height: 1.4;
|
|||
|
|
color: rgba(0, 0, 0, 0.65);
|
|||
|
|
transition:
|
|||
|
|
background 0.2s,
|
|||
|
|
color 0.2s,
|
|||
|
|
box-shadow 0.2s;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(.ant-tabs-tab-active) {
|
|||
|
|
background: #fff !important;
|
|||
|
|
color: #1677ff !important;
|
|||
|
|
font-weight: 500;
|
|||
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(.ant-tabs-tab:hover:not(.ant-tabs-tab-active)) {
|
|||
|
|
color: rgba(0, 0, 0, 0.85);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(.ant-tabs-ink-bar) {
|
|||
|
|
display: none !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(.ant-tabs-content-holder) {
|
|||
|
|
border-top: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(.ant-tabs-content) {
|
|||
|
|
padding-top: 4px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.json-mock-toolbar {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: flex-end;
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.json-textarea {
|
|||
|
|
:deep(textarea) {
|
|||
|
|
font-family: Consolas, 'Courier New', monospace;
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.center-panel {
|
|||
|
|
flex: 1;
|
|||
|
|
min-width: 0;
|
|||
|
|
min-height: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.right-panel {
|
|||
|
|
width: 320px;
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
min-width: 0;
|
|||
|
|
min-height: 0;
|
|||
|
|
border-left: 1px solid #f0f0f0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.preview-frame {
|
|||
|
|
width: 100%;
|
|||
|
|
height: calc(100vh - 120px);
|
|||
|
|
border: 1px solid #f0f0f0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.image-analyze-body {
|
|||
|
|
min-height: 120px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.image-analyze-file-input {
|
|||
|
|
position: absolute;
|
|||
|
|
width: 1px;
|
|||
|
|
height: 1px;
|
|||
|
|
opacity: 0;
|
|||
|
|
left: -100px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.image-analyze-drop {
|
|||
|
|
position: relative;
|
|||
|
|
border: 1px dashed #d9d9d9;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
padding: 24px 16px;
|
|||
|
|
text-align: center;
|
|||
|
|
background: #fafafa;
|
|||
|
|
transition: border-color 0.2s, background 0.2s;
|
|||
|
|
|
|||
|
|
&.is-dragover {
|
|||
|
|
border-color: #1677ff;
|
|||
|
|
background: #f0f7ff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&.is-busy {
|
|||
|
|
pointer-events: none;
|
|||
|
|
opacity: 0.72;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.image-analyze-progress {
|
|||
|
|
margin-top: 16px;
|
|||
|
|
padding: 14px 16px;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
background: #f0f7ff;
|
|||
|
|
border: 1px solid #d6e4ff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.image-analyze-progress-tip {
|
|||
|
|
margin: 10px 0 0;
|
|||
|
|
font-size: 13px;
|
|||
|
|
color: rgba(0, 0, 0, 0.78);
|
|||
|
|
font-weight: 500;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.image-analyze-progress-sub {
|
|||
|
|
margin: 6px 0 0;
|
|||
|
|
font-size: 12px;
|
|||
|
|
color: rgba(0, 0, 0, 0.45);
|
|||
|
|
line-height: 1.5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.image-analyze-drop-title {
|
|||
|
|
margin: 0 0 4px;
|
|||
|
|
color: rgba(0, 0, 0, 0.65);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.image-analyze-hint {
|
|||
|
|
margin-top: 10px;
|
|||
|
|
font-size: 12px;
|
|||
|
|
color: rgba(0, 0, 0, 0.55);
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.image-analyze-preview-wrap {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 12px;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.image-analyze-thumb {
|
|||
|
|
flex: 0 0 200px;
|
|||
|
|
max-width: 100%;
|
|||
|
|
|
|||
|
|
img {
|
|||
|
|
max-width: 100%;
|
|||
|
|
max-height: 280px;
|
|||
|
|
object-fit: contain;
|
|||
|
|
border: 1px solid #f0f0f0;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
background: #fff;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.image-analyze-preview-frame-wrap {
|
|||
|
|
flex: 1;
|
|||
|
|
min-width: 260px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.thumb-label {
|
|||
|
|
font-size: 12px;
|
|||
|
|
color: rgba(0, 0, 0, 0.45);
|
|||
|
|
margin-bottom: 6px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.image-analyze-preview-frame {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 320px;
|
|||
|
|
border: 1px solid #f0f0f0;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
background: #fff;
|
|||
|
|
}
|
|||
|
|
</style>
|