| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826 |
- @php
- $point = $knowledgePoint ?? null;
- $phaseParam = request()->query('phase');
- @endphp
- <x-filament-panels::page>
- @if($point)
- <div class="space-y-6">
- <!-- 头部信息 - 紧凑版本 -->
- <div class="rounded-xl bg-gradient-to-br from-primary-500 via-primary-600 to-indigo-600 px-4 py-4 text-white shadow-xl">
- <div class="flex items-center justify-between">
- <div>
- <h2 class="text-2xl font-bold">{{ $point['cn_name'] }}</h2>
- <p class="text-sm mt-1 opacity-90">{{ $point['kp_code'] }}</p>
- </div>
- <div class="flex gap-3 text-xs">
- <span class="bg-white/20 px-2 py-1 rounded-full">{{ $point['category'] ?? '未分类' }}</span>
- <span class="bg-white/20 px-2 py-1 rounded-full">{{ $point['phase'] ?? '未知学段' }}</span>
- @if($point['grade'])
- <span class="bg-white/20 px-2 py-1 rounded-full">{{ $point['grade'] }}年级</span>
- @endif
- </div>
- </div>
- </div>
- <!-- 图谱信息统计 -->
- @if(!empty($point['edge_summary']))
- <div class="rounded-xl border border-gray-200 bg-gray-50/50 px-6 py-6 shadow-sm dark:border-gray-800 dark:bg-gray-900/50 mb-6">
- <h3 class="text-lg font-semibold mb-4 text-gray-900 dark:text-gray-100">图谱统计</h3>
- <div class="grid gap-4 md:grid-cols-4">
- <div class="text-center">
- <p class="text-2xl font-bold text-blue-600">{{ $point['depth'] ?? 0 }}</p>
- <p class="text-sm text-gray-600 dark:text-gray-300">图谱层数</p>
- </div>
- <div class="text-center">
- <p class="text-2xl font-bold text-green-600">{{ count($point['prerequisite_kps'] ?? []) }}</p>
- <p class="text-sm text-gray-600 dark:text-gray-300">前置必修</p>
- </div>
- <div class="text-center">
- <p class="text-2xl font-bold text-yellow-600">{{ count($point['post_kps'] ?? []) }}</p>
- <p class="text-sm text-gray-600 dark:text-gray-300">可进阶</p>
- </div>
- <div class="text-center">
- <p class="text-2xl font-bold text-purple-600">{{ count($point['related_kps'] ?? []) }}</p>
- <p class="text-sm text-gray-600 dark:text-gray-300">平行关联</p>
- </div>
- </div>
- </div>
- @endif
- <!-- 知识图谱 -->
- <div class="rounded-xl border border-gray-200 bg-white px-6 py-6 shadow-sm dark:border-gray-800 dark:bg-gray-900/50">
- <div class="flex items-center justify-between mb-4">
- <div>
- <h3 class="text-xl font-semibold">知识关系图谱</h3>
- <p class="text-xs text-gray-500 mt-1">💡 点击节点查看技能详情,支持拖拽和缩放</p>
- </div>
- <div class="flex gap-4 text-xs">
- <div class="flex items-center gap-1">
- <span class="w-4 h-4 rounded-full bg-green-500 border border-green-600"></span>
- <span class="font-medium text-green-700">前置必修</span>
- </div>
- <div class="flex items-center gap-1">
- <span class="w-4 h-4 rounded-full bg-blue-500 border border-blue-600"></span>
- <span class="font-medium text-blue-700">当前知识点</span>
- </div>
- <div class="flex items-center gap-1">
- <span class="w-4 h-4 rounded-full bg-yellow-500 border border-yellow-600"></span>
- <span class="font-medium text-yellow-700">可进阶</span>
- </div>
- <div class="flex items-center gap-1">
- <span class="w-4 h-4 rounded-full bg-purple-500 border border-purple-600"></span>
- <span class="font-medium text-purple-700">平行关联</span>
- </div>
- </div>
- </div>
- <div id="knowledge-graph" class="w-full bg-gray-50 rounded-lg" style="height: 500px;"></div>
- </div>
- @php
- $parentNodes = $point['parent_nodes'] ?? [];
- $childNodes = $point['child_nodes'] ?? [];
- @endphp
- @if(!empty($parentNodes) || !empty($childNodes))
- <!-- 上下游知识点列表 -->
- <div class="grid gap-6 lg:grid-cols-2">
- <div class="rounded-2xl border border-green-200 bg-green-50/70 px-5 py-5 shadow-sm dark:border-green-700 dark:bg-green-900/20">
- <div class="flex items-center justify-between mb-4">
- <div>
- <h3 class="text-base font-semibold text-green-900 dark:text-green-100">前置必修</h3>
- <p class="text-xs text-green-700/80 dark:text-green-200/70 mt-1">这些知识点是进入当前节点的基础</p>
- </div>
- <span class="text-xs font-semibold bg-white text-green-700 px-2 py-1 rounded-full shadow-sm">
- {{ count($childNodes) }} 个
- </span>
- </div>
- <div class="space-y-3">
- @forelse($childNodes as $parent)
- <div class="rounded-xl border border-green-100 bg-white px-4 py-3 shadow-sm dark:border-green-700/60 dark:bg-gray-900/60">
- <div class="flex items-start justify-between gap-3">
- <div>
- <p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
- {{ $parent['cn_name'] ?? $parent['kp_code'] ?? '未命名父节点' }}
- </p>
- <p class="text-xs text-gray-500 mt-0.5">{{ $parent['kp_code'] ?? '未知编号' }}</p>
- <div class="flex flex-wrap gap-2 mt-2 text-[11px] text-gray-600 dark:text-gray-300">
- @if(!empty($parent['phase']))
- <span class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-0.5 text-green-700 dark:bg-green-900/30 dark:text-green-200">{{ $parent['phase'] }}</span>
- @endif
- @if(!empty($parent['grade']))
- <span class="inline-flex items-center gap-1 rounded-full bg-emerald-50 px-2 py-0.5 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-200">{{ $parent['grade'] }}年级</span>
- @endif
- @if(!empty($parent['category']))
- <span class="inline-flex items-center gap-1 rounded-full bg-lime-50 px-2 py-0.5 text-lime-700 dark:bg-lime-900/30 dark:text-lime-200">{{ $parent['category'] }}</span>
- @endif
- </div>
- </div>
- @if(!empty($parent['kp_code']))
- <a
- href="/admin/knowledge-point-detail?kp_code={{ $parent['kp_code'] }}@if($phaseParam)&phase={{ urlencode($phaseParam) }}@endif"
- class="text-xs font-semibold text-primary-600 hover:text-primary-700 dark:text-primary-400"
- >
- 查看
- </a>
- @endif
- </div>
- @if(!empty($parent['description']))
- <p class="text-xs text-gray-500 mt-2">{{ Str::limit($parent['description'], 110) }}</p>
- @endif
- </div>
- @empty
- <p class="text-sm text-gray-600 dark:text-gray-300">暂无前置必修,可能是一级知识点。</p>
- @endforelse
- </div>
- </div>
- <div class="rounded-2xl border border-amber-200 bg-amber-50/80 px-5 py-5 shadow-sm dark:border-amber-700 dark:bg-amber-900/20">
- <div class="flex items-center justify-between mb-4">
- <div>
- <h3 class="text-base font-semibold text-amber-900 dark:text-amber-100">可进阶</h3>
- <p class="text-xs text-amber-700/80 dark:text-amber-200/70 mt-1">掌握当前节点后可继续学习的内容</p>
- </div>
- <span class="text-xs font-semibold bg-white text-amber-700 px-2 py-1 rounded-full shadow-sm">
- {{ count($parentNodes) }} 个
- </span>
- </div>
- <div class="space-y-3">
- @forelse($parentNodes as $child)
- <div class="rounded-xl border border-amber-100 bg-white px-4 py-3 shadow-sm dark:border-amber-700/60 dark:bg-gray-900/60">
- <div class="flex items-start justify-between gap-3">
- <div>
- <p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
- {{ $child['cn_name'] ?? $child['kp_code'] ?? '未命名子节点' }}
- </p>
- <p class="text-xs text-gray-500 mt-0.5">{{ $child['kp_code'] ?? '未知编号' }}</p>
- <div class="flex flex-wrap gap-2 mt-2 text-[11px] text-gray-600 dark:text-gray-300">
- @if(!empty($child['phase']))
- <span class="inline-flex items-center gap-1 rounded-full bg-amber-50 px-2 py-0.5 text-amber-700 dark:bg-amber-900/30 dark:text-amber-200">{{ $child['phase'] }}</span>
- @endif
- @if(!empty($child['grade']))
- <span class="inline-flex items-center gap-1 rounded-full bg-orange-50 px-2 py-0.5 text-orange-700 dark:bg-orange-900/30 dark:text-orange-200">{{ $child['grade'] }}年级</span>
- @endif
- @if(!empty($child['category']))
- <span class="inline-flex items-center gap-1 rounded-full bg-yellow-50 px-2 py-0.5 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-200">{{ $child['category'] }}</span>
- @endif
- </div>
- </div>
- @if(!empty($child['kp_code']))
- <a
- href="/admin/knowledge-point-detail?kp_code={{ $child['kp_code'] }}@if($phaseParam)&phase={{ urlencode($phaseParam) }}@endif"
- class="text-xs font-semibold text-primary-600 hover:text-primary-700 dark:text-primary-400"
- >
- 查看
- </a>
- @endif
- </div>
- @if(!empty($child['description']))
- <p class="text-xs text-gray-500 mt-2">{{ Str::limit($child['description'], 110) }}</p>
- @endif
- </div>
- @empty
- <p class="text-sm text-gray-600 dark:text-gray-300">暂无可进阶知识点。</p>
- @endforelse
- </div>
- </div>
- </div>
- @endif
- <!-- 关联知识点展示 -->
- <div class="grid gap-6 lg:grid-cols-3">
- @if(!empty($point['post_kps']))
- <div class="rounded-xl border border-yellow-200 bg-yellow-50/50 px-6 py-6 shadow-sm dark:border-yellow-800 dark:bg-yellow-900/20">
- <div class="flex items-center justify-between mb-4">
- <h3 class="text-lg font-semibold text-yellow-800 dark:text-yellow-200">可进阶</h3>
- <span class="bg-yellow-200 text-yellow-800 px-2 py-1 rounded-full text-xs font-medium">{{ count($point['post_kps']) }} 项</span>
- </div>
- <div class="space-y-3">
- @foreach($point['post_kps'] as $item)
- <div class="rounded-lg border border-yellow-200 bg-white p-4 dark:border-yellow-700 dark:bg-yellow-900/40">
- <div class="flex justify-between items-start mb-2">
- <div>
- <h4 class="font-semibold text-gray-900 dark:text-gray-100">{{ $item['cn_name'] ?? '' }}</h4>
- <p class="text-xs text-gray-500">{{ $item['kp_code'] ?? '' }}</p>
- </div>
- @if(isset($item['distance']))
- <span class="text-xs bg-yellow-100 text-yellow-700 px-2 py-1 rounded">距 {{ $item['distance'] }} 层</span>
- @endif
- </div>
- @if(!empty($item['skills']))
- <div class="text-xs text-gray-600 dark:text-gray-300">
- <span class="font-medium">技能:</span> {{ count($item['skills']) }} 项
- </div>
- @endif
- </div>
- @endforeach
- </div>
- </div>
- @endif
- @if(!empty($point['related_kps']))
- <div class="rounded-xl border border-purple-200 bg-purple-50/50 px-6 py-6 shadow-sm dark:border-purple-800 dark:bg-purple-900/20">
- <div class="flex items-center justify-between mb-4">
- <h3 class="text-lg font-semibold text-purple-800 dark:text-purple-200">平行关联</h3>
- <span class="bg-purple-200 text-purple-800 px-2 py-1 rounded-full text-xs font-medium">{{ count($point['related_kps']) }} 项</span>
- </div>
- <div class="space-y-3">
- @foreach($point['related_kps'] as $item)
- <div class="rounded-lg border border-purple-200 bg-white p-4 dark:border-purple-700 dark:bg-purple-900/40">
- <div class="flex justify-between items-start mb-2">
- <div>
- <h4 class="font-semibold text-gray-900 dark:text-gray-100">{{ $item['cn_name'] ?? '' }}</h4>
- <p class="text-xs text-gray-500">{{ $item['kp_code'] ?? '' }}</p>
- </div>
- @if(isset($item['edge']['relation_type']))
- <span class="text-xs bg-purple-100 text-purple-700 px-2 py-1 rounded">{{ $item['edge']['relation_type'] }}</span>
- @endif
- </div>
- @if(!empty($item['skills']))
- <div class="text-xs text-gray-600 dark:text-gray-300">
- <span class="font-medium">技能:</span> {{ count($item['skills']) }} 项
- </div>
- @endif
- </div>
- @endforeach
- </div>
- </div>
- @endif
- @if(!empty($point['prerequisite_kps']))
- <div class="rounded-xl border border-green-200 bg-green-50/50 px-6 py-6 shadow-sm dark:border-green-800 dark:bg-green-900/20">
- <div class="flex items-center justify-between mb-4">
- <h3 class="text-lg font-semibold text-green-800 dark:text-green-200">前置必修</h3>
- <span class="bg-green-200 text-green-800 px-2 py-1 rounded-full text-xs font-medium">{{ count($point['prerequisite_kps']) }} 项</span>
- </div>
- <div class="space-y-3">
- @foreach($point['prerequisite_kps'] as $item)
- <div class="rounded-lg border border-green-200 bg-white p-4 dark:border-green-700 dark:bg-green-900/40">
- <div class="flex justify-between items-start mb-2">
- <div>
- <h4 class="font-semibold text-gray-900 dark:text-gray-100">{{ $item['cn_name'] ?? '' }}</h4>
- <p class="text-xs text-gray-500">{{ $item['kp_code'] ?? '' }}</p>
- </div>
- @if(isset($item['distance']))
- <span class="text-xs bg-green-100 text-green-700 px-2 py-1 rounded">距 {{ $item['distance'] }} 层</span>
- @endif
- </div>
- @if(!empty($item['skills']))
- <div class="text-xs text-gray-600 dark:text-gray-300">
- <span class="font-medium">技能:</span> {{ count($item['skills']) }} 项
- </div>
- @endif
- </div>
- @endforeach
- </div>
- </div>
- @endif
- </div>
- <!-- 知识点技能关系网 -->
- <div class="rounded-xl border border-gray-200 bg-white px-6 py-6 shadow-sm dark:border-gray-800 dark:bg-gray-900/50">
- <div class="flex items-center justify-between mb-6">
- <div>
- <h3 class="text-xl font-semibold">知识点技能关系网</h3>
- <p class="text-xs text-gray-500 mt-1">💡 使用紧凑树状布局展示知识点与技能的关联强弱,权重越高越靠近中心。</p>
- </div>
- <div class="flex gap-4 text-xs text-gray-600">
- <div class="flex items-center gap-1">
- <span class="w-3 h-3 rounded-full bg-blue-500"></span>
- <span>知识点</span>
- </div>
- <div class="flex items-center gap-1">
- <span class="w-3 h-3 rounded-full bg-emerald-500"></span>
- <span>技能节点</span>
- </div>
- </div>
- </div>
- <div id="skill-graph" class="w-full bg-gray-50 rounded-lg" style="height: 360px;"></div>
- </div>
- <!-- 技能展示(底部呈现) -->
- <div class="rounded-xl border border-gray-200 bg-white px-6 py-6 shadow-sm dark:border-gray-800 dark:bg-gray-900/50">
- <div class="flex items-center justify-between mb-4">
- <h3 class="text-xl font-semibold">当前知识点技能 ({{ count($point['skills']) }} 项)</h3>
- <p class="text-xs text-gray-500">列表按原始顺序展示,可与上方关系网互相对照。</p>
- </div>
- @if(!empty($point['skills']))
- <div class="w-full overflow-hidden">
- <div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
- @foreach($point['skills'] as $skill)
- <div class="rounded-lg border border-gray-200 p-4 dark:border-gray-700 break-words max-w-full">
- <div class="flex items-start justify-between mb-2 gap-2 min-w-0">
- <h4 class="font-semibold text-sm leading-tight truncate">{{ $skill['skill_name'] ?? '未知技能' }}</h4>
- <span class="text-xs bg-blue-100 text-blue-800 px-2 py-0.5 rounded flex-shrink-0 whitespace-nowrap">
- {{ $skill['skill_type'] ?? ($skill['type'] ?? '通用') }}
- </span>
- </div>
- <div class="mt-2">
- @php
- // 计算权重百分比,如果权重 > 1 说明它已经是百分比格式(如 5.0 表示 5%)
- $weightValue = $skill['weight'] ?? 0;
- $weightPercentage = $weightValue > 1 ? $weightValue : ($weightValue * 100);
- $weightPercentage = min(100, max(0, $weightPercentage)); // 限制在 0-100 之间
- @endphp
- <div class="flex items-center justify-between text-xs text-gray-600 mb-1">
- <span>权重</span>
- <span>{{ number_format($weightPercentage, 1) }}%</span>
- </div>
- <div class="w-full bg-gray-200 rounded-full h-2 dark:bg-gray-700">
- <div class="bg-primary-500 h-2 rounded-full transition-all duration-300" style="width: {{ $weightPercentage }}%"></div>
- </div>
- </div>
- @if(!empty($skill['description']))
- <p class="text-xs text-gray-500 mt-2">{{ Str::limit($skill['description'] ?? '', 100) }}</p>
- @endif
- </div>
- @endforeach
- </div>
- @else
- <p class="text-gray-500 text-sm">该知识点暂无技能配置</p>
- @endif
- </div>
- </div>
- @else
- <div class="rounded-xl border border-dashed border-gray-300 px-6 py-12 text-center">
- <p class="text-gray-500">未找到指定的知识点</p>
- </div>
- @endif
- <!-- 脚本 -->
- @push('scripts')
- <script>
- document.addEventListener('DOMContentLoaded', function() {
- console.log('页面加载完成,开始初始化G6');
- const G6 = window.G6;
- const SnaplinePlugin = window.G6Snapline ?? G6?.SnapLine;
- if (!G6) {
- console.error('G6 资源未加载,请确认 Vite 构建是否包含 @antv/g6');
- return;
- }
- const graphContainer = document.getElementById('knowledge-graph');
- const skillGraphContainer = document.getElementById('skill-graph');
- const skillList = @json($point['skills'] ?? []);
- const currentPointMeta = @json([
- 'kp_code' => $point['kp_code'] ?? null,
- 'cn_name' => $point['cn_name'] ?? ''
- ]);
- if (!graphContainer) {
- console.error('找不到图谱容器');
- return;
- }
- // 获取数据
- const data = @json($graphData);
- console.log('完整图数据:', data);
- console.log('原始节点数据:');
- data.nodes.forEach((node, index) => {
- console.log(`节点${index}:`, {
- id: node.id,
- label: node.label,
- type: node.type,
- distance: node.distance
- });
- });
- if (!data.nodes || data.nodes.length === 0) {
- graphContainer.innerHTML = '<div class="flex items-center justify-center h-full text-gray-500">暂无图谱数据</div>';
- return;
- }
- console.log('开始创建G6图形');
- try {
- // 找到当前节点
- const currentNode = data.nodes.find(n => n.type === 'current') || data.nodes[0];
- console.log('当前节点:', currentNode);
- const nodeMetaMap = new Map();
- const registerNodeMeta = (id, meta) => {
- nodeMetaMap.set(id, meta);
- return meta;
- };
- // 按照G6官方文档构建mindMap数据结构
- const buildMindmapData = (nodes, edges, currentNode) => {
- // 分组节点
- const leftNodes = []; // 前置知识点 - 左侧
- const rightNodes = []; // 后置和关联知识点 - 右侧
- nodes.forEach(node => {
- if (node.type === 'prerequisite') {
- leftNodes.push(node);
- } else if (node.type === 'post' || node.type === 'related') {
- rightNodes.push(node);
- }
- });
- console.log('节点分组:', {
- left: leftNodes.length,
- right: rightNodes.length
- });
- // 构建标准树形结构,明确设置side属性
- const mindmapData = {
- id: currentNode.id,
- label: currentNode.label,
- type: 'rect',
- data: registerNodeMeta(currentNode.id, {
- role: currentNode.type,
- side: null,
- distance: currentNode.distance ?? 0
- }),
- children: []
- };
- const createChildPayload = (node, side) => ({
- id: node.id,
- label: node.label,
- type: 'rect',
- data: registerNodeMeta(node.id, {
- role: node.type,
- side,
- distance: node.distance ?? 1
- }),
- children: []
- });
- // 添加左侧子节点,明确设置side: 'left'
- leftNodes.forEach(node => {
- mindmapData.children.push(createChildPayload(node, 'left'));
- });
- // 添加右侧子节点,明确设置side: 'right'
- rightNodes.forEach(node => {
- mindmapData.children.push(createChildPayload(node, 'right'));
- });
- return mindmapData;
- };
- const mindmapData = buildMindmapData(data.nodes, data.edges, currentNode);
- console.log('MindMap数据结构:', mindmapData);
- // 创建MindMap图形
- const typeColors = {
- current: { fill: '#2563EB', stroke: '#1D4ED8' },
- prerequisite: { fill: '#16A34A', stroke: '#15803D' },
- post: { fill: '#F97316', stroke: '#C2410C' },
- related: { fill: '#A855F7', stroke: '#9333EA' }
- };
- const plugins = [];
- if (SnaplinePlugin) {
- plugins.push(new SnaplinePlugin({
- line: {
- stroke: '#CBD5F5',
- lineWidth: 1,
- lineDash: [4, 4]
- }
- }));
- } else {
- console.warn('Snapline plugin not available in current G6 bundle, skipping.');
- }
- const graph = new G6.TreeGraph({
- container: 'knowledge-graph',
- width: graphContainer.clientWidth,
- height: 500,
- modes: {
- default: ['drag-canvas', 'zoom-canvas', 'drag-node']
- },
- plugins,
- defaultNode: {
- type: 'rect',
- size: [120, 50],
- style: {
- fill: '#5B8FF9',
- stroke: '#5B8FF9',
- lineWidth: 2,
- radius: 8 // 圆角矩形
- },
- labelCfg: {
- position: 'center',
- offset: [0, 0],
- style: {
- fill: '#fff',
- fontSize: 13,
- fontWeight: 'bold',
- textAlign: 'center',
- textBaseline: 'middle'
- }
- }
- },
- defaultEdge: {
- type: 'cubic-horizontal',
- style: {
- stroke: '#A3B1BF',
- lineWidth: 2
- }
- },
- layout: {
- type: 'mindmap',
- direction: 'H',
- getSide: function(node) {
- const meta = nodeMetaMap.get(node.id) || node.data || {};
- const type = meta.role;
- const side = meta.side;
- console.log('getSide node:', node.id, 'type:', type, 'side:', side);
- // 优先使用 buildMindmapData 写入的 side,其次回退到 type 判断
- if (side === 'left' || type === 'prerequisite') {
- console.log('前置知识点 -> left');
- return 'left';
- }
- if (side === 'right' || type === 'post' || type === 'related') {
- console.log('后置/关联知识点 -> right');
- return 'right';
- }
- console.log('默认 -> right');
- return 'right';
- },
- getHeight: () => 50,
- getWidth: () => 120,
- getVGap: node => {
- const meta = nodeMetaMap.get(node.id) || node.data || {};
- const distance = meta.distance ?? 1;
- return 20 + (distance - 1) * 10;
- },
- getHGap: node => {
- const meta = nodeMetaMap.get(node.id) || node.data || {};
- const distance = meta.distance ?? 1;
- return 100 + (distance - 1) * 40;
- },
- radial: false
- }
- });
- // 自定义节点样式
- graph.node(model => {
- const meta = nodeMetaMap.get(model.id) || model.data || {};
- const nodeType = meta.role || 'related';
- const palette = typeColors[nodeType] || { fill: '#5B8FF9', stroke: '#1D4ED8' };
- const isCenter = model.id === currentNode.id;
- return {
- type: 'rect',
- size: isCenter ? [150, 60] : [130, 54],
- style: {
- fill: palette.fill,
- stroke: palette.stroke,
- lineWidth: isCenter ? 3 : 2,
- radius: 10
- },
- labelCfg: {
- style: {
- fill: '#fff',
- fontSize: isCenter ? 15 : 13,
- fontWeight: 'bold'
- }
- }
- };
- });
- graph.edge(model => {
- const targetMeta = nodeMetaMap.get(model.target) || {};
- const palette = typeColors[targetMeta.role] || { stroke: '#94A3B8' };
- const isPrerequisite = targetMeta.role === 'prerequisite' || targetMeta.side === 'left';
- return {
- type: 'cubic-horizontal',
- style: {
- stroke: palette.stroke,
- lineWidth: 2,
- startArrow: isPrerequisite
- ? {
- // 左侧节点需要箭头朝向中心(起点),使用 startArrow 让箭头落在子节点一侧指向中心
- path: 'M 0,0 L 8,4 L 8,-4 Z',
- fill: palette.stroke
- }
- : false,
- endArrow: !isPrerequisite
- ? {
- path: 'M 0,0 L 8,4 L 8,-4 Z',
- fill: palette.stroke
- }
- : false
- }
- };
- });
- // 数据处理和样式映射
- graph.data(mindmapData);
- graph.render();
- graph.getNodes().forEach((nodeItem) => {
- const model = nodeItem.getModel();
- const meta = nodeMetaMap.get(model.id) || model.data || {};
- const nodeType = meta.role || model.type;
- const palette = typeColors[nodeType] || { fill: '#5B8FF9', stroke: '#1D4ED8' };
- const isCenter = model.id === currentNode.id;
- graph.updateItem(nodeItem, {
- type: 'rect',
- size: isCenter ? [150, 60] : [130, 54],
- style: {
- fill: palette.fill,
- stroke: palette.stroke,
- radius: 10,
- lineWidth: isCenter ? 3 : 2,
- },
- labelCfg: {
- style: {
- fill: '#fff',
- fontSize: isCenter ? 15 : 13,
- fontWeight: 'bold',
- },
- },
- });
- });
- graph.refresh();
- // 自适应视图
- setTimeout(() => {
- graph.fitView(20);
- console.log('G6图形渲染完成');
- }, 200);
- // ------------- 知识点技能关系网(紧凑树) -----------------
- if (skillGraphContainer) {
- if (!skillList || skillList.length === 0) {
- skillGraphContainer.innerHTML = '<div class="flex items-center justify-center h-full text-gray-500">暂无技能数据</div>';
- } else {
- const skillWeights = new Map();
- const skillTreeData = {
- id: currentPointMeta.kp_code || currentNode.id,
- label: currentPointMeta.cn_name || currentNode.label,
- type: 'knowledge',
- data: {
- role: 'knowledge',
- weight: 1,
- },
- children: skillList.map((skill, index) => {
- const weight = typeof skill.weight === 'number' ? skill.weight : 0.3;
- const nodeId = skill.skill_code ? `skill-${skill.skill_code}` : `skill-${index}`;
- skillWeights.set(nodeId, weight);
- return {
- id: nodeId,
- label: skill.skill_name || `技能 ${index + 1}`,
- type: 'skill',
- data: {
- role: 'skill',
- weight: weight,
- skillType: skill.skill_type || '技能',
- description: skill.description || '',
- },
- children: [],
- };
- }),
- };
- skillWeights.set(skillTreeData.id, 1);
- const compactGraph = new G6.TreeGraph({
- container: 'skill-graph',
- width: skillGraphContainer.clientWidth,
- height: 360,
- modes: { default: ['drag-canvas', 'zoom-canvas'] },
- defaultNode: {
- type: 'rect',
- size: [120, 48],
- style: {
- radius: 8,
- lineWidth: 2,
- },
- labelCfg: {
- position: 'center',
- style: {
- fill: '#fff',
- fontWeight: '600',
- },
- },
- },
- defaultEdge: {
- type: 'cubic-horizontal',
- style: {
- stroke: '#94A3B8',
- lineWidth: 1.5,
- endArrow: {
- path: 'M 0,0 L 8,4 L 8,-4 Z',
- fill: '#94A3B8',
- },
- },
- },
- layout: {
- type: 'compactBox',
- direction: 'TB',
- getId: (node) => node.id,
- getHeight: () => 48,
- getWidth: (node) => {
- const weight = Math.max(skillWeights.get(node.id) ?? node.data?.weight ?? 0.2, 0.05);
- return node.data?.role === 'knowledge' ? 200 : 120 + weight * 60;
- },
- getVGap: (node) => {
- if (node.data?.role === 'knowledge') {
- return 60;
- }
- const weight = Math.max(skillWeights.get(node.id) ?? node.data?.weight ?? 0.2, 0.05);
- const spread = 1 - Math.min(weight, 0.95);
- return 40 + spread * 80;
- },
- getHGap: () => 90,
- },
- });
- compactGraph.node((node) => {
- const role = node.data?.role;
- const weight = Math.max(skillWeights.get(node.id) ?? node.data?.weight ?? 0.2, 0.05);
- const isKnowledge = role === 'knowledge';
- const palette = isKnowledge
- ? { fill: '#2563EB', stroke: '#1D4ED8' }
- : weight >= 0.6
- ? { fill: '#059669', stroke: '#047857' }
- : weight >= 0.3
- ? { fill: '#10B981', stroke: '#059669' }
- : { fill: '#34D399', stroke: '#059669' };
- const width = isKnowledge ? 200 : 120 + weight * 60;
- return {
- type: 'rect',
- size: [width, 48],
- style: {
- fill: palette.fill,
- stroke: palette.stroke,
- radius: 10,
- lineWidth: isKnowledge ? 3 : 2,
- },
- labelCfg: {
- style: {
- fill: '#fff',
- fontSize: isKnowledge ? 16 : 13,
- fontWeight: 'bold',
- },
- },
- };
- });
- compactGraph.edge((edge) => {
- const weight = Math.max(skillWeights.get(edge.target) ?? 0.2, 0.05);
- return {
- type: 'cubic-horizontal',
- style: {
- stroke: '#0EA5E9',
- lineWidth: 1 + weight * 4,
- endArrow: {
- path: 'M 0,0 L 8,4 L 8,-4 Z',
- fill: '#0EA5E9',
- },
- },
- };
- });
- compactGraph.data(skillTreeData);
- compactGraph.render();
- compactGraph.getNodes().forEach((nodeItem) => {
- const model = nodeItem.getModel();
- const weight = Math.max(skillWeights.get(model.id) ?? model.data?.weight ?? 0.2, 0.05);
- const isKnowledge = model.data?.role === 'knowledge';
- const palette = isKnowledge
- ? { fill: '#2563EB', stroke: '#1D4ED8' }
- : weight >= 0.6
- ? { fill: '#059669', stroke: '#047857' }
- : weight >= 0.3
- ? { fill: '#10B981', stroke: '#059669' }
- : { fill: '#34D399', stroke: '#059669' };
- compactGraph.updateItem(nodeItem, {
- type: 'rect',
- size: isKnowledge ? [200, 52] : [120 + weight * 60, 48],
- style: {
- fill: palette.fill,
- stroke: palette.stroke,
- radius: 10,
- lineWidth: isKnowledge ? 3 : 2,
- },
- labelCfg: {
- style: {
- fill: '#fff',
- fontSize: isKnowledge ? 16 : 13,
- fontWeight: 'bold',
- },
- },
- });
- });
- setTimeout(() => compactGraph.fitView(40), 200);
- }
- }
- } catch (error) {
- console.error('G6创建失败:', error);
- graphContainer.innerHTML = '<div class="flex items-center justify-center h-full text-red-500">G6图形加载失败: ' + error.message + '</div>';
- }
- });
- </script>
- @endpush
- </x-filament-panels::page>
|