<template>
  <div ref="propertiesPanelRootRef" class="properties-panel">
    <div class="properties-panel__header">
      <span class="properties-panel__title">设计栏</span>
      <div class="properties-panel__header-actions">
        <button type="button" class="properties-panel__icon-btn" title="全部折叠" aria-label="全部折叠" @click="collapseAllSections">
          <svg class="properties-panel__icon-svg" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
            <path fill="currentColor" d="M4 5h16v2H4V5zm0 6h16v2H4v-2zm0 6h9v2H4v-2z" />
          </svg>
        </button>
        <button type="button" class="properties-panel__icon-btn" title="全部展开" aria-label="全部展开" @click="expandAllSections">
          <svg class="properties-panel__icon-svg" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
            <path fill="currentColor" d="M4 5h16v2H4V5zm0 6h16v2H4v-2zm0 6h16v2H4v-2z" />
          </svg>
        </button>
      </div>
    </div>
    <div class="properties-panel__body">
      <template v-if="selectedElement">
      <details class="section-card" open>
        <summary class="panel-title">元素属性</summary>
        <a-space direction="vertical" style="width: 100%">
          <a-input :value="selectedElement.type" addon-before="类型" disabled />
          <div
            v-if="selectedElement.type !== 'table' && selectedElement.type !== 'detailTable' && selectedElement.type !== 'freeTable'"
            class="bind-param-compact"
          >
            <span class="bind-param-compact__addon">绑定参数</span>
            <a-select
              :value="resolveParamBindSelectValue(selectedElement.bindField)"
              :options="bindingParamOptions"
              allow-clear
              show-search
              option-filter-prop="label"
              placeholder="请先在左侧「参数」页维护"
              class="bind-param-compact__select"
              @update:value="handleElementBindParamChange"
            />
          </div>
          <a-input-number v-if="!isReportBand(selectedElement.type)" :value="selectedElement.x" addon-before="X(mm)" style="width: 100%" @update:value="updateRect('x', $event)" />
          <a-input-number v-if="!isReportBand(selectedElement.type)" :value="selectedElement.y" addon-before="Y(mm)" style="width: 100%" @update:value="updateRect('y', $event)" />
          <a-input-number v-if="!isReportBand(selectedElement.type)" :value="selectedElement.w" addon-before="宽(mm)" style="width: 100%" :min="6" @update:value="updateRect('w', $event)" />
          <a-input-number v-if="!isReportBand(selectedElement.type)" :value="selectedElement.h" addon-before="高(mm)" style="width: 100%" :min="6" @update:value="updateRect('h', $event)" />
          <a-input-number
            :value="selectedElement.style?.fontSize || 12"
            addon-before="字体(px)"
            :min="8"
            :max="72"
            style="width: 100%"
            @update:value="updateStyle('fontSize', $event)"
          />
          <template v-if="showPlainTextStyleColors">
            <div class="color-input-row">
              <a-input
                :value="selectedElement.style?.color || '#111111'"
                addon-before="文字色"
                placeholder="#111111 / rgb(...)"
                @update:value="updateStyle('color', $event)"
              />
              <input
                type="color"
                class="native-color-picker-trigger"
                title="选择文字颜色"
                :value="toColorHex(String(selectedElement.style?.color || '#111111'))"
                @input="handleStyleColorInput('color', $event, '#111111')"
              />
            </div>
            <div class="color-input-row">
              <a-input
                :value="String(selectedElement.style?.backgroundColor ?? 'transparent')"
                addon-before="背景色"
                placeholder="transparent / #ffffff"
                @update:value="updateStyle('backgroundColor', $event)"
              />
              <input
                type="color"
                class="native-color-picker-trigger"
                title="选择背景颜色"
                :value="toColorHex(plainTextBackgroundForPicker)"
                @input="handlePlainTextBackgroundColorInput($event)"
              />
            </div>
          </template>
        </a-space>
      </details>
      <template v-if="isText(selectedElement.type)">
        <a-divider />
        <a-input :value="(selectedElement as any).text" addon-before="内容" @update:value="updateField('text', $event)" />
      </template>
      <template v-if="isReportBand(selectedElement.type)">
        <a-divider />
        <div class="panel-subtitle">布局</div>
        <a-space direction="vertical" style="width: 100%">
          <a-input-number :value="selectedElement.h" addon-before="高度" :min="4" :max="200" style="width: 100%" @update:value="updateRect('h', $event)" />
        </a-space>
        <div class="panel-subtitle" style="margin-top: 8px">外观</div>
        <div class="color-input-row">
          <a-input
            :value="selectedElement.style?.backgroundColor || '#ffffff'"
            addon-before="背景色"
            placeholder="#ffffff / rgb(...)"
            @update:value="updateStyle('backgroundColor', $event)"
          />
          <input
            type="color"
            class="native-color-picker-trigger"
            title="选择背景颜色"
            :value="toColorHex(selectedElement.style?.backgroundColor || '#ffffff')"
            @input="handleStyleColorInput('backgroundColor', $event, '#ffffff')"
          />
        </div>
        <div class="color-input-row">
          <a-input
            :value="selectedElement.style?.color || '#111111'"
            addon-before="文字色"
            placeholder="#111111 / rgb(...)"
            @update:value="updateStyle('color', $event)"
          />
          <input
            type="color"
            class="native-color-picker-trigger"
            title="选择文字颜色"
            :value="toColorHex(String(selectedElement.style?.color || '#111111'))"
            @input="handleStyleColorInput('color', $event, '#111111')"
          />
        </div>
        <div class="panel-subtitle" style="margin-top: 8px">数据</div>
        <a-space direction="vertical" style="width: 100%">
          <a-input :value="(selectedElement as any).bookmarkText || ''" addon-before="书签文本" @update:value="updateField('bookmarkText', $event)" />
        </a-space>
        <div class="panel-subtitle" style="margin-top: 8px">行为</div>
        <a-space direction="vertical" style="width: 100%">
          <a-switch :checked="(selectedElement as any).keepTogether !== false" checked-children="保持同页" un-checked-children="允许拆分" @update:checked="updateField('keepTogether', $event)" />
          <a-switch :checked="(selectedElement as any).centerWithDetail !== false" checked-children="跟随明细居中" un-checked-children="不居中" @update:checked="updateField('centerWithDetail', $event)" />
          <a-select
            :value="(selectedElement as any).refreshPage || 'none'"
            :options="REFRESH_PAGE_OPTIONS"
            style="width: 100%"
            @update:value="updateField('refreshPage', $event)"
          />
          <a-switch :checked="(selectedElement as any).visible !== false" checked-children="可见" un-checked-children="隐藏" @update:checked="updateField('visible', $event)" />
          <a-switch :checked="(selectedElement as any).stretch === true" checked-children="可伸展" un-checked-children="不可伸展" @update:checked="updateField('stretch', $event)" />
          <a-switch :checked="(selectedElement as any).shrink === true" checked-children="可收缩" un-checked-children="不可收缩" @update:checked="updateField('shrink', $event)" />
          <a-switch :checked="(selectedElement as any).printRepeated === true" checked-children="每页重复打印" un-checked-children="不重复" @update:checked="updateField('printRepeated', $event)" />
          <a-switch
            v-if="isReportFooter(selectedElement.type)"
            :checked="(selectedElement as any).printAtPageBottom === true"
            checked-children="打印在页底"
            un-checked-children="常规位置"
            @update:checked="updateField('printAtPageBottom', $event)"
          />
          <a-switch
            v-if="isReportFooter(selectedElement.type)"
            :checked="(selectedElement as any).removeBlankWhenNoData === true"
            checked-children="空白行排除"
            un-checked-children="保留空白"
            @update:checked="updateField('removeBlankWhenNoData', $event)"
          />
        </a-space>
      </template>
      <template v-if="selectedElement.type === 'image'">
        <a-divider />
        <a-input :value="(selectedElement as any).src" addon-before="图片URL" @update:value="updateField('src', $event)" />
      </template>
      <template v-if="selectedElement.type === 'qrcode' || selectedElement.type === 'barcode'">
        <a-divider />
        <a-input :value="(selectedElement as any).value" addon-before="编码值" @update:value="updateField('value', $event)" />
      </template>
      <template v-if="selectedElement.type === 'table' || selectedElement.type === 'detailTable'">
        <a-divider />
        <details class="section-card" open>
          <summary class="panel-subtitle">表格配置</summary>
          <a-space direction="vertical" style="width: 100%">
            <div class="bind-param-compact">
              <span class="bind-param-compact__addon">数据源</span>
              <a-select
                :value="resolveTableSourceSelectValue((selectedElement as any).source)"
                :options="detailTableSourceOptions"
                allow-clear
                show-search
                option-filter-prop="label"
                placeholder="请选择左侧「字段」页登记的明细数据源"
                class="bind-param-compact__select"
                @update:value="handleTableSourceChange"
              />
            </div>
            <a-select
              :value="(selectedElement as any).tableHeightMode || 'autoPage'"
              style="width: 100%"
              :options="TABLE_HEIGHT_MODE_OPTIONS"
              @update:value="updateField('tableHeightMode', $event)"
            />
            <a-input-number
              v-if="((selectedElement as any).tableHeightMode || 'autoPage') === 'fixedRows'"
              :value="(selectedElement as any).fixedRows || 5"
              addon-before="分页行数"
              :min="1"
              :max="500"
              style="width: 100%"
              @update:value="updateField('fixedRows', Number($event || 5))"
            />
          </a-space>
        </details>
        <details class="section-card" open>
          <summary class="panel-subtitle">分组合并</summary>
          <a-space direction="vertical" style="width: 100%">
            <a-select
              mode="multiple"
              :value="(selectedElement as any).mergeColumnKeys || []"
              :options="mergeColumnOptions"
              placeholder="选择分组列（顺序决定优先级）"
              style="width: 100%"
              @update:value="updateMergeColumns($event)"
            />
            <a-switch
              :checked="(selectedElement as any).strictGrouping !== false"
              checked-children="强制分组"
              un-checked-children="宽松分组"
              @update:checked="updateField('strictGrouping', $event)"
            />
          </a-space>
        </details>
        <details class="section-card" open>
          <summary class="panel-subtitle">表头</summary>
          <a-space direction="vertical" style="width: 100%">
          <a-switch
            :checked="(selectedElement as any).showHeader"
            checked-children="显示表头"
            un-checked-children="隐藏表头"
            @update:checked="updateField('showHeader', $event)"
          />
          <a-input-number :value="(selectedElement as any).headerHeight" addon-before="表头高(mm)" style="width: 100%" @update:value="updateField('headerHeight', $event)" />
          <a-input-number :value="(selectedElement as any).headerFontSize || 12" addon-before="表头字号(px)" :min="8" :max="72" style="width: 100%" @update:value="updateField('headerFontSize', Number($event || 12))" />
          <div class="color-input-row">
            <a-input
              :value="(selectedElement as any).headerBgColor || '#f5f5f5'"
              addon-before="表头背景"
              placeholder="#f5f5f5 / rgb(...)"
              @update:value="updateField('headerBgColor', $event)"
            />
            <input
              type="color"
              class="native-color-picker-trigger"
              title="选择表头背景色"
              :value="toColorHex((selectedElement as any).headerBgColor || '#f5f5f5')"
              @input="handleColorInput('headerBgColor', $event, '#f5f5f5')"
            />
          </div>
          <div class="color-input-row">
            <a-input
              :value="(selectedElement as any).headerTextColor || '#111111'"
              addon-before="表头文字"
              placeholder="#111111 / rgb(...)"
              @update:value="updateField('headerTextColor', $event)"
            />
            <input
              type="color"
              class="native-color-picker-trigger"
              title="选择表头文字色"
              :value="toColorHex((selectedElement as any).headerTextColor || '#111111')"
              @input="handleColorInput('headerTextColor', $event, '#111111')"
            />
          </div>
          </a-space>
        </details>
        <details class="section-card" open>
          <summary class="panel-subtitle">表体</summary>
          <a-space direction="vertical" style="width: 100%">
          <a-input-number :value="(selectedElement as any).rowHeight" addon-before="表体行高(mm)" style="width: 100%" @update:value="updateField('rowHeight', $event)" />
          <a-input-number :value="(selectedElement as any).bodyFontSize || 12" addon-before="表体字号(px)" :min="8" :max="72" style="width: 100%" @update:value="updateField('bodyFontSize', Number($event || 12))" />
          <a-button type="primary" block class="column-width-entry-btn" @click="columnWidthModalOpen = true">打开绑定字段列宽设置</a-button>
          </a-space>
        </details>
        <details class="section-card" open>
          <summary class="panel-subtitle">底部</summary>
          <a-space direction="vertical" style="width: 100%">
            <a-switch
              :checked="(selectedElement as any).footerShowTotal !== false"
              checked-children="显示底部合计"
              un-checked-children="隐藏底部合计"
              @update:checked="updateField('footerShowTotal', $event)"
            />
          <a-select
            :value="(selectedElement as any).footerTotalMode || 'overall'"
            style="width: 100%"
            :options="FOOTER_TOTAL_MODE_OPTIONS"
            @update:value="updateField('footerTotalMode', $event)"
          />
          <a-select
            :value="resolveFooterLabelColumnKey()"
            style="width: 100%"
            :options="footerLabelColumnOptions"
            @update:value="updateField('footerLabelColumnKey', $event)"
          />
          <a-input
            :value="(selectedElement as any).footerLabelText || '合计'"
            addon-before="底部文字"
            @update:value="updateField('footerLabelText', $event)"
          />
            <a-switch
              :checked="(selectedElement as any).footerLabelCenter !== false"
              checked-children="底部居中"
              un-checked-children="底部左对齐"
              @update:checked="updateField('footerLabelCenter', $event)"
            />
          </a-space>
        </details>
        <details class="section-card" open>
          <summary class="panel-subtitle">多级表头</summary>
          <a-space direction="vertical" style="width: 100%">
          <a-switch
            :checked="(selectedElement as any).enableMultiHeader === true"
            checked-children="已开启"
            un-checked-children="未开启"
            @update:checked="handleMultiHeaderToggle($event)"
          />
            <a-button type="primary" ghost :disabled="(selectedElement as any).enableMultiHeader !== true" @click="headerConfigModalOpen = true">
              打开多级表头设置
            </a-button>
          </a-space>
        </details>
        <a-modal
          v-model:open="headerConfigModalOpen"
          title="多级表头设置"
          :width="900"
          :footer="null"
          destroy-on-close
        >
          <TableHeaderConfigEditor
            :row-count="resolveHeaderConfigRowCount()"
            :col-count="tableColumns.length || 1"
            :column-titles="tableColumns.map((item) => item?.title || item?.key || '')"
            :value="(selectedElement as any).headerConfig"
            @update:value="handleHeaderConfigChange($event)"
          />
        </a-modal>
        <a-modal
          v-model:open="columnWidthModalOpen"
          title="绑定字段列宽设置"
          :width="760"
          :footer="null"
          destroy-on-close
        >
          <div class="column-width-modal">
            <div class="column-width-modal-tip">按绑定字段分别设置列宽，修改后会实时同步到画布。</div>
            <div class="column-width-list">
              <div v-for="(col, idx) in tableColumns" :key="`width_${col.key}`" class="column-width-item">
                <div class="column-width-item-head">
                  <span class="column-width-index">列{{ idx + 1 }}</span>
                  <span class="column-width-title">{{ col.title || col.key }}</span>
                </div>
                <a-input :value="col.bindField || col.field || col.key" addon-before="字段" disabled />
                <a-input-number
                  :value="col.width"
                  addon-before="列宽"
                  addon-after="mm"
                  :min="10"
                  :max="500"
                  style="width: 100%"
                  @update:value="updateTableColumn(idx, 'width', Number($event || 10))"
                />
              </div>
            </div>
            <div class="column-width-modal-footer">
              <a-button @click="columnWidthModalOpen = false">关闭</a-button>
            </div>
          </div>
        </a-modal>
        <details class="section-card" open>
          <summary class="panel-subtitle">当前列属性（双击表头选择列）</summary>
          <div v-if="selectedTableColumnInfo" class="table-column-list">
          <div class="table-column-item">
            <a-space direction="vertical" style="width: 100%" size="small">
              <a-input
                :value="selectedTableColumnInfo.col.title"
                addon-before="列标题"
                @update:value="updateSelectedTableColumn('title', $event)"
              />
              <div class="bind-param-compact">
                <span class="bind-param-compact__addon">绑定字段</span>
                <a-select
                  :value="resolveDetailFieldBindSelectValue(selectedTableColumnInfo.col.bindField)"
                  :options="detailFieldBindOptionsForColumn"
                  allow-clear
                  show-search
                  option-filter-prop="label"
                  placeholder="请选择当前数据源下的字段"
                  class="bind-param-compact__select"
                  @update:value="handleTableColumnBindDetailFieldChange"
                />
              </div>
              <a-input-number
                :value="selectedTableColumnInfo.col.width"
                addon-before="列宽"
                :min="10"
                style="width: 100%"
                @update:value="updateSelectedTableColumn('width', Number($event || 10))"
              />
              <a-select
                :value="selectedTableColumnInfo.col.contentType || 'text'"
                style="width: 100%"
                :options="TABLE_COLUMN_CONTENT_TYPE_OPTIONS"
                @update:value="handleColumnContentTypeChange($event)"
              />
              <template v-if="(selectedTableColumnInfo.col.contentType || 'text') === 'text'">
                <a-select
                  :value="selectedTableColumnInfo.col.fontFamily || ''"
                  style="width: 100%"
                  :options="TABLE_FONT_OPTIONS"
                  @update:value="updateSelectedTableColumn('fontFamily', $event)"
                />
                <a-switch
                  :checked="!!selectedTableColumnInfo.col.useCustomFontSize"
                  checked-children="单列字号"
                  un-checked-children="跟随表体字号"
                  @update:checked="updateSelectedTableColumn('useCustomFontSize', $event)"
                />
                <a-input-number
                  :value="selectedTableColumnInfo.col.fontSize || 12"
                  addon-before="字体大小"
                  :min="8"
                  :max="72"
                  style="width: 100%"
                  :disabled="!selectedTableColumnInfo.col.useCustomFontSize"
                  @update:value="updateSelectedTableColumn('fontSize', Number($event || 12))"
                />
                <div class="color-input-row">
                  <a-input
                    :value="selectedTableColumnInfo.col.fontColor || '#111111'"
                    addon-before="字体颜色"
                    placeholder="#111111 / rgb(...)"
                    @update:value="updateSelectedTableColumn('fontColor', $event)"
                  />
                  <input
                    type="color"
                    class="native-color-picker-trigger"
                    title="选择字体颜色"
                    :value="toColorHex(selectedTableColumnInfo.col.fontColor || '#111111')"
                    @input="handleColumnColorInput($event)"
                  />
                </div>
              </template>
              <template v-else-if="(selectedTableColumnInfo.col.contentType || 'text') === 'image'">
                <a-select
                  :value="selectedTableColumnInfo.col.imageFit || 'contain'"
                  style="width: 100%"
                  :options="IMAGE_FIT_OPTIONS"
                  @update:value="updateSelectedTableColumn('imageFit', $event)"
                />
                <a-input-number
                  :value="selectedTableColumnInfo.col.contentScale || 100"
                  addon-before="图片缩放(%)"
                  :min="10"
                  :max="100"
                  style="width: 100%"
                  @update:value="updateSelectedTableColumn('contentScale', Number($event || 100))"
                />
                <a-switch
                  :checked="selectedTableColumnInfo.col.fillCell !== false"
                  checked-children="填满单元格"
                  un-checked-children="按缩放显示"
                  @update:checked="updateSelectedTableColumn('fillCell', $event)"
                />
              </template>
              <template v-else-if="(selectedTableColumnInfo.col.contentType || 'text') === 'qrcode'">
                <a-select
                  :value="selectedTableColumnInfo.col.qrLevel || 'M'"
                  style="width: 100%"
                  :options="QRCODE_LEVEL_OPTIONS"
                  @update:value="updateSelectedTableColumn('qrLevel', $event)"
                />
                <a-select
                  :value="selectedTableColumnInfo.col.qrRenderType || 'image/png'"
                  style="width: 100%"
                  :options="QRCODE_RENDER_OPTIONS"
                  @update:value="updateSelectedTableColumn('qrRenderType', $event)"
                />
                <a-input-number
                  :value="selectedTableColumnInfo.col.contentScale || 100"
                  addon-before="二维码缩放(%)"
                  :min="10"
                  :max="100"
                  style="width: 100%"
                  @update:value="updateSelectedTableColumn('contentScale', Number($event || 100))"
                />
                <a-switch
                  :checked="selectedTableColumnInfo.col.fillCell !== false"
                  checked-children="填满单元格"
                  un-checked-children="按缩放显示"
                  @update:checked="updateSelectedTableColumn('fillCell', $event)"
                />
              </template>
              <template v-else-if="(selectedTableColumnInfo.col.contentType || 'text') === 'barcode'">
                <a-select
                  :value="selectedTableColumnInfo.col.barcodeFormat || 'CODE128'"
                  style="width: 100%"
                  :options="BARCODE_FORMAT_OPTIONS"
                  @update:value="updateSelectedTableColumn('barcodeFormat', $event)"
                />
                <a-input-number
                  :value="selectedTableColumnInfo.col.contentScale || 100"
                  addon-before="条码缩放(%)"
                  :min="10"
                  :max="100"
                  style="width: 100%"
                  @update:value="updateSelectedTableColumn('contentScale', Number($event || 100))"
                />
                <a-switch
                  :checked="selectedTableColumnInfo.col.fillCell !== false"
                  checked-children="填满单元格"
                  un-checked-children="按缩放显示"
                  @update:checked="updateSelectedTableColumn('fillCell', $event)"
                />
              </template>
              <template v-else-if="(selectedTableColumnInfo.col.contentType || 'text') === 'number' || (selectedTableColumnInfo.col.contentType || 'text') === 'amount'">
                <a-input-number
                  :value="selectedTableColumnInfo.col.decimalPlaces ?? 2"
                  addon-before="小数位数"
                  :min="0"
                  :max="6"
                  style="width: 100%"
                  @update:value="updateSelectedTableColumn('decimalPlaces', Number($event ?? 2))"
                />
                <a-switch
                  :checked="selectedTableColumnInfo.col.roundHalfUp !== false"
                  checked-children="四舍五入"
                  un-checked-children="截断"
                  @update:checked="updateSelectedTableColumn('roundHalfUp', $event)"
                />
                <a-select
                  v-if="(selectedTableColumnInfo.col.contentType || 'text') === 'amount'"
                  :value="selectedTableColumnInfo.col.amountType || 'CNY'"
                  style="width: 100%"
                  :options="AMOUNT_TYPE_OPTIONS"
                  @update:value="updateSelectedTableColumn('amountType', $event)"
                />
                <a-switch
                  :checked="!!selectedTableColumnInfo.col.enableFooterTotal"
                  checked-children="参与底部合计"
                  un-checked-children="不参与合计"
                  @update:checked="updateSelectedTableColumn('enableFooterTotal', $event)"
                />
              </template>
              <a-select
                :value="selectedTableColumnInfo.col.align || 'left'"
                style="width: 100%"
                @update:value="updateSelectedTableColumn('align', $event)"
              >
                <a-select-option value="left">左对齐</a-select-option>
                <a-select-option value="center">居中</a-select-option>
                <a-select-option value="right">右对齐</a-select-option>
              </a-select>
              <a-space v-if="(selectedTableColumnInfo.col.contentType || 'text') === 'text'">
                <a-switch
                  :checked="selectedTableColumnInfo.col.autoWrap !== false"
                  checked-children="自动换行"
                  un-checked-children="不换行"
                  @update:checked="updateSelectedTableColumn('autoWrap', $event)"
                />
                <a-switch
                  :checked="!!selectedTableColumnInfo.col.autoFitFont"
                  checked-children="自动适配字号"
                  un-checked-children="固定字号"
                  @update:checked="updateSelectedTableColumn('autoFitFont', $event)"
                />
              </a-space>
              <a-button danger size="small" @click="removeSelectedTableColumn">删除当前列</a-button>
            </a-space>
          </div>
          <a-button block size="small" type="dashed" @click="addTableColumn">新增列</a-button>
          </div>
          <a-empty v-else description="请先在画布中双击表格表头选择列" />
        </details>
      </template>
      <template v-if="selectedElement.type === 'freeTable'">
        <a-divider />
        <details class="section-card" open>
          <summary class="panel-subtitle">自由表格</summary>
          <a-space direction="vertical" style="width: 100%">
            <div class="free-table-merge-tip">合并：先点击起始格，再按住 Shift 点击结束格，然后点「合并选中区域」。</div>
            <div class="free-table-dim-row">
              <div class="free-table-dim-item">
                <span class="free-table-dim-label">行数</span>
                <a-input-number
                  :value="(selectedElement as any).rowCount || 1"
                  :min="1"
                  :max="100"
                  size="small"
                  disabled
                />
              </div>
              <div class="free-table-dim-item">
                <span class="free-table-dim-label">列数</span>
                <a-input-number
                  :value="(selectedElement as any).colCount || 1"
                  :min="1"
                  :max="50"
                  size="small"
                  disabled
                />
              </div>
            </div>
            <a-space>
              <a-button size="small" @click="addFreeTableRow">新增行</a-button>
              <a-button size="small" @click="removeFreeTableRow">删除行</a-button>
              <a-button size="small" @click="addFreeTableCol">新增列</a-button>
              <a-button size="small" @click="removeFreeTableCol">删除列</a-button>
            </a-space>
            <div class="free-table-track-head">单元格合并</div>
            <a-button type="primary" size="small" block :disabled="!canMergeFreeTableSelection" @click="mergeFreeTableSelection">合并选中区域</a-button>
            <a-button size="small" block :disabled="!canSplitFreeTableMerged" @click="splitFreeTableMerged">拆分当前合并</a-button>
          </a-space>
        </details>
        <details class="section-card" open>
          <summary class="panel-subtitle">表格样式</summary>
          <a-space direction="vertical" style="width: 100%">
            <div class="free-table-merge-tip">外边框控制整张表最外一圈；内边框控制行间横线、列间竖线。选中单元格后可在下方单独隐藏该格某一侧边框。</div>
            <div class="color-input-row">
              <a-input
                :value="(selectedElement as any).borderColor || '#d9d9d9'"
                addon-before="边框色"
                placeholder="#d9d9d9"
                @update:value="updateField('borderColor', $event)"
              />
              <input
                type="color"
                class="native-color-picker-trigger"
                title="选择边框颜色"
                :value="toColorHex((selectedElement as any).borderColor || '#d9d9d9')"
                @input="handleColorInput('borderColor', $event, '#d9d9d9')"
              />
            </div>
            <a-input-number
              :value="(selectedElement as any).borderWidth || 1"
              addon-before="边框宽(px)"
              :min="1"
              :max="6"
              style="width: 100%"
              @update:value="updateField('borderWidth', Number($event || 1))"
            />
            <div class="bind-param-compact">
              <span class="bind-param-compact__addon">外框线型</span>
              <a-select
                :value="(selectedElement as any).outerBorderLineStyle || 'solid'"
                class="bind-param-compact__select"
                @update:value="updateField('outerBorderLineStyle', $event)"
              >
                <a-select-option
                  v-for="opt in FREE_TABLE_LINE_STYLE_OPTIONS"
                  :key="`ft_os_${opt.value}`"
                  :value="opt.value"
                  :label="opt.label"
                >
                  <div class="free-table-line-style-option">
                    <span :class="['free-table-line-preview', `free-table-line-preview--${opt.value}`]" />
                    <span>{{ opt.label }}</span>
                  </div>
                </a-select-option>
              </a-select>
            </div>
            <div class="bind-param-compact">
              <span class="bind-param-compact__addon">横线线型</span>
              <a-select
                :value="(selectedElement as any).innerBorderHorizontalLineStyle || 'solid'"
                class="bind-param-compact__select"
                @update:value="updateField('innerBorderHorizontalLineStyle', $event)"
              >
                <a-select-option
                  v-for="opt in FREE_TABLE_LINE_STYLE_OPTIONS"
                  :key="`ft_hs_${opt.value}`"
                  :value="opt.value"
                  :label="opt.label"
                >
                  <div class="free-table-line-style-option">
                    <span :class="['free-table-line-preview', `free-table-line-preview--${opt.value}`]" />
                    <span>{{ opt.label }}</span>
                  </div>
                </a-select-option>
              </a-select>
            </div>
            <div class="bind-param-compact">
              <span class="bind-param-compact__addon">竖线线型</span>
              <a-select
                :value="(selectedElement as any).innerBorderVerticalLineStyle || 'solid'"
                class="bind-param-compact__select"
                @update:value="updateField('innerBorderVerticalLineStyle', $event)"
              >
                <a-select-option
                  v-for="opt in FREE_TABLE_LINE_STYLE_OPTIONS"
                  :key="`ft_vs_${opt.value}`"
                  :value="opt.value"
                  :label="opt.label"
                >
                  <div class="free-table-line-style-option">
                    <span :class="['free-table-line-preview', `free-table-line-preview--${opt.value}`]" />
                    <span>{{ opt.label }}</span>
                  </div>
                </a-select-option>
              </a-select>
            </div>
            <div class="free-table-track-head">外边框（显示）</div>
            <div class="free-table-border-switch-grid">
              <div class="free-table-border-switch-cell">
                <span class="free-table-border-switch-label">上</span>
                <a-switch
                  size="small"
                  :checked="(selectedElement as any).outerBorder?.top !== false"
                  checked-children="开"
                  un-checked-children="关"
                  @update:checked="patchFreeTableOuterSide('top', $event)"
                />
              </div>
              <div class="free-table-border-switch-cell">
                <span class="free-table-border-switch-label">右</span>
                <a-switch
                  size="small"
                  :checked="(selectedElement as any).outerBorder?.right !== false"
                  checked-children="开"
                  un-checked-children="关"
                  @update:checked="patchFreeTableOuterSide('right', $event)"
                />
              </div>
              <div class="free-table-border-switch-cell">
                <span class="free-table-border-switch-label">下</span>
                <a-switch
                  size="small"
                  :checked="(selectedElement as any).outerBorder?.bottom !== false"
                  checked-children="开"
                  un-checked-children="关"
                  @update:checked="patchFreeTableOuterSide('bottom', $event)"
                />
              </div>
              <div class="free-table-border-switch-cell">
                <span class="free-table-border-switch-label">左</span>
                <a-switch
                  size="small"
                  :checked="(selectedElement as any).outerBorder?.left !== false"
                  checked-children="开"
                  un-checked-children="关"
                  @update:checked="patchFreeTableOuterSide('left', $event)"
                />
              </div>
            </div>
            <div class="free-table-track-head">内边框（显示）</div>
            <a-space wrap>
              <a-switch
                :checked="(selectedElement as any).innerBorder?.horizontal !== false"
                checked-children="行间横线"
                un-checked-children="行间横线关"
                @update:checked="patchFreeTableInnerSide('horizontal', $event)"
              />
              <a-switch
                :checked="(selectedElement as any).innerBorder?.vertical !== false"
                checked-children="列间竖线"
                un-checked-children="列间竖线关"
                @update:checked="patchFreeTableInnerSide('vertical', $event)"
              />
            </a-space>
          </a-space>
        </details>
        <details class="section-card" open>
          <summary class="panel-subtitle">表格设置</summary>
          <a-space direction="vertical" style="width: 100%">
            <div class="free-table-merge-tip">
              列宽总和等于元素宽度、行高总和等于元素高度（单位 mm）。输入某一列/行时会与相邻列/行自动补偿；也可在画布中拖动蓝色分隔线调整。
            </div>
            <a-space wrap>
              <a-button size="small" @click="evenFreeTableColWidths">均分列宽</a-button>
              <a-button size="small" @click="evenFreeTableRowHeights">均分行高</a-button>
            </a-space>
            <div class="free-table-track-head">列宽（mm）</div>
            <div v-for="ci in freeTableColIndices" :key="`ft_cw_${ci}`" class="free-table-track-row">
              <span class="free-table-track-label">列{{ ci + 1 }}</span>
              <a-input-number
                size="small"
                :min="MIN_FREE_TABLE_TRACK_MM"
                :max="freeTableMaxColWidthMm"
                :value="freeTableColWidthsResolved[ci]"
                style="width: 160px"
                @update:value="handleFreeTableColWidthChange(ci, $event)"
              />
            </div>
            <div class="free-table-track-head">行高（mm）</div>
            <div v-for="ri in freeTableRowIndices" :key="`ft_rh_${ri}`" class="free-table-track-row">
              <span class="free-table-track-label">行{{ ri + 1 }}</span>
              <a-input-number
                size="small"
                :min="MIN_FREE_TABLE_TRACK_MM"
                :max="freeTableMaxRowHeightMm"
                :value="freeTableRowHeightsResolved[ri]"
                style="width: 160px"
                @update:value="handleFreeTableRowHeightChange(ri, $event)"
              />
            </div>
          </a-space>
        </details>
        <details class="section-card" open>
          <summary class="panel-subtitle">当前单元格（点击画布单元格后编辑）</summary>
          <a-space v-if="selectedFreeTableCellInfo" direction="vertical" style="width: 100%">
            <a-input :value="`R${selectedFreeTableCellInfo.row + 1} C${selectedFreeTableCellInfo.col + 1}`" addon-before="位置" disabled />
            <a-input
              :value="selectedFreeTableCellInfo.cell.text || ''"
              addon-before="文本"
              @update:value="updateSelectedFreeTableCell('text', $event)"
            />
            <div class="bind-param-compact">
              <span class="bind-param-compact__addon">绑定参数</span>
              <a-select
                :value="resolveParamBindSelectValue(selectedFreeTableCellInfo.cell.bindField)"
                :options="bindingParamOptions"
                allow-clear
                show-search
                option-filter-prop="label"
                placeholder="请先在左侧「参数」页维护"
                class="bind-param-compact__select"
                @update:value="handleFreeTableCellBindParamChange"
              />
            </div>
            <a-select
              :value="selectedFreeTableCellInfo.cell.align || 'left'"
              :options="FREE_TABLE_ALIGN_OPTIONS"
              style="width: 100%"
              @update:value="updateSelectedFreeTableCell('align', $event)"
            />
            <a-select
              :value="selectedFreeTableCellInfo.cell.verticalAlign || 'middle'"
              :options="FREE_TABLE_VALIGN_OPTIONS"
              style="width: 100%"
              @update:value="updateSelectedFreeTableCell('verticalAlign', $event)"
            />
            <div class="bind-param-compact">
              <span class="bind-param-compact__addon">数据类型</span>
              <a-select
                :value="selectedFreeTableCellInfo.cell.contentType || 'text'"
                :options="TABLE_COLUMN_CONTENT_TYPE_OPTIONS"
                class="bind-param-compact__select"
                @update:value="updateSelectedFreeTableCell('contentType', $event)"
              />
            </div>
            <template v-if="(selectedFreeTableCellInfo.cell.contentType || 'text') === 'image'">
              <a-select
                :value="selectedFreeTableCellInfo.cell.imageFit || 'contain'"
                style="width: 100%"
                :options="IMAGE_FIT_OPTIONS"
                @update:value="updateSelectedFreeTableCell('imageFit', $event)"
              />
              <a-input-number
                :value="selectedFreeTableCellInfo.cell.contentScale ?? 100"
                addon-before="图片缩放(%)"
                :min="10"
                :max="100"
                style="width: 100%"
                @update:value="updateSelectedFreeTableCell('contentScale', Number($event ?? 100))"
              />
              <a-switch
                :checked="selectedFreeTableCellInfo.cell.fillCell !== false"
                checked-children="填满单元格"
                un-checked-children="按缩放显示"
                @update:checked="updateSelectedFreeTableCell('fillCell', $event)"
              />
            </template>
            <template v-else-if="(selectedFreeTableCellInfo.cell.contentType || 'text') === 'qrcode'">
              <a-select
                :value="selectedFreeTableCellInfo.cell.qrLevel || 'M'"
                style="width: 100%"
                :options="QRCODE_LEVEL_OPTIONS"
                @update:value="updateSelectedFreeTableCell('qrLevel', $event)"
              />
              <a-select
                :value="selectedFreeTableCellInfo.cell.qrRenderType || 'image/png'"
                style="width: 100%"
                :options="QRCODE_RENDER_OPTIONS"
                @update:value="updateSelectedFreeTableCell('qrRenderType', $event)"
              />
              <a-input-number
                :value="selectedFreeTableCellInfo.cell.contentScale ?? 100"
                addon-before="二维码缩放(%)"
                :min="10"
                :max="100"
                style="width: 100%"
                @update:value="updateSelectedFreeTableCell('contentScale', Number($event ?? 100))"
              />
              <a-switch
                :checked="selectedFreeTableCellInfo.cell.fillCell !== false"
                checked-children="填满单元格"
                un-checked-children="按缩放显示"
                @update:checked="updateSelectedFreeTableCell('fillCell', $event)"
              />
            </template>
            <template v-else-if="(selectedFreeTableCellInfo.cell.contentType || 'text') === 'barcode'">
              <a-select
                :value="selectedFreeTableCellInfo.cell.barcodeFormat || 'CODE128'"
                style="width: 100%"
                :options="BARCODE_FORMAT_OPTIONS"
                @update:value="updateSelectedFreeTableCell('barcodeFormat', $event)"
              />
              <a-input-number
                :value="selectedFreeTableCellInfo.cell.contentScale ?? 100"
                addon-before="条码缩放(%)"
                :min="10"
                :max="100"
                style="width: 100%"
                @update:value="updateSelectedFreeTableCell('contentScale', Number($event ?? 100))"
              />
              <a-switch
                :checked="selectedFreeTableCellInfo.cell.fillCell !== false"
                checked-children="填满单元格"
                un-checked-children="按缩放显示"
                @update:checked="updateSelectedFreeTableCell('fillCell', $event)"
              />
            </template>
            <template v-else-if="(selectedFreeTableCellInfo.cell.contentType || 'text') === 'number' || (selectedFreeTableCellInfo.cell.contentType || 'text') === 'amount'">
              <a-input-number
                :value="selectedFreeTableCellInfo.cell.decimalPlaces ?? 2"
                addon-before="小数位数"
                :min="0"
                :max="6"
                style="width: 100%"
                @update:value="updateSelectedFreeTableCell('decimalPlaces', Number($event ?? 2))"
              />
              <a-switch
                :checked="selectedFreeTableCellInfo.cell.roundHalfUp !== false"
                checked-children="四舍五入"
                un-checked-children="截断"
                @update:checked="updateSelectedFreeTableCell('roundHalfUp', $event)"
              />
              <a-select
                v-if="(selectedFreeTableCellInfo.cell.contentType || 'text') === 'amount'"
                :value="selectedFreeTableCellInfo.cell.amountType || 'CNY'"
                style="width: 100%"
                :options="AMOUNT_TYPE_OPTIONS"
                @update:value="updateSelectedFreeTableCell('amountType', $event)"
              />
            </template>
            <a-space v-if="(selectedFreeTableCellInfo.cell.contentType || 'text') === 'text'">
              <a-switch
                :checked="selectedFreeTableCellInfo.cell.autoWrap !== false"
                checked-children="自动换行"
                un-checked-children="不换行"
                @update:checked="updateSelectedFreeTableCell('autoWrap', $event)"
              />
              <a-switch
                :checked="!!selectedFreeTableCellInfo.cell.autoFitFont"
                checked-children="自动适配字号"
                un-checked-children="固定字号"
                @update:checked="updateSelectedFreeTableCell('autoFitFont', $event)"
              />
            </a-space>
            <a-input-number
              :value="selectedFreeTableCellInfo.cell.fontSize || 12"
              addon-before="字号(px)"
              :min="8"
              :max="72"
              style="width: 100%"
              @update:value="updateSelectedFreeTableCell('fontSize', Number($event || 12))"
            />
            <div class="color-input-row">
              <a-input
                :value="selectedFreeTableCellInfo.cell.color || '#111111'"
                addon-before="文字色"
                placeholder="#111111"
                @update:value="updateSelectedFreeTableCell('color', $event)"
              />
              <input
                type="color"
                class="native-color-picker-trigger"
                title="选择文字颜色"
                :value="toColorHex(selectedFreeTableCellInfo.cell.color || '#111111')"
                @input="handleFreeTableCellColorInput('color', $event, '#111111')"
              />
            </div>
            <div class="color-input-row">
              <a-input
                :value="selectedFreeTableCellInfo.cell.backgroundColor || '#ffffff'"
                addon-before="背景色"
                placeholder="#ffffff"
                @update:value="updateSelectedFreeTableCell('backgroundColor', $event)"
              />
              <input
                type="color"
                class="native-color-picker-trigger"
                title="选择背景颜色"
                :value="toColorHex(selectedFreeTableCellInfo.cell.backgroundColor || '#ffffff')"
                @input="handleFreeTableCellColorInput('backgroundColor', $event, '#ffffff')"
              />
            </div>
            <a-divider plain orientation="left" style="margin: 8px 0">单元格边线（隐藏）</a-divider>
            <div class="free-table-merge-tip">以下为当前锚点格（含合并区域）单独隐藏某侧边框，与「表格样式」中的外框/内线叠加生效。</div>
            <div class="free-table-border-switch-grid">
              <div class="free-table-border-switch-cell">
                <span class="free-table-border-switch-label">隐上</span>
                <a-switch
                  size="small"
                  :checked="!!selectedFreeTableCellInfo.cell.hideBorderTop"
                  checked-children="隐"
                  un-checked-children="显"
                  @update:checked="updateSelectedFreeTableCell('hideBorderTop', $event)"
                />
              </div>
              <div class="free-table-border-switch-cell">
                <span class="free-table-border-switch-label">隐右</span>
                <a-switch
                  size="small"
                  :checked="!!selectedFreeTableCellInfo.cell.hideBorderRight"
                  checked-children="隐"
                  un-checked-children="显"
                  @update:checked="updateSelectedFreeTableCell('hideBorderRight', $event)"
                />
              </div>
              <div class="free-table-border-switch-cell">
                <span class="free-table-border-switch-label">隐下</span>
                <a-switch
                  size="small"
                  :checked="!!selectedFreeTableCellInfo.cell.hideBorderBottom"
                  checked-children="隐"
                  un-checked-children="显"
                  @update:checked="updateSelectedFreeTableCell('hideBorderBottom', $event)"
                />
              </div>
              <div class="free-table-border-switch-cell">
                <span class="free-table-border-switch-label">隐左</span>
                <a-switch
                  size="small"
                  :checked="!!selectedFreeTableCellInfo.cell.hideBorderLeft"
                  checked-children="隐"
                  un-checked-children="显"
                  @update:checked="updateSelectedFreeTableCell('hideBorderLeft', $event)"
                />
              </div>
            </div>
          </a-space>
          <a-empty v-else description="请先在画布中点击自由表格的单元格" />
        </details>
      </template>
      </template>
      <template v-else>
        <a-empty description="请先选中元素" />
      </template>
    </div>
  </div>
</template>

<script lang="ts" setup>
  import { computed, ref } from 'vue';
  import { useMessage } from '/@/hooks/web/useMessage';
  import { TABLE_FONT_OPTIONS } from '../core/fontOptions';
  import TableHeaderConfigEditor from './TableHeaderConfigEditor.vue';
  import type {
    NativeDataBindingDetailTable,
    NativeDataBindingParam,
    NativeElement,
    NativeTemplateSchema,
  } from '../core/types';
  import {
    canMergeFreeTableRegion,
    clipAnchorsToGrid,
    getFreeTableOwnerAt,
    mergeFreeTableRegion,
    normalizeFreeTableAnchors,
    splitFreeTableAt,
  } from '../core/freeTableGrid';
  import { FREE_TABLE_LINE_STYLE_OPTIONS } from '../core/freeTableLineStyles';
  import {
    MIN_FREE_TABLE_TRACK_MM,
    buildEvenColWidths,
    buildEvenRowHeights,
    colWidthsAfterAddCol,
    colWidthsAfterRemoveCol,
    resolveFreeTableColWidthsMm,
    resolveFreeTableRowHeightsMm,
    rowHeightsAfterAddRow,
    rowHeightsAfterRemoveRow,
    setColWidthAt,
    setRowHeightAt,
  } from '../core/freeTableTracks';

  const { createMessage } = useMessage();

  const propertiesPanelRootRef = ref<HTMLElement | null>(null);

  function getSectionDetails(): HTMLDetailsElement[] {
    const root = propertiesPanelRootRef.value;
    if (!root) return [];
    return Array.from(root.querySelectorAll('details.section-card')) as HTMLDetailsElement[];
  }

  /** 折叠所有功能分区（details.section-card） */
  function collapseAllSections() {
    getSectionDetails().forEach((el) => {
      el.open = false;
    });
  }

  /** 展开所有功能分区 */
  function expandAllSections() {
    getSectionDetails().forEach((el) => {
      el.open = true;
    });
  }

  const props = defineProps<{
    schema: NativeTemplateSchema;
    selectedElement?: NativeElement;
    selectedTableColumnKey?: string;
    selectedFreeTableCell?: { row: number; col: number } | null;
    /** 画布 Shift 选取的矩形（含首尾），用于合并 */
    selectedFreeTableMergeRect?: { r0: number; r1: number; c0: number; c1: number } | null;
  }>();

  /** 标题/正文等：在「元素属性」中显示 style 文字色与背景色 */
  const showPlainTextStyleColors = computed(() => {
    const el = props.selectedElement;
    if (!el) return false;
    return ['title', 'subtitle', 'text', 'date', 'pageNo'].includes(el.type);
  });

  /** 透明/无色时取色器用白色占位 */
  const plainTextBackgroundForPicker = computed(() => {
    const raw = String(props.selectedElement?.style?.backgroundColor ?? '').trim().toLowerCase();
    if (!raw || raw === 'transparent' || raw === 'none' || raw === 'rgba(0,0,0,0)' || raw === 'rgba(0, 0, 0, 0)') {
      return '#ffffff';
    }
    return raw;
  });

  /** 左侧「参数」页维护的键，供下拉绑定 */
  const bindingParamOptions = computed(() =>
    (props.schema.dataBinding?.params ?? []).map((p: NativeDataBindingParam) => ({
      value: p.key,
      label: p.label ? `${p.key}（${p.label}）` : p.key,
    })),
  );

  /** 左侧「字段」页登记的明细数据源，供普通/明细表格 source 下拉 */
  const detailTableSourceOptions = computed(() => {
    const tables = (props.schema.dataBinding?.detailTables ?? []) as NativeDataBindingDetailTable[];
    const opts = tables.map((t) => ({
      value: t.tableKey,
      label: t.label ? `${t.tableKey}（${t.label}）` : t.tableKey,
    }));
    const el = props.selectedElement;
    if (!el || (el.type !== 'table' && el.type !== 'detailTable')) return opts;
    const cur = String((el as any).source || '').trim();
    if (!cur || opts.some((o) => o.value === cur)) return opts;
    return [{ value: cur, label: `${cur}（未登记，请迁移或到「字段」页登记）` }, ...opts];
  });

  /** 当前表格元素 source 对应的明细字段列表 */
  const detailFieldOptionsForCurrentSource = computed(() => {
    const el = props.selectedElement;
    if (!el || (el.type !== 'table' && el.type !== 'detailTable')) return [];
    const src = String((el as any).source || '').trim();
    const dt = (props.schema.dataBinding?.detailTables ?? []).find((t) => t.tableKey === src) as
      | NativeDataBindingDetailTable
      | undefined;
    const fields = dt?.fields ?? [];
    return fields.map((f) => ({
      value: f.key,
      label: f.label ? `${f.key}（${f.label}）` : f.key,
    }));
  });

  function resolveParamBindSelectValue(raw: string | undefined | null) {
    const k = String(raw || '').trim();
    if (!k) return undefined;
    const keys = new Set((props.schema.dataBinding?.params ?? []).map((p: NativeDataBindingParam) => p.key));
    return keys.has(k) ? k : undefined;
  }

  /** 表格数据源：已登记则正常；未登记的旧模板仍回显当前键 */
  function resolveTableSourceSelectValue(raw: string | undefined | null) {
    const k = String(raw || '').trim();
    return k || undefined;
  }

  /** 列绑定字段：空为 undefined 便于 allow-clear */
  function resolveDetailFieldBindSelectValue(raw: string | undefined | null) {
    const k = String(raw || '').trim();
    return k || undefined;
  }

  /** 参数显示名：有 label 用 label，否则用键 */
  function resolveParamDisplayName(paramKey: string) {
    const k = String(paramKey || '').trim();
    if (!k) return '';
    const p = (props.schema.dataBinding?.params ?? []).find((x: NativeDataBindingParam) => x.key === k);
    if (p?.label && String(p.label).trim()) return String(p.label).trim();
    return k;
  }

  /** 元素绑定参数变更：标题/文本/日期类自动把「文本」设为参数显示名 */
  function handleElementBindParamChange(value: string | undefined) {
    if (!props.selectedElement) return;
    const v = value ?? '';
    const t = props.selectedElement.type;
    const patch: Record<string, unknown> = { bindField: v };
    if (v && ['title', 'subtitle', 'text', 'date'].includes(t)) {
      patch.text = resolveParamDisplayName(v);
    }
    emit('update-element', { id: props.selectedElement.id, patch: patch as any });
  }

  /** 明细字段显示名：有 label 用 label，否则用字段键 */
  function resolveDetailFieldDisplayName(tableKey: string, fieldKey: string) {
    const tk = String(tableKey || '').trim();
    const fk = String(fieldKey || '').trim();
    if (!tk || !fk) return '';
    const dt = (props.schema.dataBinding?.detailTables ?? []).find((t) => t.tableKey === tk);
    const f = dt?.fields?.find((x) => x.key === fk);
    if (f?.label && String(f.label).trim()) return String(f.label).trim();
    return fk;
  }

  /** 表格列绑定明细字段：同步列标题为字段显示名（或字段键） */
  function handleTableColumnBindDetailFieldChange(value: string | undefined) {
    const v = value ?? '';
    if (!props.selectedElement || (props.selectedElement.type !== 'table' && props.selectedElement.type !== 'detailTable')) {
      return;
    }
    if (!selectedTableColumnInfo.value) return;
    const src = String((props.selectedElement as any).source || '').trim();
    const idx = selectedTableColumnInfo.value.index;
    const columns = tableColumns.value.map((item) => ({ ...item }));
    const target = columns[idx];
    if (!target) return;
    target.bindField = v;
    target.field = v;
    if (v) {
      const display = resolveDetailFieldDisplayName(src, v);
      target.title = display || v;
    }
    emit('update-element', { id: props.selectedElement.id, patch: { columns } as any });
  }

  /** 切换数据源：清空不在新数据源中的列绑定 */
  function handleTableSourceChange(value: string | undefined) {
    if (!props.selectedElement || (props.selectedElement.type !== 'table' && props.selectedElement.type !== 'detailTable')) {
      return;
    }
    const v = String(value || '').trim();
    const fieldKeys = new Set(
      (props.schema.dataBinding?.detailTables ?? []).find((t) => t.tableKey === v)?.fields?.map((f) => f.key) ?? [],
    );
    const columns = tableColumns.value.map((item) => {
      const c = { ...item };
      const bf = String(c.bindField || '').trim();
      if (bf && !fieldKeys.has(bf)) {
        c.bindField = '';
        c.field = '';
      }
      return c;
    });
    emit('update-element', { id: props.selectedElement.id, patch: { source: v || undefined, columns } as any });
  }

  /** 自由表格单元格绑定参数：同步单元格文本为参数显示名（单次提交避免状态不同步） */
  function handleFreeTableCellBindParamChange(value: string | undefined) {
    const v = value ?? '';
    if (!props.selectedElement || props.selectedElement.type !== 'freeTable') return;
    const info = selectedFreeTableCellInfo.value;
    if (!info) return;
    const rowCount = Math.max(1, Number((props.selectedElement as any).rowCount || 1));
    const colCount = Math.max(1, Number((props.selectedElement as any).colCount || 1));
    const anchors = normalizeFreeTableAnchors(rowCount, colCount, (props.selectedElement as any).cells || []);
    const display = v ? resolveParamDisplayName(v) : undefined;
    const next = anchors.map((item: any) => {
      if (item.row === info.row && item.col === info.col) {
        const row: any = { ...item, bindField: v };
        if (v && display) row.text = display;
        return row;
      }
      return { ...item };
    });
    updateFreeTableElement({ cells: next });
  }

  const emit = defineEmits(['update-element']);
  const headerConfigModalOpen = ref(false);
  const columnWidthModalOpen = ref(false);
  const TABLE_COLUMN_CONTENT_TYPE_OPTIONS = [
    { label: '文本', value: 'text' },
    { label: '数字', value: 'number' },
    { label: '金额', value: 'amount' },
    { label: '图片', value: 'image' },
    { label: '二维码', value: 'qrcode' },
    { label: '条形码', value: 'barcode' },
  ];
  const AMOUNT_TYPE_OPTIONS = [
    { label: '人民币(CNY)', value: 'CNY' },
    { label: '美元(USD)', value: 'USD' },
    { label: '欧元(EUR)', value: 'EUR' },
  ];
  const TABLE_HEIGHT_MODE_OPTIONS = [
    { label: '按数据自动分页', value: 'autoPage' },
    { label: '固定行数分页', value: 'fixedRows' },
  ];
  const FOOTER_TOTAL_MODE_OPTIONS = [
    { label: '合计模式：总合计', value: 'overall' },
    { label: '合计模式：按页合计', value: 'page' },
  ];
  const IMAGE_FIT_OPTIONS = [
    { label: '拉伸填充', value: 'fill' },
    { label: '等比包含', value: 'contain' },
    { label: '等比裁切', value: 'cover' },
  ];
  const QRCODE_LEVEL_OPTIONS = [
    { label: 'L（低容错）', value: 'L' },
    { label: 'M（中容错）', value: 'M' },
    { label: 'Q（高容错）', value: 'Q' },
    { label: 'H（最高容错）', value: 'H' },
  ];
  const QRCODE_RENDER_OPTIONS = [
    { label: 'PNG', value: 'image/png' },
    { label: 'JPEG', value: 'image/jpeg' },
    { label: 'WEBP', value: 'image/webp' },
  ];
  const BARCODE_FORMAT_OPTIONS = [
    { label: 'CODE128', value: 'CODE128' },
    { label: 'CODE39', value: 'CODE39' },
    { label: 'EAN13', value: 'EAN13' },
    { label: 'EAN8', value: 'EAN8' },
    { label: 'ITF14', value: 'ITF14' },
    { label: 'MSI', value: 'MSI' },
    { label: 'pharmacode', value: 'pharmacode' },
  ];
  const REFRESH_PAGE_OPTIONS = [
    { label: '刷新页：不应用', value: 'none' },
    { label: '刷新页：每次', value: 'always' },
    { label: '刷新页：溢出时', value: 'onOverflow' },
  ];
  const FREE_TABLE_ALIGN_OPTIONS = [
    { label: '单元格对齐：左', value: 'left' },
    { label: '单元格对齐：中', value: 'center' },
    { label: '单元格对齐：右', value: 'right' },
  ];
  const FREE_TABLE_VALIGN_OPTIONS = [
    { label: '垂直对齐：上', value: 'top' },
    { label: '垂直对齐：中', value: 'middle' },
    { label: '垂直对齐：下', value: 'bottom' },
  ];

  function updateRect(key: string, value: any) {
    if (!props.selectedElement) return;
    emit('update-element', { id: props.selectedElement.id, patch: { [key]: Number(value || 0) } as any });
  }

  function updateStyle(key: string, value: any) {
    if (!props.selectedElement) return;
    emit('update-element', {
      id: props.selectedElement.id,
      patch: {
        style: {
          ...(props.selectedElement.style || {}),
          [key]: value,
        },
      } as any,
    });
  }

  function updateField(key: string, value: any) {
    if (!props.selectedElement) return;
    emit('update-element', { id: props.selectedElement.id, patch: { [key]: value } as any });
  }

  const tableColumns = computed<any[]>(() => {
    if (!props.selectedElement || (props.selectedElement.type !== 'table' && props.selectedElement.type !== 'detailTable')) {
      return [];
    }
    return Array.isArray((props.selectedElement as any).columns) ? (props.selectedElement as any).columns : [];
  });

  const selectedTableColumnInfo = computed<{ index: number; col: any } | null>(() => {
    if (!tableColumns.value.length || !props.selectedTableColumnKey) {
      return null;
    }
    const index = tableColumns.value.findIndex((item) => item?.key === props.selectedTableColumnKey);
    if (index < 0) {
      return null;
    }
    return { index, col: tableColumns.value[index] };
  });

  /** 当前列绑定字段下拉：含未登记的旧值，避免下拉空白 */
  const detailFieldBindOptionsForColumn = computed(() => {
    const base = detailFieldOptionsForCurrentSource.value;
    const col = selectedTableColumnInfo.value?.col;
    const bf = String(col?.bindField || col?.field || '').trim();
    if (!bf || base.some((o) => o.value === bf)) return base;
    return [{ value: bf, label: `${bf}（未登记）` }, ...base];
  });

  const selectedFreeTableCellInfo = computed<{ row: number; col: number; cell: any } | null>(() => {
    if (!props.selectedElement || props.selectedElement.type !== 'freeTable') {
      return null;
    }
    const row = Number(props.selectedFreeTableCell?.row ?? -1);
    const col = Number(props.selectedFreeTableCell?.col ?? -1);
    if (row < 0 || col < 0) {
      return null;
    }
    const rowCount = Math.max(1, Number((props.selectedElement as any).rowCount || 1));
    const colCount = Math.max(1, Number((props.selectedElement as any).colCount || 1));
    if (row >= rowCount || col >= colCount) {
      return null;
    }
    const anchors = normalizeFreeTableAnchors(rowCount, colCount, (props.selectedElement as any).cells || []);
    const owner = getFreeTableOwnerAt(anchors, row, col);
    return { row: owner.row, col: owner.col, cell: owner };
  });

  const canMergeFreeTableSelection = computed(() => {
    const rect = props.selectedFreeTableMergeRect;
    if (!rect || !props.selectedElement || props.selectedElement.type !== 'freeTable') {
      return false;
    }
    if (rect.r0 === rect.r1 && rect.c0 === rect.c1) {
      return false;
    }
    const el = props.selectedElement as any;
    const rowCount = Math.max(1, Number(el.rowCount || 1));
    const colCount = Math.max(1, Number(el.colCount || 1));
    const anchors = normalizeFreeTableAnchors(rowCount, colCount, el.cells || []);
    return canMergeFreeTableRegion(anchors, rowCount, colCount, rect.r0, rect.c0, rect.r1, rect.c1);
  });

  const canSplitFreeTableMerged = computed(() => {
    const info = selectedFreeTableCellInfo.value;
    if (!info) return false;
    const rs = Math.max(1, Number(info.cell.rowspan || 1));
    const cs = Math.max(1, Number(info.cell.colspan || 1));
    return rs > 1 || cs > 1;
  });

  const freeTableTrackEl = computed(() => {
    if (!props.selectedElement || props.selectedElement.type !== 'freeTable') {
      return null;
    }
    return props.selectedElement as any;
  });

  const freeTableColIndices = computed(() => {
    const el = freeTableTrackEl.value;
    if (!el) {
      return [];
    }
    const n = Math.max(1, Number(el.colCount || 1));
    return Array.from({ length: n }, (_, i) => i);
  });

  const freeTableRowIndices = computed(() => {
    const el = freeTableTrackEl.value;
    if (!el) {
      return [];
    }
    const n = Math.max(1, Number(el.rowCount || 1));
    return Array.from({ length: n }, (_, i) => i);
  });

  const freeTableColWidthsResolved = computed(() => {
    const el = freeTableTrackEl.value;
    if (!el) {
      return [];
    }
    return resolveFreeTableColWidthsMm(el);
  });

  const freeTableRowHeightsResolved = computed(() => {
    const el = freeTableTrackEl.value;
    if (!el) {
      return [];
    }
    return resolveFreeTableRowHeightsMm(el);
  });

  const freeTableMaxColWidthMm = computed(() => {
    const el = freeTableTrackEl.value;
    if (!el) {
      return 200;
    }
    const n = Math.max(1, Number(el.colCount || 1));
    const w = Math.max(0.01, Number(el.w) || 0.01);
    return Math.max(MIN_FREE_TABLE_TRACK_MM, w - MIN_FREE_TABLE_TRACK_MM * Math.max(0, n - 1));
  });

  const freeTableMaxRowHeightMm = computed(() => {
    const el = freeTableTrackEl.value;
    if (!el) {
      return 200;
    }
    const n = Math.max(1, Number(el.rowCount || 1));
    const h = Math.max(0.01, Number(el.h) || 0.01);
    return Math.max(MIN_FREE_TABLE_TRACK_MM, h - MIN_FREE_TABLE_TRACK_MM * Math.max(0, n - 1));
  });

  const footerLabelColumnOptions = computed(() => {
    const options = tableColumns.value.map((item) => ({
      label: `标签列：${item?.title || item?.key}`,
      value: item?.key,
    }));
    return options;
  });
  const mergeColumnOptions = computed(() => {
    return tableColumns.value.map((item) => ({
      label: `${item?.title || item?.key}（${item?.bindField || item?.field || item?.key}）`,
      value: item?.key,
    }));
  });

  function updateTableColumn(index: number, key: string, value: any) {
    if (!props.selectedElement) return;
    const columns = tableColumns.value.map((item) => ({ ...item }));
    const target = columns[index];
    if (!target) return;
    target[key] = value;
    if (key === 'bindField' && !target.field) {
      target.field = value;
    }
    if (key === 'fontSize') {
      target.useCustomFontSize = true;
    }
    emit('update-element', { id: props.selectedElement.id, patch: { columns } as any });
  }

  function updateMergeColumns(values: string[]) {
    if (!props.selectedElement) return;
    const selected = Array.isArray(values) ? values.map((item) => String(item || '')).filter(Boolean) : [];
    const validKeys = new Set(tableColumns.value.map((item) => String(item?.key || '')));
    const next = selected.filter((item) => validKeys.has(item));
    updateField('mergeColumnKeys', next);
  }

  function addTableColumn() {
    if (!props.selectedElement) return;
    const columns = tableColumns.value.map((item) => ({ ...item }));
    const nextIndex = columns.length + 1;
    columns.push({
      key: `col_${Date.now()}_${nextIndex}`,
      title: `列${nextIndex}`,
      field: `field${nextIndex}`,
      bindField: `field${nextIndex}`,
      width: 30,
      align: 'left',
      contentType: 'text',
      fontFamily: '',
      fontSize: 12,
      useCustomFontSize: false,
      fontColor: '#111111',
      autoFitFont: false,
      autoWrap: true,
      fillCell: true,
      contentScale: 100,
      imageFit: 'contain',
      qrLevel: 'M',
      qrRenderType: 'image/png',
      barcodeFormat: 'CODE128',
      decimalPlaces: 2,
      roundHalfUp: true,
      amountType: 'CNY',
      enableFooterTotal: false,
    });
    emit('update-element', { id: props.selectedElement.id, patch: { columns, headerConfig: undefined } as any });
  }

  function removeTableColumn(index: number) {
    if (!props.selectedElement) return;
    const columns = tableColumns.value.map((item) => ({ ...item })).filter((_, idx) => idx !== index);
    const validKeys = new Set(columns.map((item) => String(item?.key || '')));
    const mergeColumnKeys = Array.isArray((props.selectedElement as any).mergeColumnKeys)
      ? (props.selectedElement as any).mergeColumnKeys.filter((item: string) => validKeys.has(String(item || '')))
      : [];
    emit('update-element', { id: props.selectedElement.id, patch: { columns, headerConfig: undefined, mergeColumnKeys } as any });
  }

  function updateSelectedTableColumn(key: string, value: any) {
    if (!selectedTableColumnInfo.value) return;
    updateTableColumn(selectedTableColumnInfo.value.index, key, value);
  }

  function updateFreeTableElement(nextPatch: any) {
    if (!props.selectedElement || props.selectedElement.type !== 'freeTable') return;
    emit('update-element', { id: props.selectedElement.id, patch: nextPatch as any });
  }

  function patchFreeTableOuterSide(side: 'top' | 'right' | 'bottom' | 'left', checked: boolean) {
    if (!props.selectedElement || props.selectedElement.type !== 'freeTable') {
      return;
    }
    const el = props.selectedElement as any;
    updateField('outerBorder', { ...(el.outerBorder || {}), [side]: checked });
  }

  function patchFreeTableInnerSide(key: 'horizontal' | 'vertical', checked: boolean) {
    if (!props.selectedElement || props.selectedElement.type !== 'freeTable') {
      return;
    }
    const el = props.selectedElement as any;
    updateField('innerBorder', { ...(el.innerBorder || {}), [key]: checked });
  }

  function handleFreeTableColWidthChange(index: number, val: number | string | null) {
    const el = freeTableTrackEl.value;
    if (!el) {
      return;
    }
    const v = Number(val);
    if (!Number.isFinite(v)) {
      return;
    }
    const cur = resolveFreeTableColWidthsMm(el);
    const next = setColWidthAt(cur, index, v, Math.max(0.01, Number(el.w) || 0.01));
    if (!next) {
      createMessage.warning(`无法满足该列宽（单列最小约 ${MIN_FREE_TABLE_TRACK_MM}mm）`);
      return;
    }
    updateFreeTableElement({ colWidths: next });
  }

  function handleFreeTableRowHeightChange(index: number, val: number | string | null) {
    const el = freeTableTrackEl.value;
    if (!el) {
      return;
    }
    const v = Number(val);
    if (!Number.isFinite(v)) {
      return;
    }
    const cur = resolveFreeTableRowHeightsMm(el);
    const next = setRowHeightAt(cur, index, v, Math.max(0.01, Number(el.h) || 0.01));
    if (!next) {
      createMessage.warning(`无法满足该行高（单行最小约 ${MIN_FREE_TABLE_TRACK_MM}mm）`);
      return;
    }
    updateFreeTableElement({ rowHeights: next });
  }

  function evenFreeTableColWidths() {
    const el = freeTableTrackEl.value;
    if (!el) {
      return;
    }
    const cc = Math.max(1, Number(el.colCount || 1));
    const w = Math.max(0.01, Number(el.w) || 0.01);
    updateFreeTableElement({ colWidths: buildEvenColWidths(cc, w) });
  }

  function evenFreeTableRowHeights() {
    const el = freeTableTrackEl.value;
    if (!el) {
      return;
    }
    const rc = Math.max(1, Number(el.rowCount || 1));
    const h = Math.max(0.01, Number(el.h) || 0.01);
    updateFreeTableElement({ rowHeights: buildEvenRowHeights(rc, h) });
  }

  /** 行/列变化后：裁剪越界锚点并填满网格 */
  function rebuildFreeTableCells(rowCount: number, colCount: number, baseCells: any[] = []) {
    const clipped = clipAnchorsToGrid(rowCount, colCount, baseCells);
    return normalizeFreeTableAnchors(rowCount, colCount, clipped);
  }

  function mergeFreeTableSelection() {
    if (!props.selectedElement || props.selectedElement.type !== 'freeTable') return;
    const rect = props.selectedFreeTableMergeRect;
    if (!rect) return;
    const el = props.selectedElement as any;
    const rowCount = Math.max(1, Number(el.rowCount || 1));
    const colCount = Math.max(1, Number(el.colCount || 1));
    let anchors = normalizeFreeTableAnchors(rowCount, colCount, el.cells || []);
    if (!canMergeFreeTableRegion(anchors, rowCount, colCount, rect.r0, rect.c0, rect.r1, rect.c1)) {
      createMessage.warning('所选区域无法合并（需均为未合并的 1×1 单元格，且为完整矩形）');
      return;
    }
    anchors = mergeFreeTableRegion(anchors, rowCount, colCount, rect.r0, rect.c0, rect.r1, rect.c1);
    updateFreeTableElement({ cells: anchors });
    createMessage.success('已合并单元格');
  }

  function splitFreeTableMerged() {
    const info = selectedFreeTableCellInfo.value;
    if (!props.selectedElement || props.selectedElement.type !== 'freeTable' || !info) return;
    const el = props.selectedElement as any;
    const rowCount = Math.max(1, Number(el.rowCount || 1));
    const colCount = Math.max(1, Number(el.colCount || 1));
    const anchors = normalizeFreeTableAnchors(rowCount, colCount, el.cells || []);
    const next = splitFreeTableAt(anchors, rowCount, colCount, info.row, info.col);
    updateFreeTableElement({ cells: next });
    createMessage.success('已拆分合并单元格');
  }

  function addFreeTableRow() {
    if (!props.selectedElement || props.selectedElement.type !== 'freeTable') return;
    const el = props.selectedElement as any;
    const rowCount = Math.max(1, Number(el.rowCount || 1)) + 1;
    const colCount = Math.max(1, Number(el.colCount || 1));
    const cells = rebuildFreeTableCells(rowCount, colCount, el.cells || []);
    const prevRh = resolveFreeTableRowHeightsMm(el);
    const nextRh = rowHeightsAfterAddRow(prevRh, Math.max(0.01, Number(el.h) || 0.01));
    updateFreeTableElement({ rowCount, cells, rowHeights: nextRh });
  }

  function removeFreeTableRow() {
    if (!props.selectedElement || props.selectedElement.type !== 'freeTable') return;
    const el = props.selectedElement as any;
    const prevRowCount = Math.max(1, Number(el.rowCount || 1));
    if (prevRowCount <= 1) return;
    const rowCount = prevRowCount - 1;
    const colCount = Math.max(1, Number(el.colCount || 1));
    const cells = rebuildFreeTableCells(rowCount, colCount, el.cells || []);
    const prevRh = resolveFreeTableRowHeightsMm(el);
    const nextRh = rowHeightsAfterRemoveRow(prevRh, prevRowCount - 1, Math.max(0.01, Number(el.h) || 0.01));
    updateFreeTableElement({ rowCount, cells, rowHeights: nextRh });
  }

  function addFreeTableCol() {
    if (!props.selectedElement || props.selectedElement.type !== 'freeTable') return;
    const el = props.selectedElement as any;
    const rowCount = Math.max(1, Number(el.rowCount || 1));
    const colCount = Math.max(1, Number(el.colCount || 1)) + 1;
    const cells = rebuildFreeTableCells(rowCount, colCount, el.cells || []);
    const prevCw = resolveFreeTableColWidthsMm(el);
    const nextCw = colWidthsAfterAddCol(prevCw, Math.max(0.01, Number(el.w) || 0.01));
    updateFreeTableElement({ colCount, cells, colWidths: nextCw });
  }

  function removeFreeTableCol() {
    if (!props.selectedElement || props.selectedElement.type !== 'freeTable') return;
    const el = props.selectedElement as any;
    const rowCount = Math.max(1, Number(el.rowCount || 1));
    const prevColCount = Math.max(1, Number(el.colCount || 1));
    if (prevColCount <= 1) return;
    const colCount = prevColCount - 1;
    const cells = rebuildFreeTableCells(rowCount, colCount, el.cells || []);
    const prevCw = resolveFreeTableColWidthsMm(el);
    const nextCw = colWidthsAfterRemoveCol(prevCw, prevColCount - 1, Math.max(0.01, Number(el.w) || 0.01));
    updateFreeTableElement({ colCount, cells, colWidths: nextCw });
  }

  function updateSelectedFreeTableCell(key: string, value: any) {
    if (!props.selectedElement || props.selectedElement.type !== 'freeTable') return;
    const info = selectedFreeTableCellInfo.value;
    if (!info) return;
    const rowCount = Math.max(1, Number((props.selectedElement as any).rowCount || 1));
    const colCount = Math.max(1, Number((props.selectedElement as any).colCount || 1));
    const anchors = normalizeFreeTableAnchors(rowCount, colCount, (props.selectedElement as any).cells || []);
    const next = anchors.map((item: any) => {
      if (item.row === info.row && item.col === info.col) {
        const row = { ...item };
        if (String(key).startsWith('hideBorder')) {
          if (value) {
            row[key] = true;
          } else {
            delete row[key];
          }
        } else {
          row[key] = value;
        }
        return row;
      }
      return { ...item };
    });
    updateFreeTableElement({ cells: next });
  }

  function removeSelectedTableColumn() {
    if (!selectedTableColumnInfo.value) return;
    removeTableColumn(selectedTableColumnInfo.value.index);
  }

  function resolveFooterLabelColumnKey() {
    const key = String((props.selectedElement as any)?.footerLabelColumnKey || '');
    if (key) {
      return key;
    }
    return tableColumns.value?.[0]?.key || '';
  }

  function resolveHeaderConfigRowCount() {
    const count = Number((props.selectedElement as any)?.headerConfig?.rowCount || 1);
    if (Number.isFinite(count) && count > 0) {
      return count;
    }
    return 1;
  }

  function handleHeaderConfigChange(nextHeaderConfig: any) {
    if (!props.selectedElement) return;
    const nextColumns = syncColumnTitlesByHeaderConfig(tableColumns.value, nextHeaderConfig);
    emit('update-element', {
      id: props.selectedElement.id,
      patch: {
        headerConfig: nextHeaderConfig,
        columns: nextColumns,
      } as any,
    });
  }

  function handleMultiHeaderToggle(checked: boolean) {
    updateField('enableMultiHeader', checked === true);
    if (checked !== true) {
      headerConfigModalOpen.value = false;
    }
  }

  function syncColumnTitlesByHeaderConfig(columns: any[], headerConfig: any) {
    const colCount = columns.length;
    const rowCount = Math.max(1, Number(headerConfig?.rowCount || 1));
    const owner: any[][] = Array.from({ length: rowCount }, () => Array.from({ length: colCount }, () => null));
    const cells = Array.isArray(headerConfig?.cells) ? headerConfig.cells : [];
    cells.forEach((item: any) => {
      const row = Math.max(0, Number(item?.row || 0));
      const col = Math.max(0, Number(item?.col || 0));
      const rowspan = Math.max(1, Number(item?.rowspan || 1));
      const colspan = Math.max(1, Number(item?.colspan || 1));
      if (row >= rowCount || col >= colCount || owner[row][col]) return;
      const maxRow = Math.min(rowCount, row + rowspan);
      const maxCol = Math.min(colCount, col + colspan);
      for (let r = row; r < maxRow; r += 1) {
        for (let c = col; c < maxCol; c += 1) {
          if (owner[r][c]) return;
        }
      }
      const next = { ...item, row, col, rowspan: maxRow - row, colspan: maxCol - col };
      for (let r = row; r < maxRow; r += 1) {
        for (let c = col; c < maxCol; c += 1) {
          owner[r][c] = next;
        }
      }
    });
    return columns.map((col, index) => {
      const bottomCell = owner[rowCount - 1]?.[index];
      const nextTitle =
        bottomCell && Number(bottomCell?.row ?? rowCount - 1) < rowCount - 1
          ? String(col?.title || `列${index + 1}`)
          : String(bottomCell?.title || col?.title || `列${index + 1}`);
      return { ...col, title: nextTitle };
    });
  }

  function handleColumnContentTypeChange(value: string) {
    const type = String(value || 'text');
    updateSelectedTableColumn('contentType', type);
    if (type === 'text') {
      updateSelectedTableColumn('autoWrap', true);
      updateSelectedTableColumn('autoFitFont', false);
      return;
    }
    if (type === 'number' || type === 'amount') {
      updateSelectedTableColumn('decimalPlaces', 2);
      updateSelectedTableColumn('roundHalfUp', true);
      updateSelectedTableColumn('amountType', 'CNY');
      updateSelectedTableColumn('enableFooterTotal', false);
      return;
    }
    if (type === 'image') {
      updateSelectedTableColumn('fillCell', true);
      updateSelectedTableColumn('contentScale', 100);
      updateSelectedTableColumn('imageFit', 'contain');
      return;
    }
    if (type === 'qrcode') {
      updateSelectedTableColumn('fillCell', true);
      updateSelectedTableColumn('contentScale', 100);
      updateSelectedTableColumn('qrLevel', 'M');
      updateSelectedTableColumn('qrRenderType', 'image/png');
      return;
    }
    if (type === 'barcode') {
      updateSelectedTableColumn('fillCell', true);
      updateSelectedTableColumn('contentScale', 100);
      updateSelectedTableColumn('barcodeFormat', 'CODE128');
    }
  }

  function isText(type: string) {
    return ['title', 'subtitle', 'text', 'date', 'pageNo', 'reportHeader', 'reportFooter'].includes(type);
  }

  function isReportBand(type: string) {
    return type === 'reportHeader' || type === 'reportFooter';
  }

  function isReportFooter(type: string) {
    return type === 'reportFooter';
  }

  function toColorHex(value: string) {
    if (!value) return '#ffffff';
    const color = String(value).trim();
    const lower = color.toLowerCase();
    if (lower === 'transparent' || lower === 'none' || lower === 'rgba(0,0,0,0)' || lower === 'rgba(0, 0, 0, 0)') {
      return '#ffffff';
    }
    if (/^#([0-9a-fA-F]{6})$/.test(color)) return color;
    if (/^#([0-9a-fA-F]{3})$/.test(color)) {
      const short = color.slice(1);
      return `#${short[0]}${short[0]}${short[1]}${short[1]}${short[2]}${short[2]}`;
    }
    return '#000000';
  }

  function handleColorInput(key: string, event: Event, fallback: string) {
    const value = (event?.target as HTMLInputElement | null)?.value || fallback;
    updateField(key, value);
  }

  function handleColumnColorInput(event: Event) {
    const value = (event?.target as HTMLInputElement | null)?.value || '#111111';
    updateSelectedTableColumn('fontColor', value);
  }

  function handleStyleColorInput(key: string, event: Event, fallback: string) {
    const value = (event?.target as HTMLInputElement | null)?.value || fallback;
    updateStyle(key, value);
  }

  function handlePlainTextBackgroundColorInput(event: Event) {
    const value = (event?.target as HTMLInputElement | null)?.value || '#ffffff';
    updateStyle('backgroundColor', value);
  }

  function handleFreeTableCellColorInput(key: 'color' | 'backgroundColor', event: Event, fallback: string) {
    const value = (event?.target as HTMLInputElement | null)?.value || fallback;
    updateSelectedFreeTableCell(key, value);
  }
</script>

<style scoped lang="less">
  .properties-panel {
    display: flex;
    flex-direction: column;
    height: calc(100vh - 130px);
    padding: 0;
    overflow: hidden;
    border-left: 1px solid #f0f0f0;
    background: #fff;

    .properties-panel__header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 8px;
      flex-shrink: 0;
      padding: 10px 12px;
      border-bottom: 1px solid #f0f0f0;
      background: #fafafa;
      min-height: 44px;
      box-sizing: border-box;
    }

    .properties-panel__title {
      font-size: 14px;
      font-weight: 600;
      color: rgba(0, 0, 0, 0.85);
      line-height: 1.4;
    }

    .properties-panel__header-actions {
      display: flex;
      align-items: center;
      gap: 4px;
      flex-shrink: 0;
    }

    .properties-panel__icon-btn {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 32px;
      height: 32px;
      padding: 0;
      margin: 0;
      border: 1px solid transparent;
      border-radius: 6px;
      background: transparent;
      cursor: pointer;
      color: rgba(0, 0, 0, 0.55);
      line-height: 1;

      &:hover {
        color: #1677ff;
        background: rgba(22, 119, 255, 0.08);
        border-color: rgba(22, 119, 255, 0.2);
      }
    }

    .properties-panel__icon-svg {
      display: block;
      flex-shrink: 0;
    }

    .properties-panel__body {
      flex: 1;
      min-height: 0;
      overflow: auto;
      padding: 12px;
    }

    .panel-title {
      font-size: 13px;
      font-weight: 600;
      margin-bottom: 10px;
      cursor: pointer;
    }

    .panel-subtitle {
      margin-bottom: 6px;
      font-size: 12px;
      color: #666;
      cursor: pointer;
    }

    .section-card {
      border: 1px solid #f0f0f0;
      border-radius: 6px;
      padding: 8px;
      margin-bottom: 8px;
      background: #fff;

      > summary {
        list-style: none;
      }

      > summary::-webkit-details-marker {
        display: none;
      }
    }

    .table-column-list {
      display: flex;
      flex-direction: column;
      gap: 8px;

      .table-column-item {
        border: 1px solid #f0f0f0;
        border-radius: 6px;
        padding: 8px;
        background: #fafafa;
      }
    }

    .color-input-row {
      display: flex;
      align-items: center;
      gap: 8px;

      :deep(.ant-input-group-wrapper) {
        flex: 1;
      }
    }

    /** 原生取色器：与文本框并排展示 */
    .native-color-picker-trigger {
      flex: 0 0 28px;
      width: 28px;
      height: 28px;
      padding: 0;
      border: 1px solid #d9d9d9;
      border-radius: 4px;
      cursor: pointer;
      box-sizing: border-box;
      overflow: hidden;
      background: #fff;
    }

    .column-width-entry-btn {
      margin-top: 4px;
      font-weight: 600;
    }

    .column-width-modal {
      .column-width-modal-tip {
        margin-bottom: 10px;
        padding: 8px 10px;
        border-radius: 6px;
        background: #f6ffed;
        border: 1px solid #b7eb8f;
        color: #389e0d;
        font-size: 12px;
      }

      .column-width-list {
        max-height: 420px;
        overflow: auto;
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 10px;
        padding-right: 4px;
      }

      .column-width-item {
        border: 1px solid #f0f0f0;
        border-radius: 8px;
        padding: 10px;
        background: #fafafa;
        display: flex;
        flex-direction: column;
        gap: 8px;
      }

      .column-width-item-head {
        display: flex;
        align-items: center;
        justify-content: space-between;
      }

      .column-width-index {
        display: inline-block;
        min-width: 48px;
        text-align: center;
        border-radius: 10px;
        padding: 0 8px;
        line-height: 20px;
        font-size: 12px;
        color: #1677ff;
        background: #e6f4ff;
      }

      .column-width-title {
        color: #666;
        font-size: 12px;
      }

      .column-width-modal-footer {
        margin-top: 12px;
        text-align: right;
      }
    }

    .free-table-merge-tip {
      font-size: 12px;
      color: #666;
      line-height: 1.5;
      padding: 6px 8px;
      background: #fafafa;
      border-radius: 6px;
      border: 1px dashed #d9d9d9;
    }

    /* 行数/列数并排：避免 addon 组合框在窄侧栏溢出重叠 */
    .free-table-dim-row {
      display: grid;
      grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
      gap: 8px;
      width: 100%;
    }

    .free-table-dim-item {
      display: flex;
      align-items: center;
      gap: 6px;
      min-width: 0;
    }

    .free-table-dim-label {
      flex: 0 0 auto;
      font-size: 12px;
      color: rgba(0, 0, 0, 0.55);
      white-space: nowrap;
    }

    .free-table-dim-item :deep(.ant-input-number) {
      flex: 1;
      min-width: 0;
      width: 100%;
    }

    .free-table-track-head {
      font-size: 12px;
      font-weight: 600;
      color: rgba(0, 0, 0, 0.75);
      margin-top: 4px;
    }

    .free-table-track-row {
      display: flex;
      align-items: center;
      gap: 10px;
      flex-wrap: wrap;
    }

    .free-table-track-label {
      flex: 0 0 52px;
      font-size: 12px;
      color: rgba(0, 0, 0, 0.55);
    }

    .free-table-border-switch-grid {
      display: grid;
      grid-template-columns: repeat(4, minmax(0, 1fr));
      gap: 8px 6px;
      align-items: center;
      width: 100%;
    }

    .free-table-border-switch-cell {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 4px;
    }

    .free-table-border-switch-label {
      font-size: 11px;
      color: rgba(0, 0, 0, 0.55);
    }

    /** 自由表格线型下拉：与画笔样式类似，用线条预览 */
    .free-table-line-style-option {
      display: flex;
      align-items: center;
      gap: 8px;
      min-height: 26px;
    }

    .free-table-line-preview {
      flex: 0 0 56px;
      height: 0;
      border-top: 2px solid rgba(0, 0, 0, 0.82);
      box-sizing: border-box;
    }

    .free-table-line-preview--solid {
      border-top-style: solid;
    }

    .free-table-line-preview--dashed {
      border-top-style: dashed;
    }

    .free-table-line-preview--dotted {
      border-top-style: dotted;
    }

    .free-table-line-preview--dash_dot {
      border-top: none;
      height: 2px;
      background: repeating-linear-gradient(
        90deg,
        rgba(0, 0, 0, 0.82) 0 5px,
        transparent 5px 7px,
        rgba(0, 0, 0, 0.82) 7px 8px,
        transparent 8px 14px
      );
    }

    .free-table-line-preview--double_dash_dot {
      border-top: none;
      height: 2px;
      background: repeating-linear-gradient(
        90deg,
        rgba(0, 0, 0, 0.82) 0 5px,
        transparent 5px 7px,
        rgba(0, 0, 0, 0.82) 7px 8px,
        transparent 8px 9px,
        rgba(0, 0, 0, 0.82) 9px 10px,
        transparent 10px 16px
      );
    }

    .bind-param-compact {
      display: flex;
      width: 100%;
      align-items: stretch;
      border: 1px solid #d9d9d9;
      border-radius: 6px;
      overflow: hidden;
      background: #fff;

      .bind-param-compact__addon {
        flex: 0 0 80px;
        max-width: 88px;
        padding: 0 10px;
        display: flex;
        align-items: center;
        background: #fafafa;
        border-right: 1px solid #d9d9d9;
        font-size: 14px;
        color: rgba(0, 0, 0, 0.88);
        line-height: 32px;
      }

      .bind-param-compact__select {
        flex: 1;
        min-width: 0;

        :deep(.ant-select-selector) {
          border: none !important;
          box-shadow: none !important;
        }
      }
    }
  }
</style>
