263 lines
9.8 KiB
HTML
263 lines
9.8 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>08 PlanB Canvas Diagrams</title>
|
||
<style>
|
||
:root {
|
||
--bg: #f8fafc;
|
||
--ink: #0f172a;
|
||
--muted: #475569;
|
||
--outer: #fff7ed;
|
||
--outer-border: #ea580c;
|
||
--inner: #eff6ff;
|
||
--inner-border: #2563eb;
|
||
--svc: #f5f3ff;
|
||
--svc-border: #7c3aed;
|
||
--data: #ecfdf5;
|
||
--data-border: #059669;
|
||
--bank: #fef2f2;
|
||
--bank-border: #dc2626;
|
||
--backup: #fffbeb;
|
||
--backup-border: #d97706;
|
||
}
|
||
body {
|
||
margin: 0;
|
||
padding: 24px;
|
||
background: var(--bg);
|
||
color: var(--ink);
|
||
font: 14px/1.5 -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||
}
|
||
h1 {
|
||
margin: 0 0 16px;
|
||
font-size: 22px;
|
||
}
|
||
h2 {
|
||
margin: 24px 0 8px;
|
||
font-size: 18px;
|
||
}
|
||
p {
|
||
margin: 0 0 12px;
|
||
color: var(--muted);
|
||
}
|
||
.card {
|
||
background: white;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 16px;
|
||
padding: 16px;
|
||
margin-bottom: 20px;
|
||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
|
||
}
|
||
canvas {
|
||
width: 100%;
|
||
height: auto;
|
||
display: block;
|
||
background: #ffffff;
|
||
border-radius: 12px;
|
||
}
|
||
.tip {
|
||
margin-top: 8px;
|
||
color: var(--muted);
|
||
font-size: 13px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>福建水务营收系统 Canvas 图示</h1>
|
||
<p>该文件用于替代当前 Mermaid 版本,便于你审阅更接近正式交付效果的网络图与访问图。</p>
|
||
|
||
<div class="card">
|
||
<h2>图 A 访问与应用软件拓扑图</h2>
|
||
<canvas id="access" width="1500" height="720"></canvas>
|
||
<div class="tip">公网域与内部网络域已明确拆分,PC 端走内网,移动端与第三方走公网接入代理。</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>图 B 网络连接图</h2>
|
||
<canvas id="network" width="1600" height="980"></canvas>
|
||
<div class="tip">该图更偏网络工程师评审视角,强调边界、链路和跨域访问关系。</div>
|
||
</div>
|
||
|
||
<script>
|
||
const COLORS = {
|
||
ink: "#0f172a",
|
||
muted: "#475569",
|
||
outer: "#fff7ed",
|
||
outerBorder: "#ea580c",
|
||
inner: "#eff6ff",
|
||
innerBorder: "#2563eb",
|
||
svc: "#f5f3ff",
|
||
svcBorder: "#7c3aed",
|
||
data: "#ecfdf5",
|
||
dataBorder: "#059669",
|
||
bank: "#fef2f2",
|
||
bankBorder: "#dc2626",
|
||
backup: "#fffbeb",
|
||
backupBorder: "#d97706",
|
||
line: "#334155"
|
||
};
|
||
|
||
function roundRect(ctx, x, y, w, h, r, fill, stroke, lw = 2) {
|
||
ctx.beginPath();
|
||
ctx.moveTo(x + r, y);
|
||
ctx.arcTo(x + w, y, x + w, y + h, r);
|
||
ctx.arcTo(x + w, y + h, x, y + h, r);
|
||
ctx.arcTo(x, y + h, x, y, r);
|
||
ctx.arcTo(x, y, x + w, y, r);
|
||
ctx.closePath();
|
||
ctx.fillStyle = fill;
|
||
ctx.fill();
|
||
ctx.lineWidth = lw;
|
||
ctx.strokeStyle = stroke;
|
||
ctx.stroke();
|
||
}
|
||
|
||
function text(ctx, str, x, y, opts = {}) {
|
||
const { size = 18, color = COLORS.ink, align = "center", weight = "600" } = opts;
|
||
ctx.fillStyle = color;
|
||
ctx.font = `${weight} ${size}px -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif`;
|
||
ctx.textAlign = align;
|
||
ctx.textBaseline = "middle";
|
||
const lines = Array.isArray(str) ? str : [str];
|
||
lines.forEach((line, i) => ctx.fillText(line, x, y + i * (size + 6)));
|
||
}
|
||
|
||
function arrow(ctx, x1, y1, x2, y2, label = "") {
|
||
ctx.strokeStyle = COLORS.line;
|
||
ctx.fillStyle = COLORS.line;
|
||
ctx.lineWidth = 2;
|
||
ctx.beginPath();
|
||
ctx.moveTo(x1, y1);
|
||
ctx.lineTo(x2, y2);
|
||
ctx.stroke();
|
||
|
||
const angle = Math.atan2(y2 - y1, x2 - x1);
|
||
const len = 10;
|
||
ctx.beginPath();
|
||
ctx.moveTo(x2, y2);
|
||
ctx.lineTo(x2 - len * Math.cos(angle - Math.PI / 6), y2 - len * Math.sin(angle - Math.PI / 6));
|
||
ctx.lineTo(x2 - len * Math.cos(angle + Math.PI / 6), y2 - len * Math.sin(angle + Math.PI / 6));
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
|
||
if (label) {
|
||
const mx = (x1 + x2) / 2;
|
||
const my = (y1 + y2) / 2 - 10;
|
||
roundRect(ctx, mx - 72, my - 16, 144, 28, 8, "#ffffff", "#cbd5e1", 1);
|
||
text(ctx, label, mx, my - 1, { size: 12, weight: "500", color: COLORS.muted });
|
||
}
|
||
}
|
||
|
||
function drawAccess() {
|
||
const c = document.getElementById("access");
|
||
const ctx = c.getContext("2d");
|
||
ctx.clearRect(0, 0, c.width, c.height);
|
||
|
||
roundRect(ctx, 40, 50, 500, 210, 20, COLORS.outer, COLORS.outerBorder);
|
||
text(ctx, "公网域", 290, 80, { size: 22 });
|
||
roundRect(ctx, 80, 115, 160, 60, 14, "#ffffff", COLORS.outerBorder);
|
||
roundRect(ctx, 290, 115, 160, 60, 14, "#ffffff", COLORS.outerBorder);
|
||
roundRect(ctx, 170, 190, 220, 60, 14, "#ffffff", COLORS.outerBorder);
|
||
text(ctx, "移动端用户", 160, 145);
|
||
text(ctx, "第三方系统", 370, 145);
|
||
text(ctx, "公网接入代理", 280, 220);
|
||
|
||
roundRect(ctx, 600, 50, 280, 210, 20, COLORS.inner, COLORS.innerBorder);
|
||
text(ctx, "内部网络域", 740, 80, { size: 22 });
|
||
roundRect(ctx, 645, 115, 190, 60, 14, "#ffffff", COLORS.innerBorder);
|
||
roundRect(ctx, 645, 190, 190, 60, 14, "#ffffff", COLORS.innerBorder);
|
||
text(ctx, "PC 端用户", 740, 145);
|
||
text(ctx, "内网 Nginx", 740, 220);
|
||
|
||
roundRect(ctx, 930, 80, 240, 220, 20, COLORS.inner, COLORS.innerBorder);
|
||
roundRect(ctx, 1220, 80, 240, 220, 20, COLORS.inner, COLORS.innerBorder);
|
||
text(ctx, "业务应用节点 1", 1050, 110, { size: 20 });
|
||
text(ctx, "业务应用节点 2", 1340, 110, { size: 20 });
|
||
roundRect(ctx, 980, 145, 140, 54, 12, "#ffffff", COLORS.innerBorder);
|
||
roundRect(ctx, 1270, 145, 140, 54, 12, "#ffffff", COLORS.innerBorder);
|
||
roundRect(ctx, 980, 220, 140, 54, 12, "#ffffff", COLORS.innerBorder);
|
||
roundRect(ctx, 1270, 220, 140, 54, 12, "#ffffff", COLORS.innerBorder);
|
||
text(ctx, "Gateway", 1050, 172);
|
||
text(ctx, "Gateway", 1340, 172);
|
||
text(ctx, "业务服务", 1050, 247);
|
||
text(ctx, "业务服务", 1340, 247);
|
||
|
||
arrow(ctx, 390, 220, 645, 220, "跨公网/内网边界转发");
|
||
arrow(ctx, 740, 220, 980, 172, "负载到节点 1");
|
||
arrow(ctx, 740, 220, 1270, 172, "负载到节点 2");
|
||
arrow(ctx, 1050, 199, 1050, 220, "");
|
||
arrow(ctx, 1340, 199, 1340, 220, "");
|
||
}
|
||
|
||
function drawNetwork() {
|
||
const c = document.getElementById("network");
|
||
const ctx = c.getContext("2d");
|
||
ctx.clearRect(0, 0, c.width, c.height);
|
||
|
||
roundRect(ctx, 40, 40, 440, 180, 18, COLORS.outer, COLORS.outerBorder);
|
||
text(ctx, "公网域", 260, 72, { size: 22 });
|
||
roundRect(ctx, 70, 105, 120, 54, 12, "#fff", COLORS.outerBorder);
|
||
roundRect(ctx, 215, 105, 120, 54, 12, "#fff", COLORS.outerBorder);
|
||
roundRect(ctx, 125, 170, 180, 54, 12, "#fff", COLORS.outerBorder);
|
||
text(ctx, "移动端用户", 130, 132, { size: 15 });
|
||
text(ctx, "第三方系统", 275, 132, { size: 15 });
|
||
text(ctx, "公网接入代理", 215, 197, { size: 15 });
|
||
|
||
roundRect(ctx, 540, 40, 230, 180, 18, COLORS.inner, COLORS.innerBorder);
|
||
text(ctx, "内网接入区", 655, 72, { size: 22 });
|
||
roundRect(ctx, 575, 130, 160, 60, 12, "#fff", COLORS.innerBorder);
|
||
text(ctx, "内网 Nginx", 655, 160);
|
||
|
||
roundRect(ctx, 830, 40, 360, 220, 18, COLORS.inner, COLORS.innerBorder);
|
||
text(ctx, "应用区", 1010, 72, { size: 22 });
|
||
roundRect(ctx, 865, 115, 140, 95, 12, "#fff", COLORS.innerBorder);
|
||
roundRect(ctx, 1015, 115, 140, 95, 12, "#fff", COLORS.innerBorder);
|
||
text(ctx, ["应用节点 1", "Gateway + Biz"], 935, 145, { size: 15 });
|
||
text(ctx, ["应用节点 2", "Gateway + Biz"], 1085, 145, { size: 15 });
|
||
|
||
roundRect(ctx, 1240, 40, 300, 220, 18, COLORS.svc, COLORS.svcBorder);
|
||
text(ctx, "综合服务区", 1390, 72, { size: 22 });
|
||
roundRect(ctx, 1275, 115, 110, 54, 12, "#fff", COLORS.svcBorder);
|
||
roundRect(ctx, 1395, 115, 110, 54, 12, "#fff", COLORS.svcBorder);
|
||
roundRect(ctx, 1335, 185, 110, 54, 12, "#fff", COLORS.svcBorder);
|
||
text(ctx, "缓存服务", 1330, 142, { size: 14 });
|
||
text(ctx, "文件服务", 1450, 142, { size: 14 });
|
||
text(ctx, "数据库控制", 1390, 212, { size: 14 });
|
||
|
||
roundRect(ctx, 40, 310, 300, 180, 18, COLORS.bank, COLORS.bankBorder);
|
||
text(ctx, "银行文件交换区", 190, 342, { size: 22 });
|
||
roundRect(ctx, 90, 390, 200, 60, 12, "#fff", COLORS.bankBorder);
|
||
text(ctx, "SFTP / FTP 文件交换服务器", 190, 420, { size: 15 });
|
||
|
||
roundRect(ctx, 430, 310, 360, 180, 18, COLORS.data, COLORS.dataBorder);
|
||
text(ctx, "数据区", 610, 342, { size: 22 });
|
||
roundRect(ctx, 470, 390, 120, 60, 12, "#fff", COLORS.dataBorder);
|
||
roundRect(ctx, 630, 390, 120, 60, 12, "#fff", COLORS.dataBorder);
|
||
text(ctx, "主库", 530, 420);
|
||
text(ctx, "热备", 690, 420);
|
||
|
||
roundRect(ctx, 860, 310, 260, 180, 18, COLORS.backup, COLORS.backupBorder);
|
||
text(ctx, "备份归档区", 990, 342, { size: 22 });
|
||
roundRect(ctx, 900, 390, 180, 60, 12, "#fff", COLORS.backupBorder);
|
||
text(ctx, "备份归档存储", 990, 420);
|
||
|
||
arrow(ctx, 305, 197, 575, 160, "公网转内网");
|
||
arrow(ctx, 655, 190, 935, 115, "API / 网关端口");
|
||
arrow(ctx, 655, 190, 1085, 115, "API / 网关端口");
|
||
arrow(ctx, 935, 210, 1330, 142, "6379 / 8848 / 9000");
|
||
arrow(ctx, 1085, 210, 1390, 212, "6432 / 5000");
|
||
arrow(ctx, 935, 210, 190, 390, "21 或 22");
|
||
arrow(ctx, 1390, 239, 530, 390, "5432");
|
||
arrow(ctx, 1390, 239, 690, 390, "5432 状态探测");
|
||
arrow(ctx, 590, 420, 630, 420, "主备同步");
|
||
arrow(ctx, 1390, 239, 990, 390, "归档");
|
||
arrow(ctx, 690, 450, 990, 450, "备份副本");
|
||
}
|
||
|
||
drawAccess();
|
||
drawNetwork();
|
||
</script>
|
||
</body>
|
||
</html>
|