AnthologyReadController.php 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. <?php
  2. namespace App\Http\Controllers\Library;
  3. use App\Http\Controllers\Controller;
  4. use App\Services\CollectionService;
  5. use App\Services\ArticleService;
  6. use Illuminate\Http\Request;
  7. class AnthologyReadController extends Controller
  8. {
  9. public function __construct(
  10. private CollectionService $collectionService,
  11. private ArticleService $articleService,
  12. ) {}
  13. // =========================================================================
  14. // read
  15. // GET /library/anthology/{anthology}/read/{article}
  16. // =========================================================================
  17. public function read(Request $request, string $anthologyId, string $articleId)
  18. {
  19. // ── 1. 获取文集信息 ───────────────────────────────────────────────────
  20. $colResult = $this->collectionService->getCollection($anthologyId);
  21. if (isset($colResult['error'])) {
  22. abort($colResult['code'] ?? 404, $colResult['error']);
  23. }
  24. $col = is_array($colResult['data'])
  25. ? $colResult['data']
  26. : $colResult['data']->toArray(request());
  27. // ── 2. 构建目录(方案A:展开祖先链,其余折叠) ───────────────────────
  28. $fullArticleList = collect($col['article_list'] ?? []);
  29. $toc = $this->buildCollapsedToc($fullArticleList->toArray(), $articleId);
  30. // ── 3. 获取当前文章内容 ───────────────────────────────────────────────
  31. $artResult = $this->articleService->getArticle($articleId);
  32. if (isset($artResult['error'])) {
  33. abort($artResult['code'] ?? 404, $artResult['error']);
  34. }
  35. // ArticleResource 需要 format=html
  36. $urlParam = [
  37. 'mode' => 'read',
  38. 'format' => 'html',
  39. 'anthology' => $anthologyId,
  40. 'channel' => $request->input('channel', null),
  41. 'origin' => 'true',
  42. 'paragraph' => true,
  43. ];
  44. $fakeRequest = Request::create('', 'GET', $urlParam);
  45. $artResource = $artResult['data'];
  46. $artArray = $artResource->toArray($fakeRequest);
  47. // content 统一包装成 book.read 期望的格式:
  48. // [ ['id'=>..., 'level'=>1, 'text'=>[[ html_string ]]], ... ]
  49. $content = [[
  50. 'id' => $articleId,
  51. 'level' => 100,
  52. 'text' => [[$artArray['html'] ?? $artArray['content'] ?? 'null']],
  53. ]];
  54. // ── 4. 计算翻页(pagination) ─────────────────────────────────────────
  55. // 只在 level=1 的节点间翻页(与目录一致)
  56. $level1 = $fullArticleList
  57. ->filter(fn($a) => ($a['level'] ?? 1) === 1)
  58. ->values();
  59. $currentIndex = $level1->search(
  60. fn($a) => $a['article_id'] === $articleId
  61. );
  62. $prev = null;
  63. $next = null;
  64. if ($currentIndex !== false) {
  65. if ($currentIndex > 0) {
  66. $prevItem = $level1[$currentIndex - 1];
  67. $prev = [
  68. 'id' => $prevItem['article_id'],
  69. 'title' => $prevItem['title'],
  70. ];
  71. }
  72. if ($currentIndex < $level1->count() - 1) {
  73. $nextItem = $level1[$currentIndex + 1];
  74. $next = [
  75. 'id' => $nextItem['article_id'],
  76. 'title' => $nextItem['title'],
  77. ];
  78. }
  79. }
  80. $pagination = [
  81. 'start' => 0,
  82. 'end' => 0,
  83. 'prev' => $prev,
  84. 'next' => $next,
  85. ];
  86. // ── 5. 组装 $book(对齐 book.read 的数据结构) ───────────────────────
  87. $studio = $col['studio'] ?? [];
  88. // blade 用 $book['publisher']->username / ->nickname,必须是对象
  89. $publisher = (object) [
  90. 'username' => $studio['studioName'] ?? '',
  91. 'nickname' => $studio['nickName'] ?? $studio['studioName'] ?? '',
  92. ];
  93. $book = [
  94. 'id' => $articleId,
  95. 'title' => $artArray['title'] ?? ($col['title'] ?? ''),
  96. 'author' => $studio['nickName'] ?? $studio['studioName'] ?? '',
  97. 'publisher' => $publisher,
  98. 'type' => '',
  99. 'category_id' => null,
  100. 'cover' => null,
  101. 'description' => $col['summary'] ?? '',
  102. 'language' => $col['lang'] ?? '',
  103. 'anthology' => [
  104. 'id' => $anthologyId,
  105. 'title' => $col['title'] ?? '',
  106. ],
  107. 'categories' => [],
  108. 'tags' => [],
  109. 'downloads' => [],
  110. 'toc' => $toc,
  111. 'pagination' => $pagination,
  112. 'content' => $content,
  113. ];
  114. $channels = $this->articleService->articleChannels($articleId);
  115. // blade 里有 $relatedBooks,传空数组防止 undefined variable
  116. $relatedBooks = [];
  117. $editor_link = config('mint.server.dashboard_base_path') . "/workspace/anthology/{$anthologyId}/{$book['id']}";
  118. // 翻页路由需要 anthologyId,传给 blade 供覆盖路由使用
  119. return view('library.book.read', compact('book', 'relatedBooks', 'anthologyId', 'channels', 'editor_link'));
  120. }
  121. // =========================================================================
  122. // buildCollapsedToc
  123. //
  124. // 规则:
  125. // 1. 所有 level=1 节点始终显示
  126. // 2. 找出当前节点的「祖先链」(从 root 到当前节点经过的每个节点)
  127. // 3. 祖先链上每个节点的「直接子节点」都展开显示
  128. // 4. 当前节点本身若有子节点,同样展开一级
  129. // 5. 其余节点隐藏
  130. // =========================================================================
  131. private function buildCollapsedToc(array $list, string $currentId): array
  132. {
  133. // ── Step 1:为每个节点推导父节点 id ──────────────────────────────────
  134. // article_list 是有序的,父节点 = 当前节点之前最近的 level 更小的节点
  135. $parents = []; // article_id => parent_article_id | null
  136. $stack = []; // 维护祖先栈 [ ['id'=>..., 'level'=>...], ... ]
  137. foreach ($list as $item) {
  138. $id = $item['article_id'];
  139. $level = (int) ($item['level'] ?? 1);
  140. // 弹出所有 level >= 当前 level 的栈顶
  141. while (!empty($stack) && $stack[count($stack) - 1]['level'] >= $level) {
  142. array_pop($stack);
  143. }
  144. $parents[$id] = empty($stack) ? null : $stack[count($stack) - 1]['id'];
  145. $stack[] = ['id' => $id, 'level' => $level];
  146. }
  147. // ── Step 2:找出当前节点的祖先链 ─────────────────────────────────────
  148. $ancestorIds = [];
  149. $cursor = $currentId;
  150. while (isset($parents[$cursor]) && $parents[$cursor] !== null) {
  151. $cursor = $parents[$cursor];
  152. $ancestorIds[] = $cursor;
  153. }
  154. $ancestorSet = array_flip($ancestorIds);
  155. // ── Step 3:需要展开子节点的集合 = 祖先链 + 当前节点 ─────────────────
  156. $expandParentSet = $ancestorSet;
  157. $expandParentSet[$currentId] = true;
  158. // ── Step 4:过滤构建 toc ──────────────────────────────────────────────
  159. $toc = [];
  160. foreach ($list as $item) {
  161. $id = $item['article_id'];
  162. $level = (int) ($item['level'] ?? 1);
  163. $parentId = $parents[$id];
  164. $isActive = $id === $currentId;
  165. // 显示条件:
  166. // a) level=1(始终显示)
  167. // b) 父节点在 expandParentSet 中(祖先链或当前节点的直接子节点)
  168. $visible = $level === 1
  169. || ($parentId !== null && isset($expandParentSet[$parentId]));
  170. if (!$visible) {
  171. continue;
  172. }
  173. $toc[] = [
  174. 'id' => $id,
  175. 'title' => $item['title'],
  176. 'summary' => '',
  177. 'progress' => 0,
  178. 'level' => $level,
  179. 'disabled' => $isActive,
  180. 'active' => $isActive,
  181. ];
  182. }
  183. return $toc;
  184. }
  185. }