Browse Source

Merge pull request #2360 from visuddhinanda/development

Development
visuddhinanda 1 day ago
parent
commit
1f0b074da9
94 changed files with 6995 additions and 5145 deletions
  1. 2 16
      api-v12/app/Console/Commands/CreateOpenSearchIndex.php
  2. 11 15
      api-v12/app/Console/Commands/ExportPaliSynonyms.php
  3. 41 0
      api-v12/app/Console/Commands/IndexOpenSearch.php
  4. 33 0
      api-v12/app/Console/Commands/PostInstall.php
  5. 71 0
      api-v12/app/Console/Commands/UpdateOpenSearchIndex.php
  6. 4 0
      api-v12/app/Console/Kernel.php
  7. 10 2
      api-v12/app/DTO/Search/HitItemDTO.php
  8. 16 10
      api-v12/app/Http/Api/TemplateRender.php
  9. 311 48
      api-v12/app/Http/Controllers/CategoryController.php
  10. 6 6
      api-v12/app/Http/Controllers/Library/AnthologyController.php
  11. 29 18
      api-v12/app/Http/Controllers/Library/BookController.php
  12. 3 3
      api-v12/app/Http/Controllers/Library/SearchController.php
  13. 5 4
      api-v12/app/Http/Controllers/Library/WikiController.php
  14. 4 1
      api-v12/app/Providers/AppServiceProvider.php
  15. 47 1
      api-v12/app/Services/OpenSearchService.php
  16. 53 1
      api-v12/app/Services/PaliContentService.php
  17. 91 0
      api-v12/app/View/Composers/BlogViewComposer.php
  18. 384 0
      api-v12/documents/wikipali-frontend-spec.md
  19. 0 0
      api-v12/public/assets/css/blog/style.min.css
  20. 0 11
      api-v12/resources/css/app.css
  21. 16 0
      api-v12/resources/css/base/_reset.css
  22. 13 0
      api-v12/resources/css/base/_typography.css
  23. 39 0
      api-v12/resources/css/base/_variables.css
  24. 4 0
      api-v12/resources/css/components/_badge.css
  25. 199 0
      api-v12/resources/css/components/_card-book.css
  26. 287 0
      api-v12/resources/css/components/_card.css
  27. 64 0
      api-v12/resources/css/components/_pagination.css
  28. 13 0
      api-v12/resources/css/components/_search-input.css
  29. 9 84
      api-v12/resources/css/components/_search-results.css
  30. 92 0
      api-v12/resources/css/layout/_drawer.css
  31. 4 0
      api-v12/resources/css/layout/_footer.css
  32. 85 0
      api-v12/resources/css/layout/_grid.css
  33. 76 0
      api-v12/resources/css/layout/_hero.css
  34. 119 0
      api-v12/resources/css/layout/_navbar.css
  35. 4 0
      api-v12/resources/css/layout/_toolbar.css
  36. 33 0
      api-v12/resources/css/library.css
  37. 0 478
      api-v12/resources/css/main.css
  38. 284 0
      api-v12/resources/css/modules/_anthology.css
  39. 308 0
      api-v12/resources/css/modules/_library-index.css
  40. 313 0
      api-v12/resources/css/modules/_reader.css
  41. 355 0
      api-v12/resources/css/modules/_tipitaka.css
  42. 503 0
      api-v12/resources/css/modules/_wiki.css
  43. 5 237
      api-v12/resources/css/reader.css
  44. 360 0
      api-v12/resources/css/tufte.css
  45. 0 208
      api-v12/resources/css/wiki-content.css
  46. 0 574
      api-v12/resources/css/wiki.css
  47. 7 2
      api-v12/resources/js/app.js
  48. 30 0
      api-v12/resources/js/modules/navbar.js
  49. 233 0
      api-v12/resources/js/modules/term-tooltip.js
  50. 73 43
      api-v12/resources/js/term-tooltip.js
  51. 29 18
      api-v12/resources/views/blog/category.blade.php
  52. 53 413
      api-v12/resources/views/blog/index.blade.php
  53. 296 468
      api-v12/resources/views/blog/layouts/app.blade.php
  54. 0 28
      api-v12/resources/views/components/book-item.blade.php
  55. 0 118
      api-v12/resources/views/components/book-list.blade.php
  56. 24 0
      api-v12/resources/views/components/library/footer.blade.php
  57. 3 15
      api-v12/resources/views/components/library/header.blade.php
  58. 62 0
      api-v12/resources/views/components/library/navbar.blade.php
  59. 40 0
      api-v12/resources/views/components/ui/author-avatar.blade.php
  60. 46 0
      api-v12/resources/views/components/ui/book-cover.blade.php
  61. 21 0
      api-v12/resources/views/components/ui/book-grid.blade.php
  62. 69 0
      api-v12/resources/views/components/ui/card-anthology.blade.php
  63. 52 0
      api-v12/resources/views/components/ui/card-book.blade.php
  64. 35 0
      api-v12/resources/views/components/ui/empty-state.blade.php
  65. 42 0
      api-v12/resources/views/components/ui/search-input.blade.php
  66. 0 5
      api-v12/resources/views/components/wiki/search-box.blade.php
  67. 26 0
      api-v12/resources/views/layouts/base.blade.php
  68. 37 0
      api-v12/resources/views/layouts/library.blade.php
  69. 13 0
      api-v12/resources/views/layouts/reader.blade.php
  70. 48 315
      api-v12/resources/views/library/anthology/index.blade.php
  71. 17 16
      api-v12/resources/views/library/anthology/pagination.blade.php
  72. 68 597
      api-v12/resources/views/library/anthology/show.blade.php
  73. 28 0
      api-v12/resources/views/library/book/_toc.blade.php
  74. 316 505
      api-v12/resources/views/library/book/read.blade.php
  75. 0 143
      api-v12/resources/views/library/book/show.blade.php
  76. 0 62
      api-v12/resources/views/library/category.blade.php
  77. 154 65
      api-v12/resources/views/library/index.blade.php
  78. 4 9
      api-v12/resources/views/library/layouts/app.blade.php
  79. 112 0
      api-v12/resources/views/library/search.blade.php
  80. 239 0
      api-v12/resources/views/library/tipitaka/category.blade.php
  81. 67 0
      api-v12/resources/views/library/tipitaka/index.blade.php
  82. 165 0
      api-v12/resources/views/library/tipitaka/show.blade.php
  83. 79 0
      api-v12/resources/views/library/wiki/home.blade.php
  84. 1 1
      api-v12/resources/views/library/wiki/index.blade.php
  85. 78 0
      api-v12/resources/views/library/wiki/layouts/app.blade.php
  86. 5 1
      api-v12/resources/views/library/wiki/show.blade.php
  87. 0 337
      api-v12/resources/views/wiki/home.blade.php
  88. 0 69
      api-v12/resources/views/wiki/layouts/app.blade.php
  89. 0 126
      api-v12/resources/views/wiki/search.blade.php
  90. 5 3
      api-v12/routes/web.php
  91. 6 3
      api-v12/vite.config.js
  92. 61 27
      dashboard-v6/src/components/article/components/ArticleReaderToolbar.tsx
  93. 42 39
      dashboard-v6/src/locales/en-US/buttons.ts
  94. 3 0
      dashboard-v6/src/locales/zh-Hans/buttons.ts

+ 2 - 16
api-v12/app/Console/Commands/CreateOpenSearchIndex.php

@@ -64,26 +64,12 @@ class CreateOpenSearchIndex extends Command
         } catch (\Exception $e) {
             if (str_contains($e->getMessage(), 'exists')) {
                 $this->warn('Index already exists, attempting to update...');
-                try {
-                    $update = $openSearch->updateIndex();
-                    if (!empty($update['settings']) && $update['settings']['acknowledged']) {
-                        $this->info('Index settings updated successfully');
-                    }
-                    if (!empty($update['mappings']) && $update['mappings']['acknowledged']) {
-                        $this->info('Index mappings updated successfully');
-                    }
-                    if (empty($update['settings']) && empty($update['mappings'])) {
-                        $this->warn('No settings or mappings provided for update');
-                    }
-                } catch (\Exception $updateException) {
-                    $this->error('Failed to update index: ' . $updateException->getMessage());
-                    return 1;
-                }
             } else {
                 $this->error('Failed to create index: ' . $e->getMessage());
-                return 1;
             }
+            return 1;
         }
+
         return 0;
     }
 }

+ 11 - 15
api-v12/app/Console/Commands/ExportPaliSynonyms.php

@@ -13,17 +13,17 @@ class ExportPaliSynonyms extends Command
 {
     /**
      * The name and signature of the console command.
-     * php artisan export:pali.synonyms
+     * php artisan export:pali.synonyms --output=
      * @var string
      */
-    protected $signature = 'export:pali.synonyms';
+    protected $signature = 'export:pali.synonyms {--output=}';
 
     /**
      * The console command description.
      *
      * @var string
      */
-    protected $description = 'Command description';
+    protected $description = '导出openSearch用的巴利语变格表';
 
     /**
      * Create a new command instance.
@@ -42,27 +42,23 @@ class ExportPaliSynonyms extends Command
      */
     public function handle()
     {
+        if (!$this->option('output')) {
+            $this->error('please set output file option --output=file');
+            return 1;
+        }
         //irregular
         $dictId = ['4d3a0d92-0adc-4052-80f5-512a2603d0e8'];
         //regular
         $dictId[] = DictApi::getSysDict('system_regular');
-        $path = storage_path('app/export/fts');
-        if (!is_dir($path)) {
-            $res = mkdir($path, 0700, true);
-            if (!$res) {
-                Log::error('mkdir fail path=' . $path);
-                return 1;
-            }
-        }
 
-        $filename = "/pali_synonyms.txt";
-        $fp = fopen($path . $filename, 'w') or die("Unable to open file!");
-        foreach ($dictId as $key => $dict) {
+        $filename = $this->option('output');
+        $fp = fopen($filename, 'w') or die("Unable to open file!");
+        foreach ($dictId as  $dict) {
             $parents = UserDict::where('dict_id', $dict)
                 ->select('parent')
                 ->groupBy('parent')->cursor();
 
-            foreach ($parents as $key => $parent) {
+            foreach ($parents as  $parent) {
                 $words = UserDict::where('dict_id', $dict)
                     ->where('parent', $parent->parent)
                     ->select('word')

+ 41 - 0
api-v12/app/Console/Commands/IndexOpenSearch.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Services\OpenSearchService;
+use Illuminate\Support\Facades\App;
+
+
+class IndexOpenSearch extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'app:index-open-search';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '如果是新的索引 全量更新openSearch';
+
+    /**
+     * Execute the console command.
+     */
+    public function handle()
+    {
+        //
+        $service = app(OpenSearchService::class);
+        if ($service->count() === 0) {
+            $this->call('opensearch:index-pali');
+        } else {
+            if (App::environment('local')) {
+                $this->info('data exist');
+            }
+        }
+    }
+}

+ 33 - 0
api-v12/app/Console/Commands/PostInstall.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+
+class DeplyInit extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'app:post-install';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Execute the console command.
+     */
+    public function handle()
+    {
+        //
+        $this->info('deploy init start');
+        $this->call('create:opensearch.index');
+        $this->info('deploy init done');
+    }
+}

+ 71 - 0
api-v12/app/Console/Commands/UpdateOpenSearchIndex.php

@@ -0,0 +1,71 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Services\OpenSearchService;
+use Illuminate\Console\Command;
+
+class UpdateOpenSearchIndex extends Command
+{
+    /**
+     * The name and signature of the console command.
+     * php artisan create:opensearch.index
+     * @var string
+     */
+    protected $signature = 'update:opensearch.index';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        $openSearch = app(OpenSearchService::class);
+
+        // Test OpenSearch connection
+        $open = $openSearch->testConnection();
+        if ($open[0]) {
+            $this->info($open[1]);
+        } else {
+            $this->error($open[1]);
+            return 1; // Exit with error code
+        }
+
+        // Attempt to create or update index
+        $this->warn('Index already exists, attempting to update...');
+        try {
+            $update = $openSearch->updateIndex();
+            if (!empty($update['settings']) && $update['settings']['acknowledged']) {
+                $this->info('Index settings updated successfully');
+            }
+            if (!empty($update['mappings']) && $update['mappings']['acknowledged']) {
+                $this->info('Index mappings updated successfully');
+            }
+            if (empty($update['settings']) && empty($update['mappings'])) {
+                $this->warn('No settings or mappings provided for update');
+            }
+        } catch (\Exception $updateException) {
+            $this->error('Failed to update index: ' . $updateException->getMessage());
+            return 1;
+        }
+        return 0;
+    }
+}

+ 4 - 0
api-v12/app/Console/Kernel.php

@@ -23,6 +23,10 @@ class Kernel extends ConsoleKernel
         $schedule->command('upgrade:weekly')
             ->weekly()
             ->emailOutputOnFailure(config("mint.email.ScheduleEmailOutputOnFailure"));
+
+        $schedule->command('app:index-open-search')
+            ->everyFiveMinutes()
+            ->emailOutputOnFailure(config("mint.email.ScheduleEmailOutputOnFailure"));
     }
 
     /**

+ 10 - 2
api-v12/app/DTO/Search/HitItemDTO.php

@@ -18,7 +18,7 @@ class HitItemDTO
     public static function fromArray(array $data): self
     {
         $source = $data['_source'];
-        $highlight = $data['highlight'];
+        $highlight = $data['highlight'] ?? null;
         $highlightArray = [];
         if (is_array($highlight)) {
             foreach ($highlight as $key => $value) {
@@ -33,6 +33,14 @@ class HitItemDTO
                 $contentArray[] = $value;
             }
         }
+        $titleArray = [];
+        if (is_array($source['title'])) {
+            foreach ($source['title'] as $key => $value) {
+                $titleArray[] = $value;
+            }
+        }
+        $title = implode('', $titleArray);
+
         $category = [];
         if (is_array($source['category'])) {
             $category = $source['category'];
@@ -43,7 +51,7 @@ class HitItemDTO
         return new self(
             id: $source['id'],
             score: $data['_score'],
-            title: $source['title']['pali'] ?? '',
+            title: $title,
             content: implode('', $contentArray),
             path: $source['path'] ?? '',
             category: $category,

+ 16 - 10
api-v12/app/Http/Api/TemplateRender.php

@@ -446,7 +446,7 @@ class TemplateRender
     private  function render_note()
     {
         $note = $this->get_param($this->param, "text", 1);
-        $trigger = $this->get_param($this->param, "trigger", 2);
+        $trigger = $this->get_param($this->param, "trigger", 2, '');
         $props = ["note" => $note];
         $innerString = "";
         if (!empty($trigger)) {
@@ -487,18 +487,19 @@ class TemplateRender
                     $GLOBALS['note_sn'] = 1;
                     $GLOBALS['note'] = array();
                 }
+                $noteContent = MdRender::render(
+                    $props["note"],
+                    $this->channel_id,
+                    null,
+                    'read',
+                    'translation',
+                    'markdown',
+                    'html'
+                );
                 $GLOBALS['note'][] = [
                     'sn' => $GLOBALS['note_sn'],
                     'trigger' => $trigger,
-                    'content' => MdRender::render(
-                        $props["note"],
-                        $this->channel_id,
-                        null,
-                        'read',
-                        'translation',
-                        'markdown',
-                        'html'
-                    ),
+                    'content' => $noteContent,
                 ];
 
                 $link = "<a href='#footnote-" . $GLOBALS['note_sn'] . "' name='note-" . $GLOBALS['note_sn'] . "'>";
@@ -507,6 +508,11 @@ class TemplateRender
                 } else {
                     $output = $link . $trigger . "</a>";
                 }
+                $output = "<label for=\"sn-{$GLOBALS['note_sn']}\"
+                class=\"margin-toggle sidenote-number\" >{$trigger}</label>
+                <input type=\"checkbox\" id=\"sn-{$GLOBALS['note_sn']}\"
+                class=\"margin-toggle\"/>
+                <span class=\"sidenote\">{$noteContent}</span>";
                 break;
             case 'text':
                 $output = $trigger;

+ 311 - 48
api-v12/app/Http/Controllers/CategoryController.php

@@ -2,6 +2,9 @@
 
 namespace App\Http\Controllers;
 
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\File;
+
 use Illuminate\Support\Facades\DB;
 use App\Models\PaliText;
 use App\Models\ProgressChapter;
@@ -11,7 +14,48 @@ use App\Models\TagMap;
 
 class CategoryController extends Controller
 {
+    // 封面渐变色池:uid 首字节取余循环,保证同一文集颜色稳定
+    private array $coverGradients = [
+        'linear-gradient(160deg, #2d1020, #ae6b8b)',
+        'linear-gradient(160deg, #1a2d10,rgba(75, 114, 36, 0.61))',
+        'linear-gradient(160deg, #0d1f3c,rgb(55, 98, 150))',
+        'linear-gradient(160deg, #2d1020,rgb(151, 69, 94))',
+        'linear-gradient(160deg, #1a1a2d,rgb(76, 68, 146))',
+        'linear-gradient(160deg, #1a2820,rgb(55, 124, 99))',
+    ];
+    // -------------------------------------------------------------------------
+    // 从 uid / id 字符串中提取一个稳定的整数,用于色池取余
+    // -------------------------------------------------------------------------
+    private function colorIndex(string $uid): int
+    {
+        return hexdec(substr(str_replace('-', '', $uid), 0, 4)) % 255;
+    }
     protected static int $nextId = 1;
+    public function home()
+    {
+        $categories = $this->loadCategories();
+
+        // 获取一级分类和对应的书籍
+        $categoryData = [];
+        foreach ($categories as $category) {
+            if ($category['level'] == 1) {
+                $children = $this->subCategories($categories, $category['id']);
+                $categoryData[] = [
+                    'category' => $category,
+                    'children' => $children,
+                ];
+            }
+        }
+        $recentBooks = $this->getRecent();
+
+        return view('library.index', compact(
+            'categoryData',
+            'categories',
+            'recentBooks'
+        ));
+    }
+
+
     public function index()
     {
         $categories = $this->loadCategories();
@@ -27,39 +71,233 @@ class CategoryController extends Controller
                 ];
             }
         }
-        $update = $this->getUpdateBooks();
 
-        return view('library.index', compact('categoryData', 'categories', 'update'));
+        return view('library.index', compact('categoryData', 'categories'));
     }
 
-    public function show($id)
+    // app/Http/Controllers/Library/CategoryController.php
+    // category() 方法修改版
+    // 变更:
+    //   1. $id 改为可选参数,无参数时显示顶级分类(首页复用)
+    //   2. 新增 $filters 过滤参数(type / lang / author / sort)
+    //   3. 新增右边栏数据:$recommended / $activeAuthors
+    //   4. 新增 $filterOptions(过滤器选项 + 计数)
+    //   5. 新增 $totalCount
+
+    public function category(?int $id = null)
     {
+
         $categories = $this->loadCategories();
 
-        $currentCategory = collect($categories)->firstWhere('id', $id);
-        if (!$currentCategory) {
-            abort(404);
+        // ── 当前分类 ──────────────────────────────────────────
+        if ($id) {
+            $currentCategory = collect($categories)->firstWhere('id', $id);
+            if (!$currentCategory) {
+                abort(404);
+            }
+            $breadcrumbs = $this->getBreadcrumbs($currentCategory, $categories);
+        } else {
+            // 首页:虚拟顶级分类
+            $currentCategory = ['id' => null, 'name' => '三藏'];
+            $breadcrumbs     = [];
         }
 
-        // 获取子分类
-        $subCategories = array_filter($categories, function ($cat) use ($id) {
-            return $cat['parent_id'] == $id;
-        });
+        // ── 子分类 ─────────────────────────────────────────────
+        $subCategories = array_values(array_filter(
+            $categories,
+            fn($cat) => $cat['parent_id'] == $id
+        ));
 
-        // 获取该分类下的书籍
-        $categoryBooks = $this->getBooks($categories, $id);
-        // 获取面包屑
-        $breadcrumbs = $this->getBreadcrumbs($currentCategory, $categories);
+        // ── 过滤参数 ────────────────────────────────────────────
+        $selectedType   = request('type',   'all');
+        $selectedLang   = request('lang',   'all');
+        $selectedAuthor = request('author', 'all');
+        $selectedSort   = request('sort',   'updated_at');
+
+        $selected = [
+            'type'   => $selectedType,
+            'lang'   => $selectedLang,
+            'author' => $selectedAuthor,
+            'sort'   => $selectedSort,
+        ];
+
+        // ── 书籍列表(过滤+排序,真实实现替换此处) ──────────────
+        $categoryBooks = $this->getBooks($categories, $id, $selected);
+        // TODO: 将 $selected 传入 getBooks() 做实际过滤
 
-        return view('library.category', compact('currentCategory', 'subCategories', 'categoryBooks', 'breadcrumbs'));
+        $totalCount = count($categoryBooks);
+
+        // ── 过滤器选项(mock,真实实现从书籍数据聚合) ────────────
+        $filterOptions = [
+            'types' => [
+                ['value' => 'all',         'label' => '全部',    'count' => $totalCount],
+                ['value' => 'original',    'label' => '原文',    'count' => 0],
+                ['value' => 'translation', 'label' => '译文',    'count' => 0],
+                ['value' => 'nissaya',     'label' => 'Nissaya', 'count' => 0],
+            ],
+            'languages' => [
+                ['value' => 'all',  'label' => '全部',   'count' => $totalCount],
+                ['value' => 'zh-Hans',   'label' => '简体中文',   'count' => 0],
+                ['value' => 'zh-Hant',   'label' => '繁体中文',   'count' => 0],
+                ['value' => 'pi',   'label' => '巴利语', 'count' => 0],
+                ['value' => 'en',   'label' => '英语',   'count' => 0],
+            ],
+            'authors' => $this->getAuthorOptions($categoryBooks),
+        ];
+
+        // ── 右边栏:本周推荐(mock) ────────────────────────────
+        $recommended = [
+            ['id' => 1, 'title' => '相应部·因缘篇',  'category' => '经藏'],
+            ['id' => 2, 'title' => '法句经',          'category' => '经藏'],
+            ['id' => 3, 'title' => '清净道论',        'category' => '注释'],
+            ['id' => 4, 'title' => '律藏·波罗夷',    'category' => '律藏'],
+            ['id' => 5, 'title' => '长部·梵网经',    'category' => '经藏'],
+        ];
+
+        // ── 右边栏:活跃译者(mock) ────────────────────────────
+        $activeAuthors = [
+            [
+                'name'    => 'Bhikkhu Bodhi',
+                'avatar'  => null,
+                'color'   => '#2d5a8e',
+                'initials' => 'BB',
+                'count'   => 24,
+            ],
+            [
+                'name'    => 'Bhikkhu Sujato',
+                'avatar'  => null,
+                'color'   => '#5a2d8e',
+                'initials' => 'BS',
+                'count'   => 18,
+            ],
+            [
+                'name'    => 'Buddhaghosa',
+                'avatar'  => null,
+                'color'   => '#8e5a2d',
+                'initials' => 'BG',
+                'count'   => 12,
+            ],
+            [
+                'name'    => 'Bhikkhu Brahmali',
+                'avatar'  => null,
+                'color'   => '#2d8e5a',
+                'initials' => 'BR',
+                'count'   => 9,
+            ],
+        ];
+
+        $types = $this->types();
+
+        return view('library.tipitaka.category', compact(
+            'currentCategory',
+            'subCategories',
+            'categoryBooks',
+            'breadcrumbs',
+            'types',
+            'selected',
+            'filterOptions',
+            'totalCount',
+            'recommended',
+            'activeAuthors',
+        ));
+    }
+
+    // ── 辅助:从书籍列表聚合作者选项(mock,真实实现替换) ─────────
+    private function getAuthorOptions(array $books): array
+    {
+        // TODO: 从 $books 聚合真实作者列表
+        return [
+            ['value' => 'all',             'label' => '全部作者',      'count' => count($books)],
+            ['value' => 'bhikkhu-bodhi',   'label' => 'Bhikkhu Bodhi', 'count' => 0],
+            ['value' => 'bhikkhu-sujato',  'label' => 'Bhikkhu Sujato', 'count' => 0],
+            ['value' => 'buddhaghosa',     'label' => 'Buddhaghosa',   'count' => 0],
+            ['value' => 'bhikkhu-brahmali', 'label' => 'Bhikkhu Brahmali', 'count' => 0],
+        ];
     }
 
+    private function types()
+    {
+        return [
+            ['id' => '1', 'name' => 'sutta'],
+            ['id' => '48', 'name' => 'vinaya'],
+            ['id' => '66', 'name' => 'abhidhamma'],
+            ['id' => '82', 'name' => 'añña']
+        ];
+    }
+
+
+
     private function subCategories($categories, int $id)
     {
         return array_filter($categories, function ($cat) use ($id) {
             return $cat['parent_id'] == $id;
         });
     }
+    private function getRecent()
+    {
+        return [
+            [
+                'id'         => 'book-001',
+                'title'      => '相应部·因缘篇',
+                'author'     => 'Bhikkhu Bodhi',
+                'cover'      => null,                          // 无封面时显示渐变
+                'cover_gradient' => 'linear-gradient(135deg, #2d5a8e 0%, #1a3a5c 100%)',
+                'updated_at' => '2小时前',
+                'is_new'     => true,                          // true=新增, false=更新
+                'category'   => '经藏',
+            ],
+            [
+                'id'         => 'book-002',
+                'title'      => '长部·梵网经',
+                'author'     => 'Bhikkhu Sujato',
+                'cover'      => null,
+                'cover_gradient' => 'linear-gradient(135deg, #5a2d8e 0%, #3a1a5c 100%)',
+                'updated_at' => '昨天',
+                'is_new'     => false,
+                'category'   => '经藏',
+            ],
+            [
+                'id'         => 'book-003',
+                'title'      => '法句经注',
+                'author'     => 'Buddhaghosa',
+                'cover'      => null,
+                'cover_gradient' => 'linear-gradient(135deg, #8e5a2d 0%, #5c3a1a 100%)',
+                'updated_at' => '3天前',
+                'is_new'     => false,
+                'category'   => '注释',
+            ],
+            [
+                'id'         => 'book-004',
+                'title'      => '律藏·波罗夷',
+                'author'     => 'Bhikkhu Brahmali',
+                'cover'      => null,
+                'cover_gradient' => 'linear-gradient(135deg, #2d8e5a 0%, #1a5c3a 100%)',
+                'updated_at' => '5天前',
+                'is_new'     => true,
+                'category'   => '律藏',
+            ],
+            [
+                'id'         => 'book-005',
+                'title'      => '清净道论',
+                'author'     => 'Buddhaghosa',
+                'cover'      => null,
+                'cover_gradient' => 'linear-gradient(135deg, #8e2d2d 0%, #5c1a1a 100%)',
+                'updated_at' => '1周前',
+                'is_new'     => false,
+                'category'   => '注释',
+            ],
+            [
+                'id'         => 'book-006',
+                'title'      => '增支部·一集',
+                'author'     => 'Bhikkhu Bodhi',
+                'cover'      => null,
+                'cover_gradient' => 'linear-gradient(135deg, #2d7a8e 0%, #1a4a5c 100%)',
+                'updated_at' => '1周前',
+                'is_new'     => false,
+                'category'   => '经藏',
+            ],
+        ];
+    }
 
     private function getUpdateBooks()
     {
@@ -77,25 +315,25 @@ class CategoryController extends Controller
 
         return $this->getBooksInfo($books);
     }
-    private function getBooks($categories, $id)
+    private function getBooks($categories, $id, $filters)
     {
-        $currentCategory = collect($categories)->firstWhere('id', $id);
-        if (!$currentCategory) {
-            abort(404);
-        }
 
-        // 标签查章节
-        $tagNames = $currentCategory['tag'];
-        $tm = (new TagMap)->getTable();
-        $tg = (new Tag)->getTable();
-        $pt = (new PaliText)->getTable();
-        $where1 = " where co = " . count($tagNames);
-        $a = implode(",", array_fill(0, count($tagNames), '?'));
-        $in1 = "and t.name in ({$a})";
-        $param = $tagNames;
-        $where2 = "where level = 1";
-        $query = "
-                        select uid as id,book,paragraph,level,toc as title,chapter_strlen,parent,path from (
+        if ($id) {
+            $currentCategory = collect($categories)->firstWhere('id', $id);
+            if (!$currentCategory) {
+                abort(404);
+            }
+            // 标签查章节
+            $tagNames = $currentCategory['tag'];
+            $tm = (new TagMap)->getTable();
+            $tg = (new Tag)->getTable();
+            $pt = (new PaliText)->getTable();
+            $where1 = " where co = " . count($tagNames);
+            $a = implode(",", array_fill(0, count($tagNames), '?'));
+            $in1 = "and t.name in ({$a})";
+            $param = $tagNames;
+            $where2 = "where level = 1";
+            $query = "select uid as id,book,paragraph,level,toc as title,chapter_strlen,parent,path from (
                             select anchor_id as cid from (
                                 select tm.anchor_id , count(*) as co
                                     from $tm as  tm
@@ -110,20 +348,35 @@ class CategoryController extends Controller
                         $where2
                         order by book,paragraph";
 
-        $chapters = DB::select($query, $param);
-        $chaptersParam = [];
-        foreach ($chapters as $key => $chapter) {
-            $chaptersParam[] = [$chapter->book, $chapter->paragraph];
+            $chapters = DB::select($query, $param);
+            $chaptersParam = [];
+            foreach ($chapters as $key => $chapter) {
+                $chaptersParam[] = [$chapter->book, $chapter->paragraph];
+            }
+            // 获取该分类下的章节
+            $books = ProgressChapter::with('channel.owner')
+                ->whereIns(['progress_chapters.book', 'progress_chapters.para'], $chaptersParam)
+                ->whereHas('channel', function ($query) {
+                    $query->where('status', 30);
+                })
+                ->where('progress', '>', config('mint.library.list_min_progress'))
+                ->get();
+        } else {
+            $booksChapter = PaliText::select(['book', 'paragraph'])->where('level', 1)->get();
+            $chapters = [];
+            foreach ($booksChapter as $key => $value) {
+                $chapters[] = [$value->book, $value->paragraph];
+            }
+            $books = ProgressChapter::with('channel.owner')
+                ->whereHas('channel', function ($query) use ($filters) {
+                    $filters['type'] === 'all' ? $query->where('status', 30) :
+                        $query->where('status', 30)->where('type', $filters['type']);
+                })
+                ->where('progress', '>', config('mint.library.list_min_progress'))
+                ->whereIns(['book', 'para'], $chapters)
+                ->take(100)
+                ->get();
         }
-        // 获取该分类下的章节
-        $books = ProgressChapter::with('channel.owner')
-            ->whereIns(['progress_chapters.book', 'progress_chapters.para'], $chaptersParam)
-            ->whereHas('channel', function ($query) {
-                $query->where('status', 30);
-            })
-            ->where('progress', '>', config('mint.library.list_min_progress'))
-            ->get();
-
         return $this->getBooksInfo($books);
     }
 
@@ -142,13 +395,23 @@ class CategoryController extends Controller
                 return $item->book == $book->book
                     && $item->paragraph == $book->para;
             })?->pcd_book_id;
+
+            $coverFile = "/assets/images/cover/zh-hans/1/{$pcd_book_id}.png";
+            if (File::exists(public_path($coverFile))) {
+                $coverUrl = $coverFile;
+            } else {
+                $coverUrl = null;
+            }
+            $colorIdx = $this->colorIndex($book->uid);
+
             $categoryBooks[] = [
                 "id" => $book->uid,
                 "title" => $title,
                 "author" => $book->channel->name,
                 "publisher" => $book->channel->owner,
                 "type" => __('labels.' . $book->channel->type),
-                "cover" => "/assets/images/cover/zh-hans/1/{$pcd_book_id}.png",
+                "cover" => $coverUrl,
+                'cover_gradient' => $this->coverGradients[$colorIdx % count($this->coverGradients)],
                 "description" => $book->summary ?? "比库戒律的详细说明",
                 "language" => __('language.' . $book->channel->lang),
             ];
@@ -163,7 +426,7 @@ class CategoryController extends Controller
         return $flat;
     }
 
-    public static function flattenWithIds(array $tree, int $parentId = 0, int $level = 1): array
+    public static function flattenWithIds(array $tree,  int $parentId = 0, int $level = 1): array
     {
 
         $flat = [];
@@ -184,7 +447,7 @@ class CategoryController extends Controller
 
             if (isset($node['children']) && is_array($node['children'])) {
                 $childrenLevel = $level + 1;
-                $flat = array_merge($flat, self::flattenWithIds($node['children'], $currentId, $childrenLevel));
+                $flat = array_merge($flat, self::flattenWithIds($node['children'],  $currentId, $childrenLevel));
             }
         }
 

+ 6 - 6
api-v12/app/Http/Controllers/Library/AnthologyController.php

@@ -10,12 +10,12 @@ class AnthologyController extends Controller
 {
     // 封面渐变色池:uid 首字节取余循环,保证同一文集颜色稳定
     private array $coverGradients = [
-        'linear-gradient(160deg, #2d2010, #4a3010)',
-        'linear-gradient(160deg, #1a2d10, #2d4a18)',
-        'linear-gradient(160deg, #0d1f3c, #1a3660)',
-        'linear-gradient(160deg, #2d1020, #4a1830)',
-        'linear-gradient(160deg, #1a1a2d, #2a2a50)',
-        'linear-gradient(160deg, #1a2820, #2d4438)',
+        'linear-gradient(160deg, #2d2010,rgb(94, 60, 20))',
+        'linear-gradient(160deg, #1a2d10,rgb(64, 105, 34))',
+        'linear-gradient(160deg, #0d1f3c,rgb(31, 64, 112))',
+        'linear-gradient(160deg, #2d1020,rgb(109, 35, 71))',
+        'linear-gradient(160deg, #1a1a2d,rgb(48, 48, 92))',
+        'linear-gradient(160deg, #1a2820,rgb(53, 80, 66))',
     ];
 
     // 作者色池:同上,根据 studio.id 首字节取余

+ 29 - 18
api-v12/app/Http/Controllers/Library/BookController.php

@@ -10,6 +10,7 @@ use App\Models\PaliText;
 use App\Models\Sentence;
 
 use App\Services\ChapterService;
+use App\Services\PaliContentService;
 
 class BookController extends Controller
 {
@@ -44,7 +45,7 @@ class BookController extends Controller
             $otherVersions[] = $this->getBookInfo($book);
         });
 
-        return view('library.book.show', compact('book', 'otherVersions'));
+        return view('library.tipitaka.show', compact('book', 'otherVersions'));
     }
 
 
@@ -204,30 +205,40 @@ class BookController extends Controller
             }
         }
 
+        $paraStart = $curr->paragraph;
+        $paraEnd = $endParagraph;
+        $paragraphs = app(PaliContentService::class)->paragraphs(
+            $book->book,
+            $paraStart,
+            $paraEnd,
+            [$book->channel_id],
+            ['mode' => 'read', 'format' => 'html', 'original' => false]
+        );
+
         //获取句子数据
-        $sentences = Sentence::where('book_id', $book->book)
-            ->whereBetween('paragraph', [$curr->paragraph, $endParagraph])
-            ->where('channel_uid', $book->channel_id)
-            ->orderBy('paragraph')
-            ->orderBy('word_start')
-            ->get();
+
         $pali = PaliText::where('book', $book->book)
             ->whereBetween('paragraph', [$curr->paragraph, $endParagraph])
+            ->select(['paragraph', 'level'])
             ->orderBy('paragraph')
             ->get();
         $result = [];
-        for ($i = $curr->paragraph; $i <= $endParagraph; $i++) {
-            $texts = array_filter($sentences->toArray(), function ($sentence) use ($i) {
-                return $sentence['paragraph'] == $i;
-            });
-            $contents = array_map(function ($text) {
-                return $text['content'];
-            }, $texts);
-            $currPali = $pali->firstWhere('paragraph', $i);
+        foreach ($paragraphs as $key => $paragraph) {
+            $content = [];
+            foreach ($paragraph['children'] as $key => $sent) {
+                if (isset($sent['translation'])) {
+                    foreach ($sent['translation'] as $key => $translation) {
+                        $curr = $translation['html'] ?? $translation['content'];
+                        $content[] = "<span class='sentence'>{$curr}</span>";
+                    }
+                }
+            }
+            $currPaliPara = $pali->firstWhere('paragraph', $paragraph['para']);
+            $level = $currPaliPara->level;
             $paragraph = [
-                'id' => $i,
-                'level' => $currPali->level,
-                'text' => [[implode('', $contents)]],
+                'id' => $paragraph['para'],
+                'level' => $level,
+                'text' => [[implode('', $content)]],
             ];
             $result[] = $paragraph;
         }

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

@@ -34,12 +34,12 @@ class SearchController extends Controller
         $results = [];
         foreach ($dto->hits->items as $key => $item) {
             $results[] = [
-                'word'     => 'word',
+                'word'     => $item->id,
                 'zh'       => $item->title,
                 'lang'     => 'pi',
                 'category' => '法义术语',
                 'quality'  => 'featured',
-                'snippet'  => $item->highlight,
+                'snippet'  => !empty($item->highlight) ? $item->highlight : $item->content,
                 'updated'  => '2025-11-12',
             ];
         }
@@ -54,7 +54,7 @@ class SearchController extends Controller
             'last_page'    => max(1, (int) ceil($dto->hits->total / $perPage)),
         ];
 
-        return view('wiki.search', [
+        return view('library.search', [
             'lang'           => $lang,
             'query'          => $query,
             'results'        => $results,

+ 5 - 4
api-v12/app/Http/Controllers/WikiController.php → api-v12/app/Http/Controllers/Library/WikiController.php

@@ -1,7 +1,8 @@
 <?php
 
-namespace App\Http\Controllers;
+namespace App\Http\Controllers\Library;
 
+use App\Http\Controllers\Controller;
 use Illuminate\Http\Request;
 use App\Helpers\WikiContentParser;
 use App\Services\TermService;
@@ -95,7 +96,7 @@ HTML,
         ];
 
 
-        return view('wiki.index', [
+        return view('library.wiki.index', [
             'today'         => $today,
             'featured'      => $this->featured($terms),
             'stats'         => $this->mockStats(),
@@ -134,7 +135,7 @@ HTML,
         ];
         $parsed  = WikiContentParser::parse($entry['content']);
 
-        return view('wiki.show', [
+        return view('library.wiki.show', [
             'entry' => array_merge($entry, [
                 'content' => $parsed['content'],
                 'toc'     => $parsed['toc'],
@@ -195,7 +196,7 @@ HTML,
             'meaning'   => '佛陀的偈颂集,佛教最重要的经典之一',
         ];
 
-        return view('wiki.home', [
+        return view('library.wiki.home', [
             'languages'     => $languages,
             'currentLang'   => $lang,
             'stats'         => $stats,

+ 4 - 1
api-v12/app/Providers/AppServiceProvider.php

@@ -8,7 +8,8 @@ use Godruoyi\Snowflake\LaravelSequenceResolver;
 use App\Tools\QueryBuilderMacro;
 use Illuminate\Database\Query\Builder as QueryBuilder;
 use App\Services\RomanizeService;
-
+use Illuminate\Support\Facades\View;
+use App\View\Composers\BlogViewComposer;
 
 class AppServiceProvider extends ServiceProvider
 {
@@ -42,5 +43,7 @@ class AppServiceProvider extends ServiceProvider
     {
         //
         QueryBuilder::mixin($queryBuilderMacro = $this->app->make(QueryBuilderMacro::class));
+        View::composer('blog.*', BlogViewComposer::class);
+        View::composer('layouts.blog', BlogViewComposer::class);
     }
 }

+ 47 - 1
api-v12/app/Services/OpenSearchService.php

@@ -90,6 +90,7 @@ class OpenSearchService
                     'pali_synonyms' => [
                         'type' => 'synonym_graph',
                         'synonyms_path' => 'analysis/pali_synonyms.txt',
+                        'updateable' => true,
                     ],
                 ],
                 'char_filter' => [
@@ -289,7 +290,11 @@ class OpenSearchService
             return [false, $message];
         }
     }
-
+    public function indexExists()
+    {
+        $index = config('mint.opensearch.index');
+        return $this->client->indices()->exists(['index' => $index]);
+    }
     /** 索引管理方法保持不变... **/
 
     public function createIndex()
@@ -334,6 +339,47 @@ class OpenSearchService
         $index = config('mint.opensearch.index');
         return $this->client->indices()->delete(['index' => $index]);
     }
+    /**
+     * 获取索引文档数量(支持条件统计)
+     *
+     * @param  array|null  $query  可选的查询条件(OpenSearch DSL query 部分)
+     *                             例如:
+     *                             [
+     *                                 'term' => ['language' => 'zh']
+     *                             ]
+     *
+     * @return int  文档总数
+     *
+     * @throws \Exception
+     *
+     * @example
+     * // 获取索引全部文档数量
+     * $count = $service->count();
+     *
+     * // 按条件统计(例如:只统计有 embedding 的文档)
+     * $count = $service->count([
+     *     'exists' => ['field' => 'content.vector']
+     * ]);
+     */
+    public function count(?array $query = null): int
+    {
+        $index = config('mint.opensearch.index');
+
+        $params = [
+            'index' => $index,
+        ];
+
+        // 如果传入 query,则按条件统计
+        if (!empty($query)) {
+            $params['body'] = [
+                'query' => $query
+            ];
+        }
+
+        $response = $this->client->count($params);
+
+        return (int) ($response['count'] ?? 0);
+    }
 
     public function create(string $id, array $body)
     {

+ 53 - 1
api-v12/app/Services/PaliContentService.php

@@ -1,5 +1,5 @@
 <?php
-// api-v8/app/Services/OpenSearchService.php
+// api-v8/app/Services/PaliContentService.php
 namespace App\Services;
 
 use App\Models\Sentence;
@@ -580,4 +580,56 @@ class PaliContentService
         $result = $this->makeContentObj($record, $mode, $indexChannel);
         return $result;
     }
+
+    public function paragraphs(int $book, int $start, int $end, array $channelIds, array $param)
+    {
+
+        $channels = [];
+        foreach ($channelIds as $key => $channel) {
+            if (Str::isUuid($channel)) {
+                $channels[] = $channel;
+            }
+        }
+        $channelId = false;
+        if ($param['original']) {
+            if ($param['mode'] === 'read') {
+                //阅读模式加载html格式原文
+                $channelId = ChannelApi::getSysChannel('_System_Pali_VRI_');
+            } else {
+                //翻译模式加载json格式原文
+                $channelId = ChannelApi::getSysChannel('_System_Wbw_VRI_');
+            }
+        }
+
+
+        if ($channelId !== false) {
+            $channels[] = $channelId;
+        }
+        #获取channel索引表
+        $tranChannels = [];
+        $channelInfo = Channel::whereIn("uid", $channels)
+            ->select(['uid', 'type', 'lang', 'name'])->get();
+        foreach ($channelInfo as $key => $value) {
+            # code...
+            if ($value->type === "translation") {
+                $tranChannels[] = $value->uid;
+            }
+        }
+        $indexChannel = [];
+        $paliService = app(PaliContentService::class);
+        $indexChannel = $paliService->getChannelIndex($channels);
+        // the end of channel
+
+        //content
+        $record = Sentence::select($this->selectCol)
+            ->where('book_id', $book)
+            ->whereBetween('paragraph', [$start, $end])
+            ->whereIn('channel_uid', $channels)
+            ->orderBy('paragraph')
+            ->orderBy('word_start')
+            ->get();
+
+        $result = $paliService->makeContentObj($record, $param['mode'], $indexChannel, $param['format']);
+        return $result;
+    }
 }

+ 91 - 0
api-v12/app/View/Composers/BlogViewComposer.php

@@ -0,0 +1,91 @@
+<?php
+
+// app/View/Composers/BlogViewComposer.php
+// 自动向所有 blog.* 视图注入右边栏数据。
+// 在 AppServiceProvider 的 boot() 方法中注册:
+//
+//   use App\View\Composers\BlogViewComposer;
+//   View::composer('blog.*', BlogViewComposer::class);
+//   View::composer('layouts.blog', BlogViewComposer::class);
+//
+// 注意:layouts.blog 也需要注册,否则布局文件拿不到数据。
+
+namespace App\View\Composers;
+
+use Illuminate\View\View;
+
+class BlogViewComposer
+{
+    public function compose(View $view): void
+    {
+        $data = $view->getData();
+
+        // $user 由各 Controller 方法传入,Composer 不覆盖
+        $user = $data['user'] ?? null;
+
+        if (!$user) {
+            return;
+        }
+
+        $userName = $user['userName'] ?? null;
+
+        if (!$userName) {
+            return;
+        }
+
+        // 避免重复查询:如果已有数据则跳过
+        if ($view->offsetExists('categories') && $view->offsetExists('archives')) {
+            return;
+        }
+
+        // 以下查询根据你的实际 Service/Model 替换
+        // 示例结构,保持与 BlogController 现有数据结构一致
+
+        // Categories:[['id' => ..., 'label' => ...], ...]
+        if (!$view->offsetExists('categories')) {
+            $view->with('categories', $this->getCategories($userName));
+        }
+
+        // Tags:[['name' => ...], ...]
+        if (!$view->offsetExists('tags')) {
+            $view->with('tags', $this->getTags($userName));
+        }
+
+        // Archives:[['year' => ..., 'count' => ...], ...]
+        if (!$view->offsetExists('archives')) {
+            $view->with('archives', $this->getArchives($userName));
+        }
+    }
+
+    protected function getCategories(string $userName): array
+    {
+        // 替换为你的实际查询
+        // 例:return BlogService::getCategories($userName);
+        return [
+            ['id' => 'dharma',   'label' => '佛法'],
+            ['id' => 'vinaya',   'label' => '戒律'],
+            ['id' => 'pali',     'label' => '巴利文'],
+            ['id' => 'practice', 'label' => '禅修'],
+        ];
+    }
+
+    protected function getTags(string $userName): array
+    {
+        // 替换为你的实际查询
+        return [
+            ['name' => 'Pāli'],
+            ['name' => 'Vinaya'],
+            ['name' => 'Sutta'],
+            ['name' => 'Abhidhamma'],
+            ['name' => '三藏'],
+            ['name' => '注释'],
+        ];
+    }
+
+    protected function getArchives(string $userName): array
+    {
+        // 替换为你的实际查询
+        // 返回格式:[['year' => 2024, 'count' => 12], ...]
+        return [['year' => 2024, 'count' => 12]];
+    }
+}

+ 384 - 0
api-v12/documents/wikipali-frontend-spec.md

@@ -0,0 +1,384 @@
+# WikiPali 前端开发规范
+
+> 版本:1.0 · 适用范围:`library/*` · `blog/*` 全站前端
+
+---
+
+## 1. 技术栈
+
+| 层级 | 技术 |
+|---|---|
+| 后端框架 | Laravel 12 |
+| 模板引擎 | Blade |
+| CSS 框架 | Tabler CSS(基于 Bootstrap 5) |
+| 构建工具 | Vite |
+| 交互 | Vanilla JS + Tabler Offcanvas/Dropdown |
+| 搜索引擎 | OpenSearch |
+
+无 Livewire、无 Alpine.js、无额外前端框架。JS 保持轻量,仅在必要时引入模块。
+
+---
+
+## 2. 目录结构
+
+```
+resources/
+├── css/
+│   ├── library.css              # library/* 入口(纯 @import)
+│   ├── blog.css                 # blog/* 入口(纯 @import)
+│   ├── reader.css               # 全站阅读页入口(纯 @import)
+│   ├── app.css                  # 过渡期保留,稳定后删除
+│   │
+│   ├── base/
+│   │   ├── _variables.css       # Tabler 变量覆盖 + 自定义 token
+│   │   ├── _reset.css           # normalize / reset
+│   │   └── _typography.css      # 字体、行高、标题
+│   │
+│   ├── layout/
+│   │   ├── _grid.css            # 断点定义 + 1/2/3 栏容器(全站唯一)
+│   │   ├── _navbar.css          # 顶部导航(desktop 展开 / mobile 汉堡)
+│   │   ├── _drawer.css          # 左右 Drawer(基于 Tabler Offcanvas)
+│   │   ├── _hero.css            # Hero 区域
+│   │   ├── _footer.css
+│   │   └── _toolbar.css         # 页内工具条
+│   │
+│   ├── components/
+│   │   ├── _search-input.css    # 搜索框 + 提示下拉
+│   │   ├── _search-results.css  # 搜索结果页布局
+│   │   ├── _card.css            # 通用卡片(title + list)
+│   │   ├── _card-book.css       # 书籍卡片(封面 + 标题)
+│   │   ├── _badge.css
+│   │   └── _pagination.css
+│   │
+│   └── modules/                 # 各栏目差异样式,仅覆盖变量或追加规则
+│       ├── _wiki.css            # wiki 专属(词条详情、质量徽章等)
+│       ├── _tipitaka.css
+│       ├── _anthology.css
+│       ├── _blog.css
+│       └── _reader.css          # 阅读正文区(字号、行高、夜间模式)
+│
+├── js/
+│   ├── app.js                   # 全局入口
+│   ├── bootstrap.js
+│   └── modules/
+│       ├── term-tooltip.js      # 术语提示(原 term-tooltip.js)
+│       ├── toc.js               # TOC 折叠 + 滚动高亮
+│       ├── theme.js             # 夜间模式切换
+│       └── search-suggest.js    # 搜索提示下拉
+│
+└── views/
+    ├── layouts/                 # 布局文件(全站仅 3 个)
+    │   ├── base.blade.php       # HTML 骨架
+    │   ├── library.blade.php    # library/* 含 navbar + footer
+    │   ├── blog.blade.php       # blog/* 独立三栏,无 library 导航
+    │   └── reader.blade.php     # 全站阅读页,沉浸式,无导航
+    │
+    ├── components/
+    │   ├── ui/                  # 纯 UI 组件,全站通用
+    │   │   ├── drawer.blade.php
+    │   │   ├── search-input.blade.php
+    │   │   ├── card.blade.php
+    │   │   ├── card-book.blade.php
+    │   │   ├── badge.blade.php
+    │   │   └── pagination.blade.php
+    │   │
+    │   └── library/             # library 布局专用
+    │       ├── navbar.blade.php
+    │       ├── nav-links.blade.php
+    │       ├── hero.blade.php
+    │       ├── toolbar.blade.php
+    │       ├── footer.blade.php
+    │       ├── toc.blade.php
+    │       ├── layout-1col.blade.php
+    │       ├── layout-2col.blade.php
+    │       └── layout-3col.blade.php
+    │
+    ├── library/
+    │   ├── index.blade.php
+    │   ├── search.blade.php          # 统一搜索结果页
+    │   ├── tipitaka/
+    │   │   ├── index.blade.php
+    │   │   ├── category.blade.php
+    │   │   ├── show.blade.php
+    │   │   └── read.blade.php
+    │   ├── anthology/
+    │   │   ├── index.blade.php
+    │   │   ├── show.blade.php
+    │   │   └── read.blade.php
+    │   └── wiki/
+    │       ├── home.blade.php
+    │       ├── index.blade.php
+    │       └── show.blade.php
+    │
+    └── blog/
+        ├── index.blade.php
+        ├── category.blade.php
+        └── show.blade.php
+```
+
+---
+
+## 3. 路由与页面对应
+
+```
+/library                           → library/index.blade.php
+/library/search?q=&type=          → library/search.blade.php
+/library/tipitaka                  → library/tipitaka/index.blade.php
+/library/tipitaka/category/{id}    → library/tipitaka/category.blade.php
+/library/tipitaka/{id}             → library/tipitaka/show.blade.php
+/library/tipitaka/{id}/read        → library/tipitaka/read.blade.php
+/library/anthology                 → library/anthology/index.blade.php
+/library/anthology/{id}            → library/anthology/show.blade.php
+/library/anthology/{id}/read/{art} → library/anthology/read.blade.php
+/library/wiki                      → library/wiki/home.blade.php
+/library/wiki/{lang}               → library/wiki/index.blade.php
+/library/wiki/{lang}/{word}        → library/wiki/show.blade.php
+/blog/{user}                       → blog/index.blade.php
+/blog/{user}/{post}                → blog/show.blade.php
+/blog/{user}/category/...          → blog/category.blade.php
+```
+
+---
+
+## 4. 栏目划分
+
+| 栏目 | 路由前缀 | 布局 | 说明 |
+|---|---|---|---|
+| Library 门户 | `/library` | `layouts/library` | 入口页,含 Hero |
+| Tipitaka(巴利三藏) | `/library/tipitaka` | `layouts/library` | 含阅读页 |
+| Anthology(文集) | `/library/anthology` | `layouts/library` | 含阅读页 |
+| Wiki(词典) | `/library/wiki` | `layouts/library` | 词条详情为阅读页 |
+| Search(搜索) | `/library/search` | `layouts/library` | 统一结果页,`?type=` 区分栏目 |
+| Blog(博客) | `/blog` | `layouts/blog` | 独立三栏,与 library 完全隔离 |
+
+**阅读页统一使用 `layouts/reader`**,包括:
+
+- `library/tipitaka/read.blade.php`
+- `library/anthology/read.blade.php`
+- `library/wiki/show.blade.php`(词条详情本质为阅读)
+- `blog/show.blade.php`(博文详情)
+
+---
+
+## 5. Blade 调用层级
+
+```
+layouts/base.blade.php
+│   HTML 骨架,@vite,@stack('styles'),@stack('scripts')
+│   不含任何导航或布局
+│
+├── layouts/library.blade.php
+│   @extends('layouts.base')
+│   包含:navbar + hero(可选) + toolbar(可选) + @yield('content') + footer
+│   页面通过 @section('content') 填充正文
+│   栏目模块 CSS 通过 @push('styles') 注入
+│
+├── layouts/blog.blade.php
+│   @extends('layouts.base')
+│   独立三栏:左侧博主信息 + 中间内容 + 右侧过滤工具
+│   无 library navbar / footer
+│
+└── layouts/reader.blade.php
+    @extends('layouts.base')
+    沉浸式,无 navbar,无 footer
+    包含:TOC(desktop 常驻,mobile 左侧 Drawer)+ 正文区
+```
+
+**页面文件示例:**
+
+```blade
+{{-- library/wiki/show.blade.php --}}
+@extends('layouts.reader')
+
+@push('styles')
+    @vite('resources/css/modules/_wiki.css')  {{-- wiki 专属样式 --}}
+@endpush
+
+@section('content')
+    ...
+@endsection
+```
+
+---
+
+## 6. CSS 架构与复用
+
+### 基准层
+
+wiki 栏目的现有 CSS 作为全站基准,其他栏目默认继承,需要时通过 `modules/_xxx.css` 覆盖。
+
+### 入口文件职责
+
+入口文件(`library.css` / `blog.css` / `reader.css`)**只做 `@import`,不写任何样式规则**。
+
+```css
+/* library.css */
+@import './base/_variables.css';
+@import './base/_reset.css';
+@import './base/_typography.css';
+@import './layout/_grid.css';
+@import './layout/_navbar.css';
+@import './layout/_drawer.css';
+@import './layout/_hero.css';
+@import './layout/_footer.css';
+@import './layout/_toolbar.css';
+@import './components/_search-input.css';
+@import './components/_card.css';
+@import './components/_card-book.css';
+@import './components/_badge.css';
+@import './components/_pagination.css';
+/* modules 不在此引入,由各页面按需 @push */
+```
+
+### 变量覆盖机制
+
+`_variables.css` 覆盖 Tabler 的 CSS custom properties,各栏目 module 文件**只重定义变量,不重写规则**:
+
+```css
+/* modules/_wiki.css */
+:root {
+  --color-primary: #5c6bc0;
+}
+/* 仅追加 wiki 专属规则 */
+```
+
+### Vite 入口配置
+
+```js
+// vite.config.js
+laravel({
+    input: [
+        'resources/css/library.css',
+        'resources/css/blog.css',
+        'resources/css/reader.css',
+        'resources/js/app.js',
+    ],
+    refresh: true,
+})
+```
+
+---
+
+## 7. 页面布局(从上到下)
+
+### library/* 页面
+
+```
+┌────────────────────────────────────┐
+│  Navbar                            │  所有 library 页面
+│  左:面包屑  右:导航按钮          │
+├────────────────────────────────────┤
+│  Hero(可选)                      │  首页、栏目首页
+├────────────────────────────────────┤
+│  Toolbar(可选)                   │  列表页、详情页
+├──────────┬────────────┬────────────┤
+│  左边栏  │  主内容    │  右边栏    │  3 栏(wiki 词条等)
+│(可选)  │            │(可选)    │  2 栏(tipitaka 等)
+│          │            │            │  1 栏(首页、搜索)
+└──────────┴────────────┴────────────┘
+│  Footer                            │
+└────────────────────────────────────┘
+```
+
+### blog/* 页面
+
+```
+┌──────────┬────────────┬────────────┐
+│  左边栏  │  文章列表  │  右边栏    │
+│          │            │            │
+│  头像    │  卡片列表  │  搜索框    │
+│  徽章    │  分页      │  标签云    │
+│  栏目导航│            │  过滤器    │
+└──────────┴────────────┴────────────┘
+```
+
+### 阅读页(reader)
+
+```
+┌──────────┬────────────────────────┐
+│  TOC     │  正文                  │
+│  常驻    │  无 navbar             │
+│          │  无 footer             │
+└──────────┴────────────────────────┘
+```
+
+---
+
+## 8. 响应式规范
+
+断点统一定义在 `layout/_grid.css`,全站唯一,不在各栏目重复定义。
+
+| 区域 | Desktop (>1024px) | Tablet (768–1024px) | Mobile (<768px) |
+|---|---|---|---|
+| 顶部导航 | 完整展开 | 完整展开 | 汉堡按钮 → 右侧 Drawer |
+| 左边栏 | 常驻 | 常驻 | 左侧 Drawer,默认收起 |
+| 右边栏 | 常驻 | 隐藏 | 隐藏 |
+| 阅读页 TOC | 左侧常驻 | 左侧 Drawer | 左侧 Drawer + 悬浮触发按钮 |
+| Blog 左边栏 | 常驻 | 左侧 Drawer | 左侧 Drawer |
+| Blog 右边栏 | 常驻 | 隐藏 | 隐藏 |
+
+### Drawer 组件
+
+基于 Tabler Offcanvas,左右方向通过 `side` prop 控制:
+
+```blade
+{{-- 导航 Drawer(右侧) --}}
+<x-ui.drawer id="nav-drawer" side="end" title="导航">
+    <x-library.nav-links />
+</x-ui.drawer>
+
+{{-- TOC Drawer(左侧) --}}
+<x-ui.drawer id="toc-drawer" side="start" title="目录">
+    <x-library.toc :items="$toc" />
+</x-ui.drawer>
+```
+
+---
+
+## 9. 可复用组件清单
+
+| 组件 | 路径 | Props | 用途 |
+|---|---|---|---|
+| `<x-ui.drawer>` | `components/ui/drawer` | `id` `side=start\|end` `title` | 左/右抽屉 |
+| `<x-ui.search-input>` | `components/ui/search-input` | `name` `action` `placeholder` `suggest-url` | 搜索框 + 提示 |
+| `<x-ui.card>` | `components/ui/card` | `title` `items` `href` | 通用卡片 |
+| `<x-ui.card-book>` | `components/ui/card-book` | `title` `cover` `href` `layout=vertical\|horizontal` | 书籍卡片 |
+| `<x-ui.badge>` | `components/ui/badge` | `type` `label` | 徽章 |
+| `<x-ui.pagination>` | `components/ui/pagination` | `paginator` | 分页 |
+| `<x-library.navbar>` | `components/library/navbar` | — | 含汉堡触发 |
+| `<x-library.toc>` | `components/library/toc` | `items` | TOC,自适应 Drawer |
+| `<x-library.hero>` | `components/library/hero` | `title` `subtitle` | Hero 区域 |
+| `<x-library.toolbar>` | `components/library/toolbar` | slot | 页内工具条 |
+| `<x-library.layout-1col>` | `components/library/layout-1col` | slot | 单栏容器 |
+| `<x-library.layout-2col>` | `components/library/layout-2col` | slot `main` `aside` | 双栏容器 |
+| `<x-library.layout-3col>` | `components/library/layout-3col` | slot `left` `main` `right` | 三栏容器 |
+
+---
+
+## 10. 搜索规范
+
+搜索底层统一使用 OpenSearch,结果页统一为 `library/search.blade.php`。
+
+| 参数 | 说明 | 示例 |
+|---|---|---|
+| `q` | 搜索关键词 | `?q=dukkha` |
+| `type` | 栏目过滤 | `?type=wiki` `?type=tipitaka` |
+| `lang` | 语言过滤 | `?lang=zh` |
+
+各栏目的搜索入口(如 wiki 首页的搜索框)跳转至统一搜索页,并预填 `type` 参数。搜索提示下拉由 `search-suggest.js` 处理,调用各自的 suggest API。
+
+---
+
+## 11. 迁移策略
+
+重构为**增量迁移**,不一次性切换,降低风险。
+
+1. **以 wiki 为基准**:wiki 现有 CSS 和组件作为公共层的起点
+2. **提取公共层**:将 wiki 中可复用的部分移至 `layouts/`、`components/ui/`、`base/`
+3. **wiki 改为引用公共层**:行为不变,验证通过后继续
+4. **逐栏目迁移顺序**:wiki → anthology → tipitaka → blog(从简单到复杂)
+5. **旧文件保留至迁移完成**:`app.css`、各栏目旧 `layouts/app.blade.php` 在对应栏目迁移完成后再删除
+
+---
+
+*本文档随重构进展持续更新*

+ 0 - 0
api-v12/public/assets/css/blog/style.min.663803bebe609202d5b39d848f2d7c2dc8b598a2d879efa079fa88893d29c49c.css → api-v12/public/assets/css/blog/style.min.css


+ 0 - 11
api-v12/resources/css/app.css

@@ -1,11 +0,0 @@
-@import 'tailwindcss';
-
-@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
-@source '../../storage/framework/views/*.php';
-@source '../**/*.blade.php';
-@source '../**/*.js';
-
-@theme {
-    --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
-        'Segoe UI Symbol', 'Noto Color Emoji';
-}

+ 16 - 0
api-v12/resources/css/base/_reset.css

@@ -0,0 +1,16 @@
+/* resources/css/base/_reset.css
+   最小化 reset,补充 Tabler 未覆盖的部分。
+   不覆盖 Tabler 已处理好的规则。
+*/
+
+*,
+*::before,
+*::after {
+    box-sizing: border-box;
+}
+
+img,
+video {
+    max-width: 100%;
+    height: auto;
+}

+ 13 - 0
api-v12/resources/css/base/_typography.css

@@ -0,0 +1,13 @@
+/* resources/css/base/_typography.css
+   全站基础排版。
+   Noto Serif 已在 library.css 入口处 @import,此处只设置使用规则。
+*/
+
+/* 正文衬线字体作用域由各 module 自行声明(如 .wiki-content-body)。
+   全站默认保持 Tabler 的 sans-serif 体系,不在此全局覆盖。 */
+
+/* 标题基础 */
+h1, h2, h3, h4, h5, h6 {
+    line-height: 1.3;
+    font-weight: 600;
+}

+ 39 - 0
api-v12/resources/css/base/_variables.css

@@ -0,0 +1,39 @@
+/* resources/css/base/_variables.css
+   全站 CSS 变量
+   - 第一层:覆盖 Tabler 默认值
+   - 第二层:WikiPali 自定义 token
+   所有颜色、间距、字体 token 统一在此定义,各 module 只引用变量,不硬编码色值
+*/
+
+/* ── Tabler 变量覆盖 ── */
+:root {
+    --tblr-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+    --tblr-border-radius: 0.375rem;
+    --tblr-border-radius-lg: 0.5rem;
+}
+
+/* ── WikiPali 品牌色 token ── */
+:root {
+    /* 主色:暖金/琥珀,源自 main.css --sf */
+    --wp-brand:        #c8860a;
+    --wp-brand-light:  #f5e6c8;
+    --wp-brand-pale:   #fdf8f0;
+    --wp-brand-dark:   #9a6508;
+
+    /* 文字层级,源自 main.css --ink */
+    --wp-ink:          #1a1208;
+    --wp-ink-soft:     #4a3f2f;
+    --wp-ink-muted:    #8a7a68;
+
+    /* 边框 / 背景 */
+    --wp-border:       #e8ddd0;
+    --wp-card-bg:      #fffdf9;
+    --wp-surface-alt:  #fdf8f0;
+}
+
+/* ── 阅读页专属 token(reader.css 会用到) ── */
+:root {
+    --wp-reader-font-size:    1rem;
+    --wp-reader-line-height:  1.875;
+    --wp-reader-max-width:    720px;
+}

+ 4 - 0
api-v12/resources/css/components/_badge.css

@@ -0,0 +1,4 @@
+/* resources/css/components/_badge.css
+   通用徽章。wiki 质量徽章(.wiki-quality-badge)在 modules/_wiki.css 中定义。
+   此文件预留全站徽章扩展。
+*/

+ 199 - 0
api-v12/resources/css/components/_card-book.css

@@ -0,0 +1,199 @@
+/* resources/css/components/_card-book.css
+   书籍封面组件(.book-cover)+ 纵向书籍卡片(.card-book)。
+   .book-cover 从 _anthology.css 提取,供 anthology / tipitaka 共用。
+*/
+
+/* ══════════════════════════════════════════
+   一、书籍封面(.book-cover)
+   ══════════════════════════════════════════ */
+
+.book-cover {
+    position: relative;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+}
+
+/* 尺寸 */
+.book-cover--sm { width: 34px;  min-width: 34px;  height: 46px; }
+.book-cover--md { width: 130px; min-width: 130px; height: 180px; }
+.book-cover--lg { width: 155px; min-width: 155px; height: 215px; border-radius: 3px 9px 9px 3px; }
+
+/* 3D 书脊 */
+.book-cover--3d {
+    box-shadow:
+        -4px 0 0 rgba(0,0,0,.3),
+        -6px 4px 14px rgba(0,0,0,.4),
+        4px 4px 18px rgba(0,0,0,.3);
+}
+
+.book-cover--3d::before {
+    content: '';
+    position: absolute;
+    left: 0; top: 0; bottom: 0;
+    width: 13px;
+    background: linear-gradient(to right, rgba(0,0,0,.4), rgba(0,0,0,.1));
+    border-radius: 3px 0 0 3px;
+    z-index: 2;
+}
+
+/* 纹理叠加 */
+.book-cover::after {
+    content: '';
+    position: absolute;
+    inset: 0;
+    background: repeating-linear-gradient(
+        45deg, transparent, transparent 8px,
+        rgba(255,255,255,.015) 8px, rgba(255,255,255,.015) 9px
+    );
+    z-index: 1;
+}
+
+/* 图片封面 */
+.book-cover__img {
+    position: absolute;
+    inset: 0;
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+}
+
+/* 文字封面 */
+.book-cover__text {
+    position: relative;
+    z-index: 3;
+    text-align: center;
+    padding: 0 .5rem;
+}
+
+.book-cover__title {
+    font-family: 'Noto Serif SC', 'Noto Serif', Georgia, serif;
+    font-size: 1rem;
+    font-weight: 600;
+    color: #fff;
+    line-height: 1.6;
+    letter-spacing: .12em;
+    word-break: break-all;
+}
+
+.book-cover--sm .book-cover__title {
+    font-size: .6rem;
+    letter-spacing: .04em;
+    line-height: 1.3;
+}
+
+.book-cover__divider {
+    width: 28px;
+    height: 1px;
+    background: var(--wp-brand);
+    margin: .5rem auto;
+}
+
+.book-cover__subtitle {
+    font-size: .65rem;
+    color: rgba(255,255,255,.45);
+    letter-spacing: .04em;
+}
+
+/* ══════════════════════════════════════════
+   二、纵向书籍卡片(.card-book)
+   tipitaka 列表页使用
+   ══════════════════════════════════════════ */
+
+.card-book {
+    transition: transform 0.2s, box-shadow 0.2s;
+}
+
+.card-book:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 8px 24px rgba(0,0,0,.1);
+}
+
+.card-book__link {
+    display: block;
+    text-decoration: none;
+    color: inherit;
+}
+
+.card-book__link:hover {
+    text-decoration: none;
+    color: inherit;
+}
+
+/* 封面撑满卡片宽度 */
+.card-book .book-cover--md {
+    width: 100%;
+    min-width: unset;
+    height: 200px;
+    border-radius: var(--tblr-border-radius);
+}
+
+@media (max-width: 768px) {
+    .card-book .book-cover--md { height: 150px; }
+}
+
+.card-book__info { padding: .75rem 0 0; }
+
+.card-book__title {
+    font-size: 0.9375rem;
+    font-weight: 600;
+    color: var(--tblr-body-color);
+    margin-bottom: .25rem;
+    line-height: 1.4;
+    display: -webkit-box;
+    -webkit-line-clamp: 2;
+    -webkit-box-orient: vertical;
+    overflow: hidden;
+}
+
+.card-book:hover .card-book__title { color: var(--tblr-primary); }
+
+.card-book__author,
+.card-book__publisher {
+    font-size: .8125rem;
+    color: var(--tblr-secondary);
+    margin-bottom: .25rem;
+}
+
+.card-book__publisher-link {
+    color: var(--tblr-primary);
+    text-decoration: none;
+}
+
+.card-book__publisher-link:hover { text-decoration: underline; }
+
+.card-book__badges {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 4px;
+    margin-top: .375rem;
+}
+
+.card-book__badge {
+    display: inline-block;
+    padding: 2px 8px;
+    background: var(--tblr-bg-surface-secondary);
+    border: 1px solid var(--tblr-border-color);
+    border-radius: 20px;
+    font-size: .6875rem;
+    color: var(--tblr-secondary);
+}
+
+/* ══════════════════════════════════════════
+   三、书籍网格
+   ══════════════════════════════════════════ */
+
+.book-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
+    gap: 1.25rem;
+}
+
+@media (max-width: 575px) {
+    .book-grid {
+        grid-template-columns: repeat(2, 1fr);
+        gap: .875rem;
+    }
+}

+ 287 - 0
api-v12/resources/css/components/_card.css

@@ -0,0 +1,287 @@
+/* resources/css/components/_card.css
+   全站通用卡片、侧边栏、列表组件。
+   从 _wiki.css 提取,供 wiki / tipitaka / anthology / search 共用。
+   wiki 专属样式(质量徽章、条目头部、term popover 等)保留在 modules/_wiki.css。
+*/
+
+/* ══════════════════════════════════════════
+   一、通用卡片
+   ══════════════════════════════════════════ */
+
+.wiki-card {
+    background: var(--tblr-bg-surface);
+    border: 1px solid var(--tblr-border-color);
+    border-radius: var(--tblr-border-radius-lg);
+    padding: 1.5rem;
+}
+
+/* ══════════════════════════════════════════
+   二、侧边栏区块
+   ══════════════════════════════════════════ */
+
+.wiki-sidebar-section {
+    background: var(--tblr-bg-surface);
+    border: 1px solid var(--tblr-border-color);
+    border-radius: var(--tblr-border-radius-lg);
+    padding: 1rem 1.125rem;
+    margin-bottom: 1rem;
+}
+
+.wiki-sidebar-title {
+    font-size: 0.6875rem;
+    font-weight: 500;
+    letter-spacing: 0.05em;
+    text-transform: uppercase;
+    color: var(--tblr-secondary);
+    margin-bottom: 0.75rem;
+}
+
+/* ══════════════════════════════════════════
+   三、分类列表
+   ══════════════════════════════════════════ */
+
+.wiki-cat-list {
+    list-style: none;
+    padding: 0;
+    margin: 0;
+}
+
+.wiki-cat-list li {
+    margin-bottom: 2px;
+}
+
+.wiki-cat-list a {
+    display: flex;
+    align-items: center;
+    font-size: 0.8125rem;
+    color: var(--tblr-body-color);
+    text-decoration: none;
+    padding: 5px 8px;
+    border-radius: var(--tblr-border-radius);
+    transition: background 0.12s;
+}
+
+.wiki-cat-list a:hover {
+    background: var(--tblr-bg-surface-secondary);
+}
+
+.wiki-cat-list a.active {
+    background: var(--tblr-bg-surface-secondary);
+    font-weight: 500;
+    color: var(--tblr-primary);
+}
+
+.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;
+}
+
+/* ══════════════════════════════════════════
+   四、TOC 列表
+   ══════════════════════════════════════════ */
+
+.wiki-toc-list {
+    list-style: none;
+    padding: 0;
+    margin: 0;
+}
+
+.wiki-toc-list li {
+    border-bottom: 1px solid var(--tblr-border-color);
+}
+
+.wiki-toc-list li:last-child {
+    border-bottom: none;
+}
+
+.wiki-toc-list a {
+    display: block;
+    font-size: 0.8125rem;
+    color: var(--tblr-secondary);
+    text-decoration: none;
+    padding: 5px 0;
+    transition: color 0.12s;
+}
+
+.wiki-toc-list a:hover  { color: var(--tblr-body-color); }
+.wiki-toc-list a.active { color: var(--tblr-body-color); font-weight: 500; }
+
+.wiki-toc-list .toc-level-2 a { padding-left: 0.5rem; }
+.wiki-toc-list .toc-level-3 a { padding-left: 1rem; }
+
+.wiki-toc-num {
+    color: var(--tblr-secondary);
+    margin-right: 5px;
+    font-size: 0.75rem;
+}
+
+/* ══════════════════════════════════════════
+   五、相关条目列表
+   ══════════════════════════════════════════ */
+
+.wiki-related-list {
+    list-style: none;
+    padding: 0;
+    margin: 0;
+}
+
+.wiki-related-list li {
+    border-bottom: 1px solid var(--tblr-border-color);
+}
+
+.wiki-related-list li:last-child { border-bottom: none; }
+
+.wiki-related-list a {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    font-size: 0.8125rem;
+    color: var(--tblr-primary);
+    text-decoration: none;
+    padding: 6px 0;
+}
+
+.wiki-related-zh {
+    font-size: 0.75rem;
+    color: var(--tblr-secondary);
+}
+
+/* ══════════════════════════════════════════
+   六、元信息表格
+   ══════════════════════════════════════════ */
+
+.wiki-meta-table {
+    width: 100%;
+    font-size: 0.8125rem;
+    border-collapse: collapse;
+}
+
+.wiki-meta-table td { padding: 3px 0; }
+
+.wiki-meta-table td:last-child {
+    text-align: right;
+    color: var(--tblr-secondary);
+}
+
+/* ══════════════════════════════════════════
+   七、条目头部(通用标题区)
+   ══════════════════════════════════════════ */
+
+.wiki-entry-header { margin-bottom: 1.25rem; }
+
+.wiki-entry-title {
+    font-family: "Noto Serif", Georgia, serif;
+    font-size: 1.75rem;
+    font-weight: 600;
+    line-height: 1.25;
+    margin: 0.375rem 0 0.75rem;
+    color: var(--tblr-body-color);
+}
+
+/* ══════════════════════════════════════════
+   八、精选卡片网格(tipitaka index / wiki index 共用)
+   ══════════════════════════════════════════ */
+
+.wiki-featured-grid {
+    display: grid;
+    grid-template-columns: repeat(3, minmax(0, 1fr));
+    gap: 8px;
+}
+
+.wiki-featured-card {
+    border: 1px solid var(--tblr-border-color);
+    border-radius: var(--tblr-border-radius);
+    padding: 10px 12px;
+    cursor: pointer;
+    text-decoration: none;
+    display: block;
+    transition: background 0.12s;
+    color: var(--tblr-body-color);
+}
+
+.wiki-featured-card:hover {
+    background: var(--tblr-bg-surface-secondary);
+    color: var(--tblr-body-color);
+}
+
+.wiki-featured-label {
+    font-size: 0.6875rem;
+    font-weight: 500;
+    text-transform: uppercase;
+    letter-spacing: 0.05em;
+    color: var(--tblr-secondary);
+    margin-bottom: 5px;
+}
+
+.wiki-featured-title {
+    font-size: 0.875rem;
+    font-weight: 500;
+    margin-bottom: 2px;
+}
+
+.wiki-featured-pali {
+    font-size: 0.75rem;
+    font-style: italic;
+    color: var(--tblr-secondary);
+}
+
+@media (max-width: 768px) {
+    .wiki-featured-grid {
+        grid-template-columns: repeat(2, minmax(0, 1fr));
+    }
+}
+
+/* ══════════════════════════════════════════
+   九、作者头像组件(从 _anthology.css 提取)
+   ══════════════════════════════════════════ */
+
+.author-avatar {
+    display: flex;
+    align-items: center;
+    gap: .5rem;
+}
+
+.author-avatar__img,
+.author-avatar__initials {
+    border-radius: 50%;
+    flex-shrink: 0;
+    object-fit: cover;
+}
+
+.author-avatar--sm .author-avatar__img,
+.author-avatar--sm .author-avatar__initials { width: 24px; height: 24px; font-size: .65rem; }
+
+.author-avatar--md .author-avatar__img,
+.author-avatar--md .author-avatar__initials { width: 28px; height: 28px; font-size: .68rem; }
+
+.author-avatar--lg .author-avatar__img,
+.author-avatar--lg .author-avatar__initials { width: 48px; height: 48px; font-size: .95rem; }
+
+.author-avatar__initials {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-weight: 700;
+    color: #fff;
+}
+
+.author-avatar__name {
+    font-size: .8rem;
+    color: var(--tblr-body-color);
+    font-weight: 500;
+    display: block;
+}
+
+.author-avatar--lg .author-avatar__name { font-size: .9rem; }
+
+.author-avatar__sub {
+    font-size: .72rem;
+    color: var(--tblr-secondary);
+    display: block;
+}

+ 64 - 0
api-v12/resources/css/components/_pagination.css

@@ -0,0 +1,64 @@
+/* resources/css/components/_pagination.css
+   全站通用分页组件。
+   来源:wiki.css / wiki-search.css 的 .wiki-pagination 段落(两处相同,已去重)。
+*/
+
+.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;
+}

+ 13 - 0
api-v12/resources/css/components/_search-input.css

@@ -0,0 +1,13 @@
+/* resources/css/components/_search-input.css
+   搜索输入框 + 提示下拉。
+   主要样式由 Tabler input-group 提供,此处补充 WikiPali 专属覆盖。
+*/
+
+.search-box {
+    background: white;
+    border-radius: 0.5rem;
+    padding: 0.5rem;
+    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+    max-width: 500px;
+    margin: 0 auto;
+}

+ 9 - 84
api-v12/resources/css/wiki-search.css → api-v12/resources/css/components/_search-results.css

@@ -1,4 +1,8 @@
-/* ── 追加到 resources/css/wiki.css 末尾 ── */
+/* resources/css/components/_search-results.css
+   统一搜索结果页组件样式。
+   来源:wiki-search.css(与 wiki.css 末尾重复内容已去重,以 wiki 栏目为准)。
+   适用于 /library/search?type= 所有类型。
+*/
 
 /* ── 搜索栏 ── */
 .wiki-search-bar-wrap {
@@ -24,7 +28,7 @@
 
 /* ── 搜索结果列表容器 ── */
 .wiki-search-results {
-    padding: 0;         /* 覆盖 wiki-card 默认 padding,由卡片自身管理间距 */
+    padding: 0;
 }
 
 /* ── 搜索结果卡片 ── */
@@ -64,7 +68,7 @@
 }
 
 .wiki-search-card-word {
-    font-family: 'Noto Serif', Georgia, serif;
+    font-family: "Noto Serif", Georgia, serif;
     font-size: 0.875rem;
     font-style: italic;
     color: var(--tblr-secondary);
@@ -78,10 +82,9 @@
     margin: 0 0 6px;
 }
 
-/* highlight 高亮词 */
 .wiki-search-card-snippet mark {
-    background: #FAEEDA;
-    color: #854F0B;
+    background: #faeeda;
+    color: #854f0b;
     padding: 1px 2px;
     border-radius: 3px;
     font-style: normal;
@@ -99,23 +102,6 @@
     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;
@@ -150,64 +136,3 @@
     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;
-}

+ 92 - 0
api-v12/resources/css/layout/_drawer.css

@@ -0,0 +1,92 @@
+/* resources/css/layout/_drawer.css
+   右侧 mobile 导航抽屉(nav drawer)。
+   来源:main.css 的 .bc-mobile-overlay / .bc-mobile-drawer 段落。
+
+   左侧内容抽屉(TOC、侧边栏)复用 Tabler Offcanvas,
+   通过 <x-ui.drawer side="start|end"> 组件控制方向,无需额外 CSS。
+*/
+
+/* ── 遮罩 ── */
+.bc-mobile-overlay {
+    display: none;
+    position: fixed;
+    inset: 0;
+    background: rgba(0, 0, 0, 0.4);
+    z-index: 1040;
+}
+
+.bc-mobile-overlay.open {
+    display: block;
+}
+
+/* ── 抽屉面板 ── */
+.bc-mobile-drawer {
+    position: fixed;
+    top: 0;
+    right: -100%;
+    width: 240px;
+    height: 100vh;
+    background: var(--wp-card-bg);
+    border-left: 1px solid var(--wp-border);
+    z-index: 1050;
+    transition: right 0.25s ease;
+    padding: 1rem 0;
+    box-shadow: -4px 0 20px rgba(0, 0, 0, 0.1);
+}
+
+.bc-mobile-drawer.open {
+    right: 0;
+}
+
+/* ── 抽屉头部 ── */
+.bc-mobile-drawer-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 0.5rem 1.25rem 0.75rem;
+    border-bottom: 1px solid var(--wp-border);
+    margin-bottom: 0.5rem;
+}
+
+.bc-mobile-drawer-header span {
+    font-size: 0.85rem;
+    font-weight: 600;
+    color: var(--wp-ink-soft);
+}
+
+.bc-mobile-drawer-close {
+    background: none;
+    border: none;
+    cursor: pointer;
+    color: var(--wp-ink-muted);
+    font-size: 1rem;
+    line-height: 1;
+    padding: 2px;
+}
+
+/* ── 抽屉导航链接 ── */
+.bc-mobile-nav {
+    list-style: none;
+    padding: 0;
+    margin: 0;
+}
+
+.bc-mobile-nav li a {
+    display: block;
+    padding: 0.65rem 1.25rem;
+    font-size: 0.9rem;
+    color: var(--wp-ink-soft);
+    text-decoration: none;
+    border-bottom: 1px solid rgba(232, 221, 208, 0.5);
+    transition: background 0.15s;
+}
+
+.bc-mobile-nav li a:hover {
+    background: var(--wp-surface-alt);
+    color: var(--wp-brand);
+}
+
+.bc-mobile-nav li a.active {
+    color: var(--wp-brand);
+    font-weight: 600;
+}

+ 4 - 0
api-v12/resources/css/layout/_footer.css

@@ -0,0 +1,4 @@
+/* resources/css/layout/_footer.css
+   全站 footer 样式。
+   当前 library 无独立 footer,此文件预留,待 footer 组件建立后填充。
+*/

+ 85 - 0
api-v12/resources/css/layout/_grid.css

@@ -0,0 +1,85 @@
+/* resources/css/layout/_grid.css
+   全站断点定义 + 通用栏容器。
+   包含从 _wiki.css 提取的三栏布局,供所有栏目复用。
+*/
+
+/* ── 断点 token ── */
+:root {
+    --bp-sm:  576px;
+    --bp-md:  768px;
+    --bp-lg:  992px;
+    --bp-xl:  1200px;
+}
+
+/* ══════════════════════════════════════════
+   三栏布局(原 .wiki-layout,全站通用)
+   ══════════════════════════════════════════ */
+
+.wiki-layout {
+    display: grid;
+    grid-template-columns: 200px 1fr 200px;
+    grid-template-areas: "left main right";
+    gap: 1.5rem;
+    align-items: start;
+    padding-top: 1.5rem;
+    padding-bottom: 3rem;
+}
+
+.wiki-sidebar-left  { grid-area: left; }
+.wiki-sidebar-right { grid-area: right; }
+
+.wiki-main {
+    grid-area: main;
+    min-width: 0;
+    display: flex;
+    flex-direction: column;
+    gap: 1rem;
+}
+
+@media (max-width: 992px) {
+    .wiki-layout {
+        grid-template-columns: 180px 1fr;
+        grid-template-areas:
+            "left main"
+            "left right";
+    }
+
+    .wiki-sidebar-right {
+        display: flex;
+        flex-wrap: wrap;
+        gap: 1rem;
+    }
+
+    .wiki-sidebar-right .wiki-sidebar-section {
+        flex: 1 1 180px;
+        margin-bottom: 0;
+    }
+}
+
+@media (max-width: 768px) {
+    .wiki-layout {
+        grid-template-columns: 1fr;
+        grid-template-areas: "main" "right" "left";
+    }
+
+    .wiki-sidebar-left,
+    .wiki-sidebar-right {
+        display: none;
+    }
+}
+
+/* ── 通用两栏容器(预留) ── */
+.layout-2col {
+    display: grid;
+    grid-template-columns: 220px 1fr;
+    gap: 1.5rem;
+    align-items: start;
+    padding-top: 1.5rem;
+    padding-bottom: 3rem;
+}
+
+@media (max-width: 768px) {
+    .layout-2col {
+        grid-template-columns: 1fr;
+    }
+}

+ 76 - 0
api-v12/resources/css/layout/_hero.css

@@ -0,0 +1,76 @@
+/* resources/css/layout/_hero.css
+   Hero 区域(封面图 + 叠加层 + 标题文字)。
+   来源:main.css 的 .hero-section、.hero-overlay、.hero-content 段落。
+*/
+
+.hero-wrapper {
+    position: relative;
+}
+
+.hero-section {
+    height: 250px;
+    width: 100%;
+    background-size: cover;
+    background-position: center;
+    background-repeat: no-repeat;
+    position: relative;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.hero-overlay {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background: rgba(0, 0, 0, 0.2);
+}
+
+.hero-content {
+    position: relative;
+    z-index: 2;
+    text-align: center;
+    color: white;
+    max-width: 600px;
+    padding: 0 1rem;
+}
+
+.hero-title {
+    font-size: 2.5rem;
+    font-weight: bold;
+    margin-bottom: 1rem;
+    text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
+}
+
+.hero-subtitle {
+    font-size: 1.2rem;
+    margin-bottom: 2rem;
+    text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
+}
+
+/* ── 响应式 ── */
+@media (max-width: 768px) {
+    .hero-section {
+        height: 250px;
+    }
+
+    .hero-title {
+        font-size: 2rem;
+    }
+
+    .hero-subtitle {
+        font-size: 1rem;
+    }
+}
+
+@media (max-width: 576px) {
+    .hero-title {
+        font-size: 1.5rem;
+    }
+
+    .hero-subtitle {
+        font-size: 0.9rem;
+    }
+}

+ 119 - 0
api-v12/resources/css/layout/_navbar.css

@@ -0,0 +1,119 @@
+/* resources/css/layout/_navbar.css
+   顶部导航栏:面包屑 bar + desktop 导航链接 + mobile 汉堡按钮。
+   来源:main.css 的 .anthology-breadcrumb-bar、.bc-nav、.bc-hamburger 段落。
+   以 wiki.css 中同名规则为准(main.css 无冲突,wiki.css 中无同名规则,直接迁移)。
+*/
+
+/* ── Breadcrumb bar ── */
+.anthology-breadcrumb-bar {
+    background: rgba(255, 255, 255, 0.55);
+    border-bottom: 1px solid var(--wp-border);
+    padding: 0.5rem 0;
+}
+
+.anthology-breadcrumb-bar .bc-inner {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: 1rem;
+}
+
+.anthology-breadcrumb-bar .breadcrumb {
+    margin: 0;
+    font-size: 0.78rem;
+    flex-shrink: 0;
+}
+
+.anthology-breadcrumb-bar .breadcrumb-item a {
+    color: var(--wp-brand);
+    text-decoration: none;
+}
+
+.anthology-breadcrumb-bar .breadcrumb-item.active {
+    color: var(--wp-ink-muted);
+}
+
+.anthology-breadcrumb-bar .breadcrumb-item + .breadcrumb-item::before {
+    color: var(--wp-ink-muted);
+}
+
+/* ── Hero 覆盖状态:breadcrumb bar 透明悬浮于 hero 之上 ── */
+.hero-wrapper:has(.hero-section) .anthology-breadcrumb-bar {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    z-index: 10;
+    background: transparent;
+    border-bottom: none;
+}
+
+.hero-wrapper:has(.hero-section) .bc-nav li a {
+    color: white;
+}
+
+.hero-wrapper:has(.hero-section) .bc-nav li a:hover {
+    color: rgba(255, 255, 255, 0.75);
+}
+
+.hero-wrapper:has(.hero-section) .bc-hamburger {
+    border-color: rgba(255, 255, 255, 0.6);
+    color: white;
+}
+
+/* ── Desktop 导航链接 ── */
+.bc-nav {
+    display: flex;
+    align-items: center;
+    gap: 1.25rem;
+    list-style: none;
+    margin: 0;
+    padding: 0;
+    flex-shrink: 0;
+}
+
+.bc-nav li a {
+    font-size: 0.82rem;
+    color: var(--wp-ink-soft);
+    text-decoration: none;
+    white-space: nowrap;
+    transition: color 0.15s;
+}
+
+.bc-nav li a:hover {
+    color: var(--wp-brand);
+}
+
+.bc-nav li a.active {
+    color: var(--wp-brand);
+    font-weight: 600;
+}
+
+/* ── 汉堡按钮(mobile) ── */
+.bc-hamburger {
+    display: none;
+    background: none;
+    border: 1px solid var(--wp-border);
+    border-radius: 5px;
+    padding: 4px 8px;
+    cursor: pointer;
+    color: var(--wp-ink-soft);
+    line-height: 1;
+}
+
+.bc-hamburger:hover {
+    border-color: var(--wp-brand);
+    color: var(--wp-brand);
+}
+
+/* ── 响应式 ── */
+@media (max-width: 640px) {
+    .bc-nav {
+        display: none;
+    }
+
+    .bc-hamburger {
+        display: inline-flex;
+        align-items: center;
+    }
+}

+ 4 - 0
api-v12/resources/css/layout/_toolbar.css

@@ -0,0 +1,4 @@
+/* resources/css/layout/_toolbar.css
+   页内工具条(过滤器、排序、批量操作等)。
+   当前以 Tabler .page-header 为基础,此文件预留扩展。
+*/

+ 33 - 0
api-v12/resources/css/library.css

@@ -0,0 +1,33 @@
+/* resources/css/library.css
+   library/* 页面 CSS 入口。
+   只做 @import,不写任何样式规则。
+   各栏目专属样式通过页面级 @push('styles') 按需追加。
+*/
+
+/* 1. Tabler 核心 */
+@import "@tabler/core/dist/css/tabler.min.css";
+@import "@tabler/icons-webfont/dist/tabler-icons.min.css";
+
+/* 2. 全站字体 */
+@import url("https://fonts.googleapis.com/css2?family=Noto+Serif:ital,wght@0,400;0,600;1,400&display=swap");
+
+/* 3. 基础变量 */
+@import "./base/_variables.css";
+@import "./base/_reset.css";
+@import "./base/_typography.css";
+
+/* 4. 布局层 */
+@import "./layout/_grid.css";
+@import "./layout/_navbar.css";
+@import "./layout/_drawer.css";
+@import "./layout/_hero.css";
+@import "./layout/_footer.css";
+@import "./layout/_toolbar.css";
+
+/* 5. 公共组件 */
+@import "./components/_search-input.css";
+@import "./components/_search-results.css";
+@import "./components/_card.css";
+@import "./components/_card-book.css";
+@import "./components/_badge.css";
+@import "./components/_pagination.css";

+ 0 - 478
api-v12/resources/css/main.css

@@ -1,478 +0,0 @@
-.book-card {
-    transition: transform 0.2s;
-}
-
-.book-card:hover {
-    transform: translateY(-2px);
-}
-
-.book-cover {
-    height: 200px;
-    object-fit: cover;
-}
-
-@media (max-width: 768px) {
-    .book-cover {
-        height: 150px;
-    }
-}
-
-.nav-overlay {
-    position: absolute;
-}
-
-.hero-section {
-    height: 250px;
-    width: 100%;
-    background-image: url('{{ URL::asset("assets/images/hero-2.jpg") }}');
-    background-size: cover;
-    background-position: center;
-    background-repeat: no-repeat;
-    position: relative;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-}
-
-.hero-overlay {
-    position: absolute;
-    top: 0;
-    left: 0;
-    right: 0;
-    bottom: 0;
-    background: rgba(0, 0, 0, 0.2);
-}
-
-.hero-content {
-    position: relative;
-    z-index: 2;
-    text-align: center;
-    color: white;
-    max-width: 600px;
-    padding: 0 1rem;
-}
-
-.hero-title {
-    font-size: 2.5rem;
-    font-weight: bold;
-    margin-bottom: 1rem;
-    text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
-}
-
-.hero-subtitle {
-    font-size: 1.2rem;
-    margin-bottom: 2rem;
-    text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
-}
-
-.search-box {
-    background: white;
-    border-radius: 0.5rem;
-    padding: 0.5rem;
-    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
-    max-width: 500px;
-    margin: 0 auto;
-}
-
-.feature-card {
-    transition: transform 0.3s ease, box-shadow 0.3s ease;
-    height: 100%;
-}
-
-.feature-card:hover {
-    transform: translateY(-5px);
-    box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
-}
-
-.stats-section {
-    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-    color: white;
-}
-
-.stat-item {
-    text-align: center;
-    padding: 2rem 1rem;
-}
-
-.stat-number {
-    font-size: 2.5rem;
-    font-weight: bold;
-    display: block;
-}
-
-.stat-label {
-    font-size: 1rem;
-    opacity: 0.9;
-    margin-top: 0.5rem;
-}
-
-/* Navigation Styles */
-.top-nav {
-    height: 50px;
-    width: 100%;
-    display: flex;
-    justify-content: flex-end;
-    align-items: center;
-    padding: 0 2rem;
-    position: relative;
-    z-index: 10;
-}
-
-.nav-menu {
-    display: flex;
-    align-items: center;
-    gap: 1.5rem;
-    list-style: none;
-    margin: 0;
-    padding: 0;
-}
-
-.nav-item a {
-    color: white;
-    text-decoration: none;
-    font-size: 0.95rem;
-    font-weight: 500;
-    transition: opacity 0.2s;
-    white-space: nowrap;
-}
-
-.nav-item a:hover {
-    opacity: 0.8;
-}
-
-/* Hamburger Menu */
-.hamburger-btn {
-    display: none;
-    background: rgba(255, 255, 255, 0.2);
-    border: 2px solid white;
-    border-radius: 0.375rem;
-    color: white;
-    font-size: 1.5rem;
-    cursor: pointer;
-    padding: 0.5rem 0.75rem;
-    z-index: 1001;
-    transition: background 0.2s;
-    width: 44px;
-    height: 44px;
-    align-items: center;
-    justify-content: center;
-}
-
-.hamburger-btn:hover {
-    background: rgba(255, 255, 255, 0.3);
-}
-
-/* CSS Hamburger Icon */
-.hamburger-icon {
-    display: flex;
-    flex-direction: column;
-    gap: 4px;
-    width: 24px;
-}
-
-.hamburger-icon span {
-    display: block;
-    height: 2px;
-    background: white;
-    border-radius: 2px;
-    transition: all 0.3s;
-}
-
-.hamburger-btn.active .hamburger-icon span:nth-child(1) {
-    transform: translateY(6px) rotate(45deg);
-}
-
-.hamburger-btn.active .hamburger-icon span:nth-child(2) {
-    opacity: 0;
-}
-
-.hamburger-btn.active .hamburger-icon span:nth-child(3) {
-    transform: translateY(-6px) rotate(-45deg);
-}
-
-.mobile-menu {
-    display: none;
-    position: fixed;
-    top: 0;
-    right: -100%;
-    width: 280px;
-    height: 100vh;
-    background: rgba(0, 0, 0, 0.95);
-    backdrop-filter: blur(10px);
-    transition: right 0.3s ease;
-    z-index: 1000;
-    padding-top: 60px;
-}
-
-.mobile-menu.active {
-    right: 0;
-}
-
-.mobile-nav-menu {
-    list-style: none;
-    padding: 0;
-    margin: 0;
-}
-
-.mobile-nav-item {
-    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
-}
-
-.mobile-nav-item a {
-    display: block;
-    color: white;
-    text-decoration: none;
-    padding: 1rem 2rem;
-    font-size: 1rem;
-    transition: background 0.2s;
-}
-
-.mobile-nav-item a:hover {
-    background: rgba(255, 255, 255, 0.1);
-}
-
-.mobile-overlay {
-    display: none;
-    position: fixed;
-    top: 0;
-    left: 0;
-    width: 100%;
-    height: 100%;
-    background: rgba(0, 0, 0, 0.5);
-    z-index: 999;
-}
-
-.mobile-overlay.active {
-    display: block;
-}
-
-/* Responsive */
-@media (max-width: 768px) {
-    .hero-title {
-        font-size: 2rem;
-    }
-
-    .hero-subtitle {
-        font-size: 1rem;
-    }
-
-    .hero-section {
-        height: 250px;
-    }
-
-    .stat-number {
-        font-size: 2rem;
-    }
-
-    .top-nav {
-        padding: 0 1rem;
-    }
-
-    .nav-menu {
-        display: none;
-    }
-
-    .hamburger-btn {
-        display: flex;
-    }
-
-    .mobile-menu {
-        display: block;
-    }
-}
-
-@media (max-width: 576px) {
-    .hero-title {
-        font-size: 1.5rem;
-    }
-
-    .hero-subtitle {
-        font-size: 0.9rem;
-    }
-
-    .top-nav {
-        padding: 0 0.5rem;
-    }
-}
-
-:root {
-    --sf: #c8860a;
-    --sf-light: #f5e6c8;
-    --sf-pale: #fdf8f0;
-    --ink: #1a1208;
-    --ink-soft: #4a3f2f;
-    --ink-muted: #8a7a68;
-    --bdr: #e8ddd0;
-    --card-bg: #fffdf9;
-}
-
-/* Breadcrumb bar */
-.anthology-breadcrumb-bar {
-    background: rgba(255, 255, 255, 0.55);
-    border-bottom: 1px solid var(--bdr);
-    padding: 0.5rem 0;
-}
-
-.anthology-breadcrumb-bar .bc-inner {
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-    gap: 1rem;
-}
-
-.anthology-breadcrumb-bar .breadcrumb {
-    margin: 0;
-    font-size: 0.78rem;
-    flex-shrink: 0;
-}
-
-.anthology-breadcrumb-bar .breadcrumb-item a {
-    color: var(--sf);
-    text-decoration: none;
-}
-
-.anthology-breadcrumb-bar .breadcrumb-item.active {
-    color: var(--ink-muted);
-}
-
-.anthology-breadcrumb-bar .breadcrumb-item + .breadcrumb-item::before {
-    color: var(--ink-muted);
-}
-
-/* Top nav inside breadcrumb bar */
-.bc-nav {
-    display: flex;
-    align-items: center;
-    gap: 1.25rem;
-    list-style: none;
-    margin: 0;
-    padding: 0;
-    flex-shrink: 0;
-}
-
-.bc-nav li a {
-    font-size: 0.82rem;
-    color: var(--ink-soft);
-    text-decoration: none;
-    white-space: nowrap;
-    transition: color 0.15s;
-}
-
-.bc-nav li a:hover {
-    color: var(--sf);
-}
-
-.bc-nav li a.active {
-    color: var(--sf);
-    font-weight: 600;
-}
-
-/* Mobile nav: hamburger */
-.bc-hamburger {
-    display: none;
-    background: none;
-    border: 1px solid var(--bdr);
-    border-radius: 5px;
-    padding: 4px 8px;
-    cursor: pointer;
-    color: var(--ink-soft);
-    line-height: 1;
-}
-
-.bc-hamburger:hover {
-    border-color: var(--sf);
-    color: var(--sf);
-}
-
-/* Mobile drawer */
-.bc-mobile-overlay {
-    display: none;
-    position: fixed;
-    inset: 0;
-    background: rgba(0, 0, 0, 0.4);
-    z-index: 1040;
-}
-
-.bc-mobile-overlay.open {
-    display: block;
-}
-
-.bc-mobile-drawer {
-    position: fixed;
-    top: 0;
-    right: -100%;
-    width: 240px;
-    height: 100vh;
-    background: var(--card-bg);
-    border-left: 1px solid var(--bdr);
-    z-index: 1050;
-    transition: right 0.25s ease;
-    padding: 1rem 0;
-    box-shadow: -4px 0 20px rgba(0, 0, 0, 0.1);
-}
-
-.bc-mobile-drawer.open {
-    right: 0;
-}
-
-.bc-mobile-drawer-header {
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-    padding: 0.5rem 1.25rem 0.75rem;
-    border-bottom: 1px solid var(--bdr);
-    margin-bottom: 0.5rem;
-}
-
-.bc-mobile-drawer-header span {
-    font-size: 0.85rem;
-    font-weight: 600;
-    color: var(--ink-soft);
-}
-
-.bc-mobile-drawer-close {
-    background: none;
-    border: none;
-    cursor: pointer;
-    color: var(--ink-muted);
-    font-size: 1rem;
-    line-height: 1;
-    padding: 2px;
-}
-
-.bc-mobile-nav {
-    list-style: none;
-    padding: 0;
-    margin: 0;
-}
-
-.bc-mobile-nav li a {
-    display: block;
-    padding: 0.65rem 1.25rem;
-    font-size: 0.9rem;
-    color: var(--ink-soft);
-    text-decoration: none;
-    border-bottom: 1px solid rgba(232, 221, 208, 0.5);
-    transition: background 0.15s;
-}
-
-.bc-mobile-nav li a:hover {
-    background: var(--sf-pale);
-    color: var(--sf);
-}
-
-.bc-mobile-nav li a.active {
-    color: var(--sf);
-    font-weight: 600;
-}
-
-@media (max-width: 640px) {
-    .bc-nav {
-        display: none;
-    }
-
-    .bc-hamburger {
-        display: inline-flex;
-        align-items: center;
-    }
-}

+ 284 - 0
api-v12/resources/css/modules/_anthology.css

@@ -0,0 +1,284 @@
+/* resources/css/modules/_anthology.css
+   文集栏目专属样式。
+   已提取到公共层:
+     - components/_card-book.css → .book-cover 封面组件
+     - components/_card.css      → .author-avatar 头像组件
+*/
+
+/* ══════════════════════════════════════════
+   一、文集卡片(.anthology-card)横向布局
+   ══════════════════════════════════════════ */
+
+.anthology-card {
+    background: var(--wp-card-bg);
+    border: 1px solid var(--wp-border);
+    border-radius: 10px;
+    overflow: hidden;
+    display: flex;
+    transition: box-shadow .25s, transform .25s;
+    margin-bottom: 1.1rem;
+    text-decoration: none;
+    color: inherit;
+}
+
+.anthology-card:hover {
+    box-shadow: 0 8px 28px rgba(200,134,10,.12), 0 2px 8px rgba(0,0,0,.06);
+    transform: translateY(-2px);
+    color: inherit;
+    text-decoration: none;
+}
+
+.anthology-card .book-cover { border-radius: 0; }
+
+.anthology-card__body {
+    padding: 1.1rem 1.4rem;
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    min-width: 0;
+}
+
+.anthology-card__title {
+    font-family: 'Noto Serif SC', 'Noto Serif', Georgia, serif;
+    font-size: 1.1rem;
+    font-weight: 600;
+    color: var(--wp-ink);
+    margin-bottom: .35rem;
+    line-height: 1.4;
+}
+
+.anthology-card:hover .anthology-card__title { color: var(--wp-brand); }
+
+.anthology-card__desc {
+    font-size: .8rem;
+    color: var(--wp-ink-muted);
+    margin-bottom: .65rem;
+    line-height: 1.65;
+}
+
+.anthology-card__author { margin-bottom: .7rem; }
+
+.anthology-card__tags {
+    display: flex;
+    flex-wrap: wrap;
+    gap: .3rem;
+    margin-top: auto;
+}
+
+.anthology-tag {
+    font-size: .7rem;
+    color: var(--wp-ink-muted);
+    background: var(--wp-brand-light);
+    border: 1px solid var(--wp-border);
+    padding: 1px 7px;
+    border-radius: 4px;
+    white-space: nowrap;
+}
+
+.anthology-tag--more {
+    background: transparent;
+    border-color: transparent;
+    color: var(--wp-brand);
+}
+
+.anthology-card__meta {
+    display: flex;
+    align-items: center;
+    gap: .85rem;
+    margin-top: .65rem;
+    padding-top: .65rem;
+    border-top: 1px solid var(--wp-border);
+}
+
+.anthology-meta-item {
+    font-size: .72rem;
+    color: var(--wp-ink-muted);
+    display: flex;
+    align-items: center;
+    gap: .25rem;
+}
+
+/* ══════════════════════════════════════════
+   二、页面头部(index 页)
+   ══════════════════════════════════════════ */
+
+.anthology-page-header {
+    background: linear-gradient(135deg, var(--wp-ink) 0%, #2d2010 100%);
+    padding: 2.25rem 0 2rem;
+    position: relative;
+    overflow: hidden;
+}
+
+.anthology-page-header::before {
+    content: '藏';
+    font-family: 'Noto Serif SC', serif;
+    font-size: 16rem;
+    font-weight: 700;
+    color: rgba(255,255,255,.03);
+    position: absolute;
+    right: -1rem;
+    top: -2.5rem;
+    line-height: 1;
+    pointer-events: none;
+}
+
+.anthology-page-header h1 {
+    font-family: 'Noto Serif SC', 'Noto Serif', Georgia, serif;
+    font-size: 1.75rem;
+    font-weight: 600;
+    color: #fff;
+    margin: 0 0 .3rem;
+    letter-spacing: .08em;
+}
+
+.anthology-page-header p { color: rgba(255,255,255,.45); font-size: .85rem; margin: 0; }
+
+.result-badge {
+    background: var(--wp-brand);
+    color: var(--wp-ink);
+    font-size: .75rem;
+    font-weight: 700;
+    padding: 2px 9px;
+    border-radius: 20px;
+    margin-left: .6rem;
+    vertical-align: middle;
+}
+
+/* ══════════════════════════════════════════
+   三、侧边栏卡片(index + show 共用)
+   ══════════════════════════════════════════ */
+
+.sb-card {
+    background: var(--wp-card-bg);
+    border: 1px solid var(--wp-border);
+    border-radius: 10px;
+    overflow: hidden;
+    margin-bottom: 1.15rem;
+}
+
+.sb-head {
+    padding: .8rem 1.2rem;
+    border-bottom: 1px solid var(--wp-border);
+    font-family: 'Noto Serif SC', 'Noto Serif', Georgia, serif;
+    font-size: .875rem;
+    font-weight: 600;
+    color: var(--wp-ink-soft);
+    letter-spacing: .04em;
+    display: flex;
+    align-items: center;
+    gap: .45rem;
+}
+
+.sb-head::before {
+    content: '';
+    display: block;
+    width: 3px;
+    height: 13px;
+    background: var(--wp-brand);
+    border-radius: 2px;
+}
+
+.smeta-row {
+    display: flex;
+    padding: .7rem 1.2rem;
+    border-bottom: 1px solid var(--wp-border);
+    font-size: .8rem;
+    align-items: flex-start;
+    gap: .45rem;
+}
+.smeta-row:last-child { border-bottom: none; }
+.smeta-label { color: var(--wp-ink-muted); min-width: 65px; flex-shrink: 0; }
+.smeta-value { color: var(--wp-ink-soft); font-weight: 500; }
+.smeta-value a { color: var(--wp-brand); text-decoration: none; }
+.smeta-value a:hover { text-decoration: underline; }
+
+.author-block { display: flex; align-items: center; gap: .8rem; padding: 1.1rem 1.2rem; }
+.author-block-name { font-weight: 600; font-size: .9rem; color: var(--wp-ink); margin-bottom: .18rem; }
+.author-block-stats { font-size: .75rem; color: var(--wp-ink-muted); }
+.author-bio {
+    font-size: .78rem; color: var(--wp-ink-muted); line-height: 1.65;
+    padding: .9rem 1.2rem 1.1rem;
+    border-top: 1px solid var(--wp-border);
+}
+
+.related-ul { list-style: none; padding: 0; margin: 0; }
+.related-ul li a {
+    display: flex; align-items: center; gap: .7rem;
+    padding: .7rem 1.2rem;
+    border-bottom: 1px solid var(--wp-border);
+    text-decoration: none;
+    transition: background .15s;
+}
+.related-ul li:last-child a { border-bottom: none; }
+.related-ul li a:hover { background: var(--wp-surface-alt); }
+.related-t { font-size: .8rem; color: var(--wp-ink-soft); font-weight: 500; margin-bottom: .18rem; line-height: 1.3; }
+.related-ul li a:hover .related-t { color: var(--wp-brand); }
+.related-a { font-size: .7rem; color: var(--wp-ink-muted); }
+
+.author-ul { list-style: none; padding: .35rem 0; margin: 0; }
+.author-ul li a {
+    display: flex; align-items: center; gap: .6rem;
+    padding: .45rem 1.15rem;
+    text-decoration: none;
+    transition: background .15s;
+}
+.author-ul li a:hover { background: var(--wp-surface-alt); }
+
+/* ══════════════════════════════════════════
+   四、文章目录(show 页)
+   ══════════════════════════════════════════ */
+
+.sec-card { background: var(--wp-card-bg); border: 1px solid var(--wp-border); border-radius: 10px; overflow: hidden; margin-bottom: 1.3rem; }
+.sec-header { padding: .85rem 1.4rem; border-bottom: 1px solid var(--wp-border); display: flex; align-items: center; gap: .55rem; }
+.sec-bar { width: 3px; height: 15px; background: var(--wp-brand); border-radius: 2px; flex-shrink: 0; }
+.sec-title { font-family: 'Noto Serif SC', 'Noto Serif', Georgia, serif; font-size: .9rem; font-weight: 600; color: var(--wp-ink-soft); letter-spacing: .04em; }
+.sec-count { margin-left: auto; font-size: .75rem; color: var(--wp-ink-muted); background: var(--wp-brand-light); padding: 2px 8px; border-radius: 10px; }
+.sec-body { padding: 1.15rem 1.4rem; font-size: .855rem; color: var(--wp-ink-soft); line-height: 1.95; }
+.sec-body p { margin-bottom: .8rem; }
+.sec-body p:last-child { margin-bottom: 0; }
+
+.toc-ul { list-style: none; padding: .35rem 0; margin: 0; }
+.toc-ul li a { display: flex; align-items: center; padding: .65rem 1.4rem; text-decoration: none; border-bottom: 1px solid rgba(232,221,208,.5); transition: background .15s; }
+.toc-ul li:last-child a { border-bottom: none; }
+.toc-ul li a:hover { background: var(--wp-surface-alt); }
+.toc-num { font-size: .72rem; color: var(--wp-ink-muted); width: 26px; flex-shrink: 0; }
+.toc-name { font-size: .855rem; color: var(--wp-ink-soft); flex: 1; line-height: 1.4; }
+.toc-ul li a:hover .toc-name { color: var(--wp-brand); }
+.toc-arrow { color: var(--wp-border); font-size: .85rem; }
+.toc-ul li a:hover .toc-arrow { color: var(--wp-brand); }
+
+/* ══════════════════════════════════════════
+   五、Hero(show 页)
+   ══════════════════════════════════════════ */
+
+.anthology-hero { background: linear-gradient(135deg, var(--wp-ink) 0%, #2d2010 100%); padding: 2.5rem 0; }
+.hero-inner { display: flex; gap: 2.25rem; align-items: flex-start; }
+.hero-content { flex: 1; min-width: 0; }
+
+.hero-title { font-family: 'Noto Serif SC', 'Noto Serif', Georgia, serif; font-size: 1.75rem; font-weight: 700; color: #fff; line-height: 1.3; margin-bottom: .4rem; }
+.hero-subtitle { font-size: .88rem; color: rgba(255,255,255,.45); font-style: italic; letter-spacing: .04em; margin-bottom: 1.1rem; }
+.hero-tags { display: flex; flex-wrap: wrap; gap: .35rem; margin-bottom: 1.3rem; }
+.hero-tag { font-size: .72rem; padding: 2px 9px; border-radius: 20px; background: rgba(200,134,10,.2); color: var(--wp-brand); border: 1px solid rgba(200,134,10,.3); }
+.hero-info-row { display: flex; flex-wrap: wrap; gap: 1.4rem; margin-bottom: 1.3rem; }
+.hi-item { display: flex; align-items: center; gap: .45rem; }
+.hi-label { font-size: .72rem; color: rgba(255,255,255,.4); letter-spacing: .04em; display: block; }
+.hi-value { font-size: .83rem; color: rgba(255,255,255,.82); display: block; }
+.hero-desc { font-size: .85rem; color: rgba(255,255,255,.6); line-height: 1.85; margin-bottom: 1.6rem; max-width: 600px; }
+
+.btn-read-primary { background: var(--wp-brand); color: var(--wp-ink); font-weight: 700; font-size: .88rem; padding: .55rem 1.6rem; border-radius: 6px; border: none; cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; gap: .45rem; transition: background .2s, transform .15s; }
+.btn-read-primary:hover { background: #dea020; color: var(--wp-ink); transform: translateY(-1px); }
+.btn-outline-hero { background: transparent; color: rgba(255,255,255,.7); font-size: .85rem; padding: .5rem 1.3rem; border-radius: 6px; border: 1px solid rgba(255,255,255,.2); cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; gap: .4rem; transition: all .2s; margin-left: .65rem; }
+.btn-outline-hero:hover { border-color: rgba(255,255,255,.5); color: #fff; }
+
+/* ══════════════════════════════════════════
+   六、响应式
+   ══════════════════════════════════════════ */
+
+@media (max-width: 900px) {
+    .hero-inner { flex-direction: column; align-items: center; }
+}
+
+@media (max-width: 768px) {
+    .anthology-card { flex-direction: column; }
+    .anthology-card .book-cover--md { width: 100%; min-width: unset; height: 90px; }
+}

+ 308 - 0
api-v12/resources/css/modules/_library-index.css

@@ -0,0 +1,308 @@
+/* resources/css/modules/_library-index.css
+   Library 门户首页专属样式。
+*/
+
+/* ══════════════════════════════════════════
+   一、区块通用结构
+   ══════════════════════════════════════════ */
+
+.lib-section {
+    margin-top: 2rem;
+    margin-bottom: 0.5rem;
+}
+
+.lib-section__header {
+    display: flex;
+    align-items: center;
+    gap: 0.75rem;
+    margin-bottom: 1rem;
+    padding-bottom: 0.625rem;
+    border-bottom: 1px solid var(--tblr-border-color);
+}
+
+.lib-section__title {
+    font-size: 1rem;
+    font-weight: 600;
+    color: var(--tblr-body-color);
+    margin: 0;
+    display: flex;
+    align-items: center;
+    gap: 0.4rem;
+    flex: 1;
+}
+
+.lib-section__title .ti {
+    font-size: 1.125rem;
+    color: var(--tblr-secondary);
+}
+
+.lib-section__more {
+    font-size: 0.8125rem;
+    color: var(--tblr-primary);
+    text-decoration: none;
+    white-space: nowrap;
+    display: flex;
+    align-items: center;
+    gap: 0.25rem;
+    flex-shrink: 0;
+}
+
+.lib-section__more:hover {
+    text-decoration: underline;
+}
+
+/* ══════════════════════════════════════════
+   二、三藏分类卡片头部
+   ══════════════════════════════════════════ */
+
+.lib-cat-card__head {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 0.75rem;
+}
+
+.lib-cat-card__name {
+    font-size: 0.9375rem;
+    font-weight: 600;
+    color: var(--tblr-body-color);
+}
+
+.lib-cat-card__more {
+    font-size: 0.75rem;
+    color: var(--tblr-primary);
+    text-decoration: none;
+    display: flex;
+    align-items: center;
+    gap: 0.2rem;
+    flex-shrink: 0;
+}
+
+.lib-cat-card__more:hover {
+    text-decoration: underline;
+}
+
+/* ══════════════════════════════════════════
+   三、"持续更新中" 标签
+   ══════════════════════════════════════════ */
+
+.lib-live-badge {
+    display: inline-flex;
+    align-items: center;
+    gap: 5px;
+    font-size: 0.6875rem;
+    font-weight: 500;
+    color: #3b6d11;
+    background: #eaf3de;
+    border: 1px solid #c0dd97;
+    border-radius: 20px;
+    padding: 2px 8px;
+    margin-left: 0.25rem;
+}
+
+.lib-live-dot {
+    width: 6px;
+    height: 6px;
+    border-radius: 50%;
+    background: #639922;
+    flex-shrink: 0;
+    animation: lib-live-pulse 2s ease-in-out infinite;
+}
+
+@keyframes lib-live-pulse {
+    0%, 100% { opacity: 1; }
+    50%       { opacity: 0.4; }
+}
+
+/* ══════════════════════════════════════════
+   四、最新译文列表
+   ══════════════════════════════════════════ */
+
+.lib-recent {
+    padding: 0;   /* 覆盖 wiki-card 默认 padding,由 item 自己管理 */
+}
+
+.lib-recent__item {
+    display: flex;
+    align-items: center;
+    gap: 0.875rem;
+    padding: 0.75rem 1.25rem;
+    border-bottom: 1px solid var(--tblr-border-color);
+    text-decoration: none;
+    color: inherit;
+    transition: background 0.12s;
+}
+
+.lib-recent__item:last-child {
+    border-bottom: none;
+}
+
+.lib-recent__item:hover {
+    background: var(--tblr-bg-surface-secondary);
+    color: inherit;
+    text-decoration: none;
+}
+
+/* 封面缩略图固定不缩 */
+.lib-recent__item .book-cover {
+    flex-shrink: 0;
+}
+
+.lib-recent__info {
+    flex: 1;
+    min-width: 0;
+}
+
+.lib-recent__title {
+    font-size: 0.9375rem;
+    font-weight: 500;
+    color: var(--tblr-body-color);
+    margin-bottom: 0.25rem;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+.lib-recent__item:hover .lib-recent__title {
+    color: var(--tblr-primary);
+}
+
+.lib-recent__meta {
+    font-size: 0.75rem;
+    color: var(--tblr-secondary);
+    display: flex;
+    align-items: center;
+    gap: 0.375rem;
+    flex-wrap: wrap;
+}
+
+.lib-recent__sep {
+    opacity: 0.5;
+}
+
+.lib-recent__right {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-end;
+    gap: 4px;
+    flex-shrink: 0;
+}
+
+.lib-recent__time {
+    font-size: 0.75rem;
+    color: var(--tblr-secondary);
+    white-space: nowrap;
+}
+
+/* 新增 / 更新 徽章 */
+.lib-new-badge {
+    font-size: 0.625rem;
+    font-weight: 600;
+    padding: 1px 6px;
+    border-radius: 20px;
+    background: #e6f1fb;
+    color: #185fa5;
+    border: 1px solid #b5d4f4;
+    white-space: nowrap;
+}
+
+.lib-update-badge {
+    font-size: 0.625rem;
+    font-weight: 600;
+    padding: 1px 6px;
+    border-radius: 20px;
+    background: #f1efe8;
+    color: #5f5e5a;
+    border: 1px solid #d3d1c7;
+    white-space: nowrap;
+}
+
+/* ══════════════════════════════════════════
+   五、栏目导航卡片
+   ══════════════════════════════════════════ */
+
+.lib-nav-card {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    text-align: center;
+    padding: 1.25rem 1rem;
+    background: var(--tblr-bg-surface);
+    border: 1px solid var(--tblr-border-color);
+    border-radius: var(--tblr-border-radius-lg);
+    text-decoration: none;
+    color: inherit;
+    transition: background 0.12s, transform 0.15s, box-shadow 0.15s;
+    height: 100%;
+}
+
+.lib-nav-card:hover {
+    background: var(--tblr-bg-surface-secondary);
+    transform: translateY(-2px);
+    box-shadow: 0 4px 12px rgba(0,0,0,.06);
+    color: inherit;
+    text-decoration: none;
+}
+
+.lib-nav-card__icon {
+    font-size: 1.75rem;
+    color: var(--tblr-primary);
+    margin-bottom: 0.625rem;
+    display: block;
+}
+
+.lib-nav-card__name {
+    font-size: 0.9375rem;
+    font-weight: 600;
+    color: var(--tblr-body-color);
+    margin-bottom: 0.25rem;
+}
+
+.lib-nav-card__desc {
+    font-size: 0.75rem;
+    color: var(--tblr-secondary);
+    line-height: 1.4;
+}
+
+/* ══════════════════════════════════════════
+   六、响应式
+   ══════════════════════════════════════════ */
+
+/* 最新译文:手机隐藏作者,只显示标题+分类+时间 */
+@media (max-width: 576px) {
+    .lib-recent__item {
+        padding: 0.625rem 1rem;
+        gap: 0.625rem;
+    }
+
+    .lib-recent__author,
+    .lib-recent__sep:last-of-type {
+        display: none;
+    }
+
+    .lib-recent__title {
+        font-size: 0.875rem;
+    }
+}
+
+/* 栏目导航:手机 2 列,平板 3 列,桌面 5 列(Bootstrap row g-3 自动处理) */
+@media (max-width: 575px) {
+    .lib-nav-card {
+        padding: 1rem 0.75rem;
+    }
+
+    .lib-nav-card__icon {
+        font-size: 1.5rem;
+    }
+
+    .lib-nav-card__desc {
+        display: none;   /* 手机隐藏描述,只显示图标+名称 */
+    }
+}
+
+/* 三藏分类卡片:手机 2 列已由 Bootstrap col-6 处理 */
+@media (max-width: 575px) {
+    .lib-cat-card__more {
+        display: none;  /* 手机隐藏"更多"链接,避免挤压 */
+    }
+}

+ 313 - 0
api-v12/resources/css/modules/_reader.css

@@ -0,0 +1,313 @@
+/* resources/css/modules/_reader.css
+   全站阅读页专属样式。
+   来源:原 resources/css/reader.css(旧)+ reader 重构新增内容合并。
+   以旧 reader.css 内容为准,新增部分追加在末尾。
+*/
+
+/* ══════════════════════════════════════════
+   一、基础
+   ══════════════════════════════════════════ */
+
+body {
+    font-family: "Inter", sans-serif;
+    transition: background-color 0.3s, color 0.3s;
+}
+
+/* ══════════════════════════════════════════
+   二、主布局容器
+   ══════════════════════════════════════════ */
+
+.main-container {
+    display: flex;
+    gap: 20px;
+    padding: 20px;
+    max-width: 1400px;
+    margin: 0 auto;
+}
+
+/* ── TOC 侧边栏(左) ── */
+.toc-sidebar {
+    width: 250px;
+    flex-shrink: 0;
+    display: none;
+    position: sticky;
+    top: 0;
+    height: 100vh;
+    overflow-y: auto;
+    overflow-x: hidden;
+    scrollbar-width: thin;
+}
+
+.toc-sidebar .card-body {
+    height: 100%;
+    overflow-y: auto;
+    overflow-x: hidden;
+}
+
+/* ── 正文区(中)── */
+.content-area {
+    flex: 1;            /* 替代原 flex-grow:1,配合 min-width 防止收缩 */
+    min-width: 0;       /* 修复:内容较窄时列不收缩 */
+}
+
+/* ── 右侧边栏 ── */
+.right-sidebar {
+    width: 220px;       /* 收窄,原 300px 偏宽 */
+    flex-shrink: 0;
+    display: none;
+}
+
+.related-books {
+    margin-top: 30px;
+}
+
+.card-img-container {
+    height: 150px;
+    overflow: hidden;
+}
+
+.card-img-container img {
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+}
+
+/* ══════════════════════════════════════════
+   三、响应式
+   ══════════════════════════════════════════ */
+
+@media (max-width: 767px) {
+    .content-area {
+        width: 100%;
+    }
+
+    .main-container {
+        padding: 0;
+    }
+
+    .card {
+        border: none;
+    }
+}
+
+@media (min-width: 768px) {
+    .toc-sidebar {
+        display: block;
+    }
+    /* content-area 不设 max-width,由 flex:1 + min-width:0 自动占满剩余空间 */
+}
+
+@media (min-width: 992px) {
+    .right-sidebar {
+        display: block;
+    }
+    /* 同上,不限制 max-width */
+}
+
+/* ══════════════════════════════════════════
+   四、暗色模式
+   ══════════════════════════════════════════ */
+
+.dark-mode {
+    background-color: #1a1a1a;
+    color: #ffffff;
+}
+
+.dark-mode .card {
+    background-color: #2a2a2a;
+    border-color: #3a3a3a;
+    color: #ffffff;
+}
+
+.dark-mode .navbar {
+    background-color: #2a2a2a;
+}
+
+.dark-mode .offcanvas {
+    background-color: #2a2a2a;
+    color: #ffffff;
+}
+
+.dark-mode .offcanvas .nav-link {
+    color: #ffffff;
+}
+
+.dark-mode .toc-sidebar,
+.dark-mode .right-sidebar {
+    background-color: #2a2a2a;
+}
+
+/* ══════════════════════════════════════════
+   五、目录样式(wiki toc 风格)
+   ══════════════════════════════════════════ */
+
+.toc-sidebar ul,
+.offcanvas-body ul {
+    list-style: none;
+    padding: 0;
+}
+
+.toc-sidebar ul li,
+.offcanvas-body ul li {
+    padding: 5px 0;
+}
+
+.toc-sidebar ul li a,
+.offcanvas-body ul li a {
+    color: #206bc4;
+    text-decoration: none;
+}
+
+.toc-sidebar ul li a:hover      { text-decoration: none; }
+.offcanvas-body ul li a:hover   { text-decoration: underline; }
+
+.dark-mode .toc-sidebar ul li a,
+.dark-mode .offcanvas-body ul li a {
+    color: #4dabf7;
+}
+
+.toc-level-1 { padding-left: 0 !important; }
+.toc-level-2 { padding-left: 16px !important; }
+.toc-level-3 { padding-left: 24px !important; }
+.toc-level-4 { padding-left: 36px !important; }
+
+.toc-disabled {
+    color: #6c757d;
+    cursor: not-allowed;
+    pointer-events: none;
+}
+
+.dark-mode .toc-disabled { color: #adb5bd; }
+
+.toc_item {
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+.toc_item:hover {
+    color: #206bc4 !important;
+    background: rgba(32, 107, 196, 0.15);
+    border-radius: 2px;
+    cursor: pointer;
+}
+
+.toc_item a,
+.toc_item span {
+    display: block;
+    width: 100%;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+.toc-active {
+    color: #206bc4 !important;
+    font-weight: 600;
+    background: rgba(32, 107, 196, 0.08);
+    border-left: 3px solid #206bc4;
+    border-radius: 2px;
+    cursor: default;
+    pointer-events: none;
+}
+
+.dark-mode .toc-active {
+    color: #4dabf7 !important;
+    background: rgba(77, 171, 247, 0.1);
+    border-left-color: #4dabf7;
+}
+
+/* ══════════════════════════════════════════
+   六、正文内容
+   ══════════════════════════════════════════ */
+
+.origin {
+    color: darkred;
+}
+
+/* 术语引用 */
+.term-ref {
+    cursor: pointer;
+    text-decoration: underline dotted;
+    text-underline-offset: 4px;
+}
+
+.term-ref:hover {
+    color: var(--tblr-primary);
+}
+
+.term_invalid {
+    color: red;
+}
+
+/* ══════════════════════════════════════════
+   七、Navbar 图标尺寸修正
+   ti 图标显式设定字号,防止继承后偏小
+   ══════════════════════════════════════════ */
+
+.navbar .nav-link .ti {
+    font-size: 1.125rem;
+    vertical-align: middle;
+}
+
+/* ══════════════════════════════════════════
+   八、版本卡片(右侧边栏,wiki 侧边栏同款)
+   ══════════════════════════════════════════ */
+
+.reader-channel-card {
+    background: var(--tblr-bg-surface);
+    border: 1px solid var(--tblr-border-color);
+    border-radius: var(--tblr-border-radius-lg);
+    padding: 1rem 1.125rem;
+    margin-bottom: 1rem;
+}
+
+.dark-mode .reader-channel-card {
+    background-color: #2a2a2a;
+    border-color: #3a3a3a;
+}
+
+.reader-channel-title {
+    font-size: 0.6875rem;
+    font-weight: 500;
+    letter-spacing: 0.05em;
+    text-transform: uppercase;
+    color: var(--tblr-secondary);
+    margin-bottom: 0.75rem;
+}
+
+.reader-channel-list {
+    list-style: none;
+    padding: 0;
+    margin: 0;
+}
+
+.reader-channel-list li {
+    margin-bottom: 2px;
+}
+
+.reader-channel-list a {
+    display: block;
+    font-size: 0.8125rem;
+    color: var(--tblr-body-color);
+    text-decoration: none;
+    padding: 5px 8px;
+    border-radius: var(--tblr-border-radius);
+    transition: background 0.12s;
+}
+
+.reader-channel-list a:hover {
+    background: var(--tblr-bg-surface-secondary);
+}
+
+.reader-channel-list a.active {
+    background: var(--tblr-bg-surface-secondary);
+    font-weight: 500;
+    color: var(--tblr-primary);
+}
+
+.reader-channel-lang {
+    font-size: 0.75rem;
+    color: var(--tblr-secondary);
+    display: block;
+}

+ 355 - 0
api-v12/resources/css/modules/_tipitaka.css

@@ -0,0 +1,355 @@
+/* resources/css/modules/_tipitaka.css
+   Tipitaka 栏目专属样式。
+   来源:book-list.blade.php / book-item.blade.php 内联样式提取,
+   去除 CDN 引入,风格与 wiki 对齐。
+*/
+
+/* ══════════════════════════════════════════
+   一、书籍卡片(.card-book)
+   纵向:封面上 + 信息下
+   ══════════════════════════════════════════ */
+
+.card-book {
+    transition: transform 0.2s, box-shadow 0.2s;
+}
+
+.card-book:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
+}
+
+.card-book__link {
+    display: block;
+    text-decoration: none;
+    color: inherit;
+}
+
+.card-book__link:hover {
+    text-decoration: none;
+    color: inherit;
+}
+
+/* 封面:撑满卡片宽度 */
+.card-book .book-cover--md {
+    width: 100%;
+    min-width: unset;
+    height: 200px;
+    border-radius: var(--tblr-border-radius);
+}
+
+@media (max-width: 768px) {
+    .card-book .book-cover--md {
+        height: 150px;
+    }
+}
+
+/* 信息区 */
+.card-book__info {
+    padding: 0.75rem 0 0;
+}
+
+.card-book__title {
+    font-size: 0.9375rem;
+    font-weight: 600;
+    color: var(--tblr-body-color);
+    margin-bottom: 0.25rem;
+    line-height: 1.4;
+    display: -webkit-box;
+    -webkit-line-clamp: 2;
+    -webkit-box-orient: vertical;
+    overflow: hidden;
+}
+
+.card-book:hover .card-book__title {
+    color: var(--tblr-primary);
+}
+
+.card-book__author {
+    font-size: 0.8125rem;
+    color: var(--tblr-secondary);
+    margin-bottom: 0.25rem;
+}
+
+.card-book__publisher {
+    font-size: 0.8125rem;
+    color: var(--tblr-secondary);
+    margin-bottom: 0.375rem;
+}
+
+.card-book__publisher-link {
+    color: var(--tblr-primary);
+    text-decoration: none;
+}
+
+.card-book__publisher-link:hover {
+    text-decoration: underline;
+}
+
+.card-book__badges {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 4px;
+    margin-top: 0.375rem;
+}
+
+.card-book__badge {
+    display: inline-block;
+    padding: 2px 8px;
+    background: var(--tblr-bg-surface-secondary);
+    border: 1px solid var(--tblr-border-color);
+    border-radius: 20px;
+    font-size: 0.6875rem;
+    color: var(--tblr-secondary);
+}
+
+/* ══════════════════════════════════════════
+   二、书籍网格(.book-grid)
+   ══════════════════════════════════════════ */
+
+.book-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
+    gap: 1.25rem;
+}
+
+@media (max-width: 575px) {
+    .book-grid {
+        grid-template-columns: repeat(2, 1fr);
+        gap: 0.875rem;
+    }
+}
+
+/* ══════════════════════════════════════════
+   三、show 页 — 文字截断
+   ══════════════════════════════════════════ */
+
+.line2 {
+    display: -webkit-box;
+    -webkit-line-clamp: 2;
+    -webkit-box-orient: vertical;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+/* ══════════════════════════════════════════
+   四、子分类网格
+   ══════════════════════════════════════════ */
+
+.tipitaka-sub-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
+    gap: 8px;
+}
+
+/* ══════════════════════════════════════════
+   五、过滤器区
+   ══════════════════════════════════════════ */
+
+.tipitaka-filters {
+    display: flex;
+    flex-direction: column;
+    gap: 0.875rem;
+}
+
+.tipitaka-filter-row {
+    display: flex;
+    align-items: flex-start;
+    gap: 0.75rem;
+}
+
+.tipitaka-filter-label {
+    font-size: 0.75rem;
+    font-weight: 500;
+    color: var(--tblr-secondary);
+    white-space: nowrap;
+    padding-top: 4px;
+    min-width: 2.5rem;
+}
+
+.tipitaka-filter-pills {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 6px;
+    flex: 1;
+}
+
+.tipitaka-pill {
+    display: inline-flex;
+    align-items: center;
+    gap: 4px;
+    font-size: 0.8125rem;
+    padding: 3px 10px;
+    border-radius: 20px;
+    border: 1px solid var(--tblr-border-color);
+    color: var(--tblr-body-color);
+    background: var(--tblr-bg-surface);
+    text-decoration: none;
+    transition: background 0.12s, border-color 0.12s, color 0.12s;
+    white-space: nowrap;
+}
+
+.tipitaka-pill:hover {
+    background: var(--tblr-bg-surface-secondary);
+    border-color: var(--tblr-border-color-dark, #adb5bd);
+    color: var(--tblr-body-color);
+    text-decoration: none;
+}
+
+.tipitaka-pill--active {
+    background: var(--tblr-primary);
+    border-color: var(--tblr-primary);
+    color: #fff;
+    pointer-events: none;
+}
+
+.tipitaka-pill--active:hover {
+    background: var(--tblr-primary);
+    color: #fff;
+}
+
+.tipitaka-pill-count {
+    font-size: 0.6875rem;
+    opacity: 0.75;
+}
+
+.tipitaka-author-select {
+    max-width: 220px;
+}
+
+.tipitaka-filter-clear {
+    display: flex;
+    justify-content: flex-end;
+    padding-top: 0.25rem;
+    border-top: 1px solid var(--tblr-border-color);
+}
+
+.tipitaka-clear-btn {
+    font-size: 0.75rem;
+    color: var(--tblr-secondary);
+    text-decoration: none;
+    display: inline-flex;
+    align-items: center;
+    gap: 4px;
+    transition: color 0.12s;
+}
+
+.tipitaka-clear-btn:hover {
+    color: var(--tblr-danger);
+}
+
+/* ══════════════════════════════════════════
+   六、排序栏
+   ══════════════════════════════════════════ */
+
+.tipitaka-sort-bar {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 0 0.25rem;
+}
+
+.tipitaka-sort-bar__count {
+    font-size: 0.8125rem;
+    color: var(--tblr-secondary);
+}
+
+.tipitaka-sort-bar__count strong {
+    color: var(--tblr-body-color);
+    font-weight: 500;
+}
+
+.tipitaka-sort-bar__right {
+    display: flex;
+    align-items: center;
+    gap: 0.5rem;
+}
+
+.tipitaka-sort-bar__label {
+    font-size: 0.8125rem;
+    color: var(--tblr-secondary);
+    white-space: nowrap;
+}
+
+.tipitaka-sort-select {
+    width: auto;
+    min-width: 80px;
+}
+
+/* ══════════════════════════════════════════
+   七、活跃译者列表
+   ══════════════════════════════════════════ */
+
+.tipitaka-author-list {
+    list-style: none;
+    padding: 0;
+    margin: 0;
+}
+
+.tipitaka-author-list li {
+    border-bottom: 1px solid var(--tblr-border-color);
+}
+
+.tipitaka-author-list li:last-child {
+    border-bottom: none;
+}
+
+.tipitaka-author-item {
+    display: flex;
+    align-items: center;
+    gap: 0.625rem;
+    padding: 6px 0;
+    text-decoration: none;
+    color: inherit;
+    transition: background 0.12s;
+}
+
+.tipitaka-author-item:hover {
+    color: var(--tblr-primary);
+    text-decoration: none;
+}
+
+.tipitaka-author-item__info {
+    display: flex;
+    flex-direction: column;
+    min-width: 0;
+}
+
+.tipitaka-author-item__name {
+    font-size: 0.8125rem;
+    font-weight: 500;
+    color: var(--tblr-body-color);
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+.tipitaka-author-item:hover .tipitaka-author-item__name {
+    color: var(--tblr-primary);
+}
+
+.tipitaka-author-item__count {
+    font-size: 0.6875rem;
+    color: var(--tblr-secondary);
+}
+
+/* ══════════════════════════════════════════
+   八、响应式
+   ══════════════════════════════════════════ */
+
+@media (max-width: 768px) {
+    /* 过滤器:label 和 pills 垂直排列 */
+    .tipitaka-filter-row {
+        flex-direction: column;
+        gap: 0.375rem;
+    }
+
+    .tipitaka-filter-label {
+        padding-top: 0;
+    }
+
+    /* 作者下拉撑满 */
+    .tipitaka-author-select {
+        max-width: 100%;
+        width: 100%;
+    }
+}

+ 503 - 0
api-v12/resources/css/modules/_wiki.css

@@ -0,0 +1,503 @@
+/* resources/css/modules/_wiki.css
+   Wiki 栏目专属样式。
+   公共部分(布局、卡片、侧边栏、TOC、相关列表、meta表格、条目头部、精选网格)
+   已提取至:
+     - layout/_grid.css      → .wiki-layout 三栏布局
+     - components/_card.css  → .wiki-card / .wiki-sidebar-* / .wiki-cat-list /
+                               .wiki-toc-list / .wiki-related-list / .wiki-meta-table /
+                               .wiki-entry-header / .wiki-featured-grid / .author-avatar
+   本文件只保留 wiki 专属内容。
+*/
+
+/* ══════════════════════════════════════════
+   一、语言版本切换
+   ══════════════════════════════════════════ */
+
+.wiki-entry-langs-inline {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 6px;
+    margin-bottom: 0.5rem;
+}
+
+.wiki-lang-pill {
+    font-size: 0.75rem;
+    padding: 3px 10px;
+    border-radius: 20px;
+    border: 1px solid var(--tblr-border-color);
+    color: var(--tblr-secondary);
+}
+
+.wiki-lang-pill strong {
+    font-weight: 500;
+    color: var(--tblr-body-color);
+    margin-right: 3px;
+}
+
+.wiki-entry-lang-switcher {
+    display: flex;
+    flex-wrap: wrap;
+    align-items: center;
+    gap: 6px;
+    padding: 0.625rem 0.875rem;
+    background: var(--tblr-bg-surface-secondary);
+    border-radius: var(--tblr-border-radius);
+    margin-bottom: 1.25rem;
+    font-size: 0.8125rem;
+}
+
+.wiki-entry-lang-label { color: var(--tblr-secondary); }
+
+.wiki-entry-lang-btn {
+    font-size: 0.75rem;
+    padding: 3px 10px;
+    border-radius: 20px;
+    border: 1px solid var(--tblr-border-color);
+    color: var(--tblr-primary);
+    text-decoration: none;
+    transition: background 0.12s;
+}
+
+.wiki-entry-lang-btn:hover { background: var(--tblr-bg-surface); }
+
+/* ══════════════════════════════════════════
+   二、质量标签
+   ══════════════════════════════════════════ */
+
+.wiki-quality-badge {
+    display: inline-flex;
+    align-items: center;
+    gap: 5px;
+    font-size: 0.6875rem;
+    padding: 3px 10px;
+    border-radius: 20px;
+    font-weight: 500;
+    margin-bottom: 4px;
+}
+
+.wiki-quality-dot {
+    width: 6px;
+    height: 6px;
+    border-radius: 50%;
+    flex-shrink: 0;
+}
+
+.wiki-badge--featured { background: #eaf3de; color: #3b6d11; border: 1px solid #c0dd97; }
+.wiki-badge--featured .wiki-quality-dot { background: #639922; }
+
+.wiki-badge--review   { background: #faeeda; color: #854f0b; border: 1px solid #fac775; }
+.wiki-badge--review   .wiki-quality-dot { background: #ba7517; }
+
+.wiki-badge--stub     { background: #f1efe8; color: #5f5e5a; border: 1px solid #d3d1c7; }
+.wiki-badge--stub     .wiki-quality-dot { background: #888780; }
+
+/* ══════════════════════════════════════════
+   三、标签
+   ══════════════════════════════════════════ */
+
+.wiki-tags {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 6px;
+    padding-top: 1rem;
+    border-top: 1px solid var(--tblr-border-color);
+    margin-top: 1.5rem;
+}
+
+.wiki-tag {
+    font-size: 0.75rem;
+    padding: 3px 10px;
+    border-radius: 20px;
+    border: 1px solid var(--tblr-border-color);
+    color: var(--tblr-secondary);
+    text-decoration: none;
+    transition: background 0.12s;
+}
+
+.wiki-tag:hover {
+    background: var(--tblr-bg-surface-secondary);
+    color: var(--tblr-body-color);
+}
+
+/* ══════════════════════════════════════════
+   四、首页组件
+   ══════════════════════════════════════════ */
+
+.wiki-today-banner {
+    background: var(--tblr-bg-surface-secondary);
+    border: 1px solid var(--tblr-border-color);
+    border-radius: var(--tblr-border-radius-lg);
+    padding: 1.25rem 1.5rem;
+    display: flex;
+    gap: 1.25rem;
+    align-items: flex-start;
+}
+
+.wiki-today-icon {
+    width: 48px;
+    height: 48px;
+    border-radius: var(--tblr-border-radius);
+    background: #e1f5ee;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 1.375rem;
+    flex-shrink: 0;
+}
+
+.wiki-today-label {
+    font-size: 0.6875rem;
+    text-transform: uppercase;
+    letter-spacing: 0.05em;
+    color: var(--tblr-secondary);
+    margin-bottom: 4px;
+}
+
+.wiki-today-title {
+    font-family: "Noto Serif", Georgia, serif;
+    font-size: 1.125rem;
+    font-weight: 600;
+    margin-bottom: 6px;
+    color: var(--tblr-body-color);
+}
+
+.wiki-today-snippet {
+    font-size: 0.875rem;
+    color: var(--tblr-secondary);
+    line-height: 1.6;
+}
+
+.wiki-today-link {
+    font-size: 0.8125rem;
+    color: var(--tblr-primary);
+    text-decoration: none;
+    margin-top: 10px;
+    display: inline-block;
+}
+
+/* ══════════════════════════════════════════
+   五、term-ref 行内术语标记
+   ══════════════════════════════════════════ */
+
+.term-ref {
+    font-style: italic;
+    color: var(--tblr-primary);
+    border-bottom: 1px dotted var(--tblr-primary);
+    cursor: pointer;
+}
+
+/* ══════════════════════════════════════════
+   六、Term Popover(桌面)
+   ══════════════════════════════════════════ */
+
+.wiki-term-popover.popover {
+    max-width: 280px;
+    font-size: 0.8125rem;
+    border: 0.5px solid var(--tblr-border-color);
+    border-radius: var(--tblr-border-radius-lg);
+    box-shadow: none;
+}
+
+.wiki-term-popover .popover-arrow { display: none; }
+.wiki-term-popover .popover-body  { padding: 0; }
+
+.wiki-popover-word {
+    font-family: "Noto Serif", Georgia, serif;
+    font-size: 0.9375rem;
+    font-style: italic;
+    font-weight: 600;
+    color: var(--tblr-primary);
+    padding: 10px 14px;
+    border-bottom: 0.5px solid var(--tblr-border-color);
+}
+
+.wiki-popover-meaning {
+    font-size: 0.9rem;
+    font-weight: 500;
+    color: var(--tblr-body-color);
+    padding: 8px 14px 4px;
+}
+
+.wiki-popover-summary {
+    font-size: 0.8125rem;
+    color: var(--tblr-secondary);
+    line-height: 1.55;
+    padding: 0 14px 12px;
+}
+
+/* ══════════════════════════════════════════
+   七、Term Drawer(移动端,所有阅读页公用)
+   ══════════════════════════════════════════ */
+
+.wiki-term-drawer {
+    border-radius: 1rem 1rem 0 0;
+    max-height: 55vh;
+}
+
+.wiki-term-drawer .offcanvas-header {
+    padding-top: 0.6rem;
+    padding-bottom: 0.6rem;
+}
+
+.wiki-drawer-word {
+    font-family: "Noto Serif", Georgia, serif;
+    font-size: 1.125rem;
+    font-style: italic;
+    font-weight: 600;
+    color: var(--tblr-primary);
+}
+
+.wiki-drawer-meaning {
+    font-size: 0.9375rem;
+    font-weight: 500;
+    color: var(--tblr-body-color);
+    margin-top: 2px;
+}
+
+.wiki-drawer-summary {
+    font-size: 0.9375rem;
+    color: var(--tblr-body-color);
+    line-height: 1.65;
+}
+
+.wiki-drawer-link {
+    display: inline-block;
+    margin-top: 1rem;
+    font-size: 0.875rem;
+    color: var(--tblr-primary);
+    text-decoration: none;
+}
+
+.wiki-term-card-word {
+    font-family: "Noto Serif", Georgia, serif;
+    font-size: 0.9375rem;
+    font-style: italic;
+    font-weight: 600;
+    color: var(--tblr-primary);
+    padding: 8px;
+    margin-bottom: 8px;
+    border-bottom: 0.5px solid var(--tblr-border-color);
+}
+
+.wiki-term-card-meaning {
+    font-size: 0.875rem;
+    font-weight: 500;
+    color: var(--tblr-body-color);
+    margin-bottom: 5px;
+}
+
+.wiki-term-card-summary {
+    font-size: 0.8125rem;
+    color: var(--tblr-secondary);
+    line-height: 1.55;
+}
+
+/* Skeleton */
+.wiki-term-skeleton-word,
+.wiki-term-skeleton-line {
+    background: linear-gradient(90deg, #e8e8e8 25%, #f5f5f5 50%, #e8e8e8 75%);
+    background-size: 200% 100%;
+    animation: wiki-skeleton-shimmer 1.2s infinite;
+    border-radius: 4px;
+}
+
+.wiki-term-skeleton-word { height: 18px; width: 120px; margin-bottom: 8px; }
+.wiki-term-skeleton-line { height: 13px; width: 100%; margin-bottom: 6px; }
+.wiki-term-skeleton-line.short { width: 60%; }
+
+@keyframes wiki-skeleton-shimmer {
+    0%   { background-position: 200% 0; }
+    100% { background-position: -200% 0; }
+}
+
+/* ══════════════════════════════════════════
+   八、正文排版(.wiki-content-body)
+   ══════════════════════════════════════════ */
+
+.wiki-content-body {
+    font-family: "Noto Serif", Georgia, "Times New Roman", serif;
+    font-size: 1rem;
+    line-height: 1.875;
+    color: var(--tblr-body-color);
+    word-break: break-word;
+    overflow-wrap: break-word;
+}
+
+.wiki-content-body p { margin-top: 0; margin-bottom: 1.125em; }
+
+.wiki-content-body h1,
+.wiki-content-body h2,
+.wiki-content-body h3,
+.wiki-content-body h4,
+.wiki-content-body h5,
+.wiki-content-body h6 {
+    font-family: "Noto Serif", Georgia, serif;
+    font-weight: 600;
+    line-height: 1.3;
+    color: var(--tblr-body-color);
+    margin-top: 2em;
+    margin-bottom: 0.6em;
+    scroll-margin-top: 80px;
+}
+
+.wiki-content-body h1 { font-size: 1.375rem; padding-bottom: .4em; border-bottom: 2px solid var(--tblr-border-color); }
+.wiki-content-body h2 { font-size: 1.25rem;  padding-bottom: .4em; border-bottom: 1px solid var(--tblr-border-color); }
+.wiki-content-body h3 { font-size: 1.0625rem; }
+.wiki-content-body h4 { font-size: 0.9375rem; font-weight: 600; color: var(--tblr-secondary); }
+
+.wiki-content-body strong, .wiki-content-body b { font-weight: 600; }
+.wiki-content-body em,     .wiki-content-body i { font-style: italic; }
+
+.wiki-content-body a {
+    color: var(--tblr-primary);
+    text-decoration: none;
+    border-bottom: 1px solid transparent;
+    transition: border-color 0.12s;
+}
+.wiki-content-body a:hover { border-bottom-color: var(--tblr-primary); }
+
+.wiki-content-body blockquote {
+    margin: 1.5em 0;
+    padding: 0.875rem 1.125rem;
+    border-left: 3px solid var(--tblr-border-color-dark, #adb5bd);
+    background: var(--tblr-bg-surface-secondary);
+    border-radius: 0 var(--tblr-border-radius) var(--tblr-border-radius) 0;
+    font-size: 0.9375rem;
+    line-height: 1.75;
+}
+
+.wiki-content-body blockquote p          { margin-bottom: 0.5em; }
+.wiki-content-body blockquote p:last-child { margin-bottom: 0; }
+.wiki-content-body blockquote cite {
+    display: block;
+    margin-top: .625rem;
+    font-size: .8125rem;
+    font-style: normal;
+    color: var(--tblr-secondary);
+}
+
+.wiki-content-body ul,
+.wiki-content-body ol { padding-left: 1.5em; margin-bottom: 1.125em; }
+.wiki-content-body li { margin-bottom: .375em; line-height: 1.75; }
+
+.wiki-content-body table { width: 100%; border-collapse: collapse; font-size: .9rem; margin-bottom: 1.5em; }
+.wiki-content-body th {
+    font-family: "Noto Serif", Georgia, serif;
+    font-weight: 600; font-size: .8125rem; text-align: left;
+    padding: 8px 12px;
+    background: var(--tblr-bg-surface-secondary);
+    border-bottom: 2px solid var(--tblr-border-color);
+    color: var(--tblr-secondary);
+    text-transform: uppercase; letter-spacing: .04em;
+}
+.wiki-content-body td { padding: 8px 12px; border-bottom: 1px solid var(--tblr-border-color); vertical-align: top; }
+.wiki-content-body tr:last-child td { border-bottom: none; }
+
+.wiki-content-body code {
+    font-family: var(--tblr-font-monospace, "SFMono-Regular", Consolas, monospace);
+    font-size: .875em;
+    background: var(--tblr-bg-surface-secondary);
+    border: 1px solid var(--tblr-border-color);
+    border-radius: 4px;
+    padding: 1px 5px;
+}
+
+.wiki-content-body hr  { border: none; border-top: 1px solid var(--tblr-border-color); margin: 2em 0; }
+.wiki-content-body img { max-width: 100%; height: auto; border-radius: var(--tblr-border-radius); margin: .75em 0; }
+.wiki-content-body figure { margin: 1.5em 0; text-align: center; }
+.wiki-content-body figcaption { font-size: .8125rem; color: var(--tblr-secondary); margin-top: .5em; font-style: italic; }
+
+/* ══════════════════════════════════════════
+   九、Wiki 首页
+   ══════════════════════════════════════════ */
+
+.wiki-home-container {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    min-height: calc(100vh - 300px);
+    padding: 3rem 1.5rem;
+}
+
+.wiki-home-wheel        { margin-bottom: 1.5rem; }
+.wiki-home-wheel-img    { width: 120px; height: auto; opacity: .85; transition: transform .3s ease; }
+.wiki-home-wheel-img:hover { transform: scale(1.05); }
+
+.wiki-home-title        { text-align: center; margin-bottom: 2rem; }
+.wiki-home-title h1 {
+    font-family: "Noto Serif", Georgia, serif;
+    font-size: 2.25rem; font-weight: 600;
+    color: var(--tblr-body-color); margin-bottom: .5rem;
+}
+
+.wiki-home-search       { width: 100%; max-width: 640px; margin: 0 auto 1.5rem; }
+
+.wiki-home-hot-tags     { text-align: center; margin-bottom: 2.5rem; font-size: .9rem; }
+.wiki-hot-tag {
+    display: inline-block;
+    padding: .3rem .75rem;
+    background: var(--tblr-bg-surface-secondary);
+    border: 1px solid var(--tblr-border-color);
+    border-radius: 20px;
+    color: var(--tblr-secondary);
+    font-size: .8125rem;
+    text-decoration: none;
+    margin: 2px;
+    transition: background .12s, color .12s;
+}
+.wiki-hot-tag:hover {
+    background: var(--wp-brand-light);
+    border-color: var(--wp-brand);
+    color: var(--wp-brand-dark);
+}
+
+.wiki-home-languages    { width: 100%; max-width: 800px; margin: 0 auto 2rem; }
+.wiki-home-divider {
+    display: flex; align-items: center; text-align: center;
+    margin-bottom: 1.5rem; gap: 1.5rem;
+    color: var(--tblr-secondary); font-size: .875rem;
+}
+.wiki-home-divider::before,
+.wiki-home-divider::after { content: ''; flex: 1; border-bottom: 1px solid var(--tblr-border-color); }
+
+.wiki-language-tags     { display: flex; flex-wrap: wrap; justify-content: center; gap: .75rem; }
+.wiki-language-tag {
+    display: inline-block;
+    padding: .4rem 1.125rem;
+    background: var(--tblr-bg-surface-secondary);
+    border: 1px solid var(--tblr-border-color);
+    border-radius: 30px;
+    color: var(--tblr-body-color);
+    font-size: .875rem;
+    text-decoration: none;
+    transition: background .12s, border-color .12s;
+}
+.wiki-language-tag:hover {
+    background: var(--wp-brand-light);
+    border-color: var(--wp-brand);
+    color: var(--wp-brand-dark);
+}
+.wiki-language-tag.active {
+    background: var(--wp-brand);
+    border-color: var(--wp-brand);
+    color: #fff;
+}
+.wiki-language-tag.active:hover {
+    background: var(--wp-brand-dark);
+    border-color: var(--wp-brand-dark);
+}
+
+.wiki-home-stats {
+    text-align: center; font-size: .875rem;
+    padding-top: 1.5rem;
+    border-top: 1px solid var(--tblr-border-color);
+    width: 100%; max-width: 640px;
+}
+
+@media (max-width: 768px) {
+    .wiki-home-container    { min-height: calc(100vh - 200px); padding: 2rem 1rem; }
+    .wiki-home-wheel-img    { width: 90px; }
+    .wiki-home-title h1     { font-size: 1.75rem; }
+    .wiki-language-tag      { padding: .35rem .875rem; font-size: .8125rem; }
+}

+ 5 - 237
api-v12/resources/css/reader.css

@@ -1,238 +1,6 @@
-/* resources/css/reader.css */
+/* resources/css/reader.css
+   全站阅读页 CSS 入口。
+   只做 @import,不写任何样式规则。
+*/
 
-/* 基础样式 */
-body {
-    font-family: "Inter", sans-serif;
-    transition: background-color 0.3s, color 0.3s;
-}
-
-.main-container {
-    display: flex;
-    gap: 20px;
-    padding: 20px;
-    max-width: 1400px;
-    margin: 0 auto;
-}
-
-.toc-sidebar {
-    width: 250px;
-    flex-shrink: 0;
-    display: none;
-    position: sticky;
-    top: 0;
-    height: 100vh;
-    overflow-y: auto;
-    overflow-x: hidden;
-    scrollbar-width: thin;
-}
-
-.toc-sidebar .card-body {
-    height: 100%;
-    overflow-y: auto;
-    overflow-x: hidden;
-}
-
-.content-area {
-    flex-grow: 1;
-    max-width: 100%;
-}
-
-.right-sidebar {
-    width: 300px;
-    flex-shrink: 0;
-    display: none;
-}
-
-.related-books {
-    margin-top: 30px;
-}
-
-.card-img-container {
-    height: 150px;
-    overflow: hidden;
-}
-
-.card-img-container img {
-    width: 100%;
-    height: 100%;
-    object-fit: cover;
-}
-
-/* 响应式布局 */
-@media (max-width: 767px) {
-    .content-area {
-        width: 100%;
-    }
-
-    .main-container {
-        padding: 0;
-    }
-
-    .card {
-        border: none;
-    }
-}
-
-@media (min-width: 768px) {
-    .toc-sidebar {
-        display: block;
-    }
-
-    .content-area {
-        max-width: calc(100% - 270px);
-    }
-}
-
-@media (min-width: 992px) {
-    .right-sidebar {
-        display: block;
-    }
-
-    .content-area {
-        max-width: calc(100% - 570px);
-    }
-}
-
-/* 暗色模式 */
-.dark-mode {
-    background-color: #1a1a1a;
-    color: #ffffff;
-}
-
-.dark-mode .card {
-    background-color: #2a2a2a;
-    border-color: #3a3a3a;
-    color: #ffffff;
-}
-
-.dark-mode .navbar {
-    background-color: #2a2a2a;
-}
-
-.dark-mode .offcanvas {
-    background-color: #2a2a2a;
-    color: #ffffff;
-}
-
-.dark-mode .offcanvas .nav-link {
-    color: #ffffff;
-}
-
-.dark-mode .toc-sidebar,
-.dark-mode .right-sidebar {
-    background-color: #2a2a2a;
-}
-
-/* 目录样式 */
-.toc-sidebar ul,
-.offcanvas-body ul {
-    list-style: none;
-    padding: 0;
-}
-
-.toc-sidebar ul li,
-.offcanvas-body ul li {
-    padding: 5px 0;
-}
-
-.toc-sidebar ul li a,
-.offcanvas-body ul li a {
-    color: #206bc4;
-    text-decoration: none;
-}
-
-.toc-sidebar ul li a:hover {
-    text-decoration: none;
-}
-
-.offcanvas-body ul li a:hover {
-    text-decoration: underline;
-}
-
-.dark-mode .toc-sidebar ul li a,
-.dark-mode .offcanvas-body ul li a {
-    color: #4dabf7;
-}
-
-.toc-level-1 {
-    padding-left: 0 !important;
-}
-
-.toc-level-2 {
-    padding-left: 16px !important;
-}
-
-.toc-level-3 {
-    padding-left: 24px !important;
-}
-
-.toc-level-4 {
-    padding-left: 36px !important;
-}
-
-.toc-disabled {
-    color: #6c757d;
-    cursor: not-allowed;
-    pointer-events: none;
-}
-
-.dark-mode .toc-disabled {
-    color: #adb5bd;
-}
-
-.toc_item {
-    white-space: nowrap;
-    overflow: hidden;
-    text-overflow: ellipsis;
-}
-
-.toc_item:hover {
-    color: #206bc4 !important;
-    background: rgba(32, 107, 196, 0.15);
-    border-radius: 2px;
-    cursor: pointer;
-}
-
-.toc_item a,
-.toc_item span {
-    display: block;
-    width: 100%;
-    white-space: nowrap;
-    overflow: hidden;
-    text-overflow: ellipsis;
-}
-
-.toc-active {
-    color: #206bc4 !important;
-    font-weight: 600;
-    background: rgba(32, 107, 196, 0.08);
-    border-left: 3px solid #206bc4;
-    border-radius: 2px;
-    cursor: default;
-    pointer-events: none;
-}
-
-.dark-mode .toc-active {
-    color: #4dabf7 !important;
-    background: rgba(77, 171, 247, 0.1);
-    border-left-color: #4dabf7;
-}
-
-.origin {
-    color: darkred;
-}
-
-/* 术语引用样式 */
-.term-ref {
-    cursor: pointer;
-    text-decoration: underline dotted;
-    text-underline-offset: 4px;
-}
-
-.term_invalid {
-    color: red;
-}
-
-.term-ref:hover {
-    color: var(--tblr-primary);
-}
+@import "./modules/_reader.css";

+ 360 - 0
api-v12/resources/css/tufte.css

@@ -0,0 +1,360 @@
+@charset "UTF-8";
+
+/* Import ET Book styles
+   adapted from https://github.com/edwardtufte/et-book/blob/gh-pages/et-book.css */
+
+/* Tufte CSS styles */
+
+hr {
+    display: block;
+    height: 1px;
+    width: 55%;
+    border: 0;
+    border-top: 1px solid #ccc;
+    margin: 1em 0;
+    padding: 0;
+}
+
+p {
+    line-height: 1.5;
+}
+p.subtitle {
+    font-style: italic;
+    margin-top: 1rem;
+    margin-bottom: 1rem;
+    display: block;
+}
+
+.numeral {
+    font-family: et-book-roman-old-style;
+}
+
+.danger {
+    color: red;
+}
+
+article {
+    padding: 5rem 0rem;
+}
+
+section {
+    padding-top: 1rem;
+    padding-bottom: 1rem;
+}
+
+p,
+dl,
+ol,
+ul {
+    line-height: 1.5rem;
+}
+
+p {
+    margin-bottom: 1.4rem;
+    padding-right: 0;
+    vertical-align: baseline;
+}
+
+/* Chapter Epigraphs */
+div.epigraph {
+    margin: 5em 0;
+}
+
+div.epigraph > blockquote {
+    margin-top: 3em;
+    margin-bottom: 3em;
+}
+
+div.epigraph > blockquote,
+div.epigraph > blockquote > p {
+    font-style: italic;
+}
+
+div.epigraph > blockquote > footer {
+    font-style: normal;
+}
+
+div.epigraph > blockquote > footer > cite {
+    font-style: italic;
+}
+/* end chapter epigraphs styles */
+
+blockquote {
+    font-size: 1.4rem;
+}
+
+blockquote p {
+    width: 55%;
+    margin-right: 40px;
+}
+
+blockquote footer {
+    width: 55%;
+    font-size: 1.1rem;
+    text-align: right;
+}
+
+section > p,
+section > footer,
+section > table {
+    width: 55%;
+}
+
+/* 50 + 5 == 55, to be the same width as paragraph */
+section > dl,
+section > ol,
+section > ul {
+    width: 50%;
+    -webkit-padding-start: 5%;
+}
+
+dt:not(:first-child),
+li:not(:first-child) {
+    margin-top: 0.25rem;
+}
+
+figure {
+    padding: 0;
+    border: 0;
+    font-size: 100%;
+    font: inherit;
+    vertical-align: baseline;
+    max-width: 55%;
+    -webkit-margin-start: 0;
+    -webkit-margin-end: 0;
+    margin: 0 0 3em 0;
+}
+
+figcaption {
+    float: right;
+    clear: right;
+    margin-top: 0;
+    margin-bottom: 0;
+    font-size: 1.1rem;
+    line-height: 1.6;
+    vertical-align: baseline;
+    position: relative;
+    max-width: 40%;
+}
+
+figure.fullwidth figcaption {
+    margin-right: 24%;
+}
+
+a:link,
+a:visited {
+    color: inherit;
+    text-underline-offset: 0.1em;
+    text-decoration-thickness: 0.05em;
+}
+
+/* Sidenotes, margin notes, figures, captions */
+img {
+    max-width: 100%;
+}
+
+.sidenote,
+.marginnote {
+    float: right;
+    clear: right;
+    margin-right: -60%;
+    width: 50%;
+    margin-top: 0.3rem;
+    margin-bottom: 0;
+    font-size: 100%;
+    line-height: 1.3;
+    vertical-align: baseline;
+    position: relative;
+}
+
+.sidenote-number {
+    counter-increment: sidenote-counter;
+}
+
+.sidenote-number:after,
+.sidenote:before {
+    font-family: et-book-roman-old-style;
+    position: relative;
+    vertical-align: baseline;
+}
+
+.sidenote-number:after {
+    content: counter(sidenote-counter);
+    font-size: 100%;
+    top: -0.5rem;
+    left: 0.1rem;
+}
+
+.sidenote:before {
+    content: counter(sidenote-counter) " ";
+    font-size: 100%;
+    top: -0.5rem;
+}
+
+blockquote .sidenote,
+blockquote .marginnote {
+    margin-right: -82%;
+    min-width: 59%;
+    text-align: left;
+}
+
+div.fullwidth,
+table.fullwidth {
+    width: 100%;
+}
+
+div.table-wrapper {
+    overflow-x: auto;
+    font-family: "Trebuchet MS", "Gill Sans", "Gill Sans MT", sans-serif;
+}
+
+.sans {
+    font-family: "Gill Sans", "Gill Sans MT", Calibri, sans-serif;
+    letter-spacing: 0.03em;
+}
+
+code,
+pre > code {
+    font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
+    font-size: 1rem;
+    line-height: 1.42;
+    -webkit-text-size-adjust: 100%; /* Prevent adjustments of font size after orientation changes in iOS. See https://github.com/edwardtufte/tufte-css/issues/81#issuecomment-261953409 */
+}
+
+.sans > code {
+    font-size: 1.2rem;
+}
+
+h1 > code,
+h2 > code,
+h3 > code {
+    font-size: 0.8em;
+}
+
+.marginnote > code,
+.sidenote > code {
+    font-size: 100%;
+}
+
+pre > code {
+    font-size: 0.9rem;
+    width: 52.5%;
+    margin-left: 2.5%;
+    overflow-x: auto;
+    display: block;
+}
+
+pre.fullwidth > code {
+    width: 90%;
+}
+
+.fullwidth {
+    max-width: 90%;
+    clear: both;
+}
+
+span.newthought {
+    font-variant: small-caps;
+    font-size: 1.2em;
+}
+
+input.margin-toggle {
+    display: none;
+}
+
+label.sidenote-number {
+    display: inline-block;
+    max-height: 1.5rem; /* should be less than or equal to paragraph line-height */
+}
+
+label.margin-toggle:not(.sidenote-number) {
+    display: none;
+}
+
+.iframe-wrapper {
+    position: relative;
+    padding-bottom: 56.25%; /* 16:9 */
+    padding-top: 25px;
+    height: 0;
+}
+
+.iframe-wrapper iframe {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+}
+
+@media (max-width: 760px) {
+    hr,
+    section > p,
+    section > footer,
+    section > table {
+        width: 100%;
+    }
+
+    pre > code {
+        width: 97%;
+    }
+
+    section > dl,
+    section > ol,
+    section > ul {
+        width: 90%;
+    }
+
+    figure {
+        max-width: 90%;
+    }
+
+    figcaption,
+    figure.fullwidth figcaption {
+        margin-right: 0%;
+        max-width: none;
+    }
+
+    blockquote {
+        margin-left: 1.5em;
+        margin-right: 0em;
+    }
+
+    blockquote p,
+    blockquote footer {
+        width: 100%;
+    }
+
+    label.margin-toggle:not(.sidenote-number) {
+        display: inline;
+    }
+
+    .sidenote,
+    .marginnote {
+        display: none;
+    }
+
+    .margin-toggle:checked + .sidenote,
+    .margin-toggle:checked + .marginnote {
+        display: block;
+        float: left;
+        left: 1rem;
+        clear: both;
+        width: 95%;
+        margin: 1rem 2.5%;
+        vertical-align: baseline;
+        position: relative;
+    }
+
+    label {
+        cursor: pointer;
+    }
+
+    div.table-wrapper,
+    table {
+        width: 85%;
+    }
+
+    img {
+        width: 100%;
+    }
+}

+ 0 - 208
api-v12/resources/css/wiki-content.css

@@ -1,208 +0,0 @@
-/* resources/css/wiki-content.css */
-/* WikiPāli — 百科正文排版
-   作用域:.wiki-content-body
-   设计原则:
-   - 衬线字体为主,保持阅读节奏
-   - 标题层级明确,不与 Tabler 全局样式冲突
-   - blockquote / 引用块 突出原典风格
-*/
-
-@import url('https://fonts.googleapis.com/css2?family=Noto+Serif:ital,wght@0,400;0,600;1,400&display=swap');
-
-/* ── 容器基础 ── */
-.wiki-content-body {
-    font-family: 'Noto Serif', Georgia, 'Times New Roman', serif;
-    font-size: 1rem;
-    line-height: 1.875;
-    color: var(--tblr-body-color);
-    word-break: break-word;
-    overflow-wrap: break-word;
-}
-
-/* ── 段落 ── */
-.wiki-content-body p {
-    margin-top: 0;
-    margin-bottom: 1.125em;
-}
-
-/* ── 标题 ── */
-.wiki-content-body h1,
-.wiki-content-body h2,
-.wiki-content-body h3,
-.wiki-content-body h4,
-.wiki-content-body h5,
-.wiki-content-body h6 {
-    font-family: 'Noto Serif', Georgia, serif;
-    font-weight: 600;
-    line-height: 1.3;
-    color: var(--tblr-body-color);
-    margin-top: 2em;
-    margin-bottom: 0.6em;
-    scroll-margin-top: 80px; /* 锚点跳转留出导航高度 */
-}
-
-/* h2 是正文主要分节标题 */
-.wiki-content-body h2 {
-    font-size: 1.25rem;
-    padding-bottom: 0.4em;
-    border-bottom: 1px solid var(--tblr-border-color);
-}
-
-.wiki-content-body h3 {
-    font-size: 1.0625rem;
-}
-
-.wiki-content-body h4 {
-    font-size: 0.9375rem;
-    font-weight: 600;
-    color: var(--tblr-secondary);
-}
-
-/* h1 在正文中通常不出现(条目标题由 entry-header 组件渲染)
-   若 controller 传入的 HTML 含有 h1,降级处理 */
-.wiki-content-body h1 {
-    font-size: 1.375rem;
-    padding-bottom: 0.4em;
-    border-bottom: 2px solid var(--tblr-border-color);
-}
-
-/* ── 强调 ── */
-.wiki-content-body strong,
-.wiki-content-body b {
-    font-weight: 600;
-    color: var(--tblr-body-color);
-}
-
-.wiki-content-body em,
-.wiki-content-body i {
-    font-style: italic;
-}
-
-/* ── 链接 ── */
-.wiki-content-body a {
-    color: var(--tblr-primary);
-    text-decoration: none;
-    border-bottom: 1px solid transparent;
-    transition: border-color 0.12s;
-}
-
-.wiki-content-body a:hover {
-    border-bottom-color: var(--tblr-primary);
-}
-
-/* ── 引用块(原典引用) ── */
-.wiki-content-body blockquote {
-    margin: 1.5em 0;
-    padding: 0.875rem 1.125rem;
-    border-left: 3px solid var(--tblr-border-color-dark, #adb5bd);
-    background: var(--tblr-bg-surface-secondary);
-    border-radius: 0 var(--tblr-border-radius) var(--tblr-border-radius) 0;
-    color: var(--tblr-body-color);
-    font-size: 0.9375rem;
-    line-height: 1.75;
-}
-
-.wiki-content-body blockquote p {
-    margin-bottom: 0.5em;
-}
-
-.wiki-content-body blockquote p:last-child {
-    margin-bottom: 0;
-}
-
-.wiki-content-body blockquote cite {
-    display: block;
-    margin-top: 0.625rem;
-    font-size: 0.8125rem;
-    font-style: normal;
-    color: var(--tblr-secondary);
-}
-
-/* ── 列表 ── */
-.wiki-content-body ul,
-.wiki-content-body ol {
-    padding-left: 1.5em;
-    margin-bottom: 1.125em;
-}
-
-.wiki-content-body li {
-    margin-bottom: 0.375em;
-    line-height: 1.75;
-}
-
-.wiki-content-body ul ul,
-.wiki-content-body ol ol,
-.wiki-content-body ul ol,
-.wiki-content-body ol ul {
-    margin-top: 0.375em;
-    margin-bottom: 0;
-}
-
-/* ── 表格 ── */
-.wiki-content-body table {
-    width: 100%;
-    border-collapse: collapse;
-    font-size: 0.9rem;
-    margin-bottom: 1.5em;
-}
-
-.wiki-content-body th {
-    font-family: 'Noto Serif', Georgia, serif;
-    font-weight: 600;
-    font-size: 0.8125rem;
-    text-align: left;
-    padding: 8px 12px;
-    background: var(--tblr-bg-surface-secondary);
-    border-bottom: 2px solid var(--tblr-border-color);
-    color: var(--tblr-secondary);
-    text-transform: uppercase;
-    letter-spacing: 0.04em;
-}
-
-.wiki-content-body td {
-    padding: 8px 12px;
-    border-bottom: 1px solid var(--tblr-border-color);
-    vertical-align: top;
-}
-
-.wiki-content-body tr:last-child td {
-    border-bottom: none;
-}
-
-/* ── 行内代码 ── */
-.wiki-content-body code {
-    font-family: var(--tblr-font-monospace, 'SFMono-Regular', Consolas, monospace);
-    font-size: 0.875em;
-    background: var(--tblr-bg-surface-secondary);
-    border: 1px solid var(--tblr-border-color);
-    border-radius: 4px;
-    padding: 1px 5px;
-    color: var(--tblr-body-color);
-}
-
-/* ── 分隔线 ── */
-.wiki-content-body hr {
-    border: none;
-    border-top: 1px solid var(--tblr-border-color);
-    margin: 2em 0;
-}
-
-/* ── 图片 ── */
-.wiki-content-body img {
-    max-width: 100%;
-    height: auto;
-    border-radius: var(--tblr-border-radius);
-    margin: 0.75em 0;
-}
-
-.wiki-content-body figure {
-    margin: 1.5em 0;
-    text-align: center;
-}
-
-.wiki-content-body figcaption {
-    font-size: 0.8125rem;
-    color: var(--tblr-secondary);
-    margin-top: 0.5em;
-    font-style: italic;
-}

+ 0 - 574
api-v12/resources/css/wiki.css

@@ -1,574 +0,0 @@
-/* resources/css/wiki.css */
-/* WikiPāli — 布局、导航、组件样式 */
-
-/* ── 字体 ── */
-@import url("https://fonts.googleapis.com/css2?family=Noto+Serif:ital,wght@0,400;0,600;1,400&display=swap");
-
-/* ── 三栏布局 ── */
-.wiki-layout {
-    display: grid;
-    grid-template-columns: 200px 1fr 200px;
-    grid-template-areas: "left main right";
-    gap: 1.5rem;
-    align-items: start;
-    padding-top: 1.5rem;
-    padding-bottom: 3rem;
-}
-
-.wiki-sidebar-left {
-    grid-area: left;
-}
-.wiki-main {
-    grid-area: main;
-    min-width: 0;
-    display: flex;
-    flex-direction: column;
-    gap: 1rem;
-}
-.wiki-sidebar-right {
-    grid-area: right;
-}
-
-/* ── 通用卡片 ── */
-.wiki-card {
-    background: var(--tblr-bg-surface);
-    border: 1px solid var(--tblr-border-color);
-    border-radius: var(--tblr-border-radius-lg);
-    padding: 1.5rem;
-}
-
-/* ── 侧边栏 ── */
-.wiki-sidebar-section {
-    background: var(--tblr-bg-surface);
-    border: 1px solid var(--tblr-border-color);
-    border-radius: var(--tblr-border-radius-lg);
-    padding: 1rem 1.125rem;
-    margin-bottom: 1rem;
-}
-
-.wiki-sidebar-title {
-    font-size: 0.6875rem;
-    font-weight: 500;
-    letter-spacing: 0.05em;
-    text-transform: uppercase;
-    color: var(--tblr-secondary);
-    margin-bottom: 0.75rem;
-}
-
-.wiki-cat-list {
-    list-style: none;
-    padding: 0;
-    margin: 0;
-}
-
-.wiki-cat-list li {
-    margin-bottom: 2px;
-}
-
-.wiki-cat-list a {
-    display: block;
-    font-size: 0.8125rem;
-    color: var(--tblr-body-color);
-    text-decoration: none;
-    padding: 5px 8px;
-    border-radius: var(--tblr-border-radius);
-    transition: background 0.12s;
-}
-
-.wiki-cat-list a:hover {
-    background: var(--tblr-bg-surface-secondary);
-}
-.wiki-cat-list a.active {
-    background: var(--tblr-bg-surface-secondary);
-    font-weight: 500;
-    color: var(--tblr-primary);
-}
-
-/* ── 目录 ── */
-.wiki-toc-list {
-    list-style: none;
-    padding: 0;
-    margin: 0;
-}
-
-.wiki-toc-list li {
-    border-bottom: 1px solid var(--tblr-border-color);
-}
-
-.wiki-toc-list li:last-child {
-    border-bottom: none;
-}
-
-.wiki-toc-list a {
-    display: block;
-    font-size: 0.8125rem;
-    color: var(--tblr-secondary);
-    text-decoration: none;
-    padding: 5px 0;
-    transition: color 0.12s;
-}
-
-.wiki-toc-list a:hover {
-    color: var(--tblr-body-color);
-}
-.wiki-toc-list a.active {
-    color: var(--tblr-body-color);
-    font-weight: 500;
-}
-
-.wiki-toc-list .toc-level-2 a {
-    padding-left: 0.5rem;
-}
-
-.wiki-toc-list .toc-level-3 a {
-    padding-left: 1rem;
-}
-
-.wiki-toc-num {
-    color: var(--tblr-secondary);
-    margin-right: 5px;
-    font-size: 0.75rem;
-}
-
-/* ── 相关条目 ── */
-.wiki-related-list {
-    list-style: none;
-    padding: 0;
-    margin: 0;
-}
-
-.wiki-related-list li {
-    border-bottom: 1px solid var(--tblr-border-color);
-}
-
-.wiki-related-list li:last-child {
-    border-bottom: none;
-}
-
-.wiki-related-list a {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    font-size: 0.8125rem;
-    color: var(--tblr-primary);
-    text-decoration: none;
-    padding: 6px 0;
-}
-
-.wiki-related-zh {
-    font-size: 0.75rem;
-    color: var(--tblr-secondary);
-}
-
-/* ── 元信息表格 ── */
-.wiki-meta-table {
-    width: 100%;
-    font-size: 0.8125rem;
-    border-collapse: collapse;
-}
-
-.wiki-meta-table td {
-    padding: 3px 0;
-}
-.wiki-meta-table td:last-child {
-    text-align: right;
-    color: var(--tblr-secondary);
-}
-
-/* ── 条目头部 ── */
-.wiki-entry-header {
-    margin-bottom: 1.25rem;
-}
-
-.wiki-entry-title {
-    font-family: "Noto Serif", Georgia, serif;
-    font-size: 1.75rem;
-    font-weight: 600;
-    line-height: 1.25;
-    margin: 0.375rem 0 0.75rem;
-    color: var(--tblr-body-color);
-}
-
-.wiki-entry-langs-inline {
-    display: flex;
-    flex-wrap: wrap;
-    gap: 6px;
-    margin-bottom: 0.5rem;
-}
-
-.wiki-lang-pill {
-    font-size: 0.75rem;
-    padding: 3px 10px;
-    border-radius: 20px;
-    border: 1px solid var(--tblr-border-color);
-    color: var(--tblr-secondary);
-}
-
-.wiki-lang-pill strong {
-    font-weight: 500;
-    color: var(--tblr-body-color);
-    margin-right: 3px;
-}
-
-/* ── 语言版本切换 ── */
-.wiki-entry-lang-switcher {
-    display: flex;
-    flex-wrap: wrap;
-    align-items: center;
-    gap: 6px;
-    padding: 0.625rem 0.875rem;
-    background: var(--tblr-bg-surface-secondary);
-    border-radius: var(--tblr-border-radius);
-    margin-bottom: 1.25rem;
-    font-size: 0.8125rem;
-}
-
-.wiki-entry-lang-label {
-    color: var(--tblr-secondary);
-}
-
-.wiki-entry-lang-btn {
-    font-size: 0.75rem;
-    padding: 3px 10px;
-    border-radius: 20px;
-    border: 1px solid var(--tblr-border-color);
-    color: var(--tblr-primary);
-    text-decoration: none;
-    transition: background 0.12s;
-}
-
-.wiki-entry-lang-btn:hover {
-    background: var(--tblr-bg-surface);
-}
-
-/* ── 质量标签 ── */
-.wiki-quality-badge {
-    display: inline-flex;
-    align-items: center;
-    gap: 5px;
-    font-size: 0.6875rem;
-    padding: 3px 10px;
-    border-radius: 20px;
-    font-weight: 500;
-    margin-bottom: 4px;
-}
-
-.wiki-quality-dot {
-    width: 6px;
-    height: 6px;
-    border-radius: 50%;
-    flex-shrink: 0;
-}
-
-.wiki-badge--featured {
-    background: #eaf3de;
-    color: #3b6d11;
-    border: 1px solid #c0dd97;
-}
-.wiki-badge--featured .wiki-quality-dot {
-    background: #639922;
-}
-
-.wiki-badge--review {
-    background: #faeeda;
-    color: #854f0b;
-    border: 1px solid #fac775;
-}
-.wiki-badge--review .wiki-quality-dot {
-    background: #ba7517;
-}
-
-.wiki-badge--stub {
-    background: #f1efe8;
-    color: #5f5e5a;
-    border: 1px solid #d3d1c7;
-}
-.wiki-badge--stub .wiki-quality-dot {
-    background: #888780;
-}
-
-/* ── 标签 ── */
-.wiki-tags {
-    display: flex;
-    flex-wrap: wrap;
-    gap: 6px;
-    padding-top: 1rem;
-    border-top: 1px solid var(--tblr-border-color);
-    margin-top: 1.5rem;
-}
-
-.wiki-tag {
-    font-size: 0.75rem;
-    padding: 3px 10px;
-    border-radius: 20px;
-    border: 1px solid var(--tblr-border-color);
-    color: var(--tblr-secondary);
-    text-decoration: none;
-    transition: background 0.12s;
-}
-
-.wiki-tag:hover {
-    background: var(--tblr-bg-surface-secondary);
-    color: var(--tblr-body-color);
-}
-
-/* ── 首页今日条目 ── */
-.wiki-today-banner {
-    background: var(--tblr-bg-surface-secondary);
-    border: 1px solid var(--tblr-border-color);
-    border-radius: var(--tblr-border-radius-lg);
-    padding: 1.25rem 1.5rem;
-    display: flex;
-    gap: 1.25rem;
-    align-items: flex-start;
-}
-
-.wiki-today-icon {
-    width: 48px;
-    height: 48px;
-    border-radius: var(--tblr-border-radius);
-    background: #e1f5ee;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    font-size: 1.375rem;
-    flex-shrink: 0;
-}
-
-.wiki-today-label {
-    font-size: 0.6875rem;
-    text-transform: uppercase;
-    letter-spacing: 0.05em;
-    color: var(--tblr-secondary);
-    margin-bottom: 4px;
-}
-
-.wiki-today-title {
-    font-family: "Noto Serif", Georgia, serif;
-    font-size: 1.125rem;
-    font-weight: 600;
-    margin-bottom: 6px;
-    color: var(--tblr-body-color);
-}
-
-.wiki-today-snippet {
-    font-size: 0.875rem;
-    color: var(--tblr-secondary);
-    line-height: 1.6;
-}
-
-.wiki-today-link {
-    font-size: 0.8125rem;
-    color: var(--tblr-primary);
-    text-decoration: none;
-    margin-top: 10px;
-    display: inline-block;
-}
-
-/* ── 首页精选网格 ── */
-.wiki-featured-grid {
-    display: grid;
-    grid-template-columns: repeat(3, minmax(0, 1fr));
-    gap: 8px;
-}
-
-.wiki-featured-card {
-    border: 1px solid var(--tblr-border-color);
-    border-radius: var(--tblr-border-radius);
-    padding: 10px 12px;
-    cursor: pointer;
-    text-decoration: none;
-    display: block;
-    transition: background 0.12s;
-    color: var(--tblr-body-color);
-}
-
-.wiki-featured-card:hover {
-    background: var(--tblr-bg-surface-secondary);
-}
-
-.wiki-featured-label {
-    font-size: 0.6875rem;
-    font-weight: 500;
-    text-transform: uppercase;
-    letter-spacing: 0.05em;
-    color: var(--tblr-secondary);
-    margin-bottom: 5px;
-}
-
-.wiki-featured-title {
-    font-size: 0.875rem;
-    font-weight: 500;
-    margin-bottom: 2px;
-}
-
-.wiki-featured-pali {
-    font-size: 0.75rem;
-    font-style: italic;
-    color: var(--tblr-secondary);
-}
-
-/* ── term-ref 行内术语标记 ── */
-.term-ref {
-    font-style: italic;
-    color: var(--tblr-primary);
-    border-bottom: 1px dotted var(--tblr-primary);
-    cursor: pointer;
-}
-
-/* ── Popover 覆盖样式(桌面 tooltip) ── */
-.wiki-term-popover.popover {
-    max-width: 280px;
-    font-size: 0.8125rem;
-    border: 0.5px solid var(--tblr-border-color);
-    border-radius: var(--tblr-border-radius-lg);
-    box-shadow: none;
-}
-
-.wiki-term-popover .popover-arrow {
-    display: none;
-}
-.wiki-term-popover .popover-body {
-    padding: 0;
-}
-
-/* 巴利原文标题行 */
-.wiki-popover-word {
-    font-family: "Noto Serif", Georgia, serif;
-    font-size: 0.9375rem;
-    font-style: italic;
-    font-weight: 600;
-    color: var(--tblr-primary);
-    padding: 10px 14px 10px;
-    border-bottom: 0.5px solid var(--tblr-border-color);
-}
-
-/* 释义区 */
-.wiki-popover-body {
-    padding: 10px 14px 12px;
-}
-
-.wiki-popover-meaning {
-    font-size: 0.875rem;
-    font-weight: 500;
-    color: var(--tblr-body-color);
-    margin-bottom: 5px;
-}
-
-.wiki-popover-summary {
-    font-size: 0.8125rem;
-    color: var(--tblr-secondary);
-    line-height: 1.55;
-}
-
-.wiki-popover-meaning {
-    font-size: 0.9rem;
-    font-weight: 500;
-    color: var(--tblr-body-color);
-    padding: 8px 14px 4px;
-}
-
-.wiki-popover-summary {
-    font-size: 0.8125rem;
-    color: var(--tblr-secondary);
-    line-height: 1.55;
-    padding: 0 14px 12px;
-}
-
-/* ── 移动端 Offcanvas 抽屉 ── */
-.wiki-term-drawer {
-    border-radius: 1rem 1rem 0 0;
-    max-height: 55vh;
-}
-
-.wiki-drawer-word {
-    font-family: "Noto Serif", Georgia, serif;
-    font-size: 1.125rem;
-    font-style: italic;
-    font-weight: 600;
-    color: var(--tblr-primary);
-}
-
-.wiki-drawer-meaning {
-    font-size: 0.9375rem;
-    font-weight: 500;
-    color: var(--tblr-body-color);
-    margin-top: 2px;
-}
-
-.wiki-drawer-summary {
-    font-size: 0.9375rem;
-    color: var(--tblr-secondary);
-    line-height: 1.65;
-}
-
-.wiki-drawer-link {
-    display: inline-block;
-    margin-top: 1rem;
-    font-size: 0.875rem;
-    color: var(--tblr-primary);
-    text-decoration: none;
-}
-
-/* ── 响应式 ── */
-@media (max-width: 992px) {
-    .wiki-layout {
-        grid-template-columns: 180px 1fr;
-        grid-template-areas:
-            "left main"
-            "left right";
-    }
-
-    .wiki-sidebar-right {
-        grid-area: right;
-        display: flex;
-        flex-wrap: wrap;
-        gap: 1rem;
-    }
-
-    .wiki-sidebar-right .wiki-sidebar-section {
-        flex: 1 1 180px;
-        margin-bottom: 0;
-    }
-}
-
-@media (max-width: 768px) {
-    .wiki-layout {
-        grid-template-columns: 1fr;
-        grid-template-areas: "main" "right" "left";
-    }
-
-    .wiki-featured-grid {
-        grid-template-columns: repeat(2, minmax(0, 1fr));
-    }
-
-    .wiki-sidebar-left,
-    .wiki-sidebar-right {
-        display: none;
-    }
-}
-
-/* ── term-card 共享(Popover + 抽屉复用) ── */
-.wiki-term-card-word {
-    font-family: "Noto Serif", Georgia, serif;
-    font-size: 0.9375rem;
-    font-style: italic;
-    font-weight: 600;
-    color: var(--tblr-primary);
-    padding: 8px;
-    margin-bottom: 8px;
-    border-bottom: 0.5px solid var(--tblr-border-color);
-}
-
-.wiki-term-card-meaning {
-    font-size: 0.875rem;
-    font-weight: 500;
-    color: var(--tblr-body-color);
-    margin-bottom: 5px;
-}
-
-.wiki-term-card-summary {
-    font-size: 0.8125rem;
-    color: var(--tblr-secondary);
-    line-height: 1.55;
-}

+ 7 - 2
api-v12/resources/js/app.js

@@ -1,2 +1,7 @@
-import "./bootstrap";
-import "./term-tooltip";
+// resources/js/app.js
+import './bootstrap';
+import { initNavbar } from './modules/navbar';
+
+document.addEventListener('DOMContentLoaded', () => {
+    initNavbar();
+});

+ 30 - 0
api-v12/resources/js/modules/navbar.js

@@ -0,0 +1,30 @@
+// resources/js/modules/navbar.js
+// 顶部导航 mobile 抽屉开关逻辑。
+// 来源:library/layouts/app.blade.php 内联 script(去除了无效的 mobileMenu 引用)。
+
+export function initNavbar() {
+    const btn     = document.getElementById('bcHamburger');
+    const overlay = document.getElementById('bcOverlay');
+    const drawer  = document.getElementById('bcDrawer');
+    const close   = document.getElementById('bcDrawerClose');
+
+    if (!btn) return;
+
+    function open() {
+        drawer.classList.add('open');
+        overlay.classList.add('open');
+    }
+
+    function shut() {
+        drawer.classList.remove('open');
+        overlay.classList.remove('open');
+    }
+
+    btn.addEventListener('click', open);
+    overlay.addEventListener('click', shut);
+    close.addEventListener('click', shut);
+
+    drawer.querySelectorAll('a').forEach(a => {
+        a.addEventListener('click', shut);
+    });
+}

+ 233 - 0
api-v12/resources/js/modules/term-tooltip.js

@@ -0,0 +1,233 @@
+// resources/js/modules/term-tooltip.js
+// 原 term-tooltip.js 移至此处(从 js/ 根目录移入 modules/)。
+// 所有阅读页公用(tipitaka read、anthology read、wiki show、blog show)。
+// 在 reader.js 中 import,不在 app.js 全局加载(仅阅读页需要)。
+//
+// 文件内容:将现有 term-tooltip.js 内容直接复制至此,不做修改。
+// 待 reader.js 建立后在此 import:
+//   import './modules/term-tooltip';
+
+import * as bootstrap from "bootstrap";
+
+(function () {
+    "use strict";
+
+    // ── 缓存层 ────────────────────────────────────────────────────────
+    const cache = {};
+
+    async function fetchTerm(id) {
+        if (cache[id]) return cache[id];
+        const res = await fetch(`/api/v2/terms/${id}`);
+        if (!res.ok) throw new Error(`fetchTerm ${id} failed: ${res.status}`);
+        const json = await res.json();
+        cache[id] = json.data;
+        return json.data;
+    }
+
+    // ── 设备判断 ──────────────────────────────────────────────────────
+    const isMobile = () => window.innerWidth < 768;
+
+    // ── Skeleton 模板 ─────────────────────────────────────────────────
+    function buildSkeletonContent() {
+        return `
+            <div class="wiki-term-skeleton">
+                <div class="wiki-term-skeleton-word"></div>
+                <div class="wiki-term-skeleton-line"></div>
+                <div class="wiki-term-skeleton-line short"></div>
+            </div>
+        `;
+    }
+
+    // ── Popover 内容模板 ──────────────────────────────────────────────
+    function buildPopoverContent(data) {
+        const meaning = (data.meaning || "").trim();
+        const summary = (data.summary || "").trim();
+        const showSummary = summary && summary !== meaning;
+
+        return `
+            <div class="wiki-term-card-word">${data.word || ""}</div>
+            <div class="wiki-term-card-body" style="padding: 10px 14px 12px;">
+                ${
+                    meaning
+                        ? `<div class="wiki-term-card-meaning">${meaning}</div>`
+                        : ""
+                }
+                ${
+                    showSummary
+                        ? `<div class="wiki-term-card-summary">${summary}</div>`
+                        : ""
+                }
+            </div>
+            <div><a>查看完整条目</a></div>
+        `;
+    }
+
+    // ── 桌面端:Bootstrap Popover ─────────────────────────────────────
+    function initDesktopPopover(el, content) {
+        if (el._wikiPopover) return el._wikiPopover;
+
+        const popover = new bootstrap.Popover(el, {
+            trigger: "manual",
+            html: true,
+            placement: "bottom",
+            fallbackPlacements: ["top", "bottom"],
+            customClass: "wiki-term-popover",
+            content: content,
+            sanitize: false,
+        });
+
+        el._wikiPopover = popover;
+
+        let hideTimer = null;
+
+        function scheduleHide() {
+            hideTimer = setTimeout(() => {
+                const tipEl = document.querySelector(".wiki-term-popover.show");
+                if (tipEl && tipEl.matches(":hover")) return;
+                popover.hide();
+            }, 120);
+        }
+
+        function cancelHide() {
+            clearTimeout(hideTimer);
+        }
+
+        el.addEventListener("mouseenter", () => {
+            cancelHide();
+            popover.show();
+        });
+
+        el.addEventListener("mouseleave", scheduleHide);
+
+        el.addEventListener("shown.bs.popover", () => {
+            const tipEl = document.querySelector(".wiki-term-popover.show");
+            if (!tipEl) return;
+            tipEl.addEventListener("mouseenter", cancelHide);
+            tipEl.addEventListener("mouseleave", scheduleHide);
+        });
+
+        return popover;
+    }
+
+    function updateDesktopPopover(el, data) {
+        const popover = el._wikiPopover;
+        if (!popover) return;
+        // 更新内容
+        const tip = document.querySelector(".wiki-term-popover.show");
+        if (tip) {
+            tip.querySelector(".popover-body").innerHTML =
+                buildPopoverContent(data);
+        }
+        // 同步 popover 内部 config,供下次 show 使用
+        popover._config.content = buildPopoverContent(data);
+    }
+
+    // ── 移动端:Bootstrap Offcanvas ───────────────────────────────────
+    let offcanvasInstance = null;
+
+    function getOffcanvas() {
+        if (offcanvasInstance) return offcanvasInstance;
+        const el = document.getElementById("wikiTermDrawer");
+        if (!el) return null;
+        offcanvasInstance = new bootstrap.Offcanvas(el, { scroll: false });
+
+        el.addEventListener("hidden.bs.offcanvas", () => {
+            document
+                .querySelectorAll(".offcanvas-backdrop")
+                .forEach((b) => b.remove());
+            document.body.classList.remove("modal-open");
+            document.body.style.removeProperty("overflow");
+            document.body.style.removeProperty("padding-right");
+        });
+
+        return offcanvasInstance;
+    }
+
+    function showMobileDrawerSkeleton() {
+        const oc = getOffcanvas();
+        if (!oc) return;
+
+        document.getElementById("wikiTermDrawerWord").innerHTML =
+            '<div class="wiki-term-skeleton-word"></div>';
+        document.getElementById("wikiTermDrawerMeaning").innerHTML =
+            '<div class="wiki-term-skeleton-line short"></div>';
+        document.getElementById(
+            "wikiTermCardSlot"
+        ).innerHTML = `<div class="wiki-term-skeleton">
+                <div class="wiki-term-skeleton-line"></div>
+                <div class="wiki-term-skeleton-line short"></div>
+            </div>`;
+        document.getElementById("wikiTermDrawerLink").style.display = "none";
+
+        oc.show();
+    }
+
+    function fillMobileDrawer(data) {
+        const meaning = (data.meaning || "").trim();
+        const summary = (data.summary || "").trim();
+        const showSummary = summary && summary !== meaning;
+
+        document.getElementById("wikiTermDrawerWord").textContent =
+            data.word || "";
+        document.getElementById("wikiTermDrawerMeaning").textContent = meaning;
+        document.getElementById("wikiTermCardSlot").innerHTML = showSummary
+            ? `<div class="wiki-term-card-summary">${summary}</div>`
+            : "";
+    }
+
+    // ── 入口:扫描所有 .term-ref ──────────────────────────────────────
+    function init() {
+        const refs = document.querySelectorAll(".term-ref[data-id]");
+        if (!refs.length) return;
+
+        refs.forEach((el) => {
+            // 桌面:mouseenter 时先显示 skeleton,数据回来后更新
+            el.addEventListener("mouseenter", async function onFirstEnter() {
+                if (isMobile()) return;
+
+                // 立即显示 skeleton popover
+                const popover = initDesktopPopover(el, buildSkeletonContent());
+                popover.show();
+
+                try {
+                    const data = await fetchTerm(el.dataset.id);
+                    updateDesktopPopover(el, data);
+                } catch (e) {
+                    console.warn(
+                        "[WikiPāli] term fetch failed",
+                        el.dataset.id,
+                        e
+                    );
+                    popover.hide();
+                }
+
+                el.removeEventListener("mouseenter", onFirstEnter);
+            });
+
+            // 移动端:点击立即弹出 skeleton,数据回来后填充
+            el.addEventListener("click", async (e) => {
+                if (!isMobile()) return;
+                e.preventDefault();
+
+                showMobileDrawerSkeleton();
+
+                try {
+                    const data = await fetchTerm(el.dataset.id);
+                    fillMobileDrawer(data);
+                } catch (err) {
+                    console.warn(
+                        "[WikiPāli] term fetch failed",
+                        el.dataset.id,
+                        err
+                    );
+                }
+            });
+        });
+    }
+
+    if (document.readyState === "loading") {
+        document.addEventListener("DOMContentLoaded", init);
+    } else {
+        init();
+    }
+})();

+ 73 - 43
api-v12/resources/js/term-tooltip.js

@@ -1,10 +1,7 @@
 /**
  * resources/js/term-tooltip.js
- * WikiPāli 术语 tooltip(桌面 Popover)+ 抽屉(移动端 Offcanvas)
- *
- * 依赖:Bootstrap 5(bootstrap.Popover / bootstrap.Offcanvas)
- *       已由 Tabler 全局引入,无需重复 import
  */
+import * as bootstrap from "bootstrap";
 
 (function () {
     "use strict";
@@ -24,6 +21,17 @@
     // ── 设备判断 ──────────────────────────────────────────────────────
     const isMobile = () => window.innerWidth < 768;
 
+    // ── Skeleton 模板 ─────────────────────────────────────────────────
+    function buildSkeletonContent() {
+        return `
+            <div class="wiki-term-skeleton">
+                <div class="wiki-term-skeleton-word"></div>
+                <div class="wiki-term-skeleton-line"></div>
+                <div class="wiki-term-skeleton-line short"></div>
+            </div>
+        `;
+    }
+
     // ── Popover 内容模板 ──────────────────────────────────────────────
     function buildPopoverContent(data) {
         const meaning = (data.meaning || "").trim();
@@ -49,19 +57,16 @@
     }
 
     // ── 桌面端:Bootstrap Popover ─────────────────────────────────────
-    function initDesktopPopover(el, data) {
-        // 防止重复初始化
+    function initDesktopPopover(el, content) {
         if (el._wikiPopover) return el._wikiPopover;
 
         const popover = new bootstrap.Popover(el, {
             trigger: "manual",
             html: true,
-            placement: "bottom", // flip 会自动在空间不足时翻转为 top
+            placement: "bottom",
             fallbackPlacements: ["top", "bottom"],
             customClass: "wiki-term-popover",
-            content: buildPopoverContent(data),
-            // 让 popover 自身也可以接收鼠标事件(解决移入气泡消失问题)
-            // Bootstrap 5.2+ 支持 sanitize: false 保留自定义 html
+            content: content,
             sanitize: false,
         });
 
@@ -71,7 +76,6 @@
 
         function scheduleHide() {
             hideTimer = setTimeout(() => {
-                // 如果鼠标此刻在 popover 内,不关闭
                 const tipEl = document.querySelector(".wiki-term-popover.show");
                 if (tipEl && tipEl.matches(":hover")) return;
                 popover.hide();
@@ -82,7 +86,6 @@
             clearTimeout(hideTimer);
         }
 
-        // 触发元素
         el.addEventListener("mouseenter", () => {
             cancelHide();
             popover.show();
@@ -90,7 +93,6 @@
 
         el.addEventListener("mouseleave", scheduleHide);
 
-        // Popover 显示后,给气泡本身绑定 mouseenter / mouseleave
         el.addEventListener("shown.bs.popover", () => {
             const tipEl = document.querySelector(".wiki-term-popover.show");
             if (!tipEl) return;
@@ -101,7 +103,20 @@
         return popover;
     }
 
-    // ── 移动端:Bootstrap Offcanvas(全局共享一个) ────────────────────
+    function updateDesktopPopover(el, data) {
+        const popover = el._wikiPopover;
+        if (!popover) return;
+        // 更新内容
+        const tip = document.querySelector(".wiki-term-popover.show");
+        if (tip) {
+            tip.querySelector(".popover-body").innerHTML =
+                buildPopoverContent(data);
+        }
+        // 同步 popover 内部 config,供下次 show 使用
+        popover._config.content = buildPopoverContent(data);
+    }
+
+    // ── 移动端:Bootstrap Offcanvas ───────────────────────────────────
     let offcanvasInstance = null;
 
     function getOffcanvas() {
@@ -109,37 +124,49 @@
         const el = document.getElementById("wikiTermDrawer");
         if (!el) return null;
         offcanvasInstance = new bootstrap.Offcanvas(el, { scroll: false });
+
+        el.addEventListener("hidden.bs.offcanvas", () => {
+            document
+                .querySelectorAll(".offcanvas-backdrop")
+                .forEach((b) => b.remove());
+            document.body.classList.remove("modal-open");
+            document.body.style.removeProperty("overflow");
+            document.body.style.removeProperty("padding-right");
+        });
+
         return offcanvasInstance;
     }
 
-    function showMobileDrawer(data) {
+    function showMobileDrawerSkeleton() {
         const oc = getOffcanvas();
         if (!oc) return;
 
+        document.getElementById("wikiTermDrawerWord").innerHTML =
+            '<div class="wiki-term-skeleton-word"></div>';
+        document.getElementById("wikiTermDrawerMeaning").innerHTML =
+            '<div class="wiki-term-skeleton-line short"></div>';
+        document.getElementById(
+            "wikiTermCardSlot"
+        ).innerHTML = `<div class="wiki-term-skeleton">
+                <div class="wiki-term-skeleton-line"></div>
+                <div class="wiki-term-skeleton-line short"></div>
+            </div>`;
+        document.getElementById("wikiTermDrawerLink").style.display = "none";
+
+        oc.show();
+    }
+
+    function fillMobileDrawer(data) {
         const meaning = (data.meaning || "").trim();
         const summary = (data.summary || "").trim();
         const showSummary = summary && summary !== meaning;
 
-        document.getElementById("wikiTermCardSlot").innerHTML = `
-            <div class="wiki-term-card">
-                <div class="wiki-term-card-word">${data.word || ""}</div>
-                <div class="wiki-term-card-body">
-                    ${
-                        meaning
-                            ? `<div class="wiki-term-card-meaning">${meaning}</div>`
-                            : ""
-                    }
-                    ${
-                        showSummary
-                            ? `<div class="wiki-term-card-summary">${summary}</div>`
-                            : ""
-                    }
-                </div>
-            </div>
-        `;
-
-        document.getElementById("wikiTermDrawerLink").style.display = "none";
-        oc.show();
+        document.getElementById("wikiTermDrawerWord").textContent =
+            data.word || "";
+        document.getElementById("wikiTermDrawerMeaning").textContent = meaning;
+        document.getElementById("wikiTermCardSlot").innerHTML = showSummary
+            ? `<div class="wiki-term-card-summary">${summary}</div>`
+            : "";
     }
 
     // ── 入口:扫描所有 .term-ref ──────────────────────────────────────
@@ -148,35 +175,39 @@
         if (!refs.length) return;
 
         refs.forEach((el) => {
-            // 桌面:mouseenter 时懒加载并初始化 Popover
+            // 桌面:mouseenter 时先显示 skeleton,数据回来后更新
             el.addEventListener("mouseenter", async function onFirstEnter() {
                 if (isMobile()) return;
 
+                // 立即显示 skeleton popover
+                const popover = initDesktopPopover(el, buildSkeletonContent());
+                popover.show();
+
                 try {
                     const data = await fetchTerm(el.dataset.id);
-                    const popover = initDesktopPopover(el, data);
-                    // 首次加载后直接 show(mouseenter 已触发)
-                    popover.show();
+                    updateDesktopPopover(el, data);
                 } catch (e) {
                     console.warn(
                         "[WikiPāli] term fetch failed",
                         el.dataset.id,
                         e
                     );
+                    popover.hide();
                 }
 
-                // 移除首次监听,后续由 Popover 自身管理
                 el.removeEventListener("mouseenter", onFirstEnter);
             });
 
-            // 移动端:点击触发抽屉
+            // 移动端:点击立即弹出 skeleton,数据回来后填充
             el.addEventListener("click", async (e) => {
                 if (!isMobile()) return;
                 e.preventDefault();
 
+                showMobileDrawerSkeleton();
+
                 try {
                     const data = await fetchTerm(el.dataset.id);
-                    showMobileDrawer(data);
+                    fillMobileDrawer(data);
                 } catch (err) {
                     console.warn(
                         "[WikiPāli] term fetch failed",
@@ -188,7 +219,6 @@
         });
     }
 
-    // DOM 就绪后执行
     if (document.readyState === "loading") {
         document.addEventListener("DOMContentLoaded", init);
     } else {

+ 29 - 18
api-v12/resources/views/blog/category.blade.php

@@ -1,56 +1,67 @@
+{{-- resources/views/blog/category.blade.php --}}
 @extends('blog.layouts.app')
 
-@section('title', $user["nickName"])
+@section('title', $user['nickName'] . ' · ' . collect($current)->pluck('label')->implode(' / '))
 
 @section('content')
 
 <header>
     <h3 class="section-title">Categories</h3>
-
     <div class="section-card">
         <div class="section-details">
-            <h3 class="section-count">{{ $count }} page</h3>
+            <h3 class="section-count">{{ $count }} </h3>
             <h1 class="section-term">
-                @foreach ($current as $category)
-                /<a href="{{ route('blog.category', ['user' => $user['userName'],'category1' => $category['id'],]) }}">
+                @foreach($current as $category)
+                / <a href="{{ route('blog.category', ['user' => $user['userName'], 'category1' => $category['id']]) }}">
                     {{ $category['label'] }}
                 </a>
                 @endforeach
             </h1>
+
+            {{-- 子分类标签 --}}
+            @if(!empty($tagOptions))
             <section class="widget tagCloud">
                 <div class="tagCloud-tags">
                     @foreach($tagOptions as $id => $tag)
-                    @if($tag['count'] < $count)
-                        <a href="{{ rtrim(url()->current(), '/') . '/' . $tag['tag']->name }}">{{ $tag['tag']->name }}({{ $tag['count'] }})</a>
+                        @if($tag['count'] < $count)
+                        <a href="{{ rtrim(url()->current(), '/') . '/' . $tag['tag']->name }}">
+                            {{ $tag['tag']->name }} ({{ $tag['count'] }})
+                        </a>
                         @endif
-                        @endforeach
+                    @endforeach
                 </div>
             </section>
+            @endif
         </div>
     </div>
 </header>
 
 <section class="article-list--compact">
-    @foreach ($posts as $post)
+    @forelse($posts as $post)
     <article>
-        <a href="{{ route('library.tipitaka.read', $post['uid']) }}">
+        <a href="{{ route('library.tipitaka.read', ['id' => $post['uid']]) }}">
             <div class="article-details">
                 <h2 class="article-title">{{ $post->title }}</h2>
                 <footer class="article-time">
-                    <time datetime="2022-03-06T00:00:00Z">{{ $post->formatted_updated_at }}</time>
+                    <time>{{ $post->formatted_updated_at }}</time>
                 </footer>
             </div>
+
+            @if(!empty($post->cover))
             <div class="article-image">
-                <img
-                    src="./Category_ Example Category - Hugo Theme Stack Starter_files/cover_hu6307248181568134095.jpg"
-                    width="120"
-                    height="120"
-                    alt="Hello World"
-                    loading="lazy" />
+                <img src="{{ $post->cover }}"
+                     width="120" height="120"
+                     alt="{{ $post->title }}"
+                     loading="lazy" />
             </div>
+            @endif
         </a>
     </article>
-    @endforeach
+    @empty
+    <div class="not-found-card" style="padding: 20px;">
+        <p>此分类下暂无文章</p>
+    </div>
+    @endforelse
 </section>
 
 @endsection

+ 53 - 413
api-v12/resources/views/blog/index.blade.php

@@ -1,453 +1,93 @@
+{{-- resources/views/blog/index.blade.php --}}
 @extends('blog.layouts.app')
 
-@section('title', $user["nickName"])
+@section('title', $user['nickName'])
 
 @section('content')
+<h2 class="section-title">推荐阅读</h2>
+<div class="subsection-list">
+    <div class="article-list--tile">
+        <article class="has-image"><a href="/categories/documentation/">
+                <div class="article-image"><img alt="Featured image of post Documentation" height="150" loading="lazy" src="/categories/documentation/hutomo-abrianto-l2jk-uxb1BY-unsplash_hu_543ba73fab19d0c8.jpg" srcset="/categories/documentation/hutomo-abrianto-l2jk-uxb1BY-unsplash_hu_543ba73fab19d0c8.jpg 1x, /categories/documentation/hutomo-abrianto-l2jk-uxb1BY-unsplash_hu_2b27d90ffb967881.jpg 2x" width="250"></div>
+                <div class="article-details" style="background:linear-gradient(0deg,#bc8d7280 0%,#d0cfcdC0 100%)">
+                    <h2 class="article-title">Documentation</h2>
+                </div>
+            </a></article>
+        <article><a href="/categories/syntax/">
+                <div class="article-details">
+                    <h2 class="article-title">Syntax</h2>
+                </div>
+            </a></article>
+        <article><a href="/categories/themes/">
+                <div class="article-details">
+                    <h2 class="article-title">Themes</h2>
+                </div>
+            </a></article>
+    </div>
+</div>
 <section class="article-list">
-    @foreach ($posts as $post)
-    <article class="">
+    @forelse($posts as $post)
+    <article class="{{ !empty($post->cover) ? 'has-image' : '' }}">
         <header class="article-header">
-            <div class="article-details">
-                <header class="article-category">
-                    <a href="https://demo.stack.jimmycai.com/categories/themes/">
-                        Themes
-                    </a>
-
-                    <a href="https://demo.stack.jimmycai.com/categories/syntax/">
-                        Syntax
-                    </a>
-                </header>
-
-                <div class="article-title-wrapper">
-                    <h2 class="article-title">
-                        <a
-                            href="{{ route('library.tipitaka.read', $post['uid']) }}">{{ $post->title }}</a>
-                    </h2>
-
-                    <h3 class="article-subtitle">
-                        {{ $post->summary }}
-                    </h3>
-                </div>
-
-                <footer class="article-time">
-                    <div>
-                        <svg
-                            xmlns="http://www.w3.org/2000/svg"
-                            class="icon icon-tabler icon-tabler-calendar-time"
-                            width="56"
-                            height="56"
-                            viewBox="0 0 24 24"
-                            stroke-width="2"
-                            stroke="currentColor"
-                            fill="none"
-                            stroke-linecap="round"
-                            stroke-linejoin="round">
-                            <path stroke="none" d="M0 0h24v24H0z"></path>
-                            <path
-                                d="M11.795 21h-6.795a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v4"></path>
-                            <circle cx="18" cy="18" r="4"></circle>
-                            <path d="M15 3v4"></path>
-                            <path d="M7 3v4"></path>
-                            <path d="M3 11h16"></path>
-                            <path d="M18 16.496v1.504l1 1"></path>
-                        </svg>
-                        <time class="article-time--published">{{ $post->formatted_updated_at }}</time>
-                    </div>
 
-                    <div>
-                        <svg
-                            xmlns="http://www.w3.org/2000/svg"
-                            class="icon icon-tabler icon-tabler-clock"
-                            width="24"
-                            height="24"
-                            viewBox="0 0 24 24"
-                            stroke-width="2"
-                            stroke="currentColor"
-                            fill="none"
-                            stroke-linecap="round"
-                            stroke-linejoin="round">
-                            <path stroke="none" d="M0 0h24v24H0z"></path>
-                            <circle cx="12" cy="12" r="9"></circle>
-                            <polyline points="12 7 12 12 15 15"></polyline>
-                        </svg>
-
-                        <time class="article-time--reading"> 3 minute read </time>
-                    </div>
-                </footer>
-            </div>
-        </header>
-    </article>
-    @endforeach
-    <article class="has-image">
-        <header class="article-header">
+            @if(!empty($post->cover))
             <div class="article-image">
-                <a href="https://demo.stack.jimmycai.com/p/hello-world/">
-                    <img
-                        src="./Hugo Theme Stack Starter_files/cover_hu13459586684579990428.jpg"
-                        srcset="
-                      /p/hello-world/cover_hu13459586684579990428.jpg  800w,
-                      /p/hello-world/cover_hu3425483315149503896.jpg  1600w
-                    "
-                        width="800"
-                        height="534"
+                <a href="{{ route('library.tipitaka.read', ['id' => $post['uid']]) }}">
+                    <img src="{{ $post->cover }}"
+                        width="800" height="450"
                         loading="lazy"
-                        alt="Featured image of post Hello World" />
+                        alt="{{ $post->title }}" />
                 </a>
             </div>
+            @endif
 
             <div class="article-details">
-                <header class="article-category">
-                    <a
-                        href="https://demo.stack.jimmycai.com/categories/example-category/"
-                        style="background-color: #2a9d8f; color: #fff">
-                        Example Category
-                    </a>
-                </header>
 
-                <div class="article-title-wrapper">
-                    <h2 class="article-title">
-                        <a href="https://demo.stack.jimmycai.com/p/hello-world/">Hello World</a>
-                    </h2>
-
-                    <h3 class="article-subtitle">Welcome to Hugo Theme Stack</h3>
-                </div>
-
-                <footer class="article-time">
-                    <div>
-                        <svg
-                            xmlns="http://www.w3.org/2000/svg"
-                            class="icon icon-tabler icon-tabler-calendar-time"
-                            width="56"
-                            height="56"
-                            viewBox="0 0 24 24"
-                            stroke-width="2"
-                            stroke="currentColor"
-                            fill="none"
-                            stroke-linecap="round"
-                            stroke-linejoin="round">
-                            <path stroke="none" d="M0 0h24v24H0z"></path>
-                            <path
-                                d="M11.795 21h-6.795a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v4"></path>
-                            <circle cx="18" cy="18" r="4"></circle>
-                            <path d="M15 3v4"></path>
-                            <path d="M7 3v4"></path>
-                            <path d="M3 11h16"></path>
-                            <path d="M18 16.496v1.504l1 1"></path>
-                        </svg>
-                        <time class="article-time--published">Mar 06, 2022</time>
-                    </div>
-
-                    <div>
-                        <svg
-                            xmlns="http://www.w3.org/2000/svg"
-                            class="icon icon-tabler icon-tabler-clock"
-                            width="24"
-                            height="24"
-                            viewBox="0 0 24 24"
-                            stroke-width="2"
-                            stroke="currentColor"
-                            fill="none"
-                            stroke-linecap="round"
-                            stroke-linejoin="round">
-                            <path stroke="none" d="M0 0h24v24H0z"></path>
-                            <circle cx="12" cy="12" r="9"></circle>
-                            <polyline points="12 7 12 12 15 15"></polyline>
-                        </svg>
-
-                        <time class="article-time--reading"> 1 minute read </time>
-                    </div>
-                </footer>
-            </div>
-        </header>
-    </article>
-
-    <article class="">
-        <header class="article-header">
-            <div class="article-details">
+                @if(!empty($post->categories))
                 <header class="article-category">
-                    <a href="https://demo.stack.jimmycai.com/categories/themes/">
-                        Themes
-                    </a>
-
-                    <a href="https://demo.stack.jimmycai.com/categories/syntax/">
-                        Syntax
+                    @foreach($post->categories as $category)
+                    <a href="{{ route('blog.category', ['user' => $user['userName'], 'category1' => $category['id']]) }}">
+                        {{ $category['label'] }}
                     </a>
+                    @endforeach
                 </header>
+                @endif
 
                 <div class="article-title-wrapper">
                     <h2 class="article-title">
-                        <a
-                            href="https://demo.stack.jimmycai.com/p/markdown-syntax-guide/">Markdown Syntax Guide</a>
+                        <a href="{{ route('library.tipitaka.read', ['id' => $post['uid']]) }}">
+                            {{ $post->title }}
+                        </a>
                     </h2>
-
-                    <h3 class="article-subtitle">
-                        Sample article showcasing basic Markdown syntax and
-                        formatting for HTML elements.
-                    </h3>
+                    @if(!empty($post->summary))
+                    <h3 class="article-subtitle">{{ $post->summary }}</h3>
+                    @endif
                 </div>
 
                 <footer class="article-time">
                     <div>
-                        <svg
-                            xmlns="http://www.w3.org/2000/svg"
-                            class="icon icon-tabler icon-tabler-calendar-time"
-                            width="56"
-                            height="56"
-                            viewBox="0 0 24 24"
-                            stroke-width="2"
-                            stroke="currentColor"
-                            fill="none"
-                            stroke-linecap="round"
-                            stroke-linejoin="round">
+                        <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-calendar-time"
+                            width="24" height="24" viewBox="0 0 24 24" stroke-width="2"
+                            stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
                             <path stroke="none" d="M0 0h24v24H0z"></path>
-                            <path
-                                d="M11.795 21h-6.795a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v4"></path>
+                            <path d="M11.795 21h-6.795a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v4"></path>
                             <circle cx="18" cy="18" r="4"></circle>
                             <path d="M15 3v4"></path>
                             <path d="M7 3v4"></path>
                             <path d="M3 11h16"></path>
                             <path d="M18 16.496v1.504l1 1"></path>
                         </svg>
-                        <time class="article-time--published">Sep 07, 2023</time>
-                    </div>
-
-                    <div>
-                        <svg
-                            xmlns="http://www.w3.org/2000/svg"
-                            class="icon icon-tabler icon-tabler-clock"
-                            width="24"
-                            height="24"
-                            viewBox="0 0 24 24"
-                            stroke-width="2"
-                            stroke="currentColor"
-                            fill="none"
-                            stroke-linecap="round"
-                            stroke-linejoin="round">
-                            <path stroke="none" d="M0 0h24v24H0z"></path>
-                            <circle cx="12" cy="12" r="9"></circle>
-                            <polyline points="12 7 12 12 15 15"></polyline>
-                        </svg>
-
-                        <time class="article-time--reading"> 3 minute read </time>
+                        <time>{{ $post->formatted_updated_at }}</time>
                     </div>
                 </footer>
-            </div>
-        </header>
-    </article>
-
-    <article class="has-image">
-        <header class="article-header">
-            <div class="article-image">
-                <a href="https://demo.stack.jimmycai.com/p/image-gallery/">
-                    <img
-                        src="./Hugo Theme Stack Starter_files/2_hu3578945376017100738.jpg"
-                        srcset="
-                      /p/image-gallery/2_hu3578945376017100738.jpg  800w,
-                      /p/image-gallery/2_hu15750790370579438.jpg   1600w
-                    "
-                        width="800"
-                        height="1200"
-                        loading="lazy"
-                        alt="Featured image of post Image gallery" />
-                </a>
-            </div>
 
-            <div class="article-details">
-                <div class="article-title-wrapper">
-                    <h2 class="article-title">
-                        <a href="https://demo.stack.jimmycai.com/p/image-gallery/">Image gallery</a>
-                    </h2>
-
-                    <h3 class="article-subtitle">
-                        Create beautiful interactive image gallery using Markdown
-                    </h3>
-                </div>
-
-                <footer class="article-time">
-                    <div>
-                        <svg
-                            xmlns="http://www.w3.org/2000/svg"
-                            class="icon icon-tabler icon-tabler-calendar-time"
-                            width="56"
-                            height="56"
-                            viewBox="0 0 24 24"
-                            stroke-width="2"
-                            stroke="currentColor"
-                            fill="none"
-                            stroke-linecap="round"
-                            stroke-linejoin="round">
-                            <path stroke="none" d="M0 0h24v24H0z"></path>
-                            <path
-                                d="M11.795 21h-6.795a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v4"></path>
-                            <circle cx="18" cy="18" r="4"></circle>
-                            <path d="M15 3v4"></path>
-                            <path d="M7 3v4"></path>
-                            <path d="M3 11h16"></path>
-                            <path d="M18 16.496v1.504l1 1"></path>
-                        </svg>
-                        <time class="article-time--published">Aug 26, 2023</time>
-                    </div>
-
-                    <div>
-                        <svg
-                            xmlns="http://www.w3.org/2000/svg"
-                            class="icon icon-tabler icon-tabler-clock"
-                            width="24"
-                            height="24"
-                            viewBox="0 0 24 24"
-                            stroke-width="2"
-                            stroke="currentColor"
-                            fill="none"
-                            stroke-linecap="round"
-                            stroke-linejoin="round">
-                            <path stroke="none" d="M0 0h24v24H0z"></path>
-                            <circle cx="12" cy="12" r="9"></circle>
-                            <polyline points="12 7 12 12 15 15"></polyline>
-                        </svg>
-
-                        <time class="article-time--reading"> 1 minute read </time>
-                    </div>
-                </footer>
-            </div>
-        </header>
-    </article>
-
-    <article class="has-image">
-        <header class="article-header">
-            <div class="article-image">
-                <a href="https://demo.stack.jimmycai.com/p/shortcodes/">
-                    <img
-                        src="./Hugo Theme Stack Starter_files/cover_hu5876910065799140332.jpg"
-                        srcset="
-                      /p/shortcodes/cover_hu5876910065799140332.jpg   800w,
-                      /p/shortcodes/cover_hu14584859319700861491.jpg 1600w
-                    "
-                        width="800"
-                        height="533"
-                        loading="lazy"
-                        alt="Featured image of post Shortcodes" />
-                </a>
-            </div>
-
-            <div class="article-details">
-                <div class="article-title-wrapper">
-                    <h2 class="article-title">
-                        <a href="https://demo.stack.jimmycai.com/p/shortcodes/">Shortcodes</a>
-                    </h2>
-
-                    <h3 class="article-subtitle">
-                        Useful shortcodes that can be used in Markdown
-                    </h3>
-                </div>
-
-                <footer class="article-time">
-                    <div>
-                        <svg
-                            xmlns="http://www.w3.org/2000/svg"
-                            class="icon icon-tabler icon-tabler-calendar-time"
-                            width="56"
-                            height="56"
-                            viewBox="0 0 24 24"
-                            stroke-width="2"
-                            stroke="currentColor"
-                            fill="none"
-                            stroke-linecap="round"
-                            stroke-linejoin="round">
-                            <path stroke="none" d="M0 0h24v24H0z"></path>
-                            <path
-                                d="M11.795 21h-6.795a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v4"></path>
-                            <circle cx="18" cy="18" r="4"></circle>
-                            <path d="M15 3v4"></path>
-                            <path d="M7 3v4"></path>
-                            <path d="M3 11h16"></path>
-                            <path d="M18 16.496v1.504l1 1"></path>
-                        </svg>
-                        <time class="article-time--published">Aug 25, 2023</time>
-                    </div>
-
-                    <div>
-                        <svg
-                            xmlns="http://www.w3.org/2000/svg"
-                            class="icon icon-tabler icon-tabler-clock"
-                            width="24"
-                            height="24"
-                            viewBox="0 0 24 24"
-                            stroke-width="2"
-                            stroke="currentColor"
-                            fill="none"
-                            stroke-linecap="round"
-                            stroke-linejoin="round">
-                            <path stroke="none" d="M0 0h24v24H0z"></path>
-                            <circle cx="12" cy="12" r="9"></circle>
-                            <polyline points="12 7 12 12 15 15"></polyline>
-                        </svg>
-
-                        <time class="article-time--reading"> 1 minute read </time>
-                    </div>
-                </footer>
-            </div>
-        </header>
-    </article>
-
-    <article class="">
-        <header class="article-header">
-            <div class="article-details">
-                <div class="article-title-wrapper">
-                    <h2 class="article-title">
-                        <a
-                            href="https://demo.stack.jimmycai.com/p/math-typesetting/">Math Typesetting</a>
-                    </h2>
-
-                    <h3 class="article-subtitle">Math typesetting using KaTeX</h3>
-                </div>
-
-                <footer class="article-time">
-                    <div>
-                        <svg
-                            xmlns="http://www.w3.org/2000/svg"
-                            class="icon icon-tabler icon-tabler-calendar-time"
-                            width="56"
-                            height="56"
-                            viewBox="0 0 24 24"
-                            stroke-width="2"
-                            stroke="currentColor"
-                            fill="none"
-                            stroke-linecap="round"
-                            stroke-linejoin="round">
-                            <path stroke="none" d="M0 0h24v24H0z"></path>
-                            <path
-                                d="M11.795 21h-6.795a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v4"></path>
-                            <circle cx="18" cy="18" r="4"></circle>
-                            <path d="M15 3v4"></path>
-                            <path d="M7 3v4"></path>
-                            <path d="M3 11h16"></path>
-                            <path d="M18 16.496v1.504l1 1"></path>
-                        </svg>
-                        <time class="article-time--published">Aug 24, 2023</time>
-                    </div>
-
-                    <div>
-                        <svg
-                            xmlns="http://www.w3.org/2000/svg"
-                            class="icon icon-tabler icon-tabler-clock"
-                            width="24"
-                            height="24"
-                            viewBox="0 0 24 24"
-                            stroke-width="2"
-                            stroke="currentColor"
-                            fill="none"
-                            stroke-linecap="round"
-                            stroke-linejoin="round">
-                            <path stroke="none" d="M0 0h24v24H0z"></path>
-                            <circle cx="12" cy="12" r="9"></circle>
-                            <polyline points="12 7 12 12 15 15"></polyline>
-                        </svg>
-
-                        <time class="article-time--reading"> 1 minute read </time>
-                    </div>
-                </footer>
             </div>
         </header>
     </article>
+    @empty
+    <div class="not-found-card">
+        <p>暂无文章</p>
+    </div>
+    @endforelse
 </section>
 @endsection

+ 296 - 468
api-v12/resources/views/blog/layouts/app.blade.php

@@ -1,490 +1,318 @@
-<!DOCTYPE html>
-<!-- saved from url=(0032)https://demo.stack.jimmycai.com/ -->
-<html lang="en-us" dir="ltr" data-scheme="light">
+{{-- resources/views/layouts/blog.blade.php
+     Blog 栏目布局。继承 layouts/base。
+     使用 Stack 主题 CSS(静态文件,不走 Vite)。
+     三栏结构:左边栏(博主信息+导航)/ 正文 / 右边栏(搜索+分类+标签)。
+     右边栏数据($categories, $tags, $archives)由 View Composer 自动注入。
+--}}
+@extends('layouts.base')
 
-<head>
-    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
-    <meta name="generator" content="Hugo 0.134.1" />
-    <meta name="viewport" content="width=device-width, initial-scale=1" />
-    <meta
-        name="description"
-        content="Lorem ipsum dolor sit amet, consectetur adipiscing elit." />
-    <title></title>
-    <title>@yield('title', 'home')</title>
-    <link rel="canonical" href="https://demo.stack.jimmycai.com/" />
+@push('styles')
+    <link rel="stylesheet"
+          href="{{ URL::asset('assets/css/blog/style.min.css') }}">
+    <link href="{{ URL::asset('assets/css/blog/css2') }}"
+          type="text/css" rel="stylesheet">
+@endpush
 
-    <link
-        rel="stylesheet"
-        href="{{ URL::asset('assets/css/blog/style.min.663803bebe609202d5b39d848f2d7c2dc8b598a2d879efa079fa88893d29c49c.css') }}" />
-    <meta property=" og:title" content="{{ $user["nickName"] }}" />
-    <meta
-        property="og:description"
-        content="Lorem ipsum dolor sit amet, consectetur adipiscing elit." />
-    <meta property="og:url" content="https://demo.stack.jimmycai.com/" />
-    <meta property="og:site_name" content="{{ $user["nickName"] }}" />
-    <meta property="og:type" content="website" />
-    <meta property="og:updated_time" content=" 2023-09-07T00:00:00+00:00 " />
-    <meta name="twitter:title" content="{{ $user["nickName"] }}" />
-    <meta
-        name="twitter:description"
-        content="Lorem ipsum dolor sit amet, consectetur adipiscing elit." />
-    <link
-        rel="alternate"
-        type="application/rss+xml"
-        href="https://demo.stack.jimmycai.com/index.xml" />
-    <link
-        rel="shortcut icon"
-        href="https://demo.stack.jimmycai.com/favicon.png" />
-    <link
-        href="{{ URL::asset('assets/css/blog/css2') }}"
-        type="text/css"
-        rel="stylesheet" />
-</head>
+@section('body-class', '')
 
-<body
-    class=""
-    style="transition: background-color 0.3s">
-    <script>
-        (function() {
-            const colorSchemeKey = "StackColorScheme";
-            if (!localStorage.getItem(colorSchemeKey)) {
-                localStorage.setItem(colorSchemeKey, "auto");
-            }
-        })();
-    </script>
-    <script>
-        (function() {
-            const colorSchemeKey = "StackColorScheme";
-            const colorSchemeItem = localStorage.getItem(colorSchemeKey);
-            const supportDarkMode =
-                window.matchMedia("(prefers-color-scheme: dark)").matches === true;
+@section('page')
 
-            if (
-                colorSchemeItem == "dark" ||
-                (colorSchemeItem === "auto" && supportDarkMode)
-            ) {
-                document.documentElement.dataset.scheme = "dark";
-            } else {
-                document.documentElement.dataset.scheme = "light";
-            }
-        })();
-    </script>
-    <div class="container main-container flex on-phone--column extended">
-        <aside class="sidebar left-sidebar sticky">
-            <button
-                class="hamburger hamburger--spin"
+<div class="container main-container flex on-phone--column extended">
+
+    {{-- ── 左边栏 ── --}}
+    <aside class="sidebar left-sidebar sticky">
+
+        <button class="hamburger hamburger--spin"
                 type="button"
                 id="toggle-menu"
                 aria-label="Toggle Menu">
-                <span class="hamburger-box">
-                    <span class="hamburger-inner"></span>
-                </span>
-            </button>
-
-            <header>
-                <figure class="site-avatar">
-                    <a href="https://demo.stack.jimmycai.com/">
-                        <img
-                            src="{{ $user['avatar'] ?? '' }}"
-                            width="300"
-                            height="300"
-                            class="site-logo"
-                            loading="lazy"
-                            alt="Avatar" />
-                    </a>
-
-                    <span class="emoji" style="font-size: 11px;">LV10</span>
-                </figure>
-
-                <div class="site-meta">
-                    <h1 class="site-name">
-                        <a href="https://demo.stack.jimmycai.com/">{{ $user["nickName"] }}</a>
-                    </h1>
-                    <h2 class="site-description">
-                        Lorem ipsum dolor sit amet, consectetur adipiscing elit.
-                    </h2>
-                </div>
-            </header>
-            <ol class="menu-social">
-                <li>
-                    <a
-                        href="https://github.com/CaiJimmy/hugo-theme-stack"
-                        target="_blank"
-                        title="GitHub"
-                        rel="me">
-                        <svg
-                            xmlns="http://www.w3.org/2000/svg"
-                            class="icon icon-tabler icon-tabler-brand-github"
-                            width="24"
-                            height="24"
-                            viewBox="0 0 24 24"
-                            stroke-width="2"
-                            stroke="currentColor"
-                            fill="none"
-                            stroke-linecap="round"
-                            stroke-linejoin="round">
-                            <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
-                            <path
-                                d="M9 19c-4.3 1.4 -4.3 -2.5 -6 -3m12 5v-3.5c0 -1 .1 -1.4 -.5 -2c2.8 -.3 5.5 -1.4 5.5 -6a4.6 4.6 0 0 0 -1.3 -3.2a4.2 4.2 0 0 0 -.1 -3.2s-1.1 -.3 -3.5 1.3a12.3 12.3 0 0 0 -6.2 0c-2.4 -1.6 -3.5 -1.3 -3.5 -1.3a4.2 4.2 0 0 0 -.1 3.2a4.6 4.6 0 0 0 -1.3 3.2c0 4.6 2.7 5.7 5.5 6c-.6 .6 -.6 1.2 -.5 2v3.5"></path>
-                        </svg>
-                    </a>
-                </li>
-
-                <li>
-                    <a
-                        href="https://twitter.com/"
-                        target="_blank"
-                        title="Twitter"
-                        rel="me">
-                        <svg
-                            xmlns="http://www.w3.org/2000/svg"
-                            class="icon icon-tabler icon-tabler-brand-twitter"
-                            width="24"
-                            height="24"
-                            viewBox="0 0 24 24"
-                            stroke-width="2"
-                            stroke="currentColor"
-                            fill="none"
-                            stroke-linecap="round"
-                            stroke-linejoin="round">
-                            <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
-                            <path
-                                d="M22 4.01c-1 .49 -1.98 .689 -3 .99c-1.121 -1.265 -2.783 -1.335 -4.38 -.737s-2.643 2.06 -2.62 3.737v1c-3.245 .083 -6.135 -1.395 -8 -4c0 0 -4.182 7.433 4 11c-1.872 1.247 -3.739 2.088 -6 2c3.308 1.803 6.913 2.423 10.034 1.517c3.58 -1.04 6.522 -3.723 7.651 -7.742a13.84 13.84 0 0 0 .497 -3.753c-.002 -.249 1.51 -2.772 1.818 -4.013z"></path>
-                        </svg>
-                    </a>
-                </li>
-            </ol>
-            <ol class="menu" id="main-menu">
-                <li class="current">
-                    <a href="https://demo.stack.jimmycai.com/">
-                        <svg
-                            xmlns="http://www.w3.org/2000/svg"
-                            class="icon icon-tabler icon-tabler-home"
-                            width="24"
-                            height="24"
-                            viewBox="0 0 24 24"
-                            stroke-width="2"
-                            stroke="currentColor"
-                            fill="none"
-                            stroke-linecap="round"
-                            stroke-linejoin="round">
-                            <path stroke="none" d="M0 0h24v24H0z"></path>
-                            <polyline points="5 12 3 12 12 3 21 12 19 12"></polyline>
-                            <path d="M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7"></path>
-                            <path d="M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6"></path>
-                        </svg>
-
-                        <span>Home</span>
-                    </a>
-                </li>
-
-                <li>
-                    <a href="https://demo.stack.jimmycai.com/archives/">
-                        <svg
-                            xmlns="http://www.w3.org/2000/svg"
-                            class="icon icon-tabler icon-tabler-archive"
-                            width="24"
-                            height="24"
-                            viewBox="0 0 24 24"
-                            stroke-width="2"
-                            stroke="currentColor"
-                            fill="none"
-                            stroke-linecap="round"
-                            stroke-linejoin="round">
-                            <path stroke="none" d="M0 0h24v24H0z"></path>
-                            <rect x="3" y="4" width="18" height="4" rx="2"></rect>
-                            <path d="M5 8v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-10"></path>
-                            <line x1="10" y1="12" x2="14" y2="12"></line>
-                        </svg>
-
-                        <span>Archives</span>
+            <span class="hamburger-box">
+                <span class="hamburger-inner"></span>
+            </span>
+        </button>
+
+        <header>
+            <figure class="site-avatar">
+                <a href="{{ route('blog.index', ['user' => $user['userName']]) }}">
+                    <img src="{{ $user['avatar'] ?? '' }}"
+                         width="300" height="300"
+                         class="site-logo"
+                         loading="lazy"
+                         alt="Avatar" />
+                </a>
+                @if(!empty($user['level']))
+                <span class="emoji" style="font-size: 11px;">LV{{ $user['level'] }}</span>
+                @endif
+            </figure>
+
+            <div class="site-meta">
+                <h1 class="site-name">
+                    <a href="{{ route('blog.index', ['user' => $user['userName']]) }}">
+                        {{ $user['nickName'] }}
                     </a>
-                </li>
-
-                <li>
-                    <a href="https://demo.stack.jimmycai.com/search/">
-                        <svg
-                            xmlns="http://www.w3.org/2000/svg"
-                            class="icon icon-tabler icon-tabler-search"
-                            width="24"
-                            height="24"
-                            viewBox="0 0 24 24"
-                            stroke-width="2"
-                            stroke="currentColor"
-                            fill="none"
-                            stroke-linecap="round"
-                            stroke-linejoin="round">
+                </h1>
+                @if(!empty($user['description']))
+                <h2 class="site-description">{{ $user['description'] }}</h2>
+                @endif
+            </div>
+        </header>
+
+        {{-- 社交链接 --}}
+        @if(!empty($user['social']))
+        <ol class="menu-social">
+            @foreach($user['social'] as $social)
+            <li>
+                <a href="{{ $social['url'] }}"
+                   target="_blank"
+                   title="{{ $social['name'] }}"
+                   rel="me">
+                    <i class="ti ti-brand-{{ strtolower($social['name']) }}"></i>
+                </a>
+            </li>
+            @endforeach
+        </ol>
+        @endif
+
+        {{-- 主导航 --}}
+        <ol class="menu" id="main-menu">
+            <li class="{{ request()->routeIs('blog.index') ? 'current' : '' }}">
+                <a href="{{ route('blog.index', ['user' => $user['userName']]) }}">
+                    <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-home"
+                         width="24" height="24" viewBox="0 0 24 24" stroke-width="2"
+                         stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
+                        <path stroke="none" d="M0 0h24v24H0z"></path>
+                        <polyline points="5 12 3 12 12 3 21 12 19 12"></polyline>
+                        <path d="M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7"></path>
+                        <path d="M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6"></path>
+                    </svg>
+                    <span>Home</span>
+                </a>
+            </li>
+
+            <li>
+                <a href="#">
+                    <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-user"
+                         width="24" height="24" viewBox="0 0 24 24" stroke-width="2"
+                         stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
+                        <path stroke="none" d="M0 0h24v24H0z"></path>
+                        <circle cx="12" cy="7" r="4"></circle>
+                        <path d="M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"></path>
+                    </svg>
+                    <span>About</span>
+                </a>
+            </li>
+
+            <li class="{{ request()->routeIs('blog.archives*') ? 'current' : '' }}">
+                <a href="{{ route('blog.archives', ['user' => $user['userName']]) }}">
+                    <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-archive"
+                         width="24" height="24" viewBox="0 0 24 24" stroke-width="2"
+                         stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
+                        <path stroke="none" d="M0 0h24v24H0z"></path>
+                        <rect x="3" y="4" width="18" height="4" rx="2"></rect>
+                        <path d="M5 8v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-10"></path>
+                        <line x1="10" y1="12" x2="14" y2="12"></line>
+                    </svg>
+                    <span>Archives</span>
+                </a>
+            </li>
+
+            <li class="{{ request()->routeIs('blog.search') ? 'current' : '' }}">
+                <a href="{{ route('blog.search', ['user' => $user['userName']]) }}">
+                    <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-search"
+                         width="24" height="24" viewBox="0 0 24 24" stroke-width="2"
+                         stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
+                        <path stroke="none" d="M0 0h24v24H0z"></path>
+                        <circle cx="10" cy="10" r="7"></circle>
+                        <line x1="21" y1="21" x2="15" y2="15"></line>
+                    </svg>
+                    <span>Search</span>
+                </a>
+            </li>
+
+            {{-- 暗色模式切换 --}}
+            <li class="menu-bottom-section">
+                <ol class="menu">
+                    <li id="dark-mode-toggle">
+                        <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-toggle-left"
+                             width="24" height="24" viewBox="0 0 24 24" stroke-width="2"
+                             stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
                             <path stroke="none" d="M0 0h24v24H0z"></path>
-                            <circle cx="10" cy="10" r="7"></circle>
-                            <line x1="21" y1="21" x2="15" y2="15"></line>
+                            <circle cx="8" cy="12" r="2"></circle>
+                            <rect x="2" y="6" width="20" height="12" rx="6"></rect>
                         </svg>
-
-                        <span>Search</span>
-                    </a>
-                </li>
-
-                <li>
-                    <a href="https://demo.stack.jimmycai.com/links/">
-                        <svg
-                            xmlns="http://www.w3.org/2000/svg"
-                            class="icon icon-tabler icon-tabler-link"
-                            width="24"
-                            height="24"
-                            viewBox="0 0 24 24"
-                            stroke-width="2"
-                            stroke="currentColor"
-                            fill="none"
-                            stroke-linecap="round"
-                            stroke-linejoin="round">
+                        <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-toggle-right"
+                             width="24" height="24" viewBox="0 0 24 24" stroke-width="2"
+                             stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
                             <path stroke="none" d="M0 0h24v24H0z"></path>
-                            <path
-                                d="M10 14a3.5 3.5 0 0 0 5 0l4 -4a3.5 3.5 0 0 0 -5 -5l-.5 .5"></path>
-                            <path
-                                d="M14 10a3.5 3.5 0 0 0 -5 0l-4 4a3.5 3.5 0 0 0 5 5l.5 -.5"></path>
+                            <circle cx="16" cy="12" r="2"></circle>
+                            <rect x="2" y="6" width="20" height="12" rx="6"></rect>
                         </svg>
-
-                        <span>Links</span>
-                    </a>
-                </li>
-
-                <li class="menu-bottom-section">
-                    <ol class="menu">
-                        <li id="dark-mode-toggle">
-                            <svg
-                                xmlns="http://www.w3.org/2000/svg"
-                                class="icon icon-tabler icon-tabler-toggle-left"
-                                width="24"
-                                height="24"
-                                viewBox="0 0 24 24"
-                                stroke-width="2"
-                                stroke="currentColor"
-                                fill="none"
-                                stroke-linecap="round"
-                                stroke-linejoin="round">
-                                <path stroke="none" d="M0 0h24v24H0z"></path>
-                                <circle cx="8" cy="12" r="2"></circle>
-                                <rect x="2" y="6" width="20" height="12" rx="6"></rect>
-                            </svg>
-
-                            <svg
-                                xmlns="http://www.w3.org/2000/svg"
-                                class="icon icon-tabler icon-tabler-toggle-right"
-                                width="24"
-                                height="24"
-                                viewBox="0 0 24 24"
-                                stroke-width="2"
-                                stroke="currentColor"
-                                fill="none"
-                                stroke-linecap="round"
-                                stroke-linejoin="round">
-                                <path stroke="none" d="M0 0h24v24H0z"></path>
-                                <circle cx="16" cy="12" r="2"></circle>
-                                <rect x="2" y="6" width="20" height="12" rx="6"></rect>
-                            </svg>
-
-                            <span>Dark Mode</span>
-                        </li>
-                    </ol>
-                </li>
-            </ol>
-        </aside>
-
-        <aside class="sidebar right-sidebar sticky">
-            <form
-                action="https://demo.stack.jimmycai.com/search/"
-                class="search-form widget">
-                <p>
-                    <label>Search</label>
-                    <input name="keyword" required="" placeholder="Type something..." />
-
-                    <button title="Search">
-                        <svg
-                            xmlns="http://www.w3.org/2000/svg"
-                            class="icon icon-tabler icon-tabler-search"
-                            width="24"
-                            height="24"
-                            viewBox="0 0 24 24"
-                            stroke-width="2"
-                            stroke="currentColor"
-                            fill="none"
-                            stroke-linecap="round"
-                            stroke-linejoin="round">
-                            <path stroke="none" d="M0 0h24v24H0z"></path>
-                            <circle cx="10" cy="10" r="7"></circle>
-                            <line x1="21" y1="21" x2="15" y2="15"></line>
-                        </svg>
-                    </button>
-                </p>
-            </form>
-
-            <section class="widget archives">
-                <div class="widget-icon">
-                    <svg
-                        xmlns="http://www.w3.org/2000/svg"
-                        class="icon icon-tabler icon-tabler-infinity"
-                        width="24"
-                        height="24"
-                        viewBox="0 0 24 24"
-                        stroke-width="2"
-                        stroke="currentColor"
-                        fill="none"
-                        stroke-linecap="round"
-                        stroke-linejoin="round">
-                        <path stroke="none" d="M0 0h24v24H0z"></path>
-                        <path
-                            d="M9.828 9.172a4 4 0 1 0 0 5.656 a10 10 0 0 0 2.172 -2.828a10 10 0 0 1 2.172 -2.828 a4 4 0 1 1 0 5.656a10 10 0 0 1 -2.172 -2.828a10 10 0 0 0 -2.172 -2.828"></path>
-                    </svg>
-                </div>
-                <h2 class="widget-title section-title">Archives</h2>
-
-                <div class="widget-archive--list">
-                    <div class="archives-year">
-                        <a href="https://demo.stack.jimmycai.com/archives/#2023">
-                            <span class="year">2023</span>
-                            <span class="count">4</span>
-                        </a>
-                    </div>
-                    <div class="archives-year">
-                        <a href="https://demo.stack.jimmycai.com/archives/#2022">
-                            <span class="year">2022</span>
-                            <span class="count">1</span>
-                        </a>
-                    </div>
-                </div>
+                        <span>Dark Mode</span>
+                    </li>
+                </ol>
+            </li>
+        </ol>
+
+    </aside>
+
+    {{-- ── 正文 ── --}}
+    <main class="main full-width">
+        <div>
+            @yield('content')
+        </div>
+
+        <footer class="site-footer">
+            {{-- 博主版权 --}}
+            <section class="copyright">
+                &copy; 2020 - {{ date('Y') }} {{ $user['nickName'] }}
             </section>
 
-            <section class="widget tagCloud">
-                <div class="widget-icon">
-                    <svg
-                        xmlns="http://www.w3.org/2000/svg"
-                        class="icon icon-tabler icon-tabler-hash"
-                        width="24"
-                        height="24"
-                        viewBox="0 0 24 24"
-                        stroke-width="2"
-                        stroke="currentColor"
-                        fill="none"
-                        stroke-linecap="round"
-                        stroke-linejoin="round">
-                        <path stroke="none" d="M0 0h24v24H0z"></path>
-                        <line x1="5" y1="9" x2="19" y2="9"></line>
-                        <line x1="5" y1="15" x2="19" y2="15"></line>
-                        <line x1="11" y1="4" x2="7" y2="20"></line>
-                        <line x1="17" y1="4" x2="13" y2="20"></line>
-                    </svg>
-                </div>
-                <h2 class="widget-title section-title">Categories</h2>
-
-                <div class="tagCloud-tags">
-                    @foreach($categories as $category)
-                    <a
-                        href="{{ route('blog.category', ['user' => $user['userName'],'category1' => $category['id'],]) }}"
-                        class="font_size_1">
-                        {{ $category['label'] }}
-                    </a>
-                    @endforeach
-                </div>
+            {{-- 主题版权(原样保留设计者信息) --}}
+            <section class="powerby">
+                &copy; 2020 - 2026 Hugo Theme Stack &nbsp;
+                Theme <b><a href="https://github.com/CaiJimmy/hugo-theme-stack"
+                             target="_blank" rel="noopener" data-version="3.30.0">Stack</a></b>
+                designed by
+                <a href="https://jimmycai.com/" target="_blank" rel="noopener">Jimmy</a>
             </section>
-
-            <section class="widget tagCloud">
-                <div class="widget-icon">
-                    <svg
-                        xmlns="http://www.w3.org/2000/svg"
-                        class="icon icon-tabler icon-tabler-tag"
-                        width="24"
-                        height="24"
-                        viewBox="0 0 24 24"
-                        stroke-width="2"
-                        stroke="currentColor"
-                        fill="none"
-                        stroke-linecap="round"
-                        stroke-linejoin="round">
+        </footer>
+    </main>
+
+    {{-- ── 右边栏 ── --}}
+    <aside class="sidebar right-sidebar sticky">
+
+        {{-- 搜索 --}}
+        <form action="{{ route('blog.search', ['user' => $user['userName']]) }}"
+              method="GET"
+              class="search-form widget">
+            <p>
+                <label>Search</label>
+                <input name="q"
+                       placeholder="Type something..."
+                       value="{{ request('q') }}" />
+                <button type="submit" title="Search">
+                    <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-search"
+                         width="24" height="24" viewBox="0 0 24 24" stroke-width="2"
+                         stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
                         <path stroke="none" d="M0 0h24v24H0z"></path>
-                        <path
-                            d="M11 3L20 12a1.5 1.5 0 0 1 0 2L14 20a1.5 1.5 0 0 1 -2 0L3 11v-4a4 4 0 0 1 4 -4h4"></path>
-                        <circle cx="9" cy="9" r="2"></circle>
+                        <circle cx="10" cy="10" r="7"></circle>
+                        <line x1="21" y1="21" x2="15" y2="15"></line>
                     </svg>
-                </div>
-                <h2 class="widget-title section-title">Tags</h2>
-
-                <div class="tagCloud-tags">
-                    <a
-                        href="https://demo.stack.jimmycai.com/tags/css/"
-                        class="font_size_1">
-                        Css
-                    </a>
-
-                    <a
-                        href="https://demo.stack.jimmycai.com/tags/example-tag/"
-                        class="font_size_1">
-                        Example Tag
-                    </a>
-
-                    <a
-                        href="https://demo.stack.jimmycai.com/tags/html/"
-                        class="font_size_1">
-                        Html
-                    </a>
-
-                    <a
-                        href="https://demo.stack.jimmycai.com/tags/markdown/"
-                        class="font_size_1">
-                        Markdown
-                    </a>
-
-                    <a
-                        href="https://demo.stack.jimmycai.com/tags/themes/"
-                        class="font_size_1">
-                        Themes
+                </button>
+            </p>
+        </form>
+
+        {{-- Archives --}}
+        @if(!empty($archives))
+        <section class="widget archives">
+            <div class="widget-icon">
+                <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-infinity"
+                     width="24" height="24" viewBox="0 0 24 24" stroke-width="2"
+                     stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
+                    <path stroke="none" d="M0 0h24v24H0z"></path>
+                    <path d="M9.828 9.172a4 4 0 1 0 0 5.656 a10 10 0 0 0 2.172 -2.828a10 10 0 0 1 2.172 -2.828 a4 4 0 1 1 0 5.656a10 10 0 0 1 -2.172 -2.828a10 10 0 0 0 -2.172 -2.828"></path>
+                </svg>
+            </div>
+            <h2 class="widget-title section-title">Archives</h2>
+            <div class="widget-archive--list">
+                @foreach($archives as $archive)
+                <div class="archives-year">
+                    <a href="{{ route('blog.archives.year', ['user' => $user['userName'], 'year' => $archive['year']]) }}">
+                        <span class="year">{{ $archive['year'] }}</span>
+                        <span class="count">{{ $archive['count'] }}</span>
                     </a>
                 </div>
-            </section>
-        </aside>
-
-        <main class="main full-width">
-            <div>
-                @yield('content')
+                @endforeach
             </div>
-
-            <footer class="site-footer">
-                <section class="copyright">
-                    2020 - 2025 wikipali
-                </section>
-
-                <section class="powerby">
-                    Theme
-                    <b><a
-                            href="https://github.com/CaiJimmy/hugo-theme-stack"
-                            target="_blank"
-                            rel="noopener"
-                            data-version="3.30.0">Stack</a></b>
-                    designed by
-                    <a href="https://jimmycai.com/" target="_blank" rel="noopener">Jimmy</a>
-                    © 2020 - 2025 Hugo Theme Stack Starter
-                </section>
-            </footer>
-        </main>
-    </div>
-    <script
-        src="{{ URL::asset('assets/js/blog/vibrant.min.js') }}"
+        </section>
+        @endif
+
+        {{-- Categories --}}
+        @if(!empty($categories))
+        <section class="widget tagCloud">
+            <div class="widget-icon">
+                <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-hash"
+                     width="24" height="24" viewBox="0 0 24 24" stroke-width="2"
+                     stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
+                    <path stroke="none" d="M0 0h24v24H0z"></path>
+                    <line x1="5" y1="9" x2="19" y2="9"></line>
+                    <line x1="5" y1="15" x2="19" y2="15"></line>
+                    <line x1="11" y1="4" x2="7" y2="20"></line>
+                    <line x1="17" y1="4" x2="13" y2="20"></line>
+                </svg>
+            </div>
+            <h2 class="widget-title section-title">Categories</h2>
+            <div class="tagCloud-tags">
+                @foreach($categories as $category)
+                <a href="{{ route('blog.category', ['user' => $user['userName'], 'category1' => $category['id']]) }}">
+                    {{ $category['label'] }}
+                </a>
+                @endforeach
+            </div>
+        </section>
+        @endif
+
+        {{-- Tags --}}
+        @if(!empty($tags))
+        <section class="widget tagCloud">
+            <div class="widget-icon">
+                <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-tag"
+                     width="24" height="24" viewBox="0 0 24 24" stroke-width="2"
+                     stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
+                    <path stroke="none" d="M0 0h24v24H0z"></path>
+                    <path d="M11 3L20 12a1.5 1.5 0 0 1 0 2L14 20a1.5 1.5 0 0 1 -2 0L3 11v-4a4 4 0 0 1 4 -4h4"></path>
+                    <circle cx="9" cy="9" r="2"></circle>
+                </svg>
+            </div>
+            <h2 class="widget-title section-title">Tags</h2>
+            <div class="tagCloud-tags">
+                @foreach($tags as $tag)
+                <a href="{{ route('blog.tag', ['user' => $user['userName'], 'tag' => $tag['name']]) }}"
+                   class="font_size_1">
+                    {{ $tag['name'] }}
+                </a>
+                @endforeach
+            </div>
+        </section>
+        @endif
+
+    </aside>
+
+</div>
+
+@push('scripts')
+<script>
+    (function() {
+        const colorSchemeKey = "StackColorScheme";
+        if (!localStorage.getItem(colorSchemeKey)) {
+            localStorage.setItem(colorSchemeKey, "auto");
+        }
+    })();
+    (function() {
+        const colorSchemeKey = "StackColorScheme";
+        const colorSchemeItem = localStorage.getItem(colorSchemeKey);
+        const supportDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches === true;
+        if (colorSchemeItem == "dark" || (colorSchemeItem === "auto" && supportDarkMode)) {
+            document.documentElement.dataset.scheme = "dark";
+        } else {
+            document.documentElement.dataset.scheme = "light";
+        }
+    })();
+</script>
+<script src="{{ URL::asset('assets/js/blog/vibrant.min.js') }}"
         integrity="sha256-awcR2jno4kI5X0zL8ex0vi2z+KMkF24hUW8WePSA9HM="
         crossorigin="anonymous"></script>
-    <script
-        type="text/javascript"
-        src="{{ URL::asset('assets/js/blog/main.1e9a3bafd846ced4c345d084b355fb8c7bae75701c338f8a1f8a82c780137826.js') }}"
-        defer=""></script>
-    <script>
-        (function() {
-            const customFont = document.createElement("link");
-            customFont.href =
-                "https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap";
-
-            customFont.type = "text/css";
-            customFont.rel = "stylesheet";
-
-            document.head.appendChild(customFont);
-        })();
-    </script>
-</body>
-
-</html>
+<script src="{{ URL::asset('assets/js/blog/main.1e9a3bafd846ced4c345d084b355fb8c7bae75701c338f8a1f8a82c780137826.js') }}"
+        type="text/javascript" defer></script>
+<script>
+    (function() {
+        const customFont = document.createElement("link");
+        customFont.href = "https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap";
+        customFont.type = "text/css";
+        customFont.rel = "stylesheet";
+        document.head.appendChild(customFont);
+    })();
+</script>
+@endpush
+
+@endsection

+ 0 - 28
api-v12/resources/views/components/book-item.blade.php

@@ -1,28 +0,0 @@
-{{-- resources/views/components/book-item.blade.php --}}
-<div class="book-item">
-    <div class="card h-100">
-        <div class="card-body">
-            <div class="book-cover-container">
-                <a href="{{ route('library.tipitaka.show', $book['id']) }}" class="text-decoration-none">
-                    <img src="{{ $book['cover'] ?? 'https://via.placeholder.com/300x400?text=No+Cover' }}"
-                        alt="{{ $book['title'] ?? '未知书籍' }}"
-                        class="book-cover"
-                        loading="lazy">
-                </a>
-            </div>
-            <div class="book-info">
-                <div class="book-title">{{ $book['title'] ?? '未知书籍' }}</div>
-                <div class="book-author">{{ $book['author'] ?? '未知作者' }}</div>
-                <div class="book-author">
-                    <a href="{{ route('blog.index', ['user' => $book['publisher']->username]) }}">
-                        {{ $book['publisher']->nickname }}
-                    </a>
-                </div>
-                <div class="book-language">
-                    <span class="language-badge">{{ $book['language'] ?? '未知语言' }}</span>
-                    <span class="language-badge">{{ $book['type'] ?? '未知类型' }}</span>
-                </div>
-            </div>
-        </div>
-    </div>
-</div>

+ 0 - 118
api-v12/resources/views/components/book-list.blade.php

@@ -1,118 +0,0 @@
-{{-- resources/views/components/book-list.blade.php --}}
-@once
-@push('styles')
-<link href="https://cdnjs.cloudflare.com/ajax/libs/tabler/1.0.0-beta19/css/tabler.min.css" rel="stylesheet">
-<style>
-    .book-list-container {
-        max-width: 1024px;
-        margin: 0 auto;
-        padding: 20px;
-    }
-
-    .book-item {
-        margin-bottom: 24px;
-        transition: all 0.3s ease;
-    }
-
-    .book-item:hover {
-        transform: translateY(-2px);
-        box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
-    }
-
-    .book-cover {
-        width: 100%;
-        aspect-ratio: 3/4;
-        object-fit: contain !important;
-        border-radius: 6px;
-        background-color: #f8f9fa;
-    }
-
-    .book-info {
-        padding: 16px 0;
-    }
-
-    .book-title {
-        font-size: 1.125rem;
-        font-weight: 600;
-        color: #1f2937;
-        margin-bottom: 8px;
-        line-height: 1.4;
-    }
-
-    .book-author {
-        color: #6b7280;
-        font-size: 0.95rem;
-        margin-bottom: 6px;
-    }
-
-    .book-language {
-        color: #9ca3af;
-        font-size: 0.875rem;
-    }
-
-    .language-badge {
-        display: inline-block;
-        padding: 2px 8px;
-        background-color: #e5e7eb;
-        color: #374151;
-        border-radius: 12px;
-        font-size: 0.75rem;
-        font-weight: 500;
-    }
-
-    /* 桌面端布局 */
-    @media (min-width: 576px) {
-        .book-grid {
-            display: grid;
-            grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
-            gap: 24px;
-        }
-    }
-
-    /* 手机端布局 */
-    @media (max-width: 575px) {
-        .book-item .card-body {
-            display: flex;
-            gap: 16px;
-            align-items: stretch;
-        }
-
-        .book-cover-container {
-            flex: 0 0 120px;
-        }
-
-        .book-cover {
-            height: 160px;
-            width: 120px;
-        }
-
-        .book-info {
-            flex: 1;
-            padding: 0;
-            display: flex;
-            flex-direction: column;
-            justify-content: space-between;
-        }
-
-        .book-title {
-            font-size: 1rem;
-            margin-bottom: 12px;
-        }
-    }
-</style>
-@endpush
-@endonce
-
-<div>
-    @if(!empty($books) && count($books) > 0)
-    <div class="book-grid">
-        @foreach($books as $book)
-        @include('components.book-item', ['book' => $book])
-        @endforeach
-    </div>
-    @else
-    <div class="text-center py-5">
-        <p class="text-muted">暂无图书数据</p>
-    </div>
-    @endif
-</div>

+ 24 - 0
api-v12/resources/views/components/library/footer.blade.php

@@ -0,0 +1,24 @@
+{{-- resources/views/components/library/footer.blade.php
+     Library 栏目全站 footer。
+     当前为最简占位版本,后续按需扩展。
+--}}
+<footer class="footer footer-transparent d-print-none">
+    <div class="container-xl">
+        <div class="row text-center align-items-center">
+            <div class="col-12 col-lg-auto mt-3 mt-lg-0">
+                <ul class="list-inline list-inline-dots mb-0">
+                    <li class="list-inline-item">
+                        <a href="{{ route('library.home') }}" class="link-secondary">
+                            WikiPāli
+                        </a>
+                    </li>
+                    <li class="list-inline-item">
+                        <a href="{{ route('library.download') }}" class="link-secondary">
+                            下载
+                        </a>
+                    </li>
+                </ul>
+            </div>
+        </div>
+    </div>
+</footer>

+ 3 - 15
api-v12/resources/views/components/library/header.blade.php

@@ -9,16 +9,10 @@
 
             <ul class="bc-nav">
                 <li><a href="{{ route('library.home') }}">首页</a></li>
+                <li><a href="{{ route('library.tipitaka.index') }}">三藏</a></li>
                 <li><a href="{{ route('library.wiki.home') }}">百科</a></li>
                 <li><a href="{{ route('library.anthology.index') }}">文集</a></li>
                 <li><a href="{{ route('library.download') }}">下载</a></li>
-
-                @auth
-                <li><a href="#">我的账户</a></li>
-                @else
-                <li><a href="#">注册/登录</a></li>
-                @endauth
-
                 <li>
                     <x-language-switcher />
                 </li>
@@ -46,17 +40,11 @@
 
     <ul class="bc-mobile-nav">
         <li><a href="{{ route('library.home') }}">首页</a></li>
+        <li><a href="{{ route('library.tipitaka.index') }}">三藏</a></li>
         <li><a href="{{ route('library.wiki.home') }}">百科</a></li>
         <li><a href="{{ route('library.anthology.index') }}">文集</a></li>
         <li><a href="{{ route('library.download') }}">下载</a></li>
-
-        @auth
-        <li><a href="#">我的账户</a></li>
-        @else
-        <li><a href="#">注册/登录</a></li>
-        @endauth
-
-        <li style="padding:1rem 1.25rem;">
+        <li style="padding:1rem 0.25rem;">
             <x-language-switcher />
         </li>
     </ul>

+ 62 - 0
api-v12/resources/views/components/library/navbar.blade.php

@@ -0,0 +1,62 @@
+{{-- resources/views/components/library/navbar.blade.php
+     替换原 components/library/header.blade.php。
+     文件名从 header 改为 navbar,与规范目录对齐。
+     原文件内容不变,仅做变量名规范化(--wp-* token 替代硬编码色值已在 CSS 层处理)。
+--}}
+<div class="anthology-breadcrumb-bar">
+    <div class="container-xl">
+        <div class="bc-inner">
+
+            <ol class="breadcrumb mb-0">
+                @yield('breadcrumb')
+            </ol>
+
+            <ul class="bc-nav">
+                <li><a href="{{ route('library.home') }}"
+                       class="{{ request()->routeIs('library.home') ? 'active' : '' }}">首页</a></li>
+                <li><a href="{{ route('library.tipitaka.index') }}"
+                       class="{{ request()->routeIs('library.tipitaka.*') ? 'active' : '' }}">三藏</a></li>
+                <li><a href="{{ route('library.wiki.home') }}"
+                       class="{{ request()->routeIs('library.wiki.*') ? 'active' : '' }}">百科</a></li>
+                <li><a href="{{ route('library.anthology.index') }}"
+                       class="{{ request()->routeIs('library.anthology.*') ? 'active' : '' }}">文集</a></li>
+                <li><a href="{{ route('library.download') }}"
+                       class="{{ request()->routeIs('library.download') ? 'active' : '' }}">下载</a></li>
+                <li>
+                    <x-language-switcher />
+                </li>
+            </ul>
+
+            <button class="bc-hamburger" id="bcHamburger" aria-label="打开导航">
+                <i class="ti ti-menu-2"></i>
+            </button>
+
+        </div>
+    </div>
+</div>
+
+<div class="bc-mobile-overlay" id="bcOverlay"></div>
+
+<div class="bc-mobile-drawer" id="bcDrawer">
+    <div class="bc-mobile-drawer-header">
+        <span>导航</span>
+        <button class="bc-mobile-drawer-close" id="bcDrawerClose" aria-label="关闭">
+            <i class="ti ti-x"></i>
+        </button>
+    </div>
+    <ul class="bc-mobile-nav">
+        <li><a href="{{ route('library.home') }}"
+               class="{{ request()->routeIs('library.home') ? 'active' : '' }}">首页</a></li>
+        <li><a href="{{ route('library.tipitaka.index') }}"
+               class="{{ request()->routeIs('library.tipitaka.*') ? 'active' : '' }}">三藏</a></li>
+        <li><a href="{{ route('library.wiki.home') }}"
+               class="{{ request()->routeIs('library.wiki.*') ? 'active' : '' }}">百科</a></li>
+        <li><a href="{{ route('library.anthology.index') }}"
+               class="{{ request()->routeIs('library.anthology.*') ? 'active' : '' }}">文集</a></li>
+        <li><a href="{{ route('library.download') }}"
+               class="{{ request()->routeIs('library.download') ? 'active' : '' }}">下载</a></li>
+        <li style="padding: 1rem 0.25rem;">
+            <x-language-switcher />
+        </li>
+    </ul>
+</div>

+ 40 - 0
api-v12/resources/views/components/ui/author-avatar.blade.php

@@ -0,0 +1,40 @@
+{{-- resources/views/components/ui/author-avatar.blade.php
+     作者头像组件。支持图片头像和文字缩写头像两种形式。
+     Props:
+       $avatar   — 头像图片 URL(为空时显示文字头像)
+       $color    — 文字头像背景色
+       $initials — 文字头像缩写
+       $name     — 作者名字(显示在头像右侧)
+       $size     — sm | md | lg
+       $sub      — 副文字(如文章数量),可选
+--}}
+@props([
+    'avatar'   => null,
+    'color'    => '#888888',
+    'initials' => '?',
+    'name'     => '',
+    'size'     => 'md',
+    'sub'      => null,
+])
+
+<div class="author-avatar author-avatar--{{ $size }}">
+    @if($avatar)
+        <img src="{{ $avatar }}"
+             alt="{{ $name }}"
+             class="author-avatar__img">
+    @else
+        <div class="author-avatar__initials"
+             style="background: {{ $color }}">
+            {{ $initials }}
+        </div>
+    @endif
+
+    @if($name)
+    <div class="author-avatar__info">
+        <span class="author-avatar__name">{{ $name }}</span>
+        @if($sub)
+        <span class="author-avatar__sub">{{ $sub }}</span>
+        @endif
+    </div>
+    @endif
+</div>

+ 46 - 0
api-v12/resources/views/components/ui/book-cover.blade.php

@@ -0,0 +1,46 @@
+{{-- resources/views/components/ui/book-cover.blade.php
+     书籍封面组件。支持图片封面和渐变+文字封面两种形式。
+     Props:
+       $image    — 封面图片 URL(为空时显示渐变+文字)
+       $gradient — 渐变 CSS 字符串,image 为空时使用
+       $title    — 书名(image 为空时显示在封面上)
+       $subtitle — 副标题(可选)
+       $size     — sm | md | lg(控制尺寸)
+       $style3d  — true | false(是否显示 3D 书脊效果,默认 true)
+       $alt      — img alt 文字(默认使用 $title)
+--}}
+@props([
+    'image'    => null,
+    'gradient' => 'linear-gradient(135deg, #2d2010 0%, #1a1208 100%)',
+    'title'    => '',
+    'subtitle' => '',
+    'size'     => 'md',
+    'style3d'  => true,
+    'alt'      => null,
+])
+
+@php
+$sizes = [
+    'sm' => 'book-cover--sm',
+    'md' => 'book-cover--md',
+    'lg' => 'book-cover--lg',
+];
+$sizeClass = $sizes[$size] ?? $sizes['md'];
+@endphp
+
+<div class="book-cover {{ $sizeClass }} {{ $style3d ? 'book-cover--3d' : '' }}"
+     style="{{ $image ? '' : 'background: ' . $gradient }}">
+
+    @if($image)
+        <img src="{{ $image }}" alt="{{ $alt ?? $title }}" class="book-cover__img">
+    @else
+        <div class="book-cover__text">
+            <div class="book-cover__title">{{ $title }}</div>
+            @if($subtitle)
+                <div class="book-cover__divider"></div>
+                <div class="book-cover__subtitle">{{ $subtitle }}</div>
+            @endif
+        </div>
+    @endif
+
+</div>

+ 21 - 0
api-v12/resources/views/components/ui/book-grid.blade.php

@@ -0,0 +1,21 @@
+{{-- resources/views/components/ui/book-grid.blade.php
+     书籍网格组件。替代原 components/book-list.blade.php。
+     去除内联 <style> 和 CDN 引入,样式由 modules/_tipitaka.css 提供。
+     Props:
+       $books      — 书籍数组
+       $emptyText  — 空状态文字(默认"暂无图书")
+--}}
+@props([
+    'books'     => [],
+    'emptyText' => '暂无图书',
+])
+
+@if(!empty($books) && count($books) > 0)
+<div class="book-grid">
+    @foreach($books as $book)
+        <x-ui.card-book :book="$book" />
+    @endforeach
+</div>
+@else
+<x-ui.empty-state :title="$emptyText" />
+@endif

+ 69 - 0
api-v12/resources/views/components/ui/card-anthology.blade.php

@@ -0,0 +1,69 @@
+{{-- resources/views/components/ui/card-anthology.blade.php
+     文集卡片组件。横向布局:封面左 + 内容右。
+     用于 anthology/index 列表。
+     Props:
+       $item — 文集数据数组,包含以下字段:
+               id, title, subtitle, description, cover_image, cover_gradient,
+               author{name, avatar, color, initials},
+               chapters[], children_number, updated_at
+       $href — 卡片链接
+--}}
+@props([
+    'item',
+    'href',
+])
+
+<a href="{{ $href }}" class="anthology-card">
+
+    {{-- 封面 --}}
+    <x-ui.book-cover
+        :image="$item['cover_image'] ?? null"
+        :gradient="$item['cover_gradient'] ?? ''"
+        :title="$item['title']"
+        :subtitle="$item['subtitle'] ?? ''"
+        size="md"
+        :style3d="false"
+    />
+
+    {{-- 内容 --}}
+    <div class="anthology-card__body">
+        <div class="anthology-card__title">{{ $item['title'] }}</div>
+
+        @if(!empty($item['description']))
+        <div class="anthology-card__desc">{{ $item['description'] }}</div>
+        @endif
+
+        <div class="anthology-card__author">
+            <x-ui.author-avatar
+                :avatar="$item['author']['avatar'] ?? null"
+                :color="$item['author']['color'] ?? '#888'"
+                :initials="$item['author']['initials'] ?? '?'"
+                :name="$item['author']['name']"
+                size="sm"
+            />
+        </div>
+
+        @if(!empty($item['chapters']))
+        <div class="anthology-card__tags">
+            @foreach(array_slice($item['chapters'], 0, 4) as $ch)
+            <span class="anthology-tag">{{ mb_strimwidth($ch, 0, 14, '…') }}</span>
+            @endforeach
+            @if($item['children_number'] > 4)
+            <span class="anthology-tag anthology-tag--more">+{{ $item['children_number'] - 4 }} 章</span>
+            @endif
+        </div>
+        @endif
+
+        <div class="anthology-card__meta">
+            <span class="anthology-meta-item">
+                <i class="ti ti-calendar"></i>
+                {{ $item['updated_at'] }}
+            </span>
+            <span class="anthology-meta-item">
+                <i class="ti ti-file-text"></i>
+                {{ $item['children_number'] }} 章节
+            </span>
+        </div>
+    </div>
+
+</a>

+ 52 - 0
api-v12/resources/views/components/ui/card-book.blade.php

@@ -0,0 +1,52 @@
+{{-- resources/views/components/ui/card-book.blade.php
+     书籍卡片组件。纵向布局:封面上 + 信息下。
+     用于 tipitaka 列表页。
+     Props:
+       $book — 书籍数据数组,包含:
+               id, title, author, cover(可选),
+               cover_gradient(可选), publisher(object, 可选),
+               language(可选), type(可选)
+--}}
+@props(['book'])
+
+<div class="card-book">
+    <a href="{{ route('library.tipitaka.show', $book['id']) }}" class="card-book__link">
+
+        {{-- 封面 --}}
+        <x-ui.book-cover
+            :image="$book['cover'] ?? null"
+            :gradient="$book['cover_gradient'] ?? 'linear-gradient(135deg, #2d2010 0%, #1a1208 100%)'"
+            :title="$book['title'] ?? ''"
+            :subtitle="'义注'"
+            size="md"
+            :style3d="false" />
+
+        {{-- 书籍信息 --}}
+        <div class="card-book__info">
+            <div class="card-book__title">{{ $book['title'] ?? '未知书籍' }}</div>
+
+            @if(!empty($book['author']))
+            <div class="card-book__author">{{ $book['author'] }}</div>
+            @endif
+
+            @if(isset($book['publisher']))
+            <div class="card-book__publisher">
+                <a href="{{ route('blog.index', ['user' => $book['publisher']->username]) }}"
+                    class="card-book__publisher-link">
+                    {{ $book['publisher']->nickname }}
+                </a>
+            </div>
+            @endif
+
+            <div class="card-book__badges">
+                @if(!empty($book['language']))
+                <span class="card-book__badge">{{ $book['language'] }}</span>
+                @endif
+                @if(!empty($book['type']))
+                <span class="card-book__badge">{{ $book['type'] }}</span>
+                @endif
+            </div>
+        </div>
+
+    </a>
+</div>

+ 35 - 0
api-v12/resources/views/components/ui/empty-state.blade.php

@@ -0,0 +1,35 @@
+{{-- resources/views/components/ui/empty-state.blade.php
+     通用空状态组件。
+     Props:
+       $title — 标题文字
+       $desc  — 描述文字(支持 HTML,可选)
+       $icon  — 自定义图标 slot(可选,默认搜索图标)
+--}}
+@props([
+    'title' => '未找到相关内容',
+    'desc'  => '',
+])
+
+<div class="wiki-empty-state">
+    <div class="wiki-empty-icon">
+        {{ $icon ?? '' }}
+        @unless($icon ?? false)
+        <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>
+        @endunless
+    </div>
+
+    <div class="wiki-empty-title">{{ $title }}</div>
+
+    @if ($desc)
+    <div class="wiki-empty-desc">{!! $desc !!}</div>
+    @endif
+
+    @isset($slot)
+        {{ $slot }}
+    @endisset
+</div>

+ 42 - 0
api-v12/resources/views/components/ui/search-input.blade.php

@@ -0,0 +1,42 @@
+{{-- resources/views/components/ui/search-input.blade.php
+     通用搜索输入框组件。
+     Props:
+       $action      — 表单提交路由
+       $value       — 当前搜索词(默认空)
+       $placeholder — 占位文字
+       $buttonText  — 按钮文字(默认"搜索")
+       $size        — lg | md(默认 md)
+       $hiddenFields — 额外隐藏字段 array ['name' => 'value']
+       $autofocus   — 是否自动聚焦(默认 false)
+--}}
+@props([
+'action',
+'value' => '',
+'placeholder' => '搜索…',
+'buttonText' => '搜索',
+'size' => 'md',
+'hiddenFields' => [],
+'autofocus' => false,
+])
+
+<form action="{{ $action }}" method="GET" class="search-input-form">
+    <div class="input-group {{ $size === 'lg' ? 'input-group-lg' : '' }}">
+        <input
+            type="text"
+            name="q"
+            class="form-control search-input"
+            value="{{ $value }}"
+            placeholder="{{ $placeholder }}"
+            {{ $autofocus ? 'autofocus' : '' }} />
+
+        @foreach ($hiddenFields as $name => $val)
+        @if ($val)
+        <input type="hidden" name="{{ $name }}" value="{{ $val }}">
+        @endif
+        @endforeach
+
+        <button class="btn btn-primary" type="submit">
+            {{ $buttonText }}
+        </button>
+    </div>
+</form>

+ 0 - 5
api-v12/resources/views/components/wiki/search-box.blade.php

@@ -36,11 +36,6 @@
             </div>
             <div class="col-auto">
                 <button type="submit" class="btn btn-primary">
-                    <svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
-                        <path stroke="none" d="M0 0h24v24H0z" fill="none" />
-                        <circle cx="10" cy="10" r="7" />
-                        <line x1="21" y1="21" x2="15" y2="15" />
-                    </svg>
                     <span class="d-none d-sm-inline ms-2">{{ $buttonText }}</span>
                 </button>
             </div>

+ 26 - 0
api-v12/resources/views/layouts/base.blade.php

@@ -0,0 +1,26 @@
+{{-- resources/views/layouts/base.blade.php
+     全站 HTML 骨架。不含任何导航、页脚、布局结构。
+     所有子布局继承此文件。
+--}}
+<!doctype html>
+<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
+
+<head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
+    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
+    <title>@yield('title', 'WikiPāli · 巴利佛典百科')</title>
+
+    {{-- 基础样式由子布局通过 @vite 注入 --}}
+    @stack('styles')
+</head>
+
+<body class="@yield('body-class')">
+    <div class="page">
+        @yield('page')
+    </div>
+
+    @stack('scripts')
+</body>
+
+</html>

+ 37 - 0
api-v12/resources/views/layouts/library.blade.php

@@ -0,0 +1,37 @@
+{{-- resources/views/layouts/library.blade.php
+     library/* 所有列表页、详情页的布局外壳。
+     包含:navbar(header 组件)、可选 hero、可选 toolbar、主内容区、footer。
+     阅读页使用 layouts/reader,不继承此文件。
+--}}
+@extends('layouts.base')
+
+@push('styles')
+@vite(['resources/css/library.css', 'resources/js/app.js'])
+@endpush
+
+@section('page')
+
+{{-- 导航 + 可选 Hero 包裹层(Hero 存在时 breadcrumb bar 绝对定位覆盖其上) --}}
+<div class="hero-wrapper">
+    <x-library.navbar />
+    @yield('hero')
+</div>
+
+{{-- 可选工具条 --}}
+@hasSection('toolbar')
+<div class="page-toolbar">
+    <div class="container-xl">
+        @yield('toolbar')
+    </div>
+</div>
+@endif
+
+{{-- 主内容区 --}}
+<div class="page-wrapper">
+    @yield('content')
+</div>
+
+{{-- Footer --}}
+<x-library.footer />
+
+@endsection

+ 13 - 0
api-v12/resources/views/layouts/reader.blade.php

@@ -0,0 +1,13 @@
+{{-- resources/views/layouts/reader.blade.php
+     全站阅读页布局。沉浸式,无 library navbar,无 footer。
+     用于:tipitaka/read、anthology/read、wiki/show、blog/show。
+--}}
+@extends('layouts.base')
+
+@push('styles')
+    @vite(['resources/css/library.css', 'resources/css/reader.css'])
+@endpush
+
+@section('page')
+    @yield('reader-content')
+@endsection

+ 48 - 315
api-v12/resources/views/library/anthology/index.blade.php

@@ -1,356 +1,89 @@
-@extends('library.layouts.app')
+{{-- resources/views/library/anthology/index.blade.php --}}
+@extends('layouts.library')
 
 @section('title', '文集 · 巴利书库')
 
 @push('styles')
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;600;700&family=Noto+Sans+SC:wght@300;400;500&display=swap" rel="stylesheet">
-<style>
-:root {
-    --sf: #c8860a;
-    --sf-light: #f5e6c8;
-    --sf-pale: #fdf8f0;
-    --ink: #1a1208;
-    --ink-soft: #4a3f2f;
-    --ink-muted: #8a7a68;
-    --bdr: #e8ddd0;
-    --card-bg: #fffdf9;
-}
-body { background: var(--sf-pale) !important; font-family: 'Noto Sans SC', sans-serif; }
-
-/* Page header */
-.anthology-page-header {
-    background: linear-gradient(135deg, var(--ink) 0%, #2d2010 100%);
-    padding: 2.25rem 0 2rem;
-    position: relative;
-    overflow: hidden;
-    margin-bottom: 0;
-}
-.anthology-page-header::before {
-    content: '藏';
-    font-family: 'Noto Serif SC', serif;
-    font-size: 16rem;
-    font-weight: 700;
-    color: rgba(255,255,255,0.03);
-    position: absolute;
-    right: -1rem;
-    top: -2.5rem;
-    line-height: 1;
-    pointer-events: none;
-}
-.anthology-page-header h1 {
-    font-family: 'Noto Serif SC', serif;
-    font-size: 1.75rem;
-    font-weight: 600;
-    color: #fff;
-    margin: 0 0 0.3rem;
-    letter-spacing: 0.08em;
-}
-.anthology-page-header p { color: rgba(255,255,255,0.45); font-size: 0.85rem; margin: 0; }
-.result-badge {
-    background: var(--sf);
-    color: var(--ink);
-    font-size: 0.75rem;
-    font-weight: 700;
-    padding: 2px 9px;
-    border-radius: 20px;
-    margin-left: 0.6rem;
-    vertical-align: middle;
-}
-
-/* Card */
-.anthology-card {
-    background: var(--card-bg);
-    border: 1px solid var(--bdr);
-    border-radius: 10px;
-    overflow: hidden;
-    display: flex;
-    transition: box-shadow .25s, transform .25s;
-    margin-bottom: 1.1rem;
-    text-decoration: none;
-    color: inherit;
-}
-.anthology-card:hover {
-    box-shadow: 0 8px 28px rgba(200,134,10,.12), 0 2px 8px rgba(0,0,0,.06);
-    transform: translateY(-2px);
-    color: inherit;
-    text-decoration: none;
-}
-.card-cover {
-    width: 130px;
-    min-width: 130px;
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    justify-content: center;
-    padding: 1.1rem 0.7rem;
-    position: relative;
-    overflow: hidden;
-}
-.card-cover img {
-    width: 100%;
-    height: 100%;
-    object-fit: cover;
-    position: absolute;
-    inset: 0;
-}
-.card-cover::after {
-    content: '';
-    position: absolute;
-    inset: 0;
-    background: repeating-linear-gradient(45deg, transparent, transparent 8px, rgba(255,255,255,.015) 8px, rgba(255,255,255,.015) 9px);
-}
-.cover-text-wrap { position: relative; z-index: 1; text-align: center; }
-.cover-title {
-    font-family: 'Noto Serif SC', serif;
-    font-size: 1rem;
-    font-weight: 600;
-    color: #fff;
-    line-height: 1.6;
-    letter-spacing: .12em;
-    word-break: break-all;
-}
-.cover-divider { width: 28px; height: 1px; background: var(--sf); margin: .5rem auto; }
-.cover-sub { font-size: .65rem; color: rgba(255,255,255,.45); letter-spacing: .04em; }
-
-.card-body-inner { padding: 1.1rem 1.4rem; flex: 1; display: flex; flex-direction: column; }
-.card-main-title {
-    font-family: 'Noto Serif SC', serif;
-    font-size: 1.1rem;
-    font-weight: 600;
-    color: var(--ink);
-    margin-bottom: .35rem;
-    line-height: 1.4;
-}
-.anthology-card:hover .card-main-title { color: var(--sf); }
-.card-desc { font-size: .8rem; color: var(--ink-muted); margin-bottom: .65rem; line-height: 1.65; }
-.card-author { display: flex; align-items: center; gap: .45rem; margin-bottom: .7rem; }
-.a-avatar {
-    width: 24px; height: 24px; border-radius: 50%;
-    display: flex; align-items: center; justify-content: center;
-    font-size: .65rem; font-weight: 700; color: #fff; flex-shrink: 0;
-}
-.a-avatar-img {
-    width: 24px; height: 24px; border-radius: 50%;
-    object-fit: cover; flex-shrink: 0;
-}
-.a-name { font-size: .8rem; color: var(--ink-soft); font-weight: 500; }
-.chapter-tags { display: flex; flex-wrap: wrap; gap: .3rem; margin-top: auto; }
-.c-tag {
-    font-size: .7rem; color: var(--ink-muted);
-    background: var(--sf-light); border: 1px solid var(--bdr);
-    padding: 1px 7px; border-radius: 4px; white-space: nowrap;
-}
-.c-tag.more { background: transparent; border-color: transparent; color: var(--sf); }
-.card-meta-row {
-    display: flex; align-items: center; gap: .85rem;
-    margin-top: .65rem; padding-top: .65rem;
-    border-top: 1px solid var(--bdr);
-}
-.meta-it { font-size: .72rem; color: var(--ink-muted); display: flex; align-items: center; gap: .25rem; }
-
-/* Pagination — force horizontal */
-.anthology-pagination .pagination {
-    display: flex !important;
-    flex-direction: row !important;
-    flex-wrap: wrap;
-    gap: 3px;
-    justify-content: center;
-    list-style: none;
-    padding: 0;
-    margin: 0;
-}
-.anthology-pagination .page-item { display: inline-block; }
-.anthology-pagination .page-item .page-link {
-    border: 1px solid var(--bdr);
-    color: var(--ink-soft);
-    font-size: .82rem;
-    padding: 5px 11px;
-    background: var(--card-bg);
-    border-radius: 5px;
-    display: inline-block;
-    line-height: 1.5;
-    text-decoration: none;
-}
-.anthology-pagination .page-item.active .page-link { background: var(--sf); border-color: var(--sf); color: #fff; }
-.anthology-pagination .page-item .page-link:hover { background: var(--sf-light); color: var(--sf); }
-.anthology-pagination .page-item.disabled .page-link { opacity: .45; pointer-events: none; }
-
-/* Sidebar */
-.sidebar-box {
-    background: var(--card-bg);
-    border: 1px solid var(--bdr);
-    border-radius: 10px;
-    overflow: hidden;
-    margin-bottom: 1.1rem;
-}
-.sb-header {
-    padding: .75rem 1.15rem;
-    border-bottom: 1px solid var(--bdr);
-    font-family: 'Noto Serif SC', serif;
-    font-size: .875rem;
-    font-weight: 600;
-    color: var(--ink-soft);
-    letter-spacing: .04em;
-    display: flex;
-    align-items: center;
-    gap: .45rem;
-}
-.sb-header::before {
-    content: '';
-    display: block;
-    width: 3px;
-    height: 13px;
-    background: var(--sf);
-    border-radius: 2px;
-}
-.author-ul { list-style: none; padding: .35rem 0; margin: 0; }
-.author-ul li a {
-    display: flex; align-items: center; gap: .6rem;
-    padding: .45rem 1.15rem;
-    text-decoration: none;
-    transition: background .15s;
-}
-.author-ul li a:hover { background: var(--sf-pale); }
-.au-avatar {
-    width: 28px; height: 28px; border-radius: 50%;
-    display: flex; align-items: center; justify-content: center;
-    font-size: .68rem; font-weight: 700; color: #fff; flex-shrink: 0;
-}
-.au-avatar-img {
-    width: 28px; height: 28px; border-radius: 50%;
-    object-fit: cover; flex-shrink: 0;
-}
-.au-name { font-size: .8rem; color: var(--ink-soft); font-weight: 500; display: block; }
-.au-count { font-size: .7rem; color: var(--ink-muted); }
-
-/* Search */
-.search-wrap { background: var(--card-bg); border: 1px solid var(--bdr); border-radius: 10px; padding: .85rem 1.1rem; margin-bottom: 1.1rem; }
-.search-input {
-    width: 100%;
-    border: 1px solid var(--bdr);
-    border-radius: 6px;
-    padding: .45rem .9rem;
-    font-size: .83rem;
-    font-family: 'Noto Sans SC', sans-serif;
-    background: var(--sf-pale);
-    color: var(--ink);
-    outline: none;
-    transition: border-color .2s;
-}
-.search-input:focus { border-color: var(--sf); background: #fff; }
-.search-input::placeholder { color: var(--ink-muted); }
-
-@media (max-width:768px) {
-    .anthology-card { flex-direction: column; }
-    .card-cover { width: 100%; min-width: unset; height: 90px; }
-}
-</style>
+    @vite('resources/css/modules/_anthology.css')
 @endpush
 
-@section('content')
+@section('breadcrumb')
+<li class="breadcrumb-item">
+    <a href="{{ route('library.home') }}">首页</a>
+</li>
+<li class="breadcrumb-item active">文集</li>
+@endsection
+
+@section('hero')
 <div class="anthology-page-header">
     <div class="container-xl">
         <h1>文集 <span class="result-badge">{{ $total }}</span></h1>
         <p>经论注疏 · 禅修指引 · 法义探讨</p>
     </div>
 </div>
+@endsection
 
-<div class="page-body" style="background: var(--sf-pale);">
+@section('content')
+<div class="page-body">
     <div class="container-xl">
         <div class="row mt-3">
 
-            {{-- Left --}}
+            {{-- 文集列表 --}}
             <div class="col-lg-9">
 
                 @forelse($anthologies as $item)
-                <a href="{{ route('library.anthology.show', $item['id']) }}" class="anthology-card">
-                    {{-- Cover --}}
-                    <div class="card-cover" style="{{ $item['cover_image'] ? '' : 'background: ' . $item['cover_gradient'] }}">
-                        @if($item['cover_image'])
-                            <img src="{{ $item['cover_image'] }}" alt="{{ $item['title'] }}">
-                        @else
-                            <div class="cover-text-wrap">
-                                <div class="cover-title">{{ $item['title'] }}</div>
-                                <div class="cover-divider"></div>
-                                <div class="cover-sub">{{ $item['subtitle'] ?? '' }}</div>
-                            </div>
-                        @endif
-                    </div>
-
-                    {{-- Body --}}
-                    <div class="card-body-inner">
-                        <div class="card-main-title">{{ $item['title'] }}</div>
-                        @if(!empty($item['description']))
-                        <div class="card-desc">{{ $item['description'] }}</div>
-                        @endif
-                        <div class="card-author">
-                            @if(!empty($item['author']['avatar']))
-                                <img src="{{ $item['author']['avatar'] }}" class="a-avatar-img" alt="">
-                            @else
-                                <div class="a-avatar" style="background: {{ $item['author']['color'] }}">
-                                    {{ $item['author']['initials'] }}
-                                </div>
-                            @endif
-                            <span class="a-name">{{ $item['author']['name'] }}</span>
-                        </div>
-                        <div class="chapter-tags">
-                            @foreach(array_slice($item['chapters'], 0, 4) as $ch)
-                            <span class="c-tag" title="{{ $ch }}">{{ mb_strimwidth($ch, 0, 14, '…') }}</span>
-                            @endforeach
-                            @if($item['children_number'] > 4)
-                            <span class="c-tag more">+{{ $item['children_number'] - 4 }} 章</span>
-                            @endif
-                        </div>
-                        <div class="card-meta-row">
-                            <span class="meta-it">
-                                <i class="ti ti-calendar" style="font-size:.82rem;"></i>
-                                {{ $item['updated_at'] }}
-                            </span>
-                            <span class="meta-it">
-                                <i class="ti ti-file-text" style="font-size:.82rem;"></i>
-                                {{ $item['children_number'] }} 章节
-                            </span>
-                        </div>
-                    </div>
-                </a>
+                    <x-ui.card-anthology
+                        :item="$item"
+                        :href="route('library.anthology.show', $item['id'])"
+                    />
                 @empty
-                <div class="text-center py-5 text-muted">暂无文集</div>
+                    <div class="wiki-card">
+                        <x-ui.empty-state title="暂无文集" />
+                    </div>
                 @endforelse
 
-                {{-- Pagination --}}
-                <div class="d-flex justify-content-center mt-3 anthology-pagination">
+                {{-- 分页 --}}
+                <div class="d-flex justify-content-center mt-3">
                     {{ $anthologies->links('library.anthology.pagination') }}
                 </div>
 
-            </div>{{-- /col --}}
+            </div>
 
-            {{-- Sidebar --}}
+            {{-- 侧边栏 --}}
             <div class="col-lg-3">
-                <div class="search-wrap">
-                    <input type="text" class="search-input" placeholder="搜索文集…">
+
+                {{-- 搜索 --}}
+                <div class="sb-card" style="padding: .85rem 1.1rem; margin-bottom: 1.1rem;">
+                    <x-ui.search-input
+                        :action="route('library.search')"
+                        placeholder="搜索文集…"
+                        :hidden-fields="['type' => 'anthology']"
+                    />
                 </div>
 
-                <div class="sidebar-box">
-                    <div class="sb-header">作者</div>
+                {{-- 作者列表 --}}
+                @if(!empty($authors))
+                <div class="sb-card">
+                    <div class="sb-head">作者</div>
                     <ul class="author-ul">
                         @foreach($authors as $author)
                         <li>
                             <a href="#">
-                                @if(!empty($author['avatar']))
-                                    <img src="{{ $author['avatar'] }}" class="au-avatar-img" alt="">
-                                @else
-                                    <div class="au-avatar" style="background: {{ $author['color'] }}">{{ $author['initials'] }}</div>
-                                @endif
-                                <div>
-                                    <span class="au-name">{{ $author['name'] }}</span>
-                                    <span class="au-count">{{ $author['count'] }} 篇文集</span>
-                                </div>
+                                <x-ui.author-avatar
+                                    :avatar="$author['avatar'] ?? null"
+                                    :color="$author['color']"
+                                    :initials="$author['initials']"
+                                    :name="$author['name']"
+                                    :sub="$author['count'] . ' 篇文集'"
+                                    size="md"
+                                />
                             </a>
                         </li>
                         @endforeach
                     </ul>
                 </div>
+                @endif
+
             </div>
 
         </div>

+ 17 - 16
api-v12/resources/views/library/anthology/pagination.blade.php

@@ -1,33 +1,34 @@
+{{-- api-v12/resources/views/library/anthology/pagination.blade.php --}}
 @if ($paginator->hasPages())
 <ul class="pagination">
     {{-- Previous --}}
     @if ($paginator->onFirstPage())
-        <li class="page-item disabled"><span class="page-link">«</span></li>
+    <li class="page-item disabled"><span class="page-link">«</span></li>
     @else
-        <li class="page-item"><a class="page-link" href="{{ $paginator->previousPageUrl() }}">«</a></li>
+    <li class="page-item"><a class="page-link" href="{{ $paginator->previousPageUrl() }}">«</a></li>
     @endif
 
     {{-- Pages --}}
     @foreach ($elements as $element)
-        @if (is_string($element))
-            <li class="page-item disabled"><span class="page-link">{{ $element }}</span></li>
-        @endif
-        @if (is_array($element))
-            @foreach ($element as $page => $url)
-                @if ($page == $paginator->currentPage())
-                    <li class="page-item active"><span class="page-link">{{ $page }}</span></li>
-                @else
-                    <li class="page-item"><a class="page-link" href="{{ $url }}">{{ $page }}</a></li>
-                @endif
-            @endforeach
-        @endif
+    @if (is_string($element))
+    <li class="page-item disabled"><span class="page-link">{{ $element }}</span></li>
+    @endif
+    @if (is_array($element))
+    @foreach ($element as $page => $url)
+    @if ($page == $paginator->currentPage())
+    <li class="page-item active"><span class="page-link">{{ $page }}</span></li>
+    @else
+    <li class="page-item"><a class="page-link" href="{{ $url }}">{{ $page }}</a></li>
+    @endif
+    @endforeach
+    @endif
     @endforeach
 
     {{-- Next --}}
     @if ($paginator->hasMorePages())
-        <li class="page-item"><a class="page-link" href="{{ $paginator->nextPageUrl() }}">»</a></li>
+    <li class="page-item"><a class="page-link" href="{{ $paginator->nextPageUrl() }}">»</a></li>
     @else
-        <li class="page-item disabled"><span class="page-link">»</span></li>
+    <li class="page-item disabled"><span class="page-link">»</span></li>
     @endif
 </ul>
 @endif

+ 68 - 597
api-v12/resources/views/library/anthology/show.blade.php

@@ -1,584 +1,41 @@
-@extends('library.layouts.app')
+{{-- resources/views/library/anthology/show.blade.php --}}
+@extends('layouts.library')
 
 @section('title', $anthology['title'] . ' · 巴利书库')
 
+@push('styles')
+    @vite('resources/css/modules/_anthology.css')
+@endpush
+
 @section('breadcrumb')
 <li class="breadcrumb-item">
     <a href="{{ route('library.home') }}">首页</a>
 </li>
-
 <li class="breadcrumb-item">
     <a href="{{ route('library.anthology.index') }}">文集</a>
 </li>
-
-<li class="breadcrumb-item active">
-    {{ $anthology['title'] }}
-</li>
+<li class="breadcrumb-item active">{{ $anthology['title'] }}</li>
 @endsection
 
-@once
-@push('styles')
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;600;700&family=Noto+Sans+SC:wght@300;400;500&display=swap" rel="stylesheet">
-<style>
-    body {
-        background: var(--sf-pale) !important;
-        font-family: 'Noto Sans SC', sans-serif;
-    }
-
-
-    /* Hero */
-    .anthology-hero {
-        background: linear-gradient(135deg, var(--ink) 0%, #2d2010 100%);
-        padding: 2.5rem 0;
-    }
-
-    .hero-inner {
-        display: flex;
-        gap: 2.25rem;
-        align-items: flex-start;
-    }
-
-    /* Book cover */
-    .book-cover-3d {
-        width: 155px;
-        min-width: 155px;
-        height: 215px;
-        border-radius: 3px 9px 9px 3px;
-        display: flex;
-        flex-direction: column;
-        align-items: center;
-        justify-content: center;
-        padding: 1.25rem .9rem;
-        position: relative;
-        overflow: hidden;
-        box-shadow: -4px 0 0 rgba(0, 0, 0, .3), -6px 4px 14px rgba(0, 0, 0, .4), 4px 4px 18px rgba(0, 0, 0, .3);
-        flex-shrink: 0;
-    }
-
-    .book-cover-3d img {
-        position: absolute;
-        inset: 0;
-        width: 100%;
-        height: 100%;
-        object-fit: cover;
-    }
-
-    .book-cover-3d::before {
-        content: '';
-        position: absolute;
-        left: 0;
-        top: 0;
-        bottom: 0;
-        width: 13px;
-        background: linear-gradient(to right, rgba(0, 0, 0, .4), rgba(0, 0, 0, .1));
-        border-radius: 3px 0 0 3px;
-        z-index: 2;
-    }
-
-    .book-cover-3d::after {
-        content: '';
-        position: absolute;
-        inset: 0;
-        background: repeating-linear-gradient(45deg, transparent, transparent 8px, rgba(255, 255, 255, .015) 8px, rgba(255, 255, 255, .015) 9px);
-        z-index: 1;
-    }
-
-    .book-text-wrap {
-        position: relative;
-        z-index: 3;
-        text-align: center;
-    }
-
-    .book-title-text {
-        font-family: 'Noto Serif SC', serif;
-        font-size: 1.05rem;
-        font-weight: 600;
-        color: #fff;
-        line-height: 1.65;
-        letter-spacing: .13em;
-        word-break: break-all;
-    }
-
-    .book-divider {
-        width: 32px;
-        height: 1px;
-        background: var(--sf);
-        margin: .65rem auto;
-    }
-
-    .book-sub-text {
-        font-size: .65rem;
-        color: rgba(255, 255, 255, .5);
-        letter-spacing: .06em;
-        line-height: 1.5;
-    }
-
-    /* Hero right */
-    .hero-content {
-        flex: 1;
-        min-width: 0;
-    }
-
-    .hero-title {
-        font-family: 'Noto Serif SC', serif;
-        font-size: 1.75rem;
-        font-weight: 700;
-        color: #fff;
-        line-height: 1.3;
-        margin-bottom: .4rem;
-    }
-
-    .hero-subtitle {
-        font-size: .88rem;
-        color: rgba(255, 255, 255, .45);
-        font-style: italic;
-        letter-spacing: .04em;
-        margin-bottom: 1.1rem;
-    }
-
-    .hero-tags {
-        display: flex;
-        flex-wrap: wrap;
-        gap: .35rem;
-        margin-bottom: 1.3rem;
-    }
-
-    .hero-tag {
-        font-size: .72rem;
-        padding: 2px 9px;
-        border-radius: 20px;
-        background: rgba(200, 134, 10, .2);
-        color: var(--sf);
-        border: 1px solid rgba(200, 134, 10, .3);
-    }
-
-    .hero-info-row {
-        display: flex;
-        flex-wrap: wrap;
-        gap: 1.4rem;
-        margin-bottom: 1.3rem;
-    }
-
-    .hi-item {
-        display: flex;
-        align-items: center;
-        gap: .45rem;
-    }
-
-    .hi-label {
-        font-size: .72rem;
-        color: rgba(255, 255, 255, .4);
-        letter-spacing: .04em;
-        display: block;
-    }
-
-    .hi-value {
-        font-size: .83rem;
-        color: rgba(255, 255, 255, .82);
-        display: block;
-    }
-
-    .hi-avatar {
-        width: 26px;
-        height: 26px;
-        border-radius: 50%;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        font-size: .68rem;
-        font-weight: 700;
-        flex-shrink: 0;
-    }
-
-    .hero-desc {
-        font-size: .85rem;
-        color: rgba(255, 255, 255, .6);
-        line-height: 1.85;
-        margin-bottom: 1.6rem;
-        max-width: 600px;
-    }
-
-    .btn-read-primary {
-        background: var(--sf);
-        color: var(--ink);
-        font-weight: 700;
-        font-size: .88rem;
-        padding: .55rem 1.6rem;
-        border-radius: 6px;
-        border: none;
-        cursor: pointer;
-        text-decoration: none;
-        display: inline-flex;
-        align-items: center;
-        gap: .45rem;
-        transition: background .2s, transform .15s;
-    }
-
-    .btn-read-primary:hover {
-        background: #dea020;
-        color: var(--ink);
-        transform: translateY(-1px);
-    }
-
-    .btn-outline-hero {
-        background: transparent;
-        color: rgba(255, 255, 255, .7);
-        font-size: .85rem;
-        padding: .5rem 1.3rem;
-        border-radius: 6px;
-        border: 1px solid rgba(255, 255, 255, .2);
-        cursor: pointer;
-        text-decoration: none;
-        display: inline-flex;
-        align-items: center;
-        gap: .4rem;
-        transition: all .2s;
-        margin-left: .65rem;
-    }
-
-    .btn-outline-hero:hover {
-        border-color: rgba(255, 255, 255, .5);
-        color: #fff;
-    }
-
-    /* Section card */
-    .sec-card {
-        background: var(--card-bg);
-        border: 1px solid var(--bdr);
-        border-radius: 10px;
-        overflow: hidden;
-        margin-bottom: 1.3rem;
-    }
-
-    .sec-header {
-        padding: .85rem 1.4rem;
-        border-bottom: 1px solid var(--bdr);
-        display: flex;
-        align-items: center;
-        gap: .55rem;
-    }
-
-    .sec-bar {
-        width: 3px;
-        height: 15px;
-        background: var(--sf);
-        border-radius: 2px;
-        flex-shrink: 0;
-    }
-
-    .sec-title {
-        font-family: 'Noto Serif SC', serif;
-        font-size: .9rem;
-        font-weight: 600;
-        color: var(--ink-soft);
-        letter-spacing: .04em;
-    }
-
-    .sec-count {
-        margin-left: auto;
-        font-size: .75rem;
-        color: var(--ink-muted);
-        background: var(--sf-light);
-        padding: 2px 8px;
-        border-radius: 10px;
-    }
-
-    /* About */
-    .sec-body {
-        padding: 1.15rem 1.4rem;
-        font-size: .855rem;
-        color: var(--ink-soft);
-        line-height: 1.95;
-    }
-
-    .sec-body p {
-        margin-bottom: .8rem;
-    }
-
-    .sec-body p:last-child {
-        margin-bottom: 0;
-    }
-
-    /* TOC */
-    .toc-ul {
-        list-style: none;
-        padding: .35rem 0;
-        margin: 0;
-    }
-
-    .toc-ul li a {
-        display: flex;
-        align-items: center;
-        padding: .65rem 1.4rem;
-        text-decoration: none;
-        border-bottom: 1px solid rgba(232, 221, 208, .5);
-        transition: background .15s;
-    }
-
-    .toc-ul li:last-child a {
-        border-bottom: none;
-    }
-
-    .toc-ul li a:hover {
-        background: var(--sf-pale);
-    }
-
-    .toc-num {
-        font-size: .72rem;
-        color: var(--ink-muted);
-        width: 26px;
-        flex-shrink: 0;
-    }
-
-    .toc-name {
-        font-size: .855rem;
-        color: var(--ink-soft);
-        flex: 1;
-        line-height: 1.4;
-    }
-
-    .toc-ul li a:hover .toc-name {
-        color: var(--sf);
-    }
-
-    .toc-arrow {
-        color: var(--bdr);
-        font-size: .85rem;
-    }
-
-    .toc-ul li a:hover .toc-arrow {
-        color: var(--sf);
-    }
-
-    /* Sidebar */
-    .sb-card {
-        background: var(--card-bg);
-        border: 1px solid var(--bdr);
-        border-radius: 10px;
-        overflow: hidden;
-        margin-bottom: 1.15rem;
-    }
-
-    .sb-head {
-        padding: .8rem 1.2rem;
-        border-bottom: 1px solid var(--bdr);
-        font-family: 'Noto Serif SC', serif;
-        font-size: .875rem;
-        font-weight: 600;
-        color: var(--ink-soft);
-        letter-spacing: .04em;
-        display: flex;
-        align-items: center;
-        gap: .45rem;
-    }
-
-    .sb-head::before {
-        content: '';
-        display: block;
-        width: 3px;
-        height: 13px;
-        background: var(--sf);
-        border-radius: 2px;
-    }
-
-    .smeta-row {
-        display: flex;
-        padding: .7rem 1.2rem;
-        border-bottom: 1px solid var(--bdr);
-        font-size: .8rem;
-        align-items: flex-start;
-        gap: .45rem;
-    }
-
-    .smeta-row:last-child {
-        border-bottom: none;
-    }
-
-    .smeta-label {
-        color: var(--ink-muted);
-        min-width: 65px;
-        flex-shrink: 0;
-    }
-
-    .smeta-value {
-        color: var(--ink-soft);
-        font-weight: 500;
-    }
-
-    .smeta-value a {
-        color: var(--sf);
-        text-decoration: none;
-    }
-
-    .smeta-value a:hover {
-        text-decoration: underline;
-    }
-
-    /* Author card */
-    .author-block {
-        display: flex;
-        align-items: center;
-        gap: .8rem;
-        padding: 1.1rem 1.2rem;
-    }
-
-    .author-av-lg {
-        width: 48px;
-        height: 48px;
-        border-radius: 50%;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        font-size: .95rem;
-        font-weight: 700;
-        flex-shrink: 0;
-    }
-
-    .author-av-img {
-        width: 48px;
-        height: 48px;
-        border-radius: 50%;
-        object-fit: cover;
-        flex-shrink: 0;
-    }
-
-    .hi-avatar {
-        width: 26px;
-        height: 26px;
-        border-radius: 50%;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        font-size: .68rem;
-        font-weight: 700;
-        flex-shrink: 0;
-    }
-
-    .hi-avatar-img {
-        width: 26px;
-        height: 26px;
-        border-radius: 50%;
-        object-fit: cover;
-        flex-shrink: 0;
-    }
-
-    .author-block-name {
-        font-weight: 600;
-        font-size: .9rem;
-        color: var(--ink);
-        margin-bottom: .18rem;
-    }
-
-    .author-block-stats {
-        font-size: .75rem;
-        color: var(--ink-muted);
-    }
-
-    .author-bio {
-        font-size: .78rem;
-        color: var(--ink-muted);
-        line-height: 1.65;
-        padding: 0 1.2rem 1.1rem;
-        border-top: 1px solid var(--bdr);
-        padding-top: .9rem;
-    }
-
-    /* Related */
-    .related-ul {
-        list-style: none;
-        padding: 0;
-        margin: 0;
-    }
-
-    .related-ul li a {
-        display: flex;
-        align-items: center;
-        gap: .7rem;
-        padding: .7rem 1.2rem;
-        border-bottom: 1px solid var(--bdr);
-        text-decoration: none;
-        transition: background .15s;
-    }
-
-    .related-ul li:last-child a {
-        border-bottom: none;
-    }
-
-    .related-ul li a:hover {
-        background: var(--sf-pale);
-    }
-
-    .related-cover-mini {
-        width: 34px;
-        height: 46px;
-        border-radius: 2px 5px 5px 2px;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        font-size: .6rem;
-        color: rgba(255, 255, 255, .8);
-        font-family: 'Noto Serif SC', serif;
-        flex-shrink: 0;
-        text-align: center;
-        line-height: 1.3;
-    }
-
-    .related-t {
-        font-size: .8rem;
-        color: var(--ink-soft);
-        font-weight: 500;
-        margin-bottom: .18rem;
-        line-height: 1.3;
-    }
-
-    .related-ul li a:hover .related-t {
-        color: var(--sf);
-    }
-
-    .related-a {
-        font-size: .7rem;
-        color: var(--ink-muted);
-    }
-
-    @media (max-width: 900px) {
-        .hero-inner {
-            flex-direction: column;
-            align-items: center;
-        }
-
-        .book-cover-3d {
-            height: 170px;
-        }
-    }
-</style>
-@endpush
-@endonce
-
-@section('content')
-
-
-{{-- Hero --}}
+@section('hero')
 <div class="anthology-hero">
     <div class="container-xl">
         <div class="hero-inner">
 
-            {{-- 3D Book Cover --}}
-            <div class="book-cover-3d" style="{{ $anthology['cover_image'] ? '' : 'background: ' . $anthology['cover_gradient'] }}">
-                @if($anthology['cover_image'])
-                <img src="{{ $anthology['cover_image'] }}" alt="{{ $anthology['title'] }}">
-                @else
-                <div class="book-text-wrap">
-                    <div class="book-title-text">{{ $anthology['title'] }}</div>
-                    <div class="book-divider"></div>
-                    <div class="book-sub-text">{{ $anthology['subtitle'] ?? '' }}</div>
-                </div>
-                @endif
-            </div>
-
-            {{-- Content --}}
+            {{-- 3D 书籍封面 --}}
+            <x-ui.book-cover
+                :image="$anthology['cover_image'] ?? null"
+                :gradient="$anthology['cover_gradient']"
+                :title="$anthology['title']"
+                :subtitle="$anthology['subtitle'] ?? ''"
+                size="lg"
+                :style3d="true"
+            />
+
+            {{-- 文集信息 --}}
             <div class="hero-content">
                 <div class="hero-title">{{ $anthology['title'] }}</div>
+
                 @if(!empty($anthology['subtitle']))
                 <div class="hero-subtitle">{{ $anthology['subtitle'] }}</div>
                 @endif
@@ -593,13 +50,13 @@
 
                 <div class="hero-info-row">
                     <div class="hi-item">
-                        @if(!empty($anthology['author']['avatar']))
-                        <img src="{{ $anthology['author']['avatar'] }}" class="hi-avatar-img" alt="">
-                        @else
-                        <div class="hi-avatar" style="background: {{ $anthology['author']['color'] }}; color: #fff">
-                            {{ $anthology['author']['initials'] }}
-                        </div>
-                        @endif
+                        <x-ui.author-avatar
+                            :avatar="$anthology['author']['avatar'] ?? null"
+                            :color="$anthology['author']['color']"
+                            :initials="$anthology['author']['initials']"
+                            :name="$anthology['author']['name']"
+                            size="sm"
+                        />
                         <div>
                             <span class="hi-label">作者</span>
                             <span class="hi-value">{{ $anthology['author']['name'] }}</span>
@@ -631,12 +88,17 @@
 
                 <div>
                     @if(!empty($anthology['articles']))
-                    <a href="{{ route('library.anthology.read', ['anthology' => $anthology['id'], 'article' => $anthology['articles'][0]['id']]) }}" class="btn-read-primary">
+                    <a href="{{ route('library.anthology.read', [
+                            'anthology' => $anthology['id'],
+                            'article'   => $anthology['articles'][0]['id']
+                        ]) }}"
+                       class="btn-read-primary">
                         <i class="ti ti-book-2"></i>
                         在线阅读
                     </a>
                     @endif
-                    <a href="{{ config('mint.server.dashboard_base_path') }}/workspace/anthology/{{ $anthology['id'] }}" class="btn-outline-hero">
+                    <a href="{{ config('mint.server.dashboard_base_path') }}/workspace/anthology/{{ $anthology['id'] }}"
+                       class="btn-outline-hero">
                         <i class="ti ti-pencil"></i>
                         在编辑器中打开
                     </a>
@@ -646,16 +108,17 @@
         </div>
     </div>
 </div>
+@endsection
 
-{{-- Body --}}
-<div class="page-body" style="background: var(--sf-pale);">
+@section('content')
+<div class="page-body">
     <div class="container-xl">
         <div class="row mt-2">
 
-            {{-- Left --}}
+            {{-- 主内容 --}}
             <div class="col-lg-8">
 
-                {{-- About --}}
+                {{-- 关于本文集 --}}
                 @if(!empty($anthology['about']))
                 <div class="sec-card">
                     <div class="sec-header">
@@ -664,15 +127,15 @@
                     </div>
                     <div class="sec-body">
                         @foreach(explode("\n", $anthology['about']) as $para)
-                        @if(trim($para))
-                        <p>{{ trim($para) }}</p>
-                        @endif
+                            @if(trim($para))
+                            <p>{{ trim($para) }}</p>
+                            @endif
                         @endforeach
                     </div>
                 </div>
                 @endif
 
-                {{-- TOC --}}
+                {{-- 目录 --}}
                 <div class="sec-card">
                     <div class="sec-header">
                         <div class="sec-bar"></div>
@@ -682,7 +145,10 @@
                     <ul class="toc-ul">
                         @foreach($anthology['articles'] as $article)
                         <li>
-                            <a href="{{ route('library.anthology.read', ['anthology' => $anthology['id'], 'article' => $article['id']]) }}">
+                            <a href="{{ route('library.anthology.read', [
+                                    'anthology' => $anthology['id'],
+                                    'article'   => $article['id']
+                                ]) }}">
                                 <span class="toc-num">{{ str_pad($article['order'], 2, '0', STR_PAD_LEFT) }}</span>
                                 <span class="toc-name">{{ $article['title'] }}</span>
                                 <span class="toc-arrow">›</span>
@@ -692,17 +158,19 @@
                     </ul>
                 </div>
 
-            </div>{{-- /col --}}
+            </div>
 
-            {{-- Sidebar --}}
+            {{-- 侧边栏 --}}
             <div class="col-lg-4">
 
-                {{-- Meta --}}
+                {{-- 文集信息 --}}
                 <div class="sb-card">
                     <div class="sb-head">文集信息</div>
                     <div class="smeta-row">
                         <span class="smeta-label">作者</span>
-                        <span class="smeta-value"><a href="#">{{ $anthology['author']['name'] }}</a></span>
+                        <span class="smeta-value">
+                            <a href="#">{{ $anthology['author']['name'] }}</a>
+                        </span>
                     </div>
                     @if(!empty($anthology['language']))
                     <div class="smeta-row">
@@ -730,17 +198,16 @@
                     @endif
                 </div>
 
-                {{-- Author --}}
+                {{-- 作者 --}}
                 <div class="sb-card">
                     <div class="sb-head">作者</div>
                     <div class="author-block">
-                        @if(!empty($anthology['author']['avatar']))
-                        <img src="{{ $anthology['author']['avatar'] }}" class="author-av-img" alt="">
-                        @else
-                        <div class="author-av-lg" style="background: {{ $anthology['author']['color'] }}; color: #fff">
-                            {{ $anthology['author']['initials'] }}
-                        </div>
-                        @endif
+                        <x-ui.author-avatar
+                            :avatar="$anthology['author']['avatar'] ?? null"
+                            :color="$anthology['author']['color']"
+                            :initials="$anthology['author']['initials']"
+                            size="lg"
+                        />
                         <div>
                             <div class="author-block-name">{{ $anthology['author']['name'] }}</div>
                             <div class="author-block-stats">
@@ -755,7 +222,7 @@
                     @endif
                 </div>
 
-                {{-- Related --}}
+                {{-- 相关文集 --}}
                 @if($related->count())
                 <div class="sb-card">
                     <div class="sb-head">相关文集</div>
@@ -763,9 +230,13 @@
                         @foreach($related as $rel)
                         <li>
                             <a href="{{ route('library.anthology.show', $rel['id']) }}">
-                                <div class="related-cover-mini" style="background: {{ $rel['cover_gradient'] }}">
-                                    {{ mb_substr($rel['title'], 0, 4) }}
-                                </div>
+                                <x-ui.book-cover
+                                    :image="null"
+                                    :gradient="$rel['cover_gradient']"
+                                    :title="mb_substr($rel['title'], 0, 4)"
+                                    size="sm"
+                                    :style3d="false"
+                                />
                                 <div>
                                     <div class="related-t">{{ $rel['title'] }}</div>
                                     <div class="related-a">{{ $rel['author_name'] }}</div>

+ 28 - 0
api-v12/resources/views/library/book/_toc.blade.php

@@ -0,0 +1,28 @@
+{{-- resources/views/library/book/_toc.blade.php
+     TOC 列表局部视图,在 offcanvas 和侧边栏中复用。
+     变量:$toc array,$anthologyId(可选)
+--}}
+@if(!empty($toc))
+<ul>
+    @foreach ($toc as $item)
+    <li class="toc_item toc-level-{{ $item['level'] }}
+               {{ ($item['active'] ?? false) ? 'toc-active' : ($item['disabled'] ? 'toc-disabled' : '') }}">
+        @if($item['active'] ?? false)
+            <span title="{{ $item['title'] }}">{{ $item['title'] }}</span>
+        @elseif(!$item['disabled'])
+            @if(isset($anthologyId))
+                <a href="{{ route('library.anthology.read', ['anthology' => $anthologyId, 'article' => $item['id'], 'channel' => request('channel')]) }}"
+                   title="{{ $item['title'] }}">{{ $item['title'] }}</a>
+            @else
+                <a href="{{ route('library.tipitaka.read', ['id' => $item['id'], 'channel' => request('channel')]) }}"
+                   title="{{ $item['title'] }}">{{ $item['title'] }}</a>
+            @endif
+        @else
+            <span title="{{ $item['title'] }}">{{ $item['title'] }}</span>
+        @endif
+    </li>
+    @endforeach
+</ul>
+@else
+<div class="alert alert-warning">此书没有目录</div>
+@endif

+ 316 - 505
api-v12/resources/views/library/book/read.blade.php

@@ -1,578 +1,389 @@
-<!DOCTYPE html>
-<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
-
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{{ $book['title'] }}</title>
-    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/core@1.3.2/dist/css/tabler.min.css" />
-    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
-
-    @vite(['resources/css/reader.css', 'resources/js/app.js'])
-
-    <script src="https://cdn.jsdelivr.net/npm/@tabler/core@1.3.2/dist/js/tabler.min.js"></script>
-    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
-
-
-</head>
-
+{{-- resources/views/library/book/read.blade.php
+     全站共用阅读器。供 anthology/read 和 tipitaka/read 路由使用。
+     重构:改为 @extends('layouts.reader'),移除 CDN 引入,JS 提取为模块。
+--}}
+@extends('layouts.reader')
+
+@section('title', $book['title'])
+
+@section('body-class', session('theme', 'light') . '-mode')
+
+{{-- 术语抽屉(所有阅读页统一使用 wiki.term-drawer) --}}
+@push('scripts')
+@vite(['resources/css/tufte.css', 'resources/js/modules/term-tooltip.js'])
+@endpush
+
+@section('reader-content')
+
+{{-- 术语抽屉 --}}
+<x-wiki.term-drawer />
+
+{{-- Navbar --}}
+<header class="navbar navbar-expand-md navbar-light d-print-none">
+    <div class="container-xl">
+        <button class="navbar-toggler"
+            type="button"
+            data-bs-toggle="offcanvas"
+            data-bs-target="#tocDrawer"
+            aria-controls="tocDrawer">
+            <span class="navbar-toggler-icon"></span>
+        </button>
+
+        <div class="navbar-brand d-flex flex-column lh-1">
+            @if(!empty($book['anthology']))
+            <small class="text-muted" style="font-size:.75rem;">
+                <a href="{{ route('library.anthology.show', $book['anthology']['id']) }}"
+                    class="text-muted text-decoration-none">
+                    {{ $book['anthology']['title'] }}
+                </a>
+            </small>
+            @endif
+        </div>
 
-<body class="{{ session('theme', 'light') }}-mode">
-    <x-term-drawer />
+        <div class="navbar-nav flex-row order-md-last align-items-center">
 
-    <!-- Navbar -->
-    <header class="navbar navbar-expand-md navbar-light d-print-none">
-        <div class="container-xl">
-            <button class="navbar-toggler" type="button" data-bs-toggle="offcanvas" data-bs-target="#tocDrawer" aria-controls="tocDrawer">
-                <span class="navbar-toggler-icon"></span>
-            </button>
-            {{-- 面包屑:文集标题 > 文章标题 --}}
-            <div class="navbar-brand d-flex flex-column lh-1">
-                @if(!empty($book['anthology']))
-                <small class="text-muted" style="font-size:.75rem;">
-                    <a href="{{ route('library.anthology.show', $book['anthology']['id']) }}" class="text-muted text-decoration-none">
-                        {{ $book['anthology']['title'] }}
-                    </a>
-                </small>
-                @endif
+            {{-- 编辑器按钮 --}}
+            @if(!empty($editor_link))
+            <div class="nav-item me-2">
+                <a href="{{ $editor_link }}" target="_blank" class="nav-link">
+                    <i class="ti ti-pencil me-1 d-none d-md-inline"></i>
+                    <span class="d-none d-md-inline">编辑器</span>
+                    <i class="ti ti-pencil d-md-none"></i>
+                </a>
             </div>
-            <div class="navbar-nav flex-row order-md-last align-items-center">
-                {{-- Desktop 编辑器按钮 --}}
-                @if(!empty($editor_link))
-                <div class="nav-item d-none d-md-block me-2">
-                    <a href="{{ $editor_link }}"
-                        target="_blank"
-                        class="nav-link">
-                        <i class="fas fa-pen-to-square me-1"></i>
-                        编辑器
-                    </a>
-                </div>
-
-                {{-- Mobile 编辑器按钮 --}}
-                <div class="nav-item d-md-none me-2">
-                    <a href="{{ $editor_link }}"
-                        target="_blank"
-                        class="nav-link">
-                        <i class="fas fa-pen-to-square"></i>
-                    </a>
-                </div>
-                @endif
-
-                {{-- Desktop 设置按钮 --}}
-                <div class="nav-item d-none d-md-block me-2">
-                    <a href="#"
-                        class="nav-link"
-                        data-bs-toggle="modal"
-                        data-bs-target="#settingsModal">
-                        <i class="fas fa-cog me-1"></i>
-                        设置
-                    </a>
-                </div>
-
-                {{-- Mobile 设置按钮 --}}
-                <div class="nav-item d-md-none me-2">
-                    <a href="#"
-                        class="nav-link"
-                        data-bs-toggle="modal"
-                        data-bs-target="#settingsModal">
-                        <i class="fas fa-cog"></i>
-                    </a>
-                </div>
-                {{-- Desktop Dropdown --}}
-                @if(!empty($channels))
-                <div class="nav-item dropdown d-none d-md-block me-2">
-                    <a href="#" class="nav-link" data-bs-toggle="dropdown">
-                        <i class="fas fa-layer-group me-1"></i>
-                        版本
-                    </a>
-
-                    <div class="dropdown-menu dropdown-menu-end">
-                        @foreach($channels as $channel)
-                        <a class="dropdown-item"
-                            href="{{ request()->fullUrlWithQuery(['channel' => $channel['id']]) }}">
-                            {{ $channel['name'] }}
-                            <small class="text-muted">
-                                ({{ __('language.' . $channel['lang']) }})
-                            </small>
-                        </a>
-                        @endforeach
-                    </div>
-                </div>
+            @endif
 
-                {{-- Mobile Drawer Trigger --}}
-                <div class="nav-item d-md-none me-2">
-                    <a href="#" class="nav-link"
-                        data-bs-toggle="offcanvas"
-                        data-bs-target="#channelDrawer">
-                        <i class="fas fa-layer-group"></i>
-                    </a>
-                </div>
-                @endif
-                <div class="nav-item">
-                    <a href="#" class="nav-link" id="themeToggle">
-                        <i class="fas fa-moon"></i>
-                    </a>
-                </div>
-                @auth
-                <div class="nav-item dropdown">
-                    <a href="#" class="nav-link d-flex lh-1 text-reset p-0" data-bs-toggle="dropdown">
-                        <span class="avatar avatar-sm" style="background-image: url({{ auth()->user()->avatar ?? '' }})"></span>
-                    </a>
-                    <div class="dropdown-menu dropdown-menu-end">
-                        <a class="dropdown-item" href="#">Profile</a>
-                        <a class="dropdown-item" href="{{ route('logout') }}">Logout</a>
-                    </div>
-                </div>
-                @endauth
+            {{-- 设置 --}}
+            <div class="nav-item me-2">
+                <a href="#"
+                    class="nav-link"
+                    data-bs-toggle="modal"
+                    data-bs-target="#settingsModal">
+                    <i class="ti ti-settings me-1 d-none d-md-inline"></i>
+                    <span class="d-none d-md-inline">设置</span>
+                    <i class="ti ti-settings d-md-none"></i>
+                </a>
             </div>
-        </div>
-    </header>
-
-    <!-- TOC Drawer (Mobile) -->
-    @if(!empty($channels))
-    <div class="offcanvas offcanvas-end"
-        tabindex="-1"
-        id="channelDrawer">
-
-        <div class="offcanvas-header">
-            <h5 class="offcanvas-title">选择版本</h5>
-
-            <button type="button"
-                class="btn-close"
-                data-bs-dismiss="offcanvas">
-            </button>
-        </div>
-
-        <div class="offcanvas-body">
-
-            <div class="list-group list-group-flush">
-
-                @foreach($channels as $channel)
-
-                <a
-                    href="{{ request()->fullUrlWithQuery(['channel' => $channel['id']]) }}"
-                    class="list-group-item list-group-item-action">
-
-                    <div class="fw-bold">
-                        {{ $channel['name'] }}
-                    </div>
-
-                    <small class="text-muted">
-                        {{ __('language.' . $channel['lang']) }}
-                    </small>
 
+            {{-- 版本切换:desktop 版本在右侧边栏展示,mobile 触发 offcanvas --}}
+            @if(!empty($channels))
+            <div class="nav-item d-md-none me-2">
+                <a href="#" class="nav-link"
+                    data-bs-toggle="offcanvas"
+                    data-bs-target="#channelDrawer">
+                    <i class="ti ti-layers"></i>
                 </a>
+            </div>
+            @endif
 
-                @endforeach
+            {{-- 夜间模式 --}}
+            <div class="nav-item">
+                <a href="#" class="nav-link" id="themeToggle">
+                    <i class="ti ti-moon"></i>
+                </a>
+            </div>
 
+            @auth
+            <div class="nav-item dropdown">
+                <a href="#" class="nav-link d-flex lh-1 text-reset p-0"
+                    data-bs-toggle="dropdown">
+                    <span class="avatar avatar-sm"
+                        style="background-image: url({{ auth()->user()->avatar ?? '' }})">
+                    </span>
+                </a>
+                <div class="dropdown-menu dropdown-menu-end">
+                    <a class="dropdown-item" href="{{ route('logout') }}">退出</a>
+                </div>
             </div>
+            @endauth
 
         </div>
     </div>
-    @endif
-    <div class="offcanvas offcanvas-start" tabindex="-1" id="tocDrawer" aria-labelledby="tocDrawerLabel">
-        <div class="offcanvas-header">
-            <h5 class="offcanvas-title" id="tocDrawerLabel">目录</h5>
-            <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
-        </div>
-        <div class="offcanvas-body">
-            @if(isset($book['toc']) && count($book['toc']) > 0)
-            <ul>
-                @foreach ($book['toc'] as $item)
-                <li class="toc_item toc-level-{{ $item['level'] }} {{ $item['active'] ?? false ? 'toc-active' : ($item['disabled'] ? 'toc-disabled' : '') }}">
-                    @if($item['active'] ?? false)
-                    <span title="{{ $item['title'] }}">
-                        {{ $item['title'] }}
-                    </span>
-                    @elseif(!$item['disabled'])
-
-                    @if(isset($anthologyId))
-                    {{-- ✅ 修改1:使用文集阅读路由,传 anthology + article 两个参数 --}}
-                    <a href="{{ route('library.anthology.read', ['anthology' => $anthologyId, 'article' => $item['id'],'channel' => request('channel')]) }}" title="{{ $item['title'] }}">
-                        @else
-                        <a href="{{ route('library.tipitaka.read', ['id' => $item['id'] ,'channel' => request('channel')]) }}" title="{{ $item['title'] }}">
-                            @endif
-                            {{ $item['title'] }}
-                        </a>
-                        @else
-                        <span title="{{ $item['title'] }}">
-                            {{ $item['title'] }}
-                        </span>
-                        @endif
-                </li>
-                @endforeach
-            </ul>
-            @else
-            <div class="alert alert-warning">此书没有目录</div>
-            @endif
+</header>
+
+{{-- 版本 Offcanvas(mobile) --}}
+@if(!empty($channels))
+<div class="offcanvas offcanvas-end" tabindex="-1" id="channelDrawer">
+    <div class="offcanvas-header">
+        <h5 class="offcanvas-title">选择版本</h5>
+        <button type="button" class="btn-close" data-bs-dismiss="offcanvas"></button>
+    </div>
+    <div class="offcanvas-body">
+        <div class="list-group list-group-flush">
+            @foreach($channels as $channel)
+            <a href="{{ request()->fullUrlWithQuery(['channel' => $channel['id']]) }}"
+                class="list-group-item list-group-item-action">
+                <div class="fw-bold">{{ $channel['name'] }}</div>
+                <small class="text-muted">{{ __('language.' . $channel['lang']) }}</small>
+            </a>
+            @endforeach
         </div>
     </div>
+</div>
+@endif
+
+{{-- TOC Offcanvas(mobile) --}}
+<div class="offcanvas offcanvas-start" tabindex="-1" id="tocDrawer">
+    <div class="offcanvas-header">
+        <h5 class="offcanvas-title">目录</h5>
+        <button type="button" class="btn-close" data-bs-dismiss="offcanvas"></button>
+    </div>
+    <div class="offcanvas-body">
+        @include('library.book._toc', ['toc' => $book['toc'] ?? []])
+    </div>
+</div>
 
-    <!-- Main Content Area -->
+{{-- 主内容区 --}}
+<div class="container-xl">
     <div class="main-container">
 
-        <!-- TOC Sidebar (Tablet+) -->
+        {{-- TOC 侧边栏(tablet+) --}}
         <div class="toc-sidebar card">
             <div class="card-body">
                 <h5>目录</h5>
-                @if(isset($book['toc']) && count($book['toc']) > 0)
-                <ul>
-                    @foreach ($book['toc'] as $item)
-                    <li class="toc_item toc-level-{{ $item['level'] }} {{ $item['active'] ?? false ? 'toc-active' : ($item['disabled'] ? 'toc-disabled' : '') }}">
-                        @if($item['active'] ?? false)
-                        <span>{{ $item['title'] }}</span>
-                        @elseif(!$item['disabled'])
-                        {{-- --}}
-                        @if(isset($anthologyId))
-                        <a href="{{ route('library.anthology.read', ['anthology' => $anthologyId, 'article' => $item['id'],'channel' => request('channel')]) }}">
-                            @else
-                            <a href="{{ route('library.tipitaka.read', ['id' => $item['id'],'channel' => request('channel')]) }}">
-                                @endif
-                                {{ $item['title'] }}
-                            </a>
-                            @else
-                            <span>{{ $item['title'] }}</span>
-                            @endif
-                    </li>
-                    @endforeach
-                </ul>
-                @else
-                <div class="alert alert-warning">此书没有目录</div>
-                @endif
+                @include('library.book._toc', ['toc' => $book['toc'] ?? []])
             </div>
         </div>
 
-        <!-- Main Content -->
+        {{-- 正文 --}}
         <div class="content-area card">
             <div class="card-body">
 
-                <div>
-                    <h2>{{ $book['title'] }}</h2>
-                    <p>
-                        <strong>Author:</strong>
-                        <span>
-                            {{ $book['author'] }}
-                            @if(isset($book['publisher']))
-                            @
-                            <a href="{{ route('blog.index', ['user' => $book['publisher']->username]) }}">
-                                {{ $book['publisher']->nickname }}
-                            </a>
-                            @endif
-                        </span>
-                    </p>
-
-                    <div class="content">
-                        @if(isset($book['content']) && count($book['content']) > 0)
-                        @foreach ($book['content'] as $paragraph)
-                        <div id="para-{{ $paragraph['id'] }}">
-                            @foreach ($paragraph['text'] as $rows)
-                            <div style="display:flex;">
-                                @foreach ($rows as $col)
-                                <div style="flex:1;">
-                                    @if($paragraph['level'] < 8)
-                                        {{-- ✅ 修改3:{!! !!} 不转义,渲染 HTML 内容 --}}
-                                        <h{{ $paragraph['level'] }}>{!! $col !!}</h{{ $paragraph['level'] }}>
-                                        @else
-                                        <p>{!! $col !!}</p>
-                                        @endif
-                                </div>
-                                @endforeach
+                <h2>{{ $book['title'] }}</h2>
+                <p>
+                    <strong>作者:</strong>
+                    {{ $book['author'] }}
+                    @if(isset($book['publisher']))
+                    @ <a href="{{ route('blog.index', ['user' => $book['publisher']->username]) }}">
+                        {{ $book['publisher']->nickname }}
+                    </a>
+                    @endif
+                </p>
+
+                <div class="content">
+                    @if(isset($book['content']) && count($book['content']) > 0)
+                    @foreach ($book['content'] as $paragraph)
+                    <div id="para-{{ $paragraph['id'] }}">
+                        @foreach ($paragraph['text'] as $rows)
+                        <div style="display:flex;">
+                            @foreach ($rows as $col)
+                            <div style="flex:1;">
+                                @if($paragraph['level'] < 8)
+                                    <h{{ $paragraph['level'] }}>{!! $col !!}</h{{ $paragraph['level'] }}>
+                                    @else
+                                    <p>{!! $col !!}</p>
+                                    @endif
                             </div>
                             @endforeach
                         </div>
                         @endforeach
-                        @else
-                        <div>没有内容</div>
-                        @endif
                     </div>
+                    @endforeach
+                    @else
+                    <div>没有内容</div>
+                    @endif
                 </div>
 
-                <!-- Nav buttons -->
+                {{-- 上下翻页 --}}
                 <div class="mt-6 pt-6">
                     <ul class="pagination">
                         @if(!empty($book['pagination']['prev']))
                         <li class="page-item page-prev">
-                            {{-- ✅ 修改2:翻页也用文集路由 --}}
-                            @if(isset($anthologyId))
-                            <a class="page-link"
-                                href="{{ route('library.anthology.read', [
-                                        'anthology' => $anthologyId,
-                                        'article' => $book['pagination']['prev']['id'],
-                                        'channel' => request('channel')
-                                ]) }}">
-                                @else
-                                <a class="page-link" href="{{ route('library.tipitaka.read', [
-                                        'id' => $book['pagination']['prev']['id'],
-                                        'channel' => request('channel')
-                                ]) }}">
-                                    @endif
-                                    <div class="row align-items-center">
-                                        <div class="col-auto">
-                                            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-1">
-                                                <path d="M15 6l-6 6l6 6"></path>
-                                            </svg>
-                                        </div>
-                                        <div class="col">
-                                            <div class="page-item-subtitle">上一篇</div>
-                                            <div class="page-item-title">{{ $book['pagination']['prev']['title'] }}</div>
-                                        </div>
+                            <a class="page-link" href="{{ isset($anthologyId)
+                            ? route('library.anthology.read', ['anthology' => $anthologyId, 'article' => $book['pagination']['prev']['id'], 'channel' => request('channel')])
+                            : route('library.tipitaka.read', ['id' => $book['pagination']['prev']['id'], 'channel' => request('channel')]) }}">
+                                <div class="row align-items-center">
+                                    <div class="col-auto">
+                                        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                                            <path d="M15 6l-6 6l6 6"></path>
+                                        </svg>
+                                    </div>
+                                    <div class="col">
+                                        <div class="page-item-subtitle">上一篇</div>
+                                        <div class="page-item-title">{{ $book['pagination']['prev']['title'] }}</div>
                                     </div>
-                                </a>
+                                </div>
+                            </a>
                         </li>
                         @endif
                         @if(!empty($book['pagination']['next']))
                         <li class="page-item page-next">
-                            @if(isset($anthologyId))
-                            <a class="page-link" href="{{ route('library.anthology.read', ['anthology' => $anthologyId, 'article' => $book['pagination']['next']['id'],'channel' => request('channel')]) }}">
-                                @else
-                                <a class="page-link" href="{{ route('library.tipitaka.read', ['id' => $book['pagination']['next']['id'],'channel' => request('channel')]) }}">
-                                    @endif
-                                    <div class="row align-items-center">
-                                        <div class="col">
-                                            <div class="page-item-subtitle">下一篇</div>
-                                            <div class="page-item-title">{{ $book['pagination']['next']['title'] }}</div>
-                                        </div>
-                                        <div class="col-auto">
-                                            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-1">
-                                                <path d="M9 6l6 6l-6 6"></path>
-                                            </svg>
-                                        </div>
+                            <a class="page-link" href="{{ isset($anthologyId)
+                            ? route('library.anthology.read', ['anthology' => $anthologyId, 'article' => $book['pagination']['next']['id'], 'channel' => request('channel')])
+                            : route('library.tipitaka.read', ['id' => $book['pagination']['next']['id'], 'channel' => request('channel')]) }}">
+                                <div class="row align-items-center">
+                                    <div class="col">
+                                        <div class="page-item-subtitle">下一篇</div>
+                                        <div class="page-item-title">{{ $book['pagination']['next']['title'] }}</div>
+                                    </div>
+                                    <div class="col-auto">
+                                        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                                            <path d="M9 6l6 6l-6 6"></path>
+                                        </svg>
                                     </div>
-                                </a>
+                                </div>
+                            </a>
                         </li>
                         @endif
                     </ul>
                 </div>
 
-                <!-- Related Books -->
-                @if(!empty($relatedBooks))
-                <div class="related-books">
-                    <h3>Related Books</h3>
-                    <div class="row row-cards">
-                        @foreach ($relatedBooks as $relatedBook)
-                        <div class="col-md-4">
-                            <div class="card">
-                                <div class="card-img-container">
-                                    <img src="{{ $relatedBook['image'] }}" alt="{{ $relatedBook['title'] }}">
-                                </div>
-                                <div class="card-body">
-                                    <h5 class="card-title">{{ $relatedBook['title'] }}</h5>
-                                    <p class="card-text">{{ $relatedBook['description'] }}</p>
-                                    <a href="{{ $relatedBook['link'] }}" class="btn btn-primary">Read Now</a>
-                                </div>
-                            </div>
-                        </div>
-                        @endforeach
-                    </div>
-                </div>
-                @endif
-
             </div>
         </div>
 
-        <!-- Right Sidebar (Desktop) -->
-        <div class="right-sidebar card">
-            <div class="card-body">
-                @if(!empty($book['downloads']))
-                <h5>下载</h5>
-                <ul class="list-unstyled">
+        {{-- 右侧边栏 --}}
+        <div class="right-sidebar">
+
+            {{-- 版本卡片(desktop,wiki 侧边栏同款) --}}
+            @if(!empty($channels))
+            <div class="reader-channel-card">
+                <div class="reader-channel-title">版本</div>
+                <ul class="reader-channel-list">
+                    @foreach($channels as $channel)
+                    <li>
+                        <a href="{{ request()->fullUrlWithQuery(['channel' => $channel['id']]) }}"
+                            class="{{ request('channel') == $channel['id'] ? 'active' : '' }}">
+                            {{ $channel['name'] }}
+                            <span class="reader-channel-lang">{{ __('language.' . $channel['lang']) }}</span>
+                        </a>
+                    </li>
+                    @endforeach
+                </ul>
+            </div>
+            @endif
+
+            {{-- 下载 --}}
+            @if(!empty($book['downloads']))
+            <div class="reader-channel-card">
+                <div class="reader-channel-title">下载</div>
+                <ul class="list-unstyled mb-0">
                     @foreach ($book['downloads'] as $download)
                     <li>
                         <a href="{{ $download['url'] }}" class="btn btn-outline-primary mb-2 w-100">
-                            <i class="fas fa-download me-2"></i>{{ $download['format'] }}
+                            <i class="ti ti-download me-2"></i>{{ $download['format'] }}
                         </a>
                     </li>
                     @endforeach
                 </ul>
-                @endif
-
-                @if(!empty($book['categories']))
-                <h5>分类</h5>
-                @foreach ($book['categories'] as $category)
-                <span class="badge bg-blue text-blue-fg">{{ $category['name'] }}</span>
-                @endforeach
-                @endif
+            </div>
+            @endif
 
-                @if(!empty($book['tags']))
-                <h5>标签</h5>
+            {{-- 标签 --}}
+            @if(!empty($book['tags']))
+            <div class="reader-channel-card">
+                <div class="reader-channel-title">标签</div>
                 @foreach ($book['tags'] as $tag)
                 <span class="badge me-1">{{ $tag['name'] }}</span>
                 @endforeach
-                @endif
             </div>
+            @endif
+
         </div>
 
     </div>
-
-    <!-- Settings Modal -->
-    <div class="modal modal-blur fade" id="settingsModal" tabindex="-1">
-        <div class="modal-dialog">
-            <form id="settingsForm" class="modal-content">
-                <div class="modal-header">
-                    <h5 class="modal-title">阅读设置</h5>
-
-                    <button type="button"
-                        class="btn-close"
-                        data-bs-dismiss="modal"></button>
+</div>
+
+{{-- 阅读设置 Modal --}}
+<div class="modal modal-blur fade" id="settingsModal" tabindex="-1">
+    <div class="modal-dialog">
+        <form id="settingsForm" class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">阅读设置</h5>
+                <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
+            </div>
+            <div class="modal-body">
+                <div class="mb-4">
+                    <label class="form-label">显示原文</label>
+                    <label class="form-check form-switch">
+                        <input class="form-check-input" type="checkbox" id="showOrigin">
+                        <span class="form-check-label">开启/关闭原文显示</span>
+                    </label>
                 </div>
-
-                <div class="modal-body">
-
-                    {{-- 显示原文 --}}
-                    <div class="mb-4">
-                        <label class="form-label">显示原文</label>
-
-                        <label class="form-check form-switch">
-                            <input class="form-check-input"
-                                type="checkbox"
-                                id="showOrigin">
-
-                            <span class="form-check-label">
-                                开启/关闭原文显示
-                            </span>
-                        </label>
-                    </div>
-
-                    {{-- 界面语言 --}}
-                    <div class="mb-4">
-                        <label class="form-label">界面语言</label>
-
-                        <select class="form-select" id="uiLanguage">
-                            <option value="auto">自动</option>
-                            <option value="zh">简体中文</option>
-                            <option value="en">英文</option>
-                        </select>
-                    </div>
-
-                    {{-- 巴利文脚本 --}}
-                    <div class="mb-4">
-                        <label class="form-label">巴利文脚本</label>
-
-                        <select class="form-select" id="paliScript">
-                            <option value="auto">自动</option>
-                            <option value="roman">罗马</option>
-                            <option value="myanmar">缅文</option>
-                            <option value="thai">泰文</option>
-                        </select>
-                    </div>
-
+                <div class="mb-4">
+                    <label class="form-label">界面语言</label>
+                    <select class="form-select" id="uiLanguage">
+                        <option value="auto">自动</option>
+                        <option value="zh">简体中文</option>
+                        <option value="en">英文</option>
+                    </select>
                 </div>
-
-                <div class="modal-footer">
-
-                    <button type="button"
-                        class="btn btn-link"
-                        data-bs-dismiss="modal">
-                        取消
-                    </button>
-
-                    <button type="submit"
-                        class="btn btn-primary">
-                        确定
-                    </button>
-
+                <div class="mb-4">
+                    <label class="form-label">巴利文脚本</label>
+                    <select class="form-select" id="paliScript">
+                        <option value="auto">自动</option>
+                        <option value="roman">罗马</option>
+                        <option value="myanmar">缅文</option>
+                        <option value="thai">泰文</option>
+                    </select>
                 </div>
-            </form>
-        </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-link" data-bs-dismiss="modal">取消</button>
+                <button type="submit" class="btn btn-primary">确定</button>
+            </div>
+        </form>
     </div>
-
-    <script>
-        function toggleOriginDisplay(show) {
-            document.querySelectorAll('.origin').forEach(el => {
-                el.style.display = show ? 'unset' : 'none';
-            });
-        }
-
-        function setCookie(name, value, days = 365) {
-            let expires = "";
-            const date = new Date();
-
-            date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
-
-            expires = "; expires=" + date.toUTCString();
-
-            document.cookie =
-                name + "=" + encodeURIComponent(value) +
-                expires +
-                "; path=/";
-        }
-
-        function getCookie(name) {
-            const value = `; ${document.cookie}`;
-
-            const parts = value.split(`; ${name}=`);
-
-            if (parts.length === 2)
-                return decodeURIComponent(parts.pop().split(';').shift());
-
-            return null;
-        }
-
-        // 初始化加载 cookie
-        document.addEventListener('DOMContentLoaded', function() {
-
-            const showOrigin =
-                getCookie('show_origin') === 'true';
-
-            document.getElementById('showOrigin').checked =
-                showOrigin;
-
-            document.getElementById('uiLanguage').value =
-                getCookie('ui_language') || 'auto';
-
-            document.getElementById('paliScript').value =
-                getCookie('pali_script') || 'auto';
-
-            // 应用原文显示设置
-            toggleOriginDisplay(showOrigin);
+</div>
+
+@endsection
+
+@push('scripts')
+<script>
+    // 夜间模式
+    document.getElementById('themeToggle').addEventListener('click', function(e) {
+        e.preventDefault();
+        const isDark = document.body.classList.contains('dark-mode');
+        document.body.classList.toggle('dark-mode', !isDark);
+        document.body.classList.toggle('light-mode', isDark);
+        this.innerHTML = isDark ?
+            '<i class="ti ti-moon"></i>' :
+            '<i class="ti ti-sun"></i>';
+        fetch('{{ route("theme.toggle") }}', {
+            method: 'POST',
+            headers: {
+                'X-CSRF-TOKEN': '{{ csrf_token() }}',
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify({
+                theme: isDark ? 'light' : 'dark'
+            })
         });
-
-        // 提交保存
-        document.getElementById('settingsForm')
-            .addEventListener('submit', function(e) {
-
-                e.preventDefault();
-
-                setCookie(
-                    'show_origin',
-                    document.getElementById('showOrigin').checked
-                );
-
-                setCookie(
-                    'ui_language',
-                    document.getElementById('uiLanguage').value
-                );
-
-                setCookie(
-                    'pali_script',
-                    document.getElementById('paliScript').value
-                );
-                toggleOriginDisplay(showOrigin);
-                location.reload();
-            });
-    </script>
-
-    <script>
-        const themeToggle = document.getElementById('themeToggle');
-        themeToggle.addEventListener('click', (e) => {
-            e.preventDefault();
-            const isDark = document.body.classList.contains('dark-mode');
-            document.body.classList.toggle('dark-mode', !isDark);
-            document.body.classList.toggle('light-mode', isDark);
-            fetch('{{ route("theme.toggle") }}', {
-                method: 'POST',
-                headers: {
-                    'X-CSRF-TOKEN': '{{ csrf_token() }}',
-                    'Content-Type': 'application/json'
-                },
-                body: JSON.stringify({
-                    theme: isDark ? 'light' : 'dark'
-                })
-            });
-            themeToggle.innerHTML = isDark ? '<i class="fas fa-moon"></i>' : '<i class="fas fa-sun"></i>';
+    });
+
+    // 阅读设置
+    function getCookie(name) {
+        const value = `; ${document.cookie}`;
+        const parts = value.split(`; ${name}=`);
+        if (parts.length === 2) return decodeURIComponent(parts.pop().split(';').shift());
+        return null;
+    }
+
+    function setCookie(name, value, days = 365) {
+        const date = new Date();
+        date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
+        document.cookie = name + '=' + encodeURIComponent(value) + '; expires=' + date.toUTCString() + '; path=/';
+    }
+
+    function toggleOriginDisplay(show) {
+        document.querySelectorAll('.origin').forEach(el => {
+            el.style.display = show ? 'unset' : 'none';
         });
-    </script>
-
-</body>
-
-</html>
+    }
+
+    document.addEventListener('DOMContentLoaded', function() {
+        const showOrigin = getCookie('show_origin') === 'true';
+        document.getElementById('showOrigin').checked = showOrigin;
+        document.getElementById('uiLanguage').value = getCookie('ui_language') || 'auto';
+        document.getElementById('paliScript').value = getCookie('pali_script') || 'auto';
+        toggleOriginDisplay(showOrigin);
+    });
+
+    document.getElementById('settingsForm').addEventListener('submit', function(e) {
+        e.preventDefault();
+        setCookie('show_origin', document.getElementById('showOrigin').checked);
+        setCookie('ui_language', document.getElementById('uiLanguage').value);
+        setCookie('pali_script', document.getElementById('paliScript').value);
+        location.reload();
+    });
+</script>
+@endpush

+ 0 - 143
api-v12/resources/views/library/book/show.blade.php

@@ -1,143 +0,0 @@
-@extends('library.layouts.app')
-
-@section('title', $book['title'] . ' - 巴利书库')
-
-@section('content')
-<div class="page-body">
-    <style>
-        .line2 {
-            display: -webkit-box;
-            -webkit-line-clamp: 2;
-            /* 限制显示两行 */
-            -webkit-box-orient: vertical;
-            overflow: hidden;
-            text-overflow: ellipsis;
-            /* 超出部分显示省略号 */
-        }
-    </style>
-    <div class="container-xl">
-        <div class="page-header d-print-none">
-            <div class="row align-items-center">
-                <div class="col">
-                    <nav aria-label="breadcrumb">
-                        <ol class="breadcrumb">
-                            <li class="breadcrumb-item"><a href="{{ route('library.home') }}">{{ __('labels.home') }}</a></li>
-                            <li class="breadcrumb-item active">{{ $book['title'] }}</li>
-                        </ol>
-                    </nav>
-                </div>
-            </div>
-        </div>
-
-        <div class="row">
-            <div class="col-md-4">
-                <div class="card">
-                    <img src="{{ $book['cover'] }}" class="card-img-top" alt="{{ $book['title'] }}" style="max-height: 400px; width: fit-content;">
-                    <div class="card-body text-center">
-                        <a href="{{ route('library.tipitaka.read', $book['id']) }}" class="btn btn-primary btn-lg w-100 mb-2">
-                            <svg class="icon me-2" width="24" height="24">
-                                <use xlink:href="#tabler-book-2"></use>
-                            </svg>
-                            {{ __('buttons.online-read') }}
-                        </a>
-                        <button class="btn btn-outline-secondary w-100">
-                            <svg class="icon me-2" width="24" height="24">
-                                <use xlink:href="#tabler-download"></use>
-                            </svg>
-                            下载
-                        </button>
-                    </div>
-                </div>
-            </div>
-
-            <div class="col-md-8">
-                <div class="card">
-                    <div class="card-header">
-                        <h3 class="card-title">{{ $book['title'] }}</h3>
-                    </div>
-                    <div class="card-body">
-                        <div class="row mb-3">
-                            <div class="col-sm-3"><strong>作者:</strong></div>
-                            <div class="col-sm-9">{{ $book['author'] }}</div>
-                        </div>
-                        <div class="row mb-3">
-                            <div class="col-sm-3"><strong>出版:</strong></div>
-                            <div class="col-sm-9">
-                                <a href="{{ route('blog.index', ['user' => $book['publisher']->username]) }}">
-                                    {{ $book['publisher']->nickname }}
-                                </a>
-                            </div>
-                        </div>
-                        <div class="row mb-3">
-                            <div class="col-sm-3"><strong>语言:</strong></div>
-                            <div class="col-sm-9">{{ $book['language'] ?? '巴利语' }}</div>
-                        </div>
-                        <div class="row mb-3">
-                            <div class="col-sm-3"><strong>简介:</strong></div>
-                            <div class="col-sm-9">{{ $book['description'] }}</div>
-                        </div>
-                    </div>
-                </div>
-
-                @if(isset($book['contents']) && count($book['contents']) > 0)
-                <div class="card mt-3">
-                    <div class="card-header">
-                        <h3 class="card-title">目录</h3>
-                    </div>
-                    <div class="card-body">
-                        <div class="list-group">
-                            @foreach($book['contents'] as $chapter)
-                            <a href="{{ route('library.tipitaka.read', $chapter['id']) }}?channel={{ $chapter['channel'] }}"
-                                class="list-group-item list-group-item-action">
-                                <div class="d-flex w-100 justify-content-between">
-                                    <h4 class="mb-1">{{ $chapter['title'] }}</h4>
-                                    <div class="d-flex" style="width:150px;">
-                                        @if($chapter['progress']>0)
-                                        <div class="progress">
-                                            <div class="progress-bar" style="width: {{ $chapter['progress'] }}%"></div>
-                                        </div>
-                                        @else
-                                        <small>无数据</small>
-                                        @endif
-
-                                    </div>
-
-                                </div>
-                                @if(isset($chapter['summary']))
-                                <p class="mb-1 text-muted line2">{{ $chapter['summary'] }}</p>
-                                @endif
-                            </a>
-                            @endforeach
-                        </div>
-                    </div>
-                </div>
-                @endif
-
-                @if(count($otherVersions) > 0)
-                <div class="card mt-3">
-                    <div class="card-header">
-                        <h3 class="card-title">其他版本</h3>
-                    </div>
-                    <div class="card-body">
-                        <div class="row">
-                            @foreach($otherVersions as $version)
-                            <div class="col-md-6 mb-3">
-                                <div class="d-flex">
-                                    <img src="{{ $version['cover'] }}" class="me-3" style="width: 60px; height: 80px; object-fit: cover;" alt="{{ $version['title'] }}">
-                                    <div>
-                                        <h6><a href="{{ route('library.tipitaka.show', $version['id']) }}">{{ $version['title'] }}</a></h6>
-                                        <div class="text-muted small">{{ $version['author'] }}</div>
-                                        <div class="text-muted small">{{ $version['language'] ?? '巴利语' }}</div>
-                                    </div>
-                                </div>
-                            </div>
-                            @endforeach
-                        </div>
-                    </div>
-                </div>
-                @endif
-            </div>
-        </div>
-    </div>
-</div>
-@endsection

+ 0 - 62
api-v12/resources/views/library/category.blade.php

@@ -1,62 +0,0 @@
-@extends('library.layouts.app')
-
-@section('title', $currentCategory['name'] . ' - 巴利书库')
-
-@section('breadcrumb')
-<li class="breadcrumb-item">
-    <a href="{{ route('library.home') }}">首页</a>
-</li>
-
-@foreach($breadcrumbs as $breadcrumb)
-@if($loop->last)
-<li class="breadcrumb-item active">{{ $breadcrumb['name'] }}</li>
-@else
-<li class="breadcrumb-item">
-    <a href="{{ route('library.category.show', $breadcrumb['id']) }}">{{ $breadcrumb['name'] }}</a>
-</li>
-@endif
-@endforeach
-@endsection
-
-@section('content')
-<div class="page-body">
-    <div class="container-xl">
-        <div class="page-header d-print-none">
-            <div class="row align-items-center">
-                <div class="col">
-
-                    <h2 class="page-title">{{ $currentCategory['name'] }}</h2>
-                </div>
-            </div>
-        </div>
-
-        @if(count($subCategories) > 0)
-        <div class="row row-cards mb-4">
-            @foreach($subCategories as $subCategory)
-            <div class="col-sm-6 col-lg-3">
-                <div class="card">
-                    <div class="card-body text-center">
-                        <a href="{{ route('library.category.show', $subCategory['id']) }}" class="btn btn-primary">
-                            {{ $subCategory['name'] }}
-                        </a>
-                    </div>
-                </div>
-            </div>
-            @endforeach
-        </div>
-        @endif
-
-
-
-        <div class="card">
-            <div class="card-header">
-                <h3 class="card-title">图书列表</h3>
-            </div>
-            <div class="card-body">
-                @include('components.book-list', ['books' => $categoryBooks])
-            </div>
-        </div>
-
-    </div>
-</div>
-@endsection

+ 154 - 65
api-v12/resources/views/library/index.blade.php

@@ -1,96 +1,185 @@
-{{-- api-v12/resources/views/library/index.blade.php --}}
-@extends('library.layouts.app')
+{{-- resources/views/library/index.blade.php
+     Library 门户首页。
+     区块:Hero → 三藏分类 → 最新译文 → 栏目导航
+--}}
+@extends('layouts.library')
 
-@section('title', __('labels.home'))
+@section('title', 'WikiPāli · 巴利书库')
 
-@section('hero')
-<section class="hero-section">
+@push('styles')
+    @vite('resources/css/modules/_library-index.css')
+@endpush
 
+{{-- Hero --}}
+@section('hero')
+<section class="hero-section"
+         style="background-image: url('{{ URL::asset('assets/images/hero-2.jpg') }}')">
     <div class="hero-overlay"></div>
-
     <div class="hero-content">
-        <h1 class="hero-title">巴利书库</h1>
-        <p class="hero-subtitle">探索wikipali,开启智慧之门</p>
-
+        <h1 class="hero-title">WikiPāli 巴利书库</h1>
+        <p class="hero-subtitle">探索巴利三藏 · 开启智慧之门</p>
         <div class="search-box">
-            <div class="input-group">
-                <input
-                    type="text"
-                    class="form-control form-control-lg"
-                    placeholder="搜索图书、作者或主题...">
-
-                <button class="btn btn-primary btn-lg" type="button">
-                    <i class="ti ti-search"></i>
-                </button>
-            </div>
+            <x-ui.search-input
+                :action="route('library.search')"
+                placeholder="搜索经典、词条、文集…"
+                size="lg"
+            />
         </div>
     </div>
-
 </section>
 @endsection
 
-
 @section('content')
-
 <div class="page-body">
     <div class="container-xl">
-        <div class="page-header d-print-none">
-            <div class="row align-items-center">
-                <div class="col">
-                    <h2 class="page-title">巴利书库</h2>
-                    <div class="text-muted mt-1">探索古老的佛教经典</div>
-                </div>
+
+        {{-- ── 一、三藏分类卡片 ── --}}
+        <div class="lib-section">
+            <div class="lib-section__header">
+                <h2 class="lib-section__title">
+                    <i class="ti ti-books"></i>
+                    巴利三藏
+                </h2>
+                <a href="{{ route('library.tipitaka.index') }}"
+                   class="lib-section__more">
+                    进入三藏 <i class="ti ti-arrow-right"></i>
+                </a>
             </div>
-        </div>
 
-        {{-- ✅ 所有卡片的统一容器 --}}
-        <div class="row g-4">
-            @foreach($categoryData as $data)
-            {{-- ✅ 响应式列 --}}
-            <div class="col-12 col-md-6 col-lg-3">
-                <div class="card h-100">
-                    <div class="card-header">
-                        <h3 class="card-title">
-                            <svg class="icon me-2" width="24" height="24">
-                                <use xlink:href="#tabler-book"></use>
-                            </svg>
-                            {{ $data['category']['name'] }}
-                        </h3>
-                        <div class="card-actions">
-                            <a href="{{ route('library.category.show', $data['category']['id']) }}"
-                                class="btn btn-primary btn-sm">
-                                {{ __('buttons.more') }}
-                                <svg class="icon ms-1" width="24" height="24">
-                                    <use xlink:href="#tabler-arrow-right"></use>
-                                </svg>
+            <div class="row g-3">
+                @foreach($categoryData as $data)
+                <div class="col-6 col-md-3">
+                    <div class="wiki-card h-100">
+                        <div class="lib-cat-card__head">
+                            <span class="lib-cat-card__name">
+                                {{ $data['category']['name'] }}
+                            </span>
+                            <a href="{{ route('library.tipitaka.category', ['id' => $data['category']['id']]) }}"
+                               class="lib-cat-card__more">
+                                更多 <i class="ti ti-arrow-right"></i>
                             </a>
                         </div>
+                        <ul class="wiki-cat-list">
+                            @foreach($data['children'] as $child)
+                            <li>
+                                <a href="{{ route('library.tipitaka.category', ['id' => $child['id']]) }}">
+                                    {{ $child['name'] }}
+                                </a>
+                            </li>
+                            @endforeach
+                        </ul>
                     </div>
+                </div>
+                @endforeach
+            </div>
+        </div>
 
-                    <div class="card-body">
-                        @foreach($data['children'] as $child)
-                        <div class="mb-1">
-                            <a href="{{ route('library.category.show', $child['id']) }}">
-                                {{ $child['name'] }}
-                            </a>
+        {{-- ── 二、最新译文 ── --}}
+        @isset($recentBooks)
+        <div class="lib-section">
+            <div class="lib-section__header">
+                <h2 class="lib-section__title">
+                    <i class="ti ti-clock"></i>
+                    最新译文
+                    <span class="lib-live-badge">
+                        <span class="lib-live-dot"></span>
+                        持续更新中
+                    </span>
+                </h2>
+                <a href="{{ route('library.tipitaka.index') }}"
+                   class="lib-section__more">
+                    查看全部 <i class="ti ti-arrow-right"></i>
+                </a>
+            </div>
+
+            <div class="wiki-card lib-recent">
+                @foreach($recentBooks as $book)
+                <a href="{{ route('library.tipitaka.show', $book['id']) }}"
+                   class="lib-recent__item">
+
+                    {{-- 封面缩略图 --}}
+                    <x-ui.book-cover
+                        :image="$book['cover'] ?? null"
+                        :gradient="$book['cover_gradient'] ?? 'linear-gradient(135deg,#2d2010,#1a1208)'"
+                        :title="$book['title']"
+                        size="sm"
+                        :style3d="false"
+                    />
+
+                    {{-- 信息 --}}
+                    <div class="lib-recent__info">
+                        <div class="lib-recent__title">{{ $book['title'] }}</div>
+                        <div class="lib-recent__meta">
+                            <span class="lib-recent__category">{{ $book['category'] }}</span>
+                            <span class="lib-recent__sep">·</span>
+                            <span class="lib-recent__author">{{ $book['author'] }}</span>
                         </div>
-                        @endforeach
                     </div>
-                </div>
+
+                    {{-- 右侧:标签 + 时间 --}}
+                    <div class="lib-recent__right">
+                        @if($book['is_new'])
+                        <span class="lib-new-badge">新增</span>
+                        @else
+                        <span class="lib-update-badge">更新</span>
+                        @endif
+                        <span class="lib-recent__time">{{ $book['updated_at'] }}</span>
+                    </div>
+
+                </a>
+                @endforeach
             </div>
-            @endforeach
         </div>
+        @endisset
 
-        <div class="card h-100">
-            <div class="card-header">
-                <h3 class="card-title">
-                    update
-                </h3>
+        {{-- ── 三、栏目导航 ── --}}
+        <div class="lib-section">
+            <div class="lib-section__header">
+                <h2 class="lib-section__title">
+                    <i class="ti ti-layout-grid"></i>
+                    全部栏目
+                </h2>
             </div>
-            <div class="card-body">
-                @include('components.book-list', ['books' => $update])
+
+            <div class="row g-3">
+                <div class="col-6 col-sm-4 col-md">
+                    <a href="{{ route('library.tipitaka.index') }}" class="lib-nav-card">
+                        <i class="ti ti-books lib-nav-card__icon"></i>
+                        <div class="lib-nav-card__name">三藏</div>
+                        <div class="lib-nav-card__desc">巴利文原典及译文</div>
+                    </a>
+                </div>
+                <div class="col-6 col-sm-4 col-md">
+                    <a href="{{ route('library.wiki.home') }}" class="lib-nav-card">
+                        <i class="ti ti-world lib-nav-card__icon"></i>
+                        <div class="lib-nav-card__name">百科</div>
+                        <div class="lib-nav-card__desc">佛法术语词典</div>
+                    </a>
+                </div>
+                <div class="col-6 col-sm-4 col-md">
+                    <a href="{{ route('library.anthology.index') }}" class="lib-nav-card">
+                        <i class="ti ti-notebook lib-nav-card__icon"></i>
+                        <div class="lib-nav-card__name">文集</div>
+                        <div class="lib-nav-card__desc">法义探讨与注疏</div>
+                    </a>
+                </div>
+                <div class="col-6 col-sm-4 col-md">
+                    <a href="{{ route('library.course') }}" class="lib-nav-card">
+                        <i class="ti ti-school lib-nav-card__icon"></i>
+                        <div class="lib-nav-card__name">课程</div>
+                        <div class="lib-nav-card__desc">系统学习路径</div>
+                    </a>
+                </div>
+                <div class="col-6 col-sm-4 col-md">
+                    <a href="{{ route('library.download') }}" class="lib-nav-card">
+                        <i class="ti ti-download lib-nav-card__icon"></i>
+                        <div class="lib-nav-card__name">下载</div>
+                        <div class="lib-nav-card__desc">离线阅读资源</div>
+                    </a>
+                </div>
             </div>
         </div>
+
     </div>
 </div>
 @endsection

+ 4 - 9
api-v12/resources/views/library/layouts/app.blade.php

@@ -11,20 +11,15 @@
 
     @vite(['resources/css/main.css', 'resources/js/app.js'])
 
-    <link href="https://cdn.jsdelivr.net/npm/@tabler/core@latest/dist/css/tabler.min.css" rel="stylesheet" />
-    <link href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css" rel="stylesheet" />
-
-    <script src="https://cdn.jsdelivr.net/npm/@tabler/core@1.3.2/dist/js/tabler.min.js"></script>
-    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
-
 </head>
 
 <body>
     <div class="page">
 
-        <x-library.header />
-
-        @yield('hero')
+        <div class="hero-wrapper">
+            <x-library.header />
+            @yield('hero')
+        </div>
 
         <div class="page-wrapper">
             @yield('content')

+ 112 - 0
api-v12/resources/views/library/search.blade.php

@@ -0,0 +1,112 @@
+{{-- resources/views/library/search.blade.php
+     全站统一搜索结果页。
+     路由:/library/search?q=&type=&category=&lang=
+     原 wiki/search.blade.php 移至此处,@extends 路径更新,
+     搜索框改用 <x-ui.search-input>,空状态改用 <x-ui.empty-state>。
+--}}
+@extends('library.wiki.layouts.app')
+
+@section('title', $query ? '"' . $query . '" 的搜索结果 · WikiPāli' : '搜索 · WikiPāli')
+
+@section('wiki-content')
+
+{{-- 搜索框 --}}
+<div class="wiki-search-bar-wrap">
+    <x-ui.search-input
+        :action="route('library.search')"
+        :value="$query"
+        placeholder="搜索条目、巴利文、梵文…"
+        :autofocus="true"
+        :hidden-fields="array_filter(['category' => $category !== 'all' ? $category : null])" />
+</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' ? $category : null,
+            ])" />
+    @endif
+
+@else
+
+    <div class="wiki-card">
+        <x-ui.empty-state
+            title="未找到相关条目"
+            desc="请尝试其他关键词" />
+    </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' => $query]) }}"
+               class="{{ $category === 'all' ? 'active' : '' }}">
+                全部
+                <span class="wiki-cat-count">{{ $pagination['total'] }}</span>
+            </a>
+        </li>
+        @isset($filters)
+            @foreach ($filters as $cat)
+            <li>
+                <a href="{{ route('library.search', ['q' => $query, 'category' => $cat->key]) }}"
+                   class="{{ $category === $cat->key ? 'active' : '' }}">
+                    {{ $cat->key }}
+                    <span class="wiki-cat-count">{{ $cat->doc_count }}</span>
+                </a>
+            </li>
+            @endforeach
+        @endisset
+    </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', ['q' => substr($query, 0, -1), 'lang' => $lang]) }}">
+                {{ substr($query, 0, -1) }}
+            </a>
+        </li>
+    </ul>
+</div>
+@endif
+
+@endsection

+ 239 - 0
api-v12/resources/views/library/tipitaka/category.blade.php

@@ -0,0 +1,239 @@
+{{-- resources/views/library/tipitaka/category.blade.php
+     Tipitaka 分类页 / 首页($currentCategory['id'] 为 null 时是首页)
+     三栏:左边栏(大分类)/ 中间(子分类+过滤器+书籍列表)/ 右边栏(推荐+译者)
+--}}
+@extends('layouts.library')
+
+@section('title', $currentCategory['name'] . ' · 巴利书库')
+
+@push('styles')
+@vite('resources/css/modules/_tipitaka.css')
+@endpush
+
+@section('breadcrumb')
+<li class="breadcrumb-item">
+    <a href="{{ route('library.home') }}">首页</a>
+</li>
+@if($currentCategory['id'])
+<li class="breadcrumb-item">
+    <a href="{{ route('library.tipitaka.index') }}">三藏</a>
+</li>
+@foreach($breadcrumbs as $breadcrumb)
+@if($loop->last)
+<li class="breadcrumb-item active">{{ $breadcrumb['name'] }}</li>
+@else
+<li class="breadcrumb-item">
+    <a href="{{ route('library.tipitaka.category', ['id' => $breadcrumb['id']]) }}">
+        {{ $breadcrumb['name'] }}
+    </a>
+</li>
+@endif
+@endforeach
+@else
+<li class="breadcrumb-item active">三藏</li>
+@endif
+@endsection
+
+@section('content')
+<div class="page-body">
+    <div class="container-xl wiki-layout">
+
+        {{-- ══ 左边栏:大分类导航 ══ --}}
+        <aside class="wiki-sidebar-left">
+
+
+
+            {{-- 大分类 --}}
+            <div class="wiki-sidebar-section">
+                <div class="wiki-sidebar-title">分类</div>
+                <ul class="wiki-cat-list">
+                    <li>
+                        <a href="{{ route('library.tipitaka.index') }}"
+                            class="{{ !$currentCategory['id'] ? 'active' : '' }}">
+                            全部
+                        </a>
+                    </li>
+                    @foreach($types as $type)
+                    <li>
+                        <a href="{{ route('library.tipitaka.category', ['id' => $type['id']]) }}"
+                            class="{{ ($currentCategory['id'] ?? null) == $type['id'] ? 'active' : '' }}">
+                            {{ $type['name'] }}
+                        </a>
+                    </li>
+                    @endforeach
+                </ul>
+            </div>
+
+        </aside>
+
+        {{-- ══ 主内容 ══ --}}
+        <main class="wiki-main">
+
+            {{-- 搜索框 --}}
+            <div>
+                <x-ui.search-input
+                    :action="route('library.search')"
+                    :value="request('q')"
+                    placeholder="搜索三藏原文译文"
+                    :hidden-fields="['type' => 'tipitaka']" />
+            </div>
+
+            {{-- 1. 子分类 --}}
+            @if(count($subCategories) > 0)
+            <div class="wiki-card tipitaka-filters">
+                <div class="tipitaka-filter-row">
+                    <span class="tipitaka-filter-label">子分类</span>
+                    <div class="tipitaka-filter-pills">
+                        @foreach($subCategories as $sub)
+                        <a href="{{ route('library.tipitaka.category', ['id' => $sub['id']]) }}"
+                            class="tipitaka-pill">
+                            <div class="wiki-featured-title">{{ $sub['name'] }}</div>
+                        </a>
+                        @endforeach
+                    </div>
+                </div>
+            </div>
+            @endif
+
+            {{-- 2. 过滤器区 --}}
+            <div class="wiki-card tipitaka-filters">
+
+                {{-- 类型 --}}
+                <div class="tipitaka-filter-row">
+                    <span class="tipitaka-filter-label">类型</span>
+                    <div class="tipitaka-filter-pills">
+                        @foreach($filterOptions['types'] as $opt)
+                        <a href="{{ request()->fullUrlWithQuery(['type' => $opt['value'], 'page' => null]) }}"
+                            class="tipitaka-pill {{ $selected['type'] === $opt['value'] ? 'tipitaka-pill--active' : '' }}">
+                            {{ $opt['label'] }}
+                            @if($opt['count'] > 0)
+                            <span class="tipitaka-pill-count">{{ $opt['count'] }}</span>
+                            @endif
+                        </a>
+                        @endforeach
+                    </div>
+                </div>
+
+                {{-- 语言 --}}
+                <div class="tipitaka-filter-row">
+                    <span class="tipitaka-filter-label">语言</span>
+                    <div class="tipitaka-filter-pills">
+                        @foreach($filterOptions['languages'] as $opt)
+                        <a href="{{ request()->fullUrlWithQuery(['lang' => $opt['value'], 'page' => null]) }}"
+                            class="tipitaka-pill {{ $selected['lang'] === $opt['value'] ? 'tipitaka-pill--active' : '' }}">
+                            {{ $opt['label'] }}
+                            @if($opt['count'] > 0)
+                            <span class="tipitaka-pill-count">{{ $opt['count'] }}</span>
+                            @endif
+                        </a>
+                        @endforeach
+                    </div>
+                </div>
+
+                {{-- 作者 --}}
+                <div class="tipitaka-filter-row">
+                    <span class="tipitaka-filter-label">作者</span>
+                    <select class="form-select form-select-sm tipitaka-author-select"
+                        onchange="window.location=this.value">
+                        @foreach($filterOptions['authors'] as $opt)
+                        <option value="{{ request()->fullUrlWithQuery(['author' => $opt['value'], 'page' => null]) }}"
+                            {{ $selected['author'] === $opt['value'] ? 'selected' : '' }}>
+                            {{ $opt['label'] }}@if($opt['count'] > 0)({{ $opt['count'] }})@endif
+                        </option>
+                        @endforeach
+                    </select>
+                </div>
+
+                {{-- 清除过滤器 --}}
+                @php
+                $hasFilter = $selected['type'] !== 'all'
+                || $selected['lang'] !== 'all'
+                || $selected['author'] !== 'all';
+                @endphp
+                @if($hasFilter)
+                <div class="tipitaka-filter-clear">
+                    <a href="{{ request()->fullUrlWithQuery(['type' => null, 'lang' => null, 'author' => null, 'page' => null]) }}"
+                        class="tipitaka-clear-btn">
+                        <i class="ti ti-x"></i> 清除过滤器
+                    </a>
+                </div>
+                @endif
+
+            </div>
+
+            {{-- 3. 排序 + 结果数 --}}
+            <div class="tipitaka-sort-bar">
+                <span class="tipitaka-sort-bar__count">
+                    共 <strong>{{ $totalCount }}</strong> 本
+                </span>
+                <div class="tipitaka-sort-bar__right">
+                    <span class="tipitaka-sort-bar__label">排序</span>
+                    <select class="form-select form-select-sm tipitaka-sort-select"
+                        onchange="window.location=this.value">
+                        <option value="{{ request()->fullUrlWithQuery(['sort' => 'updated_at']) }}"
+                            {{ $selected['sort'] === 'updated_at' ? 'selected' : '' }}>最新</option>
+                        <option value="{{ request()->fullUrlWithQuery(['sort' => 'title']) }}"
+                            {{ $selected['sort'] === 'title' ? 'selected' : '' }}>标题</option>
+                        <option value="{{ request()->fullUrlWithQuery(['sort' => 'author']) }}"
+                            {{ $selected['sort'] === 'author' ? 'selected' : '' }}>作者</option>
+                    </select>
+                </div>
+            </div>
+
+            {{-- 4. 书籍列表 --}}
+            <div class="wiki-card">
+                <x-ui.book-grid :books="$categoryBooks" />
+            </div>
+
+        </main>
+
+        {{-- ══ 右边栏 ══ --}}
+        <aside class="wiki-sidebar-right">
+
+            {{-- 本周推荐 --}}
+            @if(!empty($recommended))
+            <div class="wiki-sidebar-section">
+                <div class="wiki-sidebar-title">本周推荐</div>
+                <ul class="wiki-cat-list">
+                    @foreach($recommended as $item)
+                    <li>
+                        <a href="{{ route('library.tipitaka.show', $item['id']) }}">
+                            {{ $item['title'] }}
+                            <span class="wiki-cat-count">{{ $item['category'] }}</span>
+                        </a>
+                    </li>
+                    @endforeach
+                </ul>
+            </div>
+            @endif
+
+            {{-- 活跃译者 --}}
+            @if(!empty($activeAuthors))
+            <div class="wiki-sidebar-section">
+                <div class="wiki-sidebar-title">活跃译者</div>
+                <ul class="tipitaka-author-list">
+                    @foreach($activeAuthors as $author)
+                    <li>
+                        <a href="{{ request()->fullUrlWithQuery(['author' => \Str::slug($author['name']), 'page' => null]) }}"
+                            class="tipitaka-author-item">
+                            <x-ui.author-avatar
+                                :avatar="$author['avatar'] ?? null"
+                                :color="$author['color']"
+                                :initials="$author['initials']"
+                                size="sm" />
+                            <div class="tipitaka-author-item__info">
+                                <span class="tipitaka-author-item__name">{{ $author['name'] }}</span>
+                                <span class="tipitaka-author-item__count">{{ $author['count'] }} 本</span>
+                            </div>
+                        </a>
+                    </li>
+                    @endforeach
+                </ul>
+            </div>
+            @endif
+
+        </aside>
+
+    </div>
+</div>
+@endsection

+ 67 - 0
api-v12/resources/views/library/tipitaka/index.blade.php

@@ -0,0 +1,67 @@
+{{-- resources/views/library/index.blade.php
+     Library 门户首页。
+--}}
+@extends('layouts.library')
+
+@section('title', '巴利书库')
+
+@section('hero')
+<section class="hero-section"
+         style="background-image: url('{{ URL::asset('assets/images/hero-2.jpg') }}')">
+    <div class="hero-overlay"></div>
+    <div class="hero-content">
+        <h1 class="hero-title">巴利书库</h1>
+        <p class="hero-subtitle">探索 WikiPāli,开启智慧之门</p>
+        <div class="search-box">
+            <x-ui.search-input
+                :action="route('library.search')"
+                placeholder="搜索图书、作者或主题…"
+                size="lg"
+            />
+        </div>
+    </div>
+</section>
+@endsection
+
+@section('content')
+<div class="page-body">
+    <div class="container-xl">
+
+        {{-- 分类卡片 --}}
+        <div class="row g-4 mt-1">
+            @foreach($categoryData as $data)
+            <div class="col-12 col-md-6 col-lg-3">
+                <div class="wiki-card h-100">
+                    <div class="wiki-sidebar-title" style="margin-bottom: 0.75rem;">
+                        <i class="ti ti-book me-1"></i>
+                        {{ $data['category']['name'] }}
+                        <a href="{{ route('library.tipitaka.category', ['id' => $data['category']['id']]) }}"
+                           class="btn btn-sm btn-primary ms-auto" style="float:right; font-size: 0.75rem;">
+                            更多 <i class="ti ti-arrow-right"></i>
+                        </a>
+                    </div>
+                    <ul class="wiki-cat-list">
+                        @foreach($data['children'] as $child)
+                        <li>
+                            <a href="{{ route('library.tipitaka.category', ['id' => $child['id']]) }}">
+                                {{ $child['name'] }}
+                            </a>
+                        </li>
+                        @endforeach
+                    </ul>
+                </div>
+            </div>
+            @endforeach
+        </div>
+
+        {{-- 最近更新 --}}
+        @isset($update)
+        <div class="wiki-card mt-4">
+            <div class="wiki-sidebar-title" style="margin-bottom: 1rem;">最近更新</div>
+            <x-ui.book-grid :books="$update" />
+        </div>
+        @endisset
+
+    </div>
+</div>
+@endsection

+ 165 - 0
api-v12/resources/views/library/tipitaka/show.blade.php

@@ -0,0 +1,165 @@
+{{-- resources/views/library/tipitaka/show.blade.php --}}
+@extends('layouts.library')
+
+@section('title', $book['title'] . ' · 巴利书库')
+
+@push('styles')
+@vite('resources/css/modules/_tipitaka.css')
+@endpush
+
+@section('breadcrumb')
+<li class="breadcrumb-item">
+    <a href="{{ route('library.home') }}">首页</a>
+</li>
+<li class="breadcrumb-item">
+    <a href="{{ route('library.tipitaka.index') }}">三藏</a>
+</li>
+<li class="breadcrumb-item active">{{ $book['title'] }}</li>
+@endsection
+
+@section('content')
+<div class="page-body">
+    <div class="container-xl wiki-layout">
+
+        {{-- 左侧边栏:封面 + 操作按钮 --}}
+        <aside class="wiki-sidebar-left">
+            <div class="wiki-sidebar-section" style="padding: 1rem;">
+
+                {{-- 封面 --}}
+                <x-ui.book-cover
+                    :image="$book['cover'] ?? null"
+                    :gradient="$book['cover_gradient'] ?? 'linear-gradient(135deg, #2d2010 0%, #1a1208 100%)'"
+                    :title="$book['title']"
+                    size="md"
+                    :style3d="false"
+                    style="width: 100%; min-width: unset; height: 220px;" />
+
+                {{-- 操作按钮 --}}
+                <div style="margin-top: 1rem;">
+                    <a href="{{ route('library.tipitaka.read', $book['id']) }}"
+                        class="btn btn-primary w-100 mb-2">
+                        <i class="ti ti-book-2 me-1"></i>
+                        在线阅读
+                    </a>
+                    <button class="btn btn-outline-secondary w-100">
+                        <i class="ti ti-download me-1"></i>
+                        下载
+                    </button>
+                </div>
+
+            </div>
+        </aside>
+
+        {{-- 主内容区 --}}
+        <main class="wiki-main">
+
+            {{-- 书籍信息 --}}
+            <div class="wiki-card">
+                <div class="wiki-entry-header">
+                    <div class="wiki-entry-title">{{ $book['title'] }}</div>
+                </div>
+
+                <table class="wiki-meta-table" style="margin-bottom: 1.25rem;">
+                    <tr>
+                        <td>作者</td>
+                        <td>{{ $book['author'] }}</td>
+                    </tr>
+                    @if(isset($book['publisher']))
+                    <tr>
+                        <td>出版</td>
+                        <td>
+                            <a href="{{ route('blog.index', ['user' => $book['publisher']->username]) }}"
+                                style="color: var(--tblr-primary); text-decoration: none;">
+                                {{ $book['publisher']->nickname }}
+                            </a>
+                        </td>
+                    </tr>
+                    @endif
+                    <tr>
+                        <td>语言</td>
+                        <td>{{ $book['language'] ?? '巴利语' }}</td>
+                    </tr>
+                </table>
+
+                @if(!empty($book['description']))
+                <div class="wiki-content-body">
+                    <p>{{ $book['description'] }}</p>
+                </div>
+                @endif
+            </div>
+
+            {{-- 目录 --}}
+            @if(isset($book['contents']) && count($book['contents']) > 0)
+            <div class="wiki-card">
+                <div class="wiki-sidebar-title" style="margin-bottom: 1rem;">目录</div>
+                <ul class="wiki-toc-list">
+                    @foreach($book['contents'] as $chapter)
+                    <li>
+                        <a href="{{ route('library.tipitaka.read', $chapter['id']) }}?channel={{ $chapter['channel'] }}">
+                            <div style="display: flex;">
+                                <div>{{ $chapter['title'] }}</div>
+                                <div>
+                                    <span style="margin-left: auto; font-size: 0.75rem; color: var(--tblr-secondary);">
+                                        {{ $chapter['progress'] }}%
+                                    </span>
+                                </div>
+                            </div>
+                            @if(isset($chapter['summary']))
+                            <span style="font-size: 0.75rem; color: var(--tblr-secondary); margin-left: 0.5rem;" class="line2">
+                                {{ $chapter['summary'] }}
+                            </span>
+                            @endif
+                            @if(isset($chapter['progress']) && $chapter['progress'] > 0)
+
+                            @endif
+                        </a>
+                    </li>
+                    @endforeach
+                </ul>
+            </div>
+            @endif
+
+        </main>
+
+        {{-- 右侧边栏 --}}
+        <aside class="wiki-sidebar-right">
+
+            {{-- 书籍元信息 --}}
+            <div class="wiki-sidebar-section">
+                <div class="wiki-sidebar-title">书籍信息</div>
+                <table class="wiki-meta-table">
+                    <tr>
+                        <td>语言</td>
+                        <td>{{ $book['language'] ?? '巴利语' }}</td>
+                    </tr>
+                    @if(!empty($book['type']))
+                    <tr>
+                        <td>类型</td>
+                        <td>{{ $book['type'] }}</td>
+                    </tr>
+                    @endif
+                </table>
+            </div>
+
+            {{-- 其他版本 --}}
+            @if(!empty($otherVersions) && count($otherVersions) > 0)
+            <div class="wiki-sidebar-section">
+                <div class="wiki-sidebar-title">其他版本</div>
+                <ul class="wiki-related-list">
+                    @foreach($otherVersions as $version)
+                    <li>
+                        <a href="{{ route('library.tipitaka.show', $version['id']) }}">
+                            {{ $version['title'] }}
+                            <span class="wiki-related-zh">{{ $version['language'] ?? '巴利语' }}</span>
+                        </a>
+                    </li>
+                    @endforeach
+                </ul>
+            </div>
+            @endif
+
+        </aside>
+
+    </div>
+</div>
+@endsection

+ 79 - 0
api-v12/resources/views/library/wiki/home.blade.php

@@ -0,0 +1,79 @@
+{{-- resources/views/wiki/home.blade.php
+     Wiki 门户首页。
+     布局:单栏居中,法轮图标 + 标题 + 搜索框 + 热门标签 + 语言选择。
+     所有样式来自 modules/_wiki.css,无内联 <style>。
+--}}
+@extends('library.wiki.layouts.app')
+
+@section('title', 'WikiPāli · 佛教百科-重构')
+
+@section('wiki-content')
+<div class="wiki-home-container">
+
+    {{-- 法轮图标 --}}
+    <div class="wiki-home-wheel">
+        <img src="{{ asset('assets/images/dhamma-wheel.svg') }}"
+            alt="Dharma Wheel"
+            class="wiki-home-wheel-img">
+    </div>
+
+    {{-- 欢迎标题 --}}
+    <div class="wiki-home-title">
+        <h1>佛教百科</h1>
+        <p class="text-muted">探索佛法智慧 · 开启觉悟之门</p>
+    </div>
+
+    {{-- 搜索框 --}}
+    <div class="wiki-home-search">
+        <x-wiki.search-box
+            :action="route('library.search')"
+            placeholder="搜索佛法词条、经典、人物..."
+            button-text="搜索"
+            size="lg" />
+    </div>
+
+    {{-- 热门搜索标签 --}}
+    @isset($hotTags)
+    <div class="wiki-home-hot-tags">
+        <span class="text-muted me-2">热门:</span>
+        @foreach($hotTags as $tag)
+        <a href="{{ route('library.search', ['q' => $tag, 'type' => 'wiki']) }}"
+            class="wiki-hot-tag">
+            {{ $tag }}
+        </a>
+        @endforeach
+    </div>
+    @endisset
+
+    {{-- 语言选择器 --}}
+    <div class="wiki-home-languages">
+        <div class="wiki-home-divider">
+            <span>以您的语言阅读佛教百科</span>
+        </div>
+        <div class="wiki-language-tags">
+            @foreach($languages as $lang)
+            <a href="{{ route('library.wiki.index', ['lang' => $lang['code']]) }}"
+                class="wiki-language-tag {{ ($currentLang ?? 'zh-Hans') === $lang['code'] ? 'active' : '' }}">
+                {{ $lang['name'] }}
+            </a>
+            @endforeach
+        </div>
+    </div>
+
+    {{-- 统计信息 --}}
+    @isset($stats)
+    <div class="wiki-home-stats">
+        <span class="text-muted">
+            📚 {{ number_format($stats['total_articles'] ?? 0) }} 词条
+            @if(isset($stats['today_updates']))
+            · 🆕 今日更新 {{ $stats['today_updates'] }}
+            @endif
+            @if(isset($stats['contributors']))
+            · 👥 {{ number_format($stats['contributors']) }} 位贡献者
+            @endif
+        </span>
+    </div>
+    @endisset
+
+</div>
+@endsection

+ 1 - 1
api-v12/resources/views/wiki/index.blade.php → api-v12/resources/views/library/wiki/index.blade.php

@@ -1,5 +1,5 @@
 {{-- resources/views/wiki/index.blade.php --}}
-@extends('wiki.layouts.app')
+@extends('library.wiki.layouts.app')
 
 @section('title', 'WikiPāli · 巴利佛典百科')
 

+ 78 - 0
api-v12/resources/views/library/wiki/layouts/app.blade.php

@@ -0,0 +1,78 @@
+{{-- resources/views/wiki/layouts/app.blade.php
+     Wiki 栏目布局中间层。
+     继承 layouts.library(提供 navbar + footer 外壳)。
+     负责:wiki 三栏容器、wiki CSS 注入、term-drawer 全局组件。
+--}}
+@extends('layouts.library')
+
+@push('styles')
+    @vite('resources/css/modules/_wiki.css')
+@endpush
+
+@section('content')
+
+<div class="container-xl wiki-layout">
+
+    {{-- 左侧边栏 --}}
+    @hasSection('wiki-sidebar-left')
+        <aside class="wiki-sidebar-left">
+            @yield('wiki-sidebar-left')
+        </aside>
+    @else
+        @if(isset($lang))
+        <aside class="wiki-sidebar-left">
+
+            @isset($categories)
+            <div class="wiki-sidebar-section">
+                <div class="wiki-sidebar-title">分类浏览</div>
+                <ul class="wiki-cat-list">
+                    @foreach ($categories as $cat)
+                    <li>
+                        <a href="{{ route('library.wiki.index', ['lang' => $lang]) }}?category={{ $cat['slug'] }}"
+                           class="{{ (request('category', 'all') === $cat['slug']) ? 'active' : '' }}">
+                            {{ $cat['label'] }}
+                            @if(isset($cat['count']))
+                                <span class="wiki-cat-count">{{ $cat['count'] }}</span>
+                            @endif
+                        </a>
+                    </li>
+                    @endforeach
+                </ul>
+            </div>
+            @endisset
+
+            @isset($recentUpdates)
+            <div class="wiki-sidebar-section">
+                <div class="wiki-sidebar-title">最近更新</div>
+                <ul class="wiki-cat-list">
+                    @foreach ($recentUpdates as $item)
+                    <li>
+                        <a href="{{ route('library.wiki.show', [$item['lang'], $item['word']]) }}">
+                            {{ $item['word'] }}
+                        </a>
+                    </li>
+                    @endforeach
+                </ul>
+            </div>
+            @endisset
+
+        </aside>
+        @endif
+    @endif
+
+    {{-- 主内容区 --}}
+    <main class="wiki-main">
+        @yield('wiki-content')
+    </main>
+
+    {{-- 右侧边栏 --}}
+    <aside class="wiki-sidebar-right">
+        @yield('wiki-sidebar')
+    </aside>
+
+</div>
+
+{{-- 术语抽屉(移动端,所有阅读页公用,在此统一挂载) --}}
+<x-wiki.term-drawer />
+
+@endsection

+ 5 - 1
api-v12/resources/views/wiki/show.blade.php → api-v12/resources/views/library/wiki/show.blade.php

@@ -1,5 +1,5 @@
 {{-- resources/views/wiki/show.blade.php --}}
-@extends('wiki.layouts.app')
+@extends('library.wiki.layouts.app')
 
 @section('title', $entry['meaning'] . '(' . $entry['word'] . ')· WikiPāli')
 
@@ -74,3 +74,7 @@
 </div>
 
 @endsection
+
+@push('scripts')
+@vite('resources/js/modules/term-tooltip.js')
+@endpush

+ 0 - 337
api-v12/resources/views/wiki/home.blade.php

@@ -1,337 +0,0 @@
-{{-- resources/views/wiki/home.blade.php --}}
-@extends('wiki.layouts.app')
-
-@section('wiki-content')
-<div class="wiki-home-container">
-    {{-- 中央法轮图 --}}
-    <div class="dharma-wheel-wrapper">
-        <img src="{{ asset('assets/images/dhamma-wheel.svg') }}" alt="Dharma Wheel" class="dharma-wheel-img">
-    </div>
-
-    {{-- 欢迎标题 --}}
-    <div class="wiki-welcome-title">
-        <h1 class="display-4 fw-bold text-primary mb-2">佛教百科</h1>
-        <p class="text-muted">探索佛法智慧 · 开启觉悟之门</p>
-    </div>
-
-    {{-- 搜索框组件 --}}
-    <div class="wiki-search-wrapper">
-        <x-wiki.search-box
-            :action="route('library.search')"
-            placeholder="搜索佛法词条、经典、人物..."
-            button-text="搜索"
-            size="lg" />
-    </div>
-
-    {{-- 热门搜索标签 --}}
-    @isset($hotTags)
-    <div class="wiki-hot-tags">
-        <span class="text-muted me-2">热门:</span>
-        @foreach($hotTags as $tag)
-        <a href="{{ route('library.search', ['q' => $tag, 'type' => 'wiki']) }}" class="badge bg-secondary-light text-dark me-1 text-decoration-none">
-            {{ $tag }}
-        </a>
-        @endforeach
-    </div>
-    @endisset
-
-    {{-- 分隔横线 + 语言选择器 --}}
-    <div class="wiki-language-section">
-        <div class="divider">
-            <span class="divider-text">以您的语言阅读佛教百科</span>
-        </div>
-        <div class="language-tags">
-            @foreach($languages as $lang)
-            <a href="{{ route('library.wiki.index', ['lang' => $lang['code']]) }}"
-                class="language-tag {{ ($currentLang ?? 'zh-Hans') === $lang['code'] ? 'active' : '' }}">
-                {{ $lang['name'] }}
-            </a>
-            @endforeach
-        </div>
-    </div>
-
-    {{-- 统计信息 --}}
-    @isset($stats)
-    <div class="wiki-stats">
-        <span class="text-muted">
-            📚 {{ number_format($stats['total_articles'] ?? 0) }} 词条
-            @if(isset($stats['today_updates']))
-            · 🆕 今日更新 {{ $stats['today_updates'] }}
-            @endif
-            @if(isset($stats['contributors']))
-            · 👥 {{ number_format($stats['contributors']) }} 位贡献者
-            @endif
-        </span>
-    </div>
-    @endisset
-</div>
-@endsection
-
-@push('styles')
-<style>
-    .wiki-home-container {
-        display: flex;
-        flex-direction: column;
-        align-items: center;
-        justify-content: center;
-        min-height: calc(100vh - 300px);
-        padding: 3rem 1.5rem;
-        background: linear-gradient(135deg, #fef9f0 0%, #fff9f5 100%);
-        border-radius: 1rem;
-        margin: 1rem;
-    }
-
-    .dharma-wheel-wrapper {
-        margin-bottom: 1.5rem;
-        animation: subtle-float 3s ease-in-out infinite;
-    }
-
-    .dharma-wheel-img {
-        width: 140px;
-        height: auto;
-        filter: drop-shadow(0 8px 20px rgba(0, 0, 0, 0.1));
-        transition: transform 0.3s ease;
-    }
-
-    .dharma-wheel-img:hover {
-        transform: scale(1.05);
-    }
-
-    .wiki-welcome-title {
-        text-align: center;
-        margin-bottom: 2rem;
-    }
-
-    .wiki-welcome-title h1 {
-        background: linear-gradient(135deg, #8b5e3c 0%, #c49a6c 100%);
-        -webkit-background-clip: text;
-        -webkit-text-fill-color: transparent;
-        background-clip: text;
-        font-weight: 700;
-    }
-
-    .wiki-search-wrapper {
-        width: 100%;
-        max-width: 640px;
-        margin: 0 auto 1.5rem;
-    }
-
-    .wiki-hot-tags {
-        text-align: center;
-        margin-bottom: 3rem;
-        font-size: 0.9rem;
-    }
-
-    .wiki-hot-tags .badge {
-        padding: 0.4rem 0.8rem;
-        transition: all 0.2s ease;
-        background-color: #f0e6d8;
-        color: #5a3a2a;
-        font-weight: 500;
-    }
-
-    .wiki-hot-tags .badge:hover {
-        background-color: #c49a6c;
-        color: white;
-        transform: translateY(-2px);
-    }
-
-    /* 分隔横线样式 */
-    .wiki-language-section {
-        width: 100%;
-        max-width: 800px;
-        margin: 1rem auto 2rem;
-    }
-
-    .divider {
-        display: flex;
-        align-items: center;
-        text-align: center;
-        margin-bottom: 2rem;
-    }
-
-    .divider::before,
-    .divider::after {
-        content: '';
-        flex: 1;
-        border-bottom: 1px solid rgba(0, 0, 0, 0.1);
-    }
-
-    .divider::before {
-        margin-right: 1.5rem;
-    }
-
-    .divider::after {
-        margin-left: 1.5rem;
-    }
-
-    .divider-text {
-        font-size: 0.95rem;
-        color: #8b7355;
-        letter-spacing: 1px;
-        font-weight: 500;
-        white-space: nowrap;
-    }
-
-    /* 语言标签样式 */
-    .language-tags {
-        display: flex;
-        flex-wrap: wrap;
-        justify-content: center;
-        gap: 0.75rem;
-    }
-
-    .language-tag {
-        display: inline-block;
-        padding: 0.5rem 1.25rem;
-        background-color: #f5f0ea;
-        color: #5a3a2a;
-        text-decoration: none;
-        border-radius: 30px;
-        font-size: 0.9rem;
-        font-weight: 500;
-        transition: all 0.25s ease;
-        border: 1px solid transparent;
-    }
-
-    .language-tag:hover {
-        background-color: #e8ddd0;
-        transform: translateY(-2px);
-        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
-        color: #3a2518;
-    }
-
-    .language-tag.active {
-        background: linear-gradient(135deg, #8b5e3c 0%, #6b4226 100%);
-        color: white;
-        border-color: #8b5e3c;
-        box-shadow: 0 2px 8px rgba(139, 94, 60, 0.3);
-    }
-
-    .language-tag.active:hover {
-        background: linear-gradient(135deg, #9b6e4c 0%, #7b5236 100%);
-        transform: translateY(-2px);
-    }
-
-    .wiki-stats {
-        text-align: center;
-        font-size: 0.875rem;
-        margin-top: 1rem;
-        padding-top: 1.5rem;
-        border-top: 1px solid rgba(0, 0, 0, 0.08);
-    }
-
-    @keyframes subtle-float {
-
-        0%,
-        100% {
-            transform: translateY(0px);
-        }
-
-        50% {
-            transform: translateY(-6px);
-        }
-    }
-
-    /* 移动端适配 */
-    @media (max-width: 768px) {
-        .wiki-home-container {
-            min-height: calc(100vh - 200px);
-            padding: 2rem 1rem;
-            margin: 0.5rem;
-        }
-
-        .dharma-wheel-img {
-            width: 100px;
-        }
-
-        .wiki-welcome-title h1 {
-            font-size: 1.8rem;
-        }
-
-        .wiki-welcome-title p {
-            font-size: 0.9rem;
-        }
-
-        .wiki-search-wrapper {
-            max-width: 100%;
-        }
-
-        .divider-text {
-            font-size: 0.85rem;
-            white-space: normal;
-            text-align: center;
-        }
-
-        .divider::before,
-        .divider::after {
-            margin-right: 1rem;
-            margin-left: 1rem;
-        }
-
-        .language-tag {
-            padding: 0.4rem 1rem;
-            font-size: 0.85rem;
-        }
-
-        .language-tags {
-            gap: 0.6rem;
-        }
-    }
-
-    /* 暗色模式适配 */
-    @media (prefers-color-scheme: dark) {
-        .wiki-home-container {
-            background: linear-gradient(135deg, #2a2418 0%, #1f1b14 100%);
-        }
-
-        .wiki-welcome-title h1 {
-            background: linear-gradient(135deg, #d4a574 0%, #e8c4a0 100%);
-            -webkit-background-clip: text;
-            -webkit-text-fill-color: transparent;
-        }
-
-        .wiki-welcome-title p {
-            color: #a89880;
-        }
-
-        .wiki-hot-tags .badge {
-            background-color: #3a3025;
-            color: #d4c4b0;
-        }
-
-        .wiki-hot-tags .badge:hover {
-            background-color: #c49a6c;
-            color: #1f1b14;
-        }
-
-        .divider::before,
-        .divider::after {
-            border-bottom-color: rgba(255, 255, 255, 0.15);
-        }
-
-        .divider-text {
-            color: #b8a88a;
-        }
-
-        .language-tag {
-            background-color: #3a3025;
-            color: #d4c4b0;
-        }
-
-        .language-tag:hover {
-            background-color: #4a3e30;
-            color: #f0e0c0;
-        }
-
-        .language-tag.active {
-            background: linear-gradient(135deg, #c49a6c 0%, #a07850 100%);
-            color: #1f1b14;
-        }
-
-        .wiki-stats {
-            border-top-color: rgba(255, 255, 255, 0.1);
-        }
-    }
-</style>
-@endpush

+ 0 - 69
api-v12/resources/views/wiki/layouts/app.blade.php

@@ -1,69 +0,0 @@
-{{-- resources/views/wiki/layouts/app.blade.php --}}
-@extends('library.layouts.app')
-
-@push('styles')
-@vite(['resources/css/wiki.css', 'resources/css/wiki-content.css','resources/css/wiki-search.css'])
-@endpush
-
-@section('content')
-
-<div class="container-xl wiki-layout">
-
-    {{-- 左侧边栏 --}}
-    @if(isset($lang))
-    <aside class="wiki-sidebar-left">
-
-        {{-- 分类导航 --}}
-        @isset($categories)
-        <div class="wiki-sidebar-section">
-            <div class="wiki-sidebar-title">分类浏览</div>
-            <ul class="wiki-cat-list">
-                @foreach ($categories as $cat)
-                <li>
-                    <a href="{{ route('library.wiki.index',['lang'=>$lang]) }}?category={{ $cat['slug'] }}"
-                        class="{{ (request('category', 'all') === $cat['slug']) ? 'active' : '' }}">
-                        {{ $cat['label'] }}
-                    </a>
-                </li>
-                @endforeach
-            </ul>
-        </div>
-        @endisset
-
-        {{-- 最近更新 --}}
-        @isset($recentUpdates)
-        <div class="wiki-sidebar-section">
-            <div class="wiki-sidebar-title">最近更新</div>
-            <ul class="wiki-cat-list">
-                @foreach ($recentUpdates as $item)
-                <li>
-                    <a href="{{ route('library.wiki.show', [$item['lang'], $item['word']]) }}">
-                        {{ $item['word'] }}
-                    </a>
-                </li>
-                @endforeach
-        </div>
-        @endisset
-
-    </aside>
-    @endif
-
-    {{-- 主内容区 --}}
-    <main class="wiki-main">
-        @yield('wiki-content')
-    </main>
-
-    {{-- 右侧边栏 --}}
-    <aside class="wiki-sidebar-right">
-        @yield('wiki-sidebar')
-    </aside>
-
-</div>
-
-{{-- 术语抽屉(移动端,全局唯一) --}}
-<x-wiki.term-drawer />
-@endsection
-
-@push('scripts')
-@vite(['resources/js/term-tooltip.js'])
-@endpush

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

@@ -1,126 +0,0 @@
-{{-- 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

+ 5 - 3
api-v12/routes/web.php

@@ -11,7 +11,7 @@ use App\Http\Controllers\DownloadController;
 use App\Http\Controllers\Library\AnthologyController;
 use App\Http\Controllers\Library\AnthologyReadController;
 use App\Http\Controllers\Library\BookController;
-use App\Http\Controllers\WikiController;
+use App\Http\Controllers\Library\WikiController;
 use App\Http\Controllers\Library\SearchController;
 
 /*
@@ -64,9 +64,10 @@ Route::post('/logout', function () {
 })->name('logout');
 
 Route::prefix('library')->name('library.')->group(function () {
-    Route::get('/', [CategoryController::class, 'index'])->name('home');
+    Route::get('/', [CategoryController::class, 'home'])->name('home');
 
-    Route::get('/category/{id}', [CategoryController::class, 'show'])->name('category.show');
+    Route::get('/tipitaka', [CategoryController::class, 'category'])->name('tipitaka.index');
+    Route::get('/tipitaka/category/{id}', [CategoryController::class, 'category'])->name('tipitaka.category');
     Route::get('/tipitaka/{id}', [BookController::class, 'show'])->name('tipitaka.show');
     Route::get('/tipitaka/{id}/read', [BookController::class, 'read'])->name('tipitaka.read');
 
@@ -74,6 +75,7 @@ Route::prefix('library')->name('library.')->group(function () {
     Route::get('/wiki/{lang}', [WikiController::class, 'index'])->name('wiki.index');
     Route::get('/wiki/{lang}/{word}', [WikiController::class, 'show'])->name('wiki.show');
 
+    Route::get('/course', [DownloadController::class, 'index'])->name('course');
     Route::get('/download', [DownloadController::class, 'index'])->name('download');
     // 文集
     Route::get('/anthology',          [AnthologyController::class, 'index'])->name('anthology.index');

+ 6 - 3
api-v12/vite.config.js

@@ -1,13 +1,16 @@
+// vite.config.js
 import { defineConfig } from 'vite';
 import laravel from 'laravel-vite-plugin';
-import tailwindcss from '@tailwindcss/vite';
 
 export default defineConfig({
     plugins: [
         laravel({
-            input: ['resources/css/app.css', 'resources/js/app.js'],
+            input: [
+                'resources/css/library.css',   // library/* + blog 列表页
+                'resources/css/reader.css',    // 全站阅读页(待建)
+                'resources/js/app.js',
+            ],
             refresh: true,
         }),
-        tailwindcss(),
     ],
 });

+ 61 - 27
dashboard-v6/src/components/article/components/ArticleReaderToolbar.tsx

@@ -7,6 +7,9 @@ import {
   FileOutlined,
   CopyOutlined,
   InfoCircleOutlined,
+  ShareAltOutlined,
+  ExportOutlined,
+  EyeOutlined,
 } from "@ant-design/icons";
 
 import { useAppSelector } from "../../../hooks";
@@ -91,8 +94,7 @@ const TypeArticleReaderToolbarWidget = ({
               id: "buttons.edit",
             })}
           </Button>
-          {/** 刷新按钮 */}
-          <Button type="link" icon={<ReloadOutlined />} onClick={onRefresh} />
+          {/** 导出 */}
           {/** 更多 */}
           <Dropdown
             menu={{
@@ -109,41 +111,84 @@ const TypeArticleReaderToolbarWidget = ({
                 },
                 {
                   label: intl.formatMessage({
-                    id: "buttons.add_to_anthology",
+                    id: "buttons.export",
                   }),
-                  key: "add_to_anthology",
-                  icon: <InboxOutlined />,
-                  disabled: user ? false : true,
+                  key: "export",
+                  icon: <ExportOutlined />,
                 },
                 {
                   label: intl.formatMessage({
-                    id: "buttons.edit",
+                    id: "buttons.open.in.library",
                   }),
-                  key: "edit",
-                  icon: <EditOutlined />,
-                  disabled: !editable,
+                  key: "open_in_library",
+                  icon: <EyeOutlined />,
                 },
+              ],
+              onClick: ({ key }) => {
+                console.log(`Click on item ${key}`);
+                switch (key) {
+                  case "open_in_tab":
+                    window.open(
+                      fullUrl(`/article/article/${articleId}`),
+                      "_blank"
+                    );
+                    break;
+                  case "open_in_library":
+                    window.open(
+                      import.meta.env.VITE_APP_API_SERVER +
+                        `/library/anthology/${anthologyId}/read/${articleId}`,
+                      "_blank"
+                    );
+                    break;
+                  case "add_to_anthology":
+                    setAddToAnthologyOpen(true);
+                    break;
+                }
+              },
+            }}
+            placement="bottomRight"
+          >
+            <Button
+              onClick={(e) => e.preventDefault()}
+              icon={<ShareAltOutlined />}
+              size="small"
+              type="link"
+            />
+          </Dropdown>
+          {/** 刷新按钮 */}
+          <Button type="link" icon={<ReloadOutlined />} onClick={onRefresh} />
+
+          {/** 更多 */}
+          <Dropdown
+            menu={{
+              items: [
                 {
                   label: intl.formatMessage({
-                    id: "buttons.open.in.studio",
+                    id: "buttons.add_to_anthology",
                   }),
-                  key: "open-studio",
-                  icon: <EditOutlined />,
+                  key: "add_to_anthology",
+                  icon: <InboxOutlined />,
                   disabled: user ? false : true,
                 },
                 {
-                  label: "获取文章引用模版",
+                  label: intl.formatMessage({
+                    id: "buttons.get_template",
+                  }),
                   key: "tpl",
                   icon: <FileOutlined />,
                 },
                 {
-                  label: "创建副本",
+                  label: intl.formatMessage({
+                    id: "buttons.duplicate",
+                  }),
                   key: "fork",
                   icon: <CopyOutlined />,
                   disabled: user ? false : true,
                 },
                 {
-                  label: "字数统计",
+                  label: intl.formatMessage({
+                    id: "buttons.word_count",
+                  }),
                   key: "word-count",
                   icon: <InfoCircleOutlined />,
                 },
@@ -151,12 +196,6 @@ const TypeArticleReaderToolbarWidget = ({
               onClick: ({ key }) => {
                 console.log(`Click on item ${key}`);
                 switch (key) {
-                  case "open_in_tab":
-                    window.open(
-                      fullUrl(`/article/article/${articleId}`),
-                      "_blank"
-                    );
-                    break;
                   case "add_to_anthology":
                     setAddToAnthologyOpen(true);
                     break;
@@ -171,11 +210,6 @@ const TypeArticleReaderToolbarWidget = ({
                   case "word-count":
                     setWordCountOpen(true);
                     break;
-                  case "edit":
-                    if (typeof onEdit !== "undefined") {
-                      onEdit();
-                    }
-                    break;
                 }
               },
             }}

+ 42 - 39
dashboard-v6/src/locales/en-US/buttons.ts

@@ -1,28 +1,28 @@
 const items = {
-  "buttons.submit": "submit",
-  "buttons.create": "create",
-  "buttons.edit": "edit",
-  "buttons.translate": "translate",
-  "buttons.read": "read",
+  "buttons.submit": "Submit",
+  "buttons.create": "Create",
+  "buttons.edit": "Edit",
+  "buttons.translate": "Translate",
+  "buttons.read": "Read",
   "buttons.wbw": "wbw",
-  "buttons.delete": "delete",
-  "buttons.remove": "remove",
-  "buttons.delete.all": "delete all",
-  "buttons.selected": "selected",
-  "buttons.select": "select",
-  "buttons.unselect": "unselect",
-  "buttons.option": "operation",
-  "buttons.save": "save",
-  "buttons.save.publish": "save & publish",
-  "buttons.save.my.dict": "save & my dict",
-  "buttons.cancel": "cancel",
-  "buttons.setting": "setting",
-  "buttons.sign-in": "sign in",
-  "buttons.sign-out": "sign out",
-  "buttons.welcome": "welcome",
+  "buttons.delete": "Delete",
+  "buttons.remove": "Remove",
+  "buttons.delete.all": "Delete All",
+  "buttons.selected": "Selected",
+  "buttons.select": "Select",
+  "buttons.unselect": "Unselect",
+  "buttons.option": "Operation",
+  "buttons.save": "Save",
+  "buttons.save.publish": "Save & Publish",
+  "buttons.save.my.dict": "Save & My Dictionary",
+  "buttons.cancel": "Cancel",
+  "buttons.setting": "Setting",
+  "buttons.sign-in": "Sign in",
+  "buttons.sign-out": "Sign out",
+  "buttons.welcome": "Welcome",
   "buttons.click.upload": "click and upload",
-  "buttons.group.exit": "exit",
-  "buttons.group.add.member": "add member",
+  "buttons.group.exit": "Exit",
+  "buttons.group.add.member": "Add Member",
   "buttons.lesson.add.lesson": "加入",
   "buttons.ok": "ok",
   "buttons.close": "close",
@@ -35,21 +35,21 @@ const items = {
   "buttons.share": "share",
   "buttons.yes": "yes",
   "buttons.no": "no",
-  "buttons.open.in.library": "open in library",
-  "buttons.preview": "preview",
-  "buttons.view": "view",
-  "buttons.empty": "empty",
-  "buttons.basic": "basic",
-  "buttons.bookmark": "bookmark",
-  "buttons.note": "note",
-  "buttons.advance": "advance",
-  "buttons.attachments": "attachments",
-  "buttons.sim": "sim",
-  "buttons.add": "add",
-  "buttons.spell": "spell",
-  "buttons.more": "more",
-  "buttons.import": "import",
-  "buttons.export": "export",
+  "buttons.open.in.library": "Open in Library",
+  "buttons.preview": "Preview",
+  "buttons.view": "View",
+  "buttons.empty": "Empty",
+  "buttons.basic": "Basic",
+  "buttons.bookmark": "Bookmark",
+  "buttons.note": "Note",
+  "buttons.advance": "Advance",
+  "buttons.attachments": "Attachments",
+  "buttons.sim": "Sim",
+  "buttons.add": "Add",
+  "buttons.spell": "Spell",
+  "buttons.more": "More",
+  "buttons.import": "Import",
+  "buttons.export": "Export",
   "buttons.add.tag": "add tag",
   "buttons.remove.selected": "remove selected",
   "buttons.multiple.select": "multiple select",
@@ -105,14 +105,17 @@ const items = {
   "buttons.task.add.pre-task": "pre task",
   "buttons.task.add.next-task": "next task",
   "buttons.remove.milestone": "remove milestone",
-  "buttons.set.milestone": "set as milestone",
+  "buttons.set.milestone": "Set As Milestone",
   "buttons.next": "Next",
   "buttons.previous": "Previous",
   "buttons.clone": "Clone",
   "buttons.general": "General",
   "buttons.ai-models": "AI Models",
   "buttons.new": "New",
-  "buttons.sign-up": "sign-up",
+  "buttons.sign-up": "Sign Up",
+  "buttons.get_template": "Get Template",
+  "buttons.duplicate": "Duplicate",
+  "buttons.word_count": "Word Count",
 };
 
 export default items;

+ 3 - 0
dashboard-v6/src/locales/zh-Hans/buttons.ts

@@ -114,6 +114,9 @@ const items = {
   "buttons.ai-models": "AI模型",
   "buttons.new": "新建",
   "buttons.sign-up": "注册",
+  "buttons.get_template": "获取引用模版",
+  "buttons.duplicate": "创建副本",
+  "buttons.word_count": "字数统计",
 };
 
 export default items;