TipitakaController.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. <?php
  2. namespace App\Http\Controllers\Library;
  3. use App\Http\Controllers\Controller;
  4. use Illuminate\Http\Request;
  5. use Illuminate\Support\Facades\File;
  6. use Illuminate\Support\Facades\DB;
  7. use App\Models\PaliText;
  8. use App\Models\ProgressChapter;
  9. use App\Models\Tag;
  10. use App\Models\TagMap;
  11. class TipitakaController extends Controller
  12. {
  13. // 封面渐变色池:uid 首字节取余循环,保证同一文集颜色稳定
  14. private array $coverGradients = [
  15. 'linear-gradient(160deg, #2d1020, #ae6b8b)',
  16. 'linear-gradient(160deg, #1a2d10,rgba(75, 114, 36, 0.61))',
  17. 'linear-gradient(160deg, #0d1f3c,rgb(55, 98, 150))',
  18. 'linear-gradient(160deg, #2d1020,rgb(151, 69, 94))',
  19. 'linear-gradient(160deg, #1a1a2d,rgb(76, 68, 146))',
  20. 'linear-gradient(160deg, #1a2820,rgb(55, 124, 99))',
  21. ];
  22. // -------------------------------------------------------------------------
  23. // 从 uid / id 字符串中提取一个稳定的整数,用于色池取余
  24. // -------------------------------------------------------------------------
  25. private function colorIndex(string $uid): int
  26. {
  27. return hexdec(substr(str_replace('-', '', $uid), 0, 4)) % 255;
  28. }
  29. protected static int $nextId = 1;
  30. // app/Http/Controllers/Library/CategoryController.php
  31. // category() 方法修改版
  32. // 变更:
  33. // 1. $id 改为可选参数,无参数时显示顶级分类(首页复用)
  34. // 2. 新增 $filters 过滤参数(type / lang / author / sort)
  35. // 3. 新增右边栏数据:$recommended / $activeAuthors
  36. // 4. 新增 $filterOptions(过滤器选项 + 计数)
  37. // 5. 新增 $totalCount
  38. public function index(Request $request, ?int $id = null)
  39. {
  40. $categories = $this->loadCategories();
  41. // ── 当前分类 ──────────────────────────────────────────
  42. if ($id) {
  43. $currentCategory = collect($categories)->firstWhere('id', $id);
  44. if (!$currentCategory) {
  45. abort(404);
  46. }
  47. $breadcrumbs = $this->getBreadcrumbs($currentCategory, $categories);
  48. } else {
  49. // 首页:虚拟顶级分类
  50. $currentCategory = ['id' => null, 'name' => '三藏'];
  51. $breadcrumbs = [];
  52. }
  53. // ── 子分类 ─────────────────────────────────────────────
  54. $subCategories = array_values(array_filter(
  55. $categories,
  56. fn($cat) => $cat['parent_id'] == $id
  57. ));
  58. // ── 过滤参数 ────────────────────────────────────────────
  59. $selectedType = request('type', 'all');
  60. $selectedLang = request('lang', 'all');
  61. $selectedAuthor = request('author', 'all');
  62. $selectedSort = request('sort', 'updated_at');
  63. $selected = [
  64. 'type' => $selectedType,
  65. 'lang' => $selectedLang,
  66. 'author' => $selectedAuthor,
  67. 'sort' => $selectedSort,
  68. ];
  69. // ── 书籍列表(过滤+排序,真实实现替换此处) ──────────────
  70. $categoryBooks = $this->getBooks($categories, $id, $selected);
  71. // TODO: 将 $selected 传入 getBooks() 做实际过滤
  72. $totalCount = count($categoryBooks);
  73. // ── 过滤器选项(mock,真实实现从书籍数据聚合) ────────────
  74. $filterOptions = [
  75. 'types' => [
  76. ['value' => 'all', 'label' => '全部', 'count' => $totalCount],
  77. ['value' => 'original', 'label' => '原文', 'count' => 0],
  78. ['value' => 'translation', 'label' => '译文', 'count' => 0],
  79. ['value' => 'nissaya', 'label' => 'Nissaya', 'count' => 0],
  80. ],
  81. 'languages' => [
  82. ['value' => 'all', 'label' => '全部', 'count' => $totalCount],
  83. ['value' => 'zh-Hans', 'label' => '简体中文', 'count' => 0],
  84. ['value' => 'zh-Hant', 'label' => '繁体中文', 'count' => 0],
  85. ['value' => 'pi', 'label' => '巴利语', 'count' => 0],
  86. ['value' => 'en', 'label' => '英语', 'count' => 0],
  87. ],
  88. 'authors' => $this->getAuthorOptions($categoryBooks),
  89. ];
  90. // ── 右边栏:本周推荐(mock) ────────────────────────────
  91. $recommended = [
  92. ['id' => 1, 'title' => '相应部·因缘篇', 'category' => '经藏'],
  93. ['id' => 2, 'title' => '法句经', 'category' => '经藏'],
  94. ['id' => 3, 'title' => '清净道论', 'category' => '注释'],
  95. ['id' => 4, 'title' => '律藏·波罗夷', 'category' => '律藏'],
  96. ['id' => 5, 'title' => '长部·梵网经', 'category' => '经藏'],
  97. ];
  98. // ── 右边栏:活跃译者(mock) ────────────────────────────
  99. $activeAuthors = [
  100. [
  101. 'name' => 'Bhikkhu Bodhi',
  102. 'avatar' => null,
  103. 'color' => '#2d5a8e',
  104. 'initials' => 'BB',
  105. 'count' => 24,
  106. ],
  107. [
  108. 'name' => 'Bhikkhu Sujato',
  109. 'avatar' => null,
  110. 'color' => '#5a2d8e',
  111. 'initials' => 'BS',
  112. 'count' => 18,
  113. ],
  114. [
  115. 'name' => 'Buddhaghosa',
  116. 'avatar' => null,
  117. 'color' => '#8e5a2d',
  118. 'initials' => 'BG',
  119. 'count' => 12,
  120. ],
  121. [
  122. 'name' => 'Bhikkhu Brahmali',
  123. 'avatar' => null,
  124. 'color' => '#2d8e5a',
  125. 'initials' => 'BR',
  126. 'count' => 9,
  127. ],
  128. ];
  129. $types = $this->types();
  130. return view('library.tipitaka.category', compact(
  131. 'currentCategory',
  132. 'subCategories',
  133. 'categoryBooks',
  134. 'breadcrumbs',
  135. 'types',
  136. 'selected',
  137. 'filterOptions',
  138. 'totalCount',
  139. 'recommended',
  140. 'activeAuthors',
  141. ));
  142. }
  143. // ── 辅助:从书籍列表聚合作者选项(mock,真实实现替换) ─────────
  144. private function getAuthorOptions(array $books): array
  145. {
  146. // TODO: 从 $books 聚合真实作者列表
  147. return [
  148. ['value' => 'all', 'label' => '全部作者', 'count' => count($books)],
  149. ['value' => 'bhikkhu-bodhi', 'label' => 'Bhikkhu Bodhi', 'count' => 0],
  150. ['value' => 'bhikkhu-sujato', 'label' => 'Bhikkhu Sujato', 'count' => 0],
  151. ['value' => 'buddhaghosa', 'label' => 'Buddhaghosa', 'count' => 0],
  152. ['value' => 'bhikkhu-brahmali', 'label' => 'Bhikkhu Brahmali', 'count' => 0],
  153. ];
  154. }
  155. private function types()
  156. {
  157. return [
  158. ['id' => '1', 'name' => 'sutta'],
  159. ['id' => '48', 'name' => 'vinaya'],
  160. ['id' => '66', 'name' => 'abhidhamma'],
  161. ['id' => '82', 'name' => 'añña']
  162. ];
  163. }
  164. private function subCategories($categories, int $id)
  165. {
  166. return array_filter($categories, function ($cat) use ($id) {
  167. return $cat['parent_id'] == $id;
  168. });
  169. }
  170. private function getBooks($categories, $id, $filters)
  171. {
  172. if ($id) {
  173. $currentCategory = collect($categories)->firstWhere('id', $id);
  174. if (!$currentCategory) {
  175. abort(404);
  176. }
  177. // 标签查章节
  178. $tagNames = $currentCategory['tag'];
  179. $booksChapter = PaliText::withAllTags($tagNames)
  180. ->where('level', 1)->get();
  181. } else {
  182. $booksChapter = PaliText::select(['book', 'paragraph'])
  183. ->where('level', 1)
  184. ->get();
  185. }
  186. $chapters = [];
  187. foreach ($booksChapter as $key => $value) {
  188. $chapters[] = [$value->book, $value->paragraph];
  189. }
  190. $books = ProgressChapter::with('channel.owner')
  191. ->whereHas('channel', function ($query) use ($filters) {
  192. $query->where('status', 30);
  193. if ($filters['type'] !== 'all') {
  194. $query->where('type', $filters['type']);
  195. }
  196. if ($filters['lang'] !== 'all') {
  197. $query->where('lang', $filters['lang']);
  198. }
  199. })
  200. ->where('progress', '>', config('mint.library.list_min_progress'))
  201. ->whereIns(['book', 'para'], $chapters)
  202. ->take(100)
  203. ->get();
  204. return $this->getBooksInfo($books);
  205. }
  206. private function getBooksInfo($books,)
  207. {
  208. $pali = PaliText::where('level', 1)->get();
  209. // 获取该分类下的书籍
  210. $categoryBooks = [];
  211. $books->each(function ($book) use (&$categoryBooks, $pali) {
  212. $title = $book->title;
  213. if (empty($title)) {
  214. $title = $pali->firstWhere('book', $book->book)->toc;
  215. }
  216. //Log::debug('getBooksInfo', ['book' => $book->book, 'paragraph' => $book->para]);
  217. $pcd_book_id = $pali->first(function ($item) use ($book) {
  218. return $item->book == $book->book
  219. && $item->paragraph == $book->para;
  220. })?->pcd_book_id;
  221. $coverFile = "/assets/images/cover/zh-hans/1/{$pcd_book_id}.png";
  222. if (File::exists(public_path($coverFile))) {
  223. $coverUrl = $coverFile;
  224. } else {
  225. $coverUrl = null;
  226. }
  227. $colorIdx = $this->colorIndex($book->uid);
  228. $categoryBooks[] = [
  229. "id" => $book->uid,
  230. "title" => $title,
  231. "author" => $book->channel->name,
  232. "publisher" => $book->channel->owner,
  233. "type" => __('labels.' . $book->channel->type),
  234. "cover" => $coverUrl,
  235. 'cover_gradient' => $this->coverGradients[$colorIdx % count($this->coverGradients)],
  236. "description" => $book->summary ?? "比库戒律的详细说明",
  237. "language" => __('language.' . $book->channel->lang),
  238. ];
  239. });
  240. return $categoryBooks;
  241. }
  242. private function loadCategories()
  243. {
  244. $json = file_get_contents(public_path("data/category/default.json"));
  245. $tree = json_decode($json, true);
  246. $flat = self::flattenWithIds($tree);
  247. return $flat;
  248. }
  249. public static function flattenWithIds(array $tree, int $parentId = 0, int $level = 1): array
  250. {
  251. $flat = [];
  252. foreach ($tree as $node) {
  253. $currentId = self::$nextId++;
  254. $item = [
  255. 'id' => $currentId,
  256. 'parent_id' => $parentId,
  257. 'name' => $node['name'] ?? null,
  258. 'tag' => $node['tag'] ?? [],
  259. "description" => "佛教戒律经典",
  260. 'level' => $level,
  261. ];
  262. $flat[] = $item;
  263. if (isset($node['children']) && is_array($node['children'])) {
  264. $childrenLevel = $level + 1;
  265. $flat = array_merge($flat, self::flattenWithIds($node['children'], $currentId, $childrenLevel));
  266. }
  267. }
  268. return $flat;
  269. }
  270. private function getBreadcrumbs($category, $categories)
  271. {
  272. $breadcrumbs = [];
  273. $current = $category;
  274. while ($current) {
  275. array_unshift($breadcrumbs, $current);
  276. $current = collect($categories)->firstWhere('id', $current['parent_id']);
  277. }
  278. return $breadcrumbs;
  279. }
  280. }