前端体验升级专项:驾驶舱T型Hero+趋势折线+播报轮播、图谱骨架屏与右键菜单、双雷达图、主色#1890FF统一、空状态补齐、移除调试探针
This commit is contained in:
+84
-12
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user