一键全流程自动演示:影院字幕条+10镜头剧本(采集→发现→定义→审核→入图→演化→图谱四视角→时序生长→匹配→路径),自动运镜与倒计时进度条,支持暂停/步进/Esc退出;修复非JD来源标注显示与演示镜头选中淡出干扰

This commit is contained in:
FinStab
2026-08-31 19:40:41 +08:00
parent 6312dd5f4a
commit 0bd0e30aa0
4 changed files with 165 additions and 43 deletions
+132 -29
View File
@@ -166,8 +166,10 @@ async function fetchJd(sid) {
try { return await p; } catch (e) { _jdCache.delete(sid); throw e; } try { return await p; } catch (e) { _jdCache.delete(sid); throw e; }
} }
/* 来源列表行内联元信息:填充为「企业 · 平台 · 日期」可读文案(不显示内部 source_id) */ /* 来源列表行内联元信息:填充为「企业 · 平台 · 日期」可读文案(不显示内部 source_id);
* 非 JD 证据来源(如「人工审核入库」等标注)直接原文展示,不走 JD 查询 */
CN.jdMetaLine = async function (el, sid) { CN.jdMetaLine = async function (el, sid) {
if (!/\.jsonl#/.test(String(sid))) { el.textContent = String(sid); return; }
try { try {
const r = await fetchJd(sid); const r = await fetchJd(sid);
el.textContent = `${r.company || "企业未披露"} · ${r.source || "公开招聘平台"} · ${r.publish_date || "时间未知"}`; el.textContent = `${r.company || "企业未披露"} · ${r.source || "公开招聘平台"} · ${r.publish_date || "时间未知"}`;
@@ -181,7 +183,7 @@ CN.jdMetaLine = async function (el, sid) {
* ctx:{ skill, jobTitle, changeType, window, keyword } 用于生成可读结论与命中依据; * ctx:{ skill, jobTitle, changeType, window, keyword } 用于生成可读结论与命中依据;
* keyword 命中词在 JD 原文中以 <mark> 高亮 */ * keyword 命中词在 JD 原文中以 <mark> 高亮 */
async function showJdEvidence(sids, ctx = {}) { async function showJdEvidence(sids, ctx = {}) {
const list = (Array.isArray(sids) ? sids : [sids]).filter(Boolean); const list = (Array.isArray(sids) ? sids : [sids]).filter(Boolean).filter((s) => /\.jsonl#/.test(String(s)));
if (!list.length) return toast("该条目暂无关联的公开 JD 证据", "info"); if (!list.length) return toast("该条目暂无关联的公开 JD 证据", "info");
let idx = 0; let idx = 0;
const conclusion = ctx.skill const conclusion = ctx.skill
@@ -2151,41 +2153,93 @@ $("#banner-config").onclick = () => openConfigModal(false);
$("#embed-banner-config").onclick = () => openConfigModal(false); $("#embed-banner-config").onclick = () => openConfigModal(false);
$("#demo-badge").onclick = () => openConfigModal(false); $("#demo-badge").onclick = () => openConfigModal(false);
/* ================= 演示导览(6 步评委导览,右上角浮动步进条) ================= */ /* ================= 全流程自动演示导览(一键播放,录屏/评委演示用) =================
* 剧本聚焦赛题主线:多源采集 → 新岗位发现 → AI 生成定义 → 人工审核入图 →
* 既有岗位演化 → 图谱全景四视角 → 动态时序生长 → 人岗匹配 → 学习路径。
* 全程只读演示:不真实点击批准/退回,不改任何数据。 */
const DEMO_JOB = "job_Java开发工程师"; const DEMO_JOB = "job_Java开发工程师";
const DEMO_CANDIDATE = "job_大模型应用工程师"; const DEMO_CAND = "cand_提示词工程师"; // 待审核新岗位候选(发现→定义→审核镜头)
const DEMO_NEWJOB = "job_RAG工程师"; // 已入库新岗位(NEW 脉动标记镜头)
const zz = (ms) => new Promise((r) => setTimeout(r, ms));
const DEMO_STEPS = [ const DEMO_STEPS = [
{ title: "数据驾驶舱:多源数据总览", { dur: 6000,
desc: "JD 规模、岗位/技能/事件总量、演化趋势、清洗与审核状态", title: "多源数据汇聚与治理",
desc: "多平台真实招聘语料经清洗、结构化抽取、交叉验证后进入图谱管线",
go: () => switchView("dashboard") }, go: () => switchView("dashboard") },
{ title: "新岗位候选:发现 → 人工审核 → 入图", { dur: 8000,
desc: "候选岗位不直接入图:四指标证据 + 人工确认,可回查证据 JD", title: "新岗位发现:四指标聚类",
desc: "JD≥15 条 · 企业≥5 家 · 环比增长≥50% · 差异度≥0.4;候选以虚线节点呈现,绝不直接入图",
go: () => { go: () => {
switchView("graph"); switchView("graph");
const locate = () => { const locate = () => {
const has = CN.graph && CN.graph.has && CN.graph.has(DEMO_CANDIDATE); const id = (CN.graph && CN.graph.has && CN.graph.has(DEMO_CAND)) ? DEMO_CAND : DEMO_JOB;
if (CN.gotoNode) CN.gotoNode(has ? DEMO_CANDIDATE : DEMO_JOB); if (CN.gotoNode) CN.gotoNode(id);
}; };
if (CN.graph && CN.graph.ready) CN.graph.ready().then(locate); if (CN.graph && CN.graph.ready) CN.graph.ready().then(locate);
else if (CN.gotoNode) CN.gotoNode(DEMO_CANDIDATE);
} }, } },
{ title: "岗位局部图谱:Java 开发工程师", { dur: 9000,
desc: "技能点级局部图谱:必备/加分技能、支撑 JD 数与可读证据", title: "大模型生成结构化岗位定义",
go: () => { desc: "岗位名称 / 核心职责 / 必备技能 / 加分技能 / 典型场景自动归纳,四指标证据全部可回查原始 JD",
go: async (alive) => {
await zz(400); if (!alive()) return;
openReviewDesk((CN.graph && CN.graph.has && CN.graph.has(DEMO_CAND)) ? DEMO_CAND : undefined);
} },
{ dur: 10000,
title: "人工审核把关:批准后才入图",
desc: "人机协同防幻觉——批准 / 退回全程留痕;已入库新岗位(如 RAG 工程师)带 NEW 脉动标记进入全景图谱",
go: async (alive) => {
await zz(3600); if (!alive()) return;
const btn = document.querySelector('#review-desk-body [data-act="accept"]');
if (btn) btn.classList.add("tour-glow");
await zz(3600); if (!alive()) return;
if (btn) btn.classList.remove("tour-glow");
$("#review-desk-mask").classList.add("hidden");
switchView("graph"); switchView("graph");
const enter = () => { if (CN.graph && CN.graph.solo) CN.graph.solo(DEMO_JOB); }; const enter = () => { if (CN.gotoNode) CN.gotoNode(DEMO_NEWJOB); };
if (CN.graph && CN.graph.ready) CN.graph.ready().then(enter); if (CN.graph && CN.graph.ready) CN.graph.ready().then(enter);
} }, } },
{ title: "岗位演化报告:能力动态更新", { dur: 8500,
desc: "本期结论、新增/增强/减弱/退出、时点对比,每条可回查证据", title: "既有岗位能力动态更新",
go: () => { desc: "本期结论 + 四色 Diff:新增 / 增强 / 减弱 / 退出,每条结论可回查支撑 JD 证据链",
go: async (alive) => {
switchView("evolution"); switchView("evolution");
$("#evo-job-select").value = DEMO_JOB; $("#evo-job-select").value = DEMO_JOB;
$("#evo-job-select").dispatchEvent(new Event("change")); $("#evo-job-select").dispatchEvent(new Event("change"));
await zz(1400); if (!alive()) return;
const d = $("#evo-diff"); if (d) d.scrollIntoView({ block: "center", behavior: "smooth" });
} }, } },
{ title: "人岗诊断:示例简历自动跑通", { dur: 12000,
desc: "载入示例简历 → 解析确认 → 匹配结论 / 核心缺口 / 可迁移能力", title: "全景图谱:四种视角看岗位生态",
go: async () => { desc: "全景总览 → 技术栈气泡聚类 → 职级能力树,点击节点可下钻详情与证据",
go: async (alive) => {
switchView("graph");
if (CN.graph && CN.graph.ready) await CN.graph.ready();
if (!alive()) return;
if (CN.graph.deselect) CN.graph.deselect(); // 巡览全图前清选中,避免非相关节点淡出
const tab = (v) => { const b = document.querySelector(`[data-gview="${v}"]`); if (b) b.click(); };
tab("overview");
await zz(3400); if (!alive()) return;
tab("tech");
await zz(4000); if (!alive()) return;
tab("level");
} },
{ dur: 9500,
title: "动态时序:图谱生长回放",
desc: "拖动 / 播放时间轴,看岗位能力图谱从 2019 到 2026Q3 的逐年扩张,左栏同步解读当季变化",
go: async (alive) => {
switchView("graph");
if (CN.graph && CN.graph.ready) await CN.graph.ready();
if (!alive()) return;
const b = document.querySelector('[data-gview="timeline"]'); if (b) b.click();
if (CN.graph.deselect) CN.graph.deselect(); // 生长回放看全图,清选中避免淡出
await zz(700); if (!alive()) return;
const p = $("#gt-play"); if (p && p.textContent === "播放") p.click();
} },
{ dur: 13000,
title: "人岗匹配:示例简历自动跑通",
desc: "载入示例简历 → 解析确认 → 双雷达差距分析:命中 / 缺失 / 可迁移一目了然",
go: async (alive) => {
switchView("match"); switchView("match");
$$("#view-match .match-tab").forEach((x) => x.classList.toggle("active", x.dataset.mtab === "single")); $$("#view-match .match-tab").forEach((x) => x.classList.toggle("active", x.dataset.mtab === "single"));
$("#match-single").classList.remove("hidden"); $("#match-single").classList.remove("hidden");
@@ -2195,39 +2249,88 @@ const DEMO_STEPS = [
$("#btn-load-sample").click(); $("#btn-load-sample").click();
try { try {
const p = await post("/api/resume/parse", { text: $("#resume-text").value }); const p = await post("/api/resume/parse", { text: $("#resume-text").value });
if (!alive()) return;
applyParsedProfile(p, "演示示例简历"); applyParsedProfile(p, "演示示例简历");
$("#btn-run-match").click(); $("#btn-run-match").click();
} catch (e) { toast(friendlyErr(e), "error"); } } catch (e) { toast(friendlyErr(e), "error"); }
} }, } },
{ title: "学习路径:可执行的三阶段路线", { dur: 7500,
title: "学习路径:可执行的三阶段路线",
desc: "自动带入岗位与技能缺口:基础补齐 → 岗位核心 → 项目验证", desc: "自动带入岗位与技能缺口:基础补齐 → 岗位核心 → 项目验证",
go: () => { go: () => {
switchView("path"); switchView("path");
$("#path-job-select").value = DEMO_JOB; $("#path-job-select").value = DEMO_JOB;
loadPathJob(DEMO_JOB, matchState.skills).then(() => $("#btn-path").click()); loadPathJob(DEMO_JOB, matchState.skills).then(() => $("#btn-path").click());
} }, } },
{ dur: 5500,
title: "全流程闭环",
desc: "采集 → 发现 → 定义 → 审核 → 演化 → 图谱 → 匹配 → 路径,全链路可追溯、可验证",
go: () => switchView("dashboard") },
]; ];
let tourIdx = -1; let tourIdx = -1;
function tourGo(i) { let tourToken = 0; // 场景令牌:切镜/退出时作废旧镜头的延时动作与计时器
tourIdx = Math.max(0, Math.min(DEMO_STEPS.length - 1, i)); let tourAuto = false; // 自动播放中
function updateTourPlayBtn() {
const b = $("#tour-play");
if (b) b.textContent = tourAuto ? "暂停" : "继续";
}
function tourGo(i, auto) {
if (i >= DEMO_STEPS.length) { endTour(); toast("全流程演示完毕", "success"); return; }
tourToken++; // 作废旧场景的所有延时动作
const tk = tourToken;
tourIdx = Math.max(0, i);
tourAuto = !!auto;
const step = DEMO_STEPS[tourIdx]; const step = DEMO_STEPS[tourIdx];
$("#tour-bar").classList.remove("hidden"); $("#tour-bar").classList.remove("hidden");
$("#tour-prog").textContent = `演示 ${tourIdx + 1}/${DEMO_STEPS.length}`; $("#tour-prog").textContent = `${tourIdx + 1}/${DEMO_STEPS.length}`;
$("#tour-title").textContent = step.title; $("#tour-title").textContent = step.title;
$("#tour-desc").textContent = step.desc; $("#tour-desc").textContent = step.desc;
$("#tour-prev").disabled = tourIdx === 0; $("#tour-prev").disabled = tourIdx === 0;
$("#tour-next").textContent = tourIdx === DEMO_STEPS.length - 1 ? "完成" : "下一步"; $("#tour-next").textContent = tourIdx === DEMO_STEPS.length - 1 ? "完成" : "下一步";
Promise.resolve(step.go()).catch((e) => toast(friendlyErr(e), "error")); $("#tour-fill").style.width = "0%";
updateTourPlayBtn();
const alive = () => tk === tourToken && tourIdx >= 0;
Promise.resolve(step.go(alive)).catch((e) => toast(friendlyErr(e), "error"));
if (auto) autoAdvance(tourIdx, tk);
} }
/* 自动播放计时:暂停时冻结进度,令牌失效即废弃 */
async function autoAdvance(i, tk) {
const dur = DEMO_STEPS[i].dur || 6000;
const fill = $("#tour-fill");
let elapsed = 0;
while (elapsed < dur) {
await zz(120);
if (tk !== tourToken || tourIdx < 0) return;
if (!tourAuto) continue; // 暂停:不计时
elapsed += 120;
if (fill) fill.style.width = `${Math.min(100, (elapsed / dur) * 100)}%`;
}
if (tk === tourToken && tourIdx >= 0) tourGo(i + 1, true);
}
function endTour() { function endTour() {
tourIdx = -1; tourIdx = -1;
tourToken++;
tourAuto = false;
$("#tour-bar").classList.add("hidden"); $("#tour-bar").classList.add("hidden");
const mask = $("#review-desk-mask");
if (mask) mask.classList.add("hidden");
} }
$("#btn-demo-tour").onclick = () => tourGo(0);
$("#tour-prev").onclick = () => tourGo(tourIdx - 1); $("#btn-demo-tour").onclick = () => tourGo(0, true); // 一键自动播放全流程
$("#tour-next").onclick = () => (tourIdx === DEMO_STEPS.length - 1 ? endTour() : tourGo(tourIdx + 1)); $("#tour-play").onclick = () => {
tourAuto = !tourAuto;
updateTourPlayBtn();
if (tourAuto && tourIdx >= 0) autoAdvance(tourIdx, tourToken); // 从当前场景重新计时继续
};
$("#tour-prev").onclick = () => tourGo(tourIdx - 1, false);
$("#tour-next").onclick = () => tourGo(tourIdx + 1, false);
$("#tour-exit").onclick = endTour; $("#tour-exit").onclick = endTour;
document.addEventListener("keydown", (e) => { if (e.key === "Escape" && tourIdx >= 0) endTour(); });
function embedStatusHtml(emb) { function embedStatusHtml(emb) {
if (emb.ready) { if (emb.ready) {
+2
View File
@@ -2043,6 +2043,8 @@
/* 数据装载完成 Promise(演示导览等需要等图谱就绪后定位) */ /* 数据装载完成 Promise(演示导览等需要等图谱就绪后定位) */
ready() { return this._p || Promise.resolve(); }, ready() { return this._p || Promise.resolve(); },
has(id) { return id in G.idx; }, has(id) { return id in G.idx; },
/* 清空选中(演示巡览等需要全图无聚焦的场景) */
deselect() { selectNode(null, false); },
/* 进入指定岗位的局部子图(演示导览第 3 步):同心分层布局 */ /* 进入指定岗位的局部子图(演示导览第 3 步):同心分层布局 */
solo(id) { solo(id) {
if (!(id in G.idx)) return; if (!(id in G.idx)) return;
+8 -6
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CareerNexus 职联 · 岗位与能力图谱平台</title> <title>CareerNexus 职联 · 岗位与能力图谱平台</title>
<link rel="stylesheet" href="/static/style.css?v=20260828-2"> <link rel="stylesheet" href="/static/style.css?v=20260831">
</head> </head>
<body> <body>
<!-- ============ 顶部栏 56px ============ --> <!-- ============ 顶部栏 56px ============ -->
@@ -692,19 +692,21 @@
</aside> </aside>
</div> </div>
<!-- 评委演示导览浮动条(右上角固定) --> <!-- 全流程自动演示字幕条(底部居中影院风,一键播放,录屏友好) -->
<div class="tour-bar hidden" id="tour-bar"> <div class="tour-bar hidden" id="tour-bar">
<span class="tour-prog" id="tour-prog">演示 1/6</span> <span class="tour-prog" id="tour-prog">1/10</span>
<span class="tour-text"> <span class="tour-text">
<b id="tour-title"></b> <b id="tour-title"></b>
<span class="tour-desc" id="tour-desc"></span> <span class="tour-desc" id="tour-desc"></span>
</span> </span>
<button class="btn btn-secondary btn-sm" id="tour-play">暂停</button>
<button class="btn btn-secondary btn-sm" id="tour-prev">上一步</button> <button class="btn btn-secondary btn-sm" id="tour-prev">上一步</button>
<button class="btn btn-primary btn-sm" id="tour-next">下一步</button> <button class="btn btn-secondary btn-sm" id="tour-next">下一步</button>
<button class="btn btn-text btn-sm" id="tour-exit">退出</button> <button class="btn btn-text btn-sm" id="tour-exit">退出</button>
<i class="tour-track"><i id="tour-fill"></i></i>
</div> </div>
<script src="/static/app.js?v=20260827"></script> <script src="/static/app.js?v=20260831"></script>
<script src="/static/graph.js?v=20260828-2"></script> <script src="/static/graph.js?v=20260831"></script>
</body> </body>
</html> </html>
+23 -8
View File
@@ -1110,21 +1110,36 @@ svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width:
/* ============ 2026-08 答辩级改造新增样式 ============ */ /* ============ 2026-08 答辩级改造新增样式 ============ */
/* 评委演示导览浮动条(右上角固定) */ /* 全流程自动演示字幕条(底部居中,深色影院风,录屏友好) */
.tour-bar { .tour-bar {
position: fixed; top: calc(var(--topbar-h) + 10px); right: 16px; z-index: 900; position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%); z-index: 900;
display: flex; align-items: center; gap: 10px; display: flex; align-items: center; gap: 12px;
background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; background: rgba(29,33,41,0.9); border: none; border-radius: 14px;
box-shadow: var(--shadow-pop); padding: 8px 12px; max-width: 640px; box-shadow: var(--shadow-pop); padding: 12px 18px 20px; max-width: 880px; width: max-content;
backdrop-filter: blur(8px); color: #fff;
} }
.tour-prog { .tour-prog {
font-size: 12px; font-weight: 600; color: #fff; background: var(--primary); font-size: 12px; font-weight: 600; color: #fff; background: var(--primary);
border-radius: 999px; padding: 3px 10px; white-space: nowrap; border-radius: 999px; padding: 3px 10px; white-space: nowrap;
} }
.tour-text { display: flex; flex-direction: column; min-width: 0; } .tour-text { display: flex; flex-direction: column; min-width: 0; max-width: 540px; }
.tour-text b { font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .tour-text b { font-size: 15px; color: #fff; }
.tour-desc { font-size: 11px; color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .tour-desc { font-size: 12px; color: rgba(255,255,255,0.72); white-space: normal; line-height: 1.5; margin-top: 2px; }
.tour-bar .btn { white-space: nowrap; } .tour-bar .btn { white-space: nowrap; }
.tour-bar .btn-secondary { background: rgba(255,255,255,0.14); border-color: transparent; color: #fff; }
.tour-bar .btn-secondary:hover { background: rgba(255,255,255,0.24); }
.tour-bar .btn-secondary:disabled { opacity: .4; }
.tour-bar .btn-text { color: rgba(255,255,255,0.65); }
.tour-bar .btn-text:hover { color: #fff; }
/* 场景进度条(自动播放倒计时) */
.tour-track {
position: absolute; left: 16px; right: 16px; bottom: 8px; height: 3px;
border-radius: 2px; background: rgba(255,255,255,0.16); overflow: hidden; display: block;
}
.tour-track i { display: block; height: 100%; width: 0; background: #fff; border-radius: 2px; }
/* 演示高亮脉冲(如审核台批准按钮,不真实点击) */
@keyframes tourGlow { 0%,100% { box-shadow: 0 0 0 0 rgba(24,144,255,0.55); } 50% { box-shadow: 0 0 0 8px rgba(24,144,255,0); } }
.tour-glow { animation: tourGlow 1.1s ease-in-out infinite; }
/* 证据弹窗:结论 / 来源 / 命中依据 / 弱化内部编号 */ /* 证据弹窗:结论 / 来源 / 命中依据 / 弱化内部编号 */
.ev-conclusion { font-size: 15px; font-weight: 600; margin-bottom: 8px; } .ev-conclusion { font-size: 15px; font-weight: 600; margin-bottom: 8px; }