<template>
  <div>
    <BasicTable @register="registerTable" :rowSelection="rowSelection">
      <template #tableTitle>
        <a-space>
          <a-select
            v-model:value="selectedPrinterName"
            :options="printerOptions"
            style="width: 260px"
            allow-clear
            show-search
            option-filter-prop="label"
            placeholder="选择打印机（本地/网络）"
          />
          <a-input
            v-model:value="manualPrinterName"
            style="width: 180px"
            placeholder="手动输入打印机名称"
            @pressEnter="addManualPrinter"
          />
          <a-button @click="addManualPrinter">添加打印机</a-button>
          <a-button @click="refreshPrinterOptions">刷新打印机</a-button>
          <a-checkbox v-model:checked="printDotEnabled" @change="onPrintDotEnabledChange">PrintDot 桥接</a-checkbox>
          <a-input
            v-model:value="printDotWsUrl"
            style="width: 200px"
            placeholder="WS 地址"
            @blur="persistPrintDotConfig"
          />
          <a-input-password
            v-model:value="printDotKey"
            style="width: 120px"
            placeholder="密钥(可选)"
            autocomplete="new-password"
            @blur="persistPrintDotConfig"
          />
        </a-space>
        <a-button type="primary" ghost @click="handleCreateNative" v-auth="'print:template:add'">新增原生模板</a-button>
        <a-button type="primary" @click="openQuickPrintModal" v-auth="'print:template:list'">快速打印</a-button>
        <a-dropdown v-if="selectedRowKeys.length > 0">
          <template #overlay>
            <a-menu>
              <a-menu-item key="1" @click="batchHandleDelete" v-auth="'print:template:delete'">
                <Icon icon="ant-design:delete-outlined" />
                删除
              </a-menu-item>
            </a-menu>
          </template>
          <a-button>
            批量操作
            <Icon icon="mdi:chevron-down" />
          </a-button>
        </a-dropdown>
      </template>
      <template #action="{ record }">
        <TableAction :actions="getTableAction(record)" />
      </template>
    </BasicTable>
    <PrintTemplateModal @register="registerModal" @success="handleSuccess" />
    <NativeTemplateListPreviewModal v-model:open="nativeListPreviewOpen" :template-id="nativeListPreviewTemplateId" />
    <a-modal v-model:open="quickPrintVisible" title="快速打印" width="820px" :confirm-loading="quickPrintLoading" @ok="handleQuickPrint">
      <a-space direction="vertical" style="width: 100%" size="middle">
        <a-radio-group v-model:value="quickPrintMode">
          <a-radio-button value="templateStyle">按模板样式打印（推荐）</a-radio-button>
          <a-radio-button value="lodopTemplate">Lodop实验（模板样式）</a-radio-button>
          <a-radio-button value="pdfServer">前端转PDF后端打印</a-radio-button>
          <a-radio-button value="printDotBridge">PrintDot 本地桥接（PDF）</a-radio-button>
          <a-radio-button value="serverText">服务端直打（纯文本）</a-radio-button>
        </a-radio-group>
        <div v-if="quickPrintMode === 'printDotBridge'" style="font-size: 12px; color: rgba(0, 0, 0, 0.55)">
          需本机运行 PrintDot 客户端（默认 WebSocket ws://127.0.0.1:1122/ws）。勾选「PrintDot
          桥接」并刷新打印机后，可选择桥接器上报的打印机；支持原生模板与 hiprint 模板（均在前端转 PDF 后送出）。
        </div>
        <a-space style="width: 100%">
          <a-select
            v-model:value="quickPrintForm.templateCode"
            :options="templateCodeOptions"
            style="width: 280px"
            show-search
            option-filter-prop="label"
            placeholder="选择模板编号"
          />
          <a-select
            v-model:value="quickPrintForm.printerName"
            :options="printerOptions"
            style="width: 320px"
            show-search
            allow-clear
            option-filter-prop="label"
            placeholder="选择打印机（可为空使用系统默认）"
          />
        </a-space>
        <a-textarea
          v-model:value="quickPrintForm.dataJson"
          :rows="12"
          placeholder='传入打印数据JSON，例如：{"docNo":"MO-001","mainTable":[{"materialCode":"M01","qty":10}]}'
        />
        <a-divider plain orientation="left" style="margin: 4px 0">预览</a-divider>
        <div style="font-size: 12px; color: rgba(0, 0, 0, 0.55); line-height: 1.6">
          打开「打印设计器」并自动执行与工具栏「预览」相同的逻辑（同一套 hiprint 预览、样式注入与表格合并后处理），确保与在设计器内预览一致。
        </div>
        <a-button type="button" :loading="quickPreviewLoading" @click.prevent.stop="handleQuickPrintDesignerPreview">
          预览（与设计器一致）
        </a-button>
      </a-space>
    </a-modal>

    <a-modal
      v-model:open="skillConvertVisible"
      :title="skillModalTitle"
      width="960px"
      :footer="null"
      destroy-on-close
      @cancel="resetSkillConvertState"
    >
      <a-space direction="vertical" style="width: 100%" size="middle">
        <a-alert type="info" show-icon message="流程说明" :description="skillModalAlertDescription" />
        <a-row :gutter="12">
          <a-col :span="10">
            <div class="skill-field-label">模板（支持编号/名称搜索）</div>
            <a-input
              v-model:value="skillTemplateSearch"
              allow-clear
              placeholder="输入关键字过滤模板列表"
              style="margin-bottom: 8px"
            />
            <a-select
              v-model:value="skillConvertForm.templateCode"
              :options="skillTemplateFilteredOptions"
              style="width: 100%"
              show-search
              :filter-option="false"
              option-filter-prop="label"
              placeholder="请选择模板编号"
            />
          </a-col>
          <a-col :span="14">
            <div class="skill-field-label">打印机</div>
            <a-select
              v-model:value="skillConvertForm.printerName"
              :options="printerOptions"
              style="width: 100%"
              show-search
              allow-clear
              option-filter-prop="label"
              placeholder="可为空使用系统默认打印机"
            />
          </a-col>
        </a-row>
        <div>
          <div class="skill-field-label">打印数据 JSON</div>
          <a-textarea v-model:value="skillConvertForm.dataJson" :rows="10" :placeholder="skillDataJsonPlaceholder" />
          <div v-if="skillJsonError" class="skill-json-error">{{ skillJsonError }}</div>
          <a-space style="margin-top: 8px" wrap>
            <a-button size="small" @click="handleSkillInsertExample">填入示例</a-button>
            <a-button size="small" @click="handleSkillFormatDataJson">格式化 JSON</a-button>
            <a-button size="small" type="primary" ghost @click="handleSkillValidateJson">校验 JSON</a-button>
            <a-button size="small" :loading="skillPreviewLoading" @click="handleSkillPreview">生成预览</a-button>
            <a-button size="small" type="primary" :loading="skillPrintLoading" @click="handleSkillSubmitPrint">提交后端 PDF 打印</a-button>
          </a-space>
        </div>
        <div>
          <div class="skill-field-label">预览（与 Lodop 包装一致的 HTML，供确认表头合并与纸张）</div>
          <a-spin :spinning="skillPreviewLoading">
            <div class="skill-preview-wrap">
              <iframe v-if="skillPreviewSrcdoc" class="skill-preview-iframe" :title="skillPreviewIframeTitle" :srcdoc="skillPreviewSrcdoc" />
              <a-empty v-else description="请先选择模板并点击「生成预览」" />
            </div>
          </a-spin>
        </div>
      </a-space>
    </a-modal>
  </div>
</template>

<script lang="ts" name="PrintTemplateList" setup>
  import { computed, onMounted, ref, watch } from 'vue';
  import 'vue-plugin-hiprint/dist/print-lock.css';
  import { Icon } from '/@/components/Icon';
  import { BasicTable, TableAction } from '/@/components/Table';
  import { useModal } from '/@/components/Modal';
  import { useListPage } from '/@/hooks/system/useListPage';
  import { useMessage } from '/@/hooks/web/useMessage';
  import { useRouter } from 'vue-router';
  import { columns, searchFormSchema } from './printTemplate.data';
  import {
    list,
    add,
    deleteOne,
    batchDelete,
    queryPrinters,
    directPrint,
    directPrintPdf,
    queryByCode,
    queryById,
  } from './printTemplate.api';
  import { ensureClodopScriptLoaded } from './lodopLoader';
  import { resolveProviders } from './hiprint/qhmesProvider';
  import PrintTemplateModal from './components/PrintTemplateModal.vue';
  import NativeTemplateListPreviewModal from './components/NativeTemplateListPreviewModal.vue';
  import { QUICK_PRINT_PREVIEW_STORAGE_KEY } from './quickPrintPreviewStorage';
  import {
    buildPdfBase64FromHtmlFragment,
    extractBodyInnerHtmlFromFullDocument,
  } from './utils/printHtmlToPdfBase64';
  import {
    fetchPrintDotPrinters,
    getPrintDotBridgeConfig,
    printDotSendPdf,
    resolvePrintDotPrinterName,
    setPrintDotBridgeConfig,
  } from './utils/printDotBridge';
  import { normalizeImportedNativeSchema } from './native/core/nativeSchemaNormalize';
  import { renderNativePrintHtml } from './native/core/printRenderer';
  import { PRINT_TEMPLATE_SELECTED_PRINTER_KEY } from './utils/printNativeViaPrintDot';

  defineOptions({ name: 'PrintTemplateList' });

  /** 原生模板列表「预览」弹窗 */
  const nativeListPreviewOpen = ref(false);
  const nativeListPreviewTemplateId = ref<string | null>(null);

  const router = useRouter();
  const { createMessage } = useMessage();
  const [registerModal, { openModal }] = useModal();
  const selectedPrinterName = ref<string | undefined>();
  const printerOptions = ref<Array<{ label: string; value: string }>>([]);
  const manualPrinterName = ref('');
  const quickPrintVisible = ref(false);
  const quickPrintLoading = ref(false);
  const quickPreviewLoading = ref(false);
  const templateCodeOptions = ref<Array<{ label: string; value: string }>>([]);
  const quickPrintForm = ref<{ templateCode: string; printerName?: string; dataJson: string }>({
    templateCode: '',
    printerName: '__system_default__',
    dataJson: '',
  });
  const quickPrintMode = ref<'templateStyle' | 'lodopTemplate' | 'pdfServer' | 'printDotBridge' | 'serverText'>(
    'templateStyle',
  );
  const LS_PRINT_DOT_ENABLED = 'qhmes_print_dot_enabled';
  const printDotEnabled = ref(localStorage.getItem(LS_PRINT_DOT_ENABLED) === '1');
  const printDotCfg = getPrintDotBridgeConfig();
  const printDotWsUrl = ref(printDotCfg.wsUrl);
  const printDotKey = ref(printDotCfg.key);

  function persistPrintDotConfig() {
    setPrintDotBridgeConfig(printDotWsUrl.value, printDotKey.value);
  }

  function onPrintDotEnabledChange() {
    localStorage.setItem(LS_PRINT_DOT_ENABLED, printDotEnabled.value ? '1' : '0');
    void refreshPrinterOptions(false);
  }
  /** 技能转换打印：示例数据（与快速打印占位说明一致） */
  const SKILL_DATA_JSON_EXAMPLE = `{
  "docNo": "MO-001",
  "mainTable": [
    { "materialCode": "M01", "materialName": "示例物料", "qty": 10 }
  ]
}`;
  const skillDataJsonPlaceholder = `请填写对象或数组形式的打印数据，例如：\n${SKILL_DATA_JSON_EXAMPLE}`;
  const skillConvertVisible = ref(false);
  const skillConvertForm = ref<{ templateCode: string; printerName?: string; dataJson: string }>({
    templateCode: '',
    printerName: '__system_default__',
    dataJson: SKILL_DATA_JSON_EXAMPLE,
  });
  const skillTemplateSearch = ref('');
  const skillPreviewSrcdoc = ref('');
  const skillPreviewLoading = ref(false);
  const skillPrintLoading = ref(false);
  const skillJsonError = ref('');
  /** convert：原「技能转换打印」；guide：对齐仓库 Cursor 技能 hiprint-export-print */
  const skillPrintModalMode = ref<'convert' | 'guide'>('convert');

  const skillModalTitle = computed(() =>
    skillPrintModalMode.value === 'guide'
      ? '技能指南打印（hiprint-export-print）'
      : '技能转换打印（hiprint → PDF → 后端队列）',
  );

  const skillModalAlertDescription = computed(() =>
    skillPrintModalMode.value === 'guide'
      ? '与项目 Cursor 技能「hiprint-export-print」约定一致：hiprint 模板 JSON + 打印数据 JSON → 校验 → 生成预览（Lodop 同源 HTML 包装）→ html2canvas + jsPDF 生成 PDF → 调用后端 directPrintPdf。开发人员可查阅仓库 .cursor/skills/hiprint-export-print/SKILL.md 中的文件路径、自检项与常见问题。'
      : '选择模板并填写打印数据 JSON → 校验 → 生成预览确认版式 → 提交为 PDF 由后端发送到所选打印机。',
  );

  const skillPreviewIframeTitle = computed(() =>
    skillPrintModalMode.value === 'guide' ? '技能指南打印预览' : '技能转换预览',
  );

  const skillTemplateFilteredOptions = computed(() => {
    const keyword = skillTemplateSearch.value.trim().toLowerCase();
    const all = templateCodeOptions.value;
    if (!keyword) {
      return all;
    }
    return all.filter(
      (item) =>
        String(item.value || '')
          .toLowerCase()
          .includes(keyword) || String(item.label || '').toLowerCase().includes(keyword),
    );
  });

  let hiprint: any = null;
  function resolveHiprint(module: any) {
    const defaultExport = module?.default || {};
    hiprint = module?.hiprint || defaultExport?.hiprint || (window as any)?.hiprint;
    return hiprint;
  }

  async function initHiprintForQuickPrint() {
    if (hiprint) {
      return;
    }
    const module = await import('vue-plugin-hiprint');
    const hp = resolveHiprint(module);
    if (!hp) {
      throw new Error('未获取到 hiprint 实例');
    }
    hp.init({ providers: resolveProviders(module) });
  }


  const { tableContext } = useListPage({
    tableProps: {
      title: '打印模板',
      api: list,
      columns,
      rowKey: 'id',
      formConfig: { schemas: searchFormSchema },
      actionColumn: { width: 300 },
    },
  });

  const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;

  async function refreshPrinterOptions(showMessage = true) {
    const payload = (await queryPrinters()) as Record<string, any>;
    const names = [
      ...(Array.isArray(payload?.serverPrinters) ? payload.serverPrinters : []),
      ...(Array.isArray(payload?.networkPrinters) ? payload.networkPrinters : []),
    ]
      .map((item) => String(item || '').trim())
      .filter(Boolean)
      .filter((item, index, arr) => arr.indexOf(item) === index);
    const optionMap = new Map<string, { label: string; value: string }>();
    // 永远保留“系统默认打印机”兜底项，避免插件不可用时无法选择
    optionMap.set('__system_default__', { label: '系统默认打印机', value: '__system_default__' });
    names.forEach((item) => {
      optionMap.set(item, { label: item, value: item });
    });
    if (selectedPrinterName.value && !optionMap.has(selectedPrinterName.value)) {
      optionMap.set(selectedPrinterName.value, {
        label: `${selectedPrinterName.value}（手动）`,
        value: selectedPrinterName.value,
      });
    }
    printerOptions.value = Array.from(optionMap.values());
    if (printDotEnabled.value) {
      try {
        const dotList = await fetchPrintDotPrinters();
        dotList.forEach((p) => {
          const name = String(p.name || '').trim();
          if (!name) {
            return;
          }
          if (!optionMap.has(name)) {
            optionMap.set(name, { label: `[PrintDot] ${name}`, value: name });
          }
        });
        if (showMessage && dotList.length) {
          createMessage.success(`PrintDot 已连接，识别 ${dotList.length} 台打印机`);
        }
      } catch (e: any) {
        if (showMessage) {
          createMessage.warning(`PrintDot：${e?.message || '无法连接本地桥接器'}`);
        }
      }
    }
    if (showMessage) {
      if (names.length) {
        createMessage.success(`已从服务端识别到 ${names.length} 台打印机`);
      } else if (!printDotEnabled.value) {
        const reason = String(payload?.capability?.localReason || '').trim();
        createMessage.warning(`服务端未返回可用打印机。${reason || '请在后端配置网络打印机后重试。'}`);
      }
    }
  }

  function addManualPrinter() {
    const name = String(manualPrinterName.value || '').trim();
    if (!name) {
      return;
    }
    const exists = printerOptions.value.some((item) => item.value === name);
    if (!exists) {
      printerOptions.value = [...printerOptions.value, { label: `${name}（手动）`, value: name }];
    }
    selectedPrinterName.value = name;
    manualPrinterName.value = '';
    createMessage.success('已添加手动打印机名称');
  }

  async function loadTemplateCodeOptions() {
    const pageData = (await list({ pageNo: 1, pageSize: 500 })) as Record<string, any>;
    const records = (pageData?.records || pageData?.result?.records || []) as Record<string, any>[];
    templateCodeOptions.value = records
      .map((item) => ({
        value: String(item?.templateCode || '').trim(),
        label: `${String(item?.templateCode || '').trim()} ${item?.templateName ? `- ${item.templateName}` : ''}`.trim(),
      }))
      .filter((item) => !!item.value);
  }

  async function openQuickPrintModal() {
    quickPrintForm.value.printerName = selectedPrinterName.value || '__system_default__';
    quickPrintMode.value = 'templateStyle';
    if (!templateCodeOptions.value.length) {
      await loadTemplateCodeOptions();
    }
    quickPrintVisible.value = true;
  }

  /**
   * 跳转打印设计器并自动调用与设计器「预览」相同的 previewTemplate 逻辑（见 PrintDesigner runQuickPrintPreviewFromSessionStorage）
   */
  async function handleQuickPrintDesignerPreview() {
    const templateCode = String(quickPrintForm.value.templateCode || '').trim();
    if (!templateCode) {
      createMessage.warning('请先选择模板编号');
      return;
    }
    const dataText = String(quickPrintForm.value.dataJson || '').trim();
    if (!dataText) {
      createMessage.warning('请先输入打印数据 JSON');
      return;
    }
    try {
      JSON.parse(dataText);
    } catch (error: any) {
      createMessage.error(`打印数据JSON格式错误：${error?.message || '未知错误'}`);
      return;
    }
    quickPreviewLoading.value = true;
    try {
      const tpl = (await queryByCode(templateCode)) as Record<string, any>;
      const id = String(tpl?.id ?? (tpl as any)?.result?.id ?? '').trim();
      if (!id) {
        createMessage.error(`未找到模板记录主键，无法打开设计器预览。返回字段：${Object.keys(tpl || {}).join(', ') || '空'}`);
        return;
      }
      sessionStorage.setItem(
        QUICK_PRINT_PREVIEW_STORAGE_KEY,
        JSON.stringify({
          dataJsonText: dataText,
        }),
      );
      const previewQuery = {
        id,
        quickPrintPreview: '1',
        _qpt: String(Date.now()),
      } as Record<string, string>;
      const navigateToDesignerPreview = async () => {
        const isDuplicateNav = (err: unknown) => /redundant|duplicated|Avoided/i.test(String((err as any)?.message ?? err ?? ''));
        const candidates = [
          { name: 'print-designer' as const, query: previewQuery },
          { path: '/print/designer', query: previewQuery },
        ];
        let lastErr: unknown = null;
        for (const loc of candidates) {
          try {
            await router.push(loc as any);
            return;
          } catch (e: any) {
            lastErr = e;
            if (isDuplicateNav(e)) {
              await router.replace(loc as any);
              return;
            }
          }
        }
        throw lastErr;
      };
      await navigateToDesignerPreview();
      createMessage.success('正在打开设计器预览…');
      quickPrintVisible.value = false;
    } catch (error: any) {
      sessionStorage.removeItem(QUICK_PRINT_PREVIEW_STORAGE_KEY);
      createMessage.error(`无法打开预览：${error?.message || '未知错误'}`);
    } finally {
      quickPreviewLoading.value = false;
    }
  }

  function resetSkillConvertState() {
    skillPreviewSrcdoc.value = '';
    skillJsonError.value = '';
    skillTemplateSearch.value = '';
  }

  function handleSkillInsertExample() {
    skillConvertForm.value.dataJson = SKILL_DATA_JSON_EXAMPLE;
    skillJsonError.value = '';
    createMessage.success('已填入示例 JSON');
  }

  function handleSkillFormatDataJson() {
    const text = String(skillConvertForm.value.dataJson || '').trim();
    if (!text) {
      createMessage.warning('请先输入 JSON');
      return;
    }
    try {
      skillConvertForm.value.dataJson = JSON.stringify(JSON.parse(text), null, 2);
      skillJsonError.value = '';
      createMessage.success('已格式化');
    } catch (error: any) {
      skillJsonError.value = error?.message || 'JSON 格式错误';
      createMessage.error(`无法格式化：${skillJsonError.value}`);
    }
  }

  /** silent=true 时不弹出“格式正确”提示，供预览/打印前校验 */
  function validateSkillDataJsonInternal(silent: boolean): boolean {
    const text = String(skillConvertForm.value.dataJson || '').trim();
    if (!text) {
      skillJsonError.value = '内容为空';
      createMessage.warning('请先输入打印数据 JSON');
      return false;
    }
    try {
      JSON.parse(text);
      skillJsonError.value = '';
      if (!silent) {
        createMessage.success('JSON 格式正确');
      }
      return true;
    } catch (error: any) {
      skillJsonError.value = error?.message || 'JSON 解析失败';
      createMessage.error(`校验失败：${skillJsonError.value}`);
      return false;
    }
  }

  function handleSkillValidateJson() {
    validateSkillDataJsonInternal(false);
  }

  /** hiprint 渲染 HTML → html2canvas → jsPDF → 提交后端 directPrintPdf */
  async function executePdfServerPrint(params: {
    templateCode: string;
    printerName?: string;
    dataJson: any;
    fileName?: string;
  }) {
    const templateCode = String(params.templateCode || '').trim();
    await initHiprintForQuickPrint();
    const tplData = (await queryByCode(templateCode)) as Record<string, any>;
    const templateJsonText = String(tplData?.templateJson || '').trim();
    if (!templateJsonText) {
      throw new Error('模板JSON为空，无法生成 PDF');
    }
    let templateJson: any;
    try {
      templateJson = JSON.parse(templateJsonText);
    } catch (error: any) {
      throw new Error(`模板JSON格式错误：${error?.message || '未知错误'}`);
    }
    const runtimeTemplate = new hiprint.PrintTemplate({
      template: templateJson,
    });
    const html = optimizeMergedHeaderHtml(await resolveTemplateHtml(runtimeTemplate, params.dataJson), templateJson);
    if (!html) {
      throw new Error('当前模板未生成可用预览内容，无法转 PDF');
    }
    const panel = Array.isArray(templateJson?.panels) && templateJson.panels.length ? templateJson.panels[0] : {};
    const widthMm = Number(panel?.width || 210);
    const heightMm = Number(panel?.height || 297);
    const pdfBase64 = await buildPdfBase64FromHtmlFragment(html, widthMm, heightMm);
    const printer = String(params.printerName || '').trim();
    await directPrintPdf({
      templateCode,
      printerName: printer,
      dataJson: params.dataJson,
      pdfBase64,
      fileName: params.fileName || `${templateCode}.pdf`,
    });
  }

  async function handleSkillPreview() {
    const templateCode = String(skillConvertForm.value.templateCode || '').trim();
    if (!templateCode) {
      createMessage.warning('请先选择模板');
      return;
    }
    if (!validateSkillDataJsonInternal(true)) {
      return;
    }
    let dataJson: any;
    try {
      dataJson = JSON.parse(String(skillConvertForm.value.dataJson || '').trim());
    } catch {
      return;
    }
    skillPreviewLoading.value = true;
    try {
      await initHiprintForQuickPrint();
      const tplData = (await queryByCode(templateCode)) as Record<string, any>;
      const templateJsonText = String(tplData?.templateJson || '').trim();
      if (!templateJsonText) {
        createMessage.error('模板JSON为空');
        return;
      }
      let templateJson: any;
      try {
        templateJson = JSON.parse(templateJsonText);
      } catch (error: any) {
        createMessage.error(`模板JSON格式错误：${error?.message || ''}`);
        return;
      }
      const runtimeTemplate = new hiprint.PrintTemplate({
        template: templateJson,
      });
      const html = optimizeMergedHeaderHtml(await resolveTemplateHtml(runtimeTemplate, dataJson), templateJson);
      if (!html) {
        createMessage.error('未能从 hiprint 生成预览 HTML，请检查模板与数据字段是否匹配');
        return;
      }
      const panel = Array.isArray(templateJson?.panels) && templateJson.panels.length ? templateJson.panels[0] : {};
      const widthMm = Number(panel?.width || 0);
      const heightMm = Number(panel?.height || 0);
      skillPreviewSrcdoc.value = buildLodopDocumentHtml(html, widthMm > 0 ? widthMm : undefined, heightMm > 0 ? heightMm : undefined);
      createMessage.success('预览已生成');
    } catch (error: any) {
      createMessage.error(`预览失败：${error?.message || '未知错误'}`);
    } finally {
      skillPreviewLoading.value = false;
    }
  }

  async function handleSkillSubmitPrint() {
    const templateCode = String(skillConvertForm.value.templateCode || '').trim();
    if (!templateCode) {
      createMessage.warning('请先选择模板');
      return;
    }
    if (!validateSkillDataJsonInternal(true)) {
      return;
    }
    let dataJson: any;
    try {
      dataJson = JSON.parse(String(skillConvertForm.value.dataJson || '').trim());
    } catch {
      return;
    }
    skillPrintLoading.value = true;
    try {
      await executePdfServerPrint({
        templateCode,
        printerName: skillConvertForm.value.printerName,
        dataJson,
        fileName:
          skillPrintModalMode.value === 'guide' ? `${templateCode}-hiprint-export-print.pdf` : `${templateCode}-skill.pdf`,
      });
      createMessage.success('已提交 PDF 到后端打印队列');
      skillConvertVisible.value = false;
    } catch (error: any) {
      createMessage.error(`提交失败：${error?.message || '未知错误'}`);
    } finally {
      skillPrintLoading.value = false;
    }
  }

  function normalizeHtmlOutput(value: any): string {
    if (!value) {
      return '';
    }
    if (typeof value === 'string') {
      return value.trim();
    }
    if (Array.isArray(value)) {
      return value
        .map((item) => normalizeHtmlOutput(item))
        .filter(Boolean)
        .join('');
    }
    if (value instanceof HTMLElement) {
      return value.outerHTML || value.innerHTML || '';
    }
    if (value && typeof value === 'object') {
      // 兼容 jQuery 对象：优先取第一个元素的 HTML
      const first = (value as any)[0];
      if (first instanceof HTMLElement) {
        return first.outerHTML || first.innerHTML || '';
      }
      if (typeof (value as any).html === 'function') {
        try {
          const htmlValue = (value as any).html();
          return typeof htmlValue === 'string' ? htmlValue.trim() : '';
        } catch (_error) {
          // ignore
        }
      }
      if (typeof (value as any).outerHTML === 'string') {
        return String((value as any).outerHTML).trim();
      }
      if (typeof (value as any).innerHTML === 'string') {
        return String((value as any).innerHTML).trim();
      }
    }
    return '';
  }

  async function resolveTemplateHtml(runtimeTemplate: any, dataJson: any): Promise<string> {
    const buildDataCandidates = (rawData: any): any[] => {
      const candidates: any[] = [rawData];
      if (Array.isArray(rawData)) {
        candidates.push({
          detailList: rawData,
          mainTable: rawData,
          list: rawData,
          items: rawData,
          dataList: rawData,
        });
        return candidates;
      }
      if (rawData && typeof rawData === 'object') {
        const objectData = rawData as Record<string, any>;
        const firstArrayKey = Object.keys(objectData).find((key) => Array.isArray(objectData[key]));
        if (firstArrayKey) {
          const arr = objectData[firstArrayKey];
          candidates.push({
            ...objectData,
            detailList: objectData.detailList ?? arr,
            mainTable: objectData.mainTable ?? arr,
            list: objectData.list ?? arr,
            items: objectData.items ?? arr,
            dataList: objectData.dataList ?? arr,
          });
        }
      }
      return candidates;
    };

    const fixedCandidates = ['getHtml', 'toHtml', 'getHtmlByData'];
    const dataCandidates = buildDataCandidates(dataJson);
    for (const methodName of fixedCandidates) {
      const fn = runtimeTemplate?.[methodName];
      if (typeof fn !== 'function') {
        continue;
      }
      for (const dataItem of dataCandidates) {
        try {
          const maybeValue = await Promise.resolve(fn.call(runtimeTemplate, dataItem));
          const html = normalizeHtmlOutput(maybeValue);
          if (html) {
            return html;
          }
        } catch (_error) {
          // ignore
        }
      }
    }
    // 兜底：扫描原型上的潜在导出方法（不同版本命名不一致）
    const proto = Object.getPrototypeOf(runtimeTemplate);
    const methodNames = Object.getOwnPropertyNames(proto || {})
      .filter((name) => name !== 'constructor')
      .filter((name) => /html|preview|content/i.test(name))
      .filter((name) => !/^print$/i.test(name));
    for (const methodName of methodNames) {
      const fn = runtimeTemplate?.[methodName];
      if (typeof fn !== 'function') {
        continue;
      }
      for (const dataItem of dataCandidates) {
        const argSets = [[dataItem], [dataItem, {}], []] as any[][];
        for (const args of argSets) {
          try {
            const maybeValue = await Promise.resolve(fn.apply(runtimeTemplate, args));
            const html = normalizeHtmlOutput(maybeValue);
            if (html) {
              return html;
            }
          } catch (_error) {
            // ignore
          }
        }
      }
    }
    return '';
  }

  function normalizeHeaderRowsFromTemplate(options: Record<string, any>): any[][] {
    const candidates = [options?.headerColumns, options?.columns, options?.tableColumns];
    for (const candidate of candidates) {
      if (!Array.isArray(candidate) || !candidate.length) {
        continue;
      }
      if (Array.isArray(candidate[0])) {
        return candidate.map((row: any) => (Array.isArray(row) ? row : []));
      }
      return [candidate];
    }
    return [];
  }

  function resolveTemplateHeaderMeta(templateJson?: any): { rows: any[][]; options: Record<string, any> | null } {
    const panels = Array.isArray(templateJson?.panels) ? templateJson.panels : [];
    for (const panel of panels) {
      const printElements = Array.isArray(panel?.printElements) ? panel.printElements : [];
      for (const element of printElements) {
        const options = element?.options || {};
        const rows = normalizeHeaderRowsFromTemplate(options);
        if (rows.length) {
          return { rows, options };
        }
      }
    }
    return { rows: [], options: null };
  }

  /** 与 PrintDesigner 一致：兼容 {{field}} / item.xxx */
  function normalizeBindField(raw: unknown) {
    let field = String(raw ?? '').trim();
    if (!field) {
      return '';
    }
    const mustacheMatch = field.match(/^\{\{\s*([^}]+)\s*\}\}$/);
    if (mustacheMatch?.[1]) {
      field = mustacheMatch[1].trim();
    }
    field = field.replace(/^item\./i, '').trim();
    return field;
  }

  /**
   * 与 PrintDesigner.resolveBodyColumnsByHeaderRows 一致：按表头网格得到叶子列顺序（含无 field 的占位列，如行号）
   */
  function resolveBodyColumnsByHeaderRows(headerRows: any[]): any[] {
    if (!Array.isArray(headerRows) || !headerRows.length) {
      return [];
    }
    const rowCount = headerRows.length;
    let totalCols = 0;
    headerRows.forEach((row) => {
      const width = (Array.isArray(row) ? row : []).reduce(
        (sum: number, cell: any) => sum + (Number(cell?.colspan || cell?.colSpan || 1) || 1),
        0,
      );
      totalCols = Math.max(totalCols, width);
    });
    if (!totalCols) {
      return [];
    }
    const grid: any[][] = Array.from({ length: rowCount }, () => Array.from({ length: totalCols }, () => null));
    for (let r = 0; r < rowCount; r += 1) {
      const row = Array.isArray(headerRows[r]) ? headerRows[r] : [];
      let c = 0;
      row.forEach((cell: any) => {
        while (c < totalCols && grid[r][c]) {
          c += 1;
        }
        const rowspan = Number(cell?.rowspan || cell?.rowSpan || 1) || 1;
        const colspan = Number(cell?.colspan || cell?.colSpan || 1) || 1;
        for (let rr = r; rr < Math.min(rowCount, r + rowspan); rr += 1) {
          for (let cc = c; cc < Math.min(totalCols, c + colspan); cc += 1) {
            grid[rr][cc] = cell;
          }
        }
        c += colspan;
      });
    }
    const result: any[] = [];
    for (let c = 0; c < totalCols; c += 1) {
      let hitCell: any = null;
      for (let r = rowCount - 1; r >= 0; r -= 1) {
        const cell = grid[r][c];
        const field = cell?.field || cell?.dataIndex || cell?.key || cell?.name || '';
        if (field) {
          hitCell = cell;
          break;
        }
      }
      if (!hitCell) {
        result.push({ field: '', title: '', align: '', halign: '', width: '', autoWrap: false });
        continue;
      }
      const field = normalizeBindField(hitCell?.field || hitCell?.dataIndex || hitCell?.key || hitCell?.name || '');
      const align = String(hitCell?.align || '');
      const halign = String(hitCell?.halign || hitCell?.align || '');
      const width = hitCell?.width || hitCell?.minWidth || '';
      const autoWrap = Boolean(hitCell?.autoWrap ?? hitCell?.tableAutoWrap ?? hitCell?.wrap ?? hitCell?.wordWrap ?? false);
      result.push({ field, title: hitCell?.title || '', align, halign, width, autoWrap });
    }
    return result;
  }

  function resolveBodyColumnsFromTemplateOptions(options: Record<string, any>): any[] {
    const headerRows = normalizeHeaderRowsFromTemplate(options);
    if (headerRows.length) {
      return resolveBodyColumnsByHeaderRows(headerRows);
    }
    if (Array.isArray(options?.fields) && options.fields.length) {
      return options.fields.map((item: any) =>
        typeof item === 'string'
          ? { field: normalizeBindField(item), title: item, align: '', halign: '', width: '', autoWrap: false }
          : {
              field: normalizeBindField(item?.field || item?.dataIndex || item?.key || ''),
              title: item?.title || item?.text || item?.label || '',
              width: item?.width || item?.columnWidth || item?.w || '',
              align: item?.align || item?.halign || '',
              halign: item?.halign || item?.align || '',
              autoWrap: item?.autoWrap ?? item?.tableAutoWrap ?? item?.wrap ?? item?.wordWrap,
            },
      );
    }
    return [];
  }

  function applyBodyColumnMetaToTable(table: HTMLTableElement, options: Record<string, any>) {
    const tbodyRow = table.querySelector('tbody tr');
    if (!tbodyRow) {
      return;
    }
    const bodyCellCount = tbodyRow.querySelectorAll('td').length;
    if (!bodyCellCount) {
      return;
    }
    let bodyColumns = resolveBodyColumnsFromTemplateOptions(options);
    if (!bodyColumns.length) {
      return;
    }
    if (bodyColumns.length > bodyCellCount) {
      bodyColumns = bodyColumns.slice(0, bodyCellCount);
    } else if (bodyColumns.length < bodyCellCount) {
      const pad = bodyCellCount - bodyColumns.length;
      for (let i = 0; i < pad; i += 1) {
        bodyColumns.push({ field: '', title: '', align: '', halign: '', width: '', autoWrap: false });
      }
    }

    const tableWidthPx = Number(options?.width || 0);
    const rawWidths = bodyColumns.map((colMeta) => {
      const w = Number(colMeta?.width || 0);
      return Number.isFinite(w) && w > 0 ? w : 0;
    });
    const definedWidths = rawWidths.filter((v) => v > 0);
    const avgWidth = definedWidths.length
      ? definedWidths.reduce((sum, v) => sum + v, 0) / definedWidths.length
      : tableWidthPx > 0
        ? tableWidthPx / Math.max(1, bodyColumns.length)
        : 100;
    const effectiveWidths = rawWidths.map((v) => (v > 0 ? v : avgWidth));
    const totalEffectiveWidth = effectiveWidths.reduce((sum, v) => sum + v, 0);

    table.style.tableLayout = 'fixed';
    table.style.width = '100%';

    const colgroup = document.createElement('colgroup');
    effectiveWidths.forEach((w) => {
      const col = document.createElement('col');
      const pct = totalEffectiveWidth > 0 ? ((w / totalEffectiveWidth) * 100).toFixed(6) : '';
      col.setAttribute('style', pct ? `width:${pct}%;` : '');
      colgroup.appendChild(col);
    });
    const old = table.querySelector('colgroup');
    if (old) {
      old.replaceWith(colgroup);
    } else if (table.firstChild) {
      table.insertBefore(colgroup, table.firstChild);
    } else {
      table.appendChild(colgroup);
    }

    const bodyRows = Array.from(table.querySelectorAll('tbody tr'));
    bodyRows.forEach((tr) => {
      const cells = Array.from(tr.querySelectorAll('td')) as HTMLTableCellElement[];
      cells.forEach((td, idx) => {
        const col = bodyColumns[idx];
        if (!col) {
          return;
        }
        const wrap = Boolean(col?.autoWrap);
        const align = String(col?.align || col?.halign || '').trim();
        if (wrap) {
          td.style.whiteSpace = 'normal';
          td.style.wordBreak = 'break-word';
          td.style.overflowWrap = 'break-word';
          td.style.lineHeight = td.style.lineHeight || '1.2';
        } else {
          td.style.whiteSpace = 'nowrap';
          td.style.wordBreak = 'normal';
          td.style.overflowWrap = 'normal';
        }
        if (align) {
          td.style.textAlign = align;
        }
      });
    });
  }

  function resolveHeaderRowHeight(options: Record<string, any>): string {
    const candidates = [
      options?.headerRowHeight,
      options?.tableHeaderRowHeight,
      options?.thRowHeight,
      options?.rowHeight,
    ];
    for (const value of candidates) {
      if (value === null || value === undefined || value === '') {
        continue;
      }
      const text = String(value).trim();
      if (!text) {
        continue;
      }
      return /\d$/.test(text) ? `${text}px` : text;
    }
    return '';
  }

  function buildHeaderThStyle(cell: any, options: Record<string, any>, rowHeight: string): string {
    const headerBg =
      options?.headerBackground ||
      options?.tableHeaderBackground ||
      options?.headerBg ||
      options?.thBackground ||
      '';
    const headerFontSize = options?.headerFontSize || options?.thFontSize || '';
    const headerLineHeight = options?.headerLineHeight || options?.thLineHeight || '';
    const headerFontWeight = options?.headerFontWeight || options?.thFontWeight || '600';
    const headerFontFamily = options?.headerFontFamily || options?.thFontFamily || '';
    const headerAlign = String(cell?.halign || cell?.align || options?.headerAlign || 'center');
    const headerAutoWrap = Boolean(cell?.autoWrap ?? cell?.tableAutoWrap ?? cell?.wrap ?? cell?.wordWrap ?? false);
    return [
      'border:1px solid #333',
      'padding:2px 4px',
      `text-align:${headerAlign}`,
      'vertical-align:middle',
      headerAutoWrap ? 'white-space:normal' : 'white-space:nowrap',
      headerAutoWrap ? 'word-break:break-all' : '',
      headerAutoWrap ? 'overflow-wrap:anywhere' : '',
      `font-weight:${headerFontWeight}`,
      headerFontFamily ? `font-family:${headerFontFamily}` : '',
      headerBg ? `background:${headerBg}` : '',
      headerFontSize ? `font-size:${headerFontSize}` : '',
      headerLineHeight ? `line-height:${headerLineHeight}` : '',
      rowHeight ? `height:${rowHeight}` : '',
      rowHeight ? `min-height:${rowHeight}` : '',
      !headerLineHeight && rowHeight ? `line-height:${rowHeight}` : '',
      'print-color-adjust:exact',
      '-webkit-print-color-adjust:exact',
    ]
      .filter(Boolean)
      .join(';');
  }

  function optimizeMergedHeaderHtml(sourceHtml: string, templateJson?: any): string {
    if (!sourceHtml || !sourceHtml.includes('<table')) {
      return sourceHtml;
    }
    const wrapper = document.createElement('div');
    wrapper.innerHTML = sourceHtml;
    const templateHeaderMeta = resolveTemplateHeaderMeta(templateJson);
    const tables = Array.from(wrapper.querySelectorAll('table'));
    tables.forEach((table) => {
      const buildTemplateHeader = () => {
        if (!templateHeaderMeta.rows.length || !templateHeaderMeta.options) {
          return null;
        }
        const rowHeight = resolveHeaderRowHeight(templateHeaderMeta.options);
        const thead = document.createElement('thead');
        templateHeaderMeta.rows.forEach((row) => {
          const tr = document.createElement('tr');
          if (rowHeight) {
            tr.setAttribute('style', `height:${rowHeight};min-height:${rowHeight};`);
          }
          row.forEach((cell) => {
            const th = document.createElement('th');
            const rowspan = Math.max(1, Number(cell?.rowspan || cell?.rowSpan || 1));
            const colspan = Math.max(1, Number(cell?.colspan || cell?.colSpan || 1));
            if (rowspan > 1) {
              th.rowSpan = rowspan;
            }
            if (colspan > 1) {
              th.colSpan = colspan;
            }
            th.innerHTML = String(cell?.title || cell?.text || cell?.label || '').trim();
            th.setAttribute('style', buildHeaderThStyle(cell, templateHeaderMeta.options as Record<string, any>, rowHeight));
            tr.appendChild(th);
          });
          thead.appendChild(tr);
        });
        return thead;
      };

      const injectedThead = buildTemplateHeader();
      if (injectedThead) {
        const oldThead = table.querySelector('thead');
        if (oldThead) {
          oldThead.replaceWith(injectedThead);
        } else {
          const allRows = Array.from(table.querySelectorAll('tr'));
          const maybeHeaderRows = allRows.slice(0, templateHeaderMeta.rows.length);
          maybeHeaderRows.forEach((row) => row.remove());
          if (table.firstChild) {
            table.insertBefore(injectedThead, table.firstChild);
          } else {
            table.appendChild(injectedThead);
          }
        }
      }
      if (templateHeaderMeta.options) {
        applyBodyColumnMetaToTable(table as HTMLTableElement, templateHeaderMeta.options as Record<string, any>);
      }

      const detectHeaderRows = () => {
        const thead = table.querySelector('thead');
        if (thead) {
          const headRows = Array.from(thead.querySelectorAll('tr'));
          if (headRows.length) {
            return headRows;
          }
        }
        const allRows = Array.from(table.querySelectorAll('tr'));
        const result: HTMLTableRowElement[] = [];
        for (const row of allRows) {
          const cells = Array.from(row.querySelectorAll('th,td')) as HTMLTableCellElement[];
          if (!cells.length) {
            continue;
          }
          const firstText = String(cells[0]?.textContent || '')
            .replace(/\u00a0/g, '')
            .replace(/\s+/g, '')
            .trim();
          // 命中明细行（首列纯数字）即停止，前面都视为表头区域
          if (/^\d+$/.test(firstText)) {
            break;
          }
          result.push(row as HTMLTableRowElement);
          if (result.length >= 6) {
            break;
          }
        }
        return result;
      };
      const rows = detectHeaderRows();
      if (rows.length < 2) {
        return;
      }
      const normalizeHeaderText = (cell: HTMLTableCellElement | null | undefined) =>
        String(cell?.textContent || '')
          .replace(/\u00a0/g, '')
          .replace(/\s+/g, '')
          .trim();
      type HeaderCellInfo = { el: HTMLTableCellElement; row: number; col: number; rowSpan: number; colSpan: number };
      const matrix: Array<Array<HeaderCellInfo | null>> = [];
      rows.forEach((row, rowIdx) => {
        matrix[rowIdx] = matrix[rowIdx] || [];
        let colIdx = 0;
        const cells = Array.from(row.querySelectorAll('th,td')) as HTMLTableCellElement[];
        cells.forEach((cell) => {
          while (matrix[rowIdx][colIdx]) {
            colIdx += 1;
          }
          const rowSpan = Math.max(1, Number(cell.getAttribute('rowspan') || 1));
          const colSpan = Math.max(1, Number(cell.getAttribute('colspan') || 1));
          const info: HeaderCellInfo = { el: cell, row: rowIdx, col: colIdx, rowSpan, colSpan };
          for (let r = rowIdx; r < rowIdx + rowSpan; r += 1) {
            matrix[r] = matrix[r] || [];
            for (let c = colIdx; c < colIdx + colSpan; c += 1) {
              matrix[r][c] = info;
            }
          }
          colIdx += colSpan;
        });
      });

      // 处理“上一行应 rowspan，但下一行是空占位或同名重复”的情况
      for (let r = 0; r < rows.length - 1; r += 1) {
        const currentRow = matrix[r] || [];
        const nextRow = matrix[r + 1] || [];
        for (let c = 0; c < currentRow.length; c += 1) {
          const info = currentRow[c];
          if (!info || info.row !== r || info.col !== c || info.rowSpan > 1) {
            continue;
          }
          const currentText = normalizeHeaderText(info.el);
          if (!currentText) {
            continue;
          }
          const mergeCells: HTMLTableCellElement[] = [];
          let canMergeDown = true;
          for (let k = 0; k < info.colSpan; k += 1) {
            const below = nextRow[c + k];
            if (!below || below.row !== r + 1 || below.col !== c + k || below.colSpan > 1 || below.rowSpan > 1) {
              canMergeDown = false;
              break;
            }
            const belowText = normalizeHeaderText(below.el);
            const isPlaceholder = !belowText || belowText === '-';
            const isSameTitle = !!currentText && belowText === currentText;
            if (!isPlaceholder && !isSameTitle) {
              canMergeDown = false;
              break;
            }
            mergeCells.push(below.el);
          }
          if (canMergeDown && mergeCells.length) {
            const nextRowSpan = Math.max(1, Number(info.el.getAttribute('rowspan') || 1));
            info.el.setAttribute('rowspan', String(nextRowSpan + 1));
            info.el.classList.add('qh-merged-header-cell');
            info.el.style.borderBottom = 'none';
            mergeCells.forEach((el) => {
              el.style.borderTop = 'none';
              el.remove();
            });
          }
        }
      }
    });
    return wrapper.innerHTML;
  }

  function buildLodopDocumentHtml(html: string, widthMm?: number, heightMm?: number): string {
    const styleTexts = '';
    // 仅保留 hiprint 打印相关样式，避免把业务页面全局样式带入后污染列宽计算
    const stylesheetLinks = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
      .map((node) => (node as HTMLLinkElement).href)
      .filter(Boolean)
      .filter((href) => /print-lock|hiprint/i.test(href))
      .map((href) => `<link rel="stylesheet" href="${href}" />`)
      .join('\n');
    const pageCss =
      widthMm && heightMm
        ? `@page { size: ${widthMm}mm ${heightMm}mm; margin: 0; }`
        : '@page { margin: 0; }';
    const rootSizeCss =
      widthMm && heightMm
        ? `.lodop-print-root { width: ${widthMm}mm; margin: 0; padding: 0; overflow: visible; }`
        : '.lodop-print-root { margin: 0; padding: 0; overflow: visible; }';
    const compatCss = `
      html, body { margin: 0; padding: 0; background: #fff; }
      ${rootSizeCss}
      .hiprint-printTemplate {
        transform: none !important;
        transform-origin: top left !important;
        zoom: 1 !important;
        -webkit-print-color-adjust: exact !important;
        print-color-adjust: exact !important;
      }
      /* 打印态去掉设计器预览缩放，避免整体变小与合并表头被裁切 */
      .hiprint-printTemplate [style*="transform: scale"] {
        transform: none !important;
      }
      .hiprint-printTemplate table {
        border-collapse: collapse !important;
        border-spacing: 0 !important;
      }
      .hiprint-printTemplate th,
      .hiprint-printTemplate td {
        border-color: #333 !important;
        border-style: solid !important;
        border-width: 1px !important;
        overflow: visible !important;
        text-overflow: clip !important;
        white-space: normal !important;
        vertical-align: middle !important;
        word-break: normal !important;
        line-height: 1.35 !important;
      }
      .hiprint-printTemplate thead th {
        line-height: 1.45 !important;
        padding-top: 2px !important;
        padding-bottom: 2px !important;
      }
      .hiprint-printTemplate .qh-merged-header-cell {
        border-bottom: none !important;
      }
      .hiprint-printTemplate .hiprint-printElement,
      .hiprint-printTemplate .hiprint-printElement > div,
      .hiprint-printTemplate .hiprint-printElement span,
      .hiprint-printTemplate .hiprint-printElement p {
        overflow: visible !important;
        text-overflow: clip !important;
      }
      /* 合并单元格专门处理：避免跨列跨行时边框与文字被裁切 */
      .hiprint-printTemplate th[colspan], .hiprint-printTemplate td[colspan],
      .hiprint-printTemplate th[rowspan], .hiprint-printTemplate td[rowspan] {
        overflow: visible !important;
        white-space: normal !important;
        text-align: center !important;
        vertical-align: middle !important;
        position: relative;
        z-index: 1;
        background-clip: padding-box !important;
      }
      .hiprint-printTemplate tr {
        page-break-inside: avoid !important;
      }
      .hiprint-printTemplate img, .hiprint-printTemplate canvas {
        max-width: none !important;
        max-height: none !important;
      }
    `;
    return `<!doctype html>
<html>
<head>
  <meta charset="UTF-8" />
  ${stylesheetLinks}
  <style>${styleTexts}</style>
  <style>${pageCss}\n${compatCss}</style>
</head>
<body><div class="lodop-print-root">${html}</div></body>
</html>`;
  }

  async function handleQuickPrint() {
    const templateCode = String(quickPrintForm.value.templateCode || '').trim();
    if (!templateCode) {
      createMessage.warning('请选择模板编号');
      return;
    }
    const dataText = String(quickPrintForm.value.dataJson || '').trim();
    if (!dataText) {
      createMessage.warning('请先输入打印数据JSON');
      return;
    }
    let dataJson: any;
    try {
      dataJson = JSON.parse(dataText);
    } catch (error: any) {
      createMessage.error(`打印数据JSON格式错误：${error?.message || '未知错误'}`);
      return;
    }
    quickPrintLoading.value = true;
    try {
      const printer = String(quickPrintForm.value.printerName || '').trim();
      if (quickPrintMode.value === 'serverText') {
        await directPrint({
          templateCode,
          printerName: printer,
          dataJson,
        });
        createMessage.success('已提交服务端直打任务');
      } else if (quickPrintMode.value === 'printDotBridge') {
        const tplData = (await queryByCode(templateCode)) as Record<string, any>;
        const templateJsonText = String(tplData?.templateJson || '').trim();
        if (!templateJsonText) {
          createMessage.error('模板JSON为空');
          return;
        }
        let templateJson: any;
        try {
          templateJson = JSON.parse(templateJsonText);
        } catch (error: any) {
          createMessage.error(`模板JSON格式错误：${error?.message || '未知错误'}`);
          return;
        }
        let pdfBase64: string;
        if (templateJson?.engine === 'native') {
          const normalized = normalizeImportedNativeSchema(templateJson);
          const pw = Number(tplData?.paperWidthMm);
          const ph = Number(tplData?.paperHeightMm);
          if (pw > 0) {
            normalized.page.width = pw;
          }
          if (ph > 0) {
            normalized.page.height = ph;
          }
          const fullHtml = await renderNativePrintHtml(normalized, dataJson);
          const inner = extractBodyInnerHtmlFromFullDocument(fullHtml);
          pdfBase64 = await buildPdfBase64FromHtmlFragment(
            inner,
            normalized.page.width,
            normalized.page.height,
            { paginate: true },
          );
        } else {
          await initHiprintForQuickPrint();
          const runtimeTemplate = new hiprint.PrintTemplate({
            template: templateJson,
          });
          const html = optimizeMergedHeaderHtml(await resolveTemplateHtml(runtimeTemplate, dataJson), templateJson);
          if (!html) {
            createMessage.error('未能生成预览 HTML，无法转 PDF');
            return;
          }
          const panel = Array.isArray(templateJson?.panels) && templateJson.panels.length ? templateJson.panels[0] : {};
          const widthMm = Number(panel?.width || 210);
          const heightMm = Number(panel?.height || 297);
          pdfBase64 = await buildPdfBase64FromHtmlFragment(html, widthMm, heightMm);
        }
        const dotPrinters = await fetchPrintDotPrinters();
        const resolvedPrinter = resolvePrintDotPrinterName(printer, dotPrinters);
        if (!resolvedPrinter) {
          createMessage.error('PrintDot：请选择具体打印机，或确认桥接器已返回打印机列表');
          return;
        }
        const dotResult = await printDotSendPdf({
          printer: resolvedPrinter,
          pdfBase64,
          jobName: templateCode,
          timeoutMs: 180000,
        });
        if (!dotResult.ok) {
          throw new Error(dotResult.message || 'PrintDot 打印失败');
        }
        createMessage.success('已通过 PrintDot 提交打印');
      } else {
        await initHiprintForQuickPrint();
        if (quickPrintMode.value === 'pdfServer') {
          await executePdfServerPrint({
            templateCode,
            printerName: printer,
            dataJson,
            fileName: `${templateCode}.pdf`,
          });
          createMessage.success('已提交PDF到后端打印');
        } else {
          const tplData = (await queryByCode(templateCode)) as Record<string, any>;
          const templateJsonText = String(tplData?.templateJson || '').trim();
          if (!templateJsonText) {
            createMessage.error('模板JSON为空，无法按模板样式打印');
            return;
          }
          let templateJson: any;
          try {
            templateJson = JSON.parse(templateJsonText);
          } catch (error: any) {
            createMessage.error(`模板JSON格式错误：${error?.message || '未知错误'}`);
            return;
          }
          const runtimeTemplate = new hiprint.PrintTemplate({
            template: templateJson,
          });
          if (quickPrintMode.value === 'lodopTemplate') {
            try {
              await ensureClodopScriptLoaded();
            } catch (e: any) {
              createMessage.error(
                `无法连接 C-Lodop：${e?.message || '加载 CLodopfuncs.js 失败'}。请确认本机服务已启动；若站点为 HTTPS，需安装扩展版并在浏览器中访问一次 https://localhost.lodop.net:8443 信任证书。`,
              );
              return;
            }
            const lodop =
              (typeof (window as any).getLodop === 'function' ? (window as any).getLodop() : null) ||
              (window as any)?.LODOP ||
              (window as any)?.CLODOP;
            if (!lodop) {
              createMessage.error('未检测到 LODOP/C-Lodop，请先安装并启动后重试');
              return;
            }
            const html = optimizeMergedHeaderHtml(await resolveTemplateHtml(runtimeTemplate, dataJson), templateJson);
            if (!html) {
              const proto = Object.getPrototypeOf(runtimeTemplate);
              const methodNames = Object.getOwnPropertyNames(proto || {}).filter((name) => name !== 'constructor');
              console.warn('[Lodop实验] 未拿到模板HTML，PrintTemplate methods:', methodNames);
              createMessage.error('当前 hiprint 版本未导出可用 HTML，暂无法走 Lodop 实验模式（可先用“按模板样式打印”）');
              return;
            }
            const panel = Array.isArray(templateJson?.panels) && templateJson.panels.length ? templateJson.panels[0] : {};
            const widthMm = Number(panel?.width || 0);
            const heightMm = Number(panel?.height || 0);
            lodop.PRINT_INIT(`QH-MES-${templateCode}`);
            if (widthMm > 0 && heightMm > 0 && typeof lodop.SET_PRINT_PAGESIZE === 'function') {
              lodop.SET_PRINT_PAGESIZE(1, Math.round(widthMm * 10), Math.round(heightMm * 10), '');
            }
            if (printer && printer !== '__system_default__' && typeof lodop.SET_PRINTER_INDEXA === 'function') {
              lodop.SET_PRINTER_INDEXA(printer);
            }
            const docHtml = buildLodopDocumentHtml(html, widthMm > 0 ? widthMm : undefined, heightMm > 0 ? heightMm : undefined);
            const printWidth = widthMm > 0 ? `${widthMm}mm` : 'RightMargin:0mm';
            lodop.ADD_PRINT_HTM('0mm', '0mm', printWidth, 'BottomMargin:0mm', docHtml);
            if (typeof lodop.SET_PRINT_MODE === 'function') {
              // 关闭强制满宽，尽量保持模板原始列宽比例
              lodop.SET_PRINT_MODE('FULL_WIDTH_FOR_OVERFLOW', false);
            }
            lodop.PRINT();
            createMessage.success('已通过 Lodop 提交打印任务（实验模式）');
          } else {
            if (printer && printer !== '__system_default__') {
              try {
                runtimeTemplate.print(dataJson, { printer });
              } catch (_error) {
                runtimeTemplate.print(dataJson, { printerName: printer });
              }
            } else {
              runtimeTemplate.print(dataJson);
            }
            createMessage.success('已按模板样式发起打印');
          }
        }
      }
      quickPrintVisible.value = false;
    } catch (error: any) {
      createMessage.error(`打印失败：${error?.message || '未知错误'}`);
    } finally {
      quickPrintLoading.value = false;
    }
  }

  function handleCreateNative() {
    openModal(true, { isUpdate: false, isNative: true });
  }

  function handleEdit(record: Recordable) {
    openModal(true, { isUpdate: true, record });
  }

  function isNativeTemplate(record: Recordable) {
    const raw = record?.templateJson;
    if (!raw) {
      return false;
    }
    if (typeof raw === 'object') {
      return raw?.engine === 'native';
    }
    if (typeof raw !== 'string') {
      return false;
    }
    try {
      const parsed = JSON.parse(raw);
      return parsed?.engine === 'native';
    } catch (_error) {
      return false;
    }
  }

  function handleDesign(record: Recordable) {
    const path = isNativeTemplate(record) ? '/print/native-designer' : '/print/designer';
    router.push({ path, query: { id: record.id } });
  }

  function handleNativeListPreview(record: Recordable) {
    nativeListPreviewTemplateId.value = String(record.id || '');
    nativeListPreviewOpen.value = true;
  }

  async function handleDelete(record: Recordable) {
    await deleteOne({ id: record.id }, reload);
  }

  /** 复制为一条新模板（新编码 + 名称后缀，内容与原模板一致） */
  async function handleCopy(record: Recordable) {
    const id = String(record?.id || '').trim();
    if (!id) {
      createMessage.warning('无法复制：缺少模板主键');
      return;
    }
    try {
      const full = (await queryById(id)) as Record<string, any>;
      const baseCode = String(full?.templateCode ?? record?.templateCode ?? 'TPL').trim() || 'TPL';
      const ts = Date.now();
      let templateCode = `${baseCode}_CP_${ts}`;
      if (templateCode.length > 64) {
        templateCode = `CP_${ts}`;
      }
      const baseName = String(full?.templateName ?? record?.templateName ?? baseCode).trim() || '模板';
      const templateName = `${baseName}_副本`;
      await add({
        templateCode,
        templateName,
        category: String(full?.category ?? record?.category ?? 'form'),
        paperWidthMm: full?.paperWidthMm ?? record?.paperWidthMm,
        paperHeightMm: full?.paperHeightMm ?? record?.paperHeightMm,
        paperOrientation: String(full?.paperOrientation ?? record?.paperOrientation ?? 'portrait'),
        templateJson: String(full?.templateJson ?? record?.templateJson ?? '{}'),
        remark: full?.remark != null ? String(full.remark) : record?.remark != null ? String(record.remark) : '',
      });
      createMessage.success('已复制为新模板');
      await reload();
    } catch (e: any) {
      createMessage.error(e?.message || '复制失败');
    }
  }

  async function batchHandleDelete() {
    await batchDelete({ ids: selectedRowKeys.value.join(',') }, reload);
  }

  async function handleSuccess(payload?: Recordable) {
    reload();
    if (payload?.isNative !== true || payload?.isUpdate === true) {
      return;
    }
    const savedId =
      payload?.savedResult?.id ||
      payload?.savedResult?.result?.id ||
      payload?.values?.id;
    if (savedId) {
      router.push({ path: '/print/native-designer', query: { id: String(savedId) } });
      return;
    }
    const templateCode = String(payload?.values?.templateCode || '').trim();
    if (!templateCode) {
      return;
    }
    try {
      const record = (await queryByCode(templateCode)) as Record<string, any>;
      if (record?.id) {
        router.push({ path: '/print/native-designer', query: { id: String(record.id) } });
      }
    } catch (_error) {
      // ignore
    }
  }

  function getTableAction(record: Recordable) {
    const isNative = isNativeTemplate(record);
    const actions: any[] = [
      { label: isNative ? '原生设计' : 'Hiprint设计', onClick: handleDesign.bind(null, record), auth: 'print:template:edit' },
    ];
    if (isNative) {
      actions.push({ label: '预览', onClick: handleNativeListPreview.bind(null, record), auth: 'print:template:list' });
    }
    actions.push(
      { label: '编辑', onClick: handleEdit.bind(null, record), auth: 'print:template:edit' },
      { label: '复制', onClick: handleCopy.bind(null, record), auth: 'print:template:add' },
      {
        label: '删除',
        color: 'error',
        popConfirm: {
          title: '确认删除？',
          confirm: handleDelete.bind(null, record),
        },
        auth: 'print:template:delete',
      },
    );
    return actions;
  }

  watch(
    selectedPrinterName,
    (value) => {
      if (!value) {
        localStorage.removeItem(PRINT_TEMPLATE_SELECTED_PRINTER_KEY);
        return;
      }
      localStorage.setItem(PRINT_TEMPLATE_SELECTED_PRINTER_KEY, value);
    },
    { immediate: false },
  );

  onMounted(async () => {
    try {
      await initHiprintForQuickPrint();
    } catch (_error) {
      // ignore
    }
    selectedPrinterName.value = localStorage.getItem(PRINT_TEMPLATE_SELECTED_PRINTER_KEY) || '__system_default__';
    await refreshPrinterOptions(false);
    if (!selectedPrinterName.value) {
      selectedPrinterName.value = '__system_default__';
    }
  });
</script>

<style scoped lang="less">
  .skill-field-label {
    margin-bottom: 6px;
    font-size: 13px;
    color: rgba(0, 0, 0, 0.65);
  }

  .skill-json-error {
    margin-top: 6px;
    color: #cf1322;
    font-size: 12px;
  }

  .skill-preview-wrap {
    min-height: 360px;
    max-height: 520px;
    overflow: auto;
    border: 1px solid #f0f0f0;
    border-radius: 4px;
    background: #fafafa;
  }

  .skill-preview-iframe {
    width: 100%;
    min-height: 360px;
    height: 480px;
    border: none;
    background: #fff;
  }
</style>
