|
@@ -1,53 +1,74 @@
|
|
|
<template>
|
|
<template>
|
|
|
<view class="detail-container">
|
|
<view class="detail-container">
|
|
|
- <!-- loading -->
|
|
|
|
|
<view v-if="loading" class="loading-wrap">
|
|
<view v-if="loading" class="loading-wrap">
|
|
|
<view class="loading-spinner"></view>
|
|
<view class="loading-spinner"></view>
|
|
|
<text class="loading-text">加载中...</text>
|
|
<text class="loading-text">加载中...</text>
|
|
|
</view>
|
|
</view>
|
|
|
-
|
|
|
|
|
- <!-- 错误状态 -->
|
|
|
|
|
<view v-else-if="error" class="error-wrap">
|
|
<view v-else-if="error" class="error-wrap">
|
|
|
<text class="error-icon">📄</text>
|
|
<text class="error-icon">📄</text>
|
|
|
<text class="error-text">{{ errorMsg }}</text>
|
|
<text class="error-text">{{ errorMsg }}</text>
|
|
|
<button class="retry-btn" @click="loadDetail(articleId)">重新加载</button>
|
|
<button class="retry-btn" @click="loadDetail(articleId)">重新加载</button>
|
|
|
</view>
|
|
</view>
|
|
|
-
|
|
|
|
|
<template v-else-if="article">
|
|
<template v-else-if="article">
|
|
|
- <!-- 文章内容区 -->
|
|
|
|
|
<scroll-view scroll-y class="content-scroll" @scrolltolower="onScrollToBottom">
|
|
<scroll-view scroll-y class="content-scroll" @scrolltolower="onScrollToBottom">
|
|
|
- <!-- 封面 -->
|
|
|
|
|
<image v-if="article.coverImage" class="detail-cover" :src="article.coverImage" mode="widthFix" />
|
|
<image v-if="article.coverImage" class="detail-cover" :src="article.coverImage" mode="widthFix" />
|
|
|
- <!-- 标题 -->
|
|
|
|
|
<text class="detail-title">{{ article.title }}</text>
|
|
<text class="detail-title">{{ article.title }}</text>
|
|
|
- <!-- 元信息 -->
|
|
|
|
|
<view class="detail-meta">
|
|
<view class="detail-meta">
|
|
|
<text class="meta-author">{{ article.author || '浠艾福' }}</text>
|
|
<text class="meta-author">{{ article.author || '浠艾福' }}</text>
|
|
|
<text class="meta-sep">|</text>
|
|
<text class="meta-sep">|</text>
|
|
|
<text class="meta-date">{{ formatDate(article.publishedAt) }}</text>
|
|
<text class="meta-date">{{ formatDate(article.publishedAt) }}</text>
|
|
|
<text class="meta-sep">|</text>
|
|
<text class="meta-sep">|</text>
|
|
|
<text class="meta-readtime">{{ article.readTime || 3 }}分钟阅读</text>
|
|
<text class="meta-readtime">{{ article.readTime || 3 }}分钟阅读</text>
|
|
|
- <text class="reading-time-badge">{{ formatReadingTime(readingSeconds) }}</text>
|
|
|
|
|
|
|
+ <text class="reading-time-badge" v-if="!readingCompleted">{{ formatReadingTime(readingSeconds) }}</text>
|
|
|
|
|
+ <text class="reading-time-badge completed" v-else>✅ 阅读完成</text>
|
|
|
</view>
|
|
</view>
|
|
|
- <!-- 分类 -->
|
|
|
|
|
<view class="detail-category-row">
|
|
<view class="detail-category-row">
|
|
|
<text class="detail-category">{{ article.categoryName || '' }}</text>
|
|
<text class="detail-category">{{ article.categoryName || '' }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
- <!-- 五维权重彩条 -->
|
|
|
|
|
<view v-if="article.relatedDimensions" class="detail-dimensions">
|
|
<view v-if="article.relatedDimensions" class="detail-dimensions">
|
|
|
<view v-for="dim in parseDimensions(article)" :key="dim.code" class="dim-bar-item">
|
|
<view v-for="dim in parseDimensions(article)" :key="dim.code" class="dim-bar-item">
|
|
|
<view class="dim-bar" :style="{ background: dim.color, width: dim.weight + '%' }"></view>
|
|
<view class="dim-bar" :style="{ background: dim.color, width: dim.weight + '%' }"></view>
|
|
|
<text class="dim-label">{{ dim.name }}</text>
|
|
<text class="dim-label">{{ dim.name }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
- <!-- 分割线 -->
|
|
|
|
|
<view class="divider"></view>
|
|
<view class="divider"></view>
|
|
|
- <!-- 正文 -->
|
|
|
|
|
- <view class="detail-body">
|
|
|
|
|
- <rich-text :nodes="article.content"></rich-text>
|
|
|
|
|
|
|
+ <view class="detail-body"><rich-text :nodes="article.content"></rich-text></view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 评论区 -->
|
|
|
|
|
+ <view class="comment-section">
|
|
|
|
|
+ <view class="comment-header">
|
|
|
|
|
+ <text class="comment-title">评论 ({{ comments.length }})</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="comment-input-row">
|
|
|
|
|
+ <input class="comment-input" v-model="commentText" :placeholder="commentPlaceholder" confirm-type="send" @confirm="submitComment" />
|
|
|
|
|
+ <button class="comment-submit" @click="submitComment">发送</button>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-if="comments.length === 0" class="comment-empty">暂无评论,来写第一条吧</view>
|
|
|
|
|
+ <view v-for="(c, i) in comments" :key="c.id || i" class="comment-item">
|
|
|
|
|
+ <view class="comment-user">
|
|
|
|
|
+ <text class="comment-avatar">{{ (c.userName || '?').charAt(0) }}</text>
|
|
|
|
|
+ <text class="comment-name">{{ c.userName || '匿名' }}</text>
|
|
|
|
|
+ <text class="comment-time">{{ formatDate(c.createdAt) }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="comment-content">{{ c.content }}</view>
|
|
|
|
|
+ <view class="comment-actions">
|
|
|
|
|
+ <text class="comment-reply-btn" @click="startReply(c)">回复</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <!-- 回复列表 -->
|
|
|
|
|
+ <view class="reply-list" v-if="c.replies && c.replies.length > 0">
|
|
|
|
|
+ <view v-for="(r, ri) in c.replies" :key="ri" class="reply-item">
|
|
|
|
|
+ <view class="reply-user">
|
|
|
|
|
+ <text class="reply-avatar">{{ (r.userName || '?').charAt(0) }}</text>
|
|
|
|
|
+ <text class="reply-name">{{ r.userName || '匿名' }}</text>
|
|
|
|
|
+ <text class="reply-time">{{ formatDate(r.createdAt) }}</text>
|
|
|
|
|
+ <text class="reply-to" v-if="r.replyToId">回复</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="reply-content">{{ r.content }}</view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
</view>
|
|
</view>
|
|
|
- <!-- 底部占位 -->
|
|
|
|
|
- <view style="height: 160rpx;"></view>
|
|
|
|
|
|
|
+ <view style="height: 200rpx;"></view>
|
|
|
</scroll-view>
|
|
</scroll-view>
|
|
|
|
|
|
|
|
<!-- 底部 -->
|
|
<!-- 底部 -->
|
|
@@ -63,19 +84,40 @@
|
|
|
</view>
|
|
</view>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
- <ContentSharePoster
|
|
|
|
|
- :show="showPoster"
|
|
|
|
|
- :title="article && article.title"
|
|
|
|
|
- :coverImage="article && article.coverImage"
|
|
|
|
|
- :qrCodeBase64="posterQrCode"
|
|
|
|
|
- typeLabel="好文推荐"
|
|
|
|
|
- @close="showPoster = false"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ <ContentSharePoster :show="showPoster" :title="article && article.title" :coverImage="article && article.coverImage" :qrCodeBase64="posterQrCode" typeLabel="好文推荐" @close="showPoster = false" />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 浠宝答题弹窗 -->
|
|
|
|
|
+ <view class="quiz-mask" v-if="showQuiz" @click="showQuiz = false">
|
|
|
|
|
+ <view class="quiz-dialog" @click.stop>
|
|
|
|
|
+ <view class="quiz-mascot">
|
|
|
|
|
+ <text class="mascot-icon">{{ mascotIcon }}</text>
|
|
|
|
|
+ <text class="mascot-name">{{ mascotName }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <text class="quiz-intro">阅读完成!考考你三个问题~</text>
|
|
|
|
|
+ <view class="quiz-question" v-for="(q, qi) in quizQuestions" :key="qi">
|
|
|
|
|
+ <text class="q-title">问题{{ qi+1 }}: {{ q.question }}</text>
|
|
|
|
|
+ <view class="q-options">
|
|
|
|
|
+ <text v-for="(opt, oi) in q.options" :key="oi" class="q-option" :class="{ selected: quizAnswers[qi] === String.fromCharCode(65 + oi) }" @click="quizAnswers[qi] = String.fromCharCode(65 + oi)">{{ opt }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <button class="quiz-submit" @click="submitQuiz">提交答案</button>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 答题结果 -->
|
|
|
|
|
+ <view class="result-mask" v-if="showResult">
|
|
|
|
|
+ <view class="result-dialog">
|
|
|
|
|
+ <text class="result-icon">{{ resultIcon }}</text>
|
|
|
|
|
+ <text class="result-text">答对 {{ quizResult.correctCount }}/{{ quizResult.totalQuestions }} 题</text>
|
|
|
|
|
+ <text class="result-energy">获得 {{ quizResult.earnedEnergy }} 能量 ⚡</text>
|
|
|
|
|
+ <button class="result-btn" @click="showResult = false">知道了</button>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
</view>
|
|
</view>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
|
<script>
|
|
|
-import { getArticleDetail, reportReadingTime, getShareQrCode } from '@/utils/api.js'
|
|
|
|
|
|
|
+import { getArticleDetail, reportReadingTime, getShareQrCode, completeArticleRead, getArticleComments, createArticleComment, 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'
|
|
|
|
|
|
|
@@ -84,170 +126,153 @@ export default {
|
|
|
components: { ContentSharePoster },
|
|
components: { ContentSharePoster },
|
|
|
data() {
|
|
data() {
|
|
|
return {
|
|
return {
|
|
|
- articleId: '',
|
|
|
|
|
- article: null,
|
|
|
|
|
- loading: true,
|
|
|
|
|
- error: false,
|
|
|
|
|
- errorMsg: '',
|
|
|
|
|
- readingSeconds: 0,
|
|
|
|
|
- lastReportedSeconds: 0,
|
|
|
|
|
- isTimerRunning: false,
|
|
|
|
|
- timerHandle: null,
|
|
|
|
|
- syncHandle: null,
|
|
|
|
|
- showPoster: false,
|
|
|
|
|
- posterQrCode: ''
|
|
|
|
|
|
|
+ articleId: '', article: null, loading: true, error: false, errorMsg: '',
|
|
|
|
|
+ readingSeconds: 0, lastReportedSeconds: 0, readingCompleted: false,
|
|
|
|
|
+ isTimerRunning: false, timerHandle: null, syncHandle: null,
|
|
|
|
|
+ showPoster: false, posterQrCode: '',
|
|
|
|
|
+ // 评论
|
|
|
|
|
+ comments: [], commentText: '', replyTarget: null,
|
|
|
|
|
+ // 答题
|
|
|
|
|
+ showQuiz: false, quizQuestions: [], quizAnswers: [], showResult: false, quizResult: {},
|
|
|
|
|
+ mascotList: [{ icon: '🐶', name: '浠宝' }, { icon: '🐼', name: '福宝' }],
|
|
|
|
|
+ mascotIcon: '🐶', mascotName: '浠宝'
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
- onLoad(options) {
|
|
|
|
|
- if (options && options.id) {
|
|
|
|
|
- this.articleId = options.id
|
|
|
|
|
- this.loadDetail(options.id)
|
|
|
|
|
- } else {
|
|
|
|
|
- this.error = true
|
|
|
|
|
- this.errorMsg = '参数错误'
|
|
|
|
|
- this.loading = false
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- onShow: function() {
|
|
|
|
|
- if (this.article && !this.error) {
|
|
|
|
|
- this.startReadingTimer()
|
|
|
|
|
|
|
+ computed: {
|
|
|
|
|
+ commentPlaceholder: function() {
|
|
|
|
|
+ return this.replyTarget ? '回复 ' + (this.replyTarget.userName || '') + ':' : '写下你的评论...'
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
- onHide: function() {
|
|
|
|
|
- this.pauseReadingTimer()
|
|
|
|
|
- },
|
|
|
|
|
- onUnload: function() {
|
|
|
|
|
- this.pauseReadingTimer()
|
|
|
|
|
|
|
+ onLoad(options) {
|
|
|
|
|
+ this.mascotIcon = this.mascotList[Math.floor(Math.random() * 2)].icon
|
|
|
|
|
+ this.mascotName = this.mascotList[Math.floor(Math.random() * 2)].name
|
|
|
|
|
+ if (options && options.id) { this.articleId = options.id; this.loadDetail(options.id); this.loadComments() }
|
|
|
|
|
+ else { this.error = true; this.errorMsg = '参数错误'; this.loading = false }
|
|
|
},
|
|
},
|
|
|
|
|
+ onShow() { if (this.article && !this.error) this.startReadingTimer() },
|
|
|
|
|
+ onHide() { this.pauseReadingTimer() },
|
|
|
|
|
+ onUnload() { this.pauseReadingTimer() },
|
|
|
methods: {
|
|
methods: {
|
|
|
- async generatePoster() {
|
|
|
|
|
- if (!this.articleId) return
|
|
|
|
|
- uni.showLoading({ title: '生成海报中...' })
|
|
|
|
|
- try {
|
|
|
|
|
- var page = 'pages/article-center/article-detail'
|
|
|
|
|
- var scene = 'id=' + this.articleId
|
|
|
|
|
- var res = await getShareQrCode(page, scene)
|
|
|
|
|
- if (res && res.data) {
|
|
|
|
|
- this.posterQrCode = res.data.qrCodeBase64 || ''
|
|
|
|
|
- this.showPoster = true
|
|
|
|
|
- }
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- uni.showToast({ title: '生成失败', icon: 'none' })
|
|
|
|
|
- } finally {
|
|
|
|
|
- uni.hideLoading()
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
async loadDetail(id) {
|
|
async loadDetail(id) {
|
|
|
- this.loading = true
|
|
|
|
|
- this.error = false
|
|
|
|
|
|
|
+ this.loading = true; this.error = false
|
|
|
try {
|
|
try {
|
|
|
var res = await getArticleDetail({ id: id })
|
|
var res = await getArticleDetail({ id: id })
|
|
|
if (res.code === 200 && res.data) {
|
|
if (res.code === 200 && res.data) {
|
|
|
this.article = res.data
|
|
this.article = res.data
|
|
|
this.setShareInfo('推荐阅读: ' + (res.data.title || ''), '/pages/article-center/article-detail?id=' + id)
|
|
this.setShareInfo('推荐阅读: ' + (res.data.title || ''), '/pages/article-center/article-detail?id=' + id)
|
|
|
this.startReadingTimer()
|
|
this.startReadingTimer()
|
|
|
- } else {
|
|
|
|
|
- this.error = true
|
|
|
|
|
- this.errorMsg = '文章不存在或无权限查看'
|
|
|
|
|
- }
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- this.error = true
|
|
|
|
|
- this.errorMsg = '加载失败,请稍后重试'
|
|
|
|
|
- } finally {
|
|
|
|
|
- this.loading = false
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ } else { this.error = true; this.errorMsg = '文章不存在或无权限查看' }
|
|
|
|
|
+ } catch (e) { this.error = true; this.errorMsg = '加载失败' }
|
|
|
|
|
+ finally { this.loading = false }
|
|
|
},
|
|
},
|
|
|
- formatDate(dateStr) {
|
|
|
|
|
- if (!dateStr) return ''
|
|
|
|
|
- return dateStr.slice(0, 10)
|
|
|
|
|
|
|
+ async loadComments() {
|
|
|
|
|
+ try { var res = await getArticleComments({ articleId: this.articleId }); if (res.code === 200) this.comments = res.data || [] }
|
|
|
|
|
+ catch (e) {}
|
|
|
},
|
|
},
|
|
|
|
|
+ formatDate(d) { return d ? d.slice(0, 10) : '' },
|
|
|
parseDimensions: function(article) {
|
|
parseDimensions: function(article) {
|
|
|
- var dimMap = {
|
|
|
|
|
- body: { code: 'body', color: '#FF8C42', name: '身' },
|
|
|
|
|
- mind: { code: 'mind', color: '#FF6B9D', name: '心' },
|
|
|
|
|
- wisdom: { code: 'wisdom', color: '#6366F1', name: '智' },
|
|
|
|
|
- action: { code: 'action', color: '#10B981', name: '行' },
|
|
|
|
|
- wealth: { code: 'wealth', color: '#F59E0B', name: '富' }
|
|
|
|
|
- }
|
|
|
|
|
- var raw = article && article.relatedDimensions
|
|
|
|
|
- ? article.relatedDimensions.split(',').map(function(s) { return s.trim().toLowerCase() })
|
|
|
|
|
- : []
|
|
|
|
|
|
|
+ var dimMap = { body: { code: 'body', color: '#FF8C42', name: '身' }, mind: { code: 'mind', color: '#FF6B9D', name: '心' }, wisdom: { code: 'wisdom', color: '#6366F1', name: '智' }, action: { code: 'action', color: '#10B981', name: '行' }, wealth: { code: 'wealth', color: '#F59E0B', name: '富' } }
|
|
|
|
|
+ var raw = article && article.relatedDimensions ? article.relatedDimensions.split(',').map(function(s) { return s.trim().toLowerCase() }) : []
|
|
|
var weights = null
|
|
var weights = null
|
|
|
- if (article && article.dimensionWeights) {
|
|
|
|
|
- try {
|
|
|
|
|
- var w = JSON.parse(article.dimensionWeights)
|
|
|
|
|
- if (w && typeof w === 'object') weights = w
|
|
|
|
|
- } catch (e) {}
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (article && article.dimensionWeights) try { var w = JSON.parse(article.dimensionWeights); if (w && typeof w === 'object') weights = w } catch (e) {}
|
|
|
if (!weights) {
|
|
if (!weights) {
|
|
|
var n = raw.filter(function(c) { return dimMap[c] }).length
|
|
var n = raw.filter(function(c) { return dimMap[c] }).length
|
|
|
- if (n > 0) {
|
|
|
|
|
- var base = Math.floor(100 / n)
|
|
|
|
|
- var rem = 100 - base * n
|
|
|
|
|
- weights = {}
|
|
|
|
|
- var i = 0
|
|
|
|
|
- raw.forEach(function(c) {
|
|
|
|
|
- if (!dimMap[c]) return
|
|
|
|
|
- weights[c] = i < rem ? base + 1 : base
|
|
|
|
|
- i++
|
|
|
|
|
- })
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (n > 0) { var base = Math.floor(100 / n); var rem = 100 - base * n; weights = {}; var i = 0; raw.forEach(function(c) { if (!dimMap[c]) return; weights[c] = i < rem ? base + 1 : base; i++ }) }
|
|
|
}
|
|
}
|
|
|
- return raw.filter(function(c) { return dimMap[c] }).map(function(c) {
|
|
|
|
|
- return {
|
|
|
|
|
- code: dimMap[c].code,
|
|
|
|
|
- name: dimMap[c].name,
|
|
|
|
|
- color: dimMap[c].color,
|
|
|
|
|
- weight: (weights && weights[c]) ? weights[c] : 20
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
- },
|
|
|
|
|
- onScrollToBottom() {
|
|
|
|
|
- // 滚动到底部
|
|
|
|
|
|
|
+ return raw.filter(function(c) { return dimMap[c] }).map(function(c) { return { code: dimMap[c].code, name: dimMap[c].name, color: dimMap[c].color, weight: (weights && weights[c]) ? weights[c] : 20 } })
|
|
|
},
|
|
},
|
|
|
|
|
+ onScrollToBottom() {},
|
|
|
startReadingTimer: function() {
|
|
startReadingTimer: function() {
|
|
|
- if (this.isTimerRunning) return
|
|
|
|
|
|
|
+ if (this.isTimerRunning || this.readingCompleted) return
|
|
|
this.isTimerRunning = true
|
|
this.isTimerRunning = true
|
|
|
var self = this
|
|
var self = this
|
|
|
- this.timerHandle = setInterval(function() {
|
|
|
|
|
- self.readingSeconds++
|
|
|
|
|
- }, 1000)
|
|
|
|
|
- this.syncHandle = setInterval(function() {
|
|
|
|
|
- self.reportReadingTime()
|
|
|
|
|
- }, 30000)
|
|
|
|
|
|
|
+ this.timerHandle = setInterval(function() { self.readingSeconds++ }, 1000)
|
|
|
|
|
+ this.syncHandle = setInterval(function() { self.reportReadingTime() }, 30000)
|
|
|
},
|
|
},
|
|
|
pauseReadingTimer: function() {
|
|
pauseReadingTimer: function() {
|
|
|
if (!this.isTimerRunning) return
|
|
if (!this.isTimerRunning) return
|
|
|
this.isTimerRunning = false
|
|
this.isTimerRunning = false
|
|
|
- if (this.timerHandle) {
|
|
|
|
|
- clearInterval(this.timerHandle)
|
|
|
|
|
- this.timerHandle = null
|
|
|
|
|
- }
|
|
|
|
|
- if (this.syncHandle) {
|
|
|
|
|
- clearInterval(this.syncHandle)
|
|
|
|
|
- this.syncHandle = null
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (this.timerHandle) { clearInterval(this.timerHandle); this.timerHandle = null }
|
|
|
|
|
+ if (this.syncHandle) { clearInterval(this.syncHandle); this.syncHandle = null }
|
|
|
this.reportReadingTime()
|
|
this.reportReadingTime()
|
|
|
},
|
|
},
|
|
|
- reportReadingTime: function() {
|
|
|
|
|
|
|
+ async reportReadingTime() {
|
|
|
var delta = this.readingSeconds - this.lastReportedSeconds
|
|
var delta = this.readingSeconds - this.lastReportedSeconds
|
|
|
if (delta <= 0) return
|
|
if (delta <= 0) return
|
|
|
this.lastReportedSeconds = this.readingSeconds
|
|
this.lastReportedSeconds = this.readingSeconds
|
|
|
var childId = uni.getStorageSync('currentChildId')
|
|
var childId = uni.getStorageSync('currentChildId')
|
|
|
if (!childId || !this.article) return
|
|
if (!childId || !this.article) return
|
|
|
- reportReadingTime({
|
|
|
|
|
- articleId: this.article.id,
|
|
|
|
|
- childId: parseInt(childId),
|
|
|
|
|
- durationSeconds: delta
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ reportReadingTime({ articleId: this.article.id, childId: parseInt(childId), durationSeconds: delta })
|
|
|
|
|
+
|
|
|
|
|
+ // 达到阅读时长自动完成
|
|
|
|
|
+ var targetSeconds = (this.article.readTime || 3) * 60
|
|
|
|
|
+ if (!this.readingCompleted && this.readingSeconds >= targetSeconds) {
|
|
|
|
|
+ this.readingCompleted = true
|
|
|
|
|
+ this.pauseReadingTimer()
|
|
|
|
|
+ try { await completeArticleRead({ articleId: this.article.id, childId: parseInt(childId), durationSeconds: this.readingSeconds }) } catch (e) {}
|
|
|
|
|
+ uni.showToast({ title: '阅读完成 +5能量', icon: 'success' })
|
|
|
|
|
+ // 弹出答题
|
|
|
|
|
+ this.startQuiz()
|
|
|
|
|
+ }
|
|
|
},
|
|
},
|
|
|
formatReadingTime: function(seconds) {
|
|
formatReadingTime: function(seconds) {
|
|
|
- if (seconds < 60) {
|
|
|
|
|
- return '已读 ' + seconds + '秒'
|
|
|
|
|
- }
|
|
|
|
|
- var min = Math.floor(seconds / 60)
|
|
|
|
|
- var sec = seconds % 60
|
|
|
|
|
|
|
+ if (seconds < 60) return '已读 ' + seconds + '秒'
|
|
|
|
|
+ var min = Math.floor(seconds / 60); var sec = seconds % 60
|
|
|
return '已读 ' + min + '分' + (sec > 0 ? sec + '秒' : '')
|
|
return '已读 ' + min + '分' + (sec > 0 ? sec + '秒' : '')
|
|
|
|
|
+ },
|
|
|
|
|
+ async generatePoster() {
|
|
|
|
|
+ if (!this.articleId) return
|
|
|
|
|
+ uni.showLoading({ title: '生成海报中...' })
|
|
|
|
|
+ try {
|
|
|
|
|
+ var res = await getShareQrCode('pages/article-center/article-detail', 'id=' + this.articleId)
|
|
|
|
|
+ if (res && res.data) { this.posterQrCode = res.data.qrCodeBase64 || ''; this.showPoster = true }
|
|
|
|
|
+ } catch (e) { uni.showToast({ title: '生成失败', icon: 'none' }) }
|
|
|
|
|
+ finally { uni.hideLoading() }
|
|
|
|
|
+ },
|
|
|
|
|
+ // 评论
|
|
|
|
|
+ startReply: function(comment) {
|
|
|
|
|
+ this.replyTarget = comment
|
|
|
|
|
+ this.commentText = ''
|
|
|
|
|
+ uni.pageScrollTo({ selector: '.comment-input-row', duration: 300 })
|
|
|
|
|
+ },
|
|
|
|
|
+ async submitComment() {
|
|
|
|
|
+ var text = this.commentText.trim()
|
|
|
|
|
+ if (!text) return
|
|
|
|
|
+ try {
|
|
|
|
|
+ var params = { articleId: this.articleId, content: text }
|
|
|
|
|
+ if (this.replyTarget) { params.parentId = this.replyTarget.id; params.replyToId = this.replyTarget.id }
|
|
|
|
|
+ var res = await createArticleComment(params)
|
|
|
|
|
+ if (res.code === 200) {
|
|
|
|
|
+ uni.showToast({ title: '评论已提交,等待审核', icon: 'success' })
|
|
|
|
|
+ this.commentText = ''; this.replyTarget = null
|
|
|
|
|
+ } else { uni.showToast({ title: res.message || '评论失败', icon: 'none' }) }
|
|
|
|
|
+ } catch (e) { uni.showToast({ title: '网络错误', icon: 'none' }) }
|
|
|
|
|
+ },
|
|
|
|
|
+ // 答题
|
|
|
|
|
+ async startQuiz() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ var res = await generateQuiz({ articleId: this.articleId })
|
|
|
|
|
+ if (res.code === 200 && res.data && res.data.length > 0) {
|
|
|
|
|
+ this.quizQuestions = res.data
|
|
|
|
|
+ this.quizAnswers = []
|
|
|
|
|
+ this.showQuiz = true
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {}
|
|
|
|
|
+ },
|
|
|
|
|
+ async submitQuiz() {
|
|
|
|
|
+ var correct = 0
|
|
|
|
|
+ for (var i = 0; i < this.quizQuestions.length; i++) {
|
|
|
|
|
+ if (this.quizAnswers[i] === this.quizQuestions[i].answer) correct++
|
|
|
|
|
+ }
|
|
|
|
|
+ var childId = uni.getStorageSync('currentChildId')
|
|
|
|
|
+ try {
|
|
|
|
|
+ var res = await submitQuiz({ correctCount: correct, childId: parseInt(childId || 0) })
|
|
|
|
|
+ if (res.code === 200) {
|
|
|
|
|
+ this.quizResult = res.data || { correctCount: correct, totalQuestions: 3, earnedEnergy: correct * 5 }
|
|
|
|
|
+ this.showQuiz = false
|
|
|
|
|
+ this.showResult = true
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -257,19 +282,20 @@ export default {
|
|
|
.detail-container { min-height: 100vh; background: #fff; }
|
|
.detail-container { min-height: 100vh; background: #fff; }
|
|
|
.loading-wrap { display: flex; flex-direction: column; align-items: center; padding-top: 300rpx; }
|
|
.loading-wrap { display: flex; flex-direction: column; align-items: center; padding-top: 300rpx; }
|
|
|
.loading-spinner { width: 60rpx; height: 60rpx; border: 4rpx solid #e0e0e0; border-top-color: #5B9BD5; border-radius: 50%; animation: spin 0.8s linear infinite; margin-bottom: 20rpx; }
|
|
.loading-spinner { width: 60rpx; height: 60rpx; border: 4rpx solid #e0e0e0; border-top-color: #5B9BD5; border-radius: 50%; animation: spin 0.8s linear infinite; margin-bottom: 20rpx; }
|
|
|
-@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
|
|
|
|
|
|
+@keyframes spin { 0% { transform: rotate(0deg); } 360% { transform: rotate(360deg); } }
|
|
|
.loading-text { font-size: 26rpx; color: #999; }
|
|
.loading-text { font-size: 26rpx; color: #999; }
|
|
|
.error-wrap { display: flex; flex-direction: column; align-items: center; padding-top: 300rpx; }
|
|
.error-wrap { display: flex; flex-direction: column; align-items: center; padding-top: 300rpx; }
|
|
|
.error-icon { font-size: 100rpx; margin-bottom: 24rpx; }
|
|
.error-icon { font-size: 100rpx; margin-bottom: 24rpx; }
|
|
|
.error-text { font-size: 28rpx; color: #999; margin-bottom: 30rpx; }
|
|
.error-text { font-size: 28rpx; color: #999; margin-bottom: 30rpx; }
|
|
|
.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; }
|
|
|
-.retry-btn::after { 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: 36rpx; 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; }
|
|
|
|
|
|
|
+.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; }
|
|
|
|
|
+.reading-time-badge { margin-left: auto; font-size: 20rpx; color: #5B9BD5; background: rgba(91,155,213,0.08); padding: 4rpx 12rpx; border-radius: 20rpx; white-space: nowrap; }
|
|
|
|
|
+.reading-time-badge.completed { color: #10B981; background: rgba(16,185,129,0.1); }
|
|
|
.detail-category-row { padding: 16rpx 30rpx 0; }
|
|
.detail-category-row { padding: 16rpx 30rpx 0; }
|
|
|
.detail-category { display: inline-block; font-size: 20rpx; color: #5B9BD5; background: rgba(91,155,213,0.1); padding: 4rpx 16rpx; border-radius: 8rpx; }
|
|
.detail-category { display: inline-block; font-size: 20rpx; color: #5B9BD5; background: rgba(91,155,213,0.1); padding: 4rpx 16rpx; border-radius: 8rpx; }
|
|
|
.detail-dimensions { padding: 16rpx 30rpx 0; display: flex; gap: 12rpx; }
|
|
.detail-dimensions { padding: 16rpx 30rpx 0; display: flex; gap: 12rpx; }
|
|
@@ -278,12 +304,53 @@ export default {
|
|
|
.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; }
|
|
|
.divider { height: 1rpx; background: #eee; margin: 24rpx 30rpx; }
|
|
.divider { height: 1rpx; background: #eee; margin: 24rpx 30rpx; }
|
|
|
.detail-body { padding: 0 30rpx; font-size: 28rpx; color: #444; line-height: 1.8; }
|
|
.detail-body { padding: 0 30rpx; font-size: 28rpx; color: #444; line-height: 1.8; }
|
|
|
-.detail-body rich-text { word-break: break-word; }
|
|
|
|
|
-/* 底部按钮 */
|
|
|
|
|
|
|
+
|
|
|
|
|
+/* 评论区 */
|
|
|
|
|
+.comment-section { padding: 30rpx; border-top: 16rpx solid #F5F7FA; }
|
|
|
|
|
+.comment-header { margin-bottom: 20rpx; }
|
|
|
|
|
+.comment-title { font-size: 30rpx; font-weight: 700; color: #333; }
|
|
|
|
|
+.comment-input-row { display: flex; gap: 12rpx; margin-bottom: 24rpx; }
|
|
|
|
|
+.comment-input { flex: 1; border: 2rpx solid #E5E7EB; border-radius: 12rpx; padding: 16rpx 20rpx; font-size: 26rpx; height: 72rpx; box-sizing: border-box; }
|
|
|
|
|
+.comment-submit { width: 120rpx; height: 72rpx; line-height: 72rpx; background: #5B9BD5; color: #fff; font-size: 26rpx; border-radius: 12rpx; text-align: center; border: none; }
|
|
|
|
|
+.comment-empty { text-align: center; padding: 40rpx 0; color: #999; font-size: 26rpx; }
|
|
|
|
|
+.comment-item { padding: 20rpx 0; border-bottom: 2rpx solid #F5F5F5; }
|
|
|
|
|
+.comment-user { display: flex; align-items: center; margin-bottom: 8rpx; }
|
|
|
|
|
+.comment-avatar { width: 40rpx; height: 40rpx; border-radius: 50%; background: #5B9BD5; color: #fff; font-size: 20rpx; text-align: center; line-height: 40rpx; margin-right: 12rpx; }
|
|
|
|
|
+.comment-name { font-size: 26rpx; color: #333; font-weight: 500; }
|
|
|
|
|
+.comment-time { font-size: 20rpx; color: #999; margin-left: 12rpx; }
|
|
|
|
|
+.comment-content { font-size: 26rpx; color: #444; line-height: 1.5; padding-left: 52rpx; }
|
|
|
|
|
+.comment-actions { padding-left: 52rpx; margin-top: 8rpx; }
|
|
|
|
|
+.comment-reply-btn { font-size: 22rpx; color: #5B9BD5; }
|
|
|
|
|
+.reply-list { padding-left: 52rpx; margin-top: 12rpx; background: #FAFAFA; border-radius: 8rpx; padding: 12rpx; }
|
|
|
|
|
+.reply-item { padding: 8rpx 0; }
|
|
|
|
|
+.reply-user { display: flex; align-items: center; }
|
|
|
|
|
+.reply-avatar { width: 32rpx; height: 32rpx; border-radius: 50%; background: #94A3B8; color: #fff; font-size: 16rpx; text-align: center; line-height: 32rpx; margin-right: 8rpx; }
|
|
|
|
|
+.reply-name { font-size: 24rpx; color: #333; }
|
|
|
|
|
+.reply-time { font-size: 18rpx; color: #999; margin-left: 8rpx; }
|
|
|
|
|
+.reply-to { font-size: 18rpx; color: #5B9BD5; margin-left: 8rpx; }
|
|
|
|
|
+.reply-content { font-size: 24rpx; color: #444; padding-left: 40rpx; margin-top: 4rpx; }
|
|
|
|
|
+
|
|
|
.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; line-height: 72rpx; background: #f5f5f5; color: #666; font-size: 24rpx; border-radius: 36rpx; padding: 0 24rpx; display: flex; align-items: center; border: none; }
|
|
.share-btn { height: 72rpx; line-height: 72rpx; background: #f5f5f5; color: #666; font-size: 24rpx; border-radius: 36rpx; padding: 0 24rpx; display: flex; align-items: center; border: none; }
|
|
|
.share-btn::after { border: none; }
|
|
.share-btn::after { border: none; }
|
|
|
.share-btn-icon { font-size: 28rpx; margin-right: 6rpx; }
|
|
.share-btn-icon { font-size: 28rpx; margin-right: 6rpx; }
|
|
|
.share-btn-text { font-size: 24rpx; }
|
|
.share-btn-text { font-size: 24rpx; }
|
|
|
-.reading-time-badge { margin-left: auto; font-size: 20rpx; color: #5B9BD5; background: rgba(91,155,213,0.08); padding: 4rpx 12rpx; border-radius: 20rpx; white-space: nowrap; }
|
|
|
|
|
-</style>
|
|
|
|
|
|
|
+
|
|
|
|
|
+/* 答题弹窗 */
|
|
|
|
|
+.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-mascot { display: flex; align-items: center; justify-content: center; gap: 12rpx; margin-bottom: 16rpx; }
|
|
|
|
|
+.mascot-icon { font-size: 60rpx; }
|
|
|
|
|
+.mascot-name { font-size: 32rpx; font-weight: 700; color: #F97316; }
|
|
|
|
|
+.quiz-intro { text-align: center; font-size: 28rpx; color: #666; margin-bottom: 24rpx; }
|
|
|
|
|
+.quiz-question { margin-bottom: 20rpx; }
|
|
|
|
|
+.q-title { font-size: 26rpx; font-weight: 600; color: #333; margin-bottom: 12rpx; }
|
|
|
|
|
+.q-options { display: flex; flex-direction: column; gap: 8rpx; }
|
|
|
|
|
+.q-option { padding: 14rpx 20rpx; border: 2rpx solid #E5E7EB; border-radius: 12rpx; font-size: 24rpx; color: #333; }
|
|
|
|
|
+.q-option.selected { border-color: #F97316; background: #FFF7ED; color: #92400E; }
|
|
|
|
|
+.quiz-submit, .result-btn { width: 100%; padding: 20rpx; background: linear-gradient(135deg, #F97316, #FB923C); color: #fff; font-size: 28rpx; border-radius: 12rpx; border: none; margin-top: 20rpx; }
|
|
|
|
|
+.result-dialog { text-align: center; }
|
|
|
|
|
+.result-icon { font-size: 80rpx; }
|
|
|
|
|
+.result-text { font-size: 32rpx; font-weight: 700; color: #333; margin: 16rpx 0; }
|
|
|
|
|
+.result-energy { font-size: 28rpx; color: #F97316; }
|
|
|
|
|
+</style>
|