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>
|