Explorar o código

Merge branch 'development' of github.com:visuddhinanda/mint into development

visuddhinanda hai 10 meses
pai
achega
46e4325693
Modificáronse 53 ficheiros con 4417 adicións e 917 borrados
  1. 1 1
      api-v8/app/Console/Commands/MqAiTranslate.php
  2. 74 76
      api-v8/app/Console/Commands/UpgradePaliText.php
  3. 1 1
      api-v8/app/Http/Api/TemplateRender.php
  4. 174 0
      api-v8/app/Http/Controllers/BlogController.php
  5. 366 0
      api-v8/app/Http/Controllers/BookController.php
  6. 180 0
      api-v8/app/Http/Controllers/CategoryController.php
  7. 103 92
      api-v8/app/Http/Controllers/CollectionController.php
  8. 68 71
      api-v8/app/Http/Controllers/PaliTextController.php
  9. 168 162
      api-v8/app/Http/Controllers/ProgressChapterController.php
  10. 1 0
      api-v8/app/Http/Kernel.php
  11. 75 0
      api-v8/app/Http/Middleware/SetLocale.php
  12. 14 1
      api-v8/app/Models/Channel.php
  13. 13 1
      api-v8/app/Models/PaliText.php
  14. 36 11
      api-v8/app/Models/ProgressChapter.php
  15. 3 3
      api-v8/app/Models/TagMap.php
  16. 6 0
      api-v8/app/Models/UserInfo.php
  17. 28 0
      api-v8/app/View/Components/BookItem.php
  18. 28 0
      api-v8/app/View/Components/BookList.php
  19. 28 0
      api-v8/app/View/Components/LanguageSwitcher.php
  20. 9 1
      api-v8/config/mint.php
  21. 650 493
      api-v8/public/app/palicanon/category/default.json
  22. 54 0
      api-v8/public/assets/css/blog/css2
  23. 4 0
      api-v8/public/assets/css/blog/style.min.663803bebe609202d5b39d848f2d7c2dc8b598a2d879efa079fa88893d29c49c.css
  24. BIN=BIN
      api-v8/public/assets/images/cover/1/214.jpg
  25. BIN=BIN
      api-v8/public/assets/images/hero-2.jpg
  26. 0 0
      api-v8/public/assets/js/blog/main.1e9a3bafd846ced4c345d084b355fb8c7bae75701c338f8a1f8a82c780137826.js
  27. 0 0
      api-v8/public/assets/js/blog/vibrant.min.js
  28. 5 0
      api-v8/resources/lang/en/buttons.php
  29. 5 0
      api-v8/resources/lang/en/labels.php
  30. 9 0
      api-v8/resources/lang/en/language.php
  31. 5 0
      api-v8/resources/lang/zh-Hans/buttons.php
  32. 4 0
      api-v8/resources/lang/zh-Hans/label.php
  33. 5 0
      api-v8/resources/lang/zh-Hans/labels.php
  34. 5 0
      api-v8/resources/lang/zh-Hant/buttons.php
  35. 5 0
      api-v8/resources/lang/zh-Hant/labels.php
  36. 45 0
      api-v8/resources/views/blog/category.blade.php
  37. 453 0
      api-v8/resources/views/blog/index.blade.php
  38. 495 0
      api-v8/resources/views/blog/layouts/app.blade.php
  39. 24 0
      api-v8/resources/views/components/book-item.blade.php
  40. 118 0
      api-v8/resources/views/components/book-list.blade.php
  41. 29 0
      api-v8/resources/views/components/language-switcher.blade.php
  42. 477 0
      api-v8/resources/views/library/book/read.blade.php
  43. 135 0
      api-v8/resources/views/library/book/show.blade.php
  44. 59 0
      api-v8/resources/views/library/category.blade.php
  45. 42 0
      api-v8/resources/views/library/index.blade.php
  46. 192 0
      api-v8/resources/views/library/layouts/app.blade.php
  47. 28 0
      api-v8/routes/web.php
  48. 145 0
      dashboard-v4/dashboard/src/components/ai/ModelSelector.tsx
  49. 1 0
      dashboard-v4/dashboard/src/components/auth/setting/SettingArticle.tsx
  50. 28 2
      dashboard-v4/dashboard/src/components/auth/setting/default.ts
  51. 7 2
      dashboard-v4/dashboard/src/components/template/Term.tsx
  52. 6 0
      dashboard-v4/dashboard/src/locales/en-US/setting/index.ts
  53. 6 0
      dashboard-v4/dashboard/src/locales/zh-Hans/setting/index.ts

+ 1 - 1
api-v8/app/Console/Commands/MqAiTranslate.php

@@ -334,7 +334,7 @@ class MqAiTranslate extends Command
     {
         $url = config('app.url') . '/api/v2/task-status/' . $taskId;
         $data = [
-            'status' => 'done',
+            'status' => $status,
         ];
         Log::debug('ai_translate task status request', ['url' => $url, 'data' => $data]);
         $response = Http::timeout(10)->withToken($token)->patch($url, $data);

+ 74 - 76
api-v8/app/Console/Commands/UpgradePaliText.php

@@ -1,8 +1,10 @@
 <?php
+
 /**
  * 计算章节的父子,前后关系
  * 输入: csv文件
  */
+
 namespace App\Console\Commands;
 
 use Illuminate\Console\Command;
@@ -44,55 +46,53 @@ class UpgradePaliText extends Command
      */
     public function handle()
     {
-        if(\App\Tools\Tools::isStop()){
+        if (\App\Tools\Tools::isStop()) {
             return 0;
         }
-		$this->info("upgrade pali text");
-		$startTime = time();
-
-		$_from = $this->argument('from');
-		$_to = $this->argument('to');
-		if(empty($_from) && empty($_to)){
-			$_from = 1;
-			$_to = 217;
-		}else if(empty($_to)){
-			$_to = $_from;
-		}
-#载入文件列表
-		$fileListFileName = config("mint.path.palitext_filelist");
-
-		$filelist = array();
-
-		if (($handle = fopen($fileListFileName, 'r')) !== false) {
-			while (($filelist[] = fgetcsv($handle, 0, ',')) !== false) {
-			}
-		}
-
-		$bar = $this->output->createProgressBar($_to-$_from+1);
-
-		for ($from=$_from; $from <= $_to; $from++) {
-			$inputRow = 0;
-			$arrInserString = array();
-			#载入csv数据
-			$FileName = $filelist[$from-1][1];
-			$csvFile = config("mint.path.pali_title") .'/'. $from.'_pali.csv';
-			if (($fp = fopen($csvFile, "r")) !== false) {
+        $this->info("upgrade pali text");
+        $startTime = time();
+
+        $_from = $this->argument('from');
+        $_to = $this->argument('to');
+        if (empty($_from) && empty($_to)) {
+            $_from = 1;
+            $_to = 217;
+        } else if (empty($_to)) {
+            $_to = $_from;
+        }
+        #载入文件列表
+        $fileListFileName = config("mint.path.palitext_filelist");
+
+        $filelist = array();
+
+        if (($handle = fopen($fileListFileName, 'r')) !== false) {
+            while (($filelist[] = fgetcsv($handle, 0, ',')) !== false) {
+            }
+        }
+
+        for ($from = $_from; $from <= $_to; $from++) {
+            $this->info("book: " . $from);
+            $inputRow = 0;
+            $arrInserString = array();
+            #载入csv数据
+            $FileName = $filelist[$from - 1][1];
+            $csvFile = config("mint.path.pali_title") . '/' . $from . '_pali.csv';
+            if (($fp = fopen($csvFile, "r")) !== false) {
                 Log::info("csv load:" . $csvFile);
-				while (($data = fgetcsv($fp, 0, ',')) !== false) {
-					if ($inputRow > 0) {
-						array_push($arrInserString, $data);
-					}
-					$inputRow++;
-				}
-				fclose($fp);
-
-			} else {
-				$this->error( "can not open csv file. filename=" . $csvFile. PHP_EOL) ;
-				Log::error( "can not open csv file. filename=" . $csvFile) ;
-				continue;
-			}
-			$title_data = PaliText::select(['book','paragraph','level','parent','toc','lenght'])
-								->where('book',$from)->orderby('paragraph','asc')->get();
+                while (($data = fgetcsv($fp, 0, ',')) !== false) {
+                    if ($inputRow > 0) {
+                        array_push($arrInserString, $data);
+                    }
+                    $inputRow++;
+                }
+                fclose($fp);
+            } else {
+                $this->error("can not open csv file. filename=" . $csvFile . PHP_EOL);
+                Log::error("can not open csv file. filename=" . $csvFile);
+                continue;
+            }
+            $title_data = PaliText::select(['book', 'paragraph', 'level', 'parent', 'toc', 'lenght'])
+                ->where('book', $from)->orderby('paragraph', 'asc')->get();
 
             $paragraph_count = count($title_data);
             $paragraph_info = array();
@@ -104,7 +104,7 @@ class UpgradePaliText extends Command
             }
 
             for ($iPar = 0; $iPar < count($title_data); $iPar++) {
-                $book = $from ;
+                $book = $from;
                 $paragraph = $title_data[$iPar]["paragraph"];
                 $true_level = (int) $title_data[$iPar]["level"];
 
@@ -135,7 +135,7 @@ class UpgradePaliText extends Command
                 $prev = -1;
                 if ($iPar > 0) {
                     for ($iPar1 = $iPar - 1; $iPar1 >= 0; $iPar1--) {
-                        if ($title_data[$iPar1]["level"] < 8 && $title_data[$iPar1+1]["level"]==100) {
+                        if ($title_data[$iPar1]["level"] < 8 && $title_data[$iPar1 + 1]["level"] == 100) {
                             $prev = $title_data[$iPar1]["paragraph"];
                             break;
                         }
@@ -147,8 +147,8 @@ class UpgradePaliText extends Command
                 */
                 $next = -1;
                 if ($iPar < count($title_data) - 1) {
-                    for ($iPar1 = $iPar + 1; $iPar1 < count($title_data)-1; $iPar1++) {
-                        if ($title_data[$iPar1]["level"] <8 && $title_data[$iPar1+1]["level"]==100) {
+                    for ($iPar1 = $iPar + 1; $iPar1 < count($title_data) - 1; $iPar1++) {
+                        if ($title_data[$iPar1]["level"] < 8 && $title_data[$iPar1 + 1]["level"] == 100) {
                             $next = $title_data[$iPar1]["paragraph"];
                             break;
                         }
@@ -178,9 +178,9 @@ class UpgradePaliText extends Command
                     'next_chapter' => $next,
                     'prev_chapter' => $prev,
                     'parent' => $parent,
-                    'chapter_strlen'=> $iChapter_strlen,
+                    'chapter_strlen' => $iChapter_strlen,
                 ];
-                if((int)$arrInserString[$iPar][3] < 8){
+                if ((int)$arrInserString[$iPar][3] < 8) {
                     $newData['title'] = strtolower($arrInserString[$iPar][6]);
                     $newData['title_en'] = \App\Tools\Tools::getWordEn($newData['title']);
                 }
@@ -197,59 +197,57 @@ class UpgradePaliText extends Command
                 $currParent = $parent;
 
                 $iLoop = 0;
-                while ($currParent != -1 && $iLoop<7) {
+                while ($currParent != -1 && $iLoop < 7) {
                     # code...
-                    $pathTitle = $title_data[$currParent-1]["toc"];
-                    $pathLevel = $title_data[$currParent-1]['level'];
-                    $path[] = ["book"=>$book,"paragraph"=>$currParent,"title"=>$pathTitle,"level"=>$pathLevel];
-                    $currParent = $title_data[$currParent-1]["parent"];
+                    $pathTitle = $title_data[$currParent - 1]["toc"];
+                    $pathLevel = $title_data[$currParent - 1]['level'];
+                    $path[] = ["book" => $book, "paragraph" => $currParent, "title" => $pathTitle, "level" => $pathLevel];
+                    $currParent = $title_data[$currParent - 1]["parent"];
                     $iLoop++;
                 }
 
                 //插入书名
-                if(count($path)>0){
+                if (count($path) > 0) {
                     $bookPara = end($path)['paragraph'];
-                }else{
+                } else {
                     $bookPara = $paragraph;
                 }
 
-                $pcd_book = BookTitle::where('book',$book)
-                                    ->where('paragraph',$bookPara)
-                                    ->first();
-                if($pcd_book){
-                    if(empty($pcd_book)){
-                        Log::error('no pcd book:'.$book.'-'.$bookPara);
+                $pcd_book = BookTitle::where('book', $book)
+                    ->where('paragraph', $bookPara)
+                    ->first();
+                if ($pcd_book) {
+                    if (empty($pcd_book)) {
+                        Log::error('no pcd book:' . $book . '-' . $bookPara);
                     }
                     $book_id = $pcd_book->sn;
-                    if(!empty($book_id)){
+                    if (!empty($book_id)) {
                         $newData['pcd_book_id'] = $book_id;
                     }
-                    $path[] = ["book"=>$book_id,"paragraph"=>$book_id,"title"=>$pcd_book->title,"level"=>0];
+                    $path[] = ["book" => $book_id, "paragraph" => $book_id, "title" => $pcd_book->title, "level" => 0];
                 }
 
                 # 将路径反向
                 $path1 = [];
-                for ($i=count($path)-1; $i >=0 ; $i--) {
+                for ($i = count($path) - 1; $i >= 0; $i--) {
                     # code...
                     $path1[] = $path[$i];
                 }
                 $newData['path'] = $path1;
 
 
-                PaliText::where('book',$book)
-                        ->where('paragraph',$paragraph)
-                        ->update($newData);
+                PaliText::where('book', $book)
+                    ->where('paragraph', $paragraph)
+                    ->update($newData);
 
                 if ($curr_level > 0 && $curr_level < 8) {
                     $paragraph_info[] = array($book, $paragraph, $length, $prev, $next, $parent);
                 }
             }
-			$bar->advance();
-		}
-		$bar->finish();
+        }
 
-		$this->info("instert pali text finished. in ". time()-$startTime . "s" );
-		Log::info("instert pali text finished. in ". time()-$startTime . "s");
+        $this->info("all done in " . time() - $startTime . "s");
+        Log::info("all done in  " . time() - $startTime . "s");
         return 0;
     }
 }

+ 1 - 1
api-v8/app/Http/Api/TemplateRender.php

@@ -192,7 +192,7 @@ class TemplateRender
                 }
             }
             if (!isset($channelInfo)) {
-                Log::error('channel is null');
+                Log::warning('channel is null');
                 $output = [
                     "word" => $word,
                     'innerHtml' => '',

+ 174 - 0
api-v8/app/Http/Controllers/BlogController.php

@@ -0,0 +1,174 @@
+<?php
+// app/Http/Controllers/BlogController.php
+namespace App\Http\Controllers;
+
+use App\Models\Post;
+use App\Models\Category;
+use App\Models\Tag;
+use App\Models\ProgressChapter;
+
+use Illuminate\Http\Request;
+use Carbon\Carbon;
+use App\Http\Api\UserApi;
+use Illuminate\Support\Facades\Log;
+
+class BlogController extends Controller
+{
+    protected         $categories = [
+        ['id' => 'sutta', 'label' => 'sutta'],
+        ['id' => 'vinaya', 'label' => 'vinaya'],
+        ['id' => 'abhidhamma', 'label' => 'abhidhamma'],
+    ];
+    // 首页 - 最新博文列表
+    public function index($user)
+    {
+        $user = UserApi::getByName($user);
+        $posts = ProgressChapter::with('channel')
+            ->where('progress', '>', 0.9)
+            ->whereHas('channel', function ($query) use ($user) {
+                $query->where('status', 30)->where('owner_uid', $user['id']);
+            })
+            ->latest()
+            ->paginate(10);
+
+        Log::info($posts[0]->formatted_created_at);
+        $categories = $this->categories;
+        /*
+        $posts = Post::published()
+            ->with(['category', 'tags'])
+            ->latest()
+            ->paginate(10);
+
+        $categories = Category::withCount('posts')->get();
+        $popularPosts = Post::published()
+            ->orderBy('views_count', 'desc')
+            ->take(5)
+            ->get();
+*/
+        //return view('blog.index', compact('posts', 'categories', 'popularPosts'));
+        return view('blog.index', compact('user', 'posts', 'categories'));
+    }
+
+    /*
+    // 博文详情页
+    public function show(Post $post)
+    {
+        if (!$post->is_published) {
+            abort(404);
+        }
+
+        $post->incrementViews();
+        $post->load(['category', 'tags']);
+
+        // 相关文章
+        $relatedPosts = Post::published()
+            ->where('category_id', $post->category_id)
+            ->where('id', '!=', $post->id)
+            ->take(3)
+            ->get();
+
+        // 上一篇和下一篇
+        $prevPost = Post::published()
+            ->where('published_at', '<', $post->published_at)
+            ->latest()
+            ->first();
+
+        $nextPost = Post::published()
+            ->where('published_at', '>', $post->published_at)
+            ->oldest()
+            ->first();
+
+        return view('blog.show', compact('post', 'relatedPosts', 'prevPost', 'nextPost'));
+    }
+
+    // 分类列表
+    public function categories()
+    {
+        $categories = Category::withCount('posts')
+            ->having('posts_count', '>', 0)
+            ->orderBy('posts_count', 'desc')
+            ->get();
+
+        return view('blog.categories', compact('categories'));
+    }
+*/
+    // 分类下的文章
+    public function category(
+        $user,
+        $category1,
+        $category2 = null,
+        $category3 = null,
+        $category4 = null,
+        $category5 = null
+    ) {
+        $user = UserApi::getByName($user);
+        $posts = [];
+        $categories = $this->categories;
+
+        return view('blog.category', compact('user', 'categories', 'posts'));
+    }
+    /*
+    // 年度归档
+    public function archives()
+    {
+        $archives = Post::published()
+            ->selectRaw('YEAR(published_at) as year, COUNT(*) as count')
+            ->groupBy('year')
+            ->orderBy('year', 'desc')
+            ->get();
+
+        return view('blog.archives', compact('archives'));
+    }
+
+    // 指定年份的文章
+    public function archivesByYear($year)
+    {
+        $posts = Post::published()
+            ->whereYear('published_at', $year)
+            ->with(['category', 'tags'])
+            ->latest()
+            ->paginate(15);
+
+        // 按月分组
+        $postsByMonth = $posts->getCollection()->groupBy(function ($post) {
+            return $post->published_at->format('Y-m');
+        });
+
+        return view('blog.archives-year', compact('posts', 'postsByMonth', 'year'));
+    }
+
+    // 标签页面
+    public function tag(Tag $tag)
+    {
+        $posts = $tag->posts()
+            ->published()
+            ->with(['category', 'tags'])
+            ->latest()
+            ->paginate(10);
+
+        return view('blog.tag', compact('tag', 'posts'));
+    }
+
+    // 搜索
+    public function search(Request $request)
+    {
+        $query = $request->get('q');
+
+        if (empty($query)) {
+            return redirect()->route('blog.index');
+        }
+
+        $posts = Post::published()
+            ->where(function ($q) use ($query) {
+                $q->where('title', 'LIKE', "%{$query}%")
+                    ->orWhere('content', 'LIKE', "%{$query}%")
+                    ->orWhere('excerpt', 'LIKE', "%{$query}%");
+            })
+            ->with(['category', 'tags'])
+            ->latest()
+            ->paginate(10);
+
+        return view('blog.search', compact('posts', 'query'));
+    }
+        */
+}

+ 366 - 0
api-v8/app/Http/Controllers/BookController.php

@@ -0,0 +1,366 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Storage;
+use App\Models\ProgressChapter;
+use App\Models\PaliText;
+use App\Models\Sentence;
+
+class BookController extends Controller
+{
+    protected $maxChapterLen = 50000;
+    protected $minChapterLen = 100;
+    public function show($id)
+    {
+        $bookRaw = $this->loadBook($id);
+
+        if (!$bookRaw) {
+            abort(404);
+        }
+
+        //查询章节
+        $channelId = $bookRaw->channel_id; // 替换为具体的 channel_id 值
+
+        $book = $this->getBookInfo($bookRaw);
+        $book['contents'] = $this->getBookToc($bookRaw->book, $bookRaw->para, $channelId);
+
+        // 获取其他版本
+        $others = ProgressChapter::with('channel.owner')
+            ->where('book', $bookRaw->book)
+            ->where('para', $bookRaw->para)
+            ->whereHas('channel', function ($query) {
+                $query->where('status', 30);
+            })
+            ->where('progress', '>', 0.2)
+            ->get();
+
+        $otherVersions = [];
+        $others->each(function ($book) use (&$otherVersions, $id) {
+            $otherVersions[] = $this->getBookInfo($book);
+        });
+
+        return view('library.book.show', compact('book', 'otherVersions'));
+    }
+
+
+    public function read($id)
+    {
+        $bookRaw = $this->loadBook($id);
+
+        if (!$bookRaw) {
+            abort(404);
+        }
+        $channelId = $bookRaw->channel_id; // 替换为具体的 channel_id 值
+
+        $book = $this->getBookInfo($bookRaw);
+        $book['toc'] = $this->getBookToc($bookRaw->book, $bookRaw->para, $channelId, 2, 7);
+        $book['categories'] = $this->getBookCategory($bookRaw->book, $bookRaw->para);
+        $book['tags'] = [];
+        $book['pagination'] = $this->pagination($bookRaw);
+        $book['content'] = $this->getBookContent($id);
+        return view('library.book.read', compact('book'));
+    }
+
+    private function loadBook($id)
+    {
+        $book = ProgressChapter::with('channel.owner')->find($id);
+        return $book;
+    }
+
+    public function toggleTheme(Request $request)
+    {
+        $theme = $request->input('theme', 'light');
+        session(['theme' => $theme]);
+        return response()->json(['status' => 'success']);
+    }
+
+    private function getBookInfo($book)
+    {
+        $title = $book->title;
+        if (empty($title)) {
+            $title = PaliText::where('book', $book->book)
+                ->where('paragraph', $book->para)->first()->toc;
+        }
+        return [
+            "id" => $book->uid,
+            "title" => $title,
+            "author" => $book->channel->name,
+            "publisher" => $book->channel->owner->nickname,
+            "type" => __('label.' . $book->channel->type),
+            "category_id" => 11,
+            "cover" => "/assets/images/cover/1/214.jpg",
+            "description" => $book->summary ?? "",
+            "language" => __('language.' . $book->channel->lang),
+        ];
+    }
+
+    private function getBookToc(int $book, int $paragraph, string $channelId, $minLevel = 2, $maxLevel = 2)
+    {
+        //先找到书的起始(书名)章节
+        //一个book 里面可以有多本书
+        $currBook = $this->bookStart($book, $paragraph);
+        $start = $currBook->paragraph;
+        $end = $currBook->paragraph + $currBook->chapter_len - 1;
+        $paliTexts = PaliText::where('book', $book)
+            ->whereBetween('paragraph',  [$start, $end])
+            ->whereBetween('level', [$minLevel, $maxLevel])->orderBy('paragraph')->get();
+
+        $chapters = ProgressChapter::where('book', $book)
+            ->whereBetween('para', [$start, $end])
+            ->where('channel_id', $channelId)->orderBy('para')->get();
+
+        $toc = $paliTexts->map(function ($paliText) use ($chapters) {
+            $title = $paliText->toc;
+            if (count($chapters) > 0) {
+                $found = array_filter($chapters->toArray(), function ($chapter) use ($paliText) {
+                    return $chapter['book'] == $paliText->book && $chapter['para'] == $paliText->paragraph;
+                });
+                if (count($found) > 0) {
+                    $chapter = array_shift($found);
+                    if (!empty($chapter['title'])) {
+                        $title = $chapter['title'];
+                    }
+                    if (!empty($chapter['summary'])) {
+                        $summary = $chapter['summary'];
+                    }
+                    $progress = (int)($chapter['progress'] * 100);
+                    $id = $chapter['uid'];
+                }
+            }
+            return [
+                "id" => $id ?? '',
+                "title" => $title,
+                "summary" => $summary ?? "",
+                "progress" => $progress ?? 0,
+                "level" => (int)$paliText->level,
+                "disabled" => !isset($progress),
+            ];
+        })->toArray();
+        return $toc;
+    }
+
+    public function getBookCategory($book, $paragraph)
+    {
+        $tags = PaliText::with('tagMaps.tags')
+            ->where('book', $book)
+            ->where('paragraph', $paragraph)
+            ->first()->tagMaps->map(function ($tagMap) {
+                return $tagMap->tags;
+            })->toArray();
+        return $tags;
+    }
+
+    private function bookStart($book, $paragraph)
+    {
+        $currBook = PaliText::where('book', $book)
+            ->where('paragraph', '<=', $paragraph)
+            ->where('level', 1)
+            ->orderBy('paragraph', 'desc')
+            ->first();
+        return $currBook;
+    }
+    public function getBookContent($id)
+    {
+        //查询book信息
+        $book = $this->loadBook($id);
+        $currBook = $this->bookStart($book->book, $book->para);
+        $start = $currBook->paragraph;
+        $end = $currBook->paragraph + $currBook->chapter_len - 1;
+        // 查询起始段落
+        $paragraphs = PaliText::where('book', $book->book)
+            ->whereBetween('paragraph', [$start, $end])
+            ->where('level', '<', 8)
+            ->orderBy('paragraph')
+            ->get();
+        $curr = $paragraphs->firstWhere('paragraph', $book->para);
+        $endParagraph = $curr->paragraph + $curr->chapter_len - 1;
+        if ($curr->chapter_strlen > $this->maxChapterLen) {
+            //太大了,修改结束位置 找到下一级
+            foreach ($paragraphs as $key => $paragraph) {
+                if ($paragraph->paragraph > $curr->paragraph) {
+                    if ($paragraph->chapter_strlen <= $this->maxChapterLen) {
+                        $endParagraph = $paragraph->paragraph + $paragraph->chapter_len - 1;
+                        break;
+                    }
+                    if ($paragraph->level <= $curr->level) {
+                        //不能往下走了,就是它了
+                        $endParagraph = $paragraphs[$key - 1]->paragraph + $paragraphs[$key - 1]->chapter_len - 1;
+                        break;
+                    }
+                }
+            }
+        }
+
+        //获取句子数据
+        $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])
+            ->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);
+            $paragraph = [
+                'id' => $i,
+                'level' => $currPali->level,
+                'text' => [[implode('', $contents)]],
+            ];
+            $result[] = $paragraph;
+        }
+
+        return $result;
+    }
+
+    public function pagination($book)
+    {
+        $currBook = $this->bookStart($book->book, $book->para);
+        $start = $currBook->paragraph;
+        $end = $currBook->paragraph + $currBook->chapter_len - 1;
+        // 查询起始段落
+        $paragraphs = PaliText::where('book', $book->book)
+            ->whereBetween('paragraph', [$start, $end])
+            ->where('level', '<', 8)
+            ->orderBy('paragraph')
+            ->get();
+        $curr = $paragraphs->firstWhere('paragraph', $book->para);
+        $current = $curr; //实际显示的段落
+        $endParagraph = $curr->paragraph + $curr->chapter_len - 1;
+        if ($curr->chapter_strlen > $this->maxChapterLen) {
+            //太大了,修改结束位置 找到下一级
+            foreach ($paragraphs as $key => $paragraph) {
+                if ($paragraph->paragraph > $curr->paragraph) {
+                    if ($paragraph->chapter_strlen <= $this->maxChapterLen) {
+                        $endParagraph = $paragraph->paragraph + $paragraph->chapter_len - 1;
+                        $current = $paragraph;
+                        break;
+                    }
+                    if ($paragraph->level <= $curr->level) {
+                        //不能往下走了,就是它了
+                        $endParagraph = $paragraphs[$key - 1]->paragraph + $paragraphs[$key - 1]->chapter_len - 1;
+                        $current = $paragraph;
+                        break;
+                    }
+                }
+            }
+        }
+        $start = $curr->paragraph;
+        $end = $endParagraph;
+        $nextPali = $this->next($current->book, $current->paragraph, $current->level);
+        $prevPali = $this->prev($current->book, $current->paragraph, $current->level);
+
+        $next = null;
+        if ($nextPali) {
+            $nextTranslation = ProgressChapter::with('channel.owner')
+                ->where('book', $nextPali->book)
+                ->where('para', $nextPali->paragraph)
+                ->where('channel_id', $book->channel_id)
+                ->first();
+            if ($nextTranslation) {
+                if (!empty($nextTranslation->title)) {
+                    $next['title'] = $nextTranslation->title;
+                } else {
+                    $next['title'] = $nextPali->toc;
+                }
+                $next['id'] = $nextTranslation->uid;
+            }
+        }
+
+        $prev = null;
+        if ($prevPali) {
+            $prevTranslation = ProgressChapter::with('channel.owner')
+                ->where('book', $prevPali->book)
+                ->where('para', $prevPali->paragraph)
+                ->where('channel_id', $book->channel_id)
+                ->first();
+            if ($prevTranslation) {
+                if (!empty($prevTranslation->title)) {
+                    $prev['title'] = $prevTranslation->title;
+                } else {
+                    $prev['title'] = $prevPali->toc;
+                }
+                $prev['id'] = $prevTranslation->uid;
+            }
+        }
+
+        return compact('start', 'end', 'next', 'prev');
+    }
+
+    public function next($book, $paragraph, $level)
+    {
+        $next = PaliText::where('book', $book)
+            ->where('paragraph', '>', $paragraph)
+            ->where('level', $level)
+            ->orderBy('paragraph')
+            ->first();
+        return $next ?? null;
+    }
+    public function prev($book, $paragraph, $level)
+    {
+        $prev = PaliText::where('book', $book)
+            ->where('paragraph', '<', $paragraph)
+            ->where('level', $level)
+            ->orderBy('paragraph', 'desc')
+            ->first();
+
+        return $prev ?? null;
+    }
+    public function show2($id)
+    {
+        // Sample book data (replace with database query)
+        $book = [
+            'title' => 'Sample Book Title',
+            'author' => 'John Doe',
+            'category' => 'Fiction',
+            'tags' => ['Adventure', 'Mystery', 'Bestseller'],
+            'toc' => ['Introduction', 'Chapter 1', 'Chapter 2', 'Conclusion'],
+            'content' => [
+                'This is the introduction to the book...',
+                'Chapter 1 content goes here...',
+                'Chapter 2 content goes here...',
+                'Conclusion of the book...',
+            ],
+            'downloads' => [
+                ['format' => 'PDF', 'url' => '#'],
+                ['format' => 'EPUB', 'url' => '#'],
+                ['format' => 'MOBI', 'url' => '#'],
+            ],
+        ];
+
+        // Sample related books (replace with database query)
+        $relatedBooks = [
+            [
+                'title' => 'Related Book 1',
+                'description' => 'A thrilling adventure...',
+                'image' => 'https://via.placeholder.com/300x200',
+                'link' => '#',
+            ],
+            [
+                'title' => 'Related Book 2',
+                'description' => 'A mystery novel...',
+                'image' => 'https://via.placeholder.com/300x200',
+                'link' => '#',
+            ],
+            [
+                'title' => 'Related Book 3',
+                'description' => 'A bestseller...',
+                'image' => 'https://via.placeholder.com/300x200',
+                'link' => '#',
+            ],
+        ];
+
+        return view('library.book.read2', compact('book', 'relatedBooks'));
+    }
+}

+ 180 - 0
api-v8/app/Http/Controllers/CategoryController.php

@@ -0,0 +1,180 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Facades\DB;
+use App\Models\PaliText;
+use App\Models\ProgressChapter;
+use App\Models\Tag;
+use App\Models\TagMap;
+
+class CategoryController extends Controller
+{
+    protected static int $nextId = 1;
+    public function index()
+    {
+        $categories = $this->loadCategories();
+
+        // 获取一级分类和对应的书籍
+        $categoryData = [];
+        foreach ($categories as $category) {
+            if ($category['level'] == 1) {
+                $categoryBooks = $this->getBooks($categories, $category['id']);
+                $categoryData[] = [
+                    'category' => $category,
+                    'books' => array_slice(array_values($categoryBooks), 0, 3)
+                ];
+            }
+        }
+
+        return view('library.index', compact('categoryData', 'categories'));
+    }
+
+    public function show($id)
+    {
+        $categories = $this->loadCategories();
+
+        $currentCategory = collect($categories)->firstWhere('id', $id);
+        if (!$currentCategory) {
+            abort(404);
+        }
+
+        // 获取子分类
+        $subCategories = array_filter($categories, function ($cat) use ($id) {
+            return $cat['parent_id'] == $id;
+        });
+
+        // 获取该分类下的书籍
+        $categoryBooks = $this->getBooks($categories, $id);
+        // 获取面包屑
+        $breadcrumbs = $this->getBreadcrumbs($currentCategory, $categories);
+
+        return view('library.category', compact('currentCategory', 'subCategories', 'categoryBooks', 'breadcrumbs'));
+    }
+
+    private function getBooks($categories, $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
+                                    left join $tg as t on tm.tag_id = t.id
+                                    where tm.table_name  = 'pali_texts'
+                                    $in1
+                                    group by tm.anchor_id
+                            ) T
+                                $where1
+                        ) CID
+                        left join $pt as pt on CID.cid = pt.uid
+                        $where2
+                        order by book,paragraph";
+
+        $chapters = DB::select($query, $param);
+        $chaptersParam = [];
+        foreach ($chapters as $key => $chapter) {
+            $chaptersParam[] = [$chapter->book, $chapter->paragraph];
+        }
+        // 获取该分类下的章节
+        $books = ProgressChapter::with('channel.owner')->whereIns(['book', 'para'], $chaptersParam)
+            ->whereHas('channel', function ($query) {
+                $query->where('status', 30);
+            })
+            ->where('progress', '>', config('mint.library.list_min_progress'))
+            ->get();
+
+        $pali = PaliText::where('level', 1)->get();
+        // 获取该分类下的书籍
+        $categoryBooks = [];
+        $books->each(function ($book) use (&$categoryBooks, $id, $pali) {
+            $title = $book->title;
+            if (empty($title)) {
+                $title = $pali->firstWhere('book', $book->book)->toc;
+            }
+            $categoryBooks[] = [
+                "id" => $book->uid,
+                "title" => $title,
+                "author" => $book->channel->name,
+                "publisher" => $book->channel->owner->nickname,
+                "type" => __('labels.' . $book->channel->type),
+                "category_id" => $id,
+                "cover" => "/assets/images/cover/1/214.jpg",
+                "description" => $book->summary ?? "比库戒律的详细说明",
+                "language" => __('language.' . $book->channel->lang),
+                "contents" => [
+                    [
+                        "title" => "比库戒本",
+                        "content" => "诸恶莫作,众善奉行,自净其意,是诸佛教...",
+                        "summary" => "基本戒律",
+                    ]
+                ],
+            ];
+        });
+        return $categoryBooks;
+    }
+    private function loadCategories()
+    {
+        $json = file_get_contents(public_path("app/palicanon/category/default.json"));
+        $tree = json_decode($json, true);
+        $flat = self::flattenWithIds($tree);
+        return $flat;
+    }
+
+    public static function flattenWithIds(array $tree, int $parentId = 0, int $level = 1): array
+    {
+
+        $flat = [];
+
+        foreach ($tree as $node) {
+            $currentId = self::$nextId++;
+
+            $item = [
+                'id' => $currentId,
+                'parent_id' => $parentId,
+                'name' => $node['name'] ?? null,
+                'tag' => $node['tag'] ?? [],
+                "description" => "佛教戒律经典",
+                'level' => $level,
+            ];
+
+            $flat[] = $item;
+
+            if (isset($node['children']) && is_array($node['children'])) {
+                $childrenLevel = $level + 1;
+                $flat = array_merge($flat, self::flattenWithIds($node['children'], $currentId, $childrenLevel));
+            }
+        }
+
+        return $flat;
+    }
+
+    private function getBreadcrumbs($category, $categories)
+    {
+        $breadcrumbs = [];
+        $current = $category;
+
+        while ($current) {
+            array_unshift($breadcrumbs, $current);
+            $current = collect($categories)->firstWhere('id', $current['parent_id']);
+        }
+
+        return $breadcrumbs;
+    }
+}

+ 103 - 92
api-v8/app/Http/Controllers/CollectionController.php

@@ -22,123 +22,135 @@ class CollectionController extends Controller
     public function index(Request $request)
     {
 
-		$result=false;
-		$indexCol = ['uid','title','subtitle','summary',
-                      'article_list','owner','status',
-                      'default_channel','lang',
-                      'updated_at','created_at'];
-		switch ($request->get('view')) {
+        $result = false;
+        $indexCol = [
+            'uid',
+            'title',
+            'subtitle',
+            'summary',
+            'article_list',
+            'owner',
+            'status',
+            'default_channel',
+            'lang',
+            'updated_at',
+            'created_at'
+        ];
+        switch ($request->get('view')) {
             case 'studio_list':
-		        $indexCol = ['owner'];
+                $indexCol = ['owner'];
                 //TODO ?
                 $table = Collection::select($indexCol)
-                                    ->selectRaw('count(*) as count')
-                                    ->where('status', 30)
-                                    ->groupBy('owner');
+                    ->selectRaw('count(*) as count')
+                    ->where('status', 30)
+                    ->groupBy('owner');
                 break;
-			case 'studio':
+            case 'studio':
                 $user = AuthApi::current($request);
-                if(!$user){
+                if (!$user) {
                     return $this->error(__('auth.failed'));
                 }
                 $studioId = StudioApi::getIdByName($request->get('name'));
                 //判断当前用户是否有指定的studio的权限
-                if($user['user_uid'] !== $studioId){
+                if ($user['user_uid'] !== $studioId) {
                     return $this->error(__('auth.failed'));
                 }
                 $table = Collection::select($indexCol);
-                if($request->get('view2','my')==='my'){
+                if ($request->get('view2', 'my') === 'my') {
                     $table = $table->where('owner', $studioId);
-                }else{
+                } else {
                     //协作
-                    $resList = ShareApi::getResList($studioId,4);
-                    $resId=[];
+                    $resList = ShareApi::getResList($studioId, 4);
+                    $resId = [];
                     foreach ($resList as $res) {
                         $resId[] = $res['res_id'];
                     }
-                    $table = $table->whereIn('uid', $resId)->where('owner','<>', $studioId);
+                    $table = $table->whereIn('uid', $resId)->where('owner', '<>', $studioId);
                 }
-				break;
-			case 'public':
+                break;
+            case 'public':
                 //全网公开
-				$table = Collection::select($indexCol)->where('status', 30);
-                if($request->has('studio')){
+                $table = Collection::select($indexCol)->where('status', 30);
+                if ($request->has('studio')) {
                     $studioId = StudioApi::getIdByName($request->get('studio'));
-                    $table = $table->where('owner',$studioId);
+                    $table = $table->where('owner', $studioId);
                 }
-				break;
-			default:
-				# code...
-			    return $this->error("无法识别的view参数",200,200);
-				break;
-		}
-        if($request->has("search") && !empty($request->has("search"))){
-            $table = $table->where('title', 'like', "%".$request->get("search")."%");
+                break;
+            default:
+                # code...
+                return $this->error("无法识别的view参数", 200, 200);
+                break;
+        }
+        if ($request->has("search") && !empty($request->has("search"))) {
+            $table = $table->where('title', 'like', "%" . $request->get("search") . "%");
         }
         $count = $table->count();
-        if($request->has("order") && $request->has("dir")){
-            $table = $table->orderBy($request->get("order"),$request->get("dir"));
-        }else{
-            if($request->get('view') === 'studio_list'){
-                $table = $table->orderBy('count','desc');
-            }else{
-                $table = $table->orderBy('updated_at','desc');
+        if ($request->has("order") && $request->has("dir")) {
+            $table = $table->orderBy($request->get("order"), $request->get("dir"));
+        } else {
+            if ($request->get('view') === 'studio_list') {
+                $table = $table->orderBy('count', 'desc');
+            } else {
+                $table = $table->orderBy('updated_at', 'desc');
             }
         }
 
-        $table = $table->skip($request->get("offset",0))
-                       ->take($request->get("limit",1000));
+        $table = $table->skip($request->get("offset", 0))
+            ->take($request->get("limit", 1000));
 
         $result = $table->get();
-		return $this->ok(["rows"=>CollectionResource::collection($result),"count"=>$count]);
+        return $this->ok(["rows" => CollectionResource::collection($result), "count" => $count]);
     }
 
-            /**
+    /**
      * Display a listing of the resource.
      *
      * @return \Illuminate\Http\Response
      */
-    public function showMyNumber(Request $request){
+    public function showMyNumber(Request $request)
+    {
         $user = AuthApi::current($request);
-        if(!$user){
+        if (!$user) {
             return $this->error(__('auth.failed'));
         }
         //判断当前用户是否有指定的studio的权限
         $studioId = StudioApi::getIdByName($request->get('studio'));
-        if($user['user_uid'] !== $studioId){
+        if ($user['user_uid'] !== $studioId) {
             return $this->error(__('auth.failed'));
         }
         //我的
         $my = Collection::where('owner', $studioId)->count();
         //协作
-        $resList = ShareApi::getResList($studioId,4);
-        $resId=[];
+        $resList = ShareApi::getResList($studioId, 4);
+        $resId = [];
         foreach ($resList as $res) {
             $resId[] = $res['res_id'];
         }
-        $collaboration = Collection::whereIn('uid', $resId)->where('owner','<>', $studioId)->count();
+        $collaboration = Collection::whereIn('uid', $resId)->where('owner', '<>', $studioId)->count();
 
-        return $this->ok(['my'=>$my,'collaboration'=>$collaboration]);
+        return $this->ok(['my' => $my, 'collaboration' => $collaboration]);
     }
 
-    public static function UserCanEdit($user_uid,$collection){
-        if($collection->owner === $user_uid){
+    public static function UserCanEdit($user_uid, $collection)
+    {
+        if ($collection->owner === $user_uid) {
             return true;
         }
         //查协作
-        $currPower = ShareApi::getResPower($user_uid,$collection->uid);
-        if($currPower >= 20){
+        $currPower = ShareApi::getResPower($user_uid, $collection->uid);
+        if ($currPower >= 20) {
             return true;
         }
         return false;
     }
-    public static function UserCanRead($user_uid,$collection){
-        if($collection->owner === $user_uid){
+    public static function UserCanRead($user_uid, $collection)
+    {
+        if ($collection->owner === $user_uid) {
             return true;
         }
         //查协作
-        $currPower = ShareApi::getResPower($user_uid,$collection->uid);
-        if($currPower >= 10){
+        $currPower = ShareApi::getResPower($user_uid, $collection->uid);
+        if ($currPower >= 10) {
             return true;
         }
         return false;
@@ -152,17 +164,17 @@ class CollectionController extends Controller
     public function store(Request $request)
     {
         $user = \App\Http\Api\AuthApi::current($request);
-        if(!$user){
-            return $this->error(__('auth.failed'),401,401);
+        if (!$user) {
+            return $this->error(__('auth.failed'), 401, 401);
         }
         //判断当前用户是否有指定的studio的权限
-        if($user['user_uid'] !== \App\Http\Api\StudioApi::getIdByName($request->get('studio'))){
-            return $this->error(__('auth.failed'),403,403);
+        if ($user['user_uid'] !== \App\Http\Api\StudioApi::getIdByName($request->get('studio'))) {
+            return $this->error(__('auth.failed'), 403, 403);
         }
         //查询是否重复
-        if(Collection::where('title',$request->get('title'))->where('owner',$user['user_uid'])->exists()){
-            return $this->error(__('validation.exists'),200,200);
-        }else{
+        if (Collection::where('title', $request->get('title'))->where('owner', $user['user_uid'])->exists()) {
+            return $this->error(__('validation.exists'), 200, 200);
+        } else {
             $newOne = new Collection;
             $newOne->id = app('snowflake')->id();
             $newOne->uid = Str::uuid();
@@ -172,8 +184,8 @@ class CollectionController extends Controller
             $newOne->owner = $user['user_uid'];
             $newOne->owner_id = $user['user_id'];
             $newOne->editor_id = $user['user_id'];
-            $newOne->create_time = time()*1000;
-            $newOne->modify_time = time()*1000;
+            $newOne->create_time = time() * 1000;
+            $newOne->modify_time = time() * 1000;
             $newOne->save();
             return $this->ok(new CollectionResource($newOne));
         }
@@ -185,33 +197,32 @@ class CollectionController extends Controller
      * @param  string  $id
      * @return \Illuminate\Http\Response
      */
-    public function show(Request  $request,$id)
+    public function show(Request  $request, $id)
     {
-		$result  = Collection::where('uid', $id)->first();
-		if(!$result){
-            return $this->error("没有查询到数据");
+        $result  = Collection::where('uid', $id)->first();
+        if (!$result) {
+            return $this->warning("没有查询到数据 id={$id}");
         }
-        if($result->status<30){
+        if ($result->status < 30) {
             //私有文章,判断权限
-            Log::error('私有文章,判断权限'.$id);
+            Log::info('私有文章,判断权限' . $id);
             $user = \App\Http\Api\AuthApi::current($request);
-            if(!$user){
-                Log::error('未登录');
-                return $this->error(__('auth.failed'),401,401);
+            if (!$user) {
+                Log::warning('未登录');
+                return $this->error(__('auth.failed'), 403, 403);
             }
             //判断当前用户是否有指定的studio的权限
-            if($user['user_uid'] !== $result->owner){
-                Log::error($user["user_uid"].'私有文章,判断权限'.$id);
+            if ($user['user_uid'] !== $result->owner) {
+                Log::info($user["user_uid"] . '私有文章,判断权限' . $id);
                 //非所有者
-                if(CollectionController::UserCanRead($user['user_uid'],$result)===false){
-                    Log::error($user["user_uid"].'没有读取权限');
-                    return $this->error(__('auth.failed'),403,403);
+                if (CollectionController::UserCanRead($user['user_uid'], $result) === false) {
+                    Log::warning($user["user_uid"] . '没有读取权限');
+                    return $this->error(__('auth.failed'), 403, 403);
                 }
             }
         }
         $result->fullArticleList = true;
         return $this->ok(new CollectionResource($result));
-
     }
 
     /**
@@ -225,27 +236,27 @@ class CollectionController extends Controller
     {
         //
         $collection  = Collection::find($id);
-        if(!$collection){
+        if (!$collection) {
             return $this->error("no recorder");
         }
         //鉴权
         $user = AuthApi::current($request);
-        if(!$user){
-            return $this->error(__('auth.failed'),401,401);
+        if (!$user) {
+            return $this->error(__('auth.failed'), 401, 401);
         }
-        if(!CollectionController::UserCanEdit($user["user_uid"],$collection)){
-            return $this->error(__('auth.failed'),403,403);
+        if (!CollectionController::UserCanEdit($user["user_uid"], $collection)) {
+            return $this->error(__('auth.failed'), 403, 403);
         }
         $collection->title = $request->get('title');
         $collection->subtitle = $request->get('subtitle');
         $collection->summary = $request->get('summary');
-        if($request->has('aritcle_list')){
+        if ($request->has('aritcle_list')) {
             $collection->article_list = \json_encode($request->get('aritcle_list'));
         }
         $collection->lang = $request->get('lang');
         $collection->status = $request->get('status');
         $collection->default_channel = $request->get('default_channel');
-        $collection->modify_time = time()*1000;
+        $collection->modify_time = time() * 1000;
         $collection->save();
         return $this->ok(new CollectionResource($collection));
     }
@@ -256,20 +267,20 @@ class CollectionController extends Controller
      * @param  string  $id
      * @return \Illuminate\Http\Response
      */
-    public function destroy(Request $request,string $id)
+    public function destroy(Request $request, string $id)
     {
         //
         $user = AuthApi::current($request);
-        if(!$user){
+        if (!$user) {
             return $this->error(__('auth.failed'));
         }
         //判断当前用户是否有指定的studio的权限
         $collection = Collection::find($id);
-        if($user['user_uid'] !== $collection['owner']){
+        if ($user['user_uid'] !== $collection['owner']) {
             return $this->error(__('auth.failed'));
         }
         $delete = 0;
-        DB::transaction(function() use($collection,$delete){
+        DB::transaction(function () use ($collection, $delete) {
             //TODO 删除文集中的文章
             $delete = $collection->delete();
         });

+ 68 - 71
api-v8/app/Http/Controllers/PaliTextController.php

@@ -8,9 +8,7 @@ use App\Models\BookTitle;
 use App\Models\Tag;
 use App\Models\TagMap;
 use Illuminate\Http\Request;
-use Illuminate\Support\Facades\Cache;
 use App\Tools\RedisClusters;
-use Illuminate\Support\Facades\Log;
 
 class PaliTextController extends Controller
 {
@@ -28,22 +26,22 @@ class PaliTextController extends Controller
                 $tm = (new TagMap)->getTable();
                 $tg = (new Tag)->getTable();
                 $pt = (new PaliText)->getTable();
-                if($request->get('tags') && $request->get('tags')!==''){
-                    $tags = explode(',',$request->get('tags'));
+                if ($request->get('tags') && $request->get('tags') !== '') {
+                    $tags = explode(',', $request->get('tags'));
                     foreach ($tags as $tag) {
                         # code...
-                        if(!empty($tag)){
+                        if (!empty($tag)) {
                             $tagNames[] = $tag;
                         }
                     }
                 }
 
-                if(isset($tagNames)){
-                    $where1 = " where co = ".count($tagNames);
-                    $a = implode(",",array_fill(0, count($tagNames), '?')) ;
+                if (isset($tagNames)) {
+                    $where1 = " where co = " . count($tagNames);
+                    $a = implode(",", array_fill(0, count($tagNames), '?'));
                     $in1 = "and t.name in ({$a})";
                     $param  = $tagNames;
-                }else{
+                } else {
                     $where1 = " ";
                     $in1 = " ";
                 }
@@ -68,36 +66,35 @@ class PaliTextController extends Controller
                         left join $tg on $tg.id = tid.tag_id
                         order by count desc
                     ";
-                    if(isset($param)){
-                        $chapters = DB::select($query,$param);
-                    }else{
-                        $chapters = DB::select($query);
-                    }
-                    $all_count = count($chapters);
+                if (isset($param)) {
+                    $chapters = DB::select($query, $param);
+                } else {
+                    $chapters = DB::select($query);
+                }
+                $all_count = count($chapters);
                 break;
 
             case 'chapter':
-                $tm = (new TagMap)->getTable();
-                $tg = (new Tag)->getTable();
-                $pt = (new PaliText)->getTable();
-                if($request->get('tags') && $request->get('tags')!==''){
-                    $tags = explode(',',$request->get('tags'));
+                if ($request->get('tags') && $request->get('tags') !== '') {
+                    $tags = explode(',', $request->get('tags'));
                     foreach ($tags as $tag) {
                         # code...
-                        if(!empty($tag)){
+                        if (!empty($tag)) {
                             $tagNames[] = $tag;
                         }
                     }
                 }
 
-
-                if(isset($tagNames)){
-                    $where1 = " where co = ".count($tagNames);
-                    $a = implode(",",array_fill(0, count($tagNames), '?')) ;
+                $tm = (new TagMap)->getTable();
+                $tg = (new Tag)->getTable();
+                $pt = (new PaliText)->getTable();
+                if (isset($tagNames)) {
+                    $where1 = " where co = " . count($tagNames);
+                    $a = implode(",", array_fill(0, count($tagNames), '?'));
                     $in1 = "and t.name in ({$a})";
                     $param = $tagNames;
                     $where2 = "where level < 3";
-                }else{
+                } else {
                     $where1 = " ";
                     $in1 = " ";
                     $where2 = "where level = 1";
@@ -118,28 +115,28 @@ class PaliTextController extends Controller
                         $where2
                         order by book,paragraph";
 
-                    if(isset($param)){
-                        $chapters = DB::select($query,$param);
-                    }else{
-                        $chapters = DB::select($query);
-                    }
+                if (isset($param)) {
+                    $chapters = DB::select($query, $param);
+                } else {
+                    $chapters = DB::select($query);
+                }
 
                 $all_count = count($chapters);
                 break;
             case 'chapter_children':
-                $table = PaliText::where('book',$request->get('book'))
-                                ->where('parent',$request->get('para'))
-                                ->where('level','<',8);
+                $table = PaliText::where('book', $request->get('book'))
+                    ->where('parent', $request->get('para'))
+                    ->where('level', '<', 8);
                 $all_count = $table->count();
                 $chapters = $table->orderBy('paragraph')->get();
                 break;
             case 'paragraph':
-                $result = PaliText::where('book',$request->get('book'))
-                                  ->where('paragraph',$request->get('para'))
-                                  ->first();
-                if($result){
+                $result = PaliText::where('book', $request->get('book'))
+                    ->where('paragraph', $request->get('para'))
+                    ->first();
+                if ($result) {
                     return $this->ok($result);
-                }else{
+                } else {
                     return $this->error("no data");
                 }
                 break;
@@ -156,75 +153,75 @@ class PaliTextController extends Controller
                  * 4. 获取全部书的目录
                  */
 
-                if($request->has('series')){
+                if ($request->has('series')) {
                     $book_title = $request->get('series');
                     //获取丛书书目列表
-                    $books = BookTitle::where('title',$request->get('series'))->get();
-                }else{
+                    $books = BookTitle::where('title', $request->get('series'))->get();
+                } else {
                     //查询这个目录的顶级目录
-                    $path = PaliText::where('book',$request->get('book'))
-                                    ->where('paragraph',$request->get('para'))
-                                    ->select('path')->first();
-                    if(!$path){
+                    $path = PaliText::where('book', $request->get('book'))
+                        ->where('paragraph', $request->get('para'))
+                        ->select('path')->first();
+                    if (!$path) {
                         return $this->error("no data");
                     }
                     $json = \json_decode($path->path);
                     $root = null;
                     foreach ($json as $key => $value) {
                         # code...
-                        if( $value->level == 1 ){
+                        if ($value->level == 1) {
                             $root = $value;
                             break;
                         }
                     }
-                    if($root===null){
+                    if ($root === null) {
                         return $this->error("no data");
                     }
                     //查询书起始段落
-                    $rootPara = PaliText::where('book',$root->book)
-                                    ->where('paragraph',$root->paragraph)
-                                    ->first();
+                    $rootPara = PaliText::where('book', $root->book)
+                        ->where('paragraph', $root->paragraph)
+                        ->first();
                     //获取丛书书名
-                    $book_title = BookTitle::where('book',$rootPara->book)
-                                            ->where('paragraph',$rootPara->paragraph)
-                                            ->value('title');
+                    $book_title = BookTitle::where('book', $rootPara->book)
+                        ->where('paragraph', $rootPara->paragraph)
+                        ->value('title');
                     //获取丛书书目列表
-                    $books = BookTitle::where('title',$book_title)->get();
+                    $books = BookTitle::where('title', $book_title)->get();
                 }
 
 
                 $chapters = [];
-                $chapters[] = ['book'=>0,'paragraph'=>0,'toc'=>$book_title,'level'=>1];
+                $chapters[] = ['book' => 0, 'paragraph' => 0, 'toc' => $book_title, 'level' => 1];
                 foreach ($books as  $book) {
                     # code...
-                    $rootPara = PaliText::where('book',$book->book)
-                                ->where('paragraph',$book->paragraph)
-                                ->first();
-                    $table = PaliText::where('book',$rootPara->book)
-                                    ->whereBetween('paragraph',[$rootPara->paragraph,($rootPara->paragraph+$rootPara->chapter_len-1)])
-                                    ->where('level','<',8);
+                    $rootPara = PaliText::where('book', $book->book)
+                        ->where('paragraph', $book->paragraph)
+                        ->first();
+                    $table = PaliText::where('book', $rootPara->book)
+                        ->whereBetween('paragraph', [$rootPara->paragraph, ($rootPara->paragraph + $rootPara->chapter_len - 1)])
+                        ->where('level', '<', 8);
                     $all_count = $table->count();
-                    $curr_chapters = $table->select(['book','paragraph','toc','level'])->orderBy('paragraph')->get();
+                    $curr_chapters = $table->select(['book', 'paragraph', 'toc', 'level'])->orderBy('paragraph')->get();
                     foreach ($curr_chapters as  $chapter) {
                         # code...
-                        $chapters[] = ['book'=>$chapter->book,'paragraph'=>$chapter->paragraph,'toc'=>$chapter->toc,'level'=>($chapter->level+1)];
+                        $chapters[] = ['book' => $chapter->book, 'paragraph' => $chapter->paragraph, 'toc' => $chapter->toc, 'level' => ($chapter->level + 1)];
                     }
                 }
 
                 break;
-            }
-        if($chapters){
-            if($request->get('view') !== 'book-toc'){
+        }
+        if ($chapters) {
+            if ($request->get('view') !== 'book-toc') {
                 foreach ($chapters as $key => $value) {
-                    if(is_object($value)){
+                    if (is_object($value)) {
                         //TODO $value->book 可能不存在
-                        $progress_key="/chapter_dynamic/{$value->book}/{$value->paragraph}/global";
+                        $progress_key = "/chapter_dynamic/{$value->book}/{$value->paragraph}/global";
                         $chapters[$key]->progress_line = RedisClusters::get($progress_key);
                     }
                 }
             }
-            return $this->ok(["rows"=>$chapters,"count"=>$all_count]);
-        }else{
+            return $this->ok(["rows" => $chapters, "count" => $all_count]);
+        } else {
             return $this->error("no data");
         }
     }

+ 168 - 162
api-v8/app/Http/Controllers/ProgressChapterController.php

@@ -13,7 +13,6 @@ use App\Models\View;
 use App\Models\Like;
 use Illuminate\Http\Request;
 use App\Http\Api\StudioApi;
-use Illuminate\Support\Facades\Cache;
 use App\Tools\RedisClusters;
 
 class ProgressChapterController extends Controller
@@ -26,58 +25,57 @@ class ProgressChapterController extends Controller
     public function index(Request $request)
     {
 
-        $minProgress = (float)$request->get('progress',0.8);
+        $minProgress = (float)$request->get('progress', 0.8);
 
-        $offset = (int)$request->get('offset',0);
+        $offset = (int)$request->get('offset', 0);
 
-        $limit = (int)$request->get('limit',20);
+        $limit = (int)$request->get('limit', 20);
 
         $channel_id = $request->get('channel');
 
         //
 
-        $chapters=false;
+        $chapters = false;
         switch ($request->get('view')) {
             case 'ids':
-                $aChannel = explode(',',$request->get('channel'));
+                $aChannel = explode(',', $request->get('channel'));
                 $chapters = ProgressChapter::select("channel_id")->selectRaw("uid as id")
-                                         ->with(['channel' => function($query) {  //city对应上面province模型中定义的city方法名  闭包内是子查询
-                                                return $query->select('*');
-                                            }])
-                                        ->where("book",$request->get('book'))
-                                        ->where("para",$request->get('par'))
-                                        ->whereIn('channel_id', $aChannel)->get();
+                    ->with(['channel' => function ($query) {  //city对应上面province模型中定义的city方法名  闭包内是子查询
+                        return $query->select('*');
+                    }])
+                    ->where("book", $request->get('book'))
+                    ->where("para", $request->get('par'))
+                    ->whereIn('channel_id', $aChannel)->get();
                 $all_count = count($chapters);
                 break;
-			case 'studio':
+            case 'studio':
                 #查询该studio的channel
-				$name = $request->get('name');
+                $name = $request->get('name');
                 $studioId = StudioApi::getIdByName($request->get('name'));
-				if($studioId === false){
-					return $this->error('no user');
-				}
-                $table = Channel::where('owner_uid',$studioId);
-                if($request->get('public')==="true"){
-                    $table = $table->where('status',30);
+                if ($studioId === false) {
+                    return $this->error('no user');
+                }
+                $table = Channel::where('owner_uid', $studioId);
+                if ($request->get('public') === "true") {
+                    $table = $table->where('status', 30);
                 }
                 $channels = $table->select('uid')->get();
                 $chapters = ProgressChapter::whereIn('progress_chapters.channel_id', $channels)
-                                        ->leftJoin('pali_texts', function($join)
-                                                {
-                                                    $join->on('progress_chapters.book', '=', 'pali_texts.book');
-                                                    $join->on('progress_chapters.para','=','pali_texts.paragraph');
-                                                })
-                                        ->where('progress','>',0.85)
-                                        ->orderby('progress_chapters.created_at','desc')
-                                        ->skip($request->get("offset",0))
-                                        ->take($request->get("limit",1000))
-                                        ->get();
-				$all_count = ProgressChapter::whereIn('progress_chapters.channel_id', $channels)
-									->where('progress','>',0.85)->count();
+                    ->leftJoin('pali_texts', function ($join) {
+                        $join->on('progress_chapters.book', '=', 'pali_texts.book');
+                        $join->on('progress_chapters.para', '=', 'pali_texts.paragraph');
+                    })
+                    ->where('progress', '>', 0.85)
+                    ->orderby('progress_chapters.created_at', 'desc')
+                    ->skip($request->get("offset", 0))
+                    ->take($request->get("limit", 1000))
+                    ->get();
+                $all_count = ProgressChapter::whereIn('progress_chapters.channel_id', $channels)
+                    ->where('progress', '>', 0.85)->count();
                 break;
             case 'tag':
                 $tm = (new TagMap)->getTable();
-                $pc =(new ProgressChapter)->getTable();
+                $pc = (new ProgressChapter)->getTable();
                 $t = (new Tag)->getTable();
                 $query = "select t.name,count(*) from $tm  tm
                             join tags as t on tm.tag_id = t.id
@@ -86,40 +84,39 @@ class ProgressChapterController extends Controller
                             pc.progress > ?
                             group by t.name;";
                 $chapters = DB::select($query, [$minProgress]);
-                if($chapters){
+                if ($chapters) {
                     $all_count = count($chapters);
-                }else{
+                } else {
                     $all_count = 0;
                 }
                 break;
             case 'chapter-tag':
-                $tm = (new TagMap)->getTable();
-                $pc =(new ProgressChapter)->getTable();
-                $tg = (new Tag)->getTable();
-                $pt = (new PaliText)->getTable();
-                if($request->get('tags') && $request->get('tags')!==''){
-                    $tags = explode(',',$request->get('tags'));
+                if ($request->get('tags') && $request->get('tags') !== '') {
+                    $tags = explode(',', $request->get('tags'));
                     foreach ($tags as $tag) {
                         # code...
-                        if(!empty($tag)){
+                        if (!empty($tag)) {
                             $tagNames[] = $tag;
                         }
                     }
                 }
-
+                $tm = (new TagMap)->getTable();
+                $pc = (new ProgressChapter)->getTable();
+                $tg = (new Tag)->getTable();
+                $pt = (new PaliText)->getTable();
                 $param[] = $minProgress;
-                if(isset($tagNames)){
-                    $where1 = " where co = ".count($tagNames);
-                    $a = implode(",",array_fill(0, count($tagNames), '?')) ;
+                if (isset($tagNames)) {
+                    $where1 = " where co = " . count($tagNames);
+                    $a = implode(",", array_fill(0, count($tagNames), '?'));
                     $in1 = "and t.name in ({$a})";
                     $param  = array_merge($param, $tagNames);
-                }else{
+                } else {
                     $where1 = " ";
                     $in1 = " ";
                 }
-                if(Str::isUuid($channel_id)){
+                if (Str::isUuid($channel_id)) {
                     $channel = "and channel_id = '{$channel_id}' ";
-                }else{
+                } else {
                     $channel = "";
                 }
 
@@ -146,88 +143,97 @@ class ProgressChapterController extends Controller
                         left join $tg on $tg.id = tid.tag_id
                         order by count desc
                     ";
-                    if(isset($param)){
-                        $chapters = DB::select($query,$param);
-                    }else{
-                        $chapters = DB::select($query);
-                    }
-                    $all_count = count($chapters);
+                if (isset($param)) {
+                    $chapters = DB::select($query, $param);
+                } else {
+                    $chapters = DB::select($query);
+                }
+                $all_count = count($chapters);
                 break;
             case 'lang':
 
                 $chapters = ProgressChapter::select('lang')
-                                            ->selectRaw('count(*) as count')
-                                            ->where("progress",">",$minProgress)
-                                            ->groupBy('lang')
-                                            ->get();
+                    ->selectRaw('count(*) as count')
+                    ->where("progress", ">", $minProgress)
+                    ->groupBy('lang')
+                    ->get();
                 $all_count = count($chapters);
                 break;
             case 'channel-type':
                 break;
             case 'channel':
-            /**
-             * 总共有多少channel
-             */
+                /**
+                 * 总共有多少channel
+                 */
                 $chapters = ProgressChapter::select('channel_id')
-                                           ->selectRaw('count(*) as count')
-                                           ->with(['channel' => function($query) {
-                                                return $query->select('*');
-                                            }])
-                                           ->leftJoin('channels','progress_chapters.channel_id', '=', 'channels.uid')
-                                           ->where("progress",">",$minProgress)
-										   ->where('channels.status','>=',30);
-                if(!empty($request->get('channel_type'))){
-                    $chapters =  $chapters->where('channels.type',$request->get('channel_type'));
+                    ->selectRaw('count(*) as count')
+                    ->with(['channel' => function ($query) {
+                        return $query->select('*');
+                    }])
+                    ->leftJoin('channels', 'progress_chapters.channel_id', '=', 'channels.uid')
+                    ->where("progress", ">", $minProgress)
+                    ->where('channels.status', '>=', 30);
+                if (!empty($request->get('channel_type'))) {
+                    $chapters =  $chapters->where('channels.type', $request->get('channel_type'));
                 }
-                if(!empty($request->get('lang'))){
-                    $chapters =  $chapters->where('progress_chapters.lang',$request->get('lang'));
+                if (!empty($request->get('lang'))) {
+                    $chapters =  $chapters->where('progress_chapters.lang', $request->get('lang'));
                 }
                 $chapters =  $chapters->groupBy('channel_id')
-                                            ->orderBy('count','desc')
-                                            ->get();
+                    ->orderBy('count', 'desc')
+                    ->get();
                 foreach ($chapters as $key => $chapter) {
                     $chapter->studio = StudioApi::getById($chapter->channel->owner_uid);
                 }
                 $all_count = count($chapters);
                 break;
             case 'chapter_channels':
-            /**
-             * 某个章节 有多少channel
-             */
+                /**
+                 * 某个章节 有多少channel
+                 */
 
-                $chapters = ProgressChapter::select('book','para','progress_chapters.uid',
-                                                    'progress_chapters.channel_id','progress',
-                                                    'channels.name','channels.type','channels.owner_uid',
-                                                    'progress_chapters.updated_at')
-                                            ->leftJoin('channels','progress_chapters.channel_id', '=', 'channels.uid')
-                                            ->where("book",$request->get('book'))
-                                            ->where("para",$request->get('par'))
-                                            ->orderBy('progress','desc')
-                                            ->get();
+                $chapters = ProgressChapter::select(
+                    'book',
+                    'para',
+                    'progress_chapters.uid',
+                    'progress_chapters.channel_id',
+                    'progress',
+                    'channels.name',
+                    'channels.type',
+                    'channels.owner_uid',
+                    'progress_chapters.updated_at'
+                )
+                    ->leftJoin('channels', 'progress_chapters.channel_id', '=', 'channels.uid')
+                    ->where("book", $request->get('book'))
+                    ->where("para", $request->get('par'))
+                    ->orderBy('progress', 'desc')
+                    ->get();
                 foreach ($chapters as $key => $value) {
                     # code...
-                    $chapters[$key]->views = View::where("target_id",$value->uid)->count();
+                    $chapters[$key]->views = View::where("target_id", $value->uid)->count();
 
-                    $likes = Like::where("target_id",$value->uid)
-                                ->groupBy("type")
-                                ->select("type")
-                                ->selectRaw("count(*)")
-                                ->get();
-                    if(isset($_COOKIE["user_uid"])){
+                    $likes = Like::where("target_id", $value->uid)
+                        ->groupBy("type")
+                        ->select("type")
+                        ->selectRaw("count(*)")
+                        ->get();
+                    if (isset($_COOKIE["user_uid"])) {
                         foreach ($likes as $key1 => $like) {
                             # 查看这些点赞里有没有我点的
-                            $myLikeId =Like::where(["target_id"=>$value->uid,
-                                            'type'=>$like->type,
-                                            'user_id'=>$_COOKIE["user_uid"]])->value('id');
-                            if($myLikeId){
+                            $myLikeId = Like::where([
+                                "target_id" => $value->uid,
+                                'type' => $like->type,
+                                'user_id' => $_COOKIE["user_uid"]
+                            ])->value('id');
+                            if ($myLikeId) {
                                 $likes[$key1]->selected = $myLikeId;
                             }
                         }
                     }
                     $chapters[$key]->likes = $likes;
                     $chapters[$key]->studio = StudioApi::getById($value->owner_uid);
-                    $chapters[$key]->channel = ['uid'=>$value->channel_id,'name'=>$value->name,'type'=>$value->type];
-                    $progress_key="/chapter_dynamic/{$value->book}/{$value->para}/ch_{$value->channel_id}";
+                    $chapters[$key]->channel = ['uid' => $value->channel_id, 'name' => $value->name, 'type' => $value->type];
+                    $progress_key = "/chapter_dynamic/{$value->book}/{$value->para}/ch_{$value->channel_id}";
                     $chapters[$key]->progress_line = RedisClusters::get($progress_key);
                 }
 
@@ -235,48 +241,48 @@ class ProgressChapterController extends Controller
                 break;
             case 'chapter':
                 $tm = (new TagMap)->getTable();
-                $pc =(new ProgressChapter)->getTable();
+                $pc = (new ProgressChapter)->getTable();
                 $tg = (new Tag)->getTable();
                 $pt = (new PaliText)->getTable();
 
                 //标签过滤
-                if($request->has('tags') && !empty($request->get('tags'))){
-                    $tags = explode(',',$request->get('tags'));
+                if ($request->has('tags') && !empty($request->get('tags'))) {
+                    $tags = explode(',', $request->get('tags'));
                     foreach ($tags as $tag) {
                         # code...
-                        if(!empty($tag)){
+                        if (!empty($tag)) {
                             $tagNames[] = $tag;
                         }
                     }
                 }
-                if(isset($tagNames)){
-                    $where1 = " where co = ".count($tagNames);
-                    $a = implode(",",array_fill(0, count($tagNames), '?')) ;
+                if (isset($tagNames)) {
+                    $where1 = " where co = " . count($tagNames);
+                    $a = implode(",", array_fill(0, count($tagNames), '?'));
                     $in1 = "and t.name in ({$a})";
                     $param = $tagNames;
-                }else{
+                } else {
                     $where1 = " ";
                     $in1 = " ";
                 }
-                if($request->has('studio')){
+                if ($request->has('studio')) {
                     $studioId = StudioApi::getIdByName($request->get('studio'));
-                    $table = Channel::where('owner_uid',$studioId);
-                    if($request->get('public')==="true"){
-                        $table = $table->where('status',30);
+                    $table = Channel::where('owner_uid', $studioId);
+                    if ($request->get('public') === "true") {
+                        $table = $table->where('status', 30);
                     }
                     $channels = $table->select('uid')->get();
                     $arrChannel = [];
                     foreach ($channels as $oneChannel) {
                         # code...
-                        if(Str::isUuid($oneChannel->uid)){
+                        if (Str::isUuid($oneChannel->uid)) {
                             $arrChannel[] = "'{$oneChannel->uid}'";
                         }
                     }
-                    $channel = "and channel_id in (".implode(',',$arrChannel).") ";
-                }else{
-                    if(Str::isUuid($channel_id)){
+                    $channel = "and channel_id in (" . implode(',', $arrChannel) . ") ";
+                } else {
+                    if (Str::isUuid($channel_id)) {
                         $channel = "and channel_id = '{$channel_id}' ";
-                    }else{
+                    } else {
                         $channel = "";
                     }
                 }
@@ -285,19 +291,19 @@ class ProgressChapterController extends Controller
                 $param[] = $minProgress;
 
                 //语言过滤
-                if(!empty($request->get('lang'))){
+                if (!empty($request->get('lang'))) {
                     $whereLang = " and pc.lang = ? ";
                     $param[] = $request->get('lang');
-                }else{
+                } else {
                     $whereLang = "   ";
                 }
                 //channel type过滤
-				if($request->has('channel_type') && !empty($request->get('channel_type'))){
-					$channel_type = "and ch.type = ? ";
-					$param[] = $request->get('channel_type');
-				}else{
-					$channel_type = "";
-				}
+                if ($request->has('channel_type') && !empty($request->get('channel_type'))) {
+                    $channel_type = "and ch.type = ? ";
+                    $param[] = $request->get('channel_type');
+                } else {
+                    $channel_type = "";
+                }
 
                 $param_count = $param;
                 $param[] = $offset;
@@ -331,17 +337,17 @@ class ProgressChapterController extends Controller
                         limit {$limit} offset ?
                     ) tpc
                     left join $pt as pt on tpc.book = pt.book and tpc.para = pt.paragraph;";
-                $chapters = DB::select($query,$param);
+                $chapters = DB::select($query, $param);
                 foreach ($chapters as $key => $chapter) {
                     # code...
-                    $chapter->channel = Channel::where('uid',$chapter->channel_id)->select(['name','owner_uid'])->first();
+                    $chapter->channel = Channel::where('uid', $chapter->channel_id)->select(['name', 'owner_uid'])->first();
                     $chapter->studio = StudioApi::getById($chapter->channel["owner_uid"]);
-                    $chapter->views = View::where("target_id",$chapter->uid)->count();
-                    $chapter->likes = Like::where(["type"=>"like","target_id"=>$chapter->uid])->count();
-                    $chapter->tags = TagMap::where("anchor_id",$chapter->uid)
-                                                ->leftJoin('tags','tag_maps.tag_id', '=', 'tags.id')
-                                                ->select(['tags.id','tags.name','tags.description'])
-                                                ->get();
+                    $chapter->views = View::where("target_id", $chapter->uid)->count();
+                    $chapter->likes = Like::where(["type" => "like", "target_id" => $chapter->uid])->count();
+                    $chapter->tags = TagMap::where("anchor_id", $chapter->uid)
+                        ->leftJoin('tags', 'tag_maps.tag_id', '=', 'tags.id')
+                        ->select(['tags.id', 'tags.name', 'tags.description'])
+                        ->get();
                 }
 
                 //计算按照这个条件搜索到的总数
@@ -369,28 +375,28 @@ class ProgressChapterController extends Controller
 							where ch.status >= 30 $channel_type
 
                 ";
-                $count = DB::select($query,$param_count);
+                $count = DB::select($query, $param_count);
                 $all_count = $count[0]->count;
                 break;
             case 'top':
                 break;
             case 'search':
                 $key = $request->get('key');
-                $table = ProgressChapter::where('title','like',"%{$key}%");
+                $table = ProgressChapter::where('title', 'like', "%{$key}%");
                 //获取记录总条数
                 $all_count = $table->count();
                 //处理排序
-                if($request->has("order") && $request->has("dir")){
-                    $table = $table->orderBy($request->get("order"),$request->get("dir"));
-                }else{
+                if ($request->has("order") && $request->has("dir")) {
+                    $table = $table->orderBy($request->get("order"), $request->get("dir"));
+                } else {
                     //默认排序
-                    $table = $table->orderBy('updated_at','desc');
+                    $table = $table->orderBy('updated_at', 'desc');
                 }
                 //处理分页
-                if($request->has("limit")){
-                    if($request->has("offset")){
+                if ($request->has("limit")) {
+                    if ($request->has("offset")) {
                         $offset = $request->get("offset");
-                    }else{
+                    } else {
                         $offset = 0;
                     }
                     $table = $table->skip($offset)->take($request->get("limit"));
@@ -400,39 +406,39 @@ class ProgressChapterController extends Controller
                 //TODO 移到resource
                 foreach ($chapters as $key => $chapter) {
                     # code...
-                    $chapter->toc = PaliText::where('book',$chapter->book)->where('paragraph',$chapter->para)->value('toc');
-                    $chapter->path = PaliText::where('book',$chapter->book)->where('paragraph',$chapter->para)->value('path');
-                    $chapter->channel = Channel::where('uid',$chapter->channel_id)->select(['name','owner_uid'])->first();
-                    if($chapter->channel){
+                    $chapter->toc = PaliText::where('book', $chapter->book)->where('paragraph', $chapter->para)->value('toc');
+                    $chapter->path = PaliText::where('book', $chapter->book)->where('paragraph', $chapter->para)->value('path');
+                    $chapter->channel = Channel::where('uid', $chapter->channel_id)->select(['name', 'owner_uid'])->first();
+                    if ($chapter->channel) {
                         $chapter->studio = StudioApi::getById($chapter->channel["owner_uid"]);
-                    }else{
+                    } else {
                         $chapter->channel = [
-                            'name'=>"unknown",
-                            'owner_uid'=>"unknown",
+                            'name' => "unknown",
+                            'owner_uid' => "unknown",
                         ];
                         $chapter->studio = [
-                            'id'=>"",
-                            'nickName'=>"unknown",
-                            'realName'=>"unknown",
-                            'avatar'=>'',
+                            'id' => "",
+                            'nickName' => "unknown",
+                            'realName' => "unknown",
+                            'avatar' => '',
                         ];
                     }
 
-                    $chapter->views = View::where("target_id",$chapter->uid)->count();
-                    $chapter->likes = Like::where(["type"=>"like","target_id"=>$chapter->uid])->count();
-                    $chapter->tags = TagMap::where("anchor_id",$chapter->uid)
-                                                ->leftJoin('tags','tag_maps.tag_id', '=', 'tags.id')
-                                                ->select(['tags.id','tags.name','tags.description'])
-                                                ->get();
+                    $chapter->views = View::where("target_id", $chapter->uid)->count();
+                    $chapter->likes = Like::where(["type" => "like", "target_id" => $chapter->uid])->count();
+                    $chapter->tags = TagMap::where("anchor_id", $chapter->uid)
+                        ->leftJoin('tags', 'tag_maps.tag_id', '=', 'tags.id')
+                        ->select(['tags.id', 'tags.name', 'tags.description'])
+                        ->get();
                 }
                 break;
             case 'public':
                 break;
         }
 
-        if($chapters){
-            return $this->ok(["rows"=>$chapters,"count"=>$all_count]);
-        }else{
+        if ($chapters) {
+            return $this->ok(["rows" => $chapters, "count" => $all_count]);
+        } else {
             return $this->error("no data");
         }
     }

+ 1 - 0
api-v8/app/Http/Kernel.php

@@ -39,6 +39,7 @@ class Kernel extends HttpKernel
             \Illuminate\View\Middleware\ShareErrorsFromSession::class,
             \App\Http\Middleware\VerifyCsrfToken::class,
             \Illuminate\Routing\Middleware\SubstituteBindings::class,
+            \App\Http\Middleware\SetLocale::class, //语言设置
         ],
 
         'api' => [

+ 75 - 0
api-v8/app/Http/Middleware/SetLocale.php

@@ -0,0 +1,75 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Closure;
+use Illuminate\Support\Facades\App;
+use Illuminate\Support\Facades\Cookie;
+
+class SetLocale
+{
+    public function handle($request, Closure $next)
+    {
+        // 支持的语言列表
+        $supportedLocales = ['en', 'zh-Hans', 'zh-Hant'];
+        $defaultLocale = config('app.locale', 'en');
+
+        // 1. 检查 URL 参数中的 lang
+        $locale = $request->query('lang');
+
+        // 2. 如果没有 URL 参数,检查 Cookie 中的 language
+        if (!$locale) {
+            $locale = Cookie::get('language');
+        }
+
+        // 3. 如果没有 Cookie,检测浏览器语言
+        if (!$locale) {
+            $locale = $this->getBrowserLocale($request, $supportedLocales);
+        }
+
+        // 如果检测到的语言不在支持列表中,使用默认语言
+        if (!in_array($locale, $supportedLocales)) {
+            $locale = $defaultLocale;
+        }
+
+        // 设置应用语言
+        App::setLocale($locale);
+
+        // 存储到 Session 和 Cookie(可选:Cookie 有效期为 1 年)
+        session(['locale' => $locale]);
+        Cookie::queue('language', $locale, 525600); // 525600 分钟 = 1 年
+
+        return $next($request);
+    }
+
+    /**
+     * 从 HTTP Accept-Language 头获取浏览器语言
+     *
+     * @param \Illuminate\Http\Request $request
+     * @param array $supportedLocales
+     * @return string
+     */
+    protected function getBrowserLocale($request, $supportedLocales)
+    {
+        $acceptLanguage = $request->header('Accept-Language');
+        if (!$acceptLanguage) {
+            return config('app.locale', 'en');
+        }
+
+        // 解析 Accept-Language 头(格式如:zh-CN,zh;q=0.9,en;q=0.8)
+        $languages = array_map(function ($lang) {
+            return explode(';', $lang)[0]; // 提取语言代码(如 zh-CN, en)
+        }, explode(',', $acceptLanguage));
+
+        // 提取主语言代码并匹配支持的语言
+        foreach ($languages as $lang) {
+            $mainLang = explode('-', $lang)[0]; // 提取 zh 或 en
+            if (in_array($mainLang, $supportedLocales)) {
+                return $mainLang;
+            }
+        }
+
+        // 如果没有匹配的语言,返回默认语言
+        return config('app.locale', 'en');
+    }
+}

+ 14 - 1
api-v8/app/Models/Channel.php

@@ -8,11 +8,24 @@ use Illuminate\Database\Eloquent\Model;
 class Channel extends Model
 {
     use HasFactory;
-    protected $fillable = ['name','summary','type','lang','status','updated_at','created_at'];
+    protected $fillable = ['name', 'summary', 'type', 'lang', 'status', 'updated_at', 'created_at'];
 
     protected $primaryKey = 'uid';
     protected $casts = [
         'uid' => 'string'
     ];
     public  $incrementing = false;
+
+    /**
+     * 反向关联到 ProgressChapter
+     */
+    public function progressChapters()
+    {
+        return $this->hasMany(ProgressChapter::class, 'channel_id', 'uid');
+    }
+
+    public function owner()
+    {
+        return $this->belongsTo(UserInfo::class, 'owner_uid', 'userid');
+    }
 }

+ 13 - 1
api-v8/app/Models/PaliText.php

@@ -8,5 +8,17 @@ use Illuminate\Database\Eloquent\Model;
 class PaliText extends Model
 {
     use HasFactory;
-	protected $fillable = ['book','paragraph','level','class','toc','text','html','lenght'];
+    protected $fillable = ['book', 'paragraph', 'level', 'class', 'toc', 'text', 'html', 'lenght'];
+
+    public function progressChapters()
+    {
+        return $this->hasMany(ProgressChapter::class, null, null)
+            ->whereColumn('progress_chapters.book', 'pali_texts.book')
+            ->whereColumn('progress_chapters.para', 'pali_texts.paragraph');
+    }
+
+    public function tagMaps()
+    {
+        return $this->hasMany(TagMap::class, 'anchor_id', 'uid');
+    }
 }

+ 36 - 11
api-v8/app/Models/ProgressChapter.php

@@ -4,18 +4,28 @@ namespace App\Models;
 
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
+use Carbon\Carbon;
 
 class ProgressChapter extends Model
 {
     use HasFactory;
-	protected $fillable = ['book' , 'book', 'channel_id','lang'=>'en',
-                            'all_trans','public','progress',
-                            'title','created_at','updated_at'];
-      protected $casts = [
-            'uid' => 'string'
-        ];
+    protected $fillable = [
+        'book',
+        'book',
+        'channel_id',
+        'lang' => 'en',
+        'all_trans',
+        'public',
+        'progress',
+        'title',
+        'created_at',
+        'updated_at'
+    ];
+    protected $casts = [
+        'uid' => 'string'
+    ];
     protected $primaryKey = 'uid';
-    
+
     //protected $dateFormat = 'U';
 
     public function tagid()
@@ -25,16 +35,31 @@ class ProgressChapter extends Model
 
     public function tags()
     {
-        return $this->belongsToMany('App\Models\Tag','tag_maps','anchor_id','tag_id');
+        return $this->belongsToMany('App\Models\Tag', 'tag_maps', 'anchor_id', 'tag_id');
     }
-
+    /**
+     * 关联到 Channel 模型
+     * channel_id 关联到 channel 表的 uid 字段
+     */
     public function channel()
     {
-        return $this->hasOne('App\Models\Channel', 'uid', 'channel_id'); //参数一:需要关联的子表类名,前面必须加上命名空间  参数二:子表关联父表的字段  参数三:父表关联子表的字段
+        return $this->hasOne(Channel::class, 'uid', 'channel_id');
     }
-
     public function views()
     {
         return $this->hasMany('App\Models\View', 'target_id', 'uid');
     }
+
+    // 访问器格式化 created_at 字段
+    public function getFormattedCreateAtAttribute($value)
+    {
+        return Carbon::parse($value)->format('Y-m-d H:i:s');
+        // 你也可以使用其他格式:format('d/m/Y'), format('Y年m月d日') 等
+    }
+    public function getFormattedUpdatedAtAttribute()
+    {
+        //return Carbon::parse($value)->format('Y-m-d H:i:s');
+        return $this->updated_at->format('Y年m月d日 H:i');
+        // 你也可以使用其他格式:format('d/m/Y'), format('Y年m月d日') 等
+    }
 }

+ 3 - 3
api-v8/app/Models/TagMap.php

@@ -12,16 +12,16 @@ class TagMap extends Model
     protected $casts = [
         'id' => 'string'
     ];
-	protected $fillable = ['table_name' , 'anchor_id', 'tag_id'];
+    protected $fillable = ['table_name', 'anchor_id', 'tag_id'];
     use HasFactory;
 
-    public function progresschapter()
+    public function progressChapter()
     {
         return $this->belongsTo('App\Models\ProgressChapter', 'anchor_id', 'uid'); //参数一:需要关联的父表类名,前面必须加上命名空间  注意:参数二:子表关联父表的字段 参数三:父表关联子表的字段
     }
 
     public function tags()
     {
-        return $this->hasOne('App\Models\Tag', 'tag_id', 'id');
+        return $this->hasOne('App\Models\Tag', 'id', 'tag_id');
     }
 }

+ 6 - 0
api-v8/app/Models/UserInfo.php

@@ -10,4 +10,10 @@ class UserInfo extends Model
     use HasFactory;
     protected $primaryKey = 'id';
     public  $incrementing = true;
+
+    // 可选:定义反向关联
+    public function channels()
+    {
+        return $this->hasMany(Channel::class, 'owner_id', 'userid');
+    }
 }

+ 28 - 0
api-v8/app/View/Components/BookItem.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace App\View\Components;
+
+use Illuminate\View\Component;
+
+class BookItem extends Component
+{
+    /**
+     * Create a new component instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        //
+    }
+
+    /**
+     * Get the view / contents that represent the component.
+     *
+     * @return \Illuminate\Contracts\View\View|\Closure|string
+     */
+    public function render()
+    {
+        return view('components.book-item');
+    }
+}

+ 28 - 0
api-v8/app/View/Components/BookList.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace App\View\Components;
+
+use Illuminate\View\Component;
+
+class BookList extends Component
+{
+    /**
+     * Create a new component instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        //
+    }
+
+    /**
+     * Get the view / contents that represent the component.
+     *
+     * @return \Illuminate\Contracts\View\View|\Closure|string
+     */
+    public function render()
+    {
+        return view('components.book-list');
+    }
+}

+ 28 - 0
api-v8/app/View/Components/LanguageSwitcher.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace App\View\Components;
+
+use Illuminate\View\Component;
+
+class LanguageSwitcher extends Component
+{
+    /**
+     * Create a new component instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        //
+    }
+
+    /**
+     * Get the view / contents that represent the component.
+     *
+     * @return \Illuminate\Contracts\View\View|\Closure|string
+     */
+    public function render()
+    {
+        return view('components.language-switcher');
+    }
+}

+ 9 - 1
api-v8/config/mint.php

@@ -5,7 +5,15 @@ return [
         'icp_code' => env('APP_ICP_CODE', ''),
         'mps_code' => env('APP_MPS_CODE', ''),
     ],
-
+    'languages' => [
+        'en' => 'English',
+        'zh-Hans' => '简体中文',
+        'zh-Hant' => '繁体中文',
+    ],
+    'default_language' => 'en',
+    'library' => [
+        'list_min_progress' => env('LIBRARY_LIST_MIN_PROGRESS', 0.5),
+    ],
     'snowflake' => [
         'data_center_id' => env('SNOWFLAKE_DATA_CENTER_ID', 1),
         'worker_id' => env('SNOWFLAKE_WORKER_ID', 1),

+ 650 - 493
api-v8/public/app/palicanon/category/default.json

@@ -1,659 +1,816 @@
 [
-           {
-                "name":"Suttapiṭaka",
-                "tag":["sutta"],
-                "children":[
-                    {
-                        "name":"dīghanikāya",
-                        "tag":["sutta","dīghanikāya"],
-                        "children":[
-                            {
-                                "name":"Sīlakkhandhavagga",
-                                "tag":["sutta","dīghanikāya","sīlakkhandhavagga"]
-                            },
-                            {
-                                "name":"Mahāvagga",
-                                "tag":["sutta","dīghanikāya","mahāvagga"]
-                            },
-                            {
-                                "name":"Pāthikavagga",
-                                "tag":["sutta","dīghanikāya","pāthikavagga"]
-                            }
-                        ]
-                    },
-                    {
-                        "name":"Majjhimanikāya",
-                        "tag":["sutta","majjhimanikāya"],
-                        "children":[
-                            {
-                                "name":"Mūlapaṇṇāsa",
-                                "tag":["sutta","majjhimanikāya","mūlapaṇṇāsa"]
-                            },
-                            {
-                                "name":"Majjhimapaṇṇāsa",
-                                "tag":["sutta","majjhimanikāya","majjhimapaṇṇāsa"]
-                            },
-                            {
-                                "name":"Uparipaṇṇāsa",
-                                "tag":["sutta","majjhimanikāya","uparipaṇṇāsa"]
-                            }
-                        ]
-                    },
-                    {
-                        "name":"saṃyuttanikāya",
-                        "tag":["sutta","saṃyuttanikāya"],
-                        "children":[
-                            {
-                                "name":"Sagāthāvagga",
-                                "tag":["sutta","saṃyuttanikāya","sagāthāvagga"]
-                            },
-                            {
-                                "name":"Nidānavagga",
-                                "tag":["sutta","saṃyuttanikāya","nidānavagga"]
-                            },
-                            {
-                                "name":"Khandhavagga",
-                                "tag":["sutta","saṃyuttanikāya","khandhavagga"]
-                            },
-                            {
-                                "name":"Saḷāyatanavagga",
-                                "tag":["sutta","saṃyuttanikāya","saḷāyatanavagga"]
-                            },
-                            {
-                                "name":"Mahāvagga",
-                                "tag":["sutta","saṃyuttanikāya","mahāvagga"]
-                            }
-                        ]
-                    },
-                    {
-                        "name":"aṅguttaranikāya",
-                        "tag":["sutta","aṅguttaranikāya"],
-                        "children":[
-                            {
-                                "name":"Ekakanipāta",
-                                "tag":["sutta","aṅguttaranikāya","ekakanipāta"]
-                            },
-                            {
-                                "name":"Dukanipāta",
-                                "tag":["sutta","aṅguttaranikāya","dukanipāta"]
-                            },
-                            {
-                                "name":"Tikanipāta",
-                                "tag":["sutta","aṅguttaranikāya","tikanipāta"]
-                            },
-                            {
-                                "name":"Catukkanipāta",
-                                "tag":["sutta","aṅguttaranikāya","catukkanipāta"]
-                            },
-                            {
-                                "name":"Pañcakanipāta",
-                                "tag":["sutta","aṅguttaranikāya","pañcakanipāta"]
-                            },
-                            {
-                                "name":"Chakkanipāta",
-                                "tag":["sutta","aṅguttaranikāya","chakkanipāta"]
-                            },
-                            {
-                                "name":"Sattakanipāta",
-                                "tag":["sutta","aṅguttaranikāya","sattakanipāta"]
-                            },
-                            {
-                                "name":"Aṭṭhakanipāta",
-                                "tag":["sutta","aṅguttaranikāya","aṭṭhakanipāta"]
-                            },
-                            {
-                                "name":"Navakanipāta",
-                                "tag":["sutta","aṅguttaranikāya","navakanipāta"]
-                            },
-                            {
-                                "name":"Dasakanipāta",
-                                "tag":["sutta","aṅguttaranikāya","dasakanipāta"]
-                            },
-                            {
-                                "name":"Ekādasakanipāta",
-                                "tag":["sutta","aṅguttaranikāya","ekādasakanipāta"]
-                            }
-                        ]
-                    },
-                    {
-                        "name":"khuddakanikāya",
-                        "tag":["sutta","khuddakanikāya"],
-                        "children":[
-                            {
-                                "name":"Khuddakapāṭha",
-                                "tag":["sutta","khuddakanikāya","khuddakapāṭha"]
-                            },
-                            {
-                                "name":"Dhammapada",
-                                "tag":["sutta","khuddakanikāya","dhammapada"]
-                            },
-                            {
-                                "name":"Udāna",
-                                "tag":["sutta","khuddakanikāya","udāna"]
-                            },
-                            {
-                                "name":"Itivuttaka",
-                                "tag":["sutta","khuddakanikāya","itivuttaka"]
-                            },
-                            {
-                                "name":"Suttanipāta",
-                                "tag":["sutta","khuddakanikāya","suttanipāta"]
-                            },
-                            {
-                                "name":"Vimānavatthu",
-                                "tag":["sutta","khuddakanikāya","vimānavatthu"]
-                            },
-                            {
-                                "name":"Petavatthu",
-                                "tag":["sutta","khuddakanikāya","petavatthu"]
-                            },
-                            {
-                                "name":"Theragāthā",
-                                "tag":["sutta","khuddakanikāya","theragāthā"]
-                            },
-                            {
-                                "name":"Therīgāthā",
-                                "tag":["sutta","khuddakanikāya","therīgāthā"]
-                            },
-                            {
-                                "name":"Therāpadāna",
-                                "tag":["sutta","khuddakanikāya","therāpadāna"]
-                            },
-                            {
-                                "name":"Buddhavaṃsa",
-                                "tag":["sutta","khuddakanikāya","buddhavaṃsa"]
-                            },
-                            {
-                                "name":"Cariyāpiṭaka",
-                                "tag":["sutta","khuddakanikāya","cariyāpiṭaka"]
-                            },
-                            {
-                                "name":"Jātaka",
-                                "tag":["sutta","khuddakanikāya","jātaka"]
-                            },
-                            {
-                                "name":"Mahāniddesa",
-                                "tag":["sutta","khuddakanikāya","mahāniddesa"]
-                            },
-                            {
-                                "name":"Cūḷaniddesa",
-                                "tag":["sutta","khuddakanikāya","cūḷaniddesa"]
-                            },
-                            {
-                                "name":"Paṭisambhidāmagga",
-                                "tag":["sutta","khuddakanikāya","paṭisambhidāmagga"]
-                            },
-                            {
-                                "name":"Nettippakaraṇa",
-                                "tag":["sutta","khuddakanikāya","nettippakaraṇa"]
-                            },
-                            {
-                                "name":"Milindapañha",
-                                "tag":["sutta","khuddakanikāya","milindapañha"]
-                            },
-                            {
-                                "name":"Peṭakopadesa",
-                                "tag":["sutta","khuddakanikāya","peṭakopadesa"]
-                            }
-                        ]
+    {
+        "name": "suttapiṭaka",
+        "tag": ["sutta"],
+        "children": [
+            {
+                "name": "dīghanikāya",
+                "tag": ["sutta", "dīghanikāya"],
+                "children": [
+                    {
+                        "name": "sīlakkhandhavagga",
+                        "tag": ["sutta", "dīghanikāya", "sīlakkhandhavagga"]
+                    },
+                    {
+                        "name": "mahāvagga",
+                        "tag": ["sutta", "dīghanikāya", "mahāvagga"]
+                    },
+                    {
+                        "name": "pāthikavagga",
+                        "tag": ["sutta", "dīghanikāya", "pāthikavagga"]
                     }
                 ]
             },
             {
-                "name":"Vinayapiṭaka",
-                "tag":["vinaya"],
-                "children":[
-                    {
-                        "name":"Mahāvibhaṅga",
-                        "tag":["vinaya","mahāvibhaṅga"]
-                    },
-                    {
-                        "name":"Bhikkhunīvibhaṅga",
-                        "tag":["vinaya","bhikkhunīvibhaṅga"]
-                    },
-                    {
-                        "name":"Mahāvagga",
-                        "tag":["vinaya","mahāvagga"]
-                    },
-                    {
-                        "name":"Cūḷavagga",
-                        "tag":["vinaya","cūḷavagga"]
-                    },
-                    {
-                        "name":"Parivāra",
-                        "tag":["vinaya","parivāra"]
-                    },
-                    {
-                        "name":"Ṭīkā",
-                        "tag":["vinaya","ṭīkā"],
-						"children":[
-							{
-								"name":"Sāratthadīpanī",
-								"tag":["vinaya","ṭīkā","sāratthadīpanī"]
-							},
-							{
-								"name":"Pātimokkha",
-								"tag":["vinaya","ṭīkā","pātimokkha"]
-							},
-							{
-								"name":"Vajirabuddhi",
-								"tag":["vinaya","ṭīkā","vajirabuddhi"]
-							},
-							{
-								"name":"Vimativinodanī",
-								"tag":["vinaya","ṭīkā","vimativinodanī"]
-							},
-							{
-								"name":"Vinayavinicchayo",
-								"tag":["vinaya","ṭīkā","vinayavinicchaya"]
-							},
-                            {
-								"name":"Vinayasaṅgaha",
-								"tag":["vinaya","ṭīkā","vinayasaṅgaha"]
-							},
-                            {
-								"name":"Vinayālaṅkāra",
-								"tag":["vinaya","ṭīkā","vinayālaṅkāra"]
-							},
-							{
-								"name":"Uttaravinicchaya",
-								"tag":["vinaya","ṭīkā","uttaravinicchaya"]
-							},
-							{
-								"name":"Pācityādiyojanā",
-								"tag":["vinaya","ṭīkā","pācityādiyojanā"]
-							},
-							{
-								"name":"Khuddasikkhā",
-								"tag":["vinaya","ṭīkā","khuddasikkhā"]
-							},
-							{
-								"name":"Mūlasikkhā",
-								"tag":["vinaya","ṭīkā","mūlasikkhā"]
-							}
-						]
+                "name": "majjhimanikāya",
+                "tag": ["sutta", "majjhimanikāya"],
+                "children": [
+                    {
+                        "name": "mūlapaṇṇāsa",
+                        "tag": ["sutta", "majjhimanikāya", "mūlapaṇṇāsa"]
+                    },
+                    {
+                        "name": "majjhimapaṇṇāsa",
+                        "tag": ["sutta", "majjhimanikāya", "majjhimapaṇṇāsa"]
+                    },
+                    {
+                        "name": "uparipaṇṇāsa",
+                        "tag": ["sutta", "majjhimanikāya", "uparipaṇṇāsa"]
                     }
                 ]
             },
             {
-                "name":"Abhidhammapiṭaka",
-                "tag":["abhidhamma"],
-                "children":[
-                    {
-                        "name":"Dhammasaṅgaṇī",
-                        "tag":["abhidhamma","dhammasaṅgaṇī"]
-                    },
-                    {
-                        "name":"Vibhaṅga",
-                        "tag":["abhidhamma","vibhaṅga"]
-                    },
-                    {
-                        "name":"Dhātukathā",
-                        "tag":["abhidhamma","dhātukathā"]
-                    },
-                                {
-                        "name":"Puggalapaññatti",
-                        "tag":["abhidhamma","puggalapaññatti"]
-                    },
-                                {
-                        "name":"Kathāvatthu",
-                        "tag":["abhidhamma","kathāvatthu"]
-                    },
-                    {
-                        "name":"Yamaka",
-                        "tag":["abhidhamma","yamaka"]
-                    },
-                    {
-                        "name":"Paṭṭhāna",
-                        "tag":["abhidhamma","paṭṭhāna"]
-                    },
-                    {
-                        "name":"Añña",
-                        "tag":[],
-						"children":[
-							{
-								"name":"Abhidhammatthasaṅgaha",
-								"tag":["abhidhamma","abhidhammatthasaṅgaha"]
-							},
-							{
-								"name":"Abhidhammāvatāra",
-								"tag":["abhidhamma","abhidhammāvatāra"]
-							},
-							{
-								"name":"Nāmarūpaparicchedo",
-								"tag":["abhidhamma","nāmarūpapariccheda"]
-							},
-							{
-								"name":"Paramatthavinicchayo",
-								"tag":["abhidhamma","paramatthavinicchaya"]
-							},
-							{
-								"name":"Saccasaṅkhepo",
-								"tag":["abhidhamma","saccasaṅkhepa"]
-							},
-							{
-								"name":"Abhidhammamātikāpāḷi",
-								"tag":["abhidhamma","abhidhammamātikāpāḷi"]
-							},
-							{
-								"name":"Mohavicchedanī",
-								"tag":["abhidhamma","mohavicchedanī"]
-							}
-						]
+                "name": "saṃyuttanikāya",
+                "tag": ["sutta", "saṃyuttanikāya"],
+                "children": [
+                    {
+                        "name": "sagāthāvagga",
+                        "tag": ["sutta", "saṃyuttanikāya", "sagāthāvagga"]
+                    },
+                    {
+                        "name": "nidānavagga",
+                        "tag": ["sutta", "saṃyuttanikāya", "nidānavagga"]
+                    },
+                    {
+                        "name": "khandhavagga",
+                        "tag": ["sutta", "saṃyuttanikāya", "khandhavagga"]
+                    },
+                    {
+                        "name": "saḷāyatanavagga",
+                        "tag": ["sutta", "saṃyuttanikāya", "saḷāyatanavagga"]
+                    },
+                    {
+                        "name": "mahāvagga",
+                        "tag": ["sutta", "saṃyuttanikāya", "mahāvagga"]
                     }
                 ]
             },
-    {
-        "name":"Añña",
-        "tag":["añña"],
-        "children":[
             {
-                "name":"Visuddhimagga",
-                "tag":["añña","visuddhimagga"],
-                "children":[
+                "name": "aṅguttaranikāya",
+                "tag": ["sutta", "aṅguttaranikāya"],
+                "children": [
+                    {
+                        "name": "ekakanipāta",
+                        "tag": ["sutta", "aṅguttaranikāya", "ekakanipāta"]
+                    },
                     {
-                        "name":"Visuddhimagga",
-                        "tag":["añña","visuddhimagga","aṭṭhakathā"]
+                        "name": "dukanipāta",
+                        "tag": ["sutta", "aṅguttaranikāya", "dukanipāta"]
                     },
                     {
-                        "name":"Visuddhimagga-mahāṭīkā",
-                        "tag":["añña","visuddhimagga","ṭīkā","visuddhimaggamahāṭīkā"]
+                        "name": "tikanipāta",
+                        "tag": ["sutta", "aṅguttaranikāya", "tikanipāta"]
                     },
                     {
-                        "name":"Visuddhimagga-nidānakathā",
-                        "tag":["añña","visuddhimagga","nidānakathā"]
+                        "name": "Catukkanipāta",
+                        "tag": ["sutta", "aṅguttaranikāya", "catukkanipāta"]
+                    },
+                    {
+                        "name": "Pañcakanipāta",
+                        "tag": ["sutta", "aṅguttaranikāya", "pañcakanipāta"]
+                    },
+                    {
+                        "name": "Chakkanipāta",
+                        "tag": ["sutta", "aṅguttaranikāya", "chakkanipāta"]
+                    },
+                    {
+                        "name": "Sattakanipāta",
+                        "tag": ["sutta", "aṅguttaranikāya", "sattakanipāta"]
+                    },
+                    {
+                        "name": "Aṭṭhakanipāta",
+                        "tag": ["sutta", "aṅguttaranikāya", "aṭṭhakanipāta"]
+                    },
+                    {
+                        "name": "Navakanipāta",
+                        "tag": ["sutta", "aṅguttaranikāya", "navakanipāta"]
+                    },
+                    {
+                        "name": "Dasakanipāta",
+                        "tag": ["sutta", "aṅguttaranikāya", "dasakanipāta"]
+                    },
+                    {
+                        "name": "Ekādasakanipāta",
+                        "tag": ["sutta", "aṅguttaranikāya", "ekādasakanipāta"]
                     }
                 ]
             },
             {
-                "name":"Saṃgāyanassa-Pucchā Vissajjanā",
-                "tag":["añña","saṅgayana-puccha vissajjanā"],
-                "children":[
+                "name": "khuddakanikāya",
+                "tag": ["sutta", "khuddakanikāya"],
+                "children": [
+                    {
+                        "name": "Khuddakapāṭha",
+                        "tag": ["sutta", "khuddakanikāya", "khuddakapāṭha"]
+                    },
+                    {
+                        "name": "Dhammapada",
+                        "tag": ["sutta", "khuddakanikāya", "dhammapada"]
+                    },
+                    {
+                        "name": "Udāna",
+                        "tag": ["sutta", "khuddakanikāya", "udāna"]
+                    },
+                    {
+                        "name": "Itivuttaka",
+                        "tag": ["sutta", "khuddakanikāya", "itivuttaka"]
+                    },
+                    {
+                        "name": "Suttanipāta",
+                        "tag": ["sutta", "khuddakanikāya", "suttanipāta"]
+                    },
                     {
-                        "name":"Dīghanikāya(Pu-Vi)",
-                        "tag":["añña","saṅgayana-puccha vissajjanā","dīghanikāya"]
+                        "name": "Vimānavatthu",
+                        "tag": ["sutta", "khuddakanikāya", "vimānavatthu"]
                     },
                     {
-                        "name":"Majjhimanikāya(Pu-Vi)",
-                        "tag":["añña","saṅgayana-puccha vissajjanā","majjhimanikāya"]
+                        "name": "Petavatthu",
+                        "tag": ["sutta", "khuddakanikāya", "petavatthu"]
                     },
                     {
-                        "name":"Saṃyuttanikāya(Pu-Vi)",
-                        "tag":["añña","saṅgayana-puccha vissajjanā","saṃyuttanikāya"]
+                        "name": "Theragāthā",
+                        "tag": ["sutta", "khuddakanikāya", "theragāthā"]
                     },
                     {
-                        "name":"Aṅguttaranikāya(Pu-Vi)",
-                        "tag":["añña","saṅgayana-puccha vissajjanā","aṅguttaranikāya"]
+                        "name": "Therīgāthā",
+                        "tag": ["sutta", "khuddakanikāya", "therīgāthā"]
                     },
                     {
-                        "name":"Vinayapiṭaka(Pu-Vi)",
-                        "tag":["añña","saṅgayana-puccha vissajjanā","vinayapiṭaka"]
+                        "name": "Therāpadāna",
+                        "tag": ["sutta", "khuddakanikāya", "therāpadāna"]
                     },
                     {
-                        "name":"Abhidhammapiṭaka(Pu-Vi)",
-                        "tag":["añña","saṅgayana-puccha vissajjanā","abhidhammapiṭaka"]
+                        "name": "Buddhavaṃsa",
+                        "tag": ["sutta", "khuddakanikāya", "buddhavaṃsa"]
                     },
                     {
-                        "name":"Aṭṭhakathā(Pu-Vi)",
-                        "tag":["añña","saṅgayana-puccha vissajjanā","aṭṭhakathā"]
+                        "name": "Cariyāpiṭaka",
+                        "tag": ["sutta", "khuddakanikāya", "cariyāpiṭaka"]
+                    },
+                    {
+                        "name": "Jātaka",
+                        "tag": ["sutta", "khuddakanikāya", "jātaka"]
+                    },
+                    {
+                        "name": "Mahāniddesa",
+                        "tag": ["sutta", "khuddakanikāya", "mahāniddesa"]
+                    },
+                    {
+                        "name": "Cūḷaniddesa",
+                        "tag": ["sutta", "khuddakanikāya", "cūḷaniddesa"]
+                    },
+                    {
+                        "name": "Paṭisambhidāmagga",
+                        "tag": ["sutta", "khuddakanikāya", "paṭisambhidāmagga"]
+                    },
+                    {
+                        "name": "Nettippakaraṇa",
+                        "tag": ["sutta", "khuddakanikāya", "nettippakaraṇa"]
+                    },
+                    {
+                        "name": "Milindapañha",
+                        "tag": ["sutta", "khuddakanikāya", "milindapañha"]
+                    },
+                    {
+                        "name": "Peṭakopadesa",
+                        "tag": ["sutta", "khuddakanikāya", "peṭakopadesa"]
                     }
                 ]
+            }
+        ]
+    },
+    {
+        "name": "vinayapiṭaka",
+        "tag": ["vinaya"],
+        "children": [
+            {
+                "name": "mahāvibhaṅga",
+                "tag": ["vinaya", "mahāvibhaṅga"]
+            },
+            {
+                "name": "bhikkhunīvibhaṅga",
+                "tag": ["vinaya", "bhikkhunīvibhaṅga"]
+            },
+            {
+                "name": "mahāvagga",
+                "tag": ["vinaya", "mahāvagga"]
             },
             {
-                "name":"Leḍī Sayādo Gantha-Saṅgaho",
-                "tag":["añña","leḍī sayādo"],
-                "children":[
+                "name": "cūḷavagga",
+                "tag": ["vinaya", "cūḷavagga"]
+            },
+            {
+                "name": "parivāra",
+                "tag": ["vinaya", "parivāra"]
+            },
+            {
+                "name": "Ṭīkā",
+                "tag": ["vinaya", "ṭīkā"],
+                "children": [
+                    {
+                        "name": "Sāratthadīpanī",
+                        "tag": ["vinaya", "ṭīkā", "sāratthadīpanī"]
+                    },
+                    {
+                        "name": "Pātimokkha",
+                        "tag": ["vinaya", "ṭīkā", "pātimokkha"]
+                    },
+                    {
+                        "name": "Vajirabuddhi",
+                        "tag": ["vinaya", "ṭīkā", "vajirabuddhi"]
+                    },
+                    {
+                        "name": "Vimativinodanī",
+                        "tag": ["vinaya", "ṭīkā", "vimativinodanī"]
+                    },
+                    {
+                        "name": "Vinayavinicchayo",
+                        "tag": ["vinaya", "ṭīkā", "vinayavinicchaya"]
+                    },
+                    {
+                        "name": "Vinayasaṅgaha",
+                        "tag": ["vinaya", "ṭīkā", "vinayasaṅgaha"]
+                    },
+                    {
+                        "name": "Vinayālaṅkāra",
+                        "tag": ["vinaya", "ṭīkā", "vinayālaṅkāra"]
+                    },
                     {
-                        "name":"Niruttidīpanīpāṭha",
-                        "tag":["añña","leḍī sayādo","niruttidīpanīpāṭha"]
+                        "name": "Uttaravinicchaya",
+                        "tag": ["vinaya", "ṭīkā", "uttaravinicchaya"]
                     },
                     {
-                        "name":"Paramatthadīpanī",
-                        "tag":["añña","leḍī sayādo","paramatthadīpanī"]
+                        "name": "Pācityādiyojanā",
+                        "tag": ["vinaya", "ṭīkā", "pācityādiyojanā"]
                     },
                     {
-                        "name":"Anudīpanīpāṭha",
-                        "tag":["añña","leḍī sayādo","anudīpanīpāṭha"]
+                        "name": "Khuddasikkhā",
+                        "tag": ["vinaya", "ṭīkā", "khuddasikkhā"]
                     },
                     {
-                        "name":"Paṭṭhānuddesa dīpanīpāṭha",
-                        "tag":["añña","leḍī sayādo","paṭṭhānuddesa dīpanīpāṭha"]
+                        "name": "Mūlasikkhā",
+                        "tag": ["vinaya", "ṭīkā", "mūlasikkhā"]
                     }
                 ]
+            }
+        ]
+    },
+    {
+        "name": "Abhidhammapiṭaka",
+        "tag": ["abhidhamma"],
+        "children": [
+            {
+                "name": "Dhammasaṅgaṇī",
+                "tag": ["abhidhamma", "dhammasaṅgaṇī"]
+            },
+            {
+                "name": "Vibhaṅga",
+                "tag": ["abhidhamma", "vibhaṅga"]
+            },
+            {
+                "name": "Dhātukathā",
+                "tag": ["abhidhamma", "dhātukathā"]
+            },
+            {
+                "name": "Puggalapaññatti",
+                "tag": ["abhidhamma", "puggalapaññatti"]
+            },
+            {
+                "name": "Kathāvatthu",
+                "tag": ["abhidhamma", "kathāvatthu"]
+            },
+            {
+                "name": "Yamaka",
+                "tag": ["abhidhamma", "yamaka"]
+            },
+            {
+                "name": "Paṭṭhāna",
+                "tag": ["abhidhamma", "paṭṭhāna"]
             },
             {
-                "name":"Buddha-Vandanā Gantha-Saṅgaho",
-                "tag":["añña","buddha-vandanā ganthasaṅgaha"],
-                "children":[
+                "name": "Añña",
+                "tag": [],
+                "children": [
                     {
-                        "name":"Namakkāratīkā",
-                        "tag":["añña","buddha-vandanā ganthasaṅgaha","namakkāra"]
+                        "name": "Abhidhammatthasaṅgaha",
+                        "tag": ["abhidhamma", "abhidhammatthasaṅgaha"]
                     },
                     {
-                        "name":"Mahāpaṇāmapāṭha",
-                        "tag":["añña","buddha-vandanā ganthasaṅgaha","mahāpaṇāmapāṭha"]
+                        "name": "Abhidhammāvatāra",
+                        "tag": ["abhidhamma", "abhidhammāvatāra"]
                     },
                     {
-                        "name":"Lakkhaṇāto Buddhathomanāgāthā",
-                        "tag":["añña","buddha-vandanā ganthasaṅgaha","lakkhaṇāto"]
+                        "name": "Nāmarūpaparicchedo",
+                        "tag": ["abhidhamma", "nāmarūpapariccheda"]
                     },
                     {
-                        "name":"Suttavandanā",
-                        "tag":["añña","buddha-vandanā ganthasaṅgaha","suttavandanā"]
+                        "name": "Paramatthavinicchayo",
+                        "tag": ["abhidhamma", "paramatthavinicchaya"]
                     },
                     {
-                        "name":"Jinālaṅkāra",
-                        "tag":["añña","buddha-vandanā ganthasaṅgaha","jinālaṅkāra"]
+                        "name": "Saccasaṅkhepo",
+                        "tag": ["abhidhamma", "saccasaṅkhepa"]
                     },
                     {
-                        "name":"Kamalāñjali",
-                        "tag":["añña","buddha-vandanā ganthasaṅgaha","kamalāñjali"]
+                        "name": "Abhidhammamātikāpāḷi",
+                        "tag": ["abhidhamma", "abhidhammamātikāpāḷi"]
                     },
                     {
-                        "name":"Pajjamadhu",
-                        "tag":["añña","buddha-vandanā ganthasaṅgaha","pajjamadhu"]
+                        "name": "Mohavicchedanī",
+                        "tag": ["abhidhamma", "mohavicchedanī"]
+                    }
+                ]
+            }
+        ]
+    },
+    {
+        "name": "Añña",
+        "tag": ["añña"],
+        "children": [
+            {
+                "name": "Visuddhimagga",
+                "tag": ["añña", "visuddhimagga"],
+                "children": [
+                    {
+                        "name": "Visuddhimagga",
+                        "tag": ["añña", "visuddhimagga", "aṭṭhakathā"]
+                    },
+                    {
+                        "name": "Visuddhimagga-mahāṭīkā",
+                        "tag": [
+                            "añña",
+                            "visuddhimagga",
+                            "ṭīkā",
+                            "visuddhimaggamahāṭīkā"
+                        ]
                     },
                     {
-                        "name":"Buddhaguṇagāthāvalī",
-                        "tag":["añña","buddha-vandanā ganthasaṅgaha","buddhaguṇagāthāvalī"]
+                        "name": "Visuddhimagga-nidānakathā",
+                        "tag": ["añña", "visuddhimagga", "nidānakathā"]
                     }
                 ]
             },
             {
-                "name":"Vaṃsa Gantha-Saṅgaho",
-                "tag":["añña","vaṃsa ganthasaṅgaha"],
-                "children":[
+                "name": "Saṃgāyanassa-Pucchā Vissajjanā",
+                "tag": ["añña", "saṅgayana-puccha vissajjanā"],
+                "children": [
+                    {
+                        "name": "Dīghanikāya(Pu-Vi)",
+                        "tag": [
+                            "añña",
+                            "saṅgayana-puccha vissajjanā",
+                            "dīghanikāya"
+                        ]
+                    },
                     {
-                        "name":"Cūḷaganthavaṃsa",
-                        "tag":["añña","vaṃsa ganthasaṅgaha","cūḷaganthavaṃsapāḷi"]
+                        "name": "Majjhimanikāya(Pu-Vi)",
+                        "tag": [
+                            "añña",
+                            "saṅgayana-puccha vissajjanā",
+                            "majjhimanikāya"
+                        ]
+                    },
+                    {
+                        "name": "Saṃyuttanikāya(Pu-Vi)",
+                        "tag": [
+                            "añña",
+                            "saṅgayana-puccha vissajjanā",
+                            "saṃyuttanikāya"
+                        ]
                     },
                     {
-                        "name":"sāsanavaṃsa",
-                        "tag":["añña","vaṃsa ganthasaṅgaha","sāsanavaṃsappadīpikā"]
+                        "name": "Aṅguttaranikāya(Pu-Vi)",
+                        "tag": [
+                            "añña",
+                            "saṅgayana-puccha vissajjanā",
+                            "aṅguttaranikāya"
+                        ]
                     },
                     {
-                        "name":"Mahāvaṃsa",
-                        "tag":["añña","vaṃsa ganthasaṅgaha","mahāvaṃsapāḷi"]
+                        "name": "Vinayapiṭaka(Pu-Vi)",
+                        "tag": [
+                            "añña",
+                            "saṅgayana-puccha vissajjanā",
+                            "vinayapiṭaka"
+                        ]
+                    },
+                    {
+                        "name": "Abhidhammapiṭaka(Pu-Vi)",
+                        "tag": [
+                            "añña",
+                            "saṅgayana-puccha vissajjanā",
+                            "abhidhammapiṭaka"
+                        ]
+                    },
+                    {
+                        "name": "Aṭṭhakathā(Pu-Vi)",
+                        "tag": [
+                            "añña",
+                            "saṅgayana-puccha vissajjanā",
+                            "aṭṭhakathā"
+                        ]
                     }
                 ]
             },
             {
-                "name":"Byākaraṇa Gantha-Saṅgaho",
-                "tag":["añña","byākaraṇa ganthasaṅgaha"],
-                "children":[
-					{
-                        "name":"Moggallānasuttapāṭha",
-                        "tag":["añña","byākaraṇa ganthasaṅgaha","moggallānasuttapāṭha"]
+                "name": "Leḍī Sayādo Gantha-Saṅgaho",
+                "tag": ["añña", "leḍī sayādo"],
+                "children": [
+                    {
+                        "name": "Niruttidīpanīpāṭha",
+                        "tag": ["añña", "leḍī sayādo", "niruttidīpanīpāṭha"]
+                    },
+                    {
+                        "name": "Paramatthadīpanī",
+                        "tag": ["añña", "leḍī sayādo", "paramatthadīpanī"]
                     },
                     {
-                        "name":"Moggallānabyākaraṇa",
-                        "tag":["añña","byākaraṇa ganthasaṅgaha","moggallānabyākaraṇa"]
+                        "name": "Anudīpanīpāṭha",
+                        "tag": ["añña", "leḍī sayādo", "anudīpanīpāṭha"]
+                    },
+                    {
+                        "name": "Paṭṭhānuddesa dīpanīpāṭha",
+                        "tag": [
+                            "añña",
+                            "leḍī sayādo",
+                            "paṭṭhānuddesa dīpanīpāṭha"
+                        ]
+                    }
+                ]
+            },
+            {
+                "name": "Buddha-Vandanā Gantha-Saṅgaho",
+                "tag": ["añña", "buddha-vandanā ganthasaṅgaha"],
+                "children": [
+                    {
+                        "name": "Namakkāratīkā",
+                        "tag": [
+                            "añña",
+                            "buddha-vandanā ganthasaṅgaha",
+                            "namakkāra"
+                        ]
                     },
                     {
-                        "name":"Kaccāyanabyākaraṇaṃ",
-                        "tag":["añña","byākaraṇa ganthasaṅgaha","kaccāyanabyākaraṇa"]
+                        "name": "Mahāpaṇāmapāṭha",
+                        "tag": [
+                            "añña",
+                            "buddha-vandanā ganthasaṅgaha",
+                            "mahāpaṇāmapāṭha"
+                        ]
                     },
                     {
-                        "name":"Saddanītippakaraṇaṃ",
-                        "tag":["añña","byākaraṇa ganthasaṅgaha","saddanītippakaraṇa"]
+                        "name": "Lakkhaṇāto Buddhathomanāgāthā",
+                        "tag": [
+                            "añña",
+                            "buddha-vandanā ganthasaṅgaha",
+                            "lakkhaṇāto"
+                        ]
                     },
                     {
-                        "name":"Padarūpasiddhi",
-                        "tag":["añña","byākaraṇa ganthasaṅgaha","padarūpasiddhi"]
+                        "name": "Suttavandanā",
+                        "tag": [
+                            "añña",
+                            "buddha-vandanā ganthasaṅgaha",
+                            "suttavandanā"
+                        ]
                     },
                     {
-                        "name":"Moggallāna pañcikā ṭīkā",
-                        "tag":["añña","byākaraṇa ganthasaṅgaha","moggallāna pañcikā ṭīkā"]
+                        "name": "Jinālaṅkāra",
+                        "tag": [
+                            "añña",
+                            "buddha-vandanā ganthasaṅgaha",
+                            "jinālaṅkāra"
+                        ]
                     },
                     {
-                        "name":"Payogasiddhipāṭha",
-                        "tag":["añña","byākaraṇa ganthasaṅgaha","payogasiddhipāḷi"]
+                        "name": "Kamalāñjali",
+                        "tag": [
+                            "añña",
+                            "buddha-vandanā ganthasaṅgaha",
+                            "kamalāñjali"
+                        ]
                     },
                     {
-                        "name":"Vuttodayaṃ",
-                        "tag":["añña","byākaraṇa ganthasaṅgaha","vuttodaya"]
+                        "name": "Pajjamadhu",
+                        "tag": [
+                            "añña",
+                            "buddha-vandanā ganthasaṅgaha",
+                            "pajjamadhu"
+                        ]
                     },
                     {
-                        "name":"Abhidhānappadīpikāpāṭha",
-                        "tag":["añña","byākaraṇa ganthasaṅgaha","abhidhānappadīpikā"]
+                        "name": "Buddhaguṇagāthāvalī",
+                        "tag": [
+                            "añña",
+                            "buddha-vandanā ganthasaṅgaha",
+                            "buddhaguṇagāthāvalī"
+                        ]
+                    }
+                ]
+            },
+            {
+                "name": "Vaṃsa Gantha-Saṅgaho",
+                "tag": ["añña", "vaṃsa ganthasaṅgaha"],
+                "children": [
+                    {
+                        "name": "Cūḷaganthavaṃsa",
+                        "tag": [
+                            "añña",
+                            "vaṃsa ganthasaṅgaha",
+                            "cūḷaganthavaṃsapāḷi"
+                        ]
                     },
                     {
-                        "name":"Subodhālaṅkārapāṭha",
-                        "tag":["añña","byākaraṇa ganthasaṅgaha","subodhālaṅkāra"]
+                        "name": "sāsanavaṃsa",
+                        "tag": [
+                            "añña",
+                            "vaṃsa ganthasaṅgaha",
+                            "sāsanavaṃsappadīpikā"
+                        ]
                     },
                     {
-                        "name":"Bālāvatāra",
-                        "tag":["añña","byākaraṇa ganthasaṅgaha","bālāvatāra"]
+                        "name": "Mahāvaṃsa",
+                        "tag": ["añña", "vaṃsa ganthasaṅgaha", "mahāvaṃsapāḷi"]
                     }
                 ]
             },
             {
-                "name":"Nīti-Gantha-Saṅgaho",
-                "tag":["añña","nīti-ganthasaṅgaha"],
-                "children":[
+                "name": "Byākaraṇa Gantha-Saṅgaho",
+                "tag": ["añña", "byākaraṇa ganthasaṅgaha"],
+                "children": [
+                    {
+                        "name": "Moggallānasuttapāṭha",
+                        "tag": [
+                            "añña",
+                            "byākaraṇa ganthasaṅgaha",
+                            "moggallānasuttapāṭha"
+                        ]
+                    },
                     {
-                        "name":"Kavidappaṇanīti",
-                        "tag":["añña","nīti-ganthasaṅgaha","kavidappaṇanīti"]
+                        "name": "Moggallānabyākaraṇa",
+                        "tag": [
+                            "añña",
+                            "byākaraṇa ganthasaṅgaha",
+                            "moggallānabyākaraṇa"
+                        ]
                     },
                     {
-                        "name":"Nītimañjarī",
-                        "tag":["añña","nīti-ganthasaṅgaha","nītimañjarī"]
+                        "name": "Kaccāyanabyākaraṇaṃ",
+                        "tag": [
+                            "añña",
+                            "byākaraṇa ganthasaṅgaha",
+                            "kaccāyanabyākaraṇa"
+                        ]
                     },
                     {
-                        "name":"Dhammanīti",
-                        "tag":["añña","nīti-ganthasaṅgaha","dhammanīti"]
+                        "name": "Saddanītippakaraṇaṃ",
+                        "tag": [
+                            "añña",
+                            "byākaraṇa ganthasaṅgaha",
+                            "saddanītippakaraṇa"
+                        ]
                     },
                     {
-                        "name":"Mahārahanīti",
-                        "tag":["añña","nīti-ganthasaṅgaha","mahārahanīti"]
+                        "name": "Padarūpasiddhi",
+                        "tag": [
+                            "añña",
+                            "byākaraṇa ganthasaṅgaha",
+                            "padarūpasiddhi"
+                        ]
                     },
                     {
-                        "name":"Lokanīti",
-                        "tag":["añña","nīti-ganthasaṅgaha","lokanīti"]
+                        "name": "Moggallāna pañcikā ṭīkā",
+                        "tag": [
+                            "añña",
+                            "byākaraṇa ganthasaṅgaha",
+                            "moggallāna pañcikā ṭīkā"
+                        ]
                     },
                     {
-                        "name":"Suttantanīti",
-                        "tag":["añña","nīti-ganthasaṅgaha","suttantanīti"]
+                        "name": "Payogasiddhipāṭha",
+                        "tag": [
+                            "añña",
+                            "byākaraṇa ganthasaṅgaha",
+                            "payogasiddhipāḷi"
+                        ]
                     },
                     {
-                        "name":"Sūrassatīnīti",
-                        "tag":["añña","nīti-ganthasaṅgaha","sūrassatīnīti"]
+                        "name": "Vuttodayaṃ",
+                        "tag": ["añña", "byākaraṇa ganthasaṅgaha", "vuttodaya"]
                     },
                     {
-                        "name":"Cāṇakyanītipāḷi",
-                        "tag":["añña","nīti-ganthasaṅgaha","cāṇakyanītipāḷi"]
+                        "name": "Abhidhānappadīpikāpāṭha",
+                        "tag": [
+                            "añña",
+                            "byākaraṇa ganthasaṅgaha",
+                            "abhidhānappadīpikā"
+                        ]
                     },
                     {
-                        "name":"Naradakkhadīpanī",
-                        "tag":["añña","nīti-ganthasaṅgaha","naradakkhadīpanī"]
+                        "name": "Subodhālaṅkārapāṭha",
+                        "tag": [
+                            "añña",
+                            "byākaraṇa ganthasaṅgaha",
+                            "subodhālaṅkāra"
+                        ]
                     },
                     {
-                        "name":"Caturārakkhadīpanī",
-                        "tag":["añña","nīti-ganthasaṅgaha","caturārakkhadīpanī"]
+                        "name": "Bālāvatāra",
+                        "tag": ["añña", "byākaraṇa ganthasaṅgaha", "bālāvatāra"]
                     }
                 ]
             },
             {
-                "name":"Pakiṇṇaka-Gantha-Saṅgaho",
-                "tag":["añña","pakiṇṇaka-ganthasaṅgaha"],
-                "children":[
+                "name": "Nīti-Gantha-Saṅgaho",
+                "tag": ["añña", "nīti-ganthasaṅgaha"],
+                "children": [
+                    {
+                        "name": "Kavidappaṇanīti",
+                        "tag": ["añña", "nīti-ganthasaṅgaha", "kavidappaṇanīti"]
+                    },
+                    {
+                        "name": "Nītimañjarī",
+                        "tag": ["añña", "nīti-ganthasaṅgaha", "nītimañjarī"]
+                    },
+                    {
+                        "name": "Dhammanīti",
+                        "tag": ["añña", "nīti-ganthasaṅgaha", "dhammanīti"]
+                    },
                     {
-                        "name":"Rasavāhinī",
-                        "tag":["añña","pakiṇṇaka-ganthasaṅgaha","rasavāhinī"]
+                        "name": "Mahārahanīti",
+                        "tag": ["añña", "nīti-ganthasaṅgaha", "mahārahanīti"]
                     },
                     {
-                        "name":"Sīmavisodhanī",
-                        "tag":["añña","pakiṇṇaka-ganthasaṅgaha","sīmavisodhanī"]
+                        "name": "Lokanīti",
+                        "tag": ["añña", "nīti-ganthasaṅgaha", "lokanīti"]
                     },
                     {
-                        "name":"Vessantarāgīti",
-                        "tag":["añña","pakiṇṇaka-ganthasaṅgaha","vessantarāgīti"]
+                        "name": "Suttantanīti",
+                        "tag": ["añña", "nīti-ganthasaṅgaha", "suttantanīti"]
+                    },
+                    {
+                        "name": "Sūrassatīnīti",
+                        "tag": ["añña", "nīti-ganthasaṅgaha", "sūrassatīnīti"]
+                    },
+                    {
+                        "name": "Cāṇakyanītipāḷi",
+                        "tag": ["añña", "nīti-ganthasaṅgaha", "cāṇakyanītipāḷi"]
+                    },
+                    {
+                        "name": "Naradakkhadīpanī",
+                        "tag": [
+                            "añña",
+                            "nīti-ganthasaṅgaha",
+                            "naradakkhadīpanī"
+                        ]
+                    },
+                    {
+                        "name": "Caturārakkhadīpanī",
+                        "tag": [
+                            "añña",
+                            "nīti-ganthasaṅgaha",
+                            "caturārakkhadīpanī"
+                        ]
                     }
                 ]
             },
             {
-                "name":"Sihaḷa-Gantha-Saṅgaho",
-                "tag":["añña","sihaḷa-ganthasaṅgaha"],
-                "children":[
+                "name": "Pakiṇṇaka-Gantha-Saṅgaho",
+                "tag": ["añña", "pakiṇṇaka-ganthasaṅgaha"],
+                "children": [
+                    {
+                        "name": "Rasavāhinī",
+                        "tag": ["añña", "pakiṇṇaka-ganthasaṅgaha", "rasavāhinī"]
+                    },
+                    {
+                        "name": "Sīmavisodhanī",
+                        "tag": [
+                            "añña",
+                            "pakiṇṇaka-ganthasaṅgaha",
+                            "sīmavisodhanī"
+                        ]
+                    },
                     {
-                        "name":"Moggallāna vuttivivaraṇapañcikā",
-                        "tag":["añña","sihaḷa-ganthasaṅgaha","moggallāna vuttivivaraṇapañcikā"]
+                        "name": "Vessantarāgīti",
+                        "tag": [
+                            "añña",
+                            "pakiṇṇaka-ganthasaṅgaha",
+                            "vessantarāgīti"
+                        ]
+                    }
+                ]
+            },
+            {
+                "name": "Sihaḷa-Gantha-Saṅgaho",
+                "tag": ["añña", "sihaḷa-ganthasaṅgaha"],
+                "children": [
+                    {
+                        "name": "Moggallāna vuttivivaraṇapañcikā",
+                        "tag": [
+                            "añña",
+                            "sihaḷa-ganthasaṅgaha",
+                            "moggallāna vuttivivaraṇapañcikā"
+                        ]
                     },
                     {
-                        "name":"Thupavaṃsa",
-                        "tag":["añña","sihaḷa-ganthasaṅgaha","thupavaṃsa"]
+                        "name": "Thupavaṃsa",
+                        "tag": ["añña", "sihaḷa-ganthasaṅgaha", "thupavaṃsa"]
                     },
                     {
-                        "name":"Dāṭhāvaṃsa",
-                        "tag":["añña","sihaḷa-ganthasaṅgaha","dāṭhāvaṃsa"]
+                        "name": "Dāṭhāvaṃsa",
+                        "tag": ["añña", "sihaḷa-ganthasaṅgaha", "dāṭhāvaṃsa"]
                     },
                     {
-                        "name":"Dhātupāṭha vilāsiniyā",
-                        "tag":["añña","sihaḷa-ganthasaṅgaha","dhātupāṭha vilāsiniyā"]
+                        "name": "Dhātupāṭha vilāsiniyā",
+                        "tag": [
+                            "añña",
+                            "sihaḷa-ganthasaṅgaha",
+                            "dhātupāṭha vilāsiniyā"
+                        ]
                     },
                     {
-                        "name":"Dhātuvaṃsa",
-                        "tag":["añña","sihaḷa-ganthasaṅgaha","dhātuvaṃsa"]
+                        "name": "Dhātuvaṃsa",
+                        "tag": ["añña", "sihaḷa-ganthasaṅgaha", "dhātuvaṃsa"]
                     },
                     {
-                        "name":"Hatthavanagallavihāravaṃsa",
-                        "tag":["añña","sihaḷa-ganthasaṅgaha","hatthavanagallavihāravaṃsa"]
+                        "name": "Hatthavanagallavihāravaṃsa",
+                        "tag": [
+                            "añña",
+                            "sihaḷa-ganthasaṅgaha",
+                            "hatthavanagallavihāravaṃsa"
+                        ]
                     },
                     {
-                        "name":"Jinacaritaya",
-                        "tag":["añña","sihaḷa-ganthasaṅgaha","jinacaritaya"]
+                        "name": "Jinacaritaya",
+                        "tag": ["añña", "sihaḷa-ganthasaṅgaha", "jinacaritaya"]
                     },
                     {
-                        "name":"Jinavaṃsadīpaṃ",
-                        "tag":["añña","sihaḷa-ganthasaṅgaha","jinavaṃsadīpa"]
+                        "name": "Jinavaṃsadīpaṃ",
+                        "tag": ["añña", "sihaḷa-ganthasaṅgaha", "jinavaṃsadīpa"]
                     },
                     {
-                        "name":"Telakaṭāhagāthā",
-                        "tag":["añña","sihaḷa-ganthasaṅgaha","telakaṭāhagāthā"]
+                        "name": "Telakaṭāhagāthā",
+                        "tag": [
+                            "añña",
+                            "sihaḷa-ganthasaṅgaha",
+                            "telakaṭāhagāthā"
+                        ]
                     },
                     {
-                        "name":"Milidaṭīkā",
-                        "tag":["añña","sihaḷa-ganthasaṅgaha","milidaṭīkā"]
+                        "name": "Milidaṭīkā",
+                        "tag": ["añña", "sihaḷa-ganthasaṅgaha", "milidaṭīkā"]
                     },
                     {
-                        "name":"Padamañjarī",
-                        "tag":["añña","sihaḷa-ganthasaṅgaha","padamañjarī"]
+                        "name": "Padamañjarī",
+                        "tag": ["añña", "sihaḷa-ganthasaṅgaha", "padamañjarī"]
                     },
                     {
-                        "name":"Padasādhana",
-                        "tag":["añña","sihaḷa-ganthasaṅgaha","padasādhana"]
+                        "name": "Padasādhana",
+                        "tag": ["añña", "sihaḷa-ganthasaṅgaha", "padasādhana"]
                     },
                     {
-                        "name":"saddabindupakaraṇa",
-                        "tag":["añña","sihaḷa-ganthasaṅgaha","saddabindupakaraṇa"]
+                        "name": "saddabindupakaraṇa",
+                        "tag": [
+                            "añña",
+                            "sihaḷa-ganthasaṅgaha",
+                            "saddabindupakaraṇa"
+                        ]
                     },
                     {
-                        "name":"Kaccāyanadhātumañjūsā",
-                        "tag":["añña","sihaḷa-ganthasaṅgaha","kaccāyanadhātumañjūsā"]
+                        "name": "Kaccāyanadhātumañjūsā",
+                        "tag": [
+                            "añña",
+                            "sihaḷa-ganthasaṅgaha",
+                            "kaccāyanadhātumañjūsā"
+                        ]
                     },
                     {
-                        "name":"Samantakūṭavaṇṇanā",
-                        "tag":["añña","sihaḷa-ganthasaṅgaha","samantakūṭavaṇṇanā"]
+                        "name": "Samantakūṭavaṇṇanā",
+                        "tag": [
+                            "añña",
+                            "sihaḷa-ganthasaṅgaha",
+                            "samantakūṭavaṇṇanā"
+                        ]
                     }
                 ]
             }

+ 54 - 0
api-v8/public/assets/css/blog/css2

@@ -0,0 +1,54 @@
+/* latin-ext */
+@font-face {
+  font-family: 'Lato';
+  font-style: normal;
+  font-weight: 300;
+  font-display: swap;
+  src: url(https://fonts.gstatic.com/s/lato/v24/S6u9w4BMUTPHh7USSwaPGR_p.woff2) format('woff2');
+  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+  font-family: 'Lato';
+  font-style: normal;
+  font-weight: 300;
+  font-display: swap;
+  src: url(https://fonts.gstatic.com/s/lato/v24/S6u9w4BMUTPHh7USSwiPGQ.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* latin-ext */
+@font-face {
+  font-family: 'Lato';
+  font-style: normal;
+  font-weight: 400;
+  font-display: swap;
+  src: url(https://fonts.gstatic.com/s/lato/v24/S6uyw4BMUTPHjxAwXjeu.woff2) format('woff2');
+  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+  font-family: 'Lato';
+  font-style: normal;
+  font-weight: 400;
+  font-display: swap;
+  src: url(https://fonts.gstatic.com/s/lato/v24/S6uyw4BMUTPHjx4wXg.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* latin-ext */
+@font-face {
+  font-family: 'Lato';
+  font-style: normal;
+  font-weight: 700;
+  font-display: swap;
+  src: url(https://fonts.gstatic.com/s/lato/v24/S6u9w4BMUTPHh6UVSwaPGR_p.woff2) format('woff2');
+  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+  font-family: 'Lato';
+  font-style: normal;
+  font-weight: 700;
+  font-display: swap;
+  src: url(https://fonts.gstatic.com/s/lato/v24/S6u9w4BMUTPHh6UVSwiPGQ.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 4 - 0
api-v8/public/assets/css/blog/style.min.663803bebe609202d5b39d848f2d7c2dc8b598a2d879efa079fa88893d29c49c.css


BIN=BIN
api-v8/public/assets/images/cover/1/214.jpg


BIN=BIN
api-v8/public/assets/images/hero-2.jpg


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
api-v8/public/assets/js/blog/main.1e9a3bafd846ced4c345d084b355fb8c7bae75701c338f8a1f8a82c780137826.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
api-v8/public/assets/js/blog/vibrant.min.js


+ 5 - 0
api-v8/resources/lang/en/buttons.php

@@ -0,0 +1,5 @@
+<?php
+return [
+    'more' => 'More',
+    'online-read' =>  'Online Read',
+];

+ 5 - 0
api-v8/resources/lang/en/labels.php

@@ -0,0 +1,5 @@
+<?php
+return [
+    'home' => 'Home',
+    'translation' => 'Translation',
+];

+ 9 - 0
api-v8/resources/lang/en/language.php

@@ -0,0 +1,9 @@
+<?php
+return [
+    'en' => 'English',
+    'pali' => 'pali',
+    'zh' => '中文',
+    'zh-Hans' => '简体中文',
+    'zh-Hant' => '繁体中文',
+    'my' => 'မြန်မာဘာသာ',
+];

+ 5 - 0
api-v8/resources/lang/zh-Hans/buttons.php

@@ -0,0 +1,5 @@
+<?php
+return [
+    'more' => '更多',
+    'online-read' =>  '在线阅读',
+];

+ 4 - 0
api-v8/resources/lang/zh-Hans/label.php

@@ -0,0 +1,4 @@
+<?php
+return [
+    'translation' => '译文',
+];

+ 5 - 0
api-v8/resources/lang/zh-Hans/labels.php

@@ -0,0 +1,5 @@
+<?php
+return [
+    'home' => '首页',
+    'translation' => '译文',
+];

+ 5 - 0
api-v8/resources/lang/zh-Hant/buttons.php

@@ -0,0 +1,5 @@
+<?php
+return [
+    'more' => '更多',
+    'online-read' =>  '在线阅读',
+];

+ 5 - 0
api-v8/resources/lang/zh-Hant/labels.php

@@ -0,0 +1,5 @@
+<?php
+return [
+    'home' => '首页',
+    'translation' => '译文',
+];

+ 45 - 0
api-v8/resources/views/blog/category.blade.php

@@ -0,0 +1,45 @@
+@extends('blog.layouts.app')
+
+@section('title', $user["nickName"])
+
+@section('content')
+
+<header>
+    <h3 class="section-title">Categories</h3>
+
+    <div class="section-card">
+        <div class="section-details">
+            <h3 class="section-count">1 page</h3>
+            <h1 class="section-term">Example Category</h1>
+
+            <h2 class="section-description">
+                A description of this category
+            </h2>
+        </div>
+    </div>
+</header>
+
+<section class="article-list--compact">
+    @foreach ($posts as $post)
+    <article>
+        <a href="https://demo.stack.jimmycai.com/p/hello-world/">
+            <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>
+                </footer>
+            </div>
+            <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" />
+            </div>
+        </a>
+    </article>
+    @endforeach
+</section>
+
+@endsection

+ 453 - 0
api-v8/resources/views/blog/index.blade.php

@@ -0,0 +1,453 @@
+@extends('blog.layouts.app')
+
+@section('title', $user["nickName"])
+
+@section('content')
+<section class="article-list">
+    @foreach ($posts as $post)
+    <article class="">
+        <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('book.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">
+            <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"
+                        loading="lazy"
+                        alt="Featured image of post Hello World" />
+                </a>
+            </div>
+
+            <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">
+                <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="https://demo.stack.jimmycai.com/p/markdown-syntax-guide/">Markdown Syntax Guide</a>
+                    </h2>
+
+                    <h3 class="article-subtitle">
+                        Sample article showcasing basic Markdown syntax and
+                        formatting for HTML elements.
+                    </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">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>
+                    </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>
+</section>
+@endsection

+ 495 - 0
api-v8/resources/views/blog/layouts/app.blade.php

@@ -0,0 +1,495 @@
+<!DOCTYPE html>
+<!-- saved from url=(0032)https://demo.stack.jimmycai.com/ -->
+<html lang="en-us" dir="ltr" data-scheme="light">
+
+<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/" />
+
+    <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>
+
+<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;
+
+            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"
+                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">🍥</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>
+                    </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">
+                            <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>
+                    <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">
+                            <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>
+                        </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>
+            </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('category', [
+    'user' => $user['userName'],
+    'category1' => $category['id'],
+]) }}"
+                        class="font_size_1">
+                        {{ $category['label'] }}
+                    </a>
+                    @endforeach
+                </div>
+            </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">
+                        <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">
+                    <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
+                    </a>
+                </div>
+            </section>
+        </aside>
+
+        <main class="main full-width">
+            <div>
+                @yield('content')
+            </div>
+
+            <footer class="site-footer">
+                <section class="copyright">
+                    © 2020 - 2025 Hugo Theme Stack Starter
+                </section>
+
+                <section class="powerby">
+                    Built with
+                    <a href="https://gohugo.io/" target="_blank" rel="noopener">Hugo</a>
+                    <br />
+                    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>
+            </footer>
+        </main>
+    </div>
+    <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>

+ 24 - 0
api-v8/resources/views/components/book-item.blade.php

@@ -0,0 +1,24 @@
+{{-- 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('book.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">{{ $book['publisher'] ?? '未知出版者' }}</div>
+                <div class="book-language">
+                    <span class="language-badge">{{ $book['language'] ?? '未知语言' }}</span>
+                    <span class="language-badge">{{ $book['type'] ?? '未知类型' }}</span>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>

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

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

+ 29 - 0
api-v8/resources/views/components/language-switcher.blade.php

@@ -0,0 +1,29 @@
+@php
+$currentLocale = app()->getLocale();
+$languages = config('mint.languages');
+$currentLanguage = $languages[$currentLocale] ?? 'English';
+@endphp
+
+<div class="dropdown">
+    <a class="btn btn-ghost d-flex align-items-center" type="button" data-bs-toggle="dropdown" aria-expanded="false">
+        <svg t="1749033276791" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4471" width="200" height="200" fill="currentColor">
+            <path d="M512 929.959184c-230.4 0-417.959184-187.559184-417.959184-417.959184s187.559184-417.959184 417.959184-417.959184 417.959184 187.559184 417.959184 417.959184-187.559184 417.959184-417.959184 417.959184z m0-794.122449c-207.412245 0-376.163265 168.75102-376.163265 376.163265s168.75102 376.163265 376.163265 376.163265 376.163265-168.75102 376.163265-376.163265-168.75102-376.163265-376.163265-376.163265z" fill="#333333" p-id="4472"></path>
+            <path d="M512 929.959184c-11.493878 0-20.897959-9.404082-20.897959-20.89796V114.938776c0-11.493878 9.404082-20.897959 20.897959-20.89796s20.897959 9.404082 20.897959 20.89796v794.122448c0 11.493878-9.404082 20.897959-20.897959 20.89796z" fill="#333333" p-id="4473"></path>
+            <path d="M909.061224 532.897959H114.938776c-11.493878 0-20.897959-9.404082-20.89796-20.897959s9.404082-20.897959 20.89796-20.897959h794.122448c11.493878 0 20.897959 9.404082 20.89796 20.897959s-9.404082 20.897959-20.89796 20.897959z" fill="#333333" p-id="4474"></path>
+            <path d="M227.787755 809.795918c-5.22449 0-10.971429-2.089796-15.15102-6.269387C136.359184 725.159184 94.040816 621.191837 94.040816 512s42.318367-213.159184 118.595919-291.526531c7.836735-8.359184 21.420408-8.359184 29.779592-0.522449 8.359184 7.836735 8.359184 21.420408 0.522449 29.779592C173.97551 320.261224 135.836735 413.257143 135.836735 512s38.138776 191.738776 106.579592 262.269388c7.836735 8.359184 7.836735 21.420408-0.522449 29.779592-3.657143 3.657143-8.881633 5.746939-14.106123 5.746938z" fill="#333333" p-id="4475"></path>
+            <path d="M504.163265 929.959184c-0.522449 0-0.522449 0 0 0-110.759184-2.089796-214.204082-47.020408-291.52653-126.432653-7.836735-8.359184-7.836735-20.897959 0-29.257143 39.183673-40.228571 84.636735-71.57551 135.836734-92.995919 5.22449-2.089796 10.971429-2.089796 16.195919 0s9.404082 6.269388 11.493877 11.493878c29.779592 76.8 78.889796 146.285714 141.583674 200.097959 6.791837 5.746939 8.881633 15.15102 5.746939 23.510204-3.134694 7.836735-10.971429 13.583673-19.330613 13.583674z m-246.595918-141.061225c53.289796 49.110204 118.073469 80.979592 188.604082 93.518368-42.318367-44.930612-76.277551-97.17551-100.832653-153.6-31.869388 15.673469-61.64898 35.526531-87.771429 60.081632zM356.310204 344.293878c-2.612245 0-5.746939-0.522449-8.359184-1.567347-51.2-21.942857-96.653061-53.289796-135.836734-92.995919-7.836735-8.359184-7.836735-20.897959 0-29.257143C289.959184 141.061224 392.881633 96.653061 503.640816 94.040816c8.881633-0.522449 16.718367 5.22449 19.853062 13.583674s0.522449 17.763265-5.746939 23.510204C454.530612 184.946939 405.420408 253.910204 375.640816 331.232653c-2.089796 5.22449-6.269388 9.404082-11.493877 11.493878-2.089796 1.044898-5.22449 1.567347-7.836735 1.567347zM257.567347 235.102041c26.644898 24.555102 55.902041 44.408163 87.771429 60.604081 24.555102-56.42449 59.036735-108.669388 100.832653-153.6-70.530612 12.016327-135.314286 43.885714-188.604082 92.995919zM796.212245 809.795918c-5.22449 0-10.44898-2.089796-14.628572-5.746938-8.359184-7.836735-8.359184-21.420408-0.522449-29.779592C850.02449 703.738776 888.163265 610.742857 888.163265 512s-38.138776-191.738776-106.579592-262.269388c-7.836735-8.359184-7.836735-21.420408 0.522449-29.779592 8.359184-7.836735 21.420408-7.836735 29.779592 0.522449C887.640816 298.840816 929.959184 402.808163 929.959184 512s-42.318367 213.159184-118.595919 291.526531c-4.179592 4.179592-9.404082 6.269388-15.15102 6.269387z" fill="#333333" p-id="4476"></path>
+            <path d="M514.612245 929.959184c-8.881633 0-16.718367-5.22449-19.330612-13.583674-3.134694-8.359184-0.522449-17.240816 5.746938-22.987755 63.738776-54.334694 112.84898-124.342857 142.628572-202.187755 2.089796-5.22449 6.269388-9.404082 11.493877-11.493878 5.22449-2.089796 10.971429-2.089796 16.195919 0 52.767347 21.942857 100.310204 53.812245 140.538775 95.085715 7.836735 8.359184 7.836735 20.897959 0 29.257143-78.889796 80.457143-184.42449 124.865306-297.273469 125.910204z m159.869388-203.755102c-25.077551 57.991837-59.559184 111.281633-102.922449 157.257142 72.620408-11.493878 140.016327-43.885714 194.873469-94.563265-27.689796-25.6-58.514286-46.497959-91.95102-62.693877zM662.987755 346.383673c-2.612245 0-5.746939-0.522449-8.359184-1.567346-5.22449-2.089796-9.404082-6.269388-11.493877-11.493878-29.779592-77.844898-78.889796-147.853061-142.628572-202.187755-6.791837-5.746939-8.881633-15.15102-5.746938-22.987755 3.134694-8.359184 10.971429-13.583673 19.330612-13.583674 112.84898 0.522449 217.861224 45.453061 296.75102 126.432653 7.836735 8.359184 7.836735 20.897959 0 29.257143-40.228571 41.273469-87.24898 73.142857-140.538775 95.085715-1.567347 0.522449-4.702041 1.044898-7.314286 1.044897z m-91.428571-205.844897c42.840816 45.97551 77.844898 99.265306 102.922449 157.257142 33.436735-16.195918 64.783673-37.093878 91.95102-62.693877-54.857143-50.677551-122.253061-83.069388-194.873469-94.563265z" fill="#333333" p-id="4477"></path>
+            <path d="M356.310204 721.502041c-2.612245 0-5.746939-0.522449-8.359184-1.567347-5.22449-2.089796-9.404082-6.269388-11.493877-11.493878-24.032653-62.693878-36.571429-128.522449-36.571429-195.918367s12.016327-133.22449 36.571429-195.918367c2.089796-5.22449 6.269388-9.404082 11.493877-11.493878s10.971429-2.089796 16.195919 0c47.020408 19.330612 96.653061 29.257143 147.853061 29.257143 49.632653 0 97.697959-9.404082 143.15102-28.212245 5.22449-2.089796 10.971429-2.089796 16.195919 0s9.404082 6.269388 11.493877 11.493878c23.510204 62.171429 35.526531 127.477551 35.526531 193.828571 0 66.873469-12.016327 132.179592-35.526531 193.828571-2.089796 5.22449-6.269388 9.404082-11.493877 11.493878-5.22449 2.089796-10.971429 2.089796-16.195919 0-45.453061-18.808163-93.518367-28.212245-143.15102-28.212245-51.2 0-100.832653 9.926531-147.330612 29.779592-2.612245 2.612245-5.746939 3.134694-8.359184 3.134694z m12.538776-370.416327c-17.763265 51.722449-26.644898 106.057143-26.644898 160.914286s8.881633 109.191837 26.644898 160.914286c45.97551-16.718367 94.040816-25.077551 143.15102-25.077551 47.542857 0 94.040816 7.836735 138.44898 23.510204 17.240816-51.2 26.122449-105.012245 26.122449-159.346939 0-54.857143-8.881633-108.146939-26.122449-159.346939-44.408163 15.673469-90.906122 23.510204-138.44898 23.510204-49.632653 0-97.697959-8.359184-143.15102-25.077551z" fill="#333333" p-id="4478"></path>
+        </svg>
+        {{ $currentLanguage }}
+    </a>
+    <ul class="dropdown-menu">
+        @foreach ($languages as $locale => $language)
+        <li>
+            <a class="dropdown-item" href="{{ route(Route::currentRouteName(), array_merge(request()->route()->parameters(), ['lang' => $locale])) }}">
+                {{ $language }}
+            </a>
+        </li>
+        @endforeach
+    </ul>
+</div>

+ 477 - 0
api-v8/resources/views/library/book/read.blade.php

@@ -0,0 +1,477 @@
+<!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 Reading - {{ $book['title'] }}</title>
+    <!-- Tabler CSS -->
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/core@1.3.2/dist/css/tabler.min.css" />
+    <!-- FontAwesome for icons -->
+    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
+
+    <script
+        src="https://cdn.jsdelivr.net/npm/@tabler/core@1.3.2/dist/js/tabler.min.js">
+    </script>
+    <style>
+        /* Custom styles for responsive layout */
+        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;
+        }
+
+        .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;
+        }
+
+        /* Mobile: Show only content, TOC in drawer */
+        @media (max-width: 767px) {
+            .content-area {
+                width: 100%;
+            }
+        }
+
+        /* Tablet: Show TOC and content */
+        @media (min-width: 768px) {
+            .toc-sidebar {
+                display: block;
+            }
+
+            .content-area {
+                max-width: calc(100% - 270px);
+            }
+        }
+
+        /* Desktop: Show TOC, content, and right sidebar */
+        @media (min-width: 992px) {
+            .right-sidebar {
+                display: block;
+            }
+
+            .content-area {
+                max-width: calc(100% - 570px);
+            }
+        }
+
+        /* Dark mode styles */
+        .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 {
+            list-style: none;
+            padding: 0;
+        }
+
+        .toc-sidebar ul li {
+            padding: 10px 0;
+        }
+
+        .toc-sidebar ul li a {
+            color: #206bc4;
+            text-decoration: none;
+        }
+
+        .toc-sidebar ul li a:hover {
+            text-decoration: underline;
+        }
+
+        .dark-mode .toc-sidebar ul li a {
+            color: #4dabf7;
+        }
+
+        /* Multi-level TOC styles */
+        .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,
+        .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;
+        }
+
+        /* Indentation for TOC levels */
+        .toc-level-1 {
+            padding-left: 0 !important;
+        }
+
+        .toc-level-2 {
+            padding-left: 10px !important;
+        }
+
+        .toc-level-3 {
+            padding-left: 20px !important;
+        }
+
+        .toc-level-4 {
+            padding-left: 30px !important;
+        }
+
+        /* Disabled TOC item styles */
+        .toc-disabled {
+            color: #6c757d;
+            cursor: not-allowed;
+            pointer-events: none;
+        }
+
+        .dark-mode .toc-disabled {
+            color: #adb5bd;
+        }
+    </style>
+</head>
+
+<body class="{{ session('theme', 'light') }}-mode">
+    <!-- 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>
+            <h1 class="navbar-brand">{{ $book['title'] }}</h1>
+            <div class="navbar-nav flex-row order-md-last">
+                <!-- Theme Toggle -->
+                <div class="nav-item">
+                    <a href="#" class="nav-link" id="themeToggle">
+                        <i class="fas fa-moon"></i>
+                    </a>
+                </div>
+                <!-- User Settings -->
+                <div class="nav-item dropdown">
+                    <a href="#" class="nav-link d-flex lh-1 text-reset p-0" data-bs-toggle="dropdown" aria-label="Open user menu">
+                        <span class="avatar avatar-sm" style="background-image: url({{ auth()->user()->avatar ?? 'https://via.placeholder.com/40' }})">use</span>
+                    </a>
+                    <div class="dropdown-menu dropdown-menu-end">
+                        <a class="dropdown-item" href="#">Profile</a>
+                        <a class="dropdown-item" href="#">Settings</a>
+                        <a class="dropdown-item" href="{{ route('logout') }}">Logout</a>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </header>
+
+    <!-- TOC Drawer for Mobile -->
+    <div class="offcanvas offcanvas-start" tabindex="-1" id="tocDrawer" aria-labelledby="tocDrawerLabel">
+        <div class="offcanvas-header">
+            <h5 class="offcanvas-title" id="tocDrawerLabel">Table of Contents</h5>
+            <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
+        </div>
+        <div class="offcanvas-body">
+            @if(isset($book['toc']) && is_array($book['toc']) && count($book['toc']) > 0)
+            <ul>
+                @foreach ($book['toc'] as $index => $item)
+                <li class="toc-level-{{ $item['level'] }} {{ $item['disabled'] ? 'toc-disabled' : '' }}">
+                    @if (!$item['disabled'])
+                    <a href="#section-{{ $index }}">{{ $item['title'] }}</a>
+                    @else
+                    <span>{{ $item['title'] }}</span>
+                    @endif
+                </li>
+                @endforeach
+            </ul>
+            @else
+            <div class="alert alert-warning">
+                此书没有目录
+            </div>
+            @endif
+        </div>
+    </div>
+
+    <!-- Main Content Area -->
+    <div class="main-container">
+        <!-- Table of Contents Sidebar (Tablet+) -->
+        <div class="toc-sidebar card">
+            <div class="card-body">
+                <h5>Table of Contents</h5>
+                @if(isset($book['toc']) && is_array($book['toc']) && count($book['toc']) > 0)
+                <ul>
+                    @foreach ($book['toc'] as $index => $item)
+                    <li class="toc-level-{{ $item['level'] }} {{ $item['disabled'] ? 'toc-disabled' : '' }}">
+                        @if (!$item['disabled'])
+                        <a href="/library/book/{{ $item['id'] }}/read">{{ $item['title'] }}</a>
+                        @else
+                        <span>{{ $item['title'] }}</span>
+                        @endif
+                    </li>
+                    @endforeach
+                </ul>
+                @else
+                <div class="alert alert-warning">
+                    此书没有目录
+                </div>
+                @endif
+            </div>
+        </div>
+
+        <!-- Main Content -->
+        <div class="content-area card">
+
+            <div class="card-body">
+                <!-- text area -->
+                <div>
+                    <h2>{{ $book['title'] }}</h2>
+                    <p><strong>Author:</strong> {{ $book['author'] }}</p>
+
+                    <div class="content">
+                        @if(isset($book['content']))
+                        @foreach ($book['content'] as $index => $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
+                        </div>
+                        @endforeach
+                        @else
+                        <div>没有内容</div>
+                        @endif
+                    </div>
+                </div>
+                <!-- Nav buttons -->
+                <div class="mt-6 pt-6">
+                    <ul class="pagination">
+                        @if(!empty($book["pagination"]["prev"]))
+                        <li class="page-item page-prev">
+                            <a class="page-link" href='{{ route("book.read",$book["pagination"]["prev"]["id"]) }}'>
+                                <div class="row align-items-center">
+                                    <div class="col-auto">
+                                        <!-- Download SVG icon from http://tabler.io/icons/icon/chevron-left -->
+                                        <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">previous</div>
+                                        <div class="page-item-title">{{ $book["pagination"]["prev"]["title"] }}</div>
+                                    </div>
+                                </div>
+                            </a>
+                        </li>
+                        @endif
+                        @if(!empty($book["pagination"]["next"]))
+                        <li class="page-item page-next">
+                            <a class="page-link" href='{{ route("book.read",$book["pagination"]["next"]["id"]) }}'>
+                                <div class="row align-items-center">
+                                    <div class="col">
+                                        <div class="page-item-subtitle">next</div>
+                                        <div class="page-item-title">{{ $book["pagination"]["next"]["title"] }}</div>
+                                    </div>
+                                    <div class="col-auto">
+                                        <!-- Download SVG icon from http://tabler.io/icons/icon/chevron-right -->
+                                        <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>
+                                </div>
+                            </a>
+                        </li>
+                        @endif
+                    </ul>
+                </div>
+                <!-- Related Books -->
+                <div class="related-books">
+                    <h3>Related Books</h3>
+                    <div class="row row-cards">
+                        @if(isset($relatedBooks))
+                        @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
+                        @else
+                        <div>没有相关章节</div>
+                        @endif
+                    </div>
+                </div>
+
+            </div>
+        </div>
+
+        <!-- Right Sidebar (Desktop) -->
+        <div class="right-sidebar card">
+            <div class="card-body">
+                <h5>Download</h5>
+                <ul class="list-unstyled">
+                    @if(isset($book['downloads']))
+                    @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'] }}</a></li>
+                    @endforeach
+                    @else
+                    <div>没有下载链接</div>
+                    @endif
+                </ul>
+                <h5>Category</h5>
+                @foreach ($book['categories'] as $category)
+                <span class="badge bg-blue text-blue-fg">{{ $category['name'] }}</span>
+                @endforeach
+                <h5>Tags</h5>
+                <div>
+                    @foreach ($book['tags'] as $tag)
+                    <span class="badge me-1">{{ $tag['name'] }}</span>
+                    @endforeach
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <!-- Tabler JS and Bootstrap -->
+    <script src="https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta21/dist/js/tabler.min.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
+    <script>
+        // Theme Toggle
+        const themeToggle = document.getElementById('themeToggle');
+        const body = document.body;
+        themeToggle.addEventListener('click', (e) => {
+            e.preventDefault();
+            if (body.classList.contains('light-mode')) {
+                body.classList.remove('light-mode');
+                body.classList.add('dark-mode');
+                fetch('{{ route("theme.toggle") }}', {
+                    method: 'POST',
+                    headers: {
+                        'X-CSRF-TOKEN': '{{ csrf_token() }}',
+                        'Content-Type': 'application/json'
+                    },
+                    body: JSON.stringify({
+                        theme: 'dark'
+                    })
+                });
+                themeToggle.innerHTML = '<i class="fas fa-sun"></i>';
+            } else {
+                body.classList.remove('dark-mode');
+                body.classList.add('light-mode');
+                fetch('{{ route("theme.toggle") }}', {
+                    method: 'POST',
+                    headers: {
+                        'X-CSRF-TOKEN': '{{ csrf_token() }}',
+                        'Content-Type': 'application/json'
+                    },
+                    body: JSON.stringify({
+                        theme: 'light'
+                    })
+                });
+                themeToggle.innerHTML = '<i class="fas fa-moon"></i>';
+            }
+        });
+
+        // Smooth scroll for TOC links
+        document.querySelectorAll('.toc-sidebar a, .offcanvas-body a').forEach(anchor => {
+            anchor.addEventListener('click', function(e) {
+                e.preventDefault();
+                const targetId = this.getAttribute('href');
+                const targetElement = document.querySelector(targetId);
+                targetElement.scrollIntoView({
+                    behavior: 'smooth'
+                });
+                // Close drawer on mobile after clicking
+                if (window.innerWidth < 768) {
+                    const drawer = document.querySelector('#tocDrawer');
+                    const bsDrawer = bootstrap.Offcanvas.getInstance(drawer);
+                    bsDrawer.hide();
+                }
+            });
+        });
+    </script>
+</body>
+
+</html>

+ 135 - 0
api-v8/resources/views/library/book/show.blade.php

@@ -0,0 +1,135 @@
+@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('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('book.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">{{ $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('book.read', $chapter['id']) }}?chapter={{ $loop->iteration }}"
+                                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('book.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

+ 59 - 0
api-v8/resources/views/library/category.blade.php

@@ -0,0 +1,59 @@
+@extends('library.layouts.app')
+
+@section('title', $currentCategory['name'] . ' - 巴利书库')
+
+@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">
+                    <nav aria-label="breadcrumb">
+                        <ol class="breadcrumb">
+                            <li class="breadcrumb-item"><a href="{{ route('home') }}">{{ __('labels.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('category.show', $breadcrumb['id']) }}">{{ $breadcrumb['name'] }}</a>
+                            </li>
+                            @endif
+                            @endforeach
+                        </ol>
+                    </nav>
+                    <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('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

+ 42 - 0
api-v8/resources/views/library/index.blade.php

@@ -0,0 +1,42 @@
+@extends('library.layouts.app')
+
+@section('title', __('labels.home'))
+
+@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>
+        </div>
+
+        @foreach($categoryData as $data)
+        <div class="card mb-4">
+            <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('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>
+                    </a>
+                </div>
+            </div>
+            <div class="card-body">
+                @include('components.book-list', ['books' => $data['books']])
+            </div>
+        </div>
+        @endforeach
+    </div>
+</div>
+@endsection

+ 192 - 0
api-v8/resources/views/library/layouts/app.blade.php

@@ -0,0 +1,192 @@
+<!doctype html>
+<html lang="zh">
+
+<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', '巴利书库')</title>
+    @stack('styles')
+    <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@latest/icons-sprite.svg" rel="stylesheet" />
+    <script
+        src="https://cdn.jsdelivr.net/npm/@tabler/core@1.3.2/dist/js/tabler.min.js">
+    </script>
+
+    <style>
+        .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;
+            }
+        }
+
+        .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;
+        }
+
+        @media (max-width: 768px) {
+            .hero-title {
+                font-size: 2rem;
+            }
+
+            .hero-subtitle {
+                font-size: 1rem;
+            }
+
+            .hero-section {
+                height: 250px;
+            }
+
+            .stat-number {
+                font-size: 2rem;
+            }
+        }
+
+        @media (max-width: 576px) {
+            .hero-title {
+                font-size: 1.5rem;
+            }
+
+            .hero-subtitle {
+                font-size: 0.9rem;
+            }
+        }
+    </style>
+</head>
+
+<body>
+    <div class="page">
+        <!-- Hero Section -->
+        <section class="hero-section">
+            <div class="hero-overlay">
+                <div style="height:30px;width:100%;display: flex;justify-content: center;">
+                    <div style="color:white;flex:1;">
+                    </div>
+                    <div style="color:white;">
+                        <x-language-switcher />
+                    </div>
+                </div>
+            </div>
+
+            <div class="hero-content">
+
+                <h1 class="hero-title">巴利书库</h1>
+                <p class="hero-subtitle">探索wikipali,开启智慧之门</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>
+                </div>
+            </div>
+        </section>
+
+        <div class="page-wrapper">
+            @yield('content')
+        </div>
+    </div>
+
+    <!-- Tabler JS and Bootstrap -->
+    <script src="https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta21/dist/js/tabler.min.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
+    @stack('scripts')
+</body>
+
+</html>

+ 28 - 0
api-v8/routes/web.php

@@ -5,6 +5,8 @@ use App\Http\Controllers\SentenceInfoController;
 use App\Http\Controllers\WbwAnalysisController;
 use App\Http\Controllers\PageIndexController;
 use App\Http\Controllers\AssetsController;
+use App\Http\Controllers\BlogController;
+
 /*
 |--------------------------------------------------------------------------
 | Web Routes
@@ -39,3 +41,29 @@ Route::get('/book/{id}', function ($id) {
     return view('book', ['id' => $id]);
 });
 Route::redirect('/privacy', '/privacy/index');
+
+use App\Http\Controllers\CategoryController;
+use App\Http\Controllers\BookController;
+
+Route::get('/library', [CategoryController::class, 'index'])->name('home');
+Route::get('/library/category/{id}', [CategoryController::class, 'show'])->name('category.show');
+Route::get('/library/book/{id}', [BookController::class, 'show'])->name('book.show');
+Route::get('/library/book/{id}/read', [BookController::class, 'read'])->name('book.read');
+Route::post('/theme/toggle', [BookController::class, 'toggleTheme'])->name('theme.toggle');
+Route::post('/logout', function () {
+    // Handle logout
+    //Auth::logout();
+    return redirect('/login');
+})->name('logout');
+
+// 博客路由
+Route::prefix('blog')->group(function () {
+    Route::get('/{user}', [BlogController::class, 'index'])->name('index');
+    Route::get('/{user}/categories', [BlogController::class, 'categories'])->name('categories');
+    Route::get('/{user}/category/{category1}/{category2?}/{category3?}/{category4?}/{category5?}', [BlogController::class, 'category'])->name('category');
+    Route::get('/{user}/archives', [BlogController::class, 'archives'])->name('archives');
+    Route::get('/{user}/archives/{year}', [BlogController::class, 'archivesByYear'])->name('archives.year');
+    Route::get('/{user}/tag/{tag}', [BlogController::class, 'tag'])->name('tag');
+    Route::get('/{user}/search', [BlogController::class, 'search'])->name('search');
+    Route::get('/{user}/{post}', [BlogController::class, 'show'])->name('show');
+});

+ 145 - 0
dashboard-v4/dashboard/src/components/ai/ModelSelector.tsx

@@ -0,0 +1,145 @@
+import { Button, Dropdown, Typography, Tag } from "antd";
+import {
+  DownOutlined,
+  ReloadOutlined,
+  GlobalOutlined,
+} from "@ant-design/icons";
+import type { MenuProps } from "antd";
+
+const { Text } = Typography;
+
+const ModelSelector = () => {
+  const modelItems: MenuProps["items"] = [
+    {
+      key: "auto",
+      label: (
+        <div className="py-2">
+          <div className="font-medium text-gray-900">Auto</div>
+        </div>
+      ),
+    },
+    {
+      key: "gpt-4o",
+      label: (
+        <div className="py-2">
+          <div className="font-medium text-gray-900">
+            GPT-4o<Tag>翻译</Tag>
+          </div>
+          <Text type="secondary">适用于大多数任务</Text>
+        </div>
+      ),
+    },
+    {
+      key: "o4-mini",
+      label: (
+        <div className="py-2">
+          <div className="font-medium text-gray-900">o4-mini</div>
+          <Text type="secondary">快速进行高级推理</Text>
+        </div>
+      ),
+    },
+    {
+      key: "gpt-4.1-mini",
+      label: (
+        <div className="py-2">
+          <div className="font-medium text-gray-900">GPT-4.1-mini</div>
+          <Text type="secondary">适合处理日常任务</Text>
+        </div>
+      ),
+    },
+    {
+      type: "divider",
+    },
+    {
+      key: "refresh",
+      label: (
+        <div className="py-2 flex items-center space-x-2 text-gray-700">
+          <ReloadOutlined />
+          <span>重试</span>
+          <div className="ml-auto">
+            <Text type="secondary">GPT-4o</Text>
+          </div>
+        </div>
+      ),
+    },
+    {
+      key: "search",
+      label: (
+        <div className="py-2 flex items-center space-x-2 text-gray-700">
+          <GlobalOutlined />
+          <span>搜索网页</span>
+        </div>
+      ),
+    },
+  ];
+
+  const handleMenuClick: MenuProps["onClick"] = ({ key }) => {
+    if (key === "refresh") {
+      console.log("重试操作");
+      return;
+    }
+    if (key === "search") {
+      console.log("搜索网页");
+      return;
+    }
+  };
+
+  return (
+    <div className="bg-gray-50 min-h-screen">
+      <div className="max-w-md mx-auto">
+        <Dropdown
+          menu={{
+            items: modelItems,
+            onClick: handleMenuClick,
+            className: "w-64",
+          }}
+          trigger={["click"]}
+          placement="bottomLeft"
+          overlayClassName="model-selector-dropdown"
+        >
+          <Button
+            className="flex items-center justify-between w-48 h-12 px-4 border border-gray-300 rounded-lg bg-white hover:bg-gray-50 shadow-sm"
+            type="text"
+          >
+            <div className="flex items-center space-x-2">
+              <span className="font-medium text-gray-900 model_name">
+                {"AI"}
+              </span>
+              <DownOutlined className="text-gray-500 text-sm" />
+            </div>
+          </Button>
+        </Dropdown>
+      </div>
+
+      <style>{`
+        .model-selector-dropdown .ant-dropdown-menu {
+          padding: 8px;
+          border-radius: 12px;
+          box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
+          border: 1px solid #e5e7eb;
+        }
+
+        .model-selector-dropdown .ant-dropdown-menu-item {
+          padding: 0;
+          margin: 2px 0;
+          border-radius: 8px;
+        }
+
+        .model-selector-dropdown .ant-dropdown-menu-item:hover {
+          background-color: #f3f4f6;
+        }
+
+        .model-selector-dropdown .ant-dropdown-menu-item-selected {
+          background-color: #eff6ff;
+        }
+
+        .model-selector-dropdown .ant-dropdown-menu-divider {
+          margin: 8px 0;
+          background-color: #e5e7eb;
+        }
+      `}</style>
+    </div>
+  );
+};
+
+export default ModelSelector;

+ 1 - 0
dashboard-v4/dashboard/src/components/auth/setting/SettingArticle.tsx

@@ -25,6 +25,7 @@ const SettingArticleWidget = () => {
       <SettingItem
         data={SettingFind("setting.pali.script.secondary", settings)}
       />
+      <SettingItem data={SettingFind("setting.term.first.show", settings)} />
       <Divider>
         {intl.formatMessage({
           id: `buttons.translate`,

+ 28 - 2
dashboard-v4/dashboard/src/components/auth/setting/default.ts

@@ -1,5 +1,4 @@
-import { useAppSelector } from "../../../hooks";
-import { ISettingItem, settingInfo } from "../../../reducers/setting";
+import { ISettingItem } from "../../../reducers/setting";
 
 export interface ISettingItemOption {
   label: string;
@@ -186,6 +185,33 @@ export const defaultSetting: ISetting[] = [
       },
     ],
   },
+  {
+    /**
+     * 术语首次显示
+     */
+    key: "setting.term.first.show",
+    label: "setting.term.first.show.label",
+    description: "setting.term.first.show.description",
+    defaultValue: "meaning_pali_others",
+    options: [
+      {
+        value: "meaning_pali_others",
+        label: "term.first.show.meaning_pali_others",
+      },
+      {
+        value: "meaning_pali",
+        label: "term.first.show.meaning_pali",
+      },
+      {
+        value: "meaning_others",
+        label: "term.first.show.meaning_others",
+      },
+      {
+        value: "meaning",
+        label: "term.first.show.meaning",
+      },
+    ],
+  },
   {
     /**
      * nissaya 显示模式切换

+ 7 - 2
dashboard-v4/dashboard/src/components/template/Term.tsx

@@ -254,7 +254,10 @@ export const TermCtl = ({
           placement="bottom"
         >
           <Typography.Link
-            style={{ color: community ? "green" : undefined }}
+            style={{
+              color: community ? "green" : undefined,
+              wordBreak: "keep-all",
+            }}
             onClick={() => {
               console.debug("term send redux");
               store.dispatch(click(termData));
@@ -281,7 +284,9 @@ export const TermCtl = ({
         }}
         trigger={
           <Typography.Link>
-            <Text type="danger">{termData?.word}</Text>
+            <Text type="danger" style={{ wordBreak: "keep-all" }}>
+              {termData?.word}
+            </Text>
           </Typography.Link>
         }
         word={termData?.word}

+ 6 - 0
dashboard-v4/dashboard/src/locales/en-US/setting/index.ts

@@ -33,6 +33,12 @@ const items = {
   "setting.nissaya.layout.inline.label": "inline",
   "setting.nissaya.layout.list.label": "list",
   "setting.wbw.order.label": "wbw input order",
+  "setting.term.first.show.label": "First Term",
+  "setting.term.first.show.description": "Show First Term",
+  "term.first.show.meaning_pali_others": "Meaning & Pali & Others Meaning",
+  "term.first.show.meaning_pali": "Meaning & Pali",
+  "term.first.show.meaning_others": "Meaning & Others Meaning",
+  "term.first.show.meaning": "Meaning Only",
 };
 
 export default items;

+ 6 - 0
dashboard-v4/dashboard/src/locales/zh-Hans/setting/index.ts

@@ -33,6 +33,12 @@ const items = {
   "setting.nissaya.layout.inline.label": "连续",
   "setting.nissaya.layout.list.label": "列表",
   "setting.wbw.order.label": "显示输入顺序提示",
+  "setting.term.first.show.label": "第一个术语",
+  "setting.term.first.show.description": "第一个术语的显示方式",
+  "term.first.show.meaning_pali_others": "意思巴利及其他意思",
+  "term.first.show.meaning_pali": "意思巴利",
+  "term.first.show.meaning_others": "意思其他解释",
+  "term.first.show.meaning": "仅意思",
 };
 
 export default items;

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio