fzzj преди 10 месеца
родител
ревизия
a46d2d2495

+ 90
- 0
RuoYi-App/App.vue Целия файл

@@ -51,4 +51,94 @@ onLaunch(() => {
51 51
   background-color: var(--bg-color);
52 52
   color: var(--text-color);
53 53
 }
54
+
55
+/* 全局主题变量 */
56
+:root {
57
+  --primary-color: #1890ff;
58
+  --bg-color: #f8f9fa;
59
+  --text-color: #333;
60
+  --card-bg: #ffffff;
61
+  
62
+  /* 确保所有页面继承主题 */
63
+  background-color: var(--bg-color);
64
+  color: var(--text-color);
65
+}
66
+
67
+/* 哎呀科技蓝主题变量覆盖 */
68
+.theme-aydzBlue {
69
+  --primary-color: #2a5caa;
70
+  --bg-color: #e6f7ff;
71
+  --text-color: #1a3353;
72
+  --card-bg: #d0e8ff;
73
+}
74
+
75
+/* 深色模式主题变量覆盖 */
76
+.theme-darkMode {
77
+  --primary-color: #52c41a;
78
+  --bg-color: #1a1a1a;
79
+  --text-color: #e6e6e6;
80
+  --card-bg: #2a2a2a;
81
+}
82
+
83
+/* 公共组件样式 */
84
+.card {
85
+  background-color: var(--card-bg);
86
+  border-radius: 12px;
87
+  padding: 16px;
88
+  margin-bottom: 16px;
89
+  box-shadow: 0 2px 8px rgba(0,0,0,0.05);
90
+}
91
+
92
+.primary-button {
93
+  background-color: var(--primary-color);
94
+  color: white;
95
+  border: none;
96
+  border-radius: 24px;
97
+  padding: 10px 20px;
98
+  font-weight: bold;
99
+}/* 全局主题变量 */
100
+:root {
101
+  --primary-color: #1890ff;
102
+  --bg-color: #f8f9fa;
103
+  --text-color: #333;
104
+  --card-bg: #ffffff;
105
+  
106
+  /* 确保所有页面继承主题 */
107
+  background-color: var(--bg-color);
108
+  color: var(--text-color);
109
+}
110
+
111
+/* 哎呀科技蓝主题变量覆盖 */
112
+.theme-aydzBlue {
113
+  --primary-color: #2a5caa;
114
+  --bg-color: #e6f7ff;
115
+  --text-color: #1a3353;
116
+  --card-bg: #d0e8ff;
117
+}
118
+
119
+/* 深色模式主题变量覆盖 */
120
+.theme-darkMode {
121
+  --primary-color: #52c41a;
122
+  --bg-color: #1a1a1a;
123
+  --text-color: #e6e6e6;
124
+  --card-bg: #2a2a2a;
125
+}
126
+
127
+/* 公共组件样式 */
128
+.card {
129
+  background-color: var(--card-bg);
130
+  border-radius: 12px;
131
+  padding: 16px;
132
+  margin-bottom: 16px;
133
+  box-shadow: 0 2px 8px rgba(0,0,0,0.05);
134
+}
135
+
136
+.primary-button {
137
+  background-color: var(--primary-color);
138
+  color: white;
139
+  border: none;
140
+  border-radius: 24px;
141
+  padding: 10px 20px;
142
+  font-weight: bold;
143
+}
54 144
 </style>

+ 36
- 0
RuoYi-App/api/vip.js Целия файл

@@ -0,0 +1,36 @@
1
+import { request } from '@/utils/request'
2
+
3
+export const vipApi = {
4
+  // 获取会员状态
5
+  getStatus(userId) {
6
+    return request({
7
+      url: '/vip/status',
8
+      method: 'GET',
9
+      data: { userId },
10
+      headers: {
11
+        'Authorization': `Bearer ${uni.getStorageSync('token')}`
12
+      }
13
+    })
14
+  },
15
+  
16
+  // 购买会员
17
+  purchase(order) {
18
+    return request({
19
+      url: '/vip/purchase',
20
+      method: 'POST',
21
+      data: order,
22
+      headers: {
23
+        'Content-Type': 'application/json',
24
+        'Authorization': `Bearer ${uni.getStorageSync('token')}`
25
+      }
26
+    })
27
+  },
28
+  
29
+  // 获取会员权益列表
30
+  getPrivileges() {
31
+    return request({
32
+      url: '/vip/privileges',
33
+      method: 'GET'
34
+    })
35
+  }
36
+}

+ 335
- 91
RuoYi-App/components/NovelReader.vue Целия файл

@@ -1,25 +1,65 @@
1 1
 <template>
2 2
   <view class="reader-container">
3
+    <!-- 顶部操作栏 -->
4
+    <view class="reader-header">
5
+      <view class="left-actions">
6
+        <button class="back-btn" @click="goBack">
7
+          <uni-icons type="arrowleft" size="24" color="#333"></uni-icons>
8
+        </button>
9
+        <text class="chapter-title">{{ chapterTitle }}</text>
10
+      </view>
11
+      <view class="right-actions">
12
+        <button class="action-btn" @click="toggleTheme">
13
+          <uni-icons type="color" size="20"></uni-icons>
14
+        </button>
15
+        <button class="action-btn" @click="toggleFontSize">
16
+          <uni-icons type="font" size="20"></uni-icons>
17
+        </button>
18
+        <button class="action-btn" @click="toggleMode">
19
+          <uni-icons :type="readingMode === 'scroll' ? 'list' : 'grid'" size="20"></uni-icons>
20
+        </button>
21
+      </view>
22
+    </view>
23
+
3 24
     <!-- 阅读模式选择器 -->
4
-    <view class="mode-selector">
5
-      <button @click="readingMode = 'scroll'" :class="{ active: readingMode === 'scroll' }">
6
-        滚动模式
7
-      </button>
8
-      <button @click="readingMode = 'page'" :class="{ active: readingMode === 'page' }">
9
-        翻页模式
25
+    <view v-if="showModeSelector" class="mode-selector">
26
+      <button 
27
+        v-for="mode in readingModes" 
28
+        :key="mode.value" 
29
+        :class="['mode-btn', { active: readingMode === mode.value }]"
30
+        @click="changeMode(mode.value)"
31
+      >
32
+        {{ mode.label }}
10 33
       </button>
11 34
     </view>
12
-    
35
+
36
+    <!-- 字体大小选择器 -->
37
+    <view v-if="showFontSizeSelector" class="font-size-selector">
38
+      <button class="size-btn" @click="decreaseFontSize">A-</button>
39
+      <slider 
40
+        :value="fontSize" 
41
+        min="14" 
42
+        max="24" 
43
+        step="2" 
44
+        @change="changeFontSize" 
45
+        class="font-slider"
46
+      />
47
+      <button class="size-btn" @click="increaseFontSize">A+</button>
48
+    </view>
49
+
13 50
     <!-- 滚动阅读模式 -->
14 51
     <scroll-view 
15 52
       v-if="readingMode === 'scroll'" 
16 53
       scroll-y 
17 54
       class="scroll-reader"
55
+      :style="{ fontSize: fontSize + 'px' }"
56
+      :scroll-top="scrollTop"
57
+      @scroll="onScroll"
18 58
       @scrolltolower="loadNextChapter"
19 59
     >
20 60
       <rich-text :nodes="formatContent(chapterContent)" />
21 61
     </scroll-view>
22
-    
62
+
23 63
     <!-- 翻页阅读模式 -->
24 64
     <swiper 
25 65
       v-else 
@@ -29,13 +69,22 @@
29 69
       @change="onPageChange"
30 70
     >
31 71
       <swiper-item v-for="(page, index) in paginatedContent" :key="index">
32
-        <rich-text :nodes="page" />
72
+        <view :style="{ fontSize: fontSize + 'px', padding: '20px' }">
73
+          <rich-text :nodes="page" />
74
+        </view>
33 75
       </swiper-item>
34 76
     </swiper>
35
-    
77
+
78
+    <!-- 底部操作栏 -->
79
+    <view class="reader-footer">
80
+      <button class="footer-btn" @click="prevChapter">上一章</button>
81
+      <button class="footer-btn" @click="showChapterList">目录</button>
82
+      <button class="footer-btn" @click="nextChapter">下一章</button>
83
+    </view>
84
+
36 85
     <!-- 底部广告 -->
37
-    <view v-if="feedAd" class="bottom-ad">
38
-      <ad :unit-id="feedAd.unitId" ad-type="feed" />
86
+    <view v-if="showBottomAd && !isVIP" class="bottom-ad">
87
+      <ad :unit-id="bottomAdUnitId" ad-type="feed" />
39 88
     </view>
40 89
   </view>
41 90
 </template>
@@ -44,141 +93,336 @@
44 93
 import { ref, computed, onMounted, watch } from 'vue'
45 94
 import { useAdManager } from '@/utils/adManager'
46 95
 import { useUserStore } from '@/stores/user'
96
+import { useThemeStore } from '@/stores/theme'
97
+import { useReadingProgress } from '@/composables/useReadingProgress'
98
+import { cleanNovelContent, paginateContent } from '@/utils/contentUtils'
47 99
 
48 100
 const props = defineProps({
49
-  chapterId: Number,
50
-  novelId: Number
101
+  chapterId: {
102
+    type: Number,
103
+    required: true
104
+  },
105
+  novelId: {
106
+    type: Number,
107
+    required: true
108
+  },
109
+  chapterTitle: {
110
+    type: String,
111
+    default: '加载中...'
112
+  }
51 113
 })
52 114
 
115
+const emit = defineEmits(['back', 'prev', 'next', 'show-chapters'])
116
+
53 117
 const userStore = useUserStore()
54
-const { showRewardAd, showFeedAd } = useAdManager()
118
+const themeStore = useThemeStore()
119
+const { showRewardAd } = useAdManager()
120
+const { saveProgress, loadProgress } = useReadingProgress()
55 121
 
56 122
 // 阅读状态
57 123
 const readingMode = ref('scroll') // 'scroll' | 'page'
58
-const currentPage = ref(0)
124
+const readingModes = ref([
125
+  { label: '滚动模式', value: 'scroll' },
126
+  { label: '翻页模式', value: 'page' }
127
+])
128
+const showModeSelector = ref(false)
129
+const showFontSizeSelector = ref(false)
130
+const fontSize = ref(18)
59 131
 const chapterContent = ref('')
60 132
 const paginatedContent = ref([])
61
-const lastReadPosition = ref(0)
62
-const feedAd = ref(null)
133
+const currentPage = ref(0)
134
+const scrollTop = ref(0)
135
+const lastScrollPosition = ref(0)
136
+const showBottomAd = ref(true)
137
+const bottomAdUnitId = ref('')
138
+const isVIP = computed(() => userStore.isVIP)
63 139
 
64 140
 // 获取章节内容
65 141
 const fetchChapterContent = async () => {
66
-  const res = await uni.request({
67
-    url: `https://php-backend.aiyadianzi.ltd/chapter/${props.chapterId}`,
68
-    method: 'GET'
69
-  })
70
-  
71
-  // 清洗内容
72
-  chapterContent.value = cleanContent(res.data.content)
73
-  
74
-  // 分页处理
75
-  paginatedContent.value = paginateContent(chapterContent.value)
76
-  
77
-  // 恢复阅读位置
78
-  if (userStore.isLoggedIn) {
79
-    const position = await getReadingPosition()
80
-    currentPage.value = position.page
81
-    lastReadPosition.value = position.scrollTop
142
+  try {
143
+    const res = await uni.request({
144
+      url: `https://php-backend.aiyadianzi.ltd/chapter/${props.chapterId}`,
145
+      method: 'GET'
146
+    })
147
+    
148
+    if (res.statusCode === 200) {
149
+      // 清洗内容并移除原始网站信息
150
+      chapterContent.value = cleanNovelContent(res.data.content)
151
+      
152
+      // 分页处理
153
+      paginatedContent.value = paginateContent(chapterContent.value, 800)
154
+      
155
+      // 恢复阅读位置
156
+      restoreReadingPosition()
157
+    } else {
158
+      throw new Error('章节加载失败')
159
+    }
160
+  } catch (error) {
161
+    uni.showToast({
162
+      title: error.message || '加载章节失败',
163
+      icon: 'none'
164
+    })
82 165
   }
83 166
 }
84 167
 
85
-// 内容清洗(移除原始网站信息)
86
-const cleanContent = (content) => {
87
-  return content
88
-    .replace(/最新网址[::]?\s*[a-z0-9.-]+/gi, '')
89
-    .replace(/www\.[a-z0-9]+\.[a-z]{2,}/gi, '')
90
-}
91
-
92
-// 内容分页(每页800字符)
93
-const paginateContent = (content) => {
94
-  const pages = []
95
-  const pageSize = 800
96
-  let start = 0
168
+// 恢复阅读位置
169
+const restoreReadingPosition = async () => {
170
+  const progress = await loadProgress(props.novelId, props.chapterId)
97 171
   
98
-  while (start < content.length) {
99
-    pages.push(content.substring(start, start + pageSize))
100
-    start += pageSize
172
+  if (progress) {
173
+    if (readingMode.value === 'scroll') {
174
+      scrollTop.value = progress.scrollTop
175
+    } else {
176
+      currentPage.value = progress.page
177
+    }
101 178
   }
102
-  
103
-  return pages
104 179
 }
105 180
 
106 181
 // 翻页事件处理
107 182
 const onPageChange = (e) => {
108 183
   currentPage.value = e.detail.current
109 184
   saveReadingPosition()
110
-  showRewardAd(props.chapterId)
185
+  
186
+  // 每5页触发广告
187
+  if (currentPage.value % 5 === 0) {
188
+    showRewardAd(props.chapterId)
189
+  }
190
+}
191
+
192
+// 滚动事件处理
193
+const onScroll = (e) => {
194
+  lastScrollPosition.value = e.detail.scrollTop
195
+  saveReadingPosition()
111 196
 }
112 197
 
113 198
 // 保存阅读位置
114 199
 const saveReadingPosition = () => {
115
-  if (!userStore.isLoggedIn) return
116
-  
117
-  uni.request({
118
-    url: 'https://api.aiyadianzi.ltd/reading/progress',
119
-    method: 'POST',
120
-    data: {
121
-      userId: userStore.userId,
122
-      novelId: props.novelId,
123
-      chapterId: props.chapterId,
124
-      page: currentPage.value,
125
-      scrollTop: lastReadPosition.value
126
-    }
200
+  saveProgress(props.novelId, props.chapterId, {
201
+    page: currentPage.value,
202
+    scrollTop: lastScrollPosition.value
127 203
   })
128 204
 }
129 205
 
130
-// 初始化
131
-onMounted(async () => {
132
-  await fetchChapterContent()
133
-  feedAd.value = showFeedAd()
134
-})
206
+// 切换阅读模式
207
+const toggleMode = () => {
208
+  showModeSelector.value = !showModeSelector.value
209
+}
135 210
 
136
-// VIP状态变化时更新广告
137
-watch(() => userStore.isVIP, (isVip) => {
138
-  if (isVip) {
139
-    feedAd.value = null
140
-  } else {
141
-    feedAd.value = showFeedAd()
142
-  }
211
+// 改变阅读模式
212
+const changeMode = (mode) => {
213
+  readingMode.value = mode
214
+  showModeSelector.value = false
215
+  saveReadingPosition()
216
+}
217
+
218
+// 切换字体大小选择器
219
+const toggleFontSize = () => {
220
+  showFontSizeSelector.value = !showFontSizeSelector.value
221
+}
222
+
223
+// 改变字体大小
224
+const changeFontSize = (e) => {
225
+  fontSize.value = e.detail.value
226
+}
227
+
228
+// 增大字体
229
+const increaseFontSize = () => {
230
+  if (fontSize.value < 24) fontSize.value += 2
231
+}
232
+
233
+// 减小字体
234
+const decreaseFontSize = () => {
235
+  if (fontSize.value > 14) fontSize.value -= 2
236
+}
237
+
238
+// 切换主题
239
+const toggleTheme = () => {
240
+  const themes = Object.keys(themeStore.themes)
241
+  const currentIndex = themes.indexOf(themeStore.currentTheme)
242
+  const nextIndex = (currentIndex + 1) % themes.length
243
+  themeStore.setTheme(themes[nextIndex])
244
+}
245
+
246
+// 加载下一章
247
+const loadNextChapter = () => {
248
+  emit('next')
249
+}
250
+
251
+// 返回
252
+const goBack = () => {
253
+  emit('back')
254
+}
255
+
256
+// 上一章
257
+const prevChapter = () => {
258
+  emit('prev')
259
+}
260
+
261
+// 下一章
262
+const nextChapter = () => {
263
+  emit('next')
264
+}
265
+
266
+// 显示章节列表
267
+const showChapterList = () => {
268
+  emit('show-chapters')
269
+}
270
+
271
+// 初始化
272
+onMounted(() => {
273
+  fetchChapterContent()
274
+  
275
+  // 设置广告单元ID
276
+  // #ifdef MP-WEIXIN
277
+  bottomAdUnitId.value = 'wechat_feed_ad_123456'
278
+  // #endif
279
+  // #ifdef MP-TOUTIAO
280
+  bottomAdUnitId.value = 'douyin_feed_ad_654321'
281
+  // #endif
282
+  // #ifdef H5
283
+  bottomAdUnitId.value = 'h5_feed_ad_789012'
284
+  // #endif
285
+  
286
+  // 监听VIP状态变化
287
+  watch(() => userStore.isVIP, (isVip) => {
288
+    showBottomAd.value = !isVip
289
+  })
143 290
 })
144 291
 </script>
145 292
 
146 293
 <style scoped>
147 294
 .reader-container {
148
-  height: 100vh;
149 295
   display: flex;
150 296
   flex-direction: column;
297
+  height: 100vh;
298
+  background-color: var(--reader-bg-color);
299
+  color: var(--reader-text-color);
300
+}
301
+
302
+.reader-header {
303
+  display: flex;
304
+  justify-content: space-between;
305
+  align-items: center;
306
+  padding: 10px 15px;
307
+  background-color: var(--header-bg-color);
308
+  border-bottom: 1px solid var(--border-color);
309
+  height: 50px;
310
+  box-sizing: border-box;
311
+}
312
+
313
+.left-actions {
314
+  display: flex;
315
+  align-items: center;
316
+  flex: 1;
317
+}
318
+
319
+.back-btn {
320
+  margin-right: 15px;
321
+  background: none;
322
+  border: none;
323
+  padding: 0;
324
+}
325
+
326
+.chapter-title {
327
+  font-size: 16px;
328
+  font-weight: bold;
329
+  white-space: nowrap;
330
+  overflow: hidden;
331
+  text-overflow: ellipsis;
332
+  max-width: 60vw;
333
+}
334
+
335
+.right-actions {
336
+  display: flex;
337
+}
338
+
339
+.action-btn {
340
+  background: none;
341
+  border: none;
342
+  padding: 5px 10px;
343
+  margin-left: 10px;
151 344
 }
152 345
 
153 346
 .mode-selector {
154 347
   display: flex;
155 348
   padding: 10px;
156
-  background: var(--header-bg);
157
-  
158
-  button {
159
-    flex: 1;
160
-    margin: 0 5px;
161
-    font-size: 14px;
162
-    
163
-    &.active {
164
-      background: var(--primary-color);
165
-      color: white;
166
-    }
167
-  }
349
+  background-color: var(--header-bg-color);
350
+  justify-content: center;
351
+  border-bottom: 1px solid var(--border-color);
352
+}
353
+
354
+.mode-btn {
355
+  margin: 0 10px;
356
+  padding: 5px 15px;
357
+  border-radius: 15px;
358
+  background-color: var(--button-bg);
359
+  color: var(--button-color);
360
+  border: none;
361
+  font-size: 14px;
362
+}
363
+
364
+.mode-btn.active {
365
+  background-color: var(--primary-color);
366
+  color: white;
367
+}
368
+
369
+.font-size-selector {
370
+  display: flex;
371
+  align-items: center;
372
+  padding: 10px 15px;
373
+  background-color: var(--header-bg-color);
374
+  border-bottom: 1px solid var(--border-color);
375
+}
376
+
377
+.size-btn {
378
+  background: none;
379
+  border: 1px solid var(--border-color);
380
+  border-radius: 4px;
381
+  width: 40px;
382
+  height: 40px;
383
+  display: flex;
384
+  align-items: center;
385
+  justify-content: center;
386
+  font-weight: bold;
387
+}
388
+
389
+.font-slider {
390
+  flex: 1;
391
+  margin: 0 15px;
168 392
 }
169 393
 
170 394
 .scroll-reader {
171 395
   flex: 1;
172
-  padding: 15px;
396
+  padding: 20px;
173 397
   overflow-y: auto;
398
+  line-height: 1.8;
174 399
 }
175 400
 
176 401
 .page-reader {
177 402
   flex: 1;
178 403
 }
179 404
 
405
+.reader-footer {
406
+  display: flex;
407
+  justify-content: space-between;
408
+  padding: 10px 15px;
409
+  background-color: var(--header-bg-color);
410
+  border-top: 1px solid var(--border-color);
411
+}
412
+
413
+.footer-btn {
414
+  flex: 1;
415
+  margin: 0 5px;
416
+  padding: 8px 0;
417
+  border-radius: 4px;
418
+  background-color: var(--button-bg);
419
+  color: var(--button-color);
420
+  border: none;
421
+  font-size: 14px;
422
+}
423
+
180 424
 .bottom-ad {
181 425
   height: 100px;
182
-  background: #f5f5f5;
426
+  background-color: #f5f5f5;
183 427
 }
184 428
 </style>

+ 183
- 0
RuoYi-App/components/PartnerTask.vue Целия файл

@@ -0,0 +1,183 @@
1
+<template>
2
+  <view class="partner-task">
3
+    <view class="header">
4
+      <text class="title">合作任务</text>
5
+      <text class="subtitle">完成任务赚金币</text>
6
+    </view>
7
+    
8
+    <scroll-view scroll-x class="task-scroll">
9
+      <view 
10
+        v-for="task in tasks" 
11
+        :key="task.id"
12
+        class="task-card"
13
+        @click="handleTask(task)"
14
+      >
15
+        <image :src="task.logo" class="logo" />
16
+        <view class="info">
17
+          <text class="name">{{ task.name }}</text>
18
+          <text class="reward">+{{ task.reward }}金币</text>
19
+        </view>
20
+        <button class="action-btn">{{ task.completed ? '已完成' : '去完成' }}</button>
21
+      </view>
22
+    </scroll-view>
23
+  </view>
24
+</template>
25
+
26
+<script setup>
27
+import { ref } from 'vue'
28
+
29
+// 合作任务数据
30
+const tasks = ref([
31
+  {
32
+    id: 1,
33
+    name: '百度地图签到',
34
+    logo: '/static/partners/baidu.png',
35
+    reward: 30,
36
+    completed: false,
37
+    handler: () => {
38
+      uni.navigateToMiniProgram({
39
+        appId: 'wx4f1b24bdc99fa23e',
40
+        path: 'pages/index/index',
41
+        success: () => console.log('跳转百度地图成功')
42
+      })
43
+    }
44
+  },
45
+  {
46
+    id: 2,
47
+    name: '快手看视频',
48
+    logo: '/static/partners/kuaishou.png',
49
+    reward: 50,
50
+    completed: false,
51
+    handler: () => {
52
+      uni.navigateToMiniProgram({
53
+        appId: 'wx9188fa3c6a2d1e87',
54
+        path: 'pages/index/index',
55
+        success: () => console.log('跳转快手成功')
56
+      })
57
+    }
58
+  },
59
+  {
60
+    id: 3,
61
+    name: '京东金融',
62
+    logo: '/static/partners/jd.png',
63
+    reward: 80,
64
+    completed: true,
65
+    handler: () => {
66
+      uni.navigateToMiniProgram({
67
+        appId: 'wx91d27dbf599dff74',
68
+        path: 'pages/index/index',
69
+        success: () => console.log('跳转京东金融成功')
70
+      })
71
+    }
72
+  },
73
+  {
74
+    id: 4,
75
+    name: '美团外卖',
76
+    logo: '/static/partners/meituan.png',
77
+    reward: 100,
78
+    completed: false,
79
+    handler: () => {
80
+      uni.navigateToMiniProgram({
81
+        appId: 'wxde8ac0a21135c07d',
82
+        path: 'pages/index/index',
83
+        success: () => console.log('跳转美团成功')
84
+      })
85
+    }
86
+  }
87
+])
88
+
89
+// 处理任务点击
90
+const handleTask = (task) => {
91
+  if (task.completed) {
92
+    uni.showToast({ title: '任务已完成', icon: 'none' })
93
+    return
94
+  }
95
+  
96
+  // 执行任务处理逻辑
97
+  task.handler()
98
+  
99
+  // 模拟完成任务
100
+  setTimeout(() => {
101
+    task.completed = true
102
+    uni.showToast({ title: `任务完成!获得${task.reward}金币`, icon: 'success' })
103
+  }, 2000)
104
+}
105
+</script>
106
+
107
+<style scoped>
108
+.partner-task {
109
+  background-color: var(--card-bg);
110
+  border-radius: 16px;
111
+  padding: 20px;
112
+  margin: 20px 0;
113
+}
114
+
115
+.header {
116
+  margin-bottom: 15px;
117
+}
118
+
119
+.title {
120
+  font-size: 18px;
121
+  font-weight: bold;
122
+  display: block;
123
+}
124
+
125
+.subtitle {
126
+  font-size: 14px;
127
+  color: #666;
128
+}
129
+
130
+.task-scroll {
131
+  white-space: nowrap;
132
+}
133
+
134
+.task-card {
135
+  display: inline-block;
136
+  width: 280px;
137
+  background: linear-gradient(135deg, #f9f9f9 0%, #ffffff 100%);
138
+  border-radius: 12px;
139
+  padding: 15px;
140
+  margin-right: 15px;
141
+  box-shadow: 0 2px 8px rgba(0,0,0,0.05);
142
+}
143
+
144
+.logo {
145
+  width: 50px;
146
+  height: 50px;
147
+  border-radius: 10px;
148
+  margin-right: 12px;
149
+  vertical-align: middle;
150
+}
151
+
152
+.info {
153
+  display: inline-block;
154
+  vertical-align: middle;
155
+  width: calc(100% - 120px);
156
+}
157
+
158
+.name {
159
+  font-size: 16px;
160
+  font-weight: 500;
161
+  display: block;
162
+}
163
+
164
+.reward {
165
+  font-size: 14px;
166
+  color: #f5222d;
167
+  display: block;
168
+}
169
+
170
+.action-btn {
171
+  display: inline-block;
172
+  vertical-align: middle;
173
+  background-color: var(--primary-color);
174
+  color: white;
175
+  border: none;
176
+  border-radius: 16px;
177
+  height: 36px;
178
+  line-height: 36px;
179
+  padding: 0 15px;
180
+  font-size: 14px;
181
+  margin-left: 10px;
182
+}
183
+</style>

+ 107
- 0
RuoYi-App/components/ThemePicker.vue Целия файл

@@ -0,0 +1,107 @@
1
+<template>
2
+  <view class="theme-picker">
3
+    <view class="current-theme" @click="showSelector = !showSelector">
4
+      <text>{{ themeStore.getThemeLabel(themeStore.currentTheme) }}</text>
5
+      <uni-icons :type="showSelector ? 'arrowup' : 'arrowdown'" size="16"></uni-icons>
6
+    </view>
7
+    
8
+    <view v-if="showSelector" class="theme-selector">
9
+      <view 
10
+        v-for="theme in themeStore.themeOptions"
11
+        :key="theme.name"
12
+        class="theme-item"
13
+        :class="{ active: theme.name === themeStore.currentTheme }"
14
+        @click="changeTheme(theme.name)"
15
+      >
16
+        <view class="color-preview">
17
+          <view 
18
+            v-for="(color, key) in theme.colors" 
19
+            v-if="key.includes('color')"
20
+            :key="key"
21
+            class="color-block"
22
+            :style="{ backgroundColor: color }"
23
+          ></view>
24
+        </view>
25
+        <text class="theme-label">{{ theme.label }}</text>
26
+      </view>
27
+    </view>
28
+  </view>
29
+</template>
30
+
31
+<script setup>
32
+import { ref } from 'vue'
33
+import { useThemeStore } from '@/stores/theme'
34
+
35
+const themeStore = useThemeStore()
36
+const showSelector = ref(false)
37
+
38
+// 切换主题
39
+const changeTheme = (themeName) => {
40
+  themeStore.setTheme(themeName)
41
+  showSelector.value = false
42
+}
43
+</script>
44
+
45
+<style scoped>
46
+.theme-picker {
47
+  position: relative;
48
+  z-index: 100;
49
+}
50
+
51
+.current-theme {
52
+  display: flex;
53
+  align-items: center;
54
+  padding: 8px 12px;
55
+  background-color: var(--card-bg);
56
+  border-radius: 20px;
57
+  font-size: 14px;
58
+  cursor: pointer;
59
+}
60
+
61
+.theme-selector {
62
+  position: absolute;
63
+  top: 40px;
64
+  right: 0;
65
+  width: 180px;
66
+  background-color: var(--card-bg);
67
+  border-radius: 12px;
68
+  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
69
+  padding: 10px;
70
+  z-index: 200;
71
+}
72
+
73
+.theme-item {
74
+  padding: 10px;
75
+  border-radius: 8px;
76
+  margin-bottom: 8px;
77
+  cursor: pointer;
78
+  transition: all 0.3s;
79
+}
80
+
81
+.theme-item.active {
82
+  background-color: rgba(42, 92, 170, 0.1);
83
+}
84
+
85
+.theme-item:hover {
86
+  background-color: rgba(0,0,0,0.05);
87
+}
88
+
89
+.color-preview {
90
+  display: flex;
91
+  height: 20px;
92
+  border-radius: 4px;
93
+  overflow: hidden;
94
+  margin-bottom: 6px;
95
+}
96
+
97
+.color-block {
98
+  flex: 1;
99
+  height: 100%;
100
+}
101
+
102
+.theme-label {
103
+  font-size: 13px;
104
+  text-align: center;
105
+  display: block;
106
+}
107
+</style>

+ 73
- 0
RuoYi-App/composables/useReadingProgress.js Целия файл

@@ -0,0 +1,73 @@
1
+import { ref } from 'vue'
2
+
3
+export function useReadingProgress() {
4
+  // 保存阅读进度
5
+  const saveProgress = (novelId, chapterId, progress) => {
6
+    try {
7
+      const key = `reading_progress_${novelId}_${chapterId}`
8
+      uni.setStorageSync(key, JSON.stringify(progress))
9
+      
10
+      // 同时保存到云端(如果有登录)
11
+      if (uni.getStorageSync('token')) {
12
+        uni.request({
13
+          url: 'https://api.aiyadianzi.ltd/reading/progress',
14
+          method: 'POST',
15
+          data: {
16
+            novelId,
17
+            chapterId,
18
+            ...progress
19
+          },
20
+          header: {
21
+            'Authorization': `Bearer ${uni.getStorageSync('token')}`
22
+          }
23
+        })
24
+      }
25
+    } catch (e) {
26
+      console.error('保存阅读进度失败', e)
27
+    }
28
+  }
29
+  
30
+  // 加载阅读进度
31
+  const loadProgress = (novelId, chapterId) => {
32
+    return new Promise((resolve) => {
33
+      try {
34
+        // 先尝试从本地加载
35
+        const key = `reading_progress_${novelId}_${chapterId}`
36
+        const localData = uni.getStorageSync(key)
37
+        if (localData) {
38
+          resolve(JSON.parse(localData))
39
+          return
40
+        }
41
+        
42
+        // 如果登录了,尝试从云端加载
43
+        if (uni.getStorageSync('token')) {
44
+          uni.request({
45
+            url: `https://api.aiyadianzi.ltd/reading/progress?novelId=${novelId}&chapterId=${chapterId}`,
46
+            method: 'GET',
47
+            header: {
48
+              'Authorization': `Bearer ${uni.getStorageSync('token')}`
49
+            },
50
+            success: (res) => {
51
+              if (res.data.success && res.data.data) {
52
+                resolve(res.data.data)
53
+              } else {
54
+                resolve(null)
55
+              }
56
+            },
57
+            fail: () => resolve(null)
58
+          })
59
+        } else {
60
+          resolve(null)
61
+        }
62
+      } catch (e) {
63
+        console.error('加载阅读进度失败', e)
64
+        resolve(null)
65
+      }
66
+    })
67
+  }
68
+  
69
+  return {
70
+    saveProgress,
71
+    loadProgress
72
+  }
73
+}

+ 265
- 0
RuoYi-App/pages/vip/index.vue Целия файл

@@ -0,0 +1,265 @@
1
+<template>
2
+  <view class="vip-page">
3
+    <!-- 会员状态卡片 -->
4
+    <view class="vip-card" :class="{'active': isVIP}">
5
+      <view class="card-header">
6
+        <text class="title">{{ isVIP ? `超级会员 · 有效期至${expireDate}` : '立即开通会员' }}</text>
7
+        <text v-if="!isVIP" class="tag">限时6折</text>
8
+      </view>
9
+      
10
+      <view class="privileges">
11
+        <view v-for="(item, index) in privileges" :key="index" class="privilege-item">
12
+          <uni-icons type="checkmark" size="16" color="#fff"></uni-icons>
13
+          <text>{{ item }}</text>
14
+        </view>
15
+      </view>
16
+      
17
+      <button v-if="!isVIP" class="purchase-btn" @click="showPlans">立即开通</button>
18
+      <button v-else class="renew-btn" @click="showPlans">续费会员</button>
19
+    </view>
20
+    
21
+    <!-- 会员套餐列表 -->
22
+    <view v-if="showPlanSelector" class="plan-container">
23
+      <view class="plan-header">
24
+        <text class="plan-title">选择套餐</text>
25
+        <uni-icons type="close" size="24" @click="showPlanSelector = false"></uni-icons>
26
+      </view>
27
+      
28
+      <view class="plan-list">
29
+        <view 
30
+          v-for="plan in vipPlans" 
31
+          :key="plan.id"
32
+          :class="['plan-item', { 'recommended': plan.recommended }]"
33
+          @click="selectPlan(plan)"
34
+        >
35
+          <view class="plan-top">
36
+            <text class="duration">{{ plan.duration }}个月</text>
37
+            <text v-if="plan.recommended" class="rec-tag">推荐</text>
38
+          </view>
39
+          <view class="price">
40
+            <text class="symbol">¥</text>
41
+            <text class="amount">{{ plan.price }}</text>
42
+            <text class="original">¥{{ plan.originalPrice }}</text>
43
+          </view>
44
+          <text class="daily">日均¥{{ (plan.price / (plan.duration * 30)).toFixed(2) }}</text>
45
+        </view>
46
+      </view>
47
+      
48
+      <button class="confirm-btn" @click="handlePurchase">确认支付</button>
49
+    </view>
50
+  </view>
51
+</template>
52
+
53
+<script setup>
54
+import { ref, computed } from 'vue'
55
+import { useVipStore } from '@/stores/vip'
56
+
57
+const vipStore = useVipStore()
58
+const showPlanSelector = ref(false)
59
+const selectedPlan = ref(null)
60
+
61
+// 会员套餐配置
62
+const vipPlans = ref([
63
+  { id: 1, duration: 1, price: 15, originalPrice: 30, recommended: false },
64
+  { id: 2, duration: 3, price: 40, originalPrice: 90, recommended: true },
65
+  { id: 3, duration: 12, price: 120, originalPrice: 360, recommended: false }
66
+])
67
+
68
+// 计算到期日显示
69
+const expireDate = computed(() => {
70
+  if (!vipStore.expireTime) return ''
71
+  const date = new Date(vipStore.expireTime)
72
+  return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
73
+})
74
+
75
+// 显示套餐选择器
76
+const showPlans = () => {
77
+  showPlanSelector.value = true
78
+}
79
+
80
+// 选择套餐
81
+const selectPlan = (plan) => {
82
+  selectedPlan.value = plan
83
+}
84
+
85
+// 执行购买
86
+const handlePurchase = async () => {
87
+  if (!selectedPlan.value) {
88
+    uni.showToast({ title: '请选择套餐', icon: 'none' })
89
+    return
90
+  }
91
+  
92
+  const success = await vipStore.purchaseVip(selectedPlan.value)
93
+  if (success) {
94
+    showPlanSelector.value = false
95
+  }
96
+}
97
+</script>
98
+
99
+<style scoped>
100
+.vip-page {
101
+  padding: 20px;
102
+}
103
+
104
+.vip-card {
105
+  background: linear-gradient(135deg, #2a5caa 0%, #1a3353 100%);
106
+  border-radius: 16px;
107
+  padding: 24px;
108
+  color: white;
109
+  position: relative;
110
+  overflow: hidden;
111
+}
112
+
113
+.card-header {
114
+  display: flex;
115
+  justify-content: space-between;
116
+  align-items: center;
117
+  margin-bottom: 20px;
118
+}
119
+
120
+.title {
121
+  font-size: 20px;
122
+  font-weight: bold;
123
+}
124
+
125
+.tag {
126
+  background-color: #ffc53d;
127
+  color: #1a3353;
128
+  padding: 3px 8px;
129
+  border-radius: 12px;
130
+  font-size: 12px;
131
+}
132
+
133
+.privileges {
134
+  margin-bottom: 24px;
135
+}
136
+
137
+.privilege-item {
138
+  display: flex;
139
+  align-items: center;
140
+  margin-bottom: 10px;
141
+  font-size: 15px;
142
+}
143
+
144
+.privilege-item text {
145
+  margin-left: 8px;
146
+}
147
+
148
+.purchase-btn, .renew-btn {
149
+  background-color: #ffc53d;
150
+  color: #1a3353;
151
+  border: none;
152
+  border-radius: 24px;
153
+  height: 44px;
154
+  line-height: 44px;
155
+  font-weight: bold;
156
+}
157
+
158
+.plan-container {
159
+  position: fixed;
160
+  bottom: 0;
161
+  left: 0;
162
+  right: 0;
163
+  background-color: #fff;
164
+  border-top-left-radius: 24px;
165
+  border-top-right-radius: 24px;
166
+  padding: 20px;
167
+  box-shadow: 0 -5px 20px rgba(0,0,0,0.1);
168
+  z-index: 100;
169
+}
170
+
171
+.plan-header {
172
+  display: flex;
173
+  justify-content: space-between;
174
+  align-items: center;
175
+  margin-bottom: 20px;
176
+}
177
+
178
+.plan-title {
179
+  font-size: 18px;
180
+  font-weight: bold;
181
+}
182
+
183
+.plan-list {
184
+  display: flex;
185
+  justify-content: space-between;
186
+  margin-bottom: 24px;
187
+}
188
+
189
+.plan-item {
190
+  border: 1px solid #eee;
191
+  border-radius: 12px;
192
+  padding: 15px;
193
+  width: 30%;
194
+  text-align: center;
195
+}
196
+
197
+.plan-item.recommended {
198
+  border-color: #2a5caa;
199
+  background-color: #e6f7ff;
200
+  position: relative;
201
+}
202
+
203
+.rec-tag {
204
+  position: absolute;
205
+  top: -10px;
206
+  right: 10px;
207
+  background-color: #2a5caa;
208
+  color: white;
209
+  font-size: 12px;
210
+  padding: 2px 8px;
211
+  border-radius: 10px;
212
+}
213
+
214
+.plan-top {
215
+  display: flex;
216
+  flex-direction: column;
217
+  align-items: center;
218
+  margin-bottom: 10px;
219
+}
220
+
221
+.duration {
222
+  font-size: 16px;
223
+  font-weight: bold;
224
+}
225
+
226
+.price {
227
+  display: flex;
228
+  justify-content: center;
229
+  align-items: baseline;
230
+  margin-bottom: 5px;
231
+}
232
+
233
+.symbol {
234
+  font-size: 16px;
235
+  color: #f5222d;
236
+}
237
+
238
+.amount {
239
+  font-size: 24px;
240
+  font-weight: bold;
241
+  color: #f5222d;
242
+  margin: 0 3px;
243
+}
244
+
245
+.original {
246
+  font-size: 12px;
247
+  color: #999;
248
+  text-decoration: line-through;
249
+}
250
+
251
+.daily {
252
+  font-size: 12px;
253
+  color: #666;
254
+}
255
+
256
+.confirm-btn {
257
+  background: linear-gradient(to right, #2a5caa, #1a3353);
258
+  color: white;
259
+  border: none;
260
+  border-radius: 24px;
261
+  height: 44px;
262
+  line-height: 44px;
263
+  font-weight: bold;
264
+}
265
+</style>

+ 56
- 8
RuoYi-App/store/theme.js Целия файл

@@ -1,33 +1,35 @@
1 1
 import { defineStore } from 'pinia'
2
-import { ref } from 'vue'
2
+import { ref, computed } from 'vue'
3 3
 
4 4
 export const useThemeStore = defineStore('theme', () => {
5
+  // 可用主题列表
5 6
   const themes = ref({
6 7
     default: {
7 8
       '--primary-color': '#1890ff',
8 9
       '--bg-color': '#f8f9fa',
9 10
       '--text-color': '#333',
10
-      '--header-bg': '#ffffff'
11
+      '--card-bg': '#ffffff'
11 12
     },
12 13
     aydzBlue: {
13 14
       '--primary-color': '#2a5caa',
14 15
       '--bg-color': '#e6f7ff',
15 16
       '--text-color': '#1a3353',
16
-      '--header-bg': '#2a5caa'
17
+      '--card-bg': '#d0e8ff'
17 18
     },
18 19
     darkMode: {
19 20
       '--primary-color': '#52c41a',
20 21
       '--bg-color': '#1a1a1a',
21 22
       '--text-color': '#e6e6e6',
22
-      '--header-bg': '#2a2a2a'
23
+      '--card-bg': '#2a2a2a'
23 24
     }
24 25
   })
25 26
   
26
-  const currentTheme = ref('default')
27
+  // 当前主题
28
+  const currentTheme = ref('aydzBlue')
27 29
   
28 30
   // 初始化主题
29 31
   const initTheme = () => {
30
-    const savedTheme = uni.getStorageSync('selectedTheme') || 'default'
32
+    const savedTheme = uni.getStorageSync('selectedTheme') || 'aydzBlue'
31 33
     setTheme(savedTheme)
32 34
   }
33 35
   
@@ -39,11 +41,57 @@ export const useThemeStore = defineStore('theme', () => {
39 41
     uni.setStorageSync('selectedTheme', themeName)
40 42
     
41 43
     // 应用CSS变量
44
+    const root = document.documentElement
42 45
     const themeVars = themes.value[themeName]
46
+    
43 47
     Object.keys(themeVars).forEach(key => {
44
-      document.documentElement.style.setProperty(key, themeVars[key])
48
+      root.style.setProperty(key, themeVars[key])
45 49
     })
50
+    
51
+    // 动态设置导航栏颜色
52
+    if (themeName === 'darkMode') {
53
+      uni.setNavigationBarColor({
54
+        frontColor: '#ffffff',
55
+        backgroundColor: '#1a1a1a'
56
+      })
57
+    } else if (themeName === 'aydzBlue') {
58
+      uni.setNavigationBarColor({
59
+        frontColor: '#ffffff',
60
+        backgroundColor: '#2a5caa'
61
+      })
62
+    } else {
63
+      uni.setNavigationBarColor({
64
+        frontColor: '#000000',
65
+        backgroundColor: '#f8f9fa'
66
+      })
67
+    }
46 68
   }
47 69
   
48
-  return { themes, currentTheme, initTheme, setTheme }
70
+  // 主题配置(用于UI显示)
71
+  const themeOptions = computed(() => {
72
+    return Object.keys(themes.value).map(key => ({
73
+      name: key,
74
+      label: getThemeLabel(key),
75
+      colors: themes.value[key]
76
+    }))
77
+  })
78
+  
79
+  // 获取主题显示名称
80
+  const getThemeLabel = (themeKey) => {
81
+    const labels = {
82
+      default: '默认主题',
83
+      aydzBlue: '哎呀科技蓝',
84
+      darkMode: '深色模式'
85
+    }
86
+    return labels[themeKey] || themeKey
87
+  }
88
+
89
+  return { 
90
+    themes,
91
+    currentTheme,
92
+    themeOptions,
93
+    initTheme,
94
+    setTheme,
95
+    getThemeLabel
96
+  }
49 97
 })

+ 74
- 0
RuoYi-App/store/vipStore.js Целия файл

@@ -0,0 +1,74 @@
1
+import { defineStore } from 'pinia'
2
+import { ref } from 'vue'
3
+import { vipApi } from '@/api/vip'
4
+
5
+export const useVipStore = defineStore('vip', () => {
6
+  // VIP状态
7
+  const isVIP = ref(false)
8
+  const vipType = ref(null) // 0-非会员 1-初级 2-中级 3-高级 4-超级
9
+  const expireTime = ref('')
10
+  const privileges = ref([]) // 会员特权列表
11
+  
12
+  // 从Java后端加载会员状态
13
+  const loadVipStatus = async (userId) => {
14
+    try {
15
+      const res = await vipApi.getStatus(userId)
16
+      isVIP.value = res.data.isEffective
17
+      vipType.value = res.data.type
18
+      expireTime.value = res.data.expireTime
19
+      privileges.value = res.data.privileges || [
20
+        '免广告', 
21
+        '专属内容', 
22
+        '双倍阅读金币'
23
+      ]
24
+      return true
25
+    } catch (err) {
26
+      console.error('VIP状态加载失败', err)
27
+      uni.showToast({ title: '会员信息获取失败', icon: 'none' })
28
+      return false
29
+    }
30
+  }
31
+  
32
+  // 购买VIP(对接Java支付接口)
33
+  const purchaseVip = async (plan) => {
34
+    try {
35
+      uni.showLoading({ title: '支付中...', mask: true })
36
+      
37
+      // 调用Java支付接口
38
+      const payRes = await vipApi.purchase({
39
+        userId: uni.getStorageSync('userId'),
40
+        planId: plan.id,
41
+        amount: plan.price
42
+      })
43
+      
44
+      // 处理支付结果
45
+      if (payRes.data.success) {
46
+        await loadVipStatus() // 刷新状态
47
+        uni.showToast({ title: '开通成功!', icon: 'success' })
48
+        return true
49
+      } else {
50
+        throw new Error(payRes.data.message || '支付失败')
51
+      }
52
+    } catch (err) {
53
+      uni.showToast({ title: err.message, icon: 'none' })
54
+      return false
55
+    } finally {
56
+      uni.hideLoading()
57
+    }
58
+  }
59
+  
60
+  // 会员特权检测(例如广告跳过)
61
+  const hasPrivilege = (privilegeKey) => {
62
+    return isVIP.value && privileges.value.includes(privilegeKey)
63
+  }
64
+
65
+  return { 
66
+    isVIP, 
67
+    vipType, 
68
+    expireTime, 
69
+    privileges,
70
+    loadVipStatus,
71
+    purchaseVip,
72
+    hasPrivilege
73
+  }
74
+})

+ 192
- 18
RuoYi-App/utils/adManager.js Целия файл

@@ -4,20 +4,75 @@ import { useUserStore } from '@/stores/user'
4 4
 // 广告平台配置
5 5
 const AD_PLATFORMS = {
6 6
   WECHAT: {
7
-    reward: adUnitId => wx.createRewardedVideoAd({ adUnitId }),
8
-    feed: adUnitId => wx.createBannerAd({ adUnitId })
7
+    reward: (adUnitId) => {
8
+      const ad = wx.createRewardedVideoAd({
9
+        adUnitId,
10
+        multiton: true
11
+      })
12
+      ad.onError(err => console.error('微信激励广告错误:', err))
13
+      return ad
14
+    },
15
+    feed: (adUnitId) => {
16
+      return wx.createBannerAd({
17
+        adUnitId,
18
+        adIntervals: 30,
19
+        style: {
20
+          left: 0,
21
+          top: 0,
22
+          width: 320
23
+        }
24
+      })
25
+    },
26
+    interstitial: (adUnitId) => {
27
+      return wx.createInterstitialAd({ adUnitId })
28
+    }
9 29
   },
10 30
   DOUYIN: {
11
-    reward: adUnitId => tt.createRewardedVideoAd({ adUnitId }),
12
-    feed: adUnitId => tt.createBannerAd({ adUnitId })
31
+    reward: (adUnitId) => {
32
+      const ad = tt.createRewardedVideoAd({ adUnitId })
33
+      ad.onError(err => console.error('抖音激励广告错误:', err))
34
+      return ad
35
+    },
36
+    feed: (adUnitId) => {
37
+      return tt.createBannerAd({
38
+        adUnitId,
39
+        style: {
40
+          left: 0,
41
+          top: 0,
42
+          width: 300
43
+        }
44
+      })
45
+    },
46
+    interstitial: (adUnitId) => {
47
+      return tt.createInterstitialAd({ adUnitId })
48
+    }
13 49
   },
14 50
   H5: {
15
-    reward: adUnitId => {
51
+    reward: (adUnitId) => {
16 52
       console.log('H5激励广告:', adUnitId)
17
-      return { show: () => Promise.resolve() }
53
+      return { 
54
+        show: () => {
55
+          console.log('展示激励广告')
56
+          return Promise.resolve()
57
+        },
58
+        onClose: (callback) => {
59
+          // 模拟关闭事件
60
+          setTimeout(() => callback({ isEnded: true }), 3000)
61
+        }
62
+      }
63
+    },
64
+    feed: (adUnitId) => {
65
+      console.log('H5信息流广告:', adUnitId)
66
+      return {
67
+        show: () => console.log('展示信息流广告'),
68
+        destroy: () => {}
69
+      }
18 70
     },
19
-    feed: adUnitId => {
20
-      return { show: () => console.log('H5信息流广告展示') }
71
+    interstitial: (adUnitId) => {
72
+      console.log('H5插屏广告:', adUnitId)
73
+      return {
74
+        show: () => console.log('展示插屏广告')
75
+      }
21 76
     }
22 77
   }
23 78
 }
@@ -26,13 +81,14 @@ const AD_PLATFORMS = {
26 81
 export function useAdManager() {
27 82
   const userStore = useUserStore()
28 83
   const chapterCount = ref(0)
84
+  const lastAdShownTime = ref(0)
29 85
   
30 86
   // 获取当前平台适配器
31 87
   const getPlatformAdapter = () => {
32 88
     // #ifdef MP-WEIXIN
33 89
     return AD_PLATFORMS.WECHAT
34 90
     // #endif
35
-    // #ifdef MP-DOUYIN
91
+    // #ifdef MP-TOUTIAO
36 92
     return AD_PLATFORMS.DOUYIN
37 93
     // #endif
38 94
     return AD_PLATFORMS.H5
@@ -42,30 +98,107 @@ export function useAdManager() {
42 98
   const showRewardAd = async (chapterId) => {
43 99
     if (userStore.isVIP) return
44 100
     
101
+    // 广告冷却时间(至少30秒)
102
+    const now = Date.now()
103
+    if (now - lastAdShownTime.value < 30000) return
104
+    
45 105
     chapterCount.value++
46 106
     if (chapterCount.value % 5 !== 0) return
47 107
     
108
+    const platform = getCurrentPlatform()
48 109
     const adapter = getPlatformAdapter()
49
-    const ad = adapter.reward('your_ad_unit_id')
110
+    const adUnitId = getAdUnitId(platform, 'reward')
111
+    
112
+    if (!adUnitId) {
113
+      console.error('未配置广告单元ID')
114
+      return
115
+    }
116
+    
117
+    const ad = adapter.reward(adUnitId)
50 118
     
51 119
     try {
52
-      await ad.show()
53
-      logAdView(chapterId, 'reward')
120
+      // 监听广告关闭事件
121
+      return new Promise((resolve) => {
122
+        ad.onClose(res => {
123
+          if (res && res.isEnded) {
124
+            // 完整观看,记录广告
125
+            logAdView(chapterId, 'reward', platform)
126
+            resolve(true)
127
+          } else {
128
+            resolve(false)
129
+          }
130
+        })
131
+        
132
+        // 展示广告
133
+        ad.show().catch(err => {
134
+          console.error('广告展示失败:', err)
135
+          resolve(false)
136
+        })
137
+        
138
+        lastAdShownTime.value = Date.now()
139
+      })
54 140
     } catch (err) {
55 141
       console.error('广告展示失败:', err)
142
+      return false
56 143
     }
57 144
   }
58 145
 
59 146
   // 展示底部信息流广告
60
-  const showFeedAd = () => {
147
+  const showFeedAd = (adUnitId) => {
61 148
     if (userStore.isVIP) return null
62 149
     
150
+    const platform = getCurrentPlatform()
63 151
     const adapter = getPlatformAdapter()
64
-    return adapter.feed('your_feed_ad_id')
152
+    
153
+    if (!adUnitId) {
154
+      adUnitId = getAdUnitId(platform, 'feed')
155
+    }
156
+    
157
+    if (!adUnitId) {
158
+      console.error('未配置信息流广告单元ID')
159
+      return null
160
+    }
161
+    
162
+    const ad = adapter.feed(adUnitId)
163
+    try {
164
+      ad.show()
165
+      return ad
166
+    } catch (err) {
167
+      console.error('信息流广告展示失败:', err)
168
+      return null
169
+    }
65 170
   }
66 171
 
67
-  // 广告日志记录(对接Java接口)
68
-  const logAdView = (chapterId, adType) => {
172
+  // 展示插屏广告
173
+  const showInterstitialAd = () => {
174
+    if (userStore.isVIP) return
175
+    
176
+    // 广告冷却时间(至少60秒)
177
+    const now = Date.now()
178
+    if (now - lastAdShownTime.value < 60000) return
179
+    
180
+    const platform = getCurrentPlatform()
181
+    const adapter = getPlatformAdapter()
182
+    const adUnitId = getAdUnitId(platform, 'interstitial')
183
+    
184
+    if (!adUnitId) {
185
+      console.error('未配置插屏广告单元ID')
186
+      return
187
+    }
188
+    
189
+    const ad = adapter.interstitial(adUnitId)
190
+    try {
191
+      ad.show()
192
+      lastAdShownTime.value = Date.now()
193
+      return true
194
+    } catch (err) {
195
+      console.error('插屏广告展示失败:', err)
196
+      return false
197
+    }
198
+  }
199
+
200
+  // 广告日志记录
201
+  const logAdView = (chapterId, adType, platform) => {
69 202
     uni.request({
70 203
       url: 'https://api.aiyadianzi.ltd/ad/log',
71 204
       method: 'POST',
@@ -73,10 +206,51 @@ export function useAdManager() {
73 206
         userId: userStore.userId,
74 207
         chapterId,
75 208
         adType,
76
-        platform: process.env.VUE_APP_PLATFORM
209
+        platform
210
+      },
211
+      header: {
212
+        'Authorization': `Bearer ${uni.getStorageSync('token')}`
77 213
       }
78 214
     })
79 215
   }
216
+  
217
+  // 获取当前平台
218
+  const getCurrentPlatform = () => {
219
+    // #ifdef MP-WEIXIN
220
+    return 'wechat'
221
+    // #endif
222
+    // #ifdef MP-TOUTIAO
223
+    return 'douyin'
224
+    // #endif
225
+    return 'h5'
226
+  }
227
+  
228
+  // 获取广告单元ID
229
+  const getAdUnitId = (platform, adType) => {
230
+    const config = {
231
+      wechat: {
232
+        reward: 'wechat_reward_ad_123456',
233
+        feed: 'wechat_feed_ad_654321',
234
+        interstitial: 'wechat_interstitial_ad_789012'
235
+      },
236
+      douyin: {
237
+        reward: 'douyin_reward_ad_abcdef',
238
+        feed: 'douyin_feed_ad_fedcba',
239
+        interstitial: 'douyin_interstitial_ad_123abc'
240
+      },
241
+      h5: {
242
+        reward: 'h5_reward_ad_123',
243
+        feed: 'h5_feed_ad_456',
244
+        interstitial: 'h5_interstitial_ad_789'
245
+      }
246
+    }
247
+    
248
+    return config[platform]?.[adType]
249
+  }
80 250
 
81
-  return { showRewardAd, showFeedAd }
251
+  return { 
252
+    showRewardAd, 
253
+    showFeedAd,
254
+    showInterstitialAd
255
+  }
82 256
 }

+ 92
- 0
RuoYi-App/utils/contentUtils.js Целия файл

@@ -0,0 +1,92 @@
1
+/**
2
+ * 清洗小说内容,移除原始网站信息
3
+ * @param {string} content - 原始内容
4
+ * @returns {string} 清洗后的内容
5
+ */
6
+export function cleanNovelContent(content) {
7
+  if (!content) return ''
8
+  
9
+  // 移除特定网站信息
10
+  const cleanContent = content
11
+    .replace(/最新网址[::]?\s*[a-z0-9.-]+/gi, '')
12
+    .replace(/www\.[a-z0-9]+\.[a-z]{2,}/gi, '')
13
+    .replace(/请收藏本站:https:\/\/www\.\w+\.\w+/g, '')
14
+    .replace(/&nbsp;|&#160;/g, ' ') // 替换空格实体
15
+    .replace(/<br\s*\/?>/g, '\n') // 替换换行标签
16
+    .replace(/<[^>]+>/g, '') // 移除所有HTML标签
17
+  
18
+  // 移除多余空行
19
+  return cleanContent
20
+    .split('\n')
21
+    .map(line => line.trim())
22
+    .filter(line => line.length > 0)
23
+    .join('\n\n')
24
+}
25
+
26
+/**
27
+ * 分页处理小说内容
28
+ * @param {string} content - 清洗后的内容
29
+ * @param {number} pageSize - 每页字符数
30
+ * @returns {string[]} 分页后的内容数组
31
+ */
32
+export function paginateContent(content, pageSize = 800) {
33
+  const pages = []
34
+  let currentPage = ''
35
+  let currentLength = 0
36
+  const paragraphs = content.split('\n\n')
37
+  
38
+  for (const paragraph of paragraphs) {
39
+    // 如果当前页加上新段落不会超长
40
+    if (currentLength + paragraph.length <= pageSize) {
41
+      currentPage += (currentPage ? '\n\n' : '') + paragraph
42
+      currentLength += paragraph.length
43
+    } 
44
+    // 如果段落本身超过一页
45
+    else if (paragraph.length > pageSize) {
46
+      // 先保存当前页
47
+      if (currentPage) {
48
+        pages.push(currentPage)
49
+        currentPage = ''
50
+        currentLength = 0
51
+      }
52
+      
53
+      // 将长段落分割成多页
54
+      let start = 0
55
+      while (start < paragraph.length) {
56
+        const end = start + pageSize
57
+        let pageContent = paragraph.substring(start, end)
58
+        
59
+        // 尽量在句号处分页
60
+        const lastPunctuation = Math.max(
61
+          pageContent.lastIndexOf('。'),
62
+          pageContent.lastIndexOf('!'),
63
+          pageContent.lastIndexOf('?'),
64
+          pageContent.lastIndexOf('.'),
65
+          pageContent.lastIndexOf('!'),
66
+          pageContent.lastIndexOf('?')
67
+        )
68
+        
69
+        if (lastPunctuation > -1 && lastPunctuation > start + pageSize * 0.8) {
70
+          pageContent = pageContent.substring(0, lastPunctuation + 1)
71
+          start = start + lastPunctuation + 1
72
+        } else {
73
+          start = end
74
+        }
75
+        
76
+        pages.push(pageContent)
77
+      }
78
+    }
79
+    // 如果段落会导致当前页超长
80
+    else {
81
+      pages.push(currentPage)
82
+      currentPage = paragraph
83
+      currentLength = paragraph.length
84
+    }
85
+  }
86
+  
87
+  if (currentPage) {
88
+    pages.push(currentPage)
89
+  }
90
+  
91
+  return pages
92
+}

+ 0
- 0
RuoYi-App/utils/signUtils.js Целия файл


Loading…
Отказ
Запис