一键全流程自动演示:影院字幕条+10镜头剧本(采集→发现→定义→审核→入图→演化→图谱四视角→时序生长→匹配→路径),自动运镜与倒计时进度条,支持暂停/步进/Esc退出;修复非JD来源标注显示与演示镜头选中淡出干扰
This commit is contained in:
+132
-29
@@ -166,8 +166,10 @@ async function fetchJd(sid) {
|
||||
try { return await p; } catch (e) { _jdCache.delete(sid); throw e; }
|
||||
}
|
||||
|
||||
/* 来源列表行内联元信息:填充为「企业 · 平台 · 日期」可读文案(不显示内部 source_id) */
|
||||
/* 来源列表行内联元信息:填充为「企业 · 平台 · 日期」可读文案(不显示内部 source_id);
|
||||
* 非 JD 证据来源(如「人工审核入库」等标注)直接原文展示,不走 JD 查询 */
|
||||
CN.jdMetaLine = async function (el, sid) {
|
||||
if (!/\.jsonl#/.test(String(sid))) { el.textContent = String(sid); return; }
|
||||
try {
|
||||
const r = await fetchJd(sid);
|
||||
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 } 用于生成可读结论与命中依据;
|
||||
* keyword 命中词在 JD 原文中以 <mark> 高亮 */
|
||||
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");
|
||||
let idx = 0;
|
||||
const conclusion = ctx.skill
|
||||
@@ -2151,41 +2153,93 @@ $("#banner-config").onclick = () => openConfigModal(false);
|
||||
$("#embed-banner-config").onclick = () => openConfigModal(false);
|
||||
$("#demo-badge").onclick = () => openConfigModal(false);
|
||||
|
||||
/* ================= 演示导览(6 步评委导览,右上角浮动步进条) ================= */
|
||||
/* ================= 全流程自动演示导览(一键播放,录屏/评委演示用) =================
|
||||
* 剧本聚焦赛题主线:多源采集 → 新岗位发现 → AI 生成定义 → 人工审核入图 →
|
||||
* 既有岗位演化 → 图谱全景四视角 → 动态时序生长 → 人岗匹配 → 学习路径。
|
||||
* 全程只读演示:不真实点击批准/退回,不改任何数据。 */
|
||||
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 = [
|
||||
{ title: "数据驾驶舱:多源数据总览",
|
||||
desc: "JD 规模、岗位/技能/事件总量、演化趋势、清洗与审核状态",
|
||||
{ dur: 6000,
|
||||
title: "多源数据汇聚与治理",
|
||||
desc: "多平台真实招聘语料经清洗、结构化抽取、交叉验证后进入图谱管线",
|
||||
go: () => switchView("dashboard") },
|
||||
{ title: "新岗位候选:发现 → 人工审核 → 入图",
|
||||
desc: "候选岗位不直接入图:四指标证据 + 人工确认,可回查证据 JD",
|
||||
{ dur: 8000,
|
||||
title: "新岗位发现:四指标聚类",
|
||||
desc: "JD≥15 条 · 企业≥5 家 · 环比增长≥50% · 差异度≥0.4;候选以虚线节点呈现,绝不直接入图",
|
||||
go: () => {
|
||||
switchView("graph");
|
||||
const locate = () => {
|
||||
const has = CN.graph && CN.graph.has && CN.graph.has(DEMO_CANDIDATE);
|
||||
if (CN.gotoNode) CN.gotoNode(has ? DEMO_CANDIDATE : DEMO_JOB);
|
||||
const id = (CN.graph && CN.graph.has && CN.graph.has(DEMO_CAND)) ? DEMO_CAND : DEMO_JOB;
|
||||
if (CN.gotoNode) CN.gotoNode(id);
|
||||
};
|
||||
if (CN.graph && CN.graph.ready) CN.graph.ready().then(locate);
|
||||
else if (CN.gotoNode) CN.gotoNode(DEMO_CANDIDATE);
|
||||
} },
|
||||
{ title: "岗位局部图谱:Java 开发工程师",
|
||||
desc: "技能点级局部图谱:必备/加分技能、支撑 JD 数与可读证据",
|
||||
go: () => {
|
||||
{ dur: 9000,
|
||||
title: "大模型生成结构化岗位定义",
|
||||
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");
|
||||
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);
|
||||
} },
|
||||
{ title: "岗位演化报告:能力动态更新",
|
||||
desc: "本期结论、新增/增强/减弱/退出、时点对比,每条可回查证据",
|
||||
go: () => {
|
||||
{ dur: 8500,
|
||||
title: "既有岗位能力动态更新",
|
||||
desc: "本期结论 + 四色 Diff:新增 / 增强 / 减弱 / 退出,每条结论可回查支撑 JD 证据链",
|
||||
go: async (alive) => {
|
||||
switchView("evolution");
|
||||
$("#evo-job-select").value = DEMO_JOB;
|
||||
$("#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: "人岗诊断:示例简历自动跑通",
|
||||
desc: "载入示例简历 → 解析确认 → 匹配结论 / 核心缺口 / 可迁移能力",
|
||||
go: async () => {
|
||||
{ dur: 12000,
|
||||
title: "全景图谱:四种视角看岗位生态",
|
||||
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");
|
||||
$$("#view-match .match-tab").forEach((x) => x.classList.toggle("active", x.dataset.mtab === "single"));
|
||||
$("#match-single").classList.remove("hidden");
|
||||
@@ -2195,39 +2249,88 @@ const DEMO_STEPS = [
|
||||
$("#btn-load-sample").click();
|
||||
try {
|
||||
const p = await post("/api/resume/parse", { text: $("#resume-text").value });
|
||||
if (!alive()) return;
|
||||
applyParsedProfile(p, "演示示例简历");
|
||||
$("#btn-run-match").click();
|
||||
} catch (e) { toast(friendlyErr(e), "error"); }
|
||||
} },
|
||||
{ title: "学习路径:可执行的三阶段路线",
|
||||
{ dur: 7500,
|
||||
title: "学习路径:可执行的三阶段路线",
|
||||
desc: "自动带入岗位与技能缺口:基础补齐 → 岗位核心 → 项目验证",
|
||||
go: () => {
|
||||
switchView("path");
|
||||
$("#path-job-select").value = DEMO_JOB;
|
||||
loadPathJob(DEMO_JOB, matchState.skills).then(() => $("#btn-path").click());
|
||||
} },
|
||||
{ dur: 5500,
|
||||
title: "全流程闭环",
|
||||
desc: "采集 → 发现 → 定义 → 审核 → 演化 → 图谱 → 匹配 → 路径,全链路可追溯、可验证",
|
||||
go: () => switchView("dashboard") },
|
||||
];
|
||||
|
||||
let tourIdx = -1;
|
||||
function tourGo(i) {
|
||||
tourIdx = Math.max(0, Math.min(DEMO_STEPS.length - 1, i));
|
||||
let tourToken = 0; // 场景令牌:切镜/退出时作废旧镜头的延时动作与计时器
|
||||
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];
|
||||
$("#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-desc").textContent = step.desc;
|
||||
$("#tour-prev").disabled = tourIdx === 0;
|
||||
$("#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() {
|
||||
tourIdx = -1;
|
||||
tourToken++;
|
||||
tourAuto = false;
|
||||
$("#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);
|
||||
$("#tour-next").onclick = () => (tourIdx === DEMO_STEPS.length - 1 ? endTour() : tourGo(tourIdx + 1));
|
||||
|
||||
$("#btn-demo-tour").onclick = () => tourGo(0, true); // 一键自动播放全流程
|
||||
$("#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;
|
||||
document.addEventListener("keydown", (e) => { if (e.key === "Escape" && tourIdx >= 0) endTour(); });
|
||||
|
||||
function embedStatusHtml(emb) {
|
||||
if (emb.ready) {
|
||||
|
||||
@@ -2043,6 +2043,8 @@
|
||||
/* 数据装载完成 Promise(演示导览等需要等图谱就绪后定位) */
|
||||
ready() { return this._p || Promise.resolve(); },
|
||||
has(id) { return id in G.idx; },
|
||||
/* 清空选中(演示巡览等需要全图无聚焦的场景) */
|
||||
deselect() { selectNode(null, false); },
|
||||
/* 进入指定岗位的局部子图(演示导览第 3 步):同心分层布局 */
|
||||
solo(id) {
|
||||
if (!(id in G.idx)) return;
|
||||
|
||||
+8
-6
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CareerNexus 职联 · 岗位与能力图谱平台</title>
|
||||
<link rel="stylesheet" href="/static/style.css?v=20260828-2">
|
||||
<link rel="stylesheet" href="/static/style.css?v=20260831">
|
||||
</head>
|
||||
<body>
|
||||
<!-- ============ 顶部栏 56px ============ -->
|
||||
@@ -692,19 +692,21 @@
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<!-- 评委演示导览浮动条(右上角固定) -->
|
||||
<!-- 全流程自动演示字幕条(底部居中影院风,一键播放,录屏友好) -->
|
||||
<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">
|
||||
<b id="tour-title"></b>
|
||||
<span class="tour-desc" id="tour-desc"></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-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>
|
||||
<i class="tour-track"><i id="tour-fill"></i></i>
|
||||
</div>
|
||||
|
||||
<script src="/static/app.js?v=20260827"></script>
|
||||
<script src="/static/graph.js?v=20260828-2"></script>
|
||||
<script src="/static/app.js?v=20260831"></script>
|
||||
<script src="/static/graph.js?v=20260831"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+23
-8
@@ -1110,21 +1110,36 @@ svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width:
|
||||
|
||||
/* ============ 2026-08 答辩级改造新增样式 ============ */
|
||||
|
||||
/* 评委演示导览浮动条(右上角固定) */
|
||||
/* 全流程自动演示字幕条(底部居中,深色影院风,录屏友好) */
|
||||
.tour-bar {
|
||||
position: fixed; top: calc(var(--topbar-h) + 10px); right: 16px; z-index: 900;
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px;
|
||||
box-shadow: var(--shadow-pop); padding: 8px 12px; max-width: 640px;
|
||||
position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%); z-index: 900;
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
background: rgba(29,33,41,0.9); border: none; border-radius: 14px;
|
||||
box-shadow: var(--shadow-pop); padding: 12px 18px 20px; max-width: 880px; width: max-content;
|
||||
backdrop-filter: blur(8px); color: #fff;
|
||||
}
|
||||
.tour-prog {
|
||||
font-size: 12px; font-weight: 600; color: #fff; background: var(--primary);
|
||||
border-radius: 999px; padding: 3px 10px; white-space: nowrap;
|
||||
}
|
||||
.tour-text { display: flex; flex-direction: column; min-width: 0; }
|
||||
.tour-text b { font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.tour-desc { font-size: 11px; color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.tour-text { display: flex; flex-direction: column; min-width: 0; max-width: 540px; }
|
||||
.tour-text b { font-size: 15px; color: #fff; }
|
||||
.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-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; }
|
||||
|
||||
Reference in New Issue
Block a user