// ─── 知识库操作功能(与 app.js 配合)───
// 标签页切换(扩展支持 analysis)
var _origSwitch = kbSwitchTab;
kbSwitchTab = function(tab) {
window._kbActiveTab = tab;
['wiki','datalib','profile','analysis'].forEach(function(t) {
var tabEl = document.getElementById('kbTab' + t.charAt(0).toUpperCase() + t.slice(1));
var paneEl = document.getElementById('kbPane' + t.charAt(0).toUpperCase() + t.slice(1));
if (tabEl) tabEl.classList.toggle('active', t === tab);
if (paneEl) paneEl.style.display = t === tab ? '' : 'none';
});
// 分析面板进入时自动加载
if (tab === 'analysis') {
kbRunLint();
kbLoadContradictions();
}
};
// ─── 新建 ───
var _kbEditingPath = null;
function kbShowCreateModal() {
_kbEditingPath = null;
document.getElementById('kbEditorTitle').textContent = '📝 新建文章';
document.getElementById('kbEditorArticleTitle').value = '';
document.getElementById('kbEditorContent').value = '';
document.getElementById('kbEditorSaveBtn').textContent = '创建';
kbLoadTopicSelect('kbEditorTopic');
document.getElementById('kbEditorModal').style.display = 'flex';
}
function kbShowEditModal() {
_kbEditingPath = document.getElementById('kbArticleTitle').textContent;
document.getElementById('kbEditorTitle').textContent = '✏️ 编辑文章';
document.getElementById('kbEditorSaveBtn').textContent = '保存';
kbLoadTopicSelect('kbEditorTopic');
// 加载当前内容
fetch('/api/knowledge/article?path=' + encodeURIComponent(_kbEditingPath))
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code !== 0) { alert(res.msg); return; }
var text = res.data.content;
// 提取标题和内容
var lines = text.split('\n');
var title = '';
var content = '';
for (var i = 0; i < lines.length; i++) {
if (lines[i].startsWith('# ')) {
title = lines[i].substring(2).trim();
} else if (i > 0 || !lines[i].startsWith('# ')) {
content += lines[i] + '\n';
}
}
document.getElementById('kbEditorArticleTitle').value = title;
document.getElementById('kbEditorContent').value = content.trim();
// 设置主题选择
var topic = _kbEditingPath.split('/')[0];
var sel = document.getElementById('kbEditorTopic');
for (var j = 0; j < sel.options.length; j++) {
if (sel.options[j].value === topic) { sel.selectedIndex = j; break; }
}
});
document.getElementById('kbEditorModal').style.display = 'flex';
}
function kbCloseEditor() {
document.getElementById('kbEditorModal').style.display = 'none';
}
function kbSaveArticle() {
var title = document.getElementById('kbEditorArticleTitle').value.trim();
var content = document.getElementById('kbEditorContent').value.trim();
if (!title) { alert('请输入标题'); return; }
if (!content) { alert('请输入内容'); return; }
var topic = _kbEditingPath ? _kbEditingPath.split('/')[0] : document.getElementById('kbEditorTopic').value;
if (_kbEditingPath) {
// 更新
var md = '# ' + title + '\n\n' + content;
fetch('/api/knowledge/article/update', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({path: _kbEditingPath, content: md})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
kbCloseEditor();
kbBackToList();
kbLoadIndex();
} else {
alert(res.msg);
}
});
} else {
// 新建
var md = '# ' + title + '\n\n' + content;
fetch('/api/knowledge/article/create', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({title: title, content: md, topic: topic})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
kbCloseEditor();
kbLoadIndex();
} else {
alert(res.msg);
}
});
}
}
function kbLoadTopicSelect(elementId) {
fetch('/api/knowledge/topics')
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code !== 0) return;
var sel = document.getElementById(elementId);
if (!sel) return;
sel.innerHTML = '';
res.data.forEach(function(t) {
sel.innerHTML += '';
});
});
}
// ─── 删除 ───
function kbDeleteArticle() {
var path = document.getElementById('kbArticleTitle').textContent;
showCenteredConfirm('确认删除', '确定要删除「' + path + '」吗?此操作不可撤销。').then(function(ok) {
if (!ok) return;
fetch('/api/knowledge/article/delete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({path: path})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
kbBackToList();
kbLoadIndex();
} else {
alert(res.msg);
}
});
});
}
// ─── 导出 ───
function kbExportArticle() {
var path = document.getElementById('kbArticleTitle').textContent;
fetch('/api/knowledge/export?path=' + encodeURIComponent(path))
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code !== 0) { alert(res.msg); return; }
var blob = new Blob([res.data.content], {type: 'text/markdown'});
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = res.data.filename;
a.click();
URL.revokeObjectURL(url);
});
}
// ─── 导入 ───
function kbShowImportModal() {
kbLoadTopicSelect('kbImportTopic');
document.getElementById('kbImportFile').value = '';
document.getElementById('kbImportModal').style.display = 'flex';
}
function kbDoImport() {
var topic = document.getElementById('kbImportTopic').value;
var fileInput = document.getElementById('kbImportFile');
if (!fileInput.files || !fileInput.files[0]) { alert('请选择文件'); return; }
var formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('topic', topic);
fetch('/api/knowledge/import', {
method: 'POST',
body: formData
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code === 0) {
document.getElementById('kbImportModal').style.display = 'none';
kbSwitchTab('wiki');
kbLoadIndex();
} else {
alert(res.msg);
}
});
}
// ─── Lint ───
function kbRunLint() {
var el = document.getElementById('kbLintResult');
el.innerHTML = '
⏳ 正在检查...
';
fetch('/api/knowledge/lint', {method: 'POST'})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code !== 0) { el.innerHTML = '❌ 检查失败'; return; }
var data = res.data;
var lines = (data.stdout || '').split('\n').filter(function(l) { return l.trim(); });
var html = '' + (data.success ? '✅ ' : '⚠️ ') + '检查完成
';
html += '';
lines.forEach(function(l) { html += escHtml(l) + '
'; });
html += '
';
el.innerHTML = html;
})
.catch(function() {
el.innerHTML = '❌ 检查出错';
});
}
// ─── 矛盾分析 ───
function kbLoadContradictions() {
var el = document.getElementById('kbContradictionResult');
fetch('/api/knowledge/contradictions')
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code !== 0 || res.count === 0) {
el.innerHTML = '✅ 未发现明显矛盾
';
return;
}
var html = '找到 ' + res.count + ' 个关联主题
';
res.data.forEach(function(item) {
html += '';
html += '
' + escHtml(item.concept) + ' (' + escHtml(item.keyword) + ')
';
html += '
出现在 ' + item.articles.length + ' 篇文章中:';
item.articles.forEach(function(a) {
html += '
';
});
html += '
';
});
el.innerHTML = html;
})
.catch(function() {
el.innerHTML = '❌ 分析失败';
});
}
// ─── 关系图谱 ───
function kbLoadGraph() {
var container = document.getElementById('kbGraphContainer');
container.innerHTML = '⏳ 正在加载关系图谱...
';
fetch('/api/knowledge/graph')
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.code !== 0) { container.innerHTML = '❌ 加载失败'; return; }
var data = res.data;
var nodes = (data.nodes || []).map(function(n, i) { return {id: n.id, label: n.label, topic: n.topic, x: 0, y: 0, vx: 0, vy: 0}; });
var links = (data.edges || []).map(function(e) { return {source: e.source, target: e.target}; });
if (nodes.length === 0) {
container.innerHTML = '暂无数据
';
return;
}
var width = container.clientWidth || 760;
var height = 420;
// 主题颜色映射
var topicColors = {
'采购管理': '#e74c3c', '库存管理': '#3498db', '供应链流程': '#27ae60',
'数据分析': '#f39c12', '产品知识': '#8e44ad', '行业资讯': '#16a085'
};
container.innerHTML = '';
// 构建 Canvas
var canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
canvas.style.cssText = 'width:' + width + 'px;height:' + height + 'px;background:#fafafa;border-radius:8px;cursor:pointer;display:block';
container.appendChild(canvas);
var ctx = canvas.getContext('2d');
// 构建节点索引
var nodeIndex = {};
nodes.forEach(function(n) { nodeIndex[n.id] = n; });
// 解析 links 为索引引用
var linkData = [];
links.forEach(function(l) {
var s = typeof l.source === 'object' ? l.source.id : l.source;
var t = typeof l.target === 'object' ? l.target.id : l.target;
if (nodeIndex[s] && nodeIndex[t]) {
linkData.push({source: nodeIndex[s], target: nodeIndex[t]});
}
});
// 初始化位置
var cx = width / 2, cy = height / 2;
var angleStep = (2 * Math.PI) / nodes.length;
nodes.forEach(function(n, i) {
n.x = cx + Math.cos(angleStep * i) * (Math.min(width, height) * 0.3);
n.y = cy + Math.sin(angleStep * i) * (Math.min(width, height) * 0.3);
});
// 计算每个节点的链接数
var linkCount = {};
nodes.forEach(function(n) { linkCount[n.id] = 0; });
linkData.forEach(function(l) {
linkCount[l.source.id]++;
linkCount[l.target.id]++;
});
// 力模拟参数
var repulsion = 800;
var attraction = 0.01;
var damping = 0.85;
var minDist = 60;
function simulateStep() {
// 排斥力
for (var i = 0; i < nodes.length; i++) {
for (var j = i + 1; j < nodes.length; j++) {
var a = nodes[i], b = nodes[j];
var dx = a.x - b.x;
var dy = a.y - b.y;
var dist = Math.sqrt(dx * dx + dy * dy) || 1;
var force = repulsion / (dist * dist);
var fx = (dx / dist) * force;
var fy = (dy / dist) * force;
a.vx += fx; a.vy += fy;
b.vx -= fx; b.vy -= fy;
}
}
// 吸引力(边)
linkData.forEach(function(l) {
var dx = l.target.x - l.source.x;
var dy = l.target.y - l.source.y;
var dist = Math.sqrt(dx * dx + dy * dy) || 1;
var force = (dist - 120) * attraction;
var fx = (dx / dist) * force;
var fy = (dy / dist) * force;
l.source.vx += fx; l.source.vy += fy;
l.target.vx -= fx; l.target.vy -= fy;
});
// 中心吸引力
nodes.forEach(function(n) {
n.vx += (cx - n.x) * 0.005;
n.vy += (cy - n.y) * 0.005;
});
// 碰撞检测
for (var i = 0; i < nodes.length; i++) {
for (var j = i + 1; j < nodes.length; j++) {
var a = nodes[i], b = nodes[j];
var dx = a.x - b.x;
var dy = a.y - b.y;
var dist = Math.sqrt(dx * dx + dy * dy) || 1;
if (dist < minDist) {
var overlap = (minDist - dist) / 2;
var fx = (dx / dist) * overlap;
var fy = (dy / dist) * overlap;
a.x += fx; a.y += fy;
b.x -= fx; b.y -= fy;
}
}
}
// 应用速度和阻尼
nodes.forEach(function(n) {
n.vx *= damping;
n.vy *= damping;
n.x += n.vx;
n.y += n.vy;
// 边界约束
n.x = Math.max(30, Math.min(width - 30, n.x));
n.y = Math.max(30, Math.min(height - 30, n.y));
});
}
// 模拟 200 步
for (var step = 0; step < 200; step++) {
simulateStep();
}
// 渲染
function render() {
ctx.clearRect(0, 0, width, height);
// 画边
ctx.strokeStyle = 'rgba(150,150,150,0.4)';
ctx.lineWidth = 1.5;
linkData.forEach(function(l) {
ctx.beginPath();
ctx.moveTo(l.source.x, l.source.y);
ctx.lineTo(l.target.x, l.target.y);
ctx.stroke();
});
// 画节点
nodes.forEach(function(n) {
var cnt = linkCount[n.id] || 0;
ctx.beginPath();
ctx.arc(n.x, n.y, cnt > 2 ? 14 : 10, 0, Math.PI * 2);
ctx.fillStyle = topicColors[n.topic] || '#95a5a6';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
// 标签
ctx.fillStyle = '#333';
ctx.font = '11px -apple-system, sans-serif';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
var label = n.label.length > 8 ? n.label.substring(0, 8) + '..' : n.label;
ctx.fillText(label, n.x + 16, n.y);
// 链接数
ctx.fillStyle = 'rgba(0,0,0,0.35)';
ctx.font = '9px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(linkCount[n.id], n.x, n.y);
});
}
render();
// 鼠标交互
var dragTarget = null;
var dragOffsetX = 0, dragOffsetY = 0;
canvas.addEventListener('mousedown', function(e) {
var rect = canvas.getBoundingClientRect();
var mx = e.clientX - rect.left;
var my = e.clientY - rect.top;
for (var i = nodes.length - 1; i >= 0; i--) {
var n = nodes[i];
var cnt = linkCount[n.id] || 0; var r = cnt > 2 ? 14 : 10;
var dx = mx - n.x, dy = my - n.y;
if (dx * dx + dy * dy < r * r + 10) {
dragTarget = n;
dragOffsetX = mx - n.x;
dragOffsetY = my - n.y;
return;
}
}
});
canvas.addEventListener('mousemove', function(e) {
if (!dragTarget) return;
var rect = canvas.getBoundingClientRect();
dragTarget.x = e.clientX - rect.left - dragOffsetX;
dragTarget.y = e.clientY - rect.top - dragOffsetY;
render();
});
canvas.addEventListener('mouseup', function(e) {
if (!dragTarget) {
// 点击节点跳转
var rect = canvas.getBoundingClientRect();
var mx = e.clientX - rect.left;
var my = e.clientY - rect.top;
for (var i = nodes.length - 1; i >= 0; i--) {
var n = nodes[i];
var cnt = linkCount[n.id] || 0; var r = cnt > 2 ? 14 : 10;
var dx = mx - n.x, dy = my - n.y;
if (dx * dx + dy * dy < r * r + 10) {
kbSwitchTab('wiki');
kbOpenArticle(n.id);
return;
}
}
}
dragTarget = null;
// 释放后再模拟几步
for (var i = 0; i < 50; i++) simulateStep();
render();
});
// 图例
var legendDiv = document.createElement('div');
legendDiv.style.cssText = 'display:flex;gap:14px;padding:8px 14px;font-size:12px;flex-wrap:wrap';
for (var t in topicColors) {
var has = nodes.some(function(n) { return n.topic === t; });
if (has) {
var item = document.createElement('div');
item.style.cssText = 'display:flex;align-items:center;gap:4px';
item.innerHTML = ' ' + t;
legendDiv.appendChild(item);
}
}
var tip = document.createElement('div');
tip.style.cssText = 'color:#999;margin-left:auto';
tip.textContent = '🖱️ 拖拽移动 · 点击打开文章';
legendDiv.appendChild(tip);
container.appendChild(legendDiv);
})
.catch(function(err) {
console.error('Graph error:', err);
container.innerHTML = '❌ 加载失败: ' + (err.message || '未知错误') + '
';
});
}