yemeishu 1 miesiąc temu
rodzic
commit
d4bcf49e03

+ 63 - 64
app/Filament/Pages/KnowledgePoints.php

@@ -4,7 +4,9 @@ namespace App\Filament\Pages;
 
 use App\Services\KnowledgeServiceApi;
 use BackedEnum;
+use Filament\Actions\Action;
 use Filament\Pages\Page;
+use Illuminate\Http\Request;
 use Illuminate\Support\Collection;
 use Illuminate\Support\Str;
 use UnitEnum;
@@ -29,48 +31,52 @@ class KnowledgePoints extends Page
 
     public int $perPage = 8;
 
-    protected ?array $selectedPoint = null;
-
     protected bool $loading = false;
 
     protected ?KnowledgeServiceApi $knowledgeService = null;
 
-    public function mount(): void
+    protected ?string $selectedKpCode = null;
+
+    // Private property to store loaded data
+    private ?array $selectedPointData = null;
+
+    public function mount(Request $request): void
     {
-        $this->hydratePoints();
+        // Only set properties that need to be available immediately
+        // The search and filter properties will be set via getter
+        $this->selectedKpCode = $request->query('selected');
+        $this->page = (int) $request->query('page', 1);
     }
 
-    protected function getKnowledgeService(): KnowledgeServiceApi
+    public function getSelectedPointProperty(): ?array
     {
-        if (!$this->knowledgeService) {
-            $this->knowledgeService = app(KnowledgeServiceApi::class);
+        if ($this->selectedPointData) {
+            return $this->selectedPointData;
         }
 
-        return $this->knowledgeService;
-    }
+        // If we have a specific kp code to load
+        if ($this->selectedKpCode) {
+            $this->selectedPointData = $this->getKnowledgeService()->getKnowledgePointDetail($this->selectedKpCode);
+            return $this->selectedPointData;
+        }
 
-    public function updatedPhaseFilter(): void
-    {
-        $this->page = 1;
-        $this->hydratePoints();
-    }
+        // Get first point from paginated results
+        $points = $this->paginatedPoints;
+        if (!empty($points['data']) && count($points['data']) > 0) {
+            $kpCode = $points['data'][0]['kp_code'];
+            $this->selectedPointData = $this->getKnowledgeService()->getKnowledgePointDetail($kpCode);
+        }
 
-    public function updatedCategoryFilter(): void
-    {
-        $this->page = 1;
-        $this->hydratePoints();
+        return $this->selectedPointData;
     }
 
-    public function updatedSearch(): void
+    protected function getKnowledgeService(): KnowledgeServiceApi
     {
-        $this->page = 1;
-        $this->hydratePoints();
-    }
+        if (!$this->knowledgeService) {
+            $this->knowledgeService = app(KnowledgeServiceApi::class);
+        }
 
-    public function changePage(int $page): void
-    {
-        $this->page = max($page, 1);
-        $this->hydratePoints();
+        return $this->knowledgeService;
     }
 
     public function selectPoint(string $kpCode): void
@@ -119,52 +125,57 @@ class KnowledgePoints extends Page
 
     public function getPaginatedPointsProperty(): array
     {
+        // Get filters from URL
+        $request = request();
+        $phaseFilter = $request->query('phase');
+        $categoryFilter = $request->query('category');
+        $searchTerm = trim((string) $request->query('search'));
+
+        // Get all points
         $filters = array_filter([
-            'phase' => $this->phaseFilter,
-            'category' => $this->categoryFilter,
+            'phase' => $phaseFilter,
+            'category' => $categoryFilter,
         ]);
 
-        $response = $this->getKnowledgeService()->paginateKnowledgePoints(
-            page: $this->page,
-            perPage: $this->perPage,
+        $allPoints = $this->getKnowledgeService()->listKnowledgePoints(
+            perPage: 200,
             filters: $filters
         );
 
-        $records = collect($response['data'] ?? []);
-
-        if (filled($this->search)) {
-            $search = Str::lower($this->search);
-            $records = $records->filter(function (array $record) use ($search): bool {
-                return Str::contains(Str::lower($record['cn_name'] ?? ''), $search)
-                    || Str::contains(Str::lower($record['kp_code'] ?? ''), $search);
+        // Apply search filter on the client side
+        if ($searchTerm !== '') {
+            $allPoints = $allPoints->filter(function (array $record) use ($searchTerm): bool {
+                return Str::contains(Str::lower($record['cn_name'] ?? ''), Str::lower($searchTerm))
+                    || Str::contains(Str::lower($record['kp_code'] ?? ''), Str::lower($searchTerm))
+                    || Str::contains(Str::lower($record['description'] ?? ''), Str::lower($searchTerm));
             });
         }
 
-        $meta = $response['meta'] ?? [];
+        // Paginate the results
+        $total = $allPoints->count();
+        $totalPages = (int) ceil($total / $this->perPage);
+        $offset = ($this->page - 1) * $this->perPage;
+        $records = $allPoints->slice($offset, $this->perPage)->values();
 
         return [
-            'data' => $records->values(),
-            'total' => $meta['total'] ?? $records->count(),
-            'page' => $meta['page'] ?? $this->page,
-            'total_pages' => $meta['total_pages'] ?? 1,
-            'has_prev' => $meta['has_prev'] ?? ($this->page > 1),
-            'has_next' => $meta['has_next'] ?? false,
+            'data' => $records,
+            'total' => $total,
+            'page' => $this->page,
+            'total_pages' => $totalPages,
+            'has_prev' => $this->page > 1,
+            'has_next' => $this->page < $totalPages,
         ];
     }
 
     public function getSelectedSkillsProperty(): Collection
     {
-        return collect($this->selectedPoint['skills'] ?? []);
-    }
-
-    public function getSelectedPointProperty(): ?array
-    {
-        return $this->selectedPoint;
+        $selectedPoint = $this->selectedPointData;
+        return collect($selectedPoint['skills'] ?? []);
     }
 
     public function getRelatedNodesProperty(): array
     {
-        $point = $this->selectedPoint;
+        $point = $this->selectedPointData;
 
         if (! $point) {
             return ['parents' => [], 'children' => []];
@@ -187,16 +198,4 @@ class KnowledgePoints extends Page
         return ['parents' => $parents, 'children' => $children];
     }
 
-    protected function hydratePoints(): void
-    {
-        $this->loading = true;
-        $this->selectedPoint = null;
-        $points = $this->paginatedPoints['data'];
-
-        if ($points->isNotEmpty()) {
-            $this->selectPoint($points->first()['kp_code']);
-        }
-
-        $this->loading = false;
-    }
 }

+ 34 - 1
app/Services/KnowledgeServiceApi.php

@@ -75,6 +75,7 @@ class KnowledgeServiceApi
             'phase' => $filters['phase'] ?? null,
             'category' => $filters['category'] ?? null,
             'grade' => $filters['grade'] ?? null,
+            'search' => $filters['search'] ?? null,
         ], fn ($value) => filled($value));
 
         $response = $this->request('GET', '/knowledge-points', $query);
@@ -126,6 +127,34 @@ class KnowledgeServiceApi
         );
     }
 
+    /**
+     * Search knowledge points by keyword
+     *
+     * @return Collection<int, array<string, mixed>>
+     */
+    public function searchKnowledgePoints(string $keyword, int $limit = 50): Collection
+    {
+        // Ensure keyword is not empty
+        $keyword = trim($keyword);
+        if ($keyword === '') {
+            return collect();
+        }
+
+        // Use query params array for proper encoding (no cache during debugging)
+        try {
+            $response = $this->request('GET', 'knowledge-points/search', [
+                'keyword' => $keyword,
+                'limit' => $limit,
+            ]);
+        } catch (\Exception $e) {
+            // Log error but don't fail completely
+            \Log::error('Search API error: ' . $e->getMessage());
+            return collect();
+        }
+
+        return collect($response);
+    }
+
     /**
      * @throws RequestException
      * @return mixed
@@ -144,6 +173,10 @@ class KnowledgeServiceApi
         return Http::baseUrl($this->baseUrl)
             ->acceptJson()
             ->timeout($this->timeout)
-            ->retry(2, 200);
+            ->retry(2, 200)
+            ->withHeaders([
+                'User-Agent' => 'Laravel/' . (app()->version()),
+                'Accept' => 'application/json',
+            ]);
     }
 }

+ 77 - 9
resources/views/filament/pages/knowledge-points.blade.php

@@ -1,8 +1,12 @@
 @php
+    // Access properties to trigger lazy loading
     $stats = $this->stats;
     $points = $this->paginatedPoints['data'];
     $pagination = $this->paginatedPoints;
-    $selectedPoint = $selectedPoint ?? $this->selectedPoint ?? null;
+
+    // Explicitly call the getter to ensure selectedPoint is loaded
+    $selectedPointData = $this->selectedPoint;
+    $selectedPoint = $selectedPointData;
     $selectedSkills = isset($selectedPoint) ? collect($selectedPoint['skills'] ?? []) : collect();
 @endphp
 
@@ -27,10 +31,23 @@
                         </span>
                         <input
                             type="text"
-                            wire:model.debounce.500ms="search"
+                            id="search-input"
+                            x-on:change="handleSearch($event)"
+                            x-on:keydown.enter="handleSearch($event)"
                             placeholder="搜索知识点或编号..."
                             class="w-full rounded-xl border border-gray-200 bg-white/80 py-2 pl-10 pr-3 text-sm placeholder:text-gray-400 focus:border-primary-300 focus:outline-none dark:border-gray-700 dark:bg-gray-900/60"
                         />
+                        <script>
+                            // Initialize search input from URL on page load
+                            document.addEventListener('DOMContentLoaded', () => {
+                                const urlParams = new URLSearchParams(window.location.search);
+                                const searchParam = urlParams.get('search');
+                                const searchInput = document.getElementById('search-input');
+                                if (searchInput && searchParam) {
+                                    searchInput.value = searchParam;
+                                }
+                            });
+                        </script>
                     </div>
                     <div class="mt-4 grid gap-3 md:grid-cols-2">
                         <div class="relative">
@@ -38,7 +55,7 @@
                                 @svg('heroicon-m-academic-cap', 'h-4 w-4')
                             </span>
                             <select
-                                wire:model="phaseFilter"
+                                onchange="updateFilter('phase', this.value)"
                                 class="w-full rounded-xl border border-gray-200 bg-white/80 py-2 pl-10 pr-3 text-sm focus:border-primary-300 focus:outline-none dark:border-gray-700 dark:bg-gray-900/60"
                             >
                                 <option value="">全部学段</option>
@@ -52,7 +69,7 @@
                                 @svg('heroicon-m-tag', 'h-4 w-4')
                             </span>
                             <select
-                                wire:model="categoryFilter"
+                                onchange="updateFilter('category', this.value)"
                                 class="w-full rounded-xl border border-gray-200 bg-white/80 py-2 pl-10 pr-3 text-sm focus:border-primary-300 focus:outline-none dark:border-gray-700 dark:bg-gray-900/60"
                             >
                                 <option value="">全部类别</option>
@@ -72,7 +89,7 @@
                     <div class="space-y-3">
                         @forelse($points as $point)
                             <button
-                                wire:click="selectPoint('{{ $point['kp_code'] }}')"
+                                onclick="selectPoint('{{ $point['kp_code'] }}')"
                                 class="w-full rounded-xl border px-4 py-3 text-left transition @if($selectedPoint && $selectedPoint['kp_code'] === $point['kp_code']) border-primary-500 bg-primary-50/70 dark:bg-primary-500/10 @else border-gray-200 hover:border-primary-200 dark:border-gray-800 @endif"
                             >
                                 <p class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ $point['cn_name'] }}</p>
@@ -88,16 +105,16 @@
                     </div>
                     <div class="mt-4 flex items-center justify-between text-xs text-gray-500">
                         <button
-                            wire:click="changePage({{ $pagination['page'] - 1 }})"
-                            @disabled(!$pagination['has_prev'])"
+                            onclick="navigateToPage({{ $pagination['page'] - 1 }})"
+                            @disabled(!$pagination['has_prev'])
                             class="rounded-full border px-3 py-1 @if(!$pagination['has_prev']) opacity-40 cursor-not-allowed @endif"
                         >
                             上一页
                         </button>
                         <span>第 {{ $pagination['page'] }} / {{ $pagination['total_pages'] }} 页</span>
                         <button
-                            wire:click="changePage({{ $pagination['page'] + 1 }})"
-                            @disabled(!$pagination['has_next'])"
+                            onclick="navigateToPage({{ $pagination['page'] + 1 }})"
+                            @disabled(!$pagination['has_next'])
                             class="rounded-full border px-3 py-1 @if(!$pagination['has_next']) opacity-40 cursor-not-allowed @endif"
                         >
                             下一页
@@ -213,4 +230,55 @@
             </div>
         </div>
     </div>
+
+    <script>
+        function handleSearch(event) {
+            const query = event.target.value.trim();
+
+            if (query === '') {
+                // If search is cleared, remove search param
+                const url = new URL(window.location.href);
+                url.searchParams.delete('search');
+                // Reset to page 1
+                url.searchParams.set('page', 1);
+                window.location.href = url.toString();
+                return;
+            }
+
+            // Only update if query changed
+            const currentUrl = new URL(window.location.href);
+            const currentSearch = currentUrl.searchParams.get('search') || '';
+
+            if (query !== currentSearch) {
+                const url = new URL(window.location.href);
+                url.searchParams.set('search', query);
+                url.searchParams.set('page', 1); // Reset to first page
+                window.location.href = url.toString();
+            }
+        }
+
+        function navigateToPage(page) {
+            const url = new URL(window.location.href);
+            url.searchParams.set('page', page);
+            window.location.href = url.toString();
+        }
+
+        function selectPoint(kpCode) {
+            const url = new URL(window.location.href);
+            url.searchParams.set('selected', kpCode);
+            window.location.href = url.toString();
+        }
+
+        function updateFilter(filterName, value) {
+            const url = new URL(window.location.href);
+            if (value) {
+                url.searchParams.set(filterName, value);
+            } else {
+                url.searchParams.delete(filterName);
+            }
+            // Reset to page 1 when filters change
+            url.searchParams.set('page', 1);
+            window.location.href = url.toString();
+        }
+    </script>
 </x-filament-panels::page>