visuddhinanda 1 天之前
父节点
当前提交
f3c2ad7526

+ 117 - 0
api-v12/app/Helpers/MarkdownHelper.php

@@ -0,0 +1,117 @@
+<?php
+
+namespace App\Helpers;
+
+use Illuminate\Support\Str;
+
+class MarkdownHelper
+{
+    /**
+     * 将 Markdown 字符串按段落、标题、表格、列表、代码块等规则拆分成数组
+     *
+     * @param string $markdown
+     * @return array
+     */
+    public static function splitByParagraphs(string $markdown): array
+    {
+        // 保护代码块内容,防止内部换行被拆分
+        $codeBlocks = [];
+        $markdown = preg_replace_callback('/```(.*?)```/s', function ($matches) use (&$codeBlocks) {
+            $placeholder = '%%CODE_BLOCK_' . count($codeBlocks) . '%%';
+            $codeBlocks[$placeholder] = $matches[0];
+            return $placeholder;
+        }, $markdown);
+
+        // 按两个及以上换行符拆分
+        $parts = preg_split('/\n\s*\n/', $markdown);
+
+        $result = [];
+
+        foreach ($parts as $part) {
+            $part = trim($part);
+            if ($part === '') {
+                continue;
+            }
+
+            // 恢复代码块
+            foreach ($codeBlocks as $placeholder => $codeBlock) {
+                if (strpos($part, $placeholder) !== false) {
+                    $part = str_replace($placeholder, $codeBlock, $part);
+                }
+            }
+
+            // 进一步按标题、表格、列表拆分(这些通常不会被两个换行分隔)
+            $subParts = self::splitBySpecialBlocks($part);
+            foreach ($subParts as $subPart) {
+                $subPart = trim($subPart);
+                if ($subPart !== '') {
+                    $result[] = $subPart;
+                }
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * 按标题、表格、列表等特殊块进一步拆分
+     *
+     * @param string $text
+     * @return array
+     */
+    protected static function splitBySpecialBlocks(string $text): array
+    {
+        // 保护代码块(防止被误拆)
+        $codeBlocks = [];
+        $text = preg_replace_callback('/```(.*?)```/s', function ($matches) use (&$codeBlocks) {
+            $placeholder = '%%CODE_BLOCK_' . count($codeBlocks) . '%%';
+            $codeBlocks[$placeholder] = $matches[0];
+            return $placeholder;
+        }, $text);
+
+        // 按标题 (#, ##, 等)
+        $lines = preg_split('/(^#{1,6}\s+.*$)/m', $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
+
+        $result = [];
+
+        foreach ($lines as $line) {
+            $line = trim($line);
+            if ($line === '') {
+                continue;
+            }
+
+            // 如果包含表格(简单判断:包含 | 且多个行)
+            if (strpos($line, '|') !== false && substr_count($line, "\n") >= 1) {
+                $rows = explode("\n", $line);
+                $table = [];
+                foreach ($rows as $row) {
+                    if (trim($row) !== '') {
+                        $table[] = $row;
+                    }
+                }
+                if (!empty($table)) {
+                    $result[] = implode("\n", $table);
+                }
+                continue;
+            }
+
+            // 如果包含列表(无序列表 - 或 *,有序列表 1. 2.)
+            if (preg_match('/^(\s*[-*+]\s+|\s*\d+\.\s+)/m', $line)) {
+                // 保留整个列表块(连续列表行)
+                $result[] = $line;
+                continue;
+            }
+
+            // 恢复代码块
+            foreach ($codeBlocks as $placeholder => $codeBlock) {
+                if (strpos($line, $placeholder) !== false) {
+                    $line = str_replace($placeholder, $codeBlock, $line);
+                }
+            }
+
+            $result[] = $line;
+        }
+
+        return $result;
+    }
+}

+ 67 - 0
api-v12/app/Helpers/WikiContentParser.php

@@ -0,0 +1,67 @@
+<?php
+// app/Support/WikiContentParser.php
+
+namespace App\Helpers;
+
+class WikiContentParser
+{
+    /**
+     * 给 HTML 中的 h1~h3 注入 id,并提取目录结构
+     * 返回 ['content' => string, 'toc' => array]
+     */
+    public static function parse(string $html): array
+    {
+        $toc = [];
+        $slugCount = [];
+
+        $content = preg_replace_callback(
+            '/<(h[123])([^>]*)>(.*?)<\/\1>/si',
+            function ($matches) use (&$toc, &$slugCount) {
+                [$full, $tag, $attrs, $inner] = $matches;
+
+                if (preg_match('/\bid=["\']([^"\']+)["\']/', $attrs, $m)) {
+                    $id = $m[1];
+                } else {
+                    $text = strip_tags($inner);
+                    $id   = self::slugify($text);
+
+                    if (isset($slugCount[$id])) {
+                        $slugCount[$id]++;
+                        $id .= '-' . $slugCount[$id];
+                    } else {
+                        $slugCount[$id] = 0;
+                    }
+
+                    $attrs .= ' id="' . $id . '"';
+                }
+
+                $toc[] = [
+                    'id'    => $id,
+                    'text'  => strip_tags($inner),
+                    'level' => (int) substr($tag, 1),
+                ];
+
+                return "<{$tag}{$attrs}>{$inner}</{$tag}>";
+            },
+            $html
+        );
+
+        // 归一化层级:找最小 level,所有条目 level = level - minLevel + 1
+        if (!empty($toc)) {
+            $minLevel = min(array_column($toc, 'level'));
+            foreach ($toc as &$item) {
+                $item['level'] = $item['level'] - $minLevel + 1;
+            }
+            unset($item);
+        }
+
+        return ['content' => $content, 'toc' => $toc];
+    }
+
+    private static function slugify(string $text): string
+    {
+        // 保留中文、字母、数字,其余转连字符
+        $slug = preg_replace('/[^\p{L}\p{N}]+/u', '-', trim($text));
+        return strtolower(trim($slug, '-')) ?: 'section';
+    }
+}

+ 206 - 0
api-v12/app/Http/Controllers/WikiController.php

@@ -0,0 +1,206 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use App\Helpers\WikiContentParser;
+use App\Services\TermService;
+
+class WikiController extends Controller
+{
+    public function __construct(
+        private TermService    $termService
+    ) {}
+
+    // ── Mock 数据 ────────────────────────────────────────────────
+
+    private function mockEntries(): array
+    {
+        return [
+            [
+                'word'      => 'Anicca',
+                'lang'      => 'zh-Hans',
+                'slug'      => 'anicca',
+                'meaning'        => '无常',
+                'quality'   => 'featured',   // featured | stub | review | null
+                'category'  => '法义术语',
+                'tags'      => ['三相', '法义术语', '相应部', '内观', '五蕴'],
+                'langs'     => [
+                    ['lang' => 'zh-Hant', 'label' => '繁体中文',   'word' => '无常'],
+                    ['lang' => 'en', 'label' => 'English', 'word' => 'Impermanence'],
+                ],
+                'related' => [
+                    ['word' => 'Dukkha',    'zh' => '苦',   'lang' => 'pi'],
+                    ['word' => 'Anattā',    'zh' => '无我', 'lang' => 'pi'],
+                    ['word' => 'Vipassanā', 'zh' => '内观', 'lang' => 'pi'],
+                    ['word' => 'Ti-lakkhaṇa', 'zh' => '三相', 'lang' => 'pi'],
+                ],
+                'content' => <<<HTML
+<h2>词源与释义</h2>
+
+<blockquote>
+    「诸比丘,色是无常的。无常的即是苦的。苦的即是无我的。无我的即应如实以正慧观察……」
+    <cite>SN 22.15 · Khandha-saṃyutta · 相应部·蕴相应</cite>
+</blockquote>
+
+HTML,
+            ],
+        ];
+    }
+
+    private function featured(array $all): array
+    {
+        return array_map(function ($item) {
+            return ['word' => $item['word'],     'zh' => $item['meaning'],   'lang' => $item['language'], 'category' => '法义术语'];
+        }, $all);
+    }
+
+    private function mockStats(): array
+    {
+        return [
+            'total'        => 2847,
+            'this_month'   => 43,
+            'contributors' => 128,
+        ];
+    }
+
+    private function mockRecentUpdates(): array
+    {
+        return [
+            ['word' => 'Nibbāna',  'lang' => 'pi'],
+            ['word' => '四圣谛',   'lang' => 'zh'],
+            ['word' => '阿含经',   'lang' => 'zh'],
+            ['word' => 'Rājagaha', 'lang' => 'pi'],
+        ];
+    }
+
+    // ── Actions ──────────────────────────────────────────────────
+
+    public function index(string $lang)
+    {
+        $result = $this->termService->communityTerms($lang);
+        $fakeRequest = Request::create('', 'GET', []);
+        $termResource = $result['data'];
+        $terms    = $termResource->toArray($fakeRequest);
+
+        $first = $terms[0];
+        $today = [
+            'word'      => $first['word'],
+            'lang'      => $first['language'],
+            'slug'      => $first['word'],
+            'meaning'        => $first['meaning'],
+            'quality'   => 'featured',   // featured | stub | review | null
+            'category'  => '法义术语',
+            'content' => $first['summary']
+        ];
+
+
+        return view('wiki.index', [
+            'today'         => $today,
+            'featured'      => $this->featured($terms),
+            'stats'         => $this->mockStats(),
+            'recentUpdates' => $this->mockRecentUpdates(),
+            'categories'    => $this->categories(),
+            'lang' => $lang
+        ]);
+    }
+
+    public function show(string $lang, string $word)
+    {
+
+        $term = $this->termService->communityTerm($word, $lang);
+        $urlParam = ['format' => 'html'];
+        $fakeRequest = Request::create('', 'GET', $urlParam);
+        $termArray    = $term->toArray($fakeRequest);
+        $entry = [
+            'word'      => $termArray['word'],
+            'lang'      => $termArray['language'],
+            'slug'      => $termArray['word'],
+            'meaning'        => $termArray['meaning'],
+            'quality'   => 'featured',   // featured | stub | review | null
+            'category'  => '法义术语',
+            'tags'      => [],
+            'langs'     => [
+                ['lang' => 'zh-Hant', 'label' => '繁体中文',   'word' => '无常'],
+                ['lang' => 'en', 'label' => 'English', 'word' => 'Impermanence'],
+            ],
+            'related' => [
+                ['word' => 'Dukkha',    'zh' => '苦',   'lang' => 'pi'],
+                ['word' => 'Anattā',    'zh' => '无我', 'lang' => 'pi'],
+                ['word' => 'Vipassanā', 'zh' => '内观', 'lang' => 'pi'],
+                ['word' => 'Ti-lakkhaṇa', 'zh' => '三相', 'lang' => 'pi'],
+            ],
+            'content' => $termArray['html']
+        ];
+        $parsed  = WikiContentParser::parse($entry['content']);
+
+        return view('wiki.show', [
+            'entry' => array_merge($entry, [
+                'content' => $parsed['content'],
+                'toc'     => $parsed['toc'],
+            ]),
+            'categories' => $this->categories(),
+            'lang' => $lang
+        ]);
+    }
+
+    // ── Helpers ──────────────────────────────────────────────────
+
+    private function categories(): array
+    {
+        return [
+            ['slug' => 'all',      'label' => '全部'],
+            ['slug' => 'term',     'label' => '法义术语'],
+            ['slug' => 'person',   'label' => '人物传记'],
+            ['slug' => 'text',     'label' => '经典文献'],
+            ['slug' => 'school',   'label' => '宗派历史'],
+            ['slug' => 'practice', 'label' => '修行方法'],
+            ['slug' => 'place',    'label' => '佛教地理'],
+        ];
+    }
+
+    // 在 WikiController.php 中添加此方法
+
+    // 在 WikiController.php 中添加
+    public function home(string $lang = 'zh-Hans')
+    {
+        // Mock 语言列表
+        $languages = [
+            ['code' => 'zh-Hans', 'name' => '简体中文'],
+            ['code' => 'zh-Hant', 'name' => '繁體中文'],
+            ['code' => 'en',       'name' => 'English'],
+            ['code' => 'ja',       'name' => '日本語'],
+            ['code' => 'ko',       'name' => '한국어'],
+            ['code' => 'vi',       'name' => 'Tiếng Việt'],
+            ['code' => 'my',       'name' => 'မြန်မာစာ'],
+        ];
+
+        // Mock 统计数据
+        $stats = [
+            'total_articles'   => 2847,
+            'total_terms'      => 1256,
+            'languages_count'  => 10,
+            'contributors'     => 328,
+            'today_updates'    => 12,
+        ];
+
+        // Mock 热门搜索标签
+        $hotTags = ['无常', 'Anicca', '四圣谛', '涅槃', '内观', '业力', '慈悲', '般若'];
+
+        // Mock 每日一词(可选展示)
+        $dailyTerm = [
+            'word'      => 'Dhammapada',
+            'zh'        => '法句经',
+            'lang'      => 'pi',
+            'meaning'   => '佛陀的偈颂集,佛教最重要的经典之一',
+        ];
+
+        return view('wiki.home', [
+            'languages'     => $languages,
+            'currentLang'   => $lang,
+            'stats'         => $stats,
+            'hotTags'       => $hotTags,
+            'dailyTerm'     => $dailyTerm,
+        ]);
+    }
+}

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

@@ -0,0 +1,478 @@
+.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;
+    }
+}

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

@@ -0,0 +1,208 @@
+/* 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;
+}

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

@@ -0,0 +1,574 @@
+/* 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;
+}

+ 83 - 0
api-v12/resources/js/term-tooltip.old.js

@@ -0,0 +1,83 @@
+/**
+ * <span
+    class="term-ref"
+    data-id="b45e7b10-2b75-4f5f-ac63-be686116043c"
+    data-term="anicca"
+>
+    无常
+</span>
+ */
+document.addEventListener("DOMContentLoaded", () => {
+    const cache = {};
+
+    const drawerEl = document.getElementById("termDrawer");
+
+    const drawer = new bootstrap.Offcanvas(drawerEl);
+
+    const drawerTitle = document.getElementById("termDrawerTitle");
+
+    const drawerBody = document.getElementById("termDrawerBody");
+
+    function isMobile() {
+        return window.innerWidth < 768;
+    }
+
+    async function fetchTerm(term) {
+        if (cache[term]) return cache[term];
+        console.info("term", term);
+        const res = await fetch(`/api/v2/terms/${term}`);
+
+        const data = await res.json();
+
+        cache[term] = data.data;
+
+        return data.data;
+    }
+
+    document.querySelectorAll(".term-ref").forEach((el) => {
+        let popover = null;
+
+        el.addEventListener("mouseenter", async () => {
+            console.info("mouseenter");
+            if (isMobile()) return;
+
+            if (popover) return;
+            const pali = el.dataset.term;
+            const data = await fetchTerm(el.dataset.id);
+
+            popover = new bootstrap.Popover(el, {
+                trigger: "manual",
+                html: true,
+                placement: "bottom",
+
+                content: `
+            <div style="max-width:300px">
+                <h4>${data.word}</h4>
+                <div>${data.summary}</div>
+            </div>
+        `,
+            });
+
+            popover.show();
+        });
+
+        el.addEventListener("mouseleave", () => {
+            if (popover) {
+                popover.dispose();
+                popover = null;
+            }
+        });
+
+        el.addEventListener("click", async () => {
+            if (!isMobile()) return;
+
+            const data = await fetchTerm(el.dataset.id);
+
+            drawerTitle.innerHTML = data.word;
+
+            drawerBody.innerHTML = data.summary;
+
+            drawer.show();
+        });
+    });
+});

+ 11 - 0
api-v12/resources/views/components/wiki/entry-header.blade.php

@@ -0,0 +1,11 @@
+{{-- resources/views/components/wiki/entry-header.blade.php --}}
+@props(['entry'])
+
+<div class="wiki-entry-header">
+
+    <x-wiki.quality-badge :quality="$entry['quality']" />
+
+    <h1 class="wiki-entry-title">{{ $entry['meaning'] }}</h1>
+    <div>{{ $entry['word'] }}</div>
+
+</div>

+ 16 - 0
api-v12/resources/views/components/wiki/entry-langs.blade.php

@@ -0,0 +1,16 @@
+{{-- resources/views/components/wiki/entry-langs.blade.php --}}
+@props(['langs', 'current'])
+
+@if (count($langs) > 1)
+<div class="wiki-entry-lang-switcher">
+    <span class="wiki-entry-lang-label">其他语言版本:</span>
+    @foreach ($langs as $item)
+    @if ($item['lang'] !== $current)
+    <a class="wiki-entry-lang-btn"
+        href="{{ route('library.wiki.show', [$item['lang'], $item['word']]) }}">
+        {{ $item['label'] }} · {{ $item['word'] }}
+    </a>
+    @endif
+    @endforeach
+</div>
+@endif

+ 18 - 0
api-v12/resources/views/components/wiki/quality-badge.blade.php

@@ -0,0 +1,18 @@
+{{-- resources/views/components/wiki/quality-badge.blade.php --}}
+@props(['quality' => null])
+
+@php
+    $map = [
+        'featured' => ['label' => '精选条目', 'class' => 'wiki-badge--featured'],
+        'review'   => ['label' => '待审核',   'class' => 'wiki-badge--review'],
+        'stub'     => ['label' => '存根',     'class' => 'wiki-badge--stub'],
+    ];
+    $config = $map[$quality] ?? null;
+@endphp
+
+@if ($config)
+    <span class="wiki-quality-badge {{ $config['class'] }}">
+        <span class="wiki-quality-dot"></span>
+        {{ $config['label'] }}
+    </span>
+@endif

+ 18 - 0
api-v12/resources/views/components/wiki/related-entries.blade.php

@@ -0,0 +1,18 @@
+{{-- resources/views/components/wiki/related-entries.blade.php --}}
+@props(['entries' => []])
+
+@if (count($entries))
+<div class="wiki-sidebar-section">
+    <div class="wiki-sidebar-title">相关条目</div>
+    <ul class="wiki-related-list">
+        @foreach ($entries as $entry)
+        <li>
+            <a href="{{ route('library.wiki.show', [$entry['lang'], $entry['word']]) }}">
+                {{ $entry['word'] }}
+                <span class="wiki-related-zh">{{ $entry['zh'] }}</span>
+            </a>
+        </li>
+        @endforeach
+    </ul>
+</div>
+@endif

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

@@ -0,0 +1,55 @@
+{{-- resources/views/components/wiki/search-box.blade.php (Tabler 增强版) --}}
+@props([
+'action' => '/library/search',
+'method' => 'GET',
+'placeholder' => '搜索佛法词条、经典、人物...',
+'buttonText' => '搜索',
+'queryParam' => 'q',
+'typeParam' => 'type',
+'typeValue' => 'wiki',
+'autoFocus' => false,
+'size' => 'md', // sm, md, lg
+'class' => '',
+])
+
+<div {{ $attributes->merge(['class' => $class]) }}>
+    <form action="{{ $action }}" method="{{ $method }}" role="search">
+        <div class="row g-2 align-items-center">
+            <div class="col">
+                <div class="input-icon">
+                    <span class="input-icon-addon">
+                        <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>
+                    <input
+                        type="text"
+                        name="{{ $queryParam }}"
+                        class="form-control"
+                        placeholder="{{ $placeholder }}"
+                        value="{{ request($queryParam) }}"
+                        {{ $autoFocus ? 'autofocus' : '' }}
+                        aria-label="{{ $placeholder }}">
+                </div>
+            </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>
+        </div>
+
+        @if($typeParam && $typeValue)
+        <input type="hidden" name="{{ $typeParam }}" value="{{ $typeValue }}">
+        @endif
+
+        {{ $slot }}
+    </form>
+</div>

+ 17 - 0
api-v12/resources/views/components/wiki/term-card.blade.php

@@ -0,0 +1,17 @@
+{{-- resources/views/components/wiki/term-card.blade.php --}}
+{{-- props: word, meaning, summary --}}
+@props(['word', 'meaning' => '', 'summary' => ''])
+
+@php $showSummary = $summary && $summary !== $meaning; @endphp
+
+<div class="wiki-term-card">
+    <div class="wiki-term-card-word">{{ $word }}</div>
+    <div class="wiki-term-card-body">
+        @if ($meaning)
+        <div class="wiki-term-card-meaning">{{ $meaning }}</div>
+        @endif
+        @if ($showSummary)
+        <div class="wiki-term-card-summary">{{ $summary }}</div>
+        @endif
+    </div>
+</div>

+ 30 - 0
api-v12/resources/views/components/wiki/term-drawer.blade.php

@@ -0,0 +1,30 @@
+{{-- resources/views/components/wiki/term-drawer.blade.php --}}
+{{--
+    全局唯一的术语抽屉(移动端)。
+    JS 在点击 .term-ref 时填充内容并调用 show()。
+--}}
+<div class="offcanvas offcanvas-bottom wiki-term-drawer"
+    tabindex="-1"
+    id="wikiTermDrawer"
+    aria-labelledby="wikiTermDrawerLabel">
+
+    <div class="offcanvas-header">
+        <div>
+            <div id="wikiTermDrawerWord" class="wiki-drawer-word"></div>
+            <div id="wikiTermDrawerMeaning" class="wiki-drawer-meaning"></div>
+        </div>
+        <button type="button"
+            class="btn-close"
+            data-bs-dismiss="offcanvas"
+            aria-label="关闭">
+        </button>
+    </div>
+
+    <div class="offcanvas-body">
+        <div id="wikiTermCardSlot"></div>
+        <a id="wikiTermDrawerLink" class="wiki-drawer-link" href="#" style="display:none;">
+            查看完整条目 →
+        </a>
+    </div>
+
+</div>

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

@@ -0,0 +1,337 @@
+{{-- 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('images/dharma-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

+ 68 - 0
api-v12/resources/views/wiki/index.blade.php

@@ -0,0 +1,68 @@
+{{-- resources/views/wiki/index.blade.php --}}
+@extends('wiki.layouts.app')
+
+@section('title', 'WikiPāli · 巴利佛典百科')
+
+@section('wiki-content')
+{{-- 搜索框组件 --}}
+<div class="wiki-search-wrapper">
+    <x-wiki.search-box
+        :action="route('library.search')"
+        placeholder="搜索佛法词条、经典、人物..."
+        button-text="搜索"
+        size="lg" />
+</div>
+{{-- 今日条目 --}}
+<div class="wiki-today-banner">
+    <div class="wiki-today-icon">☸</div>
+    <div class="wiki-today-body">
+        <div class="wiki-today-label">今日条目</div>
+        <div class="wiki-today-title">{{ $today['meaning'] }}({{ $today['word'] }})</div>
+        <div class="wiki-today-snippet">
+            {!! Str::limit(strip_tags($today['content']), 120) !!}
+        </div>
+        <a class="wiki-today-link"
+            href="{{ route('library.wiki.show', [$today['lang'], $today['word']]) }}">
+            阅读完整条目 →
+        </a>
+    </div>
+</div>
+
+{{-- 精选条目 --}}
+<div class="wiki-card">
+    <div class="wiki-sidebar-title" style="margin-bottom: 14px;">精选条目</div>
+    <div class="wiki-featured-grid">
+        @foreach ($featured as $item)
+        <a class="wiki-featured-card"
+            href="{{ route('library.wiki.show', [$item['lang'], $item['word']]) }}">
+            <div class="wiki-featured-label">{{ $item['category'] }}</div>
+            <div class="wiki-featured-title">{{ $item['zh'] }}</div>
+            <div class="wiki-featured-pali">{{ $item['word'] }}</div>
+        </a>
+        @endforeach
+    </div>
+</div>
+
+@endsection
+
+@section('wiki-sidebar')
+
+<div class="wiki-sidebar-section">
+    <div class="wiki-sidebar-title">统计</div>
+    <table class="wiki-meta-table">
+        <tr>
+            <td>条目总数</td>
+            <td>{{ number_format($stats['total']) }}</td>
+        </tr>
+        <tr>
+            <td>本月新增</td>
+            <td>{{ $stats['this_month'] }}</td>
+        </tr>
+        <tr>
+            <td>贡献者</td>
+            <td>{{ $stats['contributors'] }}</td>
+        </tr>
+    </table>
+</div>
+
+@endsection

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

@@ -0,0 +1,69 @@
+{{-- resources/views/wiki/layouts/app.blade.php --}}
+@extends('library.layouts.app')
+
+@push('styles')
+@vite(['resources/css/wiki.css', 'resources/css/wiki-content.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

+ 76 - 0
api-v12/resources/views/wiki/show.blade.php

@@ -0,0 +1,76 @@
+{{-- resources/views/wiki/show.blade.php --}}
+@extends('wiki.layouts.app')
+
+@section('title', $entry['meaning'] . '(' . $entry['word'] . ')· WikiPāli')
+
+@section('wiki-content')
+{{-- 搜索框组件 --}}
+<div class="wiki-search-wrapper">
+    <x-wiki.search-box
+        :action="route('library.search')"
+        placeholder="搜索佛法词条、经典、人物..."
+        button-text="搜索"
+        size="lg" />
+</div>
+<article class="wiki-card">
+
+    {{-- 条目头部 --}}
+    <x-wiki.entry-header :entry="$entry" />
+
+    {{-- 语言版本切换 --}}
+    <x-wiki.entry-langs :langs="$entry['langs']" :current="$entry['lang']" />
+
+    {{-- 正文 --}}
+    <div class="wiki-content-body">
+        {!! $entry['content'] !!}
+    </div>
+
+    {{-- 标签 --}}
+    <div class="wiki-tags">
+        @foreach ($entry['tags'] as $tag)
+        <a class="wiki-tag" href="{{ route('library.wiki.index') }}?tag={{ $tag }}">
+            {{ $tag }}
+        </a>
+        @endforeach
+    </div>
+
+</article>
+
+@endsection
+
+@section('wiki-sidebar')
+
+{{-- 目录 --}}
+<div class="wiki-sidebar-section">
+    <div class="wiki-sidebar-title">目录</div>
+    <ul class="wiki-toc-list">
+        @foreach ($entry['toc'] as $i => $item)
+        <li class="toc-level-{{ $item['level'] }}">
+            <a href="#{{ $item['id'] }}">
+                <span class="wiki-toc-num">{{ $i + 1 }}</span>
+                {{ $item['text'] }}
+            </a>
+        </li>
+        @endforeach
+    </ul>
+</div>
+
+{{-- 相关条目 --}}
+<x-wiki.related-entries :entries="$entry['related']" />
+
+{{-- 条目元信息 --}}
+<div class="wiki-sidebar-section">
+    <div class="wiki-sidebar-title">条目信息</div>
+    <table class="wiki-meta-table">
+        <tr>
+            <td>分类</td>
+            <td>{{ $entry['category'] }}</td>
+        </tr>
+        <tr>
+            <td>质量</td>
+            <td><x-wiki.quality-badge :quality="$entry['quality']" /></td>
+        </tr>
+    </table>
+</div>
+
+@endsection