yinshaojie 10 месяцев назад
Родитель
Сommit
ef1d4bb3ae

+ 10
- 1
RuoYi-App/App.vue Просмотреть файл

@@ -11,9 +11,18 @@ import { onLaunch, onShow } from '@dcloudio/uni-app'
11 11
 //import { useThemeStore } from '@/stores/theme'
12 12
 import { useUserStore } from '@/stores/user'
13 13
 import { useVipStore } from '@/stores/vip'
14
-
14
+import { useReminder } from '@/utils/reminder'
15
+import { readingService } from '@/services/readingService'
15 16
 // 初始化主题
16 17
 onLaunch(() => {
18
+  // 登录成功后同步进度
19
+  uni.$on('loginSuccess', async () => {
20
+    await readingService.syncAllLocalProgress()
21
+    uni.showToast({ title: '阅读进度已同步', icon: 'success' })
22
+  })
23
+	  // 初始化签到提醒
24
+	  useReminder.init()
25
+	  
17 26
   const themeStore = useThemeStore()
18 27
   themeStore.initTheme()
19 28
 })

+ 33
- 1
RuoYi-App/components/NovelReader.vue Просмотреть файл

@@ -100,6 +100,34 @@ import { cleanNovelContent, paginateContent } from '@/utils/contentUtils'
100 100
 import { contentCleaner } from '@/utils/contentCleaner'
101 101
 //import { useAdManager } from '@/utils/adManager'
102 102
 import { useVipStore } from '@/stores/vip'
103
+import { cleanMonitor } from '@/utils/cleanMonitor'
104
+import { readingService } from '@/services/readingService'
105
+
106
+// 保存阅读进度
107
+const saveProgress = () => {
108
+  const progress = {
109
+    novelId: props.novelId,
110
+    chapterId: props.chapterId,
111
+    page: currentPage.value,
112
+    scrollTop: lastScrollPosition.value,
113
+    timestamp: new Date().toISOString()
114
+  }
115
+  
116
+  readingService.saveProgress(progress)
117
+}
118
+
119
+// 恢复阅读进度
120
+const restoreReadingPosition = async () => {
121
+  const progress = await readingService.getProgress(props.novelId, props.chapterId)
122
+  
123
+  if (progress) {
124
+    if (readingMode.value === 'scroll') {
125
+      scrollTop.value = progress.scrollTop
126
+    } else {
127
+      currentPage.value = progress.page
128
+    }
129
+  }
130
+}
103 131
 
104 132
 const props = defineProps({
105 133
   chapterId: {
@@ -151,7 +179,11 @@ const fetchChapterContent = async () => {
151 179
       url: `https://php-backend.aiyadianzi.ltd/chapter/${props.chapterId}`,
152 180
       method: 'GET'
153 181
     })
154
-    
182
+      // 清洗内容并监控质量
183
+      chapterContent.value = cleanMonitor.cleanAndMonitor(
184
+        res.data.content, 
185
+        props.chapterId
186
+      )
155 187
     if (res.statusCode === 200) {
156 188
       // 清洗内容并移除原始网站信息
157 189
       chapterContent.value = cleanNovelContent(res.data.content)

+ 398
- 0
RuoYi-App/pages/novel-detail/index.vue Просмотреть файл

@@ -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>

+ 86
- 0
RuoYi-App/services/readingService.js Просмотреть файл

@@ -0,0 +1,86 @@
1
+import { request } from '@/utils/request'
2
+
3
+export const readingService = {
4
+  // 保存阅读进度
5
+  async saveProgress(progress) {
6
+    // 本地保存
7
+    const key = `progress_${progress.novelId}_${progress.chapterId}`
8
+    uni.setStorageSync(key, JSON.stringify(progress))
9
+    
10
+    // 用户登录时同步到服务器
11
+    if (uni.getStorageSync('token')) {
12
+      try {
13
+        await request({
14
+          url: '/reading/progress',
15
+          method: 'POST',
16
+          data: progress,
17
+          headers: {
18
+            'Content-Type': 'application/json',
19
+            'Authorization': `Bearer ${uni.getStorageSync('token')}`
20
+          }
21
+        })
22
+      } catch (e) {
23
+        console.error('进度同步失败', e)
24
+      }
25
+    }
26
+  },
27
+  
28
+  // 获取阅读进度
29
+  async getProgress(novelId, chapterId) {
30
+    // 优先从本地获取
31
+    const key = `progress_${novelId}_${chapterId}`
32
+    const localData = uni.getStorageSync(key)
33
+    if (localData) {
34
+      return JSON.parse(localData)
35
+    }
36
+    
37
+    // 用户登录时从服务器获取
38
+    if (uni.getStorageSync('token')) {
39
+      try {
40
+        const res = await request({
41
+          url: `/reading/progress?novelId=${novelId}&chapterId=${chapterId}`,
42
+          method: 'GET',
43
+          headers: {
44
+            'Authorization': `Bearer ${uni.getStorageSync('token')}`
45
+          }
46
+        })
47
+        
48
+        if (res.data.success) {
49
+          return res.data.data
50
+        }
51
+      } catch (e) {
52
+        console.error('进度获取失败', e)
53
+      }
54
+    }
55
+    
56
+    return null
57
+  },
58
+  
59
+  // 同步所有本地进度
60
+  async syncAllLocalProgress() {
61
+    if (!uni.getStorageSync('token')) return
62
+    
63
+    const allKeys = uni.getStorageInfoSync().keys
64
+    const progressKeys = allKeys.filter(key => key.startsWith('progress_'))
65
+    
66
+    for (const key of progressKeys) {
67
+      const progress = JSON.parse(uni.getStorageSync(key))
68
+      try {
69
+        await request({
70
+          url: '/reading/progress',
71
+          method: 'POST',
72
+          data: progress,
73
+          headers: {
74
+            'Content-Type': 'application/json',
75
+            'Authorization': `Bearer ${uni.getStorageSync('token')}`
76
+          }
77
+        })
78
+        
79
+        // 同步成功后移除本地记录
80
+        uni.removeStorageSync(key)
81
+      } catch (e) {
82
+        console.error(`进度同步失败: ${key}`, e)
83
+      }
84
+    }
85
+  }
86
+}

+ 8
- 0
RuoYi-App/utils/adManager.js Просмотреть файл

@@ -78,6 +78,8 @@ const AD_PLATFORMS = {
78 78
   }
79 79
 }
80 80
 
81
+
82
+
81 83
 // 广告触发控制器
82 84
 export function useAdManager() {
83 85
   const userStore = useUserStore()
@@ -97,6 +99,9 @@ export function useAdManager() {
97 99
 
98 100
   // 展示激励广告(每5章触发)
99 101
   const showRewardAd = async (chapterId) => {
102
+  // VIP用户跳过广告
103
+    if (vipStore.hasPrivilege('免广告')) return
104
+    
100 105
     if (userStore.isVIP) return
101 106
     
102 107
     // 广告冷却时间(至少30秒)
@@ -146,6 +151,9 @@ export function useAdManager() {
146 151
 
147 152
   // 展示底部信息流广告
148 153
   const showFeedAd = (adUnitId) => {
154
+      // VIP用户跳过广告
155
+    if (vipStore.hasPrivilege('免广告')) return null
156
+    
149 157
     if (userStore.isVIP) return null
150 158
     
151 159
     const platform = getCurrentPlatform()

+ 67
- 0
RuoYi-App/utils/cleanMonitor.js Просмотреть файл

@@ -0,0 +1,67 @@
1
+import { contentCleaner } from '@/utils/contentCleaner'
2
+import { request } from '@/utils/request'
3
+
4
+// 内容清洗质量监控
5
+export const cleanMonitor = {
6
+  // 清洗并记录质量
7
+  cleanAndMonitor(content, chapterId) {
8
+    const startTime = Date.now()
9
+    const originalLength = content.length
10
+    
11
+    // 执行清洗
12
+    const cleanedContent = contentCleaner.clean(content)
13
+    const cleanedLength = cleanedContent.length
14
+    
15
+    // 计算清洗指标
16
+    const metrics = {
17
+      chapterId,
18
+      originalLength,
19
+      cleanedLength,
20
+      reductionRate: (originalLength - cleanedLength) / originalLength,
21
+      timeCost: Date.now() - startTime,
22
+      timestamp: new Date().toISOString()
23
+    }
24
+    
25
+    // 记录到本地
26
+    this.logLocal(metrics)
27
+    
28
+    // 异步上报到服务器
29
+    this.reportMetrics(metrics)
30
+    
31
+    return cleanedContent
32
+  },
33
+  
34
+  // 本地日志记录
35
+  logLocal(metrics) {
36
+    const logs = uni.getStorageSync('cleanLogs') || []
37
+    logs.push(metrics)
38
+    
39
+    // 最多保留100条记录
40
+    if (logs.length > 100) {
41
+      logs.shift()
42
+    }
43
+    
44
+    uni.setStorageSync('cleanLogs', logs)
45
+  },
46
+  
47
+  // 上报到服务器
48
+  async reportMetrics(metrics) {
49
+    try {
50
+      await request({
51
+        url: '/monitor/clean',
52
+        method: 'POST',
53
+        data: metrics,
54
+        headers: {
55
+          'Content-Type': 'application/json'
56
+        }
57
+      })
58
+    } catch (e) {
59
+      console.error('清洗指标上报失败', e)
60
+    }
61
+  },
62
+  
63
+  // 获取本地日志
64
+  getLocalLogs() {
65
+    return uni.getStorageSync('cleanLogs') || []
66
+  }
67
+}

+ 68
- 0
RuoYi-App/utils/reminder.js Просмотреть файл

@@ -0,0 +1,68 @@
1
+import { request } from '@/utils/request'
2
+
3
+// 签到提醒系统
4
+export const useReminder = {
5
+  // 检查是否需要提醒
6
+  async checkSignReminder() {
7
+    // 获取用户最后签到时间
8
+    const lastSign = uni.getStorageSync('lastSignDate') || ''
9
+    const today = new Date().toISOString().split('T')[0]
10
+    
11
+    // 今天已签到
12
+    if (lastSign === today) return false
13
+    
14
+    // 获取用户设置
15
+    const settings = uni.getStorageSync('userSettings') || {}
16
+    if (settings.signReminder === false) return false
17
+    
18
+    // 检查服务器端签到状态
19
+    try {
20
+      const res = await request({
21
+        url: '/sign/status',
22
+        method: 'GET',
23
+        headers: { 'Authorization': `Bearer ${uni.getStorageSync('token')}` }
24
+      })
25
+      
26
+      return !res.data.todaySigned
27
+    } catch (e) {
28
+      console.error('签到状态检查失败', e)
29
+      return false
30
+    }
31
+  },
32
+  
33
+  // 显示签到提醒
34
+  showReminder() {
35
+    uni.showModal({
36
+      title: '每日签到',
37
+      content: '您今天还未签到,立即签到可获得金币奖励!',
38
+      confirmText: '去签到',
39
+      cancelText: '稍后提醒',
40
+      success: (res) => {
41
+        if (res.confirm) {
42
+          uni.navigateTo({ url: '/pages/welfare/index' })
43
+        } else {
44
+          // 1小时后再次提醒
45
+          setTimeout(this.showReminder, 60 * 60 * 1000)
46
+        }
47
+      }
48
+    })
49
+  },
50
+  
51
+  // 初始化签到提醒
52
+  init() {
53
+    // 每天10点检查签到
54
+    setInterval(async () => {
55
+      const shouldRemind = await this.checkSignReminder()
56
+      if (shouldRemind) {
57
+        this.showReminder()
58
+      }
59
+    }, 60 * 60 * 1000) // 每小时检查一次
60
+    
61
+    // 应用启动时检查
62
+    this.checkSignReminder().then(shouldRemind => {
63
+      if (shouldRemind) {
64
+        setTimeout(() => this.showReminder(), 3000) // 3秒后显示
65
+      }
66
+    })
67
+  }
68
+}

Загрузка…
Отмена
Сохранить