Files

496 lines
27 KiB
JavaScript
Raw Permalink Normal View History

window.__ModuleLoader__.load({
id: "dsh-plugin-douyin-video-detail",
factory: (require) => {
var module = { exports: {} };
var exports = module.exports;
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
let react = require("react");
let react_dom = require("react-dom");
//#region plugin css(暗色适配:嵌套变量 + 无浅色 fallback,见踩坑手册 3.4)
const css = ".dvv_page{position:relative;background:var(--dsw-alias-bg-base);width:100%;min-width:0;height:100%;min-height:0;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);flex:1;display:grid;grid-template-rows:minmax(0,1fr);overflow:hidden}.dvv_body{display:flex;flex-direction:column;height:100%;min-height:0;padding:16px;box-sizing:border-box;gap:12px}.dvv_toolbar{display:flex;align-items:center;gap:10px;min-width:0}.dvv_btn{border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));border-radius:8px;padding:6px 14px;font:var(--dsw-font-s-14);cursor:pointer;background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated));color:var(--dsw-alias-label-primary);transition:background var(--ds-transition-duration-fast) var(--ds-ease-in-out)}.dvv_btn:hover{background:var(--dsw-alias-interactive-bg-hover)}.dvv_title{font:var(--dsw-font-l-16);font-weight:600;color:var(--dsw-alias-label-primary);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.dvv_link{color:var(--dsw-alias-accent-fg);cursor:pointer;text-decoration:none;border-bottom:1px dashed var(--dsw-alias-accent-fg,transparent)}.dvv_link:hover{text-decoration:underline}.dvv_infoCard{flex:0 0 auto;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));border-radius:8px;padding:14px 16px;background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated))}.dvv_infoGrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:10px 18px}.dvv_infoItem{display:flex;flex-direction:column;gap:4px;min-width:0}.dvv_infoItemWide{grid-column:1/-1}.dvv_infoLabel{font:var(--dsw-font-s-12);color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary))}.dvv_infoValue{font:var(--dsw-font-s-14);color:var(--dsw-alias-label-primary);word-break:break-word;white-space:pre-wrap;line-height:1.6}.dvv_analysisHead{flex:0 0 auto;font:var(--dsw-font-l-15);font-weight:600;color:var(--dsw-alias-label-primary)}.dvv_analysisWrap{flex:1;min-height:0;overflow:auto;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));border-radius:8px;padding:14px 16px;background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated));box-sizing:border-box}.dvv_state{padding:28px 16px;text-align:center;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));font:var(--dsw-font-s-14)}.dvv_loading{display:flex;align-items:center;justify-content:center;height:100%;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary))}.dvv_error{display:flex;align-items:center;justify-content:center;height:100%;color:var(--dsw-alias-danger-fg,var(--dsw-alias-error-fg))}.dvv_empty{display:flex;align-items:center;justify-content:center;height:100%;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary))}.dvv_md{font:var(--dsw-font-s-14);line-height:1.75;color:var(--dsw-alias-label-primary);word-break:break-word}.dvv_md h1,.dvv_md h2,.dvv_md h3,.dvv_md h4{margin:18px 0 8px;font-weight:600;line-height:1.4}.dvv_md h1{font-size:20px;margin-top:26px}.dvv_md h2{font-size:17px;margin-top:24px}.dvv_md h3{font-size:15px;margin-top:20px}.dvv_md h4{font-size:14px;margin-top:18px}.dvv_md p{margin:8px 0}.dvv_md ul,.dvv_md ol{margin:8px 0;padding-left:22px}.dvv_md li{margin:2px 0}.dvv_md a{color:var(--dsw-alias-accent-fg);text-decoration:none;border-bottom:1px dashed var(--dsw-alias-accent-fg,transparent)}.dvv_md a:hover{text-decoration:underline}.dvv_md strong{font-weight:600}.dvv_md em{font-style:italic}.dvv_md code{font-family:var(--dsw-font-mono,monospace);font-size:.9em;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));border-radius:4px;padding:1px 5px}.dvv_md pre{background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));border-radius:8px;padding:12px;overflow:auto;margin:10px 0;line-height:1.55}.dvv_md pre code{background:none;border:none;padding:0;display:block;white-space:pre;font-size:13px}.dvv_md blockquote{margin:10px 0;padding:8px 14px;border-left:3px solid var(--dsw-alias-border-l2,var
const key = "dsh-plugin-douyin-video-detail/VideoDetail.module.css";
if (!document.querySelector("style[data-plugin-css=" + JSON.stringify(key) + "]")) {
const el = document.createElement("style");
el.dataset.plugin = "dsh-plugin-douyin-video-detail";
el.dataset.pluginCss = key;
el.textContent = css;
document.head.appendChild(el);
}
const cssMap = {
"page": "dvv_page",
"body": "dvv_body",
"toolbar": "dvv_toolbar",
"btn": "dvv_btn",
"title": "dvv_title",
"link": "dvv_link",
"infoCard": "dvv_infoCard",
"infoGrid": "dvv_infoGrid",
"infoItem": "dvv_infoItem",
"infoItemWide": "dvv_infoItemWide",
"infoLabel": "dvv_infoLabel",
"infoValue": "dvv_infoValue",
"analysisHead": "dvv_analysisHead",
"analysisWrap": "dvv_analysisWrap",
"tabs": "dvv_tabs",
"tab": "dvv_tab",
"tabActive": "dvv_tabActive",
"tabBody": "dvv_tabBody",
"state": "dvv_state",
"loading": "dvv_loading",
"error": "dvv_error",
"empty": "dvv_empty",
"md": "dvv_md"
};
//#endregion
//#region 极简 markdown 渲染(视频分析内容:无第三方依赖,渲染为 React 元素避免 XSS)
// 行内语法:**粗体** / *斜体* / `行内代码` / [链接](url)(仅允许 http/https/mailto)
const INLINE_RE = /(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`|\[[^\]]+\]\([^)]+\))/g;
// 文本段:含 \n 时拆为文本 + <br/>(段落内换行在 HTML 中默认折叠为空格,需显式换行)
function textWithBreaks(seg, keyBase) {
if (!seg.includes("\n")) return [seg];
const parts = seg.split("\n");
const out = [];
parts.forEach((p, i) => {
if (p) out.push(p);
if (i < parts.length - 1) out.push(react.createElement("br", { key: keyBase + "-br" + i }));
});
return out;
}
function parseInline(text, keyBase) {
if (!text) return [text];
const nodes = [];
let last = 0, k = 0;
for (const m of text.matchAll(INLINE_RE)) {
if (m.index > last) nodes.push(...textWithBreaks(text.slice(last, m.index), keyBase + "-t" + k));
const tok = m[0];
if (tok.startsWith("**") && tok.endsWith("**") && tok.length > 4) {
nodes.push(react.createElement("strong", { key: keyBase + "-b" + k }, tok.slice(2, -2)));
} else if (tok.startsWith("*") && tok.endsWith("*") && tok.length > 2) {
nodes.push(react.createElement("em", { key: keyBase + "-i" + k }, tok.slice(1, -1)));
} else if (tok.startsWith("`") && tok.endsWith("`") && tok.length > 2) {
nodes.push(react.createElement("code", { key: keyBase + "-c" + k }, tok.slice(1, -1)));
} else if (tok.startsWith("[")) {
const mm = tok.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
const href = mm && /^(https?:|mailto:)/i.test(mm[2]) ? mm[2] : null;
nodes.push(href
? react.createElement("a", { key: keyBase + "-a" + k, href: href, target: "_blank", rel: "noopener noreferrer" }, mm[1])
: (mm ? mm[1] : tok));
} else {
nodes.push(tok);
}
last = m.index + tok.length;
k++;
}
if (last < text.length) nodes.push(...textWithBreaks(text.slice(last), keyBase + "-t" + k));
return nodes;
}
// 表格块:rows 为已去掉分隔行的字符串数组
function mdTable(rows, keyBase) {
const split = (r) => r.replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
const head = split(rows[0]);
const body = rows.slice(1).map(split);
return react.createElement("table", { key: keyBase },
react.createElement("thead", null,
react.createElement("tr", null, head.map((h, i) => react.createElement("th", { key: "h" + i }, parseInline(h, keyBase + "-h" + i))))),
react.createElement("tbody", null,
body.map((row, ri) => react.createElement("tr", { key: "r" + ri },
row.map((c, ci) => react.createElement("td", { key: "c" + ci }, parseInline(c, keyBase + "-c" + ri + "-" + ci)))))));
}
// 块级解析:标题/代码块/引用/表格/列表/分隔线/段落
function mdToReact(md) {
if (typeof md !== "string" || !md.trim()) return null;
const lines = md.replace(/\r\n/g, "\n").split("\n");
const out = [];
let i = 0, k = 0;
while (i < lines.length) {
const t = lines[i].trim();
// 围栏代码块
const fence = t.match(/^```(\S*)\s*$/);
if (fence) {
const buf = [];
i++;
while (i < lines.length && !/^```\s*$/.test(lines[i].trim())) { buf.push(lines[i]); i++; }
i++;
out.push(react.createElement("pre", { key: "md" + k++ },
react.createElement("code", null, buf.join("\n"))));
continue;
}
// 标题
const h = t.match(/^(#{1,4})\s+(.*)$/);
if (h) {
out.push(react.createElement("h" + h[1].length, { key: "md" + k++ }, parseInline(h[2], "h" + k)));
i++;
continue;
}
// 分隔线
if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(t)) {
out.push(react.createElement("hr", { key: "md" + k++ }));
i++;
continue;
}
// 引用
if (t.startsWith(">")) {
const buf = [];
while (i < lines.length && lines[i].trim().startsWith(">")) { buf.push(lines[i].trim().replace(/^>\s?/, "")); i++; }
out.push(react.createElement("blockquote", { key: "md" + k++ }, mdToReact(buf.join("\n"))));
continue;
}
// 表格:当前行含 | 且下一行为 |---| 分隔行
if (t.includes("|") && i + 1 < lines.length && /^\s*\|?[\s:|-]+\|?\s*$/.test(lines[i + 1]) && lines[i + 1].includes("-")) {
const sep = lines[i + 1].replace(/\s/g, "");
if (/^\|?(:?-+:?\|)+:?-+:?\|?$/.test(sep)) {
const buf = [t];
i += 2;
while (i < lines.length && lines[i].trim().includes("|")) { buf.push(lines[i].trim()); i++; }
out.push(mdTable(buf, "md" + k++));
continue;
}
}
// 无序列表
const ul = t.match(/^[-*+]\s+(.*)$/);
if (ul) {
const items = [];
while (i < lines.length) {
const mt = lines[i].trim().match(/^[-*+]\s+(.*)$/);
if (mt) { items.push(mt[1]); i++; continue; }
break;
}
out.push(react.createElement("ul", { key: "md" + k++ },
items.map((it, ii) => react.createElement("li", { key: ii }, parseInline(it, "li" + k + "-" + ii)))));
continue;
}
// 有序列表
const ol = t.match(/^\d+[.)]\s+(.*)$/);
if (ol) {
const items = [];
while (i < lines.length) {
const mt = lines[i].trim().match(/^\d+[.)]\s+(.*)$/);
if (mt) { items.push(mt[1]); i++; continue; }
break;
}
out.push(react.createElement("ol", { key: "md" + k++ },
items.map((it, ii) => react.createElement("li", { key: ii }, parseInline(it, "li" + k + "-" + ii)))));
continue;
}
// 空行跳过
if (t === "") { i++; continue; }
// 段落:合并连续普通行(不跨空行)
const para = [t];
i++;
while (i < lines.length) {
const nt = lines[i].trim();
if (nt === "" || /^(#{1,4}\s|```|>|[-*+]\s|\d+[.)]\s|.*\|)/.test(nt) || /^(-{3,}|\*{3,}|_{3,})\s*$/.test(nt)) break;
para.push(nt);
i++;
}
out.push(react.createElement("p", { key: "md" + k++ }, parseInline(para.join("\n"), "p" + k)));
}
return out;
}
//#endregion
//#region 视频详情页
const fmt = (v) => (v == null || v === "") ? "-" : String(v);
// 时长格式化:秒 → 「X分Y秒」(不足 1 分钟显示「X秒」)
const fmtDuration = (v) => {
const s = Number(v);
if (!Number.isFinite(s) || s <= 0) return "-";
const m = Math.floor(s / 60);
const sec = Math.round(s % 60);
return m > 0 ? `${m}分${sec}秒` : `${sec}秒`;
};
//#region 视频解析任务跟踪(V1.0 同步:解析/刷新 + 断点恢复;localStorage 持久化,页面重建后继续盯任务)
// 记录:{ ts, force };ts=提交时刻。轮询 /mcn/api/task/status 的 analyze[](videoId→running)。
// 结束判定:提交后 90s 宽限窗(服务端 60s 防抖)+ 超时上限 35 分钟。
const PARSE_DEADLINE_MS = 35 * 60 * 1000;
const PARSE_START_WINDOW_MS = 90 * 1000;
const taskKeyOf = (id) => "dvv_parse_task_" + id;
const readTaskRec = (id) => {
try { const raw = localStorage.getItem(taskKeyOf(id)); if (!raw) return null; const o = JSON.parse(raw); return o && o.ts ? o : null; } catch (e) { return null; }
};
const writeTaskRec = (id, force) => {
try { localStorage.setItem(taskKeyOf(id), JSON.stringify({ ts: Date.now(), force: !!force })); } catch (e) {}
};
const clearTaskRec = (id) => {
try { localStorage.removeItem(taskKeyOf(id)); } catch (e) {}
};
//#endregion
// 视频拆解(analysis.json dict,12-13 维度)→ markdown 文本(每个维度一节,供 mdToReact 渲染)
const analysisToMarkdown = (obj) => Object.entries(obj || {})
.map(([k, v]) => `## ${k}\n\n${v}`)
.join("\n\n---\n\n");
// 详情页 tab:视频脚本(content)→ 视频拆解(analysis)
const DETAIL_TABS = [
{ id: "script", label: "视频脚本" },
{ id: "deconstruct", label: "视频拆解" },
];
function VideoDetailPage({ params }) {
const videoId = params && params.videoId != null ? Number(params.videoId) : null;
const awemeId = params && params.awemeId != null ? String(params.awemeId) : null;
const accountId = params && params.accountId != null ? Number(params.accountId) : null;
const [video, setVideo] = react.useState(null);
const [analysis, setAnalysis] = react.useState(null);
const [parse, setParse] = react.useState(null);
const [activeTab, setActiveTab] = react.useState("script");
const [loading, setLoading] = react.useState(videoId != null || awemeId != null);
const [error, setError] = react.useState(null);
// V1.0 同步:解析任务状态 idle|submitting|running + 提示条 + 手动重载计数
const [parseState, setParseState] = react.useState("idle");
const [info, setInfo] = react.useState(null);
const [reloadTick, setReloadTick] = react.useState(0);
const parseVideoId = videoId; // 解析任务只能按库内视频发起(aweme 直链无行不提供解析)
react.useEffect(() => {
if (videoId == null && awemeId == null) return;
let alive = true;
setLoading(true);
setError(null);
setVideo(null);
setAnalysis(null);
setParse(null);
setActiveTab("script");
const qs = new URLSearchParams();
if (awemeId) qs.set("awemeId", awemeId);
else qs.set("videoId", String(videoId));
fetch("/mcn/api/video/detail?" + qs.toString())
.then((r) => r.json())
.then((d) => {
if (!d.ok) throw new Error(d.error || "加载失败");
if (!alive) return;
setVideo(d.video || null);
setAnalysis(d.analysis || null);
setParse(d.parse || null);
})
.catch((e) => { if (alive) setError(e.message); })
.finally(() => { if (alive) setLoading(false); });
return () => { alive = false; };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [videoId, awemeId, reloadTick]);
// V1.0 同步:解析任务轮询(5s,断点恢复:localStorage 有记录 → 页面重建后继续盯)。
// 初始态恢复:挂载后立即检查一次(record 存在且未超时 → running)。
react.useEffect(() => {
if (parseVideoId == null) return;
let finishedOnce = false;
const check = () => {
const rec = readTaskRec(parseVideoId);
fetch("/mcn/api/task/status")
.then((r) => r.json())
.then((d) => {
if (!d.ok) return;
const arr = Array.isArray(d.analyze) ? d.analyze : [];
const item = arr.find((a) => Number(a.videoId) === parseVideoId);
const serverRunning = !!(item && item.status === "running");
if (rec) {
const age = Date.now() - rec.ts;
if (age > PARSE_DEADLINE_MS) {
clearTaskRec(parseVideoId);
setParseState("idle");
setInfo("解析任务超过 35 分钟未完成(可能已中断),可重新发起");
return;
}
if (serverRunning || age < PARSE_START_WINDOW_MS) {
setParseState((prev) => (prev === "submitting" ? "running" : prev === "running" ? "running" : "running"));
return;
}
// 记录存在、服务端空闲、宽限窗已过 → 任务结束
clearTaskRec(parseVideoId);
setParseState("idle");
if (!finishedOnce) {
finishedOnce = true;
setInfo("解析任务已结束,正在刷新数据…");
setTimeout(() => setReloadTick((x) => x + 1), 300);
}
} else if (serverRunning) {
setParseState("running");
} else if (parseState === "submitting") {
// 提交后宽限期内服务端尚无条目:保持 waiting,交给 rec 分支处理
}
})
.catch(() => {});
};
// 挂载恢复 + 周期轮询
check();
const timer = setInterval(check, 5000);
return () => clearInterval(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [parseVideoId]);
// 提示条自动消隐
react.useEffect(() => {
if (!info) return;
const timer = setTimeout(() => setInfo(null), 6000);
return () => clearTimeout(timer);
}, [info]);
// 提交解析/刷新(V1.0 同步:force=覆盖重解析;服务端 60s 防抖 + 8 并发上限)
const submitParse = (force) => {
if (parseVideoId == null) return;
const tip = force
? "该视频已有解析结果,「刷新」将重新发起 MCP 解析并覆盖(约 3-5 分钟)。确认继续?"
: "对该视频发起 AI 解析(MCP 内容解析,约 3-5 分钟,产出「视频脚本」+「视频拆解」)。确认继续?";
if (!window.confirm(tip)) return;
setParseState("submitting");
fetch("/mcn/api/video/analyze", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ videoId: parseVideoId }),
})
.then((r) => r.json())
.then((d) => {
if (d.ok) {
writeTaskRec(parseVideoId, force);
setParseState("running");
setInfo("已提交解析任务(约 3-5 分钟),完成后自动刷新。可在「MCN任务会话」查看执行过程");
} else {
setParseState("idle");
setInfo(d.error || "提交失败");
}
})
.catch((e) => { setParseState("idle"); setInfo("提交失败: " + e.message); });
};
// 无参数(从工作台直接打开):提示从账号详情进入
if (videoId == null && awemeId == null) {
return react.createElement("main", { className: cssMap.page, "aria-label": "视频详情" },
react.createElement("div", { className: cssMap.body },
react.createElement("div", { className: cssMap.toolbar },
react.createElement("button", { type: "button", className: cssMap.btn, title: "返回账号列表", onClick: () => window.__mcnNav?.open("douyin-accounts") }, "←")),
react.createElement("div", { className: cssMap.empty }, "请从「账号详情」的视频列表点击视频ID进入")));
}
const infoItem = (label, value, wide) => {
const cls = cssMap.infoItem + (wide ? " " + cssMap.infoItemWide : "");
return react.createElement("div", { className: cls, key: label },
react.createElement("div", { className: cssMap.infoLabel }, label),
react.createElement("div", { className: cssMap.infoValue }, value));
};
const backTitle = video ? (video.video_title || "视频详情") : "视频详情";
// V1.0 同步:解析/刷新按钮(有解析结果=「刷新」强制覆盖;无=「解析」发起)
const hasParsed = !!(parse && (parse.content || parse.analysis));
const parseBusy = parseState === "submitting" || parseState === "running";
let parseBtn = null;
if (video && parseVideoId != null) {
parseBtn = react.createElement("button", {
type: "button",
className: cssMap.btn,
disabled: parseBusy,
title: parseBusy
? "解析任务执行中(可在 MCN任务会话 查看)"
: (hasParsed
? "重新发起 MCP 解析并覆盖现有脚本/拆解(约 3-5 分钟)"
: "对该视频发起 MCP 内容解析(约 3-5 分钟)"),
onClick: () => submitParse(hasParsed),
}, parseBusy ? (parseState === "submitting" ? "提交中…" : "解析中…") : (hasParsed ? "刷新" : "解析"));
}
// V1.0 对齐:查看原视频(外链)
let viewVideoBtn = null;
if (video && video.video_url) {
viewVideoBtn = react.createElement("a", { className: cssMap.btn, href: video.video_url, target: "_blank", rel: "noopener noreferrer", title: "在抖音打开原视频", style: { marginLeft: "auto" } }, "查看视频 ↗");
}
// 视频信息卡
let infoCard = null;
let analysisArea = null;
if (loading) {
analysisArea = react.createElement("div", { className: cssMap.loading }, "加载中…");
} else if (error) {
analysisArea = react.createElement("div", { className: cssMap.error }, error);
} else if (video) {
const titleVal = video.video_url
? react.createElement("a", {
className: cssMap.link,
href: video.video_url,
target: "_blank",
rel: "noopener noreferrer",
title: "在新页面打开视频",
}, fmt(video.video_title))
: fmt(video.video_title);
infoCard = react.createElement("div", { className: cssMap.infoCard },
react.createElement("div", { className: cssMap.infoGrid },
infoItem("账号", video.account_id ? react.createElement("a", { className: cssMap.link, style: { cursor: "pointer" }, title: "查看账号详情", onClick: () => { try { window.__mcnNav && window.__mcnNav.open("douyin-account-detail", { accountId: video.account_id, accountName: video.account_name }); } catch (e) {} } }, fmt(video.account_name)) : fmt(video.account_name)),
infoItem("视频ID", fmt(video.aweme_id)),
infoItem("时长", fmtDuration(video.duration)),
infoItem("发布时间", fmt(video.publish_time)),
infoItem("点赞", fmt(video.like_display || video.like_count)),
infoItem("评论", fmt(video.comment_count)),
infoItem("分享", fmt(video.share_count)),
infoItem("收藏", fmt(video.collect_count)),
infoItem("标签", fmt(video.tags), true)));
// 三个 tab 内容:视频脚本(MCP content)→ 视频拆解(MCP analysis)→ 视频分析(拆解分析)
const scriptMd = parse && parse.content;
const deconstructMd = parse && parse.analysis ? analysisToMarkdown(parse.analysis) : null;
const analysisMd = analysis && analysis.content;
let tabBody = null;
if (activeTab === "script") {
tabBody = scriptMd
? react.createElement("div", { className: cssMap.md }, mdToReact(scriptMd))
: react.createElement("div", { className: cssMap.state }, "该视频暂无视频脚本(未做原视频解析,可点右上角「解析」发起)");
} else if (activeTab === "deconstruct") {
tabBody = deconstructMd
? react.createElement("div", { className: cssMap.md }, mdToReact(deconstructMd))
: react.createElement("div", { className: cssMap.state }, "该视频暂无视频拆解(MCP 结构化分析,可点右上角「解析」发起)");
} else {
tabBody = analysisMd
? react.createElement("div", { className: cssMap.md }, mdToReact(analysisMd))
: react.createElement("div", { className: cssMap.state }, "该视频暂无视频分析(完成视频分析后展示)");
}
analysisArea = react.createElement(react.Fragment, null,
react.createElement("div", { className: cssMap.tabs },
DETAIL_TABS.map((t) => react.createElement("button", {
key: t.id,
type: "button",
className: cssMap.tab + (activeTab === t.id ? " " + cssMap.tabActive : ""),
onClick: () => setActiveTab(t.id),
}, t.label))),
react.createElement("div", { className: cssMap.tabBody }, tabBody));
}
return react.createElement("main", { className: cssMap.page, "aria-label": "视频详情" },
react.createElement("div", { className: cssMap.body },
react.createElement("div", { className: cssMap.toolbar },
react.createElement("button", {
type: "button",
className: cssMap.btn,
title: "返回账号详情",
onClick: () => window.__mcnNav?.back(),
}, "←"),
react.createElement("span", { className: cssMap.title }, backTitle),
viewVideoBtn,
parseBtn),
info ? react.createElement("div", { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary,#8a8f98)" } }, info) : null,
infoCard,
analysisArea));
}
//#endregion
//#region plugin entry:注册到 MCN 工作台入口注册表(hidden:不显示在工作台网格,仅程序化导航打开)
// 统一线性 SVG 图标(Lucide 风格)
const IconVideo = ({ size = 16 }) => react.createElement("svg", {
viewBox: "0 0 24 24",
width: size,
height: size,
fill: "none",
stroke: "currentColor",
strokeWidth: "2",
strokeLinecap: "round",
strokeLinejoin: "round",
"aria-hidden": "true",
}, [react.createElement("path", { key: "p1", d: "m16 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.87a.5.5 0 0 0-.752-.432L16 10.5" }), react.createElement("rect", { key: "p2", x: "2", y: "6", width: "14", height: "12", rx: "2" })]);
const inject = [];
function apply() {
window.__mcnEntries = window.__mcnEntries || [];
// 幂等:避免插件热重载重复注册
if (!window.__mcnEntries.some((e) => e.id === "douyin-video-detail")) {
window.__mcnEntries.push({
id: "douyin-video-detail",
icon: IconVideo,
title: "视频详情",
desc: "视频信息与拆解分析(从账号详情视频列表进入)",
hidden: true,
component: VideoDetailPage,
});
}
}
//#endregion
exports.apply = apply;
exports.inject = inject;
return module.exports;
}
});