You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

425 lines
19 KiB

<template>
23 hours ago
<view class="detail-page">
<view class="nav" :style="{ paddingTop: statusBarTop + 'px' }">
<view class="back" @tap="back"><uni-icons type="left" size="24" color="#18372f"></uni-icons></view>
<view class="nav-title">{{post && post.postType === 'partner' ? '搭子详情' : '校园墙详情'}}</view>
<view class="nav-right"></view>
</view>
<scroll-view scroll-y class="content-scroll" :style="{ paddingTop: navHeight + 'px' }" @scrolltolower="loadMoreComments">
<view class="card post-card" v-if="post">
<view class="author-row">
<image class="avatar" :src="displayAvatar(post)" mode="aspectFill"></image>
<view class="author-main">
<view class="author">{{displayName(post)}}<text>已实名</text></view>
<view class="meta">{{post.createTime || ''}} · {{post.regionName || '本校'}}</view>
</view>
</view>
<view class="title">{{post.title}}</view>
<view class="content">{{post.contentText || stripHtml(post.content)}}</view>
<view v-if="imageList.length" class="image-grid" :class="'image-grid--' + Math.min(imageList.length, 3)">
<image
v-for="(pic,index) in imageList"
:key="pic"
:src="pic"
:mode="imageList.length === 1 ? 'aspectFit' : 'aspectFill'"
:style="getPostImageStyle(pic, imageList.length)"
@load="onPostImageLoad(pic, $event)"
@tap="previewImages(index)"
></image>
</view>
<view class="tag-row" v-if="tagList.length">
<text v-for="tag in tagList" :key="tag">{{tag}}</text>
</view>
<view class="partner-box" v-if="post.postType === 'partner'">
<view><text>地点</text>{{post.location || '可商量'}}</view>
<view><text>时间</text>{{post.activityTime || '可商量'}}</view>
<view><text>人数</text>{{post.joinedCount || 0}}/{{post.targetCount || '不限'}}</view>
</view>
<view class="contact-box" v-if="hasContact">
<view v-if="post.contactWechat"><text>微信</text>{{post.contactWechat}}</view>
<view v-if="post.contactPhone"><text>手机</text>{{post.contactPhone}}</view>
</view>
<view class="stats">{{post.likeCount || 0}} · {{post.commentCount || 0}} 评论 · 热度 {{post.hotScore || 0}}</view>
</view>
<view class="card comment-card">
<view class="section-head">
<text>评论 {{commentsTotal}}</text>
<view @tap="focusComment">我要评论</view>
</view>
<view class="comment-item" v-for="item in comments" :key="item.id">
<image class="comment-avatar" :src="displayAvatar(item)" mode="aspectFill"></image>
<view class="comment-main">
<view class="comment-name">{{displayName(item)}}</view>
<view class="comment-content">{{item.content}}</view>
<view class="reply-list" v-if="item.replies && item.replies.length">
<view class="reply-item" v-for="reply in item.replies" :key="reply.id">
<text>{{displayName(reply)}}</text>
<text v-if="reply.replyToUserName"> 回复 {{reply.replyToUserName}}</text>
{{reply.content}}
</view>
</view>
<view class="comment-actions">
<text>{{item.createTime || ''}}</text>
<text @tap="replyComment(item)">回复</text>
<text @tap="reportComment(item)">举报</text>
<text v-if="item.userId == currentUserId" @tap="deleteComment(item)">删除</text>
</view>
</view>
</view>
<view class="empty" v-if="!comments.length">还没有评论快来聊聊</view>
<view class="load-state">{{commentLoading ? '评论加载中...' : (commentHasMore ? '上滑加载更多评论' : '评论到底了')}}</view>
</view>
<view class="bottom-space"></view>
</scroll-view>
<uni-popup ref="commentPopup" type="center" background-color="transparent">
<view class="comment-popup">
<view class="popup-title">{{replyTarget.id ? '回复评论' : '发表评论'}}</view>
<view class="popup-desc">评论内容会进行审核请友善交流</view>
<textarea v-model.trim="commentText" maxlength="200" :placeholder="replyTarget.id ? '回复 ' + (replyTarget.userName || '校园同学') : '说点什么...'" />
<view class="anonymous-comment-row">
<text>匿名评论</text>
<switch :checked="commentAnonymous" color="#34c7a2" @change="toggleCommentAnonymous" />
</view>
<view class="popup-actions">
<view @tap="closeCommentPopup">取消</view>
<view :class="{ disabled: commentSubmitting }" @tap="submitComment">{{commentSubmitting ? '发送中' : '发送'}}</view>
</view>
</view>
</uni-popup>
<view class="bottom-bar" v-if="post">
<view class="bar-action" @tap="focusComment"><uni-icons type="chat" size="22" color="#59706a"></uni-icons><text></text></view>
<view class="bar-action" @tap="likePost"><uni-icons :type="post.liked ? 'heart-filled' : 'heart'" size="22" :color="post.liked ? '#ff6868' : '#59706a'"></uni-icons><text></text></view>
<view class="bar-action" @tap="reportPost"><uni-icons type="flag" size="22" color="#59706a"></uni-icons><text></text></view>
<view class="chat-btn" v-if="canUsePartnerChat" @tap="goChat">搭一下</view>
</view>
<ie-auth-dialog />
</view>
</template>
<script>
23 hours ago
import {
getCampusPostDetail,
toggleCampusLike,
addCampusComment,
pageCampusComments,
deleteCampusComment,
submitCampusComplaint,
createCampusChatSession
} from '@/common/campusApi.js'
import { getIeProfile } from '@/common/ieApi.js'
import { ensureIeVerifiedBeforeAction } from '@/common/ieRealNameAuth.js'
import IeAuthDialog from '@/package1/components/ie-auth-dialog/ie-auth-dialog.vue'
export default {
components: { IeAuthDialog },
data() {
return {
id: '',
post: null,
statusBarTop: 24,
comments: [],
commentsTotal: 0,
commentPageNum: 1,
commentPageSize: 10,
commentTotalPages: 1,
commentLoading: false,
commentText: '',
commentSubmitting: false,
commentAnonymous: false,
replyTarget: {},
currentUserId: String(uni.getStorageSync('id') || ''),
defaultAvatar: '/static/images/img/songshu.png',
likeLocked: false,
imageSizeMap: {}
}
},
computed: {
navHeight() { return this.statusBarTop + uni.upx2px(104) },
imageList() { return this.post && this.post.imageList ? this.post.imageList : [] },
tagList() { return this.post && this.post.tagList ? this.post.tagList : [] },
commentHasMore() { return this.commentPageNum < this.commentTotalPages },
hasContact() { return !!(this.post && (this.post.contactWechat || this.post.contactPhone)) },
canUsePartnerChat() {
return !!(this.post && this.post.postType === 'partner' && this.post.partnerType !== 'game' && !this.post.mine)
}
},
onLoad(options = {}) {
this.id = options.id || ''
if (uni.getMenuButtonBoundingClientRect) this.statusBarTop = uni.getMenuButtonBoundingClientRect().top || this.statusBarTop
this.loadDetail()
this.loadComments(true)
},
methods: {
back() { uni.navigateBack() },
loadDetail() {
getCampusPostDetail(this.id).then(post => {
this.post = post
})
},
loadComments(reset) {
if (this.commentLoading) return
if (reset) {
this.commentPageNum = 1
this.commentTotalPages = 1
}
this.commentLoading = true
pageCampusComments(this.id, this.commentPageNum, this.commentPageSize).then(page => {
const records = page && (page.records || page.content) ? (page.records || page.content) : []
this.commentTotalPages = Number(page && (page.pages || page.totalPages)) || 1
this.commentsTotal = Number(page && (page.total || page.totalElements)) || records.length
this.comments = reset ? records : this.comments.concat(records)
uni.$emit('campusUnreadChange')
}).finally(() => {
this.commentLoading = false
})
},
loadMoreComments() {
if (this.commentLoading || !this.commentHasMore) return
this.commentPageNum += 1
this.loadComments(false)
},
previewImages(index) {
uni.previewImage({ urls: this.imageList, current: this.imageList[index] || this.imageList[0] })
},
onPostImageLoad(pic, e) {
if (!pic || !e || !e.detail) return
this.$set(this.imageSizeMap, this.getImageKey(pic), {
width: Number(e.detail.width) || 0,
height: Number(e.detail.height) || 0
})
},
getImageKey(pic) {
const value = String(pic || '')
let hash = 0
for (let i = 0; i < value.length; i++) {
hash = ((hash << 5) - hash) + value.charCodeAt(i)
hash |= 0
}
return 'img_' + Math.abs(hash)
},
getPostImageStyle(pic, count) {
if (count > 1) return ''
const maxWidth = 640
const maxHeight = 520
const size = this.imageSizeMap[this.getImageKey(pic)]
if (!size || !size.width || !size.height) {
return 'width:' + maxWidth + 'rpx;height:360rpx;max-width:' + maxWidth + 'rpx;max-height:' + maxHeight + 'rpx;'
}
const ratio = size.width / size.height
let width = maxWidth
let height = Math.round(width / ratio)
if (height > maxHeight) {
height = maxHeight
width = Math.round(height * ratio)
}
return 'width:' + width + 'rpx;height:' + height + 'rpx;max-width:' + maxWidth + 'rpx;max-height:' + maxHeight + 'rpx;'
},
displayName(item) {
return item && item.userName ? item.userName : '半径同学'
},
displayAvatar(item) {
return item && item.userAvatar ? item.userAvatar : this.defaultAvatar
},
stripHtml(html = '') { return String(html).replace(/<[^>]+>/g, '') },
async ensureVerified(actionText) {
const profile = await getIeProfile().catch(() => null)
return ensureIeVerifiedBeforeAction({
profile: profile || {},
actionText,
authDialog: { badge: '校园圈', title: '实名认证', desc: actionText + '前需完成实名认证' },
reload: getIeProfile
})
},
async focusComment() {
const ok = await this.ensureVerified('评论校园圈内容')
if (!ok) return
this.replyTarget = {}
this.commentText = ''
this.commentAnonymous = false
this.$refs.commentPopup.open()
},
async replyComment(item) {
const ok = await this.ensureVerified('评论校园圈内容')
if (!ok) return
this.replyTarget = item || {}
this.commentText = ''
this.commentAnonymous = false
this.$refs.commentPopup.open()
},
toggleCommentAnonymous(e) {
this.commentAnonymous = !!(e && e.detail && e.detail.value)
},
closeCommentPopup() {
this.$refs.commentPopup.close()
},
async submitComment() {
if (this.commentSubmitting) return
if (!this.commentText) {
uni.showToast({ title: '请输入评论内容', icon: 'none' })
return
}
const ok = await this.ensureVerified('评论校园圈内容')
if (!ok) return
this.commentSubmitting = true
try {
const comment = await addCampusComment({
postId: this.id,
...this.getSubmitCommentProfile(),
parentId: this.replyTarget.parentId ? this.replyTarget.parentId : this.replyTarget.id,
replyToUserId: this.replyTarget.userId,
replyToUserName: this.replyTarget.userName,
content: this.commentText
})
if (!this.isCommentSubmitSuccess(comment)) {
this.showCommentSubmitFail(comment)
return
}
this.closeCommentPopup()
this.commentText = ''
this.replyTarget = {}
this.loadDetail()
this.loadComments(true)
} catch (e) {
this.showCommentSubmitFail(e)
} finally {
this.commentSubmitting = false
}
},
isCommentSubmitSuccess(comment) {
if (!comment) return false
if (comment.success === false) return false
if (comment.code !== undefined && Number(comment.code) !== 200) return false
return !!(comment.id || comment.content || comment.createTime)
},
showCommentSubmitFail(error) {
const message = (error && (error.message || error.msg)) || '评论内容审核未通过,请修改后重试'
uni.showToast({ title: message, icon: 'none', duration: 2200 })
},
deleteComment(item) {
uni.showModal({
title: '删除评论',
content: '确认删除这条评论吗?',
success: res => {
if (!res.confirm) return
deleteCampusComment(item.id).then(() => this.loadComments(true))
}
})
},
likePost() {
if (this.likeLocked) return
this.likeLocked = true
toggleCampusLike({ targetType: 'post', targetId: this.id }).then(res => {
if (!res) return
this.post.liked = !!res.liked
this.post.likeCount = Number(res.count) || 0
}).finally(() => {
setTimeout(() => { this.likeLocked = false }, 800)
})
},
reportPost() {
this.submitReport({ complaintType: 'post', postId: this.id })
},
reportComment(item) {
this.submitReport({ complaintType: 'comment', postId: this.id, commentId: item.id })
},
submitReport(payload) {
uni.showModal({
title: '举报',
editable: true,
placeholderText: '请填写举报原因',
success: res => {
if (!res.confirm) return
const reason = (res.content || '').trim()
if (!reason) {
uni.showToast({ title: '请填写举报原因', icon: 'none' })
return
}
submitCampusComplaint({ ...payload, reason }).then(() => {
uni.showToast({ title: '举报已提交', icon: 'success' })
})
}
})
},
async goChat() {
if (!this.canUsePartnerChat) return
const ok = await this.ensureVerified('联系校园圈搭子')
if (!ok) return
createCampusChatSession(this.id).then(session => {
if (!session || !session.sessionId) return
uni.navigateTo({ url: '/package1/campus/campusChat?sessionId=' + session.sessionId })
})
},
getSubmitCommentProfile() {
if (this.commentAnonymous) {
return {
userName: '半径同学',
userAvatar: ''
}
}
return {
userName: uni.getStorageSync('nickName') || uni.getStorageSync('nickname') || uni.getStorageSync('userName') || '',
userAvatar: uni.getStorageSync('avatar') || uni.getStorageSync('avatarUrl') || uni.getStorageSync('headimgurl') || ''
}
}
}
}
</script>
23 hours ago
<style scoped>
.detail-page { min-height: 100vh; background: #f6faf8; }
.nav { position: fixed; top: 0; left: 0; right: 0; z-index: 10; display: flex; align-items: center; justify-content: space-between; padding: 16rpx 24rpx; background: rgba(246,250,248,.96); }
.back, .nav-right { width: 64rpx; height: 64rpx; border-radius: 50%; background: #fff; display: flex; align-items: center; justify-content: center; }
.nav-title { font-size: 32rpx; font-weight: 800; color: #18372f; }
.content-scroll { height: 100vh; box-sizing: border-box; }
.card { margin: 20rpx 24rpx; padding: 26rpx; background: #fff; border-radius: 30rpx; box-shadow: 0 12rpx 32rpx rgba(38, 76, 69, .08); }
.author-row { display: flex; align-items: center; }
.avatar, .comment-avatar { width: 64rpx; height: 64rpx; border-radius: 50%; background: #dff8f0; color: #257c69; text-align: center; line-height: 64rpx; font-weight: 700; }
.author-main { flex: 1; margin-left: 14rpx; }
.author { font-size: 27rpx; font-weight: 700; color: #263d38; }
.author text { margin-left: 10rpx; padding: 4rpx 10rpx; border-radius: 999rpx; background: #e9faf5; color: #23a987; font-size: 20rpx; }
.meta, .stats { margin-top: 8rpx; color: #8b9995; font-size: 23rpx; }
.title { margin-top: 24rpx; color: #172d28; font-size: 36rpx; font-weight: 800; }
.content { margin-top: 16rpx; color: #4d625d; font-size: 28rpx; line-height: 1.7; }
.image-grid { display: grid; gap: 10rpx; margin-top: 18rpx; }
.image-grid--1 { display: flex; align-items: center; justify-content: flex-start; max-width: 640rpx; max-height: 520rpx; overflow: hidden; border-radius: 20rpx; background: transparent; }
.image-grid--2 { grid-template-columns: repeat(2, 1fr); }
.image-grid--3 { grid-template-columns: repeat(3, 1fr); }
.image-grid image { width: 100%; height: 210rpx; max-width: 100%; border-radius: 20rpx; background: #eef5f3; }
.image-grid--1 image { max-width: 640rpx; max-height: 520rpx; background: transparent; }
.tag-row { display: flex; flex-wrap: wrap; gap: 10rpx; margin-top: 18rpx; }
.tag-row text { padding: 8rpx 16rpx; border-radius: 999rpx; background: #eefaf6; color: #2f9a81; font-size: 22rpx; }
.partner-box { margin-top: 20rpx; display: grid; gap: 12rpx; }
.partner-box view { background: #f6faf8; border-radius: 18rpx; padding: 16rpx; color: #344a45; font-size: 25rpx; }
.partner-box text { color: #8a9994; margin-right: 16rpx; }
.contact-box { margin-top: 18rpx; display: grid; gap: 12rpx; }
.contact-box view { padding: 16rpx; border-radius: 18rpx; background: #f0fbf7; color: #256d5f; font-size: 25rpx; }
.contact-box text { color: #8a9994; margin-right: 16rpx; }
.section-head { display: flex; justify-content: space-between; font-size: 30rpx; font-weight: 800; color: #1d352f; }
.section-head view { color: #22a786; font-size: 24rpx; }
.comment-item { display: flex; padding: 24rpx 0; border-bottom: 1rpx solid #eef3f1; }
.comment-main { flex: 1; margin-left: 16rpx; min-width: 0; }
.comment-name { font-size: 25rpx; font-weight: 700; color: #263d38; }
.comment-content { margin-top: 8rpx; color: #435852; font-size: 26rpx; line-height: 1.5; }
.reply-list { margin-top: 12rpx; padding: 14rpx; background: #f7faf9; border-radius: 18rpx; color: #4d625d; font-size: 24rpx; }
.reply-item { margin-bottom: 8rpx; }
.reply-item text { color: #1b9278; }
.comment-actions { display: flex; gap: 24rpx; margin-top: 12rpx; color: #8a9994; font-size: 22rpx; }
.empty, .load-state { text-align: center; color: #8a9692; padding: 26rpx 0; font-size: 24rpx; }
.bottom-space { height: 160rpx; }
.bottom-bar { position: fixed; left: 0; right: 0; bottom: 0; padding: 16rpx 24rpx 32rpx; display: flex; align-items: center; gap: 16rpx; background: rgba(255,255,255,.96); box-shadow: 0 -8rpx 28rpx rgba(31, 67, 59, .08); }
.bar-action { width: 96rpx; display: flex; flex-direction: column; align-items: center; color: #59706a; font-size: 21rpx; }
.chat-btn { flex: 1; height: 82rpx; line-height: 82rpx; text-align: center; border-radius: 999rpx; background: #34c7a2; color: #fff; font-weight: 800; }
.comment-popup { width: 620rpx; padding: 30rpx; border-radius: 30rpx; background: #fff; }
.popup-title { font-size: 30rpx; font-weight: 800; color: #18372f; }
.popup-desc { margin-top: 8rpx; color: #81918c; font-size: 23rpx; }
.comment-popup textarea { margin-top: 18rpx; width: 100%; height: 190rpx; box-sizing: border-box; background: #f6faf8; border-radius: 20rpx; padding: 18rpx; font-size: 26rpx; }
.anonymous-comment-row { margin-top: 18rpx; display: flex; align-items: center; justify-content: space-between; color: #526761; font-size: 25rpx; }
.popup-actions { display: flex; justify-content: flex-end; gap: 18rpx; margin-top: 20rpx; }
.popup-actions view { padding: 14rpx 28rpx; border-radius: 999rpx; background: #eef3f1; color: #526761; }
.popup-actions view:last-child { background: #34c7a2; color: #fff; }
.popup-actions .disabled { opacity: .6; }
</style>