505 lines
11 KiB
Vue
505 lines
11 KiB
Vue
<template>
|
||
<view class="book-review-page">
|
||
<!-- 导航栏 -->
|
||
<nav-bar :title="$t('bookDetails.title_comment')"></nav-bar>
|
||
|
||
<scroll-view
|
||
scroll-y
|
||
class="review-scroll"
|
||
:style="{ height: scrollHeight + 'px' }"
|
||
@scrolltolower="loadMore"
|
||
>
|
||
<!-- 书籍信息卡片 -->
|
||
<view class="book-card">
|
||
<image :src="bookInfo.images" class="cover" mode="aspectFill" />
|
||
<view class="info">
|
||
<text class="title">{{ bookInfo.name }}</text>
|
||
<text class="author">{{ $t('bookDetails.authorName') }}{{ bookInfo.author?.authorName }}</text>
|
||
</view>
|
||
<wd-button
|
||
v-if="bookInfo.isBuy"
|
||
type="primary"
|
||
size="small"
|
||
@click="showCommentDialog()"
|
||
>
|
||
{{ $t('bookDetails.makeComment') }}
|
||
</wd-button>
|
||
</view>
|
||
|
||
<view class="divider-line" />
|
||
|
||
<!-- 评论列表 -->
|
||
<view class="comments-wrapper">
|
||
<view class="section-header">
|
||
<text class="section-title">{{ $t('bookDetails.message') }}</text>
|
||
</view>
|
||
|
||
<CommentList
|
||
v-if="commentList.length > 0"
|
||
:comments="commentList"
|
||
:has-more="hasMore"
|
||
@like="handleLike"
|
||
@reply="showCommentDialog"
|
||
@delete="handleDelete"
|
||
@load-more="loadMore"
|
||
/>
|
||
|
||
<text v-else class="empty-text">{{ nullText }}</text>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 评论输入弹窗 -->
|
||
<wd-popup v-model="commentVisible" position="bottom">
|
||
<view class="comment-dialog">
|
||
<text class="dialog-title">
|
||
{{ replyTarget ? $t('bookDetails.reply') + '"' + replyTarget.name + '"' + $t('bookDetails.dpl') : $t('bookDetails.makeComment') }}
|
||
</text>
|
||
|
||
<!-- 富文本编辑器 -->
|
||
<editor
|
||
id="editor"
|
||
class="editor"
|
||
:placeholder="placeholder"
|
||
@ready="onEditorReady"
|
||
@statuschange="onStatusChange"
|
||
/>
|
||
|
||
<!-- Emoji选择器 -->
|
||
<view class="emoji-section">
|
||
<view class="emoji-btn" @click="toggleEmoji">
|
||
<image src="@/static/biaoqing.png" mode="aspectFit" />
|
||
<text>Emoji</text>
|
||
</view>
|
||
|
||
<!-- 这里需要一个Emoji选择器组件,暂时简化处理 -->
|
||
<view v-if="showEmoji" class="emoji-picker">
|
||
<EmotionPicker @emotion="handleEmj" :height="220"></EmotionPicker>
|
||
</view>
|
||
</view>
|
||
|
||
<wd-button
|
||
type="primary"
|
||
custom-class="submit-btn"
|
||
@click="submitComment"
|
||
>
|
||
{{ $t('common.submit_text') }}
|
||
</wd-button>
|
||
</view>
|
||
</wd-popup>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed } from 'vue'
|
||
import { onLoad } from '@dcloudio/uni-app'
|
||
import { useI18n } from 'vue-i18n'
|
||
import { bookApi } from '@/api/modules/book'
|
||
import type { IBookDetail, IComment } from '@/types/book'
|
||
import CommentList from '@/components/book/CommentList.vue'
|
||
import EmotionPicker from '@/components/bkhumor-emojiplus/index.vue'
|
||
|
||
const { t } = useI18n()
|
||
|
||
// 路由参数
|
||
const bookId = ref(0)
|
||
const pageType = ref('')
|
||
|
||
// 数据状态
|
||
const bookInfo = ref<IBookDetail>({
|
||
id: 0,
|
||
name: '',
|
||
images: '',
|
||
author: { authorName: '', introduction: '' },
|
||
isBuy: false,
|
||
freeChapterCount: 0
|
||
})
|
||
const commentList = ref<IComment[]>([])
|
||
const page = ref({
|
||
current: 1,
|
||
limit: 10
|
||
})
|
||
const commentsCount = ref(0)
|
||
const nullText = ref('')
|
||
const scrollHeight = ref(0)
|
||
|
||
// 评论相关
|
||
const commentVisible = ref(false)
|
||
const replyTarget = ref<{ id: number; name: string } | null>(null)
|
||
const placeholder = ref('')
|
||
const showEmoji = ref(false)
|
||
const editorCtx = ref<any>(null)
|
||
const htmlContent = ref('')
|
||
|
||
// 计算属性
|
||
const hasMore = computed(() => commentList.value.length < commentsCount.value)
|
||
|
||
// 生命周期
|
||
onLoad((options: any) => {
|
||
if (options.bookId) {
|
||
bookId.value = Number(options.bookId)
|
||
}
|
||
if (options.page) {
|
||
pageType.value = options.page
|
||
}
|
||
|
||
initScrollHeight()
|
||
loadBookInfo()
|
||
loadComments()
|
||
})
|
||
|
||
// 初始化滚动区域高度
|
||
function initScrollHeight() {
|
||
const systemInfo = uni.getSystemInfoSync()
|
||
const statusBarHeight = systemInfo.statusBarHeight || 0
|
||
let navBarHeight = 44
|
||
if (systemInfo.model.includes('iPhone')) {
|
||
const modelNumber = parseInt(systemInfo.model.match(/\d+/)?.[0] || '0')
|
||
if (modelNumber >= 11) {
|
||
navBarHeight = 48
|
||
}
|
||
}
|
||
const totalNavHeight = statusBarHeight + navBarHeight
|
||
scrollHeight.value = systemInfo.windowHeight - totalNavHeight
|
||
}
|
||
|
||
// 加载书籍信息
|
||
async function loadBookInfo() {
|
||
const res = await bookApi.getBookInfo(bookId.value)
|
||
if (res.bookInfo) {
|
||
bookInfo.value = res.bookInfo
|
||
}
|
||
}
|
||
|
||
// 加载评论列表
|
||
async function loadComments() {
|
||
if (!hasMore.value && commentList.value.length > 0) {
|
||
return
|
||
}
|
||
|
||
const res = await bookApi.getBookComments(bookId.value, page.value.current, page.value.limit)
|
||
|
||
commentsCount.value = res.commentsCount || 0
|
||
|
||
if (res.commentsTree && res.commentsTree.length > 0) {
|
||
commentList.value = [...commentList.value, ...res.commentsTree]
|
||
page.value.current += 1
|
||
} else if (commentList.value.length === 0) {
|
||
nullText.value = t('common.data_null')
|
||
}
|
||
}
|
||
|
||
// 加载更多
|
||
function loadMore() {
|
||
if (hasMore.value) {
|
||
loadComments()
|
||
}
|
||
}
|
||
|
||
// 显示评论对话框
|
||
function showCommentDialog(comment?: IComment) {
|
||
if (!bookInfo.value.isBuy) {
|
||
uni.showToast({
|
||
title: t('book.afterPurchase'),
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
if (comment) {
|
||
const userName = comment.userEntity.nickname || comment.userEntity.name || 'TA'
|
||
replyTarget.value = { id: comment.id, name: userName }
|
||
placeholder.value = t('bookDetails.replyText') + '"' + userName + '"' + t('bookDetails.dpl')
|
||
} else {
|
||
replyTarget.value = null
|
||
placeholder.value = t('bookDetails.enterText')
|
||
}
|
||
|
||
htmlContent.value = ''
|
||
commentVisible.value = true
|
||
showEmoji.value = false
|
||
}
|
||
|
||
// 编辑器就绪
|
||
function onEditorReady() {
|
||
uni.createSelectorQuery()
|
||
.select('#editor')
|
||
.context((res) => {
|
||
editorCtx.value = res.context
|
||
editorCtx.value.clear()
|
||
})
|
||
.exec()
|
||
}
|
||
|
||
// 编辑器状态变化
|
||
function onStatusChange(e: any) {
|
||
// 可以在这里处理编辑器状态
|
||
}
|
||
|
||
// 获取编辑器内容
|
||
function getEditorContent(): Promise<string> {
|
||
return new Promise((resolve, reject) => {
|
||
if (!editorCtx.value) {
|
||
reject('Editor not ready')
|
||
return
|
||
}
|
||
|
||
editorCtx.value.getContents({
|
||
success: (res: any) => {
|
||
resolve(res.html || '')
|
||
},
|
||
fail: (err: any) => {
|
||
reject(err)
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
//获得输入的表情数组
|
||
function handleEmj(i: any) {
|
||
editorCtx.value.insertImage({
|
||
src: i.emotion,
|
||
alt: "emoji",
|
||
className: 'emoji_image',
|
||
success: function() {},
|
||
});
|
||
}
|
||
|
||
// 提交评论
|
||
async function submitComment() {
|
||
const content = await getEditorContent()
|
||
|
||
if (!content || content === '<p><br></p>') {
|
||
uni.showToast({
|
||
title: t('bookDetails.enterText'),
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
const pid = replyTarget.value?.id || 0
|
||
await bookApi.insertComment(bookId.value, content, pid)
|
||
|
||
uni.showToast({
|
||
title: t('workOrder.submit_success'),
|
||
icon: 'success',
|
||
duration: 500
|
||
})
|
||
|
||
setTimeout(() => {
|
||
editorCtx.value?.clear()
|
||
resetComments()
|
||
}, 500)
|
||
}
|
||
|
||
// 点赞/取消点赞
|
||
async function handleLike(comment: IComment) {
|
||
if (!bookInfo.value.isBuy) {
|
||
uni.showToast({
|
||
title: t('book.afterPurchase'),
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
if (comment.isLike === 0) {
|
||
await bookApi.likeComment(comment.id)
|
||
uni.showToast({
|
||
title: t('bookDetails.supportSuccess'),
|
||
icon: 'success',
|
||
duration: 1000
|
||
})
|
||
} else {
|
||
await bookApi.unlikeComment(comment.id)
|
||
uni.showToast({
|
||
title: t('bookDetails.supportCancel'),
|
||
icon: 'success',
|
||
duration: 1000
|
||
})
|
||
}
|
||
|
||
setTimeout(() => {
|
||
resetComments()
|
||
}, 200)
|
||
}
|
||
|
||
// 删除评论
|
||
function handleDelete(comment: IComment) {
|
||
uni.showModal({
|
||
title: t('common.limit_title'),
|
||
content: t('bookDetails.deleteText'),
|
||
cancelText: t('common.cancel_text'),
|
||
confirmText: t('common.confirm_text'),
|
||
success: async (res) => {
|
||
if (res.confirm) {
|
||
await bookApi.deleteComment(comment.id)
|
||
uni.showToast({
|
||
title: t('bookDetails.deleteSuccess'),
|
||
icon: 'success',
|
||
duration: 500
|
||
})
|
||
|
||
setTimeout(() => {
|
||
resetComments()
|
||
}, 500)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 重置评论列表
|
||
function resetComments() {
|
||
commentVisible.value = false
|
||
commentList.value = []
|
||
page.value.current = 1
|
||
nullText.value = ''
|
||
loadComments()
|
||
}
|
||
|
||
// 切换Emoji选择器
|
||
function toggleEmoji() {
|
||
showEmoji.value = !showEmoji.value
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.book-review-page {
|
||
background: #f7faf9;
|
||
min-height: 100vh;
|
||
|
||
.review-scroll {
|
||
.book-card {
|
||
margin: 20rpx;
|
||
padding: 30rpx;
|
||
background: #fff;
|
||
border-radius: 15rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.cover {
|
||
width: 180rpx;
|
||
height: 240rpx;
|
||
border-radius: 10rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.info {
|
||
flex: 1;
|
||
padding: 0 20rpx;
|
||
|
||
.title {
|
||
display: block;
|
||
font-size: 36rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
line-height: 44rpx;
|
||
max-height: 88rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.author {
|
||
display: block;
|
||
font-size: 30rpx;
|
||
padding-top: 15rpx;
|
||
color: #666;
|
||
}
|
||
}
|
||
}
|
||
|
||
.divider-line {
|
||
height: 20rpx;
|
||
background: #f7faf9;
|
||
}
|
||
|
||
.comments-wrapper {
|
||
background: #fff;
|
||
padding: 30rpx;
|
||
min-height: 400rpx;
|
||
|
||
.section-header {
|
||
margin-bottom: 20rpx;
|
||
|
||
.section-title {
|
||
font-size: 34rpx;
|
||
line-height: 50rpx;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
}
|
||
|
||
.empty-text {
|
||
display: block;
|
||
text-align: center;
|
||
padding: 100rpx 0;
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
}
|
||
}
|
||
|
||
.comment-dialog {
|
||
padding: 40rpx;
|
||
|
||
.dialog-title {
|
||
display: block;
|
||
font-size: 34rpx;
|
||
color: #333;
|
||
text-align: center;
|
||
margin-bottom: 30rpx;
|
||
}
|
||
|
||
.editor {
|
||
border: 1px solid #ddd;
|
||
width: 100%;
|
||
min-height: 200rpx;
|
||
height: 200rpx;
|
||
padding: 10rpx;
|
||
border-radius: 10rpx;
|
||
margin-bottom: 20rpx;
|
||
|
||
:deep() .ql-editor {
|
||
img {
|
||
width: 44rpx;
|
||
height: 44rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
.emoji-section {
|
||
margin-bottom: 20rpx;
|
||
|
||
.emoji-btn {
|
||
width: 110rpx;
|
||
height: 110rpx;
|
||
border-radius: 20rpx;
|
||
background-color: #f4f5f7;
|
||
text-align: center;
|
||
padding-top: 20rpx;
|
||
|
||
image {
|
||
width: 44rpx;
|
||
height: 44rpx;
|
||
display: block;
|
||
margin: 0 auto 5rpx;
|
||
}
|
||
|
||
text {
|
||
display: block;
|
||
font-size: 26rpx;
|
||
line-height: 36rpx;
|
||
color: #999;
|
||
}
|
||
}
|
||
|
||
.emoji-picker {
|
||
margin-top: 20rpx;
|
||
|
||
.emoji-tip {
|
||
font-size: 26rpx;
|
||
color: #999;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|