|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+<template>
|
|
|
2
|
+ <scroll-view scroll-y class="detail-page">
|
|
|
3
|
+ <!-- 封面区域 -->
|
|
|
4
|
+ <view class="cover-section">
|
|
|
5
|
+ <image :src="novel.cover" class="cover-image" />
|
|
|
6
|
+ <view class="cover-overlay">
|
|
|
7
|
+ <text class="title">{{ novel.title }}</text>
|
|
|
8
|
+ <text class="author">{{ novel.author }}</text>
|
|
|
9
|
+
|
|
|
10
|
+ <!-- VIP专享标识 -->
|
|
|
11
|
+ <view v-if="isVIPOnly" class="vip-tag">
|
|
|
12
|
+ <uni-icons type="crown-filled" color="#ffc53d" size="16"></uni-icons>
|
|
|
13
|
+ <text>VIP专享</text>
|
|
|
14
|
+ </view>
|
|
|
15
|
+ </view>
|
|
|
16
|
+ </view>
|
|
|
17
|
+
|
|
|
18
|
+ <!-- 基本信息 -->
|
|
|
19
|
+ <view class="info-card">
|
|
|
20
|
+ <view class="info-item">
|
|
|
21
|
+ <uni-icons type="flag" size="18" color="#666"></uni-icons>
|
|
|
22
|
+ <text>状态:{{ novel.status }}</text>
|
|
|
23
|
+ </view>
|
|
|
24
|
+ <view class="info-item">
|
|
|
25
|
+ <uni-icons type="list" size="18" color="#666"></uni-icons>
|
|
|
26
|
+ <text>分类:{{ novel.category }}</text>
|
|
|
27
|
+ </view>
|
|
|
28
|
+ <view class="info-item">
|
|
|
29
|
+ <uni-icons type="eye" size="18" color="#666"></uni-icons>
|
|
|
30
|
+ <text>人气:{{ formatCount(novel.views) }}</text>
|
|
|
31
|
+ </view>
|
|
|
32
|
+ <view class="info-item">
|
|
|
33
|
+ <uni-icons type="calendar" size="18" color="#666"></uni-icons>
|
|
|
34
|
+ <text>更新:{{ novel.updateTime }}</text>
|
|
|
35
|
+ </view>
|
|
|
36
|
+ </view>
|
|
|
37
|
+
|
|
|
38
|
+ <!-- 操作按钮 -->
|
|
|
39
|
+ <view class="action-buttons">
|
|
|
40
|
+ <button class="read-btn" @click="startReading">
|
|
|
41
|
+ {{ lastChapter ? '继续阅读' : '开始阅读' }}
|
|
|
42
|
+ </button>
|
|
|
43
|
+ <button class="add-btn" @click="addToBookshelf">
|
|
|
44
|
+ <uni-icons :type="inBookshelf ? 'star-filled' : 'star'" color="#ffc53d" size="18"></uni-icons>
|
|
|
45
|
+ {{ inBookshelf ? '已在书架' : '加入书架' }}
|
|
|
46
|
+ </button>
|
|
|
47
|
+ </view>
|
|
|
48
|
+
|
|
|
49
|
+ <!-- 简介 -->
|
|
|
50
|
+ <view class="description">
|
|
|
51
|
+ <text class="section-title">内容简介</text>
|
|
|
52
|
+ <text class="content">{{ novel.description }}</text>
|
|
|
53
|
+ </view>
|
|
|
54
|
+
|
|
|
55
|
+ <!-- 章节列表 -->
|
|
|
56
|
+ <view class="chapter-list">
|
|
|
57
|
+ <text class="section-title">目录</text>
|
|
|
58
|
+ <view class="chapter-header">
|
|
|
59
|
+ <text>共{{ novel.chapterCount }}章</text>
|
|
|
60
|
+ <text @click="reverseOrder">{{ sortDesc ? '正序' : '倒序' }}</text>
|
|
|
61
|
+ </view>
|
|
|
62
|
+
|
|
|
63
|
+ <view class="chapter-item"
|
|
|
64
|
+ v-for="chapter in sortedChapters"
|
|
|
65
|
+ :key="chapter.id"
|
|
|
66
|
+ @click="readChapter(chapter)"
|
|
|
67
|
+ >
|
|
|
68
|
+ <text class="chapter-title">{{ chapter.title }}</text>
|
|
|
69
|
+ <view class="chapter-meta">
|
|
|
70
|
+ <text>{{ formatDate(chapter.updateTime) }}</text>
|
|
|
71
|
+ <!-- VIP章节标识 -->
|
|
|
72
|
+ <view v-if="chapter.vip" class="chapter-vip">
|
|
|
73
|
+ <uni-icons type="crown" size="14" color="#ffc53d"></uni-icons>
|
|
|
74
|
+ <text>VIP</text>
|
|
|
75
|
+ </view>
|
|
|
76
|
+ </view>
|
|
|
77
|
+ </view>
|
|
|
78
|
+ </view>
|
|
|
79
|
+ </scroll-view>
|
|
|
80
|
+</template>
|
|
|
81
|
+
|
|
|
82
|
+<script setup>
|
|
|
83
|
+import { ref, computed, onMounted } from 'vue'
|
|
|
84
|
+import { useUserStore } from '@/stores/user'
|
|
|
85
|
+import { useVipStore } from '@/stores/vip'
|
|
|
86
|
+import { request } from '@/utils/request'
|
|
|
87
|
+
|
|
|
88
|
+const props = defineProps({
|
|
|
89
|
+ novelId: {
|
|
|
90
|
+ type: Number,
|
|
|
91
|
+ required: true
|
|
|
92
|
+ }
|
|
|
93
|
+})
|
|
|
94
|
+
|
|
|
95
|
+const novel = ref({
|
|
|
96
|
+ id: 1,
|
|
|
97
|
+ title: '哎呀电子科技传奇',
|
|
|
98
|
+ author: '哎呀作者',
|
|
|
99
|
+ cover: '/static/covers/novel1.jpg',
|
|
|
100
|
+ status: '连载中',
|
|
|
101
|
+ category: '都市异能',
|
|
|
102
|
+ views: 12500,
|
|
|
103
|
+ updateTime: '2025-06-10 12:30',
|
|
|
104
|
+ description: '这是一个关于哎呀电子科技崛起的故事,讲述了一群程序员如何通过技术创新改变世界...',
|
|
|
105
|
+ chapterCount: 120,
|
|
|
106
|
+ vipOnly: true // 整本VIP专享
|
|
|
107
|
+})
|
|
|
108
|
+
|
|
|
109
|
+const chapters = ref([
|
|
|
110
|
+ { id: 1, title: '第1章 命运的转折', updateTime: '2025-05-01', vip: false },
|
|
|
111
|
+ { id: 2, title: '第2章 初遇哎呀科技', updateTime: '2025-05-03', vip: false },
|
|
|
112
|
+ // ...中间章节
|
|
|
113
|
+ { id: 100, title: '第100章 突破性进展', updateTime: '2025-06-05', vip: true },
|
|
|
114
|
+ { id: 101, title: '第101章 VIP专享章节', updateTime: '2025-06-10', vip: true }
|
|
|
115
|
+])
|
|
|
116
|
+
|
|
|
117
|
+const sortDesc = ref(false)
|
|
|
118
|
+const lastChapter = ref(null)
|
|
|
119
|
+const inBookshelf = ref(false)
|
|
|
120
|
+
|
|
|
121
|
+const userStore = useUserStore()
|
|
|
122
|
+const vipStore = useVipStore()
|
|
|
123
|
+
|
|
|
124
|
+// 是否是VIP专享书籍
|
|
|
125
|
+const isVIPOnly = computed(() => {
|
|
|
126
|
+ return novel.value.vipOnly
|
|
|
127
|
+})
|
|
|
128
|
+
|
|
|
129
|
+// 排序后的章节列表
|
|
|
130
|
+const sortedChapters = computed(() => {
|
|
|
131
|
+ return sortDesc.value
|
|
|
132
|
+ ? [...chapters.value].reverse()
|
|
|
133
|
+ : chapters.value
|
|
|
134
|
+})
|
|
|
135
|
+
|
|
|
136
|
+// 格式化数字
|
|
|
137
|
+const formatCount = (num) => {
|
|
|
138
|
+ if (num > 10000) return (num / 10000).toFixed(1) + '万'
|
|
|
139
|
+ return num
|
|
|
140
|
+}
|
|
|
141
|
+
|
|
|
142
|
+// 格式化日期
|
|
|
143
|
+const formatDate = (dateStr) => {
|
|
|
144
|
+ return dateStr.slice(5) // 显示月-日
|
|
|
145
|
+}
|
|
|
146
|
+
|
|
|
147
|
+// 开始阅读
|
|
|
148
|
+const startReading = () => {
|
|
|
149
|
+ if (isVIPOnly.value && !vipStore.isVIP) {
|
|
|
150
|
+ uni.showModal({
|
|
|
151
|
+ title: 'VIP专享内容',
|
|
|
152
|
+ content: '此书籍为VIP专享,开通会员即可无限制阅读',
|
|
|
153
|
+ confirmText: '开通会员',
|
|
|
154
|
+ success: (res) => {
|
|
|
155
|
+ if (res.confirm) {
|
|
|
156
|
+ uni.navigateTo({ url: '/pages/vip/index' })
|
|
|
157
|
+ }
|
|
|
158
|
+ }
|
|
|
159
|
+ })
|
|
|
160
|
+ return
|
|
|
161
|
+ }
|
|
|
162
|
+
|
|
|
163
|
+ const chapterId = lastChapter.value?.id || chapters.value[0].id
|
|
|
164
|
+ readChapter({ id: chapterId })
|
|
|
165
|
+}
|
|
|
166
|
+
|
|
|
167
|
+// 阅读章节
|
|
|
168
|
+const readChapter = (chapter) => {
|
|
|
169
|
+ if (chapter.vip && !vipStore.isVIP) {
|
|
|
170
|
+ uni.showModal({
|
|
|
171
|
+ title: 'VIP章节',
|
|
|
172
|
+ content: '此章节为VIP专享,开通会员即可阅读',
|
|
|
173
|
+ confirmText: '开通会员',
|
|
|
174
|
+ success: (res) => {
|
|
|
175
|
+ if (res.confirm) {
|
|
|
176
|
+ uni.navigateTo({ url: '/pages/vip/index' })
|
|
|
177
|
+ }
|
|
|
178
|
+ }
|
|
|
179
|
+ })
|
|
|
180
|
+ return
|
|
|
181
|
+ }
|
|
|
182
|
+
|
|
|
183
|
+ uni.navigateTo({
|
|
|
184
|
+ url: `/pages/reader/index?novelId=${novel.value.id}&chapterId=${chapter.id}`
|
|
|
185
|
+ })
|
|
|
186
|
+}
|
|
|
187
|
+
|
|
|
188
|
+// 加入书架
|
|
|
189
|
+const addToBookshelf = () => {
|
|
|
190
|
+ inBookshelf.value = !inBookshelf.value
|
|
|
191
|
+ uni.showToast({
|
|
|
192
|
+ title: inBookshelf.value ? '已加入书架' : '已移除书架',
|
|
|
193
|
+ icon: 'none'
|
|
|
194
|
+ })
|
|
|
195
|
+}
|
|
|
196
|
+
|
|
|
197
|
+// 切换排序
|
|
|
198
|
+const reverseOrder = () => {
|
|
|
199
|
+ sortDesc.value = !sortDesc.value
|
|
|
200
|
+}
|
|
|
201
|
+
|
|
|
202
|
+// 加载数据
|
|
|
203
|
+onMounted(async () => {
|
|
|
204
|
+ // 获取小说详情
|
|
|
205
|
+ const res = await request({
|
|
|
206
|
+ url: `/novel/${props.novelId}`,
|
|
|
207
|
+ method: 'GET'
|
|
|
208
|
+ })
|
|
|
209
|
+ novel.value = res.data.novel
|
|
|
210
|
+ chapters.value = res.data.chapters
|
|
|
211
|
+
|
|
|
212
|
+ // 获取最后阅读章节
|
|
|
213
|
+ if (userStore.userId) {
|
|
|
214
|
+ const progressRes = await request({
|
|
|
215
|
+ url: `/reading/progress?novelId=${props.novelId}`,
|
|
|
216
|
+ method: 'GET',
|
|
|
217
|
+ headers: {
|
|
|
218
|
+ 'Authorization': `Bearer ${uni.getStorageSync('token')}`
|
|
|
219
|
+ }
|
|
|
220
|
+ })
|
|
|
221
|
+ lastChapter.value = progressRes.data.lastChapter
|
|
|
222
|
+ }
|
|
|
223
|
+})
|
|
|
224
|
+</script>
|
|
|
225
|
+
|
|
|
226
|
+<style scoped>
|
|
|
227
|
+.detail-page {
|
|
|
228
|
+ height: 100vh;
|
|
|
229
|
+ background-color: var(--bg-color);
|
|
|
230
|
+ padding-bottom: 50px;
|
|
|
231
|
+}
|
|
|
232
|
+
|
|
|
233
|
+.cover-section {
|
|
|
234
|
+ position: relative;
|
|
|
235
|
+ height: 300px;
|
|
|
236
|
+}
|
|
|
237
|
+
|
|
|
238
|
+.cover-image {
|
|
|
239
|
+ width: 100%;
|
|
|
240
|
+ height: 100%;
|
|
|
241
|
+ object-fit: cover;
|
|
|
242
|
+}
|
|
|
243
|
+
|
|
|
244
|
+.cover-overlay {
|
|
|
245
|
+ position: absolute;
|
|
|
246
|
+ bottom: 0;
|
|
|
247
|
+ left: 0;
|
|
|
248
|
+ right: 0;
|
|
|
249
|
+ background: linear-gradient(transparent, rgba(0,0,0,0.7));
|
|
|
250
|
+ padding: 20px;
|
|
|
251
|
+ color: white;
|
|
|
252
|
+}
|
|
|
253
|
+
|
|
|
254
|
+.title {
|
|
|
255
|
+ font-size: 24px;
|
|
|
256
|
+ font-weight: bold;
|
|
|
257
|
+ display: block;
|
|
|
258
|
+ margin-bottom: 5px;
|
|
|
259
|
+}
|
|
|
260
|
+
|
|
|
261
|
+.author {
|
|
|
262
|
+ font-size: 16px;
|
|
|
263
|
+ opacity: 0.8;
|
|
|
264
|
+ display: block;
|
|
|
265
|
+}
|
|
|
266
|
+
|
|
|
267
|
+.vip-tag {
|
|
|
268
|
+ position: absolute;
|
|
|
269
|
+ top: 20px;
|
|
|
270
|
+ right: 20px;
|
|
|
271
|
+ background: rgba(0,0,0,0.6);
|
|
|
272
|
+ border: 1px solid #ffc53d;
|
|
|
273
|
+ border-radius: 15px;
|
|
|
274
|
+ padding: 5px 10px;
|
|
|
275
|
+ display: flex;
|
|
|
276
|
+ align-items: center;
|
|
|
277
|
+ font-size: 14px;
|
|
|
278
|
+}
|
|
|
279
|
+
|
|
|
280
|
+.vip-tag text {
|
|
|
281
|
+ margin-left: 5px;
|
|
|
282
|
+}
|
|
|
283
|
+
|
|
|
284
|
+.info-card {
|
|
|
285
|
+ background-color: var(--card-bg);
|
|
|
286
|
+ border-radius: 12px;
|
|
|
287
|
+ padding: 15px;
|
|
|
288
|
+ margin: 15px;
|
|
|
289
|
+ box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
|
|
290
|
+}
|
|
|
291
|
+
|
|
|
292
|
+.info-item {
|
|
|
293
|
+ display: flex;
|
|
|
294
|
+ align-items: center;
|
|
|
295
|
+ margin-bottom: 10px;
|
|
|
296
|
+ font-size: 14px;
|
|
|
297
|
+}
|
|
|
298
|
+
|
|
|
299
|
+.info-item text {
|
|
|
300
|
+ margin-left: 8px;
|
|
|
301
|
+}
|
|
|
302
|
+
|
|
|
303
|
+.action-buttons {
|
|
|
304
|
+ display: flex;
|
|
|
305
|
+ padding: 0 15px;
|
|
|
306
|
+ margin: 20px 0;
|
|
|
307
|
+}
|
|
|
308
|
+
|
|
|
309
|
+.read-btn {
|
|
|
310
|
+ flex: 2;
|
|
|
311
|
+ background: linear-gradient(to right, #2a5caa, #1a3353);
|
|
|
312
|
+ color: white;
|
|
|
313
|
+ border: none;
|
|
|
314
|
+ border-radius: 24px;
|
|
|
315
|
+ height: 44px;
|
|
|
316
|
+ line-height: 44px;
|
|
|
317
|
+ font-weight: bold;
|
|
|
318
|
+ margin-right: 10px;
|
|
|
319
|
+}
|
|
|
320
|
+
|
|
|
321
|
+.add-btn {
|
|
|
322
|
+ flex: 1;
|
|
|
323
|
+ background-color: var(--card-bg);
|
|
|
324
|
+ border: 1px solid var(--primary-color);
|
|
|
325
|
+ color: var(--primary-color);
|
|
|
326
|
+ border-radius: 24px;
|
|
|
327
|
+ height: 44px;
|
|
|
328
|
+ line-height: 44px;
|
|
|
329
|
+ display: flex;
|
|
|
330
|
+ justify-content: center;
|
|
|
331
|
+ align-items: center;
|
|
|
332
|
+}
|
|
|
333
|
+
|
|
|
334
|
+.description {
|
|
|
335
|
+ background-color: var(--card-bg);
|
|
|
336
|
+ border-radius: 12px;
|
|
|
337
|
+ padding: 15px;
|
|
|
338
|
+ margin: 15px;
|
|
|
339
|
+}
|
|
|
340
|
+
|
|
|
341
|
+.section-title {
|
|
|
342
|
+ font-size: 18px;
|
|
|
343
|
+ font-weight: bold;
|
|
|
344
|
+ display: block;
|
|
|
345
|
+ margin-bottom: 10px;
|
|
|
346
|
+}
|
|
|
347
|
+
|
|
|
348
|
+.content {
|
|
|
349
|
+ font-size: 14px;
|
|
|
350
|
+ line-height: 1.8;
|
|
|
351
|
+ color: var(--text-color);
|
|
|
352
|
+}
|
|
|
353
|
+
|
|
|
354
|
+.chapter-list {
|
|
|
355
|
+ background-color: var(--card-bg);
|
|
|
356
|
+ border-radius: 12px;
|
|
|
357
|
+ padding: 15px;
|
|
|
358
|
+ margin: 15px;
|
|
|
359
|
+}
|
|
|
360
|
+
|
|
|
361
|
+.chapter-header {
|
|
|
362
|
+ display: flex;
|
|
|
363
|
+ justify-content: space-between;
|
|
|
364
|
+ font-size: 14px;
|
|
|
365
|
+ color: #666;
|
|
|
366
|
+ margin-bottom: 10px;
|
|
|
367
|
+ padding-bottom: 10px;
|
|
|
368
|
+ border-bottom: 1px solid #eee;
|
|
|
369
|
+}
|
|
|
370
|
+
|
|
|
371
|
+.chapter-item {
|
|
|
372
|
+ padding: 12px 0;
|
|
|
373
|
+ border-bottom: 1px solid #f5f5f5;
|
|
|
374
|
+}
|
|
|
375
|
+
|
|
|
376
|
+.chapter-title {
|
|
|
377
|
+ font-size: 16px;
|
|
|
378
|
+ display: block;
|
|
|
379
|
+ margin-bottom: 5px;
|
|
|
380
|
+}
|
|
|
381
|
+
|
|
|
382
|
+.chapter-meta {
|
|
|
383
|
+ display: flex;
|
|
|
384
|
+ justify-content: space-between;
|
|
|
385
|
+ font-size: 12px;
|
|
|
386
|
+ color: #999;
|
|
|
387
|
+}
|
|
|
388
|
+
|
|
|
389
|
+.chapter-vip {
|
|
|
390
|
+ display: flex;
|
|
|
391
|
+ align-items: center;
|
|
|
392
|
+ color: #ffc53d;
|
|
|
393
|
+}
|
|
|
394
|
+
|
|
|
395
|
+.chapter-vip text {
|
|
|
396
|
+ margin-left: 3px;
|
|
|
397
|
+}
|
|
|
398
|
+</style>
|