// 1 · 收集爆款 — 真实视频列表（筛选 + 分页 + 选中 → 发起拆解）

// 多模态 LLM provider 白名单（与 multimodal_client.py 优先级对齐）
const _MULTIMODAL_PROVIDERS = ["gemini", "openai_multimodal"];

// 行业候选清单（与后端 app/core/catalog.py:INDUSTRY_OPTIONS 对齐）
const INDUSTRY_OPTIONS = [
  { key: "",                 label: "全部行业" },
  { key: "real_estate",      label: "房地产" },
  { key: "mother_baby",      label: "母婴" },
  { key: "travel",           label: "文旅" },
  { key: "hotel",            label: "酒店" },
  { key: "insurance",        label: "保险" },
  { key: "finance",          label: "理财" },
  { key: "automotive",       label: "汽车" },
  { key: "education",        label: "教育" },
  { key: "home_improvement", label: "家装" },
  { key: "general",          label: "通用" },
];

// 数据排序字段：5 个数值 + 1 个入库时间
const SORT_FIELDS = [
  { key: "digg",      label: "点赞" },
  { key: "comment",   label: "评论" },
  { key: "share",     label: "分享" },
  { key: "recommend", label: "推荐 / 播放" },
  { key: "collect",   label: "收藏" },
];
const SORT_ACCESSOR = {
  received:  (v) => v.receivedAt ? Date.parse(v.receivedAt) : 0,
  digg:      (v) => v.digg || 0,
  comment:   (v) => v.comment || 0,
  share:     (v) => v.share || 0,
  recommend: (v) => v.recommend || 0,
  collect:   (v) => v.collect || 0,
};

// 由 jobs 列表反推一条 inbox 视频的拆解状态（与 teardown.jsx 同步保持一致）
//   - 路径 1：inbox.job_id 精确反查
//   - 路径 2：title 兜底匹配最新一条
function _deriveStatus(v, jobs) {
  const map = (s) => {
    if (s === "completed") return "done";
    if (s === "failed") return "failed";
    if (s === "running" || s === "pending") return "in_progress";
    return null;
  };
  if (!v) return "untouched";
  if (v.job_id) {
    const j = jobs.find(x => x.job_id === v.job_id);
    return j ? (map(j.status) || "untouched") : "untouched";
  }
  if (v.title && jobs.length) {
    const sameTitle = jobs.filter(j => j.title && j.title === v.title);
    if (sameTitle.length) {
      sameTitle.sort((a, b) => (b.created_at || "").localeCompare(a.created_at || ""));
      return map(sameTitle[0].status) || "untouched";
    }
  }
  return "untouched";
}

const CollectPage = () => {
  const [pool, setPool] = React.useState([]);
  const [meta, setMeta] = React.useState({ count: 0, pull: null, pullHistory: [] });
  const [loading, setLoading] = React.useState(true);
  const [loadError, setLoadError] = React.useState("");
  const [pulling, setPulling] = React.useState(false);
  const [newIds, setNewIds] = React.useState(new Set());
  const [page, setPage] = React.useState(1);
  const knownIdsRef = React.useRef(new Set());
  const firstLoadRef = React.useRef(true);

  // ─── 拆解相关 state ───
  const [jobs, setJobs] = React.useState([]);
  const [templates, setTemplates] = React.useState([]);
  const [credentials, setCredentials] = React.useState([]);
  const [rulePacks, setRulePacks] = React.useState([]);
  // 模板多选：Set<id>。空集合 = 后端按默认模板单 job
  const [selTplIds, setSelTplIds] = React.useState(() => new Set());
  const [selCredId, setSelCredId] = React.useState(null);
  // 规则模板（pack_id 字符串）；空 = 无规则限制。localStorage 记忆上次选择
  const [selRulePackId, setSelRulePackId] = React.useState(() => {
    try { return localStorage.getItem("collect.ruleId") || ""; } catch { return ""; }
  });
  const [selRowIds, setSelRowIds] = React.useState(() => new Set());
  const [listTab, setListTab] = React.useState("pending"); // pending | done
  const [starting, setStarting] = React.useState(false);
  const [batchProgress, setBatchProgress] = React.useState({ done: 0, total: 0 });
  const [recentlyStarted, setRecentlyStarted] = React.useState([]);

  // ─── 大类切换：视频 / 图文 ───
  const [viewKind, setViewKind] = React.useState("video"); // video | image

  // ─── 筛选器 state ───
  const [filterCity, setFilterCity] = React.useState("");
  const [filterMinDigg, setFilterMinDigg] = React.useState("");
  const [filterMinComment, setFilterMinComment] = React.useState("");
  const [filterMinCollect, setFilterMinCollect] = React.useState("");
  const [filterIndustry, setFilterIndustry] = React.useState(""); // "" = 全部行业
  const [filterReceived, setFilterReceived] = React.useState("all"); // all | today | 24h | 7d | 30d

  // ─── 排序 state ───
  // sortKey: received | digg | comment | share | recommend | collect
  const [sortKey, setSortKey] = React.useState("received");
  const [sortOrder, setSortOrder] = React.useState("desc"); // desc | asc

  const TODAY = new Date().toISOString().slice(0, 10);
  const PAGE_SIZE = 10;

  const formatWan = (n) => {
    const v = Number(n || 0);
    if (v >= 10000) return `${(v / 10000).toFixed(v >= 100000 ? 0 : 1)}w`;
    return String(v);
  };

  const toBJ = (s) => {
    if (!s) return null;
    const d = new Date(s);
    if (isNaN(d.getTime())) return null;
    return d;
  };

  const fmtDtBJ = (s) => {
    const d = toBJ(s);
    if (!d) return "—";
    const M = String(d.getMonth() + 1).padStart(2, "0");
    const D = String(d.getDate()).padStart(2, "0");
    const h = String(d.getHours()).padStart(2, "0");
    const m = String(d.getMinutes()).padStart(2, "0");
    return `${M}-${D} ${h}:${m}`;
  };

  const fmtDtSecBJ = (s) => {
    const d = toBJ(s);
    if (!d) return "—";
    const M = String(d.getMonth() + 1).padStart(2, "0");
    const D = String(d.getDate()).padStart(2, "0");
    const h = String(d.getHours()).padStart(2, "0");
    const m = String(d.getMinutes()).padStart(2, "0");
    const sec = String(d.getSeconds()).padStart(2, "0");
    return `${M}-${D} ${h}:${m}:${sec}`;
  };

  const normalizeItem = (item) => {
    const stats = item.stats || {};
    return {
      raw: item,
      id: item.id,
      t: item.title || item.external_id || "未命名视频",
      a: item.author ? `@${item.author}` : "@未知作者",
      p: item.platform || "douyin",
      v: formatWan(stats.diggCount || stats.collectCount || stats.shareCount || 0),
      url: item.video_url,
      isRealEstate: !!item.is_real_estate || item.industry === "real_estate",
      city: item.estate_city || "",
      estateType: item.estate_type || "",
      contentType: item.content_type || "video", // "video" | "image_post"
      externalId: item.external_id || item.aweme_id || "",
      digg: Number(stats.diggCount || 0),
      comment: Number(stats.commentCount || 0),
      collect: Number(stats.collectCount || 0),
      share: Number(stats.shareCount || 0),
      recommend: Number(stats.recommendCount || 0),
      capturedAt: item.captured_at,
      receivedAt: item.received_at,
    };
  };

  const loadInbox = async ({ silent = false } = {}) => {
    if (!silent) setLoading(true);
    setLoadError("");
    try {
      const data = await API.inbox.list({ status: "all", limit: 5000 });
      const items = (data.items || []).map(normalizeItem);
      const ids = new Set(items.map((item) => item.id));
      if (!firstLoadRef.current) {
        const incoming = items
          .map((item) => item.id)
          .filter((id) => id && !knownIdsRef.current.has(id));
        if (incoming.length) {
          setNewIds(new Set(incoming));
          window.setTimeout(() => {
            setNewIds((prev) => {
              const next = new Set(prev);
              incoming.forEach((id) => next.delete(id));
              return next;
            });
          }, 8000);
        }
      }
      firstLoadRef.current = false;
      knownIdsRef.current = ids;
      setPool(items);
      setMeta({
        count: data.total || data.count || items.length,
        pull: data.pull || null,
        pullHistory: data.pull_history || [],
      });
    } catch (err) {
      setLoadError(String(err.message || err));
    } finally {
      if (!silent) setLoading(false);
    }
  };

  const pullOnce = async () => {
    setPulling(true);
    setLoadError("");
    try {
      await API.inbox.pull({ page_size: 10 });
      await loadInbox({ silent: true });
    } catch (err) {
      setLoadError(String(err.message || err));
    } finally {
      setPulling(false);
    }
  };

  const loadJobs = async () => {
    try {
      const d = await API.jobs.list();
      setJobs(d.items || []);
    } catch (_) {}
  };

  const loadTemplates = async () => {
    try {
      // 拆解模板含 teardown（视频）+ image_teardown（图文）两个 category；都列给用户选
      // 后端会按视频/图文实际形态匹配合适的模板；用户也能跨类目选
      const [a, b] = await Promise.all([
        API.templates.list({ category: "teardown" }).catch(() => ({ items: [] })),
        API.templates.list({ category: "image_teardown" }).catch(() => ({ items: [] })),
      ]);
      const items = [...(a.items || []), ...(b.items || [])];
      setTemplates(items);
      // 首次加载：把默认模板预选上；用户清空后不再回填
      const def = items.find(t => t.is_default);
      if (def) {
        setSelTplIds(prev => prev.size === 0 ? new Set([def.id]) : prev);
      }
    } catch (_) {}
  };

  const loadRulePacks = async () => {
    try {
      const d = await API.rulePacks.list();
      setRulePacks((d.items || []).filter(r => r.enabled !== false));
    } catch (_) {}
  };

  const loadCredentials = async () => {
    try {
      const results = await Promise.all(_MULTIMODAL_PROVIDERS.map(
        p => API.credentials.list(p).catch(() => ({ items: [] }))
      ));
      const all = results.flatMap(r => r.items || []);
      setCredentials(all);
      const def = all.find(c => c.provider === "gemini" && c.is_active)
               || all.find(c => c.is_active)
               || all[0];
      if (def) setSelCredId(prev => prev ?? def.id);
    } catch (_) {}
  };

  React.useEffect(() => {
    loadInbox();
    loadJobs();
    loadTemplates();
    loadCredentials();
    loadRulePacks();
    const inboxTimer = window.setInterval(() => loadInbox({ silent: true }), 5000);
    const jobsTimer = window.setInterval(loadJobs, 5000);
    return () => {
      window.clearInterval(inboxTimer);
      window.clearInterval(jobsTimer);
    };
  }, []);

  const [countdown, setCountdown] = React.useState("");
  const lastPullAt = meta.pull?.last_pull_at;
  const intervalSec = meta.pull?.interval_sec || 300;

  React.useEffect(() => {
    if (!lastPullAt) return;
    const tick = () => {
      const last = new Date(lastPullAt).getTime();
      const next = last + intervalSec * 1000;
      const diff = Math.max(0, Math.floor((next - Date.now()) / 1000));
      if (diff <= 0) {
        setCountdown("即将拉取…");
      } else {
        const m = Math.floor(diff / 60);
        const s = diff % 60;
        setCountdown(`${m}分${s.toString().padStart(2, "0")}秒`);
      }
    };
    tick();
    const t = window.setInterval(tick, 1000);
    return () => window.clearInterval(t);
  }, [lastPullAt, intervalSec]);

  const realEstateCount = pool.filter((v) => v.isRealEstate).length;
  const nonRealEstateCount = pool.length - realEstateCount;
  const lastResult = meta.pull?.last_result || {};
  const pullHistory = meta.pullHistory || [];

  // ─── 城市选项（从已加载的 pool 动态去重）───
  const cityOptions = React.useMemo(() => {
    const set = new Set();
    for (const v of pool) if (v.city) set.add(v.city);
    return Array.from(set).sort();
  }, [pool]);

  // ─── 大类拆分：视频在前、图文在后；统计两类总量 ───
  // 大类切换在 filtered 之外做：让两个 tab 的计数恒等于"全部 pool 中该类的数量"，不被其它筛选条件搅动
  const kindCounts = React.useMemo(() => {
    let video = 0, image = 0;
    for (const v of pool) {
      if (v.contentType === "image_post") image++;
      else video++;
    }
    return { video, image };
  }, [pool]);

  // ─── 主筛选管道（AND）───
  const filtered = React.useMemo(() => {
    const minDigg = parseInt(filterMinDigg, 10) || 0;
    const minComment = parseInt(filterMinComment, 10) || 0;
    const minCollect = parseInt(filterMinCollect, 10) || 0;
    let receivedMin = 0;
    if (filterReceived !== "all") {
      const now = Date.now();
      if (filterReceived === "today") {
        const d = new Date(); d.setHours(0, 0, 0, 0);
        receivedMin = d.getTime();
      } else if (filterReceived === "24h") receivedMin = now - 24 * 3600 * 1000;
      else if (filterReceived === "7d")  receivedMin = now - 7  * 24 * 3600 * 1000;
      else if (filterReceived === "30d") receivedMin = now - 30 * 24 * 3600 * 1000;
    }
    const arr = pool.filter(v => {
      // 大类
      if (viewKind === "image" && v.contentType !== "image_post") return false;
      if (viewKind === "video" && v.contentType === "image_post") return false;
      // 行业（房产兼容：选房产时也接受 is_real_estate=true 的老数据）
      if (filterIndustry) {
        const matchIndustry = v.raw?.industry === filterIndustry
          || (filterIndustry === "real_estate" && v.isRealEstate);
        if (!matchIndustry) return false;
      }
      if (filterCity && v.city !== filterCity) return false;
      if (minDigg && (v.digg || 0) < minDigg) return false;
      if (minComment && (v.comment || 0) < minComment) return false;
      if (minCollect && (v.collect || 0) < minCollect) return false;
      if (receivedMin) {
        const t = v.receivedAt ? new Date(v.receivedAt).getTime() : 0;
        if (!t || t < receivedMin) return false;
      }
      return true;
    });
    const get = SORT_ACCESSOR[sortKey] || SORT_ACCESSOR.received;
    arr.sort((a, b) => sortOrder === "asc" ? get(a) - get(b) : get(b) - get(a));
    return arr;
  }, [pool, viewKind, filterIndustry, filterCity, filterMinDigg, filterMinComment, filterMinCollect, filterReceived, sortKey, sortOrder]);

  // ─── 按拆解状态分组（在 filtered 基础上）───
  const groups = React.useMemo(() => {
    const done = [], pending = [];
    for (const v of filtered) {
      const status = _deriveStatus(v.raw, jobs);
      (status === "done" ? done : pending).push({ ...v, _status: status });
    }
    return { done, pending };
  }, [filtered, jobs]);

  const visible = listTab === "done" ? groups.done : groups.pending;
  const totalPages = Math.max(1, Math.ceil(visible.length / PAGE_SIZE));
  const pagedItems = visible.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);

  // 筛选 / tab 变化时把 page 拉回 1
  React.useEffect(() => {
    setPage(1);
  }, [viewKind, filterIndustry, filterCity, filterMinDigg, filterMinComment, filterMinCollect, filterReceived, listTab]);

  React.useEffect(() => {
    if (page > totalPages) setPage(totalPages);
  }, [visible.length, totalPages]);

  const hasFilter = !!filterIndustry || !!filterCity || !!filterMinDigg || !!filterMinComment || !!filterMinCollect || filterReceived !== "all";

  // ─── 多选行 + 批量发起拆解 ───
  // selRowIds 是 Set<id>。从 pool 里把对应的视频 + 状态拎出来给「发起拆解」卡渲染。
  const selVids = React.useMemo(() => {
    if (!selRowIds.size) return [];
    return pool.filter(v => selRowIds.has(v.id))
               .map(v => ({ ...v, _status: _deriveStatus(v.raw, jobs) }));
  }, [selRowIds, pool, jobs]);

  const toggleRow = (id) => {
    setSelRowIds(prev => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id); else next.add(id);
      return next;
    });
  };

  // 当前页 vs 全部已选 — 用于头部 checkbox 的三态（none/some/all）
  const pagedAllSelected = pagedItems.length > 0 && pagedItems.every(v => selRowIds.has(v.id));
  const pagedSomeSelected = pagedItems.some(v => selRowIds.has(v.id));

  const togglePageAll = () => {
    setSelRowIds(prev => {
      const next = new Set(prev);
      if (pagedAllSelected) {
        for (const v of pagedItems) next.delete(v.id);
      } else {
        for (const v of pagedItems) next.add(v.id);
      }
      return next;
    });
  };

  // 可发起拆解的：有 url、不在拆解中
  const launchable = selVids.filter(v => v.url && v._status !== "in_progress");
  const skippedInProgress = selVids.filter(v => v._status === "in_progress").length;
  const skippedNoUrl = selVids.filter(v => !v.url).length;

  const startTeardown = async () => {
    if (!launchable.length) return;
    setStarting(true);
    const tplIds = Array.from(selTplIds);
    // 每条视频会 spawn N 个 job（N = 已选模板数，0 时后端按默认 = 1）
    const jobsPerVid = Math.max(1, tplIds.length);
    setBatchProgress({ done: 0, total: launchable.length * jobsPerVid });
    const successes = [];
    const failures = [];
    let doneJobs = 0;
    // 串行避免一次打爆后端 + 让"最近发起"按顺序追加
    for (let i = 0; i < launchable.length; i++) {
      const v = launchable[i];
      try {
        const res = await API.analyze.link({
          url: v.url,
          title: v.t || "",
          industry_key: v.raw?.industry || "",
          template_ids: tplIds,
          // 兼容老接口：单选时把首个 id 也填进去
          template_id: tplIds.length === 1 ? tplIds[0] : null,
          multimodal_credential_id: selCredId,
          inbox_id: v.id,
          // 规则模板：空则后端按"无规则限制"处理
          rule_pack_id: selRulePackId || null,
        });
        // 后端返回 { job_ids: [...], job_id: <第一个>, count: N }
        const jobIds = Array.isArray(res.job_ids) && res.job_ids.length ? res.job_ids : [res.job_id];
        for (const jid of jobIds) {
          successes.push({ jobId: jid, vidId: v.id, title: v.t, at: Date.now() });
        }
        doneJobs += jobIds.length;
      } catch (e) {
        failures.push({ vidId: v.id, title: v.t, error: String(e.message || e) });
        doneJobs += jobsPerVid;
      }
      setBatchProgress({ done: doneJobs, total: launchable.length * jobsPerVid });
    }
    if (successes.length) {
      setRecentlyStarted(prev => {
        const successVids = new Set(successes.map(x => x.vidId));
        const merged = [...successes, ...prev.filter(x => !successVids.has(x.vidId))];
        return merged.slice(0, 20);
      });
    }
    setStarting(false);
    setBatchProgress({ done: 0, total: 0 });
    loadJobs();
    loadInbox({ silent: true });
    // 成功的清出选择（让用户能重新挑下一批）；失败的留着方便重试
    if (successes.length) {
      setSelRowIds(prev => {
        const next = new Set(prev);
        for (const s of successes) next.delete(s.vidId);
        return next;
      });
    }
    if (failures.length) {
      alert(`部分发起失败（${failures.length}/${launchable.length}）：\n` +
            failures.slice(0, 3).map(f => `- ${f.title}: ${f.error}`).join("\n") +
            (failures.length > 3 ? `\n…还有 ${failures.length - 3} 条` : ""));
    }
  };

  const teardownBtn = (() => {
    if (!launchable.length) {
      if (skippedInProgress) return { text: "选中的均在拆解中", primary: false, disabled: true };
      return { text: "开始拆解", primary: true, disabled: true };
    }
    if (starting) {
      return {
        text: `发起中 ${batchProgress.done}/${batchProgress.total}…`,
        primary: false, disabled: true,
      };
    }
    const tplCount = Math.max(1, selTplIds.size);
    const totalJobs = launchable.length * tplCount;
    const suffix = tplCount > 1 ? `（${launchable.length} 视频 × ${tplCount} 模板 = ${totalJobs} 任务）` : "";
    if (launchable.length === 1) {
      const v = launchable[0];
      if (v._status === "done")   return { text: `重新拆解${suffix}`, primary: false, disabled: false };
      if (v._status === "failed") return { text: `重新拆解${suffix}`, primary: false, disabled: false };
      return { text: `开始拆解${suffix}`, primary: true, disabled: false };
    }
    return {
      text: tplCount > 1 ? `批量拆解 ${suffix}` : `批量拆解 ${launchable.length} 条`,
      primary: true, disabled: false,
    };
  })();

  const TH = ({ children, style = {} }) => (
    <th
      style={{
        padding: "8px 10px",
        textAlign: "left",
        fontWeight: 600,
        color: "var(--fg-3)",
        fontSize: 10,
        textTransform: "uppercase",
        letterSpacing: "0.04em",
        whiteSpace: "nowrap",
        background: "var(--bg-1)",
        position: "sticky",
        top: 0,
        zIndex: 1,
        ...style,
      }}
    >
      {children}
    </th>
  );

  const TD = ({ children, style = {} }) => (
    <td
      style={{
        padding: "7px 10px",
        fontSize: 11.5,
        color: "var(--fg-1)",
        borderTop: "1px solid var(--line-soft)",
        whiteSpace: "nowrap",
        ...style,
      }}
    >
      {children}
    </td>
  );

  return (
    <>
      <Topbar
        crumbs={["1 · 收集爆款", "视频列表"]}
        updated={lastResult.next_page ? `下页 ${lastResult.next_page}` : "等待拉取"}
        right={
          <>
            <span className="pill live">
              {loading ? "加载中" : `${pool.length} 条入库${newIds.size ? ` · 新增 ${newIds.size}` : ""}`}
            </span>
            <button
              className="btn ghost"
              onClick={() => {
                loadInbox();
              }}
            >
              <Icon name="refresh" size={11} />
              刷新
            </button>
            <button className="btn primary" disabled={pulling} onClick={pullOnce}>
              <Icon name="plus" size={12} />
              {pulling ? "拉取中" : "拉 10 条"}
            </button>
          </>
        }
      />

      {/* 顶部 KPI */}
      <div className="row" style={{ padding: "14px 20px", background: "var(--bg-1)", borderBottom: "1px solid var(--line-soft)", gap: 0 }}>
        {[
          { n: "01", l: "本地入库", v: pool.length.toLocaleString(), hi: true },
          { n: "02", l: "房产视频", v: realEstateCount.toLocaleString() },
          { n: "03", l: "非房产视频", v: nonRealEstateCount.toLocaleString() },
          { n: "04", l: "单次拉取", v: (lastResult.page_size || 10).toLocaleString() + " 条" },
          { n: "05", l: "本次去重", v: (lastResult.skipped || 0).toLocaleString() },
        ].map((s, i) => (
          <div key={i} className="col gap-3" style={{ flex: 1, padding: "2px 14px", borderLeft: i === 0 ? "none" : "1px solid var(--line-1)" }}>
            <span className="mono" style={{ fontSize: 10, color: "var(--fg-4)" }}>{s.n}</span>
            <span style={{ fontSize: 11, color: "var(--fg-2)" }}>{s.l}</span>
            <span className="mono" style={{ fontSize: 20, fontWeight: 600, color: s.hi ? "var(--cyan)" : "var(--fg-0)", letterSpacing: "-.02em" }}>{s.v}</span>
          </div>
        ))}
      </div>

      {/* 大类切换：视频爆款 / 图文爆款（视频在前） */}
      <div className="row" style={{
        padding: "10px 20px",
        background: "var(--bg-0)",
        borderBottom: "1px solid var(--line-soft)",
        gap: 8,
      }}>
        {[
          { k: "video", icon: "play",   label: "视频爆款", count: kindCounts.video },
          { k: "image", icon: "layers", label: "图文爆款", count: kindCounts.image },
        ].map(t => {
          const on = viewKind === t.k;
          return (
            <button key={t.k}
              type="button"
              onClick={() => setViewKind(t.k)}
              style={{
                display: "flex", alignItems: "center", gap: 6,
                padding: "6px 14px",
                border: `1px solid ${on ? "var(--cyan)" : "var(--line-1)"}`,
                borderRadius: 6,
                background: on ? "color-mix(in oklch, var(--cyan) 10%, var(--bg-1))" : "var(--bg-1)",
                color: on ? "var(--cyan)" : "var(--fg-1)",
                fontWeight: on ? 600 : 500,
                fontSize: 12.5,
                cursor: "pointer",
                fontFamily: "inherit",
              }}>
              <Icon name={t.icon} size={13}/>
              <span>{t.label}</span>
              <span className="mono" style={{ fontSize: 10.5, opacity: 0.75 }}>{t.count}</span>
            </button>
          );
        })}
      </div>

      <div className="main-body">
        {/* 左：视频表格 */}
        <div className="col scroll grow" style={{ minWidth: 0, background: "var(--bg-0)" }}>
          {loadError && (
            <div
              style={{
                margin: "12px 16px 0",
                padding: "10px 12px",
                background: "var(--bg-1)",
                border: "1px solid var(--pink)",
                borderRadius: 5,
                fontSize: 11,
                color: "var(--pink)",
              }}
            >
              {loadError}
            </div>
          )}

          <div style={{ padding: "12px 16px", minHeight: 0 }}>
            {/* 第一行：tab + 命中数 */}
            <div className="row between" style={{
              gap: 18, padding: "0 4px",
              borderBottom: "1px solid var(--line-soft)",
              marginBottom: 10,
            }}>
              <div className="row" style={{ gap: 18 }}>
                {[
                  { id: "pending", label: "未拆解", count: groups.pending.length },
                  { id: "done", label: "已拆解", count: groups.done.length },
                ].map(t => {
                  const active = listTab === t.id;
                  return (
                    <button
                      key={t.id}
                      type="button"
                      onClick={() => setListTab(t.id)}
                      style={{
                        display: "flex", alignItems: "center", gap: 6,
                        padding: "8px 4px", border: "none", background: "transparent",
                        fontFamily: "inherit", cursor: "pointer",
                        color: active ? "var(--fg-0)" : "var(--fg-3)",
                        fontWeight: active ? 600 : 500,
                        fontSize: 12,
                        borderBottom: `2px solid ${active ? "var(--cyan)" : "transparent"}`,
                        marginBottom: -1,
                      }}>
                      {active && <span style={{
                        width: 6, height: 6, borderRadius: "50%",
                        background: "var(--cyan)", display: "inline-block",
                      }} />}
                      <span>{t.label}</span>
                      <span className="mono" style={{
                        fontSize: 10,
                        color: active ? "var(--fg-2)" : "var(--fg-3)",
                      }}>{t.count}</span>
                    </button>
                  );
                })}
              </div>
              <span className="mono" style={{ fontSize: 11, color: "var(--fg-3)" }}>
                {(() => {
                  if (loading && pool.length === 0) return "加载中…";
                  // 当前大类的总量（视频 / 图文 各自的池子）
                  const kindTotal = viewKind === "image" ? kindCounts.image : kindCounts.video;
                  if (hasFilter) return `${filtered.length} / ${kindTotal} 条（已筛选）`;
                  return `共 ${kindTotal} 条`;
                })()}
                {newIds.size > 0 && (
                  <span className="tag orange" style={{ marginLeft: 6 }}>+{newIds.size} 新</span>
                )}
              </span>
            </div>

            {/* 第二行：数据排序面板（5 字段 + 入库时间，每个支持升降） */}
            <div className="row" style={{
              gap: 8, padding: "0 4px 8px",
              alignItems: "center", flexWrap: "wrap",
            }}>
              <span style={{ fontSize: 11, color: "var(--fg-3)" }}>数据排序</span>
              {SORT_FIELDS.map(f => {
                const on = sortKey === f.key;
                return (
                  <button key={f.key}
                    type="button"
                    onClick={() => {
                      if (on) setSortOrder(o => o === "desc" ? "asc" : "desc");
                      else setSortKey(f.key);
                    }}
                    title={on ? `点击切换升/降序（当前 ${sortOrder === "desc" ? "降序" : "升序"}）` : `按${f.label}排序`}
                    style={{
                      display: "flex", alignItems: "center", gap: 4,
                      padding: "4px 10px",
                      border: `1px solid ${on ? "var(--cyan)" : "var(--line-1)"}`,
                      borderRadius: 4,
                      background: on ? "color-mix(in oklch, var(--cyan) 14%, var(--bg-1))" : "var(--bg-1)",
                      color: on ? "var(--cyan)" : "var(--fg-2)",
                      fontWeight: on ? 600 : 500,
                      fontSize: 11.5,
                      cursor: "pointer",
                      fontFamily: "inherit",
                    }}>
                    <span>{f.label}</span>
                    {on && <Icon name={sortOrder === "desc" ? "down" : "up"} size={10}/>}
                  </button>
                );
              })}
              <span style={{ width: 1, height: 16, background: "var(--line-1)", margin: "0 4px" }}/>
              <button
                type="button"
                onClick={() => {
                  if (sortKey === "received") setSortOrder(o => o === "desc" ? "asc" : "desc");
                  else setSortKey("received");
                }}
                title="按入库时间排序"
                style={{
                  display: "flex", alignItems: "center", gap: 4,
                  padding: "4px 10px",
                  border: `1px solid ${sortKey === "received" ? "var(--cyan)" : "var(--line-1)"}`,
                  borderRadius: 4,
                  background: sortKey === "received" ? "color-mix(in oklch, var(--cyan) 14%, var(--bg-1))" : "var(--bg-1)",
                  color: sortKey === "received" ? "var(--cyan)" : "var(--fg-2)",
                  fontWeight: sortKey === "received" ? 600 : 500,
                  fontSize: 11.5,
                  cursor: "pointer",
                  fontFamily: "inherit",
                }}>
                <span>入库时间</span>
                {sortKey === "received" && <Icon name={sortOrder === "desc" ? "down" : "up"} size={10}/>}
              </button>
            </div>

            {/* 第三行：行业 / 城市 / 时间 / 阈值（紧凑单行 + 自适应换行） */}
            <div className="row" style={{
              gap: 6, padding: "0 4px 12px",
              alignItems: "center", flexWrap: "wrap",
            }}>
              <select
                className="select"
                style={{ width: 130, fontSize: 12 }}
                value={filterIndustry}
                onChange={e => setFilterIndustry(e.target.value)}
                title="按行业筛选">
                {INDUSTRY_OPTIONS.map(o => (
                  <option key={o.key || "_all"} value={o.key}>{o.label}</option>
                ))}
              </select>

              <select
                className="select"
                style={{ width: 120, fontSize: 12 }}
                value={filterCity}
                onChange={e => setFilterCity(e.target.value)}>
                <option value="">全部城市</option>
                {cityOptions.map(c => <option key={c} value={c}>{c}</option>)}
              </select>

              <select
                className="select"
                style={{ width: 120, fontSize: 12 }}
                value={filterReceived}
                onChange={e => setFilterReceived(e.target.value)}
                title="按入库时间范围筛选">
                <option value="all">全部时间</option>
                <option value="today">今天入库</option>
                <option value="24h">最近 24 小时</option>
                <option value="7d">最近 7 天</option>
                <option value="30d">最近 30 天</option>
              </select>

              <input
                className="input"
                placeholder="最低点赞"
                type="number"
                min="0"
                style={{ width: 100, fontSize: 12 }}
                value={filterMinDigg}
                onChange={e => setFilterMinDigg(e.target.value)} />

              <input
                className="input"
                placeholder="最低评论"
                type="number"
                min="0"
                style={{ width: 100, fontSize: 12 }}
                value={filterMinComment}
                onChange={e => setFilterMinComment(e.target.value)} />

              <input
                className="input"
                placeholder="最低收藏"
                type="number"
                min="0"
                style={{ width: 100, fontSize: 12 }}
                value={filterMinCollect}
                onChange={e => setFilterMinCollect(e.target.value)} />

              {hasFilter && (
                <button
                  className="btn sm"
                  onClick={() => {
                    setFilterIndustry("");
                    setFilterCity("");
                    setFilterMinDigg("");
                    setFilterMinComment("");
                    setFilterMinCollect("");
                    setFilterReceived("all");
                  }}>
                  <Icon name="close" size={10}/>清空
                </button>
              )}
            </div>

            <div
              style={{
                border: "1px solid var(--line-1)",
                borderRadius: 6,
                overflow: "auto",
                maxHeight: "calc(100vh - 360px)",
                background: "var(--bg-0)",
              }}
            >
              <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 11.5 }}>
                <thead>
                  <tr>
                    <TH style={{ width: 36, textAlign: "center" }}>
                      <input
                        type="checkbox"
                        checked={pagedAllSelected}
                        ref={el => { if (el) el.indeterminate = !pagedAllSelected && pagedSomeSelected; }}
                        onChange={togglePageAll}
                        title={pagedAllSelected ? "取消选择本页" : "选择本页全部"}
                        style={{ cursor: "pointer" }}
                      />
                    </TH>
                    <TH style={{ width: 40, textAlign: "center" }}>平台</TH>
                    <TH>标题</TH>
                    <TH>作者</TH>
                    <TH>awemeId</TH>
                    <TH>房产</TH>
                    <TH style={{ textAlign: "right" }}>点赞</TH>
                    <TH style={{ textAlign: "right" }}>评论</TH>
                    <TH style={{ textAlign: "right" }}>收藏</TH>
                    <TH style={{ textAlign: "right" }}>分享</TH>
                    <TH>发布时间</TH>
                    <TH>入库时间</TH>
                    <TH style={{ textAlign: "center" }}>操作</TH>
                  </tr>
                </thead>
                <tbody>
                  {pagedItems.map((v) => {
                    const isNew = newIds.has(v.id);
                    const isSel = selRowIds.has(v.id);
                    return (
                      <tr
                        key={v.id}
                        onClick={() => toggleRow(v.id)}
                        style={{
                          background: isSel
                            ? "color-mix(in oklch, var(--cyan) 8%, transparent)"
                            : isNew
                              ? "color-mix(in oklch, var(--orange) 6%, transparent)"
                              : "transparent",
                          cursor: "pointer",
                        }}
                      >
                        <TD style={{ textAlign: "center" }}>
                          <input
                            type="checkbox"
                            checked={isSel}
                            onChange={() => toggleRow(v.id)}
                            onClick={(e) => e.stopPropagation()}
                            style={{ cursor: "pointer" }}
                          />
                        </TD>
                        <TD style={{ textAlign: "center" }}>
                          <PF type={v.p} />
                        </TD>
                        <TD>
                          <div className="row" style={{ minWidth: 0, gap: 6, alignItems: "center" }}>
                            <span
                              style={{
                                minWidth: 0,
                                overflow: "hidden",
                                textOverflow: "ellipsis",
                                whiteSpace: "nowrap",
                                maxWidth: 220,
                              }}
                              title={v.t}
                            >
                              {v.t}
                            </span>
                            {v.contentType === "image_post" && (
                              <span
                                className="tag"
                                style={{ fontSize: 9, color: "var(--fg-2)", borderColor: "var(--fg-3)" }}
                                title="图文笔记（非视频）"
                              >
                                图文
                              </span>
                            )}
                            {isNew && <span className="tag orange" style={{ fontSize: 9 }}>新</span>}
                          </div>
                        </TD>
                        <TD>
                          <span
                            style={{
                              overflow: "hidden",
                              textOverflow: "ellipsis",
                              whiteSpace: "nowrap",
                              display: "inline-block",
                              maxWidth: 100,
                            }}
                            title={v.a}
                          >
                            {v.a}
                          </span>
                        </TD>
                        <TD>
                          <span
                            className="mono"
                            style={{
                              overflow: "hidden",
                              textOverflow: "ellipsis",
                              whiteSpace: "nowrap",
                              display: "inline-block",
                              maxWidth: 120,
                              color: "var(--fg-2)",
                              fontSize: 10.5,
                            }}
                            title={v.externalId}
                          >
                            {v.externalId}
                          </span>
                        </TD>
                        <TD>
                          {v.isRealEstate ? (
                            <span className="tag green" style={{ fontSize: 9 }}>
                              房产{v.city ? `·${v.city}` : ""}
                              {v.estateType ? `·${v.estateType}` : ""}
                            </span>
                          ) : (
                            <span className="tag" style={{ fontSize: 9, color: "var(--fg-3)", borderColor: "transparent" }}>
                              非房产
                            </span>
                          )}
                        </TD>
                        <TD style={{ textAlign: "right" }}>
                          <span className="mono tnum">{formatWan(v.digg)}</span>
                        </TD>
                        <TD style={{ textAlign: "right" }}>
                          <span className="mono tnum">{formatWan(v.comment)}</span>
                        </TD>
                        <TD style={{ textAlign: "right" }}>
                          <span className="mono tnum">{formatWan(v.collect)}</span>
                        </TD>
                        <TD style={{ textAlign: "right" }}>
                          <span className="mono tnum">{formatWan(v.share)}</span>
                        </TD>
                        <TD>
                          <span className="mono" style={{ fontSize: 10.5, color: "var(--fg-2)" }}>
                            {fmtDtBJ(v.capturedAt)}
                          </span>
                        </TD>
                        <TD>
                          <span className="mono" style={{ fontSize: 10.5, color: "var(--fg-2)" }}>
                            {fmtDtBJ(v.receivedAt)}
                          </span>
                        </TD>
                        <TD style={{ textAlign: "center" }}>
                          {v.url ? (
                            <a
                              href={v.url}
                              target="_blank"
                              rel="noreferrer"
                              className="btn sm"
                              style={{ height: 20, padding: "0 6px", fontSize: 10 }}
                              onClick={(e) => e.stopPropagation()}
                            >
                              <Icon name="ext" size={10} />
                              打开
                            </a>
                          ) : (
                            <span style={{ fontSize: 10, color: "var(--fg-4)" }}>—</span>
                          )}
                        </TD>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
              {pool.length === 0 && !loading && (
                <div style={{ padding: "32px 20px", textAlign: "center", fontSize: 12, color: "var(--fg-3)" }}>
                  暂无视频数据，点击右上角「拉 10 条」开始采集
                </div>
              )}
              {pool.length === 0 && loading && (
                <div style={{ padding: "32px 20px", textAlign: "center", fontSize: 12, color: "var(--fg-3)" }}>
                  正在加载视频列表…
                </div>
              )}
              {pool.length > 0 && visible.length === 0 && !loading && (
                <div style={{ padding: "32px 20px", textAlign: "center", fontSize: 12, color: "var(--fg-3)", lineHeight: 1.7 }}>
                  {hasFilter
                    ? "当前筛选条件下没有匹配的视频，调整筛选条件试试"
                    : (listTab === "done" ? "暂无已拆解视频" : "未拆解列表为空")}
                </div>
              )}
            </div>

            {visible.length > 0 && (
              <div className="row between" style={{ marginTop: 10, padding: "4px 2px" }}>
                <span style={{ fontSize: 11, color: "var(--fg-3)", fontFamily: "var(--font-mono)" }}>
                  第 {page} / {totalPages} 页，共 {visible.length} 条{hasFilter && pool.length !== filtered.length ? `（筛后）` : ""}
                </span>
                <div className="row gap-6">
                  <button className="btn sm" disabled={page <= 1} onClick={() => setPage((p) => Math.max(1, p - 1))}>
                    上一页
                  </button>
                  <button className="btn sm" disabled={page >= totalPages} onClick={() => setPage((p) => Math.min(totalPages, p + 1))}>
                    下一页
                  </button>
                </div>
              </div>
            )}

            {/* ─── 发起拆解 ─── */}
            {selVids.length === 0 ? (
              <div className="card col center" style={{
                marginTop: 14, padding: "20px", textAlign: "center", gap: 6,
              }}>
                <Icon name="layers" size={20} style={{ color: "var(--fg-3)" }} />
                <div style={{ fontSize: 12, color: "var(--fg-1)" }}>请在上方表格中勾选要拆解的视频（支持多选）</div>
                <div style={{ fontSize: 11, color: "var(--fg-3)" }}>勾选后下方出现「发起拆解」表单</div>
              </div>
            ) : (
              <div className="card" style={{ marginTop: 14, overflow: "hidden" }}>
                <div className="card-hd">
                  <span className="card-title">发起拆解</span>
                  <span className="card-sub" style={{
                    overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap",
                    maxWidth: "70%", display: "inline-block",
                  }}>
                    {selVids.length === 1
                      ? `目标：${selVids[0].t} · ${selVids[0].a}`
                      : `已选 ${selVids.length} 条`}
                    {selVids.length === 1 && selVids[0]._status === "done" && <span className="tag green" style={{ marginLeft: 8, fontSize: 9 }}>已拆解</span>}
                    {selVids.length === 1 && selVids[0]._status === "in_progress" && <span className="tag cyan" style={{ marginLeft: 8, fontSize: 9 }}>拆解中</span>}
                    {selVids.length === 1 && selVids[0]._status === "failed" && <span className="tag pink" style={{ marginLeft: 8, fontSize: 9 }}>上次失败</span>}
                    {selVids.length > 1 && (
                      <button
                        type="button"
                        onClick={() => setSelRowIds(new Set())}
                        className="btn sm"
                        style={{ marginLeft: 8, height: 18, padding: "0 6px", fontSize: 10 }}>
                        清空选择
                      </button>
                    )}
                  </span>
                </div>
                <div className="col" style={{ padding: 12, gap: 10 }}>
                  {selVids.length > 1 && (skippedInProgress > 0 || skippedNoUrl > 0) && (
                    <div style={{
                      padding: "6px 10px", borderRadius: 5, fontSize: 11, lineHeight: 1.6,
                      background: "var(--orange-soft)", color: "var(--orange)",
                    }}>
                      {skippedInProgress > 0 && <>已选 {skippedInProgress} 条正在拆解中，将自动跳过。</>}
                      {skippedNoUrl > 0 && <>{skippedInProgress > 0 ? " " : ""}已选 {skippedNoUrl} 条无链接，将自动跳过。</>}
                      {launchable.length > 0 && ` 实际发起 ${launchable.length} 条。`}
                    </div>
                  )}

                  <div className="col gap-4">
                    <div className="row" style={{ alignItems: "baseline", gap: 6 }}>
                      <span className="kpi-l" style={{ width: 50 }}>模板</span>
                      <span style={{ fontSize: 10.5, color: "var(--fg-3)" }}>
                        可多选 · 每个模板生成一套独立任务
                      </span>
                    </div>
                    <div className="row gap-6" style={{ flexWrap: "wrap", paddingLeft: 56 }}>
                      {templates.map(t => {
                        const on = selTplIds.has(t.id);
                        return (
                          <button key={t.id}
                            type="button"
                            onClick={() => {
                              setSelTplIds(prev => {
                                const next = new Set(prev);
                                if (next.has(t.id)) next.delete(t.id);
                                else next.add(t.id);
                                return next;
                              });
                            }}
                            style={{
                              display: "flex", alignItems: "center", gap: 4,
                              padding: "4px 10px",
                              border: `1px solid ${on ? "var(--cyan)" : "var(--line-1)"}`,
                              borderRadius: 4,
                              background: on ? "color-mix(in oklch, var(--cyan) 14%, var(--bg-1))" : "var(--bg-1)",
                              color: on ? "var(--cyan)" : "var(--fg-1)",
                              fontWeight: on ? 600 : 500,
                              fontSize: 11.5,
                              cursor: "pointer",
                              fontFamily: "inherit",
                            }}>
                            {on && <Icon name="check" size={10}/>}
                            <span>{t.name}</span>
                            <span style={{
                              fontSize: 9, padding: "1px 4px", borderRadius: 3,
                              background: on ? "var(--bg-0)" : "var(--bg-2)",
                              color: t.category === "image_teardown" ? "var(--purple)" : "var(--cyan)",
                              opacity: 0.85,
                            }}>
                              {t.category === "image_teardown" ? "图文" : "视频"}
                            </span>
                            {t.is_default && (
                              <span style={{
                                fontSize: 9, padding: "1px 4px", borderRadius: 3,
                                background: on ? "var(--bg-0)" : "var(--bg-2)",
                                color: "var(--fg-3)",
                              }}>默认</span>
                            )}
                          </button>
                        );
                      })}
                      {!templates.length && (
                        <span style={{ fontSize: 11, color: "var(--fg-3)" }}>
                          暂无可用模板 · 请先到 <a href="/templates" style={{ color: "var(--cyan)" }}>模板库</a> 新建
                        </span>
                      )}
                    </div>
                    <span style={{ fontSize: 10.5, color: "var(--fg-3)", paddingLeft: 56 }}>
                      {selTplIds.size === 0
                        ? "未选 · 后端将自动用默认模板（每条视频 1 个任务）"
                        : `已选 ${selTplIds.size} 个 · 每条视频会发起 ${selTplIds.size} 个并行任务`}
                    </span>
                  </div>

                  <div className="row gap-8" style={{ alignItems: "center", flexWrap: "wrap" }}>
                    <span className="kpi-l" style={{ width: 50 }}>模型</span>
                    {credentials.length ? (
                      <select
                        className="select"
                        style={{ flex: 1, minWidth: 200, fontSize: 12 }}
                        value={selCredId || ""}
                        onChange={e => setSelCredId(Number(e.target.value) || null)}>
                        {credentials.map(c => {
                          const label = [c.name, c.provider, c.extra?.model_name].filter(Boolean).join(" · ");
                          return (
                            <option key={c.id} value={c.id}>
                              {c.is_active ? "✓ " : ""}{label}
                            </option>
                          );
                        })}
                      </select>
                    ) : (
                      <span style={{ fontSize: 11, color: "var(--fg-3)", flex: 1 }}>
                        暂无可用模型 · 请先在 <a href="/models" style={{ color: "var(--cyan)" }}>模型管理</a> 配置
                      </span>
                    )}
                  </div>

                  <div className="row gap-8" style={{ alignItems: "center", flexWrap: "wrap" }}>
                    <span className="kpi-l" style={{ width: 50 }}>规则</span>
                    <select
                      className="select"
                      style={{ flex: 1, minWidth: 200, fontSize: 12 }}
                      value={selRulePackId}
                      onChange={e => {
                        setSelRulePackId(e.target.value);
                        try { localStorage.setItem("collect.ruleId", e.target.value); } catch {}
                      }}
                      title="选规则模板：拆解 / 后续复刻的 LLM 都会在该规则约束下产出。不选 = 无规则限制">
                      <option value="">无规则限制</option>
                      {rulePacks.map(r => (
                        <option key={r.pack_id} value={r.pack_id}>{r.display_name}</option>
                      ))}
                    </select>
                    {selRulePackId && (
                      <span style={{ fontSize: 11, color: "var(--purple)" }}>
                        ✓ 已绑定规则
                      </span>
                    )}
                  </div>

                  <div className="row gap-8" style={{ paddingTop: 4 }}>
                    <button
                      className={`btn ${teardownBtn.primary ? "primary" : ""}`}
                      disabled={teardownBtn.disabled}
                      onClick={startTeardown}>
                      <Icon name="magic" size={12}/>{teardownBtn.text}
                    </button>
                    {selVids.length === 1 && !selVids[0].url && (
                      <span style={{ fontSize: 11, color: "var(--pink)" }}>该视频无链接，无法拆解</span>
                    )}
                  </div>
                </div>
              </div>
            )}

            {/* ─── 最近发起 ─── */}
            {recentlyStarted.length > 0 && (
              <div className="card" style={{ marginTop: 14, overflow: "hidden" }}>
                <div className="card-hd">
                  <span className="card-title">最近发起（{recentlyStarted.length}）</span>
                  <span className="card-sub">点击「查看进度」前往拆解爆款页查看完整进度</span>
                </div>
                <div className="col" style={{ padding: 12, gap: 8 }}>
                  {recentlyStarted.map(r => {
                    const j = jobs.find(x => x.job_id === r.jobId);
                    const st = j?.status || "pending";
                    const tag = st === "completed" ? "green" : st === "failed" ? "pink" : "cyan";
                    const text = st === "completed" ? "已完成" : st === "failed" ? "失败" : st === "running" ? "进行中" : "排队中";
                    const pct = j?.progress?.pct ?? 0;
                    return (
                      <div key={r.jobId} className="row between" style={{
                        padding: "8px 10px", borderRadius: 5, gap: 12,
                        background: "var(--bg-1)", border: "1px solid var(--line-1)",
                      }}>
                        <div className="col gap-3" style={{ minWidth: 0, flex: 1 }}>
                          <span style={{
                            fontSize: 12, color: "var(--fg-0)", fontWeight: 500,
                            overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap",
                          }}>{r.title}</span>
                          <span className="mono" style={{ fontSize: 10, color: "var(--fg-3)" }}>
                            {r.jobId.slice(0, 10)}… · 进度 {pct}%
                          </span>
                        </div>
                        <div className="row gap-6" style={{ flexShrink: 0 }}>
                          <span className={`tag ${tag}`} style={{ fontSize: 10 }}>{text}</span>
                          <a href="/workspace" className="btn sm">
                            <Icon name="ext" size={10}/>查看进度
                          </a>
                        </div>
                      </div>
                    );
                  })}
                </div>
              </div>
            )}
          </div>
        </div>

        {/* 右：简洁信息面板 */}
        <div
          className="col scroll"
          style={{
            width: 280,
            flexShrink: 0,
            padding: "16px",
            gap: 18,
            borderLeft: "1px solid var(--line-soft)",
            background: "var(--bg-0)",
          }}
        >
          {meta.pull?.last_error && (
            <div
              style={{
                padding: "10px 12px",
                background: "var(--pink-soft)",
                border: "1px solid var(--pink)",
                borderRadius: 5,
                fontSize: 11,
                color: "var(--pink)",
                lineHeight: 1.5,
              }}
            >
              <div style={{ fontWeight: 600, marginBottom: 4 }}>⚠ 上次拉取失败</div>
              {meta.pull.last_error}
            </div>
          )}

          <div>
            <div style={{ fontSize: 11, color: "var(--fg-3)", letterSpacing: ".02em", marginBottom: 10 }}>
              采集状态
            </div>
            <div style={{ position: "relative", paddingLeft: 22 }}>
              <div style={{ position: "absolute", left: 9, top: 8, bottom: 8, width: 1, background: "var(--line-1)" }} />
              {[
                { s: "光年总量", v: (lastResult.total || 0).toLocaleString() + " 条", ok: !!lastResult.total },
                { s: "本地入库", v: pool.length.toLocaleString() + " 条", ok: pool.length > 0 },
                { s: "上次拉取", v: (
                  <div className="col" style={{ gap: 2 }}>
                    <span>{fmtDtSecBJ(lastPullAt) || "—"}</span>
                    <span>新增 {lastResult.inserted ?? 0}</span>
                    <span>重复 {lastResult.skipped || 0}</span>
                  </div>
                ), ok: !!lastPullAt },
                { s: "下次拉取", v: countdown || "—", ok: countdown && !countdown.includes("即将") },
              ].map((r, i) => (
                <div key={i} className="row" style={{ padding: "7px 0", gap: 10, position: "relative" }}>
                  <span
                    style={{
                      position: "absolute",
                      left: -22,
                      top: 9,
                      width: 19,
                      height: 19,
                      borderRadius: "50%",
                      background: r.ok ? "var(--green)" : "var(--bg-2)",
                      color: r.ok ? "var(--bg-0)" : "var(--fg-4)",
                      display: "flex",
                      alignItems: "center",
                      justifyContent: "center",
                      fontSize: 10,
                      fontWeight: 700,
                      border: "2px solid var(--bg-0)",
                    }}
                  >
                    {r.ok ? "✓" : "—"}
                  </span>
                  <span style={{ fontSize: 11, color: "var(--fg-3)", width: 66 }}>{r.s}</span>
                  <span style={{ flex: 1, fontSize: 11.5, color: "var(--fg-1)", wordBreak: "break-all" }}>{r.v}</span>
                </div>
              ))}
            </div>
          </div>

          <div>
            <div style={{ fontSize: 11, color: "var(--fg-3)", letterSpacing: ".02em", marginBottom: 10 }}>
              拉取历史（最近 {pullHistory.length} 次）
            </div>
            <div className="col" style={{ gap: 2 }}>
              {pullHistory.length === 0 && (
                <span style={{ fontSize: 11, color: "var(--fg-3)" }}>暂无拉取记录</span>
              )}
              {pullHistory.slice().reverse().map((h, i) => (
                <div
                  key={i}
                  className="row between"
                  style={{
                    fontSize: 10.5,
                    padding: "6px 0",
                    borderBottom: i === pullHistory.length - 1 ? "none" : "1px solid var(--line-1)",
                    alignItems: "center",
                  }}
                >
                  <span className="mono" style={{ color: "var(--fg-2)", fontSize: 10 }}>
                    {fmtDtSecBJ(h.at)}
                  </span>
                  {h.ok ? (
                    <div className="col" style={{ gap: 2, alignItems: "flex-end" }}>
                      <span className="tag green" style={{ fontSize: 9 }}>新增 {h.result?.inserted ?? 0}</span>
                      <span className="tag" style={{ fontSize: 9, color: "var(--fg-3)", borderColor: "transparent" }}>重复 {h.result?.skipped || 0}</span>
                    </div>
                  ) : (
                    <span className="tag pink" style={{ fontSize: 9 }} title={h.error}>
                      失败
                    </span>
                  )}
                </div>
              ))}
            </div>
          </div>

          <div>
            <div style={{ fontSize: 11, color: "var(--fg-3)", letterSpacing: ".02em", marginBottom: 8 }}>
              快速操作
            </div>
            <div className="row gap-6" style={{ flexWrap: "wrap" }}>
              <button className="btn sm" onClick={loadInbox}>
                <Icon name="refresh" size={10} />
                刷新列表
              </button>
              <button className="btn sm primary" disabled={pulling} onClick={pullOnce}>
                <Icon name="plus" size={10} />
                {pulling ? "拉取中" : "拉 10 条"}
              </button>
            </div>
          </div>

          <div style={{ fontSize: 10.5, color: "var(--fg-3)", lineHeight: 1.7, padding: "0 2px" }}>
            数据源：<span className="mono">/api/inbox</span>
            <br />
            当前已入库 {pool.length.toLocaleString()} 条，其中房产 {realEstateCount.toLocaleString()} 条，非房产{" "}
            {nonRealEstateCount.toLocaleString()} 条。
            <br />
            页面每 5 秒刷新列表，后台每 {intervalSec} 秒自动拉取一次光年AI OpenAPI。
          </div>
        </div>
      </div>
    </>
  );
};

Object.assign(window, { CollectPage });
