|
|
@@ -3,15 +3,25 @@
|
|
|
// 内容资产「成品货架」:把本次入池(ADD_TO_CONTENT_POOL)的视频以交付清单形式摆出来。
|
|
|
// 每张卡 = 可播放 iframe + 来历 + Gemini 判定 + 抖音真实热度 + 打开原帖,
|
|
|
// 卡底预留「我方平台实测表现」位(入库后回填,与 Gemini 预测对照)。数据全部来自已加载的 content-items,无新增取数。
|
|
|
-import { useState } from "react";
|
|
|
-import { ExternalLink, Lock, MessageCircle, PlayCircle, Share2, ShieldCheck, Star, ThumbsUp } from "lucide-react";
|
|
|
-import { contentUrl, embedPlayerUrl, platformLabel } from "@/lib/platform/content";
|
|
|
-import type { ContentItemsResponse } from "@/lib/api/types";
|
|
|
+import { useEffect, useState, type ReactNode } from "react";
|
|
|
+import { Eye, ExternalLink, Lock, MessageCircle, PlayCircle, Share2, ShieldCheck, Star, ThumbsUp } from "lucide-react";
|
|
|
+import { contentUrl, embedPlayerUrl, HEAT_FIELD_LABELS, platformLabel } from "@/lib/platform/content";
|
|
|
+import { getConfigPlatforms } from "@/lib/api/client";
|
|
|
+import type { ContentItemsResponse, PlatformDescriptor } from "@/lib/api/types";
|
|
|
|
|
|
type AnyRec = Record<string, unknown>;
|
|
|
|
|
|
const POOLED = "ADD_TO_CONTENT_POOL";
|
|
|
|
|
|
+// 统一互动指标键 → 图标(标签走 HEAT_FIELD_LABELS,均由平台 profile 的 heat 字段驱动)。
|
|
|
+const HEAT_ICON: Record<string, ReactNode> = {
|
|
|
+ digg_count: <ThumbsUp size={13} />,
|
|
|
+ comment_count: <MessageCircle size={13} />,
|
|
|
+ share_count: <Share2 size={13} />,
|
|
|
+ collect_count: <Star size={13} />,
|
|
|
+ play_count: <Eye size={13} />
|
|
|
+};
|
|
|
+
|
|
|
function decisionAction(it: AnyRec): string {
|
|
|
const rd = (it.rule_decision as AnyRec) || {};
|
|
|
const pmr = (it.pattern_match_result as AnyRec) || {};
|
|
|
@@ -61,19 +71,30 @@ function AssetMedia({ item }: { item: AnyRec }) {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
-function StatPill({ icon, value }: { icon: React.ReactNode; value: string }) {
|
|
|
+// 互动数据行:只显示该平台真实可得的指标(profile.heat_fields 驱动);
|
|
|
+// 抖音=赞/评/分享/收藏,视频号=只点赞,快手=含播放…缺指标不显示「0」。无 heat 字段则整行不渲染。
|
|
|
+function HeatStats({ item, descriptor }: { item: AnyRec; descriptor?: PlatformDescriptor }) {
|
|
|
+ const st = (item.statistics as AnyRec) || {};
|
|
|
+ const fields = descriptor?.heat_fields || [];
|
|
|
+ if (!fields.length) return null;
|
|
|
+ const label = platformLabel(item.platform);
|
|
|
return (
|
|
|
- <span className="asset-stat">
|
|
|
- {icon}
|
|
|
- {value}
|
|
|
- </span>
|
|
|
+ <div className="asset-stats">
|
|
|
+ <span className="asset-stats-label">{label}热度</span>
|
|
|
+ {fields.map((f) => (
|
|
|
+ <span className="asset-stat" key={f} title={HEAT_FIELD_LABELS[f] || f}>
|
|
|
+ {HEAT_ICON[f] || null}
|
|
|
+ <em>{HEAT_FIELD_LABELS[f] || f}</em>
|
|
|
+ {fmtStat(st[f])}
|
|
|
+ </span>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
-function AssetCard({ item }: { item: AnyRec }) {
|
|
|
+function AssetCard({ item, descriptor }: { item: AnyRec; descriptor?: PlatformDescriptor }) {
|
|
|
const pmr = (item.pattern_match_result as AnyRec) || {};
|
|
|
const rd = (item.rule_decision as AnyRec) || {};
|
|
|
- const st = (item.statistics as AnyRec) || {};
|
|
|
const title = String(item.description || item.title || `${platformLabel(item.platform)}视频`);
|
|
|
const author = String(item.author_display_name || item.platform_author_id || "未知作者");
|
|
|
const queries = (item.matched_search_queries as unknown[]) || [];
|
|
|
@@ -118,13 +139,7 @@ function AssetCard({ item }: { item: AnyRec }) {
|
|
|
|
|
|
{reason ? <p className="asset-reason">{reason}</p> : null}
|
|
|
|
|
|
- <div className="asset-stats">
|
|
|
- <span className="asset-stats-label">抖音热度</span>
|
|
|
- <StatPill icon={<ThumbsUp size={13} />} value={fmtStat(st.digg_count)} />
|
|
|
- <StatPill icon={<MessageCircle size={13} />} value={fmtStat(st.comment_count)} />
|
|
|
- <StatPill icon={<Star size={13} />} value={fmtStat(st.collect_count)} />
|
|
|
- <StatPill icon={<Share2 size={13} />} value={fmtStat(st.share_count)} />
|
|
|
- </div>
|
|
|
+ <HeatStats item={item} descriptor={descriptor} />
|
|
|
|
|
|
{tags.length ? (
|
|
|
<div className="asset-tags">
|
|
|
@@ -180,6 +195,17 @@ export function ContentAssetShelf({
|
|
|
}) {
|
|
|
const items = (contentItems.items as AnyRec[]) || [];
|
|
|
const pooled = items.filter((it) => decisionAction(it) === POOLED);
|
|
|
+ // 平台展示目录(label + 每平台真实互动指标),加平台只改 profile JSON、前端零改动。
|
|
|
+ const [catalog, setCatalog] = useState<Record<string, PlatformDescriptor>>({});
|
|
|
+ useEffect(() => {
|
|
|
+ let alive = true;
|
|
|
+ getConfigPlatforms()
|
|
|
+ .then((r) => alive && setCatalog(r.platforms || {}))
|
|
|
+ .catch(() => {});
|
|
|
+ return () => {
|
|
|
+ alive = false;
|
|
|
+ };
|
|
|
+ }, []);
|
|
|
|
|
|
return (
|
|
|
<div className="asset-shelf">
|
|
|
@@ -196,13 +222,17 @@ export function ContentAssetShelf({
|
|
|
<span>淘汰</span>
|
|
|
<strong>{rejected}</strong>
|
|
|
</div>
|
|
|
- <p className="asset-summary-note">本次交付的「成品」=入池资产;下面逐条可播放、看判定与抖音热度。</p>
|
|
|
+ <p className="asset-summary-note">本次交付的「成品」=入池资产;下面逐条可播放、看判定与各平台互动热度。</p>
|
|
|
</div>
|
|
|
|
|
|
{pooled.length ? (
|
|
|
<div className="asset-shelf-list">
|
|
|
{pooled.map((it, i) => (
|
|
|
- <AssetCard item={it} key={String(it.platform_content_id || i)} />
|
|
|
+ <AssetCard
|
|
|
+ item={it}
|
|
|
+ descriptor={catalog[String(it.platform || "").toLowerCase()]}
|
|
|
+ key={String(it.platform_content_id || i)}
|
|
|
+ />
|
|
|
))}
|
|
|
</div>
|
|
|
) : (
|