Explorar o código

fix(image): 云文件绝对URL不再重复叠加 API_BASE_URL 前缀

Sisyphus Agent hai 1 semana
pai
achega
3cef518425

+ 16 - 11
cfc-frontend/pages/activity/activity-detail/activity-detail.vue

@@ -1,5 +1,5 @@
 <template>
-  <view class="container">
+  <view class="container" :style="{ '--font-scale': fontScale }">
     <!-- 登录浮层(全局组件,页面内弹出底部登录) -->
     <login-sheet ref="loginSheet" @success="onSheetLoginSuccess" />
     <!-- 加载状态 -->
@@ -79,7 +79,7 @@
         </view>
       </view>
 
-      </scroll-view>
+    </scroll-view>
 
     <!-- 底部操作栏:必须与 scroll-view 平级,否则 iOS 上 position: fixed 在 scroll-view 内不生效(按钮不显示) -->
     <view class="bottom-bar" v-if="!loading && activity.id">
@@ -281,12 +281,14 @@ import MpHtml from '@/components/mp-html/mp-html.vue'
 import ContentSharePoster from '@/components/ContentSharePoster.vue'
 import { backfillProfile } from '../../../utils/profile-backfill.js'
 import { parseDate } from '@/utils/format.js'
+import { getFontScale } from '@/utils/fontScale.js'
 
 export default {
   mixins: [shareMixin],
   components: { MpHtml, ContentSharePoster },
   data() {
     return {
+      fontScale: 1,
       activityId: null,
       activity: {},
       loading: false,
@@ -355,10 +357,13 @@ export default {
 
     this.checkMemberStatus()
   },
+  onShow: function() {
+    this.fontScale = getFontScale()
+  },
   methods: {
     // ===== 工具方法 =====
     getImageUrl: function(path) {
- cff388913 (fix(image): 云文件绝对 URL 不再重复叠加 API_BASE_URL 前缀)
+
       if (!path || path === 'null' || path === 'undefined' || path === '/null') return ''
       var s = String(path).trim()
       if (!s || s === 'null' || s === '/null') return ''
@@ -966,9 +971,9 @@ export default {
   padding: 30rpx;
 }
 .activity-title {
-  font-size: 36rpx;
+  font-size: calc(36rpx * var(--font-scale, 1));
   font-weight: bold;
-  color: #1f2937;
+  color: #333;
   line-height: 1.4;
 }
 .status-row {
@@ -1008,7 +1013,7 @@ export default {
   text-align: center;
 }
 .meta-text {
-  font-size: 26rpx;
+  font-size: calc(26rpx * var(--font-scale, 1));
   color: #4b5563;
   flex: 1;
   line-height: 1.4;
@@ -1031,14 +1036,14 @@ export default {
   padding: 24rpx;
 }
 .section-title {
-  font-size: 28rpx;
+  font-size: calc(28rpx * var(--font-scale, 1));
   font-weight: 600;
   color: #1f2937;
   display: block;
   margin-bottom: 12rpx;
 }
 .desc-text {
-  font-size: 26rpx;
+  font-size: calc(26rpx * var(--font-scale, 1));
   color: #4b5563;
   line-height: 1.6;
 }
@@ -1063,7 +1068,7 @@ export default {
   flex-shrink: 0;
 }
 .refund-notice-text {
-  font-size: 24rpx;
+  font-size: calc(24rpx * var(--font-scale, 1));
   color: #DC2626;
   line-height: 1.4;
 }
@@ -1213,7 +1218,7 @@ export default {
   border-radius: 16rpx;
 }
 .feedback-header { margin-bottom: 20rpx; }
-.feedback-title { font-size: 32rpx; font-weight: bold; color: #333; }
+.feedback-title { font-size: calc(32rpx * var(--font-scale, 1)); font-weight: bold; color: #333; }
 .feedback-btn {
   background: #F97316; color: #fff; border: none;
   border-radius: 40rpx; padding: 20rpx 40rpx; font-size: 28rpx;
@@ -1224,7 +1229,7 @@ export default {
 .fr-row { display: flex; justify-content: space-between; padding: 8rpx 0; }
 .fr-label { font-size: 28rpx; color: #333; }
 .fr-stars { font-size: 28rpx; color: #F97316; }
-.feedback-comment-text { font-size: 26rpx; color: #666; margin: 10rpx 0; line-height: 1.5; }
+.feedback-comment-text { font-size: calc(26rpx * var(--font-scale, 1)); color: #666; margin: 10rpx 0; line-height: 1.5; }
 .feedback-thanks { font-size: 24rpx; color: #999; text-align: center; margin-top: 16rpx; }
 .feedback-modal-overlay {
   position: fixed;

+ 1 - 1
cfc-frontend/pages/activity/index.vue

@@ -376,7 +376,7 @@ export default {
   },
   methods: {
     getImageUrl: function(path) {
- cff388913 (fix(image): 云文件绝对 URL 不再重复叠加 API_BASE_URL 前缀)
+
       if (!path || path === 'null' || path === 'undefined' || path === '/null') return ''
       var s = String(path).trim()
       if (!s || s === 'null' || s === '/null') return ''

+ 75 - 9
cfc-frontend/pages/article-center/article-detail.vue

@@ -1,5 +1,5 @@
 <template>
-  <view class="detail-container">
+  <view class="detail-container" :style="{ '--font-scale': fontScale }">
     <!-- 登录浮层(全局组件,页面内弹出底部登录) -->
     <login-sheet ref="loginSheet" @success="onSheetLoginSuccess" />
     <view v-if="loading" class="loading-wrap">
@@ -47,6 +47,10 @@
 
       <!-- 底部 -->
       <view class="detail-footer">
+        <view class="listen-btn" :class="{ playing: audioPlaying }" @click="toggleArticleAudio">
+          <text class="listen-btn-icon">{{ audioLoading ? '⏳' : (audioPlaying ? '⏸' : '🔊') }}</text>
+          <text class="listen-btn-text">{{ audioLoading ? '生成中' : (audioPlaying ? '停止' : '听文章') }}</text>
+        </view>
         <view class="share-btn" @click="generatePoster">
           <text class="share-btn-icon">🖼</text>
           <text class="share-btn-text">生成海报</text>
@@ -87,11 +91,12 @@
 </template>
 
 <script>
-import { getArticleDetail, reportReadingTime, getShareQrCode, completeArticleRead, generateQuiz, submitQuiz } from '@/utils/api.js'
+import { getArticleDetail, reportReadingTime, getArticleAudio, getShareQrCode, completeArticleRead, generateQuiz, submitQuiz } from '@/utils/api.js'
 import shareMixin from '../../components/share-mixin.js'
 import ContentSharePoster from '@/components/ContentSharePoster.vue'
 import MpHtml from '@/components/mp-html/mp-html.vue'
 import config from '@/config.js'
+import { getFontScale } from '@/utils/fontScale.js'
 
 export default {
   mixins: [shareMixin],
@@ -102,16 +107,24 @@ export default {
       readingSeconds: 0, lastReportedSeconds: 0, readingCompleted: false,
       isTimerRunning: false, timerHandle: null, syncHandle: null,
       currentChildId: null, isLogin: false,
+      // 听文章(AI 朗读)
+      audioUrls: [], audioPlaying: false, audioLoading: false, audioContext: null, audioChunkIndex: 0,
       showPoster: false, posterQrCode: '',
       posterDimension: '', posterSummary: '', posterInviteText: '',
-      contentTagStyle: { p: 'font-size:32rpx;color:#444;line-height:1.8;', div: 'font-size:32rpx;color:#444;line-height:1.8;', section: 'font-size:32rpx;color:#444;line-height:1.8;', li: 'font-size:32rpx;color:#444;line-height:1.8;', blockquote: 'font-size:32rpx;color:#444;line-height:1.8;' },
+      fontScale: 1,
       // 答题
       showQuiz: false, quizQuestions: [], quizAnswers: [], showResult: false, quizResult: {}, quizRecordId: null,
       mascotList: [{ icon: '🐶', name: '浠宝' }, { icon: '🐼', name: '福宝' }],
       mascotIcon: '🐶', mascotName: '浠宝'
     }
   },
-  computed: {},
+  computed: {
+    contentTagStyle() {
+      var scale = this.fontScale || 1
+      var fontSize = 32 * scale
+      return { p: 'font-size:' + fontSize + 'rpx;color:#444;line-height:1.8;', div: 'font-size:' + fontSize + 'rpx;color:#444;line-height:1.8;', section: 'font-size:' + fontSize + 'rpx;color:#444;line-height:1.8;', li: 'font-size:' + fontSize + 'rpx;color:#444;line-height:1.8;', blockquote: 'font-size:' + fontSize + 'rpx;color:#444;line-height:1.8;' }
+    }
+  },
   onLoad(options) {
     this.mascotIcon = this.mascotList[Math.floor(Math.random() * 2)].icon
     this.mascotName = this.mascotList[Math.floor(Math.random() * 2)].name
@@ -137,13 +150,14 @@ export default {
     this.isLogin = !!uni.getStorageSync('token')
   },
   onShow() {
+    this.fontScale = getFontScale()
     var memberId = uni.getStorageSync('currentChildId')
     if (!this.article || this.error) return
     // 已登录用户(家长/成员/规划师)均可计时,匿名用户不计时
     if (this.isLogin) this.startNewReadingSession()
   },
-  onHide() { this.pauseReadingTimer() },
-  onUnload() { this.pauseReadingTimer() },
+  onHide() { this.pauseReadingTimer(); this.stopArticleAudio() },
+  onUnload() { this.pauseReadingTimer(); this.stopArticleAudio() },
   methods: {
     async loadDetail(id) {
       this.loading = true; this.error = false
@@ -251,6 +265,53 @@ export default {
       var min = Math.floor(seconds / 60); var sec = seconds % 60
       return '已读 ' + min + '分' + (sec > 0 ? sec + '秒' : '')
     },
+    // 听文章:首次点击拉取(或复用已生成)音频,多段顺序播放;播放期间保证阅读计时运行
+    async toggleArticleAudio() {
+      if (this.audioPlaying) { this.stopArticleAudio(); return }
+      if (this.audioLoading || !this.article) return
+      if (this.audioUrls && this.audioUrls.length > 0) { this.playAudioChunk(0); return }
+      this.audioLoading = true
+      try {
+        var res = await getArticleAudio({ articleId: this.article.id })
+        if (res.code === 200 && res.data && res.data.audioUrls && res.data.audioUrls.length > 0) {
+          this.audioUrls = res.data.audioUrls
+          this.playAudioChunk(0)
+        } else {
+          uni.showToast({ title: (res && res.message) || '抱歉,音频暂不可用', icon: 'none' })
+        }
+      } catch (e) {
+        uni.showToast({ title: '音频生成失败,请稍后重试', icon: 'none' })
+      } finally {
+        this.audioLoading = false
+      }
+    },
+    playAudioChunk: function(index) {
+      var self = this
+      // 播放期间保证阅读计时运行(未登录不计时)
+      if (this.isLogin && !this.isTimerRunning && !this.readingCompleted) this.startReadingTimer()
+      this.audioChunkIndex = index
+      var ctx = uni.createInnerAudioContext()
+      this.audioContext = ctx
+      ctx.src = this.audioUrls[index]
+      ctx.onEnded(function() {
+        if (index + 1 < self.audioUrls.length) { self.playAudioChunk(index + 1) }
+        else { self.stopArticleAudio() }
+      })
+      ctx.onError(function() {
+        uni.showToast({ title: '播放失败', icon: 'none' })
+        self.stopArticleAudio()
+      })
+      ctx.play()
+      this.audioPlaying = true
+    },
+    stopArticleAudio: function() {
+      if (this.audioContext) {
+        try { this.audioContext.stop() } catch (e) {}
+        try { this.audioContext.destroy() } catch (e) {}
+        this.audioContext = null
+      }
+      this.audioPlaying = false
+    },
     async generatePoster() {
       if (!this.articleId) return
       uni.showLoading({ title: '生成海报中...' })
@@ -334,7 +395,7 @@ export default {
 .retry-btn { width: 240rpx; height: 72rpx; line-height: 72rpx; background: #5B9BD5; color: #fff; font-size: 28rpx; border-radius: 36rpx; text-align: center; border: none; }
 .content-scroll { height: calc(100vh - 120rpx); }
 .detail-cover { width: 100%; display: block; }
-.detail-title { display: block; font-size: 36rpx; font-weight: bold; color: #333; line-height: 1.4; padding: 30rpx 30rpx 0; }
+.detail-title { display: block; font-size: calc(36rpx * var(--font-scale, 1)); font-weight: bold; color: #333; line-height: 1.4; padding: 30rpx 30rpx 0; }
 .detail-meta { display: flex; align-items: center; padding: 16rpx 30rpx 0; font-size: 22rpx; color: #999; flex-wrap: wrap; }
 .meta-author { color: #5B9BD5; }
 .meta-sep { margin: 0 12rpx; color: #ddd; }
@@ -351,9 +412,9 @@ export default {
 .dim-bar { height: 8rpx; border-radius: 4rpx; }
 .dim-label { font-size: 18rpx; color: #999; text-align: center; display: block; margin-top: 4rpx; }
 .detail-summary { padding: 20rpx 30rpx; }
-.detail-summary-text { font-size: 26rpx; color: #888; line-height: 1.7; display: block; border-left: 4rpx solid #F97316; padding-left: 20rpx; }
+.detail-summary-text { font-size: calc(26rpx * var(--font-scale, 1)); color: #888; line-height: 1.7; display: block; border-left: 4rpx solid #F97316; padding-left: 20rpx; }
 .divider { height: 1rpx; background: #eee; margin: 24rpx 30rpx; }
-.detail-body { padding: 0 30rpx; font-size: 32rpx; color: #444; line-height: 1.8; }
+.detail-body { padding: 0 30rpx; font-size: calc(32rpx * var(--font-scale, 1)); color: #444; line-height: 1.8; }
 
 .detail-footer { position: fixed; bottom: 0; left: 0; right: 0; background: #fff; padding: 20rpx 30rpx; display: flex; align-items: center; gap: 16rpx; box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.06); z-index: 10; }
 .share-btn { height: 72rpx; padding: 0 30rpx; background: linear-gradient(135deg, #F97316, #FB923C); border-radius: 36rpx; display: flex; align-items: center; justify-content: center; border: none; }
@@ -361,6 +422,11 @@ export default {
 .share-btn-icon { font-size: 30rpx; margin-right: 8rpx; }
 .share-btn-text { font-size: 26rpx; color: #fff; font-weight: 600; }
 
+.listen-btn { height: 72rpx; padding: 0 30rpx; background: rgba(91,155,213,0.1); border: 2rpx solid #5B9BD5; border-radius: 36rpx; display: flex; align-items: center; justify-content: center; }
+.listen-btn.playing { background: rgba(91,155,213,0.2); }
+.listen-btn-icon { font-size: 30rpx; margin-right: 8rpx; }
+.listen-btn-text { font-size: 26rpx; color: #5B9BD5; font-weight: 600; }
+
 /* 答题弹窗 */
 .quiz-mask, .result-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 999; display: flex; align-items: center; justify-content: center; }
 .quiz-dialog, .result-dialog { background: #fff; border-radius: 24rpx; width: 650rpx; padding: 32rpx; }

+ 14 - 9
cfc-frontend/pages/discover-detail/product-detail/product-detail.vue

@@ -1,5 +1,5 @@
 <template>
-  <view class="container">
+  <view class="container" :style="{ '--font-scale': fontScale }">
     <!-- 通用登录浮层 -->
     <login-sheet ref="loginSheet" @success="onSheetLoginSuccess" />
     <view v-if="loading" class="loading-wrap">
@@ -153,12 +153,14 @@ import { productDetail, productOrderCreate, productOrderPay, cartAdd, getShareQr
 import MpHtml from '@/components/mp-html/mp-html.vue'
 import ContentSharePoster from '@/components/ContentSharePoster.vue'
 import shareMixin from '../../../components/share-mixin.js'
+import { getFontScale } from '@/utils/fontScale.js'
 
 export default {
   components: { MpHtml, ContentSharePoster },
   mixins: [shareMixin],
   data() {
     return {
+      fontScale: 1,
       product: {},
       loading: false,
       showPoster: false,
@@ -325,6 +327,9 @@ export default {
     // 实时查询会员身份,用于会员价展示和实付价计算
     this.loadMembership()
   },
+  onShow() {
+    this.fontScale = getFontScale()
+  },
   methods: {
     loadMembership: function() {
       var that = this
@@ -708,7 +713,7 @@ export default {
   margin-bottom: 16rpx;
 }
 .product-name {
-  font-size: 34rpx;
+  font-size: calc(34rpx * var(--font-scale, 1));
   font-weight: bold;
   color: #333;
   line-height: 1.4;
@@ -795,7 +800,7 @@ export default {
   color: #999;
 }
 .vendor-name {
-  font-size: 24rpx;
+  font-size: calc(24rpx * var(--font-scale, 1));
   color: #666;
 }
 .desc-section,
@@ -805,7 +810,7 @@ export default {
   margin-bottom: 20rpx;
 }
 .section-title {
-  font-size: 28rpx;
+  font-size: calc(28rpx * var(--font-scale, 1));
   font-weight: bold;
   color: #333;
   margin-bottom: 16rpx;
@@ -813,7 +818,7 @@ export default {
 }
 .desc-text,
 .intro-text {
-  font-size: 26rpx;
+  font-size: calc(26rpx * var(--font-scale, 1));
   color: #666;
   line-height: 1.6;
   display: block;
@@ -824,8 +829,8 @@ export default {
 .bundle-item:last-child { border-bottom: none; }
 .bundle-item-img { width: 80rpx; height: 80rpx; border-radius: 12rpx; margin-right: 16rpx; flex-shrink: 0; background: #f7f7f7; }
 .bundle-item-info { flex: 1; display: flex; flex-direction: column; min-width: 0; }
-.bundle-item-name { font-size: 28rpx; color: #333; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.bundle-item-spec { font-size: 24rpx; color: #999; margin-top: 4rpx; }
+.bundle-item-name { font-size: calc(28rpx * var(--font-scale, 1)); color: #333; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.bundle-item-spec { font-size: calc(24rpx * var(--font-scale, 1)); color: #999; margin-top: 4rpx; }
 .bundle-item-qty { font-size: 30rpx; font-weight: bold; color: #F97316; margin-left: 16rpx; flex-shrink: 0; }
 /* SKU 选择器 */
 .sku-section {
@@ -1082,7 +1087,7 @@ export default {
   flex-shrink: 0;
 }
 .hint-text {
-  font-size: 24rpx;
+  font-size: calc(24rpx * var(--font-scale, 1));
   color: #666;
   line-height: 1.6;
   flex: 1;
@@ -1099,7 +1104,7 @@ export default {
   padding: 24rpx;
 }
 .gift-select-title {
-  font-size: 28rpx;
+  font-size: calc(28rpx * var(--font-scale, 1));
   font-weight: bold;
   color: #333;
   margin-bottom: 16rpx;

+ 1 - 1
cfc-frontend/pages/growth/upload.vue

@@ -279,7 +279,7 @@ export default {
           if (!s || s === 'null' || s === '/null') return ''
           if (s.indexOf('http://') === 0 || s.indexOf('https://') === 0) return s
           return config.API_BASE_URL + (s.charAt(0) === '/' ? s : '/' + s)
-        } cff388913 (fix(image): 云文件绝对 URL 不再重复叠加 API_BASE_URL 前缀)
+        }
     }
   }
 }

+ 1 - 1
cfc-frontend/pages/guide/activities/detail.vue

@@ -324,7 +324,7 @@ export default {
           if (!s || s === 'null' || s === '/null') return ''
           if (s.indexOf('http://') === 0 || s.indexOf('https://') === 0) return s
           return config.API_BASE_URL + (s.charAt(0) === '/' ? s : '/' + s)
-        } cff388913 (fix(image): 云文件绝对 URL 不再重复叠加 API_BASE_URL 前缀)
+        }
     }
   }
 }

+ 1 - 1
cfc-frontend/pages/guide/activities/index.vue

@@ -213,7 +213,7 @@ export default {
           if (!s || s === 'null' || s === '/null') return ''
           if (s.indexOf('http://') === 0 || s.indexOf('https://') === 0) return s
           return config.API_BASE_URL + (s.charAt(0) === '/' ? s : '/' + s)
-        } cff388913 (fix(image): 云文件绝对 URL 不再重复叠加 API_BASE_URL 前缀)
+        }
     }
   }
 }

+ 1 - 1
cfc-frontend/pages/guide/packages/share.vue

@@ -160,7 +160,7 @@ export default {
           if (!s || s === 'null' || s === '/null') return ''
           if (s.indexOf('http://') === 0 || s.indexOf('https://') === 0) return s
           return config.API_BASE_URL + (s.charAt(0) === '/' ? s : '/' + s)
-        } cff388913 (fix(image): 云文件绝对 URL 不再重复叠加 API_BASE_URL 前缀)
+        }
     }
   }
 }

+ 1 - 1
cfc-frontend/pages/meal/recipe-detail.vue

@@ -153,7 +153,7 @@ export default {
           if (!s || s === 'null' || s === '/null') return ''
           if (s.indexOf('http://') === 0 || s.indexOf('https://') === 0) return s
           return config.API_BASE_URL + (s.charAt(0) === '/' ? s : '/' + s)
-        } cff388913 (fix(image): 云文件绝对 URL 不再重复叠加 API_BASE_URL 前缀)
+        }
     }
   }
 }

+ 1 - 1
cfc-frontend/pages/mind-detail/articles.vue

@@ -260,7 +260,7 @@ onShow: function() {
           if (!s || s === 'null' || s === '/null') return ''
           if (s.indexOf('http://') === 0 || s.indexOf('https://') === 0) return s
           return config.API_BASE_URL + (s.charAt(0) === '/' ? s : '/' + s)
-        } cff388913 (fix(image): 云文件绝对 URL 不再重复叠加 API_BASE_URL 前缀)
+        }
     }
   }
 }

+ 1 - 1
cfc-frontend/pages/promotion/material.vue

@@ -226,7 +226,7 @@ export default {
           if (!s || s === 'null' || s === '/null') return ''
           if (s.indexOf('http://') === 0 || s.indexOf('https://') === 0) return s
           return config.API_BASE_URL + (s.charAt(0) === '/' ? s : '/' + s)
-        } cff388913 (fix(image): 云文件绝对 URL 不再重复叠加 API_BASE_URL 前缀)
+        }
     }
   }
 }

+ 1 - 1
cfc-frontend/pages/shop/checkout/checkout.vue

@@ -1121,7 +1121,7 @@ showOnlineSlotPicker: function() {
           if (!s || s === 'null' || s === '/null') return ''
           if (s.indexOf('http://') === 0 || s.indexOf('https://') === 0) return s
           return config.API_BASE_URL + (s.charAt(0) === '/' ? s : '/' + s)
-        } cff388913 (fix(image): 云文件绝对 URL 不再重复叠加 API_BASE_URL 前缀)
+        }
     }
   }
 }

+ 1 - 1
cfc-frontend/pages/shop/order-detail/order-detail.vue

@@ -296,7 +296,7 @@ export default {
           if (!s || s === 'null' || s === '/null') return ''
           if (s.indexOf('http://') === 0 || s.indexOf('https://') === 0) return s
           return config.API_BASE_URL + (s.charAt(0) === '/' ? s : '/' + s)
-        } cff388913 (fix(image): 云文件绝对 URL 不再重复叠加 API_BASE_URL 前缀)
+        }
     },
     // 履约状态标签
     fulfillStatusLabel: function(status) {

+ 1 - 1
cfc-frontend/pages/shop/order-list/order-list.vue

@@ -362,7 +362,7 @@ export default {
           if (!s || s === 'null' || s === '/null') return ''
           if (s.indexOf('http://') === 0 || s.indexOf('https://') === 0) return s
           return config.API_BASE_URL + (s.charAt(0) === '/' ? s : '/' + s)
-        } cff388913 (fix(image): 云文件绝对 URL 不再重复叠加 API_BASE_URL 前缀)
+        }
     }
   }
 }