feat: 数据流Tab(🔀)+功能树折叠
This commit is contained in:
+113
-19
@@ -50,11 +50,13 @@ td.wrap{white-space:normal;font-size:11px;max-width:200px}
|
||||
<div class="tab active" onclick="switchTab(0)">🌳 功能树</div>
|
||||
<div class="tab" onclick="switchTab(1)">🔧 全部流程/Cron</div>
|
||||
<div class="tab" onclick="switchTab(2)">🗃️ 数据实体</div>
|
||||
<div class="tab" onclick="switchTab(3)">🔀 数据流</div>
|
||||
</div>
|
||||
|
||||
<div id="panel0" class="panel active"></div>
|
||||
<div id="panel1" class="panel"></div>
|
||||
<div id="panel2" class="panel"></div>
|
||||
<div id="panel3" class="panel"></div>
|
||||
|
||||
<script>
|
||||
let data = null;
|
||||
@@ -65,41 +67,130 @@ function switchTab(idx) {
|
||||
}
|
||||
|
||||
function renderFeatureTree(tree) {
|
||||
let html = '<table><thead><tr><th style="width:40%">功能模块</th><th>来源</th><th>类型</th><th>调度</th><th>状态</th><th>最后运行</th></tr></thead><tbody>';
|
||||
function walk(node, depth) {
|
||||
const indent = 'style="padding-left:'+(depth*20+8)+'px"';
|
||||
let grp = (function(){let i=0;return function(){return 'ftg-'+(++i);}})();
|
||||
let html = '<table><thead><tr><th style="width:40%">功能模块</th><th>来源</th><th>类型</th><th>调度</th><th>状态</th><th>最后运行</th></tr></thead>';
|
||||
|
||||
function walk(node, depth, groupId) {
|
||||
const indent = 'padding-left:'+(depth*20+8)+'px';
|
||||
const cls = node.status || 'ok';
|
||||
|
||||
// 尝试匹配cron
|
||||
const hasKids = node.children && node.children.length > 0;
|
||||
const pipes = node.pipes || [];
|
||||
const isCat = hasKids && pipes.length === 0;
|
||||
|
||||
if (pipes.length === 0) {
|
||||
// 无对应cron的功能节点
|
||||
html += `<tr><td ${indent}><span class="badge ${cls}"></span><strong>${node.label}</strong><span class="cron-detail">(纯逻辑节点)</span></td>`;
|
||||
html += `<td>-</td><td>-</td><td>-</td><td class="pipeline-${cls==='ok'?'ok':'warn'}">${cls}</td><td>-</td></tr>`;
|
||||
} else {
|
||||
// 有对应cron的节点
|
||||
if (isCat) {
|
||||
// 分类节点——生成一个可折叠组
|
||||
const g = grp();
|
||||
html += `<tr id="${g}-h" onclick="toggleFt('${g}')" style="cursor:pointer" data-group="${groupId||''}">`;
|
||||
html += `<td style="${indent}"><span id="${g}-ico">▼</span> <span class="badge ${cls}"></span><strong>${node.label}</strong></td>`;
|
||||
html += `<td>-</td><td>-</td><td>-</td><td class="pipeline-${cls}">${cls}</td><td>-</td></tr>`;
|
||||
// 子节点都带上这个组的data-sub
|
||||
node.children.forEach(c => walk(c, depth+1, g));
|
||||
return;
|
||||
}
|
||||
|
||||
if (pipes.length > 0) {
|
||||
pipes.forEach((p, i) => {
|
||||
const indent2 = i===0 ? indent : 'style="padding-left:'+(depth*20+32)+'px"';
|
||||
const indent2 = i===0 ? indent : 'padding-left:'+(depth*20+32)+'px';
|
||||
const tagCls = p.status==='ok'?'ok':p.status==='error'?'error':'warn';
|
||||
const profileBadge = p.profile==='position-analyst' ? '📋' : '📦';
|
||||
const pBadge = p.profile==='position-analyst' ? '📋' : '📦';
|
||||
// 子节点显式带data-group
|
||||
const groupAttr = groupId ? `data-group="${groupId}"` : '';
|
||||
if (i===0) {
|
||||
html += `<tr><td ${indent2}><span class="badge ${cls}"></span><strong>${node.label}</strong></td>`;
|
||||
html += `<tr ${groupAttr}><td style="${indent2}"><span class="badge ${cls}"></span><strong>${node.label}</strong></td>`;
|
||||
} else {
|
||||
html += `<tr><td ${indent2}><span style="color:#30363d">└</span> ${p.name||p.script||'LLM'}</td>`;
|
||||
html += `<tr ${groupAttr}><td style="${indent2}"><span style="color:#30363d">└</span> ${p.name||p.script||'LLM'}</td>`;
|
||||
}
|
||||
html += `<td style="font-size:11px">${profileBadge} ${p.profile||'?'}</td>`;
|
||||
html += `<td style="font-size:11px">${pBadge} ${p.profile||'?'}</td>`;
|
||||
html += `<td><span class="cron-tag ${tagCls}">${p.type||'LLM'}</span></td>`;
|
||||
html += `<td style="font-size:11px">${p.schedule||'-'}</td>`;
|
||||
html += `<td class="pipeline-${tagCls}">${p.status}</td>`;
|
||||
html += `<td style="font-size:11px">${p.last_run||'-'}</td></tr>`;
|
||||
});
|
||||
} else {
|
||||
// 无cron的叶子
|
||||
const groupAttr = groupId ? `data-group="${groupId}"` : '';
|
||||
html += `<tr ${groupAttr}><td style="${indent}"><span class="badge ${cls}"></span><strong>${node.label}</strong></td>`;
|
||||
html += `<td>-</td><td>-</td><td>-</td><td class="pipeline-${cls}">${cls}</td><td>-</td></tr>`;
|
||||
}
|
||||
|
||||
if (node.children) node.children.forEach(c => walk(c, depth+1));
|
||||
if (hasKids && pipes.length > 0) {
|
||||
node.children.forEach(c => walk(c, depth+1, groupId));
|
||||
}
|
||||
}
|
||||
walk(tree, 0);
|
||||
html += '</tbody></table>';
|
||||
|
||||
walk(tree, 0, null);
|
||||
html += '</table>';
|
||||
return html;
|
||||
}
|
||||
|
||||
// 折叠切换:通过data-group控制
|
||||
let ftState = {};
|
||||
function toggleFt(g) {
|
||||
const ico = document.getElementById(g+'-ico');
|
||||
const isHidden = ftState[g];
|
||||
ftState[g] = !isHidden;
|
||||
ico.textContent = isHidden ? '▼' : '▶';
|
||||
// 隐藏/显示所有带data-group={g}的行
|
||||
document.querySelectorAll(`[data-group="${g}"]`).forEach(el => {
|
||||
el.style.display = isHidden ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function renderDataFlow(entities, jsonFiles) {
|
||||
// 只显示有写入方或读取方的实体
|
||||
const filtered = entities.filter(e => (e.writers||[]).length > 0 || (e.readers||[]).length > 0);
|
||||
// 只显示有读写关系的json
|
||||
const jf = jsonFiles.filter(j => (j.readers||[]).length > 0);
|
||||
|
||||
let html = '<div style="margin-bottom:10px;color:#8b949e;font-size:13px">';
|
||||
html += `显示 ${filtered.length} 个数据表 + ${jf.length} 个JSON文件的读写流向。绿色→写入,蓝色→读取。</div>`;
|
||||
|
||||
// 按写入方数量排序,最活跃的排前面
|
||||
filtered.sort((a,b) => (b.writers||[]).length - (a.writers||[]).length);
|
||||
|
||||
filtered.forEach(e => {
|
||||
const w = e.writers || [];
|
||||
const r = e.readers || [];
|
||||
if (w.length === 0 && r.length === 0) return;
|
||||
|
||||
html += '<div style="background:#161b22;border:1px solid #30363d;border-radius:6px;padding:10px;margin-bottom:8px">';
|
||||
html += `<div style="font-weight:bold;font-size:14px;color:#58a6ff;margin-bottom:6px">🗄️ ${e.name}</div>`;
|
||||
html += `<div style="font-size:11px;color:#8b949e;margin-bottom:6px">${e.desc||''} · ${e.rows}行</div>`;
|
||||
|
||||
// 写入方
|
||||
if (w.length > 0) {
|
||||
html += '<div style="margin-bottom:4px">';
|
||||
html += '<span style="color:#3fb950;font-size:11px">✏️ 写入:</span> ';
|
||||
html += w.map(s => `<span class="cron-tag ok" style="font-size:11px">${s}</span>`).join(' ');
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// 读取方
|
||||
if (r.length > 0) {
|
||||
html += '<div>';
|
||||
html += '<span style="color:#58a6ff;font-size:11px">📖 读取:</span> ';
|
||||
html += r.map(s => `<span class="cron-tag" style="border-color:#58a6ff;color:#58a6ff;font-size:11px">${s}</span>`).join(' ');
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
// JSON文件
|
||||
jf.forEach(j => {
|
||||
const r = j.readers || [];
|
||||
html += '<div style="background:#161b22;border:1px solid #30363d;border-radius:6px;padding:10px;margin-bottom:8px">';
|
||||
html += `<div style="font-weight:bold;font-size:14px;color:#d29922;margin-bottom:6px">📄 ${j.name}</div>`;
|
||||
html += `<div style="font-size:11px;color:#8b949e;margin-bottom:6px">${j.desc||''} · ${j.size_kb}KB</div>`;
|
||||
if (r.length > 0) {
|
||||
html += '<div>';
|
||||
html += '<span style="color:#58a6ff;font-size:11px">📖 读取:</span> ';
|
||||
html += r.map(s => `<span class="cron-tag" style="border-color:#58a6ff;color:#58a6ff;font-size:11px">${s}</span>`).join(' ');
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
@@ -170,6 +261,9 @@ function loadData() {
|
||||
});
|
||||
h2 += '</tbody></table>';
|
||||
document.getElementById('panel2').innerHTML = h2;
|
||||
|
||||
// Tab 3: 数据流
|
||||
document.getElementById('panel3').innerHTML = renderDataFlow(d.entities, d.json_files||[]);
|
||||
})
|
||||
.catch(e => document.getElementById('summary').innerHTML = '❌ 加载失败: ' + e.message);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user