<template>
  <div>
    <BasicTable @register="registerTable">
      <template #tableTitle>
        <a-space>
          <a-button type="primary" @click="openCreate" v-auth="'print:bizBind:add'">新增绑定</a-button>
          <a-button @click="openPrintBizWhitelist" :loading="whitelistLoading" v-auth="'print:bizBind:whitelist'">
            打印业务白名单
          </a-button>
        </a-space>
      </template>
      <template #action="{ record }">
        <TableAction
          :actions="[
            {
              label: '编辑',
              onClick: () => openEdit(record),
              auth: 'print:bizBind:edit',
            },
            {
              label: '删除',
              color: 'error',
              popConfirm: {
                title: '确认删除该绑定？',
                confirm: () => handleDelete(record),
              },
              auth: 'print:bizBind:delete',
            },
          ]"
        />
      </template>
    </BasicTable>

    <BasicModal
      @register="registerModal"
      :title="modalTitle"
      width="1000px"
      @ok="submitModal"
      :confirm-loading="modalSubmitLoading"
      destroy-on-close
      wrap-class-name="biz-bind-modal-wrap"
    >
      <a-spin :spinning="modalDataLoading || parseLoading">
        <div class="biz-bind-form">
          <a-alert
            type="info"
            show-icon
            class="bind-alert"
            message="配置说明"
            description="按卡片顺序操作：先选业务与模板 → 若模板含明细占位，在「明细数据来源」中选择主实体上的集合/嵌套对象 → 点击「解析模板占位字段」→ 在下方「主表参数」「明细与表格」中分别为每个占位选择业务字段。主表参数一般映射主实体字段；明细占位可选带「明细前缀」的路径（如 lines.qty）。支持 lines.qty（首行）或 lines.0.qty。"
          />

          <a-card title="基础信息" size="small" :bordered="true" class="bind-card">
            <a-form layout="vertical" class="bind-card-form">
              <a-row :gutter="16">
                <a-col :span="12">
                  <a-form-item
                    label="业务"
                    required
                    extra="业务编码为菜单 id；后端按 print_biz_perm_entity 或菜单 component 推断主实体并反射主表字段。"
                  >
                    <a-select
                      v-model:value="form.bizCode"
                      :options="bizSelectOptions"
                      placeholder="选择业务"
                      show-search
                      option-filter-prop="label"
                      :disabled="isEditMode"
                      @change="onBizCodeChange"
                    />
                  </a-form-item>
                </a-col>
                <a-col :span="12">
                  <a-form-item label="打印模板" required extra="请选择已发布的原生打印模板">
                    <a-select
                      v-model:value="form.templateId"
                      :options="tplSelectOptions"
                      placeholder="选择模板"
                      show-search
                      option-filter-prop="label"
                      @change="onTemplateChange"
                    />
                  </a-form-item>
                </a-col>
              </a-row>
              <a-form-item label="备注">
                <a-input v-model:value="form.remark" placeholder="可选" />
              </a-form-item>
            </a-form>
          </a-card>

          <a-card size="small" :bordered="true" class="bind-card">
            <template #title>
              <span class="bind-card-head">明细数据来源</span>
              <span class="bind-card-head-extra">（可选）</span>
            </template>
            <template #extra>
              <span class="bind-card-sub">有明细/表格占位时需配置</span>
            </template>
            <p class="bind-card-desc">
              选择主实体类上的明细集合属性（如 List&lt;明细实体&gt;）或嵌套对象；系统将明细类字段并入下方「明细与表格」中的业务字段下拉。
            </p>
            <a-select
              v-model:value="selectedDetailProperty"
              allow-clear
              show-search
              option-filter-prop="label"
              placeholder="无需明细请留空"
              :options="detailSlotSelectOptions"
              :loading="detailFieldsLoading"
              style="width: 100%"
              @change="onDetailSlotChange"
            />
          </a-card>

          <a-card size="small" :bordered="true" class="bind-card bind-card--mapping">
            <template #title>
              <span class="bind-card-head">字段映射</span>
            </template>
            <template #extra>
              <a-space wrap>
                <a-button type="primary" ghost size="small" @click="reloadTemplateFields" :loading="parseLoading">
                  解析模板占位字段
                </a-button>
                <a-button
                  size="small"
                  @click="autoMatchFields"
                  :disabled="(!bizFields.length && !detailBizFields.length) || !tplFields.length"
                >
                  同名自动匹配
                </a-button>
                <a-button size="small" @click="addPlaceholderParamRow">添加空占位</a-button>
              </a-space>
            </template>

            <div v-if="!form.templateId" class="bind-placeholder">请先在上方的「基础信息」中选择打印模板</div>

            <template v-else-if="tplFields.length">
              <div class="bind-map-section">
                <div class="bind-section-bar">
                  <span class="bind-section-title">① 主表参数</span>
                  <span class="bind-section-hint">对应模板 dataBinding.params / 画布参数；请选择主实体 JSON 字段</span>
                </div>
                <a-table
                  v-if="mappingRowsParam.length"
                  size="small"
                  row-key="templateField"
                  :pagination="false"
                  :columns="mapTableColumnsParam"
                  :data-source="mappingRowsParam"
                  bordered
                  class="bind-map-table"
                >
                  <template #bodyCell="{ column, record }">
                    <template v-if="column.key === 'bizField'">
                      <a-select
                        v-model:value="record.bizField"
                        :options="bizFieldOptionsMain"
                        allow-clear
                        show-search
                        option-filter-prop="label"
                        style="width: 100%"
                        placeholder="选择主表业务字段"
                      />
                    </template>
                  </template>
                </a-table>
                <a-empty v-else class="bind-empty" description="本模板未解析到「参数」类占位" />
              </div>

              <div class="bind-map-section bind-map-section--detail">
                <div class="bind-section-bar">
                  <span class="bind-section-title">② 明细与表格列</span>
                  <span class="bind-section-hint">对应明细字段、表格列等；可选主表字段或上方明细来源生成的前缀字段</span>
                </div>
                <a-table
                  v-if="mappingRowsDetail.length"
                  size="small"
                  row-key="templateField"
                  :pagination="false"
                  :columns="mapTableColumnsDetail"
                  :data-source="mappingRowsDetail"
                  bordered
                  class="bind-map-table"
                >
                  <template #bodyCell="{ column, record }">
                    <template v-if="column.key === 'tplKind'">
                      {{ templateFieldKindLabel(record.elementType) }}
                    </template>
                    <template v-if="column.key === 'bizField'">
                      <a-select
                        v-model:value="record.bizField"
                        :options="bizFieldOptions"
                        allow-clear
                        show-search
                        option-filter-prop="label"
                        style="width: 100%"
                        placeholder="选择业务字段"
                      />
                    </template>
                  </template>
                </a-table>
                <a-empty v-else class="bind-empty" description="本模板未解析到明细/表格列占位" />
              </div>
            </template>

            <a-empty
              v-else-if="form.templateId && !parseLoading"
              class="bind-empty"
              description="请点击右上角「解析模板占位字段」或切换模板"
            />
          </a-card>

          <a-card title="映射预览（可选）" size="small" :bordered="true" class="bind-card">
            <a-textarea
              v-model:value="previewBizJson"
              placeholder='粘贴业务 JSON，例如：{"barcode":"TEST001","materialName":"胶料A"}'
              :rows="4"
            />
            <div style="margin-top: 10px">
              <a-button type="dashed" @click="runPreview" :loading="previewLoading">生成打印数据预览</a-button>
            </div>
            <pre v-if="previewResult" class="preview-pre">{{ previewResult }}</pre>
          </a-card>
        </div>
      </a-spin>
    </BasicModal>

    <BasicModal
      @register="registerWhitelistModal"
      title="打印业务白名单"
      width="760px"
      @ok="submitWhitelistModal"
      :confirm-loading="whitelistSubmitLoading"
      destroy-on-close
    >
      <a-spin :spinning="whitelistLoading">
        <a-alert
          type="info"
          show-icon
          style="margin-bottom: 12px"
          message="说明"
          description="勾选允许的菜单 id，保存时会写入 print_biz_perm_entity（能推断出实体类的菜单）。打开弹窗已优化为不再加载全量业务目录。白名单为空时「新增绑定」下拉仅展示表里已有映射；白名单非空时展示勾选中且能解析的菜单。"
        />
        <a-space style="margin-bottom: 8px">
          <a-button size="small" @click="expandWhitelistTree(true)">展开全部</a-button>
          <a-button size="small" @click="expandWhitelistTree(false)">折叠全部</a-button>
          <a-button size="small" @click="checkedKeysWhitelist = []">清空勾选</a-button>
        </a-space>
        <BasicTree
          ref="whitelistTreeRef"
          checkable
          :treeData="whitelistTreeData"
          :checkedKeys="checkedKeysWhitelist"
          :expandedKeys="expandedKeysWhitelist"
          :selectedKeys="selectedKeysWhitelist"
          :clickRowToExpand="false"
          :checkStrictly="true"
          title="系统菜单（权限树）"
          @check="onWhitelistCheck"
        >
          <template #title="{ slotTitle, ruleFlag }">
            {{ slotTitle }}
            <Icon v-if="ruleFlag" icon="ant-design:align-left-outlined" style="margin-left: 5px; color: red" />
          </template>
        </BasicTree>
      </a-spin>
    </BasicModal>
  </div>
</template>

<script lang="ts" setup>
  import { computed, ref, unref } from 'vue';
  import { BasicTable, TableAction, useTable } from '/@/components/Table';
  import { BasicModal, useModal } from '/@/components/Modal';
  import { BasicTree, TreeItem } from '/@/components/Tree';
  import { Icon } from '/@/components/Icon';
  import { useMessage } from '/@/hooks/web/useMessage';
  import { useI18n } from '/@/hooks/web/useI18n';
  import { columns } from './bizTemplateBind.data';
  import * as Api from './bizTemplateBind.api';
  import { list as tplList } from '../template/printTemplate.api';
  import { queryTreeListForRole } from '/@/views/system/role/role.api';

  const { createMessage } = useMessage();
  const { t } = useI18n();

  interface BizTypeItem {
    bizCode: string;
    bizName: string;
    fields: { fieldKey: string; label: string; description?: string }[];
  }

  interface TplFieldItem {
    bindField: string;
    elementType?: string;
    titleHint?: string;
  }

  interface MappingRow {
    templateField: string;
    bizField?: string;
    elementType?: string;
    titleHint?: string;
  }

  interface DetailSlotItem {
    propertyName: string;
    itemEntityClassFqn: string;
    slotKind: string;
    label: string;
  }

  const bizTypesRef = ref<BizTypeItem[]>([]);
  const tplListRef = ref<{ id: string; templateCode: string; templateName: string }[]>([]);
  /** 弹窗内：业务列表 + 模板下拉并行加载中（不再阻塞 openModal，避免点按钮好几秒才出框） */
  const modalDataLoading = ref(false);
  const parseLoading = ref(false);
  const modalSubmitLoading = ref(false);
  const previewLoading = ref(false);
  const previewBizJson = ref('');
  const previewResult = ref('');

  const form = ref({
    id: '' as string | undefined,
    bizCode: undefined as string | undefined,
    bizName: '' as string | undefined,
    templateId: undefined as string | undefined,
    remark: '' as string | undefined,
  });

  const tplFields = ref<TplFieldItem[]>([]);
  const bizFields = ref<BizTypeItem['fields']>([]);
  const mappingRows = ref<MappingRow[]>([]);
  const detailSlots = ref<DetailSlotItem[]>([]);
  const selectedDetailProperty = ref<string | undefined>(undefined);
  const detailBizFields = ref<BizTypeItem['fields']>([]);
  const detailFieldsLoading = ref(false);

  const isEditMode = ref(false);
  const modalTitle = computed(() => (unref(isEditMode) ? '编辑业务打印绑定' : '新增业务打印绑定'));

  const bizSelectOptions = computed(() =>
    unref(bizTypesRef).map((b) => ({
      label: `${b.bizName}（${b.bizCode}）`,
      value: b.bizCode,
    })),
  );

  const tplSelectOptions = computed(() =>
    unref(tplListRef).map((t) => ({
      label: `${t.templateName}（${t.templateCode}）`,
      value: t.id,
    })),
  );

  const detailSlotSelectOptions = computed(() =>
    unref(detailSlots).map((s) => ({
      label: `${s.label}（${s.propertyName} · ${s.slotKind}）`,
      value: s.propertyName,
    })),
  );

  const bizFieldOptionsMain = computed(() =>
    unref(bizFields).map((f) => ({
      label: f.label ? `${f.label}（${f.fieldKey}）` : f.fieldKey,
      value: f.fieldKey,
    })),
  );

  /** 主表 + 明细前缀字段（用于明细/表格占位） */
  const bizFieldOptions = computed(() => {
    const main = unref(bizFields).map((f) => ({
      label: f.label ? `${f.label}（${f.fieldKey}）` : f.fieldKey,
      value: f.fieldKey,
    }));
    const detail = unref(detailBizFields).map((f) => ({
      label: f.label ? `${f.label}（${f.fieldKey}）` : f.fieldKey,
      value: f.fieldKey,
    }));
    return [...main, ...detail];
  });

  /** 主表参数行：模板 elementType 为 param */
  const mappingRowsParam = computed(() =>
    unref(mappingRows).filter((r) => (r.elementType || '') === 'param'),
  );

  /** 非参数占位（明细字段、表格列、其它画布元素） */
  const mappingRowsDetail = computed(() =>
    unref(mappingRows).filter((r) => (r.elementType || '') !== 'param'),
  );

  function templateFieldKindLabel(t?: string) {
    const m: Record<string, string> = {
      param: '主表·参数',
      detailField: '模板·明细',
      column: '表格列',
    };
    return m[t || ''] || (t || '—');
  }

  const mapTableColumnsParam = [
    { title: '模板参数占位（bindField）', dataIndex: 'templateField', width: 220 },
    { title: '标题/提示', dataIndex: 'titleHint', ellipsis: true },
    { title: '业务字段（主表）', key: 'bizField', width: 280 },
  ];

  const mapTableColumnsDetail = [
    { title: '模板类型', key: 'tplKind', width: 104, ellipsis: true },
    { title: '模板占位（bindField）', dataIndex: 'templateField', width: 188 },
    { title: '标题/提示', dataIndex: 'titleHint', ellipsis: true },
    { title: '业务字段', key: 'bizField', width: 260 },
  ];

  const [registerTable, { reload }] = useTable({
    title: '业务打印绑定',
    api: Api.list,
    columns,
    useSearchForm: false,
    showTableSetting: true,
    bordered: true,
    showIndexColumn: true,
    // 必须与模板插槽 #action 对应，否则操作列不会渲染（useTable 无 useListPage 的默认 slots）
    actionColumn: {
      width: 160,
      title: '操作',
      fixed: 'right',
      dataIndex: 'action',
      slots: { customRender: 'action' },
    },
  });

  const [registerModal, { openModal, closeModal }] = useModal();
  const [registerWhitelistModal, { openModal: openWhitelistDlg, closeModal: closeWhitelistDlg }] = useModal();

  const whitelistLoading = ref(false);
  const whitelistSubmitLoading = ref(false);
  const whitelistTreeRef = ref<InstanceType<typeof BasicTree> | null>(null);
  const whitelistTreeData = ref<TreeItem[]>([]);
  const allWhitelistTreeKeys = ref<string[]>([]);
  const checkedKeysWhitelist = ref<any>([]);
  const expandedKeysWhitelist = ref<any>([]);
  const selectedKeysWhitelist = ref<any>([]);

  /** 将菜单树标题中的 t('...') 表达式转为文案（与角色授权树一致） */
  function translateWhitelistTitle(data: TreeItem[] | undefined): TreeItem[] {
    if (data?.length) {
      data.forEach((item) => {
        if (item.slotTitle && typeof item.slotTitle === 'string' && item.slotTitle.includes("t('")) {
          try {
            item.slotTitle = new Function('t', `return ${item.slotTitle}`)(t) as string;
          } catch {
            /* 忽略解析失败 */
          }
        }
        if (item.children?.length) {
          translateWhitelistTitle(item.children as TreeItem[]);
        }
      });
    }
    return data ?? [];
  }

  function expandWhitelistTree(expand: boolean) {
    expandedKeysWhitelist.value = expand ? [...unref(allWhitelistTreeKeys)] : [];
  }

  function onWhitelistCheck(o: { checked?: any } | any) {
    checkedKeysWhitelist.value = o?.checked !== undefined ? o.checked : o;
  }

  /** 树勾选键转为字符串数组（兼容 checkStrictly） */
  function normalizeCheckedKeys(keys: unknown): string[] {
    if (Array.isArray(keys)) {
      return keys.map((k) => String(k));
    }
    if (keys && typeof keys === 'object' && 'checked' in (keys as object)) {
      const c = (keys as { checked?: unknown }).checked;
      if (Array.isArray(c)) {
        return c.map((k) => String(k));
      }
    }
    return [];
  }

  async function openPrintBizWhitelist() {
    whitelistLoading.value = true;
    try {
      const [treeResult, wl] = await Promise.all([queryTreeListForRole(), Api.getPermWhitelist()]);
      whitelistTreeData.value = translateWhitelistTitle(treeResult.treeList);
      allWhitelistTreeKeys.value = treeResult.ids || [];
      expandedKeysWhitelist.value = treeResult.ids || [];
      checkedKeysWhitelist.value = wl?.permIds?.length ? [...wl.permIds] : [];
      openWhitelistDlg(true);
    } finally {
      whitelistLoading.value = false;
    }
  }

  async function submitWhitelistModal() {
    whitelistSubmitLoading.value = true;
    try {
      const tree = unref(whitelistTreeRef) as any;
      let raw = tree?.getCheckedKeys?.() ?? unref(checkedKeysWhitelist);
      const permIds = normalizeCheckedKeys(raw);
      await Api.savePermWhitelist({ permIds });
      createMessage.success('保存成功');
      closeWhitelistDlg();
    } finally {
      whitelistSubmitLoading.value = false;
    }
  }

  async function loadBizTypes() {
    const res = await Api.bizTypesForBinding();
    bizTypesRef.value = res || [];
  }

  async function loadAllTemplates() {
    const res = await tplList({ pageNo: 1, pageSize: 500 });
    tplListRef.value = res?.records ?? [];
  }

  async function refreshDetailSlots(code: string | undefined) {
    detailSlots.value = [];
    selectedDetailProperty.value = undefined;
    detailBizFields.value = [];
    if (!code) {
      return;
    }
    try {
      detailSlots.value = (await Api.detailSlots(code)) || [];
    } catch {
      detailSlots.value = [];
    }
  }

  async function onDetailSlotChange(propertyName: string | undefined) {
    selectedDetailProperty.value = propertyName;
    if (!propertyName || !form.value.bizCode) {
      detailBizFields.value = [];
      return;
    }
    const slot = unref(detailSlots).find((s) => s.propertyName === propertyName);
    const kind = slot?.slotKind || 'LIST';
    detailFieldsLoading.value = true;
    try {
      const list = await Api.bizFieldsForDetailSlot({
        bizCode: form.value.bizCode,
        detailProperty: propertyName,
        slotKind: kind,
      });
      detailBizFields.value = (list || []) as BizTypeItem['fields'];
    } catch {
      detailBizFields.value = [];
    } finally {
      detailFieldsLoading.value = false;
    }
  }

  async function onBizCodeChange(code: string) {
    const hit = unref(bizTypesRef).find((b) => b.bizCode === code);
    bizFields.value = hit?.fields ?? [];
    form.value.bizName = hit?.bizName;
    await refreshDetailSlots(code);
  }

  async function onTemplateChange() {
    tplFields.value = [];
    mappingRows.value = [];
    await reloadTemplateFields();
  }

  async function reloadTemplateFields() {
    const tid = form.value.templateId;
    if (!tid) {
      tplFields.value = [];
      mappingRows.value = [];
      return;
    }
    parseLoading.value = true;
    try {
      const list = (await Api.parseTemplateFields(tid)) as TplFieldItem[];
      tplFields.value = list || [];
      rebuildMappingRows();
    } finally {
      parseLoading.value = false;
    }
  }

  function rebuildMappingRows() {
    const saved = unref(savedMappingRef);
    const tplList = unref(tplFields);
    const tplByField = new Map(tplList.map((t) => [t.bindField, t]));

    // 先按已保存 JSON 的顺序收录键（含「模板未再声明」的占位如 Parameter3 + 空 bizField），避免仅依赖解析结果而丢行
    const orderedKeys: string[] = [];
    const seen = new Set<string>();
    for (const s of saved) {
      const k = (s.templateField || '').trim();
      if (!k || seen.has(k)) continue;
      seen.add(k);
      orderedKeys.push(k);
    }
    for (const t of tplList) {
      const k = (t.bindField || '').trim();
      if (!k || seen.has(k)) continue;
      seen.add(k);
      orderedKeys.push(k);
    }

    mappingRows.value = orderedKeys.map((templateField) => {
      const t = tplByField.get(templateField);
      const hit = saved.find((x) => x.templateField === templateField);
      return {
        templateField,
        bizField: hit?.bizField ?? '',
        elementType: t?.elementType || 'param',
        titleHint:
          t?.titleHint ||
          (!t ? '已保存映射（当前模板 JSON 未声明该占位，仍可按空业务字段输出）' : ''),
      };
    });
  }

  const savedMappingRef = ref<{ templateField: string; bizField?: string }[]>([]);

  function autoMatchFields() {
    const merged = [...unref(bizFields), ...unref(detailBizFields)];
    const set = new Map(merged.map((f) => [f.fieldKey, f.fieldKey]));
    for (const row of unref(mappingRows)) {
      if (set.has(row.templateField)) {
        row.bizField = row.templateField;
      }
    }
    mappingRows.value = [...unref(mappingRows)];
  }

  /** 手动增加仅输出空值的模板占位（写入 savedMappingRef 并重建行，避免再次「解析模板」时丢失） */
  function addPlaceholderParamRow() {
    const raw = window.prompt(
      '请输入模板参数 bindField（如 Parameter3）。业务字段将固定为空字符串，不参与业务 JSON 取值。',
      'Parameter3',
    );
    const k = (raw || '').trim();
    if (!k) return;
    if (unref(savedMappingRef).some((x) => x.templateField === k) || unref(mappingRows).some((r) => r.templateField === k)) {
      createMessage.warning('该占位已存在');
      return;
    }
    savedMappingRef.value = [...unref(savedMappingRef), { templateField: k, bizField: '' }];
    rebuildMappingRows();
  }

  function buildFieldMappingJson() {
    const arr = unref(mappingRows)
      .filter((r) => r.templateField)
      .map((r) => ({ templateField: r.templateField, bizField: r.bizField ?? '' }));
    return JSON.stringify(arr);
  }

  async function openCreate() {
    isEditMode.value = false;
    savedMappingRef.value = [];
    form.value = { id: undefined, bizCode: undefined, bizName: undefined, templateId: undefined, remark: undefined };
    tplFields.value = [];
    bizFields.value = [];
    mappingRows.value = [];
    detailSlots.value = [];
    selectedDetailProperty.value = undefined;
    detailBizFields.value = [];
    previewBizJson.value = '';
    previewResult.value = '';
    openModal(true);
    modalDataLoading.value = true;
    try {
      await Promise.all([loadBizTypes(), loadAllTemplates()]);
    } finally {
      modalDataLoading.value = false;
    }
  }

  async function openEdit(record: Recordable) {
    isEditMode.value = true;
    try {
      savedMappingRef.value = JSON.parse(record.fieldMappingJson || '[]');
    } catch {
      savedMappingRef.value = [];
    }
    form.value = {
      id: record.id,
      bizCode: record.bizCode,
      bizName: record.bizName,
      templateId: record.templateId,
      remark: record.remark,
    };
    previewBizJson.value = '';
    previewResult.value = '';
    tplFields.value = [];
    mappingRows.value = [];
    bizFields.value = [];
    detailSlots.value = [];
    selectedDetailProperty.value = undefined;
    detailBizFields.value = [];
    openModal(true);
    modalDataLoading.value = true;
    try {
      await Promise.all([loadBizTypes(), loadAllTemplates()]);
      await onBizCodeChange(record.bizCode as string);
      await reloadTemplateFields();
    } finally {
      modalDataLoading.value = false;
    }
  }

  async function submitModal() {
    if (!form.value.bizCode) {
      createMessage.warning('请选择业务');
      return Promise.reject();
    }
    if (!form.value.templateId) {
      createMessage.warning('请选择打印模板');
      return Promise.reject();
    }
    modalSubmitLoading.value = true;
    try {
      const payload = {
        id: form.value.id,
        bizCode: form.value.bizCode,
        bizName: form.value.bizName,
        templateId: form.value.templateId,
        remark: form.value.remark,
        fieldMappingJson: buildFieldMappingJson(),
      };
      if (unref(isEditMode)) {
        await Api.edit(payload);
      } else {
        await Api.add(payload);
      }
      closeModal();
      reload();
    } finally {
      modalSubmitLoading.value = false;
    }
  }

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

  async function runPreview() {
    if (!form.value.bizCode) {
      createMessage.warning('请先选择业务；预览读取的是服务器已保存的绑定配置');
      return;
    }
    let obj: Record<string, unknown> = {};
    try {
      obj = previewBizJson.value ? (JSON.parse(previewBizJson.value) as Record<string, unknown>) : {};
    } catch {
      previewResult.value = '业务 JSON 格式不正确';
      return;
    }
    previewLoading.value = true;
    try {
      const res = await Api.previewMappedData({ bizCode: form.value.bizCode, bizDataJson: obj });
      previewResult.value = JSON.stringify(res, null, 2);
    } catch (e: unknown) {
      previewResult.value = e instanceof Error ? e.message : String(e);
    } finally {
      previewLoading.value = false;
    }
  }
</script>

<style scoped>
  .biz-bind-form {
    max-height: calc(100vh - 220px);
    overflow-y: auto;
    padding-right: 4px;
  }

  .bind-alert {
    margin-bottom: 12px;
  }

  .bind-card {
    margin-bottom: 12px;
    border-radius: 6px;
  }

  .bind-card :deep(.ant-card-head) {
    min-height: 42px;
    padding: 0 12px;
  }

  .bind-card :deep(.ant-card-body) {
    padding: 12px 16px;
  }

  .bind-card-form :deep(.ant-form-item) {
    margin-bottom: 12px;
  }

  .bind-card-head {
    font-weight: 600;
    font-size: 14px;
  }

  .bind-card-head-extra {
    font-weight: 400;
    font-size: 13px;
    color: rgba(0, 0, 0, 0.45);
    margin-left: 6px;
  }

  .bind-card-sub {
    font-size: 12px;
    color: rgba(0, 0, 0, 0.45);
  }

  .bind-card-desc {
    margin: 0 0 10px;
    font-size: 13px;
    color: rgba(0, 0, 0, 0.65);
    line-height: 1.5;
  }

  .bind-placeholder {
    padding: 16px;
    text-align: center;
    color: rgba(0, 0, 0, 0.45);
    background: #fafafa;
    border-radius: 4px;
    margin-bottom: 8px;
  }

  .bind-map-section {
    margin-bottom: 16px;
  }

  .bind-map-section--detail {
    margin-bottom: 0;
    padding-top: 8px;
    border-top: 1px dashed #f0f0f0;
  }

  .bind-section-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 8px 12px;
    margin-bottom: 8px;
  }

  .bind-section-title {
    font-weight: 600;
    font-size: 13px;
    color: rgba(0, 0, 0, 0.85);
  }

  .bind-section-hint {
    font-size: 12px;
    color: rgba(0, 0, 0, 0.45);
  }

  .bind-map-table {
    margin-bottom: 0;
  }

  .bind-empty {
    margin: 8px 0;
  }

  .preview-pre {
    background: #f5f5f5;
    padding: 12px;
    border-radius: 4px;
    max-height: 240px;
    overflow: auto;
    font-size: 12px;
    margin-top: 10px;
  }
</style>
