<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue';

import {
  Button,
  Drawer,
  Empty,
  Image,
  Input,
  InputNumber,
  message,
  Progress,
  Select,
  Space,
  Table,
  Tag,
} from 'antdv-next';

import { addGift } from '#/api/legacy/gift';
import { getAccessImgUrl, simpleUploadFile } from '#/api/legacy/oss';
import { regionConfigTable } from '#/api/legacy/system';
import { normalizeResourceCode } from '#/views/_shared/resource-code';

import { GIFT_CONFIG_TAB_OPTIONS } from '../constants';

type AssetKind = 'cover' | 'source';
type RowStatus = 'error' | 'ready' | 'saved' | 'saving' | 'uploading';

interface BatchGiftRow {
  code: string;
  cover: string;
  coverError: string;
  coverFileName: string;
  coverUploading: boolean;
  errors: string[];
  giftCandy: number;
  giftIntegral: number;
  key: string;
  name: string;
  saved: boolean;
  saving: boolean;
  sort: number;
  sourceError: string;
  sourceFileName: string;
  sourceUploading: boolean;
  sourceUrl: string;
  status: RowStatus;
}

interface ParsedGiftFile {
  amount: number;
  assetKind: AssetKind;
  code: string;
  name: string;
}

interface UploadTask {
  file: File;
  fileName: string;
  kind: AssetKind;
  row: BatchGiftRow;
}

const props = defineProps<{
  initialSysOrigin?: number | string;
  open: boolean;
  sysOriginOptions: Array<{ label: string; value: string }>;
}>();

const emit = defineEmits<{
  close: [];
  success: [];
}>();

const batchForm = reactive({
  giftTab: 'ORDINARY',
  regionList: [] as Array<number | string>,
  sysOrigin: '',
});

const rows = ref<BatchGiftRow[]>([]);
const activeRowKey = ref('');
const dragging = ref(false);
const folderInputRef = ref<HTMLInputElement>();
const fileInputRef = ref<HTMLInputElement>();
const regions = ref<Array<Record<string, any>>>([]);
const selectedRowKeys = ref<string[]>([]);
const sequence = ref(0);
const showOnlyErrors = ref(false);
const submitting = ref(false);
const droppedFilePathMap = new WeakMap<File, string>();
const uploadQueue: UploadTask[] = [];
const MAX_UPLOAD_CONCURRENCY = 1;
let activeUploadCount = 0;

const giftTabOptions = GIFT_CONFIG_TAB_OPTIONS.map((item) => ({
  label: item.name,
  value: item.value as any,
}));
const regionOptions = computed(() =>
  regions.value.map((item) => ({
    label: String(item.regionName || item.name || item.id || '-'),
    value: item.id as any,
  })),
);

const columns = [
  { dataIndex: 'cover', key: 'cover', title: '封面', width: 116 },
  { dataIndex: 'sourceUrl', key: 'sourceUrl', title: '资源', width: 180 },
  { dataIndex: 'name', key: 'name', title: '名称', width: 180 },
  { dataIndex: 'code', key: 'code', title: '编码', width: 180 },
  { dataIndex: 'giftCandy', key: 'giftCandy', title: '金币', width: 116 },
  { dataIndex: 'giftIntegral', key: 'giftIntegral', title: '积分', width: 116 },
  { dataIndex: 'sort', key: 'sort', title: '排序', width: 96 },
  { dataIndex: 'status', key: 'status', title: '状态', width: 120 },
  { dataIndex: 'actions', key: 'actions', title: '操作', width: 92 },
];

const visibleRows = computed(() =>
  showOnlyErrors.value
    ? rows.value.filter((row) => row.errors.length > 0)
    : rows.value,
);

const activeRow = computed(() => {
  if (activeRowKey.value) {
    const matched = rows.value.find((row) => row.key === activeRowKey.value);
    if (matched) {
      return matched;
    }
  }
  return rows.value[0] || null;
});

const uploadAssetTotal = computed(() =>
  rows.value.reduce(
    (total, row) =>
      total + (row.coverFileName ? 1 : 0) + (row.sourceFileName ? 1 : 0),
    0,
  ),
);

const uploadAssetFinished = computed(() =>
  rows.value.reduce(
    (total, row) =>
      total
      + (row.coverFileName && !row.coverUploading ? 1 : 0)
      + (row.sourceFileName && !row.sourceUploading ? 1 : 0),
    0,
  ),
);

const uploadPercent = computed(() => {
  if (uploadAssetTotal.value === 0) {
    return 0;
  }
  return Math.round((uploadAssetFinished.value / uploadAssetTotal.value) * 100);
});

const errorCount = computed(() =>
  rows.value.filter((row) => row.errors.length > 0).length,
);
const submitTargetCount = computed(
  () => rows.value.filter((row) => isRowSubmittable(row)).length,
);

const rowSelection = computed(() => ({
  selectedRowKeys: selectedRowKeys.value,
  onChange: (keys: Array<number | string>) => {
    selectedRowKeys.value = keys.map(String);
  },
}));

function resetBatch() {
  batchForm.sysOrigin =
    String(props.initialSysOrigin || props.sysOriginOptions[0]?.value || 'LIKEI');
  batchForm.giftTab = 'ORDINARY';
  batchForm.regionList = [];
  rows.value = [];
  selectedRowKeys.value = [];
  activeRowKey.value = '';
  showOnlyErrors.value = false;
  sequence.value = 0;
  uploadQueue.length = 0;
}

watch(
  () => props.open,
  (open) => {
    if (!open) {
      return;
    }
    resetBatch();
    void loadRegions();
  },
  { immediate: true },
);

watch(
  () => batchForm.sysOrigin,
  () => {
    if (props.open) {
      void loadRegions();
    }
  },
);

watch(
  () => [batchForm.sysOrigin, batchForm.regionList.length],
  () => {
    refreshAllRows();
  },
);

async function loadRegions() {
  if (!batchForm.sysOrigin) {
    regions.value = [];
    batchForm.regionList = [];
    return;
  }
  regions.value = (await regionConfigTable({ sysOrigin: batchForm.sysOrigin })) || [];
  batchForm.regionList = regions.value.map((item) => item.id);
  refreshAllRows();
}

function normalizeAmount(value: unknown) {
  const amount = Number(value ?? 0);
  return Number.isFinite(amount) && amount >= 0 ? amount : 0;
}

function getPathFileName(path: string) {
  const segments = path.split('/');
  return segments[segments.length - 1] || path;
}

function getFilePath(file: File) {
  return (
    droppedFilePathMap.get(file)
    || (file as any).webkitRelativePath
    || file.name
  );
}

function isImageFile(file: File) {
  return (
    file.type.startsWith('image/')
    || /\.(apng|avif|gif|jpe?g|png|svg|webp)$/i.test(file.name)
  );
}

function isSourceFile(file: File) {
  return /\.(mp4|pag|svga|zip)$/i.test(file.name);
}

function parseGiftFile(file: File): null | ParsedGiftFile {
  const fileName = getPathFileName(getFilePath(file));
  const baseName = fileName.replace(/\.[^.]+$/, '').trim();
  const parts = baseName
    .split('_')
    .map((part) => part.trim())
    .filter(Boolean);
  if (parts[0] !== '礼物') {
    return null;
  }
  const assetToken = String(parts[parts.length - 1] || '').toLowerCase();
  let assetKind: '' | AssetKind = '';
  if (assetToken === 'cover') {
    assetKind = 'cover';
  } else if (assetToken === 'animation') {
    assetKind = 'source';
  }
  if (!assetKind) {
    return null;
  }
  if (assetKind === 'cover' && !isImageFile(file)) {
    return null;
  }
  if (assetKind === 'source' && !isSourceFile(file)) {
    return null;
  }
  const nameParts = parts.slice(1, -1);
  const maybeDuration = String(nameParts[nameParts.length - 1] || '');
  if (/^\d+天$/.test(maybeDuration)) {
    nameParts.pop();
  }
  let amount = 0;
  const maybeAmount = String(nameParts[nameParts.length - 1] || '');
  if (/^\d+(\.\d+)?$/.test(maybeAmount)) {
    amount = normalizeAmount(nameParts.pop());
  }
  const name = nameParts.join('_').trim();
  if (!name) {
    return null;
  }
  return {
    amount,
    assetKind,
    code: normalizeResourceCode(name),
    name,
  };
}

function findOrCreateRow(parsed: ParsedGiftFile) {
  const matched = rows.value.find(
    (row) => row.code === parsed.code || row.name === parsed.name,
  );
  if (matched) {
    matched.giftCandy = parsed.amount;
    matched.giftIntegral = parsed.amount;
    matched.saved = false;
    return matched;
  }
  const amount = normalizeAmount(parsed.amount);
  const row: BatchGiftRow = {
    code: parsed.code,
    cover: '',
    coverError: '',
    coverFileName: '',
    coverUploading: false,
    errors: [],
    giftCandy: amount,
    giftIntegral: amount,
    key: `gift-batch-${Date.now()}-${sequence.value++}`,
    name: parsed.name,
    saved: false,
    saving: false,
    sort: rows.value.length + 1,
    sourceError: '',
    sourceFileName: '',
    sourceUploading: false,
    sourceUrl: '',
    status: 'error',
  };
  rows.value.push(row);
  if (!activeRowKey.value) {
    activeRowKey.value = row.key;
  }
  return row;
}

function setRowAsset(row: BatchGiftRow, kind: AssetKind, file: File) {
  const fileName = getPathFileName(getFilePath(file));
  if (kind === 'cover') {
    row.cover = '';
    row.coverError = '';
    row.coverFileName = fileName;
    row.coverUploading = true;
  } else {
    row.sourceUrl = '';
    row.sourceError = '';
    row.sourceFileName = fileName;
    row.sourceUploading = true;
  }
  row.saved = false;
  refreshAllRows();
  enqueueUpload({ file, fileName, kind, row });
}

function enqueueUpload(task: UploadTask) {
  uploadQueue.push(task);
  runUploadQueue();
}

function runUploadQueue() {
  while (activeUploadCount < MAX_UPLOAD_CONCURRENCY && uploadQueue.length > 0) {
    const task = uploadQueue.shift();
    if (!task) {
      return;
    }
    activeUploadCount += 1;
    void uploadAsset(task.row, task.kind, task.file, task.fileName).finally(() => {
      activeUploadCount -= 1;
      runUploadQueue();
    });
  }
}

async function uploadAsset(
  row: BatchGiftRow,
  kind: AssetKind,
  file: File,
  fileName: string,
) {
  try {
    const result = await simpleUploadFile(file, 'gifts');
    if (kind === 'cover' && row.coverFileName === fileName) {
      row.cover = result.name;
    } else if (kind === 'source' && row.sourceFileName === fileName) {
      row.sourceUrl = result.name;
    }
  } catch (error) {
    console.error(error);
    if (kind === 'cover' && row.coverFileName === fileName) {
      row.coverError = '封面上传失败';
    } else if (kind === 'source' && row.sourceFileName === fileName) {
      row.sourceError = '资源上传失败';
    }
  } finally {
    if (kind === 'cover' && row.coverFileName === fileName) {
      row.coverUploading = false;
    } else if (kind === 'source' && row.sourceFileName === fileName) {
      row.sourceUploading = false;
    }
    refreshAllRows();
  }
}

function processFiles(files: File[]) {
  let accepted = 0;
  let ignored = 0;
  files.forEach((file) => {
    const parsed = parseGiftFile(file);
    if (!parsed) {
      ignored += 1;
      return;
    }
    const row = findOrCreateRow(parsed);
    setRowAsset(row, parsed.assetKind, file);
    accepted += 1;
  });
  if (accepted === 0 && files.length > 0) {
    message.warning('未识别到符合 礼物_ 命名规则的文件');
    return;
  }
  if (accepted > 0) {
    message.success(
      ignored > 0
        ? `已导入 ${accepted} 个文件，忽略 ${ignored} 个非规则文件`
        : `已导入 ${accepted} 个文件`,
    );
  }
}

function handleFileInput(event: Event) {
  const target = event.target as HTMLInputElement;
  const files = target.files ? [...target.files] : [];
  processFiles(files);
  target.value = '';
}

async function handleDrop(event: DragEvent) {
  dragging.value = false;
  const files = await collectDroppedFiles(event.dataTransfer);
  processFiles(files);
}

async function collectDroppedFiles(dataTransfer?: DataTransfer | null) {
  if (!dataTransfer) {
    return [];
  }
  const items = [...(dataTransfer.items || [])];
  const entries = items
    .map((item) => (item as any).webkitGetAsEntry?.())
    .filter(Boolean);
  if (entries.length === 0) {
    return [...(dataTransfer.files || [])];
  }
  const files: File[] = [];
  await Promise.all(entries.map((entry) => readEntry(entry, files)));
  return files;
}

async function readEntry(entry: any, result: File[], parentPath = ''): Promise<void> {
  const currentPath = `${parentPath}${entry.name}`;
  if (entry.isFile) {
    await new Promise<void>((resolve) => {
      entry.file((file: File) => {
        droppedFilePathMap.set(file, currentPath);
        result.push(file);
        resolve();
      }, resolve);
    });
    return;
  }
  if (!entry.isDirectory) {
    return;
  }
  const reader = entry.createReader();
  while (true) {
    const entries = await new Promise<any[]>((resolve, reject) => {
      reader.readEntries(resolve, reject);
    });
    if (entries.length === 0) {
      break;
    }
    await Promise.all(
      entries.map((child) => readEntry(child, result, `${currentPath}/`)),
    );
  }
}

function getDuplicateMap(field: 'code' | 'name') {
  const map = new Map<string, number>();
  rows.value.forEach((row) => {
    const value = String(row[field] || '').trim();
    if (!value) {
      return;
    }
    map.set(value, (map.get(value) || 0) + 1);
  });
  return map;
}

function getRowErrors(
  row: BatchGiftRow,
  duplicateCodes = getDuplicateMap('code'),
  duplicateNames = getDuplicateMap('name'),
) {
  const errors: string[] = [];
  if (!batchForm.sysOrigin) {
    errors.push('请选择平台');
  }
  if (batchForm.regionList.length === 0) {
    errors.push('请选择区域');
  }
  if (!String(row.name || '').trim()) {
    errors.push('缺少名称');
  }
  if (!String(row.code || '').trim()) {
    errors.push('缺少编码');
  }
  if ((duplicateCodes.get(String(row.code).trim()) || 0) > 1) {
    errors.push('本批次编码重复');
  }
  if ((duplicateNames.get(String(row.name).trim()) || 0) > 1) {
    errors.push('本批次名称重复');
  }
  if (row.coverError) {
    errors.push(row.coverError);
  }
  if (row.sourceError) {
    errors.push(row.sourceError);
  }
  if (!row.cover && !row.coverUploading) {
    errors.push('缺少封面');
  }
  if (!Number.isFinite(Number(row.giftCandy)) || Number(row.giftCandy) < 0) {
    errors.push('金币不正确');
  }
  if (!Number.isFinite(Number(row.giftIntegral)) || Number(row.giftIntegral) < 0) {
    errors.push('积分不正确');
  }
  if (!Number.isFinite(Number(row.sort))) {
    errors.push('排序不正确');
  }
  return errors;
}

function refreshRow(
  row: BatchGiftRow,
  duplicateCodes = getDuplicateMap('code'),
  duplicateNames = getDuplicateMap('name'),
) {
  row.errors = getRowErrors(row, duplicateCodes, duplicateNames);
  if (row.saving) {
    row.status = 'saving';
  } else if (row.coverUploading || row.sourceUploading) {
    row.status = 'uploading';
  } else if (row.saved) {
    row.status = 'saved';
  } else if (row.errors.length > 0) {
    row.status = 'error';
  } else {
    row.status = 'ready';
  }
}

function refreshAllRows() {
  const duplicateCodes = getDuplicateMap('code');
  const duplicateNames = getDuplicateMap('name');
  rows.value.forEach((row) => refreshRow(row, duplicateCodes, duplicateNames));
}

function isRowSubmittable(row: BatchGiftRow) {
  refreshRow(row);
  return row.status === 'ready' && !row.saved;
}

function markRowEdited(record: Record<string, any>) {
  const row = record as BatchGiftRow;
  row.saved = false;
  refreshAllRows();
}

function deleteRow(record: Record<string, any>) {
  const row = record as BatchGiftRow;
  rows.value = rows.value.filter((item) => item.key !== row.key);
  selectedRowKeys.value = selectedRowKeys.value.filter((key) => key !== row.key);
  if (activeRowKey.value === row.key) {
    activeRowKey.value = rows.value[0]?.key || '';
  }
  refreshAllRows();
}

function deleteSelectedRows() {
  if (selectedRowKeys.value.length === 0) {
    message.warning('请先选择礼物');
    return;
  }
  const selected = new Set(selectedRowKeys.value);
  rows.value = rows.value.filter((row) => !selected.has(row.key));
  selectedRowKeys.value = [];
  if (!rows.value.some((row) => row.key === activeRowKey.value)) {
    activeRowKey.value = rows.value[0]?.key || '';
  }
  refreshAllRows();
}

function clearRows() {
  rows.value = [];
  selectedRowKeys.value = [];
  activeRowKey.value = '';
}

function getStatusText(status: RowStatus) {
  const map: Record<RowStatus, string> = {
    error: '待处理',
    ready: '可提交',
    saved: '已保存',
    saving: '保存中',
    uploading: '上传中',
  };
  return map[status];
}

function getStatusColor(status: RowStatus) {
  const map: Record<RowStatus, string> = {
    error: 'error',
    ready: 'success',
    saved: 'default',
    saving: 'processing',
    uploading: 'processing',
  };
  return map[status];
}

function setActiveRow(row: BatchGiftRow) {
  activeRowKey.value = row.key;
}

function getRowClassName(record: Record<string, any>) {
  const row = record as BatchGiftRow;
  if (row.key === activeRowKey.value) {
    return row.errors.length > 0
      ? 'batch-row batch-row--active batch-row--error'
      : 'batch-row batch-row--active';
  }
  return row.errors.length > 0 ? 'batch-row batch-row--error' : 'batch-row';
}

function customRow(record: Record<string, any>) {
  const row = record as BatchGiftRow;
  return {
    onClick: () => setActiveRow(row),
  };
}

function buildPayload(row: BatchGiftRow) {
  return {
    account: '',
    explanationGift: false,
    expiredTime: '',
    giftCandy: Number(row.giftCandy || 0),
    giftCode: String(row.code || '').trim(),
    giftIntegral: Number(row.giftIntegral || 0),
    giftName: String(row.name || '').trim(),
    giftPhoto: row.cover,
    giftSourceUrl: row.sourceUrl,
    giftTab: batchForm.giftTab,
    regionList: [...batchForm.regionList],
    sort: Number(row.sort || 0),
    specialList: [],
    standardId: '',
    sysOrigin: batchForm.sysOrigin,
    type: 'GOLD',
  };
}

async function submitBatch() {
  refreshAllRows();
  if (rows.value.some((row) => row.status === 'uploading')) {
    message.warning('还有文件正在上传，请稍后提交');
    return;
  }
  const targetRows = rows.value.filter((row) => row.status === 'ready');
  if (targetRows.length === 0) {
    message.warning('暂无可提交礼物');
    return;
  }
  submitting.value = true;
  let successCount = 0;
  let failedCount = 0;
  for (const row of targetRows) {
    row.saving = true;
    refreshRow(row);
    try {
      await addGift(buildPayload(row));
      row.saved = true;
      row.errors = [];
      successCount += 1;
    } catch (error) {
      console.error(error);
      row.saved = false;
      row.errors = ['保存失败'];
      failedCount += 1;
    } finally {
      row.saving = false;
      refreshRow(row);
    }
  }
  submitting.value = false;
  if (failedCount === 0) {
    message.success(`已提交 ${successCount} 个礼物`);
    emit('success');
    emit('close');
    return;
  }
  message.error(`已提交 ${successCount} 个礼物，${failedCount} 个失败`);
}
</script>

<template>
  <Drawer
    destroy-on-hidden
    :open="open"
    root-class="gift-batch-drawer-root"
    size="100vw"
    :styles="{
      body: { padding: '0', overflow: 'hidden' },
      footer: { padding: '0' },
    }"
    title="批量导入礼物"
    @close="emit('close')"
  >
    <div class="batch-drawer">
      <div class="batch-common">
        <div class="common-field">
          <span class="common-label">平台</span>
          <SysOriginSelect
            v-model:value="batchForm.sysOrigin"
            :options="sysOriginOptions"
          />
        </div>
        <div class="common-field">
          <span class="common-label">礼物类型</span>
          <Select
            v-model:value="batchForm.giftTab"
            option-label-prop="label"
            :options="giftTabOptions"
          />
        </div>
        <div class="common-field common-field--wide">
          <span class="common-label">区域</span>
          <Select
            v-model:value="batchForm.regionList"
            mode="multiple"
            option-label-prop="label"
            :options="regionOptions"
          />
        </div>
        <div class="common-meta">收费类型：金币</div>
      </div>

      <div class="batch-workspace">
        <aside class="import-panel">
          <input
            ref="fileInputRef"
            accept="image/*,.svga,.pag,.mp4,.zip"
            class="hidden-input"
            multiple
            type="file"
            @change="handleFileInput"
          />
          <input
            ref="folderInputRef"
            accept="image/*,.svga,.pag,.mp4,.zip"
            class="hidden-input"
            multiple
            type="file"
            webkitdirectory
            @change="handleFileInput"
          />
          <div
            class="dropzone"
            :class="{ 'dropzone--active': dragging }"
            @click="fileInputRef?.click()"
            @dragenter.prevent="dragging = true"
            @dragleave.prevent="dragging = false"
            @dragover.prevent="dragging = true"
            @drop.prevent="handleDrop"
          >
            <div class="dropzone-title">拖拽文件到这里</div>
            <div class="dropzone-desc">只解析 礼物_ 开头的文件</div>
            <Space wrap>
              <Button type="primary" @click.stop="fileInputRef?.click()">
                选择文件
              </Button>
              <Button @click.stop="folderInputRef?.click()">选择文件夹</Button>
            </Space>
          </div>

          <div class="import-rules">
            <div class="side-title">识别规则</div>
            <div class="rule-item">礼物_物品名_价格_cover/animation</div>
            <div class="rule-item">礼物_物品名_价格_1天_cover/animation</div>
            <div class="rule-item">cover 写入封面，animation 写入资源</div>
            <div class="rule-item">默认金币，不填写有效期</div>
          </div>

          <div class="import-progress">
            <div class="progress-head">
              <span>导入进度</span>
              <span>{{ uploadAssetFinished }}/{{ uploadAssetTotal }}</span>
            </div>
            <Progress :percent="uploadPercent" size="small" />
            <div class="progress-meta">
              已识别 {{ rows.length }} 个，错误 {{ errorCount }} 个
            </div>
          </div>
        </aside>

        <main class="table-panel">
          <div class="table-toolbar">
            <Space wrap>
              <Button
                :disabled="rows.length === 0"
                @click="showOnlyErrors = !showOnlyErrors"
              >
                {{ showOnlyErrors ? '查看全部' : '只看错误' }}
              </Button>
              <Button
                danger
                :disabled="selectedRowKeys.length === 0"
                @click="deleteSelectedRows"
              >
                删除选中
              </Button>
            </Space>
            <span class="selection-text">
              已选 {{ selectedRowKeys.length }} 项
            </span>
          </div>

          <Table
            bordered
            :columns="columns"
            :custom-row="customRow"
            :data-source="visibleRows"
            :pagination="false"
            :row-class-name="getRowClassName"
            row-key="key"
            :row-selection="rowSelection"
            :scroll="{ x: 1120, y: 'calc(100vh - 360px)' }"
            size="small"
          >
            <template #bodyCell="{ column, record }">
              <template v-if="column.key === 'cover'">
                <div class="asset-cell">
                  <Image
                    v-if="record.cover"
                    class="asset-cover"
                    :src="getAccessImgUrl(record.cover)"
                  />
                  <div v-else class="asset-empty">
                    {{ record.coverUploading ? '上传中' : '缺少封面' }}
                  </div>
                  <div class="file-name">{{ record.coverFileName || '-' }}</div>
                </div>
              </template>
              <template v-else-if="column.key === 'sourceUrl'">
                <div class="file-cell">
                  <Tag :color="record.sourceUrl ? 'success' : 'default'">
                    {{ record.sourceUploading ? '上传中' : record.sourceUrl ? '已上传' : '未上传' }}
                  </Tag>
                  <div class="file-name">{{ record.sourceFileName || '-' }}</div>
                </div>
              </template>
              <template v-else-if="column.key === 'name'">
                <Input
                  v-model:value="record.name"
                  size="small"
                  @blur="markRowEdited(record)"
                />
              </template>
              <template v-else-if="column.key === 'code'">
                <Input
                  v-model:value="record.code"
                  size="small"
                  @blur="markRowEdited(record)"
                />
              </template>
              <template v-else-if="column.key === 'giftCandy'">
                <InputNumber
                  v-model:value="record.giftCandy"
                  :min="0"
                  size="small"
                  style="width: 96px"
                  @change="markRowEdited(record)"
                />
              </template>
              <template v-else-if="column.key === 'giftIntegral'">
                <InputNumber
                  v-model:value="record.giftIntegral"
                  :min="0"
                  size="small"
                  style="width: 96px"
                  @change="markRowEdited(record)"
                />
              </template>
              <template v-else-if="column.key === 'sort'">
                <InputNumber
                  v-model:value="record.sort"
                  size="small"
                  style="width: 76px"
                  @change="markRowEdited(record)"
                />
              </template>
              <template v-else-if="column.key === 'status'">
                <Tag :color="getStatusColor(record.status)">
                  {{ getStatusText(record.status) }}
                </Tag>
              </template>
              <template v-else-if="column.key === 'actions'">
                <Button danger size="small" type="link" @click.stop="deleteRow(record)">
                  删除
                </Button>
              </template>
            </template>
          </Table>
        </main>

        <aside class="preview-panel">
          <template v-if="activeRow">
            <div class="side-title">礼物预览</div>
            <div class="preview-cover">
              <Image
                v-if="activeRow.cover"
                class="preview-image"
                :src="getAccessImgUrl(activeRow.cover)"
              />
              <div v-else class="preview-empty">未上传封面</div>
            </div>
            <div class="preview-name">{{ activeRow.name || '-' }}</div>
            <div class="preview-meta">编码：{{ activeRow.code || '-' }}</div>
            <div class="preview-meta">封面：{{ activeRow.coverFileName || '-' }}</div>
            <div class="preview-meta">资源：{{ activeRow.sourceFileName || '-' }}</div>
            <div class="side-title side-title--spaced">校验结果</div>
            <div v-if="activeRow.errors.length === 0" class="success-text">
              可提交
            </div>
            <div v-else class="error-list">
              <Tag
                v-for="error in activeRow.errors"
                :key="error"
                color="error"
              >
                {{ error }}
              </Tag>
            </div>
          </template>
          <Empty v-else description="暂无礼物" />
        </aside>
      </div>
    </div>

    <template #footer>
      <div class="batch-footer">
        <div class="footer-stats">
          <span>已识别 {{ rows.length }} 个</span>
          <span>错误 {{ errorCount }} 个</span>
          <span>可提交 {{ submitTargetCount }} 个</span>
        </div>
        <Space>
          <Button :disabled="rows.length === 0 || submitting" @click="clearRows">
            清空
          </Button>
          <Button :disabled="submitting" @click="emit('close')">取消</Button>
          <Button
            :disabled="submitTargetCount === 0"
            :loading="submitting"
            type="primary"
            @click="submitBatch"
          >
            提交 {{ submitTargetCount }} 个
          </Button>
        </Space>
      </div>
    </template>
  </Drawer>
</template>

<style scoped>
.batch-drawer {
  background: #f5f7fb;
  display: flex;
  flex-direction: column;
  height: calc(100vh - 55px);
  min-width: 0;
}

.batch-common {
  align-items: flex-end;
  background: #fff;
  border-bottom: 1px solid #e5e7eb;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  padding: 14px 20px;
}

.common-field {
  display: grid;
  gap: 6px;
  min-width: 180px;
}

.common-field--short {
  min-width: 120px;
}

.common-field--wide {
  min-width: 320px;
}

.common-label,
.common-meta {
  color: #475569;
  font-size: 12px;
  line-height: 18px;
}

.common-meta {
  min-height: 32px;
  padding-top: 7px;
}

.batch-workspace {
  display: grid;
  flex: 1;
  gap: 12px;
  grid-template-columns: 260px minmax(620px, 1fr) 300px;
  min-height: 0;
  padding: 12px;
}

.import-panel,
.preview-panel,
.table-panel {
  background: #fff;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  min-height: 0;
}

.import-panel,
.preview-panel {
  display: flex;
  flex-direction: column;
  gap: 14px;
  overflow: auto;
  padding: 14px;
}

.table-panel {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.hidden-input {
  display: none;
}

.dropzone {
  align-items: center;
  background: #f8fafc;
  border: 1px dashed #94a3b8;
  border-radius: 8px;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 10px;
  min-height: 180px;
  padding: 20px 14px;
  text-align: center;
}

.dropzone:hover,
.dropzone--active {
  background: #eef6ff;
  border-color: #1677ff;
}

.dropzone-title {
  color: #111827;
  font-size: 16px;
  font-weight: 600;
}

.dropzone-desc {
  color: #64748b;
  font-size: 13px;
}

.side-title {
  color: #111827;
  font-weight: 600;
}

.side-title--spaced {
  margin-top: 6px;
}

.import-rules {
  display: grid;
  gap: 8px;
}

.rule-item,
.progress-meta,
.preview-meta {
  color: #64748b;
  font-size: 12px;
  line-height: 20px;
}

.progress-head {
  align-items: center;
  color: #334155;
  display: flex;
  font-size: 13px;
  justify-content: space-between;
  margin-bottom: 4px;
}

.table-toolbar {
  align-items: center;
  border-bottom: 1px solid #e5e7eb;
  display: flex;
  gap: 12px;
  justify-content: space-between;
  padding: 12px;
}

.selection-text {
  color: #64748b;
  flex-shrink: 0;
  font-size: 12px;
}

.asset-cell,
.file-cell {
  display: grid;
  gap: 6px;
  min-width: 0;
}

.asset-cover,
.asset-empty {
  height: 52px;
  width: 52px;
}

.asset-cover {
  border-radius: 6px;
  display: block;
  overflow: hidden;
}

.asset-cover :deep(.ant-image-img) {
  height: 52px;
  object-fit: contain;
  width: 52px;
}

.asset-empty {
  align-items: center;
  background: #f8fafc;
  border: 1px dashed #cbd5e1;
  border-radius: 6px;
  color: #94a3b8;
  display: flex;
  font-size: 12px;
  justify-content: center;
  text-align: center;
}

.file-name {
  color: #64748b;
  font-size: 12px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.preview-cover,
.preview-empty {
  align-items: center;
  background: #f8fafc;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  display: flex;
  justify-content: center;
  min-height: 180px;
  overflow: hidden;
}

.preview-empty {
  color: #94a3b8;
}

.preview-image {
  max-height: 220px;
  object-fit: contain;
  width: 100%;
}

.preview-name {
  color: #111827;
  font-size: 16px;
  font-weight: 600;
  overflow-wrap: anywhere;
}

.success-text {
  color: #16a34a;
}

.error-list {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.batch-footer {
  align-items: center;
  background: #fff;
  border-top: 1px solid #e5e7eb;
  display: flex;
  justify-content: space-between;
  padding: 12px 20px;
}

.footer-stats {
  color: #334155;
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  font-size: 13px;
}

:deep(.batch-row--error > td) {
  background: #fff7f7;
}

:deep(.batch-row--active > td) {
  background: #eef6ff;
}

@media (width <= 1180px) {
  .batch-workspace {
    grid-template-columns: 240px minmax(520px, 1fr);
  }

  .preview-panel {
    display: none;
  }
}
</style>
