diff --git a/app/api/routes.py b/app/api/routes.py index f85720e..827c3df 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -327,9 +327,16 @@ def restart_service() -> dict: @app.get("/api/graph") def graph() -> dict: - """返回节点与边(含时间区间)与演化事件,供前端渲染全景图谱。""" + """返回节点与边(含时间区间)与演化事件,供前端渲染全景图谱。 + + 技能节点附带词表 level(L1/L2/L3),前端职级演进视图按此分带。 + """ kg = _get_kg() nodes = [{"id": n, **d} for n, d in kg.g.nodes(data=True)] + skill_lv = {s["name"]: s.get("level") for s in load_lexicon(resolve(_cfg, "lexicon_path"))["skills"]} + for n in nodes: + if n.get("type") == "Skill" and not n.get("level"): + n["level"] = skill_lv.get(n["id"], "L2") edges = [{"source": u, "target": v, **d} for u, v, d in kg.g.edges(data=True)] events = sorted((e.model_dump() for e in kg.events), key=lambda e: e["window"]) return _ok({"nodes": nodes, "edges": edges, "events": events, "stats": kg.stats()}) diff --git a/webui/graph.js b/webui/graph.js index ef80fff..08be7cb 100644 --- a/webui/graph.js +++ b/webui/graph.js @@ -99,9 +99,9 @@ /* ================= 视图切换:全景 / 技术栈 / 职级演进 / 动态时序 ================= */ const VIEW_NAMES = { overview: "全景总览", tech: "技术栈视图", level: "职级演进视图", timeline: "动态时序视图" }; const LEVEL_BANDS = [ - { name: "初级 L1 · ≤2 年", y: -340 }, + { name: "初级 L1 · ≤2 年", y: -420 }, { name: "中级 L2 · 3-5 年", y: 0 }, - { name: "高级 L3 · >5 年", y: 340 }, + { name: "高级 L3 · >5 年", y: 420 }, ]; /* 技术栈视图:类别锚点网格分布,岗位聚在锚点旁,技能/场景/行业靠向所属岗位质心 */ @@ -153,19 +153,19 @@ const yBand = LEVEL_BANDS[b].y; const jl = G.nodes.filter((n) => n.type === "Job" && levelBandOf(n) === b) .sort((x, y) => String(x.title || x.id).localeCompare(String(y.title || y.id), "zh")); - // 岗位带内多行排列(一行排开会横向过长,fitView 按比例缩成横带) - const jPerRow = Math.max(3, Math.ceil(Math.sqrt(jl.length) * 1.8)); + // 岗位在带线下方(最多 2 行),技能在带线上方(最多 3 行),两者不重叠 + const jPerRow = Math.max(3, Math.ceil(jl.length / 2)); jl.forEach((n, i) => { n.x = ((i % jPerRow) - (jPerRow - 1) / 2) * 190; - n.y = yBand + 150 - Math.floor(i / jPerRow) * 85; + n.y = yBand + 90 + Math.floor(i / jPerRow) * 85; n.vx = 0; n.vy = 0; }); const sl = G.nodes.filter((n) => n.type !== "Job" && levelBandOf(n) === b) .sort((x, y) => String(x.title || x.id).localeCompare(String(y.title || y.id), "zh")); - const perRow = Math.max(4, Math.ceil(Math.sqrt(sl.length) * 1.9)); + const perRow = Math.max(4, Math.ceil(sl.length / 4)); sl.forEach((n, i) => { - n.x = ((i % perRow) - (perRow - 1) / 2) * 108; - n.y = yBand - 120 + Math.floor(i / perRow) * 72; + n.x = ((i % perRow) - (perRow - 1) / 2) * 100; + n.y = yBand - 170 + Math.floor(i / perRow) * 62; n.vx = 0; n.vy = 0; }); });