visuddhinanda 16 hours ago
parent
commit
c0bdd93dba

+ 26 - 0
api-v12/app/DTO/Search/AggregationDTO.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace App\DTO\Search;
+
+/**
+ * 通用聚合结果DTO
+ * 用于处理所有结构相同的聚合桶数据
+ */
+class AggregationDTO
+{
+    public function __construct(
+        public int $doc_count_error_upper_bound,
+        public int $sum_other_doc_count,
+        /** @var BucketDTO[] */
+        public array $buckets,
+    ) {}
+
+    public static function fromArray(array $data): self
+    {
+        return new self(
+            doc_count_error_upper_bound: $data['doc_count_error_upper_bound'],
+            sum_other_doc_count: $data['sum_other_doc_count'],
+            buckets: array_map(fn($bucket) => BucketDTO::fromArray($bucket), $data['buckets']),
+        );
+    }
+}

+ 23 - 0
api-v12/app/DTO/Search/AggregationsDTO.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace App\DTO\Search;
+
+class AggregationsDTO
+{
+    public function __construct(
+        public AggregationDTO $granularity,
+        public AggregationDTO $resource_type,
+        public AggregationDTO $language,
+        public AggregationDTO $category,
+    ) {}
+
+    public static function fromArray(array $data): self
+    {
+        return new self(
+            granularity: AggregationDTO::fromArray($data['granularity']),
+            resource_type: AggregationDTO::fromArray($data['resource_type']),
+            language: AggregationDTO::fromArray($data['language']),
+            category: AggregationDTO::fromArray($data['category']),
+        );
+    }
+}

+ 19 - 0
api-v12/app/DTO/Search/BucketDTO.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace App\DTO\Search;
+
+class BucketDTO
+{
+    public function __construct(
+        public string $key,
+        public int $doc_count,
+    ) {}
+
+    public static function fromArray(array $data): self
+    {
+        return new self(
+            key: $data['key'],
+            doc_count: $data['doc_count'],
+        );
+    }
+}

+ 21 - 0
api-v12/app/DTO/Search/QueryInfoDTO.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace App\DTO\Search;
+
+class QueryInfoDTO
+{
+    public function __construct(
+        public string $original_query,
+        public string $search_mode,
+        public string $request_method,
+    ) {}
+
+    public static function fromArray(array $data): self
+    {
+        return new self(
+            original_query: $data['original_query'],
+            search_mode: $data['search_mode'],
+            request_method: $data['request_method'],
+        );
+    }
+}

+ 23 - 0
api-v12/app/DTO/Search/ShardsDTO.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace App\DTO\Search;
+
+class ShardsDTO
+{
+    public function __construct(
+        public int $total,
+        public int $successful,
+        public int $skipped,
+        public int $failed,
+    ) {}
+
+    public static function fromArray(array $data): self
+    {
+        return new self(
+            total: $data['total'],
+            successful: $data['successful'],
+            skipped: $data['skipped'],
+            failed: $data['failed'],
+        );
+    }
+}

+ 82 - 0
api-v12/app/Http/Controllers/Library/SearchController.php

@@ -0,0 +1,82 @@
+<?php
+
+namespace App\Http\Controllers\Library;
+
+use App\Http\Controllers\Controller;
+
+use Illuminate\Http\Request;
+use App\Services\OpenSearchService;
+use App\DTO\Search\SearchDataDTO;
+
+
+class SearchController extends Controller
+{
+    public function search(Request $request)
+    {
+        $query    = trim($request->input('q', ''));
+        $category = $request->input('category', 'all');
+        $page     = max(1, (int) $request->input('page', 1));
+        $perPage  = 10;
+        $lang = $request->input('lang');
+
+        $search = app(OpenSearchService::class);
+        // 组装搜索参数
+        $params = [
+            'query'        => $query,
+            'pageSize'     => $perPage,
+            'page'     => $page,
+            'language'     => $lang,
+            'resourceType'     => $request->input('type'),
+        ];
+        $result = $search->search($params);
+
+        $dto = SearchDataDTO::fromArray($result);
+        $results = [];
+        foreach ($dto->hits->items as $key => $item) {
+            $results[] = [
+                'word'     => 'word',
+                'zh'       => $item->title,
+                'lang'     => 'pi',
+                'category' => '法义术语',
+                'quality'  => 'featured',
+                'snippet'  => $item->highlight,
+                'updated'  => '2025-11-12',
+            ];
+        }
+
+        $category = $dto->aggregations->category->buckets;
+
+        // 分页对象(兼容 Blade paginator 风格)
+        $pagination = [
+            'total'        => $dto->hits->total,
+            'per_page'     => $perPage,
+            'current_page' => $page,
+            'last_page'    => max(1, (int) ceil($dto->hits->total / $perPage)),
+        ];
+
+        return view('wiki.search', [
+            'lang'           => $lang,
+            'query'          => $query,
+            'results'        => $results,
+            'pagination'     => $pagination,
+            'category'       => 'all',
+            'filters'     => $category,
+            'categories'     => $this->types(),
+            'recentUpdates'  => [],
+        ]);
+    }
+    // ── Helpers ──────────────────────────────────────────────────
+
+    private function types(): array
+    {
+        return [
+            ['slug' => 'all',      'label' => '全部'],
+            ['slug' => 'term',     'label' => '法义术语'],
+            ['slug' => 'original_text',   'label' => '原文'],
+            ['slug' => 'translation',     'label' => '译文'],
+            ['slug' => 'article',   'label' => '文章'],
+            ['slug' => 'course', 'label' => '课程'],
+            ['slug' => 'dictionary',    'label' => '字典'],
+        ];
+    }
+}

+ 70 - 0
api-v12/public/assets/images/dhamma-wheel.svg

@@ -0,0 +1,70 @@
+<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <!-- 金色渐变 -->
+    <radialGradient id="gold" cx="50%" cy="50%" r="50%">
+      <stop offset="0%" stop-color="#fff7cc"/>
+      <stop offset="60%" stop-color="#d4af37"/>
+      <stop offset="100%" stop-color="#8c6b1f"/>
+    </radialGradient>
+
+    <!-- 外圈花纹 -->
+    <pattern id="ornament" patternUnits="userSpaceOnUse" width="20" height="20">
+      <circle cx="10" cy="10" r="2" fill="#b8962e"/>
+    </pattern>
+  </defs>
+
+  <!-- 外圈 -->
+  <circle cx="256" cy="256" r="240" fill="url(#gold)" stroke="#5a4715" stroke-width="6"/>
+
+  <!-- 花纹装饰环 -->
+  <circle cx="256" cy="256" r="210" fill="none" stroke="url(#ornament)" stroke-width="10"/>
+
+  <!-- 内圈 -->
+  <circle cx="256" cy="256" r="180" fill="none" stroke="#5a4715" stroke-width="6"/>
+
+  <!-- 辐条 (24根) -->
+  <g stroke="#5a4715" stroke-width="4" stroke-linecap="round">
+    <!-- 使用旋转生成 -->
+    <line x1="256" y1="256" x2="256" y2="80"/>
+    <line x1="256" y1="256" x2="256" y2="80" transform="rotate(15 256 256)"/>
+    <line x1="256" y1="256" x2="256" y2="80" transform="rotate(30 256 256)"/>
+    <line x1="256" y1="256" x2="256" y2="80" transform="rotate(45 256 256)"/>
+    <line x1="256" y1="256" x2="256" y2="80" transform="rotate(60 256 256)"/>
+    <line x1="256" y1="256" x2="256" y2="80" transform="rotate(75 256 256)"/>
+    <line x1="256" y1="256" x2="256" y2="80" transform="rotate(90 256 256)"/>
+    <line x1="256" y1="256" x2="256" y2="80" transform="rotate(105 256 256)"/>
+    <line x1="256" y1="256" x2="256" y2="80" transform="rotate(120 256 256)"/>
+    <line x1="256" y1="256" x2="256" y2="80" transform="rotate(135 256 256)"/>
+    <line x1="256" y1="256" x2="256" y2="80" transform="rotate(150 256 256)"/>
+    <line x1="256" y1="256" x2="256" y2="80" transform="rotate(165 256 256)"/>
+    <line x1="256" y1="256" x2="256" y2="80" transform="rotate(180 256 256)"/>
+    <line x1="256" y1="256" x2="256" y2="80" transform="rotate(195 256 256)"/>
+    <line x1="256" y1="256" x2="256" y2="80" transform="rotate(210 256 256)"/>
+    <line x1="256" y1="256" x2="256" y2="80" transform="rotate(225 256 256)"/>
+    <line x1="256" y1="256" x2="256" y2="80" transform="rotate(240 256 256)"/>
+    <line x1="256" y1="256" x2="256" y2="80" transform="rotate(255 256 256)"/>
+    <line x1="256" y1="256" x2="256" y2="80" transform="rotate(270 256 256)"/>
+    <line x1="256" y1="256" x2="256" y2="80" transform="rotate(285 256 256)"/>
+    <line x1="256" y1="256" x2="256" y2="80" transform="rotate(300 256 256)"/>
+    <line x1="256" y1="256" x2="256" y2="80" transform="rotate(315 256 256)"/>
+    <line x1="256" y1="256" x2="256" y2="80" transform="rotate(330 256 256)"/>
+    <line x1="256" y1="256" x2="256" y2="80" transform="rotate(345 256 256)"/>
+  </g>
+
+  <!-- 中心莲花 -->
+  <g>
+    <circle cx="256" cy="256" r="40" fill="#fff7cc" stroke="#5a4715" stroke-width="4"/>
+    <!-- 花瓣 -->
+    <g fill="#d4af37">
+      <ellipse cx="256" cy="210" rx="12" ry="28"/>
+      <ellipse cx="256" cy="302" rx="12" ry="28"/>
+      <ellipse cx="210" cy="256" rx="28" ry="12"/>
+      <ellipse cx="302" cy="256" rx="28" ry="12"/>
+      <ellipse cx="226" cy="226" rx="10" ry="22" transform="rotate(-45 226 226)"/>
+      <ellipse cx="286" cy="226" rx="10" ry="22" transform="rotate(45 286 226)"/>
+      <ellipse cx="226" cy="286" rx="10" ry="22" transform="rotate(45 226 286)"/>
+      <ellipse cx="286" cy="286" rx="10" ry="22" transform="rotate(-45 286 286)"/>
+    </g>
+  </g>
+
+</svg>

+ 213 - 0
api-v12/resources/css/wiki-search.css

@@ -0,0 +1,213 @@
+/* ── 追加到 resources/css/wiki.css 末尾 ── */
+
+/* ── 搜索栏 ── */
+.wiki-search-bar-wrap {
+    margin-bottom: 0.875rem;
+}
+
+.wiki-search-form .wiki-search-input {
+    font-size: 0.9375rem;
+}
+
+/* ── 结果摘要行 ── */
+.wiki-search-summary {
+    font-size: 0.875rem;
+    color: var(--tblr-secondary);
+    margin-bottom: 1rem;
+    padding: 0 0.25rem;
+}
+
+.wiki-search-summary strong {
+    color: var(--tblr-body-color);
+    font-weight: 500;
+}
+
+/* ── 搜索结果列表容器 ── */
+.wiki-search-results {
+    padding: 0;         /* 覆盖 wiki-card 默认 padding,由卡片自身管理间距 */
+}
+
+/* ── 搜索结果卡片 ── */
+.wiki-search-card {
+    padding: 1rem 1.5rem;
+    border-bottom: 1px solid var(--tblr-border-color);
+}
+
+.wiki-search-card:last-child {
+    border-bottom: none;
+}
+
+.wiki-search-card:hover {
+    background: var(--tblr-bg-surface-secondary);
+}
+
+.wiki-search-card-header {
+    display: flex;
+    align-items: baseline;
+    gap: 10px;
+    margin-bottom: 5px;
+    flex-wrap: wrap;
+}
+
+.wiki-search-card-title {
+    font-size: 1rem;
+    font-weight: 500;
+    color: var(--tblr-primary);
+    text-decoration: none;
+    display: inline-flex;
+    align-items: baseline;
+    gap: 6px;
+}
+
+.wiki-search-card-title:hover {
+    text-decoration: underline;
+}
+
+.wiki-search-card-word {
+    font-family: 'Noto Serif', Georgia, serif;
+    font-size: 0.875rem;
+    font-style: italic;
+    color: var(--tblr-secondary);
+    font-weight: 400;
+}
+
+.wiki-search-card-snippet {
+    font-size: 0.875rem;
+    color: var(--tblr-secondary);
+    line-height: 1.6;
+    margin: 0 0 6px;
+}
+
+/* highlight 高亮词 */
+.wiki-search-card-snippet mark {
+    background: #FAEEDA;
+    color: #854F0B;
+    padding: 1px 2px;
+    border-radius: 3px;
+    font-style: normal;
+}
+
+.wiki-search-card-meta {
+    font-size: 0.75rem;
+    color: var(--tblr-secondary);
+    display: flex;
+    align-items: center;
+    gap: 5px;
+}
+
+.wiki-search-card-sep {
+    color: var(--tblr-border-color-dark, #adb5bd);
+}
+
+/* ── 分类筛选 badge ── */
+.wiki-cat-count {
+    font-size: 0.6875rem;
+    background: var(--tblr-bg-surface-secondary);
+    border: 1px solid var(--tblr-border-color);
+    border-radius: 20px;
+    padding: 1px 7px;
+    color: var(--tblr-secondary);
+    margin-left: auto;
+    flex-shrink: 0;
+}
+
+.wiki-cat-list a {
+    display: flex;           /* 覆盖原 block,让 count badge 右对齐 */
+    align-items: center;
+}
+
+/* ── 空状态 ── */
+.wiki-empty-state {
+    text-align: center;
+    padding: 3rem 2rem;
+}
+
+.wiki-empty-icon {
+    width: 56px;
+    height: 56px;
+    border-radius: 50%;
+    background: var(--tblr-bg-surface-secondary);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin: 0 auto 1rem;
+    color: var(--tblr-secondary);
+}
+
+.wiki-empty-title {
+    font-size: 1rem;
+    font-weight: 500;
+    margin-bottom: 0.5rem;
+}
+
+.wiki-empty-desc {
+    font-size: 0.875rem;
+    color: var(--tblr-secondary);
+    line-height: 1.6;
+}
+
+.wiki-empty-desc a {
+    color: var(--tblr-primary);
+    text-decoration: none;
+}
+
+/* ── 分页 ── */
+.wiki-pagination {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 4px;
+    padding: 1.5rem 0 0.5rem;
+    flex-wrap: wrap;
+}
+
+.wiki-page-btn {
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    min-width: 34px;
+    height: 34px;
+    padding: 0 6px;
+    border-radius: var(--tblr-border-radius);
+    border: 1px solid var(--tblr-border-color);
+    font-size: 0.875rem;
+    color: var(--tblr-body-color);
+    text-decoration: none;
+    background: var(--tblr-bg-surface);
+    transition: background 0.12s, border-color 0.12s;
+    user-select: none;
+}
+
+.wiki-page-btn:hover:not(.wiki-page-btn--active):not(.wiki-page-btn--disabled) {
+    background: var(--tblr-bg-surface-secondary);
+    border-color: var(--tblr-border-color-dark, #adb5bd);
+    color: var(--tblr-body-color);
+    text-decoration: none;
+}
+
+.wiki-page-btn--active {
+    background: var(--tblr-primary);
+    border-color: var(--tblr-primary);
+    color: #fff;
+    font-weight: 500;
+    cursor: default;
+    pointer-events: none;
+}
+
+.wiki-page-btn--disabled {
+    color: var(--tblr-secondary);
+    cursor: default;
+    pointer-events: none;
+    opacity: 0.5;
+}
+
+.wiki-page-ellipsis {
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    min-width: 28px;
+    height: 34px;
+    font-size: 0.875rem;
+    color: var(--tblr-secondary);
+    user-select: none;
+}

+ 95 - 0
api-v12/resources/views/components/wiki/pagination.blade.php

@@ -0,0 +1,95 @@
+{{-- resources/views/components/wiki/pagination.blade.php --}}
+{{--
+    props:
+      $pagination  array  { total, per_page, current_page, last_page }
+      $routeName   string  route name
+      $routeParams array   route params (不含分页参数)
+      $queryParams array   额外 query string 参数,如 ['q'=>'...', 'category'=>'...']
+--}}
+@props([
+    'pagination',
+    'routeName',
+    'routeParams'  => [],
+    'queryParams'  => [],
+])
+
+@php
+    $current  = $pagination['current_page'];
+    $last     = $pagination['last_page'];
+
+    if ($last <= 1) return;  // 只有一页不渲染
+
+    // 生成带页码的 URL
+    $pageUrl = function (int $page) use ($routeName, $routeParams, $queryParams): string {
+        $qs = array_merge($queryParams, ['page' => $page]);
+        return route($routeName, $routeParams) . '?' . http_build_query($qs);
+    };
+
+    // 计算显示的页码范围(当前页前后各 2 页)
+    $window = 2;
+    $start  = max(1, $current - $window);
+    $end    = min($last, $current + $window);
+@endphp
+
+<nav class="wiki-pagination" aria-label="分页导航">
+
+    {{-- 上一页 --}}
+    @if ($current > 1)
+        <a class="wiki-page-btn" href="{{ $pageUrl($current - 1) }}" aria-label="上一页">
+            <svg width="14" height="14" viewBox="0 0 16 16" fill="none">
+                <path d="M10 12L6 8l4-4" stroke="currentColor" stroke-width="1.5"
+                      stroke-linecap="round" stroke-linejoin="round"/>
+            </svg>
+        </a>
+    @else
+        <span class="wiki-page-btn wiki-page-btn--disabled" aria-disabled="true">
+            <svg width="14" height="14" viewBox="0 0 16 16" fill="none">
+                <path d="M10 12L6 8l4-4" stroke="currentColor" stroke-width="1.5"
+                      stroke-linecap="round" stroke-linejoin="round"/>
+            </svg>
+        </span>
+    @endif
+
+    {{-- 第一页 + 省略号 --}}
+    @if ($start > 1)
+        <a class="wiki-page-btn" href="{{ $pageUrl(1) }}">1</a>
+        @if ($start > 2)
+            <span class="wiki-page-ellipsis">…</span>
+        @endif
+    @endif
+
+    {{-- 页码窗口 --}}
+    @for ($p = $start; $p <= $end; $p++)
+        @if ($p === $current)
+            <span class="wiki-page-btn wiki-page-btn--active" aria-current="page">{{ $p }}</span>
+        @else
+            <a class="wiki-page-btn" href="{{ $pageUrl($p) }}">{{ $p }}</a>
+        @endif
+    @endfor
+
+    {{-- 省略号 + 最后一页 --}}
+    @if ($end < $last)
+        @if ($end < $last - 1)
+            <span class="wiki-page-ellipsis">…</span>
+        @endif
+        <a class="wiki-page-btn" href="{{ $pageUrl($last) }}">{{ $last }}</a>
+    @endif
+
+    {{-- 下一页 --}}
+    @if ($current < $last)
+        <a class="wiki-page-btn" href="{{ $pageUrl($current + 1) }}" aria-label="下一页">
+            <svg width="14" height="14" viewBox="0 0 16 16" fill="none">
+                <path d="M6 4l4 4-4 4" stroke="currentColor" stroke-width="1.5"
+                      stroke-linecap="round" stroke-linejoin="round"/>
+            </svg>
+        </a>
+    @else
+        <span class="wiki-page-btn wiki-page-btn--disabled" aria-disabled="true">
+            <svg width="14" height="14" viewBox="0 0 16 16" fill="none">
+                <path d="M6 4l4 4-4 4" stroke="currentColor" stroke-width="1.5"
+                      stroke-linecap="round" stroke-linejoin="round"/>
+            </svg>
+        </span>
+    @endif
+
+</nav>

+ 23 - 0
api-v12/resources/views/components/wiki/search-result-card.blade.php

@@ -0,0 +1,23 @@
+{{-- resources/views/components/wiki/search-result-card.blade.php --}}
+@props(['result', 'lang'])
+
+<div class="wiki-search-card">
+
+    <div class="wiki-search-card-header">
+        <a class="wiki-search-card-title"
+           href="{{ route('library.wiki.show', [$result['lang'], $result['word']]) }}">
+            {{ $result['zh'] }}
+            <span class="wiki-search-card-word">{{ $result['word'] }}</span>
+        </a>
+        <x-wiki.quality-badge :quality="$result['quality']" />
+    </div>
+
+    <p class="wiki-search-card-snippet">{!! $result['snippet'] !!}</p>
+
+    <div class="wiki-search-card-meta">
+        <span class="wiki-search-card-category">{{ $result['category'] }}</span>
+        <span class="wiki-search-card-sep">·</span>
+        <span class="wiki-search-card-date">更新于 {{ $result['updated'] }}</span>
+    </div>
+
+</div>

+ 126 - 0
api-v12/resources/views/wiki/search.blade.php

@@ -0,0 +1,126 @@
+{{-- resources/views/wiki/search.blade.php --}}
+@extends('wiki.layouts.app')
+
+@section('title', $query ? '"' . $query . '" 的搜索结果 · WikiPāli' : '搜索 · WikiPāli')
+
+@section('wiki-content')
+
+{{-- 搜索栏 --}}
+<div class="wiki-search-bar-wrap">
+    <form action="{{ route('library.search') }}"
+        method="GET"
+        class="wiki-search-form">
+        <div class="input-group">
+            <input
+                type="text"
+                name="q"
+                class="form-control wiki-search-input"
+                value="{{ $query }}"
+                placeholder="搜索条目、巴利文、梵文…"
+                autofocus />
+            @if ($category !== 'all')
+            <input type="hidden" name="category" value="{{ $category }}">
+            @endif
+            <button class="btn btn-primary" type="submit">搜索</button>
+        </div>
+    </form>
+</div>
+
+{{-- 结果摘要 --}}
+<div class="wiki-search-summary">
+    @if ($query)
+    搜索
+    <strong>「{{ $query }}」</strong>
+    @if ($pagination['total'] > 0)
+    ,共找到 <strong>{{ $pagination['total'] }}</strong> 条结果
+    @if ($pagination['last_page'] > 1)
+    (第 {{ $pagination['current_page'] }} / {{ $pagination['last_page'] }} 页)
+    @endif
+    @else
+    ,未找到相关条目
+    @endif
+    @endif
+</div>
+
+{{-- 结果列表 --}}
+@if (count($results) > 0)
+
+<div class="wiki-card wiki-search-results">
+    @foreach ($results as $result)
+    <x-wiki.search-result-card :result="$result" :lang="$lang" />
+    @endforeach
+</div>
+
+{{-- 分页 --}}
+@if ($pagination['last_page'] > 1)
+<x-wiki.pagination
+    :pagination="$pagination"
+    routeName="library.search"
+    :queryParams="array_filter(['q' => $query,'lang' => $lang, 'category' => $category === 'all' ? null : $category])" />
+@endif
+
+@else
+
+{{-- 空状态 --}}
+<div class="wiki-card wiki-empty-state">
+    <div class="wiki-empty-icon">
+        <svg width="32" height="32" viewBox="0 0 24 24" fill="none"
+            stroke="currentColor" stroke-width="1.5">
+            <circle cx="11" cy="11" r="8" />
+            <path d="M21 21l-4.35-4.35" stroke-linecap="round" />
+            <path d="M8 11h6M11 8v6" stroke-linecap="round" />
+        </svg>
+    </div>
+    <div class="wiki-empty-title">未找到相关条目</div>
+    <div class="wiki-empty-desc">
+        请尝试其他关键词
+    </div>
+</div>
+
+@endif
+
+@endsection
+
+@section('wiki-sidebar')
+
+{{-- 分类筛选 --}}
+<div class="wiki-sidebar-section">
+    <div class="wiki-sidebar-title">按分类筛选</div>
+    <ul class="wiki-cat-list">
+        <li>
+            <a href="{{ route('library.search') }}?q={{ urlencode($query) }}"
+                class="{{ $category === 'all' ? 'active' : '' }}">
+                全部
+                <span class="wiki-cat-count">{{ $pagination['total'] }}</span>
+            </a>
+        </li>
+        @if(isset($filters))
+        @foreach ($filters as $cat)
+        <li>
+            <a href="{{ route('library.search') }}?q={{ urlencode($query) }}&category={{ $cat->key }}"
+                class="{{ $category === $cat->key ? 'active' : '' }}">
+                {{ $cat->key }}
+                <span class="wiki-cat-count">{{ $cat->doc_count }}</span>
+            </a>
+        </li>
+        @endforeach
+        @endif
+
+    </ul>
+</div>
+
+{{-- 近似词条(无结果时显示) --}}
+@if (count($results) === 0 && $query)
+<div class="wiki-sidebar-section">
+    <div class="wiki-sidebar-title">你可能在找</div>
+    <ul class="wiki-related-list">
+        <li>
+            <a href="{{ route('library.search', ['lang' => $lang]) }}?q={{ urlencode(substr($query, 0, -1)) }}">
+                {{ substr($query, 0, -1) }}
+            </a>
+        </li>
+    </ul>
+</div>
+@endif
+
+@endsection