<script setup lang="ts">
import { computed, h, onMounted, reactive, ref } from 'vue';
import { NButton, NSpace, NSwitch, NTag, type DataTableColumns, type FormInst, type SelectOption, type SelectRenderOption } from 'naive-ui';
import ColorInput from '../page/home/components/ColorInput.vue';
import MediaSelectInput from './MediaSelectInput.vue';
import RichTextEditor from './RichTextEditor.vue';
import {
  fetchCreateSection,
  fetchDeleteSection,
  fetchGetSections,
  fetchSortSections,
  fetchUpdateSection
} from '@/service/api';

/**
 * Section 区块管理（可复用）
 *
 * 通过 pageType / productId 管理任意页面的区块列表：
 * - 首页：pageType='home'
 * - 产品详情：pageType='product_detail' + productId
 */
defineOptions({ name: 'SectionManager' });

const props = withDefaults(
  defineProps<{
    pageType: string;
    productId?: number | null;
    title?: string;
  }>(),
  { productId: null, title: '区块管理' }
);

const loading = ref(false);
const submitLoading = ref(false);
const dialogVisible = ref(false);
const dialogTitle = ref('');
const formRef = ref<FormInst | null>(null);
const tableData = ref<Api.Www.SectionBlock[]>([]);

type LayoutOption = SelectOption & { desc: string };

const layoutOptions: LayoutOption[] = [
  { label: '主视觉分屏', value: 'hero_split', desc: '左文案右图片，首页首屏大图' },
  { label: '通栏大横幅', value: 'full_banner', desc: '全宽背景图，标题文案居中' },
  { label: '特性网格', value: 'feature_grid', desc: '多列卡片，展示产品特性卖点' },
  { label: '图文分栏', value: 'split_content', desc: '图片与文案左右并排介绍' },
  { label: '图片画廊', value: 'gallery', desc: '多张图片网格展示' },
  { label: '数据统计', value: 'stats', desc: '大数字展示关键指标数据' },
  { label: '视频展示', value: 'video_showcase', desc: '以视频为主的视觉区块' },
  { label: '标题+媒体', value: 'title_media', desc: '顶部标题文案 + 下方图片/视频，对齐方向可配置' },
  { label: '马赛克拼贴', value: 'mosaic', desc: '一大带多小的不规则网格，每个槽位支持图片/视频、中英双素材' },
  { label: '滚动叙事', value: 'scroll_narrative', desc: '随滚动逐步展开的叙事内容' },
  { label: '引言评价', value: 'quote', desc: '引用语 / 用户评价展示' },
  { label: '参数规格表', value: 'spec_table', desc: '产品技术参数规格表格' }
];

function layoutLabel(value: string) {
  return (layoutOptions.find(o => o.value === value)?.label as string) || value;
}

function renderLayoutLabel(option: SelectOption, _selected: boolean) {
  const opt = option as LayoutOption;
  // 始终渲染 label + desc（收起态的 desc 由 CSS 隐藏，见底部 <style>）
  return h('div', { class: 'flex flex-col py-2px' }, [
    h('span', { class: 'text-14px' }, opt.label as string),
    h('span', { class: 'text-12px opacity-60 layout-desc' }, opt.desc)
  ]);
}

const renderLayoutOption: SelectRenderOption = ({ node, option }) => {
  const opt = option as LayoutOption;
  return h('div', {
    style: opt.value === layoutOptions[layoutOptions.length - 1].value
      ? ''
      : 'border-bottom: 1px solid rgba(0, 0, 0, 0.06);'
  }, [node]);
};

const themeOptions = [
  { label: '浅色', value: 'light' },
  { label: '深色', value: 'dark' }
];

const formData = reactive({
  id: null as number | null,
  layout: 'hero_split',
  theme: 'light',
  overline_color: '',
  title_color: '',
  body_color: '',
  bg_type: 'color',
  bg_value: '',
  overlay_enabled: false,
  overlay_opacity: 0,
  overline_zh: '',
  overline_en: '',
  title_zh: '',
  title_en: '',
  subtitle_zh: '',
  subtitle_en: '',
  content_zh: '',
  content_en: '',
  cta_primary_zh: '',
  cta_primary_en: '',
  cta_primary_url: '',
  cta_secondary_zh: '',
  cta_secondary_en: '',
  cta_secondary_url: '',
  media_url_zh: '',
  media_url_en: '',
  video_url: '',
  is_visible: true,
  config: { text_position: 'middle_left', width_percent: 100, container_width: '1200', border_radius: 0, padding_y: null, title_font_size: null, bg_fit: 'cover', media_position: 'left', mosaic_pattern: 'big_3', mosaic_items: emptyMosaicItems(4), min_height: null } as Record<string, unknown>
});

// 背景图/视频显示模式选项（bg_type 为图片/视频时生效）
const bgFitOptions = [
  { label: '铺满', value: 'cover' },
  { label: '完整显示', value: 'contain' },
  { label: '原尺寸', value: 'auto' }
];

// 内容区最大宽度选项（区块内容容器上限，默认 1200px）
const containerWidthOptions = [
  { label: '1200px（默认）', value: '1200' },
  { label: '1400px', value: '1400' },
  { label: '1600px', value: '1600' },
  { label: '全宽', value: 'full' }
];

// ===== 马赛克拼贴（layout=mosaic）：固定版式 + 槽位素材，存 config.mosaic_pattern / config.mosaic_items =====
interface MosaicItemForm {
  type: 'image' | 'video';
  media_zh: string;
  media_en: string;
}

const mosaicPatternOptions = [
  { label: '1 大 + 3 小', value: 'big_3', count: 4 },
  { label: '1 大 + 1 大（左右等分）', value: 'half_2', count: 2 },
  { label: '1 大 + 4 小', value: 'big_4', count: 5 },
  { label: '1 大 + 2 小（上下等分）', value: 'big_2', count: 3 }
];

function mosaicSlotCount(pattern: string) {
  return mosaicPatternOptions.find(o => o.value === pattern)?.count ?? 4;
}

// 各版式槽位说明（按顺序对应素材编辑槽位与 www 端网格位置）
function mosaicSlotLabels(pattern: string): string[] {
  return (
    {
      big_3: ['大图', '小图 1', '小图 2', '宽图'],
      half_2: ['左侧', '右侧'],
      big_4: ['大图', '小图 1', '小图 2', '小图 3', '小图 4'],
      big_2: ['大图', '上图', '下图']
    } as Record<string, string[]>
  )[pattern] || [];
}

function emptyMosaicItems(count: number): MosaicItemForm[] {
  return Array.from({ length: count }, () => ({ type: 'image', media_zh: '', media_en: '' }));
}

// 归一化回填：类型/字段容错，数量与版式槽位数对齐（保留已填素材）
function normalizeMosaicItems(raw: unknown, pattern: string): MosaicItemForm[] {
  const count = mosaicSlotCount(pattern);
  const arr = Array.isArray(raw) ? raw : [];
  return Array.from({ length: count }, (_, i) => {
    const it = (arr[i] ?? {}) as Partial<MosaicItemForm>;
    return {
      type: it.type === 'video' ? 'video' : 'image',
      media_zh: it.media_zh || '',
      media_en: it.media_en || ''
    };
  });
}

// 切换版式时同步槽位数量（保留已有素材）
function handleMosaicPatternChange(value: string) {
  formData.config.mosaic_pattern = value;
  formData.config.mosaic_items = normalizeMosaicItems(formData.config.mosaic_items, value);
}

const mosaicSlotItems = computed<MosaicItemForm[]>(() => (formData.config.mosaic_items as MosaicItemForm[]) || []);
const mosaicSlotLabelList = computed(() => mosaicSlotLabels(String(formData.config.mosaic_pattern ?? 'big_3')));

// 九宫格文案位置（hero_split 专用）
const textPositionOptions = [
  { label: '左上', value: 'top_left' },
  { label: '中上', value: 'top_center' },
  { label: '右上', value: 'top_right' },
  { label: '左中', value: 'middle_left' },
  { label: '正中', value: 'middle_center' },
  { label: '右中', value: 'middle_right' },
  { label: '左下', value: 'bottom_left' },
  { label: '中下', value: 'bottom_center' },
  { label: '右下', value: 'bottom_right' }
];

const columns = computed((): DataTableColumns<Api.Www.SectionBlock> => [
  {
    title: '排序',
    key: 'sort_order',
    width: 110,
    render(row) {
      const idx = tableData.value.findIndex(item => item.id === row.id);
      const isFirst = idx === 0;
      const isLast = idx === tableData.value.length - 1;
      return h('div', { class: 'flex-y-center gap-4px' }, [
        h('span', { class: 'text-12px text-gray-400 w-20px text-center' }, { default: () => String(row.sort_order) }),
        h(NButton, {
          text: true, size: 'tiny', type: 'primary', disabled: isFirst,
          onClick: () => handleMove(idx, -1)
        }, { default: () => '↑' }),
        h(NButton, {
          text: true, size: 'tiny', type: 'primary', disabled: isLast,
          onClick: () => handleMove(idx, 1)
        }, { default: () => '↓' })
      ]);
    }
  },
  { title: '布局', key: 'layout', width: 130, render: row => layoutLabel(row.layout) },
  {
    title: '主题',
    key: 'theme',
    width: 80,
    render(row) {
      return h(NTag, { type: row.theme === 'dark' ? 'default' : 'success', size: 'small' }, {
        default: () => (row.theme === 'dark' ? '深色' : '浅色')
      });
    }
  },
  { title: '标题（中）', key: 'title_zh', ellipsis: { tooltip: true } },
  { title: '标题（英）', key: 'title_en', ellipsis: { tooltip: true } },
  {
    title: '可见',
    key: 'is_visible',
    width: 80,
    render(row) {
      return h(NSwitch, {
        value: Boolean(row.is_visible),
        onUpdateValue: (val: boolean) => handleToggleVisible(row, val)
      });
    }
  },
  {
    title: '操作',
    key: 'actions',
    width: 240,
    fixed: 'right',
    render(row) {
      return h('div', { class: 'flex-y-center gap-8px' }, [
        h(NButton, { size: 'small', type: 'primary', onClick: () => handleEdit(row) }, { default: () => '编辑' }),
        h(NButton, { size: 'small', onClick: () => handleCopy(row) }, { default: () => '复制' }),
        h(NButton, { size: 'small', type: 'error', onClick: () => handleDelete(row) }, { default: () => '删除' })
      ]);
    }
  }
]);

async function handleMove(index: number, direction: -1 | 1) {
  const swapIndex = index + direction;
  if (swapIndex < 0 || swapIndex >= tableData.value.length) return;

  const newData = [...tableData.value];
  [newData[index], newData[swapIndex]] = [newData[swapIndex], newData[index]];
  const sortPayload = newData.map((item, i) => ({ id: item.id, sort_order: i }));

  const { error } = await fetchSortSections(sortPayload);
  if (!error) {
    window.$message?.success('排序已更新');
    loadData();
  }
}

async function loadData() {
  loading.value = true;
  const params: { page_type: string; product_id?: number } = { page_type: props.pageType };
  if (props.productId) params.product_id = props.productId;
  const { data, error } = await fetchGetSections(params);
  loading.value = false;
  if (!error && data) {
    tableData.value = data.items || [];
  }
}

function handleAdd() {
  dialogTitle.value = '新增 Section';
  formData.id = null;
  formData.layout = 'hero_split';
  formData.theme = 'light';
  formData.overline_color = '';
  formData.title_color = '';
  formData.body_color = '';
  formData.bg_type = 'color';
  formData.bg_value = '';
  formData.overlay_enabled = false;
  formData.overlay_opacity = 0;
  formData.overline_zh = '';
  formData.overline_en = '';
  formData.title_zh = '';
  formData.title_en = '';
  formData.subtitle_zh = '';
  formData.subtitle_en = '';
  formData.content_zh = '';
  formData.content_en = '';
  formData.cta_primary_zh = '';
  formData.cta_primary_en = '';
  formData.cta_primary_url = '';
  formData.cta_secondary_zh = '';
  formData.cta_secondary_en = '';
  formData.cta_secondary_url = '';
  formData.media_url_zh = '';
  formData.media_url_en = '';
  formData.video_url = '';
  formData.is_visible = true;
  formData.config = { text_position: 'middle_left', width_percent: 100, container_width: '1200', border_radius: 0, padding_y: null, title_font_size: null, bg_fit: 'cover', media_position: 'left', mosaic_pattern: 'big_3', mosaic_items: emptyMosaicItems(4), min_height: null };
  dialogVisible.value = true;
}

// 用指定行的数据填充表单（编辑/复制共用）
function fillFormFromRow(row: Api.Www.SectionBlock) {
  formData.layout = row.layout;
  formData.theme = row.theme;
  formData.overline_color = row.overline_color || '';
  formData.title_color = row.title_color || '';
  formData.body_color = row.body_color || '';
  formData.bg_type = row.bg_type;
  formData.bg_value = row.bg_value;
  formData.overlay_enabled = Boolean(row.overlay_enabled);
  formData.overlay_opacity = Number(row.overlay_opacity);
  formData.overline_zh = row.overline_zh;
  formData.overline_en = row.overline_en;
  formData.title_zh = row.title_zh;
  formData.title_en = row.title_en;
  formData.subtitle_zh = row.subtitle_zh;
  formData.subtitle_en = row.subtitle_en;
  formData.content_zh = row.content_zh;
  formData.content_en = row.content_en;
  formData.cta_primary_zh = row.cta_primary_zh;
  formData.cta_primary_en = row.cta_primary_en;
  formData.cta_primary_url = row.cta_primary_url;
  formData.cta_secondary_zh = row.cta_secondary_zh;
  formData.cta_secondary_en = row.cta_secondary_en;
  formData.cta_secondary_url = row.cta_secondary_url;
  formData.media_url_zh = row.media_url_zh;
  formData.media_url_en = row.media_url_en;
  formData.video_url = row.video_url;
  formData.is_visible = Boolean(row.is_visible);
  const cfg = (row.config || {}) as Record<string, unknown>;
  formData.config = {
    ...cfg,
    text_position: cfg.text_position || 'middle_left',
    width_percent: typeof cfg.width_percent === 'number' ? cfg.width_percent : 100,
    container_width: ['1400', '1600', 'full'].includes(String(cfg.container_width)) ? String(cfg.container_width) : '1200',
    border_radius: typeof cfg.border_radius === 'number' && cfg.border_radius > 0 ? cfg.border_radius : 0,
    padding_y: typeof cfg.padding_y === 'number' && cfg.padding_y >= 0 ? cfg.padding_y : null,
    title_font_size: typeof cfg.title_font_size === 'number' && cfg.title_font_size > 0 ? cfg.title_font_size : null,
    bg_fit: cfg.bg_fit === 'contain' || cfg.bg_fit === 'auto' ? cfg.bg_fit : 'cover',
    media_position: cfg.media_position === 'right' ? 'right' : 'left',
    min_height: typeof cfg.min_height === 'number' && cfg.min_height >= 0 ? cfg.min_height : null,
    mosaic_pattern: ['big_3', 'half_2', 'big_4', 'big_2'].includes(String(cfg.mosaic_pattern)) ? String(cfg.mosaic_pattern) : 'big_3'
  };
  formData.config.mosaic_items = normalizeMosaicItems(cfg.mosaic_items, String(formData.config.mosaic_pattern));
}

function handleEdit(row: Api.Www.SectionBlock) {
  dialogTitle.value = '编辑 Section';
  formData.id = row.id;
  fillFormFromRow(row);
  dialogVisible.value = true;
}

// 复制：以该行数据预填新增表单（不带 id，保存后创建新区块）
function handleCopy(row: Api.Www.SectionBlock) {
  dialogTitle.value = `复制 Section（原：${row.title_zh || layoutLabel(row.layout)}）`;
  formData.id = null;
  fillFormFromRow(row);
  dialogVisible.value = true;
}

async function handleToggleVisible(row: Api.Www.SectionBlock, val: boolean) {
  const { error } = await fetchUpdateSection(row.id, { is_visible: val });
  if (!error) {
    row.is_visible = val;
    window.$message?.success('已更新');
  }
}

function handleDelete(row: Api.Www.SectionBlock) {
  window.$dialog?.warning({
    title: '警告',
    content: `确定要删除 Section「${row.title_zh || row.layout}」吗？`,
    positiveText: '确定',
    negativeText: '取消',
    onPositiveClick: async () => {
      const { error } = await fetchDeleteSection(row.id);
      if (!error) {
        window.$message?.success('删除成功');
        loadData();
      }
    }
  });
}

async function handleSubmit() {
  submitLoading.value = true;
  const { id, ...payload } = formData;
  const reqData = {
    ...payload,
    page_type: props.pageType,
    product_id: props.productId || null
  };
  const { error } = id
    ? await fetchUpdateSection(id, reqData)
    : await fetchCreateSection(reqData);
  submitLoading.value = false;
  if (!error) {
    window.$message?.success(id ? '更新成功' : '创建成功');
    dialogVisible.value = false;
    loadData();
  }
}

onMounted(loadData);
</script>

<template>
  <div>
    <NCard :bordered="false" class="card-wrapper" :title="title" size="small">
      <template #header-extra>
        <NButton type="primary" @click="handleAdd">
          <template #icon>
            <SvgIcon icon="mdi:plus" />
          </template>
          新增区块
        </NButton>
      </template>

      <NDataTable :loading="loading" :columns="columns" :data="tableData" :bordered="false" size="small" />
    </NCard>

    <NModal
      v-model:show="dialogVisible"
      preset="card"
      :title="dialogTitle"
      class="w-1000px"
      :mask-closable="false"
    >
      <NForm ref="formRef" :model="formData" label-placement="left" label-width="120">
        <NDivider>基础配置</NDivider>
        <NFormItem label="布局类型">
          <NSelect v-model:value="formData.layout" :options="layoutOptions" :render-label="renderLayoutLabel" :render-option="renderLayoutOption" />
        </NFormItem>
        <NFormItem label="主题">
          <NRadioGroup v-model:value="formData.theme">
            <NRadio v-for="opt in themeOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</NRadio>
          </NRadioGroup>
        </NFormItem>
        <NFormItem label="区块宽度">
          <div class="w-full flex flex-col gap-8px">
            <NInputNumber
              :value="(formData.config.width_percent as number) ?? 100"
              :min="30"
              :max="100"
              :step="5"
              class="w-full"
              @update:value="v => (formData.config.width_percent = v ?? 100)"
            >
              <template #suffix>%</template>
            </NInputNumber>
            <span class="text-12px opacity-60">占视口宽度百分比，居中显示；100 = 全宽。移动端（小屏）始终全宽</span>
          </div>
        </NFormItem>
        <NFormItem label="内容区宽度">
          <div class="w-full flex flex-col gap-8px">
            <NSelect
              :value="(formData.config.container_width as string) ?? '1200'"
              :options="containerWidthOptions"
              @update:value="v => (formData.config.container_width = v)"
            />
            <span class="text-12px opacity-60">区块内容容器的最大宽度，居中于区块内；全宽 = 内容占满整个区块</span>
          </div>
        </NFormItem>
        <NFormItem label="区块圆角">
          <div class="w-full flex flex-col gap-8px">
            <NInputNumber
              :value="(formData.config.border_radius as number) ?? 0"
              :min="0"
              :max="60"
              :step="4"
              class="w-full"
              @update:value="v => (formData.config.border_radius = v ?? 0)"
            >
              <template #suffix>px</template>
            </NInputNumber>
            <span class="text-12px opacity-60">区块四角圆角大小；0 = 直角（默认）。建议与「区块宽度」小于 100 搭配使用，全宽时圆角不可见</span>
          </div>
        </NFormItem>
        <NFormItem label="上下内边距">
          <div class="w-full flex flex-col gap-8px">
            <NInputNumber
              :value="(formData.config.padding_y as number | null) ?? null"
              :min="0"
              :max="240"
              :step="8"
              class="w-full"
              clearable
              placeholder="留空 = 布局默认值"
              @update:value="v => (formData.config.padding_y = v)"
            >
              <template #suffix>px</template>
            </NInputNumber>
            <span class="text-12px opacity-60">内容区与区块上下边缘的间距。留空 = 布局默认（多数 96px / 全幅横幅 120px / 主视觉分屏 64px）；0 = 完全贴边</span>
          </div>
        </NFormItem>
        <NFormItem label="标题字号">
          <div class="w-full flex flex-col gap-8px">
            <NInputNumber
              :value="(formData.config.title_font_size as number | null) ?? null"
              :min="16"
              :max="96"
              :step="2"
              class="w-full"
              clearable
              placeholder="留空 = 36px（兜底）"
              @update:value="v => (formData.config.title_font_size = v)"
            >
              <template #suffix>px</template>
            </NInputNumber>
            <span class="text-12px opacity-60">桌面端标题字号；移动端自动按约 62.5% 缩小（如填 48 → 移动端 30px）。留空 = 兜底 36px；引言布局无标题字段不受影响</span>
          </div>
        </NFormItem>
        <NDivider>文案颜色（留空=跟随主题默认色）</NDivider>
        <NFormItem label="Overline 颜色">
          <ColorInput v-model:value="formData.overline_color" />
        </NFormItem>
        <NFormItem label="标题颜色">
          <ColorInput v-model:value="formData.title_color" />
        </NFormItem>
        <NFormItem label="副标题/正文颜色">
          <ColorInput v-model:value="formData.body_color" />
        </NFormItem>
        <NFormItem label="背景类型">
          <NRadioGroup v-model:value="formData.bg_type">
            <NRadioButton value="color">纯色</NRadioButton>
            <NRadioButton value="image">图片</NRadioButton>
            <NRadioButton value="gradient">渐变</NRadioButton>
            <NRadioButton value="video">视频</NRadioButton>
          </NRadioGroup>
        </NFormItem>
        <NFormItem label="背景值">
          <MediaSelectInput v-model:value="formData.bg_value" placeholder="颜色值或图片/视频路径（支持 /uploads/... 相对路径）" />
        </NFormItem>
        <NFormItem v-if="formData.bg_type === 'image' || formData.bg_type === 'video'" label="背景显示模式">
          <NRadioGroup :value="(formData.config.bg_fit as string) ?? 'cover'" @update:value="v => (formData.config.bg_fit = v)">
            <NRadio v-for="opt in bgFitOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</NRadio>
          </NRadioGroup>
        </NFormItem>
        <NFormItem v-if="formData.layout === 'hero_split' || formData.layout === 'mosaic'" :label="formData.layout === 'mosaic' ? '网格高度' : '最小高度'">
          <div class="w-full flex flex-col gap-8px">
            <NInputNumber
              :value="(formData.config.min_height as number | null) ?? null"
              :min="0"
              :max="1200"
              :step="20"
              class="w-full"
              clearable
              :placeholder="formData.layout === 'mosaic' ? '留空 = 默认 16:9 宽高比' : '留空 = 首屏高度（默认）'"
              @update:value="v => (formData.config.min_height = v)"
            >
              <template #suffix>px</template>
            </NInputNumber>
            <span class="text-12px opacity-60">
              <template v-if="formData.layout === 'mosaic'">仅桌面端生效。留空 = 默认 16:9 宽高比；填数值 = 网格固定高度（px），素材自动裁剪填充，可调低区块高度；移动端保持单列堆叠不变</template>
              <template v-else>仅主视觉分屏布局生效。留空 = 沿用首屏高度（至少 680px 约一屏）；0 = 完全自适应内容（去上下内边距，区块高度严格等于内容高度）；填数值 = 自定义最小高度</template>
            </span>
          </div>
        </NFormItem>
        <NFormItem v-if="formData.layout === 'hero_split' || formData.layout === 'title_media'" label="文案位置">
          <div class="w-full flex flex-col gap-8px">
            <div class="grid grid-cols-3 gap-4px">
              <NButton
                v-for="pos in textPositionOptions"
                :key="pos.value"
                size="tiny"
                :type="formData.config.text_position === pos.value ? 'primary' : 'default'"
                @click="formData.config.text_position = pos.value"
              >
                {{ pos.label }}
              </NButton>
            </div>
            <span v-if="formData.layout === 'title_media'" class="text-12px opacity-60">标题+媒体布局仅水平方向生效（左对齐/居中/右对齐），垂直位置忽略</span>
          </div>
        </NFormItem>
        <NFormItem v-if="formData.layout === 'split_content'" label="媒体位置">
          <NRadioGroup :value="(formData.config.media_position as string) ?? 'left'" @update:value="v => (formData.config.media_position = v)">
            <NRadio value="left">图片/视频在左</NRadio>
            <NRadio value="right">图片/视频在右</NRadio>
          </NRadioGroup>
        </NFormItem>
        <NFormItem v-if="formData.layout === 'mosaic'" label="拼贴版式">
          <div class="w-full flex flex-col gap-8px">
            <NRadioGroup :value="(formData.config.mosaic_pattern as string) ?? 'big_3'" @update:value="handleMosaicPatternChange">
              <NRadio v-for="opt in mosaicPatternOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</NRadio>
            </NRadioGroup>
            <span class="text-12px opacity-60">固定版式；切换版式会同步下方槽位数量（已填素材按顺序保留）</span>
          </div>
        </NFormItem>
        <NFormItem label="遮罩">
          <NSpace align="center">
            <NSwitch v-model:value="formData.overlay_enabled" />
            <span>透明度</span>
            <NSlider v-model:value="formData.overlay_opacity" :min="0" :max="1" :step="0.05" class="w-200px" />
            <span>{{ formData.overlay_opacity.toFixed(2) }}</span>
          </NSpace>
        </NFormItem>

        <NDivider>文案内容</NDivider>
        <NFormItem label="Overline（中）">
          <NInput v-model:value="formData.overline_zh" maxlength="100" />
        </NFormItem>
        <NFormItem label="Overline（英）">
          <NInput v-model:value="formData.overline_en" maxlength="100" />
        </NFormItem>
        <NFormItem label="标题（中）">
          <NInput v-model:value="formData.title_zh" maxlength="200" />
        </NFormItem>
        <NFormItem label="标题（英）">
          <NInput v-model:value="formData.title_en" maxlength="200" />
        </NFormItem>
        <NFormItem label="副标题（中）">
          <NInput v-model:value="formData.subtitle_zh" type="textarea" :autosize="{ minRows: 1 }" />
        </NFormItem>
        <NFormItem label="副标题（英）">
          <NInput v-model:value="formData.subtitle_en" type="textarea" :autosize="{ minRows: 1 }" />
        </NFormItem>
        <NFormItem label="正文（中）">
          <RichTextEditor v-model:value="formData.content_zh" />
        </NFormItem>
        <NFormItem label="正文（英）">
          <RichTextEditor v-model:value="formData.content_en" />
        </NFormItem>

        <NDivider>按钮</NDivider>
        <NFormItem label="主按钮（中）">
          <NInput v-model:value="formData.cta_primary_zh" maxlength="50" />
        </NFormItem>
        <NFormItem label="主按钮（英）">
          <NInput v-model:value="formData.cta_primary_en" maxlength="50" />
        </NFormItem>
        <NFormItem label="主按钮链接">
          <NInput v-model:value="formData.cta_primary_url" maxlength="500" placeholder="站内路由（如 /products/luxsin-x8）或完整 URL" />
        </NFormItem>
        <NFormItem label="次按钮（中）">
          <NInput v-model:value="formData.cta_secondary_zh" maxlength="50" />
        </NFormItem>
        <NFormItem label="次按钮（英）">
          <NInput v-model:value="formData.cta_secondary_en" maxlength="50" />
        </NFormItem>
        <NFormItem label="次按钮链接">
          <NInput v-model:value="formData.cta_secondary_url" maxlength="500" placeholder="站内路由（如 /products/luxsin-x8）或完整 URL" />
        </NFormItem>

        <NDivider>素材</NDivider>
        <template v-if="formData.layout === 'mosaic'">
          <NFormItem
            v-for="(item, idx) in mosaicSlotItems"
            :key="`${formData.config.mosaic_pattern}-${idx}`"
            :label="`槽位 ${idx + 1}：${mosaicSlotLabelList[idx] || ''}`"
          >
            <div class="w-full flex flex-col gap-8px">
              <NRadioGroup v-model:value="item.type">
                <NRadio value="image">图片</NRadio>
                <NRadio value="video">视频</NRadio>
              </NRadioGroup>
              <MediaSelectInput v-model:value="item.media_zh" :file-type="item.type === 'video' ? 'video' : 'image'" :maxlength="500" placeholder="素材（中）" />
              <MediaSelectInput v-model:value="item.media_en" :file-type="item.type === 'video' ? 'video' : 'image'" :maxlength="500" placeholder="素材（英，留空回退中文素材）" />
            </div>
          </NFormItem>
        </template>
        <template v-else>
          <NFormItem label="图片（中）">
            <MediaSelectInput v-model:value="formData.media_url_zh" file-type="image" :maxlength="500" placeholder="素材路径（支持 /uploads/... 相对路径）" />
          </NFormItem>
          <NFormItem label="图片（英）">
            <MediaSelectInput v-model:value="formData.media_url_en" file-type="image" :maxlength="500" placeholder="素材路径（支持 /uploads/... 相对路径）" />
          </NFormItem>
          <NFormItem label="视频地址">
            <MediaSelectInput v-model:value="formData.video_url" file-type="video" :maxlength="500" placeholder="视频路径或 URL（背景为视频或图文分栏媒体位使用）" />
          </NFormItem>
        </template>

        <NDivider>显示</NDivider>
        <NFormItem label="是否可见">
          <NSwitch v-model:value="formData.is_visible" />
        </NFormItem>
      </NForm>
      <template #footer>
        <NSpace justify="end">
          <NButton @click="dialogVisible = false">取消</NButton>
          <NButton type="primary" :loading="submitLoading" @click="handleSubmit">确定</NButton>
        </NSpace>
      </template>
    </NModal>
  </div>
</template>

<style>
/* 布局类型下拉：收起态（输入框内）隐藏 desc，下拉展开列表里始终显示 desc */
.n-base-selection-label .layout-desc,
.n-base-selection-tag__content .layout-desc,
.n-base-selection-tags .layout-desc {
  display: none;
}
</style>
