<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>知识图谱脑图（公开查看）</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@500;600;700&display=swap" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
    <script src="https://gw.alipayobjects.com/os/lib/antv/g6/4.8.24/dist/g6.min.js"></script>
    <style>
        :root {
            color-scheme: light;
        }
        body {
            margin: 0;
            font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            background: radial-gradient(circle at 10% 20%, #e0f2fe 0, transparent 20%), radial-gradient(circle at 90% 10%, #fce7f3 0, transparent 18%), #f8fafc;
            color: #0f172a;
        }
        .page {
            max-width: 1400px;
            margin: 0 auto;
            padding: 32px 20px 40px;
        }
        .card {
            background: white;
            border: 1px solid #e2e8f0;
            border-radius: 14px;
            box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
            padding: 18px 20px;
        }
        .stat {
            display: inline-flex;
            align-items: center;
            gap: 8px;
            padding: 8px 12px;
            border-radius: 10px;
            background: #f8fafc;
            border: 1px dashed #e2e8f0;
            font-size: 12px;
            color: #475569;
        }
        #knowledge-mindmap {
            width: 100%;
            min-height: 760px;
            height: 78vh;
            border: 1px solid #e2e8f0;
            border-radius: 14px;
            background: white;
        }
    </style>
</head>
<body>
<div class="page" x-data="knowledgeMindmap()" x-init="initMindmap()">
    <div class="card" style="margin-bottom: 16px;">
        <div style="display: flex; flex-wrap: wrap; justify-content: space-between; gap: 16px;">
            <div style="max-width: 780px;">
                <div style="font-size: 20px; font-weight: 700; color: #0f172a;">初中数学知识图谱 · 思维导图</div>
                <p style="margin: 8px 0 6px; font-size: 13px; color: #64748b; line-height: 1.5;">
                    tree.json 提供完整层级（模块 → 知识点），edges.json 描述跨节点关系；基于 AntV G6 MindMap 布局，节点可逐层展开/折叠，线条带方向。
                </p>
                <div style="display: flex; gap: 10px; flex-wrap: wrap;">
                    <span class="stat">节点总数：<span x-text="stats.nodes"></span></span>
                    <span class="stat">跨边数量：<span x-text="stats.extraEdges"></span></span>
                </div>
            </div>
            <div style="display: flex; gap: 12px; flex-wrap: wrap; align-items: center; font-size: 12px; color: #475569;">
                <span style="display:inline-flex;align-items:center;gap:6px;">
                    <span style="display:block;width:16px;height:6px;border-radius:999px;background:#2563eb;"></span> 前置
                </span>
                <span style="display:inline-flex;align-items:center;gap:6px;">
                    <span style="display:block;width:16px;height:6px;border-radius:999px;background:#dc2626;"></span> 后继
                </span>
                <span style="display:inline-flex;align-items:center;gap:6px;">
                    <span style="display:block;width:16px;height:6px;border-radius:999px;border:2px dashed #64748b;"></span> 兄弟
                </span>
                <span style="display:inline-flex;align-items:center;gap:6px;">
                    <span style="display:block;width:16px;height:6px;border-radius:999px;background:#fcd34d;"></span> 联合
                </span>
            </div>
        </div>
    </div>

    <div id="knowledge-mindmap" aria-label="知识图谱脑图"></div>
</div>

<script>
        window.knowledgeMindmap = () => ({
            isTreeGraph: true,
            graph: null,
            treeData: null,
            relationEdges: [],
            stats: { nodes: 0, extraEdges: 0 },
            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'],
                ];
            },
            // ... (omitted for brevity, but I need to target the right place)

            levelStyles: [
                {
                    fill: '#0ea5e9',
                    stroke: '#0369a1',
                    labelColor: '#0f172a',
                    fontSize: 22, // Increased from 17
                    fontWeight: 700,
                    size: 40, // Increased from 34
                    lineWidth: 4, // Explicitly set thicker line
                },
                {
                    fill: '#e0f2fe',
                    stroke: '#38bdf8',
                    labelColor: '#0f172a',
                    fontSize: 20, // Increased from 16
                    fontWeight: 700,
                    size: 36, // Increased from 30
                    lineWidth: 4,
                },
                {
                    fill: '#f1f5f9',
                    stroke: '#cbd5e1',
                    labelColor: '#0f172a',
                    fontSize: 18, // Increased from 14
                    fontWeight: 600,
                    size: 32, // Increased from 26
                    lineWidth: 3.5,
                },
            ],
            relationStyles: {
                prerequisite: {
                    type: 'quadratic',
                    curveOffset: 60,
                    style: {
                        stroke: '#2563eb',
                        lineWidth: 5, // Increased from 3.4
                        lineDash: [8, 6],
                        endArrow: {
                            path: null,
                            fill: '#2563eb',
                            d: 16, // Increased arrow size
                        },
                        startArrow: false,
                        shadowBlur: 0,
                        shadowColor: undefined,
                    },
                    label: '前置',
                },
                successor: {
                    type: 'quadratic',
                    curveOffset: 60,
                    style: {
                        stroke: '#dc2626',
                        lineWidth: 5, // Increased from 3.4
                        lineDash: [8, 6],
                        endArrow: {
                            path: null,
                            fill: '#dc2626',
                            d: 16,
                        },
                        startArrow: false,
                        shadowBlur: 0,
                        shadowColor: undefined,
                    },
                    label: '后继',
                },
                sibling: {
                    type: 'quadratic',
                    curveOffset: 50,
                    style: {
                        stroke: '#64748b',
                        lineDash: [6, 6],
                        lineWidth: 4, // Increased from 3
                        endArrow: {
                            path: null,
                            fill: '#64748b',
                            d: 14,
                        },
                        shadowBlur: 0,
                        shadowColor: undefined,
                    },
                    label: '兄弟',
                },
                joint: {
                    type: 'quadratic',
                    curveOffset: 50,
                    style: {
                        stroke: '#fcd34d',
                        lineWidth: 4, // Increased from 3
                        lineDash: [10, 8],
                        endArrow: {
                            path: null,
                            fill: '#fbbf24',
                            d: 14,
                        },
                        shadowBlur: 0,
                        shadowColor: undefined,
                    },
                    label: '联合',
                },
                default: {
                    type: 'quadratic',
                    curveOffset: 50,
                    style: {
                        stroke: '#94a3b8',
                        lineWidth: 4, // Increased from 3
                        lineDash: [10, 8],
                        endArrow: {
                            path: null,
                            fill: '#94a3b8',
                            d: 14,
                        },
                        shadowBlur: 0,
                        shadowColor: undefined,
                    },
                    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 || 14, (rel.style.endArrow.d || 14) + 4, 6);
                        }
                    });
                    await this.loadData();
                    this.applyInitialCollapse(this.treeData);
                    this.renderGraph();
                    window.addEventListener('resize', () => this.resizeGraph());
                } catch (err) {
                    console.error('初始化思维导图失败', err);
                    const container = document.getElementById('knowledge-mindmap');
                    if (container) {
                        container.innerHTML = '<div style="padding:20px;color:#dc2626;">图数据加载失败，请检查 /data/tree.json 与 /data/edges.json 是否可访问。</div>';
                    }
                }
            },
            async loadData() {
                const treeUrl = '/data/tree.json';
                const edgeUrl = '/data/edges.json';
                const [treeResp, edgesResp] = await Promise.all([fetch(treeUrl), fetch(edgeUrl)]);
                if (!treeResp.ok || !edgesResp.ok) {
                    throw new Error(`数据加载失败 tree:${treeResp.status} edge:${edgesResp.status}`);
                }
                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);
                if (!this.treeData) {
                    throw new Error('tree.json 为空或格式不正确');
                }
                this.stats = {
                    nodes: this.countNodes(this.treeData),
                    extraEdges: this.relationEdges.length,
                };
            },
            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 meta = {
                    code: node.code || node.id || '',
                    name: label,
                    direct_score: node.direct_score || [],
                    related_score: node.related_score || [],
                    skills: node.skills || [],
                };
                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 || container.clientWidth || 0, 600);
                const height = Math.max(bounds.height || container.clientHeight || 0, 600);
                const tooltipEl = document.createElement('div');
                tooltipEl.style.position = 'fixed';
                tooltipEl.style.pointerEvents = 'none';
                tooltipEl.style.zIndex = '9999';
                tooltipEl.style.display = 'none';
                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.style.display = 'block';
                };
                const hideTooltip = () => {
                    tooltipEl.style.display = 'none';
                    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: () => 40, // Increased from 32
                        getWidth: () => 140,
                        getVGap: () => 32,
                        getHGap: () => 60, // Decreased from 110
                    },
                    defaultNode: {
                        size: 26, // Increased from 22
                        style: {
                            stroke: '#94a3b8',
                            fill: '#fff',
                            radius: 4,
                            shadowColor: undefined,
                            shadowBlur: 0,
                            lineWidth: 4, // Increased from 3
                        },
                        labelCfg: {
                            style: {
                                fontSize: 16, // Increased from 13
                                fill: '#0f172a',
                                fontWeight: 600, // Increased weight
                            },
                            position: 'right',
                            offset: 14,
                        },
                    },
                    defaultEdge: {
                        type: 'cubic-horizontal',
                        style: {
                            stroke: '#cbd5f5',
                            lineWidth: 4, // Increased from 3
                            shadowBlur: 0,
                            shadowColor: undefined,
                        },
                    },
                    nodeStateStyles: {
                        selected: {
                            lineWidth: 4.5, // Increased
                            stroke: '#2563eb',
                            fill: '#e0f2fe',
                        },
                    },
                    edgeStateStyles: {
                        highlight: {
                            lineWidth: 5, // Increased
                            stroke: '#fb923c',
                        },
                    },
                    plugins: [],
                };
                try {
                    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();
                    }
                } catch (e) {
                    console.error('创建图失败', e);
                    return;
                }
                this.drawRelationEdges();
                this.graph.fitView(24);
                this.bindEvents();
                // 手动 tooltip
                const handleNodeEnter = (evt) => {
                    const { clientX, clientY } = evt;
                    showTooltip(this.buildTooltip(evt?.item?.getModel()), clientX, clientY);
                };
                const handleEdgeEnter = (evt) => {
                    const model = evt?.item?.getModel() || {};
                    const relation = model.label || '关联关系';
                    const text = `${model.source || ''} → ${model.target || ''}`;
                    const comment = model.comment ? `<div style="font-size:11px;color:#475569;margin-top:4px;white-space:pre-line;">备注：${model.comment}</div>` : '';
                    const html = `
                        <div style="border:1px solid #e2e8f0;border-radius:8px;background:white;padding:8px 10px;font-size:12px;color:#475569;box-shadow:0 10px 20px rgba(15,23,42,0.08);">
                            <div style="font-weight:700;color:#0f172a;margin-bottom:4px;">${relation}</div>
                            <div>${text}</div>
                            ${comment}
                        </div>
                    `;
                    const { clientX, clientY } = evt;
                    showTooltip(html, clientX, clientY);
                };
                const handleLeave = () => hideTooltip();
                this.graph.on('node:mouseenter', handleNodeEnter);
                this.graph.on('node:mouseleave', handleLeave);
                this.graph.on('edge:mouseenter', handleEdgeEnter);
                this.graph.on('edge:mouseleave', handleLeave);
                
                // 监听布局变化，重新绘制关联线
                this.graph.on('afterlayout', () => {
                    this.drawRelationEdges();
                });
                // 全量刷新，避免缩放重影
                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 { nodeStyle, labelCfg, size } = this.getNodeLevelStyle(node.depth);
                return {
                    id: node.id,
                    label: `${node.meta.code ? `${node.meta.code} · ` : ''}${node.label}`,
                    meta: node.meta,
                    collapsed: node.collapsed,
                    depth: node.depth,
                    size,
                    style: nodeStyle,
                    labelCfg,
                    children: node.children.map((child) => this.decorateTree(child)).filter(Boolean),
                };
            },
            getNodeLevelStyle(depth = 0) {
                const style = this.levelStyles[depth] || this.levelStyles[this.levelStyles.length - 1];
                return {
                    size: style.size || 26, // Increased default
                    nodeStyle: {
                        fill: style.fill || '#fff',
                        stroke: style.stroke || '#cbd5f5',
                        lineWidth: style.lineWidth || 4, // Increased default
                        radius: 6,
                        shadowColor: undefined,
                        shadowBlur: 0,
                    },
                    labelCfg: {
                        position: 'right',
                        offset: 14, // Increased offset
                        style: {
                            fontSize: style.fontSize || 16, // Increased default
                            fontWeight: style.fontWeight || 600,
                            fill: style.labelColor || '#0f172a',
                        },
                    },
                };
            },
            drawRelationEdges() {
                if (!this.graph || !this.relationEdges.length) return;
                const canAddEdge = typeof this.graph.addEdge === 'function';
                const canAddItem = typeof this.graph.addItem === 'function';
                const buildModel = (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),
                        };
                    }
                    return {
                        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,
                                },
                            },
                        },
                    };
                };
                if (this.isTreeGraph && (canAddEdge || canAddItem)) {
                    this.relationEdges.forEach((edge, index) => {
                        // 检查节点是否存在
                        if (!this.graph.findById(edge.source) || !this.graph.findById(edge.target)) {
                            return;
                        }
                        const model = buildModel(edge, index);
                        // 防止重复添加
                        if (this.graph.findById(model.id)) {
                            return;
                        }
                        if (canAddEdge) {
                            this.graph.addEdge(model);
                        } else {
                            this.graph.addItem('edge', model);
                        }
                    });
                    if (typeof this.graph.paint === 'function') {
                        this.graph.paint();
                    }
                } else {
                    const currentData = this.graph.save?.() || this.graphDataset || { nodes: [], edges: [] };
                    const nodes = currentData.nodes || [];
                    const mergedEdges = (currentData.edges || []).concat(
                        this.relationEdges.map((edge, index) => buildModel(edge, index))
                    );
                    const merged = { nodes, edges: mergedEdges };
                    if (typeof this.graph.changeData === 'function') {
                        this.graph.changeData(merged);
                    } else if (typeof this.graph.data === 'function' && typeof this.graph.render === 'function') {
                        this.graph.data(merged);
                        this.graph.render();
                    }
                }
            },
            buildTooltip(model) {
                const meta = model?.meta;
                if (!meta) return '<div class="text-xs text-gray-600">无数据</div>';
                const range = (value) => (value?.length ? `${value[0]}-${value[1]}` : '未配置');
                const skills = (meta.skills || [])
                    .slice(0, 6)
                    .map((skill) => `<li style="line-height:1.1;">${skill.trim()}</li>`)
                    .join('') || '<li>暂无技能</li>';
                return `
                    <div style="min-width:230px;max-width:340px;border:1px solid #e2e8f0;border-radius:8px;background:white;padding:10px;font-size:12px;color:#475569;box-shadow:0 10px 30px rgba(15,23,42,0.12);">
                        <div style="font-size:14px;font-weight:700;color:#0f172a;margin-bottom:4px;">${meta.code || model.id} · ${meta.name}</div>
                        <div style="display:flex;gap:12px;font-size:12px;">
                            <span>直接：${range(meta.direct_score)}</span>
                            <span>关联：${range(meta.related_score)}</span>
                        </div>
                        <div style="margin-top:8px;">
                            <div style="font-weight:600;">技能要点</div>
                            <ul style="padding-left:18px;margin:6px 0; list-style: disc; display: grid; gap: 4px;">
                                ${skills}
                            </ul>
                        </div>
                    </div>
                `;
            },
            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);
            },
        });
</script>
</body>
</html>
