DESKTOP-8FESTBI\Administrator 7 kuukautta sitten
vanhempi
commit
c40e20b55d

+ 19
- 1
RuoYi-App/App.vue Näytä tiedosto

@@ -49,6 +49,14 @@ export default {
49 49
     }
50 50
   },
51 51
   mounted() {
52
+	    console.log('UniApp SDK版本:', uni.getSystemInfoSync().SDKVersion);
53
+	    console.log('平台:', uni.getSystemInfoSync().platform);
54
+	    console.log('UniApp API可用性检查:');
55
+	    console.log('navigateTo:', typeof uni.navigateTo);
56
+	    console.log('redirectTo:', typeof uni.redirectTo);
57
+	    console.log('switchTab:', typeof uni.switchTab);
58
+		  // 检查页面路径是否正确
59
+		  this.checkPagePaths();
52 60
     this.isMounted = true;
53 61
     
54 62
     // 详细的环境检查
@@ -94,10 +102,20 @@ export default {
94 102
     }
95 103
   },
96 104
   methods: {
105
+	    checkPagePaths() {
106
+	      // 获取所有已注册的页面路径
107
+	      const pages = getCurrentPages();
108
+	      console.log('当前页面栈:', pages);
109
+	      
110
+	      // 检查reader页面是否存在
111
+	      const readerPageExists = pages.some(page => 
112
+	        page.route.includes('novel/reader')
113
+	      );
114
+	      console.log('Reader页面是否存在:', readerPageExists);
115
+	    },
97 116
     // 环境检查方法
98 117
     checkEnvironment() {
99 118
       console.log('开始环境检查...');
100
-      
101 119
       // 检查uni对象
102 120
       if (typeof uni === 'undefined') {
103 121
         console.error('uni对象未定义,uni-app环境可能未正确初始化');

+ 2
- 1
RuoYi-App/package.json Näytä tiedosto

@@ -3,6 +3,7 @@
3 3
     "vue": "^2.6.14",
4 4
     "@vue/composition-api": "^1.7.2",
5 5
     "uview-ui": "^1.8.4",
6
-    "uni-app": "^2.0.0"
6
+    "uni-app": "^2.0.0",
7
+	"@dcloudio/uni-mp-vue": "^2.0.0"
7 8
   }
8 9
 }

+ 13
- 3
RuoYi-App/pages/index/index.vue Näytä tiedosto

@@ -286,9 +286,19 @@ export default {
286 286
   },
287 287
 
288 288
   onUnload() {
289
-    // 保存阅读进度
290
-    uni.setStorageSync('readingProgress', this.currentChapter)
291
-    console.log('首页已加载')
289
+  // 保存阅读进度
290
+  uni.setStorage({
291
+    key: `readingProgress_${this.novelId}`,
292
+    data: {
293
+      chapterId: this.chapterId,
294
+      progress: this.progress,
295
+      lastReadTime: new Date().getTime()
296
+    }
297
+  });
298
+  
299
+  // 更新已读章节数
300
+  const readChapters = uni.getStorageSync('readChapters') || 0;
301
+  uni.setStorageSync('readChapters', readChapters + 1);
292 302
   },
293 303
   unlockChapter() {
294 304
     if (this.currentChapter > this.maxFreeChapters) {

+ 159
- 28
RuoYi-App/pages/novel/list.vue Näytä tiedosto

@@ -165,12 +165,84 @@ export default {
165 165
         this.loading = false;
166 166
       }
167 167
     },
168
+	
168 169
 // 在 list.vue 的 methods 中修改 openNovel 方法
170
+// 在 list.vue 的 methods 中添加
171
+simpleNavigate(url) {
172
+  try {
173
+    // 方法1: 直接使用最基础的导航方式
174
+    if (typeof uni !== 'undefined' && uni.navigateTo) {
175
+      // 创建一个新的URL对象,确保格式正确
176
+      const cleanUrl = url.split('?')[0];
177
+      const params = new URLSearchParams(url.split('?')[1] || '');
178
+      
179
+      uni.navigateTo({
180
+        url: `${cleanUrl}?${params.toString()}`,
181
+        fail: (err) => {
182
+          console.error('基础navigateTo失败:', err);
183
+          // 方法2: 使用页面重定向
184
+          this.simpleRedirect(url);
185
+        }
186
+      });
187
+    } else {
188
+      this.simpleRedirect(url);
189
+    }
190
+  } catch (error) {
191
+    console.error('simpleNavigate异常:', error);
192
+    this.simpleRedirect(url);
193
+  }
194
+},
195
+
196
+simpleRedirect(url) {
197
+  try {
198
+    if (typeof uni !== 'undefined' && uni.redirectTo) {
199
+      uni.redirectTo({
200
+        url: url,
201
+        fail: (err) => {
202
+          console.error('redirectTo失败:', err);
203
+          // 方法3: 使用Vue Router
204
+          this.useVueRouterFallback(url);
205
+        }
206
+      });
207
+    } else {
208
+      this.useVueRouterFallback(url);
209
+    }
210
+  } catch (error) {
211
+    console.error('simpleRedirect异常:', error);
212
+    this.useVueRouterFallback(url);
213
+  }
214
+},
215
+
216
+useVueRouterFallback(url) {
217
+  try {
218
+    // 解析URL路径和参数
219
+    const [path, queryString] = url.split('?');
220
+    const params = new URLSearchParams(queryString || '');
221
+    const query = {};
222
+    
223
+    for (const [key, value] of params.entries()) {
224
+      query[key] = value;
225
+    }
226
+    
227
+    // 使用Vue Router
228
+    if (this.$router && typeof this.$router.push === 'function') {
229
+      this.$router.push({ path, query });
230
+    } else {
231
+      // 最终方案: 使用原生跳转
232
+      window.location.href = url;
233
+    }
234
+  } catch (error) {
235
+    console.error('useVueRouterFallback异常:', error);
236
+    // 最终降级方案
237
+    window.location.href = url;
238
+  }
239
+},
240
+// 在 list.vue 中修改 openNovel 方法
169 241
 openNovel(novel) {
170 242
   try {
171 243
     console.log('准备打开小说:', novel);
172 244
     
173
-    // 检查novel对象是否存在
245
+    // 确保 novel 对象存在且有有效 ID
174 246
     if (!novel || typeof novel !== 'object') {
175 247
       console.error('novel对象无效:', novel);
176 248
       uni.showToast({
@@ -180,7 +252,7 @@ openNovel(novel) {
180 252
       return;
181 253
     }
182 254
     
183
-    // 获取小说ID
255
+    // 获取小说ID - 使用若依框架的标准方式
184 256
     const novelId = novel.id || novel.novelId || novel.bookId;
185 257
     if (!novelId) {
186 258
       console.error('无法获取小说ID:', novel);
@@ -191,33 +263,13 @@ openNovel(novel) {
191 263
       return;
192 264
     }
193 265
     
194
-    // 确保ID是数字
195
-    const id = Number(novelId);
196
-    if (isNaN(id)) {
197
-      console.error('小说ID不是有效数字:', novelId);
198
-      uni.showToast({
199
-        title: '小说ID格式错误',
200
-        icon: 'none'
201
-      });
202
-      return;
203
-    }
204
-    
205
-    console.log('跳转到阅读页面,小说ID:', id);
206
-    
207
-    // 直接使用uni.navigateTo进行跳转
208
-    uni.navigateTo({
209
-      url: `/pages/novel/reader?novelId=${id}`,
210
-      success: (res) => {
211
-        console.log('跳转成功', res);
212
-      },
213
-      fail: (err) => {
214
-        console.error('跳转失败', err);
215
-        uni.showToast({
216
-          title: '跳转失败,请重试',
217
-          icon: 'none'
218
-        });
219
-      }
266
+    console.log('跳转到阅读页面,小说ID:', novelId, '章节ID:', 1);
267
+    // 使用若依框架的标准导航方式
268
+    this.$router.push({
269
+      path: '/pages/novel/reader',
270
+      query: { novelId: novelId ,chapterId : 1}
220 271
     });
272
+    
221 273
   } catch (error) {
222 274
     console.error('openNovel方法执行异常:', error);
223 275
     uni.showToast({
@@ -227,6 +279,85 @@ openNovel(novel) {
227 279
   }
228 280
 },
229 281
 
282
+// 添加安全导航方法
283
+safeNavigateTo(url) {
284
+  try {
285
+    // 方法1: 直接使用 uni.navigateTo
286
+    if (typeof uni.navigateTo === 'function') {
287
+      uni.navigateTo({
288
+        url: url,
289
+        success: (res) => {
290
+          console.log('navigateTo跳转成功', res);
291
+        },
292
+        fail: (err) => {
293
+          console.error('navigateTo跳转失败', err);
294
+          // 方法2: 使用重定向作为备选
295
+          this.fallbackRedirect(url);
296
+        }
297
+      });
298
+    } else {
299
+      // 方法3: 使用备用导航
300
+      this.fallbackRedirect(url);
301
+    }
302
+  } catch (error) {
303
+    console.error('safeNavigateTo方法异常:', error);
304
+    this.fallbackRedirect(url);
305
+  }
306
+},
307
+
308
+// 备用重定向方法
309
+fallbackRedirect(url) {
310
+  try {
311
+    if (typeof uni.redirectTo === 'function') {
312
+      uni.redirectTo({
313
+        url: url,
314
+        success: (res) => {
315
+          console.log('redirectTo跳转成功', res);
316
+        },
317
+        fail: (err) => {
318
+          console.error('redirectTo也失败', err);
319
+          // 方法4: 使用Vue Router
320
+          this.useVueRouter(url);
321
+        }
322
+      });
323
+    } else {
324
+      this.useVueRouter(url);
325
+    }
326
+  } catch (error) {
327
+    console.error('fallbackRedirect方法异常:', error);
328
+    this.useVueRouter(url);
329
+  }
330
+},
331
+
332
+// 使用Vue Router进行导航
333
+useVueRouter(url) {
334
+  try {
335
+    // 解析URL
336
+    const path = url.split('?')[0];
337
+    const queryString = url.split('?')[1] || '';
338
+    const params = new URLSearchParams(queryString);
339
+    const query = {};
340
+    
341
+    for (const [key, value] of params.entries()) {
342
+      query[key] = value;
343
+    }
344
+    
345
+    if (this.$router && typeof this.$router.push === 'function') {
346
+      this.$router.push({
347
+        path: path,
348
+        query: query
349
+      });
350
+    } else {
351
+      // 最终降级方案
352
+      window.location.href = url;
353
+    }
354
+  } catch (error) {
355
+    console.error('useVueRouter方法异常:', error);
356
+    // 最终降级方案
357
+    window.location.href = url;
358
+  }
359
+},
360
+
230 361
 // 备用导航方法
231 362
 fallbackNavigate(novelId) {
232 363
   try {

+ 235
- 473
RuoYi-App/pages/novel/reader.vue Näytä tiedosto

@@ -1,127 +1,67 @@
1 1
 <template>
2
-  <view class="reader-container" :style="readerStyles">
3
-    <!-- 顶部导航 -->
4
-    <view class="reader-header">
5
-      <uni-icons type="arrowleft" size="28" color="#fff" @click="goBack"></uni-icons>
6
-      <text class="novel-title">{{ novelTitle }}</text>
7
-      <view class="header-actions">
8
-        <uni-icons type="more" size="28" color="#fff" @click="showMoreActions"></uni-icons>
2
+  <view class="reader-container">
3
+    <!-- 顶部导航栏 -->
4
+    <view class="reader-header" v-if="showHeader">
5
+      <view class="header-left">
6
+        <text class="iconfont icon-back" @click="goBack"></text>
7
+        <text class="novel-title">{{novelTitle}}</text>
8
+      </view>
9
+      <view class="header-right">
10
+        <text class="iconfont icon-menu" @click="showMenu"></text>
9 11
       </view>
10 12
     </view>
11
-    
12
-    <!-- 阅读区域 -->
13
-    <scroll-view scroll-y class="reader-content" :scroll-top="scrollTop" @scroll="onScroll">
14
-      <view class="chapter-title">{{ chapterDetail.title }}</view>
15
-      <view class="content-text">{{ formattedContent }}</view>
13
+
14
+    <!-- 阅读内容区域 -->
15
+    <scroll-view 
16
+      class="reader-content" 
17
+      scroll-y 
18
+      :scroll-top="scrollTop"
19
+      @scroll="onScroll"
20
+      @touchstart="onTouchStart"
21
+      @touchend="onTouchEnd"
22
+    >
23
+      <view class="chapter-title">{{chapterTitle}}</view>
24
+      <view class="content-text">
25
+        <text>{{decodedContent}}</text>
26
+      </view>
27
+      
28
+      <!-- 加载状态 -->
29
+      <view v-if="loading" class="loading-container">
30
+        <text>加载中...</text>
31
+      </view>
16 32
       
17
-      <!-- 章节结束提示 -->
18
-      <view v-if="isChapterEnd" class="chapter-end">
19
-        <text>本章结束</text>
20
-        <button v-if="!isLastChapter" class="next-chapter-btn" @click="nextChapter">下一章</button>
21
-        <text v-else>已是最新章节</text>
33
+      <!-- 错误状态 -->
34
+      <view v-if="error" class="error-container">
35
+        <text>{{error}}</text>
36
+        <button @click="loadChapterContent">重试</button>
22 37
       </view>
23 38
     </scroll-view>
24
-    
25
-    <!-- 底部操作栏 -->
26
-    <view class="reader-footer">
27
-      <view class="progress">
28
-        <text>{{ currentChapterIndex + 1 }}/{{ chapters.length }}</text>
29
-        <text>{{ readingProgress }}%</text>
39
+
40
+    <!-- 底部控制栏 -->
41
+    <view class="reader-footer" v-if="showFooter">
42
+      <view class="progress-text">
43
+        第{{currentChapter}}章 {{progress}}%
30 44
       </view>
31
-      <view class="actions">
32
-        <button class="action-btn" @click="prevChapter" :disabled="isFirstChapter">
33
-          <uni-icons type="arrow-up" size="24" color="#2a5caa"></uni-icons>
34
-          <text>上一章</text>
35
-        </button>
36
-        <button class="action-btn" @click="toggleMenu">
37
-          <uni-icons type="list" size="24" color="#2a5caa"></uni-icons>
38
-          <text>目录</text>
39
-        </button>
40
-        <button class="action-btn" @click="toggleSettings">
41
-          <uni-icons type="gear" size="24" color="#2a5caa"></uni-icons>
42
-          <text>设置</text>
43
-        </button>
44
-        <button class="action-btn" @click="nextChapter" :disabled="isLastChapter">
45
-          <uni-icons type="arrow-down" size="24" color="#2a5caa"></uni-icons>
46
-          <text>下一章</text>
47
-        </button>
45
+      <view class="control-buttons">
46
+        <text class="iconfont icon-catalog" @click="showCatalog"></text>
47
+        <text class="iconfont icon-font" @click="showFontSettings"></text>
48
+        <text class="iconfont icon-night" @click="toggleNightMode"></text>
49
+        <text class="iconfont icon-download" @click="downloadChapter"></text>
48 50
       </view>
49 51
     </view>
50
-    
51
-    <!-- 目录抽屉 -->
52
-    <uni-drawer ref="drawer" mode="right" :width="300">
53
-      <view class="drawer-content">
54
-        <text class="drawer-title">目录</text>
55
-        <view class="chapter-stats">
56
-          <text>共{{ chapters.length }}章</text>
57
-          <text>已读{{ readChaptersCount }}章</text>
58
-        </view>
59
-        <scroll-view scroll-y class="chapter-list">
60
-          <view 
61
-            v-for="(chapter, index) in chapters" 
62
-            :key="chapter.id" 
63
-            class="chapter-item"
64
-            :class="{ 
65
-              active: chapter.id === chapterDetail.id,
66
-              read: isChapterRead(chapter.id),
67
-              locked: isChapterLocked(index)
68
-            }"
69
-            @click="selectChapter(chapter, index)"
70
-          >
71
-            <text>{{ index + 1 }}. {{ chapter.title }}</text>
72
-            <uni-icons v-if="isChapterLocked(index)" type="locked" size="16" color="#999"></uni-icons>
73
-          </view>
74
-        </scroll-view>
75
-      </view>
76
-    </uni-drawer>
77
-    
52
+
53
+    <!-- 广告位 -->
54
+    <ad v-if="showAd" unit-id="您的广告单元ID" class="reader-ad"></ad>
55
+
78 56
     <!-- 设置面板 -->
79
-    <uni-popup ref="settingsPopup" type="bottom">
80
-      <view class="settings-panel">
81
-        <text class="panel-title">阅读设置</text>
82
-        <view class="setting-item">
83
-          <text>字体大小</text>
84
-          <view class="font-size-controls">
85
-            <button class="size-btn" @click="decreaseFontSize">A-</button>
86
-            <text class="size-value">{{ fontSize }}px</text>
87
-            <button class="size-btn" @click="increaseFontSize">A+</button>
88
-          </view>
89
-        </view>
90
-        <view class="setting-item">
91
-          <text>背景颜色</text>
92
-          <view class="color-options">
93
-            <view 
94
-              v-for="color in backgroundColors" 
95
-              :key="color.value"
96
-              class="color-option"
97
-              :class="{ active: readerStyles.backgroundColor === color.value }"
98
-              :style="{ backgroundColor: color.value }"
99
-              @click="changeBackgroundColor(color.value)"
100
-            >
101
-              <uni-icons v-if="readerStyles.backgroundColor === color.value" type="checkmark" size="16" color="#fff"></uni-icons>
102
-            </view>
103
-          </view>
104
-        </view>
105
-        <view class="setting-item">
106
-          <text>亮度调节</text>
107
-          <slider value="50" min="0" max="100" @change="adjustBrightness" />
108
-        </view>
109
-        <button class="close-btn" @click="$refs.settingsPopup.close()">关闭设置</button>
110
-      </view>
111
-    </uni-popup>
112
-    
113
-    <!-- 登录提示弹窗 -->
114
-    <uni-popup ref="loginPopup" type="dialog">
115
-      <uni-popup-dialog
116
-        type="info"
117
-        title="登录提示"
118
-        content="您已阅读完免费章节,登录后可继续阅读"
119
-        :duration="2000"
120
-        :before-close="true"
121
-        @close="closeLoginDialog"
122
-        @confirm="goToLogin"
123
-      ></uni-popup-dialog>
124
-    </uni-popup>
57
+    <view class="settings-panel" v-if="showSettings">
58
+      <!-- 字体大小、背景颜色等设置 -->
59
+    </view>
60
+
61
+    <!-- 目录面板 -->
62
+    <view class="catalog-panel" v-if="showCatalogPanel">
63
+      <!-- 章节列表 -->
64
+    </view>
125 65
   </view>
126 66
 </template>
127 67
 
@@ -129,446 +69,268 @@
129 69
 export default {
130 70
   data() {
131 71
     return {
132
-      novelId: null,
133
-      chapterId: null,
134
-      novelTitle: '',
135
-      chapterDetail: {},
136
-      chapterContent: '',
137
-      chapters: [],
138
-      currentChapterIndex: 0,
139
-      // 阅读统计
140
-      readChapters: [],
141
-      readingProgress: 0,
72
+      novelId: '',
73
+      chapterId: '',
74
+      novelTitle: '加载中...',
75
+      chapterTitle: '加载中...',
76
+      content: '', // Base64编码的内容
77
+      decodedContent: '正在加载内容...', // 解码后的内容
78
+      currentChapter: 1,
79
+      totalChapters: 0,
80
+      progress: 0,
142 81
       scrollTop: 0,
143
-      isChapterEnd: false,
144
-      // 阅读设置
145
-      fontSize: 32,
146
-      backgroundColors: [
147
-        { name: '护眼模式', value: '#f8f2e0' },
148
-        { name: '白色', value: '#ffffff' },
149
-        { name: '夜间', value: '#2c2c2c' },
150
-        { name: '淡蓝', value: '#e8f4f8' }
151
-      ],
152
-      readerStyles: {
153
-        fontSize: '32rpx',
154
-        lineHeight: '1.8',
155
-        backgroundColor: '#f8f2e0',
156
-        color: '#333'
157
-      }
82
+      showHeader: false,
83
+      showFooter: false,
84
+      showSettings: false,
85
+      showCatalogPanel: false,
86
+      showAd: false, // 默认不显示广告
87
+      nightMode: false,
88
+      touchStartX: 0,
89
+      touchStartY: 0,
90
+      lastScrollPosition: 0,
91
+      loading: false,
92
+      error: null
158 93
     }
159 94
   },
160
-  computed: {
161
-    // 计算属性
162
-    isFirstChapter() {
163
-      return this.currentChapterIndex === 0
164
-    },
165
-    isLastChapter() {
166
-      return this.currentChapterIndex === this.chapters.length - 1
167
-    },
168
-    readChaptersCount() {
169
-      return this.readChapters.length
170
-    },
171
-    // 检查是否达到免费章节限制
172
-    reachedFreeLimit() {
173
-      const isLoggedIn = this.$store.getters.token
174
-      return !isLoggedIn && this.readChaptersCount >= 5 // 默认5章免费
175
-    },
176
-    // 格式化内容
177
-    formattedContent() {
178
-      if (!this.chapterContent) return ''
179
-      
180
-      // 处理HTML标签和特殊字符
181
-      return this.chapterContent
182
-        .replace(/<br\s*\/?>/gi, '\n')
183
-        .replace(/&nbsp;/g, ' ')
184
-        .replace(/<p>/g, '\n\n')
185
-        .replace(/<\/p>/g, '')
186
-        .replace(/<[^>]+>/g, '')
187
-        .replace(/\n{3,}/g, '\n\n')
188
-        .trim()
189
-    }
190
-  },
191
-  async onLoad(options) {
192
-    console.log('阅读器页面参数:', options)
95
+  onLoad(options) {
96
+    console.log('阅读器页面加载,参数:', options);
193 97
     
194
-    // 确保参数正确解析
195
-    this.novelId = options.novelId;
196
-    this.chapterId = options.chapterId || null
98
+    // 确保参数正确获取
99
+    this.novelId = options.novelId || '';
100
+    this.chapterId = options.chapterId || '1';
197 101
     
198 102
     if (!this.novelId) {
199
-      uni.showToast({
200
-        title: '小说ID参数缺失',
201
-        icon: 'none'
202
-      })
203
-      setTimeout(() => {
204
-        uni.navigateBack()
205
-      }, 1500)
206
-      return
103
+      this.error = '小说ID参数缺失';
104
+      return;
207 105
     }
208 106
     
209
-    // 加载阅读进度
210
-    this.loadReadingProgress()
107
+    this.loadChapterContent();
108
+  },
109
+  methods: {
110
+    // 添加onScroll方法
111
+    onScroll(e) {
112
+      this.lastScrollPosition = e.detail.scrollTop;
113
+      // 计算阅读进度
114
+      this.calculateProgress();
115
+    },
116
+    
117
+    onTouchStart(e) {
118
+      this.touchStartX = e.touches[0].clientX;
119
+      this.touchStartY = e.touches[0].clientY;
120
+    },
211 121
     
212
-    try {
213
-      await this.loadNovelData()
122
+    onTouchEnd(e) {
123
+      const endX = e.changedTouches[0].clientX;
124
+      const endY = e.changedTouches[0].clientY;
125
+      const diffX = endX - this.touchStartX;
126
+      const diffY = endY - this.touchStartY;
214 127
       
215
-      // 如果有指定章节ID,加载该章节,否则加载第一章
216
-      if (this.chapterId) {
217
-        await this.loadChapter(this.chapterId)
218
-      } else {
219
-        // 尝试从阅读记录中恢复
220
-        const lastReadChapter = this.getLastReadChapter()
221
-        if (lastReadChapter) {
222
-          await this.loadChapter(lastReadChapter.id)
128
+      // 横向滑动大于纵向滑动,且滑动距离大于50px
129
+      if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) {
130
+        if (diffX > 0) {
131
+          this.prevChapter(); // 右滑,上一章
223 132
         } else {
224
-          await this.loadFirstChapter()
133
+          this.nextChapter(); // 左滑,下一章
225 134
         }
226 135
       }
227
-    } catch (error) {
228
-      console.error('页面初始化失败:', error)
229
-      uni.showToast({
230
-        title: '页面初始化失败',
231
-        icon: 'none'
232
-      })
233
-    }
234
-  },
235
-  methods: {
236
-    // 加载小说数据
237
-    async loadNovelData() {
136
+    },
137
+    
138
+    // Base64解码
139
+    decodeBase64(content) {
140
+      if (typeof content !== 'string') {
141
+        console.error('Base64解码失败:内容不是字符串');
142
+        return '内容格式错误';
143
+      }
144
+      
238 145
       try {
239
-        uni.showLoading({ title: '加载中...' })
240
-        
241
-        // 加载小说基本信息
242
-        const novelRes = await this.$http.get(`/novel/detail/${this.novelId}`)
146
+        // 处理可能的URL安全Base64编码
147
+        let base64 = content.replace(/-/g, '+').replace(/_/g, '/');
243 148
         
244
-        // 使用统一的响应解析方法
245
-        const novelData = this.parseResponse(novelRes)
246
-        console.log('小说详情响应:', novelData)
247
-        
248
-        if (novelData.code === 200 && novelData.data) {
249
-          this.novelTitle = novelData.data.title
250
-          uni.setNavigationBarTitle({ title: this.novelTitle })
251
-        }
252
-        
253
-        // 加载章节列表
254
-        const chapterRes = await this.$http.get(`/chapter/list/${this.novelId}`)
255
-        
256
-        // 使用统一的响应解析方法
257
-        const chapterData = this.parseResponse(chapterRes)
258
-        console.log('章节列表响应:', chapterData)
259
-        
260
-        if (chapterData.code === 200) {
261
-          // 处理不同的响应格式
262
-          if (Array.isArray(chapterData.rows)) {
263
-            this.chapters = chapterData.rows
264
-          } else if (Array.isArray(chapterData.data)) {
265
-            this.chapters = chapterData.data
266
-          }
267
-          
268
-          // 初始化当前章节索引
269
-          if (this.chapterId) {
270
-            this.currentChapterIndex = this.chapters.findIndex(ch => ch.id === this.chapterId)
149
+        // 添加必要的填充字符
150
+        const pad = base64.length % 4;
151
+        if (pad) {
152
+          if (pad === 1) {
153
+            throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding');
271 154
           }
155
+          base64 += new Array(5-pad).join('=');
272 156
         }
157
+        
158
+        // 解码
159
+        const decoded = uni.base64ToArrayBuffer(base64);
160
+        const textDecoder = new TextDecoder('utf-8');
161
+        return textDecoder.decode(decoded);
273 162
       } catch (error) {
274
-        console.error('加载小说数据失败:', error)
275
-        uni.showToast({
276
-          title: '加载失败',
277
-          icon: 'none'
278
-        })
279
-      } finally {
280
-        uni.hideLoading()
163
+        console.error('Base64解码失败:', error);
164
+        return '内容解码失败,请稍后再试。';
281 165
       }
282 166
     },
283 167
     
284
-    // 加载章节内容
285
-    async loadChapter(chapterId) {
286
-      // 检查章节是否锁定
287
-      const chapterIndex = this.chapters.findIndex(ch => ch.id === chapterId)
288
-      if (this.isChapterLocked(chapterIndex)) {
289
-        this.showLoginPrompt()
290
-        return
291
-      }
168
+    async loadChapterContent() {
169
+      this.loading = true;
170
+      this.error = null;
292 171
       
293 172
       try {
294
-        uni.showLoading({ title: '加载中...' })
295
-        
296
-        const res = await this.$http.get(`/chapter/content/${chapterId}`)
173
+        console.log('开始请求章节内容,章节ID:', this.chapterId);
174
+        const res = await this.$http.get(`/novel/chapter/${this.chapterId}`);
175
+        console.log('章节内容响应:', res);
297 176
         
298
-        // 使用统一的响应解析方法
299
-        const responseData = this.parseResponse(res)
300
-        console.log('章节内容响应:', responseData)
301
-        
302
-        if (responseData.code === 200 && responseData.data) {
303
-          this.chapterDetail = responseData.data
304
-          this.chapterContent = responseData.data.content
305
-          this.chapterId = chapterId
306
-          this.currentChapterIndex = chapterIndex
307
-          
308
-          // 标记为已读
309
-          this.markChapterAsRead(chapterId)
177
+        if (res.code === 200) {
178
+          this.novelTitle = res.data.novelTitle || '未知小说';
179
+          this.chapterTitle = res.data.chapterTitle || '未知章节';
180
+          this.content = res.data.content || '';
181
+          this.decodedContent = this.decodeBase64(this.content);
310 182
           
311
-          // 计算阅读进度
312
-          this.calculateReadingProgress()
183
+          // 设置章节信息
184
+          this.currentChapter = res.data.chapterIndex || 1;
185
+          this.totalChapters = res.data.totalChapters || 0;
186
+        } else {
187
+          console.error('API返回错误状态码:', res.code);
188
+          this.error = '加载失败,请重试';
313 189
         }
314 190
       } catch (error) {
315
-        console.error('加载章节内容失败:', error)
316
-        uni.showToast({
317
-          title: '加载失败',
318
-          icon: 'none'
319
-        })
191
+        console.error('加载章节内容失败:', error);
192
+        this.error = '加载失败,请检查网络连接';
320 193
       } finally {
321
-        uni.hideLoading()
194
+        this.loading = false;
322 195
       }
323 196
     },
324 197
     
325
-    // 字体大小控制
326
-    increaseFontSize() {
327
-      if (this.fontSize < 40) {
328
-        this.fontSize += 2
329
-        this.readerStyles.fontSize = `${this.fontSize}rpx`
330
-        this.saveReadingProgress()
198
+    calculateProgress() {
199
+      // 根据滚动位置计算阅读进度
200
+      // 这里需要获取内容区域的实际高度,由于uni-app中获取节点高度较为复杂,暂时用模拟数据
201
+      // 实际项目中应该通过uni.createSelectorQuery()获取
202
+      this.progress = Math.min(100, Math.round((this.lastScrollPosition / 1000) * 100));
203
+    },
204
+    
205
+    goBack() {
206
+      uni.navigateBack();
207
+    },
208
+    
209
+    prevChapter() {
210
+      if (this.currentChapter > 1) {
211
+        this.chapterId = parseInt(this.chapterId) - 1;
212
+        this.loadChapterContent();
213
+      } else {
214
+        uni.showToast({ title: '已经是第一章了', icon: 'none' });
331 215
       }
332 216
     },
333 217
     
334
-    decreaseFontSize() {
335
-      if (this.fontSize > 24) {
336
-        this.fontSize -= 2
337
-        this.readerStyles.fontSize = `${this.fontSize}rpx`
338
-        this.saveReadingProgress()
218
+    nextChapter() {
219
+      if (this.currentChapter < this.totalChapters) {
220
+        this.chapterId = parseInt(this.chapterId) + 1;
221
+        this.loadChapterContent();
222
+      } else {
223
+        uni.showToast({ title: '已经是最后一章了', icon: 'none' });
339 224
       }
340 225
     },
341 226
     
342
-    // 亮度调节
343
-    adjustBrightness(e) {
344
-      // 在实际应用中,这里可以调用设备API调节屏幕亮度
345
-      console.log('调整亮度:', e.detail.value)
227
+    showMenu() {
228
+      // 显示/隐藏顶部和底部栏
229
+      this.showHeader = !this.showHeader;
230
+      this.showFooter = !this.showFooter;
231
+    },
232
+    
233
+    showCatalog() {
234
+      this.showCatalogPanel = true;
235
+    },
236
+    
237
+    showFontSettings() {
238
+      this.showSettings = true;
346 239
     },
347 240
     
348
-    // 其他方法保持不变...
241
+    toggleNightMode() {
242
+      this.nightMode = !this.nightMode;
243
+      // 切换夜间模式样式
244
+    },
245
+    
246
+    setAdStrategy() {
247
+      // 设置广告显示策略,如每阅读三章显示一次广告
248
+      const readChapters = uni.getStorageSync('readChapters') || 0;
249
+      this.showAd = readChapters > 0 && readChapters % 3 === 0;
250
+    },
251
+    
252
+    downloadChapter() {
253
+      // 下载本章内容供离线阅读
254
+    }
349 255
   }
350 256
 }
351 257
 </script>
352 258
 
353
-<style scoped>
259
+<style>
354 260
 .reader-container {
355 261
   position: relative;
356 262
   height: 100vh;
357
-  padding: 20rpx;
358
-  box-sizing: border-box;
359
-  transition: all 0.3s ease;
263
+  display: flex;
264
+  flex-direction: column;
265
+  background-color: #f5f0e1; /* 羊皮纸色背景 */
360 266
 }
361 267
 
362 268
 .reader-header {
363
-  position: absolute;
364
-  top: 0;
365
-  left: 0;
366
-  right: 0;
269
+  height: 44px;
270
+  padding: 0 15px;
367 271
   display: flex;
368 272
   justify-content: space-between;
369 273
   align-items: center;
370
-  padding: 20rpx 30rpx;
371
-  background: rgba(0, 0, 0, 0.7);
274
+  background: rgba(0, 0, 0, 0.8);
372 275
   color: white;
373
-  z-index: 100;
374
-}
375
-
376
-.novel-title {
377
-  font-size: 32rpx;
378
-  max-width: 60%;
379
-  overflow: hidden;
380
-  text-overflow: ellipsis;
381
-  white-space: nowrap;
382 276
 }
383 277
 
384 278
 .reader-content {
385
-  height: calc(100vh - 200rpx);
386
-  padding-top: 80rpx;
387
-  padding-bottom: 120rpx;
279
+  flex: 1;
280
+  padding: 20px;
281
+  box-sizing: border-box;
282
+  line-height: 1.8;
283
+  font-size: 18px;
284
+  color: #333;
388 285
 }
389 286
 
390 287
 .chapter-title {
391
-  font-size: 40rpx;
392
-  font-weight: bold;
393 288
   text-align: center;
394
-  margin-bottom: 40rpx;
395
-  color: #2a5caa;
289
+  font-size: 20px;
290
+  font-weight: bold;
291
+  margin-bottom: 20px;
396 292
 }
397 293
 
398 294
 .content-text {
399
-  font-size: 32rpx;
295
+  text-indent: 2em;
400 296
   line-height: 1.8;
401
-  white-space: pre-line;
402
-}
403
-
404
-.chapter-end {
405
-  text-align: center;
406
-  margin-top: 40rpx;
407
-  padding: 20rpx;
408
-  border-top: 1rpx solid #eee;
409
-}
410
-
411
-.next-chapter-btn {
412
-  background-color: #2a5caa;
413
-  color: white;
414
-  margin-top: 20rpx;
415
-  border-radius: 10rpx;
416 297
 }
417 298
 
418 299
 .reader-footer {
419
-  position: absolute;
420
-  bottom: 0;
421
-  left: 0;
422
-  right: 0;
423
-  background: rgba(255, 255, 255, 0.95);
424
-  padding: 20rpx;
425
-  border-top: 1rpx solid #eee;
426
-  box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
427
-}
428
-
429
-.progress {
430
-  display: flex;
431
-  justify-content: space-between;
432
-  font-size: 28rpx;
433
-  color: #666;
434
-  margin-bottom: 20rpx;
435
-}
436
-
437
-.actions {
438
-  display: flex;
439
-  justify-content: space-between;
440
-}
441
-
442
-.action-btn {
443
-  display: flex;
444
-  flex-direction: column;
445
-  align-items: center;
446
-  background: none;
447
-  border: none;
448
-  font-size: 24rpx;
449
-  padding: 10rpx;
450
-  color: #2a5caa;
451
-}
452
-
453
-.action-btn:disabled {
454
-  opacity: 0.5;
455
-}
456
-
457
-.drawer-content {
458
-  padding: 30rpx;
459
-}
460
-
461
-.drawer-title {
462
-  font-size: 36rpx;
463
-  font-weight: bold;
464
-  display: block;
465
-  margin-bottom: 20rpx;
466
-  border-bottom: 1rpx solid #eee;
467
-  padding-bottom: 20rpx;
468
-}
469
-
470
-.chapter-stats {
471
-  display: flex;
472
-  justify-content: space-between;
473
-  margin-bottom: 20rpx;
474
-  font-size: 24rpx;
475
-  color: #666;
476
-}
477
-
478
-.chapter-list {
479
-  height: calc(100vh - 200rpx);
480
-}
481
-
482
-.chapter-item {
300
+  height: 50px;
301
+  padding: 0 15px;
483 302
   display: flex;
484 303
   justify-content: space-between;
485 304
   align-items: center;
486
-  padding: 20rpx 10rpx;
487
-  border-bottom: 1rpx solid #f0f0f0;
488
-  font-size: 28rpx;
489
-}
490
-
491
-.chapter-item.active {
492
-  color: #2a5caa;
493
-  font-weight: bold;
494
-}
495
-
496
-.chapter-item.read {
497
-  color: #666;
498
-}
499
-
500
-.chapter-item.locked {
501
-  color: #999;
502
-}
503
-
504
-.settings-panel {
505
-  padding: 30rpx;
506
-  background: white;
507
-  border-radius: 20rpx 20rpx 0 0;
508
-}
509
-
510
-.panel-title {
511
-  font-size: 36rpx;
512
-  font-weight: bold;
513
-  display: block;
514
-  margin-bottom: 30rpx;
515
-  text-align: center;
516
-}
517
-
518
-.setting-item {
519
-  margin-bottom: 30rpx;
305
+  background: rgba(0, 0, 0, 0.8);
306
+  color: white;
520 307
 }
521 308
 
522
-.setting-item text {
523
-  display: block;
524
-  margin-bottom: 15rpx;
525
-  font-size: 28rpx;
526
-  font-weight: bold;
309
+.reader-ad {
310
+  height: 90px;
311
+  margin-bottom: 10px;
527 312
 }
528 313
 
529
-.font-size-controls {
314
+.loading-container, .error-container {
530 315
   display: flex;
316
+  flex-direction: column;
531 317
   align-items: center;
532
-  justify-content: space-between;
533
-}
534
-
535
-.size-btn {
536
-  width: 80rpx;
537
-  height: 60rpx;
538
-  background: #f0f0f0;
539
-  border: none;
540
-  border-radius: 10rpx;
541
-  font-size: 28rpx;
542
-}
543
-
544
-.size-value {
545
-  font-size: 28rpx;
546
-  font-weight: bold;
318
+  justify-content: center;
319
+  padding: 50px 0;
547 320
 }
548 321
 
549
-.color-options {
550
-  display: flex;
551
-  justify-content: space-between;
322
+.error-container button {
323
+  margin-top: 20px;
552 324
 }
553 325
 
554
-.color-option {
555
-  width: 60rpx;
556
-  height: 60rpx;
557
-  border-radius: 50%;
558
-  border: 2rpx solid #eee;
559
-  display: flex;
560
-  align-items: center;
561
-  justify-content: center;
562
-}
563
-
564
-.color-option.active {
565
-  border-color: #2a5caa;
326
+/* 夜间模式样式 */
327
+.night-mode {
328
+  background-color: #1a1a1a;
329
+  color: #888;
566 330
 }
567 331
 
568
-.close-btn {
569
-  margin-top: 30rpx;
570
-  background: #2a5caa;
571
-  color: white;
572
-  border-radius: 10rpx;
332
+.night-mode .reader-content {
333
+  background-color: #1a1a1a;
334
+  color: #888;
573 335
 }
574 336
 </style>

+ 2
- 2
RuoYi-App/router/index.js Näytä tiedosto

@@ -16,9 +16,9 @@ const routes = [
16 16
     meta: { title: '小说列表' }
17 17
   },
18 18
   {
19
-    path: '/pages/novel/detail',
19
+    path: '/pages/novel/reader',
20 20
     name: 'NovelDetail',
21
-    component: () => import('@/pages/novel/detail'),
21
+    component: () => import('@/pages/novel/reader.vue'),
22 22
     meta: { title: '小说详情' }
23 23
   },
24 24
   {

Loading…
Peruuta
Tallenna