<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"
      destroy-on-close
      :width="imageAnalyzeModalWidth"
      :centered="false"
      wrap-class-name="native-print-image-analyze-modal"
      :body-style="imageAnalyzeModalBodyStyle"
      :closable="!imageAnalyzeLoading"
      :mask-closable="!imageAnalyzeLoading"
      @cancel="resetImageAnalyzeModal"
    >
      <template #title>
        <div class="image-analyze-modal-title">
          <span class="image-analyze-modal-title-text">上传图片分析模板</span>
          <span class="image-analyze-modal-title-tip">按住标题栏可拖动 · 右下角可拉大窗口</span>
        </div>
      </template>
      <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="image-analyze-preview-toolbar">
              <div class="thumb-label">按生成模板渲染</div>
              <a-space v-if="imageAnalyzeLayoutPaperPx" size="small" wrap @click.stop>
                <a-tooltip title="缩小">
                  <a-button type="default" size="small" class="image-analyze-zoom-btn" @click="imageAnalyzeZoomOut">
                    <Icon icon="ant-design:zoom-out-outlined" />
                  </a-button>
                </a-tooltip>
                <span class="image-analyze-zoom-pct">{{ imageAnalyzeZoomPercentLabel }}</span>
                <a-tooltip title="放大">
                  <a-button type="default" size="small" class="image-analyze-zoom-btn" @click="imageAnalyzeZoomIn">
                    <Icon icon="ant-design:zoom-in-outlined" />
                  </a-button>
                </a-tooltip>
                <a-tooltip title="按预览区大小自动适应">
                  <a-button type="default" size="small" @click="imageAnalyzeZoomFit">适应</a-button>
                </a-tooltip>
              </a-space>
            </div>
            <div
              ref="imageAnalyzePreviewHostRef"
              class="image-analyze-preview-host"
              :style="{ maxHeight: `${imageAnalyzePreviewHostMaxHeight}px` }"
            >
              <template v-if="imageAnalyzeLayoutPaperPx">
                <div class="image-analyze-preview-scroll">
                  <div class="image-analyze-zoom-slot">
                    <div
                      class="image-analyze-scale-shim"
                      :style="{
                        width: `${imageAnalyzeLayoutPaperPx.wPx * imageAnalyzeDisplayScale}px`,
                        height: `${imageAnalyzeLayoutPaperPx.hPx * imageAnalyzeDisplayScale}px`,
                      }"
                    >
                      <div
                        class="image-analyze-scale-inner"
                        :style="{
                          width: `${imageAnalyzeLayoutPaperPx.wPx}px`,
                          height: `${imageAnalyzeLayoutPaperPx.hPx}px`,
                          transform: `scale(${imageAnalyzeDisplayScale})`,
                        }"
                      >
                        <iframe
                          ref="imageAnalyzeIframeRef"
                          class="image-analyze-preview-frame"
                          :srcdoc="imageAnalyzePreviewHtml"
                          @load="onImageAnalyzeIframeLoad"
                        />
                      </div>
                    </div>
                  </div>
                </div>
              </template>
            </div>
          </div>
        </div>
        <div
          v-if="imageAnalyzeVisible"
          class="image-analyze-resize-handle"
          aria-hidden="true"
          @mousedown.prevent="onImageAnalyzeModalResizeStart"
        />
      </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, nextTick, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
  import { useDebounceFn, useResizeObserver } from '@vueuse/core';
  import { useRoute, useRouter } from 'vue-router';
  import { Icon } from '/@/components/Icon';
  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, resolvePrintPageCount } from './core/printRenderer';
  import { generateNativeMockDataObject } from './core/nativeMockData';
  import { buildNativeTemplateStylePayload } from './core/nativeTemplateStyleSerialize';
  import {
    applyDetailTableMultiHeaderFalsePositiveFix,
    applyStackedHeaderBandLayoutFix,
    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);

  /** 图片分析弹窗：宽度与内容区最大高度（可拖动右下角调整） */
  const IMAGE_ANALYZE_MODAL_W_MIN = 680;
  const IMAGE_ANALYZE_MODAL_W_MAX = 1680;
  const IMAGE_ANALYZE_BODY_H_MIN = 400;
  const IMAGE_ANALYZE_BODY_H_MAX = 960;
  const IMAGE_ANALYZE_MM_TO_CSS_PX = 96 / 25.4;
  const IMAGE_ANALYZE_ZOOM_STEP = 1.12;
  const IMAGE_ANALYZE_ZOOM_MULT_MIN = 0.25;
  const IMAGE_ANALYZE_ZOOM_MULT_MAX = 4;

  const imageAnalyzeModalWidth = ref(1000);
  const imageAnalyzeModalBodyMaxHeight = ref(760);
  const imageAnalyzeIframeRef = ref<HTMLIFrameElement | null>(null);
  const imageAnalyzePreviewHostRef = ref<HTMLElement | null>(null);
  /** iframe 内实测内容宽高，用于 autoPage 等超出理论纸张时撑开预览 */
  const imageAnalyzeContentMeasurePx = ref({ w: 0, h: 0 });
  /** 相对「适应预览区」的倍数，1 表示与自动适应一致 */
  const imageAnalyzeZoomMult = ref(1);
  const imageAnalyzeAutoFitScale = ref(1);

  const imageAnalyzeModalBodyStyle = computed(() => ({
    padding: '12px 16px 22px',
    maxHeight: `${imageAnalyzeModalBodyMaxHeight.value}px`,
    overflow: 'auto',
    position: 'relative',
  }));

  const imageAnalyzePreviewHostMaxHeight = computed(() =>
    Math.max(200, imageAnalyzeModalBodyMaxHeight.value - 280),
  );

  function getImageAnalyzeMockObject(): Record<string, any> {
    try {
      return JSON.parse(imageAnalyzeMockJson.value || '{}');
    } catch (_e) {
      return {};
    }
  }

  /** 遍历 iframe 文档估算真实内容包围盒（与列表预览一致思路） */
  function measureImageAnalyzeIframeContentBox(doc: Document): { w: number; h: number } {
    const body = doc.body;
    let minTop = Infinity;
    let maxBottom = 0;
    let minLeft = Infinity;
    let maxRight = 0;
    const visit = (el: Element) => {
      if (el instanceof HTMLElement) {
        const r = el.getBoundingClientRect();
        if (r.height > 0.5 && r.width > 0.5) {
          minTop = Math.min(minTop, r.top);
          maxBottom = Math.max(maxBottom, r.bottom);
          minLeft = Math.min(minLeft, r.left);
          maxRight = Math.max(maxRight, r.right);
        }
      }
      for (let i = 0; i < el.children.length; i++) {
        visit(el.children[i]);
      }
    };
    visit(body);
    const byRect =
      Number.isFinite(minTop) && maxBottom > 0
        ? { w: Math.ceil(maxRight - minLeft + 6), h: Math.ceil(maxBottom - minTop + 12) }
        : { w: 0, h: 0 };
    const sh = Math.max(doc.documentElement.scrollHeight, body.scrollHeight, byRect.h);
    const sw = Math.max(doc.documentElement.scrollWidth, body.scrollWidth, byRect.w);
    return { w: Math.ceil(sw), h: Math.ceil(sh) };
  }

  const imageAnalyzePaperPixelSize = computed(() => {
    const schema = imageAnalyzePendingSchema.value;
    if (!schema) {
      return null;
    }
    const mock = getImageAnalyzeMockObject();
    const pageCount = Math.max(1, resolvePrintPageCount(schema, mock));
    const wMm = Number(schema.page?.width || 210);
    const hMm = Number(schema.page?.height || 297);
    const wPx = wMm * IMAGE_ANALYZE_MM_TO_CSS_PX;
    const hPx = hMm * pageCount * IMAGE_ANALYZE_MM_TO_CSS_PX;
    return { wPx, hPx, pageCount };
  });

  const imageAnalyzeLayoutPaperPx = computed(() => {
    const ps = imageAnalyzePaperPixelSize.value;
    if (!ps) {
      return null;
    }
    const m = imageAnalyzeContentMeasurePx.value;
    return {
      wPx: Math.max(ps.wPx, m.w || 0),
      hPx: Math.max(ps.hPx, m.h || 0),
    };
  });

  const imageAnalyzeDisplayScale = computed(() => {
    const raw = imageAnalyzeAutoFitScale.value * imageAnalyzeZoomMult.value;
    if (!Number.isFinite(raw) || raw <= 0) {
      return 1;
    }
    return Math.min(4, Math.max(0.08, raw));
  });

  const imageAnalyzeZoomPercentLabel = computed(() => `${Math.round(imageAnalyzeZoomMult.value * 100)}%`);

  function computeImageAnalyzeAutoFitScale() {
    const host = imageAnalyzePreviewHostRef.value;
    const ps = imageAnalyzeLayoutPaperPx.value;
    if (!host || !ps) {
      imageAnalyzeAutoFitScale.value = 1;
      return;
    }
    const pad = 16;
    const availW = Math.max(0, host.clientWidth - pad);
    const availH = Math.max(0, host.clientHeight - pad);
    if (availW <= 0 || availH <= 0 || ps.wPx <= 0 || ps.hPx <= 0) {
      imageAnalyzeAutoFitScale.value = 1;
      return;
    }
    const raw = Math.min(availW / ps.wPx, availH / ps.hPx, 1) * 0.96;
    imageAnalyzeAutoFitScale.value = Number.isFinite(raw) && raw > 0 ? raw : 1;
  }

  const debouncedComputeImageAnalyzeScale = useDebounceFn(() => {
    computeImageAnalyzeAutoFitScale();
  }, 120);

  function onImageAnalyzeIframeLoad() {
    void nextTick(() => {
      requestAnimationFrame(() => {
        requestAnimationFrame(() => {
          const doc = imageAnalyzeIframeRef.value?.contentDocument;
          if (!doc?.body) {
            imageAnalyzeContentMeasurePx.value = { w: 0, h: 0 };
            debouncedComputeImageAnalyzeScale();
            return;
          }
          imageAnalyzeContentMeasurePx.value = measureImageAnalyzeIframeContentBox(doc);
          debouncedComputeImageAnalyzeScale();
        });
      });
    });
  }

  function imageAnalyzeZoomIn() {
    imageAnalyzeZoomMult.value = Math.min(IMAGE_ANALYZE_ZOOM_MULT_MAX, imageAnalyzeZoomMult.value * IMAGE_ANALYZE_ZOOM_STEP);
  }

  function imageAnalyzeZoomOut() {
    imageAnalyzeZoomMult.value = Math.max(IMAGE_ANALYZE_ZOOM_MULT_MIN, imageAnalyzeZoomMult.value / IMAGE_ANALYZE_ZOOM_STEP);
  }

  function imageAnalyzeZoomFit() {
    imageAnalyzeZoomMult.value = 1;
    debouncedComputeImageAnalyzeScale();
  }

  function resetImageAnalyzeModalLayout() {
    const vh = typeof window !== 'undefined' ? window.innerHeight : 900;
    imageAnalyzeModalWidth.value = 1000;
    imageAnalyzeModalBodyMaxHeight.value = Math.min(
      IMAGE_ANALYZE_BODY_H_MAX,
      Math.max(IMAGE_ANALYZE_BODY_H_MIN, vh - 140),
    );
    imageAnalyzeZoomMult.value = 1;
    imageAnalyzeAutoFitScale.value = 1;
    imageAnalyzeContentMeasurePx.value = { w: 0, h: 0 };
  }

  function clamp(n: number, lo: number, hi: number) {
    return Math.min(hi, Math.max(lo, n));
  }

  function onImageAnalyzeModalResizeStart(e: MouseEvent) {
    if (e.button !== 0) {
      return;
    }
    const startX = e.clientX;
    const startY = e.clientY;
    const startW = imageAnalyzeModalWidth.value;
    const startBh = imageAnalyzeModalBodyMaxHeight.value;
    const vh = typeof window !== 'undefined' ? window.innerHeight : 900;
    const maxBody = Math.min(IMAGE_ANALYZE_BODY_H_MAX, Math.max(IMAGE_ANALYZE_BODY_H_MIN, vh - 100));
    function move(ev: MouseEvent) {
      const dx = ev.clientX - startX;
      const dy = ev.clientY - startY;
      imageAnalyzeModalWidth.value = Math.round(clamp(startW + dx, IMAGE_ANALYZE_MODAL_W_MIN, IMAGE_ANALYZE_MODAL_W_MAX));
      imageAnalyzeModalBodyMaxHeight.value = Math.round(clamp(startBh + dy, IMAGE_ANALYZE_BODY_H_MIN, maxBody));
      debouncedComputeImageAnalyzeScale();
    }
    function up() {
      document.removeEventListener('mousemove', move);
      document.removeEventListener('mouseup', up);
    }
    document.addEventListener('mousemove', move);
    document.addEventListener('mouseup', up);
  }

  let imageAnalyzeDragTeardown: (() => void) | null = null;

  function teardownImageAnalyzeModalDrag() {
    imageAnalyzeDragTeardown?.();
    imageAnalyzeDragTeardown = null;
  }

  function findImageAnalyzeModalWrap(): HTMLElement | null {
    const list = document.querySelectorAll('.ant-modal-wrap.native-print-image-analyze-modal');
    for (let i = list.length - 1; i >= 0; i--) {
      const el = list[i] as HTMLElement;
      if (getComputedStyle(el).display !== 'none') {
        return el;
      }
    }
    return null;
  }

  /** 标题栏拖动整个弹窗（仅作用于本弹窗 wrap） */
  function setupImageAnalyzeModalDrag() {
    teardownImageAnalyzeModalDrag();
    const wrap = findImageAnalyzeModalWrap();
    if (!wrap) {
      return;
    }
    const dialogHeaderEl = wrap.querySelector('.ant-modal-header') as HTMLElement | null;
    const dragDom = wrap.querySelector('.ant-modal') as HTMLElement | null;
    if (!dialogHeaderEl || !dragDom) {
      return;
    }
    dialogHeaderEl.style.cursor = 'move';
    const getStyle = (dom: HTMLElement, attr: string) => getComputedStyle(dom)[attr as any] as string;

    const onMouseDown = (e: MouseEvent) => {
      if (e.button !== 0) {
        return;
      }
      const disX = e.clientX;
      const disY = e.clientY;
      const screenWidth = document.body.clientWidth;
      const screenHeight = document.documentElement.clientHeight;
      const dragDomWidth = dragDom.offsetWidth;
      const dragDomheight = dragDom.offsetHeight;
      const minDragDomLeft = dragDom.offsetLeft;
      const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth;
      const minDragDomTop = dragDom.offsetTop;
      let maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomheight;
      if (maxDragDomTop < 0) {
        maxDragDomTop = screenHeight - dragDom.offsetTop;
      }
      const domLeft = getStyle(dragDom, 'left');
      const domTop = getStyle(dragDom, 'top');
      let styL = +domLeft;
      let styT = +domTop;
      if (domLeft.includes('%')) {
        styL = +document.body.clientWidth * (+domLeft.replace(/%/g, '') / 100);
        styT = +document.body.clientHeight * (+domTop.replace(/%/g, '') / 100);
      } else {
        styL = +String(domLeft).replace(/px/g, '');
        styT = +String(domTop).replace(/px/g, '');
      }
      if (!Number.isFinite(styL)) {
        styL = dragDom.offsetLeft || 0;
      }
      if (!Number.isFinite(styT)) {
        styT = dragDom.offsetTop || 0;
      }

      function onMove(ev: MouseEvent) {
        let left = ev.clientX - disX;
        let top = ev.clientY - disY;
        if (-left > minDragDomLeft) {
          left = -minDragDomLeft;
        } else if (left > maxDragDomLeft) {
          left = maxDragDomLeft;
        }
        if (-top > minDragDomTop) {
          top = -minDragDomTop;
        } else if (top > maxDragDomTop) {
          top = maxDragDomTop;
        }
        dragDom.style.left = `${left + styL}px`;
        dragDom.style.top = `${top + styT}px`;
      }
      function onUp() {
        document.removeEventListener('mousemove', onMove);
        document.removeEventListener('mouseup', onUp);
      }
      document.addEventListener('mousemove', onMove);
      document.addEventListener('mouseup', onUp);
    };

    dialogHeaderEl.addEventListener('mousedown', onMouseDown);
    imageAnalyzeDragTeardown = () => {
      dialogHeaderEl.removeEventListener('mousedown', onMouseDown);
      dialogHeaderEl.style.cursor = '';
    };
  }

  useResizeObserver(imageAnalyzePreviewHostRef, () => {
    if (imageAnalyzePreviewHtml.value) {
      debouncedComputeImageAnalyzeScale();
    }
  });

  watch(
    () => imageAnalyzeVisible.value,
    (v) => {
      if (v) {
        void nextTick(() => {
          setTimeout(() => setupImageAnalyzeModalDrag(), 40);
          setTimeout(() => debouncedComputeImageAnalyzeScale(), 100);
        });
      } else {
        teardownImageAnalyzeModalDrag();
      }
    },
  );

  watch(
    () => [imageAnalyzePreviewHtml.value, imageAnalyzeLayoutPaperPx.value?.wPx] as const,
    () => {
      if (imageAnalyzePreviewHtml.value && imageAnalyzeLayoutPaperPx.value) {
        void nextTick(() => debouncedComputeImageAnalyzeScale());
      }
    },
  );

  /** 左侧工具栏宽度（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;
    imageAnalyzeContentMeasurePx.value = { w: 0, h: 0 };
    imageAnalyzeZoomMult.value = 1;
    resetImageAnalyzeProgressUi();
  }

  function resetImageAnalyzeModal() {
    invalidatePendingImageAnalyze();
    clearImageAnalyzeState();
    imageAnalyzeVisible.value = false;
  }

  function openImageAnalyzeModal() {
    clearImageAnalyzeState();
    resetImageAnalyzeModalLayout();
    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 = applyDetailTableMultiHeaderFalsePositiveFix(
        applyStackedHeaderBandLayoutFix(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();
    teardownImageAnalyzeModalDrag();
    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-modal-title {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 8px 12px;
    min-width: 0;
  }

  .image-analyze-modal-title-text {
    font-weight: 600;
    font-size: 16px;
  }

  .image-analyze-modal-title-tip {
    font-size: 12px;
    font-weight: 400;
    color: rgba(0, 0, 0, 0.45);
  }

  .image-analyze-body {
    position: relative;
    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%;

    .thumb-label {
      margin-bottom: 6px;
    }

    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;
    min-height: 0;
  }

  .image-analyze-preview-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 8px;
  }

  .image-analyze-zoom-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  .image-analyze-zoom-pct {
    font-size: 12px;
    color: rgba(0, 0, 0, 0.55);
    min-width: 40px;
    text-align: center;
  }

  .image-analyze-preview-host {
    overflow: auto;
    border: 1px solid #f0f0f0;
    border-radius: 6px;
    background: #fafafa;
    min-height: 160px;
  }

  .image-analyze-preview-scroll {
    display: flex;
    justify-content: center;
    align-items: flex-start;
    padding: 10px;
    min-width: 100%;
    min-height: 100%;
    box-sizing: border-box;
  }

  .image-analyze-zoom-slot {
    flex: 0 0 auto;
  }

  .image-analyze-scale-shim {
    position: relative;
    flex-shrink: 0;
  }

  .image-analyze-scale-inner {
    position: absolute;
    left: 0;
    top: 0;
    transform-origin: 0 0;
    will-change: transform;
  }

  .thumb-label {
    font-size: 12px;
    color: rgba(0, 0, 0, 0.45);
    margin-bottom: 0;
  }

  .image-analyze-preview-toolbar .thumb-label {
    margin-bottom: 0;
  }

  .image-analyze-preview-frame {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    border: 0;
    background: #fff;
    pointer-events: auto;
  }

  .image-analyze-resize-handle {
    position: absolute;
    right: 2px;
    bottom: 2px;
    width: 18px;
    height: 18px;
    cursor: se-resize;
    z-index: 5;
    border-radius: 0 0 6px 0;
    background: linear-gradient(135deg, transparent 50%, rgba(0, 0, 0, 0.12) 50%);
  }

  .image-analyze-resize-handle:hover {
    background: linear-gradient(135deg, transparent 45%, rgba(22, 119, 255, 0.35) 45%);
  }
</style>

<style lang="less">
  /* 弹窗挂到 body，需非 scoped 才能命中 */
  .native-print-image-analyze-modal.ant-modal-wrap .ant-modal-header {
    user-select: none;
  }
</style>
