Files
wukuang/dependency-graph.html
2026-05-23 14:05:22 +08:00

462 lines
15 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>Lingshu LCDP 包依赖关系图</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f172a;
color: #e2e8f0;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 24px;
}
h1 {
font-size: 22px;
font-weight: 600;
margin-bottom: 8px;
color: #f8fafc;
}
.subtitle {
font-size: 13px;
color: #94a3b8;
margin-bottom: 24px;
}
.legend {
display: flex;
gap: 24px;
margin-bottom: 20px;
font-size: 13px;
color: #94a3b8;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
}
.legend-dot {
width: 12px;
height: 12px;
border-radius: 3px;
}
svg {
filter: drop-shadow(0 4px 24px rgba(0, 0, 0, 0.3));
}
.node rect {
rx: 10;
ry: 10;
stroke-width: 2;
cursor: pointer;
transition: filter 0.2s;
}
.node:hover rect {
filter: brightness(1.2) drop-shadow(0 0 8px rgba(255, 255, 255, 0.15));
}
.node text {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 13px;
font-weight: 600;
fill: #fff;
pointer-events: none;
}
.node .pkg-name {
font-size: 11px;
font-weight: 400;
fill: rgba(255, 255, 255, 0.65);
}
.edge {
fill: none;
stroke-width: 1.8;
}
.arrowhead {
stroke: none;
}
.layer-label {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 11px;
fill: #475569;
font-weight: 600;
letter-spacing: 1px;
}
.layer-bg {
fill: rgba(255, 255, 255, 0.02);
rx: 8;
ry: 8;
}
.tooltip {
position: fixed;
background: #1e293b;
border: 1px solid #334155;
border-radius: 8px;
padding: 12px 16px;
font-size: 13px;
color: #e2e8f0;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s;
max-width: 320px;
z-index: 10;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
.tooltip.show {
opacity: 1;
}
.tooltip h3 {
font-size: 14px;
margin-bottom: 6px;
color: #f8fafc;
}
.tooltip .deps {
color: #94a3b8;
line-height: 1.6;
}
.tooltip .dep-tag {
display: inline-block;
background: #334155;
padding: 1px 8px;
border-radius: 4px;
margin: 2px 2px;
font-size: 11px;
color: #cbd5e1;
}
</style>
</head>
<body>
<h1>Lingshu LCDP 包依赖关系图</h1>
<p class="subtitle">箭头方向:A → B 表示 A 依赖 BA imports from B)· 无运行时循环依赖</p>
<div class="legend">
<div class="legend-item">
<div class="legend-dot" style="background: #ef4444"></div>
Layer 5 · 应用层
</div>
<div class="legend-item">
<div class="legend-dot" style="background: #e879f9"></div>
Layer 4 · 设计器层
</div>
<div class="legend-item">
<div class="legend-dot" style="background: #f59e0b"></div>
Layer 3 · 业务模块层
</div>
<div class="legend-item">
<div class="legend-dot" style="background: #3b82f6"></div>
Layer 2 · 组件层
</div>
<div class="legend-item">
<div class="legend-dot" style="background: #8b5cf6"></div>
Layer 1 · 工具层
</div>
<div class="legend-item">
<div class="legend-dot" style="background: #06b6d4"></div>
Layer 0 · 基础层
</div>
</div>
<div class="tooltip" id="tooltip">
<h3 id="tt-title"></h3>
<div class="deps" id="tt-deps"></div>
</div>
<svg id="graph" width="1100" height="850"></svg>
<script>
const packages = [
// Layer 5 - 应用层
{ id: 'lcdp', label: 'apps/lcdp', pkg: 'fe-cisdigital-lingshu-lcdp', layer: 5, x: 550, y: 60 },
// Layer 4 - 设计器层
{ id: 'form-designer', label: 'form-designer', pkg: '@lingshu/form-designer', layer: 4, x: 550, y: 190 },
// Layer 3 - 业务模块层
{ id: 'business-module', label: 'business-module', pkg: '@lingshu/business-module', layer: 3, x: 550, y: 330 },
// Layer 2 - 组件层
{ id: 'ai-agents', label: 'ai-agents', pkg: '@lingshu/ai-agents', layer: 2, x: 175, y: 480 },
{
id: 'business-components',
label: 'business-components',
pkg: '@lingshu/business-components',
layer: 2,
x: 550,
y: 480,
},
{ id: 'widget-pc', label: 'widget-pc', pkg: '@lingshu/widget-pc', layer: 2, x: 925, y: 480 },
// Layer 1 - 工具层
{ id: 'core-utils', label: 'core-utils', pkg: '@lingshu/core-utils', layer: 1, x: 400, y: 630 },
// Layer 0 - 基础类型层
{ id: 'types', label: 'types', pkg: '@lingshu/types', layer: 0, x: 350, y: 770 },
{ id: 'form-create', label: 'form-create', pkg: '@lingshu/form-create', layer: 0, x: 600, y: 770 },
{ id: 'lowcode-create', label: 'lowcode-create', pkg: '@lingshu/lowcode-create', layer: 0, x: 850, y: 770 },
]
const edges = [
// apps/lcdp deps (源码扫描)
{ from: 'lcdp', to: 'form-designer' },
{ from: 'lcdp', to: 'business-module' },
{ from: 'lcdp', to: 'business-components' },
{ from: 'lcdp', to: 'ai-agents' },
{ from: 'lcdp', to: 'core-utils' },
{ from: 'lcdp', to: 'form-create' },
// form-designer deps
{ from: 'form-designer', to: 'ai-agents' },
{ from: 'form-designer', to: 'business-components' },
{ from: 'form-designer', to: 'business-module' },
{ from: 'form-designer', to: 'core-utils' },
{ from: 'form-designer', to: 'form-create' },
{ from: 'form-designer', to: 'types' },
// business-module deps
{ from: 'business-module', to: 'business-components' },
{ from: 'business-module', to: 'core-utils' },
{ from: 'business-module', to: 'form-create' },
{ from: 'business-module', to: 'types' },
// business-components deps
{ from: 'business-components', to: 'core-utils' },
{ from: 'business-components', to: 'form-create' },
{ from: 'business-components', to: 'types' },
// ai-agents deps
{ from: 'ai-agents', to: 'business-components' },
{ from: 'ai-agents', to: 'core-utils' },
// widget-pc deps
{ from: 'widget-pc', to: 'business-components' },
{ from: 'widget-pc', to: 'types' },
{ from: 'widget-pc', to: 'lowcode-create' },
// core-utils deps
{ from: 'core-utils', to: 'types' },
]
const layerColors = {
5: { fill: '#991b1b', stroke: '#ef4444', bg: 'rgba(239,68,68,0.06)' },
4: { fill: '#701a75', stroke: '#e879f9', bg: 'rgba(232,121,249,0.06)' },
3: { fill: '#92400e', stroke: '#f59e0b', bg: 'rgba(245,158,11,0.06)' },
2: { fill: '#1e3a5f', stroke: '#3b82f6', bg: 'rgba(59,130,246,0.06)' },
1: { fill: '#4c1d95', stroke: '#8b5cf6', bg: 'rgba(139,92,246,0.06)' },
0: { fill: '#164e63', stroke: '#06b6d4', bg: 'rgba(6,182,212,0.06)' },
}
const layerNames = {
5: 'LAYER 5 · APPLICATION',
4: 'LAYER 4 · DESIGNER',
3: 'LAYER 3 · BUSINESS MODULE',
2: 'LAYER 2 · COMPONENTS',
1: 'LAYER 1 · UTILITIES',
0: 'LAYER 0 · FOUNDATION',
}
const layerYRanges = {
5: [20, 120],
4: [148, 248],
3: [278, 385],
2: [420, 540],
1: [575, 680],
0: [715, 820],
}
const nodeW = 200,
nodeH = 52
const svg = document.getElementById('graph')
const ns = 'http://www.w3.org/2000/svg'
function el(tag, attrs, parent) {
const e = document.createElementNS(ns, tag)
for (const [k, v] of Object.entries(attrs || {})) e.setAttribute(k, v)
if (parent) parent.appendChild(e)
return e
}
// Layer backgrounds
for (const [layer, [y1, y2]] of Object.entries(layerYRanges)) {
el('rect', { class: 'layer-bg', x: 30, y: y1, width: 1040, height: y2 - y1, fill: layerColors[layer].bg }, svg)
el('text', { class: 'layer-label', x: 48, y: y1 + 18 }, svg).textContent = layerNames[layer]
}
// Defs for arrowheads
const defs = el('defs', {}, svg)
for (const [layer, col] of Object.entries(layerColors)) {
const marker = el(
'marker',
{
id: `arrow-${layer}`,
viewBox: '0 0 10 7',
refX: '10',
refY: '3.5',
markerWidth: '10',
markerHeight: '7',
orient: 'auto-start-reverse',
},
defs,
)
el('polygon', { points: '0 0, 10 3.5, 0 7', fill: col.stroke, class: 'arrowhead', opacity: '0.7' }, marker)
}
// Build node map
const nodeMap = {}
packages.forEach((p) => (nodeMap[p.id] = p))
// Build dep info for tooltip
const depInfo = {}
packages.forEach((p) => {
depInfo[p.id] = { deps: [], usedBy: [] }
})
edges.forEach((e) => {
depInfo[e.from].deps.push(e.to)
depInfo[e.to].usedBy.push(e.from)
})
// Type-only circular arrow marker (amber)
const typeOnlyMarker = el(
'marker',
{
id: 'arrow-type-only',
viewBox: '0 0 10 7',
refX: '10',
refY: '3.5',
markerWidth: '10',
markerHeight: '7',
orient: 'auto-start-reverse',
},
defs,
)
el('polygon', { points: '0 0, 10 3.5, 0 7', fill: '#f59e0b', class: 'arrowhead', opacity: '0.9' }, typeOnlyMarker)
// Cubic bezier edge drawing
function drawEdge(fromNode, toNode, isCircular) {
const goingUp = fromNode.y > toNode.y
let fx, fy, tx, ty
if (isCircular) {
// Circular deps go upward: start from top of lower node, end at bottom of upper node
// Offset horizontally to avoid overlapping normal edges
const offset = fromNode.x <= toNode.x ? -30 : 30
fx = fromNode.x + offset
fy = fromNode.y - nodeH / 2
tx = toNode.x + offset
ty = toNode.y + nodeH / 2
} else {
fx = fromNode.x
fy = fromNode.y + nodeH / 2
tx = toNode.x
ty = toNode.y - nodeH / 2
}
const dy = ty - fy
const c1y = fy + dy * 0.4
const c2y = ty - dy * 0.4
const col = isCircular ? { stroke: '#f59e0b' } : layerColors[fromNode.layer]
const path = el(
'path',
{
class: 'edge',
d: `M${fx},${fy} C${fx},${c1y} ${tx},${c2y} ${tx},${ty}`,
stroke: col.stroke,
opacity: isCircular ? '0.6' : '0.25',
'stroke-dasharray': isCircular ? '6 4' : 'none',
'marker-end': isCircular ? 'url(#arrow-type-only)' : `url(#arrow-${fromNode.layer})`,
},
svg,
)
return path
}
// Draw edges
const edgeEls = edges.map((e) => {
const fromN = nodeMap[e.from],
toN = nodeMap[e.to]
return { el: drawEdge(fromN, toN, !!e.circular), from: e.from, to: e.to, circular: !!e.circular }
})
// Draw nodes
const nodeEls = {}
packages.forEach((p) => {
const col = layerColors[p.layer]
const g = el('g', { class: 'node', transform: `translate(${p.x - nodeW / 2}, ${p.y - nodeH / 2})` }, svg)
el(
'rect',
{
width: nodeW,
height: nodeH,
fill: col.fill,
stroke: col.stroke,
'stroke-opacity': '0.6',
},
g,
)
const label = el('text', { x: nodeW / 2, y: 22, 'text-anchor': 'middle' }, g)
label.textContent = p.label
const pkgLabel = el('text', { class: 'pkg-name', x: nodeW / 2, y: 40, 'text-anchor': 'middle' }, g)
pkgLabel.textContent = p.pkg
nodeEls[p.id] = g
// Hover interactions
g.addEventListener('mouseenter', (ev) => {
// Highlight connected edges
edgeEls.forEach((edge) => {
if (edge.from === p.id || edge.to === p.id) {
edge.el.setAttribute('opacity', '0.9')
edge.el.setAttribute('stroke-width', '2.8')
} else {
edge.el.setAttribute('opacity', '0.07')
}
})
// Dim other nodes
packages.forEach((other) => {
if (
other.id !== p.id &&
!depInfo[p.id].deps.includes(other.id) &&
!depInfo[p.id].usedBy.includes(other.id)
) {
nodeEls[other.id].style.opacity = '0.2'
}
})
// Tooltip
const tooltip = document.getElementById('tooltip')
const ttTitle = document.getElementById('tt-title')
const ttDeps = document.getElementById('tt-deps')
ttTitle.textContent = p.pkg
let html = ''
if (depInfo[p.id].deps.length) {
html += '<div style="margin-bottom:4px">依赖 ↓</div>'
depInfo[p.id].deps.forEach((d) => (html += `<span class="dep-tag">${nodeMap[d].label}</span>`))
}
if (depInfo[p.id].usedBy.length) {
html += '<div style="margin-top:8px;margin-bottom:4px">被依赖 ↑</div>'
depInfo[p.id].usedBy.forEach((d) => (html += `<span class="dep-tag">${nodeMap[d].label}</span>`))
}
if (!html) html = '<div style="color:#64748b">无仓库内依赖(基础包)</div>'
ttDeps.innerHTML = html
tooltip.classList.add('show')
const rect = g.getBoundingClientRect()
tooltip.style.left = Math.min(rect.right + 12, window.innerWidth - 340) + 'px'
tooltip.style.top = rect.top + 'px'
})
g.addEventListener('mouseleave', () => {
edgeEls.forEach((edge) => {
edge.el.setAttribute('opacity', edge.circular ? '0.6' : '0.25')
edge.el.setAttribute('stroke-width', '1.8')
})
packages.forEach((other) => {
nodeEls[other.id].style.opacity = '1'
})
document.getElementById('tooltip').classList.remove('show')
})
})
</script>
</body>
</html>