Files
wukuang/dependency-graph.html
T

462 lines
15 KiB
HTML
Raw Normal View History

2026-05-23 14:05:22 +08:00
<!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>