初中数学知识图谱

知识点总数:
已选中学生:
前置 后继 兄弟 联合考查 掌握度 < 60% (薄弱) 60-85% (良好) > 85% (优秀) 大师级
@push('scripts') @endpush graph: null, treeData: null, relationEdges: [], stats: { nodes: 0, extraEdges: 0 }, // 学生选择相关 // 学生选择相关 - 现在由Livewire管理 masteryData: {}, // 存储掌握度数据 { 'F01': 80, 'F02': 65, ... } arrow(w = 12, h = 14, r = 5) { if (window.G6?.Arrow?.triangle) { return G6.Arrow.triangle(w, h, r); } return [ ['M', 0, 0], ['L', w, h / 2], ['L', 0, h], ['Z'], ]; }, levelStyles: [ { fill: '#0ea5e9', stroke: '#0369a1', labelColor: '#0f172a', fontSize: 17, fontWeight: 700, size: 34, }, { fill: '#e0f2fe', stroke: '#38bdf8', labelColor: '#0f172a', fontSize: 16, fontWeight: 700, size: 30, }, { fill: '#f1f5f9', stroke: '#cbd5e1', labelColor: '#0f172a', fontSize: 14, fontWeight: 600, size: 26, }, ], relationStyles: { prerequisite: { type: 'quadratic', curveOffset: 60, style: { stroke: '#2563eb', lineWidth: 3.4, lineDash: [8, 6], endArrow: { path: null, fill: '#2563eb', d: 12, }, startArrow: false, }, label: '前置', }, successor: { type: 'quadratic', curveOffset: 60, style: { stroke: '#dc2626', lineWidth: 3.4, lineDash: [8, 6], endArrow: { path: null, fill: '#dc2626', d: 12, }, startArrow: false, }, label: '后继', }, sibling: { type: 'quadratic', curveOffset: 50, style: { stroke: '#64748b', lineDash: [6, 6], lineWidth: 3, endArrow: { path: null, fill: '#64748b', d: 10, }, }, label: '兄弟', }, joint: { type: 'quadratic', curveOffset: 50, style: { stroke: '#fcd34d', lineWidth: 3, lineDash: [10, 8], endArrow: { path: null, fill: '#fbbf24', d: 10, }, }, label: '联合', }, default: { type: 'quadratic', curveOffset: 50, style: { stroke: '#94a3b8', lineWidth: 3, lineDash: [10, 8], endArrow: { path: null, fill: '#94a3b8', d: 10, }, }, label: '', }, }, async initMindmap() { try { if (this.$nextTick) { await this.$nextTick(); } if (!window.G6) { console.error('G6 未加载'); return; } Object.keys(this.relationStyles).forEach((key) => { const rel = this.relationStyles[key]; if (rel?.style && rel.style.endArrow && !rel.style.endArrow.path) { rel.style.endArrow.path = this.arrow(rel.style.endArrow.d || 10, (rel.style.endArrow.d || 10) + 2, 4); } }); await Promise.all([ this.loadData(), ]); this.applyInitialCollapse(this.treeData); this.renderGraph(); window.addEventListener('resize', () => this.resizeGraph()); // 监听 Livewire 事件 window.addEventListener('mastery-updated', (event) => { console.log('Mastery updated:', event.detail.data); this.masteryData = event.detail.data || {}; this.refreshGraph(); }); } catch (err) { console.error('初始化思维导图失败', err); } }, // loadTeachers, loadStudents, loadMasteryData 已移除,由 Livewire 处理 async loadData() { const [treeResp, edgesResp] = await Promise.all([ fetch('/data/tree.json'), fetch('/data/edges.json'), ]); const rawTree = await treeResp.json(); const edges = await edgesResp.json(); const rawEdges = Array.isArray(edges) ? edges : edges?.edges || []; this.treeData = this.transformNode(rawTree); this.relationEdges = this.normalizeEdges(rawEdges); this.stats = { nodes: this.countNodes(this.treeData), extraEdges: this.relationEdges.length, }; }, refreshGraph() { if (this.graph && this.treeData) { // 重新装饰树数据以应用掌握度 const decoratedData = this.decorateTree(this.treeData); this.graph.changeData(decoratedData); this.graph.render(); this.graph.fitView(24); } }, transformNode(node, depth = 0) { if (!node) { return null; } const id = node.code || node.id || node.label || `node-${Math.random().toString(36).slice(2, 8)}`; const label = node.name || node.label || node.code || node.id || '未命名节点'; // 优先使用动态掌握度数据,其次回退到静态数据 const dynamicMastery = this.masteryData[id] || this.masteryData[node.code] || 0; const staticMastery = node.mastery_level || 0; const meta = { code: node.code || node.id || '', name: label, direct_score: node.direct_score || [], related_score: node.related_score || [], skills: node.skills || [], mastery_level: dynamicMastery || staticMastery, // 动态掌握度优先 }; return { id, label, meta, depth, children: (node.children || []).map((child) => this.transformNode(child, depth + 1)).filter(Boolean), }; }, applyInitialCollapse(node, depth = 0) { if (!node) { return; } if (depth >= 2 && node.children.length > 0) { node.collapsed = true; } node.children.forEach((child) => this.applyInitialCollapse(child, depth + 1)); }, countNodes(node) { if (!node) { return 0; } return 1 + node.children.reduce((sum, child) => sum + this.countNodes(child), 0); }, normalizeEdges(rawEdges) { const seen = new Set(); const normalized = []; (rawEdges || []).forEach((edge, index) => { if (!edge?.source || !edge?.target) { return; } const key = `${edge.source}-${edge.target}-${edge.type}`; if (seen.has(key)) { return; } seen.add(key); const relationStyle = this.relationStyles[edge.type] || this.relationStyles.default; normalized.push({ id: `rel-${index}-${edge.source}-${edge.target}`, source: edge.source, target: edge.target, type: relationStyle.type || 'quadratic', curveOffset: relationStyle.curveOffset || 50, style: relationStyle.style, label: relationStyle.label, comment: edge.comment || edge.note || '', }); }); return normalized; }, renderGraph(containerEl = null) { if (!this.treeData) { return; } const container = containerEl || document.getElementById('knowledge-mindmap'); if (!container) { console.error('容器未找到'); return; } const ensuredId = container.id || 'knowledge-mindmap'; if (!container.id) { container.id = ensuredId; } const bounds = container.getBoundingClientRect(); const width = Math.max(bounds.width, 600); const height = Math.max(bounds.height, 600); const tooltipEl = document.createElement('div'); tooltipEl.className = 'fixed z-50 pointer-events-none hidden'; document.body.appendChild(tooltipEl); const showTooltip = (html, x, y) => { tooltipEl.innerHTML = html; tooltipEl.style.left = `${x + 12}px`; tooltipEl.style.top = `${y + 12}px`; tooltipEl.classList.remove('hidden'); }; const hideTooltip = () => { tooltipEl.classList.add('hidden'); tooltipEl.innerHTML = ''; }; const G6Lib = window.G6?.default || window.G6; const TreeGraphClass = G6Lib?.TreeGraph || null; if (!TreeGraphClass) { console.error('G6 TreeGraph 不可用'); return; } const graphData = this.decorateTree(this.treeData); const graphConfig = { container: ensuredId, width, height, data: graphData, linkCenter: true, modes: { default: [ 'drag-canvas', 'zoom-canvas', { type: 'collapse-expand', trigger: 'click', onChange: function onChange(item, collapsed) { if (!item) return; item.getModel().collapsed = collapsed; return true; }, }, ], }, layout: { type: 'mindmap', direction: 'H', getHeight: () => 32, getWidth: () => 140, getVGap: () => 32, getHGap: () => 110, }, defaultNode: { size: 22, style: { stroke: '#94a3b8', fill: '#fff', radius: 4, shadowColor: undefined, shadowBlur: 0, lineWidth: 3, }, labelCfg: { style: { fontSize: 13, fill: '#0f172a', fontWeight: 500, }, position: 'right', offset: 12, }, }, defaultEdge: { type: 'cubic-horizontal', style: { stroke: '#cbd5f5', lineWidth: 3, shadowBlur: 0, shadowColor: undefined, }, }, nodeStateStyles: { selected: { lineWidth: 3.2, stroke: '#2563eb', fill: '#e0f2fe', }, }, edgeStateStyles: { highlight: { lineWidth: 3.4, stroke: '#fb923c', }, }, plugins: [], }; this.graph = new TreeGraphClass(graphConfig); if (typeof this.graph.data === 'function') { this.graph.data(graphData); } else if (typeof this.graph.changeData === 'function') { this.graph.changeData(graphData); } if (typeof this.graph.render === 'function') { this.graph.render(); } this.graph.data(this.decorateTree(this.treeData)); this.graph.render(); this.graph.fitView(24); this.drawRelationEdges(); this.bindEvents(); this.graph.on('node:mouseenter', (evt) => { const { clientX, clientY } = evt; showTooltip(this.buildTooltip(evt?.item?.getModel()), clientX, clientY); }); this.graph.on('node:mouseleave', hideTooltip); this.graph.on('edge:mouseenter', (evt) => { const model = evt?.item?.getModel() || {}; const relation = model.label || '关联关系'; const text = `${model.source || ''} → ${model.target || ''}`; const comment = model.comment ? `
${model.comment}
` : ''; const html = `
${relation}
${text}
${comment}
`; const { clientX, clientY } = evt; showTooltip(html, clientX, clientY); }); this.graph.on('edge:mouseleave', hideTooltip); const canvas = this.graph && typeof this.graph.get === 'function' ? this.graph.get('canvas') : null; if (canvas && typeof canvas.set === 'function') { canvas.set('localRefresh', false); const ctx = typeof canvas.get === 'function' ? canvas.get('context') : null; if (ctx) { ctx.shadowColor = 'transparent'; ctx.shadowBlur = 0; } } }, decorateTree(node) { if (!node) { return null; } // 动态获取最新掌握度数据对象 const id = node.id; const code = node.meta?.code; // masteryData 现在是对象 { 'KP_CODE': { mastery_level: 0.8, total_attempts: 5, ... } } const masteryInfo = this.masteryData[id] || (code && this.masteryData[code]) || null; const masteryLevel = masteryInfo ? (masteryInfo.mastery_level || 0) : (node.meta.mastery_level || 0); const totalAttempts = masteryInfo ? (masteryInfo.total_attempts || 0) : 0; const { nodeStyle, labelCfg, size, icon } = this.getNodeLevelStyle(node.depth, masteryLevel, totalAttempts); // 构建带图标的标签 let label = `${node.meta.code ? `${node.meta.code} · ` : ''}${node.label}`; if (icon) { label += ` ${icon}`; } // 更新meta中的掌握度,以便tooltip使用 const meta = { ...node.meta, mastery_level: masteryLevel, total_attempts: totalAttempts, mastery_info: masteryInfo }; return { id: node.id, label: label, meta: meta, collapsed: node.collapsed, depth: node.depth, size, style: nodeStyle, labelCfg, children: node.children.map((child) => this.decorateTree(child)).filter(Boolean), }; }, getNodeLevelStyle(depth = 0, masteryLevel = 0, totalAttempts = 0) { const style = this.levelStyles[depth] || this.levelStyles[this.levelStyles.length - 1]; // 根据掌握度调整颜色和样式 let fillColor, strokeColor, shadowColor, shadowBlur, icon; // 只要有答题记录(totalAttempts > 0),即使掌握度为0,也视为"薄弱"(红色) // 如果没有答题记录,则保持默认样式(白色) const hasAttempts = totalAttempts > 0; if (masteryLevel >= 85) { // 85%以上:大师级(绿色 + 光晕 + 星星) fillColor = '#dcfce7'; // green-100 strokeColor = '#16a34a'; // green-600 shadowColor = 'rgba(34, 197, 94, 0.6)'; shadowBlur = 10; icon = '★'; } else if (masteryLevel >= 60) { // 60-85%:良好(黄色) fillColor = '#fef9c3'; // yellow-100 strokeColor = '#ca8a04'; // yellow-600 shadowColor = undefined; shadowBlur = 0; icon = ''; } else if (masteryLevel > 0 || hasAttempts) { // 1-60% 或 掌握度为0但有答题记录:薄弱(红色) fillColor = '#fee2e2'; // red-100 strokeColor = '#dc2626'; // red-600 shadowColor = undefined; shadowBlur = 0; icon = ''; } else { // 未掌握且无记录:默认 fillColor = style.fill || '#fff'; strokeColor = style.stroke || '#cbd5f5'; shadowColor = undefined; shadowBlur = 0; icon = ''; } return { size: style.size || 22, icon, nodeStyle: { fill: fillColor, stroke: strokeColor, lineWidth: masteryLevel > 0 ? 3 : 3, // 保持一致线条宽度,靠颜色区分 radius: 6, shadowColor: shadowColor, shadowBlur: shadowBlur, cursor: 'pointer', }, labelCfg: { position: 'right', offset: 12, style: { fontSize: style.fontSize || 13, fontWeight: style.fontWeight || 600, fill: style.labelColor || '#0f172a', }, }, }; }, drawRelationEdges() { if (!this.graph || !this.relationEdges.length) { return; } this.relationEdges.forEach((edge, index) => { const style = { ...(edge.style || {}), lineAppendWidth: 14 }; if (style.endArrow && !style.endArrow.path) { style.endArrow = { ...style.endArrow, path: this.arrow(style.endArrow.d || 10, (style.endArrow.d || 10) + 2, 4), }; } this.graph.addItem('edge', { id: `extra-${index}`, source: edge.source, target: edge.target, type: edge.type || 'quadratic', curveOffset: edge.curveOffset || 50, style, label: edge.label, comment: edge.comment || '', labelCfg: { autoRotate: true, style: { fill: '#475569', fontSize: 11, background: { fill: 'rgba(255,255,255,0.85)', padding: [2, 4], radius: 4, }, }, }, }); }); }, buildTooltip(model) { const meta = model?.meta; if (!meta) { return '
无数据
'; } const range = (value) => (value?.length ? `${value[0]}-${value[1]}` : '未配置'); const mastery = meta.mastery_level || 0; const attempts = meta.total_attempts || 0; // 进度条颜色 let progressColorClass = 'bg-gray-300'; let masteryColor = '#9ca3af'; if (mastery >= 85) { progressColorClass = 'bg-green-500'; masteryColor = '#22c55e'; } else if (mastery >= 60) { progressColorClass = 'bg-yellow-500'; masteryColor = '#eab308'; } else if (mastery > 0 || attempts > 0) { progressColorClass = 'bg-red-500'; masteryColor = '#ef4444'; } const skills = (meta.skills || []).map(s => `
  • • ${s}
  • `).join('') || '
  • 暂无技能要点
  • '; // 下一级所需经验(模拟) const nextLevel = mastery >= 100 ? '已满级' : `距离下一级还需 ${Math.max(0, 100 - mastery)} 点`; return `
    ${meta.code || model.id} · ${meta.name}
    ${mastery >= 85 ? '★ 大师' : ''}
    掌握度 Lv.${Math.floor(mastery / 10)} (${attempts}次练习) ${mastery}%
    ${nextLevel}
    直接得分
    ${range(meta.direct_score)}
    关联得分
    ${range(meta.related_score)}
    技能要点
    `; }, bindEvents() { if (!this.graph) { return; } this.graph.on('node:click', (evt) => { const nodeId = evt?.item?.getID(); if (nodeId) { this.highlightEdges(nodeId); } }); this.graph.on('canvas:click', () => this.resetHighlight()); }, highlightEdges(nodeId) { this.graph.getNodes().forEach((node) => { this.graph.clearItemStates(node); if (node.getID() === nodeId) { this.graph.setItemState(node, 'selected', true); } }); this.graph.getEdges().forEach((edge) => { const { source, target } = edge.getModel(); const linked = source === nodeId || target === nodeId; if (linked) { this.graph.setItemState(edge, 'highlight', true); } else { this.graph.clearItemStates(edge); } }); }, resetHighlight() { if (!this.graph) return; this.graph.getNodes().forEach((node) => this.graph.clearItemStates(node)); this.graph.getEdges().forEach((edge) => this.graph.clearItemStates(edge)); }, resizeGraph() { if (!this.graph) return; const container = document.getElementById('knowledge-mindmap'); if (!container) return; this.graph.changeSize(container.clientWidth, container.clientHeight); this.graph.fitView(24); }, }); }); @endpush