1143 lines
37 KiB
Vue
1143 lines
37 KiB
Vue
<template>
|
||
<view class="page">
|
||
<u-navbar title="动态详情" :border-bottom="false" :background="bgc" back-icon-color="#262B37" title-color='#262B37'
|
||
title-size='36' height='36' id="navbar" :custom-back="btnfh">
|
||
</u-navbar>
|
||
<scroll-view class="list" @scrolltolower="handqixing" scroll-y refresher-enabled @refresherrefresh="onRefresh" :refresher-triggered="isRefreshing" refresher-default-style="black" :style="listHeightPx ? ('height:' + listHeightPx + 'px') : ''">
|
||
<view class="top">
|
||
<view class="info">
|
||
<image :src="dtobj.userAvatar" mode=""></image>
|
||
<view class="xx">
|
||
<view class="name">
|
||
{{dtobj.nickName == null ? '--' : dtobj.nickName}}
|
||
</view>
|
||
<view class="riqi">
|
||
{{dtobj.createTime == null ? '--' : dtobj.createTime}}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="guanzhu" v-if="userId != dtobj.userId">
|
||
<view @click="btngzdel" class="yiguanzhu" v-if="dtobj.isFollowed" >
|
||
已关注
|
||
</view>
|
||
<view @click="btngzadd" class="weiguanzhu" v-else>
|
||
+ 关注
|
||
</view>
|
||
</view>
|
||
<view class="guanzhu" @click="btndel" v-else>
|
||
<image src="https://api.ccttiot.com/smartmeter/img/static/upijMXHu57BobzwGJMso" style="width: 60rpx;height: 60rpx;" mode=""></image>
|
||
</view>
|
||
</view>
|
||
<view class="wrap" v-if="dtobj.picture.length > 0">
|
||
<swiper class="swiper" :current="currentIndex" :indicator-dots="true" :autoplay="false" circular @change="onSwiperChange">
|
||
<swiper-item v-for="(item, idx) in dtobj.picture" :key="idx">
|
||
<view class="media-wrap" @click="handleSwiperClick">
|
||
<image v-if="!isVideoItem(item)" :src="getImageUrl(item)" mode="aspectFill"></image>
|
||
<video
|
||
v-else
|
||
:id="'swiper-video-' + idx"
|
||
:src="getVideoUrl(item)"
|
||
:poster="item.poster || getImageUrl(item) || ''"
|
||
:autoplay="isCurrentVideo(idx)"
|
||
:muted="true"
|
||
playsinline
|
||
webkit-playsinline
|
||
x5-playsinline
|
||
controls
|
||
objectFit="cover"
|
||
></video>
|
||
</view>
|
||
</swiper-item>
|
||
</swiper>
|
||
</view>
|
||
<view class="contwz">
|
||
{{dtobj.content == null ? '...' : dtobj.content}}
|
||
</view>
|
||
<view class="dizhi" @click="btndh">
|
||
<image class="qian" src="https://api.ccttiot.com/smartmeter/img/static/ugQMH5UxepQ6r2VfKtlP" mode=""></image> {{ dtobj.location || '暂无位置' }} <image class="hou" src="https://api.ccttiot.com/smartmeter/img/static/uOtVmaBGci0kew9b0EpI" mode=""></image>
|
||
</view>
|
||
<!-- 评论 -->
|
||
<view class="comment-section">
|
||
<view class="c-title">评论</view>
|
||
<view class="c-item" v-for="(c, ci) in comments" :key="c.id">
|
||
<view class="c-hd">
|
||
<image class="avatar" :src="c.avatar" mode="aspectFill"></image>
|
||
<view class="user">
|
||
<view class="name-row">
|
||
<text class="name">{{ c.name }}</text>
|
||
<text v-if="c.isAuthor" class="tag">作者</text>
|
||
</view>
|
||
<view class="content">{{ c.content }}</view>
|
||
<view class="meta">
|
||
<text class="time">{{ c.time }}</text>
|
||
<text class="reply" @click="onReply(c)">回复</text>
|
||
</view>
|
||
</view>
|
||
<view class="like">
|
||
<image style="width: 30rpx;height: 30rpx;" @click="pldianzan(c,ci)" :src="c.liked ? 'https://api.ccttiot.com/smartmeter/img/static/uCxUJSEQTStvqaf93xox' : 'https://api.ccttiot.com/smartmeter/img/static/ubfJKqgy9ckXL1oxmWHq'" mode=""></image>
|
||
<text class="num">{{ c.likes }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 子回复 -->
|
||
<view class="replies" v-if="c.replies && c.replies.length">
|
||
<block v-if="!c.expand">
|
||
<text class="expand" @click="toggleExpand(ci)">展开{{ c.replies.length }}条回复</text>
|
||
</block>
|
||
<block v-else>
|
||
<view class="r-item" v-for="(r, ri) in c.replies" :key="r.id">
|
||
<image class="avatar small" :src="r.avatar" mode="aspectFill"></image>
|
||
<view class="r-body">
|
||
<view class="name-row">
|
||
<text class="name">{{ r.name }}</text>
|
||
<text v-if="r.isAuthor" class="tag">作者</text>
|
||
</view>
|
||
<view class="content"><text style="font-size: 24rpx;color: #4292c1;margin-right: 10rpx;">@{{r.parentNickName}}</text> {{ r.content }}</view>
|
||
<view class="meta">
|
||
<text class="time">{{ r.time }}</text>
|
||
<text class="reply" @click="onReply(r, c)">回复</text>
|
||
</view>
|
||
</view>
|
||
<view class="like">
|
||
<image style="width: 30rpx;height: 30rpx;" @click="pldianzantwo(r,ci, ri)" :src="r.liked ? 'https://api.ccttiot.com/smartmeter/img/static/uCxUJSEQTStvqaf93xox' : 'https://api.ccttiot.com/smartmeter/img/static/ubfJKqgy9ckXL1oxmWHq'" mode=""></image>
|
||
<text class="num">{{ r.likes }}</text>
|
||
</view>
|
||
</view>
|
||
<text class="collapse" @click="toggleExpand(ci)">收起回复</text>
|
||
</block>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="" style="width: 100%;margin-top: 30rpx;text-align: center;color: #ccc;">
|
||
暂时没有更多评论咯...
|
||
</view>
|
||
</scroll-view>
|
||
<!-- 回复输入栏(常驻) -->
|
||
<view class="reply-bar" id="replyBar" :style="keyboardHeight ? ('transform: translateY(' + (-keyboardHeight) + 'px)') : ''">
|
||
<view class="reply-inner">
|
||
<view class="reply-box">
|
||
<text class="icon-pencil">✎</text>
|
||
<input
|
||
class="reply-input"
|
||
v-model="replyText"
|
||
:placeholder="replyPlaceholder"
|
||
maxlength="200"
|
||
confirm-type="send"
|
||
@confirm="sendReply"
|
||
:focus="replyFocus"
|
||
@blur="onInputBlur"
|
||
:adjust-position="false"
|
||
cursor-spacing="10"
|
||
/>
|
||
</view>
|
||
<view class="reply-actions">
|
||
<view class="action">
|
||
<image class="dz" src="https://api.ccttiot.com/smartmeter/img/static/ubfJKqgy9ckXL1oxmWHq" @click="toggleStar" mode="" v-if="!dtobj.isLiked"></image>
|
||
<image class="dz" src="https://api.ccttiot.com/smartmeter/img/static/uCxUJSEQTStvqaf93xox" @click="toggleStardel" mode="" v-else></image>
|
||
<text class="num">{{ dtobj.likes }}</text>
|
||
</view>
|
||
<view class="action">
|
||
<image class="dz" src="https://api.ccttiot.com/smartmeter/img/static/ux8FkyzvEgehlxvwN49V" @click="btndianzan" mode="" v-if="!dtobj.isCollected"></image>
|
||
<image class="dz" src="https://api.ccttiot.com/smartmeter/img/static/uVV3pzN4IPMAleZ2yRVw" @click="btndianzandel" mode="" v-else></image>
|
||
<text class="num">{{ dtobj.collections }}</text>
|
||
</view>
|
||
<view class="action" @click="doShare">
|
||
<image class="fx" src="https://api.ccttiot.com/smartmeter/img/static/uoHMIOnyYlNJCJcz9eLF" mode=""></image>
|
||
<text class="num">{{ dtobj.forwards }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 分享弹窗 -->
|
||
<view v-if="showSharePopup" class="share-mask" @click="closeShare"></view>
|
||
<view v-if="showSharePopup" class="share-popup">
|
||
<view class="share-title">分享至</view>
|
||
<!-- 微信小程序:用原生 share 按钮唤起分享面板(已开启朋友圈) -->
|
||
<!-- #ifdef MP-WEIXIN -->
|
||
<button class="share-btn" open-type="share" @click.stop="btnfx">微信好友</button>
|
||
<!-- #endif -->
|
||
<!-- APP/H5:使用 uni.share 指定场景 -->
|
||
<!-- #ifdef APP-PLUS -->
|
||
<view class="share-btn" @click.stop="shareApp('session')">微信好友</view>
|
||
<view class="share-btn" @click.stop="shareApp('timeline')">朋友圈</view>
|
||
<!-- #endif -->
|
||
<view class="share-cancel" @click="closeShare">取消</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
data() {
|
||
return {
|
||
bgc: {
|
||
backgroundColor: "#fff",
|
||
},
|
||
flag:false,
|
||
currentIndex: 0,
|
||
list: [],
|
||
comments: [],
|
||
showReplyBar: true,
|
||
replyText: '',
|
||
replyFocus: false,
|
||
replyTarget: null,
|
||
replyParent: null,
|
||
replyPlaceholder: '说点什么...',
|
||
starActive: false,
|
||
starCount: 20,
|
||
shareCount: 20,
|
||
dtid:'',
|
||
dtobj:{},
|
||
pageNum:1,
|
||
parentId:'',
|
||
rootId:'',
|
||
isRefreshing:false,
|
||
total:0,
|
||
pageSize:20,
|
||
hasMore:true,
|
||
showSharePopup:false,
|
||
share:'',
|
||
userId:'',
|
||
listHeightPx: 0,
|
||
keyboardHeight: 0
|
||
}
|
||
},
|
||
onLoad(option) {
|
||
this.dtid = option.id
|
||
this.getxq()
|
||
if(option.from){
|
||
this.share = option.from
|
||
}
|
||
console.log(option);
|
||
// 启用微信分享菜单,展示好友和朋友圈
|
||
// #ifdef MP-WEIXIN
|
||
wx.showShareMenu({withShareTicket:true, menus:['shareAppMessage','shareTimeline']})
|
||
// #endif
|
||
},
|
||
onShow() {
|
||
this.userId = uni.getStorageSync('userId')
|
||
// 进入页面或从后台回到前台时,重新计算一次高度
|
||
this.$nextTick(()=>{ this.calcListHeight() })
|
||
},
|
||
onReady() {
|
||
this.$nextTick(() => {
|
||
this.syncVideoPlayState()
|
||
this.calcListHeight()
|
||
})
|
||
},
|
||
onUnload(){
|
||
// #ifdef MP-WEIXIN
|
||
if(this.__kbListener){ this.__kbListener = null }
|
||
// #endif
|
||
},
|
||
methods: {
|
||
// 点击删除
|
||
btndel(){
|
||
let that = this
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '您确定要删除当前动态吗?',
|
||
success: function(res) {
|
||
if (res.confirm) {
|
||
that.$u.delete(`/app/feed/delete/${that.dtid}`).then(res => {
|
||
if (res.code == 200) {
|
||
uni.showToast({
|
||
title: '删除成功',
|
||
icon: 'success',
|
||
duration: 2000
|
||
})
|
||
setTimeout(() => {
|
||
uni.navigateBack()
|
||
}, 1000)
|
||
}else if(res.code == 401){
|
||
uni.reLaunch({
|
||
url:'/pages/login/login'
|
||
})
|
||
} else {
|
||
uni.showToast({
|
||
title: res.msg,
|
||
icon: 'none',
|
||
duration: 2000
|
||
})
|
||
}
|
||
})
|
||
} else if (res.cancel) {}
|
||
}
|
||
})
|
||
},
|
||
calcListHeight(){
|
||
// 获取窗口高度并减去顶部导航和底部回复栏的高度,锁定列表高度,防止键盘弹出时页面位移
|
||
// #ifdef MP-WEIXIN
|
||
const sys = uni.getSystemInfoSync()
|
||
const windowH = sys.windowHeight || 0
|
||
// 测量 navbar 与 replyBar
|
||
this.$nextTick(()=>{
|
||
const q = uni.createSelectorQuery().in(this)
|
||
q.select('#navbar').boundingClientRect()
|
||
q.select('#replyBar').boundingClientRect()
|
||
q.exec(res=>{
|
||
const navH = (res && res[0] && res[0].height) ? res[0].height : 0
|
||
const replyH = (res && res[1] && res[1].height) ? res[1].height : 0
|
||
const h = Math.max(0, Math.floor(windowH - navH - replyH))
|
||
this.listHeightPx = h
|
||
})
|
||
})
|
||
// 监听键盘高度变更,抬升底部栏而不改变页面布局
|
||
if (!this.__kbListener && uni.onKeyboardHeightChange) {
|
||
this.__kbListener = uni.onKeyboardHeightChange((res)=>{
|
||
this.keyboardHeight = res.height || 0
|
||
})
|
||
}
|
||
// #endif
|
||
},
|
||
// 点击跳转导航目的地
|
||
btndh(){
|
||
// 检查坐标数据是否存在
|
||
if (!this.dtobj.latitude || !this.dtobj.location) {
|
||
uni.showToast({
|
||
title: '当前暂无位置',
|
||
icon: 'none',
|
||
duration:3000
|
||
})
|
||
return
|
||
}
|
||
// 先申请位置权限
|
||
uni.getSetting({
|
||
success: (res) => {
|
||
if (res.authSetting['scope.userLocation'] === false) {
|
||
// 用户拒绝了位置权限,引导用户开启
|
||
uni.showModal({
|
||
title: '位置权限',
|
||
content: '需要获取您的位置信息才能进行导航,请在设置中开启位置权限',
|
||
confirmText: '去设置',
|
||
success: (modalRes) => {
|
||
if (modalRes.confirm) {
|
||
uni.openSetting()
|
||
}
|
||
}
|
||
})
|
||
return
|
||
}
|
||
// 权限正常,打开地图
|
||
this.openMap()
|
||
}
|
||
})
|
||
},
|
||
// 打开地图
|
||
openMap() {
|
||
uni.openLocation({
|
||
latitude: parseFloat(this.dtobj.latitude), // 确保是数字类型
|
||
longitude: parseFloat(this.dtobj.longitude), // 确保是数字类型
|
||
name: this.dtobj.location || '目的地', // 地点名称
|
||
success: function(res) {
|
||
console.log('打开地图成功', res)
|
||
},
|
||
fail: function(err) {
|
||
console.error('打开地图失败', err)
|
||
uni.showToast({
|
||
title: '打开地图失败: ' + (err.errMsg || '未知错误'),
|
||
icon: 'none',
|
||
duration: 3000
|
||
})
|
||
}
|
||
})
|
||
},
|
||
// 判断返回条件 ,返回上一级不同的页面
|
||
btnfh(){
|
||
if(this.share == 'share'){
|
||
uni.reLaunch({
|
||
url:'/pages/myorder/index'
|
||
})
|
||
}else{
|
||
uni.navigateBack()
|
||
}
|
||
},
|
||
// 点击分享好友
|
||
btnfx(){
|
||
this.$u.put(`/app/feed/add/forwards/${this.dtid}`).then(res => {
|
||
if(res.code == 200){
|
||
this.showSharePopup = false
|
||
uni.showToast({ title: '分享成功', icon: 'success',duration:3000 })
|
||
}else{
|
||
uni.showToast({ title: res.msg, icon: 'none',duration:3000 })
|
||
}
|
||
})
|
||
},
|
||
// 上拉加载更多数据
|
||
handqixing() {
|
||
console.log(this.comments.length,this.total)
|
||
if(this.comments.length < this.total){
|
||
this.pageNum ++
|
||
this.getpl()
|
||
}
|
||
console.log('加载更多')
|
||
},
|
||
// 下拉刷新
|
||
onRefresh() {
|
||
this.isRefreshing = true
|
||
setTimeout(() => {
|
||
this.isRefreshing = false
|
||
this.pageNum = 1
|
||
this.getxq()
|
||
}, 1000)
|
||
},
|
||
// 点击添加关注
|
||
btngzadd(){
|
||
this.$u.post(`/app/follow/add?followedId=${this.dtobj.userId}`).then(res => {
|
||
if(res.code == 200){
|
||
this.dtobj.isFollowed = !this.dtobj.isFollowed
|
||
uni.showToast({ title: '关注成功', icon: 'success',duration:3000 })
|
||
}else if(res.code == 401){
|
||
uni.reLaunch({
|
||
url:'/pages/login/login'
|
||
})
|
||
}else{
|
||
uni.showToast({ title: res.msg, icon: 'none',duration:3000 })
|
||
}
|
||
})
|
||
},
|
||
// 点击取消关注
|
||
btngzdel(){
|
||
this.$u.delete(`/app/follow/cancel?followedId=${this.dtobj.userId}`).then(res => {
|
||
if(res.code == 200){
|
||
this.dtobj.isFollowed = !this.dtobj.isFollowed
|
||
uni.showToast({ title: '取关成功', icon: 'success',duration:3000 })
|
||
}else if(res.code == 401){
|
||
uni.reLaunch({
|
||
url:'/pages/login/login'
|
||
})
|
||
} else{
|
||
uni.showToast({ title: res.msg, icon: 'none',duration:3000 })
|
||
}
|
||
})
|
||
},
|
||
// 请求动态详情
|
||
getxq(){
|
||
this.$u.get(`/app/feed/detail/${this.dtid}`).then(res => {
|
||
if(res.code == 200){
|
||
this.dtobj = res.data
|
||
// 动态详情加载完成后再获取评论列表
|
||
this.getpl()
|
||
}
|
||
})
|
||
},
|
||
// 查询动态所有评论
|
||
getpl(){
|
||
// 仅分页父评,子评由后端 children 一并返回
|
||
this.$u.get(`/app/comment/list?pageNum=${this.pageNum}&pageSize=${this.pageSize}&bstTypes=1&bstId=${this.dtid}&isParent=1`).then(res => {
|
||
if(res.code == 200){
|
||
// 不再强依赖后端 total,使用 pageSize 判断是否还有更多
|
||
const rows = Array.isArray(res.rows) ? res.rows : []
|
||
// 映射为组件所需结构(rows 已为父评)
|
||
const newComments = rows.map(r => {
|
||
const root = this.mapServerComment(r, true)
|
||
const children = Array.isArray(r.children) ? r.children : []
|
||
root.replies = children.map(c => this.mapServerComment(c, false))
|
||
root.expand = false
|
||
return root
|
||
})
|
||
// 分页:第一页覆盖,之后追加并去重
|
||
if (this.pageNum == 1) {
|
||
this.comments = newComments
|
||
} else {
|
||
// 去重:只添加不存在的评论
|
||
const existingIds = this.comments.map(c => c.id)
|
||
const uniqueNewComments = newComments.filter(c => !existingIds.includes(c.id))
|
||
this.comments = this.comments.concat(uniqueNewComments)
|
||
}
|
||
// 页码推进
|
||
// this.pageNum ++
|
||
this.hasMore = newComments.length === this.pageSize
|
||
// 同步一个近似 total,便于日志观察
|
||
this.total = this.comments.length + (this.hasMore ? 1 : 0)
|
||
console.log('newComments:', newComments.length, 'hasMore:', this.hasMore, 'pageNum:', this.pageNum)
|
||
}
|
||
})
|
||
},
|
||
// 重新获取评论列表并展开相关评论
|
||
getplWithExpand(){
|
||
// 保存当前回复的目标ID,用于后续展开
|
||
const targetId = this.replyTarget ? this.replyTarget.id : null
|
||
const parentId = this.replyParent ? this.replyParent.id : null
|
||
this.$u.get(`/app/comment/list?pageNum=${this.pageNum}&pageSize=${this.pageSize}&bstTypes=1&bstId=${this.dtid}&isParent=1`).then(res => {
|
||
if(res.code == 200){
|
||
const rows = Array.isArray(res.rows) ? res.rows : []
|
||
// 映射为组件所需结构(rows 已为父评)
|
||
const newComments = rows.map(r => {
|
||
const root = this.mapServerComment(r, true)
|
||
const children = Array.isArray(r.children) ? r.children : []
|
||
root.replies = children.map(c => this.mapServerComment(c, false))
|
||
// 如果是回复的目标评论,或者包含回复的父评论,则展开
|
||
if (targetId && (r.id === targetId || (r.replies && r.replies.some(reply => reply.id === targetId)))) {
|
||
root.expand = true
|
||
} else if (parentId && r.id === parentId) {
|
||
root.expand = true
|
||
} else {
|
||
root.expand = false
|
||
}
|
||
return root
|
||
})
|
||
// 分页:第一页覆盖,之后追加并去重
|
||
if (this.pageNum == 1) {
|
||
this.comments = newComments
|
||
} else {
|
||
// 去重:只添加不存在的评论
|
||
const existingIds = this.comments.map(c => c.id)
|
||
const uniqueNewComments = newComments.filter(c => !existingIds.includes(c.id))
|
||
this.comments = this.comments.concat(uniqueNewComments)
|
||
}
|
||
// 页码推进
|
||
// this.pageNum ++
|
||
// 还有更多:返回条数等于 pageSize
|
||
this.hasMore = newComments.length === this.pageSize
|
||
// 同步一个近似 total,便于日志观察
|
||
this.total = this.comments.length + (this.hasMore ? 1 : 0)
|
||
}
|
||
})
|
||
},
|
||
// 重新获取评论列表并保持当前展开状态
|
||
getplWithCurrentExpand(){
|
||
// 保存当前展开状态的评论ID
|
||
const expandedIds = this.comments.filter(c => c.expand).map(c => c.id)
|
||
this.$u.get(`/app/comment/list?pageNum=${this.pageNum}&pageSize=${this.pageSize}&bstTypes=1&bstId=${this.dtid}&isParent=1`).then(res => {
|
||
if(res.code == 200){
|
||
const rows = Array.isArray(res.rows) ? res.rows : []
|
||
// 映射为组件所需结构(rows 已为父评)
|
||
const newComments = rows.map(r => {
|
||
const root = this.mapServerComment(r, true)
|
||
const children = Array.isArray(r.children) ? r.children : []
|
||
root.replies = children.map(c => this.mapServerComment(c, false))
|
||
// 保持之前的展开状态
|
||
root.expand = expandedIds.includes(r.id)
|
||
return root
|
||
})
|
||
// 分页:第一页覆盖,之后追加并去重
|
||
if (this.pageNum == 1) {
|
||
this.comments = newComments
|
||
} else {
|
||
// 去重:只添加不存在的评论
|
||
const existingIds = this.comments.map(c => c.id)
|
||
const uniqueNewComments = newComments.filter(c => !existingIds.includes(c.id))
|
||
this.comments = this.comments.concat(uniqueNewComments)
|
||
}
|
||
// 页码推进
|
||
// this.pageNum ++
|
||
// 还有更多:返回条数等于 pageSize
|
||
this.hasMore = newComments.length === this.pageSize
|
||
// 同步一个近似 total,便于日志观察
|
||
this.total = this.comments.length + (this.hasMore ? 1 : 0)
|
||
}
|
||
})
|
||
},
|
||
// 按需加载直到包含目标根评论,并展开它
|
||
async loadUntilRootAndExpand(targetRootId){
|
||
try{
|
||
if(!targetRootId){
|
||
// 无目标时退化为第一页刷新
|
||
this.pageNum = 1
|
||
await this.getpl()
|
||
return
|
||
}
|
||
let tempPage = 1
|
||
let accumulated = []
|
||
let found = false
|
||
while(true){
|
||
const res = await this.$u.get(`/app/comment/list?pageNum=${tempPage}&pageSize=${this.pageSize}&bstTypes=1&bstId=${this.dtid}&isParent=1`)
|
||
if(res.code != 200){ break }
|
||
const rows = Array.isArray(res.rows) ? res.rows : []
|
||
const pageComments = rows.map(r => {
|
||
const root = this.mapServerComment(r, true)
|
||
const children = Array.isArray(r.children) ? r.children : []
|
||
root.replies = children.map(c => this.mapServerComment(c, false))
|
||
root.expand = r.id === targetRootId // 默认命中页时展开
|
||
return root
|
||
})
|
||
// 去重合并
|
||
const accIds = accumulated.map(c => c.id)
|
||
const uniquePage = pageComments.filter(c => !accIds.includes(c.id))
|
||
accumulated = accumulated.concat(uniquePage)
|
||
// 是否已包含目标根评论
|
||
found = accumulated.some(c => c.id === targetRootId)
|
||
// 终止条件:找到目标,或本页不足 pageSize
|
||
if(found || rows.length < this.pageSize){
|
||
break
|
||
}
|
||
tempPage += 1
|
||
}
|
||
// 应用到界面
|
||
this.comments = accumulated
|
||
this.pageNum = tempPage + 1
|
||
this.hasMore = !found && (this.comments.length % this.pageSize === 0)
|
||
// 保证目标根评论展开
|
||
const idx = this.comments.findIndex(c => c.id === targetRootId)
|
||
if(idx !== -1){ this.comments[idx].expand = true }
|
||
}catch(e){
|
||
console.error('loadUntilRootAndExpand error:', e)
|
||
}
|
||
},
|
||
// 将服务端评论结构映射为界面所需结构
|
||
mapServerComment(row, isRoot){
|
||
return {
|
||
id: row.id,
|
||
avatar: row.userAvatar || '',
|
||
name: row.nickName || '匿名',
|
||
isAuthor: this.dtobj && row.userId === this.dtobj.userId,
|
||
content: row.content || '',
|
||
time: row.createTime || '',
|
||
likes: Number(row.likes || 0),
|
||
liked: !!row.isLiked,
|
||
parentId:row.parentId || '',
|
||
rootId:row.rootId || '',
|
||
parentNickName:row.parentNickName,
|
||
// 根评论:后续会补充 replies/expand;子回复不需要
|
||
...(isRoot ? { replies: [], expand: false } : {})
|
||
}
|
||
},
|
||
isVideoItem(item) {
|
||
if (!item) return false
|
||
if (item.video) return true
|
||
const url = item || ''
|
||
return /\.(mp4|m4v|mov|avi|wmv|flv|m3u8)(\?|#|$)/i.test(url)
|
||
},
|
||
getImageUrl(item) {
|
||
if (!item) return ''
|
||
if (item && !this.isVideoItem(item)) return item
|
||
return item.poster || ''
|
||
},
|
||
getVideoUrl(item) {
|
||
if (!item) return ''
|
||
return item || item || ''
|
||
},
|
||
// 切换轮播图
|
||
onSwiperChange(e) {
|
||
this.currentIndex = e.detail.current || 0
|
||
this.$nextTick(() => {
|
||
this.syncVideoPlayState()
|
||
})
|
||
},
|
||
handleSwiperClick() {
|
||
const index = this.currentIndex
|
||
const clickedItem = this.dtobj.picture[index]
|
||
if (!clickedItem) return
|
||
// 统一在微信小程序端使用 previewMedia 实现图文混合预览
|
||
// #ifdef MP-WEIXIN
|
||
const sources = this.dtobj.picture.map((it) => {
|
||
if (this.isVideoItem(it)) {
|
||
return { url: this.getVideoUrl(it), type: 'video', poster: it.poster || this.getImageUrl(it) || '' }
|
||
}
|
||
return { url: this.getImageUrl(it), type: 'image' }
|
||
})
|
||
uni.previewMedia({ current: index, sources })
|
||
return
|
||
// #endif
|
||
// 其他平台回退为仅图片可预览
|
||
if (this.isVideoItem(clickedItem)) {
|
||
return
|
||
}
|
||
const urls = this.dtobj.picture
|
||
.filter(it => !this.isVideoItem(it) && this.getImageUrl(it))
|
||
.map(it => this.getImageUrl(it))
|
||
if (!urls.length) return
|
||
uni.previewImage({ current: this.getImageUrl(clickedItem), urls })
|
||
},
|
||
// 点击展开/关闭 评论回复
|
||
toggleExpand(ci){
|
||
const c = this.comments[ci]
|
||
if(!c) return
|
||
c.expand = !c.expand
|
||
},
|
||
// 点击回复别的评论
|
||
onReply(target, parent){
|
||
this.replyTarget = target || null
|
||
this.replyParent = parent || null
|
||
this.replyText = ''
|
||
this.showReplyBar = true
|
||
this.replyFocus = true
|
||
this.replyPlaceholder = target && target.name ? ('回复 ' + target.name + ' …') : '说点什么...'
|
||
console.log(target,parent);
|
||
if(parent){ //判断是否是回复父级下面的评论 有就拿子级的parentId和rootId 没有就拿父级的id
|
||
this.rootId = target.rootId
|
||
this.parentId = target.parentId
|
||
}else{
|
||
this.rootId = target.id
|
||
this.parentId = target.id
|
||
}
|
||
},
|
||
// 点击键盘回车键 进行请求操作
|
||
sendReply(){
|
||
const text = (this.replyText || '').trim()
|
||
if(!text){
|
||
return uni.showToast({ title: '请输入回复内容', icon: 'none' })
|
||
}
|
||
// 显示加载状态
|
||
uni.showLoading({ title: '发送中...' })
|
||
let bstType = ''
|
||
// if(this.parentId == '' && this.rootId == ''){
|
||
// bstType = 1
|
||
// }else{
|
||
// bstType = 2
|
||
// }
|
||
let data = {
|
||
bstType: 1,
|
||
bstId: this.dtid,
|
||
content: this.replyText,
|
||
parentId: this.parentId,
|
||
rootId: this.rootId
|
||
}
|
||
// 在清空状态前记录目标根评论ID
|
||
const targetRootId = this.rootId || (this.replyParent ? this.replyParent.id : (this.replyTarget ? this.replyTarget.id : ''))
|
||
this.$u.post(`/app/comment/add`, data).then(res => {
|
||
uni.hideLoading()
|
||
if(res.code == 200){
|
||
// 回复成功后按需加载直到包含目标根评论,并展开
|
||
this.loadUntilRootAndExpand(targetRootId)
|
||
// 清空输入框和重置状态
|
||
this.replyText = ''
|
||
this.rootId = ''
|
||
this.parentId = ''
|
||
this.replyFocus = false
|
||
this.showReplyBar = false
|
||
this.replyTarget = null
|
||
this.replyParent = null
|
||
this.replyPlaceholder = '说点什么...'
|
||
uni.showToast({ title: '回复成功', icon: 'success' })
|
||
}else if(res.code == 401){
|
||
uni.reLaunch({
|
||
url:'/pages/login/login'
|
||
})
|
||
} else {
|
||
uni.showToast({ title: res.message || '回复失败', icon: 'none' })
|
||
}
|
||
}).catch(err => {
|
||
uni.hideLoading()
|
||
uni.showToast({ title: '网络错误,请重试', icon: 'none' })
|
||
console.error('回复失败:', err)
|
||
})
|
||
},
|
||
cancelReply(){
|
||
this.replyFocus = false
|
||
this.replyText = ''
|
||
this.replyPlaceholder = '说点什么...'
|
||
},
|
||
// inout失焦后执行事件
|
||
onInputBlur(){
|
||
// 失焦仅重置占位,不隐藏输入栏
|
||
this.cancelReply()
|
||
},
|
||
// 父评论点赞和取消点赞
|
||
pldianzan(item, ci){
|
||
console.log(item);
|
||
if(!item.liked){
|
||
let data = {
|
||
bstType:3,
|
||
bstId:item.id
|
||
}
|
||
this.$u.post(`/app/like/add`,data).then(res => {
|
||
if(res.code == 200){
|
||
// 本地立即高亮并加一
|
||
const c = this.comments[ci]
|
||
if(c){
|
||
c.liked = true
|
||
c.likes = Number(c.likes || 0) + 1
|
||
}
|
||
// 点赞成功后重新获取评论数据,保持展开状态
|
||
this.getplWithCurrentExpand()
|
||
}else if(res.code == 401){
|
||
uni.reLaunch({
|
||
url:'/pages/login/login'
|
||
})
|
||
} else{
|
||
uni.showToast({ title: res.msg, icon: 'none',duration:3000 })
|
||
}
|
||
})
|
||
}else{
|
||
this.$u.delete(`/app/like/cancel/${item.id}?bstType=3`).then(res => {
|
||
if(res.code == 200){
|
||
// 本地立即取消高亮并减一
|
||
const c = this.comments[ci]
|
||
if(c){
|
||
c.liked = false
|
||
c.likes = Math.max(0, Number(c.likes || 0) - 1)
|
||
}
|
||
// 取消点赞成功后重新获取评论数据,保持展开状态
|
||
this.getplWithCurrentExpand()
|
||
}else if(res.code == 401){
|
||
uni.reLaunch({
|
||
url:'/pages/login/login'
|
||
})
|
||
} else{
|
||
uni.showToast({ title: res.msg, icon: 'none',duration:3000 })
|
||
}
|
||
})
|
||
}
|
||
},
|
||
// 子评论点赞和取消点赞
|
||
pldianzantwo(item,ci,ri){
|
||
console.log(item);
|
||
if(!item.liked){
|
||
let data = {
|
||
bstType:3,
|
||
bstId:item.id
|
||
}
|
||
this.$u.post(`/app/like/add`,data).then(res => {
|
||
if(res.code == 200){
|
||
// 本地立即高亮并加一
|
||
const r = this.comments[ci]?.replies?.[ri]
|
||
if(r){
|
||
r.liked = true
|
||
r.likes = Number(r.likes || 0) + 1
|
||
}
|
||
// 点赞成功后重新获取评论数据,保持展开状态
|
||
this.getplWithCurrentExpand()
|
||
}else if(res.code == 401){
|
||
uni.reLaunch({
|
||
url:'/pages/login/login'
|
||
})
|
||
} else{
|
||
uni.showToast({ title: res.msg, icon: 'none',duration:3000 })
|
||
}
|
||
})
|
||
}else{
|
||
this.$u.delete(`/app/like/cancel/${item.id}?bstType=3`).then(res => {
|
||
if(res.code == 200){
|
||
// 本地立即取消高亮并减一
|
||
const r = this.comments[ci]?.replies?.[ri]
|
||
if(r){
|
||
r.liked = false
|
||
r.likes = Math.max(0, Number(r.likes || 0) - 1)
|
||
}
|
||
// 取消点赞成功后重新获取评论数据,保持展开状态
|
||
this.getplWithCurrentExpand()
|
||
}else if(res.code == 401){
|
||
uni.reLaunch({
|
||
url:'/pages/login/login'
|
||
})
|
||
} else{
|
||
uni.showToast({ title: res.msg, icon: 'none',duration:3000 })
|
||
}
|
||
})
|
||
}
|
||
},
|
||
// 动态底部点击进行收藏
|
||
btndianzan(){
|
||
let data = {
|
||
bstType:2,
|
||
bstId:this.dtid
|
||
}
|
||
this.$u.post(`/app/favorite/add`,data).then(res => {
|
||
if(res.code == 200){
|
||
this.dtobj.isCollected = !this.dtobj.isCollected
|
||
this.dtobj.collections = Number(this.dtobj.collections) + 1
|
||
uni.showToast({ title: '收藏成功', icon: 'success',duration:3000 })
|
||
}else if(res.code == 401){
|
||
uni.reLaunch({
|
||
url:'/pages/login/login'
|
||
})
|
||
} else{
|
||
uni.showToast({ title: res.msg, icon: 'none',duration:3000 })
|
||
}
|
||
})
|
||
},
|
||
// 动态底部点击取消收藏
|
||
btndianzandel(){
|
||
this.$u.delete(`/app/favorite/cancel/${this.dtid}?bstType=2`).then(res => {
|
||
if(res.code == 200){
|
||
this.dtobj.isCollected = !this.dtobj.isCollected
|
||
this.dtobj.collections = Number(this.dtobj.collections) - 1
|
||
uni.showToast({ title: '取消收藏成功', icon: 'success',duration:3000 })
|
||
}else if(res.code == 401){
|
||
uni.reLaunch({
|
||
url:'/pages/login/login'
|
||
})
|
||
} else{
|
||
uni.showToast({ title: res.msg, icon: 'none',duration:3000 })
|
||
}
|
||
})
|
||
},
|
||
// 动态底部进行点赞 动态底部进行点赞 动态底部进行点赞
|
||
toggleStar(){
|
||
let data = {
|
||
bstType:2,
|
||
bstId:this.dtid
|
||
}
|
||
this.$u.post(`/app/like/add`,data).then(res => {
|
||
if(res.code == 200){
|
||
this.dtobj.isLiked = !this.dtobj.isLiked
|
||
this.dtobj.likes = Number(this.dtobj.likes) + 1
|
||
uni.showToast({ title: '点赞成功', icon: 'success',duration:3000 })
|
||
}else if(res.code == 401){
|
||
uni.reLaunch({
|
||
url:'/pages/login/login'
|
||
})
|
||
}else{
|
||
uni.showToast({ title: res.msg, icon: 'none',duration:3000 })
|
||
}
|
||
})
|
||
},
|
||
// 动态底部取消点赞 动态底部取消点赞 动态底部取消点赞
|
||
toggleStardel(){
|
||
this.$u.delete(`/app/like/cancel/${this.dtid}?bstType=2`).then(res => {
|
||
if(res.code == 200){
|
||
this.dtobj.isLiked = !this.dtobj.isLiked
|
||
this.dtobj.likes = Number(this.dtobj.likes) - 1
|
||
uni.showToast({ title: '取消点赞成功', icon: 'success',duration:3000 })
|
||
}else if(res.code == 401){
|
||
uni.reLaunch({
|
||
url:'/pages/login/login'
|
||
})
|
||
} else{
|
||
uni.showToast({ title: res.msg, icon: 'none',duration:3000 })
|
||
}
|
||
})
|
||
},
|
||
// 点击转发/分享
|
||
doShare(){
|
||
this.showSharePopup = true
|
||
},
|
||
// 点击转发微信好友等...
|
||
closeShare(){
|
||
this.showSharePopup = false
|
||
},
|
||
// APP 端分享
|
||
// #ifdef APP-PLUS
|
||
shareApp(scene){
|
||
const summary = this.dtobj.content || '精彩内容分享'
|
||
const href = `/page_fenbao/guangchang/dongtaixq?from=share&id=${this.dtid}`
|
||
const imageUrl = (Array.isArray(this.dtobj.picture) && this.dtobj.picture.find(p=>!/\.(mp4|m4v|mov|avi|wmv|flv|m3u8)(\?|#|$)/i.test(p))) || ''
|
||
uni.share({
|
||
provider:'weixin',
|
||
scene: scene === 'timeline' ? 'WXSenceTimeline' : 'WXSceneSession',
|
||
type:0,
|
||
href: href,
|
||
title: this.dtobj.nickName || '分享',
|
||
summary: summary,
|
||
imageUrl: imageUrl,
|
||
success: ()=>{ this.showSharePopup = false; uni.showToast({ title:'已分享', icon:'none' }) },
|
||
fail: (e)=>{ this.showSharePopup = false; uni.showToast({ title:'分享失败', icon:'none' }) }
|
||
})
|
||
},
|
||
// #endif
|
||
syncVideoPlayState() {
|
||
// 仅在微信小程序端使用 VideoContext 精细控制
|
||
// #ifdef MP-WEIXIN
|
||
this.dtobj.picture.forEach((item, idx) => {
|
||
if (!this.isVideoItem(item)) return
|
||
const ctx = uni.createVideoContext('swiper-video-' + idx, this)
|
||
if (idx === this.currentIndex) {
|
||
ctx.play()
|
||
} else {
|
||
ctx.pause()
|
||
}
|
||
})
|
||
// #endif
|
||
},
|
||
isCurrentVideo(idx) {
|
||
return this.currentIndex === idx && this.isVideoItem(this.dtobj.picture[idx])
|
||
}
|
||
},
|
||
// 微信小程序分享配置
|
||
// #ifdef MP-WEIXIN
|
||
onShareAppMessage(){
|
||
const path = `/page_fenbao/guangchang/dongtaixq?from=share&id=${this.dtid}`
|
||
const imageUrl = (Array.isArray(this.dtobj.picture) && this.dtobj.picture.find(p=>!/\.(mp4|m4v|mov|avi|wmv|flv|m3u8)(\?|#|$)/i.test(p))) || ''
|
||
return {
|
||
title: this.dtobj.content || '分享一个有趣的内容',
|
||
path,
|
||
imageUrl
|
||
}
|
||
},
|
||
onShareTimeline(){
|
||
const query = `id=${this.dtid}`
|
||
const imageUrl = (Array.isArray(this.dtobj.picture) && this.dtobj.picture.find(p=>!/\.(mp4|m4v|mov|avi|wmv|flv|m3u8)(\?|#|$)/i.test(p))) || ''
|
||
return {
|
||
title: this.dtobj.content || '分享一个有趣的内容',
|
||
query,
|
||
imageUrl
|
||
}
|
||
}
|
||
// #endif
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss">
|
||
page {
|
||
background: #fff;
|
||
}
|
||
.list {
|
||
padding-bottom: 20rpx;
|
||
box-sizing: border-box;
|
||
height: 84vh;
|
||
overflow: scroll;
|
||
}
|
||
.dizhi{
|
||
font-weight: 600;
|
||
font-size: 26rpx;
|
||
color: #1EC28B;
|
||
display: flex;
|
||
align-items: center;
|
||
margin-top: 10rpx;
|
||
padding-left: 30rpx;
|
||
.qian{
|
||
width: 26rpx;
|
||
height: 26rpx;
|
||
margin-right: 8rpx;
|
||
}
|
||
.hou{
|
||
width: 32rpx;
|
||
height: 32rpx;
|
||
margin-left: 4rpx;
|
||
}
|
||
}
|
||
.contwz{
|
||
margin: auto;
|
||
margin-top: 28rpx;
|
||
font-size: 28rpx;
|
||
color: #3D3D3D;
|
||
width: 672rpx;
|
||
}
|
||
.wrap{
|
||
width: 672rpx !important;
|
||
height: 392rpx !important;
|
||
margin: auto;
|
||
border-radius: 10rpx;
|
||
overflow: hidden;
|
||
margin-top: 30rpx;
|
||
}
|
||
.swiper{
|
||
width: 672rpx !important;
|
||
height: 392rpx !important;
|
||
}
|
||
.media-wrap{
|
||
width: 672rpx !important;
|
||
height: 392rpx !important;
|
||
}
|
||
.media-wrap image,
|
||
.media-wrap video{
|
||
width: 100%;
|
||
height: 100%;
|
||
display: block;
|
||
}
|
||
.comment-section{
|
||
width: 672rpx;
|
||
margin: 32rpx auto 20rpx; // 预留底部输入栏高度
|
||
.c-title{
|
||
font-size: 34rpx;
|
||
font-weight: 600;
|
||
color: #3D3D3D;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
.c-item{
|
||
padding: 20rpx 0;
|
||
.c-hd{ display: flex; }
|
||
.avatar{ width: 64rpx; height: 64rpx; border-radius: 50%; margin-right: 16rpx; }
|
||
.user{ flex: 1; }
|
||
.name-row{ display: flex; align-items: center; }
|
||
.name{ font-size: 28rpx; color: #3D3D3D; }
|
||
.tag{ margin-left: 12rpx; font-size: 22rpx; color: #fff; background: #1EC28B; padding: 2rpx 10rpx; border-radius: 20rpx; }
|
||
.content{ font-size: 28rpx; color: #3D3D3D; margin: 6rpx 0 8rpx; }
|
||
.meta{ font-size: 24rpx; color: #909090; }
|
||
.meta .reply{ margin-left: 20rpx; color: #2b85e4; }
|
||
.like{ width: 80rpx; text-align: center;
|
||
image{
|
||
width: 29rpx;
|
||
height: 25rpx;
|
||
}}
|
||
.heart{ font-size: 34rpx; color: #D8D8D8; }
|
||
.heart.active{ color: #ff4d4f; }
|
||
.num{ display: block; font-size: 24rpx; color: #909090; margin-top: 4rpx; }
|
||
}
|
||
.replies{ margin-left: 80rpx; }
|
||
.expand, .collapse{ color: #2b85e4; font-size: 26rpx; }
|
||
.r-item{ display: flex; align-items: flex-start; justify-content: space-between; padding: 16rpx 0; }
|
||
.avatar.small{ width: 48rpx; height: 48rpx; }
|
||
.r-body{ flex: 1; margin-left: 16rpx; }
|
||
}
|
||
// 底部输入栏
|
||
.reply-mask{
|
||
position: fixed; left: 0; right: 0; top: 0; bottom: 0;
|
||
background: rgba(0,0,0,0.35);
|
||
z-index: 9;
|
||
}
|
||
.reply-bar{
|
||
position: fixed;
|
||
left: 0; right: 0; bottom: 0;
|
||
background: #fff;
|
||
padding: 40rpx 24rpx;
|
||
box-shadow: 0 -6rpx 20rpx rgba(0,0,0,0.06);
|
||
z-index: 10;
|
||
.reply-inner{ display: flex; align-items: center; }
|
||
.reply-box{ flex: 1; height: 72rpx; background: #e9f0fa; border-radius: 36rpx; display: flex; align-items: center; padding: 0 20rpx; }
|
||
.icon-pencil{ color: #7a9bc2; font-size: 28rpx; margin-right: 12rpx; }
|
||
.reply-input{ flex: 1; height: 72rpx; font-size: 28rpx; color: #333; }
|
||
.reply-actions{ display: flex; align-items: center; margin-left: 20rpx; }
|
||
.action{ display: flex; align-items: center; margin-left: 20rpx;
|
||
.dz,
|
||
.fx{
|
||
width: 32rpx;
|
||
height: 32rpx;
|
||
} }
|
||
.star{ font-size: 36rpx; color: #c8a15d; }
|
||
.star.active{ color: #d9a441; }
|
||
.share{ font-size: 32rpx; color: #7a9bc2; transform: rotate(90deg); }
|
||
.num{ font-size: 24rpx; color: #606060; margin-left: 8rpx; }
|
||
}
|
||
.top{
|
||
width: 672rpx;
|
||
margin: auto;
|
||
margin-top: 36rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
.guanzhu{
|
||
.yiguanzhu{
|
||
width: 118rpx;
|
||
height: 46rpx;
|
||
border-radius: 26rpx 26rpx 26rpx 26rpx;
|
||
border: 2rpx solid #606060;
|
||
font-size: 24rpx;
|
||
color: #606060;
|
||
text-align: center;
|
||
line-height: 46rpx;
|
||
}
|
||
.weiguanzhu{
|
||
width: 118rpx;
|
||
height: 46rpx;
|
||
border-radius: 26rpx 26rpx 26rpx 26rpx;
|
||
border: 2rpx solid #606060;
|
||
font-size: 24rpx;
|
||
color: #606060;
|
||
text-align: center;
|
||
line-height: 46rpx;
|
||
}
|
||
}
|
||
.info{
|
||
display: flex;
|
||
align-items: center;
|
||
image{
|
||
width: 84rpx;
|
||
height: 84rpx;
|
||
margin-right: 14rpx;
|
||
border-radius: 50%;
|
||
}
|
||
.xx{
|
||
.name{
|
||
font-weight: 600;
|
||
font-size: 34rpx;
|
||
color: #3D3D3D;
|
||
}
|
||
.riqi{
|
||
font-size: 24rpx;
|
||
color: #606060;
|
||
margin-top: 12rpx;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
/* 分享弹窗样式 */
|
||
.share-mask{ position: fixed; left:0; right:0; top:0; bottom:0; background: rgba(0,0,0,0.45); z-index: 999; }
|
||
.share-popup{ position: fixed; left:0; right:0; bottom:0; background:#fff; border-radius: 16rpx 16rpx 0 0; padding: 24rpx; z-index: 1000; }
|
||
.share-title{ text-align:center; font-size: 30rpx; color:#333; margin-bottom: 16rpx; }
|
||
.share-btn{ background:#f5f5f5; margin: 12rpx 0; padding: 20rpx; border-radius: 12rpx; text-align:center; }
|
||
.share-cancel{ text-align:center; color:#666; padding: 22rpx 0; }
|
||
</style> |