import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
  Button, Card, Input, InputNumber, Modal, QRCode, Select, Space, Table, Tag, Tooltip, Typography, theme,
} from 'antd';
import type { TableProps } from 'antd';
import {
  CheckCircleOutlined, ColumnWidthOutlined, CopyOutlined, CreditCardOutlined, ExclamationCircleOutlined, FieldTimeOutlined, GiftOutlined,
  ImportOutlined, QrcodeOutlined, ReloadOutlined,
  SearchOutlined, SettingOutlined, ShoppingOutlined, StopOutlined,
} from '@ant-design/icons';
import {
  accountApi,
  douyuApi,
  type DouyuConfig,
  type DouyuGoodsItem,
  type DouyuTaskAccountItem,
  type DouyuTaskItem,
} from '../api/modules';
import ExchangeResultImage from '../components/ExchangeResultImage';
import QrActions from '../components/QrActions';
import { usePermissions } from '../hooks/usePermissions';
import { useWebSocketLogs } from '../hooks/useWebSocketLogs';
import {
  clipboardImageSupported,
  copyImageToClipboard,
  exchangeTaskSucceeded,
  getExchangeImage,
} from '../utils/exchangeImage';
import { getErrorMessage } from '../utils/error';
import { formatTime } from '../utils/time';
import { message } from '../utils/antdMessage';

const { Text } = Typography;

type HandbookKind = 'elite' | 'esports' | 'peace';

const TASK_STATUS_COLORS: Record<string, string> = {
  planned: 'default',
  pending: 'default',
  running: 'processing',
  success: 'success',
  failed: 'error',
  error: 'error',
  stopped: 'warning',
};

const TASK_STATUS_LABELS: Record<string, string> = {
  planned: '已计划',
  pending: '等待中',
  running: '执行中',
  success: '成功',
  failed: '失败',
  error: '异常',
  stopped: '已停止',
};

const ELITE_QUICK_ACTIONS = [
  { key: 'get_bind_qr', icon: <QrcodeOutlined /> },
  { key: 'confirm_bind', icon: <CheckCircleOutlined /> },
  { key: 'create_elite_qr', icon: <CreditCardOutlined /> },
  { key: 'query_points', icon: <SearchOutlined /> },
  { key: 'query_game_name', icon: <SearchOutlined /> },
  { key: 'query_change_bind_time', icon: <FieldTimeOutlined /> },
  { key: 'query_limited_goods', icon: <SearchOutlined /> },
  { key: 'query_gold_balance', icon: <SearchOutlined /> },
  { key: 'exchange_goods', icon: <ShoppingOutlined /> },
  { key: 'create_gold_qr', icon: <CreditCardOutlined /> },
  { key: 'donate_elite_gift', icon: <GiftOutlined /> },
];

const ESPORTS_QUICK_ACTIONS = [
  { key: 'prepare_esports_bind', icon: <QrcodeOutlined /> },
  { key: 'query_esports_game_name', icon: <SearchOutlined /> },
  { key: 'create_esports_qr', icon: <CreditCardOutlined /> },
  { key: 'query_esports_points', icon: <SearchOutlined /> },
  { key: 'query_change_bind_time', icon: <FieldTimeOutlined /> },
  { key: 'query_limited_goods', icon: <SearchOutlined /> },
  { key: 'query_gold_balance', icon: <SearchOutlined /> },
  { key: 'refresh_esports_goods', icon: <ReloadOutlined /> },
  { key: 'exchange_esports_goods', icon: <ShoppingOutlined /> },
  { key: 'create_gold_qr', icon: <CreditCardOutlined /> },
  { key: 'donate_esports_chicken_gift', icon: <GiftOutlined /> },
  { key: 'donate_esports_firework_gift', icon: <GiftOutlined /> },
];

const PEACE_QUICK_ACTIONS = [
  { key: 'get_xpd_bind_qr', icon: <QrcodeOutlined /> },
  { key: 'query_xpd_bind_info', icon: <SearchOutlined /> },
  { key: 'confirm_xpd_bind', icon: <CheckCircleOutlined /> },
  { key: 'query_xpd_role', icon: <SearchOutlined /> },
  { key: 'query_xpd_balance', icon: <SearchOutlined /> },
  { key: 'query_xpd_fragments', icon: <SearchOutlined /> },
  { key: 'refresh_xpd_goods', icon: <ReloadOutlined /> },
  { key: 'exchange_xpd_goods', icon: <ShoppingOutlined /> },
];

const ELITE_TASK_TYPES = new Set([
  'get_bind_qr',
  'confirm_bind',
  'create_elite_qr',
  'create_gold_qr',
  'donate_elite_gift',
  'query_points',
  'exchange_goods',
  'query_game_name',
  'query_change_bind_time',
  'query_limited_goods',
  'query_gold_balance',
  'refresh_goods',
  'query_exchange_records',
  'prefetch_csrf_token',
]);
const ESPORTS_TASK_TYPES = new Set([
  'prepare_esports_bind',
  'get_esports_bind_qr',
  'query_esports_game_name',
  'confirm_esports_bind',
  'create_esports_qr',
  'query_esports_points',
  'query_gold_balance',
  'query_change_bind_time',
  'query_limited_goods',
  'refresh_esports_goods',
  'exchange_esports_goods',
  'create_gold_qr',
  'donate_esports_chicken_gift',
  'donate_esports_firework_gift',
]);
const PEACE_TASK_TYPES = new Set([
  'get_xpd_bind_qr',
  'query_xpd_bind_info',
  'confirm_xpd_bind',
  'query_xpd_role',
  'refresh_xpd_goods',
  'query_xpd_balance',
  'query_xpd_fragments',
  'exchange_xpd_goods',
]);
const DOUYU_GOLD_AMOUNT_STORAGE_KEY = 'douyu_task_gold_amount';
const DOUYU_GIFT_COUNT_STORAGE_KEY = 'douyu_task_gift_count';
const DOUYU_LAYOUT_MODE_STORAGE_KEY = 'douyu_task_layout_mode';
// v2 避开旧版页面切换时写入其他工作台的共享账号列表。
const DOUYU_WORKBENCH_IDS_STORAGE_KEY = (kind: HandbookKind) => `douyu_task_workbench_ids_v2_${kind}`;
const CONFIRM_FAIL_TIP_SECONDS = 6;

function resultText(result: Record<string, unknown> | null | undefined, key: string): string {
  const value = result?.[key];
  return typeof value === 'string' ? value : '';
}

function resultFlag(result: Record<string, unknown> | null | undefined, key: string): boolean {
  const value = result?.[key];
  if (value === true) return true;
  if (value === false || value == null) return false;
  const text = String(value).trim().toLowerCase();
  return text === '1' || text === 'true' || text === 'yes';
}

// 提取任务结果中的二维码 URL：绑定/电竞绑定用 url，支付类用 pay_url
function taskQrUrl(task: DouyuTaskItem | null | undefined): string {
  if (!task) return '';
  if (task.task_type === 'get_bind_qr') return resultText(task.result, 'url');
  if (task.task_type === 'get_xpd_bind_qr') return resultText(task.result, 'url');
  if (['create_elite_qr', 'create_esports_qr', 'create_gold_qr'].includes(task.task_type)) {
    return resultText(task.result, 'pay_url');
  }
  if (['prepare_esports_bind', 'get_esports_bind_qr'].includes(task.task_type)) {
    return resultText(task.result, 'url');
  }
  return '';
}

function bindReadyForConfirm(task: DouyuTaskItem | null | undefined): boolean {
  if (!task) return false;
  if (task.task_type === 'get_xpd_bind_qr') {
    if (resultFlag(task.result, 'xpd_bound') || resultFlag(task.result, 'bind_confirmed')) return false;
    // 轮询识别到角色后停在待确认（xpd_pending_confirm），有角色名即可确认绑定
    return Boolean(resultText(task.result, 'role_name'));
  }
  if (task.task_type !== 'get_bind_qr') return false;
  if (task.result?.bind_ready_for_confirm === true) return true;
  const roleName = resultText(task.result, 'role_name');
  if (!roleName) return false;
  // 已确认绑定的角色不能再次直接确认
  if (resultFlag(task.result, 'bind_confirmed') || resultFlag(task.result, 'is_bound_act')) {
    return false;
  }
  return true;
}

function hasBindQrcode(task: DouyuTaskItem | null | undefined): boolean {
  if (!task) return false;
  if (!['get_bind_qr', 'get_xpd_bind_qr'].includes(task.task_type)) return false;
  // 有二维码，或已有待确认角色（无需再扫码）都应可展示绑定面板
  return Boolean(resultText(task.result, 'url'))
    || bindReadyForConfirm(task)
    || resultFlag(task.result, 'esports_bound');
}

function hasPaymentQrcode(task: DouyuTaskItem | null | undefined): boolean {
  return Boolean(
    task
    && ['create_elite_qr', 'create_esports_qr', 'create_gold_qr'].includes(task.task_type)
    && resultText(task.result, 'pay_url'),
  );
}

function paymentArrived(task: DouyuTaskItem | null | undefined): boolean {
  if (!task || !['create_elite_qr', 'create_esports_qr', 'create_gold_qr'].includes(task.task_type)) return false;
  if (task.status !== 'success') return false;
  return task.result?.gold_recharged === true
    || task.result?.elite_opened === true
    || task.result?.esports_opened === true;
}

function bindPhaseText(phase: string, polling: boolean): string {
  if (phase === 'role_ready') return '已识别角色，待确认';
  if (phase === 'change_waiting') return '换绑冷却中';
  if (phase === 'role_timeout') return '未检测到角色';
  if (phase === 'stopped') return '任务已停止';
  if (phase === 'confirmed') return '已绑定';
  if (phase === 'confirm_failed') return '确认未生效';
  if (polling || phase === 'waiting_scan') return '等待扫码绑定';
  return phase || '等待绑定';
}

function goodsLabel(item: DouyuGoodsItem): string {
  const stock = item.raw?.count;
  const stockText =
    stock != null && Number.isFinite(Number(stock)) ? ` / 库存${stock}` : '';
  return `${item.name || item.commodity_id}${item.score ? ` / ${item.score}积分` : ''}${stockText}`;
}

// 道聚城 iPrice 为 10 倍点券单位（如 6800=680 点券），仅展示层换算，兑换请求仍传原值。
const XPD_POINT_PRICE_SCALE = 10;

function xpdPointPrice(item: DouyuGoodsItem): number {
  const xpd = item as DouyuGoodsItem & { price?: number | null };
  const raw = item.raw?.raw as Record<string, unknown> | undefined;
  const rawPrice = Number(xpd.price ?? raw?.iPrice);
  if (!Number.isFinite(rawPrice) || rawPrice <= 0) return 0;
  return Math.round(rawPrice / XPD_POINT_PRICE_SCALE);
}

function xpdGoodsLabel(item: DouyuGoodsItem): string {
  const xpd = item as DouyuGoodsItem & { price?: number | null; goods_left?: number | null };
  const raw = item.raw?.raw as Record<string, unknown> | undefined;
  const pointPrice = xpdPointPrice(item);
  const fragmentPrice = Number(raw?.iJb2Price);
  const prices = [
    Number.isFinite(pointPrice) && pointPrice > 0 ? `${pointPrice}点券` : '',
    Number.isFinite(fragmentPrice) && fragmentPrice > 0 ? `${fragmentPrice}碎片` : '',
  ].filter(Boolean);
  const stock = xpd.goods_left;
  const stockText = stock != null && Number.isFinite(Number(stock))
    ? (Number(stock) < 0 ? '库存未公开' : `库存${stock}`)
    : '库存未知';
  return `${item.name || item.commodity_id}${prices.length ? ` / ${prices.join(' 或 ')}` : ' / 暂不可兑换'} / ${stockText}`;
}

function xpdGoodsPaymentOptions(item: DouyuGoodsItem | undefined): Array<{ value: '1' | '5'; label: string }> {
  if (!item) return [];
  const raw = item.raw?.raw as Record<string, unknown> | undefined;
  const pointPrice = xpdPointPrice(item);
  const fragmentPrice = Number(raw?.iJb2Price);
  return [
    Number.isFinite(pointPrice) && pointPrice > 0
      ? { value: '1' as const, label: `使用点券兑换（${pointPrice}点券）` }
      : null,
    Number.isFinite(fragmentPrice) && fragmentPrice > 0
      ? { value: '5' as const, label: `使用扭蛋碎片兑换（${fragmentPrice}碎片）` }
      : null,
  ].filter((option): option is { value: '1' | '5'; label: string } => option !== null);
}

function formatWaitSeconds(seconds: number | null | undefined): string {
  if (seconds == null || Number.isNaN(Number(seconds))) return '';
  const total = Math.max(0, Math.floor(Number(seconds)));
  if (total <= 0) return '可换绑';
  const days = Math.floor(total / 86400);
  const hours = Math.floor((total % 86400) / 3600);
  const minutes = Math.floor((total % 3600) / 60);
  const secs = total % 60;
  if (days > 0) return `${days}天${hours}小时${minutes}分`;
  if (hours > 0) return `${hours}小时${minutes}分${secs}秒`;
  return `${minutes}分${secs}秒`;
}

function esportsRebindStatus(canChangeTime: number | null | undefined): {
  available: boolean;
  text: string;
} | null {
  if (canChangeTime == null || Number.isNaN(Number(canChangeTime))) return null;
  const timestamp = Math.floor(Number(canChangeTime));
  if (timestamp <= 0 || timestamp <= Math.floor(Date.now() / 1000)) {
    return { available: true, text: '可换绑' };
  }
  return {
    available: false,
    text: new Date(timestamp * 1000).toLocaleString('zh-CN', { hour12: false }),
  };
}

function resultNumber(result: Record<string, unknown> | null | undefined, key: string): number | null {
  const value = result?.[key];
  if (typeof value === 'number' && Number.isFinite(value)) return value;
  if (typeof value === 'string' && value.trim()) {
    const n = Number(value);
    return Number.isFinite(n) ? n : null;
  }
  return null;
}

function limitedGoodsName(item: unknown): string {
  if (!item || typeof item !== 'object') return '';
  const record = item as Record<string, unknown>;
  const value = record.commodityName ?? record.name;
  return typeof value === 'string' ? value : '';
}

function limitedGoodsTaskText(task: DouyuTaskItem | null | undefined): string {
  if (!task) return '';
  if (task.message?.trim()) return task.message.trim();
  const count = resultNumber(task.result, 'limited_count');
  if (count == null) return '';
  if (count <= 0) return '无限制商品';
  const goods = task.result?.limited_goods;
  const names = Array.isArray(goods)
    ? goods.map(limitedGoodsName).filter(Boolean).slice(0, 5)
    : [];
  return names.length ? `限兑 ${count} 个: ${names.join(', ')}` : `限兑 ${count} 个`;
}

function savedPositiveInteger(key: string, fallback = 1): number {
  const value = Number(localStorage.getItem(key));
  return Number.isInteger(value) && value >= 1 ? value : fallback;
}

export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) {
  const isPeaceHandbook = handbook === 'peace';
  const isEsportsHandbook = handbook === 'esports';
  const handbookTitle = isPeaceHandbook
    ? '和平小店工作台'
    : (isEsportsHandbook ? '电竞手册工作台' : '精英宝典工作台');
  const handbookDescription = isPeaceHandbook
    ? '和平小店绑定角色、商品与点券余额查询'
    : (isEsportsHandbook
      ? '电竞手册角色绑定与开通任务'
      : '精英宝典绑定、开通、积分与兑换任务');
  const activeTaskTypes = isPeaceHandbook
    ? PEACE_TASK_TYPES
    : (isEsportsHandbook ? ESPORTS_TASK_TYPES : ELITE_TASK_TYPES);
  const quickActions = isPeaceHandbook
    ? PEACE_QUICK_ACTIONS
    : (isEsportsHandbook ? ESPORTS_QUICK_ACTIONS : ELITE_QUICK_ACTIONS);
  const { can } = usePermissions();
  const { token } = theme.useToken();
  const [accounts, setAccounts] = useState<DouyuTaskAccountItem[]>([]);
  // 工作台已导入账号 ID（localStorage 持久化，默认空白，用户手动导入/移除）
  const [workbenchIds, setWorkbenchIds] = useState<number[]>(() => {
    const raw = localStorage.getItem(DOUYU_WORKBENCH_IDS_STORAGE_KEY(handbook));
    if (!raw) return [];
    return raw.split(',').map(Number).filter(Number.isInteger).filter((id) => id >= 1);
  });
  const [selectedIds, setSelectedIds] = useState<number[]>([]);
  const [goods, setGoods] = useState<DouyuGoodsItem[]>([]);
  const [tasks, setTasks] = useState<DouyuTaskItem[]>([]);
  const [taskTypes, setTaskTypes] = useState<Record<string, string>>({});
  const [config, setConfig] = useState<DouyuConfig | null>(null);
  const [loading, setLoading] = useState(false);
  const [runningBatchIds, setRunningBatchIds] = useState<Set<string>>(new Set());
  const [configOpen, setConfigOpen] = useState(false);
  const [concurrency, setConcurrency] = useState(3);
  const [goldAmount, setGoldAmount] = useState(() => savedPositiveInteger(DOUYU_GOLD_AMOUNT_STORAGE_KEY));
  const [giftCount, setGiftCount] = useState(() => savedPositiveInteger(DOUYU_GIFT_COUNT_STORAGE_KEY));
  const [selectedGoodsId, setSelectedGoodsId] = useState('');
  const [xpdPayType, setXpdPayType] = useState<'1' | '5'>('1');
  const [accountSearch, setAccountSearch] = useState('');
  const [layoutMode, setLayoutMode] = useState<'stack' | 'split'>(() => {
    const v = localStorage.getItem(DOUYU_LAYOUT_MODE_STORAGE_KEY);
    return v === 'split' ? 'split' : 'stack';
  });
  const accountTableAreaRef = useRef<HTMLDivElement | null>(null);
  const [accountTableAreaHeight, setAccountTableAreaHeight] = useState(360);
  const [accountPageSize, setAccountPageSize] = useState<number>(() => {
    const v = Number(localStorage.getItem('douyu_task_account_page_size'));
    return [10, 20, 50, 100].includes(v) ? v : 20;
  });
  const [accountPage, setAccountPage] = useState(1);
  const [accountTotal, setAccountTotal] = useState(0);
  const tasksLoadingRef = useRef(false);

  const [importOpen, setImportOpen] = useState(false);
  const [importPool, setImportPool] = useState<DouyuTaskAccountItem[]>([]);
  const [importSearch, setImportSearch] = useState('');
  const [importTag, setImportTag] = useState('');
  const [importTags, setImportTags] = useState<string[]>([]);
  const [importSelectedIds, setImportSelectedIds] = useState<number[]>([]);
  const [importLoading, setImportLoading] = useState(false);
  const [importAllLoading, setImportAllLoading] = useState(false);
  const [importPage, setImportPage] = useState(1);
  const [importTotal, setImportTotal] = useState(0);
  const importPageSize = 20;

  // 绑定/支付弹窗以 task 为唯一数据源；二维码弹窗支持多账号并行（Tabs 切换）
  const [qrTaskIds, setQrTaskIds] = useState<number[]>([]);
  const [activeQrTaskId, setActiveQrTaskId] = useState<number | null>(null);
  const [esportsBindTask, setEsportsBindTask] = useState<DouyuTaskItem | null>(null);
  const [payTask, setPayTask] = useState<DouyuTaskItem | null>(null);
  const qrCanvasWrapRef = useRef<HTMLDivElement | null>(null);
  const esportsQrCanvasWrapRef = useRef<HTMLDivElement | null>(null);
  const payQrCanvasWrapRef = useRef<HTMLDivElement | null>(null);
  const autoOpenedQrTaskIds = useRef<Set<number>>(new Set());
  const autoOpenedEsportsBindTaskIds = useRef<Set<number>>(new Set());
  const autoOpenedPayTaskIds = useRef<Set<number>>(new Set());
  const autoCopiedExchangeTaskIds = useRef<Set<number>>(new Set());
  const autoNotifiedConfirmFailedTaskIds = useRef<Set<number>>(new Set());
  const [confirmFailTip, setConfirmFailTip] = useState<{ taskId: number; message: string } | null>(null);
  const [confirmFailCountdown, setConfirmFailCountdown] = useState(0);
  const [exchangePreview, setExchangePreview] = useState<{ task: DouyuTaskItem; url: string } | null>(null);
  const autoOpenQrReady = useRef(false);
  const autoOpenEsportsBindReady = useRef(false);
  const autoOpenPayReady = useRef(false);

  const logs = useWebSocketLogs();
  const canConfig = can('douyu:config');
  const latestQueryGameTaskByAccount = useMemo(() => {
    const map = new Map<number, DouyuTaskItem>();
    for (const task of tasks) {
      if (task.task_type !== 'query_game_name') continue;
      const previous = map.get(task.account_id);
      if (!previous || task.id > previous.id) map.set(task.account_id, task);
    }
    return map;
  }, [tasks]);

  const latestConfirmBindTaskByAccount = useMemo(() => {
    const map = new Map<number, DouyuTaskItem>();
    for (const task of tasks) {
      if (task.task_type !== 'confirm_bind') continue;
      if (!task.result) continue;
      const previous = map.get(task.account_id);
      if (!previous || task.id > previous.id) map.set(task.account_id, task);
    }
    return map;
  }, [tasks]);

  const latestXpdQueryTaskByAccount = useMemo(() => {
    const map = new Map<number, DouyuTaskItem>();
    for (const task of tasks) {
      if (!['query_xpd_bind_info', 'query_xpd_role'].includes(task.task_type)) continue;
      const previous = map.get(task.account_id);
      if (!previous || task.id > previous.id) map.set(task.account_id, task);
    }
    return map;
  }, [tasks]);

  const latestConfirmXpdBindTaskByAccount = useMemo(() => {
    const map = new Map<number, DouyuTaskItem>();
    for (const task of tasks) {
      if (task.task_type !== 'confirm_xpd_bind') continue;
      const previous = map.get(task.account_id);
      if (!previous || task.id > previous.id) map.set(task.account_id, task);
    }
    return map;
  }, [tasks]);

  const latestConfirmBindFailureByAccount = useMemo(() => {
    const map = new Map<number, DouyuTaskItem>();
    for (const task of tasks) {
      if (task.task_type !== 'confirm_bind') continue;
      if (!['failed', 'error'].includes(task.status)) continue;
      if (!(task.message || '').includes('待绑定游戏账号')) continue;
      const previous = map.get(task.account_id);
      if (!previous || task.id > previous.id) map.set(task.account_id, task);
    }
    return map;
  }, [tasks]);

  const latestEsportsStateTaskByAccount = useMemo(() => {
    const map = new Map<number, DouyuTaskItem>();
    for (const task of tasks) {
      if (!['prepare_esports_bind', 'get_esports_bind_qr', 'query_esports_game_name', 'confirm_esports_bind']
        .includes(task.task_type) || !task.result) continue;
      const previous = map.get(task.account_id);
      if (!previous || task.id > previous.id) map.set(task.account_id, task);
    }
    return map;
  }, [tasks]);

  const visibleTasks = useMemo(
    () => tasks.filter((task) => activeTaskTypes.has(task.task_type)),
    [activeTaskTypes, tasks],
  );

  // qrTask 由 activeQrTaskId 从最新 tasks 中派生，切换 Tab 即切换关注账号
  const qrTask = useMemo(
    () => (activeQrTaskId == null ? null : tasks.find((t) => t.id === activeQrTaskId) || null),
    [tasks, activeQrTaskId],
  );

  // 换绑时间：优先最近一次查询/拦截/确认成功结果，回退账号落库字段
  const latestChangeWaitByAccount = useMemo(() => {
    const map = new Map<number, {
      wait: number | null; canChangeTime: number | null; text: string; taskId: number;
    }>();
    for (const task of tasks) {
      const taskTypes = isEsportsHandbook
        ? ['prepare_esports_bind', 'get_esports_bind_qr', 'query_esports_game_name', 'confirm_esports_bind']
        : ['query_change_bind_time', 'get_bind_qr', 'confirm_bind'];
      if (!taskTypes.includes(task.task_type)) continue;
      if (!['success', 'failed'].includes(task.status)) continue;
      if (
        !isEsportsHandbook
        && task.task_type === 'confirm_bind'
        && (
          task.status !== 'success'
          || !(
            resultFlag(task.result, 'bind_confirmed')
            || resultFlag(task.result, 'is_bound_act')
          )
        )
      ) continue;
      const wait = resultNumber(task.result, 'change_role_wait_time');
      const canChangeTime = resultNumber(task.result, 'can_change_time');
      // get_bind_qr 只有冷却拦截时才有 wait；无 wait 时跳过，避免覆盖
      if (!isEsportsHandbook && task.task_type === 'get_bind_qr' && wait == null) continue;
      const existing = map.get(task.account_id);
      if (existing && existing.taskId > task.id) continue;
      const esportsStatus = isEsportsHandbook ? esportsRebindStatus(canChangeTime) : null;
      const text = esportsStatus?.text || resultText(task.result, 'change_role_wait_text') || formatWaitSeconds(wait);
      map.set(task.account_id, { wait, canChangeTime, text, taskId: task.id });
    }
    return map;
  }, [isEsportsHandbook, tasks]);

  const latestLimitedGoodsTaskByAccount = useMemo(() => {
    const map = new Map<number, DouyuTaskItem>();
    for (const task of tasks) {
      if (task.task_type !== 'query_limited_goods') continue;
      if (!['success', 'failed', 'error'].includes(task.status)) continue;
      const previous = map.get(task.account_id);
      if (!previous || task.id > previous.id) map.set(task.account_id, task);
    }
    return map;
  }, [tasks]);

  // 每个账号最近一条任务（当前手册类型），用于账号表"最近结果"列快速查看二维码
  const latestTaskByAccount = useMemo(() => {
    const map = new Map<number, DouyuTaskItem>();
    for (const task of visibleTasks) {
      const previous = map.get(task.account_id);
      if (!previous || task.id > previous.id) map.set(task.account_id, task);
    }
    return map;
  }, [visibleTasks]);

  // 每个账号最近一条兑换成功任务，用于"兑换图片"列
  const latestExchangeTaskByAccount = useMemo(() => {
    const map = new Map<number, DouyuTaskItem>();
    for (const task of visibleTasks) {
      if (!exchangeTaskSucceeded(task)) continue;
      const previous = map.get(task.account_id);
      if (!previous || task.id > previous.id) map.set(task.account_id, task);
    }
    return map;
  }, [visibleTasks]);

  const loadData = useCallback(async () => {
    setLoading(true);
    try {
      const [accResult, goodsResult, taskResult, cfgResult, typeResult] = await Promise.allSettled([
        workbenchIds.length > 0
          ? douyuApi.listAccountsPaged({
            ids: workbenchIds.join(','),
            search: accountSearch.trim() || undefined,
            page: accountPage,
            page_size: accountPageSize,
          })
          : Promise.resolve({ items: [] as DouyuTaskAccountItem[], total: 0, page: accountPage, page_size: accountPageSize }),
        isPeaceHandbook
          ? douyuApi.listPeaceGoods()
          : (isEsportsHandbook ? douyuApi.listEsportsGoods() : douyuApi.listGoods()),
        douyuApi.listTasks(),
        canConfig ? douyuApi.getConfig() : Promise.resolve(null),
        douyuApi.taskTypes(),
      ]);
      if (accResult.status === 'fulfilled') {
        setAccounts(accResult.value.items);
        setAccountTotal(accResult.value.total);
        if (accountPage > 1 && accResult.value.items.length === 0) {
          setAccountPage(Math.max(1, Math.ceil(accResult.value.total / accountPageSize)));
        }
      }
      if (goodsResult.status === 'fulfilled') setGoods(goodsResult.value);
      if (taskResult.status === 'fulfilled') {
        setTasks(taskResult.value);
        // 仅首次加载时屏蔽历史二维码自动弹窗；后续刷新不能把新任务一并标记掉
        if (!autoOpenQrReady.current) {
          for (const task of taskResult.value) {
            if (hasBindQrcode(task)) autoOpenedQrTaskIds.current.add(task.id);
            if (task.task_type === 'prepare_esports_bind' && task.result?.esports_bind_dialog === true) {
              autoOpenedEsportsBindTaskIds.current.add(task.id);
            }
            if (hasPaymentQrcode(task)) autoOpenedPayTaskIds.current.add(task.id);
            // 历史兑换成功任务不触发自动复制，避免页面加载时打扰
            if (exchangeTaskSucceeded(task)) autoCopiedExchangeTaskIds.current.add(task.id);
            // 历史确认绑定失败任务不重复弹提示（只提示一次）
            if (
              task.task_type === 'confirm_bind'
              && ['failed', 'error'].includes(task.status)
              && (task.message || '').includes('待绑定游戏账号')
            ) {
              autoNotifiedConfirmFailedTaskIds.current.add(task.id);
            }
          }
          autoOpenQrReady.current = true;
          autoOpenEsportsBindReady.current = true;
          autoOpenPayReady.current = true;
        }
      }
      if (cfgResult.status === 'fulfilled' && cfgResult.value) setConfig(cfgResult.value);
      if (typeResult.status === 'fulfilled') setTaskTypes(typeResult.value);
    } finally {
      setLoading(false);
    }
  }, [accountPage, accountPageSize, accountSearch, canConfig, isEsportsHandbook, isPeaceHandbook, workbenchIds]);

  const loadTasks = useCallback(async () => {
    if (tasksLoadingRef.current) return;
    tasksLoadingRef.current = true;
    try {
      const data = await douyuApi.listTasks();
      setTasks(data);
      if (!data.some((task) => ['pending', 'running', 'planned'].includes(task.status))) {
        // 批次已结束时清理 busy 标记，避免 onResult 丢失导致永久锁住
        setRunningBatchIds(new Set());
      }
    } catch {
      // 轮询失败不打扰操作
    } finally {
      tasksLoadingRef.current = false;
    }
  }, []);

  useEffect(() => { loadData(); }, [loadData]);

  useEffect(() => {
    localStorage.setItem(DOUYU_LAYOUT_MODE_STORAGE_KEY, layoutMode);
  }, [layoutMode]);

  // 工作台账号集合持久化：移除账号后刷新不再出现
  useEffect(() => {
    localStorage.setItem(
      DOUYU_WORKBENCH_IDS_STORAGE_KEY(handbook),
      workbenchIds.length ? workbenchIds.join(',') : '',
    );
  }, [handbook, workbenchIds]);

  // 监听账号表格区域高度变化，动态计算 scroll.y 实现表体内部滚动
  useEffect(() => {
    const node = accountTableAreaRef.current;
    if (!node) return;
    const update = () => setAccountTableAreaHeight(Math.max(200, Math.floor(node.getBoundingClientRect().height)));
    update();
    if (typeof ResizeObserver === 'undefined') {
      window.addEventListener('resize', update);
      return () => window.removeEventListener('resize', update);
    }
    const observer = new ResizeObserver(update);
    observer.observe(node);
    return () => observer.disconnect();
  }, [layoutMode]);

  useEffect(() => {
    localStorage.setItem(DOUYU_GOLD_AMOUNT_STORAGE_KEY, String(goldAmount));
  }, [goldAmount]);

  useEffect(() => {
    localStorage.setItem(DOUYU_GIFT_COUNT_STORAGE_KEY, String(giftCount));
  }, [giftCount]);

  const hasActiveTasks = useMemo(
    () => visibleTasks.some((task) => ['pending', 'running', 'planned'].includes(task.status)),
    [visibleTasks],
  );

  // 正在执行任务的账号集合，用于按账号维度禁用，避免同一账号并发跑不同类型任务
  const runningAccountIds = useMemo(() => {
    const set = new Set<number>();
    for (const task of visibleTasks) {
      if (['pending', 'running', 'planned'].includes(task.status)) {
        set.add(task.account_id);
      }
    }
    return set;
  }, [visibleTasks]);

  // 从后端任务派生正在运行的批次 ID（刷新页面后仍可恢复，用于显示停止按钮）
  const activeBatchIds = useMemo(() => {
    const set = new Set<string>();
    for (const task of visibleTasks) {
      if (['pending', 'running', 'planned'].includes(task.status) && task.batch_id) {
        set.add(task.batch_id);
      }
    }
    return set;
  }, [visibleTasks]);

  const selectedHasRunning = selectedIds.some((id) => runningAccountIds.has(id));

  // 任务状态由 WS(level=task) 即时推送；轮询仅作兜底（多标签页/断线重连），
  // 有活跃任务或 WS 连接时 5 秒一次，空闲 30 秒轻量刷新
  useEffect(() => {
    const intervalMs = hasActiveTasks || logs.connected || runningBatchIds.size > 0 ? 5000 : 30000;
    const timer = setInterval(loadTasks, intervalMs);
    return () => clearInterval(timer);
  }, [hasActiveTasks, loadTasks, logs.connected, runningBatchIds]);

  const openQrTask = useCallback((task: DouyuTaskItem, activate = true) => {
    autoOpenedQrTaskIds.current.add(task.id);
    setQrTaskIds((prev) => (prev.includes(task.id) ? prev : [...prev, task.id]));
    if (activate) setActiveQrTaskId(task.id);
    // 绑定二维码与支付/电竞面板互斥，避免两个 Modal 叠加混淆
    setEsportsBindTask(null);
    setPayTask(null);
  }, []);

  const closeQrTask = useCallback((taskId: number) => {
    setQrTaskIds((prev) => {
      const next = prev.filter((id) => id !== taskId);
      setActiveQrTaskId((cur) => (cur === taskId ? (next.length > 0 ? next[next.length - 1] : null) : cur));
      return next;
    });
  }, []);

  const closeAllQrTasks = useCallback(() => {
    setQrTaskIds([]);
    setActiveQrTaskId(null);
  }, []);

  const openEsportsBindTask = useCallback((task: DouyuTaskItem) => {
    autoOpenedEsportsBindTaskIds.current.add(task.id);
    closeAllQrTasks();
    setPayTask(null);
    setEsportsBindTask(task);
  }, [closeAllQrTasks]);

  const openPayTask = useCallback((task: DouyuTaskItem) => {
    autoOpenedPayTaskIds.current.add(task.id);
    closeAllQrTasks();
    setEsportsBindTask(null);
    setPayTask(task);
  }, [closeAllQrTasks]);

  // 自动弹出最新绑定二维码（running 有 url 即可）；多账号并行，不互斥
  useEffect(() => {
    if (!autoOpenQrReady.current) return;
    const candidates = visibleTasks
      .filter((task) => hasBindQrcode(task) && !autoOpenedQrTaskIds.current.has(task.id))
      .sort((a, b) => b.id - a.id);
    if (candidates.length === 0) return;
    // 全部加入标签栏但不抢焦点；仅在当前无 active 标签时激活最新（id 最大）的
    for (const candidate of candidates) openQrTask(candidate, false);
    setActiveQrTaskId((cur) => cur ?? candidates[0].id);
  }, [openQrTask, visibleTasks]);

  // 电竞手册先查状态再打开独立绑定面板，不把切换角色二维码当作主流程。
  useEffect(() => {
    if (!autoOpenEsportsBindReady.current || esportsBindTask) return;
    const nextTask = visibleTasks
      .filter((task) => (
        task.task_type === 'prepare_esports_bind'
        && task.result?.esports_bind_dialog === true
        && !autoOpenedEsportsBindTaskIds.current.has(task.id)
      ))
      .sort((a, b) => b.id - a.id)[0];
    if (nextTask) openEsportsBindTask(nextTask);
  }, [esportsBindTask, openEsportsBindTask, visibleTasks]);

  // 自动弹出支付二维码
  useEffect(() => {
    if (!autoOpenPayReady.current || payTask) return;
    const nextPayTask = visibleTasks
      .filter((task) => hasPaymentQrcode(task) && !paymentArrived(task) && !autoOpenedPayTaskIds.current.has(task.id))
      .sort((a, b) => b.id - a.id)[0];
    if (nextPayTask) openPayTask(nextPayTask);
  }, [openPayTask, payTask, visibleTasks]);

  // 兑换成功后自动生成结果图片并复制到剪贴板（批量时最后完成的覆盖剪贴板）
  useEffect(() => {
    if (!autoOpenQrReady.current) return;
    const pending = visibleTasks
      .filter((task) => exchangeTaskSucceeded(task) && !autoCopiedExchangeTaskIds.current.has(task.id))
      .sort((a, b) => a.id - b.id);
    if (pending.length === 0) return;
    for (const task of pending) autoCopiedExchangeTaskIds.current.add(task.id);
    const latest = pending[pending.length - 1];
    void getExchangeImage(latest)
      .then(({ blob }) => copyImageToClipboard(blob))
      .then(
        () => message.success(`兑换图片已复制到剪贴板：${latest.account_nickname || latest.account_username || `#${latest.account_id}`}`),
        (error) => {
          if (clipboardImageSupported()) {
            message.warning('兑换图片已生成，复制到剪贴板失败');
          } else {
            message.info('当前环境不支持复制图片，请在兑换图片列点击查看');
          }
          console.warn('复制兑换图片失败', error);
        },
      );
  }, [visibleTasks]);

  // 确认绑定失败自动弹提示（页面中央弹窗，自动消失，一次性去重）
  useEffect(() => {
    if (!autoOpenQrReady.current) return;
    const failedTasks = visibleTasks
      .filter((task) => (
        task.task_type === 'confirm_bind'
        && ['failed', 'error'].includes(task.status)
        && (task.message || '').includes('待绑定游戏账号')
        && !autoNotifiedConfirmFailedTaskIds.current.has(task.id)
      ))
      .sort((a, b) => b.id - a.id);
    if (failedTasks.length === 0) return;
    for (const task of failedTasks) autoNotifiedConfirmFailedTaskIds.current.add(task.id);
    const latest = failedTasks[0];
    setConfirmFailTip({ taskId: latest.id, message: latest.message });
    setQrTaskIds((prev) => {
      const next = prev.filter((id) => {
        const task = visibleTasks.find((item) => item.id === id);
        return !task || task.account_id !== latest.account_id;
      });
      setActiveQrTaskId((cur) => (cur != null && !next.includes(cur) ? (next[next.length - 1] ?? null) : cur));
      return next;
    });
  }, [visibleTasks]);

  // 弹窗倒计时自动关闭
  useEffect(() => {
    if (!confirmFailTip) return;
    setConfirmFailCountdown(CONFIRM_FAIL_TIP_SECONDS);
    const timer = setInterval(() => {
      setConfirmFailCountdown((cur) => {
        if (cur <= 1) {
          clearInterval(timer);
          setConfirmFailTip(null);
          return 0;
        }
        return cur - 1;
      });
    }, 1000);
    return () => clearInterval(timer);
  }, [confirmFailTip]);

  const copyExchangePreview = async () => {
    if (!exchangePreview) return;
    try {
      const { blob } = await getExchangeImage(exchangePreview.task);
      await copyImageToClipboard(blob);
      message.success('已复制到剪贴板');
    } catch (error) {
      message.error(getErrorMessage(error));
    }
  };

  // qrTask 已由 useMemo 从 tasks 派生，切换 Tab / 后端 progress 写入后自动更新，无需手动跟随

  useEffect(() => {
    if (!esportsBindTask) return;
    const latest = tasks.find((task) => task.id === esportsBindTask.id);
    if (latest && latest !== esportsBindTask) setEsportsBindTask(latest);
  }, [esportsBindTask, tasks]);

  useEffect(() => {
    if (!payTask) return;
    const latest = tasks.find((task) => task.id === payTask.id);
    if (!latest) return;
    if (paymentArrived(latest)) {
      setPayTask(null);
      return;
    }
    if (latest !== payTask) setPayTask(latest);
  }, [payTask, tasks]);

  // 多批次并发：不再用全局 busy 标志阻塞 UI，刷新频率直接用各子条件判断

  const selectedXpdGoods = useMemo(
    () => goods.find((item) => item.commodity_id === selectedGoodsId),
    [goods, selectedGoodsId],
  );
  const xpdPaymentOptions = useMemo(
    () => xpdGoodsPaymentOptions(selectedXpdGoods),
    [selectedXpdGoods],
  );
  const selectedXpdGoodsSoldOut = (() => {
    const stock = (selectedXpdGoods as (DouyuGoodsItem & { goods_left?: number | null }) | undefined)?.goods_left;
    return stock != null && Number.isFinite(Number(stock)) && Number(stock) === 0;
  })();

  useEffect(() => {
    if (xpdPaymentOptions.length === 0) return;
    if (!xpdPaymentOptions.some((option) => option.value === xpdPayType)) {
      setXpdPayType(xpdPaymentOptions[0].value);
    }
  }, [xpdPayType, xpdPaymentOptions]);

  const startTask = async (
    taskType: string,
    accountIds?: number[],
    extraPayload: Record<string, unknown> = {},
  ) => {
    const ids = accountIds || selectedIds;
    if (ids.length === 0) {
      message.warning('请先选择账号');
      return;
    }
    // 多批次并发：不再拦截，允许在已有批次运行时继续创建新批次
    if (['exchange_goods', 'exchange_esports_goods', 'exchange_xpd_goods'].includes(taskType) && !selectedGoodsId) {
      message.warning('请先选择兑换商品');
      return;
    }
    if (taskType === 'exchange_xpd_goods') {
      if (selectedXpdGoodsSoldOut) {
        message.warning('该商品库存不足，请刷新商品列表后重试');
        return;
      }
      if (!xpdPaymentOptions.some((option) => option.value === xpdPayType)) {
        message.warning('该商品不支持当前兑换方式');
        return;
      }
    }
    const payload: Record<string, unknown> = { ...extraPayload };
    if (['exchange_goods', 'exchange_esports_goods', 'exchange_xpd_goods'].includes(taskType)) {
      payload.commodity_id = selectedGoodsId;
    }
    if (taskType === 'exchange_xpd_goods') payload.pay_type = xpdPayType;
    if (taskType === 'create_gold_qr') payload.amount = goldAmount;
    if (['donate_elite_gift', 'donate_esports_chicken_gift', 'donate_esports_firework_gift'].includes(taskType)) {
      payload.gift_count = giftCount;
    }
    try {
      const result = await douyuApi.createTasks({
        account_ids: ids,
        task_type: taskType,
        concurrency,
        payload,
      });
      setRunningBatchIds((prev) => new Set(prev).add(result.batch_id));
      logs.connectBatch(result.batch_id, `/api/douyu/ws/${result.batch_id}`, {
        clear: true,
        onResult: () => {
          setRunningBatchIds((prev) => {
            const next = new Set(prev);
            next.delete(result.batch_id);
            return next;
          });
          void loadData();
        },
        onTask: (rawTask) => {
          const task = rawTask as unknown as DouyuTaskItem;
          // 已有任务原位更新保持排序；新任务插入头部（与后端 id desc 一致）
          setTasks((prev) => {
            if (prev.some((t) => t.id === task.id)) {
              return prev.map((t) => (t.id === task.id ? task : t));
            }
            return [task, ...prev].slice(0, 100);
          });
        },
      });
      message.success(`已创建 ${result.count} 个任务`);
      setTimeout(() => { void loadTasks(); }, 400);
    } catch (error) {
      message.error(getErrorMessage(error));
    }
  };

  const stopBatch = async () => {
    const ids = Array.from(new Set([...runningBatchIds, ...activeBatchIds]));
    if (ids.length === 0) return;
    try {
      await Promise.all(ids.map((id) => douyuApi.stopBatch(id)));
      message.success('已停止');
    } catch (error) {
      message.error(getErrorMessage(error));
    }
  };

  const saveConfig = async (values: DouyuConfig) => {
    try {
      const saved = await douyuApi.updateConfig(values);
      setConfig(saved);
      setConfigOpen(false);
      message.success('配置已保存');
    } catch (error) {
      message.error(getErrorMessage(error));
    }
  };

  // ---- Import ----
  const loadImportPool = useCallback(async () => {
    setImportLoading(true);
    try {
      // 候选池 = 账号库按搜索/标签过滤，剔除已导入工作台的账号
      const result = await douyuApi.listAccountsPaged({
        search: importSearch.trim() || undefined,
        tag: importTag || undefined,
        page: importPage,
        page_size: importPageSize,
      });
      setImportPool(result.items.filter((a) => !workbenchIds.includes(a.id)));
      setImportTotal(result.total);
    } finally {
      setImportLoading(false);
    }
  }, [importPage, importSearch, importTag, workbenchIds]);
  useEffect(() => { if (importOpen) void loadImportPool(); }, [importOpen, loadImportPool]);
  useEffect(() => {
    if (!importOpen) return;
    accountApi.listTags().then(setImportTags).catch(() => {});
  }, [importOpen]);
  // 切换搜索/标签时清空已选，避免计数与可见行不一致
  useEffect(() => {
    setImportPage(1);
    setImportSelectedIds([]);
  }, [importSearch, importTag]);
  useEffect(() => { setImportSelectedIds([]); }, [importPage]);

  const importAccounts = (ids: number[]) => {
    const toImport = importPool.filter((a) => ids.includes(a.id));
    setWorkbenchIds((prev) => [...new Set([...prev, ...toImport.map((a) => a.id)])]);
    setAccounts((prev) => {
      const existing = new Set(prev.map((a) => a.id));
      return [...prev, ...toImport.filter((a) => !existing.has(a.id))];
    });
    setImportSelectedIds([]);
    setImportOpen(false);
    message.success(`已导入 ${toImport.length} 个账号`);
  };

  const importAllAccounts = async () => {
    setImportAllLoading(true);
    try {
      const result = await douyuApi.listAccountIds({
        search: importSearch.trim() || undefined,
        tag: importTag || undefined,
      });
      const imported = result.account_ids.filter((id) => !workbenchIds.includes(id));
      if (imported.length === 0) {
        message.info('当前筛选条件下没有可新增的账号');
        return;
      }
      setWorkbenchIds((prev) => [...new Set([...prev, ...imported])]);
      setImportSelectedIds([]);
      setImportOpen(false);
      message.success(`已一键导入 ${imported.length} 个账号`);
    } catch (error) {
      message.error(getErrorMessage(error));
    } finally {
      setImportAllLoading(false);
    }
  };

  const removeSelected = () => {
    const sel = new Set(selectedIds);
    setWorkbenchIds((prev) => prev.filter((id) => !sel.has(id)));
    setAccounts((prev) => prev.filter((a) => !sel.has(a.id)));
    setSelectedIds([]);
    message.success(`已移出 ${sel.size} 个账号`);
  };

  // ---- Context menu ----
  const [contextMenu, setContextMenu] = useState<{
    accountIds: number[]; x: number; y: number;
  } | null>(null);

  const handleRowContextMenu = (record: DouyuTaskAccountItem, event: React.MouseEvent) => {
    event.preventDefault();
    const ids = selectedIds.includes(record.id) ? selectedIds : [record.id];
    setContextMenu({ accountIds: ids, x: event.clientX, y: event.clientY });
    if (!selectedIds.includes(record.id)) setSelectedIds([record.id]);
  };

  useEffect(() => {
    if (!contextMenu) return;
    const close = () => setContextMenu(null);
    const closeOnEscape = (event: KeyboardEvent) => {
      if (event.key === 'Escape') close();
    };
    window.addEventListener('click', close);
    window.addEventListener('scroll', close, true);
    window.addEventListener('resize', close);
    window.addEventListener('keydown', closeOnEscape);
    return () => {
      window.removeEventListener('click', close);
      window.removeEventListener('scroll', close, true);
      window.removeEventListener('resize', close);
      window.removeEventListener('keydown', closeOnEscape);
    };
  }, [contextMenu]);

  const contextMenuStyle = contextMenu ? {
    position: 'fixed' as const,
    left: Math.max(8, Math.min(contextMenu.x, window.innerWidth - 220)),
    top: Math.max(8, Math.min(contextMenu.y, window.innerHeight - 390)),
    zIndex: 1050,
  } : { display: 'none' };

  const runContextAction = (taskType: string) => {
    if (!contextMenu) return;
    setContextMenu(null);
    void startTask(taskType, contextMenu.accountIds);
  };

  // ---- 电竞手册绑定面板 ----
  const esportsRelatedTasks = esportsBindTask
    ? tasks
      .filter((task) => (
        task.account_id === esportsBindTask.account_id
        && task.id >= esportsBindTask.id
        && ['prepare_esports_bind', 'get_esports_bind_qr', 'query_esports_game_name', 'confirm_esports_bind']
          .includes(task.task_type)
      ))
      .sort((a, b) => b.id - a.id)
    : [];
  const esportsLatestStateTask = esportsRelatedTasks.find((task) => task.result) || esportsBindTask;
  const esportsLatestRoleTask = esportsRelatedTasks.find(
    (task) => task.task_type === 'query_esports_game_name',
  ) || null;
  const esportsLatestQrTask = esportsRelatedTasks.find(
    (task) => Boolean(resultText(task.result, 'url')),
  ) || null;
  const esportsConfirmRunning = esportsRelatedTasks.some(
    (task) => task.task_type === 'confirm_esports_bind' && ['planned', 'pending', 'running'].includes(task.status),
  );
  const esportsRoleQueryRunning = Boolean(
    esportsLatestRoleTask && ['planned', 'pending', 'running'].includes(esportsLatestRoleTask.status),
  );
  const esportsResult = esportsLatestStateTask?.result || esportsBindTask?.result || null;
  const esportsBound = resultFlag(esportsResult, 'esports_bound') || resultFlag(esportsResult, 'bind_confirmed');
  const esportsRoleName = resultText(esportsResult, 'role_name');
  const esportsAreaName = resultText(esportsResult, 'area_name');
  const esportsPlatName = resultText(esportsResult, 'plat_name');
  const esportsRoleLine = [esportsPlatName, esportsAreaName].filter(Boolean).join(' / ');
  const esportsCanChangeRole = Boolean(esportsBindTask && !esportsConfirmRunning);
  const esportsCanChangeTime = resultNumber(esportsResult, 'can_change_time');
  const esportsRebind = esportsRebindStatus(esportsCanChangeTime);
  const esportsQrUrl = resultText(esportsLatestQrTask?.result, 'url');
  const esportsAccountName = esportsBindTask
    ? (
      esportsBindTask.account_nickname
      || esportsBindTask.account_username
      || esportsBindTask.account_uid
      || `#${esportsBindTask.account_id}`
    )
    : '';
  const esportsStatusText = esportsBound
    ? '电竞手册已绑定'
    : esportsConfirmRunning
      ? '正在完成电竞手册绑定'
      : esportsRoleQueryRunning
        ? '正在查询最新角色'
        : esportsLatestStateTask?.message || '加载电竞手册绑定状态';
  const esportsCanConfirm = Boolean(
    esportsBindTask
    && !esportsBound
    && esportsRoleName
    && !esportsConfirmRunning,
  );

  const queryEsportsRole = () => {
    if (!esportsBindTask || esportsRoleQueryRunning) return;
    void startTask('query_esports_game_name', [esportsBindTask.account_id]);
  };

  const switchEsportsRole = () => {
    if (!esportsBindTask || !esportsCanChangeRole) return;
    void startTask('get_esports_bind_qr', [esportsBindTask.account_id]);
  };

  const confirmEsportsBindFromDialog = () => {
    if (!esportsBindTask || !esportsCanConfirm) {
      message.warning('请先确认当前角色');
      return;
    }
    void startTask('confirm_esports_bind', [esportsBindTask.account_id]);
  };

  // ---- QR panel derived state ----
  const qrResult = qrTask?.result || null;
  const qrUrl = resultText(qrResult, 'url');
  const qrBindPhase = resultText(qrResult, 'bind_phase');
  const qrAutoPolling = qrTask?.status === 'running' && qrResult?.bind_polling === true;
  const isXpdBindTask = qrTask?.task_type === 'get_xpd_bind_qr';
  const qrQueryTask = qrTask
    ? (isXpdBindTask
      ? (latestXpdQueryTaskByAccount.get(qrTask.account_id) || null)
      : (latestQueryGameTaskByAccount.get(qrTask.account_id) || null))
    : null;
  const qrQueryResult = qrTask && qrQueryTask && qrQueryTask.id > qrTask.id ? qrQueryTask.result : null;
  const qrQueryRunning = Boolean(qrQueryTask && qrTask && qrQueryTask.id > qrTask.id && qrQueryTask.status === 'running');
  const qrXpdConfirmTask = isXpdBindTask && qrTask
    ? (latestConfirmXpdBindTaskByAccount.get(qrTask.account_id) || null)
    : null;
  const qrXpdBound = Boolean(qrXpdConfirmTask && qrXpdConfirmTask.status === 'success');
  const qrXpdConfirmRunning = Boolean(qrXpdConfirmTask && ['planned', 'pending', 'running'].includes(qrXpdConfirmTask.status));
  const qrQueryRoleName = resultText(qrQueryResult, 'role_name');
  const qrQueryPendingRoleName = resultText(qrQueryResult, 'pending_role_name');
  const qrQueryIsBoundAct = resultFlag(qrQueryResult, 'is_bound_act') || resultFlag(qrQueryResult, 'bind_confirmed');
  const qrQueryPending = !isXpdBindTask && Boolean(
    qrQueryResult
    && (
      qrQueryPendingRoleName
      || (
        qrQueryRoleName
        && (
          qrQueryResult.bind_ready_for_confirm === true
          || (!qrQueryIsBoundAct)
        )
      )
    ),
  );
  const qrBindReady = (bindReadyForConfirm(qrTask) || qrQueryPending) && !qrXpdBound;
  const qrRoleSourceResult = qrQueryPending
    ? qrQueryResult
    : ((bindReadyForConfirm(qrTask) || qrXpdBound) ? qrResult : null);
  // 待确认角色优先用 pending_* 字段（已绑定角色之外的扫码新角色）
  const qrRoleName = resultText(qrRoleSourceResult, 'pending_role_name') || resultText(qrRoleSourceResult, 'role_name');
  const qrAreaName = resultText(qrRoleSourceResult, 'pending_area_name') || resultText(qrRoleSourceResult, 'area_name');
  const qrPlatName = resultText(qrRoleSourceResult, 'pending_plat_name') || resultText(qrRoleSourceResult, 'plat_name');
  const qrCurrentRoleName = isXpdBindTask
    ? resultText(qrResult, 'before_role_name')
    : (qrQueryRoleName
      || resultText(qrResult, 'current_role_name')
      || (
        resultFlag(qrResult, 'is_bound_act')
          ? resultText(qrResult, 'role_name')
          : ''
      )
      || (qrQueryIsBoundAct ? qrQueryRoleName : ''));
  const qrCurrentAreaName = isXpdBindTask
    ? resultText(qrResult, 'before_area_name')
    : (resultText(qrQueryResult, 'area_name')
      || resultText(qrResult, 'current_area_name')
      || (
        resultFlag(qrResult, 'is_bound_act')
          ? resultText(qrResult, 'area_name')
          : ''
      )
      || (qrQueryIsBoundAct ? resultText(qrQueryResult, 'area_name') : ''));
  const qrCurrentPlatName = isXpdBindTask
    ? resultText(qrResult, 'before_plat_name')
    : (resultText(qrQueryResult, 'plat_name')
      || resultText(qrResult, 'current_plat_name')
      || (
        resultFlag(qrResult, 'is_bound_act')
          ? resultText(qrResult, 'plat_name')
          : ''
      )
      || (qrQueryIsBoundAct ? resultText(qrQueryResult, 'plat_name') : ''));
  const qrRoleLine = qrBindReady
    ? [qrPlatName, qrAreaName, qrRoleName].filter(Boolean).join(' - ')
    : '';
  const qrBindSummary = resultText(qrResult, 'bind_summary') || resultText(qrQueryResult, 'bind_summary');
  const qrStatusText = qrXpdBound
    ? '绑定成功'
    : (isXpdBindTask && qrBindReady)
      ? '已识别角色，待确认'
      : (isXpdBindTask && qrTask?.status === 'failed')
        ? '未检测到绑定'
        : (isXpdBindTask && qrTask?.status === 'running')
          ? '等待扫码绑定'
          : qrXpdConfirmRunning
            ? '正在确认绑定'
            : qrBindReady
              ? '已识别角色，待确认'
              : qrQueryRunning
                ? '查询角色中'
                : qrQueryTask && qrQueryTask.id > (qrTask?.id || 0) && qrQueryTask.status === 'success' && qrQueryIsBoundAct
                  ? '查询结果：仍是当前绑定角色'
                  : bindPhaseText(qrBindPhase, qrAutoPolling);
  const qrAccountName = qrTask
    ? (qrTask.account_nickname || qrTask.account_username || qrTask.account_uid || `#${qrTask.account_id}`)
    : '';

  const confirmQrBind = () => {
    if (!qrTask || !qrBindReady) {
      message.warning('请先扫码识别待确认角色');
      return;
    }
    const accountId = qrTask.account_id;
    const taskId = qrTask.id;
    closeQrTask(taskId);
    void startTask(isXpdBindTask ? 'confirm_xpd_bind' : 'confirm_bind', [accountId]);
  };

  const queryQrRole = () => {
    if (!qrTask) return;
    if (qrAutoPolling) {
      message.info('正在自动识别角色，请稍候');
      return;
    }
    void startTask(isXpdBindTask ? 'query_xpd_bind_info' : 'query_game_name', [qrTask.account_id]);
  };

  // Account table
  const accountColumns: TableProps<DouyuTaskAccountItem>['columns'] = [
    {
      title: '#', width: 45,
      render: (_, __, index) => <Text type="secondary">{index + 1}</Text>,
    },
    {
      title: '账号', dataIndex: 'username', width: 150,
      render: (_, record) => (
        <Space direction="vertical" size={0}>
          <Text strong>{record.nickname || record.username}</Text>
          <Text type="secondary" style={{ fontSize: 12 }}>UID {record.uid || '-'}</Text>
        </Space>
      ),
    },
    {
      title: '游戏名', dataIndex: 'game_name', width: 170,
      render: (_, record) => {
        if (isPeaceHandbook) {
          return record.xpd_game_name
            ? <Text ellipsis title={record.xpd_game_name}>{record.xpd_game_name}</Text>
            : <Text type="secondary">未查</Text>;
        }
        const queryTask = isEsportsHandbook
          ? latestEsportsStateTaskByAccount.get(record.id) || null
          : latestQueryGameTaskByAccount.get(record.id) || null;
        const confirmTask = isEsportsHandbook ? null : latestConfirmBindTaskByAccount.get(record.id) || null;
        const confirmSucceeded = Boolean(
          confirmTask
          && confirmTask.status === 'success'
          && (
            resultFlag(confirmTask.result, 'bind_confirmed')
            || resultFlag(confirmTask.result, 'is_bound_act')
          ),
        );
        const stateTask = (
          confirmSucceeded
          && (!queryTask || confirmTask!.id > queryTask.id)
        ) ? confirmTask : queryTask;
        const confirmFailureTask = latestConfirmBindFailureByAccount.get(record.id);
        const clearPending = Boolean(
          !isEsportsHandbook
          && confirmFailureTask
          && stateTask
          && confirmFailureTask.id > stateTask.id,
        );
        const stateResult = stateTask?.result || null;
        const stateRoleConfirmed = resultFlag(stateResult, 'is_bound_act') || resultFlag(stateResult, 'bind_confirmed');
        const stateRoleName = clearPending && !stateRoleConfirmed ? '' : resultText(stateResult, 'role_name');
        // role_name = 当前已生效绑定角色；pending_role_name = 扫码后待确认的新角色
        const roleName = stateRoleName
          || (isEsportsHandbook ? record.esports_game_name : record.game_name);
        const stateChannel = clearPending && !stateRoleConfirmed
          ? ''
          : [resultText(stateResult, 'area_name'), resultText(stateResult, 'plat_name')]
            .filter(Boolean)
            .join(' / ');
        const pendingRoleName = clearPending || confirmSucceeded ? '' : resultText(stateResult, 'pending_role_name');
        const channel = stateChannel || (isEsportsHandbook ? record.esports_game_channel : record.game_channel);
        const pendingChannel = clearPending || confirmSucceeded ? '' : [resultText(stateResult, 'pending_area_name'), resultText(stateResult, 'pending_plat_name')]
          .filter(Boolean)
          .join(' / ');
        if (!roleName && !pendingRoleName) return <Text type="secondary">未查</Text>;
        return (
          <Space direction="vertical" size={2} style={{ lineHeight: 1.35 }}>
            {roleName ? (
              <Space direction="vertical" size={0}>
                <Text ellipsis title={roleName}>
                  {pendingRoleName ? `当前: ${roleName}` : roleName}
                </Text>
                {channel ? (
                  <Text type="secondary" style={{ fontSize: 12 }} ellipsis title={channel}>
                    {channel}
                  </Text>
                ) : null}
              </Space>
            ) : null}
            {pendingRoleName ? (
              <Space direction="vertical" size={0}>
                <Text type="warning" style={{ fontSize: 12 }} ellipsis title={pendingRoleName}>
                  待确认: {pendingRoleName}
                </Text>
                {pendingChannel ? (
                  <Text type="warning" style={{ fontSize: 12 }} ellipsis title={pendingChannel}>
                    {pendingChannel}
                  </Text>
                ) : null}
              </Space>
            ) : null}
          </Space>
        );
      },
    },
    {
      title: isPeaceHandbook ? '点券' : (isEsportsHandbook ? '电竞积分' : '积分'),
      dataIndex: isPeaceHandbook ? 'xpd_balance' : (isEsportsHandbook ? 'esports_points' : 'points'),
      width: 80,
      render: (v) => v ?? <Text type="secondary">-</Text>,
    },
    // 和平小店无鱼翅/限制兑换/换绑时间概念（可无限制换绑），展示扭蛋碎片
    ...(isPeaceHandbook
      ? [{
        title: '扭蛋碎片', dataIndex: 'xpd_fragments', width: 90,
        render: (v: number | null) => v ?? <Text type="secondary">-</Text>,
      }] as NonNullable<TableProps<DouyuTaskAccountItem>['columns']>
      : [
        {
          title: '鱼翅', dataIndex: 'gold_balance', width: 70,
          render: (v: number | null) => v ?? <Text type="secondary">-</Text>,
        },
        {
          title: '限制兑换', dataIndex: 'exchange_balance', width: 150, ellipsis: true,
          render: (_, record) => {
            const latestLimitedTask = latestLimitedGoodsTaskByAccount.get(record.id);
            const text = limitedGoodsTaskText(latestLimitedTask);
            if (!text) return <Text type="secondary">未查</Text>;
            if (latestLimitedTask?.status !== 'success') {
              return <Text type="secondary">{text}</Text>;
            }
            const count = resultNumber(latestLimitedTask.result, 'limited_count');
            if (count === 0 || text === '无限制商品') return <Tag color="green">无限制商品</Tag>;
            return <Text title={text}>{text}</Text>;
          },
        },
        {
          title: '换绑时间', dataIndex: 'change_role_wait_time', width: 130,
          render: (_, record) => {
            const fromTask = latestChangeWaitByAccount.get(record.id);
            const canChangeTime = fromTask?.canChangeTime ?? (
              isEsportsHandbook ? record.esports_can_change_time : null
            );
            const esportsStatus = isEsportsHandbook ? esportsRebindStatus(canChangeTime) : null;
            if (esportsStatus) {
              return esportsStatus.available
                ? <Tag color="success">可换绑</Tag>
                : (
                  <Space direction="vertical" size={0}>
                    <Tag color="orange">暂不可换绑</Tag>
                    <Text type="secondary" style={{ fontSize: 12 }}>{esportsStatus.text}</Text>
                  </Space>
                );
            }
            const wait = fromTask?.wait ?? (
              isEsportsHandbook ? record.esports_change_role_wait_time : record.change_role_wait_time
            );
            const text = fromTask?.text || formatWaitSeconds(wait);
            if (wait == null && !text) {
              return <Text type="secondary">未查</Text>;
            }
            if (wait != null && wait <= 0) {
              return <Tag color="success">可换绑</Tag>;
            }
            if (wait != null && wait > 0) {
              return (
                <Space direction="vertical" size={0}>
                  <Tag color="orange">冷却中</Tag>
                  <Text type="secondary" style={{ fontSize: 12 }}>{text || formatWaitSeconds(wait)}</Text>
                </Space>
              );
            }
            // 接口没返回数字但有文案
            if (text === '可换绑') return <Tag color="success">可换绑</Tag>;
            return <Text type="secondary">{text || '-'}</Text>;
          },
          sorter: (a, b) => {
            const aw = isEsportsHandbook
              ? latestChangeWaitByAccount.get(a.id)?.canChangeTime ?? a.esports_can_change_time ?? -1
              : latestChangeWaitByAccount.get(a.id)?.wait
              ?? (isEsportsHandbook ? a.esports_change_role_wait_time : a.change_role_wait_time) ?? -1;
            const bw = isEsportsHandbook
              ? latestChangeWaitByAccount.get(b.id)?.canChangeTime ?? b.esports_can_change_time ?? -1
              : latestChangeWaitByAccount.get(b.id)?.wait
              ?? (isEsportsHandbook ? b.esports_change_role_wait_time : b.change_role_wait_time) ?? -1;
            return aw - bw;
          },
        },
      ] as NonNullable<TableProps<DouyuTaskAccountItem>['columns']>),
    {
      title: '最近操作', width: 220,
      render: (_, record) => {
        const latest = latestTaskByAccount.get(record.id);
        if (!latest) return <Text type="secondary">暂无操作</Text>;
        return (
          <Space direction="vertical" size={1} style={{ width: '100%' }}>
            <Space size={4} wrap>
              <Tag color={TASK_STATUS_COLORS[latest.status] || 'default'}>
                {TASK_STATUS_LABELS[latest.status] || latest.status}
              </Tag>
              <Text strong ellipsis style={{ maxWidth: 130 }}>
                {taskTypes[latest.task_type] || latest.task_type}
              </Text>
            </Space>
            {latest.message ? (
              <Text type="secondary" style={{ fontSize: 12 }} ellipsis title={latest.message}>
                {latest.message}
              </Text>
            ) : null}
            <Text type="secondary" style={{ fontSize: 12 }}>
              {formatTime(latest.finished_at || latest.created_at)}
            </Text>
          </Space>
        );
      },
    },
    {
      title: '二维码', width: 76, align: 'center',
      render: (_, record) => {
        const task = latestTaskByAccount.get(record.id);
        const url = taskQrUrl(task);
        if (!url) return null;
        const openTask = () => {
          if (!task) return;
          if (hasBindQrcode(task)) openQrTask(task);
          else if (hasPaymentQrcode(task)) openPayTask(task);
          else if (task.task_type === 'get_xpd_bind_qr') openQrTask(task);
          else openEsportsBindTask(task);
        };
        return (
          <div onClick={openTask} style={{ cursor: 'pointer', display: 'inline-block', lineHeight: 0 }} title="点击查看大图">
            <QRCode value={url} size={56} />
          </div>
        );
      },
    },
    {
      title: '兑换图片', width: 80, align: 'center',
      render: (_, record) => {
        const task = latestExchangeTaskByAccount.get(record.id);
        if (!task) return <Text type="secondary">-</Text>;
        return (
          <ExchangeResultImage
            task={task}
            onClick={(t, url) => setExchangePreview({ task: t, url })}
          />
        );
      },
    },
  ];

  const [configFormValues, setConfigFormValues] = useState<DouyuConfig | null>(null);
  useEffect(() => {
    if (configOpen && config) setConfigFormValues({ ...config });
  }, [configOpen, config]);

  const payUrl = resultText(payTask?.result, 'pay_url');
  const payAccountName = payTask
    ? (payTask.account_nickname || payTask.account_username || payTask.account_uid || `#${payTask.account_id}`)
    : '';
  const selectedText = selectedIds.length > 0 ? `已选 ${selectedIds.length} 个账号` : '请先勾选账号';
  const actionByKey = useMemo(() => new Map(quickActions.map((item) => [item.key, item])), [quickActions]);
  const actionDisabled = (taskType: string) => {
    if (selectedIds.length === 0) return true;
    if (taskType === 'exchange_xpd_goods') {
      return !selectedGoodsId
        || selectedXpdGoodsSoldOut
        || !xpdPaymentOptions.some((option) => option.value === xpdPayType);
    }
    if (['exchange_goods', 'exchange_esports_goods'].includes(taskType)) return !selectedGoodsId;
    // 选中账号中有正在执行的任务时禁用，避免同账号并发不同类型任务（并发写状态字段）
    return selectedHasRunning;
  };
  const renderActionButton = (taskType: string, type: 'default' | 'primary' = 'default') => {
    const item = actionByKey.get(taskType);
    return (
      <Button
        key={taskType}
        size="small"
        block
        type={type}
        icon={item?.icon}
        onClick={() => startTask(taskType)}
        disabled={actionDisabled(taskType)}
        style={{ justifyContent: 'flex-start' }}
      >
        {taskTypes[taskType] || taskType}
      </Button>
    );
  };
  const sectionStyle = {
    border: `1px solid ${token.colorBorderSecondary}`,
    borderRadius: 6,
    padding: 8,
    background: token.colorBgContainer,
  };
  const sectionTitleStyle = {
    display: 'flex',
    alignItems: 'center',
    gap: 6,
    marginBottom: 8,
    fontWeight: 600,
  };
  const actionGridStyle = {
    display: 'grid',
    gridTemplateColumns: '1fr 1fr',
    gap: 6,
  };
  const compactOperationGridStyle = {
    display: 'grid',
    gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
    gap: 8,
  };
  const operationExtra = (
    <Space>
      <Tag color={selectedIds.length ? 'blue' : 'default'}>{selectedText}</Tag>
      <Space.Compact>
        <Button size="small" disabled>并发</Button>
        <InputNumber
          size="small"
          min={1}
          max={10}
          value={concurrency}
          onChange={(v) => setConcurrency(v || 1)}
          style={{ width: 70 }}
        />
      </Space.Compact>
    </Space>
  );
  const operationBody = (
    <>
      {isPeaceHandbook ? (
        <div style={compactOperationGridStyle}>
          <div style={sectionStyle}>
            <div style={sectionTitleStyle}>
              <SearchOutlined />
              <span>小店查询</span>
            </div>
            <div style={actionGridStyle}>
              {renderActionButton('get_xpd_bind_qr', 'primary')}
              {renderActionButton('query_xpd_bind_info', 'primary')}
              {renderActionButton('confirm_xpd_bind', 'primary')}
              {renderActionButton('query_xpd_role', 'primary')}
              {renderActionButton('query_xpd_balance', 'primary')}
              {renderActionButton('query_xpd_fragments', 'primary')}
            </div>
          </div>
          <div style={sectionStyle}>
            <div style={sectionTitleStyle}>
              <ShoppingOutlined />
              <span>小店商品</span>
            </div>
            <Space direction="vertical" style={{ width: '100%' }} size={6}>
              <Select
                size="small"
                value={selectedGoodsId || undefined}
                onChange={setSelectedGoodsId}
                options={goods.map((item) => ({ value: item.commodity_id, label: xpdGoodsLabel(item) }))}
                placeholder="选择小店商品"
                showSearch
                optionFilterProp="label"
                style={{ width: '100%' }}
              />
              <Select
                size="small"
                value={xpdPaymentOptions.some((option) => option.value === xpdPayType) ? xpdPayType : undefined}
                onChange={setXpdPayType}
                options={xpdPaymentOptions}
                placeholder={selectedGoodsId ? '该商品暂无可用兑换方式' : '先选择商品'}
                disabled={!selectedGoodsId || xpdPaymentOptions.length === 0}
                style={{ width: '100%' }}
              />
              <div style={actionGridStyle}>
                {renderActionButton('refresh_xpd_goods')}
                {renderActionButton('exchange_xpd_goods', 'primary')}
              </div>
            </Space>
          </div>
        </div>
      ) : isEsportsHandbook ? (
        <div style={compactOperationGridStyle}>
          <div style={sectionStyle}>
            <div style={sectionTitleStyle}>
              <CreditCardOutlined />
              <span>开通手册</span>
            </div>
            {renderActionButton('create_esports_qr', 'primary')}
          </div>
          <div style={sectionStyle}>
            <div style={sectionTitleStyle}>
              <ShoppingOutlined />
              <span>皮肤兑换</span>
            </div>
            <Space direction="vertical" style={{ width: '100%' }} size={6}>
              <Select
                size="small"
                value={selectedGoodsId || undefined}
                onChange={setSelectedGoodsId}
                options={goods.map((item) => ({ value: item.commodity_id, label: goodsLabel(item) }))}
                placeholder="选择电竞皮肤"
                showSearch
                optionFilterProp="label"
                style={{ width: '100%' }}
              />
              <div style={actionGridStyle}>
                {renderActionButton('refresh_esports_goods')}
                {renderActionButton('exchange_esports_goods', 'primary')}
              </div>
            </Space>
          </div>
          <div style={sectionStyle}>
            <div style={sectionTitleStyle}>
              <CreditCardOutlined />
              <span>充值与送礼</span>
            </div>
            <Space direction="vertical" style={{ width: '100%' }} size={6}>
              <Space.Compact style={{ width: '100%' }}>
                <InputNumber
                  size="small"
                  min={1}
                  value={goldAmount}
                  onChange={(v) => setGoldAmount(v || 1)}
                  style={{ width: '100%' }}
                />
                <Button
                  size="small"
                  icon={<CreditCardOutlined />}
                  onClick={() => startTask('create_gold_qr')}
                  disabled={actionDisabled('create_gold_qr')}
                >
                  充值鱼翅
                </Button>
              </Space.Compact>
              <InputNumber
                size="small"
                min={1}
                value={giftCount}
                onChange={(v) => setGiftCount(v || 1)}
                addonAfter="赠送数量"
                style={{ width: '100%' }}
              />
              <div style={actionGridStyle}>
                {renderActionButton('donate_esports_chicken_gift', 'primary')}
                {renderActionButton('donate_esports_firework_gift', 'primary')}
              </div>
            </Space>
          </div>
        </div>
      ) : (
        <div style={compactOperationGridStyle}>
          <div style={sectionStyle}>
            <div style={sectionTitleStyle}>
              <ShoppingOutlined />
              <span>兑换</span>
            </div>
            <Space direction="vertical" style={{ width: '100%' }} size={6}>
              <Select
                size="small"
                value={selectedGoodsId || undefined}
                onChange={setSelectedGoodsId}
                options={goods.map((item) => ({ value: item.commodity_id, label: goodsLabel(item) }))}
                placeholder="选择兑换商品"
                showSearch
                optionFilterProp="label"
                style={{ width: '100%' }}
              />
              <div style={actionGridStyle}>
                {renderActionButton('refresh_goods')}
                {renderActionButton('exchange_goods', 'primary')}
              </div>
            </Space>
          </div>

          <div style={sectionStyle}>
            <div style={sectionTitleStyle}>
              <CreditCardOutlined />
              <span>开通、充值与送礼</span>
            </div>
            <Space direction="vertical" style={{ width: '100%' }} size={6}>
              {renderActionButton('create_elite_qr', 'primary')}
              <Space.Compact style={{ width: '100%' }}>
                <InputNumber
                  size="small"
                  min={1}
                  value={goldAmount}
                  onChange={(v) => setGoldAmount(v || 1)}
                  style={{ width: '100%' }}
                />
                <Button
                  size="small"
                  icon={<CreditCardOutlined />}
                  onClick={() => startTask('create_gold_qr')}
                  disabled={actionDisabled('create_gold_qr')}
                >
                  充值鱼翅
                </Button>
              </Space.Compact>
              <Space.Compact style={{ width: '100%' }}>
                <InputNumber
                  size="small"
                  min={1}
                  value={giftCount}
                  onChange={(v) => setGiftCount(v || 1)}
                  style={{ width: '100%' }}
                />
                <Button
                  size="small"
                  icon={<GiftOutlined />}
                  onClick={() => startTask('donate_elite_gift')}
                  disabled={actionDisabled('donate_elite_gift')}
                >
                  赠送精英令
                </Button>
              </Space.Compact>
            </Space>
          </div>
        </div>
      )}
      <Text type="secondary" style={{ fontSize: 12 }}>
        勾选账号后可批量执行；账号行右键可执行绑定与查询动作。
      </Text>
    </>
  );
  const accountsBody = (
    <>
      <Space style={{ marginBottom: 8, width: '100%', justifyContent: 'space-between', flexShrink: 0 }} wrap>
        <Space>
          <Input
            size="small" placeholder="搜索账号/昵称/游戏名" prefix={<SearchOutlined />}
            value={accountSearch}
            onChange={(e) => {
              setAccountSearch(e.target.value);
              setAccountPage(1);
            }}
            style={{ width: 200 }} allowClear
          />
        </Space>
        <Space>
          <Button size="small" icon={<ImportOutlined />} onClick={() => setImportOpen(true)}>导入账号</Button>
          {selectedIds.length > 0 && (
            <Button size="small" danger onClick={removeSelected}>移出选中</Button>
          )}
        </Space>
      </Space>
      <div ref={accountTableAreaRef} style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
        <Table
          rowKey="id"
          size="small"
          className="douyu-task-record-table"
          loading={loading}
          locale={{ emptyText: '工作台暂无账号，点击右上角「导入账号」添加' }}
          rowSelection={{
            selectedRowKeys: selectedIds,
            onChange: (keys) => setSelectedIds(keys.map(Number)),
          }}
          columns={accountColumns}
          dataSource={accounts}
          pagination={{
            current: accountPage,
            pageSize: accountPageSize,
            total: accountTotal,
            showSizeChanger: true,
            pageSizeOptions: [10, 20, 50, 100],
            size: 'small',
            showLessItems: true,
            showTotal: (total) => `共 ${total} 条`,
            onChange: (page, size) => {
              setAccountPage(size === accountPageSize ? page : 1);
              if (size !== accountPageSize) {
                setAccountPageSize(size);
                localStorage.setItem('douyu_task_account_page_size', String(size));
              }
            },
          }}
          tableLayout="fixed"
          scroll={{ x: 1180, y: Math.max(120, accountTableAreaHeight - 80) }}
          onRow={(record) => ({
            onContextMenu: (e) => handleRowContextMenu(record, e),
            style: { cursor: 'context-menu' },
            title: '右键打开账号动作',
          })}
        />
      </div>
    </>
  );
  // 任务记录表:表头固定、表体内部滚动、分页器固定显示在底部
  const taskRecordTableStyle = `
    .douyu-task-record-table {
      flex: 1;
      min-height: 0;
      display: flex;
      flex-direction: column;
    }
    .douyu-task-record-table .ant-spin-nested-loading,
    .douyu-task-record-table .ant-spin-container {
      flex: 1;
      min-height: 0;
      display: flex;
      flex-direction: column;
    }
    .douyu-task-record-table .ant-table {
      flex: 1;
      min-height: 0;
      font-size: 12px;
      display: flex;
      flex-direction: column;
    }
    .douyu-task-record-table .ant-table-container {
      flex: 1;
      min-height: 0;
      display: flex;
      flex-direction: column;
    }
    .douyu-task-record-table .ant-table-header {
      flex-shrink: 0;
    }
    .douyu-task-record-table .ant-table-body {
      flex: 1;
      min-height: 0;
      overflow: auto !important;
    }
    .douyu-task-record-table .ant-table-thead > tr > th,
    .douyu-task-record-table .ant-table-tbody > tr > td {
      padding: 4px 8px;
      line-height: 20px;
    }
    .douyu-task-record-table .ant-typography {
      font-size: 12px;
    }
    .douyu-task-record-table .ant-tag {
      font-size: 11px;
      line-height: 18px;
      margin-inline-end: 0;
      padding-inline: 5px;
    }
    .douyu-task-record-table .ant-pagination {
      flex-shrink: 0;
      margin: 0;
      padding: 6px 4px 2px;
      border-top: 1px solid ${token.colorBorderSecondary};
    }
  `;

  return (
    <div style={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
      <style>{taskRecordTableStyle}</style>
      {/* 头部 */}
      <Space style={{ justifyContent: 'space-between', width: '100%', marginBottom: 8, flexShrink: 0 }}>
        <div>
          <h2 style={{ margin: 0 }}>{handbookTitle}</h2>
          <Text type="secondary">{handbookDescription}</Text>
        </div>
        <Space>
          <Tooltip title={layoutMode === 'split' ? '切换为上下布局' : '切换为左右布局'}>
            <Button
              icon={<ColumnWidthOutlined />}
              onClick={() => setLayoutMode((m) => (m === 'split' ? 'stack' : 'split'))}
            />
          </Tooltip>
          <Button icon={<ReloadOutlined />} onClick={loadData} loading={loading}>刷新</Button>
          {canConfig && <Button icon={<SettingOutlined />} onClick={() => setConfigOpen(true)}>配置</Button>}
          {(activeBatchIds.size > 0 || runningBatchIds.size > 0) && <Button danger icon={<StopOutlined />} onClick={stopBatch}>停止</Button>}
        </Space>
      </Space>

      {/* 操作工具栏 + 账号表格（支持上下/左右布局切换） */}
      {layoutMode === 'split' ? (
        <div style={{ flex: 1, minHeight: 0, display: 'flex', gap: 12, overflow: 'hidden' }}>
          <Card
            size="small" title="账号"
            extra={<Tag color="blue">已选 {selectedIds.length}/{accountTotal}</Tag>}
            style={{ flex: 1, minWidth: 0, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
            styles={{ body: { flex: 1, minHeight: 0, padding: '4px 6px', overflow: 'hidden', display: 'flex', flexDirection: 'column' } }}
          >
            {accountsBody}
          </Card>
          <Card
            size="small"
            title={isPeaceHandbook ? '和平小店操作' : (isEsportsHandbook ? '电竞手册操作' : '精英宝典操作')}
            extra={operationExtra}
            style={{ width: 340, flexShrink: 0, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
            styles={{ body: { padding: 8, flex: 1, minHeight: 0, overflowY: 'auto' } }}
          >
            {operationBody}
          </Card>
        </div>
      ) : (
        <>
          <Card
            size="small"
            title={isPeaceHandbook ? '和平小店操作' : (isEsportsHandbook ? '电竞手册操作' : '精英宝典操作')}
            extra={operationExtra}
            style={{ flexShrink: 0, marginBottom: 12 }}
            styles={{ body: { padding: 8 } }}
          >
            {operationBody}
          </Card>
          <Card
            size="small" title="账号"
            extra={<Tag color="blue">已选 {selectedIds.length}/{accountTotal}</Tag>}
            style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
            styles={{ body: { flex: 1, minHeight: 0, padding: '4px 6px', overflow: 'hidden', display: 'flex', flexDirection: 'column' } }}
          >
            {accountsBody}
          </Card>
        </>
      )}

      {/* 右键菜单 */}
      {contextMenu && (
        <div style={contextMenuStyle} onClick={(e) => e.stopPropagation()}>
          <Card
            size="small"
            title={contextMenu.accountIds.length > 1 ? '批量动作' : '账号动作'}
            styles={{ body: { padding: 4 } }}
            style={{ width: 200, boxShadow: '0 4px 12px rgba(0,0,0,0.12)' }}
          >
            <Space direction="vertical" style={{ width: '100%' }}>
              {quickActions.map((item) => (
                <Button
                  key={item.key} block size="small" type="text"
                  icon={item.icon}
                  onClick={() => runContextAction(item.key)}
                  disabled={contextMenu.accountIds.some((id) => runningAccountIds.has(id))}
                  style={{ justifyContent: 'flex-start' }}
                >
                  {taskTypes[item.key] || item.key}
                </Button>
              ))}
            </Space>
          </Card>
        </div>
      )}

      {/* Import modal */}
      <Modal
        title="导入账号到操作台"
        open={importOpen}
        onCancel={() => setImportOpen(false)}
        footer={null}
        width={700}
      >
        <div style={{ marginBottom: 12, display: 'flex', flexWrap: 'wrap', gap: 8 }}>
          <Space wrap>
            <Input
              placeholder="搜索账号/昵称" prefix={<SearchOutlined />}
              value={importSearch}
              onChange={(e) => {
                setImportSearch(e.target.value);
                setImportPage(1);
              }}
              style={{ width: 220 }}
            />
            <Select
              placeholder="按标签筛选" allowClear showSearch
              value={importTag || undefined}
              onChange={(value?: string) => {
                setImportTag(value || '');
                setImportPage(1);
              }}
              style={{ width: 160 }}
              options={importTags.map((t) => ({ value: t, label: t }))}
            />
          </Space>
          <Space wrap style={{ width: '100%', justifyContent: 'flex-start' }}>
            <Button onClick={() => importAccounts(importPool.map((a) => a.id))}>
              导入本页 ({importPool.length})
            </Button>
            <Button loading={importAllLoading} onClick={() => void importAllAccounts()}>
              一键导入全部
            </Button>
            <Button type="primary" onClick={() => importAccounts(importSelectedIds)} disabled={importSelectedIds.length === 0}>
              导入选中 ({importSelectedIds.length})
            </Button>
          </Space>
        </div>
        <Table
          rowKey="id" size="small" loading={importLoading}
          rowSelection={{ selectedRowKeys: importSelectedIds, onChange: (keys) => setImportSelectedIds(keys.map(Number)) }}
          dataSource={importPool}
          columns={[
            { title: '昵称', dataIndex: 'nickname', render: (_, r) => r.nickname || r.username },
            { title: 'UID', dataIndex: 'uid' },
            { title: '标签', dataIndex: 'tag', width: 110, render: (v: string) => (v ? <Tag color="blue">{v}</Tag> : '-') },
            {
              title: '游戏名',
              dataIndex: isPeaceHandbook ? 'xpd_game_name' : (isEsportsHandbook ? 'esports_game_name' : 'game_name'),
              render: (v) => v || '-',
            },
            {
              title: isPeaceHandbook ? '点券' : (isEsportsHandbook ? '电竞积分' : '积分'),
              dataIndex: isPeaceHandbook ? 'xpd_balance' : (isEsportsHandbook ? 'esports_points' : 'points'),
              render: (v) => v ?? '-',
            },
          ]}
          pagination={{
            current: importPage,
            pageSize: importPageSize,
            total: importTotal,
            size: 'small',
            showLessItems: true,
            showTotal: (total) => `共 ${total} 条`,
            onChange: setImportPage,
          }}
          locale={{ emptyText: '没有可导入的账号' }}
        />
      </Modal>

      {/* Config modal */}
      <Modal
        title={isPeaceHandbook ? '和平小店配置' : (isEsportsHandbook ? '电竞手册配置' : '精英宝典配置')}
        open={configOpen}
        onCancel={() => setConfigOpen(false)}
        onOk={() => configFormValues && saveConfig(configFormValues)}
        okText="保存" cancelText="取消"
        width={600}
      >
        {configFormValues && (
          <Space direction="vertical" style={{ width: '100%' }} size={8}>
            {(isPeaceHandbook ? [
              { label: '小店活动代号', key: 'xpd_act_alias' },
              { label: '道聚城活动 ID', key: 'xpd_act_id' },
              { label: '房间 ID', key: 'xpd_rid' },
            ] : isEsportsHandbook ? [
              { label: '电竞手册 manualID', key: 'esports_manual_id' },
              { label: '电竞手册活动', key: 'esports_act_alias' },
              { label: '房间 ID', key: 'room_id' },
              { label: '冠军鸡腿礼物 ID', key: 'esports_chicken_gift_id' },
              { label: '冠军鸡腿皮肤 ID', key: 'esports_chicken_skin_id' },
              { label: '冠军烟花礼物 ID', key: 'esports_firework_gift_id' },
              { label: '冠军烟花皮肤 ID', key: 'esports_firework_skin_id' },
            ] : [
              { label: 'manualID', key: 'manual_id' },
              { label: 'RID', key: 'rid' },
              { label: '绑定二维码活动', key: 'bind_act_alias' },
              { label: '确认绑定活动', key: 'confirm_act_alias' },
              { label: '查询绑定活动(最新)', key: 'legacy_act_alias' },
              { label: '房间 ID', key: 'room_id' },
              { label: '精英令礼物 ID', key: 'gift_id' },
              { label: '皮肤 ID', key: 'skin_id' },
            ]).map(({ label, key }) => (
              <Space key={key} style={{ width: '100%', justifyContent: 'space-between' }}>
                <Text>{label}</Text>
                <Input
                  style={{ width: 300 }}
                  value={String(configFormValues[key as keyof DouyuConfig] ?? '')}
                  onChange={(e) => setConfigFormValues((prev) => prev ? { ...prev, [key]: e.target.value } : prev)}
                />
              </Space>
            ))}
            {(isPeaceHandbook ? [] : isEsportsHandbook ? [
              { label: '电竞手册金额(分)', key: 'esports_amount' },
            ] : [
              { label: '宝典金额(分)', key: 'elite_amount' },
              { label: '鱼翅支付方式', key: 'gold_pay_type' },
            ]).map(({ label, key }) => (
              <Space key={key} style={{ width: '100%', justifyContent: 'space-between' }}>
                <Text>{label}</Text>
                <InputNumber
                  style={{ width: 300 }}
                  value={Number(configFormValues[key as keyof DouyuConfig]) || 0}
                  onChange={(v) => setConfigFormValues((prev) => prev ? { ...prev, [key]: v ?? 0 } : prev)}
                />
              </Space>
            ))}
          </Space>
        )}
      </Modal>

      {/* 电竞手册绑定弹窗：活动绑定与角色选择是独立流程。 */}
      <Modal
        title={
          <Space>
            <QrcodeOutlined />
            <span>绑定电竞手册角色</span>
          </Space>
        }
        open={!!esportsBindTask}
        onCancel={() => {
          setEsportsBindTask(null);
        }}
        footer={null}
        centered
        width={440}
      >
        {esportsBindTask && (
          <Space direction="vertical" style={{ width: '100%' }} size={14}>
            {esportsAccountName ? (
              <Text type="secondary">{esportsAccountName}</Text>
            ) : null}
            <Tag color={esportsBound ? 'green' : esportsConfirmRunning ? 'processing' : 'blue'}>
              {esportsStatusText}
            </Tag>

            {esportsRoleName ? (
              <div>
                <Text type="secondary" style={{ display: 'block', marginBottom: 4 }}>当前游戏角色</Text>
                <Text strong style={{ fontSize: 16 }}>{esportsRoleName}</Text>
                {esportsRoleLine ? (
                  <Text type="secondary" style={{ display: 'block', marginTop: 4 }}>{esportsRoleLine}</Text>
                ) : null}
              </div>
            ) : (
              <Text type="secondary">当前未检测到游戏角色，请先切换角色。</Text>
            )}

            {esportsRebind && esportsRoleName ? (
              <Text type="secondary">
                {esportsRebind.available ? '当前可换绑' : `可换绑时间：${esportsRebind.text}`}
              </Text>
            ) : null}

            {esportsQrUrl ? (
              <div style={{ textAlign: 'center' }}>
                <div ref={esportsQrCanvasWrapRef}>
                  <QRCode value={esportsQrUrl} size={220} bordered={false} />
                </div>
                <Text type="secondary" style={{ display: 'block', marginTop: 8 }}>
                  请在腾讯页面选择角色，完成后查询最新角色。
                </Text>
              </div>
            ) : null}

            <QrActions wrapRef={esportsQrCanvasWrapRef} url={esportsQrUrl} disabled={!esportsQrUrl} />
            <Space wrap>
              <Button
                icon={<SearchOutlined />}
                onClick={queryEsportsRole}
                loading={esportsRoleQueryRunning}
                disabled={esportsConfirmRunning}
              >
                查询最新角色
              </Button>
              <Button
                icon={<QrcodeOutlined />}
                onClick={switchEsportsRole}
                disabled={!esportsCanChangeRole || esportsConfirmRunning}
              >
                切换角色
              </Button>
              <Button
                type="primary"
                icon={<CheckCircleOutlined />}
                onClick={confirmEsportsBindFromDialog}
                disabled={!esportsCanConfirm}
                loading={esportsConfirmRunning}
              >
                完成绑定
              </Button>
            </Space>
          </Space>
        )}
      </Modal>

      {/* 精英宝典绑定二维码弹窗。 */}
      <Modal
        title={
          <Space>
            <QrcodeOutlined />
            <span>
              {qrBindReady ? '已识别新角色' : qrStatusText}
              {qrTaskIds.length > 1 && ` (${qrTaskIds.length})`}
            </span>
          </Space>
        }
        open={qrTaskIds.length > 0}
        onCancel={() => closeAllQrTasks()}
        footer={null}
        centered
        width={420}
      >
        {qrTaskIds.length > 1 && (
          <Space size={4} wrap style={{ marginBottom: 12, justifyContent: 'center' }}>
            {qrTaskIds.map((id) => {
              const t = tasks.find((x) => x.id === id);
              if (!t) return null;
              const name = t.account_nickname || t.account_username || t.account_uid || `#${t.account_id}`;
              return (
                <Tag
                  key={id}
                  color={id === activeQrTaskId ? 'blue' : 'default'}
                  style={{ cursor: 'pointer' }}
                  closable
                  onClose={(e) => { e.preventDefault(); closeQrTask(id); }}
                  onClick={() => setActiveQrTaskId(id)}
                >
                  {name}
                </Tag>
              );
            })}
          </Space>
        )}
        {qrTask && (
          <div style={{ textAlign: 'center' }}>
            {qrAccountName && (
              <Text type="secondary" style={{ marginBottom: 12, display: 'block' }}>{qrAccountName}</Text>
            )}
            <Space direction="vertical" align="center" size={12}>
              <Tag color={qrXpdBound ? 'success' : qrBindReady ? 'gold' : qrBindPhase === 'role_timeout' ? 'orange' : 'processing'}>
                {qrStatusText}
              </Tag>
              {qrUrl ? (
                <div ref={qrCanvasWrapRef}>
                  <QRCode value={qrUrl} size={220} bordered={false} />
                </div>
              ) : qrBindReady ? (
                <Text type="secondary">绑定二维码暂未返回</Text>
              ) : qrTask?.status === 'running' ? (
                <Text type="secondary">二维码生成中...</Text>
              ) : (
                <Text type="secondary">本次未生成二维码</Text>
              )}
              <QrActions wrapRef={qrCanvasWrapRef} url={qrUrl} disabled={!qrUrl} />
              {qrBindReady || qrXpdBound ? (
                <>
                  <Tag color="gold" style={{ fontSize: 16, padding: '6px 16px' }}>
                    {qrRoleName}
                  </Tag>
                  {qrRoleLine && <Text type="secondary">{qrRoleLine}</Text>}
                </>
              ) : qrCurrentRoleName ? (
                <>
                  <Tag color="blue" style={{ fontSize: 14, padding: '4px 12px' }}>
                    当前绑定: {qrCurrentRoleName}
                  </Tag>
                  {(qrCurrentAreaName || qrCurrentPlatName) && (
                    <Text type="secondary">
                      {[qrCurrentPlatName, qrCurrentAreaName].filter(Boolean).join(' - ')}
                    </Text>
                  )}
                </>
              ) : null}
              {qrBindSummary ? (
                <Text type="secondary" style={{ fontSize: 12, wordBreak: 'break-all' }}>
                  {qrBindSummary}
                </Text>
              ) : null}
              {qrQueryTask && qrQueryTask.id > (qrTask?.id || 0) && qrQueryTask.message ? (
                <Text type="secondary" style={{ fontSize: 12 }}>
                  最近查询: {qrQueryTask.message}
                </Text>
              ) : null}
              <Space>
                <Button
                  icon={<SearchOutlined />}
                  onClick={queryQrRole}
                  loading={qrQueryRunning}
                  disabled={qrAutoPolling}
                >
                  {qrAutoPolling ? '自动识别中' : '查询角色'}
                </Button>
                <Button
                  type="primary"
                  icon={<CheckCircleOutlined />}
                  onClick={confirmQrBind}
                  disabled={!qrBindReady}
                >
                  确认绑定
                </Button>
              </Space>
            </Space>
          </div>
        )}
      </Modal>

      {/* Pay QR Modal */}
      <Modal
        title={
          <Space>
            <CreditCardOutlined />
            <span>支付二维码</span>
          </Space>
        }
        open={!!payTask}
        onCancel={() => setPayTask(null)}
        footer={null}
        centered
        width={400}
      >
        {payTask && (
          <div style={{ textAlign: 'center' }}>
            {payAccountName && (
              <Text type="secondary" style={{ marginBottom: 12, display: 'block' }}>{payAccountName}</Text>
            )}
            <Space direction="vertical" align="center" size={12}>
              {payUrl ? (
                <div ref={payQrCanvasWrapRef}>
                  <QRCode value={payUrl} size={220} bordered={false} />
                </div>
              ) : (
                <Text type="secondary">支付码生成中...</Text>
              )}
              <QrActions wrapRef={payQrCanvasWrapRef} url={payUrl} disabled={!payUrl} />
              <Text>{payTask.message || '请扫码支付'}</Text>
            </Space>
          </div>
        )}
      </Modal>

      {/* 兑换结果图片预览 Modal */}
      <Modal
        title={
          <Space>
            <CopyOutlined />
            <span>兑换结果图片</span>
          </Space>
        }
        open={!!exchangePreview}
        onCancel={() => setExchangePreview(null)}
        footer={
          <Space>
            <Button onClick={() => setExchangePreview(null)}>关闭</Button>
            <Button type="primary" icon={<CopyOutlined />} onClick={copyExchangePreview}>复制图片</Button>
          </Space>
        }
        centered
        width={340}
      >
        {exchangePreview && (
          <Space direction="vertical" style={{ width: '100%' }} align="center" size={8}>
            <img
              src={exchangePreview.url}
              alt="兑换结果"
              style={{ width: '100%', borderRadius: 8, border: `1px solid ${token.colorBorderSecondary}` }}
            />
            <Text type="secondary" style={{ fontSize: 12 }}>
              {exchangePreview.task.account_nickname
                || exchangePreview.task.account_username
                || `#${exchangePreview.task.account_id}`}
            </Text>
          </Space>
        )}
      </Modal>

      {/* 确认绑定失败提示 Modal（居中，自动消失） */}
      <Modal
        title={<Space><ExclamationCircleOutlined style={{ color: '#faad14' }} /><span>绑定失败提示</span></Space>}
        open={Boolean(confirmFailTip)}
        onCancel={() => setConfirmFailTip(null)}
        footer={null}
        centered
        closable={false}
        width={420}
      >
        <div style={{ textAlign: 'center', padding: '16px 8px 8px' }}>
          <ExclamationCircleOutlined style={{ fontSize: 42, color: '#faad14' }} />
          <div style={{ fontSize: 14, marginTop: 14, lineHeight: 1.7 }}>
            {confirmFailTip?.message || ''}
          </div>
          <Text type="secondary" style={{ fontSize: 12, marginTop: 12, display: 'inline-block' }}>
            {confirmFailCountdown} 秒后自动关闭
          </Text>
        </div>
      </Modal>
    </div>
  );
}
