|
|
@@ -9,7 +9,7 @@
|
|
|
<text class="error-text">{{ errorMsg }}</text>
|
|
|
<button class="retry-btn" @click="loadDetail(articleId)">重新加载</button>
|
|
|
</view>
|
|
|
- <view v-else-if="article" class="article-wrapper">
|
|
|
+ <template v-else-if="article">
|
|
|
<scroll-view scroll-y class="content-scroll" @scrolltolower="onScrollToBottom">
|
|
|
<image v-if="article.coverImage" class="detail-cover" :src="article.coverImage" mode="widthFix" />
|
|
|
<text class="detail-title">{{ article.title }}</text>
|
|
|
@@ -32,12 +32,60 @@
|
|
|
</view>
|
|
|
</view>
|
|
|
<view class="divider"></view>
|
|
|
- <!-- 正文 -->
|
|
|
- <view class="detail-body">
|
|
|
- <mp-html :content="article.content" />
|
|
|
- </view>
|
|
|
+ <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 class="review-section" v-if="isArticleAuthor && hasPendingComments">
|
|
|
+ <view class="review-header">
|
|
|
+ <text class="review-title">待审核评论 ({{ pendingComments.length }})</text>
|
|
|
+ </view>
|
|
|
+ <view v-for="(c, i) in pendingComments" :key="'pending-' + i" class="review-item">
|
|
|
+ <view class="review-user">
|
|
|
+ <text class="review-avatar">{{ (c.userName || '?').charAt(0) }}</text>
|
|
|
+ <text class="review-name">{{ c.userName || '匿名' }}</text>
|
|
|
+ <text class="review-time">{{ formatDate(c.createdAt) }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="review-content">{{ c.content }}</view>
|
|
|
+ <view class="review-actions">
|
|
|
+ <button class="review-btn approve" @click="approveComment(c.id)">通过</button>
|
|
|
+ <button class="review-btn reject" @click="rejectComment(c.id)">驳回</button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
<view style="height: 200rpx;"></view>
|
|
|
</scroll-view>
|
|
|
|
|
|
@@ -52,7 +100,7 @@
|
|
|
<text class="share-btn-text">海报</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
- </view>
|
|
|
+ </template>
|
|
|
|
|
|
<ContentSharePoster :show="showPoster" :title="article && article.title" :coverImage="article && article.coverImage" :qrCodeBase64="posterQrCode" typeLabel="好文推荐" @close="showPoster = false" />
|
|
|
|
|
|
@@ -87,14 +135,13 @@
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
-import { getArticleDetail, reportReadingTime, getShareQrCode, completeArticleRead, getArticleComments, createArticleComment, generateQuiz, submitQuiz } from '@/utils/api.js'
|
|
|
+import { getArticleDetail, reportReadingTime, getShareQrCode, completeArticleRead, getArticleComments, createArticleComment, generateQuiz, submitQuiz, getPendingComments, auditArticleComment } 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'
|
|
|
|
|
|
export default {
|
|
|
mixins: [shareMixin],
|
|
|
- components: { ContentSharePoster, MpHtml },
|
|
|
+ components: { ContentSharePoster },
|
|
|
data() {
|
|
|
return {
|
|
|
articleId: '', article: null, loading: true, error: false, errorMsg: '',
|
|
|
@@ -103,7 +150,38 @@ export default {
|
|
|
showPoster: false, posterQrCode: '',
|
|
|
// 评论
|
|
|
comments: [], commentText: '', replyTarget: null,
|
|
|
- // 答题
|
|
|
+ // 作者审核
|
|
|
+ pendingComments: [], currentUserId: null, currentRole: null,
|
|
|
+ // 作者审核
|
|
|
+ async loadPendingComments() {
|
|
|
+ try {
|
|
|
+ var res = await getPendingComments({ articleId: this.articleId })
|
|
|
+ if (res.code === 200) { this.pendingComments = res.data || [] }
|
|
|
+ } catch (e) {}
|
|
|
+ },
|
|
|
+ async approveComment(commentId) {
|
|
|
+ try {
|
|
|
+ var res = await auditArticleComment({ commentId: commentId, status: 'approved' })
|
|
|
+ if (res.code === 200) {
|
|
|
+ uni.showToast({ title: '已通过', icon: 'success' })
|
|
|
+ this.pendingComments = this.pendingComments.filter(function(c) { return c.id !== commentId })
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: res.message || '操作失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ } catch (e) { uni.showToast({ title: '网络错误', icon: 'none' }) }
|
|
|
+ },
|
|
|
+ async rejectComment(commentId) {
|
|
|
+ try {
|
|
|
+ var res = await auditArticleComment({ commentId: commentId, status: 'rejected' })
|
|
|
+ if (res.code === 200) {
|
|
|
+ uni.showToast({ title: '已驳回', icon: 'success' })
|
|
|
+ this.pendingComments = this.pendingComments.filter(function(c) { return c.id !== commentId })
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: res.message || '操作失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ } catch (e) { uni.showToast({ title: '网络错误', icon: 'none' }) }
|
|
|
+ },
|
|
|
+ // 答题
|
|
|
showQuiz: false, quizQuestions: [], quizAnswers: [], showResult: false, quizResult: {},
|
|
|
mascotList: [{ icon: '🐶', name: '浠宝' }, { icon: '🐼', name: '福宝' }],
|
|
|
mascotIcon: '🐶', mascotName: '浠宝'
|
|
|
@@ -112,11 +190,19 @@ export default {
|
|
|
computed: {
|
|
|
commentPlaceholder: function() {
|
|
|
return this.replyTarget ? '回复 ' + (this.replyTarget.userName || '') + ':' : '写下你的评论...'
|
|
|
+ },
|
|
|
+ isArticleAuthor: function() {
|
|
|
+ return this.article && this.currentUserId && this.article.createdBy === this.currentUserId
|
|
|
+ },
|
|
|
+ hasPendingComments: function() {
|
|
|
+ return this.pendingComments && this.pendingComments.length > 0
|
|
|
}
|
|
|
},
|
|
|
onLoad(options) {
|
|
|
this.mascotIcon = this.mascotList[Math.floor(Math.random() * 2)].icon
|
|
|
this.mascotName = this.mascotList[Math.floor(Math.random() * 2)].name
|
|
|
+ this.currentUserId = parseInt(uni.getStorageSync('userId') || 0)
|
|
|
+ this.currentRole = uni.getStorageSync('currentRole') || ''
|
|
|
if (options && options.id) { this.articleId = options.id; this.loadDetail(options.id); this.loadComments() }
|
|
|
else { this.error = true; this.errorMsg = '参数错误'; this.loading = false }
|
|
|
},
|
|
|
@@ -132,6 +218,10 @@ export default {
|
|
|
this.article = res.data
|
|
|
this.setShareInfo('推荐阅读: ' + (res.data.title || ''), '/pages/article-center/article-detail?id=' + id)
|
|
|
this.startReadingTimer()
|
|
|
+ // 如果当前用户是文章作者,加载待审核评论
|
|
|
+ if (this.currentUserId && res.data.createdBy === this.currentUserId) {
|
|
|
+ this.loadPendingComments()
|
|
|
+ }
|
|
|
} else { this.error = true; this.errorMsg = '文章不存在或无权限查看' }
|
|
|
} catch (e) { this.error = true; this.errorMsg = '加载失败' }
|
|
|
finally { this.loading = false }
|
|
|
@@ -301,6 +391,22 @@ export default {
|
|
|
.reply-to { font-size: 18rpx; color: #5B9BD5; margin-left: 8rpx; }
|
|
|
.reply-content { font-size: 24rpx; color: #444; padding-left: 40rpx; margin-top: 4rpx; }
|
|
|
|
|
|
+/* 作者审核区 */
|
|
|
+.review-section { padding: 30rpx; border-top: 16rpx solid #FFF3E0; background: #FFFBF5; }
|
|
|
+.review-header { margin-bottom: 16rpx; }
|
|
|
+.review-title { font-size: 28rpx; font-weight: 700; color: #E65100; }
|
|
|
+.review-item { padding: 16rpx 0; border-bottom: 2rpx solid #F5F5F5; }
|
|
|
+.review-user { display: flex; align-items: center; margin-bottom: 8rpx; }
|
|
|
+.review-avatar { width: 40rpx; height: 40rpx; border-radius: 50%; background: #E65100; color: #fff; font-size: 20rpx; text-align: center; line-height: 40rpx; margin-right: 12rpx; }
|
|
|
+.review-name { font-size: 26rpx; color: #333; font-weight: 500; }
|
|
|
+.review-time { font-size: 20rpx; color: #999; margin-left: 12rpx; }
|
|
|
+.review-content { font-size: 26rpx; color: #444; line-height: 1.5; padding-left: 52rpx; }
|
|
|
+.review-actions { display: flex; gap: 16rpx; padding-left: 52rpx; margin-top: 8rpx; }
|
|
|
+.review-btn { height: 56rpx; line-height: 56rpx; font-size: 24rpx; border-radius: 28rpx; padding: 0 24rpx; border: none; min-width: 100rpx; text-align: center; }
|
|
|
+.review-btn.approve { background: #10B981; color: #fff; }
|
|
|
+.review-btn.reject { background: #EF4444; color: #fff; }
|
|
|
+.review-btn::after { border: none; }
|
|
|
+
|
|
|
.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::after { border: none; }
|