更新:增加“我的书单”功能
This commit is contained in:
513
pages/book/review.vue
Normal file
513
pages/book/review.vue
Normal file
@@ -0,0 +1,513 @@
|
||||
<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">
|
||||
<text class="emoji-tip">Emoji功能待集成</text>
|
||||
</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 CustomNavbar from '@/components/book/CustomNavbar.vue'
|
||||
import CommentList from '@/components/book/CommentList.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 {
|
||||
uni.showLoading({ title: t('global.loading') })
|
||||
const res = await bookApi.getBookComments(bookId.value, page.value.current, page.value.limit)
|
||||
uni.hideLoading()
|
||||
|
||||
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) {
|
||||
uni.hideLoading()
|
||||
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)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 提交评论
|
||||
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: 1rpx solid #ddd;
|
||||
width: 100%;
|
||||
min-height: 200rpx;
|
||||
padding: 10rpx;
|
||||
border-radius: 10rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.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: 45rpx;
|
||||
height: 45rpx;
|
||||
display: block;
|
||||
margin: 0 auto 5rpx;
|
||||
}
|
||||
|
||||
text {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
line-height: 36rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-picker {
|
||||
margin-top: 20rpx;
|
||||
padding: 20rpx;
|
||||
background: #f4f5f7;
|
||||
border-radius: 10rpx;
|
||||
|
||||
.emoji-tip {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user