前端体验升级专项:驾驶舱T型Hero+趋势折线+播报轮播、图谱骨架屏与右键菜单、双雷达图、主色#1890FF统一、空状态补齐、移除调试探针

This commit is contained in:
RoyirinRosmontis
2026-08-27 16:13:43 +08:00
parent aa4a88c10b
commit 3058fc8aca
5 changed files with 631 additions and 175 deletions
+84 -12
View File
@@ -458,7 +458,7 @@
ctx.save();
LEVEL_BANDS.forEach((b, i) => {
const sy = b.y * G.cam.k + G.cam.y;
ctx.fillStyle = i % 2 ? "rgba(47,84,235,0.025)" : "rgba(19,194,194,0.03)";
ctx.fillStyle = i % 2 ? "rgba(24,144,255,0.025)" : "rgba(19,194,194,0.03)";
ctx.fillRect(0, sy - 175 * G.cam.k, c.clientWidth, 350 * G.cam.k);
ctx.fillStyle = "rgba(78,89,105,0.85)";
ctx.font = "bold 13px sans-serif"; ctx.textAlign = "left";
@@ -481,10 +481,10 @@
const label = `${a.cat}(${n})`;
ctx.font = "bold 13px sans-serif";
const tw = ctx.measureText(label).width;
ctx.fillStyle = "rgba(47,84,235,0.08)";
ctx.strokeStyle = "rgba(47,84,235,0.35)";
ctx.fillStyle = "rgba(24,144,255,0.08)";
ctx.strokeStyle = "rgba(24,144,255,0.35)";
ctx.beginPath(); ctx.roundRect(p.x - tw / 2 - 10, p.y - 14, tw + 20, 24, 12); ctx.fill(); ctx.stroke();
ctx.fillStyle = "#2f54eb"; ctx.textAlign = "center";
ctx.fillStyle = "#1890ff"; ctx.textAlign = "center";
ctx.fillText(label, p.x, p.y + 3);
});
ctx.restore();
@@ -505,7 +505,7 @@
ctx.setLineDash([]);
ctx.lineWidth = isHot ? 1.4 : 0.8;
} else if (isReq) {
ctx.strokeStyle = dim ? "rgba(78,89,105,0.05)" : isHot ? "rgba(47,84,235,0.75)" : "rgba(78,89,105,0.28)";
ctx.strokeStyle = dim ? "rgba(78,89,105,0.05)" : isHot ? "rgba(24,144,255,0.75)" : "rgba(78,89,105,0.28)";
ctx.setLineDash([]);
ctx.lineWidth = isHot ? 1.8 : 1;
} else {
@@ -534,7 +534,7 @@
/* 关系标签(颜色 + 文字双重编码) */
if (G.showLabels && e.rel === "REQUIRES" && G.cam.k > 0.55 && !dim) {
ctx.font = "10px sans-serif"; ctx.textAlign = "center";
ctx.fillStyle = isReq ? "rgba(47,84,235,0.85)" : "rgba(9,151,156,0.9)";
ctx.fillStyle = isReq ? "rgba(24,144,255,0.85)" : "rgba(9,151,156,0.9)";
ctx.fillText(isReq ? "必备" : "加分", (pa.x + pb.x) / 2, (pa.y + pb.y) / 2 - 3);
}
ctx.restore();
@@ -550,7 +550,7 @@
const dim = sel && !isSel && !isHop;
const meta = TYPE_META[n.type] || { color: "#86909c" };
ctx.save();
ctx.globalAlpha = dim ? 0.15 : 1;
ctx.globalAlpha = dim ? 0.2 : 1;
ctx.fillStyle = meta.color;
if (n.type === "Skill") {
const s = r * 1.7;
@@ -568,7 +568,7 @@
ctx.strokeStyle = "#1d2129"; ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(p.x, p.y, r + 4, 0, 7); ctx.stroke();
} else if (isHop) {
ctx.strokeStyle = "rgba(47,84,235,0.55)"; ctx.lineWidth = 1.5;
ctx.strokeStyle = "rgba(24,144,255,0.55)"; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.arc(p.x, p.y, r + 2.5, 0, 7); ctx.stroke();
}
/* NEW 标记:待审核候选与近一年入库新岗位,节点右上方脉动圆点 + NEW 文字(canvas 绘制,alpha 呼吸复用 requestDraw 循环) */
@@ -582,7 +582,7 @@
ctx.beginPath(); ctx.arc(bx, by, 4, 0, 7); ctx.stroke();
ctx.font = "bold 9px sans-serif"; ctx.textAlign = "left";
ctx.fillText("NEW", bx + 6, by + 3);
ctx.globalAlpha = dim ? 0.15 : 1;
ctx.globalAlpha = dim ? 0.2 : 1;
G._newPulse = true; // 有 NEW 节点在画面上:持续请求重绘形成呼吸
}
/* 标签按需显示(文字重叠治理):岗位常显;技能/场景/行业仅在选中/悬停/选中节点一跳邻居时显示;
@@ -666,7 +666,7 @@
/* 视口框 */
const c = canvas();
const tl = toWorld(0, 0), br = toWorld(c.clientWidth, c.clientHeight);
ctx.strokeStyle = "#2f54eb"; ctx.lineWidth = 1.4;
ctx.strokeStyle = "#1890ff"; ctx.lineWidth = 1.4;
ctx.strokeRect(tl.x * k + ox, tl.y * k + oy, (br.x - tl.x) * k, (br.y - tl.y) * k);
mm._proj = { k, ox, oy };
}
@@ -736,6 +736,15 @@
const node = pickNode(e.clientX - rect.left, e.clientY - rect.top);
selectNode(node ? node.id : null);
});
/* 节点右键菜单:选中节点 + 弹出自定义菜单(查看证据 / 时点对比 / 人岗诊断 / 子图开关) */
c.addEventListener("contextmenu", (e) => {
e.preventDefault();
const rect = c.getBoundingClientRect();
const node = pickNode(e.clientX - rect.left, e.clientY - rect.top);
if (!node) { hideCtxMenu(); return; }
if (G.selected !== node.id) selectNode(node.id);
showCtxMenu(node, e.clientX, e.clientY);
});
c.addEventListener("wheel", (e) => {
e.preventDefault();
const rect = c.getBoundingClientRect();
@@ -744,8 +753,71 @@
window.addEventListener("resize", () => { requestDraw(); drawMinimap(); });
}
/* ================= 节点右键菜单(CSS 定位 DOM,点空白 / Esc 关闭) ================= */
function hideCtxMenu() {
const m = $("#ctx-menu");
if (m) m.classList.add("hidden");
}
function showCtxMenu(n, clientX, clientY) {
const menu = $("#ctx-menu");
const wrap = $("#graph-canvas-wrap");
const wr = wrap.getBoundingClientRect();
const isJob = n.type === "Job" && !(n.pending || n.status === "pending");
const items = [
{ key: "evidence", label: "查看证据 JD 原文" },
{ key: "compare", label: "能力画像时点对比", disabled: !isJob },
{ key: "match", label: "开始人岗诊断", disabled: !isJob },
{ key: "solo", label: G.soloJob === n.id ? "退出岗位子图" : "岗位子图开关", disabled: !isJob },
];
menu.innerHTML = `<div class="ctx-title">${esc(n.title || n.id)}</div>` + items.map((it) =>
`<button class="ctx-item" data-key="${it.key}"${it.disabled ? " disabled" : ""}>${it.label}</button>`).join("");
menu.classList.remove("hidden");
/* 先渲染量尺寸,再定位并防止溢出画布容器 */
let x = clientX - wr.left, y = clientY - wr.top;
x = Math.max(4, Math.min(x, wr.width - menu.offsetWidth - 4));
y = Math.max(4, Math.min(y, wr.height - menu.offsetHeight - 4));
menu.style.left = `${x}px`;
menu.style.top = `${y}px`;
menu.querySelectorAll(".ctx-item:not(:disabled)").forEach((b) => {
b.onclick = () => { hideCtxMenu(); ctxAction(b.dataset.key, n); };
});
}
function ctxAction(key, n) {
if (key === "evidence") {
const sources = new Set();
G.edges.forEach((e) => {
if (e.source === n.id || e.target === n.id) (e.sources || []).forEach((s) => sources.add(s));
});
const list = [...sources];
if (!list.length) return toast("该节点暂无来源证据记录", "info");
CN.showJdEvidence(list, { jobTitle: n.title || n.id });
} else if (key === "compare") {
CN.switchView("evolution");
$("#evo-job-select").value = n.id;
$("#evo-job-select").dispatchEvent(new Event("change"));
setTimeout(() => { const d = $("#evo-diff"); if (d) d.scrollIntoView({ block: "center" }); }, 450);
} else if (key === "match") {
CN.switchView("match");
$("#match-job-select").value = n.id;
$("#match-job-select").dispatchEvent(new Event("change"));
toast("已带入目标岗位,请上传或载入示例简历", "success");
} else if (key === "solo") {
G.soloJob = G.soloJob === n.id ? null : n.id;
G._defaultApplied = true;
updateSoloBadge();
updateRecentChange();
if (G.soloJob) {
applySoloLayout();
fitSoloView();
toast(`已进入「${n.title || n.id}」岗位子图(同心分层:中心岗位 → 必备 → 加分 → 场景/行业)`, "success");
} else fitView();
}
}
/* ================= 工具条 / 筛选 ================= */
function bindToolbar() {
document.addEventListener("click", (e) => { if (!e.target.closest("#ctx-menu")) hideCtxMenu(); });
document.addEventListener("keydown", (e) => { if (e.key === "Escape") hideCtxMenu(); });
$("#tool-zoom-in").onclick = () => zoomAt(1.25);
$("#tool-zoom-out").onclick = () => zoomAt(0.8);
$("#tool-fit").onclick = fitView;
@@ -903,7 +975,7 @@
sub: `原因:聚类发现 ${c.source_count} 条 JD / ${c.company_count} 家企业指向未收录岗位 · 证据 ${c.source_count} 条`,
}));
G.events.slice(-4).reverse().forEach((e) => items.push({
color: "#2f54eb", kind: "event", id: `${e.job_id}|${e.skill}|${e.window}`, nodeId: e.job_id,
color: "#1890ff", kind: "event", id: `${e.job_id}|${e.skill}|${e.window}`, nodeId: e.job_id,
text: `「${esc(e.job_id.replace(/^job_/, ""))}」${esc(e.change_type)}技能 ${esc(e.skill)}`,
sub: `证据:窗口 ${esc(e.window)} · 需求指数 ${e.prev_weight.toFixed(2)}→${e.curr_weight.toFixed(2)} · ${(e.source_ids || []).length} 条 JD`,
}));
@@ -1328,7 +1400,7 @@
values.forEach((v, i) => {
const bw = c.width / values.length;
const h = (v / max) * (c.height - 16);
ctx.fillStyle = "#2f54eb";
ctx.fillStyle = "#1890ff";
ctx.fillRect(i * bw + 2, c.height - h - 12, bw - 4, h);
if (i % 3 === 0) {
ctx.fillStyle = "#86909c"; ctx.font = "9px sans-serif"; ctx.textAlign = "center";