<template>
  <view class="page">
    <view class="header-hero" :style="{ paddingTop: statusBarHeight + 'px' }">
      <view class="top-bar">
        <view class="top-left" @click="goBack">
          <AppIcon name="chevronLeft" size="sm" color="white" />
        </view>
        <view class="top-center">
          <text class="page-title">Label Preview</text>
          <LocationPicker />
        </view>
        <view class="top-right" @click="isMenuOpen = true">
          <AppIcon name="menu" size="sm" color="white" />
        </view>
      </view>
    </view>

    <view class="content">
      <view v-if="previewLoading" class="state-block">
        <text class="state-text">Loading preview…</text>
      </view>
      <view v-else-if="previewError" class="state-block">
        <text class="state-text">{{ previewError }}</text>
      </view>

      <template v-else>
        <text class="section-title section-title-first">Label Preview</text>

        <view class="label-card">
          <view class="label-img-wrap">
            <image v-if="previewImageSrc" :src="previewImageSrc" class="label-img" mode="widthFix" />
            <view v-else class="label-placeholder">
              <text class="label-placeholder-text">No preview image</text>
            </view>
          </view>
        </view>

        <view v-if="printOptionFieldList.length" class="print-options-card">
          <text class="print-options-title">Print choices</text>
          <text class="print-options-hint">
            Choose options for each field. Preview updates to show the selected text (no picker on the label).
          </text>
          <view v-for="el in printOptionFieldList" :key="el.id" class="print-option-block">
            <text class="print-option-label">{{ optionFieldTitle(el) }}</text>
            <view class="chip-row">
              <view
                v-for="opt in dictValuesForElement(el.id)"
                :key="el.id + '-' + opt"
                class="chip"
                :class="{ 'chip-on': isOptionPicked(el.id, opt) }"
                @click="togglePrintOption(el.id, opt)"
              >
                <text class="chip-text">{{ opt }}</text>
              </view>
            </view>
          </view>
        </view>

        <view v-if="printFreeFieldList.length" class="print-options-card">
          <text class="print-options-title">Print input</text>
          <text class="print-options-hint">
            with a box below. If the template has a unit, it is appended on the label after what you type.
          </text>
          <view v-for="el in printFreeFieldList" :key="'free-' + el.id" class="print-option-block">
            <text class="print-option-label">{{ freeFieldNameLabel(el) }}</text>
            <template v-if="freeFieldInputKind(el) === 'text'">
              <input
                class="free-field-input"
                type="text"
                :value="printFreeFieldValues[el.id] ?? ''"
                :placeholder="freeFieldPlaceholder(el)"
                @input="onFreeFieldInput(el.id, $event)"
              />
            </template>
            <template v-else-if="freeFieldInputKind(el) === 'datetime'">
              <view
                class="free-field-input picker-input"
                @click="openFreeFieldPicker(el, 'datetime')"
              >
                {{ displayDateTimeField(printFreeFieldValues[el.id], freeFieldDateFormat(el)) }}
              </view>
            </template>
            <template v-else-if="freeFieldInputKind(el) === 'date'">
              <view
                class="free-field-input picker-input"
                @click="openFreeFieldPicker(el, 'date')"
              >
                {{ displayDateValue(printFreeFieldValues[el.id], freeFieldDateFormat(el), freeFieldPlaceholder(el)) }}
              </view>
            </template>
            <template v-else>
              <view
                class="free-field-input picker-input"
                @click="openFreeFieldPicker(el, 'time')"
              >
                {{ displayTimeValue(printFreeFieldValues[el.id]) }}
              </view>
            </template>
          </view>
        </view>

        <scroll-view class="meta-footer-scroll" scroll-x enable-flex>
          <view class="meta-footer-row">
            <view class="meta-item">
              <text class="info-label">Product</text>
              <text class="info-value">{{ displayProductName || '—' }}</text>
            </view>
            <view class="meta-item">
              <text class="info-label">Category</text>
              <text class="info-value">{{ productCategory || '—' }}</text>
            </view>
            <view v-if="labelTypeName" class="meta-item">
              <text class="info-label">Type</text>
              <text class="info-value">{{ labelTypeName }}</text>
            </view>
            <view class="meta-item">
              <text class="info-label">Size</text>
              <text class="info-value">{{ templateSize || '—' }}</text>
            </view>
            <view class="meta-item">
              <text class="info-label">Template</text>
              <text class="info-value">{{ templateName || '—' }}</text>
            </view>
            <view class="meta-item">
              <text class="info-label">Label ID</text>
              <text class="info-value">{{ footerLabelIdDisplay }}</text>
            </view>
            <view class="meta-item">
              <text class="info-label">Last Edited</text>
              <text class="info-value">{{ lastEdited }}</text>
            </view>
            <view class="meta-item">
              <text class="info-label">Location</text>
              <text class="info-value">{{ locationName }}</text>
            </view>
          </view>
        </scroll-view>

        <view class="note-card">
          <AppIcon name="alert" size="sm" color="blue" />
          <text class="note-text">This is a preview of the label. Actual printed labels may vary slightly in appearance.</text>
        </view>
      </template>
    </view>

    <view class="bottom-bar">
      <view class="bottom-info">
        <text class="bottom-qty">{{ printQty }} label{{ printQty > 1 ? 's' : '' }}</text>
        <view class="bt-indicator" :class="{ connected: btConnected }">
          <text class="bt-dot" />
          <text class="bt-name">{{ btConnected ? btDeviceName : 'No printer' }}</text>
        </view>
      </view>
      <view class="bottom-actions">
        <view class="btn-preview-sq" @click="showPreviewModal = true">
          <AppIcon name="eye" size="md" color="primary" />
        </view>
        <view class="bottom-qty-control">
          <view class="qty-btn qty-btn-minus" :class="{ disabled: printQty <= 1 }" @click="decrement">
            <AppIcon name="minus" size="sm" color="gray" />
          </view>
          <text class="qty-value">{{ printQty }}</text>
          <view class="qty-btn qty-btn-plus" @click="increment">
            <AppIcon name="plus" size="sm" color="white" />
          </view>
        </view>
        <view class="print-btn" :class="{ disabled: isPrinting || previewLoading || !systemTemplate }" @click="handlePrint">
          <AppIcon name="printer" size="sm" color="white" />
          <text class="print-btn-text">{{ isPrinting ? 'Printing...' : 'Print' }}</text>
        </view>
      </view>
    </view>

    <view v-if="showPreviewModal && previewImageSrc" class="modal-mask" @click="showPreviewModal = false">
      <view class="modal-body modal-body-label-only" @click.stop>
        <view class="modal-label-wrap">
          <view class="modal-label-inner">
            <image :src="previewImageSrc" class="modal-label-img" mode="widthFix" />
          </view>
        </view>
      </view>
    </view>

    <!-- 与 printers 页 Test Print 一致：必须绑定 :width/:height 作为绘图缓冲区像素，否则 canvasGetImageData 多为空白/默认尺寸，打印不出纸 -->
    <canvas
      canvas-id="labelPreviewCanvas"
      id="labelPreviewCanvas"
      class="hidden-canvas"
      :style="{ width: canvasCssW + 'px', height: canvasCssH + 'px' }"
      :width="canvasCssW"
      :height="canvasCssH"
    />

    <NoPrinterModal v-model="showNoPrinterModal" @connect="goBluetoothPage" />

    <SideMenu v-model="isMenuOpen" />

    <view v-if="pickerDialogVisible" class="picker-dialog-mask" @click="closeFreeFieldPicker">
      <view class="picker-dialog" :class="{ 'picker-dialog--wide': pickerMode === 'datetime' }" @click.stop>
        <view class="picker-dialog-title">{{ pickerDialogTitle }}</view>
        <picker-view
          class="picker-view-box"
          :class="{ 'picker-view-box--wide': pickerMode === 'datetime' }"
          :indicator-style="pickerIndicatorStyle"
          :value="pickerSelection"
          @change="onPickerViewChange"
        >
          <picker-view-column v-if="pickerMode === 'date' || pickerMode === 'datetime'">
            <view v-for="y in pickerYears" :key="'y-' + y" class="picker-item">{{ y }}</view>
          </picker-view-column>
          <picker-view-column v-if="pickerMode === 'date' || pickerMode === 'datetime'">
            <view v-for="m in pickerMonths" :key="'m-' + m" class="picker-item">{{ m }}</view>
          </picker-view-column>
          <picker-view-column v-if="pickerMode === 'date' || pickerMode === 'datetime'">
            <view v-for="d in pickerDays" :key="'d-' + d" class="picker-item">{{ d }}</view>
          </picker-view-column>
          <picker-view-column v-if="pickerMode === 'time' || pickerMode === 'datetime'">
            <view v-for="h in pickerHours" :key="'h-' + h" class="picker-item">{{ h }}</view>
          </picker-view-column>
          <picker-view-column v-if="pickerMode === 'time' || pickerMode === 'datetime'">
            <view v-for="m in pickerMinutes" :key="'mm-' + m" class="picker-item">{{ m }}</view>
          </picker-view-column>
        </picker-view>
        <view class="picker-dialog-actions">
          <view class="picker-btn picker-btn-cancel" @click="closeFreeFieldPicker">Cancel</view>
          <view class="picker-btn picker-btn-confirm" @click="confirmFreeFieldPicker">Confirm</view>
        </view>
      </view>
    </view>
  </view>
</template>

<script setup lang="ts">
import { ref, computed, getCurrentInstance, nextTick, watch } from 'vue'
import { onLoad, onShow, onUnload } from '@dcloudio/uni-app'
import AppIcon from '../../components/AppIcon.vue'
import SideMenu from '../../components/SideMenu.vue'
import LocationPicker from '../../components/LocationPicker.vue'
import NoPrinterModal from '../../components/NoPrinterModal.vue'
import { getStatusBarHeight } from '../../utils/statusBar'
import {
  canPrintCurrentLabelViaNativeFastJob,
  getCurrentPrinterSummary,
  getCurrentPrinterDriver,
  getLastImagePrintDebug,
  printImageDataForCurrentPrinter,
  printImageForCurrentPrinter,
  printLabelPrintJobPayloadForCurrentPrinter,
  printSystemTemplateForCurrentPrinter,
  resolveRasterPrintDriver,
  shouldRasterPrintViaCanvasImageData,
} from '../../utils/print/manager/printerManager'
import classicBluetooth from '../../utils/print/bluetoothTool.js'
import { getNativeFastPrinterState, isNativeFastPrinterAvailable } from '../../utils/print/nativeFastPrinter'
import { showNativeFastPrinterDebugModal } from '../../utils/print/showNativeFastPrinterDebugModal'
import type { SystemLabelTemplate, SystemTemplateElementBase } from '../../utils/print/types/printer'
import { fetchLabelMultipleOptionById } from '../../services/labelMultipleOption'
import {
  buildPrintInputJson,
  ensureFreeFieldKeys,
  isPrintInputFreeFieldElement,
  isPrintInputOptionsElement,
  mergePrintInputFreeFields,
  mergePrintOptionSelections,
  readFreeFieldValuesFromTemplate,
  readSelectionsFromTemplate,
  validatePrintInputFreeFieldsBeforePrint,
  validatePrintInputOptionsBeforePrint,
} from '../../utils/labelPreview/printInputOptions'
import {
  buildLabelPrintJobPayload,
  setLastLabelPrintJobPayload,
} from '../../utils/labelPreview/buildLabelPrintPayload'
import { getCurrentStoreId } from '../../utils/stores'
import {
  buildApiUrl,
} from '../../utils/apiBase'
import { storedValueLooksLikeImagePath } from '../../utils/resolveMediaUrl'
import {
  buildUsAppLabelPrintRequestBody,
  postUsAppLabelPreview,
  postUsAppLabelPrint,
  US_APP_LABEL_PRINT_PATH,
} from '../../services/usAppLabeling'
import {
  applyTemplateProductDefaultValuesToTemplate,
  applyProductCodeValueToTemplateScanElements,
  extractProductCodeValueFromPreviewPayload,
  extractTemplateProductDefaultValuesFromPreviewPayload,
  normalizeLabelTemplateFromPreviewApi,
  overlayProductNameOnPreviewTemplate,
} from '../../utils/labelPreview/normalizePreviewTemplate'
import {
  applyLabelIdDisplayToTemplate,
  templateIncludesLabelIdElement,
} from '../../utils/labelPreview/labelIdElement'
import {
  applyEmployeeDisplayToTemplate,
  templateIncludesEmployeeElement,
} from '../../utils/labelPreview/employeeElement'
import { applyLiveDateTimeFieldsToTemplate } from '../../utils/labelPreview/printInputOffset'
import { readInvertColors } from '../../utils/invertColorsConfig'
import {
  getLabelPrintRasterLayout,
  getPreviewCanvasCssSize,
  renderLabelPreviewCanvasImageDataForPrint,
  renderLabelPreviewCanvasToTempPathForPrint,
  renderLabelPreviewToTempPath,
} from '../../utils/labelPreview/renderLabelPreviewCanvas'
import {
  hydrateSystemTemplateImagesForPrint,
  resetHydrateImageDebugRecords,
} from '../../utils/print/hydrateTemplateImagesForPrint'
import {
  normalizeTemplateForNativeFastJob,
  templateHasUnsupportedNativeFastElements,
} from '../../utils/print/nativeTemplateElementSupport'
import {
  ensureTemplateHeightCoversElements,
  getTemplatePhysicalSizeMm,
  isTemplateWithinNativeFastPrintBounds,
  templateContentHeightPx,
} from '../../utils/print/templatePhysicalMm'
import { isPrinterReadySync } from '../../utils/print/printerReadiness'
import {
  ensureNativeClassicTransportIfPossible,
  getBluetoothConnection,
  isNativeBaseClassicBluetoothTransport,
  getPrinterDebugSnapshot,
} from '../../utils/print/printerConnection'
import { isUsAppSessionExpiredError } from '../../utils/usAppApiRequest'
import { getDeviceFingerprint } from '../../utils/deviceInfo'

const statusBarHeight = getStatusBarHeight()
const instance = getCurrentInstance()?.proxy ?? null

const isPrinting = ref(false)
const isMenuOpen = ref(false)
const printQty = ref(1)
const showPreviewModal = ref(false)
const showNoPrinterModal = ref(false)

const btConnected = ref(false)
const btDeviceName = ref('')
/** 业务关闭：预览页不再有打印前/打印后调试弹窗 */
const ENABLE_PRINT_PREFLIGHT_MODAL = false
const ENABLE_PRINT_RESULT_MODAL = false
const ENABLE_AUTO_NATIVE_DEBUG_MODAL = false

function showPrintInProgressDebugModal(meta?: {
  jobId?: number
  route?: string
  stage?: string
}) {
  if (!ENABLE_PRINT_RESULT_MODAL) return
  try {
    uni.showModal({
      title: 'Print debug (in progress)',
      content: [
        `jobId: ${String(meta?.jobId ?? '-')}`,
        `route: ${String(meta?.route || '-')}`,
        `stage: ${String(meta?.stage || '-')}`,
        '',
        'Printing is still running. Please take a screenshot.',
      ].join('\n'),
      showCancel: false,
      confirmText: 'OK',
    })
  } catch (_) {}
}
function buildPrintDebugText(extra?: Record<string, string | number | boolean | null | undefined>): string {
  const summary = getCurrentPrinterSummary()
  const conn = getBluetoothConnection()
  const classicState = typeof classicBluetooth?.getDebugState === 'function'
    ? classicBluetooth.getDebugState()
    : null
  const native = getNativeFastPrinterState() || {}
  const lines: string[] = [
    `mode=${summary.type || '-'}`,
    `driver=${summary.driverKey || '-'}/${summary.driverName || '-'}`,
    `protocol=${summary.protocol || '-'}`,
    `deviceType=${summary.deviceType || '-'}`,
    conn?.deviceId ? `device=${conn.deviceId}` : '',
    conn?.deviceName ? `deviceName=${conn.deviceName}` : '',
    (conn as any)?.transportMode ? `transport=${String((conn as any).transportMode)}` : '',
    native?.available === false ? `nativeFast=${native.lastError || 'unavailable'}` : '',
  ].filter(Boolean)
  if (classicState) {
    lines.push(`classicState=${classicState.connectionState || '-'}`)
    lines.push(`connected=${String(!!classicState.socketConnected)}`)
    lines.push(`outputReady=${String(!!classicState.outputReady)}`)
    lines.push(`sendMode=${classicState.lastSendMode || '-'}`)
    if (classicState.lastSocketStrategy) lines.push(`socket=${classicState.lastSocketStrategy}`)
    if (classicState.lastSendError) lines.push(`sendError=${classicState.lastSendError}`)
    if (classicState.lastSendError || classicState.lastError) {
      lines.push(`lastError=${classicState.lastSendError || classicState.lastError}`)
    }
  }
  if (extra) {
    Object.keys(extra).forEach((k) => {
      const v = (extra as any)[k]
      if (v === undefined || v === null || v === '') return
      lines.push(`${k}=${String(v)}`)
    })
  }
  return lines.join('\n')
}

function shouldShowNativeFastDebugModal (): boolean {
  const summary = getCurrentPrinterSummary()
  return summary.type === 'builtin'
    || (summary.type === 'bluetooth' && isNativeBaseClassicBluetoothTransport())
}

function buildPrintPreflightDeviceInfoText (args: {
  template: SystemLabelTemplate
  useNativeTemplatePrint: boolean
  nativeUnsupported: boolean
  printerSnapshot?: any
}): string {
  const summary = getCurrentPrinterSummary()
  const conn = getBluetoothConnection()
  const driver = getCurrentPrinterDriver()
  const native = getNativeFastPrinterState() || {}
  const snap = args.printerSnapshot || null
  let sys: any = {}
  try { sys = uni.getSystemInfoSync?.() || {} } catch { sys = {} }

  const lines: string[] = []
  lines.push('Device / System')
  lines.push(`- model: ${String(sys.model || '-')}`)
  lines.push(`- brand: ${String(sys.brand || '-')}`)
  lines.push(`- platform: ${String(sys.platform || '-')}`)
  lines.push(`- system: ${String(sys.system || '-')}`)
  lines.push(`- fingerprint: ${String(getDeviceFingerprint?.() || '-')}`)
  lines.push(`- jsBuild: upos-no-silent-fallback@2026-04-22`)
  lines.push('')
  lines.push('Printer / Connection')
  lines.push(`- selectedType: ${String(summary.type || '-')}`)
  lines.push(`- displayName: ${String(summary.displayName || '-')}`)
  lines.push(`- driver: ${String(driver?.displayName || driver?.key || '-')}`)
  if (snap && typeof snap.builtinTscUserAcknowledged === 'boolean') {
    lines.push(`- builtinTscUserAcknowledged: ${snap.builtinTscUserAcknowledged ? 'YES' : 'NO'}`)
  }
  if (summary.type === 'bluetooth') {
    lines.push(`- btDeviceType: ${String(conn?.deviceType || '-')}`)
    lines.push(`- btTransportMode: ${String((conn as any)?.transportMode || '-')}`)
    lines.push(`- btDeviceName: ${String(conn?.deviceName || '-')}`)
    lines.push(`- btDeviceId: ${String(conn?.deviceId || '-')}`)
  }
  lines.push('')
  lines.push('Native-fast-printer')
  lines.push(`- available: ${String(!!isNativeFastPrinterAvailable())}`)
  if ((native as any).backend) lines.push(`- backend: ${String((native as any).backend)}`)
  if ((native as any).pluginVersion) lines.push(`- pluginVersion: ${String((native as any).pluginVersion)}`)
  if ((native as any).stage) lines.push(`- stage: ${String((native as any).stage)}`)
  if ((native as any).lastError) lines.push(`- lastError: ${String((native as any).lastError)}`)
  if (snap) {
    lines.push('')
    lines.push('Built-in route (detected)')
    lines.push(`- uposSupported: ${snap.builtin?.uposSupported ? 'YES' : 'NO'}`)
    lines.push(`- uposWillTry: ${snap.builtin?.uposWillTry ? 'YES' : 'NO'}`)
    if (snap.builtin?.uposOptions) {
      lines.push(`- uposPrefer: ${String(snap.builtin.uposOptions.prefer || '-')}`)
      lines.push(`- uposSerialPath: ${String(snap.builtin.uposOptions.serialPath || '-')}`)
      lines.push(`- uposBaudrate: ${String(snap.builtin.uposOptions.baudrate || '-')}`)
      lines.push(`- uposForce: ${snap.builtin.uposOptions.force ? 'YES' : 'NO'}`)
    }
    lines.push(`- tcpPluginAvailable: ${snap.builtin?.tcpPluginAvailable ? 'YES' : 'NO'}`)
    lines.push(`- savedPort: ${String(snap.builtin?.savedPort || '-')}`)
  }
  lines.push('')
  lines.push('Print route (this job)')
  lines.push(`- nativePrintTemplate: ${args.useNativeTemplatePrint ? 'YES' : 'NO'}`)
  lines.push(`- nativeBoundsOk: ${String(!!isTemplateWithinNativeFastPrintBounds(args.template))}`)
  lines.push(`- nativeUnsupportedElements: ${args.nativeUnsupported ? 'YES' : 'NO'}`)
  lines.push('')
  lines.push('Note')
  lines.push('- Built-in may use UPOS (on-device/serial) or localhost (127.0.0.1). If the app reports success but nothing prints, check uposWillTry / tcpPluginAvailable and native-fast-printer lastError/stage.')
  return lines.join('\n')
}

async function showPrintPreflightModal (content: string): Promise<void> {
  return new Promise((resolve, reject) => {
    uni.showModal({
      title: 'Print preflight',
      content,
      confirmText: 'Continue',
      cancelText: 'Cancel',
      success: (res) => {
        if (res.confirm) resolve()
        else reject(new Error('CANCEL'))
      },
      fail: () => reject(new Error('FAIL')),
    })
  })
}

async function showPrintResultModal (meta?: {
  jobId?: number
  route?: string
  stage?: string
}): Promise<void> {
  try {
    try { uni.hideLoading() } catch (_) {}
    const nativeStateBefore = getNativeFastPrinterState() || {}
    const snap = await getPrinterDebugSnapshot({ reason: 'after-print' })
    const native = (snap as any)?.nativeFastPrinter || {}
    const nativeStateAfter = getNativeFastPrinterState() || {}
    const lines: string[] = []
    lines.push('Native-fast-printer')
    if (native.backend) lines.push(`- backend: ${String(native.backend)}`)
    if (native.pluginVersion) lines.push(`- pluginVersion: ${String(native.pluginVersion)}`)
    if (native.buildTime) lines.push(`- buildTime: ${String(native.buildTime)}`)
    lines.push(`- stage: ${String(native.stage || '-')}`)
    lines.push(`- lastError: ${String(native.lastError || '-')}`)
    lines.push(`- commandBytes: ${String(native.commandBytes ?? '-')}`)
    lines.push(`- writeMs: ${String(native.writeMs ?? '-')}`)
    if (meta?.jobId || meta?.route || meta?.stage) {
      lines.push('')
      lines.push('Print job meta')
      lines.push(`- jobId: ${String(meta?.jobId ?? '-')}`)
      lines.push(`- route: ${String(meta?.route || '-')}`)
      lines.push(`- stage: ${String(meta?.stage || '-')}`)
    }
    const imageDebug = getLastImagePrintDebug()
    if (imageDebug) {
      lines.push('')
      lines.push('Image raster debug')
      lines.push(`- protocol: ${String(imageDebug.protocol)}`)
      lines.push(`- size: ${String(imageDebug.rasterWidth)}x${String(imageDebug.rasterHeight)}`)
      lines.push(`- blackPixels: ${String(imageDebug.blackPixels)}/${String(imageDebug.totalPixels)}`)
      lines.push(`- blackRatio: ${String((imageDebug.blackRatio * 100).toFixed(2))}%`)
      lines.push(`- commandBytes(JS): ${String(imageDebug.commandBytes)}`)
    }
    if (nativeStateBefore && Object.keys(nativeStateBefore).length > 0) {
      lines.push('')
      lines.push('Native-fast-printer (JS state BEFORE getDebugInfo)')
      lines.push(`- lastAction: ${String((nativeStateBefore as any).lastAction || '-')}`)
      lines.push(`- stage: ${String((nativeStateBefore as any).stage || '-')}`)
      lines.push(`- lastError: ${String((nativeStateBefore as any).lastError || '-')}`)
      lines.push(`- commandBytes: ${String((nativeStateBefore as any).commandBytes ?? '-')}`)
      lines.push(`- writeMs: ${String((nativeStateBefore as any).writeMs ?? '-')}`)

      lines.push('')
      lines.push('Native-fast-printer (JS state AFTER getDebugInfo)')
      lines.push(`- lastAction: ${String((nativeStateAfter as any).lastAction || '-')}`)
      lines.push(`- stage: ${String((nativeStateAfter as any).stage || '-')}`)
      lines.push(`- lastError: ${String((nativeStateAfter as any).lastError || '-')}`)
      lines.push(`- commandBytes: ${String((nativeStateAfter as any).commandBytes ?? '-')}`)
      lines.push(`- writeMs: ${String((nativeStateAfter as any).writeMs ?? '-')}`)
    }
    lines.push('')
    lines.push('Built-in route (detected)')
    lines.push(`- uposSupported: ${snap.builtin?.uposSupported ? 'YES' : 'NO'}`)
    lines.push(`- uposWillTry: ${snap.builtin?.uposWillTry ? 'YES' : 'NO'}`)
    if (snap.builtin?.uposOptions) {
      lines.push(`- uposPrefer: ${String(snap.builtin.uposOptions.prefer || '-')}`)
      lines.push(`- uposSerialPath: ${String(snap.builtin.uposOptions.serialPath || '-')}`)
      lines.push(`- uposBaudrate: ${String(snap.builtin.uposOptions.baudrate || '-')}`)
      lines.push(`- uposForce: ${snap.builtin.uposOptions.force ? 'YES' : 'NO'}`)
    }
    lines.push(`- tcpPluginAvailable: ${snap.builtin?.tcpPluginAvailable ? 'YES' : 'NO'}`)
    lines.push(`- savedPort: ${String(snap.builtin?.savedPort || '-')}`)
    await new Promise<void>((resolve) => {
      uni.showModal({
        title: 'Print result',
        content: lines.join('\n'),
        showCancel: false,
        success: () => resolve(),
        fail: () => resolve(),
      })
    })
  } catch (_) {}
}

let activePrintJobId = 0
let printJobSeed = 0

function createPrintJobId (): number {
  printJobSeed += 1
  activePrintJobId = printJobSeed
  return activePrintJobId
}

function isPrintJobActive (jobId: number): boolean {
  return jobId === activePrintJobId
}

function ensurePrintJobActive (jobId: number) {
  if (!isPrintJobActive(jobId)) {
    throw new Error('PRINT_JOB_STALE')
  }
}

function templateHasQrDataForCommandPrint(tmpl: SystemLabelTemplate): boolean {
  const els = tmpl.elements || []
  for (const el of els) {
    const type = String(el.type || '').toUpperCase()
    if (type !== 'QRCODE') continue
    const cfg = (el.config || {}) as Record<string, unknown>
    const raw = String(
      cfg.data ?? cfg.Data ?? cfg.value ?? cfg.Value ?? cfg.src ?? cfg.Src ?? cfg.url ?? cfg.Url ?? ''
    ).trim()
    if (!raw) continue
    if (storedValueLooksLikeImagePath(raw)) continue
    return true
  }
  return false
}

function templateHasUnsupportedElementsForCommandPrint(tmpl: SystemLabelTemplate): boolean {
  const els = tmpl.elements || []
  for (const el of els) {
    const type = String(el.type || '').toUpperCase()
    if (type.startsWith('TEXT_')) continue
    if (
      type === 'WEIGHT'
      || type === 'DATE'
      || type === 'TIME'
      || type === 'DURATION'
      || type === 'NUTRITION'
      || type === 'QRCODE'
      || type === 'BARCODE'
      || type === 'IMAGE'
      || type === 'BLANK'
    ) {
      continue
    }
    return true
  }
  return false
}

function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
  return new Promise<T>((resolve, reject) => {
    const tid = setTimeout(() => reject(new Error(message)), ms)
    promise.then(
      (v) => { clearTimeout(tid); resolve(v) },
      (e) => { clearTimeout(tid); reject(e) },
    )
  })
}

function applyNativeTemplateStyleScale(
  template: SystemLabelTemplate,
  options: { textScale?: number; xScale?: number; safeRightRatio?: number }
): SystemLabelTemplate {
  const xScale = Number.isFinite(options?.xScale) && (options?.xScale as number) > 0
    ? (options?.xScale as number)
    : 1
  const safeRightRatio = Number.isFinite(options?.safeRightRatio) && (options?.safeRightRatio as number) > 0
    ? Number(options?.safeRightRatio)
    : 0.94
  const scaleXNum = (v: unknown): unknown => {
    const n = Number(v)
    if (!Number.isFinite(n)) return v
    return Math.round(n * xScale * 1000) / 1000
  }
  const extras: SystemTemplateElementBase[] = []
  const elements = (template.elements || []).map((el) => {
    const next: any = { ...el }
    next.x = scaleXNum(next.x)
    next.width = scaleXNum(next.width)
    const type = String(next.type || '').toUpperCase()
    const cfg = { ...((next.config || {}) as Record<string, unknown>) }
    if (type.startsWith('TEXT_') || type === 'NUTRITION' || type === 'DATE' || type === 'TIME' || type === 'DURATION') {
      // 保持字号完全由模板 JSON 控制，不再做全局字体缩放。
      cfg.fontWeight = 'normal'
      cfg.FontWeight = 'normal'
      const sourceType = String(cfg.nativeSourceType || '').toUpperCase()
      if (sourceType === 'DATE' || sourceType === 'TIME' || sourceType === 'DURATION') {
        // 时间/日期类右对齐文本在部分机型原生 TEXT 宽度估算有误差，强制位图可避免截断。
        cfg.forceRasterText = true
      }
      if (readInvertColors(cfg)) {
        cfg.forceRasterText = true
      }
    }
    if (type === 'BARCODE') {
      const raw = String(
        cfg.data ?? cfg.Data ?? cfg.value ?? cfg.Value ?? cfg.src ?? cfg.Src ?? cfg.url ?? cfg.Url ?? ''
      ).trim()
      cfg.showText = false
      cfg.ShowText = false
      if (raw) {
        const ex: any = {
          id: `${String(next.id || 'barcode')}_label`,
          type: 'TEXT_STATIC',
          x: Number(next.x || 0),
          y: Number(next.y || 0) + Number(next.height || 0) + 8,
          width: Number(next.width || 0),
          height: 20,
          config: {
            text: raw,
            fontSize: 12,
            fontWeight: 'normal',
            textAlign: 'center',
            TextAlign: 'center',
            /** 纯数字走原生 TEXT 时宽度估算偏大，居中会变成贴左；位图用 measureText 与预览一致 */
            forceRasterText: true,
          },
        }
        extras.push(ex as SystemTemplateElementBase)
      }
    }
    if (type === 'NUTRITION') {
      cfg.nutritionTitleBold = false
      cfg.nutritionBodyBold = false
    }
    next.config = cfg
    return next as SystemTemplateElementBase
  })
  const allElements = [...elements, ...extras]
  const scaledWidthNum = Number(scaleXNum((template as any).width))
  if (Number.isFinite(scaledWidthNum) && scaledWidthNum > 0 && allElements.length) {
    const safeRight = scaledWidthNum * safeRightRatio
    const maxRight = allElements.reduce((acc, el) => {
      const x = Number((el as any).x || 0)
      const w = Number((el as any).width || 0)
      if (!Number.isFinite(x) || !Number.isFinite(w)) return acc
      return Math.max(acc, x + w)
    }, 0)
    /** 不再整体左移 x：与屏幕预览不一致且营养表会像「贴左、右边框丢失」。仅收窄超出安全右界的元素宽度 */
    const overflow = Math.max(0, maxRight - safeRight)
    if (overflow > 0.01) {
      allElements.forEach((el) => {
        const t = String((el as any).type || '').toUpperCase()
        /** BARCODE/QRCODE 的 width 直接参与原生 narrow/wide 或模块尺寸；收窄会导致「条码缩成一条」 */
        if (t === 'BARCODE' || t === 'QRCODE') return
        const x = Number((el as any).x || 0)
        const w = Number((el as any).width || 0)
        if (!Number.isFinite(x) || !Number.isFinite(w)) return
        const right = x + w
        if (right <= safeRight) return
        const room = safeRight - x
        if (room <= 0) return
        const newW = Math.min(w, Math.max(8, room))
        if (newW < w) {
          (el as any).width = Math.round(newW * 1000) / 1000
        }
      })
    }
  }
  return {
    ...template,
    width: scaleXNum((template as any).width) as any,
    elements: allElements,
  }
}

/** 打印进度：全程 0–100%；节流避免经典蓝牙按块回调时 showLoading 过频卡顿（从 0 第一次前进允许 1%） */
let lastShownPrintPct = -999

function showPrintProgress (percent: number) {
  const p = Math.max(0, Math.min(100, Math.round(percent)))
  const firstStepFromZero = lastShownPrintPct === 0 && p > 0
  if (
    !firstStepFromZero &&
    p !== 0 &&
    p !== 100 &&
    Math.abs(p - lastShownPrintPct) < 2
  ) {
    return
  }
  lastShownPrintPct = p
  try {
    uni.showLoading({ title: `Printing ${p}%`, mask: true })
  } catch (_) {}
}

async function ensureClassicReadyForPrint(): Promise<void> {
  const summary = getCurrentPrinterSummary()
  const conn = getBluetoothConnection()
  if (summary.type !== 'bluetooth' || conn?.deviceType !== 'classic') return
  await ensureNativeClassicTransportIfPossible()
  await new Promise<void>((resolve, reject) => {
    const tryConnect = typeof classicBluetooth?.connDevice === 'function'
    if (!tryConnect) {
      reject(new Error('CLASSIC_CONNECT_API_MISSING'))
      return
    }
    let settled = false
    const timeoutId = setTimeout(() => {
      if (settled) return
      settled = true
      reject(new Error('CLASSIC_CONNECT_TIMEOUT'))
    }, 8000)
    try {
      classicBluetooth.connDevice(conn.deviceId, (ok: boolean) => {
        if (settled) return
        settled = true
        clearTimeout(timeoutId)
        if (ok) resolve()
        else reject(new Error(classicBluetooth.getLastError?.() || 'CLASSIC_CONNECT_FAILED'))
      })
    } catch (e: any) {
      if (settled) return
      settled = true
      clearTimeout(timeoutId)
      reject(e instanceof Error ? e : new Error(String(e || 'CLASSIC_CONNECT_EXCEPTION')))
    }
  })
}

const labelCode = ref('')
const productId = ref('')
const displayProductName = ref('')
const productCategory = ref('')
const labelTypeName = ref('')
const templateSize = ref('')
const templateName = ref('')
const lastEdited = ref('')
const locationName = ref('')
/** 模板含 Label ID 控件时，接口返回的门店当日打印序号（yyyyMMdd-n），用于画布预览/出纸 */
const printLabelDisplayId = ref('')

const footerLabelIdDisplay = computed(() => {
  const id = printLabelDisplayId.value.trim()
  return id || '—'
})

const previewLoading = ref(true)
const previewError = ref('')
const previewImageSrc = ref('')
const systemTemplate = ref<SystemLabelTemplate | null>(null)
/** 未合并「打印多选项」前的模板，用于反复 merge */
const basePreviewTemplate = ref<SystemLabelTemplate | null>(null)
/** 接口返回的 templateProductDefaults，每次合并/重绘按当前时刻重算日期时间 */
const previewProductDefaults = ref<Record<string, string>>({})
/** 最近一次 8.2 原始响应，便于 defaults 解析失败时重试 */
const lastPreviewRawPayload = ref<unknown>(null)
const printOptionSelections = ref<Record<string, string[]>>({})
const dictLabelsByElementId = ref<Record<string, string>>({})
const dictValuesByElementId = ref<Record<string, string[]>>({})
const printFreeFieldValues = ref<Record<string, string>>({})
const pickerDialogVisible = ref(false)
const pickerMode = ref<'date' | 'time' | 'datetime'>('date')
const pickerSelection = ref<number[]>([0, 0, 0, 0, 0])
const pickerTargetId = ref('')
const pickerTargetKind = ref<'date' | 'time' | 'datetime'>('date')
const pickerTargetFormat = ref('')
const pickerDialogTitle = computed(() => {
  if (pickerMode.value === 'datetime') return 'Select date & time'
  if (pickerMode.value === 'date') return 'Select date'
  return 'Select time'
})
const pickerYears = computed(() => {
  const now = new Date().getFullYear()
  return Array.from({ length: 21 }, (_, i) => String(now - 10 + i))
})
const pickerMonths = computed(() => Array.from({ length: 12 }, (_, i) => String(i + 1).padStart(2, '0')))
const pickerHours = computed(() => Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0')))
const pickerMinutes = computed(() => Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0')))
/** 与 `.picker-item` 行高一致（px），否则各列与中间选中带错位，看起来像未水平对齐 */
const pickerIndicatorStyle = computed(() => {
  const h = uni.upx2px(72)
  return `height:${h}px;box-sizing:border-box;`
})
const pickerDays = computed(() => {
  if (pickerMode.value !== 'date' && pickerMode.value !== 'datetime') return []
  const year = Number(pickerYears.value[pickerSelection.value[0]] || new Date().getFullYear())
  const month = Number(pickerMonths.value[pickerSelection.value[1]] || 1)
  const dayCount = new Date(year, month, 0).getDate()
  return Array.from({ length: dayCount }, (_, i) => String(i + 1).padStart(2, '0'))
})

let freeFieldPreviewTimer: ReturnType<typeof setTimeout> | null = null

const printOptionFieldList = computed(() => {
  const t = basePreviewTemplate.value
  if (!t?.elements?.length) return []
  return t.elements.filter(isPrintInputOptionsElement)
})

const printFreeFieldList = computed(() => {
  const t = basePreviewTemplate.value
  if (!t?.elements?.length) return []
  return t.elements.filter(isPrintInputFreeFieldElement)
})

function dictValuesForElement(elementId: string): string[] {
  return dictValuesByElementId.value[elementId] || []
}

function optionFieldTitle(el: SystemTemplateElementBase): string {
  return (
    dictLabelsByElementId.value[el.id] ||
    el.elementName ||
    el.inputKey ||
    'Options'
  )
}

function isOptionPicked(elementId: string, value: string): boolean {
  return (printOptionSelections.value[elementId] || []).includes(value)
}

function togglePrintOption(elementId: string, value: string) {
  const cur = { ...printOptionSelections.value }
  const arr = [...(cur[elementId] || [])]
  const i = arr.indexOf(value)
  if (i >= 0) arr.splice(i, 1)
  else arr.push(value)
  if (arr.length) cur[elementId] = arr
  else delete cur[elementId]
  printOptionSelections.value = cur
  void refreshPreviewFromSelections()
}

function freeFieldNameLabel(el: SystemTemplateElementBase): string {
  const n = el.elementName || el.inputKey || 'Field'
  return `${n}:`
}

function freeFieldPlaceholder(el: SystemTemplateElementBase): string {
  const c = el.config || {}
  const type = String(el.type || '').toUpperCase()
  if (type === 'DATE' || type === 'TIME') {
    return String(c.format ?? c.Format ?? '')
  }
  return ''
}

function freeFieldDateFormat(el: SystemTemplateElementBase): string {
  const c = el.config || {}
  return String(c.format ?? c.Format ?? '').trim()
}

function freeFieldInputKind(el: SystemTemplateElementBase): 'text' | 'date' | 'time' | 'datetime' {
  const type = String(el.type || '').toUpperCase()
  const c = el.config || {}
  const inputType = String(c.inputType ?? c.InputType ?? '').toLowerCase()
  if (type === 'TIME') return 'time'
  if (type === 'DATE' && inputType === 'datetime') return 'datetime'
  if (type === 'DATE') return 'date'
  return 'text'
}

function todayYmd(): string {
  const d = new Date()
  const y = d.getFullYear()
  const m = String(d.getMonth() + 1).padStart(2, '0')
  const day = String(d.getDate()).padStart(2, '0')
  return `${y}-${m}-${day}`
}

function pickerDateValue(raw: string | undefined, format: string): string {
  const v = String(raw || '').trim()
  if (!v) return todayYmd()
  const datePart = v.split(' ')[0]
  if (/^\d{4}-\d{2}-\d{2}$/.test(datePart)) return datePart
  if (/^\d{2}\/\d{2}\/\d{4}$/.test(datePart)) {
    const [a, b, c] = datePart.split('/')
    if (format.startsWith('DD/')) return `${c}-${b}-${a}`
    return `${c}-${a}-${b}`
  }
  return todayYmd()
}

function pickerTimeValue(raw: string | undefined): string {
  const v = String(raw || '').trim()
  const m = v.match(/(\d{2}:\d{2})/)
  return m?.[1] || '12:00'
}

function formatYmdByPattern(ymd: string, format: string): string {
  const [y, m, d] = ymd.split('-')
  if (!y || !m || !d) return ymd
  if (format.startsWith('DD/')) return `${d}/${m}/${y}`
  if (format.startsWith('MM/')) return `${m}/${d}/${y}`
  return `${y}-${m}-${d}`
}

function displayDateValue(raw: string | undefined, format: string, fallback: string): string {
  const v = String(raw || '').trim()
  if (v) return v.split(' ')[0]
  return fallback || 'Select date'
}

function displayTimeValue(raw: string | undefined): string {
  const v = String(raw || '').trim()
  const m = v.match(/(\d{2}:\d{2})/)
  return m?.[1] || 'Select time'
}

function displayDateTimeField(raw: string | undefined, format: string): string {
  const v = String(raw || '').trim()
  if (v) return v
  const f = String(format || '').trim()
  return f || 'Select date & time'
}

function dateFormatFromTemplateFormat(full: string): string {
  const s = String(full || '').trim()
  if (!s) return 'YYYY-MM-DD'
  const head = s.split(/\s+/)[0] || ''
  if (/Y|M|D/.test(head)) return head
  return 'YYYY-MM-DD'
}

function parseExistingDateParts(raw: string): { y: string; m: string; d: string } {
  const v = String(raw || '').trim()
  if (!v) {
    const t = todayYmd().split('-')
    return { y: t[0], m: t[1], d: t[2] }
  }
  const datePart = v.split(' ')[0]
  if (/^\d{4}-\d{2}-\d{2}$/.test(datePart)) {
    const [y, m, d] = datePart.split('-')
    return { y, m, d }
  }
  if (/^\d{2}\/\d{2}\/\d{4}$/.test(datePart)) {
    const [a, b, c] = datePart.split('/')
    return { y: c, m: b, d: a }
  }
  const t = todayYmd().split('-')
  return { y: t[0], m: t[1], d: t[2] }
}

function openFreeFieldPicker(el: SystemTemplateElementBase, mode: 'date' | 'time' | 'datetime') {
  pickerTargetId.value = el.id
  pickerTargetKind.value = freeFieldInputKind(el)
  pickerTargetFormat.value = freeFieldDateFormat(el)
  pickerMode.value = mode
  const currentRaw = String(printFreeFieldValues.value[el.id] || '').trim()
  if (mode === 'date') {
    const p = parseExistingDateParts(currentRaw)
    const yi = Math.max(0, pickerYears.value.indexOf(p.y))
    const mi = Math.max(0, pickerMonths.value.indexOf(p.m))
    const dd = Array.from({ length: new Date(Number(p.y), Number(p.m), 0).getDate() }, (_, i) => String(i + 1).padStart(2, '0'))
    const di = Math.max(0, dd.indexOf(p.d))
    pickerSelection.value = [yi, mi, di]
  } else if (mode === 'datetime') {
    const p = parseExistingDateParts(currentRaw)
    const yi = Math.max(0, pickerYears.value.indexOf(p.y))
    const mi = Math.max(0, pickerMonths.value.indexOf(p.m))
    const dd = Array.from({ length: new Date(Number(p.y), Number(p.m), 0).getDate() }, (_, i) => String(i + 1).padStart(2, '0'))
    const di = Math.max(0, dd.indexOf(p.d))
    const hhmm = pickerTimeValue(currentRaw).split(':')
    const hi = Math.max(0, pickerHours.value.indexOf(hhmm[0] || '12'))
    const mini = Math.max(0, pickerMinutes.value.indexOf(hhmm[1] || '00'))
    pickerSelection.value = [yi, mi, di, hi, mini]
  } else {
    const hhmm = pickerTimeValue(currentRaw).split(':')
    const hi = Math.max(0, pickerHours.value.indexOf(hhmm[0] || '12'))
    const mi = Math.max(0, pickerMinutes.value.indexOf(hhmm[1] || '00'))
    pickerSelection.value = [hi, mi]
  }
  pickerDialogVisible.value = true
}

function closeFreeFieldPicker() {
  pickerDialogVisible.value = false
}

function onPickerViewChange(e: { detail?: { value?: number[] } }) {
  const raw = e.detail?.value
  if (!Array.isArray(raw)) {
    pickerSelection.value = []
    return
  }
  if (pickerMode.value === 'time') {
    pickerSelection.value = raw.slice(0, 2)
    return
  }
  let arr = [...raw]
  if (pickerMode.value === 'date') arr = arr.slice(0, 3)
  else if (pickerMode.value === 'datetime') arr = arr.slice(0, 5)
  const yi = arr[0] ?? 0
  const mi = arr[1] ?? 0
  const y = Number(pickerYears.value[yi] || new Date().getFullYear())
  const mo = Number(pickerMonths.value[mi] || 1)
  const dayCount = new Date(y, mo, 0).getDate()
  const maxDi = Math.max(0, dayCount - 1)
  if (arr.length >= 3 && (arr[2] ?? 0) > maxDi) arr[2] = maxDi
  pickerSelection.value = arr
}

function confirmFreeFieldPicker() {
  const id = pickerTargetId.value
  if (!id) {
    pickerDialogVisible.value = false
    return
  }
  const prev = String(printFreeFieldValues.value[id] || '').trim()
  const dateFmt = dateFormatFromTemplateFormat(pickerTargetFormat.value)
  if (pickerMode.value === 'datetime') {
    const y = pickerYears.value[pickerSelection.value[0]] || String(new Date().getFullYear())
    const mo = pickerMonths.value[pickerSelection.value[1]] || '01'
    const maxDi = Math.max(0, pickerDays.value.length - 1)
    const di = Math.min(Math.max(0, pickerSelection.value[2] || 0), maxDi)
    const d = pickerDays.value[di] || '01'
    const h = pickerHours.value[pickerSelection.value[3]] || '12'
    const min = pickerMinutes.value[pickerSelection.value[4]] || '00'
    const formatted = formatYmdByPattern(`${y}-${mo}-${d}`, dateFmt)
    printFreeFieldValues.value = { ...printFreeFieldValues.value, [id]: `${formatted} ${h}:${min}` }
  } else if (pickerMode.value === 'date') {
    const y = pickerYears.value[pickerSelection.value[0]] || String(new Date().getFullYear())
    const m = pickerMonths.value[pickerSelection.value[1]] || '01'
    const d = pickerDays.value[pickerSelection.value[2]] || '01'
    const formatted = formatYmdByPattern(`${y}-${m}-${d}`, dateFmt)
    if (pickerTargetKind.value === 'datetime') {
      const timePart = pickerTimeValue(prev)
      printFreeFieldValues.value = { ...printFreeFieldValues.value, [id]: `${formatted} ${timePart}` }
    } else {
      printFreeFieldValues.value = { ...printFreeFieldValues.value, [id]: formatted }
    }
  } else {
    const h = pickerHours.value[pickerSelection.value[0]] || '12'
    const m = pickerMinutes.value[pickerSelection.value[1]] || '00'
    const hhmm = `${h}:${m}`
    if (pickerTargetKind.value === 'datetime') {
      const datePart = prev.split(' ')[0] || formatYmdByPattern(todayYmd(), dateFmt)
      printFreeFieldValues.value = { ...printFreeFieldValues.value, [id]: `${datePart} ${hhmm}` }
    } else {
      printFreeFieldValues.value = { ...printFreeFieldValues.value, [id]: hhmm }
    }
  }
  if (freeFieldPreviewTimer != null) clearTimeout(freeFieldPreviewTimer)
  freeFieldPreviewTimer = setTimeout(() => {
    freeFieldPreviewTimer = null
    void refreshPreviewFromSelections()
  }, 180)
  pickerDialogVisible.value = false
}

function onFreeFieldDateChange(el: SystemTemplateElementBase, e: { detail?: { value?: string } }) {
  const ymd = String(e.detail?.value || '').trim()
  if (!ymd) return
  const id = el.id
  const kind = freeFieldInputKind(el)
  const formatted = formatYmdByPattern(ymd, freeFieldDateFormat(el))
  const prev = String(printFreeFieldValues.value[id] || '').trim()
  if (kind === 'datetime') {
    const timePart = pickerTimeValue(prev)
    printFreeFieldValues.value = { ...printFreeFieldValues.value, [id]: `${formatted} ${timePart}` }
  } else {
    printFreeFieldValues.value = { ...printFreeFieldValues.value, [id]: formatted }
  }
  if (freeFieldPreviewTimer != null) clearTimeout(freeFieldPreviewTimer)
  freeFieldPreviewTimer = setTimeout(() => {
    freeFieldPreviewTimer = null
    void refreshPreviewFromSelections()
  }, 180)
}

function onFreeFieldTimeChange(elementId: string, e: { detail?: { value?: string } }) {
  const timeVal = String(e.detail?.value || '').trim()
  if (!timeVal) return
  const prev = String(printFreeFieldValues.value[elementId] || '').trim()
  const datePart = prev.split(' ')[0] || formatYmdByPattern(todayYmd(), 'YYYY-MM-DD')
  printFreeFieldValues.value = { ...printFreeFieldValues.value, [elementId]: `${datePart} ${timeVal}` }
  if (freeFieldPreviewTimer != null) clearTimeout(freeFieldPreviewTimer)
  freeFieldPreviewTimer = setTimeout(() => {
    freeFieldPreviewTimer = null
    void refreshPreviewFromSelections()
  }, 180)
}

function onFreeFieldInput(elementId: string, e: { detail?: { value?: string } }) {
  const v = e.detail?.value ?? ''
  printFreeFieldValues.value = { ...printFreeFieldValues.value, [elementId]: v }
  if (freeFieldPreviewTimer != null) clearTimeout(freeFieldPreviewTimer)
  freeFieldPreviewTimer = setTimeout(() => {
    freeFieldPreviewTimer = null
    void refreshPreviewFromSelections()
  }, 280)
}

function computeMergedPreviewTemplate(at?: Date): SystemLabelTemplate | null {
  let base = basePreviewTemplate.value
  if (!base) return null
  const baseTime = at ?? new Date()
  let defaults = previewProductDefaults.value
  if (Object.keys(defaults).length === 0 && lastPreviewRawPayload.value != null) {
    const recovered = extractTemplateProductDefaultValuesFromPreviewPayload(lastPreviewRawPayload.value)
    if (Object.keys(recovered).length > 0) {
      defaults = recovered
      previewProductDefaults.value = recovered
    }
  }
  if (Object.keys(defaults).length > 0) {
    base = applyTemplateProductDefaultValuesToTemplate(base, defaults, baseTime)
  }
  let m = mergePrintOptionSelections(
    base,
    printOptionSelections.value,
    dictLabelsByElementId.value
  )
  m = mergePrintInputFreeFields(m, printFreeFieldValues.value)
  if (templateIncludesLabelIdElement(m) && printLabelDisplayId.value.trim()) {
    m = applyLabelIdDisplayToTemplate(m, printLabelDisplayId.value.trim())
  }
  if (templateIncludesEmployeeElement(m)) {
    m = applyEmployeeDisplayToTemplate(m)
  }
  m = applyLiveDateTimeFieldsToTemplate(m, baseTime)
  return m
}

async function loadDictionaryMetaForTemplate(t: SystemLabelTemplate) {
  const labels: Record<string, string> = {}
  const values: Record<string, string[]> = {}
  for (const el of t.elements || []) {
    if (!isPrintInputOptionsElement(el)) continue
    const mid = String(el.config?.multipleOptionId ?? el.config?.MultipleOptionId ?? '').trim()
    if (!mid) continue
    try {
      const d = await fetchLabelMultipleOptionById(mid)
      if (d) {
        labels[el.id] = d.optionName
        values[el.id] = d.optionValuesJson.length ? d.optionValuesJson : []
      }
    } catch {
      values[el.id] = []
    }
  }
  dictLabelsByElementId.value = labels
  dictValuesByElementId.value = values
}

/** 画布 :width/:height 与离屏节点布局在 H5 上晚一拍，过早 canvasToTempFilePath 会裁切底部；双 nextTick + 双 rAF 再导出 */
async function waitForCanvasLayout(): Promise<void> {
  await nextTick()
  await nextTick()
  await new Promise<void>((resolve) => {
    if (typeof requestAnimationFrame === 'function') {
      requestAnimationFrame(() => {
        requestAnimationFrame(() => resolve())
      })
    } else {
      setTimeout(() => resolve(), 32)
    }
  })
}

/** 光栅打印前同步隐藏 canvas 像素尺寸，避免缓冲区高度仍为预览值导致底部裁切 */
async function applyPrintCanvasLayout(layout: { outW: number; outH: number }) {
  canvasCssW.value = layout.outW
  canvasCssH.value = layout.outH
  await waitForCanvasLayout()
  await new Promise<void>((r) => setTimeout(r, 80))
}

let deferredPreviewRedrawTimer: ReturnType<typeof setTimeout> | null = null

/** 首屏内容与 loading 切换后布局才稳定，补绘一次与「改输入后变正常」同路径 */
function scheduleDeferredPreviewRedraw() {
  if (deferredPreviewRedrawTimer != null) clearTimeout(deferredPreviewRedrawTimer)
  deferredPreviewRedrawTimer = setTimeout(() => {
    deferredPreviewRedrawTimer = null
    void refreshPreviewFromSelections()
  }, 120)
}

async function refreshPreviewFromSelections() {
  const merged = computeMergedPreviewTemplate()
  if (!merged || !instance) return
  systemTemplate.value = merged
  const sz = getPreviewCanvasCssSize(merged, 720)
  canvasCssW.value = sz.width
  canvasCssH.value = sz.height
  await waitForCanvasLayout()
  try {
    const path = await renderLabelPreviewToTempPath('labelPreviewCanvas', instance, merged, 720)
    previewImageSrc.value = path
  } catch {
    /* keep previous image */
  }
}

const canvasCssW = ref(300)
const canvasCssH = ref(200)

onShow(() => {
  btConnected.value = isPrinterReadySync()
  const summary = getCurrentPrinterSummary()
  const conn = getBluetoothConnection()
  if (summary.type === 'bluetooth' && conn?.deviceName) {
    btDeviceName.value = conn.deviceName
  } else if (summary.type === 'builtin') {
    btDeviceName.value = summary.driverName || 'Built-in'
  } else {
    btDeviceName.value = summary.displayName || ''
  }
  const name = uni.getStorageSync('storeName')
  if (typeof name === 'string' && name.trim()) locationName.value = name.trim()
  if (basePreviewTemplate.value && !previewLoading.value) {
    void refreshPreviewFromSelections()
  }
})

watch(showPreviewModal, (open) => {
  if (open && basePreviewTemplate.value) {
    void refreshPreviewFromSelections()
  }
})

onUnload(() => {
  if (deferredPreviewRedrawTimer != null) {
    clearTimeout(deferredPreviewRedrawTimer)
    deferredPreviewRedrawTimer = null
  }
})

onLoad((opts: Record<string, string | undefined>) => {
  labelCode.value = decodeURIComponent(opts.labelCode || '')
  productId.value = decodeURIComponent(opts.productId || '')
  displayProductName.value = decodeURIComponent(opts.productName || '')
  productCategory.value = decodeURIComponent(opts.categoryName || '')
  labelTypeName.value = decodeURIComponent(opts.typeName || '')
  templateSize.value = decodeURIComponent(opts.templateSize || '')
  templateName.value = decodeURIComponent(opts.templateName || '')

  const name = uni.getStorageSync('storeName')
  if (typeof name === 'string' && name.trim()) locationName.value = name.trim()
  printLabelDisplayId.value = ''
  lastEdited.value = '—'

  loadPreview()
})

async function loadPreview() {
  const loc = getCurrentStoreId()
  if (!loc) {
    previewError.value = 'No store selected.'
    previewLoading.value = false
    return
  }
  if (!labelCode.value) {
    previewError.value = 'Missing label code.'
    previewLoading.value = false
    return
  }

  if (deferredPreviewRedrawTimer != null) {
    clearTimeout(deferredPreviewRedrawTimer)
    deferredPreviewRedrawTimer = null
  }

  previewLoading.value = true
  previewError.value = ''
  previewImageSrc.value = ''
  systemTemplate.value = null
  basePreviewTemplate.value = null
  previewProductDefaults.value = {}
  lastPreviewRawPayload.value = null
  printOptionSelections.value = {}
  printFreeFieldValues.value = {}
  dictLabelsByElementId.value = {}
  dictValuesByElementId.value = {}

  try {
    const raw = await postUsAppLabelPreview({
      locationId: loc,
      labelCode: labelCode.value,
      productId: productId.value || undefined,
      baseTime: new Date().toISOString(),
    })
    lastPreviewRawPayload.value = raw
    const root = raw as Record<string, unknown>
    const nested = root.data ?? root.Data
    const inner =
      nested != null && typeof nested === 'object' && !Array.isArray(nested)
        ? (nested as Record<string, unknown>)
        : null

    /** 8.2 预览：labelLastEdited / labelId（兼容 printLabelDisplayId、PascalCase、data 嵌套） */
    const readLastEdited = (L: Record<string, unknown> | null): string => {
      if (!L) return ''
      const v =
        L.labelLastEdited ??
        L.LabelLastEdited ??
        L.lastEdited ??
        L.LastEdited
      return typeof v === 'string' && v.trim() ? v.trim() : ''
    }
    const readPrintLabelDisplayId = (L: Record<string, unknown> | null): string => {
      if (!L) return ''
      const keys = [
        'printLabelDisplayId',
        'PrintLabelDisplayId',
        'labelDisplayId',
        'LabelDisplayId',
        'labelId',
        'LabelId',
      ]
      for (const key of keys) {
        const v = L[key]
        if (v != null && String(v).trim() !== '') return String(v).trim()
      }
      return ''
    }

    const leStr = readLastEdited(root) || readLastEdited(inner)
    lastEdited.value = leStr || '—'

    const displayIdStr =
      readPrintLabelDisplayId(root) || readPrintLabelDisplayId(inner)
    printLabelDisplayId.value = displayIdStr

    const tmplPayload =
      inner != null &&
      (inner.template != null ||
        inner.Template != null ||
        Array.isArray(inner.elements) ||
        Array.isArray(inner.Elements))
        ? inner
        : raw
    const tmplRaw = normalizeLabelTemplateFromPreviewApi(tmplPayload)
    if (!tmplRaw) {
      previewError.value = 'Invalid preview template.'
      previewLoading.value = false
      return
    }
    const lstRaw =
      root.labelSizeText ??
      root.LabelSizeText ??
      inner?.labelSizeText ??
      inner?.LabelSizeText
    const labelSizeTextFromApi = typeof lstRaw === 'string' ? lstRaw : null
    if (labelSizeTextFromApi && labelSizeTextFromApi.trim()) {
      templateSize.value = labelSizeTextFromApi.trim()
    }
    const productDefaults = extractTemplateProductDefaultValuesFromPreviewPayload(raw)
    previewProductDefaults.value = productDefaults
    let tmplBase = tmplRaw
    const productCodeValue = extractProductCodeValueFromPreviewPayload(raw)
    if (productCodeValue) {
      tmplBase = applyProductCodeValueToTemplateScanElements(tmplBase, productCodeValue)
    }
    /** 仅存原始模板 + 默认值；日期/时间在 computeMergedPreviewTemplate 内按同一 baseTime 重算一次 */
    const base = overlayProductNameOnPreviewTemplate(tmplBase, displayProductName.value)
    basePreviewTemplate.value = base
    printOptionSelections.value = readSelectionsFromTemplate(base)
    printFreeFieldValues.value = ensureFreeFieldKeys(base, readFreeFieldValuesFromTemplate(base))
    await loadDictionaryMetaForTemplate(base)
    const merged = computeMergedPreviewTemplate()
    if (!merged) {
      previewError.value = 'Invalid preview template.'
      previewLoading.value = false
      return
    }
    systemTemplate.value = merged

    const sz = getPreviewCanvasCssSize(merged, 720)
    canvasCssW.value = sz.width
    canvasCssH.value = sz.height

    await waitForCanvasLayout()
    if (!instance) {
      previewError.value = 'Preview unavailable.'
      previewLoading.value = false
      return
    }
    const path = await renderLabelPreviewToTempPath('labelPreviewCanvas', instance, merged, 720)
    previewImageSrc.value = path
    scheduleDeferredPreviewRedraw()
  } catch (e: unknown) {
    if (isUsAppSessionExpiredError(e)) return
    previewError.value = e instanceof Error ? e.message : 'Preview failed.'
  } finally {
    previewLoading.value = false
  }
}

const increment = () => {
  if (printQty.value < 99) printQty.value++
}
const decrement = () => {
  if (printQty.value > 1) printQty.value--
}
const goBack = () => uni.navigateBack()

const goBluetoothPage = () => {
  uni.navigateTo({ url: '/pages/labels/bluetooth' })
}

/** 接口 9 可选 clientRequestId（文档 10 幂等） */
function createPrintClientRequestId (): string {
  const c = typeof globalThis !== 'undefined' ? (globalThis as { crypto?: Crypto }).crypto : undefined
  if (c && typeof c.randomUUID === 'function') {
    return c.randomUUID()
  }
  return `print-${Date.now()}-${Math.random().toString(36).slice(2, 12)}`
}

const handlePrint = async () => {
  if (isPrinting.value || previewLoading.value || !systemTemplate.value) return

  if (!isPrinterReadySync()) {
    showNoPrinterModal.value = true
    return
  }

  const tmplForValidate = basePreviewTemplate.value ?? systemTemplate.value
  if (tmplForValidate) {
    const optErr = validatePrintInputOptionsBeforePrint(
      tmplForValidate,
      printOptionSelections.value,
      dictLabelsByElementId.value
    )
    if (optErr) {
      uni.showToast({ title: optErr, icon: 'none', duration: 3000 })
      return
    }
    const freeErr = validatePrintInputFreeFieldsBeforePrint(
      tmplForValidate,
      printFreeFieldValues.value
    )
    if (freeErr) {
      uni.showToast({ title: freeErr, icon: 'none', duration: 3000 })
      return
    }
  }

  try {
    await ensureClassicReadyForPrint()
  } catch (e: any) {
    const msg = e?.message ? String(e.message) : 'CLASSIC_NOT_READY'
    uni.showModal({
      title: 'Printer Not Ready',
      content: `${msg}\n\n${buildPrintDebugText({ stage: 'preflight' })}`.trim(),
      showCancel: false,
    })
    return
  }

  // Virtual BT Printer 在当前基座缺少 native-fast-printer 插件时容易出现“发送中卡死”。
  // 先 fail-fast，避免进入长时间无响应流程。
  const summaryPreflight = getCurrentPrinterSummary()
  const connPreflight = getBluetoothConnection()
  const isVirtualBtPrinter = String(connPreflight?.deviceName || '').toLowerCase().includes('virtual bt printer')
  if (
    summaryPreflight.type === 'bluetooth' &&
    summaryPreflight.driverKey === 'd320fax' &&
    isVirtualBtPrinter &&
    !isNativeFastPrinterAvailable()
  ) {
    uni.showModal({
      title: 'Print Not Supported In Current Build',
      content:
        'Virtual BT Printer (D320FAX) requires native-fast-printer in this app build. Please use a build that includes this plugin, or switch to a non-virtual Bluetooth printer.',
      showCancel: false,
    })
    return
  }

  isPrinting.value = true
  const printJobId = createPrintJobId()
  let watchdog: ReturnType<typeof setTimeout> | null = null
  let globalWatchdog: ReturnType<typeof setTimeout> | null = null
  const startedAt = Date.now()
  let printStage = 'start'
  let printRoute = '-'
  let timeoutHandledByWatchdog = false
  let printResultShown = false
  const showPrintResultOnce = async () => {
    if (printResultShown) return
    printResultShown = true
    await showPrintResultModal({
      jobId: printJobId,
      route: printRoute,
      stage: printStage,
    })
  }
  try {
    lastShownPrintPct = -999
    uni.showLoading({ title: 'Rendering…', mask: true })
    /** 出纸时刻为唯一基准：Prep / Use By 等同一次点击内共用当前时间 */
    const printBaseTime = new Date()
    const mergedForPrint = computeMergedPreviewTemplate(printBaseTime)
    const tmplBase = mergedForPrint ?? systemTemplate.value
    if (!tmplBase || !instance) {
      throw new Error('No label to print.')
    }
    let tmpl = ensureTemplateHeightCoversElements(tmplBase)
    const phys = getTemplatePhysicalSizeMm(tmpl)
    console.info(
      '[preview] print physical size',
      `${phys.widthMm}×${phys.heightMm}mm`,
      `template=${tmpl.width}×${tmpl.height}${phys.unit}`,
    )
    ensurePrintJobActive(printJobId)

    const printInputJson = buildPrintInputJson(
      tmpl,
      printOptionSelections.value,
      printFreeFieldValues.value
    )
    printStage = 'payload-ready'

    /** 模板含 Label ID 时先落库以分配当日序号，再带着真实序号出纸（与 Print Log 一致） */
    let printLogAlreadySynced = false
    let printLogRequestBody: ReturnType<typeof buildUsAppLabelPrintRequestBody> = null
    const printClientRequestId = createPrintClientRequestId()
    if (templateIncludesLabelIdElement(tmpl)) {
      printStage = 'reserve-label-id'
      const persistTemplateDocEarly = JSON.parse(
        JSON.stringify(buildLabelPrintJobPayload(tmpl, printInputJson).template),
      ) as Record<string, unknown>
      const printInputSnapshotEarly: Record<string, unknown> = {
        ...printInputJson,
        ...persistTemplateDocEarly,
      }
      printLogRequestBody = buildUsAppLabelPrintRequestBody({
        locationId: getCurrentStoreId(),
        labelCode: labelCode.value,
        productId: productId.value || undefined,
        printQuantity: printQty.value,
        mergedTemplate: printInputSnapshotEarly,
        clientRequestId: printClientRequestId,
        printerMac: getBluetoothConnection()?.deviceId || undefined,
      })
      if (printLogRequestBody) {
        const printOut = await postUsAppLabelPrint(printLogRequestBody)
        const ids =
          printOut?.printLabelDisplayIds ??
          (printOut as { PrintLabelDisplayIds?: string[] })?.PrintLabelDisplayIds ??
          []
        const firstId = Array.isArray(ids) && ids.length ? String(ids[0]).trim() : ''
        if (firstId) {
          printLabelDisplayId.value = firstId
          tmpl = ensureTemplateHeightCoversElements(
            applyLabelIdDisplayToTemplate(tmpl, firstId),
          )
        }
        printLogAlreadySynced = true
      }
    }

    /**
     * 经典蓝牙 + native-plugin（含 Virtual BT）：走原生 printTemplate（JSON，快，与预览坐标一致）。
     * Virtual BT 不做 xScale/安全区收窄（易导致营养表错位、条码丢失）；光栅仅作回退。
     * 普通蓝牙（BLE 或 classic+JS socket）：canPrintCurrentLabelViaNativeFastJob 为 false，走下方光栅/直发 TSC。
     */
    const tmplForNativeJob = normalizeTemplateForNativeFastJob(tmpl, printInputJson as any, printBaseTime)
    const currentDriver = getCurrentPrinterDriver()
    const currentConn = getBluetoothConnection()
    const isD320faxClassicNative =
      currentDriver.key === 'd320fax' && currentConn?.deviceType === 'classic'
    const useNativeTemplatePrint =
      canPrintCurrentLabelViaNativeFastJob()
      && isTemplateWithinNativeFastPrintBounds(tmpl)
      && !templateHasUnsupportedNativeFastElements(tmplForNativeJob)

    if (ENABLE_PRINT_PREFLIGHT_MODAL) {
      // 一体机无 console：打印前弹设备链路信息（仅调试模式开启）
      try {
        const printerSnap = await getPrinterDebugSnapshot({ reason: 'preflight' })
        const preflightText = buildPrintPreflightDeviceInfoText({
          template: tmpl,
          useNativeTemplatePrint,
          nativeUnsupported: templateHasUnsupportedNativeFastElements(tmplForNativeJob),
          printerSnapshot: printerSnap,
        })
        await showPrintPreflightModal(preflightText)
      } catch {
        uni.hideLoading()
        if (isPrintJobActive(printJobId)) isPrinting.value = false
        return
      }
    }
    ensurePrintJobActive(printJobId)

    /** 基座只认本地路径；http(s)、/picture/ 须先下载，否则 IMAGE 整块丢失 */
    let tmplForNativePayload = tmplForNativeJob
    if (useNativeTemplatePrint) {
      // 真机 GP-D320FX 可略收窄；Virtual BT 必须与预览同坐标，禁止 xScale。
      if (isD320faxClassicNative && !isVirtualBtPrinter) {
        tmplForNativePayload = applyNativeTemplateStyleScale(
          tmplForNativePayload,
          { textScale: 1, xScale: 0.9, safeRightRatio: 0.88 },
        )
      }
      resetHydrateImageDebugRecords()
      tmplForNativePayload = await hydrateSystemTemplateImagesForPrint(tmplForNativePayload)
    }
    const labelPrintJobPayload = buildLabelPrintJobPayload(
      useNativeTemplatePrint ? tmplForNativePayload : tmpl,
      printInputJson,
      {
        labelCode: labelCode.value,
        productId: productId.value || undefined,
        printQuantity: printQty.value,
        locationId: getCurrentStoreId() || undefined,
      },
    )
    setLastLabelPrintJobPayload(labelPrintJobPayload)

    /**
     * 原生 printTemplate：物理尺寸超幅宽则回退光栅；规范化后仍有原生未覆盖元素则回退光栅。
     */

    if (useNativeTemplatePrint) {
      printRoute = 'native-template'
      printStage = 'native-print-start'
      const builtinNative = getCurrentPrinterSummary().type === 'builtin'
      const nativeOuterMs = builtinNative ? 600000 : 40000
      const nativeUiWatchdogMs = builtinNative ? nativeOuterMs + 60000 : 45000
      showPrintInProgressDebugModal({
        jobId: printJobId,
        route: printRoute,
        stage: printStage,
      })
      /** 经典蓝牙 + native-plugin：JSON → printTemplate（不经整页光栅，速度优先） */
      uni.showLoading({ title: 'Printing…', mask: true })
      watchdog = setTimeout(() => {
        timeoutHandledByWatchdog = true
        try { uni.hideLoading() } catch (_) {}
        isPrinting.value = false
        console.warn(
          '[preview] PRINT_TIMEOUT native-fast',
          buildPrintDebugText({ branch: 'native-fast', stage: printStage, elapsedMs: Date.now() - startedAt, qty: printQty.value }),
        )
      }, nativeUiWatchdogMs)
      await withTimeout(printLabelPrintJobPayloadForCurrentPrinter(
        labelPrintJobPayload,
        { printQty: printQty.value },
        (percent) => {
          if (!isPrintJobActive(printJobId)) return
          showPrintProgress(percent)
        }
      ), nativeOuterMs, 'PRINT_TIMEOUT')
      printStage = 'native-print-done'
      if (ENABLE_PRINT_RESULT_MODAL) await showPrintResultOnce()
    } else {
      printStage = 'raster-layout'
      const driver = getCurrentPrinterDriver()
      if (driver.key === 'd320fax') {
        printRoute = 'd320fax'
        const connNow = getBluetoothConnection()
        const isVirtualBtPrinter = String(connNow?.deviceName || '').toLowerCase().includes('virtual bt printer')
        /** Virtual 整页光栅与屏幕预览同源，可打出 ￥/价格；直发模板在部分环境位图字失败会缺底行 */
        /** 大图光栅 + 慢机可能 >8min；与原生 printCommandBytes 对齐后一般会快很多 */
        const virtualJobMs = 720000
        const nonVirtualRasterMs = 220000
        const nonVirtualDirectMs = 480000
        const globalCapMs = isVirtualBtPrinter ? 780000 : 540000
        const branchWatchMs = isVirtualBtPrinter ? 760000 : 520000
        if (globalWatchdog) {
          clearTimeout(globalWatchdog)
          globalWatchdog = null
        }
        globalWatchdog = setTimeout(() => {
          timeoutHandledByWatchdog = true
          try { uni.hideLoading() } catch (_) {}
          isPrinting.value = false
          console.warn(
            '[preview] PRINT_TIMEOUT_GLOBAL d320fax',
            buildPrintDebugText({ stage: printStage, elapsedMs: Date.now() - startedAt, qty: printQty.value }),
          )
        }, globalCapMs)
        watchdog = setTimeout(() => {
          timeoutHandledByWatchdog = true
          try { uni.hideLoading() } catch (_) {}
          isPrinting.value = false
          console.warn(
            '[preview] PRINT_TIMEOUT d320fax',
            buildPrintDebugText({ branch: 'd320fax', stage: printStage, elapsedMs: Date.now() - startedAt, qty: printQty.value }),
          )
        }, branchWatchMs)
        if (isVirtualBtPrinter) {
          showPrintProgress(0)
          try {
            printStage = 'd320fax-virtual-raster-start'
            await withTimeout(
              printSystemTemplateForCurrentPrinter(
                tmpl,
                printInputJson as any,
                {
                  printQty: printQty.value,
                  canvasRaster: {
                    canvasId: 'labelPreviewCanvas',
                    componentInstance: instance,
                    applyLayout: applyPrintCanvasLayout,
                  },
                },
                (percent) => {
                  if (!isPrintJobActive(printJobId)) return
                  showPrintProgress(percent)
                },
              ),
              virtualJobMs,
              'PRINT_TIMEOUT_RASTER',
            )
            printStage = 'd320fax-virtual-raster-done'
          } catch (e: any) {
            const msg = e?.message ? String(e.message) : ''
            // 超时时底层可能仍在发送，若立刻回退直发会导致“一次点击出两张”。
            if (msg.includes('PRINT_TIMEOUT')) {
              throw e
            }
            printStage = 'd320fax-virtual-direct-fallback-start'
            showPrintProgress(0)
            await withTimeout(
              printSystemTemplateForCurrentPrinter(
                tmpl,
                printInputJson as any,
                { printQty: printQty.value },
                (percent) => {
                  if (!isPrintJobActive(printJobId)) return
                  showPrintProgress(percent)
                },
              ),
              virtualJobMs,
              'PRINT_TIMEOUT_DIRECT',
            )
            printStage = 'd320fax-virtual-direct-fallback-done'
          }
        } else {
          printStage = 'd320fax-raster-start'
          showPrintProgress(0)
          try {
            await withTimeout(
              printSystemTemplateForCurrentPrinter(
                tmpl,
                printInputJson as any,
                {
                  printQty: printQty.value,
                  canvasRaster: {
                    canvasId: 'labelPreviewCanvas',
                    componentInstance: instance,
                    applyLayout: applyPrintCanvasLayout,
                  },
                },
                (percent) => {
                  if (!isPrintJobActive(printJobId)) return
                  showPrintProgress(percent)
                },
              ),
              nonVirtualRasterMs,
              'PRINT_TIMEOUT_RASTER',
            )
            printStage = 'd320fax-raster-done'
          } catch (e: any) {
            const msg = e?.message ? String(e.message) : ''
            // 超时时底层可能仍在发送，若立刻回退直发会导致“一次点击出两张”。
            if (msg.includes('PRINT_TIMEOUT')) {
              throw e
            }
            printStage = 'd320fax-direct-fallback-start'
            showPrintProgress(0)
            await withTimeout(
              printSystemTemplateForCurrentPrinter(
                tmpl,
                printInputJson as any,
                { printQty: printQty.value },
                (percent) => {
                  if (!isPrintJobActive(printJobId)) return
                  showPrintProgress(percent)
                },
              ),
              nonVirtualDirectMs,
              'PRINT_TIMEOUT_DIRECT_FALLBACK',
            )
            printStage = 'd320fax-direct-fallback-done'
          }
        }
      } else {
      printRoute = 'generic'
      const shouldUseDirectTemplate =
        driver.protocol === 'tsc' &&
        templateHasQrDataForCommandPrint(tmpl) &&
        !templateHasUnsupportedElementsForCommandPrint(tmpl)
      if (shouldUseDirectTemplate) {
        const directJobMs = 240000
        if (globalWatchdog) {
          clearTimeout(globalWatchdog)
          globalWatchdog = null
        }
        globalWatchdog = setTimeout(() => {
          timeoutHandledByWatchdog = true
          try { uni.hideLoading() } catch (_) {}
          isPrinting.value = false
          console.warn(
            '[preview] PRINT_TIMEOUT_GLOBAL direct-qrcode',
            buildPrintDebugText({ stage: printStage, elapsedMs: Date.now() - startedAt, qty: printQty.value }),
          )
        }, directJobMs + 60000)
        showPrintProgress(0)
        watchdog = setTimeout(() => {
          timeoutHandledByWatchdog = true
          try { uni.hideLoading() } catch (_) {}
          isPrinting.value = false
          console.warn(
            '[preview] PRINT_TIMEOUT direct-qrcode',
            buildPrintDebugText({
              branch: 'direct-qrcode',
              stage: printStage,
              elapsedMs: Date.now() - startedAt,
              qty: printQty.value,
            }),
          )
        }, directJobMs + 30000)
        printStage = 'direct-qrcode-start'
        await withTimeout(
          printSystemTemplateForCurrentPrinter(
            tmpl,
            printInputJson as any,
            { printQty: printQty.value },
            (percent) => showPrintProgress(percent),
          ),
          directJobMs,
          'PRINT_TIMEOUT_DIRECT_QRCODE',
        )
        printStage = 'direct-qrcode-done'
      } else {
        const rasterDriver = resolveRasterPrintDriver(driver)
        printRoute = rasterDriver.protocol === 'esc' ? 'esc-raster' : 'tsc-raster'
        if (globalWatchdog) {
          clearTimeout(globalWatchdog)
          globalWatchdog = null
        }
        const maxDots =
          rasterDriver.imageMaxWidthDots || (rasterDriver.protocol === 'esc' ? 384 : 576)
        const escUseContentH = rasterDriver.protocol === 'esc'
        const contentHpx = escUseContentH ? templateContentHeightPx(tmpl) : undefined
        const layout = getLabelPrintRasterLayout(
          tmpl,
          maxDots,
          rasterDriver.imageDpi || 203,
          contentHpx != null ? { contentHeightPx: contentHpx } : undefined,
        )

        canvasCssW.value = layout.outW
        canvasCssH.value = layout.outH
        await nextTick()
        await new Promise<void>((r) => setTimeout(r, 50))

        const useCanvasRasterPath = shouldRasterPrintViaCanvasImageData()
        let tmpPath = ''
        if (useCanvasRasterPath) {
          try {
            printStage = 'raster-canvas-rgba'
            const imageData = await renderLabelPreviewCanvasImageDataForPrint(
              'labelPreviewCanvas',
              instance,
              tmpl,
              layout
            )
            printStage = 'raster-image-ready'
            showPrintProgress(0)
            const genericRasterJobMs = 300000
            const escBuiltinLongWatchMs = 900000
            const watchdogMs =
              rasterDriver.protocol === 'esc' && getCurrentPrinterSummary().type === 'builtin'
                ? escBuiltinLongWatchMs
                : (genericRasterJobMs + 30000)
            const rasterGlobalCapMs = watchdogMs + 120000
            globalWatchdog = setTimeout(() => {
              timeoutHandledByWatchdog = true
              try { uni.hideLoading() } catch (_) {}
              isPrinting.value = false
              console.warn(
                '[preview] PRINT_TIMEOUT_GLOBAL raster',
                buildPrintDebugText({ stage: printStage, elapsedMs: Date.now() - startedAt, qty: printQty.value }),
              )
            }, rasterGlobalCapMs)
            watchdog = setTimeout(() => {
              timeoutHandledByWatchdog = true
              try { uni.hideLoading() } catch (_) {}
              isPrinting.value = false
              console.warn(
                '[preview] PRINT_TIMEOUT raster',
                buildPrintDebugText({
                  branch: 'raster',
                  stage: printStage,
                  elapsedMs: Date.now() - startedAt,
                  qty: printQty.value,
                  outW: layout.outW,
                  outH: layout.outH,
                  tmpPath: 'canvas-rgba',
                }),
              )
            }, watchdogMs)
            printStage = 'raster-print-start'
            showPrintInProgressDebugModal({
              jobId: printJobId,
              route: printRoute,
              stage: printStage,
            })
            if (rasterDriver.protocol === 'esc') {
              await printImageDataForCurrentPrinter(
                imageData,
                {
                  printQty: printQty.value,
                  clearTopRasterRows: 0,
                  targetWidthDots: layout.outW,
                  targetHeightDots: layout.outH,
                  useContentHeight: true,
                  cutBetweenCopies: true,
                },
                (percent) => {
                  if (!isPrintJobActive(printJobId)) return
                  showPrintProgress(percent)
                }
              )
            } else {
              await withTimeout(
                printImageDataForCurrentPrinter(
                  imageData,
                  {
                    printQty: printQty.value,
                    clearTopRasterRows: 1,
                    targetWidthDots: layout.outW,
                    targetHeightDots: layout.outH,
                    labelRasterFixedHeight: true,
                  },
                  (percent) => {
                    if (!isPrintJobActive(printJobId)) return
                    showPrintProgress(percent)
                  }
                ),
                genericRasterJobMs,
                'PRINT_TIMEOUT'
              )
            }
            printStage = 'raster-print-done'
            if (ENABLE_PRINT_RESULT_MODAL) await showPrintResultOnce()
            /* 内置已处理完本分支，跳过下方 PNG 光栅 */
            tmpPath = '__builtin_canvas_done__'
          } catch (e) {
            console.warn('[preview] builtin canvasGetImageData failed, fallback PNG raster', e)
            tmpPath = ''
          }
        }
        if (tmpPath === '__builtin_canvas_done__') {
          /* 内置已在上方完成光栅+下发 */
        } else {
          tmpPath = tmpPath || await renderLabelPreviewCanvasToTempPathForPrint(
            'labelPreviewCanvas',
            instance,
            tmpl,
            layout
          )
          printStage = 'raster-image-ready'

          showPrintProgress(0)
          const genericRasterJobMs = 300000
          const escBuiltinLongWatchMs = 900000
          const watchdogMs =
            rasterDriver.protocol === 'esc' && getCurrentPrinterSummary().type === 'builtin'
              ? escBuiltinLongWatchMs
              : (genericRasterJobMs + 30000)
          /** 须 ≥ 分支 watchdog，否则内置慢光栅会先被 6min 全局定时器误杀（日志里 PRINT_TIMEOUT_GLOBAL 与 raster 仍在跑并存） */
          const rasterGlobalCapMs = watchdogMs + 120000
          globalWatchdog = setTimeout(() => {
            timeoutHandledByWatchdog = true
            try { uni.hideLoading() } catch (_) {}
            isPrinting.value = false
            console.warn(
              '[preview] PRINT_TIMEOUT_GLOBAL raster',
              buildPrintDebugText({ stage: printStage, elapsedMs: Date.now() - startedAt, qty: printQty.value }),
            )
          }, rasterGlobalCapMs)
          watchdog = setTimeout(() => {
            timeoutHandledByWatchdog = true
            try { uni.hideLoading() } catch (_) {}
            isPrinting.value = false
            console.warn(
              '[preview] PRINT_TIMEOUT raster',
              buildPrintDebugText({
                branch: 'raster',
                stage: printStage,
                elapsedMs: Date.now() - startedAt,
                qty: printQty.value,
                outW: layout.outW,
                outH: layout.outH,
                tmpPath: tmpPath ? 'ok' : 'empty',
              }),
            )
          }, watchdogMs)
          /** 与历史验证路径一致：临时 PNG → 解码光栅 → 下发。 */
          printStage = 'raster-print-start'
          showPrintInProgressDebugModal({
            jobId: printJobId,
            route: printRoute,
            stage: printStage,
          })
          /** 以实际下发协议为准（内置+界面 TSC 时 resolveRasterPrintDriver 为 ESC，须走 ESC 分支与超时） */
          if (rasterDriver.protocol === 'esc') {
            await printImageForCurrentPrinter(
              tmpPath,
              {
                printQty: printQty.value,
                clearTopRasterRows: 0,
                targetWidthDots: layout.outW,
                targetHeightDots: layout.outH,
                useContentHeight: true,
                cutBetweenCopies: true,
              },
              (percent) => {
                if (!isPrintJobActive(printJobId)) return
                showPrintProgress(percent)
              }
            )
          } else {
            await withTimeout(printImageForCurrentPrinter(
              tmpPath,
              {
                printQty: printQty.value,
                clearTopRasterRows: 1,
                targetWidthDots: layout.outW,
                targetHeightDots: layout.outH,
                labelRasterFixedHeight: true,
              },
              (percent) => {
                if (!isPrintJobActive(printJobId)) return
                showPrintProgress(percent)
              }
            ), genericRasterJobMs, 'PRINT_TIMEOUT')
          }
          printStage = 'raster-print-done'
          if (ENABLE_PRINT_RESULT_MODAL) await showPrintResultOnce()
        }
      }
      }
    }

    /** 接口 9：仅本页业务标签出纸后落库（打印机设置/蓝牙 Test Print 不会执行此段） */
    let printLogSyncFailed = false
    if (!printLogAlreadySynced) {
      printLogRequestBody = null
    }
    if (!printLogAlreadySynced && isPrintJobActive(printJobId)) try {
      const bt = getBluetoothConnection()
      /**
       * 接口 9 的 `printInputJson`：整份合并模板快照（与出纸 labelPrintJobPayload.template 同源）+
       * buildPrintInputJson 的键值（PRINT_INPUT / 多选等），后端应原样落库并在接口 10 `renderTemplateJson` 返回供重打。
       */
      const persistTemplateDoc = JSON.parse(
        JSON.stringify(labelPrintJobPayload.template)
      ) as Record<string, unknown>
      const printInputSnapshotForApi: Record<string, unknown> = {
        ...printInputJson,
        ...persistTemplateDoc,
      }

      printLogRequestBody = buildUsAppLabelPrintRequestBody({
        locationId: getCurrentStoreId(),
        labelCode: labelCode.value,
        productId: productId.value || undefined,
        printQuantity: printQty.value,
        mergedTemplate: printInputSnapshotForApi,
        clientRequestId: printClientRequestId,
        printerMac: bt?.deviceId || undefined,
      })
      if (printLogRequestBody) {
        await postUsAppLabelPrint(printLogRequestBody)
      }
    } catch (syncErr: unknown) {
      if (String((syncErr as Error)?.message || '').includes('PRINT_JOB_STALE')) {
        /* 用户极快连点打印时新任务已顶替，避免误 Toast / 二次 Loading */
      } else if (!isUsAppSessionExpiredError(syncErr)) {
        printLogSyncFailed = true
        const msg = syncErr instanceof Error ? syncErr.message : String(syncErr)
        console.error('[preview] 打印落库接口失败', {
          接口说明: 'App 打印（落库打印任务与明细）',
          接口路径: US_APP_LABEL_PRINT_PATH,
          完整URL: buildApiUrl(US_APP_LABEL_PRINT_PATH),
          方法: 'POST',
          请求体: printLogRequestBody,
          错误信息: msg,
          原始错误: syncErr,
        })
      }
      /** 401 时 usAppApiRequest 已 Toast + 跳转，此处不再处理 */
    }

    if (isPrintJobActive(printJobId)) {
      const sz = getPreviewCanvasCssSize(tmpl, 720)
      canvasCssW.value = sz.width
      canvasCssH.value = sz.height
      await waitForCanvasLayout()
      try {
        const path = await renderLabelPreviewToTempPath('labelPreviewCanvas', instance, tmpl, 720)
        previewImageSrc.value = path
      } catch {
        /* 保持上一张预览图 */
      }
    }

    uni.hideLoading()
    if (isPrintJobActive(printJobId)) {
      const qty = printQty.value
      const printedTitle = `${qty} label${qty > 1 ? 's' : ''} printed!`
      uni.showToast({
        title: printLogSyncFailed ? `${printedTitle} (log not saved)` : printedTitle,
        icon: printLogSyncFailed ? 'none' : 'success',
        duration: printLogSyncFailed ? 2800 : 2000,
      })
      if (ENABLE_AUTO_NATIVE_DEBUG_MODAL && !printResultShown && shouldShowNativeFastDebugModal()) {
        setTimeout(() => {
          void showNativeFastPrinterDebugModal()
        }, 450)
      }
    }
  } catch (e: any) {
    if (!isPrintJobActive(printJobId)) return
    uni.hideLoading()
    const msg = e?.message ? String(e.message) : 'Print failed'
    /** withTimeout 触发的超时：不弹窗，仅日志（watchdog 已静默结束打印态） */
    if (msg.includes('PRINT_TIMEOUT')) {
      console.warn(
        '[preview] print job timeout (promise)',
        msg,
        buildPrintDebugText({
          stage: printStage,
          route: printRoute,
          jobId: printJobId,
          elapsedMs: Date.now() - startedAt,
          qty: printQty.value,
        })
      )
      if (ENABLE_PRINT_RESULT_MODAL) {
        await showPrintResultOnce()
      } else {
        uni.showToast({
          title: 'Printing is taking longer, please wait…',
          icon: 'none',
          duration: 2500,
        })
      }
      return
    }
    if (timeoutHandledByWatchdog) return
    if (msg === 'BUILTIN_PLUGIN_NOT_FOUND' || (msg && msg.indexOf('Built-in printer') !== -1)) {
      uni.showModal({
        title: 'Built-in Print Not Available',
        content:
          'This device does not support TCP built-in printing. Please use Bluetooth: Printer Settings → connect your printer.',
        confirmText: 'Printer Settings',
        cancelText: 'Cancel',
        success: (res) => {
          if (res.confirm) uni.navigateTo({ url: '/pages/labels/bluetooth' })
          if (shouldShowNativeFastDebugModal()) {
            void showNativeFastPrinterDebugModal()
          }
        },
      })
    } else {
      uni.showModal({
        title: 'Print Failed',
        content: `${msg}\n\n${buildPrintDebugText({
          stage: printStage,
          route: printRoute,
          jobId: printJobId,
          elapsedMs: Date.now() - startedAt,
          qty: printQty.value,
        })}`.trim(),
        showCancel: false,
        success: () => {
          if (shouldShowNativeFastDebugModal()) {
            void showNativeFastPrinterDebugModal()
          }
        },
      })
    }
  } finally {
    if (!isPrintJobActive(printJobId)) return
    try {
      uni.hideLoading()
    } catch (_) {}
    if (watchdog) clearTimeout(watchdog)
    if (globalWatchdog) clearTimeout(globalWatchdog)
    const t = systemTemplate.value
    if (t && instance) {
      const sz = getPreviewCanvasCssSize(t, 720)
      canvasCssW.value = sz.width
      canvasCssH.value = sz.height
    }
    if (!timeoutHandledByWatchdog) {
      isPrinting.value = false
    }
  }
}
</script>

<style scoped>
.page {
  min-height: 100vh;
  background: #f9fafb;
  overflow-x: hidden;
  overflow-x: clip;
  width: 100%;
  max-width: 100vw;
  box-sizing: border-box;
}

.header-hero {
  background: linear-gradient(135deg, var(--theme-primary), var(--theme-primary-dark));
  padding: 16rpx 32rpx 24rpx;
}

.top-bar {
  height: 96rpx;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.top-left,
.top-right {
  width: 64rpx;
  height: 64rpx;
  border-radius: 999rpx;
  background: rgba(255, 255, 255, 0.15);
  display: flex;
  align-items: center;
  justify-content: center;
}

.top-center {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.page-title {
  font-size: 34rpx;
  font-weight: 600;
  color: #fff;
  display: block;
}

.content {
  padding: 32rpx;
  padding-bottom: 260rpx;
  box-sizing: border-box;
  overflow-x: hidden;
  overflow-x: clip;
  width: 100%;
  max-width: 100%;
  min-width: 0;
}

.state-block {
  padding: 80rpx 24rpx;
  text-align: center;
}

.state-text {
  font-size: 28rpx;
  color: #6b7280;
}

.qty-control,
.bottom-qty-control {
  display: flex;
  align-items: center;
  flex-shrink: 0;
}

.bottom-qty-control {
  gap: 10rpx;
}

.qty-btn {
  width: 64rpx;
  height: 64rpx;
  border-radius: 14rpx;
  background: #f3f4f6;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.bottom-qty-control .qty-btn {
  width: 72rpx;
  height: 72rpx;
  border-radius: 50%;
}

.bottom-qty-control .qty-btn-minus {
  background: #e5e7eb;
}

.bottom-qty-control .qty-btn-plus {
  background: #111827;
}

.qty-btn:active {
  opacity: 0.85;
}

.qty-btn.disabled {
  opacity: 0.35;
}

.qty-value {
  width: 88rpx;
  text-align: center;
  font-size: 36rpx;
  font-weight: 700;
  color: #111827;
}

.bottom-qty-control .qty-value {
  width: 56rpx;
  font-size: 34rpx;
  font-variant-numeric: tabular-nums;
}

.print-options-card {
  background: #fff;
  border-radius: 20rpx;
  padding: 28rpx;
  margin-bottom: 28rpx;
  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
}

.print-options-title {
  font-size: 30rpx;
  font-weight: 600;
  color: #111827;
  display: block;
  margin-bottom: 12rpx;
}

.print-options-hint {
  font-size: 24rpx;
  color: #6b7280;
  line-height: 1.45;
  display: block;
  margin-bottom: 24rpx;
}

.print-option-block {
  margin-bottom: 28rpx;
}

.print-option-block:last-child {
  margin-bottom: 0;
}

.print-option-label {
  font-size: 26rpx;
  font-weight: 600;
  color: #374151;
  display: block;
  margin-bottom: 16rpx;
}

.chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: 22rpx;
}

.chip {
  padding: 12rpx 24rpx;
  border-radius: 999rpx;
  background: #f3f4f6;
  border: 2rpx solid #e5e7eb;
}

.chip-on {
  background: #eff6ff;
  border-color: var(--theme-primary);
}

.chip-text {
  font-size: 24rpx;
  color: #374151;
}

.chip-on .chip-text {
  color: var(--theme-primary);
  font-weight: 600;
}

.free-field-input {
  width: 100%;
  box-sizing: border-box;
  height: 80rpx;
  padding: 0 24rpx;
  font-size: 28rpx;
  color: #111827;
  background: #f9fafb;
  border: 2rpx solid #e5e7eb;
  border-radius: 12rpx;
}

.picker-input {
  display: flex;
  align-items: center;
}

.picker-dialog-mask {
  position: fixed;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.45);
  z-index: 2000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24rpx;
}

.picker-dialog {
  width: 100%;
  max-width: 680rpx;
  background: #fff;
  border-radius: 20rpx;
  overflow: hidden;
}

.picker-dialog--wide {
  max-width: 720rpx;
}

.picker-dialog-title {
  font-size: 30rpx;
  font-weight: 600;
  color: #111827;
  text-align: center;
  padding: 20rpx 24rpx 8rpx;
}

.picker-view-box {
  width: 100%;
  height: 420rpx;
}

.picker-view-box--wide .picker-item {
  font-size: 24rpx;
  padding: 0 6rpx;
}

.picker-item {
  height: 72rpx;
  min-height: 72rpx;
  max-height: 72rpx;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  text-align: center;
  font-size: 30rpx;
  font-variant-numeric: tabular-nums;
  color: #111827;
}

.picker-dialog-actions {
  display: flex;
  border-top: 1rpx solid #e5e7eb;
}

.picker-btn {
  flex: 1;
  height: 88rpx;
  line-height: 88rpx;
  text-align: center;
  font-size: 30rpx;
}

.picker-btn-cancel {
  color: #6b7280;
  border-right: 1rpx solid #e5e7eb;
}

.picker-btn-confirm {
  color: var(--theme-primary);
  font-weight: 600;
}

.section-title {
  font-size: 30rpx;
  font-weight: 600;
  color: #111827;
  display: block;
  margin-bottom: 20rpx;
}

.section-title-first {
  margin-top: 0;
}

.meta-footer-scroll {
  width: 100%;
  margin-bottom: 24rpx;
  white-space: nowrap;
}

.meta-footer-row {
  display: inline-flex;
  flex-direction: row;
  align-items: stretch;
  gap: 10rpx;
  padding-bottom: 4rpx;
}

.meta-item {
  flex: 0 0 auto;
  min-width: 168rpx;
  max-width: 280rpx;
  background: #fff;
  padding: 16rpx 20rpx;
  border-radius: 16rpx;
  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
  box-sizing: border-box;
}

.meta-item .info-value {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.label-card {
  background: #fff;
  border-radius: 20rpx;
  overflow: hidden;
  overflow-x: clip;
  margin-bottom: 24rpx;
  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
  padding: 24rpx 0;
  display: block;
  width: 100%;
  max-width: 100%;
  position: relative;
}

.label-img-wrap {
  width: 100%;
  max-width: 100%;
  overflow: hidden;
  overflow-x: clip;
  box-sizing: border-box;
  position: relative;
}

.label-img {
  width: 100%;
  max-width: 100%;
  height: auto;
  display: block;
  vertical-align: top;
  object-fit: contain;
  border-radius: 12rpx;
  box-sizing: border-box;
}

.label-placeholder {
  padding: 80rpx 24rpx;
  text-align: center;
  background: #f3f4f6;
  border-radius: 12rpx;
}

.label-placeholder-text {
  font-size: 26rpx;
  color: #9ca3af;
}

.info-label {
  font-size: 22rpx;
  color: #9ca3af;
  display: block;
  margin-bottom: 6rpx;
}

.info-value {
  font-size: 26rpx;
  font-weight: 600;
  color: #111827;
  display: block;
}

.note-card {
  display: flex;
  align-items: flex-start;
  gap: 16rpx;
  padding: 24rpx 28rpx;
  background: #eff6ff;
  border: 1rpx solid #bfdbfe;
  border-radius: 16rpx;
}

.note-text {
  flex: 1;
  font-size: 26rpx;
  color: #1e40af;
  line-height: 1.5;
}

.bottom-bar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  padding: 20rpx 32rpx;
  padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 36px);
  background: #fff;
  border-top: 1rpx solid #e5e7eb;
  box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04);
}

.bottom-info {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 16rpx;
}

.bottom-qty {
  font-size: 26rpx;
  font-weight: 600;
  color: #111827;
}

.bt-indicator {
  display: flex;
  align-items: center;
  gap: 8rpx;
}

.bt-dot {
  width: 12rpx;
  height: 12rpx;
  border-radius: 50%;
  background: #d1d5db;
}

.bt-indicator.connected .bt-dot {
  background: #4ade80;
}

.bt-name {
  font-size: 24rpx;
  color: #9ca3af;
}

.bt-indicator.connected .bt-name {
  color: #16a34a;
}

.bottom-actions {
  display: flex;
  align-items: center;
  gap: 16rpx;
}

.btn-preview-sq {
  width: 88rpx;
  height: 88rpx;
  flex-shrink: 0;
  background: #fff;
  border: 2rpx solid #d1d5db;
  border-radius: 16rpx;
  display: flex;
  align-items: center;
  justify-content: center;
}

.btn-preview-sq:active {
  background: #f3f4f6;
}

.print-btn {
  flex: 1;
  min-width: 0;
  height: 88rpx;
  background: var(--theme-primary);
  border-radius: 16rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12rpx;
}

.print-btn.disabled {
  opacity: 0.6;
}

.print-btn-text {
  font-size: 30rpx;
  font-weight: 600;
  color: #fff;
  line-height: 1;
}

.hidden-canvas {
  position: fixed;
  left: -9999px;
  top: 0;
  opacity: 0;
  pointer-events: none;
}

.modal-mask {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 999;
  padding: 24rpx;
}

.modal-body {
  width: 100%;
  max-width: 700rpx;
  max-height: 85vh;
  background: #fff;
  border-radius: 24rpx;
  overflow-x: hidden;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  min-width: 0;
  -webkit-overflow-scrolling: touch;
}

.modal-body-label-only .modal-label-wrap {
  margin-bottom: 0;
  padding: 24rpx;
}

.modal-label-id {
  margin-top: 20rpx;
  padding-top: 20rpx;
  border-top: 1rpx solid #e5e7eb;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16rpx;
}

.modal-label-id-label {
  font-size: 26rpx;
  color: #6b7280;
  font-weight: 500;
}

.modal-label-id-value {
  font-size: 28rpx;
  color: #111827;
  font-weight: 600;
}

.modal-label-wrap {
  width: 100%;
  max-width: 100%;
  margin-bottom: 24rpx;
  padding: 0;
  box-sizing: border-box;
  overflow: visible;
}

.modal-label-inner {
  width: 100%;
  max-width: 100%;
  min-width: 0;
  overflow: hidden;
  overflow-x: clip;
  box-sizing: border-box;
}

.modal-label-img {
  width: 100%;
  max-width: 100%;
  height: auto;
  display: block;
  vertical-align: top;
  object-fit: contain;
  border-radius: 12rpx;
  box-sizing: border-box;
}

@media screen and (max-width: 768px) {
  .page,
  .content,
  .label-card,
  .label-img-wrap {
    max-width: 100vw;
  }
  .label-img,
  .modal-label-img {
    max-width: 100% !important;
  }
}
</style>
