Просмотр исходного кода

Merge pull request #2415 from visuddhinanda/development

Development
visuddhinanda 3 дней назад
Родитель
Сommit
49a8abbe50

+ 32 - 20
api-v13/app/Console/Commands/ExportChannel.php

@@ -1,7 +1,9 @@
 <?php
+
 /**
  * 导出离线用的channel数据
  */
+
 namespace App\Console\Commands;
 
 use Illuminate\Console\Command;
@@ -42,40 +44,50 @@ class ExportChannel extends Command
      */
     public function handle()
     {
-        if(\App\Tools\Tools::isStop()){
+        if (\App\Tools\Tools::isStop()) {
             return 0;
         }
         Log::debug('task export offline channel-table start');
-        $exportFile = storage_path('app/public/export/offline/'.$this->argument('db').'-'.date("Y-m-d").'.db3');
-        $dbh = new \PDO('sqlite:'.$exportFile, "", "", array(\PDO::ATTR_PERSISTENT => true));
+        $exportFile = storage_path('app/public/export/offline/' . $this->argument('db') . '-' . date("Y-m-d") . '.db3');
+        $dbh = new \PDO('sqlite:' . $exportFile, "", "", array(\PDO::ATTR_PERSISTENT => true));
         $dbh->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_WARNING);
         $dbh->beginTransaction();
 
         $query = "INSERT INTO channel ( id , name , type , language ,
                                     summary , owner_id , setting,created_at )
                                     VALUES ( ? , ? , ? , ? , ? , ? , ? , ?  )";
-        try{
+        try {
             $stmt = $dbh->prepare($query);
-        }catch(PDOException $e){
+        } catch (\PDOException $e) {
             Log::error($e->getMessage(), ['exception' => $e]);
             return 1;
         }
 
-        $bar = $this->output->createProgressBar(Channel::where('status',30)->count());
-        foreach (Channel::where('status',30)
-                ->select(['uid','name','type','lang',
-                          'summary','owner_uid','setting','created_at'])
-                          ->cursor() as $row) {
-                $currData = array(
-                            $row->uid,
-                            $row->name,
-                            $row->type,
-                            $row->lang,
-                            $row->summary,
-                            $row->owner_uid,
-                            $row->setting,
-                            $row->created_at,
-                            );
+        $bar = $this->output->createProgressBar(Channel::where('status', 30)->count());
+        foreach (
+            Channel::where('status', 30)
+                ->select([
+                    'uid',
+                    'name',
+                    'type',
+                    'lang',
+                    'summary',
+                    'owner_uid',
+                    'setting',
+                    'created_at'
+                ])
+                ->cursor() as $row
+        ) {
+            $currData = array(
+                $row->uid,
+                $row->name,
+                $row->type,
+                $row->lang,
+                $row->summary,
+                $row->owner_uid,
+                $row->setting,
+                $row->created_at,
+            );
             $stmt->execute($currData);
             $bar->advance();
         }

+ 43 - 34
api-v13/app/Console/Commands/ExportSentence.php

@@ -45,26 +45,26 @@ class ExportSentence extends Command
     public function handle()
     {
         Log::debug('task export offline sentence-table start');
-        if(\App\Tools\Tools::isStop()){
+        if (\App\Tools\Tools::isStop()) {
             return 0;
         }
         \App\Tools\Markdown::driver($this->option('driver'));
         $channels = [];
         $channel_id = $this->option('channel');
-        if($channel_id){
+        if ($channel_id) {
             $file_suf = $channel_id;
             $channels[] = $channel_id;
-        }else{
+        } else {
             $channel_type = $this->option('type');
             $file_suf = $channel_type;
-            if($channel_type === "original"){
+            if ($channel_type === "original") {
                 $pali_channel = ChannelApi::getSysChannel("_System_Pali_VRI_");
-                if($pali_channel === false){
+                if ($pali_channel === false) {
                     return 0;
                 }
                 $channels[] = $pali_channel;
-            }else{
-                $nissaya_channel = Channel::where('type',$channel_type)->where('status',30)->select('uid')->get();
+            } else {
+                $nissaya_channel = Channel::where('type', $channel_type)->where('status', 30)->select('uid')->get();
                 foreach ($nissaya_channel as $key => $value) {
                     # code...
                     $channels[] = $value->uid;
@@ -73,53 +73,62 @@ class ExportSentence extends Command
         }
 
 
-        $exportFile = storage_path('app/public/export/offline/wikipali-offline-'.date("Y-m-d").'.db3');
-        $dbh = new \PDO('sqlite:'.$exportFile, "", "", array(\PDO::ATTR_PERSISTENT => true));
+        $exportFile = storage_path('app/public/export/offline/wikipali-offline-' . date("Y-m-d") . '.db3');
+        $dbh = new \PDO('sqlite:' . $exportFile, "", "", array(\PDO::ATTR_PERSISTENT => true));
         $dbh->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_WARNING);
         $dbh->beginTransaction();
 
-        if($channel_type === "original"){
+        if ($channel_type === "original") {
             $table = 'sentence';
-        }else{
+        } else {
             $table = 'sentence_translation';
         }
 
         $query = "INSERT INTO {$table} ( book , paragraph ,
                                     word_start , word_end , content , channel_id  )
                                     VALUES ( ? , ? , ? , ? , ? , ? )";
-        try{
+        try {
             $stmt = $dbh->prepare($query);
-        }catch(PDOException $e){
+        } catch (\PDOException $e) {
             Log::error($e->getMessage(), ['exception' => $e]);
             return 1;
         }
 
-        $db = Sentence::whereIn('channel_uid',$channels);
+        $db = Sentence::whereIn('channel_uid', $channels);
         $bar = $this->output->createProgressBar($db->count());
-        $srcDb = $db->select(['uid','book_id','paragraph',
-                                'word_start','word_end',
-                                'content','content_type','channel_uid',
-                                'editor_uid','language','updated_at'])->cursor();
+        $srcDb = $db->select([
+            'uid',
+            'book_id',
+            'paragraph',
+            'word_start',
+            'word_end',
+            'content',
+            'content_type',
+            'channel_uid',
+            'editor_uid',
+            'language',
+            'updated_at'
+        ])->cursor();
         foreach ($srcDb as $sent) {
-            if(Str::isUuid($sent->channel_uid)){
+            if (Str::isUuid($sent->channel_uid)) {
                 $channel = ChannelApi::getById($sent->channel_uid);
                 $currData = array(
-                        $sent->book_id,
-                        $sent->paragraph,
-                        $sent->word_start,
-                        $sent->word_end,
-                        MdRender::render($sent->content,
-                                        [$sent->channel_uid],
-                                        null,
-                                        'read',
-                                        $channel['type'],
-                                        $sent->content_type,
-                                        'unity',
-                                        ),
-                        $sent->channel_uid,
-                    );
+                    $sent->book_id,
+                    $sent->paragraph,
+                    $sent->word_start,
+                    $sent->word_end,
+                    MdRender::render(
+                        $sent->content,
+                        [$sent->channel_uid],
+                        null,
+                        'read',
+                        $channel['type'],
+                        $sent->content_type,
+                        'unity',
+                    ),
+                    $sent->channel_uid,
+                );
                 $stmt->execute($currData);
-
             }
             $bar->advance();
         }

+ 6 - 6
api-v13/app/Console/Commands/ExportTag.php

@@ -41,26 +41,26 @@ class ExportTag extends Command
     public function handle()
     {
         Log::debug('task: export offline data tag-table start');
-        if(\App\Tools\Tools::isStop()){
+        if (\App\Tools\Tools::isStop()) {
             return 0;
         }
-        $exportFile = storage_path('app/public/export/offline/'.$this->argument('db').'-'.date("Y-m-d").'.db3');
-        $dbh = new \PDO('sqlite:'.$exportFile, "", "", array(\PDO::ATTR_PERSISTENT => true));
+        $exportFile = storage_path('app/public/export/offline/' . $this->argument('db') . '-' . date("Y-m-d") . '.db3');
+        $dbh = new \PDO('sqlite:' . $exportFile, "", "", array(\PDO::ATTR_PERSISTENT => true));
         $dbh->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_WARNING);
         $dbh->beginTransaction();
 
         $query = "INSERT INTO tag ( id , name ,
                                     description , color , owner_id  )
                                     VALUES ( ? , ? , ? , ? , ?  )";
-        try{
+        try {
             $stmt = $dbh->prepare($query);
-        }catch(PDOException $e){
+        } catch (\PDOException $e) {
             Log::error($e->getMessage(), ['exception' => $e]);
             return 1;
         }
 
         $bar = $this->output->createProgressBar(Tag::count());
-        foreach (Tag::select(['id','name','description','color','owner_id'])->cursor() as $row) {
+        foreach (Tag::select(['id', 'name', 'description', 'color', 'owner_id'])->cursor() as $row) {
             $currData = array(
                 $row->id,
                 $row->name,

+ 9 - 9
api-v13/app/Console/Commands/ExportTagmap.php

@@ -41,29 +41,29 @@ class ExportTagmap extends Command
     public function handle()
     {
         Log::debug('task: export offline tagmap-table start');
-        if(\App\Tools\Tools::isStop()){
+        if (\App\Tools\Tools::isStop()) {
             return 0;
         }
-        $exportFile = storage_path('app/public/export/offline/'.$this->argument('db').'-'.date("Y-m-d").'.db3');
-        $dbh = new \PDO('sqlite:'.$exportFile, "", "", array(\PDO::ATTR_PERSISTENT => true));
+        $exportFile = storage_path('app/public/export/offline/' . $this->argument('db') . '-' . date("Y-m-d") . '.db3');
+        $dbh = new \PDO('sqlite:' . $exportFile, "", "", array(\PDO::ATTR_PERSISTENT => true));
         $dbh->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_WARNING);
         $dbh->beginTransaction();
 
         $query = "INSERT INTO tag_map ( anchor_id , tag_id )
                                     VALUES ( ? , ? )";
-        try{
+        try {
             $stmt = $dbh->prepare($query);
-        }catch(PDOException $e){
+        } catch (\PDOException $e) {
             Log::error($e->getMessage(), ['exception' => $e]);
             return 1;
         }
 
         $bar = $this->output->createProgressBar(TagMap::count());
-        foreach (TagMap::select(['id','table_name','anchor_id','tag_id'])->cursor() as $row) {
+        foreach (TagMap::select(['id', 'table_name', 'anchor_id', 'tag_id'])->cursor() as $row) {
             $currData = array(
-                            $row->anchor_id,
-                            $row->tag_id,
-                            );
+                $row->anchor_id,
+                $row->tag_id,
+            );
             $stmt->execute($currData);
             $bar->advance();
         }

+ 41 - 28
api-v13/app/Console/Commands/ExportTerm.php

@@ -42,11 +42,11 @@ class ExportTerm extends Command
     {
         Log::info('task export offline term-table start');
         $startAt = time();
-        if(\App\Tools\Tools::isStop()){
+        if (\App\Tools\Tools::isStop()) {
             return 0;
         }
-        $exportFile = storage_path('app/public/export/offline/wikipali-offline-'.date("Y-m-d").'.db3');
-        $dbh = new \PDO('sqlite:'.$exportFile, "", "", array(\PDO::ATTR_PERSISTENT => true));
+        $exportFile = storage_path('app/public/export/offline/wikipali-offline-' . date("Y-m-d") . '.db3');
+        $dbh = new \PDO('sqlite:' . $exportFile, "", "", array(\PDO::ATTR_PERSISTENT => true));
         $dbh->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_WARNING);
         $dbh->beginTransaction();
 
@@ -58,42 +58,55 @@ class ExportTerm extends Command
                                             ? , ? , ? , ? ,
                                             ?, ?, ?,
                                             ?, ?, ? )";
-        try{
+        try {
             $stmt = $dbh->prepare($query);
-        }catch(PDOException $e){
+        } catch (\PDOException $e) {
             Log::error($e->getMessage(), ['exception' => $e]);
             return 1;
         }
 
         $bar = $this->output->createProgressBar(DhammaTerm::count());
-        foreach (DhammaTerm::select(['guid','word','word_en','meaning',
-                          'other_meaning','note','tag','channal',
-                          'language',"owner","editor_id",
-                          "created_at","updated_at","deleted_at"
-                          ])
-                          ->cursor() as $row) {
-                $currData = array(
-                            $row->guid,
-                            $row->word,
-                            $row->word_en,
-                            $row->meaning,
-                            $row->other_meaning,
-                            $row->note,
-                            $row->tag,
-                            $row->channal,
-                            $row->language,
-                            $row->owner,
-                            $row->editor_id,
-                            $row->created_at,
-                            $row->updated_at,
-                            $row->deleted_at,
-                            );
+        foreach (
+            DhammaTerm::select([
+                'guid',
+                'word',
+                'word_en',
+                'meaning',
+                'other_meaning',
+                'note',
+                'tag',
+                'channal',
+                'language',
+                "owner",
+                "editor_id",
+                "created_at",
+                "updated_at",
+                "deleted_at"
+            ])
+                ->cursor() as $row
+        ) {
+            $currData = array(
+                $row->guid,
+                $row->word,
+                $row->word_en,
+                $row->meaning,
+                $row->other_meaning,
+                $row->note,
+                $row->tag,
+                $row->channal,
+                $row->language,
+                $row->owner,
+                $row->editor_id,
+                $row->created_at,
+                $row->updated_at,
+                $row->deleted_at,
+            );
             $stmt->execute($currData);
             $bar->advance();
         }
         $dbh->commit();
         $bar->finish();
-        $this->info(' time='.(time()-$startAt).'s');
+        $this->info(' time=' . (time() - $startAt) . 's');
         Log::info('task export offline term-table finished');
         return 0;
     }

+ 24 - 19
api-v13/app/Console/Commands/UpgradeAITranslation.php

@@ -348,25 +348,30 @@ class UpgradeAITranslation extends Command
     private function save(array $data)
     {
         // 写入句子库
-        $sentData = [];
-        $sentData = array_map(function ($n) {
-            $sId = explode('-', $n['id']);
-
-            return [
-                'book_id' => $sId[0],
-                'paragraph' => $sId[1],
-                'word_start' => $sId[2],
-                'word_end' => $sId[3],
-                'channel_uid' => $this->workChannel['id'],
-                'content' => $n['content'],
-                'content_type' => $n['content_type'] ?? 'markdown',
-                'lang' => $this->workChannel['lang'],
-                'status' => $this->workChannel['status'],
-                'editor_uid' => $this->model['uid'],
-            ];
-        }, $data);
-        foreach ($sentData as $key => $value) {
-            $this->sentenceService->saveWithHistory($value);
+        try {
+            $sentData = [];
+            $sentData = array_map(function ($n) {
+                $sId = explode('-', $n['id']);
+
+                return [
+                    'book_id' => $sId[0],
+                    'paragraph' => $sId[1],
+                    'word_start' => $sId[2],
+                    'word_end' => $sId[3],
+                    'channel_uid' => $this->workChannel['id'],
+                    'content' => $n['content'],
+                    'content_type' => $n['content_type'] ?? 'markdown',
+                    'lang' => $this->workChannel['lang'],
+                    'status' => $this->workChannel['status'],
+                    'editor_uid' => $this->model['uid'],
+                ];
+            }, $data);
+            foreach ($sentData as $key => $value) {
+                $this->sentenceService->saveWithHistory($value);
+            }
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+            throw $e;
         }
     }
 }

+ 70 - 72
api-v13/app/Http/Controllers/Library/TipitakaController.php

@@ -2,19 +2,17 @@
 
 namespace App\Http\Controllers\Library;
 
+use App\Http\Api\ChannelApi;
 use App\Http\Controllers\Controller;
-use Illuminate\Http\Request;
-use Illuminate\Support\Facades\Cookie;
-use Illuminate\Support\Facades\File;
-use Illuminate\Support\Facades\Log;
-
-
 use App\Models\PaliText;
 use App\Models\ProgressChapter;
-use App\Services\TermService;
 use App\Models\Tag;
 use App\Models\TagMap;
-
+use App\Services\TermService;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Cookie;
+use Illuminate\Support\Facades\File;
+use Illuminate\Support\Str;
 
 class TipitakaController extends Controller
 {
@@ -30,8 +28,6 @@ class TipitakaController extends Controller
 
     /**
      * 构造函数,注入 TermService
-     *
-     * @param  \App\Services\TermService  $termService
      */
     public function __construct(
         protected TermService $termService,
@@ -44,8 +40,8 @@ class TipitakaController extends Controller
     {
         return hexdec(substr(str_replace('-', '', $uid), 0, 4)) % 255;
     }
-    protected static int $nextId = 1;
 
+    protected static int $nextId = 1;
 
     // app/Http/Controllers/Library/CategoryController.php
     // category() 方法修改版
@@ -66,32 +62,32 @@ class TipitakaController extends Controller
         // ── 当前分类 ──────────────────────────────────────────
         if ($id) {
             $currentCategory = collect($categories)->firstWhere('id', $id);
-            if (!$currentCategory) {
+            if (! $currentCategory) {
                 abort(404);
             }
             $breadcrumbs = $this->getBreadcrumbs($currentCategory, $categories);
         } else {
             // 首页:虚拟顶级分类
             $currentCategory = ['id' => null, 'name' => '三藏'];
-            $breadcrumbs     = [];
+            $breadcrumbs = [];
         }
 
         // ── 子分类 ─────────────────────────────────────────────
         $subCategories = array_values(array_filter(
             $categories,
-            fn($cat) => $cat['parent_id'] == $id
+            fn ($cat) => $cat['parent_id'] == $id
         ));
-        if (count($subCategories) === 0 && !$request->has('book')) {
+        if (count($subCategories) === 0 && ! $request->has('book')) {
             $paliBooks = $this->getPaliBooks($categories, $id);
-            foreach ($paliBooks as  $value) {
+            foreach ($paliBooks as $value) {
                 $subCategories[] = [
                     'id' => $id,
                     'name' => $value->text,
-                    'book' => "{$value->book}-{$value->paragraph}"
+                    'book' => "{$value->book}-{$value->paragraph}",
                 ];
             }
         }
-        $allNames = array_map(fn($item) => $item['name'], $subCategories);
+        $allNames = array_map(fn ($item) => $item['name'], $subCategories);
 
         // 去重
         $allNames = array_values(array_unique($allNames));
@@ -111,24 +107,29 @@ class TipitakaController extends Controller
             $subCategories[$key]['name'] = $termMap[$name] ?? $name;
         }
 
-
-
         // ── 过滤参数 ────────────────────────────────────────────
-        $selectedType   = request('type',   'all');
-        $selectedLang   = request('lang',   'all');
+        $selectedType = request('type', 'all');
+        $selectedLang = request('lang', 'all');
         $selectedAuthor = request('author', 'all');
-        $selectedSort   = request('sort',   'new');
+        $selectedSort = request('sort', 'new');
+        $selectedChannel = request('channel', 'all');
+
+        // ── 当前频道(提供 channel 参数时) ──────────────────────
+        $currentChannel = $selectedChannel !== 'all'
+            ? (ChannelApi::getById($selectedChannel) ?: null)
+            : null;
 
         $sortList = [
-            ['key' => 'new',         'label' => __('library.badge_updated'),],
-            ['key' => 'progress',    'label' => '完成度',],
+            ['key' => 'new',         'label' => __('library.badge_updated')],
+            ['key' => 'progress',    'label' => '完成度'],
         ];
 
         $selected = [
-            'type'   => $selectedType,
-            'lang'   => $selectedLang,
+            'type' => $selectedType,
+            'lang' => $selectedLang,
             'author' => $selectedAuthor,
-            'sort'   => $selectedSort,
+            'sort' => $selectedSort,
+            'channel' => $selectedChannel,
         ];
         if ($request->has('book')) {
             $selected['book'] = $request->input('book');
@@ -165,7 +166,8 @@ class TipitakaController extends Controller
             'totalCount',
             'recommended',
             'activeAuthors',
-            'sortList'
+            'sortList',
+            'currentChannel'
         ));
     }
 
@@ -200,36 +202,23 @@ class TipitakaController extends Controller
             ['id' => 5, 'title' => '长部·梵网经',    'category' => '经藏'],
         ];
     }
+
     private function mockActiveAuthors()
     {
         return [
             [
-                'name'    => 'Bhikkhu Bodhi',
-                'avatar'  => null,
-                'color'   => '#2d5a8e',
+                'name' => 'Bhikkhu Bodhi',
+                'avatar' => null,
+                'color' => '#2d5a8e',
                 'initials' => 'BB',
-                'count'   => 24,
+                'count' => 24,
             ],
             [
-                'name'    => 'Bhikkhu Sujato',
-                'avatar'  => null,
-                'color'   => '#5a2d8e',
+                'name' => 'Bhikkhu Sujato',
+                'avatar' => null,
+                'color' => '#5a2d8e',
                 'initials' => 'BS',
-                'count'   => 18,
-            ],
-            [
-                'name'    => 'Buddhaghosa',
-                'avatar'  => null,
-                'color'   => '#8e5a2d',
-                'initials' => 'BG',
-                'count'   => 12,
-            ],
-            [
-                'name'    => 'Bhikkhu Brahmali',
-                'avatar'  => null,
-                'color'   => '#2d8e5a',
-                'initials' => 'BR',
-                'count'   => 9,
+                'count' => 18,
             ],
         ];
     }
@@ -253,12 +242,10 @@ class TipitakaController extends Controller
             ['id' => '1', 'name' => 'sutta'],
             ['id' => '48', 'name' => 'vinaya'],
             ['id' => '66', 'name' => 'abhidhamma'],
-            ['id' => '82', 'name' => 'añña']
+            ['id' => '82', 'name' => 'añña'],
         ];
     }
 
-
-
     private function subCategories($categories, int $id)
     {
         return array_filter($categories, function ($cat) use ($id) {
@@ -270,7 +257,7 @@ class TipitakaController extends Controller
     {
         if ($id) {
             $currentCategory = collect($categories)->firstWhere('id', $id);
-            if (!$currentCategory) {
+            if (! $currentCategory) {
                 abort(404);
             }
             // 标签查章节
@@ -287,18 +274,22 @@ class TipitakaController extends Controller
         foreach ($booksChapter as $key => $value) {
             $chapters[] = [$value->book, $value->paragraph];
         }
+
         return $chapters;
     }
+
     private function getPaliBooks(array $categories, string $id)
     {
         $chapters = $this->getBooksIdInCat($categories, $id);
 
         $books = PaliText::whereIns(['book', 'paragraph'], $chapters)->get();
+
         return $books;
     }
+
     private function getBooks(array $categories, ?string $id, array $filters)
     {
-        //根据分类获取书号
+        // 根据分类获取书号
         if (isset($filters['book'])) {
             $chapters = [explode('-', $filters['book'])];
         } else {
@@ -316,15 +307,20 @@ class TipitakaController extends Controller
                 if ($filters['lang'] !== 'all') {
                     $query->where('lang', $filters['lang']);
                 }
+
+                if ($filters['channel'] !== 'all' && Str::isUuid($filters['channel'])) {
+                    $query->where('uid', $filters['channel']);
+                }
             })
             ->whereNotNull('last_chapter_completed_at')
             ->whereIns(['book', 'para'], $chapters);
         if ($filters['sort'] === 'new') {
             $table = $table->orderBy('last_chapter_completed_at', 'desc');
-        } else if ($filters['sort'] === 'progress') {
+        } elseif ($filters['sort'] === 'progress') {
             $table = $table->orderBy('progress', 'desc');
         }
         $books = $table->take(100)->get();
+
         return $this->getBooksInfo($books);
     }
 
@@ -333,7 +329,7 @@ class TipitakaController extends Controller
         $pali = PaliText::where('level', 1)->get();
         // 获取该分类下的书籍
         $categoryBooks = [];
-        $books->each(function ($book) use (&$categoryBooks,  $pali) {
+        $books->each(function ($book) use (&$categoryBooks, $pali) {
             $title = $book->title;
             if (empty($title)) {
                 $title = $pali->firstWhere('book', $book->book)->toc;
@@ -354,23 +350,23 @@ class TipitakaController extends Controller
             $subTitle = $this->getBookType($book->book, $book->para);
 
             $categoryBooks[] = [
-                "id" => $book->uid,
-                "title" => $title,
-                "author" => $book->channel->name,
+                'id' => $book->uid,
+                'title' => $title,
+                'author' => $book->channel->name,
                 'subTitle' => $subTitle,
-                "publisher" => $book->channel->owner,
+                'publisher' => $book->channel->owner,
                 'completed_chapters' => $book->completed_chapters,
-                "type" => __('labels.' . $book->channel->type),
-                "cover" => $coverUrl,
+                'type' => __('labels.'.$book->channel->type),
+                'cover' => $coverUrl,
                 'cover_gradient' => $this->coverGradients[$colorIdx % count($this->coverGradients)],
-                "description" => $book->summary ?? "比库戒律的详细说明",
-                "language" => __('language.' . $book->channel->lang),
+                'description' => $book->summary ?? '比库戒律的详细说明',
+                'language' => __('language.'.$book->channel->lang),
             ];
         });
+
         return $categoryBooks;
     }
 
-
     private function getBookType(int $book, int $para)
     {
 
@@ -379,21 +375,23 @@ class TipitakaController extends Controller
         $tags = Tag::whereIn('id', $tagIds)->select('name')->get();
         foreach ($tags as $key => $tag) {
             if (in_array($tag->name, ['pāḷi', 'aṭṭhakathā', 'ṭīkā'])) {
-                return __('library.' . $tag->name);
+                return __('library.'.$tag->name);
             }
         }
 
         return null;
     }
+
     private function loadCategories()
     {
-        $json = file_get_contents(public_path("data/category/default.json"));
+        $json = file_get_contents(public_path('data/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
+    public static function flattenWithIds(array $tree, int $parentId = 0, int $level = 1): array
     {
 
         $flat = [];
@@ -406,7 +404,7 @@ class TipitakaController extends Controller
                 'parent_id' => $parentId,
                 'name' => $node['name'] ?? null,
                 'tag' => $node['tag'] ?? [],
-                "description" => "佛教戒律经典",
+                'description' => '佛教戒律经典',
                 'level' => $level,
             ];
 
@@ -414,7 +412,7 @@ class TipitakaController extends Controller
 
             if (isset($node['children']) && is_array($node['children'])) {
                 $childrenLevel = $level + 1;
-                $flat = array_merge($flat, self::flattenWithIds($node['children'],  $currentId, $childrenLevel));
+                $flat = array_merge($flat, self::flattenWithIds($node['children'], $currentId, $childrenLevel));
             }
         }
 

+ 50 - 29
api-v13/app/Services/AIAssistant/PaliTranslateService.php

@@ -67,7 +67,7 @@ class PaliTranslateService
         若用户额外提供 nissaya(巴利原文的逐词缅文释义,与 pali 通过 id 一一对应,按词列出每个巴利词的语法解析与缅文释义,形如「巴利词= 缅文释义」):它是判断词义、修饰关系、指代关系和句子结构最权威的依据,翻译时应优先参照 nissaya 确定原意,遇到歧义时以 nissaya 为准。
 
         翻译要求
-        1. 语言风格为现代汉语书面语,不要使用古汉语或者半文半白。
+        1. 语言风格为现代汉语,**绝对不要**使用古汉语或者半文半白。**不要参考**阿含经和元亨寺语言风格。
         2. 译文严谨,完全贴合巴利原文,不要加入自己的理解
         3. 经名、人名、地名等专有名词:有约定俗成的标准译名时优先使用标准译名;没有标准译名的,尽量按词义意译;意译确有困难的再使用音译。同一专有名词在全文中译名须前后一致
         4. 巴利原文中的黑体字在译文中也使用黑体。其他标点符号跟随巴利原文,但应该替换为相应的汉字全角符号
@@ -150,28 +150,49 @@ class PaliTranslateService
 
         你的任务:逐句对照原文审查译文,找出其中**确实存在**的翻译问题,按严重程度分级,并把问题**就地标注在译文上**,最后输出标注后的译文。
 
-        # 问题分级(严重程度由高到低)
-        - fatal(严重错误):会让读者产生邪见或重大误解。例如主语、谓语、宾语等句子核心成分判断错误,或句意违背基本教理。
-        - error(错误,必须修改):漏译;衍译(多出原文没有的内容);词义或修饰关系译错;造成误解的表达;义理或用词与注释书不符;代词指代错误。
-        - warning(待提升):关键词有歧义却未加注释;代词指代不清;不致误解的汉语语病;标点误用;术语标记使用不当;整句逻辑表达不规范。
-        - suggestion(可提升):语言不够流畅;风格不统一;该用术语标记而未用;嵌套长句对读者不友好等仅影响阅读体验的问题。
-
-        若同一片段存在多重问题,只按其中最严重的一级标注。
+        # 问题分级与类型
+        问题分为四个**级别**(severity),每个级别下又分若干**类型**(type)。请先判断片段属于哪个级别,再从该级别下选出最贴切的类型名。**级别从高到低,标注时只取最严重的一级。**
+
+        - fatal(严重错误):会让读者对经文意思有严重误解。类型:
+            - 严重失真:主、谓(含非谓语动词)、宾中有一项判断错误,导致句子意思严重失真
+            - 教理违背:句子意思违背基本教理原则
+        - error(错误,有举必究,必须修改):类型:
+            - 漏译:原文中有的内容在译文中缺失
+            - 多译:译文中增加了原文没有的内容
+            - 词义误译:词语的意思翻译错误
+            - 修饰错误:修饰关系判断错误
+            - 误解表达:表达方式会导致读者误解
+            - 义理不符:义理与注释书不符
+            - 用词不符:用词与注释书不符
+            - 指代错误:代词指代的对象错误
+        - warning(待提升):类型:
+            - 语意不明:关键词语意不明确
+            - 缺少注释:二意场合没有添加注释
+            - 指代不明:代词指代不够明确
+            - 汉语语病:不导致误解的汉语语病
+            - 标点错误:标点符号使用错误
+            - 误用标记:不该使用术语标记时使用了术语标记
+            - 逻辑不规范:整句逻辑表达不规范
+        - suggestion(可提升,仅影响阅读体验):类型:
+            - 表达晦涩:语言表达不够流畅
+            - 代词指代:代词指代不够明确
+            - 风格统一:语言风格不统一
+            - 术语标记:该使用而没有使用术语标记
+            - 缺少注释:不常用术语需要编写注释或者百科
+            - 句式复杂:复杂的嵌套句整句语言逻辑理解困难(对读者不友好)
+
+        若同一句子存在多重问题,只按其中最严重的一级标注,并选用该级别下最贴切的类型名。
 
         # 标注方法
         只对译文中**有问题的最小片段**,用如下 span 原地包裹(不改动译文本身的文字与黑体等格式,仅在外层套标签):
 
-        <span class='evaluate-级别' style='background:颜色' title='类别·级别:问题简述|建议:修改建议'>有问题的译文片段</span>
+        <span class='evaluate evaluate-级别'  title='类型·级别:问题简述|建议:修改建议'>有问题的译文片段</span>
 
-        **span 的属性一律用单引号**(class='...' style='...' title='...'),不要用双引号——因为 content 整体是 JSON 字符串、本身由双引号包裹,属性再用双引号极易因转义出错导致整行 JSON 解析失败、整句被丢弃。title 等属性值内若要引用文字,请使用中文全角引号「」或‘’,**严禁**出现 ASCII 双引号(")或单引号(')。
+        其中 class 里的「级别」与 title 里的「级别」都必须是 fatal / error / warning / suggestion 之一;title 里的「类型」必须是上面对应级别下列出的类型名(例如 语意不明、漏译、严重失真)。**级别在前判定,类型在该级别内挑选**,不要张冠李戴(例如 语意不明 只能配 warning)
 
-        级别与背景颜色对应(越暖代表越严重):
-        - fatal      颜色 #ffcdd2
-        - error      颜色 #ffe0b2
-        - warning    颜色 #fff9c4
-        - suggestion 颜色 #c8e6c9
+        **span 的属性一律用单引号**(class='...'  title='...'),不要用双引号——因为 content 整体是 JSON 字符串、本身由双引号包裹,属性再用双引号极易因转义出错导致整行 JSON 解析失败、整句被丢弃。title 等属性值内若要引用文字,请使用中文全角引号「」或‘’,**严禁**出现 ASCII 双引号(")或单引号(')。
 
-        title 写法:先写问题类别与级别,再用一句话说清问题是什么,最后给出具体可操作的修改建议,用「|建议:」分隔。
+        title 写法:先写「类型·级别」,再用一句话说清问题是什么,最后用「|建议:」给出具体可操作的修改建议。
 
         # 必须遵守的原则
         1. 只标注你**确有把握**的真实问题;拿不准就不标。
@@ -187,7 +208,7 @@ class PaliTranslateService
         直接输出 jsonl 数据,无需解释。
 
         **输出范例**(注意 span 属性用单引号,整行是合法 JSON)
-        {"id":"1-2-3-4","content":"他于<span class='evaluate-error' style='background:#ffe0b2' title='漏译·error:原文 bhagavā 未译出|建议:补译为‘世尊’'>那时</span>住在王舍城。"}
+        {"id":"1-2-3-4","content":"他于<span class='evaluate evaluate-error'  title='漏译·error:原文 bhagavā 未译出|建议:补译为‘世尊’'>那时</span>住在王舍城。"}
         {"id":"2-3-4-5","content":"完全正确的译文原样返回。"}
         md;
 
@@ -378,7 +399,7 @@ class PaliTranslateService
      */
     public function translate(array $pali, array $nissaya = []): array
     {
-        $userText = "# pali\n\n".$this->jsonBlock($pali)."\n\n".$this->nissayaSection($nissaya);
+        $userText = "# pali\n\n" . $this->jsonBlock($pali) . "\n\n" . $this->nissayaSection($nissaya);
         Log::debug('PaliTranslate: translate', ['input' => $userText]);
 
         $content = $this->send($this->translatePrompt, $userText);
@@ -396,9 +417,9 @@ class PaliTranslateService
      */
     public function review(array $pali, array $translation, array $nissaya = []): array
     {
-        $userText = "# pali\n\n".$this->jsonBlock($pali)."\n\n"
-            ."# translation\n\n".$this->jsonBlock($translation)."\n\n"
-            .$this->nissayaSection($nissaya);
+        $userText = "# pali\n\n" . $this->jsonBlock($pali) . "\n\n"
+            . "# translation\n\n" . $this->jsonBlock($translation) . "\n\n"
+            . $this->nissayaSection($nissaya);
         Log::debug('PaliTranslate: review', ['input' => $userText]);
 
         $content = $this->send($this->reviewPrompt, $userText);
@@ -417,9 +438,9 @@ class PaliTranslateService
      */
     public function revise(array $pali, array $translation, array $review): array
     {
-        $userText = "# pali\n\n".$this->jsonBlock($pali)."\n\n"
-            ."# translation\n\n".$this->jsonBlock($translation)."\n\n"
-            ."# review\n\n".$this->jsonBlock($review)."\n\n";
+        $userText = "# pali\n\n" . $this->jsonBlock($pali) . "\n\n"
+            . "# translation\n\n" . $this->jsonBlock($translation) . "\n\n"
+            . "# review\n\n" . $this->jsonBlock($review) . "\n\n";
         Log::debug('PaliTranslate: revise', ['input' => $userText]);
 
         $content = $this->send($this->revisePrompt, $userText);
@@ -439,9 +460,9 @@ class PaliTranslateService
      */
     public function evaluate(array $pali, array $translation, array $nissaya = []): array
     {
-        $userText = "# pali\n\n".$this->jsonBlock($pali)."\n\n"
-            ."# translation\n\n".$this->jsonBlock($translation)."\n\n"
-            .$this->nissayaSection($nissaya);
+        $userText = "# pali\n\n" . $this->jsonBlock($pali) . "\n\n"
+            . "# translation\n\n" . $this->jsonBlock($translation) . "\n\n"
+            . $this->nissayaSection($nissaya);
         Log::debug('PaliTranslate: evaluate', ['input' => $userText]);
 
         $content = $this->send($this->evaluatePrompt, $userText);
@@ -525,7 +546,7 @@ class PaliTranslateService
             return '';
         }
 
-        return "# nissaya\n\n".$this->jsonBlock($nissaya)."\n\n";
+        return "# nissaya\n\n" . $this->jsonBlock($nissaya) . "\n\n";
     }
 
     /**
@@ -535,6 +556,6 @@ class PaliTranslateService
      */
     protected function jsonBlock(array $data): string
     {
-        return "```json\n".json_encode($data, JSON_UNESCAPED_UNICODE)."\n```";
+        return "```json\n" . json_encode($data, JSON_UNESCAPED_UNICODE) . "\n```";
     }
 }

+ 23 - 20
api-v13/documents/translation-evaluate.md

@@ -1,31 +1,34 @@
 # 问题分级
 
 - 第一类 严重错误 fatal(零容忍;增长普通用户邪见、降低普通用户对译文的评价)
-    1. 主谓(含非谓语动词)宾有一项判断错误
-    2. 句子意思违背基本教理原则
+    1. 严重失真:主谓(含非谓语动词)宾有一项判断错误导致句子意思严重失真)
+    2. 教理违背:句子意思违背基本教理原则
 
 - 第二类 错误 error(专家有举必究;只要发现,一定要改、只有巴利专家能发现)
-    1. 漏译(例句:32**两**糖块的体积)
-    2. 错误多译
-    3. 错译(词义,修饰关系)
-    4. 导致误解的表达
-    5. 义理或用词与注释书不符
-    6. 代词指代错误
+    1. 漏译:原文中有的内容在译文中缺失
+    2. 多译:译文中增加了原文没有的内容
+    3. 词义误译:词语的意思翻译错误
+    4. 修饰错误:修饰关系判断错误
+    5. 误解表达:表达方式会导致读者误解
+    6. 义理不符:义理与注释书不符
+    7. 用词不符:用词与注释书不符
+    8. 指代错误:代词指代的对象错误
 
 - 第三类 待提升 warning
-    1. 关键词语意不明确或二意场合没有注释(稣息、转起)
-    2. 代词指代不明确
-    3. 不导致误解的汉语语病
-    4. 标点符号使用错误
-    5. 不该使用术语标记时使用了术语标记
-    6. 整句逻辑表达不规范
+    1. 语意不明:关键词语意不明确
+    2. 缺少注释:二意场合没有添加注释
+    3. 指代不明:代词指代不够明确
+    4. 汉语语病:不导致误解的汉语语病
+    5. 标点错误:标点符号使用错误
+    6. 误用标记:不该使用术语标记时使用了术语标记
+    7. 逻辑不规范:整句逻辑表达不规范
 
 - 第四类 可提升 suggestion
-    1. 语言表达不够流畅
-    2. 代词指代可能不够明确
-    3. 语言风格不统一
-    4. 该使用而没有使用术语标记
-    5. 不常用术语编写注释或者百科
-    6. 复杂的嵌套句整句语言逻辑理解困难(对读者不友好)
+    1. 表达晦涩:语言表达不够流畅
+    2. 代词指代:代词指代不够明确
+    3. 风格统一:语言风格不统一
+    4. 术语标记:该使用而没有使用术语标记
+    5. 缺少注释:不常用术语需要编写注释或者百科
+    6. 句式复杂:复杂的嵌套句整句语言逻辑理解困难(对读者不友好)
 
 第三类是出版社编辑提出的,第四类是由有佛教背景的读者提出的

+ 1 - 0
api-v13/resources/lang/en/library.php

@@ -87,6 +87,7 @@ return [
     'about_anthology' => 'About This Anthology',
     'anthology_info' => 'Anthology Info',
     'language' => 'Language',
+    'current_channel' => 'Current Channel',
     'created' => 'Created',
     'updated' => 'Updated',
     'related_anthology' => 'Related Anthologies',

+ 1 - 0
api-v13/resources/lang/my/library.php

@@ -87,6 +87,7 @@ return [
     'about_anthology' => 'ဤလက်ရွေးစင်စာစုအကြောင်း',
     'anthology_info' => 'လက်ရွေးစင်စာစု သတင်းအချက်အလက်',
     'language' => 'ဘာသာစကား',
+    'current_channel' => 'လက်ရှိချန်နယ်',
     'created' => 'ဖန်တီးသည်',
     'updated' => 'အပ်ဒိတ်လုပ်သည်',
     'related_anthology' => 'ဆက်စပ်သော လက်ရွေးစင်စာစုများ',

+ 1 - 0
api-v13/resources/lang/si/library.php

@@ -87,6 +87,7 @@ return [
     'about_anthology' => 'මෙම සංග්‍රහය ගැන',
     'anthology_info' => 'සංග්‍රහ තොරතුරු',
     'language' => 'භාෂාව',
+    'current_channel' => 'වත්මන් නාලිකාව',
     'created' => 'නිර්මිත',
     'updated' => 'යාවත්කාලීන',
     'related_anthology' => 'සම්බන්ධිත සංග්‍රහ',

+ 1 - 0
api-v13/resources/lang/th/library.php

@@ -87,6 +87,7 @@ return [
     'about_anthology' => 'เกี่ยวกับรวมบทความนี้',
     'anthology_info' => 'ข้อมูลรวมบทความ',
     'language' => 'ภาษา',
+    'current_channel' => 'ช่องปัจจุบัน',
     'created' => 'วันที่สร้าง',
     'updated' => 'อัปเดต',
     'related_anthology' => 'รวมบทความที่เกี่ยวข้อง',

+ 2 - 1
api-v13/resources/lang/zh-Hans/library.php

@@ -87,6 +87,7 @@ return [
     'about_anthology' => '关于本文集',
     'anthology_info' => '文集信息',
     'language' => '语言',
+    'current_channel' => '当前频道',
     'created' => '创建',
     'updated' => '更新',
     'related_anthology' => '相关文集',
@@ -133,5 +134,5 @@ return [
     'no_toc' => '此书没有目录',
     'pāḷi' => '巴利原典',
     'aṭṭhakathā' => '义注',
-    'ṭīkā' => '复注'
+    'ṭīkā' => '复注',
 ];

+ 1 - 0
api-v13/resources/lang/zh-Hant/library.php

@@ -87,6 +87,7 @@ return [
     'about_anthology' => '關於此文集',
     'anthology_info' => '文集資訊',
     'language' => '語言',
+    'current_channel' => '目前頻道',
     'created' => '建立時間',
     'updated' => '更新時間',
     'related_anthology' => '相關文集',

+ 17 - 6
api-v13/resources/views/library/tipitaka/category.blade.php

@@ -23,7 +23,7 @@
 <li class="breadcrumb-item active">{{ $breadcrumb['name'] }}</li>
 @else
 <li class="breadcrumb-item">
-    <a href="{{ route('library.tipitaka.category', ['id' => $breadcrumb['id']]) }}">
+    <a href="{{ route('library.tipitaka.category', array_filter(['id' => $breadcrumb['id'], 'channel' => request('channel')])) }}">
         {{ $breadcrumb['name'] }}
     </a>
 </li>
@@ -48,14 +48,14 @@
                 <div class="wiki-sidebar-title">{{ __('library.categories') }}</div>
                 <ul class="wiki-cat-list">
                     <li>
-                        <a href="{{ route('library.tipitaka.index') }}"
+                        <a href="{{ route('library.tipitaka.index', array_filter(['channel' => request('channel')])) }}"
                             class="{{ !$currentCategory['id'] ? 'active' : '' }}">
                             {{ __('library.all') }}
                         </a>
                     </li>
                     @foreach($types as $type)
                     <li>
-                        <a href="{{ route('library.tipitaka.category', ['id' => $type['id']]) }}"
+                        <a href="{{ route('library.tipitaka.category', array_filter(['id' => $type['id'], 'channel' => request('channel')])) }}"
                             class="{{ ($currentCategory['id'] ?? null) == $type['id'] ? 'active' : '' }}">
                             {{ $type['name'] }}
                         </a>
@@ -82,6 +82,15 @@
             {{-- 2. 过滤器区 --}}
             <div class="wiki-card tipitaka-filters">
 
+                @if($currentChannel)
+                {{-- 当前频道 --}}
+                <div class="tipitaka-filter-row">
+                    <span class="tipitaka-filter-label">{{ __('library.current_channel') }}</span>
+                    <div class="tipitaka-filter-pills">
+                        <span class="tipitaka-pill tipitaka-pill--active">{{ $currentChannel['name'] }}</span>
+                    </div>
+                </div>
+                @else
                 {{-- 类型 --}}
                 <div class="tipitaka-filter-row">
                     <span class="tipitaka-filter-label">{{ __('library.type') }}</span>
@@ -113,6 +122,7 @@
                         @endforeach
                     </div>
                 </div>
+                @endif
 
                 {{-- 作者 --}}
                 <div class="tipitaka-filter-row" style="display:none">
@@ -132,11 +142,12 @@
                 @php
                 $hasFilter = $selected['type'] !== 'all'
                 || $selected['lang'] !== 'all'
-                || $selected['author'] !== 'all';
+                || $selected['author'] !== 'all'
+                || $selected['channel'] !== 'all';
                 @endphp
                 @if($hasFilter)
                 <div class="tipitaka-filter-clear">
-                    <a href="{{ request()->fullUrlWithQuery(['type' => null, 'lang' => null, 'author' => null, 'page' => null]) }}"
+                    <a href="{{ rtrim(request()->fullUrlWithQuery(['type' => null, 'lang' => null, 'author' => null, 'channel' => null, 'page' => null]), '?') }}"
                         class="tipitaka-clear-btn">
                         <i class="ti ti-x"></i> {{ __('library.clear_filters') }}
                     </a>
@@ -150,7 +161,7 @@
             <div class="wiki-card tipitaka-subcategories">
                 <div class="tipitaka-subcategory-grid">
                     @foreach($subCategories as $sub)
-                    <a href="{{ route('library.tipitaka.category', array_filter(['id' => $sub['id'], 'book' => $sub['book'] ?? null])) }}"
+                    <a href="{{ route('library.tipitaka.category', array_filter(['id' => $sub['id'], 'book' => $sub['book'] ?? null, 'channel' => request('channel')])) }}"
                         class="tipitaka-subcategory-item">
                         <i class="ti {{ !empty($sub['book']) ? 'ti-book' : 'ti-folder' }} tipitaka-subcategory-icon" aria-hidden="true"></i>
                         <span class="tipitaka-subcategory-name">{{ $sub['name'] }}</span>