skill-proficiency-radar.blade.php 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. <div>
  2. {{-- 加载状态 --}}
  3. @if ($isLoading)
  4. <div class="flex items-center justify-center h-96">
  5. <svg class="animate-spin h-8 w-8 text-indigo-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
  6. <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
  7. <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
  8. </svg>
  9. <span class="ml-3 text-gray-600">正在加载雷达图...</span>
  10. </div>
  11. @elseif ($errorMessage)
  12. <div class="rounded-md bg-red-50 p-4">
  13. <div class="flex">
  14. <div class="flex-shrink-0">
  15. <svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
  16. <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
  17. </svg>
  18. </div>
  19. <div class="ml-3">
  20. <h3 class="text-sm font-medium text-red-800">加载失败</h3>
  21. <div class="mt-2 text-sm text-red-700">
  22. <p>{{ $errorMessage }}</p>
  23. </div>
  24. </div>
  25. </div>
  26. </div>
  27. @elseif (empty($radarData['data']))
  28. <div class="text-center py-12">
  29. <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  30. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
  31. </svg>
  32. <p class="mt-2 text-sm text-gray-500">暂无技能数据</p>
  33. </div>
  34. @else
  35. <div class="space-y-6">
  36. {{-- 雷达图 --}}
  37. <div class="relative">
  38. <canvas id="skillRadarChart" class="w-full" style="max-height: 400px;"></canvas>
  39. </div>
  40. {{-- 技能详细列表 --}}
  41. <div class="bg-gray-50 rounded-lg p-4">
  42. <h4 class="text-sm font-medium text-gray-900 mb-3">技能详情</h4>
  43. <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
  44. @foreach ($radarData['data'] as $skill)
  45. <div class="bg-white p-4 rounded-lg shadow-sm border border-gray-200">
  46. <div class="flex items-center justify-between mb-2">
  47. <div class="flex items-center">
  48. <div class="w-3 h-3 rounded-full mr-2" style="background-color: {{ $this->getSkillLevelColor($skill['skill_level']) }}"></div>
  49. <span class="text-sm font-medium text-gray-900">{{ $skill['skill_name'] }}</span>
  50. </div>
  51. <span class="text-xs text-gray-500">{{ $this->getSkillLevelName($skill['skill_level']) }}</span>
  52. </div>
  53. <div class="mt-2">
  54. <div class="flex justify-between text-xs text-gray-600 mb-1">
  55. <span>熟练度</span>
  56. <span class="font-medium">{{ number_format($skill['proficiency_level'] * 100, 1) }}%</span>
  57. </div>
  58. <div class="w-full bg-gray-200 rounded-full h-1.5">
  59. <div class="h-1.5 rounded-full" style="width: {{ $skill['proficiency_level'] * 100 }}%; background-color: {{ $this->getSkillLevelColor($skill['skill_level']) }}"></div>
  60. </div>
  61. </div>
  62. <div class="mt-3 grid grid-cols-3 gap-2 text-xs">
  63. <div>
  64. <div class="text-gray-500">简单</div>
  65. <div class="font-medium text-gray-900">{{ number_format(($skill['simple_accuracy'] ?? 0) * 100, 0) }}%</div>
  66. </div>
  67. <div>
  68. <div class="text-gray-500">中等</div>
  69. <div class="font-medium text-gray-900">{{ number_format(($skill['intermediate_accuracy'] ?? 0) * 100, 0) }}%</div>
  70. </div>
  71. <div>
  72. <div class="text-gray-500">困难</div>
  73. <div class="font-medium text-gray-900">{{ number_format(($skill['advanced_accuracy'] ?? 0) * 100, 0) }}%</div>
  74. </div>
  75. </div>
  76. <div class="mt-2 text-xs text-gray-500">
  77. 已练习 {{ $skill['total_questions_attempted'] }} 题
  78. @if ($skill['practice_streak'] > 0)
  79. • 连续 {{ $skill['practice_streak'] }} 天
  80. @endif
  81. </div>
  82. </div>
  83. @endforeach
  84. </div>
  85. </div>
  86. </div>
  87. {{-- Chart.js 雷达图脚本 --}}
  88. <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
  89. <script>
  90. document.addEventListener('DOMContentLoaded', function() {
  91. const ctx = document.getElementById('skillRadarChart');
  92. if (!ctx) return;
  93. const skills = @json($radarData['data']);
  94. const data = {
  95. labels: skills.map(s => s.skill_name),
  96. datasets: [{
  97. label: '技能熟练度',
  98. data: skills.map(s => s.proficiency_level * 100),
  99. backgroundColor: 'rgba(59, 130, 246, 0.2)',
  100. borderColor: 'rgba(59, 130, 246, 1)',
  101. borderWidth: 2,
  102. pointBackgroundColor: skills.map(s => {
  103. const colors = {
  104. 'beginner': '#ef4444',
  105. 'elementary': '#f97316',
  106. 'intermediate': '#eab308',
  107. 'advanced': '#22c55e',
  108. 'proficient': '#3b82f6'
  109. };
  110. return colors[s.skill_level] || '#9ca3af';
  111. }),
  112. pointBorderColor: '#fff',
  113. pointHoverBackgroundColor: '#fff',
  114. pointHoverBorderColor: 'rgba(59, 130, 246, 1)',
  115. pointRadius: 5,
  116. pointHoverRadius: 7,
  117. }]
  118. };
  119. const config = {
  120. type: 'radar',
  121. data: data,
  122. options: {
  123. responsive: true,
  124. maintainAspectRatio: true,
  125. plugins: {
  126. legend: {
  127. display: true,
  128. position: 'top',
  129. },
  130. tooltip: {
  131. callbacks: {
  132. label: function(context) {
  133. const skill = skills[context.dataIndex];
  134. return [
  135. `熟练度: ${context.parsed.r.toFixed(1)}%`,
  136. `等级: ${skill.skill_level}`,
  137. `已练习: ${skill.total_questions_attempted} 题`
  138. ];
  139. }
  140. }
  141. }
  142. },
  143. scales: {
  144. r: {
  145. angleLines: {
  146. display: true,
  147. color: 'rgba(0, 0, 0, 0.1)'
  148. },
  149. suggestedMin: 0,
  150. suggestedMax: 100,
  151. ticks: {
  152. stepSize: 20,
  153. callback: function(value) {
  154. return value + '%';
  155. }
  156. },
  157. grid: {
  158. color: 'rgba(0, 0, 0, 0.1)'
  159. },
  160. pointLabels: {
  161. font: {
  162. size: 12
  163. }
  164. }
  165. }
  166. }
  167. }
  168. };
  169. new Chart(ctx, config);
  170. });
  171. </script>
  172. @endif
  173. </div>