521 lines
20 KiB
JavaScript
521 lines
20 KiB
JavaScript
// ─── 知识库操作功能(与 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 += '<option value="' + escHtml(t) + '">' + escHtml(t) + '</option>';
|
||
});
|
||
});
|
||
}
|
||
|
||
// ─── 删除 ───
|
||
|
||
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 = '<div style="text-align:center;padding:20px;color:var(--text-light)">⏳ 正在检查...</div>';
|
||
|
||
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 = '<div style="margin-bottom:8px">' + (data.success ? '✅ ' : '⚠️ ') + '检查完成</div>';
|
||
html += '<div style="font-family:monospace;font-size:12px;background:#f9f9f9;border-radius:4px;padding:8px 12px;line-height:1.6;max-height:300px;overflow-y:auto">';
|
||
lines.forEach(function(l) { html += escHtml(l) + '<br>'; });
|
||
html += '</div>';
|
||
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 = '<div style="padding:10px;color:var(--text-light)">✅ 未发现明显矛盾</div>';
|
||
return;
|
||
}
|
||
var html = '<div style="margin-bottom:6px;font-weight:500">找到 ' + res.count + ' 个关联主题</div>';
|
||
res.data.forEach(function(item) {
|
||
html += '<div style="margin-bottom:6px;padding:6px 8px;background:#fffbe6;border-radius:4px;border:1px solid #ffe58f">';
|
||
html += '<strong>' + escHtml(item.concept) + '</strong> (' + escHtml(item.keyword) + ')<br>';
|
||
html += '<span style="font-size:12px;color:var(--text-light)">出现在 ' + item.articles.length + ' 篇文章中:</span>';
|
||
item.articles.forEach(function(a) {
|
||
html += '<div style="font-size:12px;padding-left:10px">• <a href="javascript:void(0)" onclick="kbSwitchTab(\'wiki\');kbOpenArticle(\'' + escJs(a) + '\')">' + escHtml(a) + '</a></div>';
|
||
});
|
||
html += '</div>';
|
||
});
|
||
el.innerHTML = html;
|
||
})
|
||
.catch(function() {
|
||
el.innerHTML = '❌ 分析失败';
|
||
});
|
||
}
|
||
|
||
// ─── 关系图谱 ───
|
||
|
||
function kbLoadGraph() {
|
||
var container = document.getElementById('kbGraphContainer');
|
||
container.innerHTML = '<div style="text-align:center;padding:30px;color:var(--text-light)">⏳ 正在加载关系图谱...</div>';
|
||
|
||
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 = '<div style="text-align:center;padding:30px;color:var(--text-light)">暂无数据</div>';
|
||
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 = '<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:' + topicColors[t] + '"></span> ' + 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 = '<div style="padding:30px;text-align:center;color:var(--danger)">❌ 加载失败: ' + (err.message || '未知错误') + '</div>';
|
||
});
|
||
}
|