|
@@ -1,5 +1,5 @@
|
|
|
<template>
|
|
<template>
|
|
|
- <view class="detail-container">
|
|
|
|
|
|
|
+ <view class="detail-container" :style="{ '--font-scale': fontScale }">
|
|
|
<!-- 登录浮层(全局组件,页面内弹出底部登录) -->
|
|
<!-- 登录浮层(全局组件,页面内弹出底部登录) -->
|
|
|
<login-sheet ref="loginSheet" @success="onSheetLoginSuccess" />
|
|
<login-sheet ref="loginSheet" @success="onSheetLoginSuccess" />
|
|
|
<view v-if="loading" class="loading-wrap">
|
|
<view v-if="loading" class="loading-wrap">
|
|
@@ -47,6 +47,10 @@
|
|
|
|
|
|
|
|
<!-- 底部 -->
|
|
<!-- 底部 -->
|
|
|
<view class="detail-footer">
|
|
<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">
|
|
<view class="share-btn" @click="generatePoster">
|
|
|
<text class="share-btn-icon">🖼</text>
|
|
<text class="share-btn-icon">🖼</text>
|
|
|
<text class="share-btn-text">生成海报</text>
|
|
<text class="share-btn-text">生成海报</text>
|
|
@@ -87,11 +91,12 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
|
<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 shareMixin from '../../components/share-mixin.js'
|
|
|
import ContentSharePoster from '@/components/ContentSharePoster.vue'
|
|
import ContentSharePoster from '@/components/ContentSharePoster.vue'
|
|
|
import MpHtml from '@/components/mp-html/mp-html.vue'
|
|
import MpHtml from '@/components/mp-html/mp-html.vue'
|
|
|
import config from '@/config.js'
|
|
import config from '@/config.js'
|
|
|
|
|
+import { getFontScale } from '@/utils/fontScale.js'
|
|
|
|
|
|
|
|
export default {
|
|
export default {
|
|
|
mixins: [shareMixin],
|
|
mixins: [shareMixin],
|
|
@@ -102,16 +107,24 @@ export default {
|
|
|
readingSeconds: 0, lastReportedSeconds: 0, readingCompleted: false,
|
|
readingSeconds: 0, lastReportedSeconds: 0, readingCompleted: false,
|
|
|
isTimerRunning: false, timerHandle: null, syncHandle: null,
|
|
isTimerRunning: false, timerHandle: null, syncHandle: null,
|
|
|
currentChildId: null, isLogin: false,
|
|
currentChildId: null, isLogin: false,
|
|
|
|
|
+ // 听文章(AI 朗读)
|
|
|
|
|
+ audioUrls: [], audioPlaying: false, audioLoading: false, audioContext: null, audioChunkIndex: 0,
|
|
|
showPoster: false, posterQrCode: '',
|
|
showPoster: false, posterQrCode: '',
|
|
|
posterDimension: '', posterSummary: '', posterInviteText: '',
|
|
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,
|
|
showQuiz: false, quizQuestions: [], quizAnswers: [], showResult: false, quizResult: {}, quizRecordId: null,
|
|
|
mascotList: [{ icon: '🐶', name: '浠宝' }, { icon: '🐼', name: '福宝' }],
|
|
mascotList: [{ icon: '🐶', name: '浠宝' }, { icon: '🐼', name: '福宝' }],
|
|
|
mascotIcon: '🐶', mascotName: '浠宝'
|
|
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) {
|
|
onLoad(options) {
|
|
|
this.mascotIcon = this.mascotList[Math.floor(Math.random() * 2)].icon
|
|
this.mascotIcon = this.mascotList[Math.floor(Math.random() * 2)].icon
|
|
|
this.mascotName = this.mascotList[Math.floor(Math.random() * 2)].name
|
|
this.mascotName = this.mascotList[Math.floor(Math.random() * 2)].name
|
|
@@ -137,13 +150,14 @@ export default {
|
|
|
this.isLogin = !!uni.getStorageSync('token')
|
|
this.isLogin = !!uni.getStorageSync('token')
|
|
|
},
|
|
},
|
|
|
onShow() {
|
|
onShow() {
|
|
|
|
|
+ this.fontScale = getFontScale()
|
|
|
var memberId = uni.getStorageSync('currentChildId')
|
|
var memberId = uni.getStorageSync('currentChildId')
|
|
|
if (!this.article || this.error) return
|
|
if (!this.article || this.error) return
|
|
|
// 已登录用户(家长/成员/规划师)均可计时,匿名用户不计时
|
|
// 已登录用户(家长/成员/规划师)均可计时,匿名用户不计时
|
|
|
if (this.isLogin) this.startNewReadingSession()
|
|
if (this.isLogin) this.startNewReadingSession()
|
|
|
},
|
|
},
|
|
|
- onHide() { this.pauseReadingTimer() },
|
|
|
|
|
- onUnload() { this.pauseReadingTimer() },
|
|
|
|
|
|
|
+ onHide() { this.pauseReadingTimer(); this.stopArticleAudio() },
|
|
|
|
|
+ onUnload() { this.pauseReadingTimer(); this.stopArticleAudio() },
|
|
|
methods: {
|
|
methods: {
|
|
|
async loadDetail(id) {
|
|
async loadDetail(id) {
|
|
|
this.loading = true; this.error = false
|
|
this.loading = true; this.error = false
|
|
@@ -251,6 +265,53 @@ export default {
|
|
|
var min = Math.floor(seconds / 60); var sec = seconds % 60
|
|
var min = Math.floor(seconds / 60); var sec = seconds % 60
|
|
|
return '已读 ' + min + '分' + (sec > 0 ? sec + '秒' : '')
|
|
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() {
|
|
async generatePoster() {
|
|
|
if (!this.articleId) return
|
|
if (!this.articleId) return
|
|
|
uni.showLoading({ title: '生成海报中...' })
|
|
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; }
|
|
.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); }
|
|
.content-scroll { height: calc(100vh - 120rpx); }
|
|
|
.detail-cover { width: 100%; display: block; }
|
|
.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; }
|
|
.detail-meta { display: flex; align-items: center; padding: 16rpx 30rpx 0; font-size: 22rpx; color: #999; flex-wrap: wrap; }
|
|
|
.meta-author { color: #5B9BD5; }
|
|
.meta-author { color: #5B9BD5; }
|
|
|
.meta-sep { margin: 0 12rpx; color: #ddd; }
|
|
.meta-sep { margin: 0 12rpx; color: #ddd; }
|
|
@@ -351,9 +412,9 @@ export default {
|
|
|
.dim-bar { height: 8rpx; border-radius: 4rpx; }
|
|
.dim-bar { height: 8rpx; border-radius: 4rpx; }
|
|
|
.dim-label { font-size: 18rpx; color: #999; text-align: center; display: block; margin-top: 4rpx; }
|
|
.dim-label { font-size: 18rpx; color: #999; text-align: center; display: block; margin-top: 4rpx; }
|
|
|
.detail-summary { padding: 20rpx 30rpx; }
|
|
.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; }
|
|
.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; }
|
|
.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; }
|
|
.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-icon { font-size: 30rpx; margin-right: 8rpx; }
|
|
|
.share-btn-text { font-size: 26rpx; color: #fff; font-weight: 600; }
|
|
.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-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; }
|
|
.quiz-dialog, .result-dialog { background: #fff; border-radius: 24rpx; width: 650rpx; padding: 32rpx; }
|