// ─── 知识库操作功能(与 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 += '
• ' + escHtml(a) + '
'; }); 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 || '未知错误') + '
'; }); }