小说网站前端代码仓库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

335 lines
9.9 KiB

3 weeks ago
3 weeks ago
3 weeks ago
3 weeks ago
3 weeks ago
3 weeks ago
3 weeks ago
3 weeks ago
  1. <template>
  2. <div class="book-catalog-container">
  3. <div class="section-header">
  4. <h3 class="section-title">目录</h3>
  5. </div>
  6. <!-- 章节列表 -->
  7. <transition-group v-if="chapters.length > 0" name="chapter-transition" tag="div" class="chapter-grid">
  8. <div v-for="(chapter, index) in displayedChapters" :key="chapter.id"
  9. class="chapter-item"
  10. :class="{ 'is-current': chapter.id === currentChapterId }"
  11. @click="goToChapter(chapter.id)">
  12. <div class="chapter-title">
  13. {{ chapter.title }}
  14. <!-- <span class="current-badge" v-if="chapter.id === currentChapterId">正在阅读</span> -->
  15. <!-- <span class="new-badge" v-if="chapter.isNew">NEW</span> -->
  16. </div>
  17. <div class="chapter-pay" v-if="chapter.isPay === 'Y'">
  18. <span v-if="chapter.pay" class="paid-badge">已付费</span>
  19. <span v-else class="pay-badge">付费</span>
  20. </div>
  21. </div>
  22. </transition-group>
  23. <!-- 空状态 -->
  24. <div v-else class="empty-state">
  25. <div class="empty-icon">
  26. <svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
  27. <rect x="8" y="8" width="48" height="48" rx="4" stroke="#E5E5E5" stroke-width="2" fill="none"/>
  28. <rect x="16" y="16" width="32" height="2" rx="1" fill="#E5E5E5"/>
  29. <rect x="16" y="22" width="20" height="2" rx="1" fill="#E5E5E5"/>
  30. <rect x="16" y="28" width="24" height="2" rx="1" fill="#E5E5E5"/>
  31. <rect x="16" y="34" width="16" height="2" rx="1" fill="#E5E5E5"/>
  32. <rect x="16" y="40" width="28" height="2" rx="1" fill="#E5E5E5"/>
  33. <rect x="16" y="46" width="18" height="2" rx="1" fill="#E5E5E5"/>
  34. </svg>
  35. </div>
  36. <p class="empty-title">暂无章节</p>
  37. <p class="empty-desc">该书籍暂时没有可阅读的章节</p>
  38. </div>
  39. <div v-if="chapters.length > 0" class="catalog-footer">
  40. <div v-if="showAll" class="collapse-btn" @click="toggleShowAll">
  41. 收起 <i class="collapse-icon"></i>
  42. </div>
  43. <div v-else-if="!showAll && chapters.length > maxDisplayChapters" class="expand-btn" @click="toggleShowAll">
  44. 展开所有章节 <i class="expand-icon"></i>
  45. </div>
  46. </div>
  47. </div>
  48. </template>
  49. <script>
  50. import { defineComponent, ref, computed } from 'vue';
  51. import { useRouter } from 'vue-router';
  52. export default defineComponent({
  53. name: 'BookCatalog',
  54. emits: ['chapter-click'],
  55. props: {
  56. bookId: {
  57. type: String,
  58. default: ''
  59. },
  60. chapters: {
  61. type: Array,
  62. default: () => [
  63. ]
  64. },
  65. maxDisplayChapters: {
  66. type: Number,
  67. default: 12
  68. },
  69. currentChapterId: {
  70. type: String,
  71. default: ''
  72. }
  73. },
  74. setup(props, { emit }) {
  75. const router = useRouter();
  76. const showAll = ref(false);
  77. const displayedChapters = computed(() => {
  78. if (showAll.value) {
  79. return props.chapters;
  80. } else {
  81. return props.chapters.slice(0, props.maxDisplayChapters);
  82. }
  83. });
  84. const toggleShowAll = () => {
  85. showAll.value = !showAll.value;
  86. };
  87. const goToChapter = (chapterId) => {
  88. // 先触发事件给父组件
  89. emit('chapter-click', chapterId);
  90. // 如果没有父组件处理,则默认路由跳转
  91. if (props.bookId) {
  92. router.push(`/book/${props.bookId}/chapter/${chapterId}`);
  93. }
  94. };
  95. return {
  96. showAll,
  97. displayedChapters,
  98. toggleShowAll,
  99. goToChapter
  100. };
  101. }
  102. });
  103. </script>
  104. <style lang="scss" scoped>
  105. .book-catalog-container {
  106. background-color: #fff;
  107. border-radius: 6px;
  108. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
  109. padding: 15px;
  110. margin-bottom: 3px;
  111. .section-header {
  112. display: flex;
  113. justify-content: space-between;
  114. align-items: center;
  115. margin-bottom: 16px;
  116. padding-bottom: 12px;
  117. border-bottom: 1px solid #eee;
  118. .section-title {
  119. font-size: 18px;
  120. font-weight: bold;
  121. color: #333;
  122. margin: 0;
  123. position: relative;
  124. padding-left: 12px;
  125. &::before {
  126. content: '';
  127. position: absolute;
  128. left: 0;
  129. top: 4px;
  130. height: 18px;
  131. width: 4px;
  132. background-color: #0A2463;
  133. border-radius: 2px;
  134. }
  135. }
  136. }
  137. .empty-state {
  138. display: flex;
  139. flex-direction: column;
  140. align-items: center;
  141. justify-content: center;
  142. padding: 60px 20px;
  143. text-align: center;
  144. .empty-icon {
  145. margin-bottom: 16px;
  146. opacity: 0.6;
  147. }
  148. .empty-title {
  149. font-size: 16px;
  150. font-weight: 500;
  151. color: #666;
  152. margin: 0 0 8px 0;
  153. }
  154. .empty-desc {
  155. font-size: 14px;
  156. color: #999;
  157. margin: 0;
  158. line-height: 1.5;
  159. }
  160. }
  161. .chapter-grid {
  162. display: grid;
  163. grid-template-columns: repeat(4, 1fr);
  164. gap: 10px;
  165. margin-bottom: 20px;
  166. @media (max-width: 768px) {
  167. grid-template-columns: 2fr;
  168. }
  169. .chapter-item {
  170. padding: 8px 12px;
  171. border-radius: 4px;
  172. cursor: pointer;
  173. transition: all 0.2s;
  174. display: flex;
  175. align-items: center;
  176. justify-content: space-between;
  177. &:hover {
  178. background-color: #f8f9ff;
  179. }
  180. &.is-current {
  181. background-color: #e6f7ff;
  182. border: 1px solid #91d5ff;
  183. &:hover {
  184. background-color: #e6f7ff;
  185. }
  186. .chapter-title {
  187. color: #0A2463;
  188. font-weight: bold;
  189. }
  190. }
  191. .chapter-title {
  192. font-size: 14px;
  193. color: #333;
  194. white-space: nowrap;
  195. overflow: hidden;
  196. text-overflow: ellipsis;
  197. display: flex;
  198. align-items: center;
  199. flex: 1;
  200. .current-badge {
  201. background-color: #52c41a;
  202. color: #fff;
  203. font-size: 12px;
  204. padding: 1px 6px;
  205. border-radius: 4px;
  206. margin-left: 8px;
  207. flex-shrink: 0;
  208. }
  209. .new-badge {
  210. background-color: #ff5252;
  211. color: #fff;
  212. font-size: 12px;
  213. padding: 1px 6px;
  214. border-radius: 4px;
  215. margin-left: 8px;
  216. flex-shrink: 0;
  217. }
  218. }
  219. .chapter-pay {
  220. flex-shrink: 0;
  221. padding-left: 10px;
  222. .pay-badge {
  223. background-color: #FF9E2D;
  224. color: #fff;
  225. font-size: 12px;
  226. padding: 2px 10px;
  227. border-radius: 12px;
  228. }
  229. .paid-badge {
  230. background-color: #52c41a;
  231. color: #fff;
  232. font-size: 12px;
  233. padding: 2px 10px;
  234. border-radius: 12px;
  235. }
  236. }
  237. }
  238. }
  239. .catalog-footer {
  240. display: flex;
  241. justify-content: center;
  242. padding-top: 10px;
  243. border-top: 1px solid #f0f0f0;
  244. margin-top: 10px;
  245. .expand-btn, .collapse-btn {
  246. display: flex;
  247. align-items: center;
  248. justify-content: center;
  249. color: #0A2463;
  250. font-size: 14px;
  251. cursor: pointer;
  252. padding: 8px 0;
  253. transition: all 0.3s;
  254. &:hover {
  255. opacity: 0.8;
  256. }
  257. }
  258. .expand-btn {
  259. .expand-icon {
  260. display: inline-block;
  261. width: 18px;
  262. height: 18px;
  263. margin-left: 8px;
  264. background: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%230A2463"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>') no-repeat center;
  265. background-size: contain;
  266. transition: transform 0.3s;
  267. }
  268. }
  269. .collapse-btn {
  270. .collapse-icon {
  271. display: inline-block;
  272. width: 18px;
  273. height: 18px;
  274. margin-left: 8px;
  275. background: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%230A2463"><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/></svg>') no-repeat center;
  276. background-size: contain;
  277. transition: transform 0.3s;
  278. }
  279. }
  280. }
  281. }
  282. // 章节过渡动画
  283. .chapter-transition-enter-active,
  284. .chapter-transition-leave-active {
  285. transition: all 0.3s ease;
  286. }
  287. .chapter-transition-enter-from {
  288. opacity: 0;
  289. transform: translateY(20px);
  290. }
  291. .chapter-transition-leave-to {
  292. opacity: 0;
  293. transform: translateY(-20px);
  294. }
  295. .chapter-transition-move {
  296. transition: transform 0.3s ease;
  297. }
  298. </style>