462 lines
15 KiB
HTML
462 lines
15 KiB
HTML
<!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 依赖 B(A 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>
|