- 新增 Go 语言后端服务(server/),包含用户认证、Agent管理、数据库连接等API - 新增 Python Agent 服务(agent/),实现Agent核心逻辑和工具集 - 前端从原生HTML迁移到Vue.js框架(web/src/) - 添加 Docker Compose 支持(docker-compose.yml) - 添加项目架构文档(docs/ARCHITECTURE.md) - 添加环境变量示例(.env.example)和本地启动脚本(start-local.ps1) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
909 lines
36 KiB
HTML
909 lines
36 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>思维本体元模型 — 知识图谱</title>
|
|
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
|
<script src="./graph-data.js"></script>
|
|
<style>
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Microsoft YaHei', 'PingFang SC', -apple-system, sans-serif;
|
|
background: #0a0e1a;
|
|
color: #e6edf3;
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
#topbar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0 20px;
|
|
height: 52px;
|
|
background: linear-gradient(90deg, #0f1628, #1a1040);
|
|
border-bottom: 1px solid #2d2d55;
|
|
flex-shrink: 0;
|
|
z-index: 100;
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
#topbar .title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
#topbar .title .icon {
|
|
font-size: 22px;
|
|
}
|
|
|
|
#topbar .title h1 {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
background: linear-gradient(90deg, #8B5CF6, #3B82F6, #10B981);
|
|
-webkit-background-clip: text;
|
|
background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
|
|
#topbar .subtitle {
|
|
font-size: 12px;
|
|
color: #8b949e;
|
|
}
|
|
|
|
#topbar .controls {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
}
|
|
|
|
.ctrl-btn {
|
|
background: #1a1a2e;
|
|
border: 1px solid #2d2d55;
|
|
color: #c9d1d9;
|
|
padding: 5px 12px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
transition: all .2s;
|
|
}
|
|
|
|
.ctrl-btn:hover {
|
|
background: #2d2d55;
|
|
border-color: #8B5CF6;
|
|
color: #8B5CF6;
|
|
}
|
|
|
|
.ctrl-btn.active {
|
|
background: #4C1D95;
|
|
border-color: #8B5CF6;
|
|
color: #fff;
|
|
}
|
|
|
|
#main-layout {
|
|
display: flex;
|
|
flex: 1;
|
|
overflow: hidden;
|
|
}
|
|
|
|
#left-panel {
|
|
width: 230px;
|
|
background: #0f1628;
|
|
border-right: 1px solid #2d2d55;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow-y: auto;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.panel-section {
|
|
padding: 14px 14px 10px;
|
|
border-bottom: 1px solid #1a1a2e;
|
|
}
|
|
|
|
.panel-section-title {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.8px;
|
|
color: #8b949e;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.legend-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 5px 8px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: background .15s;
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.legend-item:hover {
|
|
background: #1a1a2e;
|
|
}
|
|
|
|
.legend-dot {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
box-shadow: 0 0 6px currentColor;
|
|
}
|
|
|
|
.legend-label {
|
|
font-size: 13px;
|
|
color: #c9d1d9;
|
|
}
|
|
|
|
.legend-count {
|
|
margin-left: auto;
|
|
font-size: 11px;
|
|
color: #8b949e;
|
|
background: #1a1a2e;
|
|
padding: 1px 6px;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.scenario-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
width: 100%;
|
|
padding: 8px 10px;
|
|
margin-bottom: 4px;
|
|
background: #1a1a2e;
|
|
border: 1px solid #2d2d55;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
color: #c9d1d9;
|
|
font-size: 12px;
|
|
text-align: left;
|
|
transition: all .2s;
|
|
}
|
|
|
|
.scenario-btn:hover {
|
|
border-color: #8B5CF6;
|
|
color: #8B5CF6;
|
|
background: #1a1040;
|
|
}
|
|
|
|
.scenario-btn.active {
|
|
border-color: var(--sc-color);
|
|
color: var(--sc-color);
|
|
background: rgba(139, 92, 246, .08);
|
|
box-shadow: 0 0 8px rgba(139, 92, 246, .2);
|
|
}
|
|
|
|
.scenario-btn .sc-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: var(--sc-color);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.scenario-btn .sc-label {
|
|
flex: 1;
|
|
}
|
|
|
|
.scenario-btn .sc-count {
|
|
font-size: 10px;
|
|
color: #8b949e;
|
|
background: #0f1628;
|
|
padding: 1px 5px;
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.rel-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 3px 0;
|
|
font-size: 11px;
|
|
color: #8b949e;
|
|
}
|
|
|
|
.rel-line {
|
|
width: 28px;
|
|
height: 2px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
#chart-container {
|
|
flex: 1;
|
|
position: relative;
|
|
}
|
|
|
|
#chart {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
#graph-hint {
|
|
position: absolute;
|
|
bottom: 16px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: rgba(15, 22, 40, 0.85);
|
|
border: 1px solid #2d2d55;
|
|
border-radius: 8px;
|
|
padding: 6px 14px;
|
|
font-size: 11px;
|
|
color: #8b949e;
|
|
pointer-events: none;
|
|
white-space: nowrap;
|
|
backdrop-filter: blur(8px);
|
|
}
|
|
|
|
#right-panel {
|
|
width: 260px;
|
|
background: #0f1628;
|
|
border-left: 1px solid #2d2d55;
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex-shrink: 0;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
#detail-header {
|
|
padding: 14px 16px 10px;
|
|
border-bottom: 1px solid #1a1a2e;
|
|
}
|
|
|
|
#detail-type-badge {
|
|
display: inline-block;
|
|
font-size: 10px;
|
|
padding: 2px 8px;
|
|
border-radius: 10px;
|
|
margin-bottom: 6px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
#detail-name {
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
color: #e6edf3;
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
#detail-tag {
|
|
font-size: 11px;
|
|
color: #8b949e;
|
|
}
|
|
|
|
#detail-body {
|
|
padding: 12px 16px;
|
|
flex: 1;
|
|
}
|
|
|
|
#detail-desc {
|
|
font-size: 12px;
|
|
color: #8b949e;
|
|
line-height: 1.7;
|
|
white-space: pre-wrap;
|
|
background: #0a0e1a;
|
|
border: 1px solid #1a1a2e;
|
|
border-radius: 6px;
|
|
padding: 10px;
|
|
}
|
|
|
|
#detail-connections {
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.conn-title {
|
|
font-size: 11px;
|
|
color: #6e7681;
|
|
text-transform: uppercase;
|
|
letter-spacing: .5px;
|
|
margin-bottom: 6px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.conn-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
margin-bottom: 2px;
|
|
font-size: 11px;
|
|
color: #8b949e;
|
|
background: #0a0e1a;
|
|
border: 1px solid #1a1a2e;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.conn-dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.conn-text {
|
|
flex: 1;
|
|
}
|
|
|
|
.conn-rel {
|
|
font-size: 10px;
|
|
color: #6e7681;
|
|
}
|
|
|
|
#detail-placeholder {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #484f58;
|
|
padding: 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
#detail-placeholder .ph-icon {
|
|
font-size: 48px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
#detail-placeholder .ph-text {
|
|
font-size: 13px;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
#stats-bar {
|
|
display: flex;
|
|
padding: 10px 16px;
|
|
gap: 10px;
|
|
border-top: 1px solid #1a1a2e;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.stat-item {
|
|
flex: 1;
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-num {
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 10px;
|
|
color: #8b949e;
|
|
}
|
|
|
|
/* 模拟控制栏 */
|
|
#sim-bar {
|
|
position: fixed;
|
|
bottom: -160px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
width: min(820px, 94vw);
|
|
background: linear-gradient(135deg, #1a1040, #0f1628);
|
|
border: 1px solid #2d2d55;
|
|
border-bottom: none;
|
|
border-radius: 14px 14px 0 0;
|
|
box-shadow: 0 -4px 32px rgba(0, 0, 0, 0.6);
|
|
z-index: 500;
|
|
transition: bottom .4s cubic-bezier(.4, 0, .2, 1);
|
|
overflow: hidden;
|
|
}
|
|
|
|
#sim-bar.visible {
|
|
bottom: 0;
|
|
}
|
|
|
|
#sim-header {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 10px 18px 8px;
|
|
gap: 10px;
|
|
border-bottom: 1px solid #1a1a2e;
|
|
}
|
|
|
|
#sim-header .sim-icon {
|
|
font-size: 16px;
|
|
}
|
|
|
|
#sim-title {
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
color: #c084fc;
|
|
flex: 1;
|
|
}
|
|
|
|
#sim-step-badge {
|
|
font-size: 11px;
|
|
background: #1a1a2e;
|
|
color: #8b949e;
|
|
padding: 3px 10px;
|
|
border-radius: 12px;
|
|
border: 1px solid #2d2d55;
|
|
font-family: monospace;
|
|
}
|
|
|
|
#sim-phase-badge {
|
|
font-size: 10px;
|
|
padding: 3px 9px;
|
|
border-radius: 10px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
#sim-progress {
|
|
height: 3px;
|
|
background: #1a1a2e;
|
|
margin: 0 18px;
|
|
}
|
|
|
|
#sim-progress-bar {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #8B5CF6, #c084fc);
|
|
border-radius: 2px;
|
|
transition: width .4s ease;
|
|
}
|
|
|
|
#sim-body {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 10px 18px 14px;
|
|
}
|
|
|
|
#sim-desc {
|
|
flex: 1;
|
|
font-size: 12px;
|
|
color: #c9d1d9;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
#sim-desc .sim-step-title {
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
color: #e6edf3;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
#sim-controls {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sim-btn {
|
|
padding: 7px 14px;
|
|
border-radius: 8px;
|
|
border: 1px solid #2d2d55;
|
|
background: #1a1a2e;
|
|
color: #c9d1d9;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
transition: all .2s;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.sim-btn:hover {
|
|
background: #2d2d55;
|
|
color: #fff;
|
|
}
|
|
|
|
.sim-btn:disabled {
|
|
opacity: .35;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.sim-btn.primary {
|
|
background: linear-gradient(135deg, #7C3AED, #8B5CF6);
|
|
border-color: #7C3AED;
|
|
color: #fff;
|
|
box-shadow: 0 0 12px rgba(124, 58, 237, .4);
|
|
}
|
|
|
|
.sim-btn.stop {
|
|
background: #2d1f1f;
|
|
border-color: #EF4444;
|
|
color: #EF4444;
|
|
}
|
|
|
|
.sim-btn.stop:hover {
|
|
background: #EF4444;
|
|
color: #fff;
|
|
}
|
|
|
|
#sim-auto-btn.playing {
|
|
background: linear-gradient(135deg, #059669, #10B981);
|
|
border-color: #10B981;
|
|
color: #fff;
|
|
}
|
|
|
|
::-webkit-scrollbar {
|
|
width: 4px;
|
|
}
|
|
|
|
::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
background: #2d2d55;
|
|
border-radius: 2px;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div id="topbar">
|
|
<div class="title">
|
|
<span class="icon">🧠</span>
|
|
<div>
|
|
<h1>思维本体元模型 — 知识图谱</h1>
|
|
<div class="subtitle">Thinking Ontology Meta-Model · ECharts Force-Directed Graph</div>
|
|
</div>
|
|
</div>
|
|
<div class="controls">
|
|
<button class="ctrl-btn" id="btn-reset">↺ 复位</button>
|
|
<button class="ctrl-btn active" id="btn-labels">标签 ON</button>
|
|
<button class="ctrl-btn active" id="btn-rules">规则边</button>
|
|
<button class="ctrl-btn" id="btn-fullscreen">⛶ 全屏</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="main-layout">
|
|
<div id="left-panel">
|
|
<div class="panel-section">
|
|
<div class="panel-section-title">节点类型图例</div>
|
|
<div class="legend-item" onclick="filterByCategory(0)">
|
|
<div class="legend-dot" style="background:#3B82F6;color:#3B82F6"></div>
|
|
<span class="legend-label">语义对象</span>
|
|
<span class="legend-count" id="cnt-0">—</span>
|
|
</div>
|
|
<div class="legend-item" onclick="filterByCategory(1)">
|
|
<div class="legend-dot" style="background:#10B981;color:#10B981"></div>
|
|
<span class="legend-label">对象行为</span>
|
|
<span class="legend-count" id="cnt-1">—</span>
|
|
</div>
|
|
<div class="legend-item" onclick="filterByCategory(2)">
|
|
<div class="legend-dot" style="background:#F59E0B;color:#F59E0B"></div>
|
|
<span class="legend-label">约束规则</span>
|
|
<span class="legend-count" id="cnt-2">—</span>
|
|
</div>
|
|
<div class="legend-item" onclick="filterByCategory(3)">
|
|
<div class="legend-dot" style="background:#8B5CF6;color:#8B5CF6"></div>
|
|
<span class="legend-label">编排流程</span>
|
|
<span class="legend-count" id="cnt-3">—</span>
|
|
</div>
|
|
</div>
|
|
<div class="panel-section">
|
|
<div class="panel-section-title">场景联动筛选</div>
|
|
<div id="scenario-buttons"></div>
|
|
<button class="ctrl-btn" style="width:100%;margin-top:6px;font-size:11px" onclick="clearScenario()">✕
|
|
清除筛选</button>
|
|
</div>
|
|
<div class="panel-section">
|
|
<div class="panel-section-title">关系类型</div>
|
|
<div class="rel-item">
|
|
<div class="rel-line" style="background:#3B82F6"></div><span>语义对象关系</span>
|
|
</div>
|
|
<div class="rel-item">
|
|
<div class="rel-line" style="background:#10B981"></div><span>行为操作关系</span>
|
|
</div>
|
|
<div class="rel-item">
|
|
<div class="rel-line"
|
|
style="background:repeating-linear-gradient(90deg,#F59E0B 0,#F59E0B 4px,transparent 4px,transparent 8px)">
|
|
</div><span>规则引用关系</span>
|
|
</div>
|
|
<div class="rel-item">
|
|
<div class="rel-line" style="background:#8B5CF6"></div><span>流程调用关系</span>
|
|
</div>
|
|
<div class="rel-item">
|
|
<div class="rel-line" style="background:#EF4444"></div><span>⭐ 核心关系</span>
|
|
</div>
|
|
</div>
|
|
<div id="stats-bar">
|
|
<div class="stat-item">
|
|
<div class="stat-num" style="color:#3B82F6" id="s-nodes">—</div>
|
|
<div class="stat-label">节点</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-num" style="color:#10B981" id="s-links">—</div>
|
|
<div class="stat-label">关系</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="chart-container">
|
|
<div id="chart"></div>
|
|
<div id="graph-hint">🖱 拖拽节点 · 滚轮缩放 · 点击查看详情 · 双击流程触发场景</div>
|
|
<div id="sim-bar">
|
|
<div id="sim-header">
|
|
<span class="sim-icon">🎬</span>
|
|
<span id="sim-title">问题分析与解决 — 动态模拟</span>
|
|
<span id="sim-phase-badge"></span>
|
|
<span id="sim-step-badge">0 / 14</span>
|
|
</div>
|
|
<div id="sim-progress">
|
|
<div id="sim-progress-bar" style="width:0%"></div>
|
|
</div>
|
|
<div id="sim-body">
|
|
<div id="sim-desc">
|
|
<div class="sim-step-title" id="sim-step-title">点击「下一步」开始模拟</div>
|
|
<div id="sim-step-desc">逐步展示问题分析与解决的完整调用链路。</div>
|
|
</div>
|
|
<div id="sim-controls">
|
|
<button class="sim-btn" id="sim-prev-btn" disabled onclick="simStep(-1)">◀ 上一步</button>
|
|
<button class="sim-btn primary" id="sim-next-btn" onclick="simStep(1)">下一步 ▶</button>
|
|
<button class="sim-btn" id="sim-auto-btn" onclick="simToggleAuto()">⏵ 自动</button>
|
|
<button class="sim-btn stop" onclick="simStop()">■ 停止</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="right-panel">
|
|
<div id="detail-placeholder">
|
|
<div class="ph-icon">🔍</div>
|
|
<div class="ph-text">点击图谱中任意节点<br>查看详细元数据信息</div>
|
|
</div>
|
|
<div id="detail-content" style="display:none;flex-direction:column;flex:1">
|
|
<div id="detail-header">
|
|
<div id="detail-type-badge"></div>
|
|
<div id="detail-name"></div>
|
|
<div id="detail-tag"></div>
|
|
</div>
|
|
<div id="detail-body">
|
|
<div class="conn-title">节点描述</div>
|
|
<div id="detail-desc"></div>
|
|
<div id="detail-connections">
|
|
<div class="conn-title" style="margin-top:14px">关联节点</div>
|
|
<div id="conn-list"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const { CATEGORIES, NODES, LINKS, SCENARIOS, SIMULATION_STEPS } = ONTOLOGY_DATA;
|
|
let chart = null, activeScenario = null, showLabels = true, showRuleEdges = true, catFilter = null;
|
|
const CAT_COLORS = ['#3B82F6', '#10B981', '#F59E0B', '#8B5CF6'];
|
|
const CAT_NAMES = ['语义对象', '对象行为', '约束规则', '编排流程'];
|
|
const nodeMap = {}; NODES.forEach(n => nodeMap[n.id] = n);
|
|
|
|
document.getElementById('s-nodes').textContent = NODES.length;
|
|
document.getElementById('s-links').textContent = LINKS.length;
|
|
for (let i = 0; i < 4; i++) document.getElementById('cnt-' + i).textContent = NODES.filter(n => n.category === i).length;
|
|
|
|
// 场景按钮
|
|
const scColors = { 'WF.ProblemSolving': '#F59E0B', 'WF.ArticleGen': '#3B82F6', 'WF.KnowledgeUpdate': '#10B981', 'WF.CognitionUpgrade': '#EF4444' };
|
|
Object.entries(SCENARIOS).forEach(([key, sc]) => {
|
|
const btn = document.createElement('button');
|
|
btn.className = 'scenario-btn'; btn.dataset.key = key;
|
|
btn.style.setProperty('--sc-color', scColors[key] || '#8B5CF6');
|
|
btn.innerHTML = `<span class="sc-dot"></span><span class="sc-label">${sc.label}</span><span class="sc-count">${sc.nodes.length}节点</span>`;
|
|
btn.onclick = () => activateScenario(key);
|
|
document.getElementById('scenario-buttons').appendChild(btn);
|
|
});
|
|
|
|
// 构建 ECharts 选项
|
|
function buildOption(highlightIds = null) {
|
|
const filteredLinks = showRuleEdges ? LINKS : LINKS.filter(l => l.relType !== 'reference');
|
|
const nodes = NODES.map(n => {
|
|
const hi = highlightIds ? highlightIds.includes(n.id) : true;
|
|
const cv = catFilter === null || n.category === catFilter;
|
|
const op = (hi && cv) ? 1 : 0.08;
|
|
return {
|
|
id: n.id, name: n.name, category: n.category,
|
|
symbolSize: n.symbolSize * (hi ? 1 : 0.85),
|
|
label: { show: showLabels && hi && cv, fontSize: n.category === 3 ? 11 : (n.category === 0 ? 10 : 9), fontWeight: n.category <= 1 ? 'bold' : 'normal' },
|
|
itemStyle: {
|
|
color: CAT_COLORS[n.category], opacity: op,
|
|
shadowBlur: hi && n.tag && n.tag.includes('⭐') ? 20 : (hi ? 8 : 0),
|
|
shadowColor: hi ? CAT_COLORS[n.category] : 'transparent',
|
|
borderColor: hi && n.tag && n.tag.includes('⭐') ? '#FFD700' : CAT_COLORS[n.category],
|
|
borderWidth: hi && n.tag && n.tag.includes('⭐') ? 2.5 : 0,
|
|
}, _data: n,
|
|
};
|
|
});
|
|
const links = filteredLinks.map(l => {
|
|
const sv = highlightIds ? highlightIds.includes(l.source) : true;
|
|
const tv = highlightIds ? highlightIds.includes(l.target) : true;
|
|
const v = sv && tv;
|
|
return {
|
|
source: l.source, target: l.target,
|
|
label: { show: v && showLabels && l.relType !== 'reference', formatter: l.label, fontSize: 9, color: '#8b949e' },
|
|
lineStyle: { ...l.lineStyle, opacity: v ? (l.relType === 'reference' ? 0.5 : 0.75) : 0.03, curveness: 0.2, type: l.lineStyle.type || 'solid' },
|
|
symbol: ['none', 'arrow'], symbolSize: [0, 7],
|
|
};
|
|
});
|
|
return {
|
|
backgroundColor: '#0a0e1a',
|
|
tooltip: {
|
|
trigger: 'item', backgroundColor: 'rgba(15,22,40,0.95)', borderColor: '#2d2d55', textStyle: { color: '#e6edf3', fontSize: 12 },
|
|
formatter: p => { if (p.dataType !== 'node') return ''; const n = nodeMap[p.data.id]; if (!n) return ''; return `<div style="max-width:260px"><div style="color:${CAT_COLORS[n.category]};font-weight:bold;font-size:13px;margin-bottom:4px">${n.name.replace(/\n/g, ' ')}</div><div style="color:#8b949e;font-size:10px;margin-bottom:6px">[${CAT_NAMES[n.category]}] ${n.tag || ''}</div><div style="color:#c9d1d9;font-size:11px;line-height:1.6;white-space:pre-wrap">${n.desc || ''}</div></div>`; }
|
|
},
|
|
legend: { show: false },
|
|
series: [{
|
|
type: 'graph', layout: 'force', animation: true, animationDuration: 800, animationEasingUpdate: 'quinticInOut',
|
|
data: nodes, links: links,
|
|
categories: CATEGORIES.map((c, i) => ({ name: c.name, itemStyle: { color: CAT_COLORS[i] } })),
|
|
roam: true, draggable: true, focusNodeAdjacency: false,
|
|
symbol: (v, p) => { const c = p.data.category; return c === 3 ? 'diamond' : c === 2 ? 'roundRect' : 'circle'; },
|
|
label: { show: showLabels, position: 'bottom', formatter: '{b}', color: '#c9d1d9', textBorderColor: '#0a0e1a', textBorderWidth: 3 },
|
|
edgeLabel: { show: showLabels, fontSize: 9, color: '#8b949e' },
|
|
force: { repulsion: 320, edgeLength: [80, 200], gravity: 0.06, layoutAnimation: true, friction: 0.6 },
|
|
lineStyle: { curveness: 0.2 },
|
|
emphasis: { focus: 'adjacency', scale: true, itemStyle: { shadowBlur: 20 }, lineStyle: { width: 3, opacity: 1 }, label: { show: true } },
|
|
}]
|
|
};
|
|
}
|
|
|
|
function initChart() {
|
|
chart = echarts.init(document.getElementById('chart'), 'dark');
|
|
chart.setOption(buildOption());
|
|
chart.on('click', p => { if (p.dataType === 'node') showDetail(p.data.id); });
|
|
chart.on('dblclick', p => { if (p.dataType === 'node' && SCENARIOS[p.data.id]) activateScenario(p.data.id); });
|
|
window.addEventListener('resize', () => chart.resize());
|
|
}
|
|
|
|
function showDetail(nodeId) {
|
|
const n = nodeMap[nodeId]; if (!n) return;
|
|
document.getElementById('detail-placeholder').style.display = 'none';
|
|
document.getElementById('detail-content').style.display = 'flex';
|
|
const color = CAT_COLORS[n.category];
|
|
const badge = document.getElementById('detail-type-badge');
|
|
badge.textContent = CAT_NAMES[n.category]; badge.style.background = color + '22'; badge.style.color = color; badge.style.border = `1px solid ${color}55`;
|
|
document.getElementById('detail-name').textContent = n.name.replace(/\n/g, ' ');
|
|
document.getElementById('detail-name').style.color = color;
|
|
document.getElementById('detail-tag').textContent = n.tag || '';
|
|
document.getElementById('detail-desc').textContent = n.desc || '暂无描述';
|
|
const connList = document.getElementById('conn-list'); connList.innerHTML = '';
|
|
const rels = LINKS.filter(l => l.source === nodeId || l.target === nodeId);
|
|
if (!rels.length) { connList.innerHTML = '<div style="color:#484f58;font-size:11px">无直接关联</div>'; return; }
|
|
rels.slice(0, 12).forEach(l => {
|
|
const isOut = l.source === nodeId, otherId = isOut ? l.target : l.source, o = nodeMap[otherId];
|
|
if (!o) return;
|
|
const div = document.createElement('div'); div.className = 'conn-item';
|
|
div.innerHTML = `<div class="conn-dot" style="background:${CAT_COLORS[o.category]}"></div><div class="conn-text">${o.name.replace(/\n/g, ' ')}</div><div class="conn-rel">${isOut ? '→' : '←'} ${l.label}</div>`;
|
|
div.onclick = () => showDetail(otherId); connList.appendChild(div);
|
|
});
|
|
}
|
|
|
|
function activateScenario(key) {
|
|
activeScenario = key;
|
|
document.querySelectorAll('.scenario-btn').forEach(b => b.classList.toggle('active', b.dataset.key === key));
|
|
chart.setOption(buildOption(SCENARIOS[key].nodes));
|
|
showDetail(key);
|
|
if (SIMULATION_STEPS[key]) simInit(key); else simHide();
|
|
}
|
|
function clearScenario() {
|
|
activeScenario = null; catFilter = null; simHide(); simClearTimer(); simCurrentStep = -1;
|
|
document.querySelectorAll('.scenario-btn').forEach(b => b.classList.remove('active'));
|
|
chart.setOption(buildOption());
|
|
}
|
|
function filterByCategory(cat) {
|
|
catFilter = catFilter === cat ? null : cat;
|
|
chart.setOption(buildOption(activeScenario ? SCENARIOS[activeScenario].nodes : null));
|
|
}
|
|
|
|
document.getElementById('btn-reset').onclick = () => { clearScenario(); chart.dispatchAction({ type: 'restore' }); };
|
|
document.getElementById('btn-labels').onclick = function () { showLabels = !showLabels; this.textContent = showLabels ? '标签 ON' : '标签 OFF'; this.classList.toggle('active', showLabels); chart.setOption(buildOption(activeScenario ? SCENARIOS[activeScenario].nodes : null)); };
|
|
document.getElementById('btn-rules').onclick = function () { showRuleEdges = !showRuleEdges; this.textContent = showRuleEdges ? '规则边' : '规则边 OFF'; this.classList.toggle('active', showRuleEdges); chart.setOption(buildOption(activeScenario ? SCENARIOS[activeScenario].nodes : null)); };
|
|
document.getElementById('btn-fullscreen').onclick = () => { if (!document.fullscreenElement) document.documentElement.requestFullscreen(); else document.exitFullscreen(); };
|
|
|
|
initChart();
|
|
|
|
// ================================================================
|
|
// 动态模拟系统
|
|
// ================================================================
|
|
let simCurrentStep = -1, simScenarioKey = null, simAutoTimer = null, simIsPlaying = false;
|
|
const PHASE_META = {
|
|
process: { color: '#8B5CF6', bg: 'rgba(139,92,246,0.15)', label: '🟣 流程启动' },
|
|
behavior: { color: '#10B981', bg: 'rgba(16,185,129,0.15)', label: '🟢 行为调用' },
|
|
rule: { color: '#F59E0B', bg: 'rgba(245,158,11,0.15)', label: '🟠 规则引用' },
|
|
entity: { color: '#3B82F6', bg: 'rgba(59,130,246,0.15)', label: '🔵 对象生成' },
|
|
complete: { color: '#FFD700', bg: 'rgba(255,215,0,0.15)', label: '✅ 完成' },
|
|
};
|
|
|
|
function simInit(key) {
|
|
simScenarioKey = key; simCurrentStep = -1; simClearTimer(); simIsPlaying = false;
|
|
document.getElementById('sim-auto-btn').classList.remove('playing');
|
|
document.getElementById('sim-auto-btn').textContent = '⏵ 自动';
|
|
document.getElementById('sim-title').textContent = (SCENARIOS[key] ? SCENARIOS[key].label : key) + ' — 动态模拟';
|
|
document.getElementById('sim-step-badge').textContent = '0 / ' + SIMULATION_STEPS[key].length;
|
|
document.getElementById('sim-step-title').textContent = '点击「下一步」开始模拟';
|
|
document.getElementById('sim-step-desc').textContent = '逐步展示问题分析与解决的完整调用链路。';
|
|
document.getElementById('sim-progress-bar').style.width = '0%';
|
|
document.getElementById('sim-phase-badge').textContent = '';
|
|
document.getElementById('sim-phase-badge').style.cssText = '';
|
|
document.getElementById('sim-prev-btn').disabled = true;
|
|
document.getElementById('sim-next-btn').disabled = false;
|
|
document.getElementById('sim-next-btn').textContent = '下一步 ▶';
|
|
document.getElementById('sim-bar').classList.add('visible');
|
|
}
|
|
function simHide() { document.getElementById('sim-bar').classList.remove('visible'); }
|
|
function simClearTimer() { if (simAutoTimer) { clearInterval(simAutoTimer); simAutoTimer = null; } }
|
|
|
|
function simStep(dir) {
|
|
if (!simScenarioKey) return;
|
|
const steps = SIMULATION_STEPS[simScenarioKey], next = simCurrentStep + dir;
|
|
if (next < 0 || next >= steps.length) return;
|
|
simCurrentStep = next; simRenderStep(steps[simCurrentStep]);
|
|
}
|
|
|
|
function simRenderStep(step) {
|
|
const steps = SIMULATION_STEPS[simScenarioKey], total = steps.length;
|
|
document.getElementById('sim-progress-bar').style.width = Math.round((step.stepNo / total) * 100) + '%';
|
|
document.getElementById('sim-step-badge').textContent = step.stepNo + ' / ' + total;
|
|
const pm = PHASE_META[step.phase] || PHASE_META.process;
|
|
const badge = document.getElementById('sim-phase-badge');
|
|
badge.textContent = pm.label; badge.style.cssText = `color:${pm.color};background:${pm.bg};border:1px solid ${pm.color}44;font-size:10px;padding:3px 9px;border-radius:10px;font-weight:700;`;
|
|
document.getElementById('sim-step-title').textContent = step.title;
|
|
document.getElementById('sim-step-desc').textContent = step.desc;
|
|
document.getElementById('sim-prev-btn').disabled = simCurrentStep <= 0;
|
|
const isLast = simCurrentStep >= total - 1;
|
|
document.getElementById('sim-next-btn').disabled = isLast;
|
|
document.getElementById('sim-next-btn').textContent = isLast ? '已完成 ✓' : '下一步 ▶';
|
|
if (isLast && simIsPlaying) simToggleAuto();
|
|
|
|
const allNodeIds = step.allNodes, newNodeIds = step.newNodes || [];
|
|
const activeEdgeSet = new Set(step.activeEdges.map(e => e.source + '__' + e.target));
|
|
const edgeSeqMap = {}; step.activeEdges.forEach(e => { edgeSeqMap[e.source + '__' + e.target] = e; });
|
|
|
|
const simNodes = NODES.map(n => {
|
|
const isActive = allNodeIds.includes(n.id), isNew = newNodeIds.includes(n.id) && !step.isComplete, isComp = step.isComplete;
|
|
let sb = 0, sc = 'transparent', bw = 0, bc = CAT_COLORS[n.category];
|
|
if (isNew) { sb = 30; sc = CAT_COLORS[n.category]; bw = 3; bc = '#fff'; }
|
|
else if (isActive && isComp) { sb = 16; sc = CAT_COLORS[n.category]; }
|
|
else if (isActive) { sb = 10; sc = CAT_COLORS[n.category]; }
|
|
return {
|
|
id: n.id, name: n.name, category: n.category,
|
|
symbolSize: n.symbolSize * (isNew ? 1.22 : (isActive ? 1 : 0.8)),
|
|
label: { show: isActive, fontSize: n.category === 3 ? 11 : (n.category === 0 ? 10 : 9), fontWeight: isNew ? 'bold' : 'normal', color: isNew ? '#fff' : '#c9d1d9', textBorderColor: '#0a0e1a', textBorderWidth: 3 },
|
|
itemStyle: { color: CAT_COLORS[n.category], opacity: isActive ? 1 : 0.05, shadowBlur: sb, shadowColor: sc, borderWidth: bw, borderColor: bc },
|
|
};
|
|
});
|
|
|
|
const simLinks = LINKS.map(l => {
|
|
const ek = l.source + '__' + l.target, ia = activeEdgeSet.has(ek), ei = edgeSeqMap[ek];
|
|
return {
|
|
source: l.source, target: l.target,
|
|
label: { show: ia && !!ei, formatter: ei ? ei.label : l.label, fontSize: 10, fontWeight: 'bold', color: '#FFD700', textBorderColor: '#0a0e1a', textBorderWidth: 2, backgroundColor: 'rgba(0,0,0,0.55)', padding: [2, 5], borderRadius: 4 },
|
|
lineStyle: { color: ia ? (pm.color || '#10B981') : l.lineStyle.color, width: ia ? (l.lineStyle.width || 1.5) + 1.5 : (l.lineStyle.width || 1.5), opacity: ia ? 0.92 : 0.03, curveness: 0.2, type: 'solid', shadowBlur: ia ? 12 : 0, shadowColor: ia ? (pm.color || '#10B981') : 'transparent' },
|
|
symbol: ['none', 'arrow'], symbolSize: ia ? [0, 9] : [0, 6],
|
|
};
|
|
});
|
|
|
|
chart.setOption({ series: [{ data: simNodes, links: simLinks, force: { repulsion: 320, edgeLength: [80, 200], gravity: 0.06, layoutAnimation: false } }] }, false);
|
|
}
|
|
|
|
function simToggleAuto() {
|
|
simIsPlaying = !simIsPlaying; const btn = document.getElementById('sim-auto-btn');
|
|
if (simIsPlaying) {
|
|
btn.classList.add('playing'); btn.textContent = '⏸ 暂停';
|
|
simAutoTimer = setInterval(() => { const s = SIMULATION_STEPS[simScenarioKey]; if (simCurrentStep >= s.length - 1) simToggleAuto(); else simStep(1); }, 2200);
|
|
} else { btn.classList.remove('playing'); btn.textContent = '⏵ 自动'; simClearTimer(); }
|
|
}
|
|
function simStop() {
|
|
simClearTimer(); simIsPlaying = false; simCurrentStep = -1; simScenarioKey = null; simHide();
|
|
if (activeScenario) chart.setOption(buildOption(SCENARIOS[activeScenario].nodes)); else chart.setOption(buildOption());
|
|
}
|
|
</script>
|
|
</body>
|
|
|
|
</html> |