import { useEffect, useMemo, useState } from "react"; import { Alert, Button, Skeleton, Table, Tag, Typography } from "antd"; import type { ColumnsType } from "antd/es/table"; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "/demand/api/v1"; const BODY_PREVIEW_LENGTH = 160; const getResolvedApiBaseUrl = () => { if (API_BASE_URL.startsWith("http://") || API_BASE_URL.startsWith("https://")) { return API_BASE_URL; } return new URL(API_BASE_URL, window.location.origin).toString(); }; type ExportRow = { id: number; item_type: string; item_text: string; point_category: string; matched_demand: string; contribution_score: number | null; wxindex_keyword: string; all_hot_keywords: string; wxindex_latest_score: number; wxindex_trend: string; contributes_to_sync: boolean; }; type HotContentSourceDetail = { wxindex_threshold: number; sync_log: { id: number; partition_dt: string; strategy: string; demand_id: string; demand_name: string; demand_type: string; record_id: number; synced_at: string; demand_id_verified: boolean; hive_weight: number | null; }; record: { id: number; source: string; title: string; article_title: string; article_body: string; hot_rank: number | null; created_at: string; contribution: Record; wxindex: Record; max_wxindex_score: number; passes_wxindex_gate: boolean; passes_point_gate: boolean; }; export_rows: ExportRow[]; }; function formatNumber(value: unknown): string { const number = Number(value); if (!Number.isFinite(number)) { return "-"; } if (Math.abs(number) >= 10000) { return `${(number / 10000).toFixed(1)}万`; } return String(Math.round(number)); } function formatScore(value: unknown): string { const number = Number(value); if (!Number.isFinite(number)) { return "-"; } return String(Number(number.toFixed(2))); } function formatPartitionDt(value: string): string { const text = value.trim(); if (/^\d{8}$/.test(text)) { return `${text.slice(0, 4)}-${text.slice(4, 6)}-${text.slice(6)}`; } return text || "-"; } function renderContributionSummary(contribution: Record) { const words = contribution["高贡献词列表"]; const points = contribution["点列表"]; let matchedWords = 0; if (Array.isArray(words)) { for (const item of words) { if ( item && typeof item === "object" && Array.isArray((item as Record)["匹配需求列表"]) && ((item as Record)["匹配需求列表"] as unknown[]).length > 0 ) { matchedWords += 1; } } } const wordCount = Array.isArray(words) ? words.length : 0; const pointCount = Array.isArray(points) ? points.length : 0; return (
高贡献词 {wordCount} 个 匹配到需求 {matchedWords} 个 点列表 {pointCount} 个
); } function renderWxindexSummary(wxindex: Record) { const wx = wxindex.wxindex && typeof wxindex.wxindex === "object" ? (wxindex.wxindex as Record) : {}; const keyword = String(wx.keyword ?? wxindex.llm_selected_word ?? "-"); const trend = String(wx.trend ?? "-"); const latest = formatNumber(wx.latest_total_score); const reason = String(wxindex.llm_reason ?? "").trim(); return ( <>
关键词 {keyword} 趋势 {trend} 当前得分 {latest}
{reason || "暂无 LLM 选词理由"}
); } type HotContentSourcePageProps = { demandName: string; demandType: string; dt: string; onBack: () => void; }; export default function HotContentSourcePage({ demandName, demandType, dt, onBack, }: HotContentSourcePageProps) { const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [detail, setDetail] = useState(null); const [bodyExpanded, setBodyExpanded] = useState(false); useEffect(() => { let cancelled = false; const load = async () => { setLoading(true); setError(""); setDetail(null); const resolvedBase = getResolvedApiBaseUrl(); const baseWithSlash = resolvedBase.endsWith("/") ? resolvedBase : `${resolvedBase}/`; const url = new URL("demand-pool/hot-content-source", baseWithSlash); url.searchParams.set("demand_name", demandName); url.searchParams.set("demand_type", demandType); url.searchParams.set("dt", dt); try { const response = await fetch(url.toString(), { method: "GET", headers: { Accept: "application/json" }, }); const payload = (await response.json()) as { detail?: string }; if (!response.ok) { throw new Error(payload.detail ?? `HTTP ${response.status}`); } if (!cancelled) { setDetail(payload as HotContentSourceDetail); } } catch (loadError) { if (!cancelled) { setError( loadError instanceof Error ? loadError.message : "加载失败,请重试", ); } } finally { if (!cancelled) { setLoading(false); } } }; void load(); return () => { cancelled = true; }; }, [demandName, demandType, dt]); const exportColumns: ColumnsType = useMemo( () => [ { title: "ID", dataIndex: "id", width: 72 }, { title: "类型", dataIndex: "item_type", width: 72 }, { title: "词条", dataIndex: "item_text", render: (value) => {value || "-"}, }, { title: "点类型", dataIndex: "point_category", width: 96, render: (value) => value || "-", }, { title: "匹配需求", dataIndex: "matched_demand", render: (value) => value || "-", }, { title: "微信热点词", dataIndex: "wxindex_keyword", width: 120, render: (value) => value || "-", }, { title: "贡献分", dataIndex: "contribution_score", width: 88, render: (value) => (value == null ? "-" : formatScore(value)), }, { title: "微信指数", dataIndex: "wxindex_latest_score", width: 96, render: (value) => formatNumber(value), }, { title: "趋势", dataIndex: "wxindex_trend", width: 88, render: (value) => value || "-", }, { title: "状态", key: "status", width: 100, render: (_, row) => { const matched = Boolean(row.matched_demand?.trim()); return ( {matched ? "匹配需求" : "未匹配"} ); }, }, ], [], ); const record = detail?.record; const syncLog = detail?.sync_log; const articleBodyText = useMemo( () => (record?.article_body ?? "").replace(/\s+/g, " ").trim(), [record?.article_body], ); useEffect(() => { setBodyExpanded(false); }, [record?.id]); const bodyTruncated = articleBodyText.length > BODY_PREVIEW_LENGTH; const displayBody = !articleBodyText ? "暂无正文" : bodyExpanded || !bodyTruncated ? articleBodyText : `${articleBodyText.slice(0, BODY_PREVIEW_LENGTH)}…`; return (
新热事件需求来源 需求:{demandName} · 类型:{demandType} · 分区:{formatPartitionDt(dt)}
{loading ? (
) : null} {!loading && error ? ( ) : null} {!loading && !error && detail && record && syncLog ? (
{record.title || syncLog.demand_name}
{record.source || "-"} record #{record.id} 同步于 {syncLog.synced_at}
微信指数{record.passes_wxindex_gate ? "达标" : "未达标"} 灵感/目的点{record.passes_point_gate ? "有匹配" : "无匹配"} demand_id{syncLog.demand_id_verified ? "校验通过" : "校验失败"}
策略 {syncLog.strategy} 分区 {formatPartitionDt(syncLog.partition_dt)} 需求类型 {syncLog.demand_type} Hive 权重 {syncLog.hive_weight == null ? "-" : formatScore(syncLog.hive_weight)} 门槛 {formatNumber(detail.wxindex_threshold)}
{record.article_title || "-"}
{displayBody}
{bodyTruncated ? ( ) : null} 贡献匹配摘要 {renderContributionSummary(record.contribution)} 微信指数摘要 {renderWxindexSummary(record.wxindex)}
元素/短语

高亮行表示参与生成该需求词的导出行。

row.contributes_to_sync ? "hot-source-export-highlight" : "" } /> ) : null} ); }