Files
taimed-international-app/pages/book/review.vue
fuchao 64abd3d4ab revert 33c22eaad9
revert 解决冲突
2025-11-27 15:38:24 +08:00

526 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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() {
try {
const res = await bookApi.getBookInfo(bookId.value)
if (res.bookInfo) {
bookInfo.value = res.bookInfo
}
} catch (error) {
console.error('Failed to load book info:', error)
}
}
// 加载评论列表
async function loadComments() {
if (!hasMore.value && commentList.value.length > 0) {
return
}
try {
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')
}
} catch (error) {
nullText.value = t('common.data_null')
console.error('Failed to load comments:', error)
}
}
// 加载更多
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() {
try {
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)
} catch (error) {
console.error('Failed to submit comment:', error)
}
}
// 点赞/取消点赞
async function handleLike(comment: IComment) {
if (!bookInfo.value.isBuy) {
uni.showToast({
title: t('book.afterPurchase'),
icon: 'none'
})
return
}
try {
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)
} catch (error) {
console.error('Failed to like comment:', error)
}
}
// 删除评论
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) {
try {
await bookApi.deleteComment(comment.id)
uni.showToast({
title: t('bookDetails.deleteSuccess'),
icon: 'success',
duration: 500
})
setTimeout(() => {
resetComments()
}, 500)
} catch (error) {
console.error('Failed to delete comment:', error)
}
}
}
})
}
// 重置评论列表
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>