竞赛演示模式:首屏降噪与预置导览
- demo_mode 配置项(默认开):不弹初始设置,LLM/语义警告横幅降级为顶栏状态点 - 顶栏新增'开始演示'导览(驾驶舱→新岗位发现→Java演化→匹配诊断四步预置路线) - 图谱默认降噪:场景/行业图层默认关、关系标签默认关(悬停/缩放显示) - 演化页默认锁定 Java 强样例;数据版本徽标标注'(模拟)';联系方式移入设置页 - start.bat/start.sh 启动脚本
This commit is contained in:
+79
-10
@@ -255,7 +255,7 @@ async function loadDashboard() {
|
||||
CN.graphData = graph;
|
||||
const s = health.graph || {};
|
||||
updateBanners(health);
|
||||
$("#data-version").textContent = `数据版本 v${(graph.events.at(-1) || {}).window || "demo"} · ${s.nodes ?? 0} 节点`;
|
||||
$("#data-version").textContent = `数据更新至 ${(graph.events.at(-1) || {}).window || "2026Q3"}(模拟)`;
|
||||
|
||||
/* 指标卡 */
|
||||
const series = quarterlyCounts(graph.events || []);
|
||||
@@ -1421,21 +1421,31 @@ $("#btn-export-path").onclick = () => {
|
||||
toast("已导出文本", "success");
|
||||
};
|
||||
|
||||
/* ================= 系统设置 / 状态横幅 ================= */
|
||||
let lastHealth = null; // 最近一次 health 响应(横幅状态源)
|
||||
/* ================= 系统设置 / 状态横幅 / 演示模式 ================= */
|
||||
let lastHealth = null; // 最近一次 health 响应(横幅与状态徽标的数据源)
|
||||
function updateBanners(health) {
|
||||
if (health) lastHealth = health;
|
||||
const h = lastHealth || {};
|
||||
const showOffline = !!h.offline_fallback
|
||||
&& sessionStorage.getItem("cn_banner_dismissed") !== "1";
|
||||
const demo = h.demo_mode !== false;
|
||||
const embed = h.embed || {};
|
||||
const showEmbed = !embed.ready
|
||||
const degraded = !!h.offline_fallback || !embed.ready;
|
||||
// 演示模式:警告横幅降级为顶栏状态徽标(首屏不出现负面字眼)
|
||||
const showOffline = !demo && !!h.offline_fallback
|
||||
&& sessionStorage.getItem("cn_banner_dismissed") !== "1";
|
||||
const showEmbed = !demo && !embed.ready
|
||||
&& sessionStorage.getItem("cn_banner2_dismissed") !== "1";
|
||||
$("#offline-banner").classList.toggle("hidden", !showOffline);
|
||||
$("#embed-banner").classList.toggle("hidden", !showEmbed);
|
||||
const n = (showOffline ? 1 : 0) + (showEmbed ? 1 : 0);
|
||||
document.body.classList.toggle("has-banner", n === 1);
|
||||
document.body.classList.toggle("has-banner2", n === 2);
|
||||
// 顶栏状态徽标:绿=全就绪,黄=离线回退/语义未就绪
|
||||
const badge = $("#demo-badge");
|
||||
badge.classList.toggle("warn", degraded);
|
||||
$("#demo-badge-text").textContent = degraded ? "离线演示" : (demo ? "演示模式" : "在线");
|
||||
badge.title = degraded
|
||||
? "离线回退中(LLM/语义模型未配置),点击查看设置"
|
||||
: "全部通道就绪,点击查看设置";
|
||||
}
|
||||
function dismissBanner(id, key) {
|
||||
sessionStorage.setItem(key, "1");
|
||||
@@ -1445,6 +1455,44 @@ $("#banner-close").onclick = () => dismissBanner("offline-banner", "cn_banner_di
|
||||
$("#embed-banner-close").onclick = () => dismissBanner("embed-banner", "cn_banner2_dismissed");
|
||||
$("#banner-config").onclick = () => openConfigModal(false);
|
||||
$("#embed-banner-config").onclick = () => openConfigModal(false);
|
||||
$("#demo-badge").onclick = () => openConfigModal(false);
|
||||
|
||||
/* ================= 演示导览(预置路线,一键串起完整闭环) ================= */
|
||||
const DEMO_STEPS = [
|
||||
{ title: "① 数据驾驶舱:多源数据总览", desc: "JD 规模、岗位/技能/事件总量、演化趋势、清洗与审核状态",
|
||||
go: () => switchView("dashboard") },
|
||||
{ title: "② 新岗位发现:候选→审核→入图", desc: "图谱定位「大模型应用工程师」:四指标证据、人工确认、证据 JD 回查",
|
||||
go: () => { switchView("graph"); if (CN.gotoNode) CN.gotoNode("job_大模型应用工程师"); } },
|
||||
{ title: "③ 既有岗位演化:Java 编年史", desc: "时点对比、变更堆叠图、生命周期甘特、事件证据链",
|
||||
go: () => {
|
||||
switchView("evolution");
|
||||
$("#evo-job-select").value = "job_Java开发工程师";
|
||||
$("#evo-job-select").dispatchEvent(new Event("change"));
|
||||
} },
|
||||
{ title: "④ 人岗匹配诊断 + 学习路径", desc: "载入示例简历 → 确认解析 → 总分/四维/差距清单 → 一键生成学习路径",
|
||||
go: () => {
|
||||
switchView("match");
|
||||
$$(".match-tab").forEach((x) => x.classList.toggle("active", x.dataset.mtab === "single"));
|
||||
$("#match-single").classList.remove("hidden");
|
||||
$("#match-batch").classList.add("hidden");
|
||||
$("#btn-load-sample").click();
|
||||
} },
|
||||
];
|
||||
$("#btn-demo-tour").onclick = () => {
|
||||
openModal("演示导览(预置路线)", `
|
||||
<div class="tour-intro">按预置路线依次演示完整闭环,每步点击「前往」自动跳转到位:</div>
|
||||
${DEMO_STEPS.map((s, i) => `
|
||||
<div class="tour-step">
|
||||
<div class="tour-step-body">
|
||||
<div class="tour-step-title">${esc(s.title)}</div>
|
||||
<div class="tour-step-desc">${esc(s.desc)}</div>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm" data-step="${i}">前往</button>
|
||||
</div>`).join("")}`);
|
||||
$$("#modal-body [data-step]").forEach((b) => {
|
||||
b.onclick = () => { closeModal(); DEMO_STEPS[+b.dataset.step].go(); };
|
||||
});
|
||||
};
|
||||
|
||||
function embedStatusHtml(emb) {
|
||||
if (emb.ready) {
|
||||
@@ -1506,6 +1554,13 @@ async function openConfigModal(needToken, firstRun) {
|
||||
<button class="btn btn-secondary btn-sm" id="cfg-token-show">显示</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label class="form-label">演示模式<span class="form-label-tip">开启后不弹初始设置、警告降级为顶栏状态点</span></label>
|
||||
<div class="seg" id="cfg-demo">
|
||||
<button class="seg-item ${cfg.demo_mode !== false ? "active" : ""}" data-demo="1">开启(评审演示)</button>
|
||||
<button class="seg-item ${cfg.demo_mode === false ? "active" : ""}" data-demo="0">关闭(开发调试)</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cfg-sec">
|
||||
<div class="cfg-sec-title">LLM API(星火 / OpenAI 兼容,保存即热生效)</div>
|
||||
@@ -1584,7 +1639,8 @@ async function openConfigModal(needToken, firstRun) {
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>`,
|
||||
</div>
|
||||
<div class="cfg-foot-note">如遇问题,请联系 support@rrrosmontis.icu</div>`,
|
||||
`${firstRun ? `<button class="btn btn-secondary" id="cfg-skip">跳过,使用离线演示模式</button>` : ""}
|
||||
<button class="btn btn-secondary" id="cfg-save-restart">保存并重启</button>
|
||||
<button class="btn btn-primary" id="cfg-save">保存设置</button>`);
|
||||
@@ -1598,6 +1654,14 @@ async function openConfigModal(needToken, firstRun) {
|
||||
};
|
||||
});
|
||||
let embedProvider = cfg.embed.provider === "api" ? "api" : "local";
|
||||
let demoMode = cfg.demo_mode !== false;
|
||||
$$("#cfg-demo .seg-item").forEach((b) => {
|
||||
b.onclick = () => {
|
||||
$$("#cfg-demo .seg-item").forEach((x) => x.classList.remove("active"));
|
||||
b.classList.add("active");
|
||||
demoMode = b.dataset.demo === "1";
|
||||
};
|
||||
});
|
||||
$$("#cfg-embed-provider .seg-item").forEach((b) => {
|
||||
b.onclick = () => {
|
||||
$$("#cfg-embed-provider .seg-item").forEach((x) => x.classList.remove("active"));
|
||||
@@ -1629,6 +1693,7 @@ async function openConfigModal(needToken, firstRun) {
|
||||
embed_model_path: $("#cfg-embed-path").value.trim(),
|
||||
embed_api_base_url: $("#cfg-embed-base").value.trim(),
|
||||
embed_api_model: $("#cfg-embed-model").value.trim(),
|
||||
demo_mode: demoMode,
|
||||
guided: true, // 保存即视为完成初始设置
|
||||
};
|
||||
const tok = $("#cfg-token").value;
|
||||
@@ -1643,7 +1708,7 @@ async function openConfigModal(needToken, firstRun) {
|
||||
else localStorage.removeItem("cn_access_token");
|
||||
}
|
||||
lastHealth = Object.assign({}, lastHealth, {
|
||||
offline_fallback: saved.offline_fallback, embed: saved.embed,
|
||||
offline_fallback: saved.offline_fallback, embed: saved.embed, demo_mode: saved.demo_mode,
|
||||
});
|
||||
updateBanners();
|
||||
return saved;
|
||||
@@ -1716,6 +1781,8 @@ window.addEventListener("resize", () => {
|
||||
CN.jobs = await api("/api/jobs");
|
||||
const opts = CN.jobs.map((j) => `<option value="${esc(j.job_id)}">${esc(j.title)}</option>`).join("");
|
||||
["#evo-job-select", "#match-job-select", "#path-job-select", "#batch-job-select"].forEach((s) => { $(s).innerHTML = opts; });
|
||||
$("#evo-job-select").value = "job_Java开发工程师"; // 演化页默认锁定最强样例
|
||||
$("#match-job-select").value = "job_Java开发工程师";
|
||||
await loadDashboard(); // 先加载图谱缓存(演化页甘特图依赖)
|
||||
await loadEvolution();
|
||||
renderEvoHeatmap();
|
||||
@@ -1723,8 +1790,10 @@ window.addEventListener("resize", () => {
|
||||
const hash = new URLSearchParams(location.hash.slice(1));
|
||||
if (hash.get("view")) switchView(hash.get("view"));
|
||||
if (hash.get("node") && CN.gotoNode) CN.gotoNode(hash.get("node"));
|
||||
/* 首次使用:引导完成基础设置(完成标记记录在服务端 config.json) */
|
||||
if (!lastHealth || !lastHealth.guided) openConfigModal(false, true);
|
||||
/* 首次使用:演示模式下不弹初始设置(评审口径),非演示模式才引导 */
|
||||
if ((!lastHealth || !lastHealth.guided) && lastHealth && lastHealth.demo_mode === false) {
|
||||
openConfigModal(false, true);
|
||||
}
|
||||
} catch (e) {
|
||||
toast("后端连接失败:" + e.message, "error");
|
||||
}
|
||||
|
||||
+4
-4
@@ -10,8 +10,8 @@
|
||||
const G = {
|
||||
nodes: [], edges: [], events: [], idx: {},
|
||||
cam: { x: 0, y: 0, k: 1 }, // 相机:世界 -> 屏幕 screen = (world - cam.pan) * k ... 使用 cam.x/y 为平移(屏幕像素)
|
||||
showLabels: true,
|
||||
filters: { types: { Job: true, Skill: true, Industry: true, Scenario: true }, category: "", level: "",
|
||||
showLabels: false, // 默认关闭:悬停/选中/缩放时才显示
|
||||
filters: { types: { Job: true, Skill: true, Industry: false, Scenario: false }, category: "", level: "",
|
||||
edges: { required: true, preferred: true, applies: true, belongs: true } },
|
||||
selected: null, // 选中节点 id
|
||||
soloJob: null, // 岗位子图模式:只看该岗位及其一跳邻居
|
||||
@@ -444,7 +444,7 @@
|
||||
$("#fp-category").onchange = (e) => { G.filters.category = e.target.value; requestDraw(); drawMinimap(); };
|
||||
$("#fp-level").onchange = (e) => { G.filters.level = e.target.value; requestDraw(); drawMinimap(); };
|
||||
$("#fp-reset").onclick = () => {
|
||||
G.filters = { types: { Job: true, Skill: true, Industry: true, Scenario: true }, category: "", level: "",
|
||||
G.filters = { types: { Job: true, Skill: true, Industry: false, Scenario: false }, category: "", level: "",
|
||||
edges: { required: true, preferred: true, applies: true, belongs: true } };
|
||||
$$("#graph-filter-panel input[data-ftype]").forEach((cb) => { cb.checked = true; });
|
||||
$$("#graph-filter-panel input[data-etype]").forEach((cb) => { cb.checked = true; });
|
||||
@@ -487,7 +487,7 @@
|
||||
if (!v) return false;
|
||||
G.cam = v.cam;
|
||||
G.filters = Object.assign(
|
||||
{ types: { Job: true, Skill: true, Industry: true, Scenario: true }, category: "", level: "",
|
||||
{ types: { Job: true, Skill: true, Industry: false, Scenario: false }, category: "", level: "",
|
||||
edges: { required: true, preferred: true, applies: true, belongs: true } },
|
||||
v.filters);
|
||||
G.filters.edges = Object.assign({ required: true, preferred: true, applies: true, belongs: true },
|
||||
|
||||
+521
-517
File diff suppressed because it is too large
Load Diff
@@ -924,3 +924,26 @@ svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width:
|
||||
width: 18px; height: 18px; border-radius: 50%; cursor: pointer; line-height: 1;
|
||||
}
|
||||
.solo-badge button:hover { background: rgba(255,255,255,.45); }
|
||||
|
||||
/* 演示模式状态徽标 / 演示导览 */
|
||||
.demo-badge {
|
||||
display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
|
||||
font-size: 12px; padding: 3px 10px; border-radius: 999px;
|
||||
background: var(--success-light); color: var(--success); border: 1px solid #b7eb8f;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.demo-badge .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--success); }
|
||||
.demo-badge.warn { background: var(--warning-light); color: #ad4e00; border-color: #ffd591; }
|
||||
.demo-badge.warn .dot { background: var(--warning); }
|
||||
#btn-demo-tour svg { width: 13px; height: 13px; fill: currentColor; }
|
||||
.tour-intro { font-size: 13px; color: var(--text-2); margin-bottom: 12px; }
|
||||
.tour-step {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
border: 1px solid var(--border); border-radius: var(--radius);
|
||||
padding: 12px 14px; margin-bottom: 10px;
|
||||
}
|
||||
.tour-step:hover { border-color: var(--primary); }
|
||||
.tour-step-body { flex: 1; }
|
||||
.tour-step-title { font-size: 13px; font-weight: 600; color: var(--text-1); margin-bottom: 2px; }
|
||||
.tour-step-desc { font-size: 12px; color: var(--text-3); }
|
||||
.cfg-foot-note { text-align: center; font-size: 12px; color: var(--text-3); padding: 4px 0 2px; }
|
||||
|
||||
Reference in New Issue
Block a user