├── php-api/ # 改造后的PHP接口层 ├── java-ad-service/ # 若依框架微服务(广告+VIP+分账) ├── uniapp-reader/ # UniApp前端项目 │ ├── pages/ # 各端页面 │ └──
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753
  1. <template>
  2. <view class="detail-container">
  3. <!-- 调试信息 -->
  4. <view v-if="debugInfo" class="debug-info">
  5. <text>调试信息: novelId={{novelId}}, loading={{loading}}, error={{error}}</text>
  6. </view>
  7. <!-- 加载状态 -->
  8. <view v-if="loading" class="loading-container">
  9. <uni-icons type="spinner-cycle" size="36" color="var(--primary-color)" class="loading-icon" />
  10. <text>加载中...</text>
  11. </view>
  12. <!-- 错误状态 -->
  13. <view v-else-if="error" class="error-container">
  14. <uni-icons type="info" size="48" color="var(--error-color)" />
  15. <text class="error-text">{{ error }}</text>
  16. <button class="btn-retry" @click="initDetail">重新加载</button>
  17. </view>
  18. <!-- 内容区域 -->
  19. <view v-else class="content-container">
  20. <!-- 小说封面和基本信息 -->
  21. <view class="novel-header">
  22. <!-- 修改封面图片绑定 -->
  23. <image
  24. :src="getSafeCoverUrl(novel.coverImg)"
  25. class="novel-cover"
  26. mode="aspectFill"
  27. @error="handleCoverError"
  28. />
  29. <view class="novel-info">
  30. <text class="update-time">
  31. 更新时间: {{ formatTime(novel.updateTime) }}
  32. </text>
  33. <text class="novel-title">{{ novel.title || '未知小说' }}</text>
  34. <text class="novel-author">{{ novel.author || '未知作者' }}</text>
  35. <text class="novel-meta">{{ novel.categoryName || '未知分类' }} · {{ novel.statusText || '连载中' }}</text>
  36. <text class="novel-meta">{{ (novel.wordCount || 0) }}字 · {{ (novel.readCount || 0) }}阅读</text>
  37. </view>
  38. </view>
  39. <!-- 小说描述 -->
  40. <view class="description-section">
  41. <text class="section-title">作品简介</text>
  42. <text class="novel-description">{{ novel.description || '暂无简介' }}</text>
  43. </view>
  44. <!-- 操作按钮 -->
  45. <view class="action-buttons">
  46. <button
  47. class="btn-add-bookshelf"
  48. :class="{ 'in-bookshelf': isInBookshelf }"
  49. @click="addToBookshelf"
  50. >
  51. {{ isInBookshelf ? '已在书架' : '加入书架' }}
  52. </button>
  53. <button class="btn-start-reading" @click="startReading">
  54. 开始阅读
  55. </button>
  56. </view>
  57. <!-- 章节列表 -->
  58. <view class="chapter-preview">
  59. <view class="section-header">
  60. <text class="section-title">章节列表</text>
  61. <text class="section-more" @click="showAllChapters = !showAllChapters">
  62. {{ showAllChapters ? '收起' : '展开' }}
  63. </text>
  64. </view>
  65. <view v-if="displayedChapters.length > 0" class="chapter-list">
  66. <view
  67. v-for="chapter in displayedChapters"
  68. :key="chapter.id"
  69. class="chapter-item"
  70. @click="readChapter(chapter)"
  71. >
  72. <text class="chapter-title">{{ chapter.title }}</text>
  73. <text class="chapter-time">
  74. {{ formatTime(chapter.updateTime, 'MM-DD HH:mm') }}
  75. </text>
  76. </view>
  77. </view>
  78. <view v-else class="empty-chapters">
  79. <text>暂无章节</text>
  80. </view>
  81. </view>
  82. </view>
  83. </view>
  84. </template>
  85. <script>
  86. // 导入 request 模块
  87. import request from '@/utils/request'
  88. import { getToken, setToken, removeToken } from '@/utils/auth'
  89. export default {
  90. data() {
  91. return {
  92. novelId: null,
  93. novel: {},
  94. chapters: [],
  95. loading: true,
  96. error: null,
  97. isInBookshelf: false,
  98. showAllChapters: false,
  99. debugInfo: true
  100. }
  101. },
  102. computed: {
  103. // 显示前5章或全部章节
  104. displayedChapters() {
  105. if (this.showAllChapters) {
  106. return this.chapters;
  107. }
  108. return this.chapters.slice(0, 5);
  109. }
  110. },
  111. onLoad(options) {
  112. console.log('🚨 DETAIL onLoad 开始执行');
  113. this.novelId = options.id || this.$route.params.id;
  114. console.log('🔍 最终 novelId:', this.novelId);
  115. if (!this.novelId) {
  116. this.error = '无法获取小说信息';
  117. this.loading = false;
  118. return;
  119. }
  120. this.initDetail();
  121. },
  122. mounted() {
  123. // 检查 uni 对象是否可用
  124. if (typeof uni === 'undefined') {
  125. console.warn('⚠️ uni 对象未定义,使用 Vue Router 进行导航');
  126. } else {
  127. console.log('✅ uni 对象可用:', typeof uni.navigateTo);
  128. }
  129. console.log('🚨 DETAIL mounted 执行');
  130. // 备选方案:如果onLoad没有获取到参数,在这里再次尝试
  131. if (!this.novelId) {
  132. console.log('🔄 mounted中重新获取参数');
  133. this.novelId = this.$route.params.id;
  134. if (this.novelId) {
  135. this.initDetail();
  136. }
  137. }
  138. },
  139. methods: {
  140. // 格式化时间
  141. formatTime(time, format = 'YYYY-MM-DD HH:mm') {
  142. if (!time) return ''
  143. const date = new Date(time)
  144. const year = date.getFullYear()
  145. const month = String(date.getMonth() + 1).padStart(2, '0')
  146. const day = String(date.getDate()).padStart(2, '0')
  147. const hour = String(date.getHours()).padStart(2, '0')
  148. const minute = String(date.getMinutes()).padStart(2, '0')
  149. const second = String(date.getSeconds()).padStart(2, '0')
  150. return format
  151. .replace('YYYY', year)
  152. .replace('MM', month)
  153. .replace('DD', day)
  154. .replace('HH', hour)
  155. .replace('mm', minute)
  156. .replace('ss', second)
  157. },
  158. // 或者使用更简单的时间格式化
  159. formatTimeSimple(time) {
  160. if (!time) return ''
  161. const date = new Date(time)
  162. return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
  163. },
  164. async initDetail() {
  165. try {
  166. console.log('🎬 initDetail 开始执行,novelId:', this.novelId)
  167. this.loading = true
  168. this.error = null
  169. // 并行请求小说详情和章节列表
  170. const [detailRes, chaptersRes] = await Promise.allSettled([
  171. this.loadNovelDetail(),
  172. this.loadChapters()
  173. ])
  174. // 处理结果
  175. if (detailRes.status === 'fulfilled') {
  176. console.log('✅ 小说详情加载完成')
  177. } else {
  178. console.error('❌ 小说详情加载失败:', detailRes.reason)
  179. }
  180. if (chaptersRes.status === 'fulfilled') {
  181. console.log('✅ 章节列表加载完成')
  182. } else {
  183. console.error('❌ 章节列表加载失败:', chaptersRes.reason)
  184. }
  185. // 检查书架状态
  186. this.checkBookshelfStatus()
  187. } catch (error) {
  188. console.error('🎬 initDetail 执行失败:', error)
  189. this.error = '加载失败,请重试'
  190. } finally {
  191. this.loading = false
  192. }
  193. },
  194. async loadNovelDetail() {
  195. try {
  196. console.log('📚 开始加载小说详情...')
  197. // 明确设置不携带token
  198. const res = await request.get(`/novel/detail/${this.novelId}`, {}, {
  199. header: {
  200. isToken: false
  201. }
  202. })
  203. console.log('✅ 小说详情API响应:', res)
  204. if (res && res.code === 200 && res.data) {
  205. this.novel = res.data
  206. console.log('📖 小说详情数据:', this.novel)
  207. } else {
  208. console.warn('⚠️ 小说详情接口返回数据异常,使用模拟数据')
  209. // 使用模拟数据的逻辑
  210. this.useMockData()
  211. }
  212. } catch (error) {
  213. console.error('❌ 加载小说详情失败:', error)
  214. console.warn('⚠️ 网络请求失败,使用模拟数据')
  215. this.useMockData()
  216. }
  217. },
  218. // detail.vue 中修改 loadChapters 方法
  219. async loadChapters() {
  220. try {
  221. console.log('📑 开始加载章节列表...');
  222. // 尝试获取token
  223. const token = uni.getStorageSync('token');
  224. console.log('🔑 当前token状态:', token ? '已存在' : '不存在');
  225. let requestConfig = {
  226. header: {
  227. isToken: false
  228. }
  229. };
  230. // 如果有token,使用token请求
  231. if (token) {
  232. requestConfig = {
  233. header: {
  234. 'Authorization': 'Bearer ' + token,
  235. isToken: true
  236. }
  237. };
  238. console.log('🔐 使用token请求章节列表');
  239. } else {
  240. console.log('👤 使用公开API请求章节列表');
  241. }
  242. // 尝试请求真实数据
  243. const res = await request.get(`/chapter/list/${this.novelId}`, {}, requestConfig);
  244. console.log('✅ 章节列表API响应:', res);
  245. if (res && res.code === 200 && res.rows) {
  246. // 注意:后端返回的是 res.rows,不是 res.data
  247. this.chapters = res.rows;
  248. console.log('📑 章节列表数据:', this.chapters);
  249. } else if (res && res.rows) {
  250. // 如果直接返回 rows 数组
  251. this.chapters = res.rows;
  252. console.log('📑 章节列表数据(rows):', this.chapters);
  253. } else {
  254. console.warn('⚠️ 章节列表接口返回异常,使用模拟数据');
  255. this.useMockChapters();
  256. }
  257. } catch (error) {
  258. console.error('❌ 加载章节列表失败:', error);
  259. // 如果是401错误,提示用户登录
  260. if (error.message && error.message.includes('认证失败')) {
  261. uni.showToast({
  262. title: '请登录后查看章节',
  263. icon: 'none'
  264. });
  265. }
  266. console.warn('⚠️ 网络请求失败,使用模拟章节数据');
  267. this.useMockChapters();
  268. }
  269. },
  270. // 改进模拟数据生成
  271. useMockChapters() {
  272. console.log('🎭 生成模拟章节数据');
  273. const mockChapters = [];
  274. const chapterTitles = [
  275. '初遇异能者', '白冰的身世', '小队集结', '首次任务', '危机四伏',
  276. '突破极限', '新的伙伴', '黑暗组织', '真相大白', '最终决战'
  277. ];
  278. // 生成更真实的模拟数据
  279. for (let i = 0; i < 15; i++) {
  280. const chapterNum = i + 1;
  281. const titleIndex = i % chapterTitles.length;
  282. mockChapters.push({
  283. id: chapterNum,
  284. title: `第${chapterNum}章 ${chapterTitles[titleIndex]}`,
  285. updateTime: new Date(Date.now() - (15 - i) * 24 * 60 * 60 * 1000),
  286. content: `这是第${chapterNum}章的内容...`,
  287. wordCount: Math.floor(Math.random() * 3000) + 1500
  288. });
  289. }
  290. this.chapters = mockChapters;
  291. console.log('📚 模拟章节数据生成完成:', this.chapters.length + '章');
  292. },
  293. useMockChapters() {
  294. // 你的模拟章节数据逻辑
  295. this.chapters = [
  296. { id: 1, title: '第一章 初遇', updateTime: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000) },
  297. { id: 2, title: '第二章 相识', updateTime: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000) },
  298. { id: 3, title: '第三章 冒险开始', updateTime: new Date() }
  299. ]
  300. },
  301. // 其他方法保持不变...
  302. getSafeCoverUrl(coverImg) {
  303. if (!coverImg) return '/static/default-cover.jpg';
  304. if (coverImg && !coverImg.includes('ishuquge.org') && !coverImg.includes('xshuquge.net')) {
  305. return coverImg;
  306. }
  307. return '/static/default-cover.jpg';
  308. },
  309. handleCoverError(event) {
  310. console.log('封面图片加载失败,使用默认图片');
  311. // 注意:在小程序中,可能需要使用不同的方式处理图片错误
  312. this.novel.coverImg = '/static/default-cover.jpg';
  313. },
  314. // 检查书架状态
  315. checkBookshelfStatus() {
  316. try {
  317. const bookshelf = uni.getStorageSync('user_bookshelf') || [];
  318. this.isInBookshelf = bookshelf.some(item => item.novelId == this.novelId);
  319. console.log('📚 书架状态:', this.isInBookshelf ? '已在书架' : '未在书架');
  320. } catch (error) {
  321. console.error('检查书架状态失败:', error);
  322. }
  323. },
  324. // 加入书架
  325. addToBookshelf() {
  326. console.log('📚 加入书架 clicked');
  327. // 立即更新状态
  328. this.isInBookshelf = true;
  329. // 保存到本地存储
  330. try {
  331. const bookshelf = uni.getStorageSync('user_bookshelf') || [];
  332. const existingIndex = bookshelf.findIndex(item => item.novelId == this.novelId);
  333. if (existingIndex === -1) {
  334. bookshelf.push({
  335. novelId: this.novelId,
  336. novel: this.novel,
  337. addTime: new Date().getTime(),
  338. lastReadChapter: 0
  339. });
  340. uni.setStorageSync('user_bookshelf', bookshelf);
  341. }
  342. uni.showToast({
  343. title: '加入书架成功',
  344. icon: 'success',
  345. duration: 2000
  346. });
  347. console.log('✅ 加入书架成功');
  348. } catch (error) {
  349. console.error('保存到书架失败:', error);
  350. uni.showToast({
  351. title: '加入书架失败',
  352. icon: 'none'
  353. });
  354. }
  355. },
  356. // 开始阅读
  357. startReading() {
  358. console.log('📖 开始阅读 clicked, novelId:', this.novelId);
  359. if (this.chapters.length === 0) {
  360. uni.showToast({
  361. title: '暂无章节',
  362. icon: 'none'
  363. });
  364. return;
  365. }
  366. const firstChapterId = this.chapters[0].id || 1;
  367. console.log('🎯 跳转到阅读器,参数:', {
  368. novelId: this.novelId,
  369. chapterId: firstChapterId
  370. });
  371. // 安全跳转方法
  372. this.safeNavigateToReader(this.novelId, firstChapterId);
  373. },
  374. // 阅读指定章节
  375. readChapter(chapter) {
  376. console.log('阅读章节:', chapter);
  377. const chapterId = chapter.id;
  378. this.safeNavigateToReader(this.novelId, chapterId);
  379. },
  380. // 安全跳转到阅读器
  381. // 安全跳转到阅读器
  382. safeNavigateToReader(novelId, chapterId) {
  383. console.log('🔒 安全跳转: novelId=' + novelId + ', chapterId=' + chapterId);
  384. // 方法1: 使用 uni.navigateTo (小程序环境)
  385. try {
  386. console.log('🔄 尝试使用 uni.navigateTo 跳转...');
  387. // 检查目标页面是否存在
  388. const pages = getCurrentPages();
  389. console.log('📄 当前页面栈:', pages.length);
  390. // 构建跳转URL - 确保路径正确
  391. const targetUrl = `/pages/novel/reader?novelId=${novelId}&chapterId=${chapterId}`;
  392. console.log('🎯 跳转目标URL:', targetUrl);
  393. uni.navigateTo({
  394. url: targetUrl,
  395. success: (res) => {
  396. console.log('✅ uni.navigateTo 跳转成功', res);
  397. },
  398. fail: (err) => {
  399. console.error('❌ uni.navigateTo 跳转失败:', err);
  400. this.fallbackNavigate(novelId, chapterId);
  401. }
  402. });
  403. } catch (uniError) {
  404. console.error('❌ uni.navigateTo 异常:', uniError);
  405. this.fallbackNavigate(novelId, chapterId);
  406. }
  407. },
  408. // 备选跳转方案
  409. fallbackNavigate(novelId, chapterId) {
  410. console.log('🔄 启动备选跳转方案');
  411. // 方法2: 直接使用 Vue Router (H5环境)
  412. try {
  413. console.log('🔄 尝试使用 Vue Router 跳转...');
  414. if (this.$router) {
  415. this.$router.push({
  416. path: '/pages/novel/reader',
  417. query: {
  418. novelId: novelId,
  419. chapterId: chapterId
  420. }
  421. });
  422. console.log('✅ Vue Router 跳转成功');
  423. return;
  424. }
  425. } catch (routerError) {
  426. console.error('❌ Vue Router 跳转失败:', routerError);
  427. }
  428. // 方法3: 使用相对路径
  429. try {
  430. console.log('🔄 尝试使用相对路径跳转...');
  431. const currentPath = window.location.pathname;
  432. const basePath = currentPath.substring(0, currentPath.lastIndexOf('/'));
  433. const targetUrl = `${basePath}/reader?novelId=${novelId}&chapterId=${chapterId}`;
  434. window.location.href = targetUrl;
  435. console.log('✅ 相对路径跳转成功');
  436. return;
  437. } catch (locationError) {
  438. console.error('❌ 相对路径跳转失败:', locationError);
  439. }
  440. // 所有方法都失败
  441. console.error('💥 所有跳转方法都失败了');
  442. uni.showToast({
  443. title: '跳转失败,请检查阅读器页面是否存在',
  444. icon: 'none',
  445. duration: 3000
  446. });
  447. },
  448. // 生成模拟小说数据
  449. useMockNovelData() {
  450. this.novel = {
  451. title: '曦与她的冰姐姐',
  452. author: '旺旺碎冰冰big',
  453. categoryName: '都市言情',
  454. statusText: '已完结',
  455. wordCount: 118383,
  456. readCount: 9972,
  457. description: '这是一个异能者的世界。孤曦和白冰都可谓同年龄天才。十岁的相遇。十五岁同队。这是一个小队成长的故事。白冰又有什么样的身世呢。变异生物和人类最终的大结局会是怎样?内多人物。'
  458. };
  459. },
  460. // 生成模拟章节数据
  461. generateMockChapters() {
  462. const chapters = [];
  463. const chapterTitles = [
  464. '初遇', '相识', '冒险开始', '新的伙伴', '危机降临',
  465. '突破', '成长', '离别', '重逢', '最终决战'
  466. ];
  467. for (let i = 0; i < 10; i++) {
  468. chapters.push({
  469. id: i + 1,
  470. title: `第${i + 1}章 ${chapterTitles[i] || '新的篇章'}`,
  471. updateTime: new Date(Date.now() - (10 - i) * 24 * 60 * 60 * 1000)
  472. });
  473. }
  474. return chapters;
  475. }
  476. }
  477. }
  478. </script>
  479. <style scoped>
  480. .detail-container {
  481. padding: 20rpx;
  482. min-height: 100vh;
  483. background-color: #f5f5f5;
  484. }
  485. /* 调试信息样式 */
  486. .debug-info {
  487. background: #ffeb3b;
  488. color: #333;
  489. padding: 10rpx;
  490. font-size: 24rpx;
  491. text-align: center;
  492. border-bottom: 1rpx solid #ffc107;
  493. }
  494. .loading-container, .error-container {
  495. display: flex;
  496. flex-direction: column;
  497. align-items: center;
  498. justify-content: center;
  499. padding: 200rpx 0;
  500. text-align: center;
  501. }
  502. .loading-icon {
  503. animation: rotate 1s linear infinite;
  504. margin-bottom: 20rpx;
  505. }
  506. @keyframes rotate {
  507. from { transform: rotate(0deg); }
  508. to { transform: rotate(360deg); }
  509. }
  510. .error-text {
  511. font-size: 32rpx;
  512. color: var(--error-color);
  513. margin: 20rpx 0;
  514. }
  515. .btn-retry {
  516. background-color: var(--primary-color);
  517. color: white;
  518. padding: 20rpx 40rpx;
  519. border-radius: 10rpx;
  520. margin-top: 20rpx;
  521. }
  522. .novel-header {
  523. display: flex;
  524. background: white;
  525. padding: 30rpx;
  526. border-radius: 16rpx;
  527. margin-bottom: 30rpx;
  528. }
  529. .novel-cover {
  530. width: 200rpx;
  531. height: 280rpx;
  532. border-radius: 12rpx;
  533. margin-right: 30rpx;
  534. background-color: #f0f0f0;
  535. }
  536. .novel-info {
  537. flex: 1;
  538. display: flex;
  539. flex-direction: column;
  540. }
  541. .novel-title {
  542. font-size: 36rpx;
  543. font-weight: bold;
  544. margin-bottom: 15rpx;
  545. line-height: 1.4;
  546. }
  547. .novel-author {
  548. font-size: 30rpx;
  549. color: #666;
  550. margin-bottom: 10rpx;
  551. }
  552. .novel-meta {
  553. font-size: 26rpx;
  554. color: #888;
  555. margin-bottom: 8rpx;
  556. }
  557. .description-section {
  558. background: white;
  559. padding: 30rpx;
  560. border-radius: 16rpx;
  561. margin-bottom: 30rpx;
  562. }
  563. .section-title {
  564. font-size: 32rpx;
  565. font-weight: bold;
  566. margin-bottom: 20rpx;
  567. display: block;
  568. }
  569. .novel-description {
  570. font-size: 28rpx;
  571. line-height: 1.6;
  572. color: #666;
  573. }
  574. .action-buttons {
  575. display: flex;
  576. gap: 20rpx;
  577. margin-bottom: 30rpx;
  578. }
  579. .btn-add-bookshelf, .btn-start-reading {
  580. flex: 1;
  581. padding: 25rpx;
  582. border-radius: 12rpx;
  583. font-size: 30rpx;
  584. text-align: center;
  585. transition: all 0.3s;
  586. }
  587. .btn-add-bookshelf {
  588. background-color: #f8f9fa;
  589. color: var(--primary-color);
  590. border: 2rpx solid var(--primary-color);
  591. }
  592. .btn-add-bookshelf.in-bookshelf {
  593. background-color: #e8f5e8;
  594. color: #67c23a;
  595. border-color: #67c23a;
  596. }
  597. .btn-start-reading {
  598. background-color: var(--primary-color);
  599. color: white;
  600. }
  601. .btn-start-reading:active {
  602. opacity: 0.8;
  603. }
  604. .chapter-preview {
  605. background: white;
  606. padding: 30rpx;
  607. border-radius: 16rpx;
  608. }
  609. .section-header {
  610. display: flex;
  611. justify-content: space-between;
  612. align-items: center;
  613. margin-bottom: 20rpx;
  614. }
  615. .section-more {
  616. font-size: 26rpx;
  617. color: var(--primary-color);
  618. }
  619. .chapter-list {
  620. margin-bottom: 20rpx;
  621. }
  622. .chapter-item {
  623. display: flex;
  624. justify-content: space-between;
  625. align-items: center;
  626. padding: 20rpx 0;
  627. border-bottom: 1rpx solid #f0f0f0;
  628. cursor: pointer;
  629. }
  630. .chapter-item:last-child {
  631. border-bottom: none;
  632. }
  633. .chapter-title {
  634. font-size: 28rpx;
  635. color: #333;
  636. flex: 1;
  637. }
  638. .chapter-time {
  639. font-size: 24rpx;
  640. color: #999;
  641. }
  642. .empty-chapters {
  643. text-align: center;
  644. padding: 40rpx;
  645. color: #999;
  646. }
  647. </style>