图谱可视化对标业内实践:枢纽技能按连接度大小编码、时序Timebar生长曲线柱图(点击跳转)、技术栈类别hull气泡轮廓、画布全屏、边悬停tooltip与点击直达JD证据、双击渐进披露(岗位子图/技能反查聚焦),修复gt-bar类名冲突,前端单测87例全过
This commit is contained in:
+224
-39
@@ -15,6 +15,9 @@
|
||||
edges: { required: true, preferred: true, applies: true, belongs: true } },
|
||||
selected: null, // 选中节点 id
|
||||
soloJob: null, // 岗位子图模式:只看该岗位及其一跳邻居
|
||||
focusSkill: null, // 技能聚焦模式:只看该技能及引用它的岗位(双击技能进入)
|
||||
hoverEdge: null, // 鼠标悬停的边(tooltip + 高亮)
|
||||
deg: {}, // 节点连接度缓存(枢纽节点大小编码)
|
||||
viewMode: "overview", // 视图:overview 全景 / tech 技术栈 / level 职级演进 / timeline 动态时序
|
||||
levelJob: null, // 职级演进视图:当前展示能力树的岗位
|
||||
compare: null, compareSets: null, // 岗位对比模式:{a,b} 与集合缓存
|
||||
@@ -209,8 +212,8 @@
|
||||
if (G.levelJob) selectNode(G.levelJob, false); // 右侧面板同步展示岗位定义,信息闭环
|
||||
}
|
||||
|
||||
/* 视野适配职级能力树(只按可见节点的包围盒) */
|
||||
function fitLevelView() {
|
||||
/* 视野适配当前可见节点(职级能力树 / 技能聚焦等局部视图通用) */
|
||||
function fitVisibleView(padX = 280, padY = 240, kMax = 1.4) {
|
||||
const c = canvas();
|
||||
let minX = 1e9, minY = 1e9, maxX = -1e9, maxY = -1e9, cnt = 0;
|
||||
G.nodes.forEach((n) => {
|
||||
@@ -222,11 +225,72 @@
|
||||
if (!cnt) return fitView();
|
||||
const w = maxX - minX || 1, h = maxY - minY || 1;
|
||||
const W = c.clientWidth, H = c.clientHeight;
|
||||
G.cam.k = Math.max(0.2, Math.min(W / (w + 280), H / (h + 240), 1.4));
|
||||
G.cam.k = Math.max(0.2, Math.min(W / (w + padX), H / (h + padY), kMax));
|
||||
G.cam.x = (W - w * G.cam.k) / 2 - minX * G.cam.k;
|
||||
G.cam.y = (H - h * G.cam.k) / 2 - minY * G.cam.k;
|
||||
requestDraw(); drawMinimap();
|
||||
}
|
||||
function fitLevelView() { fitVisibleView(280, 240, 1.4); }
|
||||
|
||||
/* ================= 双击聚焦(渐进披露):岗位子图 / 技能反查 ================= */
|
||||
function toggleSolo(n) {
|
||||
G.soloJob = G.soloJob === n.id ? null : n.id;
|
||||
G.focusSkill = null;
|
||||
G._defaultApplied = true;
|
||||
updateSoloBadge();
|
||||
updateRecentChange();
|
||||
if (G.soloJob) {
|
||||
applySoloLayout();
|
||||
fitSoloView();
|
||||
toast(`已进入「${n.title || n.id}」岗位子图(同心分层:中心岗位 → 必备 → 加分 → 场景/行业,双击空白退出)`, "success");
|
||||
} else fitView();
|
||||
}
|
||||
|
||||
/* 技能反查聚焦布局:技能居中,引用它的岗位环绕(toggle 与数据重载共用) */
|
||||
function layoutSkillFocus() {
|
||||
const sn = G.nodes[G.idx[G.focusSkill]];
|
||||
if (!sn) return;
|
||||
sn.x = 0; sn.y = 0; sn.vx = 0; sn.vy = 0;
|
||||
const js = [];
|
||||
G.edges.forEach((e) => {
|
||||
if (e.target === G.focusSkill && e.rel === "REQUIRES") {
|
||||
const s = G.nodes[G.idx[e.source]];
|
||||
if (s) js.push(s);
|
||||
}
|
||||
});
|
||||
js.sort((a, b) => String(a.title || a.id).localeCompare(String(b.title || b.id), "zh"));
|
||||
const R = Math.max(220, (js.length * 96) / (2 * Math.PI));
|
||||
js.forEach((j, i) => {
|
||||
const a = (i / Math.max(1, js.length)) * Math.PI * 2 - Math.PI / 2;
|
||||
j.x = Math.cos(a) * R; j.y = Math.sin(a) * R; j.vx = 0; j.vy = 0;
|
||||
});
|
||||
return js.length;
|
||||
}
|
||||
|
||||
/* 技能反查聚焦:技能居中,引用它的岗位环绕,回答"哪些岗位需要这项技能" */
|
||||
function toggleSkillFocus(n) {
|
||||
G.focusSkill = G.focusSkill === n.id ? null : n.id;
|
||||
G.soloJob = null;
|
||||
G._defaultApplied = true;
|
||||
updateSoloBadge();
|
||||
updateRecentChange();
|
||||
if (G.focusSkill) {
|
||||
const cnt = layoutSkillFocus() || 0;
|
||||
selectNode(n.id, false);
|
||||
fitVisibleView(260, 240, 1.5);
|
||||
toast(`已聚焦技能「${n.title || n.id}」:共 ${cnt} 个岗位引用(双击空白退出)`, "success");
|
||||
} else fitView();
|
||||
requestDraw(); drawMinimap();
|
||||
}
|
||||
|
||||
/* 退出一切聚焦状态(子图 / 技能聚焦),回到岗位生态总览 */
|
||||
function exitFocus() {
|
||||
G.soloJob = null; G.focusSkill = null;
|
||||
updateSoloBadge();
|
||||
updateRecentChange();
|
||||
fitView();
|
||||
toast("已退出聚焦,当前为岗位生态总览(全量图)", "info");
|
||||
}
|
||||
|
||||
/* 动态时序视图:窗口序列(事件窗口 ∪ 边区间起点),拖动滑块只显示该季度已存在节点与生效边 */
|
||||
function timeWindows() {
|
||||
@@ -241,6 +305,14 @@
|
||||
sl.disabled = !G.timeWins.length;
|
||||
if (G.timeIdx == null || G.timeIdx > G.timeWins.length - 1) G.timeIdx = G.timeWins.length - 1;
|
||||
sl.value = G.timeIdx;
|
||||
/* Timebar 趋势柱图:截至各季度的累计生效关系数(图谱生长曲线),与滑块下标一一对应,点击跳转 */
|
||||
const counts = G.timeWins.map((w) => G.edges.filter((e) => edgeActiveAt(e, w)).length);
|
||||
const cMax = Math.max(1, ...counts);
|
||||
$("#gt-bars").innerHTML = counts.map((v, i) =>
|
||||
`<i class="gtb-bar" data-i="${i}" style="height:${Math.max(10, (v / cMax) * 100)}%" title="${G.timeWins[i]} · 生效关系 ${v} 条"></i>`).join("");
|
||||
$$("#gt-bars .gtb-bar").forEach((el) => {
|
||||
el.onclick = () => { G.timeIdx = +el.dataset.i; sl.value = G.timeIdx; applyTimeWindow(); };
|
||||
});
|
||||
applyTimeWindow();
|
||||
}
|
||||
function applyTimeWindow() {
|
||||
@@ -252,6 +324,7 @@
|
||||
G.edges.forEach((e) => { hasEdge.add(e.source); hasEdge.add(e.target); });
|
||||
G.nodes.forEach((n) => { if (n.pending || !hasEdge.has(n.id)) G.activeNodes.add(n.id); });
|
||||
$("#gt-label").textContent = w ? `当前窗口:${w}` : "暂无时序数据";
|
||||
$$("#gt-bars .gtb-bar").forEach((el, i) => el.classList.toggle("active", i === G.timeIdx));
|
||||
renderTimePanel();
|
||||
requestDraw(); drawMinimap();
|
||||
}
|
||||
@@ -269,6 +342,7 @@
|
||||
G._overviewPos = G.nodes.map((n) => ({ x: n.x, y: n.y }));
|
||||
}
|
||||
if (mode !== "overview" && G.compare) exitCompare(true); // 对比模式只在全景视图下生效
|
||||
if (mode !== "overview" && G.focusSkill) { G.focusSkill = null; updateSoloBadge(); } // 技能聚焦只在全景下生效
|
||||
G.viewMode = mode;
|
||||
$$("#graph-view-tabs button").forEach((b) => b.classList.toggle("active", b.dataset.gview === mode));
|
||||
$("#graph-timeline").classList.toggle("hidden", mode !== "timeline");
|
||||
@@ -338,6 +412,7 @@
|
||||
});
|
||||
|
||||
buildFilterOptions();
|
||||
computeDegrees();
|
||||
layout();
|
||||
relayoutByView(); // 保持在非全景视图时重载后仍为该视图布局
|
||||
restoreView() || fitView();
|
||||
@@ -462,6 +537,25 @@
|
||||
for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0;
|
||||
return h;
|
||||
}
|
||||
/* 凸包(Andrew 单调链):技术栈视图类别气泡轮廓用(纯函数,单测可提取) */
|
||||
function convexHull(points) {
|
||||
const pts = [...points].sort((a, b) => a.x - b.x || a.y - b.y);
|
||||
if (pts.length < 3) return pts;
|
||||
const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
|
||||
const lower = [];
|
||||
for (const p of pts) {
|
||||
while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) lower.pop();
|
||||
lower.push(p);
|
||||
}
|
||||
const upper = [];
|
||||
for (let i = pts.length - 1; i >= 0; i--) {
|
||||
const p = pts[i];
|
||||
while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) upper.pop();
|
||||
upper.push(p);
|
||||
}
|
||||
lower.pop(); upper.pop();
|
||||
return lower.concat(upper);
|
||||
}
|
||||
function visible(n) {
|
||||
if (!G.filters.types[n.type]) return false;
|
||||
if (G.viewMode === "timeline" && G.activeNodes && !G.activeNodes.has(n.id)) return false;
|
||||
@@ -471,6 +565,9 @@
|
||||
if (G.soloJob) { // 岗位子图:只保留该岗位与一跳邻居
|
||||
if (n.id !== G.soloJob && !neighborsOf(G.soloJob).has(n.id)) return false;
|
||||
}
|
||||
if (G.focusSkill) { // 技能反查聚焦:只保留该技能与引用它的岗位
|
||||
if (n.id !== G.focusSkill && !neighborsOf(G.focusSkill).has(n.id)) return false;
|
||||
}
|
||||
if (n.type === "Job") {
|
||||
if (G.filters.category && n.category !== G.filters.category) return false;
|
||||
if (G.filters.level && jobLevel(n) !== G.filters.level) return false;
|
||||
@@ -501,8 +598,20 @@
|
||||
G.raf = requestAnimationFrame(() => { G.raf = null; draw(); });
|
||||
}
|
||||
|
||||
/* 节点连接度统计:枢纽技能(被大量岗位引用)在画布上按尺寸可辨 */
|
||||
function computeDegrees() {
|
||||
G.deg = {};
|
||||
G.edges.forEach((e) => {
|
||||
G.deg[e.source] = (G.deg[e.source] || 0) + 1;
|
||||
G.deg[e.target] = (G.deg[e.target] || 0) + 1;
|
||||
});
|
||||
}
|
||||
|
||||
function nodeRadius(n) {
|
||||
return n.type === "Job" ? 13 : n.type === "Scenario" ? 8 : n.type === "Industry" ? 10 : 6.5;
|
||||
const base = n.type === "Job" ? 13 : n.type === "Scenario" ? 8 : n.type === "Industry" ? 10 : 6.5;
|
||||
if (n.type !== "Skill") return base;
|
||||
const d = G.deg[n.id] || 0;
|
||||
return base + (d >= 2 ? Math.min(5.5, Math.log2(d) * 1.6) : 0); // 度数 ≥2 起对数放大,封顶接近岗位尺寸
|
||||
}
|
||||
|
||||
function draw() {
|
||||
@@ -549,6 +658,33 @@
|
||||
ctx.restore();
|
||||
} else if (G.viewMode === "tech" && G.catAnchors) {
|
||||
ctx.save();
|
||||
/* 类别气泡轮廓(hull):同类岗位凸包 + 粗圆角描边向外扩张,低饱和分色区分技术栈 */
|
||||
const HULL_FILLS = ["rgba(24,144,255,", "rgba(19,194,194,", "rgba(114,46,209,", "rgba(250,140,22,", "rgba(82,196,26,", "rgba(235,47,150,"];
|
||||
G.catAnchors.forEach((a, ci) => {
|
||||
const pts = G.nodes.filter((x) => x.type === "Job" && (x.category || "未分类") === a.cat && visible(x)).map((n) => toScreen(n));
|
||||
if (!pts.length) return;
|
||||
const tone = HULL_FILLS[ci % HULL_FILLS.length];
|
||||
ctx.save();
|
||||
ctx.fillStyle = tone + "0.06)";
|
||||
ctx.strokeStyle = tone + "0.06)";
|
||||
ctx.lineJoin = "round";
|
||||
const hull = convexHull(pts);
|
||||
if (hull.length < 3) {
|
||||
const cx = pts.reduce((s, p) => s + p.x, 0) / pts.length;
|
||||
const cy = pts.reduce((s, p) => s + p.y, 0) / pts.length;
|
||||
const rr = Math.max(...pts.map((p) => Math.hypot(p.x - cx, p.y - cy)), 0) + 52;
|
||||
ctx.beginPath(); ctx.arc(cx, cy, rr, 0, 7); ctx.fill();
|
||||
} else {
|
||||
ctx.beginPath();
|
||||
hull.forEach((p, i) => (i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y)));
|
||||
ctx.closePath();
|
||||
ctx.lineWidth = 92; // 粗描边 + 圆角连接 ≈ 凸包向外圆角扩张 46px
|
||||
ctx.stroke();
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
});
|
||||
/* 类别锚点标签 */
|
||||
G.catAnchors.forEach((a) => {
|
||||
const p = { x: a.x * G.cam.k + G.cam.x, y: a.y * G.cam.k + G.cam.y };
|
||||
const n = G.nodes.filter((x) => x.type === "Job" && (x.category || "未分类") === a.cat).length;
|
||||
@@ -782,6 +918,42 @@
|
||||
return best;
|
||||
}
|
||||
|
||||
/* 边拾取:点到线段距离 ≤8px 的最近边(悬停 tooltip / 点击查看证据) */
|
||||
function pickEdge(sx, sy) {
|
||||
let best = null, bd = 64;
|
||||
G.edges.forEach((e) => {
|
||||
const a = G.nodes[G.idx[e.source]], b = G.nodes[G.idx[e.target]];
|
||||
if (!a || !b || !visible(a) || !visible(b) || !edgeVisible(e)) return;
|
||||
const pa = toScreen(a), pb = toScreen(b);
|
||||
const dx = pb.x - pa.x, dy = pb.y - pa.y;
|
||||
const len2 = dx * dx + dy * dy || 1;
|
||||
let t = ((sx - pa.x) * dx + (sy - pa.y) * dy) / len2;
|
||||
t = Math.max(0, Math.min(1, t));
|
||||
const d2 = (sx - (pa.x + t * dx)) ** 2 + (sy - (pa.y + t * dy)) ** 2;
|
||||
if (d2 < bd) { bd = d2; best = e; }
|
||||
});
|
||||
return best;
|
||||
}
|
||||
|
||||
/* 边悬停 tooltip:关系类型 / 需求指数 / 证据量 / 起始窗口 */
|
||||
function updateEdgeTip(e, sx, sy) {
|
||||
const tip = $("#edge-tip");
|
||||
const changed = (e || null) !== (G.hoverEdge || null);
|
||||
G.hoverEdge = e || null;
|
||||
if (!e) { tip.classList.add("hidden"); if (changed) requestDraw(); return; }
|
||||
const a = G.nodes[G.idx[e.source]], b = G.nodes[G.idx[e.target]];
|
||||
const relLabel = e.rel === "REQUIRES" ? (e.importance === "required" ? "必备技能" : "加分技能")
|
||||
: e.rel === "APPLIES_TO" ? "应用场景" : "所属行业";
|
||||
const firstWin = (e.intervals || []).map((i) => i[0]).sort()[0];
|
||||
tip.innerHTML = `<div class="et-title">${esc((a && a.title) || e.source)} → ${esc((b && b.title) || e.target)}</div>
|
||||
<div class="et-sub">${relLabel}${e.weight != null ? ` · 需求指数 ${Number(e.weight).toFixed(2)}` : ""} · 证据 ${(e.sources || []).length} 条 JD${firstWin ? ` · 自 ${esc(firstWin)}` : ""}</div>`;
|
||||
const wrap = $("#graph-canvas-wrap").getBoundingClientRect();
|
||||
tip.style.left = `${Math.max(4, Math.min(sx + 14, wrap.width - 250))}px`;
|
||||
tip.style.top = `${Math.max(4, Math.min(sy + 16, wrap.height - 52))}px`;
|
||||
tip.classList.remove("hidden");
|
||||
if (changed) requestDraw();
|
||||
}
|
||||
|
||||
function bindCanvas() {
|
||||
const c = canvas();
|
||||
c.addEventListener("mousedown", (e) => {
|
||||
@@ -800,9 +972,12 @@
|
||||
const h = pickNode(sx, sy);
|
||||
const id = h ? h.id : null;
|
||||
if (id !== G.hover) { G.hover = id; requestDraw(); }
|
||||
c.classList.toggle("hovering", !!h);
|
||||
c.classList.toggle("hovering", !!h || !!pickEdge(sx, sy));
|
||||
const inCanvas = sx >= 0 && sy >= 0 && sx <= c.clientWidth && sy <= c.clientHeight;
|
||||
updateEdgeTip(!h && inCanvas ? pickEdge(sx, sy) : null, sx, sy);
|
||||
return;
|
||||
}
|
||||
updateEdgeTip(null);
|
||||
G.drag.moved = true;
|
||||
if (G.drag.mode === "pan") {
|
||||
G.cam.x = G.drag.camX + (sx - G.drag.sx);
|
||||
@@ -822,9 +997,29 @@
|
||||
c.addEventListener("click", (e) => {
|
||||
if (G.drag && G.drag.moved) return;
|
||||
const rect = c.getBoundingClientRect();
|
||||
const node = pickNode(e.clientX - rect.left, e.clientY - rect.top);
|
||||
selectNode(node ? node.id : null);
|
||||
const sx = e.clientX - rect.left, sy = e.clientY - rect.top;
|
||||
const node = pickNode(sx, sy);
|
||||
if (node) return selectNode(node.id);
|
||||
/* 点空白优先尝试边:命中带证据的边则直接打开 JD 证据链 */
|
||||
const edge = pickEdge(sx, sy);
|
||||
if (edge && (edge.sources || []).length) {
|
||||
const a = G.nodes[G.idx[edge.source]];
|
||||
return CN.showJdEvidence(edge.sources, { jobTitle: (a && (a.title || a.id)) || "" });
|
||||
}
|
||||
selectNode(null);
|
||||
});
|
||||
/* 双击渐进披露:岗位=子图开关;技能=反查聚焦(只看引用它的岗位);双击空白退出聚焦 */
|
||||
c.addEventListener("dblclick", (e) => {
|
||||
const rect = c.getBoundingClientRect();
|
||||
const node = pickNode(e.clientX - rect.left, e.clientY - rect.top);
|
||||
if (!node) {
|
||||
if (G.soloJob || G.focusSkill) exitFocus();
|
||||
return;
|
||||
}
|
||||
if (node.type === "Job" && !(node.pending || node.status === "pending")) toggleSolo(node);
|
||||
else if (node.type === "Skill") toggleSkillFocus(node);
|
||||
});
|
||||
c.addEventListener("mouseleave", () => updateEdgeTip(null));
|
||||
/* 节点右键菜单:选中节点 + 弹出自定义菜单(查看证据 / 时点对比 / 人岗诊断 / 子图开关) */
|
||||
c.addEventListener("contextmenu", (e) => {
|
||||
e.preventDefault();
|
||||
@@ -894,15 +1089,7 @@
|
||||
$("#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();
|
||||
toggleSolo(n);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1222,6 +1409,13 @@
|
||||
e.currentTarget.classList.toggle("active", G.showLabels);
|
||||
requestDraw();
|
||||
};
|
||||
/* 全屏演示:画布容器进/出全屏,尺寸变化后重绘 */
|
||||
$("#tool-fullscreen").onclick = () => {
|
||||
const wrap = $("#graph-canvas-wrap");
|
||||
if (document.fullscreenElement) document.exitFullscreen();
|
||||
else if (wrap.requestFullscreen) wrap.requestFullscreen();
|
||||
};
|
||||
document.addEventListener("fullscreenchange", () => { requestDraw(); drawMinimap(); });
|
||||
$("#tool-filter").onclick = () => $("#graph-filter-panel").classList.toggle("hidden");
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!e.target.closest("#graph-filter-panel") && !e.target.closest("#tool-filter")) {
|
||||
@@ -1481,20 +1675,16 @@
|
||||
function updateSoloBadge() {
|
||||
const badge = $("#solo-badge");
|
||||
if (!badge) return;
|
||||
badge.classList.toggle("hidden", !G.soloJob);
|
||||
if (G.soloJob) {
|
||||
const n = G.nodes[G.idx[G.soloJob]];
|
||||
$("#solo-badge-text").textContent = `岗位子图:${(n && (n.title || n.id)) || G.soloJob}`;
|
||||
const fid = G.soloJob || G.focusSkill;
|
||||
badge.classList.toggle("hidden", !fid);
|
||||
if (fid) {
|
||||
const n = G.nodes[G.idx[fid]];
|
||||
const kind = G.soloJob ? "岗位子图" : "技能聚焦";
|
||||
$("#solo-badge-text").textContent = `${kind}:${(n && (n.title || n.id)) || fid}`;
|
||||
}
|
||||
}
|
||||
const soloExit = $("#solo-exit");
|
||||
if (soloExit) soloExit.onclick = () => {
|
||||
G.soloJob = null;
|
||||
updateSoloBadge();
|
||||
updateRecentChange();
|
||||
fitView();
|
||||
toast("已退出子图,当前为岗位生态总览(全量图)", "info");
|
||||
};
|
||||
if (soloExit) soloExit.onclick = () => exitFocus();
|
||||
|
||||
/* ================= 备注(localStorage) ================= */
|
||||
const getNotes = () => JSON.parse(localStorage.getItem(NOTES_KEY) || "{}");
|
||||
@@ -1549,7 +1739,11 @@
|
||||
kv.push(["类别", n.category || "—"], ["期望年限", n.expected_years != null ? `${n.expected_years} 年` : "—"]);
|
||||
if (n.source_count != null) kv.push(["支撑 JD", `${n.source_count} 条 / ${n.company_count} 家企业`]);
|
||||
}
|
||||
if (n.type === "Skill") kv.push(["类别", n.category || "通用技能"]);
|
||||
if (n.type === "Skill") {
|
||||
kv.push(["类别", n.category || "通用技能"]);
|
||||
const refs = G.edges.filter((e) => e.target === id && e.rel === "REQUIRES").length;
|
||||
if (refs) kv.push(["枢纽度", `被 ${refs} 个岗位引用`]);
|
||||
}
|
||||
kv.push(["级别", n.level || (n.type === "Job" ? jobLevel(n) || "—" : "—")]);
|
||||
if (n.weight != null) kv.push(["需求指数", Number(n.weight).toFixed(2)]);
|
||||
if (n.importance) kv.push(["重要性", n.importance === "required" ? "必备" : n.importance]);
|
||||
@@ -1723,17 +1917,7 @@
|
||||
$("#evo-job-select").dispatchEvent(new Event("change"));
|
||||
};
|
||||
const soloBtn = $("#dt-solo");
|
||||
if (soloBtn) soloBtn.onclick = () => {
|
||||
G.soloJob = G.soloJob === id ? null : id;
|
||||
G._defaultApplied = true;
|
||||
updateSoloBadge();
|
||||
updateRecentChange();
|
||||
if (G.soloJob) {
|
||||
applySoloLayout();
|
||||
fitSoloView();
|
||||
toast(`已进入「${n.title || id}」岗位子图(同心分层:中心岗位 → 必备 → 加分 → 场景/行业)`, "success");
|
||||
} else fitView();
|
||||
};
|
||||
if (soloBtn) soloBtn.onclick = () => toggleSolo(n);
|
||||
const matchBtn = $("#dt-match");
|
||||
if (matchBtn) matchBtn.onclick = () => {
|
||||
CN.switchView("match");
|
||||
@@ -1874,6 +2058,7 @@
|
||||
async reload() {
|
||||
await load();
|
||||
if (G.soloJob && G.idx[G.soloJob]) { applySoloLayout(); fitSoloView(); } // 子图模式下重排同心分层
|
||||
if (G.focusSkill && G.idx[G.focusSkill]) { layoutSkillFocus(); fitVisibleView(260, 240, 1.5); } // 技能聚焦重排
|
||||
if (G.compare && G.compare.b) updateCompare(); // 对比模式随数据重算
|
||||
if (G.selected && !(G.selected in G.idx)) selectNode(null, false);
|
||||
else if (G.selected) renderDetail(G.selected);
|
||||
|
||||
Reference in New Issue
Block a user