CategoryController.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. <?php
  2. namespace App\Http\Controllers;
  3. use Illuminate\Http\Request;
  4. use Illuminate\Support\Facades\File;
  5. use Illuminate\Support\Facades\DB;
  6. use App\Models\PaliText;
  7. use App\Models\ProgressChapter;
  8. use App\Models\Tag;
  9. use App\Models\TagMap;
  10. class CategoryController extends Controller
  11. {
  12. // 封面渐变色池:uid 首字节取余循环,保证同一文集颜色稳定
  13. private array $coverGradients = [
  14. 'linear-gradient(160deg, #2d1020, #ae6b8b)',
  15. 'linear-gradient(160deg, #1a2d10,rgba(75, 114, 36, 0.61))',
  16. 'linear-gradient(160deg, #0d1f3c,rgb(55, 98, 150))',
  17. 'linear-gradient(160deg, #2d1020,rgb(151, 69, 94))',
  18. 'linear-gradient(160deg, #1a1a2d,rgb(76, 68, 146))',
  19. 'linear-gradient(160deg, #1a2820,rgb(55, 124, 99))',
  20. ];
  21. // -------------------------------------------------------------------------
  22. // 从 uid / id 字符串中提取一个稳定的整数,用于色池取余
  23. // -------------------------------------------------------------------------
  24. private function colorIndex(string $uid): int
  25. {
  26. return hexdec(substr(str_replace('-', '', $uid), 0, 4)) % 255;
  27. }
  28. protected static int $nextId = 1;
  29. public function home()
  30. {
  31. $categories = $this->loadCategories();
  32. // 获取一级分类和对应的书籍
  33. $categoryData = [];
  34. foreach ($categories as $category) {
  35. if ($category['level'] == 1) {
  36. $children = $this->subCategories($categories, $category['id']);
  37. $categoryData[] = [
  38. 'category' => $category,
  39. 'children' => $children,
  40. ];
  41. }
  42. }
  43. $recentBooks = $this->getRecent();
  44. return view('library.index', compact(
  45. 'categoryData',
  46. 'categories',
  47. 'recentBooks'
  48. ));
  49. }
  50. public function index()
  51. {
  52. $categories = $this->loadCategories();
  53. // 获取一级分类和对应的书籍
  54. $categoryData = [];
  55. foreach ($categories as $category) {
  56. if ($category['level'] == 1) {
  57. $children = $this->subCategories($categories, $category['id']);
  58. $categoryData[] = [
  59. 'category' => $category,
  60. 'children' => $children,
  61. ];
  62. }
  63. }
  64. return view('library.index', compact('categoryData', 'categories'));
  65. }
  66. // app/Http/Controllers/Library/CategoryController.php
  67. // category() 方法修改版
  68. // 变更:
  69. // 1. $id 改为可选参数,无参数时显示顶级分类(首页复用)
  70. // 2. 新增 $filters 过滤参数(type / lang / author / sort)
  71. // 3. 新增右边栏数据:$recommended / $activeAuthors
  72. // 4. 新增 $filterOptions(过滤器选项 + 计数)
  73. // 5. 新增 $totalCount
  74. public function category(?int $id = null)
  75. {
  76. $categories = $this->loadCategories();
  77. // ── 当前分类 ──────────────────────────────────────────
  78. if ($id) {
  79. $currentCategory = collect($categories)->firstWhere('id', $id);
  80. if (!$currentCategory) {
  81. abort(404);
  82. }
  83. $breadcrumbs = $this->getBreadcrumbs($currentCategory, $categories);
  84. } else {
  85. // 首页:虚拟顶级分类
  86. $currentCategory = ['id' => null, 'name' => '三藏'];
  87. $breadcrumbs = [];
  88. }
  89. // ── 子分类 ─────────────────────────────────────────────
  90. $subCategories = array_values(array_filter(
  91. $categories,
  92. fn($cat) => $cat['parent_id'] == $id
  93. ));
  94. // ── 过滤参数 ────────────────────────────────────────────
  95. $selectedType = request('type', 'all');
  96. $selectedLang = request('lang', 'all');
  97. $selectedAuthor = request('author', 'all');
  98. $selectedSort = request('sort', 'updated_at');
  99. $selected = [
  100. 'type' => $selectedType,
  101. 'lang' => $selectedLang,
  102. 'author' => $selectedAuthor,
  103. 'sort' => $selectedSort,
  104. ];
  105. // ── 书籍列表(过滤+排序,真实实现替换此处) ──────────────
  106. $categoryBooks = $this->getBooks($categories, $id, $selected);
  107. // TODO: 将 $selected 传入 getBooks() 做实际过滤
  108. $totalCount = count($categoryBooks);
  109. // ── 过滤器选项(mock,真实实现从书籍数据聚合) ────────────
  110. $filterOptions = [
  111. 'types' => [
  112. ['value' => 'all', 'label' => '全部', 'count' => $totalCount],
  113. ['value' => 'original', 'label' => '原文', 'count' => 0],
  114. ['value' => 'translation', 'label' => '译文', 'count' => 0],
  115. ['value' => 'nissaya', 'label' => 'Nissaya', 'count' => 0],
  116. ],
  117. 'languages' => [
  118. ['value' => 'all', 'label' => '全部', 'count' => $totalCount],
  119. ['value' => 'zh-Hans', 'label' => '简体中文', 'count' => 0],
  120. ['value' => 'zh-Hant', 'label' => '繁体中文', 'count' => 0],
  121. ['value' => 'pi', 'label' => '巴利语', 'count' => 0],
  122. ['value' => 'en', 'label' => '英语', 'count' => 0],
  123. ],
  124. 'authors' => $this->getAuthorOptions($categoryBooks),
  125. ];
  126. // ── 右边栏:本周推荐(mock) ────────────────────────────
  127. $recommended = [
  128. ['id' => 1, 'title' => '相应部·因缘篇', 'category' => '经藏'],
  129. ['id' => 2, 'title' => '法句经', 'category' => '经藏'],
  130. ['id' => 3, 'title' => '清净道论', 'category' => '注释'],
  131. ['id' => 4, 'title' => '律藏·波罗夷', 'category' => '律藏'],
  132. ['id' => 5, 'title' => '长部·梵网经', 'category' => '经藏'],
  133. ];
  134. // ── 右边栏:活跃译者(mock) ────────────────────────────
  135. $activeAuthors = [
  136. [
  137. 'name' => 'Bhikkhu Bodhi',
  138. 'avatar' => null,
  139. 'color' => '#2d5a8e',
  140. 'initials' => 'BB',
  141. 'count' => 24,
  142. ],
  143. [
  144. 'name' => 'Bhikkhu Sujato',
  145. 'avatar' => null,
  146. 'color' => '#5a2d8e',
  147. 'initials' => 'BS',
  148. 'count' => 18,
  149. ],
  150. [
  151. 'name' => 'Buddhaghosa',
  152. 'avatar' => null,
  153. 'color' => '#8e5a2d',
  154. 'initials' => 'BG',
  155. 'count' => 12,
  156. ],
  157. [
  158. 'name' => 'Bhikkhu Brahmali',
  159. 'avatar' => null,
  160. 'color' => '#2d8e5a',
  161. 'initials' => 'BR',
  162. 'count' => 9,
  163. ],
  164. ];
  165. $types = $this->types();
  166. return view('library.tipitaka.category', compact(
  167. 'currentCategory',
  168. 'subCategories',
  169. 'categoryBooks',
  170. 'breadcrumbs',
  171. 'types',
  172. 'selected',
  173. 'filterOptions',
  174. 'totalCount',
  175. 'recommended',
  176. 'activeAuthors',
  177. ));
  178. }
  179. // ── 辅助:从书籍列表聚合作者选项(mock,真实实现替换) ─────────
  180. private function getAuthorOptions(array $books): array
  181. {
  182. // TODO: 从 $books 聚合真实作者列表
  183. return [
  184. ['value' => 'all', 'label' => '全部作者', 'count' => count($books)],
  185. ['value' => 'bhikkhu-bodhi', 'label' => 'Bhikkhu Bodhi', 'count' => 0],
  186. ['value' => 'bhikkhu-sujato', 'label' => 'Bhikkhu Sujato', 'count' => 0],
  187. ['value' => 'buddhaghosa', 'label' => 'Buddhaghosa', 'count' => 0],
  188. ['value' => 'bhikkhu-brahmali', 'label' => 'Bhikkhu Brahmali', 'count' => 0],
  189. ];
  190. }
  191. private function types()
  192. {
  193. return [
  194. ['id' => '1', 'name' => 'sutta'],
  195. ['id' => '48', 'name' => 'vinaya'],
  196. ['id' => '66', 'name' => 'abhidhamma'],
  197. ['id' => '82', 'name' => 'añña']
  198. ];
  199. }
  200. private function subCategories($categories, int $id)
  201. {
  202. return array_filter($categories, function ($cat) use ($id) {
  203. return $cat['parent_id'] == $id;
  204. });
  205. }
  206. private function getRecent()
  207. {
  208. return [
  209. [
  210. 'id' => 'book-001',
  211. 'title' => '相应部·因缘篇',
  212. 'author' => 'Bhikkhu Bodhi',
  213. 'cover' => null, // 无封面时显示渐变
  214. 'cover_gradient' => 'linear-gradient(135deg, #2d5a8e 0%, #1a3a5c 100%)',
  215. 'updated_at' => '2小时前',
  216. 'is_new' => true, // true=新增, false=更新
  217. 'category' => '经藏',
  218. ],
  219. [
  220. 'id' => 'book-002',
  221. 'title' => '长部·梵网经',
  222. 'author' => 'Bhikkhu Sujato',
  223. 'cover' => null,
  224. 'cover_gradient' => 'linear-gradient(135deg, #5a2d8e 0%, #3a1a5c 100%)',
  225. 'updated_at' => '昨天',
  226. 'is_new' => false,
  227. 'category' => '经藏',
  228. ],
  229. [
  230. 'id' => 'book-003',
  231. 'title' => '法句经注',
  232. 'author' => 'Buddhaghosa',
  233. 'cover' => null,
  234. 'cover_gradient' => 'linear-gradient(135deg, #8e5a2d 0%, #5c3a1a 100%)',
  235. 'updated_at' => '3天前',
  236. 'is_new' => false,
  237. 'category' => '注释',
  238. ],
  239. [
  240. 'id' => 'book-004',
  241. 'title' => '律藏·波罗夷',
  242. 'author' => 'Bhikkhu Brahmali',
  243. 'cover' => null,
  244. 'cover_gradient' => 'linear-gradient(135deg, #2d8e5a 0%, #1a5c3a 100%)',
  245. 'updated_at' => '5天前',
  246. 'is_new' => true,
  247. 'category' => '律藏',
  248. ],
  249. [
  250. 'id' => 'book-005',
  251. 'title' => '清净道论',
  252. 'author' => 'Buddhaghosa',
  253. 'cover' => null,
  254. 'cover_gradient' => 'linear-gradient(135deg, #8e2d2d 0%, #5c1a1a 100%)',
  255. 'updated_at' => '1周前',
  256. 'is_new' => false,
  257. 'category' => '注释',
  258. ],
  259. [
  260. 'id' => 'book-006',
  261. 'title' => '增支部·一集',
  262. 'author' => 'Bhikkhu Bodhi',
  263. 'cover' => null,
  264. 'cover_gradient' => 'linear-gradient(135deg, #2d7a8e 0%, #1a4a5c 100%)',
  265. 'updated_at' => '1周前',
  266. 'is_new' => false,
  267. 'category' => '经藏',
  268. ],
  269. ];
  270. }
  271. private function getUpdateBooks()
  272. {
  273. $books = ProgressChapter::with('channel.owner')
  274. ->leftJoin('pali_texts', function ($join) {
  275. $join->on('progress_chapters.book', '=', 'pali_texts.book')
  276. ->on('progress_chapters.para', '=', 'pali_texts.paragraph');
  277. })
  278. ->whereHas('channel', function ($query) {
  279. $query->where('status', 30);
  280. })
  281. ->where('progress', '>', config('mint.library.list_min_progress'))
  282. ->take(10)
  283. ->get();
  284. return $this->getBooksInfo($books);
  285. }
  286. private function getBooks($categories, $id, $filters)
  287. {
  288. if ($id) {
  289. $currentCategory = collect($categories)->firstWhere('id', $id);
  290. if (!$currentCategory) {
  291. abort(404);
  292. }
  293. // 标签查章节
  294. $tagNames = $currentCategory['tag'];
  295. $tm = (new TagMap)->getTable();
  296. $tg = (new Tag)->getTable();
  297. $pt = (new PaliText)->getTable();
  298. $where1 = " where co = " . count($tagNames);
  299. $a = implode(",", array_fill(0, count($tagNames), '?'));
  300. $in1 = "and t.name in ({$a})";
  301. $param = $tagNames;
  302. $where2 = "where level = 1";
  303. $query = "select uid as id,book,paragraph,level,toc as title,chapter_strlen,parent,path from (
  304. select anchor_id as cid from (
  305. select tm.anchor_id , count(*) as co
  306. from $tm as tm
  307. left join $tg as t on tm.tag_id = t.id
  308. where tm.table_name = 'pali_texts'
  309. $in1
  310. group by tm.anchor_id
  311. ) T
  312. $where1
  313. ) CID
  314. left join $pt as pt on CID.cid = pt.uid
  315. $where2
  316. order by book,paragraph";
  317. $chapters = DB::select($query, $param);
  318. $chaptersParam = [];
  319. foreach ($chapters as $key => $chapter) {
  320. $chaptersParam[] = [$chapter->book, $chapter->paragraph];
  321. }
  322. // 获取该分类下的章节
  323. $books = ProgressChapter::with('channel.owner')
  324. ->whereIns(['progress_chapters.book', 'progress_chapters.para'], $chaptersParam)
  325. ->whereHas('channel', function ($query) {
  326. $query->where('status', 30);
  327. })
  328. ->where('progress', '>', config('mint.library.list_min_progress'))
  329. ->get();
  330. } else {
  331. $booksChapter = PaliText::select(['book', 'paragraph'])->where('level', 1)->get();
  332. $chapters = [];
  333. foreach ($booksChapter as $key => $value) {
  334. $chapters[] = [$value->book, $value->paragraph];
  335. }
  336. $books = ProgressChapter::with('channel.owner')
  337. ->whereHas('channel', function ($query) use ($filters) {
  338. $filters['type'] === 'all' ? $query->where('status', 30) :
  339. $query->where('status', 30)->where('type', $filters['type']);
  340. })
  341. ->where('progress', '>', config('mint.library.list_min_progress'))
  342. ->whereIns(['book', 'para'], $chapters)
  343. ->take(100)
  344. ->get();
  345. }
  346. return $this->getBooksInfo($books);
  347. }
  348. private function getBooksInfo($books,)
  349. {
  350. $pali = PaliText::where('level', 1)->get();
  351. // 获取该分类下的书籍
  352. $categoryBooks = [];
  353. $books->each(function ($book) use (&$categoryBooks, $pali) {
  354. $title = $book->title;
  355. if (empty($title)) {
  356. $title = $pali->firstWhere('book', $book->book)->toc;
  357. }
  358. //Log::debug('getBooksInfo', ['book' => $book->book, 'paragraph' => $book->para]);
  359. $pcd_book_id = $pali->first(function ($item) use ($book) {
  360. return $item->book == $book->book
  361. && $item->paragraph == $book->para;
  362. })?->pcd_book_id;
  363. $coverFile = "/assets/images/cover/zh-hans/1/{$pcd_book_id}.png";
  364. if (File::exists(public_path($coverFile))) {
  365. $coverUrl = $coverFile;
  366. } else {
  367. $coverUrl = null;
  368. }
  369. $colorIdx = $this->colorIndex($book->uid);
  370. $categoryBooks[] = [
  371. "id" => $book->uid,
  372. "title" => $title,
  373. "author" => $book->channel->name,
  374. "publisher" => $book->channel->owner,
  375. "type" => __('labels.' . $book->channel->type),
  376. "cover" => $coverUrl,
  377. 'cover_gradient' => $this->coverGradients[$colorIdx % count($this->coverGradients)],
  378. "description" => $book->summary ?? "比库戒律的详细说明",
  379. "language" => __('language.' . $book->channel->lang),
  380. ];
  381. });
  382. return $categoryBooks;
  383. }
  384. private function loadCategories()
  385. {
  386. $json = file_get_contents(public_path("data/category/default.json"));
  387. $tree = json_decode($json, true);
  388. $flat = self::flattenWithIds($tree);
  389. return $flat;
  390. }
  391. public static function flattenWithIds(array $tree, int $parentId = 0, int $level = 1): array
  392. {
  393. $flat = [];
  394. foreach ($tree as $node) {
  395. $currentId = self::$nextId++;
  396. $item = [
  397. 'id' => $currentId,
  398. 'parent_id' => $parentId,
  399. 'name' => $node['name'] ?? null,
  400. 'tag' => $node['tag'] ?? [],
  401. "description" => "佛教戒律经典",
  402. 'level' => $level,
  403. ];
  404. $flat[] = $item;
  405. if (isset($node['children']) && is_array($node['children'])) {
  406. $childrenLevel = $level + 1;
  407. $flat = array_merge($flat, self::flattenWithIds($node['children'], $currentId, $childrenLevel));
  408. }
  409. }
  410. return $flat;
  411. }
  412. private function getBreadcrumbs($category, $categories)
  413. {
  414. $breadcrumbs = [];
  415. $current = $category;
  416. while ($current) {
  417. array_unshift($breadcrumbs, $current);
  418. $current = collect($categories)->firstWhere('id', $current['parent_id']);
  419. }
  420. return $breadcrumbs;
  421. }
  422. }