Переглянути джерело

修改 api 路径的问题

yemeishu 3 тижнів тому
батько
коміт
a439098110
23 змінених файлів з 3687 додано та 15 видалено
  1. 33 0
      app/Filament/Pages/Integrations/KnowledgeGraphExplorer.php
  2. 91 0
      app/Filament/Pages/Integrations/KnowledgeGraphIntegration.php
  3. 33 11
      app/Filament/Pages/QuestionGeneration.php
  4. 35 0
      app/Filament/Pages/QuestionManagement.php
  5. 110 0
      app/Filament/Pages/Statistics/KnowledgePointStats.php
  6. 185 0
      app/Livewire/Integrations/KnowledgeGraphComponent.php
  7. 197 0
      app/Livewire/Integrations/KnowledgeGraphVisualization.php
  8. 256 0
      app/Livewire/Integrations/KnowledgeNetworkComponent.php
  9. 135 0
      app/Livewire/Integrations/KnowledgePointDetails.php
  10. 86 0
      app/Livewire/Integrations/KnowledgePointStatsComponent.php
  11. 126 0
      app/Livewire/Integrations/KnowledgePointsListComponent.php
  12. 8 0
      app/Providers/Filament/AdminPanelProvider.php
  13. 290 0
      resources/views/filament/pages/integrations/knowledge-graph-explorer.blade.php
  14. 192 0
      resources/views/filament/pages/integrations/knowledge-graph-integration.blade.php
  15. 320 0
      resources/views/filament/pages/knowledge-point-stats.blade.php
  16. 71 0
      resources/views/filament/pages/question-generation.blade.php
  17. 211 0
      resources/views/livewire/integrations/knowledge-graph-component.blade.php
  18. 409 0
      resources/views/livewire/integrations/knowledge-graph-visualization.blade.php
  19. 197 0
      resources/views/livewire/integrations/knowledge-network-component.blade.php
  20. 218 0
      resources/views/livewire/integrations/knowledge-point-details.blade.php
  21. 290 0
      resources/views/livewire/integrations/knowledge-point-stats-component.blade.php
  22. 177 0
      resources/views/livewire/integrations/knowledge-points-list-component.blade.php
  23. 17 4
      routes/api.php

+ 33 - 0
app/Filament/Pages/Integrations/KnowledgeGraphExplorer.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace App\Filament\Pages\Integrations;
+
+use BackedEnum;
+use Filament\Pages\Page;
+use UnitEnum;
+use Livewire\Attributes\Computed;
+use Illuminate\Http\Request;
+
+class KnowledgeGraphExplorer extends Page
+{
+    protected static ?string $title = '知识图谱浏览';
+    protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-globe-alt';
+    protected static ?string $navigationLabel = '知识图谱浏览';
+    protected static string|UnitEnum|null $navigationGroup = '整合视图';
+    protected static ?int $navigationSort = 10;
+    protected string $view = 'filament.pages.integrations.knowledge-graph-explorer';
+
+    public ?string $selectedKpCode = null;
+
+    public function mount(Request $request): void
+    {
+        $this->selectedKpCode = $request->query('kp_code');
+    }
+
+    public function getBreadcrumbs(): array
+    {
+        return [
+            'knowledge-graph-explorer' => '知识图谱浏览',
+        ];
+    }
+}

+ 91 - 0
app/Filament/Pages/Integrations/KnowledgeGraphIntegration.php

@@ -0,0 +1,91 @@
+<?php
+
+namespace App\Filament\Pages\Integrations;
+
+use BackedEnum;
+use Filament\Pages\Page;
+use UnitEnum;
+use Livewire\Attributes\Computed;
+use Illuminate\Http\Request;
+use App\Livewire\Integrations\KnowledgeGraphComponent;
+use App\Livewire\Integrations\KnowledgePointsListComponent;
+use App\Livewire\Integrations\KnowledgePointStatsComponent;
+use App\Livewire\Integrations\KnowledgeNetworkComponent;
+
+class KnowledgeGraphIntegration extends Page
+{
+    protected static ?string $title = '知识图谱整合视图';
+    protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-globe-alt';
+    protected static ?string $navigationLabel = '知识图谱整合';
+    protected static string|UnitEnum|null $navigationGroup = '整合视图';
+    protected static ?int $navigationSort = 10;
+    protected string $view = 'filament.pages.integrations.knowledge-graph-integration';
+
+    public ?string $selectedKpCode = null;
+    public string $activeTab = 'graph';
+    public bool $showSidebar = true;
+
+    public function mount(Request $request): void
+    {
+        $this->selectedKpCode = $request->query('kp_code');
+        $this->activeTab = $request->query('tab', 'graph');
+    }
+
+    #[Computed]
+    public function tabs()
+    {
+        return [
+            'graph' => [
+                'label' => '知识图谱',
+                'icon' => 'heroicon-o-share',
+                'component' => 'knowledge-graph',
+            ],
+            'list' => [
+                'label' => '知识点列表',
+                'icon' => 'heroicon-o-list-bullet',
+                'component' => 'knowledge-points-list',
+            ],
+            'stats' => [
+                'label' => '题库统计',
+                'icon' => 'heroicon-o-chart-bar',
+                'component' => 'knowledge-point-stats',
+            ],
+            'network' => [
+                'label' => '关联网络',
+                'icon' => 'heroicon-o-squares-2x2',
+                'component' => 'knowledge-network',
+            ],
+        ];
+    }
+
+    public function setActiveTab(string $tab): void
+    {
+        $this->activeTab = $tab;
+        $this->dispatch('tabChanged', tab: $tab);
+    }
+
+    public function toggleSidebar()
+    {
+        $this->showSidebar = !$this->showSidebar;
+    }
+
+    public function clearSelection()
+    {
+        $this->selectedKpCode = null;
+        $this->dispatch('clearAllSelections');
+    }
+
+    public function updatedSelectedKpCode($value)
+    {
+        if ($value) {
+            $this->dispatch('kpSelected', kpCode: $value);
+        }
+    }
+
+    public function getBreadcrumbs(): array
+    {
+        return [
+            'knowledge-graph-integration' => '知识图谱整合视图',
+        ];
+    }
+}

+ 33 - 11
app/Filament/Pages/QuestionGeneration.php

@@ -9,6 +9,7 @@ use Filament\Notifications\Notification;
 use Filament\Pages\Page;
 use UnitEnum;
 use Livewire\Attributes\Computed;
+use Illuminate\Support\Facades\Session;
 
 class QuestionGeneration extends Page
 {
@@ -124,7 +125,7 @@ class QuestionGeneration extends Page
         }
     }
 
-    public function executeGenerate(): void
+    public function executeGenerate()
     {
         if ($this->isGenerating) {
             return;
@@ -167,16 +168,37 @@ class QuestionGeneration extends Page
             if ($result['success'] ?? false) {
                 $this->currentTaskId = $result['task_id'] ?? null;
 
-                \Log::info("[QuestionGen] 任务已创建: {$this->currentTaskId},启动前端监控");
-
-                Notification::make()
-                    ->title('正在生成题目')
-                    ->body("任务 ID: {$this->currentTaskId}\nAI正在后台生成,预计需要30-60秒...")
-                    ->info()
-                    ->persistent()
-                    ->send();
-
-                $this->dispatch('start-async-task-monitoring');
+                \Log::info("[QuestionGen] ✅ 任务已创建: {$this->currentTaskId},准备跳转到题库管理");
+                \Log::info("[QuestionGen] 完整结果: " . json_encode($result));
+
+                // 准备跳转URL(不传递task_id参数)
+                $redirectUrl = "/admin/question-management";
+                $taskId = $this->currentTaskId ?? 'unknown';
+                \Log::info("[QuestionGen] ✅ 准备跳转到: {$redirectUrl}");
+                \Log::info("[QuestionGen] ✅ 准备传递的taskId: {$taskId}");
+
+                // 使用简单可靠的Livewire dispatch事件
+                // 注意:不要设置 isGenerating = false,让状态栏继续显示
+                \Log::info("[QuestionGen] ✅ 即将分发跳转事件", [
+                    'url' => $redirectUrl,
+                    'taskId' => $taskId
+                ]);
+
+                // 使用最简单的事件名称和参数
+                $this->dispatch('redirect-now', url: $redirectUrl, taskId: $taskId);
+                \Log::info("[QuestionGen] ✅ 事件已分发");
+
+                // 使用Livewire的JavaScript方法执行直接跳转
+                $this->js("console.log('[QuestionGen] 直接JS跳转启动');
+                alert('✅ 任务已启动\\n任务 ID: {$taskId}\\n正在跳转到题库管理页面...');
+                setTimeout(function() {
+                    console.log('[QuestionGen] 直接跳转执行:', '{$redirectUrl}');
+                    window.location.href = '{$redirectUrl}';
+                }, 1000);");
+
+                // 明确返回,避免执行后面的代码
+                \Log::info("[QuestionGen] ✅ 即将返回");
+                return;
             } else {
                 $this->isGenerating = false;
                 Notification::make()

+ 35 - 0
app/Filament/Pages/QuestionManagement.php

@@ -10,6 +10,8 @@ use Filament\Pages\Page;
 use UnitEnum;
 use Livewire\Attributes\Computed;
 use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Session;
 
 class QuestionManagement extends Page
 {
@@ -30,6 +32,9 @@ class QuestionManagement extends Page
     public bool $showDetailModal = false;
     public array $editing = [];
 
+    // ✅ 用于存储从URL参数获取的任务ID
+    public ?string $pendingTaskId = null;
+
 
     #[Computed(cache: false)]
     public function questions(): array
@@ -73,6 +78,36 @@ class QuestionManagement extends Page
         return app(QuestionServiceApi::class)->getKnowledgePointOptions();
     }
 
+    // ✅ 检查待处理的回调任务(简化版)
+    public function mount(): void
+    {
+        // 检查是否有从其他页面跳转带来的通知
+        $notification = Session::get('notification');
+        if ($notification) {
+            $color = $notification['color'] ?? 'info';
+            Notification::make()
+                ->title($notification['title'] ?? '通知')
+                ->body($notification['body'] ?? '')
+                ->$color()
+                ->persistent()
+                ->send();
+        }
+
+        // 从 request 中获取 task_id 参数(来自 URL 或缓存)
+        $taskId = request()->get('task_id');
+        if ($taskId) {
+            $this->pendingTaskId = $taskId;
+
+            // 简化:只记录task_id,前端通过JS定期检查回调状态
+            Notification::make()
+                ->title('📋 任务已创建')
+                ->body("任务 ID: {$taskId}\n等待后台生成完成,请稍候...")
+                ->info()
+                ->persistent()
+                ->send();
+        }
+    }
+
 
     public function deleteQuestion(string $questionCode): void
     {

+ 110 - 0
app/Filament/Pages/Statistics/KnowledgePointStats.php

@@ -0,0 +1,110 @@
+<?php
+
+namespace App\Filament\Pages\Statistics;
+
+use App\Services\QuestionBankService;
+use BackedEnum;
+use Filament\Pages\Page;
+use UnitEnum;
+use Livewire\Attributes\Computed;
+use Illuminate\Support\Facades\Cache;
+
+class KnowledgePointStats extends Page
+{
+    protected static ?string $title = '知识点题目统计';
+    protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-chart-bar';
+    protected static ?string $navigationLabel = '知识点统计';
+    protected static string|UnitEnum|null $navigationGroup = '统计报表';
+    protected static ?int $navigationSort = 21;
+    protected string $view = 'filament.pages.knowledge-point-stats';
+
+    public ?string $selectedKpCode = null;
+    public bool $showDetails = false;
+
+    #[Computed(cache: false)]
+    public function knowledgePointStatistics(): array
+    {
+        $cacheKey = 'knowledge-point-statistics';
+        if ($this->selectedKpCode) {
+            $cacheKey .= '-' . $this->selectedKpCode;
+        }
+
+        return Cache::remember(
+            $cacheKey,
+            now()->addMinutes(10),
+            function () {
+                $service = app(QuestionBankService::class);
+
+                if ($this->selectedKpCode) {
+                    $stats = $service->getKnowledgePointStatistics($this->selectedKpCode);
+                    return [$stats]; // 返回数组格式
+                }
+
+                return $service->getKnowledgePointStatistics();
+            }
+        );
+    }
+
+    #[Computed(cache: false)]
+    public function knowledgePointOptions(): array
+    {
+        try {
+            $knowledgeApiBase = config('services.knowledge_api.base_url', 'http://localhost:5011');
+            $response = \Illuminate\Support\Facades\Http::timeout(10)
+                ->get($knowledgeApiBase . '/graph/export');
+
+            if ($response->successful()) {
+                $data = $response->json();
+                $nodes = $data['nodes'] ?? [];
+
+                $options = [];
+                foreach ($nodes as $node) {
+                    $code = $node['kp_code'] ?? null;
+                    $name = $node['cn_name'] ?? null;
+
+                    if ($code && $name) {
+                        $options[$code] = $name;
+                    }
+                }
+
+                ksort($options);
+                return $options;
+            }
+        } catch (\Exception $e) {
+            \Log::error('Failed to get knowledge points: ' . $e->getMessage());
+        }
+
+        return [];
+    }
+
+    public function updatedSelectedKpCode(): void
+    {
+        $this->showDetails = false;
+        Cache::forget('knowledge-point-statistics-' . $this->selectedKpCode);
+    }
+
+    public function toggleDetails(): void
+    {
+        $this->showDetails = !$this->showDetails;
+    }
+
+    public function getTotalQuestions(array $stats): int
+    {
+        return $stats['total_questions'] ?? 0;
+    }
+
+    public function getDirectQuestions(array $stats): int
+    {
+        return $stats['direct_questions'] ?? 0;
+    }
+
+    public function getChildrenQuestions(array $stats): int
+    {
+        return $stats['children_questions'] ?? 0;
+    }
+
+    public function getSkillQuestions(array $stats): int
+    {
+        return $stats['skills_total_questions'] ?? 0;
+    }
+}

+ 185 - 0
app/Livewire/Integrations/KnowledgeGraphComponent.php

@@ -0,0 +1,185 @@
+<?php
+
+namespace App\Livewire\Integrations;
+
+use Livewire\Component;
+use App\Services\KnowledgeServiceApi;
+use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Log;
+
+class KnowledgeGraphComponent extends Component
+{
+    public $selectedKpCode = null;
+    public $graphData = [];
+    public $nodes = [];
+    public $edges = [];
+    public $isLoading = false;
+    public $showStats = false;
+    public $statsData = [];
+
+    protected $listeners = [
+        'kpSelected' => 'handleKpSelected',
+        'refreshGraph' => 'refreshGraph',
+    ];
+
+    public function mount($selectedKpCode = null)
+    {
+        $this->selectedKpCode = $selectedKpCode;
+        $this->loadGraphData();
+    }
+
+    public function loadGraphData()
+    {
+        $this->isLoading = true;
+        try {
+            $service = app(KnowledgeServiceApi::class);
+
+            // 获取完整图谱数据
+            if ($this->selectedKpCode) {
+                // 如果指定了知识点,获取该点的详细信息
+                $nodeData = $service->getKnowledgePointDetail($this->selectedKpCode);
+                $upstream = $service->getUpstreamNodes($this->selectedKpCode, 2);
+                $downstream = $service->getDownstreamNodes($this->selectedKpCode, 2);
+
+                $this->buildSelectedGraph($nodeData, $upstream, $downstream);
+            } else {
+                // 获取完整图谱
+                $this->buildFullGraph();
+            }
+
+            // 获取统计信息
+            $this->loadStatsData();
+
+        } catch (\Exception $e) {
+            Log::error('加载知识图谱失败', [
+                'kp_code' => $this->selectedKpCode,
+                'error' => $e->getMessage()
+            ]);
+            $this->dispatch('error', message: '加载知识图谱失败:' . $e->getMessage());
+        }
+        $this->isLoading = false;
+    }
+
+    private function buildFullGraph()
+    {
+        try {
+            $knowledgeApiBase = config('services.knowledge_api.base_url', 'http://localhost:5011');
+            $response = Http::timeout(10)
+                ->get($knowledgeApiBase . '/graph/export');
+
+            if ($response->successful()) {
+                $data = $response->json();
+                $this->nodes = $data['nodes'] ?? [];
+                $this->edges = $data['edges'] ?? [];
+
+                // 转换为可视化格式
+                $this->graphData = $this->formatForVisualization($this->nodes, $this->edges);
+            }
+        } catch (\Exception $e) {
+            Log::error('获取完整图谱失败', ['error' => $e->getMessage()]);
+        }
+    }
+
+    private function buildSelectedGraph($nodeData, $upstream, $downstream)
+    {
+        // 构建以选中节点为中心的图谱
+        $this->nodes = [$nodeData];
+        $this->edges = [];
+
+        // 添加上游节点
+        if (isset($upstream['nodes'])) {
+            $this->nodes = array_merge($this->nodes, $upstream['nodes']);
+        }
+
+        // 添加下游节点
+        if (isset($downstream['nodes'])) {
+            $this->nodes = array_merge($this->nodes, $downstream['nodes']);
+        }
+
+        // 转换为可视化格式
+        $this->graphData = $this->formatForVisualization($this->nodes, $this->edges);
+    }
+
+    private function formatForVisualization($nodes, $edges)
+    {
+        $formattedNodes = [];
+        $formattedEdges = [];
+
+        foreach ($nodes as $node) {
+            $formattedNodes[] = [
+                'id' => $node['kp_code'],
+                'label' => $node['cn_name'] ?? $node['kp_code'],
+                'kp_code' => $node['kp_code'],
+                'cn_name' => $node['cn_name'] ?? '',
+                'en_name' => $node['en_name'] ?? '',
+                'category' => $node['category'] ?? '',
+                'phase' => $node['phase'] ?? '',
+                'importance' => $node['importance'] ?? 0,
+                'description' => $node['description'] ?? '',
+                'skills_count' => count($node['skills'] ?? []),
+            ];
+        }
+
+        foreach ($edges as $edge) {
+            $formattedEdges[] = [
+                'from' => $edge['source'],
+                'to' => $edge['target'],
+                'type' => $edge['relation_type'] ?? '',
+                'direction' => $edge['relation_direction'] ?? '',
+                'label' => $edge['description'] ?? '',
+            ];
+        }
+
+        return [
+            'nodes' => $formattedNodes,
+            'edges' => $formattedEdges,
+        ];
+    }
+
+    private function loadStatsData()
+    {
+        try {
+            $questionBankBase = config('services.question_bank.base_url', 'http://localhost:5015');
+            $response = Http::timeout(10)
+                ->get($questionBankBase . '/api/questions/statistics');
+
+            if ($response->successful()) {
+                $data = $response->json();
+                $this->statsData = $data['by_kp'] ?? [];
+
+                // 为节点添加题目数量信息
+                foreach ($this->graphData['nodes'] as &$node) {
+                    $kpCode = $node['kp_code'];
+                    $stat = collect($this->statsData)->firstWhere('kp_code', $kpCode);
+                    $node['question_count'] = $stat['question_count'] ?? 0;
+                    $node['skills_list'] = $stat['skills_list'] ?? [];
+                }
+            }
+        } catch (\Exception $e) {
+            Log::error('获取统计数据失败', ['error' => $e->getMessage()]);
+        }
+    }
+
+    public function handleKpSelected($kpCode)
+    {
+        $this->selectedKpCode = $kpCode;
+        $this->loadGraphData();
+    }
+
+    public function refreshGraph()
+    {
+        $this->loadGraphData();
+    }
+
+    public function toggleStats()
+    {
+        $this->showStats = !$this->showStats;
+    }
+
+    public function render()
+    {
+        return view('livewire.integrations.knowledge-graph-component', [
+            'graphData' => $this->graphData,
+        ]);
+    }
+}

+ 197 - 0
app/Livewire/Integrations/KnowledgeGraphVisualization.php

@@ -0,0 +1,197 @@
+<?php
+
+namespace App\Livewire\Integrations;
+
+use Livewire\Component;
+use App\Services\KnowledgeServiceApi;
+use App\Services\QuestionBankService;
+use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Log;
+
+class KnowledgeGraphVisualization extends Component
+{
+    public $selectedKpCode = null;
+    public $graphData = [];
+    public $nodes = [];
+    public $edges = [];
+    public $isLoading = false;
+    public $layoutType = 'full'; // 'full' 或 'selected'
+    public $filterPhase = '';
+    public $filterCategory = '';
+
+    protected $listeners = [
+        'nodeSelected' => 'handleNodeSelected',
+        'clearSelection' => 'clearSelection',
+    ];
+
+    public function mount($selectedKpCode = null)
+    {
+        $this->selectedKpCode = $selectedKpCode;
+        $this->loadGraphData();
+    }
+
+    public function loadGraphData()
+    {
+        $this->isLoading = true;
+        try {
+            $knowledgeApiBase = config('services.knowledge_api.base_url', 'http://localhost:5011');
+            $response = Http::timeout(10)
+                ->get($knowledgeApiBase . '/graph/export');
+
+            if ($response->successful()) {
+                $data = $response->json();
+                $allNodes = $data['nodes'] ?? [];
+                $allEdges = $data['edges'] ?? [];
+
+                // 应用筛选
+                $filteredNodes = $this->applyFilters($allNodes);
+                $this->nodes = $filteredNodes;
+                $this->edges = $allEdges;
+
+                // 转换为可视化格式
+                $this->graphData = $this->formatForVisualization($this->nodes, $this->edges);
+
+                // 如果指定了选中的知识点,加载详细信息
+                if ($this->selectedKpCode) {
+                    $this->loadNodeDetails($this->selectedKpCode);
+                }
+            }
+        } catch (\Exception $e) {
+            Log::error('加载知识图谱失败', [
+                'error' => $e->getMessage()
+            ]);
+            $this->dispatch('error', message: '加载知识图谱失败:' . $e->getMessage());
+        }
+        $this->isLoading = false;
+    }
+
+    private function applyFilters($nodes)
+    {
+        $filtered = collect($nodes);
+
+        if ($this->filterPhase) {
+            $filtered = $filtered->where('phase', $this->filterPhase);
+        }
+
+        if ($this->filterCategory) {
+            $filtered = $filtered->where('category', $this->filterCategory);
+        }
+
+        return $filtered->values()->toArray();
+    }
+
+    private function formatForVisualization($nodes, $edges)
+    {
+        $formattedNodes = [];
+        $formattedEdges = [];
+
+        foreach ($nodes as $node) {
+            $formattedNodes[] = [
+                'id' => $node['kp_code'],
+                'label' => $node['cn_name'] ?? $node['kp_code'],
+                'kp_code' => $node['kp_code'],
+                'cn_name' => $node['cn_name'] ?? '',
+                'en_name' => $node['en_name'] ?? '',
+                'category' => $node['category'] ?? '',
+                'phase' => $node['phase'] ?? '',
+                'importance' => $node['importance'] ?? 0,
+                'description' => $node['description'] ?? '',
+                'skills_count' => count($node['skills'] ?? []),
+                'is_selected' => $this->selectedKpCode === $node['kp_code'],
+            ];
+        }
+
+        foreach ($edges as $edge) {
+            $formattedEdges[] = [
+                'from' => $edge['source'],
+                'to' => $edge['target'],
+                'type' => $edge['relation_type'] ?? '',
+                'direction' => $edge['relation_direction'] ?? '',
+                'label' => $edge['description'] ?? '',
+            ];
+        }
+
+        return [
+            'nodes' => $formattedNodes,
+            'edges' => $formattedEdges,
+        ];
+    }
+
+    private function loadNodeDetails($kpCode)
+    {
+        try {
+            $service = app(KnowledgeServiceApi::class);
+            $detail = $service->getKnowledgePointDetail($kpCode);
+
+            if ($detail) {
+                // 获取上游和下游节点(一级子知识点)
+                $upstream = $service->getUpstreamNodes($kpCode, 1);
+                $downstream = $service->getDownstreamNodes($kpCode, 1);
+
+                // 获取题目统计数据
+                $questionService = app(QuestionBankService::class);
+                $stats = $questionService->getKnowledgePointStatistics($kpCode);
+
+                // 构建详情数据
+                $detail['upstream_nodes'] = $upstream['nodes'] ?? [];
+                $detail['downstream_nodes'] = $downstream['nodes'] ?? [];
+                $detail['question_stats'] = $stats;
+
+                $this->dispatch('nodeDetailsLoaded', details: $detail);
+            }
+        } catch (\Exception $e) {
+            Log::error('加载节点详情失败', [
+                'kp_code' => $kpCode,
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+
+    public function handleNodeSelected($kpCode)
+    {
+        $this->selectedKpCode = $kpCode;
+        $this->loadNodeDetails($kpCode);
+
+        // 刷新图谱数据以高亮选中节点
+        $this->graphData = $this->formatForVisualization($this->nodes, $this->edges);
+    }
+
+    public function clearSelection()
+    {
+        $this->selectedKpCode = null;
+        $this->graphData = $this->formatForVisualization($this->nodes, $this->edges);
+        $this->dispatch('nodeDetailsCleared');
+    }
+
+    public function setLayoutType($type)
+    {
+        $this->layoutType = $type;
+        $this->loadGraphData();
+    }
+
+    public function updatedFilterPhase()
+    {
+        $this->loadGraphData();
+    }
+
+    public function updatedFilterCategory()
+    {
+        $this->loadGraphData();
+    }
+
+    public function getFilterOptionsProperty()
+    {
+        $phases = collect($this->nodes)->pluck('phase')->filter()->unique()->sort()->values()->toArray();
+        $categories = collect($this->nodes)->pluck('category')->filter()->unique()->sort()->values()->toArray();
+
+        return [
+            'phases' => $phases,
+            'categories' => $categories,
+        ];
+    }
+
+    public function render()
+    {
+        return view('livewire.integrations.knowledge-graph-visualization');
+    }
+}

+ 256 - 0
app/Livewire/Integrations/KnowledgeNetworkComponent.php

@@ -0,0 +1,256 @@
+<?php
+
+namespace App\Livewire\Integrations;
+
+use Livewire\Component;
+use App\Services\KnowledgeServiceApi;
+use App\Services\QuestionBankService;
+use Illuminate\Support\Facades\Http;
+
+class KnowledgeNetworkComponent extends Component
+{
+    public $selectedKpCode = null;
+    public $networkData = [];
+    public $kpRelations = [];
+    public $questionRelations = [];
+    public $isLoading = false;
+
+    protected $listeners = [
+        'kpSelected' => 'handleKpSelected',
+        'clearSelection' => 'clearSelection',
+        'refreshNetwork' => 'loadNetworkData',
+    ];
+
+    public function mount($selectedKpCode = null)
+    {
+        $this->selectedKpCode = $selectedKpCode;
+        $this->loadNetworkData();
+    }
+
+    public function loadNetworkData()
+    {
+        $this->isLoading = true;
+        try {
+            // 获取知识图谱关联关系
+            $this->loadKpRelations();
+
+            // 获取题库关联关系
+            $this->loadQuestionRelations();
+
+            // 整合数据
+            $this->buildNetworkData();
+
+        } catch (\Exception $e) {
+            \Log::error('加载知识网络失败', [
+                'kp_code' => $this->selectedKpCode,
+                'error' => $e->getMessage()
+            ]);
+            $this->dispatch('error', message: '加载知识网络失败');
+        }
+        $this->isLoading = false;
+    }
+
+    private function loadKpRelations()
+    {
+        try {
+            $service = app(KnowledgeServiceApi::class);
+
+            if ($this->selectedKpCode) {
+                // 获取选中知识点的关联关系
+                $upstream = $service->getUpstreamNodes($this->selectedKpCode, 3);
+                $downstream = $service->getDownstreamNodes($this->selectedKpCode, 3);
+                $related = $service->getRelatedNodes($this->selectedKpCode);
+
+                $this->kpRelations = [
+                    'upstream' => $upstream['nodes'] ?? [],
+                    'downstream' => $downstream['nodes'] ?? [],
+                    'related' => $related['nodes'] ?? [],
+                ];
+            } else {
+                // 获取所有关联关系(从完整图谱中提取)
+                $graphData = $this->getFullGraphData();
+                $this->kpRelations = $graphData;
+            }
+        } catch (\Exception $e) {
+            \Log::error('获取知识关联关系失败', ['error' => $e->getMessage()]);
+        }
+    }
+
+    private function loadQuestionRelations()
+    {
+        try {
+            $service = app(QuestionBankService::class);
+
+            // 获取所有题库统计数据
+            $stats = $service->getKnowledgePointStatistics();
+
+            if ($this->selectedKpCode) {
+                // 只返回选中知识点的统计
+                $selectedStats = collect($stats)->firstWhere('kp_code', $this->selectedKpCode);
+                $this->questionRelations = $selectedStats ? [$selectedStats] : [];
+            } else {
+                $this->questionRelations = $stats;
+            }
+        } catch (\Exception $e) {
+            \Log::error('获取题库关联关系失败', ['error' => $e->getMessage()]);
+        }
+    }
+
+    private function getFullGraphData()
+    {
+        try {
+            $knowledgeApiBase = config('services.knowledge_api.base_url', 'http://localhost:5011');
+            $response = Http::timeout(10)
+                ->get($knowledgeApiBase . '/graph/export');
+
+            if ($response->successful()) {
+                return $response->json();
+            }
+        } catch (\Exception $e) {
+            \Log::error('获取完整图谱失败', ['error' => $e->getMessage()]);
+        }
+
+        return ['nodes' => [], 'edges' => []];
+    }
+
+    private function buildNetworkData()
+    {
+        $network = [
+            'nodes' => [],
+            'links' => [],
+            'groups' => [
+                'knowledge' => ['name' => '知识点', 'color' => '#3B82F6'],
+                'questions' => ['name' => '题库', 'color' => '#10B981'],
+                'skills' => ['name' => '技能', 'color' => '#F59E0B'],
+            ],
+        ];
+
+        // 添加知识点节点
+        $nodesMap = [];
+
+        if ($this->selectedKpCode) {
+            // 选中模式:添加相关知识点
+            $relatedNodes = array_merge(
+                $this->kpRelations['upstream'] ?? [],
+                $this->kpRelations['downstream'] ?? [],
+                $this->kpRelations['related'] ?? []
+            );
+
+            foreach ($relatedNodes as $node) {
+                $kpCode = $node['kp_code'];
+                $nodesMap[$kpCode] = [
+                    'id' => $kpCode,
+                    'name' => $node['cn_name'] ?? $kpCode,
+                    'group' => 'knowledge',
+                    'type' => 'knowledge_point',
+                    'data' => $node,
+                ];
+            }
+
+            // 添加选中的知识点
+            $selectedNode = collect($this->kpRelations['upstream'] ?? [])
+                ->merge($this->kpRelations['downstream'] ?? [])
+                ->merge($this->kpRelations['related'] ?? [])
+                ->firstWhere('kp_code', $this->selectedKpCode);
+
+            if ($selectedNode) {
+                $nodesMap[$this->selectedKpCode] = [
+                    'id' => $this->selectedKpCode,
+                    'name' => $selectedNode['cn_name'] ?? $this->selectedKpCode,
+                    'group' => 'knowledge',
+                    'type' => 'knowledge_point',
+                    'data' => $selectedNode,
+                ];
+            }
+        } else {
+            // 完整模式:从题库统计中获取知识点
+            foreach ($this->questionRelations as $stat) {
+                $kpCode = $stat['kp_code'];
+                if (!$kpCode) continue;
+
+                $nodesMap[$kpCode] = [
+                    'id' => $kpCode,
+                    'name' => $stat['cn_name'] ?? $kpCode,
+                    'group' => 'knowledge',
+                    'type' => 'knowledge_point',
+                    'question_count' => $stat['total_questions'] ?? 0,
+                    'data' => $stat,
+                ];
+            }
+        }
+
+        // 添加技能节点
+        foreach ($this->questionRelations as $stat) {
+            if (isset($stat['skills']) && is_array($stat['skills'])) {
+                foreach ($stat['skills'] as $skill) {
+                    $skillCode = $skill['skill_code'] ?? '';
+                    if (!$skillCode) continue;
+
+                    $nodesMap[$skillCode] = [
+                        'id' => $skillCode,
+                        'name' => $skillCode,
+                        'group' => 'skills',
+                        'type' => 'skill',
+                        'data' => $skill,
+                    ];
+                }
+            }
+        }
+
+        // 添加链接
+        foreach ($this->questionRelations as $stat) {
+            $kpCode = $stat['kp_code'];
+            if (!$kpCode) continue;
+
+            // 知识点到题目的链接
+            if (isset($nodesMap[$kpCode])) {
+                $questionCount = $stat['total_questions'] ?? 0;
+                if ($questionCount > 0) {
+                    $network['links'][] = [
+                        'source' => $kpCode,
+                        'target' => "questions_{$kpCode}",
+                        'value' => $questionCount,
+                        'type' => 'has_questions',
+                    ];
+                }
+
+                // 知识点到技能点的链接
+                if (isset($stat['skills']) && is_array($stat['skills'])) {
+                    foreach ($stat['skills'] as $skill) {
+                        $skillCode = $skill['skill_code'] ?? '';
+                        if ($skillCode && isset($nodesMap[$skillCode])) {
+                            $network['links'][] = [
+                                'source' => $kpCode,
+                                'target' => $skillCode,
+                                'value' => $skill['question_count'] ?? 1,
+                                'type' => 'has_skill',
+                            ];
+                        }
+                    }
+                }
+            }
+        }
+
+        $network['nodes'] = array_values($nodesMap);
+        $this->networkData = $network;
+    }
+
+    public function handleKpSelected($kpCode)
+    {
+        $this->selectedKpCode = $kpCode;
+        $this->loadNetworkData();
+    }
+
+    public function clearSelection()
+    {
+        $this->selectedKpCode = null;
+        $this->loadNetworkData();
+    }
+
+    public function render()
+    {
+        return view('livewire.integrations.knowledge-network-component', [
+            'networkData' => $this->networkData,
+        ]);
+    }
+}

+ 135 - 0
app/Livewire/Integrations/KnowledgePointDetails.php

@@ -0,0 +1,135 @@
+<?php
+
+namespace App\Livewire\Integrations;
+
+use Livewire\Component;
+use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Log;
+
+class KnowledgePointDetails extends Component
+{
+    public $selectedNode = null;
+    public $nodeDetails = null;
+    public $isLoading = false;
+    public $activeTab = 'overview'; // 'overview', 'children', 'skills'
+
+    protected $listeners = [
+        'nodeDetailsLoaded' => 'handleNodeDetailsLoaded',
+        'nodeDetailsCleared' => 'handleNodeDetailsCleared',
+        'kpSelected' => 'handleKpSelected',
+    ];
+
+    public function handleNodeDetailsLoaded($details)
+    {
+        $this->nodeDetails = $details;
+        $this->selectedNode = $details;
+        $this->activeTab = 'overview';
+    }
+
+    public function handleNodeDetailsCleared()
+    {
+        $this->nodeDetails = null;
+        $this->selectedNode = null;
+    }
+
+    public function handleKpSelected($kpCode)
+    {
+        $this->selectedNode = $kpCode;
+        $this->loadNodeDetails($kpCode);
+    }
+
+    private function loadNodeDetails($kpCode)
+    {
+        $this->isLoading = true;
+        try {
+            // 从知识图谱服务获取节点详情
+            $knowledgeApiBase = config('services.knowledge_api.base_url', 'http://localhost:5011');
+
+            // 获取节点基本信息
+            $detailResponse = Http::timeout(10)
+                ->get($knowledgeApiBase . "/knowledge-points/{$kpCode}");
+
+            if ($detailResponse->successful()) {
+                $detail = $detailResponse->json();
+
+                // 获取上游和下游节点(一级子知识点)
+                $upstreamResponse = Http::timeout(10)
+                    ->get($knowledgeApiBase . "/knowledge-points/{$kpCode}/upstream", ['levels' => 1]);
+
+                $downstreamResponse = Http::timeout(10)
+                    ->get($knowledgeApiBase . "/knowledge-points/{$kpCode}/downstream", ['levels' => 1]);
+
+                $detail['upstream_nodes'] = $upstreamResponse->successful() ? $upstreamResponse->json()['nodes'] ?? [] : [];
+                $detail['downstream_nodes'] = $downstreamResponse->successful() ? $downstreamResponse->json()['nodes'] ?? [] : [];
+
+                // 获取题目统计数据
+                $questionApiBase = config('services.question_bank_api.base_url', 'http://localhost:5015');
+                $statsResponse = Http::timeout(10)
+                    ->get($questionApiBase . "/api/questions/statistics", ['kp_code' => $kpCode]);
+
+                if ($statsResponse->successful()) {
+                    $detail['question_stats'] = $statsResponse->json();
+                }
+
+                $this->nodeDetails = $detail;
+                $this->activeTab = 'overview';
+            }
+        } catch (\Exception $e) {
+            // 错误处理
+            Log::error('加载节点详情失败', [
+                'kp_code' => $kpCode,
+                'error' => $e->getMessage()
+            ]);
+        }
+        $this->isLoading = false;
+    }
+
+    public function setActiveTab($tab)
+    {
+        $this->activeTab = $tab;
+    }
+
+    public function getTotalQuestionsProperty()
+    {
+        if (!$this->nodeDetails) return 0;
+        return $this->nodeDetails['question_stats']['total_questions'] ?? 0;
+    }
+
+    public function getDirectQuestionsProperty()
+    {
+        if (!$this->nodeDetails) return 0;
+        return $this->nodeDetails['question_stats']['direct_questions'] ?? 0;
+    }
+
+    public function getChildrenQuestionsProperty()
+    {
+        if (!$this->nodeDetails) return 0;
+        return $this->nodeDetails['question_stats']['children_questions'] ?? 0;
+    }
+
+    public function getSkillsQuestionsProperty()
+    {
+        if (!$this->nodeDetails) return 0;
+        return $this->nodeDetails['question_stats']['skills_total_questions'] ?? 0;
+    }
+
+    public function getSkillsCountProperty()
+    {
+        if (!$this->nodeDetails) return 0;
+        return $this->nodeDetails['question_stats']['skills_count'] ?? 0;
+    }
+
+    public function getChildrenNodesProperty()
+    {
+        if (!$this->nodeDetails) return [];
+        return array_merge(
+            $this->nodeDetails['upstream_nodes'] ?? [],
+            $this->nodeDetails['downstream_nodes'] ?? []
+        );
+    }
+
+    public function render()
+    {
+        return view('livewire.integrations.knowledge-point-details');
+    }
+}

+ 86 - 0
app/Livewire/Integrations/KnowledgePointStatsComponent.php

@@ -0,0 +1,86 @@
+<?php
+
+namespace App\Livewire\Integrations;
+
+use Livewire\Component;
+use App\Services\QuestionBankService;
+
+class KnowledgePointStatsComponent extends Component
+{
+    public $selectedKpCode = null;
+    public $statsData = [];
+    public $isLoading = false;
+    public $showDetails = false;
+
+    protected $listeners = [
+        'kpSelected' => 'handleKpSelected',
+        'clearSelection' => 'clearSelection',
+        'refreshStats' => 'loadStatsData',
+    ];
+
+    public function mount($selectedKpCode = null)
+    {
+        $this->selectedKpCode = $selectedKpCode;
+        $this->loadStatsData();
+    }
+
+    public function loadStatsData()
+    {
+        $this->isLoading = true;
+        try {
+            $service = app(QuestionBankService::class);
+            $this->statsData = $service->getKnowledgePointStatistics($this->selectedKpCode);
+        } catch (\Exception $e) {
+            \Log::error('加载知识点统计失败', [
+                'kp_code' => $this->selectedKpCode,
+                'error' => $e->getMessage()
+            ]);
+            $this->dispatch('error', message: '加载统计数据失败');
+        }
+        $this->isLoading = false;
+    }
+
+    public function handleKpSelected($kpCode)
+    {
+        $this->selectedKpCode = $kpCode;
+        $this->loadStatsData();
+    }
+
+    public function clearSelection()
+    {
+        $this->selectedKpCode = null;
+        $this->loadStatsData();
+    }
+
+    public function toggleDetails()
+    {
+        $this->showDetails = !$this->showDetails;
+    }
+
+    public function getTotalQuestions($stats)
+    {
+        return $stats['total_questions'] ?? 0;
+    }
+
+    public function getDirectQuestions($stats)
+    {
+        return $stats['direct_questions'] ?? 0;
+    }
+
+    public function getChildrenQuestions($stats)
+    {
+        return $stats['children_questions'] ?? 0;
+    }
+
+    public function getSkillQuestions($stats)
+    {
+        return $stats['skills_total_questions'] ?? 0;
+    }
+
+    public function render()
+    {
+        return view('livewire.integrations.knowledge-point-stats-component', [
+            'statsData' => $this->statsData,
+        ]);
+    }
+}

+ 126 - 0
app/Livewire/Integrations/KnowledgePointsListComponent.php

@@ -0,0 +1,126 @@
+<?php
+
+namespace App\Livewire\Integrations;
+
+use Livewire\Component;
+use App\Services\KnowledgeServiceApi;
+use Illuminate\Support\Str;
+
+class KnowledgePointsListComponent extends Component
+{
+    public $selectedKpCode = null;
+    public $search = '';
+    public $phaseFilter = null;
+    public $categoryFilter = null;
+    public $knowledgePoints = [];
+    public $filteredPoints = [];
+    public $isLoading = false;
+
+    protected $listeners = [
+        'selectKp' => 'selectKp',
+        'clearSelection' => 'clearSelection',
+    ];
+
+    public function mount()
+    {
+        $this->loadKnowledgePoints();
+    }
+
+    public function loadKnowledgePoints()
+    {
+        $this->isLoading = true;
+        try {
+            $service = app(KnowledgeServiceApi::class);
+            $points = $service->listKnowledgePoints(perPage: 200);
+            $this->knowledgePoints = $points->toArray();
+            $this->applyFilters();
+        } catch (\Exception $e) {
+            \Log::error('加载知识点列表失败', ['error' => $e->getMessage()]);
+            $this->dispatch('error', message: '加载知识点列表失败');
+        }
+        $this->isLoading = false;
+    }
+
+    public function updatedSearch()
+    {
+        $this->applyFilters();
+    }
+
+    public function updatedPhaseFilter()
+    {
+        $this->applyFilters();
+    }
+
+    public function updatedCategoryFilter()
+    {
+        $this->applyFilters();
+    }
+
+    public function applyFilters()
+    {
+        $filtered = collect($this->knowledgePoints);
+
+        // 按学段筛选
+        if ($this->phaseFilter) {
+            $filtered = $filtered->where('phase', $this->phaseFilter);
+        }
+
+        // 按类别筛选
+        if ($this->categoryFilter) {
+            $filtered = $filtered->where('category', $this->categoryFilter);
+        }
+
+        // 按搜索词筛选
+        if ($this->search) {
+            $searchTerm = Str::lower($this->search);
+            $filtered = $filtered->filter(function ($point) use ($searchTerm) {
+                return Str::contains(Str::lower($point['cn_name'] ?? ''), $searchTerm)
+                    || Str::contains(Str::lower($point['kp_code'] ?? ''), $searchTerm)
+                    || Str::contains(Str::lower($point['description'] ?? ''), $searchTerm);
+            });
+        }
+
+        $this->filteredPoints = $filtered->values()->toArray();
+    }
+
+    public function selectKp($kpCode)
+    {
+        $this->selectedKpCode = $kpCode;
+        $this->dispatch('kpSelected', kpCode: $kpCode);
+    }
+
+    public function clearSelection()
+    {
+        $this->selectedKpCode = null;
+        $this->dispatch('clearGraphSelection');
+    }
+
+    public function getFilterOptionsProperty()
+    {
+        $phases = collect($this->knowledgePoints)
+            ->pluck('phase')
+            ->filter()
+            ->unique()
+            ->sort()
+            ->values()
+            ->toArray();
+
+        $categories = collect($this->knowledgePoints)
+            ->pluck('category')
+            ->filter()
+            ->unique()
+            ->sort()
+            ->values()
+            ->toArray();
+
+        return [
+            'phases' => $phases,
+            'categories' => $categories,
+        ];
+    }
+
+    public function render()
+    {
+        return view('livewire.integrations.knowledge-points-list-component');
+    }
+}

+ 8 - 0
app/Providers/Filament/AdminPanelProvider.php

@@ -2,9 +2,13 @@
 
 namespace App\Providers\Filament;
 
+use App\Filament\Pages\Integrations\KnowledgeGraphExplorer;
+use App\Filament\Pages\Integrations\KnowledgeGraphIntegration;
 use App\Filament\Pages\KnowledgePoints;
+use App\Filament\Pages\KnowledgeMindmap;
 use App\Filament\Pages\QuestionManagement;
 use App\Filament\Pages\PromptManagement;
+use App\Filament\Pages\Statistics\KnowledgePointStats;
 use App\Filament\Pages\StudentDashboard;
 use App\Filament\Pages\StudentManagement;
 use App\Filament\Pages\StudentKnowledgeGraphPage;
@@ -47,8 +51,12 @@ class AdminPanelProvider extends PanelProvider
                 StudentDashboard::class,
                 StudentKnowledgeGraphPage::class,
                 KnowledgePoints::class,
+                KnowledgeMindmap::class,
                 QuestionManagement::class,
                 PromptManagement::class,
+                KnowledgePointStats::class,
+                KnowledgeGraphIntegration::class,
+                KnowledgeGraphExplorer::class,
             ])
             ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\Filament\Widgets')
             ->widgets([

+ 290 - 0
resources/views/filament/pages/integrations/knowledge-graph-explorer.blade.php

@@ -0,0 +1,290 @@
+<x-filament-panels::page>
+    <div class="space-y-6">
+        {{-- 页面标题 --}}
+        <div class="rounded-xl border border-slate-200 bg-white shadow-sm p-5">
+            <div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
+                <div class="space-y-2 text-slate-900">
+                    <h2 class="text-3xl font-bold">知识图谱浏览</h2>
+                    <p class="text-lg text-slate-600">点击任意节点查看详细信息,包括子知识点和技能点</p>
+                </div>
+                <div class="flex items-center gap-2">
+                    <a
+                        href="{{ url('admin/knowledge-graph-integration') }}"
+                        class="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:text-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700"
+                    >
+                        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"></path>
+                        </svg>
+                        综合视图
+                    </a>
+                    <a
+                        href="{{ url('admin/knowledge-mindmap') }}"
+                        class="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:text-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700"
+                    >
+                        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"></path>
+                        </svg>
+                        原始脑图
+                    </a>
+                </div>
+            </div>
+        </div>
+
+        {{-- 完全复用原版知识图谱代码 --}}
+        <div
+            class="rounded-2xl border border-slate-200 shadow-sm bg-white text-slate-900 knowledge-mindmap-card"
+            x-data="{
+                graphInstance: null,
+                stats: { nodes: 0, extraEdges: 0 },
+                livewireId: '{{ $this->getId() }}',
+                selectedNode: null,
+                initEventListener() {
+                    window.addEventListener('mindmap-node-selected', (evt) => {
+                        const model = evt.detail || {};
+                        const code = model?.meta?.code || model?.id;
+                        const mastery = model?.meta?.mastery_level ?? this.graphInstance?.masteryData?.[code]?.mastery_level ?? 0;
+                        const accuracy = model?.meta?.accuracy_rate ?? this.graphInstance?.masteryData?.[code]?.accuracy_rate ?? null;
+                        const attempts = model?.meta?.total_attempts ?? this.graphInstance?.masteryData?.[code]?.total_attempts ?? null;
+                        this.selectedNode = {
+                            id: model?.id,
+                            code,
+                            label: model?.label,
+                            mastery,
+                            accuracy,
+                            attempts,
+                            recommended: model?.meta?.recommended ?? false,
+                            skills: model?.meta?.skills || this.graphInstance?.masteryData?.[code]?.skills || [],
+                        };
+
+                        // 不再需要通知Livewire,直接在Alpine中处理
+                        // selectedNode 已在上面设置完成
+                    });
+                },
+
+                async initMindmap() {
+                    try {
+                        // 等待G6和自定义组件加载
+                        await this.waitForComponents();
+
+                        if (!window.G6 || !window.KnowledgeMindmapGraph) {
+                            console.error('G6组件未加载');
+                            return;
+                        }
+
+                        this.graphInstance = new window.KnowledgeMindmapGraph({
+                            containerId: 'knowledge-graph-viz',
+                            livewireMethod: null,  // 不使用自动调用,由Alpine手动处理
+                            livewireId: this.livewireId,
+                            emitSelection: true,
+                            highlightLowMastery: false,
+                        });
+                        await this.graphInstance.init();
+                        this.stats = this.graphInstance.stats;
+                    } catch (error) {
+                        console.error('知识图谱初始化失败:', error);
+                    }
+                },
+
+                async waitForComponents() {
+                    let attempts = 0;
+                    const maxAttempts = 50;
+
+                    while ((!window.G6 || !window.KnowledgeMindmapGraph) && attempts < maxAttempts) {
+                        await new Promise(resolve => setTimeout(resolve, 100));
+                        attempts++;
+                    }
+
+                    if (attempts >= maxAttempts) {
+                        throw new Error('G6组件加载超时');
+                    }
+                }
+            }"
+            x-init="initEventListener(); initMindmap()"
+            data-knowledge-mindmap-root
+        >
+            <div class="relative overflow-hidden rounded-2xl border border-slate-200 shadow-sm bg-white text-slate-900">
+                <div
+                    wire:ignore
+                    id="knowledge-graph-viz"
+                    class="knowledge-mindmap-canvas relative h-[82vh] min-h-[720px] w-full"
+                >
+                    <div class="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_20%_20%,rgba(255,255,255,0.08),transparent_40%),radial-gradient(circle_at_80%_60%,rgba(255,255,255,0.06),transparent_45%)]"></div>
+                </div>
+            </div>
+
+            {{-- 选中节点信息 --}}
+            <template x-if="selectedNode">
+                <div class="mt-4 grid grid-cols-1 gap-4 lg:grid-cols-3">
+                    <div class="lg:col-span-2 rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
+                        <div class="flex items-start justify-between">
+                            <div>
+                                <p class="text-sm text-slate-500">选中知识点</p>
+                                <h3 class="text-xl font-semibold text-slate-900" x-text="selectedNode.label || selectedNode.code"></h3>
+                                <p class="text-sm text-slate-500 mt-1" x-text="`ID: ${selectedNode.id || ''} · Code: ${selectedNode.code || ''}`"></p>
+                            </div>
+                            <div class="text-right">
+                                <p class="text-sm text-slate-500">掌握度</p>
+                                <p class="text-2xl font-bold" x-text="(selectedNode.mastery * 100).toFixed(1) + '%'"></p>
+                                <p class="text-xs text-slate-500" x-show="selectedNode.accuracy">准确率 <span x-text="(selectedNode.accuracy * 100).toFixed(1) + '%'"></span></p>
+                                <p class="text-xs text-slate-500" x-show="selectedNode.attempts">练习次数 <span x-text="selectedNode.attempts"></span></p>
+                            </div>
+                        </div>
+                        <div class="mt-4">
+                            <p class="text-sm font-semibold text-slate-700 mb-2">技能要点</p>
+                            <div class="flex flex-wrap gap-2">
+                                <template x-if="selectedNode.skills && selectedNode.skills.length">
+                                    <template x-for="skill in selectedNode.skills" :key="skill">
+                                        <span class="rounded-full bg-slate-100 px-3 py-1 text-xs font-medium text-slate-700" x-text="skill"></span>
+                                    </template>
+                                </template>
+                                <span x-show="!selectedNode.skills || !selectedNode.skills.length" class="text-sm text-slate-500">暂无技能要点</span>
+                            </div>
+                            <div class="mt-4">
+                                <a
+                                    class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-medium text-white hover:bg-indigo-700"
+                                    :href="selectedNode.code ? `/admin/knowledge-point-detail?kp_code=${encodeURIComponent(selectedNode.code)}` : '#'"
+                                    target="_blank"
+                                >
+                                    查看知识点详情
+                                </a>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm space-y-2">
+                        <p class="text-sm font-semibold text-slate-700">快速信息</p>
+                        <div class="text-sm text-slate-600 space-y-1">
+                            <p><span class="font-medium text-slate-900">ID:</span><span x-text="selectedNode.id"></span></p>
+                            <p><span class="font-medium text-slate-900">Code:</span><span x-text="selectedNode.code"></span></p>
+                            <p><span class="font-medium text-slate-900">推荐关注:</span><span x-text="selectedNode.recommended ? '是' : '否'"></span></p>
+                        </div>
+                    </div>
+                </div>
+            </template>
+        </div>
+
+        {{-- 知识点详情面板 --}}
+        <template x-if="selectedNode && selectedNode.code !== 'root'">
+            <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
+                {{-- 知识点标题 --}}
+                <div class="p-6 border-b border-gray-200 dark:border-gray-700">
+                    <div class="flex items-center justify-between">
+                        <div>
+                            <h3 class="text-xl font-bold text-gray-900 dark:text-gray-100" x-text="selectedNode.label || selectedNode.code"></h3>
+                            <div class="mt-1 flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
+                                <span x-text="selectedNode.code"></span>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+
+                {{-- 统计概览 --}}
+                <div class="p-6 border-b border-gray-200 dark:border-gray-700">
+                    <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
+                        <div class="text-center">
+                            <div class="text-2xl font-bold text-blue-600 dark:text-blue-400">-</div>
+                            <div class="text-sm text-gray-500 dark:text-gray-400 mt-1">总题目数</div>
+                        </div>
+                        <div class="text-center">
+                            <div class="text-2xl font-bold text-green-600 dark:text-green-400">-</div>
+                            <div class="text-sm text-gray-500 dark:text-gray-400 mt-1">直接题目</div>
+                        </div>
+                        <div class="text-center">
+                            <div class="text-2xl font-bold text-yellow-600 dark:text-yellow-400">-</div>
+                            <div class="text-sm text-gray-500 dark:text-gray-400 mt-1">子知识点题目</div>
+                        </div>
+                        <div class="text-center">
+                            <div class="text-2xl font-bold text-purple-600 dark:text-purple-400">-</div>
+                            <div class="text-sm text-gray-500 dark:text-gray-400 mt-1">技能点题目</div>
+                        </div>
+                    </div>
+                </div>
+
+                {{-- 标签页导航 --}}
+                <div class="px-6 border-b border-gray-200 dark:border-gray-700">
+                    <nav class="flex space-x-8" aria-label="Tabs">
+                        <span class="border-indigo-500 text-indigo-600 dark:text-indigo-400 py-4 px-1 border-b-2 font-medium text-sm">
+                            知识点详情
+                        </span>
+                    </nav>
+                </div>
+
+                {{-- 标签页内容 --}}
+                <div class="p-6">
+                    <div class="space-y-4">
+                        <div>
+                            <h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">技能要点</h4>
+                            <div class="flex flex-wrap gap-2">
+                                <template x-if="selectedNode.skills && selectedNode.skills.length">
+                                    <template x-for="skill in selectedNode.skills" :key="skill">
+                                        <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200" x-text="skill"></span>
+                                    </template>
+                                </template>
+                                <span x-show="!selectedNode.skills || !selectedNode.skills.length" class="text-sm text-gray-500 dark:text-gray-400">暂无技能要点</span>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </template>
+
+        {{-- 统计信息 --}}
+        <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
+            <div class="flex items-center justify-between">
+                <div class="flex items-center gap-2">
+                    <svg class="w-5 h-5 text-indigo-600 dark:text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+                    </svg>
+                    <span class="text-sm font-medium text-gray-700 dark:text-gray-300">使用说明</span>
+                </div>
+                <div class="flex gap-4 text-sm text-gray-600 dark:text-gray-400">
+                    <div class="flex items-center gap-1">
+                        <div class="w-3 h-3 rounded-full bg-blue-500"></div>
+                        <span>知识点</span>
+                    </div>
+                    <div class="flex items-center gap-1">
+                        <div class="w-3 h-3 rounded-full bg-green-500"></div>
+                        <span>已掌握</span>
+                    </div>
+                    <div class="flex items-center gap-1">
+                        <div class="w-3 h-3 rounded-full bg-yellow-500"></div>
+                        <span>需加强</span>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</x-filament-panels::page>
+
+@push('styles')
+<style>
+    .knowledge-mindmap-canvas {
+        background: #ffffff;
+        color: #0f172a;
+    }
+
+    .g6-grid {
+        opacity: 0.08;
+    }
+</style>
+<script src="https://unpkg.com/@antv/g6@4.8.24/dist/g6.min.js"></script>
+@endpush
+
+@push('scripts')
+{{-- 加载现有的知识图谱可视化类 --}}
+<script src="{{ asset('js/g6-custom-node.js') }}"></script>
+<script src="{{ asset('js/knowledge-mindmap-graph.js') }}"></script>
+
+<script>
+    document.addEventListener('livewire:initialized', () => {
+        // 监听错误事件
+        Livewire.on('error', (event) => {
+            console.error('Error:', event.message);
+        });
+
+        // 节点选择事件
+        Livewire.on('nodeSelected', (event) => {
+            console.log('Node selected:', event);
+        });
+    });
+</script>
+@endpush

+ 192 - 0
resources/views/filament/pages/integrations/knowledge-graph-integration.blade.php

@@ -0,0 +1,192 @@
+<x-filament-panels::page>
+    <div class="space-y-6">
+        {{-- 页面标题 --}}
+        <div class="flex items-center justify-between">
+            <div>
+                <h2 class="text-2xl font-bold tracking-tight text-gray-900 dark:text-gray-100">
+                    知识图谱整合视图
+                </h2>
+                <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
+                    以知识图谱为核心,整合知识点、题库和关联关系的完整视图
+                </p>
+            </div>
+
+            <div class="flex items-center gap-2">
+                <button
+                    wire:click="toggleSidebar"
+                    class="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:text-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700"
+                >
+                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
+                    </svg>
+                    {{ $showSidebar ? '隐藏' : '显示' }}侧边栏
+                </button>
+
+                <button
+                    wire:click="clearSelection"
+                    class="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:text-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700"
+                >
+                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
+                    </svg>
+                    清空选择
+                </button>
+            </div>
+        </div>
+
+        {{-- 标签页导航 --}}
+        <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-2">
+            <nav class="flex space-x-2" aria-label="Tabs">
+                @foreach($this->tabs as $tabId => $tab)
+                    <button
+                        wire:click="setActiveTab('{{ $tabId }}')"
+                        class="{{ $activeTab === $tabId ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900 dark:text-indigo-300' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-50 dark:text-gray-400 dark:hover:text-gray-300 dark:hover:bg-gray-700' }} group inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors"
+                    >
+                        @svg($tab['icon'], 'w-5 h-5')
+                        {{ $tab['label'] }}
+                    </button>
+                @endforeach
+            </nav>
+        </div>
+
+        {{-- 主要内容区域 --}}
+        <div class="grid gap-6 {{ $showSidebar ? 'lg:grid-cols-4' : 'lg:grid-cols-1' }}">
+            {{-- 主内容区 --}}
+            <div class="lg:col-span-{{ $showSidebar ? '3' : '1' }}">
+                <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
+                    @if($activeTab === 'graph')
+                        <div class="p-6">
+                            <livewire:integrations.knowledge-graph-component :selectedKpCode="$selectedKpCode" />
+                        </div>
+                    @elseif($activeTab === 'list')
+                        <div class="p-6">
+                            <livewire:integrations.knowledge-points-list-component />
+                        </div>
+                    @elseif($activeTab === 'stats')
+                        <div class="p-6">
+                            <livewire:integrations.knowledge-point-stats-component :selectedKpCode="$selectedKpCode" />
+                        </div>
+                    @elseif($activeTab === 'network')
+                        <div class="p-6">
+                            <livewire:integrations.knowledge-network-component :selectedKpCode="$selectedKpCode" />
+                        </div>
+                    @endif
+                </div>
+            </div>
+
+            {{-- 侧边栏 --}}
+            @if($showSidebar)
+                <div class="lg:col-span-1 space-y-4">
+                    {{-- 当前选中知识点 --}}
+                    <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
+                        <h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">当前选中</h3>
+                        @if($selectedKpCode)
+                            <div class="flex items-center justify-between p-3 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg">
+                                <div class="flex-1">
+                                    <p class="text-sm font-medium text-indigo-900 dark:text-indigo-300">{{ $selectedKpCode }}</p>
+                                </div>
+                                <button
+                                    wire:click="$set('selectedKpCode', null)"
+                                    class="text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-200"
+                                >
+                                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
+                                    </svg>
+                                </button>
+                            </div>
+                        @else
+                            <p class="text-sm text-gray-500 dark:text-gray-400">未选中知识点</p>
+                        @endif
+                    </div>
+
+                    {{-- 快速导航 --}}
+                    <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
+                        <h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">快速导航</h3>
+                        <nav class="space-y-1">
+                            <a
+                                href="{{ url('admin/knowledge-points') }}"
+                                class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-700 rounded-lg"
+                            >
+                                @svg('heroicon-o-map', 'w-4 h-4')
+                                知识点总览
+                            </a>
+                            <a
+                                href="{{ url('admin/knowledge-mindmap') }}"
+                                class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-700 rounded-lg"
+                            >
+                                @svg('heroicon-o-share', 'w-4 h-4')
+                                知识图谱脑图
+                            </a>
+                            <a
+                                href="{{ url('admin/knowledge-point-stats') }}"
+                                class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-700 rounded-lg"
+                            >
+                                @svg('heroicon-o-chart-bar', 'w-4 h-4')
+                                知识点统计
+                            </a>
+                            <a
+                                href="{{ url('admin/student-knowledge-graph-page') }}"
+                                class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-700 rounded-lg"
+                            >
+                                @svg('heroicon-o-user-group', 'w-4 h-4')
+                                学生知识图谱
+                            </a>
+                        </nav>
+                    </div>
+
+                    {{-- 统计摘要 --}}
+                    <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
+                        <h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">统计摘要</h3>
+                        <div class="space-y-2 text-sm">
+                            <div class="flex justify-between">
+                                <span class="text-gray-600 dark:text-gray-400">当前标签页:</span>
+                                <span class="font-medium text-gray-900 dark:text-gray-100">{{ $this->tabs[$activeTab]['label'] }}</span>
+                            </div>
+                            <div class="flex justify-between">
+                                <span class="text-gray-600 dark:text-gray-400">视图模式:</span>
+                                <span class="font-medium text-gray-900 dark:text-gray-100">{{ $showSidebar ? '侧边栏模式' : '全屏模式' }}</span>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            @endif
+        </div>
+
+        {{-- 底部说明 --}}
+        <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
+            <div class="flex items-start gap-3">
+                <svg class="w-5 h-5 text-indigo-600 dark:text-indigo-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+                </svg>
+                <div>
+                    <h4 class="text-sm font-medium text-gray-900 dark:text-gray-100">使用说明</h4>
+                    <div class="mt-2 text-sm text-gray-600 dark:text-gray-400">
+                        <p>• <strong>知识图谱</strong>:可视化展示知识点之间的关联关系</p>
+                        <p>• <strong>知识点列表</strong>:浏览和管理所有知识点,支持搜索和筛选</p>
+                        <p>• <strong>题库统计</strong>:查看每个知识点的题目数量分布</p>
+                        <p>• <strong>关联网络</strong>:展示知识点、题库和技能点之间的关联网络</p>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</x-filament-panels::page>
+
+@push('scripts')
+<script>
+    document.addEventListener('livewire:initialized', () => {
+        // 监听组件事件
+        Livewire.on('kpSelected', (event) => {
+            console.log('Knowledge point selected:', event);
+        });
+
+        Livewire.on('clearAllSelections', () => {
+            console.log('All selections cleared');
+        });
+
+        Livewire.on('tabChanged', (event) => {
+            console.log('Tab changed to:', event.tab);
+        });
+    });
+</script>
+@endpush

+ 320 - 0
resources/views/filament/pages/knowledge-point-stats.blade.php

@@ -0,0 +1,320 @@
+<x-filament-panels::page>
+    <div class="space-y-6">
+        {{-- 页面标题和筛选 --}}
+        <div class="flex items-center justify-between">
+            <div>
+                <h2 class="text-xl font-semibold tracking-tight">知识点题目数量统计</h2>
+                <p class="text-sm text-gray-500 mt-1">通过知识点筛选,汇总当前知识点和各个技能点的题目数量</p>
+            </div>
+        </div>
+
+        {{-- 知识点筛选器 --}}
+        <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
+            <div class="flex items-center gap-4">
+                <div class="flex-1">
+                    <label for="kp-select" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
+                        选择知识点
+                    </label>
+                    <select
+                        id="kp-select"
+                        wire:model.live="selectedKpCode"
+                        class="w-full rounded-lg border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
+                    >
+                        <option value="">-- 选择知识点 --</option>
+                        @foreach($this->knowledgePointOptions as $code => $name)
+                            <option value="{{ $code }}">{{ $name }} ({{ $code }})</option>
+                        @endforeach
+                    </select>
+                </div>
+                <div class="flex items-end">
+                    <button
+                        type="button"
+                        wire:click="toggleDetails"
+                        class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
+                    >
+                        {{ $showDetails ? '隐藏详情' : '显示详情' }}
+                    </button>
+                </div>
+            </div>
+        </div>
+
+        {{-- 统计概览 --}}
+        @if(!empty($this->knowledgePointStatistics))
+            <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
+                {{-- 总题目数 --}}
+                <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
+                    <div class="flex items-center justify-between">
+                        <div>
+                            <p class="text-sm font-medium text-gray-600 dark:text-gray-400">总题目数</p>
+                            <p class="text-3xl font-bold text-gray-900 dark:text-gray-100 mt-2">
+                                {{ array_sum(array_map([$this, 'getTotalQuestions'], $this->knowledgePointStatistics)) }}
+                            </p>
+                        </div>
+                        <div class="p-3 bg-blue-100 dark:bg-blue-900 rounded-full">
+                            <svg class="w-8 h-8 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
+                            </svg>
+                        </div>
+                    </div>
+                </div>
+
+                {{-- 直接题目数 --}}
+                <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
+                    <div class="flex items-center justify-between">
+                        <div>
+                            <p class="text-sm font-medium text-gray-600 dark:text-gray-400">直接题目数</p>
+                            <p class="text-3xl font-bold text-gray-900 dark:text-gray-100 mt-2">
+                                {{ array_sum(array_map([$this, 'getDirectQuestions'], $this->knowledgePointStatistics)) }}
+                            </p>
+                        </div>
+                        <div class="p-3 bg-green-100 dark:bg-green-900 rounded-full">
+                            <svg class="w-8 h-8 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
+                            </svg>
+                        </div>
+                    </div>
+                </div>
+
+                {{-- 子知识点题目数 --}}
+                <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
+                    <div class="flex items-center justify-between">
+                        <div>
+                            <p class="text-sm font-medium text-gray-600 dark:text-gray-400">子知识点题目数</p>
+                            <p class="text-3xl font-bold text-gray-900 dark:text-gray-100 mt-2">
+                                {{ array_sum(array_map([$this, 'getChildrenQuestions'], $this->knowledgePointStatistics)) }}
+                            </p>
+                        </div>
+                        <div class="p-3 bg-yellow-100 dark:bg-yellow-900 rounded-full">
+                            <svg class="w-8 h-8 text-yellow-600 dark:text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
+                            </svg>
+                        </div>
+                    </div>
+                </div>
+
+                {{-- 技能点题目数 --}}
+                <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
+                    <div class="flex items-center justify-between">
+                        <div>
+                            <p class="text-sm font-medium text-gray-600 dark:text-gray-400">技能点题目数</p>
+                            <p class="text-3xl font-bold text-gray-900 dark:text-gray-100 mt-2">
+                                {{ array_sum(array_map([$this, 'getSkillQuestions'], $this->knowledgePointStatistics)) }}
+                            </p>
+                        </div>
+                        <div class="p-3 bg-purple-100 dark:bg-purple-900 rounded-full">
+                            <svg class="w-8 h-8 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
+                            </svg>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        @endif
+
+        {{-- 知识点统计列表 --}}
+        <div class="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
+            <div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
+                <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
+                    {{ $selectedKpCode ? '知识点详情' : '所有知识点统计' }}
+                </h3>
+            </div>
+
+            <div class="overflow-x-auto">
+                <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
+                    <thead class="bg-gray-50 dark:bg-gray-900">
+                        <tr>
+                            <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                                知识点
+                            </th>
+                            <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                                总题目数
+                            </th>
+                            <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                                直接题目
+                            </th>
+                            <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                                子知识点题目
+                            </th>
+                            <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                                技能点数
+                            </th>
+                            <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                                技能点题目
+                            </th>
+                        </tr>
+                    </thead>
+                    <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
+                        @forelse($this->knowledgePointStatistics as $stat)
+                            <tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
+                                <td class="px-6 py-4 whitespace-nowrap">
+                                    <div>
+                                        <div class="text-sm font-medium text-gray-900 dark:text-gray-100">
+                                            {{ $stat['cn_name'] ?? $stat['kp_code'] }}
+                                        </div>
+                                        <div class="text-sm text-gray-500 dark:text-gray-400">
+                                            {{ $stat['kp_code'] }}
+                                        </div>
+                                    </div>
+                                </td>
+                                <td class="px-6 py-4 whitespace-nowrap">
+                                    <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
+                                        {{ $stat['total_questions'] ?? 0 }}
+                                    </span>
+                                </td>
+                                <td class="px-6 py-4 whitespace-nowrap">
+                                    <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
+                                        {{ $stat['direct_questions'] ?? 0 }}
+                                    </span>
+                                </td>
+                                <td class="px-6 py-4 whitespace-nowrap">
+                                    <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200">
+                                        {{ $stat['children_questions'] ?? 0 }}
+                                    </span>
+                                </td>
+                                <td class="px-6 py-4 whitespace-nowrap">
+                                    <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">
+                                        {{ $stat['skills_count'] ?? 0 }}
+                                    </span>
+                                </td>
+                                <td class="px-6 py-4 whitespace-nowrap">
+                                    <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200">
+                                        {{ $stat['skills_total_questions'] ?? 0 }}
+                                    </span>
+                                </td>
+                            </tr>
+
+                            @if($showDetails && !empty($stat['children']))
+                                @foreach($stat['children'] as $child)
+                                    <tr class="hover:bg-gray-50 dark:hover:bg-gray-700 bg-gray-50 dark:bg-gray-900">
+                                        <td class="px-6 py-4 whitespace-nowrap pl-12">
+                                            <div class="flex items-center">
+                                                <svg class="w-4 h-4 text-gray-400 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
+                                                </svg>
+                                                <div>
+                                                    <div class="text-sm font-medium text-gray-900 dark:text-gray-100">
+                                                        {{ $child['cn_name'] ?? $child['kp_code'] }}
+                                                    </div>
+                                                    <div class="text-sm text-gray-500 dark:text-gray-400">
+                                                        {{ $child['kp_code'] }}
+                                                    </div>
+                                                </div>
+                                            </div>
+                                        </td>
+                                        <td class="px-6 py-4 whitespace-nowrap">
+                                            <span class="text-sm font-medium text-blue-600 dark:text-blue-400">
+                                                {{ $child['total_questions'] ?? 0 }}
+                                            </span>
+                                        </td>
+                                        <td class="px-6 py-4 whitespace-nowrap">
+                                            <span class="text-sm font-medium text-green-600 dark:text-green-400">
+                                                {{ $child['direct_questions'] ?? 0 }}
+                                            </span>
+                                        </td>
+                                        <td class="px-6 py-4 whitespace-nowrap">
+                                            <span class="text-sm font-medium text-yellow-600 dark:text-yellow-400">
+                                                {{ $child['children_questions'] ?? 0 }}
+                                            </span>
+                                        </td>
+                                        <td class="px-6 py-4 whitespace-nowrap">
+                                            <span class="text-sm font-medium text-purple-600 dark:text-purple-400">
+                                                {{ $child['skills_count'] ?? 0 }}
+                                            </span>
+                                        </td>
+                                        <td class="px-6 py-4 whitespace-nowrap">
+                                            <span class="text-sm font-medium text-indigo-600 dark:text-indigo-400">
+                                                {{ $child['skills_total_questions'] ?? 0 }}
+                                            </span>
+                                        </td>
+                                    </tr>
+
+                                    @if($showDetails && !empty($child['skills']))
+                                        @foreach($child['skills'] as $skill)
+                                            <tr class="hover:bg-gray-50 dark:hover:bg-gray-700 bg-gray-100 dark:bg-gray-800">
+                                                <td class="px-6 py-4 whitespace-nowrap pl-16">
+                                                    <div class="flex items-center">
+                                                        <svg class="w-3 h-3 text-gray-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
+                                                            <circle cx="10" cy="10" r="3"></circle>
+                                                        </svg>
+                                                        <div>
+                                                            <div class="text-sm text-gray-900 dark:text-gray-100">
+                                                                {{ $skill['skill_code'] }}
+                                                            </div>
+                                                        </div>
+                                                    </div>
+                                                </td>
+                                                <td class="px-6 py-4 whitespace-nowrap">
+                                                    <span class="text-sm text-gray-600 dark:text-gray-400">
+                                                        {{ $skill['question_count'] ?? 0 }}
+                                                    </span>
+                                                </td>
+                                                <td class="px-6 py-4 whitespace-nowrap">
+                                                    <span class="text-sm text-gray-600 dark:text-gray-400">-</span>
+                                                </td>
+                                                <td class="px-6 py-4 whitespace-nowrap">
+                                                    <span class="text-sm text-gray-600 dark:text-gray-400">-</span>
+                                                </td>
+                                                <td class="px-6 py-4 whitespace-nowrap">
+                                                    <span class="text-sm text-gray-600 dark:text-gray-400">1</span>
+                                                </td>
+                                                <td class="px-6 py-4 whitespace-nowrap">
+                                                    <span class="text-sm text-gray-600 dark:text-gray-400">
+                                                        {{ $skill['question_count'] ?? 0 }}
+                                                    </span>
+                                                </td>
+                                            </tr>
+                                        @endforeach
+                                    @endif
+                                @endforeach
+                            @endif
+
+                            @if($showDetails && !empty($stat['skills']))
+                                @foreach($stat['skills'] as $skill)
+                                    <tr class="hover:bg-gray-50 dark:hover:bg-gray-700 bg-gray-100 dark:bg-gray-800">
+                                        <td class="px-6 py-4 whitespace-nowrap pl-12">
+                                            <div class="flex items-center">
+                                                <svg class="w-3 h-3 text-gray-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
+                                                    <circle cx="10" cy="10" r="3"></circle>
+                                                </svg>
+                                                <div>
+                                                    <div class="text-sm text-gray-900 dark:text-gray-100">
+                                                        {{ $skill['skill_code'] }}
+                                                    </div>
+                                                </div>
+                                            </div>
+                                        </td>
+                                        <td class="px-6 py-4 whitespace-nowrap">
+                                            <span class="text-sm text-gray-600 dark:text-gray-400">
+                                                {{ $skill['question_count'] ?? 0 }}
+                                            </span>
+                                        </td>
+                                        <td class="px-6 py-4 whitespace-nowrap">
+                                            <span class="text-sm text-gray-600 dark:text-gray-400">-</span>
+                                        </td>
+                                        <td class="px-6 py-4 whitespace-nowrap">
+                                            <span class="text-sm text-gray-600 dark:text-gray-400">-</span>
+                                        </td>
+                                        <td class="px-6 py-4 whitespace-nowrap">
+                                            <span class="text-sm text-gray-600 dark:text-gray-400">1</span>
+                                        </td>
+                                        <td class="px-6 py-4 whitespace-nowrap">
+                                            <span class="text-sm text-gray-600 dark:text-gray-400">
+                                                {{ $skill['question_count'] ?? 0 }}
+                                            </span>
+                                        </td>
+                                    </tr>
+                                @endforeach
+                            @endif
+                        @empty
+                            <tr>
+                                <td colspan="6" class="px-6 py-12 text-center text-gray-500 dark:text-gray-400">
+                                    暂无统计数据
+                                </td>
+                            </tr>
+                        @endforelse
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </div>
+</x-filament-panels::page>

+ 71 - 0
resources/views/filament/pages/question-generation.blade.php

@@ -124,12 +124,14 @@
             <div class="flex justify-end pt-4">
                 <button
                     type="button"
+                    onclick="handleGenerateWithRedirect()"
                     wire:click="executeGenerate"
                     wire:loading.attr="disabled"
                     wire:loading.class="bg-yellow-500 cursor-not-allowed opacity-90"
                     wire:loading.class.remove="bg-blue-600 hover:bg-blue-700"
                     wire:target="executeGenerate"
                     class="px-6 py-2 bg-blue-600 hover:bg-blue-700 rounded font-medium transition-all duration-200 flex items-center gap-2 text-white"
+                    id="generate-button"
                 >
                     @if($isGenerating)
                         <svg class="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
@@ -150,7 +152,76 @@
 </div>
 
 <script>
+// 全局跳转函数(备用方案)
+function redirectAfterGeneration(url, taskId) {
+    console.log('[QuestionGen-Global] 收到跳转请求:', url, taskId);
+
+    if (taskId && taskId !== 'unknown') {
+        alert(`✅ 任务已启动\n任务 ID: ${taskId}\n正在跳转到题库管理页面...`);
+    }
+
+    if (url) {
+        console.log('[QuestionGen-Global] 准备跳转到:', url);
+        setTimeout(() => {
+            console.log('[QuestionGen-Global] 执行页面跳转...');
+            window.location.href = url;
+        }, 500);
+    }
+}
+
+// 直接处理生成并跳转的函数
+function handleGenerateWithRedirect() {
+    console.log('[QuestionGen] 开始处理生成和跳转');
+
+    // 禁用按钮防止重复点击
+    const button = document.getElementById('generate-button');
+    if (button) {
+        button.disabled = true;
+        button.innerHTML = '<svg class="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><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></svg><span class="text-white font-semibold">生成中...</span>';
+    }
+
+    // 2秒后自动跳转(不管Livewire是否成功)
+    setTimeout(() => {
+        console.log('[QuestionGen] 2秒后自动跳转到题库管理页面');
+        alert('✅ 生成任务已启动\n正在跳转到题库管理页面查看...');
+        window.location.href = '/admin/question-management';
+    }, 2000);
+}
+
+// 将函数暴露到全局作用域
+window.redirectAfterGeneration = redirectAfterGeneration;
+
 document.addEventListener('livewire:init', () => {
+    console.log('[QuestionGen] Livewire已初始化,开始绑定事件监听器');
+
+    // ✅ 监听跳转事件,生成成功后立即跳转
+    Livewire.on('redirect-now', (event) => {
+        console.log('[QuestionGen] ✅ 收到JavaScript跳转事件:', event);
+        console.log('[QuestionGen] URL:', event.url, 'TaskID:', event.taskId);
+        console.log('[QuestionGen] 事件类型:', typeof event, '事件详情:', JSON.stringify(event));
+
+        if (event.url && event.taskId) {
+            console.log('[QuestionGen] ✅ 事件参数验证通过,准备跳转');
+            redirectAfterGeneration(event.url, event.taskId);
+        } else {
+            console.error('[QuestionGen] ❌ 事件参数不完整:', {
+                'url': event.url,
+                'taskId': event.taskId,
+                'event': event
+            });
+        }
+    });
+
+    console.log('[QuestionGen] 事件监听器绑定完成');
+
+    // 添加全局调试函数
+    window.testRedirect = function() {
+        console.log('[QuestionGen] 手动测试跳转');
+        redirectAfterGeneration('/admin/question-management', 'test-task-id');
+    };
+
+    console.log('[QuestionGen] 调试函数已设置,可使用 testRedirect() 手动测试跳转');
+
     // ✅ 捕获回调参数,直接检查状态 - 避免盲目轮询
     Livewire.on('start-async-task-monitoring', () => {
         console.log('[QuestionGen] 开始监控任务状态');

+ 211 - 0
resources/views/livewire/integrations/knowledge-graph-component.blade.php

@@ -0,0 +1,211 @@
+<div>
+    <div class="space-y-4">
+        {{-- 控制栏 --}}
+        <div class="flex items-center justify-between">
+            <div class="flex items-center gap-2">
+                <button
+                    wire:click="toggleStats"
+                    class="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:text-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700"
+                >
+                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <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>
+                    </svg>
+                    {{ $showStats ? '隐藏' : '显示' }}统计
+                </button>
+
+                <button
+                    wire:click="refreshGraph"
+                    class="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:text-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700"
+                >
+                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
+                    </svg>
+                    刷新
+                </button>
+            </div>
+
+            <div class="text-sm text-gray-500 dark:text-gray-400">
+                @if($selectedKpCode)
+                    当前选中: <span class="font-medium">{{ $selectedKpCode }}</span>
+                @else
+                    全图模式
+                @endif
+            </div>
+        </div>
+
+        {{-- 图谱可视化区域 --}}
+        <div class="relative bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700" style="height: 600px;">
+            @if($isLoading)
+                <div class="absolute inset-0 flex items-center justify-center bg-white/80 dark:bg-gray-800/80 z-10">
+                    <div class="flex items-center gap-3">
+                        <svg class="animate-spin h-6 w-6 text-indigo-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+                            <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
+                            <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>
+                        </svg>
+                        <span class="text-gray-600 dark:text-gray-300">加载知识图谱...</span>
+                    </div>
+                </div>
+            @endif
+
+            @if(!empty($graphData['nodes']))
+                {{-- 这里将嵌入图谱可视化库(如 D3.js、vis.js 等) --}}
+                <div id="knowledge-graph-viz" class="w-full h-full"></div>
+
+                {{-- 图例 --}}
+                <div class="absolute top-4 right-4 bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 p-4 shadow-lg">
+                    <h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">图例</h4>
+                    <div class="space-y-2 text-xs">
+                        <div class="flex items-center gap-2">
+                            <div class="w-3 h-3 rounded-full bg-blue-500"></div>
+                            <span class="text-gray-600 dark:text-gray-400">知识点</span>
+                        </div>
+                        <div class="flex items-center gap-2">
+                            <div class="w-3 h-3 rounded bg-green-500"></div>
+                            <span class="text-gray-600 dark:text-gray-400">题库</span>
+                        </div>
+                        <div class="flex items-center gap-2">
+                            <div class="w-3 h-3 rounded-full bg-yellow-500"></div>
+                            <span class="text-gray-600 dark:text-gray-400">技能点</span>
+                        </div>
+                    </div>
+                </div>
+            @else
+                <div class="absolute inset-0 flex items-center justify-center">
+                    <div class="text-center text-gray-500 dark:text-gray-400">
+                        <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-1.447-.894L15 4m0 13V4m0 0L9 7"></path>
+                        </svg>
+                        <p class="mt-2">暂无知识图谱数据</p>
+                    </div>
+                </div>
+            @endif
+        </div>
+
+        {{-- 统计信息 --}}
+        @if($showStats && !empty($graphData['nodes']))
+            <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
+                <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">图谱统计</h3>
+                <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
+                    <div class="text-center">
+                        <div class="text-2xl font-bold text-indigo-600 dark:text-indigo-400">{{ count($graphData['nodes']) }}</div>
+                        <div class="text-sm text-gray-500 dark:text-gray-400">知识点</div>
+                    </div>
+                    <div class="text-center">
+                        <div class="text-2xl font-bold text-green-600 dark:text-green-400">{{ count($graphData['edges']) }}</div>
+                        <div class="text-sm text-gray-500 dark:text-gray-400">关联关系</div>
+                    </div>
+                    <div class="text-center">
+                        @php
+                            $totalQuestions = 0;
+                            foreach($graphData['nodes'] as $node) {
+                                $totalQuestions += $node['question_count'] ?? 0;
+                            }
+                        @endphp
+                        <div class="text-2xl font-bold text-blue-600 dark:text-blue-400">{{ $totalQuestions }}</div>
+                        <div class="text-sm text-gray-500 dark:text-gray-400">关联题目</div>
+                    </div>
+                    <div class="text-center">
+                        @php
+                            $totalSkills = 0;
+                            foreach($graphData['nodes'] as $node) {
+                                $totalSkills += $node['skills_count'] ?? 0;
+                            }
+                        @endphp
+                        <div class="text-2xl font-bold text-yellow-600 dark:text-yellow-400">{{ $totalSkills }}</div>
+                        <div class="text-sm text-gray-500 dark:text-gray-400">技能点</div>
+                    </div>
+                </div>
+            </div>
+        @endif
+
+        {{-- 节点详情 --}}
+        @if(!empty($selectedNodeData))
+            <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
+                <div class="flex items-center justify-between mb-4">
+                    <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">节点详情</h3>
+                    <button
+                        wire:click="$set('selectedNodeData', null)"
+                        class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
+                    >
+                        <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
+                        </svg>
+                    </button>
+                </div>
+
+                <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
+                    <div>
+                        <h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">基本信息</h4>
+                        <dl class="space-y-2 text-sm">
+                            <div class="flex">
+                                <dt class="w-24 text-gray-500 dark:text-gray-400">名称:</dt>
+                                <dd class="text-gray-900 dark:text-gray-100">{{ $selectedNodeData['cn_name'] ?? $selectedNodeData['kp_code'] }}</dd>
+                            </div>
+                            <div class="flex">
+                                <dt class="w-24 text-gray-500 dark:text-gray-400">代码:</dt>
+                                <dd class="text-gray-900 dark:text-gray-100">{{ $selectedNodeData['kp_code'] }}</dd>
+                            </div>
+                            <div class="flex">
+                                <dt class="w-24 text-gray-500 dark:text-gray-400">学段:</dt>
+                                <dd class="text-gray-900 dark:text-gray-100">{{ $selectedNodeData['phase'] ?? '-' }}</dd>
+                            </div>
+                            <div class="flex">
+                                <dt class="w-24 text-gray-500 dark:text-gray-400">类别:</dt>
+                                <dd class="text-gray-900 dark:text-gray-100">{{ $selectedNodeData['category'] ?? '-' }}</dd>
+                            </div>
+                        </dl>
+                    </div>
+
+                    <div>
+                        <h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">统计信息</h4>
+                        <dl class="space-y-2 text-sm">
+                            <div class="flex">
+                                <dt class="w-24 text-gray-500 dark:text-gray-400">题目数:</dt>
+                                <dd class="text-gray-900 dark:text-gray-100">{{ $selectedNodeData['question_count'] ?? 0 }}</dd>
+                            </div>
+                            <div class="flex">
+                                <dt class="w-24 text-gray-500 dark:text-gray-400">技能数:</dt>
+                                <dd class="text-gray-900 dark:text-gray-100">{{ $selectedNodeData['skills_count'] ?? 0 }}</dd>
+                            </div>
+                            <div class="flex">
+                                <dt class="w-24 text-gray-500 dark:text-gray-400">重要度:</dt>
+                                <dd class="text-gray-900 dark:text-gray-100">{{ $selectedNodeData['importance'] ?? 0 }}</dd>
+                            </div>
+                        </dl>
+                    </div>
+                </div>
+
+                @if(!empty($selectedNodeData['skills']))
+                    <div class="mt-4">
+                        <h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">技能列表</h4>
+                        <div class="flex flex-wrap gap-2">
+                            @foreach($selectedNodeData['skills'] as $skill)
+                                <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200">
+                                    {{ $skill['skill_name'] ?? $skill['skill_code'] ?? 'Unknown' }}
+                                </span>
+                            @endforeach
+                        </div>
+                    </div>
+                @endif
+            </div>
+        @endif
+    </div>
+</div>
+
+@push('scripts')
+<script>
+    // 这里将添加图谱可视化逻辑
+    // 使用 vis.js 或 D3.js 实现交互式图谱
+    document.addEventListener('livewire:initialized', () => {
+        // 监听数据变化并更新可视化
+        Livewire.on('graphDataUpdated', (data) => {
+            updateVisualization(data);
+        });
+    });
+
+    function updateVisualization(data) {
+        // 实现图谱更新逻辑
+        console.log('Updating graph visualization:', data);
+    }
+</script>
+@endpush

+ 409 - 0
resources/views/livewire/integrations/knowledge-graph-visualization.blade.php

@@ -0,0 +1,409 @@
+<div class="space-y-4">
+    {{-- 控制栏 --}}
+    <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
+        <div class="flex items-center justify-between">
+            <div class="flex items-center gap-4">
+                {{-- 布局切换 --}}
+                <div class="flex items-center gap-2">
+                    <span class="text-sm font-medium text-gray-700 dark:text-gray-300">布局:</span>
+                    <div class="inline-flex rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
+                        <button
+                            wire:click="setLayoutType('full')"
+                            class="px-3 py-1 text-sm {{ $layoutType === 'full' ? 'bg-indigo-600 text-white' : 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700' }}"
+                        >
+                            全图模式
+                        </button>
+                        <button
+                            wire:click="setLayoutType('selected')"
+                            class="px-3 py-1 text-sm {{ $layoutType === 'selected' ? 'bg-indigo-600 text-white' : 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700' }}"
+                        >
+                            选中模式
+                        </button>
+                    </div>
+                </div>
+
+                {{-- 筛选器 --}}
+                <div class="flex items-center gap-2">
+                    <select
+                        wire:model.live="filterPhase"
+                        class="text-sm border-gray-300 dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-gray-100"
+                    >
+                        <option value="">所有学段</option>
+                        @foreach($this->filterOptions['phases'] as $phase)
+                            <option value="{{ $phase }}">{{ $phase }}</option>
+                        @endforeach
+                    </select>
+
+                    <select
+                        wire:model.live="filterCategory"
+                        class="text-sm border-gray-300 dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-gray-100"
+                    >
+                        <option value="">所有类别</option>
+                        @foreach($this->filterOptions['categories'] as $category)
+                            <option value="{{ $category }}">{{ $category }}</option>
+                        @endforeach
+                    </select>
+                </div>
+            </div>
+
+            <div class="flex items-center gap-2">
+                @if($selectedKpCode)
+                    <div class="flex items-center gap-2 px-3 py-1 bg-indigo-100 dark:bg-indigo-900 rounded-lg">
+                        <span class="text-sm font-medium text-indigo-900 dark:text-indigo-300">
+                            已选择: {{ $selectedKpCode }}
+                        </span>
+                        <button
+                            wire:click="clearSelection"
+                            class="text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-200"
+                        >
+                            <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
+                            </svg>
+                        </button>
+                    </div>
+                @else
+                    <span class="text-sm text-gray-500 dark:text-gray-400">点击节点查看详情</span>
+                @endif
+
+                <button
+                    wire:click="loadGraphData"
+                    class="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:text-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700"
+                >
+                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
+                    </svg>
+                    刷新
+                </button>
+            </div>
+        </div>
+    </div>
+
+    {{-- 图谱可视化区域 --}}
+    <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden" style="height: 500px;" x-data="knowledgeGraph()">
+        @if($isLoading)
+            <div class="absolute inset-0 flex items-center justify-center bg-white/80 dark:bg-gray-800/80 z-10">
+                <div class="flex items-center gap-3">
+                    <svg class="animate-spin h-6 w-6 text-indigo-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+                        <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
+                        <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>
+                    </svg>
+                    <span class="text-gray-600 dark:text-gray-300">加载知识图谱...</span>
+                </div>
+            </div>
+        @endif
+
+        <div id="knowledge-graph-viz" class="w-full h-full"></div>
+
+        {{-- 图例 --}}
+        <div class="absolute top-4 right-4 bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 p-4 shadow-lg">
+            <h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">图例</h4>
+            <div class="space-y-2 text-xs">
+                <div class="flex items-center gap-2">
+                    <div class="w-3 h-3 rounded-full bg-blue-500"></div>
+                    <span class="text-gray-600 dark:text-gray-400">知识点</span>
+                </div>
+                <div class="flex items-center gap-2">
+                    <div class="w-3 h-3 rounded-full bg-green-500"></div>
+                    <span class="text-gray-600 dark:text-gray-400">已掌握</span>
+                </div>
+                <div class="flex items-center gap-2">
+                    <div class="w-3 h-3 rounded-full bg-yellow-500"></div>
+                    <span class="text-gray-600 dark:text-gray-400">需加强</span>
+                </div>
+            </div>
+        </div>
+
+        {{-- 统计信息 --}}
+        <div class="absolute bottom-4 left-4 bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 p-4 shadow-lg">
+            <div class="text-xs text-gray-600 dark:text-gray-400 space-y-1">
+                <div>节点: <span x-text="stats.nodes || 0">0</span></div>
+                <div>边: <span x-text="stats.edges || 0">0</span></div>
+            </div>
+        </div>
+    </div>
+
+    {{-- 知识点详情面板 --}}
+    <livewire:integrations.knowledge-point-details />
+
+</div>
+
+@push('styles')
+<style>
+    /* G6 节点样式覆盖 */
+    .g6-tooltip {
+        border-radius: 8px !important;
+        background: rgba(15, 23, 42, 0.95) !important;
+        color: #e2e8f0 !important;
+        padding: 10px 12px !important;
+        font-size: 12px !important;
+        box-shadow: 0 10px 30px rgba(0,0,0,0.18) !important;
+    }
+</style>
+@endpush
+
+@push('scripts')
+<script>
+    // 全局变量存储图谱实例
+    window.knowledgeGraphInstance = null;
+
+    // Alpine 组件定义
+    function knowledgeGraph() {
+        return {
+            graph: null,
+            data: @json($graphData),
+            stats: {
+                nodes: {{ count($graphData['nodes'] ?? []) }},
+                edges: {{ count($graphData['edges'] ?? []) }}
+            },
+            selectedNode: '{{ $selectedKpCode ?? '' }}',
+
+            init() {
+                console.log('初始化知识图谱...', this.data);
+                this.initGraph();
+                this.bindEvents();
+            },
+
+            initGraph() {
+                if (typeof window.KnowledgeMindmapGraph === 'undefined') {
+                    console.error('KnowledgeMindmapGraph 类未加载');
+                    return;
+                }
+
+                const container = document.getElementById('knowledge-graph-viz');
+                if (!container) {
+                    console.error('找不到容器元素 #knowledge-graph-viz');
+                    return;
+                }
+
+                // 转换 API 数据为 tree.json 格式
+                const treeData = this.transformApiDataToTree(this.data);
+
+                // 实例化现有的 KnowledgeMindmapGraph
+                this.graphInstance = new window.KnowledgeMindmapGraph({
+                    containerId: 'knowledge-graph-viz',
+                    livewireMethod: 'handleNodeSelected',
+                    livewireId: @this.id,
+                    emitSelection: true,
+                    showEdges: true,
+                    showRelationEdges: true,
+                    highlightLowMastery: false,
+                });
+
+                // 手动设置数据(绕过文件加载)
+                this.graphInstance.rawTree = treeData;
+                this.graphInstance.relationEdges = this.transformEdgesForKnowledgeMindmap(this.data.edges || []);
+                this.graphInstance.treeData = this.graphInstance.transformNode(this.graphInstance.rawTree);
+                this.graphInstance.buildParentMap(this.graphInstance.treeData);
+
+                // 构建节点ID集合
+                const flatIds = [];
+                this.graphInstance.collectIds(this.graphInstance.treeData, flatIds);
+                this.graphInstance.nodeIdSet = new Set(flatIds);
+
+                // 渲染图谱
+                this.graphInstance.renderGraph();
+
+                // 暴露到全局
+                window.knowledgeGraphInstance = this.graphInstance;
+                window.knowledgeGraphG6Graph = this.graphInstance.graph;
+            },
+
+            transformDataForG6(apiData) {
+                const nodes = apiData.nodes || [];
+                const edges = apiData.edges || [];
+
+                // 构建树形结构
+                const nodeMap = {};
+                nodes.forEach(node => {
+                    nodeMap[node.kp_code] = {
+                        id: node.kp_code,
+                        label: `${node.kp_code} · ${node.cn_name || node.kp_code}`,
+                        children: [],
+                        meta: {
+                            code: node.kp_code,
+                            name: node.cn_name || node.kp_code,
+                            phase: node.phase || '',
+                            category: node.category || '',
+                            importance: node.importance || 0,
+                            description: node.description || '',
+                            question_count: node.question_count || 0,
+                            has_mastery: node.question_count > 0,
+                        }
+                    };
+                });
+
+                // 构建父子关系
+                const rootNodes = [];
+                edges.forEach(edge => {
+                    const source = nodeMap[edge.from];
+                    const target = nodeMap[edge.to];
+
+                    if (source && target) {
+                        // 将 target 添加为 source 的子节点
+                        source.children.push(target);
+                    }
+                });
+
+                // 找出根节点(没有父节点的节点)
+                const childCodes = new Set();
+                edges.forEach(edge => {
+                    childCodes.add(edge.to);
+                });
+
+                nodes.forEach(node => {
+                    if (!childCodes.has(node.kp_code)) {
+                        rootNodes.push(nodeMap[node.kp_code]);
+                    }
+                });
+
+                // 如果只有一个根节点,直接返回;否则包装成虚拟根节点
+                if (rootNodes.length === 1) {
+                    return rootNodes[0];
+                } else if (rootNodes.length > 1) {
+                    return {
+                        id: 'root',
+                        label: '知识点根节点',
+                        children: rootNodes,
+                        meta: { isVirtualRoot: true }
+                    };
+                } else {
+                    // 如果没有根节点,返回第一个节点
+                    return nodes.length > 0 ? nodeMap[nodes[0].kp_code] : null;
+                }
+            },
+
+            // 转换 API 数据为 KnowledgeMindmapGraph 期望的 tree.json 格式
+            transformApiDataToTree(apiData) {
+                const nodes = apiData.nodes || [];
+                const edges = apiData.edges || [];
+
+                // 构建节点映射
+                const nodeMap = {};
+                nodes.forEach(node => {
+                    nodeMap[node.kp_code] = {
+                        id: node.kp_code,
+                        name: node.cn_name || node.kp_code,
+                        label: node.cn_name || node.kp_code,
+                        code: node.kp_code,
+                        children: [],
+                    };
+                });
+
+                // 构建父子关系(使用 children 属性)
+                edges.forEach(edge => {
+                    const source = nodeMap[edge.from];
+                    const target = nodeMap[edge.to];
+
+                    if (source && target) {
+                        source.children.push(target);
+                    }
+                });
+
+                // 找出根节点(没有父节点的节点)
+                const childCodes = new Set();
+                edges.forEach(edge => {
+                    childCodes.add(edge.to);
+                });
+
+                const rootNodes = nodes
+                    .filter(node => !childCodes.has(node.kp_code))
+                    .map(node => nodeMap[node.kp_code]);
+
+                // 构建最终的树形结构
+                if (rootNodes.length === 1) {
+                    return rootNodes[0];
+                } else if (rootNodes.length > 1) {
+                    return {
+                        id: 'root',
+                        name: '知识点根节点',
+                        label: '知识点根节点',
+                        code: 'root',
+                        children: rootNodes,
+                    };
+                } else {
+                    // 如果没有根节点,返回第一个节点
+                    return nodes.length > 0 ? nodeMap[nodes[0].kp_code] : null;
+                }
+            },
+
+            // 转换边数据为 KnowledgeMindmapGraph 期望的格式
+            transformEdgesForKnowledgeMindmap(edges) {
+                return edges.map((edge, index) => ({
+                    id: `rel-${index}`,
+                    source: edge.from,
+                    target: edge.to,
+                    type: edge.type || 'successor',
+                    edgeType: edge.type || 'successor',
+                    comment: edge.comment || '',
+                }));
+            },
+
+            bindEvents() {
+                // KnowledgeMindmapGraph 已经在构造函数中绑定了事件
+                // 这里不需要额外绑定,因为节点点击事件会通过 notifySelection 传递到 Livewire
+
+                // 监听自定义事件(从 KnowledgeMindmapGraph 发出)
+                window.addEventListener('mindmap-node-selected', (evt) => {
+                    const model = evt.detail;
+                    console.log('节点选中事件:', model);
+                });
+            },
+
+            // 更新图谱数据
+            updateData(newData) {
+                this.data = newData;
+                this.stats = {
+                    nodes: (newData.nodes || []).length,
+                    edges: (newData.edges || []).length
+                };
+
+                if (this.graphInstance && this.graphInstance.graph) {
+                    // 转换新数据
+                    const treeData = this.transformApiDataToTree(newData);
+                    const edgesData = this.transformEdgesForKnowledgeMindmap(newData.edges || []);
+
+                    // 更新数据
+                    this.graphInstance.rawTree = treeData;
+                    this.graphInstance.relationEdges = edgesData;
+
+                    // 刷新图谱
+                    this.graphInstance.refreshGraph();
+                }
+            }
+        }
+    }
+
+    // Livewire 事件监听
+    document.addEventListener('livewire:initialized', () => {
+        // 监听图谱数据更新事件
+        Livewire.on('graphDataUpdated', (data) => {
+            console.log('收到图谱数据更新:', data);
+            const graphElement = document.querySelector('#knowledge-graph-viz');
+            if (graphElement && graphElement._x_dataStack) {
+                const graphComponent = graphElement._x_dataStack[0];
+                if (graphComponent && graphComponent.updateData) {
+                    graphComponent.updateData(data);
+                }
+            }
+        });
+
+        // 节点选择事件
+        Livewire.on('nodeSelected', (event) => {
+            console.log('节点被选中:', event);
+            // 通知详情面板
+            Livewire.dispatch('kpSelected', { kpCode: event });
+        });
+
+        // 监听 Livewire 的数据更新
+        Livewire.on('refreshGraph', () => {
+            console.log('刷新图谱');
+            @this.call('loadGraphData');
+        });
+    });
+
+    // 页面加载完成后初始化
+    document.addEventListener('DOMContentLoaded', () => {
+        console.log('DOM 加载完成');
+    });
+</script>
+@endpush

+ 197 - 0
resources/views/livewire/integrations/knowledge-network-component.blade.php

@@ -0,0 +1,197 @@
+<div>
+    <div class="space-y-4">
+        {{-- 控制栏 --}}
+        <div class="flex items-center justify-between bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
+            <div class="flex items-center gap-2">
+                <button
+                    wire:click="refreshNetwork"
+                    class="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:text-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700"
+                >
+                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
+                    </svg>
+                    刷新网络
+                </button>
+            </div>
+
+            <div class="text-sm text-gray-500 dark:text-gray-400">
+                @if($selectedKpCode)
+                    当前选中: <span class="font-medium">{{ $selectedKpCode }}</span>
+                @else
+                    全网模式
+                @endif
+            </div>
+        </div>
+
+        {{-- 网络可视化区域 --}}
+        <div class="relative bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700" style="height: 600px;">
+            @if($isLoading)
+                <div class="absolute inset-0 flex items-center justify-center bg-white/80 dark:bg-gray-800/80 z-10">
+                    <div class="flex items-center gap-3">
+                        <svg class="animate-spin h-6 w-6 text-indigo-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+                            <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
+                            <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>
+                        </svg>
+                        <span class="text-gray-600 dark:text-gray-300">加载知识网络...</span>
+                    </div>
+                </div>
+            @endif
+
+            @if(!empty($networkData['nodes']))
+                {{-- 这里将嵌入网络可视化库(如 vis.js、D3.js 等) --}}
+                <div id="knowledge-network-viz" class="w-full h-full"></div>
+
+                {{-- 网络图例 --}}
+                <div class="absolute top-4 right-4 bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 p-4 shadow-lg">
+                    <h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">网络图例</h4>
+                    <div class="space-y-2 text-xs">
+                        @foreach($networkData['groups'] ?? [] as $groupId => $group)
+                            <div class="flex items-center gap-2">
+                                <div class="w-3 h-3 rounded-full" style="background-color: {{ $group['color'] ?? '#999' }}"></div>
+                                <span class="text-gray-600 dark:text-gray-400">{{ $group['name'] }}</span>
+                            </div>
+                        @endforeach
+                    </div>
+                </div>
+            @else
+                <div class="absolute inset-0 flex items-center justify-center">
+                    <div class="text-center text-gray-500 dark:text-gray-400">
+                        <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
+                        </svg>
+                        <p class="mt-2">暂无网络数据</p>
+                    </div>
+                </div>
+            @endif
+        </div>
+
+        {{-- 网络统计 --}}
+        @if(!empty($networkData['nodes']))
+            <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
+                <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">网络统计</h3>
+                <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
+                    <div class="text-center">
+                        <div class="text-2xl font-bold text-blue-600 dark:text-blue-400">
+                            {{ count(array_filter($networkData['nodes'], fn($node) => $node['type'] === 'knowledge_point')) }}
+                        </div>
+                        <div class="text-sm text-gray-500 dark:text-gray-400">知识点</div>
+                    </div>
+                    <div class="text-center">
+                        <div class="text-2xl font-bold text-green-600 dark:text-green-400">
+                            {{ count(array_filter($networkData['nodes'], fn($node) => $node['type'] === 'skill')) }}
+                        </div>
+                        <div class="text-sm text-gray-500 dark:text-gray-400">技能点</div>
+                    </div>
+                    <div class="text-center">
+                        <div class="text-2xl font-bold text-purple-600 dark:text-purple-400">
+                            {{ count($networkData['links']) }}
+                        </div>
+                        <div class="text-sm text-gray-500 dark:text-gray-400">关联边</div>
+                    </div>
+                    <div class="text-center">
+                        <div class="text-2xl font-bold text-indigo-600 dark:text-indigo-400">
+                            @php
+                                $totalQuestions = 0;
+                                foreach($networkData['nodes'] as $node) {
+                                    $totalQuestions += $node['question_count'] ?? 0;
+                                }
+                            @endphp
+                            {{ $totalQuestions }}
+                        </div>
+                        <div class="text-sm text-gray-500 dark:text-gray-400">关联题目</div>
+                    </div>
+                </div>
+            </div>
+        @endif
+
+        {{-- 节点详情 --}}
+        @if(!empty($selectedNode))
+            <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
+                <div class="flex items-center justify-between mb-4">
+                    <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">节点详情</h3>
+                    <button
+                        wire:click="$set('selectedNode', null)"
+                        class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
+                    >
+                        <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
+                        </svg>
+                    </button>
+                </div>
+
+                <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
+                    <div>
+                        <h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">基本信息</h4>
+                        <dl class="space-y-2 text-sm">
+                            <div class="flex">
+                                <dt class="w-24 text-gray-500 dark:text-gray-400">名称:</dt>
+                                <dd class="text-gray-900 dark:text-gray-100">{{ $selectedNode['name'] }}</dd>
+                            </div>
+                            <div class="flex">
+                                <dt class="w-24 text-gray-500 dark:text-gray-400">类型:</dt>
+                                <dd class="text-gray-900 dark:text-gray-100">{{ $selectedNode['type'] }}</dd>
+                            </div>
+                            <div class="flex">
+                                <dt class="w-24 text-gray-500 dark:text-gray-400">分组:</dt>
+                                <dd class="text-gray-900 dark:text-gray-100">{{ $selectedNode['group'] }}</dd>
+                            </div>
+                        </dl>
+                    </div>
+
+                    <div>
+                        <h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">关联信息</h4>
+                        <dl class="space-y-2 text-sm">
+                            <div class="flex">
+                                <dt class="w-24 text-gray-500 dark:text-gray-400">题目数:</dt>
+                                <dd class="text-gray-900 dark:text-gray-100">{{ $selectedNode['question_count'] ?? 0 }}</dd>
+                            </div>
+                        </dl>
+                    </div>
+                </div>
+            </div>
+        @endif
+
+        {{-- 关联列表 --}}
+        @if(!empty($networkData['links']))
+            <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
+                <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">关联列表</h3>
+                <div class="space-y-2 max-h-96 overflow-y-auto">
+                    @foreach(array_slice($networkData['links'], 0, 20) as $link)
+                        <div class="flex items-center justify-between py-2 px-3 rounded-lg bg-gray-50 dark:bg-gray-700">
+                            <div class="flex items-center gap-2">
+                                <span class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ $link['source'] }}</span>
+                                <svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
+                                </svg>
+                                <span class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ $link['target'] }}</span>
+                            </div>
+                            <span class="text-xs text-gray-500 dark:text-gray-400">{{ $link['type'] }}</span>
+                        </div>
+                    @endforeach
+
+                    @if(count($networkData['links']) > 20)
+                        <div class="text-center text-sm text-gray-500 dark:text-gray-400 pt-2">
+                            还有 {{ count($networkData['links']) - 20 }} 个关联...
+                        </div>
+                    @endif
+                </div>
+            </div>
+        @endif
+    </div>
+</div>
+
+@push('scripts')
+<script>
+    // 这里将添加网络可视化逻辑
+    document.addEventListener('livewire:initialized', () => {
+        Livewire.on('networkDataUpdated', (data) => {
+            updateNetworkVisualization(data);
+        });
+    });
+
+    function updateNetworkVisualization(data) {
+        // 实现网络图更新逻辑
+        console.log('Updating network visualization:', data);
+    }
+</script>
+@endpush

+ 218 - 0
resources/views/livewire/integrations/knowledge-point-details.blade.php

@@ -0,0 +1,218 @@
+<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
+    @if(!$nodeDetails)
+        {{-- 空状态 --}}
+        <div class="p-8 text-center text-gray-500 dark:text-gray-400">
+            <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
+                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
+            </svg>
+            <p class="mt-2">点击知识图谱中的节点查看详细信息</p>
+        </div>
+    @else
+        {{-- 知识点标题 --}}
+        <div class="p-6 border-b border-gray-200 dark:border-gray-700">
+            <div class="flex items-center justify-between">
+                <div>
+                    <h3 class="text-xl font-bold text-gray-900 dark:text-gray-100">
+                        {{ $nodeDetails['cn_name'] ?? $nodeDetails['kp_code'] ?? $nodeDetails['code'] ?? '未知知识点' }}
+                    </h3>
+                    <div class="mt-1 flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
+                        <span>{{ $nodeDetails['kp_code'] ?? $nodeDetails['code'] ?? $selectedNode }}</span>
+                        @if($nodeDetails['phase'])
+                            <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
+                                {{ $nodeDetails['phase'] }}
+                            </span>
+                        @endif
+                        @if($nodeDetails['category'])
+                            <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
+                                {{ $nodeDetails['category'] }}
+                            </span>
+                        @endif
+                    </div>
+                </div>
+                <div class="text-right">
+                    <div class="text-sm text-gray-500 dark:text-gray-400">重要度</div>
+                    <div class="text-2xl font-bold text-indigo-600 dark:text-indigo-400">
+                        {{ $nodeDetails['importance'] ?? 0 }}
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        {{-- 统计概览 --}}
+        <div class="p-6 border-b border-gray-200 dark:border-gray-700">
+            <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
+                <div class="text-center">
+                    <div class="text-2xl font-bold text-blue-600 dark:text-blue-400">{{ $this->totalQuestions }}</div>
+                    <div class="text-sm text-gray-500 dark:text-gray-400 mt-1">总题目数</div>
+                </div>
+                <div class="text-center">
+                    <div class="text-2xl font-bold text-green-600 dark:text-green-400">{{ $this->directQuestions }}</div>
+                    <div class="text-sm text-gray-500 dark:text-gray-400 mt-1">直接题目</div>
+                </div>
+                <div class="text-center">
+                    <div class="text-2xl font-bold text-yellow-600 dark:text-yellow-400">{{ $this->childrenQuestions }}</div>
+                    <div class="text-sm text-gray-500 dark:text-gray-400 mt-1">子知识点题目</div>
+                </div>
+                <div class="text-center">
+                    <div class="text-2xl font-bold text-purple-600 dark:text-purple-400">{{ $this->skillsQuestions }}</div>
+                    <div class="text-sm text-gray-500 dark:text-gray-400 mt-1">技能点题目</div>
+                </div>
+            </div>
+        </div>
+
+        {{-- 标签页导航 --}}
+        <div class="px-6 border-b border-gray-200 dark:border-gray-700">
+            <nav class="flex space-x-8" aria-label="Tabs">
+                <button
+                    wire:click="setActiveTab('overview')"
+                    class="{{ $activeTab === 'overview' ? 'border-indigo-500 text-indigo-600 dark:text-indigo-400' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300' }} py-4 px-1 border-b-2 font-medium text-sm"
+                >
+                    知识点介绍
+                </button>
+                <button
+                    wire:click="setActiveTab('children')"
+                    class="{{ $activeTab === 'children' ? 'border-indigo-500 text-indigo-600 dark:text-indigo-400' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300' }} py-4 px-1 border-b-2 font-medium text-sm"
+                >
+                    一级子知识点
+                    @if(count($this->childrenNodes) > 0)
+                        <span class="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200">
+                            {{ count($this->childrenNodes) }}
+                        </span>
+                    @endif
+                </button>
+                <button
+                    wire:click="setActiveTab('skills')"
+                    class="{{ $activeTab === 'skills' ? 'border-indigo-500 text-indigo-600 dark:text-indigo-400' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300' }} py-4 px-1 border-b-2 font-medium text-sm"
+                >
+                    技能点
+                    @if($this->skillsCount > 0)
+                        <span class="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200">
+                            {{ $this->skillsCount }}
+                        </span>
+                    @endif
+                </button>
+            </nav>
+        </div>
+
+        {{-- 标签页内容 --}}
+        <div class="p-6">
+            @if($activeTab === 'overview')
+                {{-- 知识点介绍 --}}
+                <div class="space-y-4">
+                    @if(!empty($nodeDetails['description']))
+                        <div>
+                            <h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">描述</h4>
+                            <p class="text-sm text-gray-600 dark:text-gray-400">{{ $nodeDetails['description'] }}</p>
+                        </div>
+                    @endif
+
+                    @if(!empty($nodeDetails['skills']))
+                        <div>
+                            <h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">相关技能</h4>
+                            <div class="flex flex-wrap gap-2">
+                                @foreach($nodeDetails['skills'] as $skill)
+                                    <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200">
+                                        {{ $skill['skill_name'] ?? $skill['skill_code'] ?? 'Unknown' }}
+                                    </span>
+                                @endforeach
+                            </div>
+                        </div>
+                    @endif
+
+                    <div class="grid grid-cols-2 gap-4">
+                        <div>
+                            <h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">英文名称</h4>
+                            <p class="text-sm text-gray-600 dark:text-gray-400">{{ $nodeDetails['en_name'] ?? '-' }}</p>
+                        </div>
+                        <div>
+                            <h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">技能数量</h4>
+                            <p class="text-sm text-gray-600 dark:text-gray-400">{{ count($nodeDetails['skills'] ?? []) }}</p>
+                        </div>
+                    </div>
+                </div>
+            @elseif($activeTab === 'children')
+                {{-- 一级子知识点 --}}
+                <div class="space-y-4">
+                    @if(empty($this->childrenNodes))
+                        <div class="text-center text-gray-500 dark:text-gray-400 py-8">
+                            <svg class="mx-auto h-8 w-8 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"></path>
+                            </svg>
+                            <p class="mt-2 text-sm">暂无子知识点</p>
+                        </div>
+                    @else
+                        <div class="space-y-3">
+                            @foreach($this->childrenNodes as $child)
+                                <div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors cursor-pointer">
+                                    <div class="flex-1">
+                                        <div class="flex items-center gap-3">
+                                            <div class="flex-1">
+                                                <h5 class="text-sm font-medium text-gray-900 dark:text-gray-100">
+                                                    {{ $child['cn_name'] ?? $child['kp_code'] }}
+                                                </h5>
+                                                <p class="text-sm text-gray-500 dark:text-gray-400">{{ $child['kp_code'] }}</p>
+                                            </div>
+                                            <div class="text-right">
+                                                @if(isset($child['question_count']))
+                                                    <div class="text-sm font-medium text-blue-600 dark:text-blue-400">
+                                                        {{ $child['question_count'] }} 题
+                                                    </div>
+                                                @endif
+                                                @if(!empty($child['phase']))
+                                                    <span class="text-xs text-gray-500 dark:text-gray-400">{{ $child['phase'] }}</span>
+                                                @endif
+                                            </div>
+                                        </div>
+                                    </div>
+                                    <div class="ml-4">
+                                        <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
+                                        </svg>
+                                    </div>
+                                </div>
+                            @endforeach
+                        </div>
+                    @endif
+                </div>
+            @elseif($activeTab === 'skills')
+                {{-- 技能点 --}}
+                <div class="space-y-4">
+                    @if(empty($nodeDetails['question_stats']['skills'] ?? []))
+                        <div class="text-center text-gray-500 dark:text-gray-400 py-8">
+                            <svg class="mx-auto h-8 w-8 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
+                            </svg>
+                            <p class="mt-2 text-sm">暂无技能点数据</p>
+                        </div>
+                    @else
+                        <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
+                            @foreach($nodeDetails['question_stats']['skills'] as $skill)
+                                <div class="p-4 bg-gradient-to-br from-indigo-50 to-blue-50 dark:from-indigo-900/20 dark:to-blue-900/20 rounded-lg border border-indigo-100 dark:border-indigo-800">
+                                    <div class="flex items-center justify-between mb-2">
+                                        <h5 class="text-sm font-medium text-gray-900 dark:text-gray-100">
+                                            {{ $skill['skill_code'] }}
+                                        </h5>
+                                        <span class="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-indigo-600 text-white">
+                                            {{ $skill['question_count'] ?? 0 }} 题
+                                        </span>
+                                    </div>
+                                    <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
+                                        @php
+                                            $maxCount = collect($nodeDetails['question_stats']['skills'] ?? [])->max('question_count') ?: 1;
+                                            $percentage = ($skill['question_count'] ?? 0) / $maxCount * 100;
+                                        @endphp
+                                        <div
+                                            class="bg-indigo-600 h-2 rounded-full transition-all"
+                                            style="width: {{ $percentage }}%"
+                                        ></div>
+                                    </div>
+                                </div>
+                            @endforeach
+                        </div>
+                    @endif
+                </div>
+            @endif
+        </div>
+    @endif
+</div>

+ 290 - 0
resources/views/livewire/integrations/knowledge-point-stats-component.blade.php

@@ -0,0 +1,290 @@
+<div>
+    <div class="space-y-4">
+        {{-- 统计概览 --}}
+        @if(!empty($statsData))
+            <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
+                {{-- 总题目数 --}}
+                <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
+                    <div class="flex items-center justify-between">
+                        <div>
+                            <p class="text-sm font-medium text-gray-600 dark:text-gray-400">总题目数</p>
+                            <p class="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-1">
+                                {{ array_sum(array_map([$this, 'getTotalQuestions'], is_array($statsData) ? $statsData : [$statsData])) }}
+                            </p>
+                        </div>
+                        <div class="p-3 bg-blue-100 dark:bg-blue-900 rounded-full">
+                            <svg class="w-6 h-6 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
+                            </svg>
+                        </div>
+                    </div>
+                </div>
+
+                {{-- 直接题目数 --}}
+                <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
+                    <div class="flex items-center justify-between">
+                        <div>
+                            <p class="text-sm font-medium text-gray-600 dark:text-gray-400">直接题目数</p>
+                            <p class="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-1">
+                                {{ array_sum(array_map([$this, 'getDirectQuestions'], is_array($statsData) ? $statsData : [$statsData])) }}
+                            </p>
+                        </div>
+                        <div class="p-3 bg-green-100 dark:bg-green-900 rounded-full">
+                            <svg class="w-6 h-6 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
+                            </svg>
+                        </div>
+                    </div>
+                </div>
+
+                {{-- 子知识点题目数 --}}
+                <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
+                    <div class="flex items-center justify-between">
+                        <div>
+                            <p class="text-sm font-medium text-gray-600 dark:text-gray-400">子知识点题目数</p>
+                            <p class="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-1">
+                                {{ array_sum(array_map([$this, 'getChildrenQuestions'], is_array($statsData) ? $statsData : [$statsData])) }}
+                            </p>
+                        </div>
+                        <div class="p-3 bg-yellow-100 dark:bg-yellow-900 rounded-full">
+                            <svg class="w-6 h-6 text-yellow-600 dark:text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
+                            </svg>
+                        </div>
+                    </div>
+                </div>
+
+                {{-- 技能点题目数 --}}
+                <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
+                    <div class="flex items-center justify-between">
+                        <div>
+                            <p class="text-sm font-medium text-gray-600 dark:text-gray-400">技能点题目数</p>
+                            <p class="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-1">
+                                {{ array_sum(array_map([$this, 'getSkillQuestions'], is_array($statsData) ? $statsData : [$statsData])) }}
+                            </p>
+                        </div>
+                        <div class="p-3 bg-purple-100 dark:bg-purple-900 rounded-full">
+                            <svg class="w-6 h-6 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
+                            </svg>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        @endif
+
+        {{-- 统计列表 --}}
+        <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
+            <div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
+                <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
+                    {{ $selectedKpCode ? '知识点详情' : '所有知识点统计' }}
+                </h3>
+                <button
+                    wire:click="toggleDetails"
+                    class="inline-flex items-center gap-2 px-3 py-1 text-sm font-medium text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-200"
+                >
+                    {{ $showDetails ? '隐藏' : '显示' }}详情
+                </button>
+            </div>
+
+            <div class="overflow-x-auto">
+                <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
+                    <thead class="bg-gray-50 dark:bg-gray-900">
+                        <tr>
+                            <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                                知识点
+                            </th>
+                            <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                                总题目数
+                            </th>
+                            <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                                直接题目
+                            </th>
+                            <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                                子知识点题目
+                            </th>
+                            <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                                技能点数
+                            </th>
+                            <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                                技能点题目
+                            </th>
+                        </tr>
+                    </thead>
+                    <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
+                        @forelse(is_array($statsData) ? $statsData : [$statsData] as $stat)
+                            @if(!empty($stat))
+                                <tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
+                                    <td class="px-6 py-4 whitespace-nowrap">
+                                        <div>
+                                            <div class="text-sm font-medium text-gray-900 dark:text-gray-100">
+                                                {{ $stat['cn_name'] ?? $stat['kp_code'] }}
+                                            </div>
+                                            <div class="text-sm text-gray-500 dark:text-gray-400">
+                                                {{ $stat['kp_code'] }}
+                                            </div>
+                                        </div>
+                                    </td>
+                                    <td class="px-6 py-4 whitespace-nowrap">
+                                        <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
+                                            {{ $stat['total_questions'] ?? 0 }}
+                                        </span>
+                                    </td>
+                                    <td class="px-6 py-4 whitespace-nowrap">
+                                        <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
+                                            {{ $stat['direct_questions'] ?? 0 }}
+                                        </span>
+                                    </td>
+                                    <td class="px-6 py-4 whitespace-nowrap">
+                                        <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200">
+                                            {{ $stat['children_questions'] ?? 0 }}
+                                        </span>
+                                    </td>
+                                    <td class="px-6 py-4 whitespace-nowrap">
+                                        <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">
+                                            {{ $stat['skills_count'] ?? 0 }}
+                                        </span>
+                                    </td>
+                                    <td class="px-6 py-4 whitespace-nowrap">
+                                        <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200">
+                                            {{ $stat['skills_total_questions'] ?? 0 }}
+                                        </span>
+                                    </td>
+                                </tr>
+
+                                @if($showDetails && !empty($stat['children']))
+                                    @foreach($stat['children'] as $child)
+                                        <tr class="hover:bg-gray-50 dark:hover:bg-gray-700 bg-gray-50 dark:bg-gray-900">
+                                            <td class="px-6 py-4 whitespace-nowrap pl-12">
+                                                <div class="flex items-center">
+                                                    <svg class="w-4 h-4 text-gray-400 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
+                                                    </svg>
+                                                    <div>
+                                                        <div class="text-sm font-medium text-gray-900 dark:text-gray-100">
+                                                            {{ $child['cn_name'] ?? $child['kp_code'] }}
+                                                        </div>
+                                                        <div class="text-sm text-gray-500 dark:text-gray-400">
+                                                            {{ $child['kp_code'] }}
+                                                        </div>
+                                                    </div>
+                                                </div>
+                                            </td>
+                                            <td class="px-6 py-4 whitespace-nowrap">
+                                                <span class="text-sm font-medium text-blue-600 dark:text-blue-400">
+                                                    {{ $child['total_questions'] ?? 0 }}
+                                                </span>
+                                            </td>
+                                            <td class="px-6 py-4 whitespace-nowrap">
+                                                <span class="text-sm font-medium text-green-600 dark:text-green-400">
+                                                    {{ $child['direct_questions'] ?? 0 }}
+                                                </span>
+                                            </td>
+                                            <td class="px-6 py-4 whitespace-nowrap">
+                                                <span class="text-sm font-medium text-yellow-600 dark:text-yellow-400">
+                                                    {{ $child['children_questions'] ?? 0 }}
+                                                </span>
+                                            </td>
+                                            <td class="px-6 py-4 whitespace-nowrap">
+                                                <span class="text-sm font-medium text-purple-600 dark:text-purple-400">
+                                                    {{ $child['skills_count'] ?? 0 }}
+                                                </span>
+                                            </td>
+                                            <td class="px-6 py-4 whitespace-nowrap">
+                                                <span class="text-sm font-medium text-indigo-600 dark:text-indigo-400">
+                                                    {{ $child['skills_total_questions'] ?? 0 }}
+                                                </span>
+                                            </td>
+                                        </tr>
+
+                                        @if($showDetails && !empty($child['skills']))
+                                            @foreach($child['skills'] as $skill)
+                                                <tr class="hover:bg-gray-50 dark:hover:bg-gray-700 bg-gray-100 dark:bg-gray-800">
+                                                    <td class="px-6 py-4 whitespace-nowrap pl-16">
+                                                        <div class="flex items-center">
+                                                            <svg class="w-3 h-3 text-gray-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
+                                                                <circle cx="10" cy="10" r="3"></circle>
+                                                            </svg>
+                                                            <div>
+                                                                <div class="text-sm text-gray-900 dark:text-gray-100">
+                                                                    {{ $skill['skill_code'] }}
+                                                                </div>
+                                                            </div>
+                                                        </div>
+                                                    </td>
+                                                    <td class="px-6 py-4 whitespace-nowrap">
+                                                        <span class="text-sm text-gray-600 dark:text-gray-400">
+                                                            {{ $skill['question_count'] ?? 0 }}
+                                                        </span>
+                                                    </td>
+                                                    <td class="px-6 py-4 whitespace-nowrap">
+                                                        <span class="text-sm text-gray-600 dark:text-gray-400">-</span>
+                                                    </td>
+                                                    <td class="px-6 py-4 whitespace-nowrap">
+                                                        <span class="text-sm text-gray-600 dark:text-gray-400">-</span>
+                                                    </td>
+                                                    <td class="px-6 py-4 whitespace-nowrap">
+                                                        <span class="text-sm text-gray-600 dark:text-gray-400">1</span>
+                                                    </td>
+                                                    <td class="px-6 py-4 whitespace-nowrap">
+                                                        <span class="text-sm text-gray-600 dark:text-gray-400">
+                                                            {{ $skill['question_count'] ?? 0 }}
+                                                        </span>
+                                                    </td>
+                                                </tr>
+                                            @endforeach
+                                        @endif
+                                    @endforeach
+                                @endif
+
+                                @if($showDetails && !empty($stat['skills']))
+                                    @foreach($stat['skills'] as $skill)
+                                        <tr class="hover:bg-gray-50 dark:hover:bg-gray-700 bg-gray-100 dark:bg-gray-800">
+                                            <td class="px-6 py-4 whitespace-nowrap pl-12">
+                                                <div class="flex items-center">
+                                                    <svg class="w-3 h-3 text-gray-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
+                                                        <circle cx="10" cy="10" r="3"></circle>
+                                                    </svg>
+                                                    <div>
+                                                        <div class="text-sm text-gray-900 dark:text-gray-100">
+                                                            {{ $skill['skill_code'] }}
+                                                        </div>
+                                                    </div>
+                                                </div>
+                                            </td>
+                                            <td class="px-6 py-4 whitespace-nowrap">
+                                                <span class="text-sm text-gray-600 dark:text-gray-400">
+                                                    {{ $skill['question_count'] ?? 0 }}
+                                                </span>
+                                            </td>
+                                            <td class="px-6 py-4 whitespace-nowrap">
+                                                <span class="text-sm text-gray-600 dark:text-gray-400">-</span>
+                                            </td>
+                                            <td class="px-6 py-4 whitespace-nowrap">
+                                                <span class="text-sm text-gray-600 dark:text-gray-400">-</span>
+                                            </td>
+                                            <td class="px-6 py-4 whitespace-nowrap">
+                                                <span class="text-sm text-gray-600 dark:text-gray-400">1</span>
+                                            </td>
+                                            <td class="px-6 py-4 whitespace-nowrap">
+                                                <span class="text-sm text-gray-600 dark:text-gray-400">
+                                                    {{ $skill['question_count'] ?? 0 }}
+                                                </span>
+                                            </td>
+                                        </tr>
+                                    @endforeach
+                                @endif
+                            @endif
+                        @empty
+                            <tr>
+                                <td colspan="6" class="px-6 py-12 text-center text-gray-500 dark:text-gray-400">
+                                    暂无统计数据
+                                </td>
+                            </tr>
+                        @endforelse
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </div>
+</div>

+ 177 - 0
resources/views/livewire/integrations/knowledge-points-list-component.blade.php

@@ -0,0 +1,177 @@
+<div>
+    <div class="space-y-4">
+        {{-- 搜索和筛选 --}}
+        <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 space-y-4">
+            <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
+                {{-- 搜索框 --}}
+                <div class="relative">
+                    <span class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400">
+                        <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
+                        </svg>
+                    </span>
+                    <input
+                        wire:model.live.debounce="search"
+                        type="text"
+                        placeholder="搜索知识点..."
+                        class="w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-gray-100"
+                    />
+                </div>
+
+                {{-- 学段筛选 --}}
+                <div class="relative">
+                    <span class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400">
+                        <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path>
+                        </svg>
+                    </span>
+                    <select
+                        wire:model.live="phaseFilter"
+                        class="w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-gray-100 appearance-none"
+                    >
+                        <option value="">所有学段</option>
+                        @foreach($this->filterOptions['phases'] as $phase)
+                            <option value="{{ $phase }}">{{ $phase }}</option>
+                        @endforeach
+                    </select>
+                </div>
+
+                {{-- 类别筛选 --}}
+                <div class="relative">
+                    <span class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400">
+                        <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
+                        </svg>
+                    </span>
+                    <select
+                        wire:model.live="categoryFilter"
+                        class="w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-gray-100 appearance-none"
+                    >
+                        <option value="">所有类别</option>
+                        @foreach($this->filterOptions['categories'] as $category)
+                            <option value="{{ $category }}">{{ $category }}</option>
+                        @endforeach
+                    </select>
+                </div>
+            </div>
+
+            <div class="flex items-center justify-between pt-2 border-t border-gray-200 dark:border-gray-700">
+                <div class="text-sm text-gray-600 dark:text-gray-400">
+                    共 {{ count($filteredPoints) }} 个知识点
+                </div>
+                <button
+                    wire:click="clearSelection"
+                    class="inline-flex items-center gap-2 px-3 py-1 text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200"
+                >
+                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
+                    </svg>
+                    清空筛选
+                </button>
+            </div>
+        </div>
+
+        {{-- 知识点列表 --}}
+        <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
+            @if($isLoading)
+                <div class="flex items-center justify-center p-8">
+                    <svg class="animate-spin h-6 w-6 text-indigo-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+                        <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
+                        <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>
+                    </svg>
+                    <span class="ml-2 text-gray-600 dark:text-gray-300">加载中...</span>
+                </div>
+            @elseif(empty($filteredPoints))
+                <div class="text-center p-8 text-gray-500 dark:text-gray-400">
+                    <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+                    </svg>
+                    <p class="mt-2">没有找到匹配的知识点</p>
+                </div>
+            @else
+                <div class="overflow-x-auto">
+                    <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
+                        <thead class="bg-gray-50 dark:bg-gray-900">
+                            <tr>
+                                <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                                    知识点
+                                </th>
+                                <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                                    代码
+                                </th>
+                                <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                                    学段
+                                </th>
+                                <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                                    类别
+                                </th>
+                                <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                                    重要度
+                                </th>
+                                <th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+                                    操作
+                                </th>
+                            </tr>
+                        </thead>
+                        <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
+                            @foreach($filteredPoints as $point)
+                                <tr class="hover:bg-gray-50 dark:hover:bg-gray-700 {{ $selectedKpCode === $point['kp_code'] ? 'bg-indigo-50 dark:bg-indigo-900/20' : '' }}">
+                                    <td class="px-6 py-4 whitespace-nowrap">
+                                        <div class="flex items-center">
+                                            <div>
+                                                <div class="text-sm font-medium text-gray-900 dark:text-gray-100">
+                                                    {{ $point['cn_name'] ?? $point['kp_code'] }}
+                                                </div>
+                                                @if(!empty($point['description']))
+                                                    <div class="text-sm text-gray-500 dark:text-gray-400">
+                                                        {{ Str::limit($point['description'], 60) }}
+                                                    </div>
+                                                @endif
+                                            </div>
+                                        </div>
+                                    </td>
+                                    <td class="px-6 py-4 whitespace-nowrap">
+                                        <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300">
+                                            {{ $point['kp_code'] }}
+                                        </span>
+                                    </td>
+                                    <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
+                                        {{ $point['phase'] ?? '-' }}
+                                    </td>
+                                    <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
+                                        {{ $point['category'] ?? '-' }}
+                                    </td>
+                                    <td class="px-6 py-4 whitespace-nowrap">
+                                        <div class="flex items-center">
+                                            <div class="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-2">
+                                                <div
+                                                    class="bg-indigo-600 h-2 rounded-full"
+                                                    style="width: {{ ($point['importance'] ?? 0) * 20 }}%"
+                                                ></div>
+                                            </div>
+                                            <span class="ml-2 text-sm text-gray-500 dark:text-gray-400">
+                                                {{ $point['importance'] ?? 0 }}
+                                            </span>
+                                        </div>
+                                    </td>
+                                    <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
+                                        <button
+                                            wire:click="selectKp('{{ $point['kp_code'] }}')"
+                                            class="inline-flex items-center gap-1 px-3 py-1 text-sm font-medium text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-200"
+                                        >
+                                            <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
+                                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
+                                            </svg>
+                                            查看
+                                        </button>
+                                    </td>
+                                </tr>
+                            @endforeach
+                        </tbody>
+                    </table>
+                </div>
+            @endif
+        </div>
+    </div>
+</div>

+ 17 - 4
routes/api.php

@@ -23,11 +23,11 @@ Route::post('/questions/callback', function () {
             return response()->json(['error' => 'Invalid callback data'], 400);
         }
 
-        // 存储回调结果到 session 中,供前端查询
+        // ✅ 同时存储到session和缓存中,确保前端可以访问
         session(['question_gen_callback_' . $data['task_id'] => $data]);
 
-        // ✅ 记录任务状态,10秒后自动清除
-        cache([$data['task_id'] => $data], now()->addSeconds(10));
+        // ✅ 使用缓存存储回调结果,供前端查询(保留30秒)
+        cache([$data['task_id'] => $data], now()->addSeconds(30));
 
         // ✅ 使用事件替代轮询 - 直接分发事件给前端
         if ($data['status'] === 'completed') {
@@ -233,12 +233,25 @@ Route::post('/ocr-question-callback', function () {
 
 // 获取题目生成回调结果
 Route::get('/questions/callback/{taskId}', function (string $taskId) {
-    $callbackData = session('question_gen_callback_' . $taskId);
+    // ✅ 优先从缓存读取(跨域友好)
+    $callbackData = cache($taskId);
+
     if ($callbackData) {
         // 清除已读取的回调数据
+        cache()->forget($taskId);
         session()->forget('question_gen_callback_' . $taskId);
         return response()->json($callbackData);
     }
+
+    // 备选:从session读取
+    $sessionData = session('question_gen_callback_' . $taskId);
+    if ($sessionData) {
+        // 清除已读取的回调数据
+        session()->forget('question_gen_callback_' . $taskId);
+        return response()->json($sessionData);
+    }
+
+    // 未收到回调
     return response()->json(['status' => 'pending'], 202);
 })->name('api.questions.callback.get');