iloom/docs/architecture.html

1564 lines
66 KiB
HTML
Raw Permalink Normal View History

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>iloom × muyu-apiserver 系统架构图</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0a0e27;
color: #e0e6ed;
min-height: 100vh;
overflow-x: hidden;
}
/* ── Header ── */
.header {
text-align: center;
padding: 32px 20px 16px;
background: linear-gradient(180deg, #111640 0%, #0a0e27 100%);
border-bottom: 1px solid rgba(99,102,241,0.2);
}
.header h1 {
font-size: 28px;
font-weight: 700;
background: linear-gradient(135deg, #818cf8, #6ee7b7);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 6px;
}
.header p { font-size: 14px; color: #64748b; }
/* ── Legend ── */
.legend {
display: flex;
justify-content: center;
gap: 24px;
padding: 12px;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: #94a3b8;
}
.legend-dot {
width: 12px; height: 12px;
border-radius: 50%;
}
.legend-line {
width: 32px; height: 3px;
border-radius: 2px;
}
/* ── Canvas ── */
.canvas {
position: relative;
max-width: 1440px;
margin: 0 auto;
padding: 20px;
}
/* ── SVG Connections ── */
.connections-svg {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
pointer-events: none;
z-index: 1;
}
.conn-line {
fill: none;
stroke-width: 2;
opacity: 0.5;
transition: opacity 0.3s, stroke-width 0.3s;
}
.conn-line.highlight {
opacity: 1;
stroke-width: 3;
}
.flow-particle {
r: 4;
opacity: 0.9;
}
/* ── Grid Layout ── */
.arch-grid {
display: grid;
grid-template-columns: 200px 1fr 320px;
grid-template-rows: auto auto auto;
gap: 20px;
position: relative;
z-index: 2;
min-height: 800px;
}
/* ── Service Card ── */
.service-card {
background: rgba(30, 35, 60, 0.85);
border: 1px solid rgba(99, 102, 241, 0.15);
border-radius: 14px;
padding: 16px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
backdrop-filter: blur(8px);
}
.service-card:hover {
border-color: rgba(99, 102, 241, 0.5);
transform: translateY(-2px);
box-shadow: 0 8px 32px rgba(99, 102, 241, 0.15);
}
.service-card.active {
border-color: #818cf8;
box-shadow: 0 0 24px rgba(99, 102, 241, 0.3);
}
.card-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.card-icon {
width: 36px; height: 36px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
.card-title {
font-size: 14px;
font-weight: 600;
color: #e2e8f0;
}
.card-port {
font-size: 11px;
color: #64748b;
font-family: 'SF Mono', 'Fira Code', monospace;
}
.card-tag {
display: inline-block;
font-size: 10px;
padding: 2px 8px;
border-radius: 20px;
font-weight: 500;
margin-top: 4px;
}
/* ── Color Themes ── */
.theme-frontend .card-icon { background: rgba(251,191,36,0.15); color: #fbbf24; }
.theme-frontend { border-color: rgba(251,191,36,0.2); }
.theme-frontend:hover { border-color: rgba(251,191,36,0.5); }
.theme-gateway .card-icon { background: rgba(129,140,248,0.15); color: #818cf8; }
.theme-gateway { border-color: rgba(129,140,248,0.2); }
.theme-iloom .card-icon { background: rgba(52,211,153,0.15); color: #34d399; }
.theme-iloom { border-color: rgba(52,211,153,0.2); }
.theme-iloom:hover { border-color: rgba(52,211,153,0.5); }
.theme-muyu .card-icon { background: rgba(251,113,133,0.15); color: #fb7185; }
.theme-muyu { border-color: rgba(251,113,133,0.2); }
.theme-muyu:hover { border-color: rgba(251,113,133,0.5); }
.theme-db .card-icon { background: rgba(96,165,250,0.15); color: #60a5fa; }
.theme-db { border-color: rgba(96,165,250,0.2); }
.theme-db:hover { border-color: rgba(96,165,250,0.5); }
.theme-infra .card-icon { background: rgba(168,85,247,0.15); color: #a855f7; }
.theme-infra { border-color: rgba(168,85,247,0.2); }
/* ── Section Titles ── */
.section-title {
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1.5px;
padding: 8px 0;
margin-bottom: 8px;
}
.section-title.iloom { color: #34d399; border-bottom: 2px solid rgba(52,211,153,0.3); }
.section-title.muyu { color: #fb7185; border-bottom: 2px solid rgba(251,113,133,0.3); }
.section-title.data { color: #60a5fa; border-bottom: 2px solid rgba(96,165,250,0.3); }
/* ── Zone Boxes ── */
.zone {
border: 1.5px dashed rgba(99,102,241,0.2);
border-radius: 16px;
padding: 16px;
position: relative;
}
.zone-label {
position: absolute;
top: -10px; left: 16px;
background: #0a0e27;
padding: 0 8px;
font-size: 11px;
font-weight: 600;
letter-spacing: 1px;
text-transform: uppercase;
}
.zone.iloom { border-color: rgba(52,211,153,0.25); }
.zone.iloom .zone-label { color: #34d399; }
.zone.muyu { border-color: rgba(251,113,133,0.25); }
.zone.muyu .zone-label { color: #fb7185; }
.zone.db { border-color: rgba(96,165,250,0.25); }
.zone.db .zone-label { color: #60a5fa; }
/* ── Detail Panel ── */
.detail-panel {
position: fixed;
right: -480px;
top: 0;
width: 460px;
height: 100vh;
background: #111640;
border-left: 1px solid rgba(99,102,241,0.2);
z-index: 100;
transition: right 0.35s cubic-bezier(0.4, 0, 0.2, 1);
overflow-y: auto;
padding: 24px;
}
.detail-panel.open { right: 0; }
.detail-panel .close-btn {
position: absolute;
top: 16px; right: 16px;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
color: #94a3b8;
width: 32px; height: 32px;
border-radius: 8px;
cursor: pointer;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.detail-panel .close-btn:hover { background: rgba(255,255,255,0.1); color: #fff; }
.detail-title {
font-size: 20px;
font-weight: 700;
margin-bottom: 4px;
}
.detail-subtitle {
font-size: 13px;
color: #64748b;
margin-bottom: 20px;
}
.detail-section {
margin-bottom: 20px;
}
.detail-section h3 {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
color: #818cf8;
margin-bottom: 10px;
}
.endpoint-list {
list-style: none;
font-size: 12px;
font-family: 'SF Mono', 'Fira Code', monospace;
}
.endpoint-list li {
padding: 5px 0;
border-bottom: 1px solid rgba(255,255,255,0.04);
display: flex;
align-items: center;
gap: 8px;
}
.method {
display: inline-block;
padding: 1px 6px;
border-radius: 4px;
font-size: 10px;
font-weight: 700;
min-width: 38px;
text-align: center;
}
.method.get { background: rgba(52,211,153,0.15); color: #34d399; }
.method.post { background: rgba(251,191,36,0.15); color: #fbbf24; }
.method.put { background: rgba(96,165,250,0.15); color: #60a5fa; }
.method.delete { background: rgba(251,113,133,0.15); color: #fb7185; }
.method.patch { background: rgba(168,85,247,0.15); color: #a855f7; }
.method.grpc { background: rgba(129,140,248,0.15); color: #818cf8; }
.method.ws { background: rgba(6,182,212,0.15); color: #06b6d4; }
.table-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 6px;
font-size: 11px;
font-family: 'SF Mono', 'Fira Code', monospace;
margin: 2px;
}
.table-badge.ilm { background: rgba(52,211,153,0.1); color: #34d399; border: 1px solid rgba(52,211,153,0.2); }
.table-badge.sys { background: rgba(251,113,133,0.1); color: #fb7185; border: 1px solid rgba(251,113,133,0.2); }
.table-badge.inv { background: rgba(251,191,36,0.1); color: #fbbf24; border: 1px solid rgba(251,191,36,0.2); }
.flow-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 10px;
border-radius: 20px;
font-size: 11px;
margin: 2px;
}
.flow-badge.read { background: rgba(52,211,153,0.1); color: #6ee7b7; }
.flow-badge.write { background: rgba(251,113,133,0.1); color: #fca5a5; }
.flow-badge.grpc { background: rgba(129,140,248,0.1); color: #a5b4fc; }
.flow-badge.http { background: rgba(251,191,36,0.1); color: #fcd34d; }
/* ── Overlay ── */
.overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.4);
z-index: 99;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
}
.overlay.show { opacity: 1; pointer-events: auto; }
/* ── Animations ── */
@keyframes pulse {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}
@keyframes flowDash {
to { stroke-dashoffset: -20; }
}
.animated-flow {
stroke-dasharray: 8 6;
animation: flowDash 0.8s linear infinite;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.service-card { animation: fadeIn 0.5s ease both; }
.service-card:nth-child(1) { animation-delay: 0.05s; }
.service-card:nth-child(2) { animation-delay: 0.1s; }
.service-card:nth-child(3) { animation-delay: 0.15s; }
.service-card:nth-child(4) { animation-delay: 0.2s; }
.service-card:nth-child(5) { animation-delay: 0.25s; }
/* ── Sub layout ── */
.services-col { display: flex; flex-direction: column; gap: 12px; }
.frontend-col {
grid-row: 1 / 3;
display: flex;
flex-direction: column;
gap: 16px;
padding-top: 40px;
}
.iloom-zone { grid-column: 2; grid-row: 1 / 2; }
.muyu-zone { grid-column: 3; grid-row: 1 / 2; }
.db-zone { grid-column: 1 / 4; grid-row: 3; }
.iloom-services {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.iloom-gateway { grid-column: 1 / 3; }
.muyu-services { display: flex; flex-direction: column; gap: 12px; }
.db-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 16px;
}
/* ── Flow Arrows between zones ── */
.flow-label {
position: absolute;
font-size: 10px;
font-weight: 600;
padding: 3px 10px;
border-radius: 12px;
white-space: nowrap;
z-index: 5;
pointer-events: none;
}
/* ── Data flow diagram ── */
.dataflow-section {
max-width: 1440px;
margin: 40px auto;
padding: 0 20px;
}
.dataflow-title {
font-size: 22px;
font-weight: 700;
text-align: center;
margin-bottom: 24px;
background: linear-gradient(135deg, #818cf8, #6ee7b7);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.flow-diagram {
display: flex;
flex-direction: column;
gap: 2px;
background: rgba(30,35,60,0.5);
border-radius: 16px;
padding: 24px;
border: 1px solid rgba(99,102,241,0.15);
}
.flow-row {
display: flex;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid rgba(255,255,255,0.03);
}
.flow-row:last-child { border-bottom: none; }
.flow-row-header {
display: contents;
}
.flow-row-head {
font-weight: 700;
color: #64748b;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1px;
}
.flow-from {
width: 160px;
flex-shrink: 0;
font-size: 13px;
font-weight: 600;
color: #e2e8f0;
}
.flow-arrow {
width: 80px;
flex-shrink: 0;
text-align: center;
position: relative;
}
.flow-arrow::before {
content: '';
position: absolute;
top: 50%;
left: 10px;
right: 10px;
height: 2px;
border-radius: 1px;
}
.flow-arrow::after {
content: '';
position: absolute;
top: 50%; right: 8px;
width: 0; height: 0;
border-left: 6px solid;
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
transform: translateY(-50%);
}
.flow-arrow.green::before { background: #34d399; }
.flow-arrow.green::after { border-left-color: #34d399; }
.flow-arrow.pink::before { background: #fb7185; }
.flow-arrow.pink::after { border-left-color: #fb7185; }
.flow-arrow.blue::before { background: #60a5fa; }
.flow-arrow.blue::after { border-left-color: #60a5fa; }
.flow-arrow.purple::before { background: #a855f7; }
.flow-arrow.purple::after { border-left-color: #a855f7; }
.flow-arrow.yellow::before { background: #fbbf24; }
.flow-arrow.yellow::after { border-left-color: #fbbf24; }
.flow-to {
width: 160px;
flex-shrink: 0;
font-size: 13px;
font-weight: 600;
color: #e2e8f0;
}
.flow-desc {
flex: 1;
font-size: 12px;
color: #94a3b8;
padding-left: 16px;
}
.flow-protocol {
width: 70px;
flex-shrink: 0;
text-align: center;
}
/* ── JWT Flow ── */
.jwt-flow {
display: flex;
align-items: stretch;
gap: 0;
margin: 32px 0;
overflow-x: auto;
padding-bottom: 10px;
}
.jwt-step {
flex: 1;
min-width: 180px;
background: rgba(30,35,60,0.85);
border: 1px solid rgba(99,102,241,0.15);
border-radius: 12px;
padding: 16px;
position: relative;
text-align: center;
}
.jwt-step:not(:last-child)::after {
content: '→';
position: absolute;
right: -14px;
top: 50%;
transform: translateY(-50%);
font-size: 20px;
color: #818cf8;
z-index: 3;
}
.jwt-step-num {
width: 28px; height: 28px;
border-radius: 50%;
background: rgba(129,140,248,0.2);
color: #818cf8;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 700;
margin: 0 auto 8px;
}
.jwt-step-title {
font-size: 13px;
font-weight: 600;
color: #e2e8f0;
margin-bottom: 6px;
}
.jwt-step-desc {
font-size: 11px;
color: #64748b;
line-height: 1.5;
}
.jwt-step code {
background: rgba(129,140,248,0.1);
padding: 1px 5px;
border-radius: 4px;
font-size: 10px;
color: #a5b4fc;
}
/* ── Tables section ── */
.tables-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-top: 16px;
}
.table-group {
background: rgba(30,35,60,0.6);
border-radius: 12px;
padding: 16px;
border: 1px solid rgba(99,102,241,0.1);
}
.table-group h4 {
font-size: 13px;
font-weight: 600;
margin-bottom: 10px;
padding-bottom: 6px;
border-bottom: 1px solid rgba(255,255,255,0.06);
}
.table-group ul {
list-style: none;
font-size: 11px;
font-family: 'SF Mono', 'Fira Code', monospace;
color: #94a3b8;
}
.table-group ul li {
padding: 3px 0;
display: flex;
align-items: center;
gap: 6px;
}
.table-group ul li::before {
content: '';
width: 6px; height: 6px;
border-radius: 2px;
flex-shrink: 0;
}
.table-group.ilm ul li::before { background: #34d399; }
.table-group.sys ul li::before { background: #fb7185; }
.table-group.inv ul li::before { background: #fbbf24; }
/* ── Responsive: Tablet ── */
@media (max-width: 1100px) {
.arch-grid {
grid-template-columns: 1fr 1fr;
grid-template-rows: auto;
min-height: auto;
}
.frontend-col {
grid-row: auto;
grid-column: 1 / 3;
flex-direction: row;
padding-top: 0;
gap: 12px;
}
.frontend-col .service-card { flex: 1; }
.iloom-zone { grid-column: 1; grid-row: auto; }
.muyu-zone { grid-column: 2; grid-row: auto; }
.db-zone { grid-column: 1 / 3; grid-row: auto; }
.db-grid { grid-template-columns: 1fr 1fr; }
.detail-panel { width: 420px; right: -420px; }
}
/* ── Responsive: Mobile ── */
@media (max-width: 768px) {
html { scroll-behavior: smooth; }
body { -webkit-text-size-adjust: 100%; }
.header { padding: 20px 16px 12px; }
.header h1 { font-size: 20px; }
.header p { font-size: 12px; }
.legend {
gap: 10px 16px;
padding: 10px 16px;
justify-content: flex-start;
overflow-x: auto;
flex-wrap: nowrap;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.legend::-webkit-scrollbar { display: none; }
.legend-item { flex-shrink: 0; font-size: 11px; }
.canvas { padding: 12px; }
/* ── Hide SVG on mobile ── */
.connections-svg { display: none; }
.arch-grid {
display: flex;
flex-direction: column;
gap: 16px;
min-height: auto;
}
.frontend-col {
flex-direction: row;
gap: 10px;
padding-top: 0;
}
.frontend-col .service-card { flex: 1; min-width: 0; }
.zone { padding: 12px; border-radius: 12px; }
.zone-label { font-size: 10px; top: -9px; }
.iloom-services {
grid-template-columns: 1fr;
}
.iloom-gateway { grid-column: 1; }
.service-card {
padding: 12px;
border-radius: 10px;
min-height: 44px;
}
.card-header { gap: 8px; margin-bottom: 6px; }
.card-icon { width: 32px; height: 32px; font-size: 16px; border-radius: 8px; }
.card-title { font-size: 13px; }
.card-port { font-size: 10px; }
.card-tag { font-size: 9px; padding: 2px 6px; }
.db-grid { grid-template-columns: 1fr; gap: 10px; }
.table-group { padding: 12px; }
.table-group h4 { font-size: 12px; }
/* ── Mobile flow arrows between zones ── */
.zone::after {
content: '▼';
display: block;
text-align: center;
color: rgba(99,102,241,0.3);
font-size: 20px;
margin-top: 8px;
}
.zone:last-child::after,
.db-zone::after { display: none; }
/* ── Detail Panel: Bottom Sheet ── */
.detail-panel {
width: 100%;
height: 85vh;
max-height: 85vh;
top: auto;
bottom: 0;
right: 0;
left: 0;
transform: translateY(100%);
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 20px 20px 0 0;
border-left: none;
border-top: 1px solid rgba(99,102,241,0.3);
padding: 16px;
padding-top: 8px;
}
.detail-panel.open {
transform: translateY(0);
right: 0;
}
.detail-panel::before {
content: '';
display: block;
width: 40px;
height: 4px;
background: rgba(255,255,255,0.2);
border-radius: 2px;
margin: 0 auto 12px;
}
.detail-panel .close-btn {
top: 12px; right: 12px;
width: 36px; height: 36px;
}
.detail-title { font-size: 17px; padding-right: 44px; }
.detail-subtitle { font-size: 12px; margin-bottom: 14px; }
.detail-section { margin-bottom: 14px; }
.endpoint-list { font-size: 11px; }
.endpoint-list li {
padding: 6px 0;
flex-wrap: wrap;
gap: 4px 8px;
line-height: 1.5;
}
.method { font-size: 9px; min-width: 32px; padding: 1px 4px; }
.table-badge { font-size: 10px; padding: 2px 6px; }
.flow-badge { font-size: 10px; padding: 2px 8px; }
/* ── Dataflow section ── */
.dataflow-section { padding: 0 12px; margin: 24px auto; }
.dataflow-title { font-size: 18px; margin-bottom: 16px; }
/* ── JWT Flow: Vertical ── */
.jwt-flow {
flex-direction: column;
gap: 8px;
padding-bottom: 0;
}
.jwt-step {
min-width: auto;
display: flex;
align-items: flex-start;
gap: 12px;
text-align: left;
padding: 12px;
}
.jwt-step:not(:last-child)::after {
content: '↓';
position: static;
transform: none;
font-size: 16px;
color: #818cf8;
position: absolute;
bottom: -16px;
left: 50%;
transform: translateX(-50%);
}
.jwt-step-num {
margin: 0;
flex-shrink: 0;
width: 32px;
height: 32px;
}
.jwt-step-body { flex: 1; min-width: 0; }
.jwt-step-title { font-size: 13px; margin-bottom: 4px; }
.jwt-step-desc { font-size: 11px; }
.jwt-step code { font-size: 9px; word-break: break-all; }
/* ── Flow Diagram: Card layout ── */
.flow-diagram { padding: 12px; border-radius: 12px; gap: 0; }
.flow-row {
flex-direction: column;
align-items: stretch;
padding: 0;
gap: 0;
border-bottom: none;
}
.flow-row-head { display: none; }
.flow-row-header {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.flow-from, .flow-to {
width: auto;
font-size: 12px;
}
.flow-arrow {
width: 32px;
height: auto;
flex-shrink: 0;
}
.flow-arrow::before {
top: 50%;
left: 2px;
right: 8px;
height: 2px;
}
.flow-arrow::after {
top: 50%;
right: 2px;
left: auto;
transform: translateY(-50%);
}
.flow-protocol { width: auto; text-align: left; }
.flow-desc {
padding-left: 0;
padding-top: 4px;
font-size: 11px;
line-height: 1.5;
}
.flow-desc code { font-size: 10px; word-break: break-all; }
/* ── Flow Row as mini-card ── */
.flow-row:not(.flow-row-head) {
background: rgba(255,255,255,0.02);
border-radius: 10px;
padding: 12px;
margin-bottom: 8px;
border-bottom: none;
}
.section-title { font-size: 12px; letter-spacing: 1px; }
}
/* ── Small phones ── */
@media (max-width: 380px) {
.header h1 { font-size: 17px; }
.frontend-col { flex-direction: column; }
.card-title { font-size: 12px; }
.detail-panel { height: 90vh; max-height: 90vh; }
}
</style>
</head>
<body>
<div class="header">
<h1>iloom × muyu-apiserver 系统架构</h1>
<p>纺织供应链管理 · 微服务架构 · 点击服务卡片查看详情</p>
</div>
<div class="legend">
<div class="legend-item"><div class="legend-dot" style="background:#fbbf24"></div>Frontend</div>
<div class="legend-item"><div class="legend-dot" style="background:#818cf8"></div>Gateway</div>
<div class="legend-item"><div class="legend-dot" style="background:#34d399"></div>iloom 服务</div>
<div class="legend-item"><div class="legend-dot" style="background:#fb7185"></div>muyu 服务</div>
<div class="legend-item"><div class="legend-dot" style="background:#60a5fa"></div>数据层</div>
<div class="legend-item"><div class="legend-line animated-flow" style="background:none;border-top:2px dashed #34d399;"></div>HTTP</div>
<div class="legend-item"><div class="legend-line" style="background:#818cf8;"></div>gRPC</div>
<div class="legend-item"><div class="legend-line animated-flow" style="background:none;border-top:2px dashed #60a5fa;"></div>SQL</div>
</div>
<!-- ════════════════════════════════════════ -->
<!-- MAIN ARCHITECTURE DIAGRAM -->
<!-- ════════════════════════════════════════ -->
<div class="canvas" id="canvas">
<svg class="connections-svg" id="connSvg"></svg>
<div class="arch-grid">
<!-- ── Column 1: Frontend ── -->
<div class="frontend-col">
<div class="service-card theme-frontend" data-service="frontend" id="card-frontend">
<div class="card-header">
<div class="card-icon"></div>
<div>
<div class="card-title">React SPA</div>
<div class="card-port">:3015</div>
</div>
</div>
<div class="card-tag" style="background:rgba(251,191,36,0.12);color:#fbbf24;">Frontend</div>
</div>
<div class="service-card theme-frontend" data-service="muyu-portal" id="card-muyu-portal" style="opacity:0.65">
<div class="card-header">
<div class="card-icon" style="font-size:14px">🖥</div>
<div>
<div class="card-title">muyu-portal</div>
<div class="card-port">:8080 (Nginx)</div>
</div>
</div>
<div class="card-tag" style="background:rgba(251,113,133,0.12);color:#fb7185;">muyu 前端</div>
</div>
</div>
<!-- ── Column 2: iloom Services ── -->
<div class="zone iloom" id="zone-iloom">
<span class="zone-label">iloom 微服务集群</span>
<div class="iloom-services">
<div class="service-card theme-gateway iloom-gateway" data-service="gateway" id="card-gateway">
<div class="card-header">
<div class="card-icon">🔀</div>
<div>
<div class="card-title">API Gateway</div>
<div class="card-port">:8080</div>
</div>
</div>
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:4px;">
<div class="card-tag" style="background:rgba(129,140,248,0.12);color:#818cf8;">JWT 验证</div>
<div class="card-tag" style="background:rgba(129,140,248,0.12);color:#818cf8;">CORS</div>
<div class="card-tag" style="background:rgba(129,140,248,0.12);color:#818cf8;">路由分发</div>
<div class="card-tag" style="background:rgba(129,140,248,0.12);color:#818cf8;">限流</div>
</div>
</div>
<div class="service-card theme-iloom" data-service="auth" id="card-auth">
<div class="card-header">
<div class="card-icon">🔐</div>
<div>
<div class="card-title">Auth Service</div>
<div class="card-port">:8081</div>
</div>
</div>
<div class="card-tag" style="background:rgba(52,211,153,0.12);color:#34d399;">认证 · 公司 · 成员</div>
</div>
<div class="service-card theme-iloom" data-service="purchaser" id="card-purchaser">
<div class="card-header">
<div class="card-icon">🛒</div>
<div>
<div class="card-title">Purchaser Service</div>
<div class="card-port">:8082</div>
</div>
</div>
<div class="card-tag" style="background:rgba(52,211,153,0.12);color:#34d399;">采购 · 计划 · 产品</div>
</div>
<div class="service-card theme-iloom" data-service="textile" id="card-textile">
<div class="card-header">
<div class="card-icon">🧵</div>
<div>
<div class="card-title">Textile Service</div>
<div class="card-port">:8083</div>
</div>
</div>
<div class="card-tag" style="background:rgba(52,211,153,0.12);color:#34d399;">纺织 · 纱线 · 工序</div>
</div>
<div class="service-card theme-iloom" data-service="washing" id="card-washing">
<div class="card-header">
<div class="card-icon">💧</div>
<div>
<div class="card-title">Washing Service</div>
<div class="card-port">:8084</div>
</div>
</div>
<div class="card-tag" style="background:rgba(52,211,153,0.12);color:#34d399;">水洗 · 成品 · 出库</div>
</div>
</div>
</div>
<!-- ── Column 3: muyu Services ── -->
<div class="zone muyu" id="zone-muyu">
<span class="zone-label">muyu-apiserver</span>
<div class="muyu-services">
<div class="service-card theme-muyu" data-service="muyu-gateway" id="card-muyu-gw">
<div class="card-header">
<div class="card-icon">🌐</div>
<div>
<div class="card-title">muyu Gateway</div>
<div class="card-port">:8888 (REST)</div>
</div>
</div>
<div class="card-tag" style="background:rgba(251,113,133,0.12);color:#fb7185;">go-zero API</div>
</div>
<div class="service-card theme-muyu" data-service="system-rpc" id="card-system-rpc">
<div class="card-header">
<div class="card-icon">👤</div>
<div>
<div class="card-title">System RPC</div>
<div class="card-port">:9001 (gRPC)</div>
</div>
</div>
<div style="display:flex;gap:4px;flex-wrap:wrap;margin-top:4px;">
<div class="card-tag" style="background:rgba(251,113,133,0.12);color:#fb7185;">用户</div>
<div class="card-tag" style="background:rgba(251,113,133,0.12);color:#fb7185;">角色</div>
<div class="card-tag" style="background:rgba(251,113,133,0.12);color:#fb7185;">菜单</div>
<div class="card-tag" style="background:rgba(251,113,133,0.12);color:#fb7185;">权限</div>
</div>
</div>
<div class="service-card theme-muyu" data-service="inventory-rpc" id="card-inv-rpc">
<div class="card-header">
<div class="card-icon">📦</div>
<div>
<div class="card-title">Inventory RPC</div>
<div class="card-port">:9002 (gRPC)</div>
</div>
</div>
<div style="display:flex;gap:4px;flex-wrap:wrap;margin-top:4px;">
<div class="card-tag" style="background:rgba(251,113,133,0.12);color:#fb7185;">产品</div>
<div class="card-tag" style="background:rgba(251,113,133,0.12);color:#fb7185;">库存</div>
<div class="card-tag" style="background:rgba(251,113,133,0.12);color:#fb7185;">盘点</div>
</div>
</div>
<div style="display:flex;gap:10px;">
<div class="service-card theme-infra" style="flex:1" id="card-redis">
<div class="card-header">
<div class="card-icon" style="width:28px;height:28px;font-size:14px;"></div>
<div>
<div class="card-title" style="font-size:12px;">Redis</div>
<div class="card-port">:6379</div>
</div>
</div>
</div>
<div class="service-card theme-infra" style="flex:1" id="card-etcd">
<div class="card-header">
<div class="card-icon" style="width:28px;height:28px;font-size:14px;">🔧</div>
<div>
<div class="card-title" style="font-size:12px;">etcd</div>
<div class="card-port">:2379</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ── Row 3: Database ── -->
<div class="zone db db-zone" id="zone-db">
<span class="zone-label">MySQL 8.0 · muyu_wms · :3306</span>
<div class="db-grid">
<div class="table-group ilm">
<h4 style="color:#34d399;">ilm_* 表 (iloom 拥有 · 直接 SQL 读写)</h4>
<ul>
<li>ilm_company <span style="color:#475569">— 公司</span></li>
<li>ilm_company_member <span style="color:#475569">— 成员</span></li>
<li>ilm_company_relationship <span style="color:#475569">— 公司关系</span></li>
<li>ilm_product_ext <span style="color:#475569">— 产品扩展</span></li>
<li>ilm_production_plan <span style="color:#475569">— 生产计划</span></li>
<li>ilm_plan_factory <span style="color:#475569">— 计划工厂</span></li>
<li>ilm_yarn_ratio <span style="color:#475569">— 纱线配比</span></li>
<li>ilm_process_step <span style="color:#475569">— 生产工序</span></li>
<li>ilm_warehouse <span style="color:#475569">— 仓库</span></li>
<li>ilm_inventory_record <span style="color:#475569">— 库存记录</span></li>
</ul>
</div>
<div class="table-group ilm">
<h4 style="color:#34d399;">ilm_* 表 (续)</h4>
<ul>
<li>ilm_product_inventory <span style="color:#475569">— 产品出入库</span></li>
<li>ilm_product_outbound <span style="color:#475569">— 产品出库</span></li>
<li>ilm_washing_plan <span style="color:#475569">— 水洗计划</span></li>
<li>ilm_washing_step <span style="color:#475569">— 水洗工序</span></li>
<li>ilm_washing_completion <span style="color:#475569">— 水洗完工</span></li>
<li>ilm_finished_product <span style="color:#475569">— 成品</span></li>
<li>ilm_finished_product_inventory <span style="color:#475569">— 成品出入库</span></li>
<li>ilm_yarn_stock <span style="color:#475569">— 纱线库存</span></li>
<li>ilm_yarn_stock_record <span style="color:#475569">— 纱线记录</span></li>
<li>ilm_accounts_payable <span style="color:#475569">— 应付账款</span></li>
<li>ilm_ap_item <span style="color:#475569">— 账款明细</span></li>
<li>ilm_payment <span style="color:#475569">— 收付款</span></li>
<li>ilm_notification <span style="color:#475569">— 通知</span></li>
<li>ilm_share_link <span style="color:#475569">— 分享链接</span></li>
<li>ilm_audit_log <span style="color:#475569">— 审计日志</span></li>
<li>ilm_product_price_history <span style="color:#475569">— 产品价格历史</span></li>
<li>ilm_production_price_history <span style="color:#475569">— 生产价格历史</span></li>
</ul>
</div>
<div class="table-group sys">
<h4 style="color:#fb7185;">sys_* / inv_* 表 (muyu 拥有 · 只读, 写走 gRPC)</h4>
<ul>
<li>sys_user <span style="color:#475569">— 用户</span></li>
<li>sys_role <span style="color:#475569">— 角色</span></li>
<li>sys_user_role <span style="color:#475569">— 用户角色绑定</span></li>
<li>sys_menu <span style="color:#475569">— 菜单</span></li>
<li>sys_role_menu <span style="color:#475569">— 角色菜单权限</span></li>
<li>sys_operation_log <span style="color:#475569">— 操作日志</span></li>
<li>sys_config <span style="color:#475569">— 系统配置</span></li>
<li style="margin-top:8px;padding-top:8px;border-top:1px solid rgba(255,255,255,0.06);">inv_product <span style="color:#475569">— 产品</span></li>
<li>inv_stock_check <span style="color:#475569">— 盘点</span></li>
<li>inv_stock_adjust <span style="color:#475569">— 调整</span></li>
<li style="margin-top:8px;padding-top:8px;border-top:1px solid rgba(255,255,255,0.06);">casbin_rule <span style="color:#475569">— RBAC 策略</span></li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- ════════════════════════════════════════ -->
<!-- DATA FLOW DIAGRAM -->
<!-- ════════════════════════════════════════ -->
<div class="dataflow-section">
<div class="dataflow-title">数据流向 · 接口调用一览</div>
<!-- JWT Flow -->
<div style="margin-bottom:32px;">
<div class="section-title iloom" style="text-align:center;">JWT 认证流程</div>
<div class="jwt-flow">
<div class="jwt-step">
<div class="jwt-step-num">1</div>
<div class="jwt-step-body">
<div class="jwt-step-title">用户登录</div>
<div class="jwt-step-desc">Frontend → Gateway<br><code>POST /api/v1/auth/login</code><br>{username, password}</div>
</div>
</div>
<div class="jwt-step">
<div class="jwt-step-num">2</div>
<div class="jwt-step-body">
<div class="jwt-step-title">gRPC 验证</div>
<div class="jwt-step-desc">Auth Service → System RPC<br><code>SystemService.Login()</code><br>muyu 验证密码</div>
</div>
</div>
<div class="jwt-step">
<div class="jwt-step-num">3</div>
<div class="jwt-step-body">
<div class="jwt-step-title">查询公司</div>
<div class="jwt-step-desc">Auth Service → MySQL<br><code>sys_user JOIN ilm_company_member</code><br>获取 company_id, role</div>
</div>
</div>
<div class="jwt-step">
<div class="jwt-step-num">4</div>
<div class="jwt-step-body">
<div class="jwt-step-title">签发 Token</div>
<div class="jwt-step-desc">JWT Claims:<br><code>{uid, cid, role, master, uname}</code><br>Secret: <code>muyu-wms-jwt-secret-key-2026</code></div>
</div>
</div>
<div class="jwt-step">
<div class="jwt-step-num">5</div>
<div class="jwt-step-body">
<div class="jwt-step-title">请求鉴权</div>
<div class="jwt-step-desc">Gateway 验证 JWT → 注入 Headers<br><code>X-User-ID</code> <code>X-Company-ID</code><br><code>X-User-Role</code> <code>X-Is-Master</code></div>
</div>
</div>
</div>
</div>
<!-- Main Data Flow -->
<div class="section-title iloom" style="text-align:center;">服务间调用 · 数据流向</div>
<div class="flow-diagram">
<div class="flow-row flow-row-head">
<div class="flow-from">调用方</div>
<div class="flow-arrow" style="visibility:hidden"></div>
<div class="flow-to">目标</div>
<div class="flow-protocol">协议</div>
<div class="flow-desc">说明</div>
</div>
<div class="flow-row">
<div class="flow-row-header">
<div class="flow-from" style="color:#fbbf24;">React SPA</div>
<div class="flow-arrow green"></div>
<div class="flow-to" style="color:#818cf8;">Gateway :8080</div>
<div class="flow-protocol"><span class="method get">HTTP</span></div>
</div>
<div class="flow-desc">所有 API 请求 + WebSocket 升级,<code>Authorization: Bearer &lt;token&gt;</code></div>
</div>
<div class="flow-row">
<div class="flow-row-header">
<div class="flow-from" style="color:#818cf8;">Gateway</div>
<div class="flow-arrow green"></div>
<div class="flow-to" style="color:#34d399;">Auth Service</div>
<div class="flow-protocol"><span class="method post">HTTP</span></div>
</div>
<div class="flow-desc"><code>/api/v1/auth/*</code>, <code>/api/v1/common/*</code> → 反向代理到 :8081</div>
</div>
<div class="flow-row">
<div class="flow-row-header">
<div class="flow-from" style="color:#818cf8;">Gateway</div>
<div class="flow-arrow green"></div>
<div class="flow-to" style="color:#34d399;">Purchaser Svc</div>
<div class="flow-protocol"><span class="method post">HTTP</span></div>
</div>
<div class="flow-desc"><code>/api/v1/purchaser/*</code> → :8082 (role=purchaser)</div>
</div>
<div class="flow-row">
<div class="flow-row-header">
<div class="flow-from" style="color:#818cf8;">Gateway</div>
<div class="flow-arrow green"></div>
<div class="flow-to" style="color:#34d399;">Textile Svc</div>
<div class="flow-protocol"><span class="method post">HTTP</span></div>
</div>
<div class="flow-desc"><code>/api/v1/textile/*</code> → :8083 (role=textile)</div>
</div>
<div class="flow-row">
<div class="flow-row-header">
<div class="flow-from" style="color:#818cf8;">Gateway</div>
<div class="flow-arrow green"></div>
<div class="flow-to" style="color:#34d399;">Washing Svc</div>
<div class="flow-protocol"><span class="method post">HTTP</span></div>
</div>
<div class="flow-desc"><code>/api/v1/washing/*</code> → :8084 (role=washing)</div>
</div>
<div class="flow-row" style="background:rgba(129,140,248,0.04);">
<div class="flow-row-header">
<div class="flow-from" style="color:#34d399;">Auth Service</div>
<div class="flow-arrow purple"></div>
<div class="flow-to" style="color:#fb7185;">System RPC</div>
<div class="flow-protocol"><span class="method grpc">gRPC</span></div>
</div>
<div class="flow-desc"><code>Login()</code> 密码验证, <code>CreateUser()</code> 注册, <code>GetUser()</code> 查询</div>
</div>
<div class="flow-row" style="background:rgba(129,140,248,0.04);">
<div class="flow-row-header">
<div class="flow-from" style="color:#34d399;">Purchaser Svc</div>
<div class="flow-arrow purple"></div>
<div class="flow-to" style="color:#fb7185;">Inventory RPC</div>
<div class="flow-protocol"><span class="method grpc">gRPC</span></div>
</div>
<div class="flow-desc"><code>CreateProduct()</code>, <code>GetProduct()</code>, <code>ListProduct()</code> 产品CRUD</div>
</div>
<div class="flow-row">
<div class="flow-row-header">
<div class="flow-from" style="color:#34d399;">Textile Svc</div>
<div class="flow-arrow yellow"></div>
<div class="flow-to" style="color:#34d399;">Purchaser Svc</div>
<div class="flow-protocol"><span class="method get">HTTP</span></div>
</div>
<div class="flow-desc">内部服务调用 — 查询采购商计划、工厂信息</div>
</div>
<div class="flow-row">
<div class="flow-row-header">
<div class="flow-from" style="color:#34d399;">Washing Svc</div>
<div class="flow-arrow yellow"></div>
<div class="flow-to" style="color:#34d399;">Purchaser Svc</div>
<div class="flow-protocol"><span class="method get">HTTP</span></div>
</div>
<div class="flow-desc">内部服务调用 — 查询采购商计划、工厂信息</div>
</div>
<div class="flow-row" style="background:rgba(96,165,250,0.04);">
<div class="flow-row-header">
<div class="flow-from" style="color:#34d399;">iloom 全部服务</div>
<div class="flow-arrow blue"></div>
<div class="flow-to" style="color:#60a5fa;">MySQL</div>
<div class="flow-protocol"><span class="method" style="background:rgba(96,165,250,0.15);color:#60a5fa;">SQL</span></div>
</div>
<div class="flow-desc"><code>ilm_*</code> 表直接读写;<code>sys_user</code> 只读查询 (JOIN)</div>
</div>
<div class="flow-row" style="background:rgba(96,165,250,0.04);">
<div class="flow-row-header">
<div class="flow-from" style="color:#fb7185;">muyu RPC 服务</div>
<div class="flow-arrow blue"></div>
<div class="flow-to" style="color:#60a5fa;">MySQL</div>
<div class="flow-protocol"><span class="method" style="background:rgba(96,165,250,0.15);color:#60a5fa;">SQL</span></div>
</div>
<div class="flow-desc"><code>sys_*</code>, <code>inv_*</code>, <code>casbin_rule</code> 全权读写</div>
</div>
<div class="flow-row" style="background:rgba(168,85,247,0.04);">
<div class="flow-row-header">
<div class="flow-from" style="color:#fb7185;">muyu RPC 服务</div>
<div class="flow-arrow" style="visibility:hidden"></div>
<div class="flow-to" style="color:#a855f7;">Redis / etcd</div>
<div class="flow-protocol"><span class="method" style="background:rgba(168,85,247,0.15);color:#a855f7;">TCP</span></div>
</div>
<div class="flow-desc">Redis: 缓存 + 会话 | etcd: 服务注册发现</div>
</div>
</div>
</div>
<!-- ════════════════════════════════════════ -->
<!-- DETAIL PANEL -->
<!-- ════════════════════════════════════════ -->
<div class="overlay" id="overlay"></div>
<div class="detail-panel" id="detailPanel">
<button class="close-btn" id="closeBtn"></button>
<div id="detailContent"></div>
</div>
<script>
// ── Service Detail Data ──
const serviceData = {
frontend: {
title: 'React SPA (iloom Frontend)',
subtitle: ':3015 · React 18 + TailwindCSS + Zustand',
sections: [
{ title: 'Tech Stack', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="table-badge ilm">React 18.3</span><span class="table-badge ilm">Webpack 5</span><span class="table-badge ilm">TailwindCSS</span><span class="table-badge ilm">Zustand</span><span class="table-badge ilm">React Router v6</span><span class="table-badge ilm">Recharts</span><span class="table-badge ilm">Framer Motion</span></div>' },
{ title: 'API 模块', content: '<ul class="endpoint-list"><li><span class="method post">AUTH</span> authApi — 登录/注册/刷新/用户信息</li><li><span class="method get">API</span> purchaserApi — 采购商全部接口</li><li><span class="method get">API</span> textileApi — 纺织厂全部接口</li><li><span class="method get">API</span> washingApi — 水洗厂全部接口</li><li><span class="method get">API</span> commonApi — 公司/成员/通知</li><li><span class="method ws">WS</span> WebSocket 实时推送</li></ul>' },
{ title: '认证机制', content: '<div style="font-size:12px;color:#94a3b8;line-height:1.8">• Access Token 存内存<br>• Refresh Token 存 Cookie (credentials: include)<br>• 401 自动刷新 + 重试<br>• Token 过期触发 auth:expired 事件 → 跳转登录</div>' }
]
},
gateway: {
title: 'API Gateway',
subtitle: ':8080 · Gin + 反向代理',
sections: [
{ title: '中间件链', content: '<ul class="endpoint-list"><li>RequestID() — 请求追踪</li><li>Logger() — 请求日志</li><li>Recovery() — Panic 恢复</li><li>CORS() — 跨域 (ALLOW_ORIGINS)</li><li>RateLimit() — 限流 100 req/s</li><li>JWTAuth() — JWT 验证 + Claims 提取</li></ul>' },
{ title: '路由规则 (JWT 验证后注入 Headers)', content: '<ul class="endpoint-list"><li><span class="method post">ANY</span>/api/v1/auth/* → auth-service:8081 <span style="color:#475569">(免 JWT)</span></li><li><span class="method post">ANY</span>/api/v1/common/* → auth-service:8081</li><li><span class="method post">ANY</span>/api/v1/purchaser/* → purchaser:8082 <span style="color:#475569">(role=purchaser)</span></li><li><span class="method post">ANY</span>/api/v1/textile/* → textile:8083 <span style="color:#475569">(role=textile)</span></li><li><span class="method post">ANY</span>/api/v1/washing/* → washing:8084 <span style="color:#475569">(role=washing)</span></li><li><span class="method ws">WS</span>/ws/v1/* → 对应服务 WebSocket</li></ul>' },
{ title: '注入的 Headers', content: '<div style="font-size:12px;font-family:monospace;color:#a5b4fc;line-height:2">X-User-ID: {claims.UserID}<br>X-Company-ID: {claims.CompanyID}<br>X-User-Role: {claims.Role}<br>X-Is-Master: {claims.IsMaster}<br>X-Username: {claims.Username}</div>' }
]
},
auth: {
title: 'Auth Service',
subtitle: ':8081 · 认证 + 公司管理 + 成员管理 + 通知',
sections: [
{ title: 'Auth 端点', content: '<ul class="endpoint-list"><li><span class="method post">POST</span>/auth/login — gRPC→SystemRPC.Login()→签发JWT</li><li><span class="method post">POST</span>/auth/register — gRPC→CreateUser()→建公司→建成员</li><li><span class="method post">POST</span>/auth/refresh — 刷新 access_token</li><li><span class="method get">GET</span>/auth/me — GetWithCompany() 用户+公司信息</li><li><span class="method post">POST</span>/auth/logout — 登出</li></ul>' },
{ title: 'Common 端点', content: '<ul class="endpoint-list"><li><span class="method get">GET</span>/common/companies — 公司关系列表</li><li><span class="method get">GET</span>/common/companies/:id — 公司详情</li><li><span class="method get">GET</span>/common/members — 公司成员列表</li><li><span class="method post">POST</span>/common/members — gRPC→CreateUser()+建成员</li><li><span class="method delete">DEL</span>/common/members/:id — 删除成员关系</li><li><span class="method get">GET</span>/common/notifications — 通知列表</li><li><span class="method patch">PATCH</span>/common/notifications/:id — 标记已读</li></ul>' },
{ title: 'gRPC 调用', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="flow-badge grpc">→ SystemRPC.Login()</span><span class="flow-badge grpc">→ SystemRPC.CreateUser()</span><span class="flow-badge grpc">→ SystemRPC.GetUser()</span></div>' },
{ title: '数据库访问', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="flow-badge read">读 sys_user</span><span class="flow-badge write">写 ilm_company</span><span class="flow-badge write">写 ilm_company_member</span><span class="flow-badge read">读 ilm_company_relationship</span><span class="flow-badge write">写 ilm_notification</span></div>' }
]
},
purchaser: {
title: 'Purchaser Service',
subtitle: ':8082 · 采购商:计划 + 产品 + 应付账款',
sections: [
{ title: '计划管理', content: '<ul class="endpoint-list"><li><span class="method get">GET</span>/plans — 计划列表</li><li><span class="method post">POST</span>/plans — 创建生产计划</li><li><span class="method get">GET</span>/plans/:id — 计划详情 (含工序/库存)</li><li><span class="method put">PUT</span>/plans/:id — 更新计划</li><li><span class="method delete">DEL</span>/plans/:id — 删除计划</li><li><span class="method post">POST</span>/plans/:id/factories — 分配工厂</li><li><span class="method get">GET</span>/plans/:id/steps — 工序步骤</li><li><span class="method get">GET</span>/plans/:id/inventory — 库存记录</li></ul>' },
{ title: '产品管理', content: '<ul class="endpoint-list"><li><span class="method get">GET</span>/products — 产品列表</li><li><span class="method post">POST</span>/products — 创建产品 (gRPC→inv_product + ilm_product_ext)</li><li><span class="method put">PUT</span>/products/:id — 更新产品</li><li><span class="method post">POST</span>/products/:id/inbound — 入库</li><li><span class="method post">POST</span>/products/:id/outbound — 出库</li><li><span class="method put">PUT</span>/products/:id/price — 更新价格</li><li><span class="method get">GET</span>/products/:id/price-history — 价格历史</li></ul>' },
{ title: '其他', content: '<ul class="endpoint-list"><li><span class="method get">GET</span>/dashboard/stats — 仪表盘统计</li><li><span class="method get">GET</span>/factories — 工厂列表</li><li><span class="method get">GET</span>/accounts-payable — 应付账款</li><li><span class="method post">POST</span>/accounts-payable/:id/pay — 付款</li><li><span class="method post">POST</span>/washing-plans — 创建水洗计划</li><li><span class="method ws">WS</span>/ws/v1/purchaser — 实时推送</li></ul>' },
{ title: 'gRPC 调用', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="flow-badge grpc">→ InventoryRPC.CreateProduct()</span><span class="flow-badge grpc">→ InventoryRPC.GetProduct()</span><span class="flow-badge grpc">→ InventoryRPC.ListProduct()</span></div>' },
{ title: '数据库表', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="table-badge ilm">ilm_production_plan</span><span class="table-badge ilm">ilm_plan_factory</span><span class="table-badge ilm">ilm_yarn_ratio</span><span class="table-badge ilm">ilm_process_step</span><span class="table-badge ilm">ilm_product_ext</span><span class="table-badge ilm">ilm_product_inventory</span><span class="table-badge ilm">ilm_product_outbound</span><span class="table-badge ilm">ilm_product_price_history</span><span class="table-badge ilm">ilm_accounts_payable</span><span class="table-badge ilm">ilm_ap_item</span><span class="table-badge ilm">ilm_washing_plan</span></div>' }
]
},
textile: {
title: 'Textile Service',
subtitle: ':8083 · 纺织厂:工序 + 纱线 + 库存 + 收款',
sections: [
{ title: 'API 端点', content: '<ul class="endpoint-list"><li><span class="method get">GET</span>/dashboard/stats — 仪表盘</li><li><span class="method get">GET</span>/plans — 生产计划 (只读)</li><li><span class="method get">GET</span>/plans/:id — 计划详情</li><li><span class="method post">POST</span>/plans/:id/steps/:stepId/complete — 完成工序</li><li><span class="method post">POST</span>/plans/:id/steps/:stepId/reject — 驳回工序</li><li><span class="method post">POST</span>/plans/:id/inventory — 入库记录</li><li><span class="method get">GET</span>/yarn-stock — 纱线库存</li><li><span class="method post">POST</span>/yarn-stock — 创建纱线</li><li><span class="method post">POST</span>/yarn-stock/:id/records — 纱线出入库</li><li><span class="method get">GET</span>/payments — 收款列表</li><li><span class="method post">POST</span>/payments/:id/confirm — 确认收款</li><li><span class="method ws">WS</span>/ws/v1/textile — 实时推送</li></ul>' },
{ title: '服务间调用', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="flow-badge http">→ Purchaser :8082 (HTTP)</span></div><div style="font-size:11px;color:#64748b;margin-top:4px">查询采购商计划、工厂信息</div>' },
{ title: '数据库表', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="table-badge ilm">ilm_production_plan</span><span class="table-badge ilm">ilm_process_step</span><span class="table-badge ilm">ilm_inventory_record</span><span class="table-badge ilm">ilm_warehouse</span><span class="table-badge ilm">ilm_yarn_stock</span><span class="table-badge ilm">ilm_yarn_stock_record</span><span class="table-badge ilm">ilm_payment</span></div>' }
]
},
washing: {
title: 'Washing Service',
subtitle: ':8084 · 水洗厂:水洗计划 + 成品 + 出库 + 收款',
sections: [
{ title: 'API 端点', content: '<ul class="endpoint-list"><li><span class="method get">GET</span>/dashboard/stats — 仪表盘</li><li><span class="method get">GET</span>/plans — 水洗计划列表</li><li><span class="method get">GET</span>/plans/pending — 待处理计划</li><li><span class="method get">GET</span>/plans/completed — 已完成计划</li><li><span class="method get">GET</span>/plans/:id — 计划详情</li><li><span class="method patch">PATCH</span>/plans/:id/status — 更新状态</li><li><span class="method post">POST</span>/plans/:id/complete — 标记完成</li><li><span class="method get">GET</span>/finished-products — 成品列表</li><li><span class="method post">POST</span>/finished-products — 创建成品</li><li><span class="method post">POST</span>/finished-products/:id/outbound — 成品出库</li><li><span class="method get">GET</span>/payments — 收款列表</li><li><span class="method post">POST</span>/payments/:id/confirm — 确认收款</li><li><span class="method ws">WS</span>/ws/v1/washing — 实时推送</li></ul>' },
{ title: '服务间调用', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="flow-badge http">→ Purchaser :8082 (HTTP)</span></div><div style="font-size:11px;color:#64748b;margin-top:4px">查询采购商计划、工厂信息</div>' },
{ title: '数据库表', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="table-badge ilm">ilm_washing_plan</span><span class="table-badge ilm">ilm_washing_step</span><span class="table-badge ilm">ilm_washing_completion</span><span class="table-badge ilm">ilm_finished_product</span><span class="table-badge ilm">ilm_finished_product_inventory</span><span class="table-badge ilm">ilm_payment</span></div>' }
]
},
'system-rpc': {
title: 'System RPC (muyu-apiserver)',
subtitle: ':9001 · go-zero gRPC · 用户/角色/权限管理',
sections: [
{ title: 'gRPC 方法', content: '<ul class="endpoint-list"><li><span class="method grpc">RPC</span>Login(username, password, ip) → LoginResp</li><li><span class="method grpc">RPC</span>CreateUser(username, password, realName, phone, roleID) → IdResp</li><li><span class="method grpc">RPC</span>GetUser(userID) → UserInfo</li><li><span class="method grpc">RPC</span>UpdateUser / DeleteUser / ListUser</li><li><span class="method grpc">RPC</span>UpdateUserStatus(userID, status)</li><li><span class="method grpc">RPC</span>CreateRole / GetRole / ListRole / DeleteRole</li><li><span class="method grpc">RPC</span>SetRolePermissions(roleID, menuIDs)</li><li><span class="method grpc">RPC</span>CreateMenu / GetMenu / ListMenu / DeleteMenu</li><li><span class="method grpc">RPC</span>CreateOperationLog / ListOperationLog</li></ul>' },
{ title: '调用者', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="flow-badge grpc">← iloom Auth Service</span><span class="flow-badge grpc">← muyu Gateway :8888</span></div>' },
{ title: '数据库表', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="table-badge sys">sys_user</span><span class="table-badge sys">sys_role</span><span class="table-badge sys">sys_user_role</span><span class="table-badge sys">sys_menu</span><span class="table-badge sys">sys_role_menu</span><span class="table-badge sys">sys_operation_log</span><span class="table-badge sys">sys_config</span><span class="table-badge sys">casbin_rule</span></div>' }
]
},
'inventory-rpc': {
title: 'Inventory RPC (muyu-apiserver)',
subtitle: ':9002 · go-zero gRPC · 产品/库存管理',
sections: [
{ title: 'gRPC 方法', content: '<ul class="endpoint-list"><li><span class="method grpc">RPC</span>CreateProduct(name, sku, ...) → IdResp</li><li><span class="method grpc">RPC</span>GetProduct(productID) → ProductInfo</li><li><span class="method grpc">RPC</span>ListProduct(page, pageSize, ...) → ListProductResp</li><li><span class="method grpc">RPC</span>UpdateProduct / DeleteProduct</li><li><span class="method grpc">RPC</span>ImportProducts(batch) → ImportResp</li><li><span class="method grpc">RPC</span>GetStockSummary / GetStockGroup</li><li><span class="method grpc">RPC</span>CreateStockCheck / ConfirmStockCheck</li><li><span class="method grpc">RPC</span>CreateStockAdjust / ApproveStockAdjust</li></ul>' },
{ title: '调用者', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="flow-badge grpc">← iloom Purchaser Service</span><span class="flow-badge grpc">← muyu Gateway :8888</span></div>' },
{ title: '数据库表', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="table-badge inv">inv_product</span><span class="table-badge inv">inv_stock_check</span><span class="table-badge inv">inv_stock_adjust</span></div>' }
]
},
'muyu-gateway': {
title: 'muyu Gateway',
subtitle: ':8888 · go-zero REST API · 管理后台接口',
sections: [
{ title: '端点 (JWT + Casbin RBAC)', content: '<ul class="endpoint-list"><li><span class="method post">POST</span>/api/v1/auth/login — 登录</li><li><span class="method get">GET</span>/api/v1/auth/info — 用户信息</li><li><span class="method get">GET</span>/api/v1/system/users — 用户管理</li><li><span class="method get">GET</span>/api/v1/system/roles — 角色管理</li><li><span class="method get">GET</span>/api/v1/system/menus — 菜单管理</li><li><span class="method get">GET</span>/api/v1/inventory/products — 产品管理</li><li><span class="method get">GET</span>/api/v1/inventory/stock-check — 盘点</li><li><span class="method get">GET</span>/api/v1/inventory/stock-adjust — 调整</li></ul>' },
{ title: '下游 gRPC', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="flow-badge grpc">→ System RPC :9001</span><span class="flow-badge grpc">→ Inventory RPC :9002</span></div>' }
]
}
};
// ── Detail Panel Logic ──
const panel = document.getElementById('detailPanel');
const overlay = document.getElementById('overlay');
const detailContent = document.getElementById('detailContent');
function openDetail(key) {
const data = serviceData[key];
if (!data) return;
let html = `<div class="detail-title">${data.title}</div><div class="detail-subtitle">${data.subtitle}</div>`;
data.sections.forEach(s => {
html += `<div class="detail-section"><h3>${s.title}</h3>${s.content}</div>`;
});
detailContent.innerHTML = html;
panel.classList.add('open');
overlay.classList.add('show');
document.querySelectorAll('.service-card').forEach(c => c.classList.remove('active'));
const activeCard = document.querySelector(`[data-service="${key}"]`);
if (activeCard) activeCard.classList.add('active');
}
function closeDetail() {
panel.classList.remove('open');
overlay.classList.remove('show');
document.querySelectorAll('.service-card').forEach(c => c.classList.remove('active'));
}
document.getElementById('closeBtn').addEventListener('click', closeDetail);
overlay.addEventListener('click', closeDetail);
// Swipe down to close on mobile
let touchStartY = 0;
panel.addEventListener('touchstart', (e) => {
if (panel.scrollTop <= 0) {
touchStartY = e.touches[0].clientY;
}
}, { passive: true });
panel.addEventListener('touchmove', (e) => {
if (panel.scrollTop <= 0 && touchStartY > 0) {
const dy = e.touches[0].clientY - touchStartY;
if (dy > 80) {
closeDetail();
touchStartY = 0;
}
}
}, { passive: true });
panel.addEventListener('touchend', () => { touchStartY = 0; }, { passive: true });
document.querySelectorAll('.service-card[data-service]').forEach(card => {
card.addEventListener('click', () => openDetail(card.dataset.service));
});
// ── Draw SVG Connections ──
function drawConnections() {
const svg = document.getElementById('connSvg');
if (window.innerWidth <= 768) {
svg.innerHTML = '';
return;
}
const canvas = document.getElementById('canvas');
const canvasRect = canvas.getBoundingClientRect();
svg.setAttribute('width', canvas.scrollWidth);
svg.setAttribute('height', canvas.scrollHeight);
svg.innerHTML = '';
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
// Gradient: green
const gGreen = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
gGreen.setAttribute('id', 'gGreen'); gGreen.setAttribute('x1','0%'); gGreen.setAttribute('y1','0%'); gGreen.setAttribute('x2','100%'); gGreen.setAttribute('y2','0%');
gGreen.innerHTML = '<stop offset="0%" style="stop-color:#34d399"/><stop offset="100%" style="stop-color:#6ee7b7"/>';
defs.appendChild(gGreen);
// Gradient: purple (gRPC)
const gPurple = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
gPurple.setAttribute('id', 'gPurple'); gPurple.setAttribute('x1','0%'); gPurple.setAttribute('y1','0%'); gPurple.setAttribute('x2','100%'); gPurple.setAttribute('y2','0%');
gPurple.innerHTML = '<stop offset="0%" style="stop-color:#818cf8"/><stop offset="100%" style="stop-color:#a855f7"/>';
defs.appendChild(gPurple);
// Gradient: blue (DB)
const gBlue = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
gBlue.setAttribute('id', 'gBlue'); gBlue.setAttribute('x1','0%'); gBlue.setAttribute('y1','0%'); gBlue.setAttribute('x2','0%'); gBlue.setAttribute('y2','100%');
gBlue.innerHTML = '<stop offset="0%" style="stop-color:#60a5fa"/><stop offset="100%" style="stop-color:#3b82f6"/>';
defs.appendChild(gBlue);
// Gradient: yellow
const gYellow = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
gYellow.setAttribute('id', 'gYellow'); gYellow.setAttribute('x1','0%'); gYellow.setAttribute('y1','0%'); gYellow.setAttribute('x2','100%'); gYellow.setAttribute('y2','0%');
gYellow.innerHTML = '<stop offset="0%" style="stop-color:#fbbf24"/><stop offset="100%" style="stop-color:#fcd34d"/>';
defs.appendChild(gYellow);
svg.appendChild(defs);
function getCenter(id) {
const el = document.getElementById(id);
if (!el) return null;
const r = el.getBoundingClientRect();
return {
x: r.left + r.width / 2 - canvasRect.left,
y: r.top + r.height / 2 - canvasRect.top,
top: r.top - canvasRect.top,
bottom: r.bottom - canvasRect.top,
left: r.left - canvasRect.left,
right: r.right - canvasRect.left,
};
}
function drawCurve(from, to, stroke, dashArray, fromSide, toSide) {
const a = getCenter(from);
const b = getCenter(to);
if (!a || !b) return;
let x1, y1, x2, y2;
if (fromSide === 'right') { x1 = a.right; y1 = (a.top + a.bottom) / 2; }
else if (fromSide === 'bottom') { x1 = (a.left + a.right) / 2; y1 = a.bottom; }
else { x1 = (a.left + a.right) / 2; y1 = (a.top + a.bottom) / 2; }
if (toSide === 'left') { x2 = b.left; y2 = (b.top + b.bottom) / 2; }
else if (toSide === 'top') { x2 = (b.left + b.right) / 2; y2 = b.top; }
else { x2 = (b.left + b.right) / 2; y2 = (b.top + b.bottom) / 2; }
const dx = x2 - x1;
const dy = y2 - y1;
let cx1, cy1, cx2, cy2;
if (fromSide === 'right' && toSide === 'left') {
cx1 = x1 + dx * 0.4; cy1 = y1;
cx2 = x2 - dx * 0.4; cy2 = y2;
} else if (fromSide === 'bottom' && toSide === 'top') {
cx1 = x1; cy1 = y1 + dy * 0.4;
cx2 = x2; cy2 = y2 - dy * 0.4;
} else {
cx1 = x1 + dx * 0.3; cy1 = y1;
cx2 = x2 - dx * 0.3; cy2 = y2;
}
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', `M${x1},${y1} C${cx1},${cy1} ${cx2},${cy2} ${x2},${y2}`);
path.setAttribute('stroke', stroke);
path.setAttribute('class', 'conn-line');
if (dashArray) {
path.setAttribute('stroke-dasharray', dashArray);
path.classList.add('animated-flow');
}
svg.appendChild(path);
}
// Frontend → Gateway
drawCurve('card-frontend', 'card-gateway', 'url(#gYellow)', '6 4', 'right', 'left');
// Gateway → Services
drawCurve('card-gateway', 'card-auth', 'url(#gGreen)', '6 4', 'bottom', 'top');
drawCurve('card-gateway', 'card-purchaser', 'url(#gGreen)', '6 4', 'bottom', 'top');
drawCurve('card-gateway', 'card-textile', 'url(#gGreen)', '6 4', 'bottom', 'top');
drawCurve('card-gateway', 'card-washing', 'url(#gGreen)', '6 4', 'bottom', 'top');
// Auth → System RPC (gRPC)
drawCurve('card-auth', 'card-system-rpc', 'url(#gPurple)', null, 'right', 'left');
// Purchaser → Inventory RPC (gRPC)
drawCurve('card-purchaser', 'card-inv-rpc', 'url(#gPurple)', null, 'right', 'left');
// Textile → Purchaser (internal HTTP)
drawCurve('card-textile', 'card-purchaser', 'url(#gYellow)', '4 3', 'right', 'left');
// Washing → Purchaser (internal HTTP)
drawCurve('card-washing', 'card-purchaser', 'url(#gYellow)', '4 3', 'right', 'left');
// Services → DB zone
drawCurve('card-auth', 'zone-db', 'url(#gBlue)', '4 3', 'bottom', 'top');
drawCurve('card-purchaser', 'zone-db', 'url(#gBlue)', '4 3', 'bottom', 'top');
drawCurve('card-textile', 'zone-db', 'url(#gBlue)', '4 3', 'bottom', 'top');
drawCurve('card-washing', 'zone-db', 'url(#gBlue)', '4 3', 'bottom', 'top');
drawCurve('card-system-rpc', 'zone-db', 'url(#gBlue)', '4 3', 'bottom', 'top');
drawCurve('card-inv-rpc', 'zone-db', 'url(#gBlue)', '4 3', 'bottom', 'top');
}
// Run after layout
window.addEventListener('load', () => { setTimeout(drawConnections, 100); });
window.addEventListener('resize', drawConnections);
</script>
</body>
</html>