更新:增加“我的书单”功能

This commit is contained in:
2025-11-10 09:16:28 +08:00
parent 33f861fa14
commit 577e782cd8
25 changed files with 4515 additions and 37 deletions

513
pages/book/review.vue Normal file
View 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>