| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320 |
- <x-filament-panels::page>
- @push('styles')
- <style>
- .graph-container {
- width: 100%;
- height: 700px;
- border: 1px solid #e5e7eb;
- border-radius: 8px;
- background: #f9fafb;
- }
- .legend-item {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 4px 12px;
- border-radius: 4px;
- }
- .color-box {
- width: 20px;
- height: 20px;
- border-radius: 4px;
- border: 1px solid #d1d5db;
- }
- </style>
- @endpush
- <div class="space-y-6">
- <!-- 页面标题和操作 -->
- <div class="flex justify-between items-center">
- <div>
- <h2 class="text-2xl font-bold text-gray-900">知识图谱可视化</h2>
- <p class="mt-1 text-sm text-gray-500">
- 基于学生掌握度的知识节点热力图,点击节点查看详细信息
- </p>
- </div>
- <div class="flex gap-3">
- button
- color="gray"
- wire:click="$refresh"
- >
- <x-heroicon-m-arrow-path class="w-5 h-5 mr-2" />
- 刷新图谱
- /button>
- </div>
- </div>
- <!-- 学生选择器 -->
- <x-filament::card>
- <div class="flex items-center gap-4">
- <div class="flex-1">
- <select
- wire:model.live="selectedStudentId"
- placeholder="选择学生(查看个人掌握度热力图)"
- >
- <option value="">-- 显示全部知识点(灰色) --</option>
- @foreach($this->getStudents() as $student)
- <option value="{{ $student->student_id }}">
- {{ $student->name ?? $student->student_id }}
- </option>
- @endforeach
- /select>
- </div>
- @if($selectedStudentId)
- <div class="text-sm text-gray-600">
- 当前查看:<span class="font-semibold">{{ $selectedStudentId }}</span> 的掌握度
- </div>
- @endif
- </div>
- </x-filament::card>
- <!-- 图例 -->
- <x-filament::card>
- <h3 class="text-sm font-medium text-gray-900 mb-3">掌握度图例</h3>
- <div class="flex flex-wrap gap-3">
- <div class="legend-item">
- <div class="color-box" style="background: #d1d5db;"></div>
- <span class="text-sm text-gray-700">未学习</span>
- </div>
- <div class="legend-item">
- <div class="color-box" style="background: #ef4444;"></div>
- <span class="text-sm text-gray-700">< 60% 需提升</span>
- </div>
- <div class="legend-item">
- <div class="color-box" style="background: #fb923c;"></div>
- <span class="text-sm text-gray-700">60-69% 及格</span>
- </div>
- <div class="legend-item">
- <div class="color-box" style="background: #fbbf24;"></div>
- <span class="text-sm text-gray-700">70-79% 中等</span>
- </div>
- <div class="legend-item">
- <div class="color-box" style="background: #34d399;"></div>
- <span class="text-sm text-gray-700">80-89% 良好</span>
- </div>
- <div class="legend-item">
- <div class="color-box" style="background: #10b981;"></div>
- <span class="text-sm text-gray-700">≥ 90% 优秀</span>
- </div>
- </div>
- </x-filament::card>
- <!-- 图谱容器 -->
- <x-filament::card>
- <div id="mountNode" class="graph-container"></div>
- </x-filament::card>
- <!-- 操作提示 -->
- <div class="text-sm text-gray-600 bg-blue-50 p-4 rounded-lg">
- <div class="font-medium text-blue-900 mb-2">💡 操作提示</div>
- <ul class="space-y-1 text-blue-800">
- <li>• 鼠标拖拽画布移动视图,滚轮缩放</li>
- <li>• 拖拽节点调整位置</li>
- <li>• 鼠标悬停节点查看掌握度详情</li>
- <li>• 点击节点查看练习建议</li>
- </ul>
- </div>
- </div>
- @push('scripts')
- <script src="https://gw.alipayobjects.com/os/lib/antv/g6/4.8.21/dist/g6.min.js"></script>
- <script>
- document.addEventListener('livewire:initialized', () => {
- const graphData = @js($graphData);
- const studentMasteryData = @js($studentMasteryData);
- const selectedStudentId = @js($selectedStudentId);
- // 构建掌握度映射
- const masteryMap = {};
- if (selectedStudentId && studentMasteryData) {
- studentMasteryData.forEach(item => {
- masteryMap[item.kp_code] = item.mastery;
- });
- }
- // 转换数据格式
- const nodes = graphData.nodes.map(node => {
- const kpCode = node.id;
- const mastery = masteryMap[kpCode];
- // 根据掌握度获取颜色
- let fillColor = '#d1d5db'; // 默认灰色(未学习)
- let strokeColor = '#9ca3af';
- if (mastery !== undefined) {
- if (mastery >= 0.9) {
- fillColor = '#10b981';
- strokeColor = '#059669';
- } else if (mastery >= 0.8) {
- fillColor = '#34d399';
- strokeColor = '#10b981';
- } else if (mastery >= 0.7) {
- fillColor = '#fbbf24';
- strokeColor = '#f59e0b';
- } else if (mastery >= 0.6) {
- fillColor = '#fb923c';
- strokeColor = '#f97316';
- } else {
- fillColor = '#ef4444';
- strokeColor = '#dc2626';
- }
- }
- return {
- ...node,
- style: {
- fill: fillColor,
- stroke: strokeColor,
- lineWidth: 2,
- radius: 6,
- shadowColor: 'rgba(0, 0, 0, 0.1)',
- shadowBlur: 10,
- shadowOffsetX: 2,
- shadowOffsetY: 2,
- },
- labelCfg: {
- style: {
- fill: '#000',
- fontSize: 12,
- fontWeight: mastery >= 0.8 ? 'bold' : 'normal',
- },
- },
- // 自定义数据用于tooltip
- mastery: mastery,
- masteryLevel: getMasteryLevel(mastery),
- };
- });
- const edges = graphData.edges.map(edge => ({
- ...edge,
- style: {
- stroke: '#94a3b8',
- lineWidth: 1.5,
- opacity: 0.6,
- endArrow: {
- path: G6.Arrow.triangle(8, 10, 4),
- d: 4,
- fill: '#94a3b8',
- },
- },
- }));
- const container = document.getElementById('mountNode');
- const width = container.scrollWidth;
- const height = container.scrollHeight;
- // 创建图表
- const graph = new G6.Graph({
- container: 'mountNode',
- width,
- height,
- fitView: true,
- fitViewPadding: [20, 20, 20, 20],
- modes: {
- default: ['drag-canvas', 'zoom-canvas', 'drag-node'],
- },
- layout: {
- type: 'dagre',
- rankdir: 'LR',
- align: 'UL',
- controlPoints: true,
- nodesep: 20,
- ranksep: 60,
- },
- defaultNode: {
- type: 'rect',
- size: [160, 50],
- labelCfg: {
- position: 'center',
- style: {
- fontSize: 12,
- },
- },
- },
- defaultEdge: {
- type: 'polyline',
- style: {
- radius: 10,
- offset: 30,
- lineAppendWidth: 8,
- },
- },
- // 节点交互状态
- nodeStateStyles: {
- hover: {
- lineWidth: 3,
- shadowColor: 'rgba(0, 0, 0, 0.3)',
- shadowBlur: 20,
- },
- selected: {
- lineWidth: 4,
- stroke: '#3b82f6',
- },
- },
- });
- // 渲染图表
- graph.data({
- nodes: nodes,
- edges: edges,
- });
- graph.render();
- // 绑定事件
- graph.on('node:mouseenter', (evt) => {
- graph.setItemState(evt.item, 'hover', true);
- });
- graph.on('node:mouseleave', (evt) => {
- graph.setItemState(evt.item, 'hover', false);
- });
- graph.on('node:click', (evt) => {
- const node = evt.item;
- const model = node.getModel();
- const kpCode = model.id;
- const kpName = model.label || kpCode;
- const mastery = model.mastery;
- // 显示详细信息
- let message = `知识点:${kpName}\n`;
- message += `代码:${kpCode}\n`;
- if (mastery !== undefined) {
- const percentage = (mastery * 100).toFixed(1);
- message += `掌握度:${percentage}%\n`;
- message += `等级:${model.masteryLevel}\n`;
- if (mastery < 0.7) {
- message += `\n建议:需要加强练习,建议进行针对性训练`;
- } else if (mastery >= 0.9) {
- message += `\n表现优秀!可以挑战更高难度题目`;
- }
- } else {
- message += `\n状态:未学习\n建议:开始学习该知识点`;
- }
- alert(message);
- });
- // 窗口大小变化时重新适应
- window.addEventListener('resize', () => {
- if (!graph || graph.get('destroyed')) return;
- const width = container.scrollWidth;
- const height = container.scrollHeight;
- graph.changeSize(width, height);
- });
- // 获取掌握度等级
- function getMasteryLevel(mastery) {
- if (mastery === undefined) return '未学习';
- if (mastery >= 0.9) return '优秀';
- if (mastery >= 0.8) return '良好';
- if (mastery >= 0.7) return '中等';
- if (mastery >= 0.6) return '及格';
- return '需提升';
- }
- });
- </script>
- @endpush
- </x-filament-panels::page>
|