dy-dashboard/static/js/knowledge-actions.js
2026-10-28 11:14:17 +08:00

521 lines
20 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─── 知识库操作功能(与 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>';
});
}