12 changed files with 1508 additions and 85 deletions
@ -0,0 +1,236 @@ |
|||
<template> |
|||
<view class="chat-page"> |
|||
<view class="nav" :style="{ paddingTop: statusBarTop + 'px' }"> |
|||
<view class="back" @tap="back"><uni-icons type="left" size="24" color="#15362f"></uni-icons></view> |
|||
<view class="nav-main"> |
|||
<view class="nav-title">校园圈聊天</view> |
|||
<view class="nav-sub">文字和图片会进行内容审核</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="post-card" v-if="session" @tap="goPost"> |
|||
<image class="cover" v-if="session.displayCover" :src="session.displayCover" mode="aspectFill"></image> |
|||
<view class="cover no-cover" v-else>圈</view> |
|||
<view class="post-main"> |
|||
<view class="post-title">{{session.displayTitle || '校园圈内容'}}</view> |
|||
<view class="post-sub">{{session.postType === 'partner' ? '找搭子' : '校园墙'}}</view> |
|||
</view> |
|||
<view class="link">查看 ›</view> |
|||
</view> |
|||
|
|||
<scroll-view |
|||
class="message-scroll" |
|||
scroll-y |
|||
:scroll-into-view="scrollIntoView" |
|||
:scroll-with-animation="true" |
|||
:style="{ paddingTop: contentTop + 'px' }" |
|||
@scrolltoupper="loadOlder" |
|||
> |
|||
<view class="load-tip" v-if="loadingOlder">加载中...</view> |
|||
<view class="safe-tip">请勿发送涉黄涉暴、辱骂诈骗等内容,审核未通过的消息不会送达对方。</view> |
|||
<view class="message-row" :class="{ mine: item.mine }" v-for="item in messages" :key="item.id" :id="'msg-' + item.id"> |
|||
<view class="avatar">{{item.mine ? '我' : 'TA'}}</view> |
|||
<view class="message-main"> |
|||
<view class="bubble text-bubble" :class="{ failed: item.localState === 'failed' }" v-if="item.messageType == 1">{{item.content}}</view> |
|||
<view class="bubble image-bubble" :class="{ failed: item.localState === 'failed' }" v-else> |
|||
<image :src="item.content" mode="aspectFill" @tap="previewImage(item.content)"></image> |
|||
</view> |
|||
<view class="message-status" v-if="item.mine && item.localState">{{statusText(item)}}</view> |
|||
</view> |
|||
</view> |
|||
<view id="chat-bottom" class="bottom-anchor"></view> |
|||
</scroll-view> |
|||
|
|||
<view class="input-bar"> |
|||
<view class="image-btn" @tap="chooseImage"><uni-icons type="image" size="24" color="#61736e"></uni-icons></view> |
|||
<input v-model.trim="draft" confirm-type="send" placeholder="请输入消息..." @confirm="sendText" /> |
|||
<view class="send-btn" :class="{ disabled: sending }" @tap="sendText">{{sending ? '发送中' : '发送'}}</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { |
|||
getCampusChatSession, |
|||
pageCampusChatMessages, |
|||
sendCampusChatMessage, |
|||
uploadCampusImage |
|||
} from '@/common/campusApi.js' |
|||
|
|||
export default { |
|||
data() { |
|||
return { |
|||
statusBarTop: 24, |
|||
sessionId: '', |
|||
session: null, |
|||
messages: [], |
|||
pageNum: 1, |
|||
pageSize: 20, |
|||
totalPages: 1, |
|||
loadingOlder: false, |
|||
draft: '', |
|||
sending: false, |
|||
scrollIntoView: 'chat-bottom', |
|||
selfId: String(uni.getStorageSync('id') || '') |
|||
} |
|||
}, |
|||
computed: { |
|||
contentTop() { |
|||
return this.statusBarTop + uni.upx2px(220) |
|||
} |
|||
}, |
|||
onLoad(options = {}) { |
|||
this.sessionId = options.sessionId || '' |
|||
if (uni.getMenuButtonBoundingClientRect) this.statusBarTop = uni.getMenuButtonBoundingClientRect().top || this.statusBarTop |
|||
this.load() |
|||
}, |
|||
methods: { |
|||
back() { uni.navigateBack() }, |
|||
load() { |
|||
if (!this.sessionId) return |
|||
getCampusChatSession(this.sessionId).then(session => { |
|||
this.session = session |
|||
}) |
|||
this.loadMessages(true) |
|||
}, |
|||
loadMessages(reset) { |
|||
if (reset) { |
|||
this.pageNum = 1 |
|||
this.totalPages = 1 |
|||
} |
|||
return pageCampusChatMessages(this.sessionId, this.pageNum, this.pageSize).then(page => { |
|||
const records = page && (page.records || page.content) ? (page.records || page.content) : [] |
|||
const normalized = records.reverse().map(this.normalizeMessage) |
|||
this.totalPages = Number(page && (page.pages || page.totalPages)) || 1 |
|||
this.messages = reset ? normalized : normalized.concat(this.messages) |
|||
this.scrollToBottom() |
|||
if (reset) uni.$emit('campusUnreadChange') |
|||
}) |
|||
}, |
|||
loadOlder() { |
|||
if (this.loadingOlder || this.pageNum >= this.totalPages) return |
|||
this.loadingOlder = true |
|||
this.pageNum += 1 |
|||
this.loadMessages(false).finally(() => { |
|||
this.loadingOlder = false |
|||
}) |
|||
}, |
|||
normalizeMessage(item) { |
|||
return { |
|||
...item, |
|||
mine: String(item.senderId) === this.selfId, |
|||
localState: item.localState || '' |
|||
} |
|||
}, |
|||
sendText() { |
|||
if (this.sending) return |
|||
const text = this.draft |
|||
if (!text) return |
|||
const clientMsgId = 'local_' + Date.now() |
|||
this.draft = '' |
|||
this.appendLocal({ id: clientMsgId, clientMsgId, messageType: 1, content: text }) |
|||
this.sending = true |
|||
sendCampusChatMessage({ sessionId: this.sessionId, messageType: 1, content: text, clientMsgId }).then(message => { |
|||
this.replaceLocal(clientMsgId, message) |
|||
}).catch(err => { |
|||
this.markFailed(clientMsgId, (err && err.message) || '发送失败') |
|||
}).finally(() => { |
|||
this.sending = false |
|||
}) |
|||
}, |
|||
chooseImage() { |
|||
if (this.sending) return |
|||
uni.chooseImage({ |
|||
count: 1, |
|||
sizeType: ['compressed'], |
|||
success: async res => { |
|||
const path = res.tempFilePaths && res.tempFilePaths[0] |
|||
if (!path) return |
|||
const clientMsgId = 'local_' + Date.now() |
|||
this.appendLocal({ id: clientMsgId, clientMsgId, messageType: 2, content: path }) |
|||
this.sending = true |
|||
try { |
|||
const url = await uploadCampusImage(path) |
|||
const message = await sendCampusChatMessage({ sessionId: this.sessionId, messageType: 2, content: url, clientMsgId }) |
|||
this.replaceLocal(clientMsgId, message) |
|||
} catch (e) { |
|||
this.markFailed(clientMsgId, (e && e.message) || '图片发送失败') |
|||
} finally { |
|||
this.sending = false |
|||
} |
|||
} |
|||
}) |
|||
}, |
|||
appendLocal(message) { |
|||
this.messages.push({ ...message, mine: true, localState: 'sending', senderId: this.selfId }) |
|||
this.scrollToBottom() |
|||
}, |
|||
replaceLocal(clientMsgId, message) { |
|||
const index = this.messages.findIndex(item => item.clientMsgId === clientMsgId) |
|||
const normalized = this.normalizeMessage({ ...message, localState: '' }) |
|||
if (index > -1) this.$set(this.messages, index, normalized) |
|||
else this.messages.push(normalized) |
|||
this.scrollToBottom() |
|||
}, |
|||
markFailed(clientMsgId, reason) { |
|||
const item = this.messages.find(row => row.clientMsgId === clientMsgId) |
|||
if (item) { |
|||
item.localState = 'failed' |
|||
item.auditReason = reason |
|||
} |
|||
uni.showToast({ title: reason, icon: 'none', duration: 3000 }) |
|||
}, |
|||
statusText(item) { |
|||
if (item.localState === 'sending') return '发送中' |
|||
if (item.localState === 'failed') return item.auditReason || '发送失败' |
|||
return '' |
|||
}, |
|||
scrollToBottom() { |
|||
this.$nextTick(() => { |
|||
this.scrollIntoView = '' |
|||
this.$nextTick(() => { this.scrollIntoView = 'chat-bottom' }) |
|||
}) |
|||
}, |
|||
previewImage(url) { |
|||
uni.previewImage({ urls: [url] }) |
|||
}, |
|||
goPost() { |
|||
if (!this.session || !this.session.postId) return |
|||
uni.navigateTo({ url: '/package1/post/postDetail?id=' + this.session.postId + '&source=campus' }) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.chat-page { min-height: 100vh; background: #f5faf8; } |
|||
.nav { position: fixed; left: 0; right: 0; top: 0; z-index: 20; display: flex; align-items: center; gap: 16rpx; padding: 14rpx 24rpx; background: rgba(245,250,248,.98); } |
|||
.back { width: 64rpx; height: 64rpx; border-radius: 50%; background: #fff; display: flex; align-items: center; justify-content: center; } |
|||
.nav-title { color: #15362f; font-size: 31rpx; font-weight: 800; } |
|||
.nav-sub { margin-top: 4rpx; color: #7b8c87; font-size: 22rpx; } |
|||
.post-card { position: fixed; left: 24rpx; right: 24rpx; top: 118rpx; z-index: 18; display: flex; align-items: center; gap: 14rpx; padding: 16rpx; border-radius: 24rpx; background: #fff; box-shadow: 0 10rpx 26rpx rgba(39, 76, 70, .08); } |
|||
.cover { width: 82rpx; height: 82rpx; border-radius: 18rpx; background: #e8f7f3; } |
|||
.no-cover { display: flex; align-items: center; justify-content: center; color: #28a889; font-weight: 800; } |
|||
.post-main { flex: 1; min-width: 0; } |
|||
.post-title { font-size: 25rpx; color: #253d37; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
|||
.post-sub, .link { margin-top: 6rpx; color: #83948f; font-size: 22rpx; } |
|||
.message-scroll { height: 100vh; box-sizing: border-box; padding-left: 24rpx; padding-right: 24rpx; padding-bottom: 130rpx; } |
|||
.load-tip, .safe-tip { text-align: center; color: #879691; font-size: 22rpx; padding: 18rpx 0; } |
|||
.safe-tip { margin: 0 8rpx 18rpx; background: #ecf8f4; border-radius: 999rpx; } |
|||
.message-row { display: flex; gap: 12rpx; margin: 18rpx 0; } |
|||
.message-row.mine { flex-direction: row-reverse; } |
|||
.avatar { width: 56rpx; height: 56rpx; line-height: 56rpx; border-radius: 50%; background: #dff8f0; color: #257c69; text-align: center; font-size: 22rpx; } |
|||
.message-main { max-width: 72%; } |
|||
.bubble { padding: 18rpx 22rpx; border-radius: 24rpx; background: #fff; color: #263d38; font-size: 27rpx; line-height: 1.5; box-shadow: 0 8rpx 22rpx rgba(39, 76, 70, .06); } |
|||
.mine .bubble { background: #34c7a2; color: #fff; } |
|||
.bubble.failed { opacity: .55; } |
|||
.image-bubble { padding: 8rpx; } |
|||
.image-bubble image { width: 220rpx; height: 220rpx; border-radius: 18rpx; } |
|||
.message-status { margin-top: 8rpx; color: #9a8a7b; font-size: 21rpx; text-align: right; } |
|||
.bottom-anchor { height: 20rpx; } |
|||
.input-bar { position: fixed; left: 0; right: 0; bottom: 0; z-index: 20; display: flex; align-items: center; gap: 14rpx; padding: 18rpx 24rpx 34rpx; background: #fff; box-shadow: 0 -8rpx 28rpx rgba(31, 67, 59, .08); } |
|||
.image-btn { width: 70rpx; height: 70rpx; border-radius: 50%; background: #f1f6f4; display: flex; align-items: center; justify-content: center; } |
|||
.input-bar input { flex: 1; height: 72rpx; border-radius: 999rpx; background: #f4f8f7; padding: 0 24rpx; font-size: 26rpx; } |
|||
.send-btn { padding: 0 24rpx; height: 72rpx; line-height: 72rpx; border-radius: 999rpx; background: #34c7a2; color: #fff; font-weight: 700; } |
|||
.send-btn.disabled { opacity: .6; } |
|||
</style> |
|||
@ -0,0 +1,475 @@ |
|||
<template> |
|||
<view class="release-page"> |
|||
<view class="nav" :style="{ paddingTop: statusBarTop + 'px' }"> |
|||
<view class="back" @tap="back"><uni-icons type="left" size="23" color="#173b32"></uni-icons></view> |
|||
<view> |
|||
<view class="nav-title">{{isEdit ? '编辑校园圈' : '发布校园圈'}}</view> |
|||
<view class="nav-sub">实名发布,内容和图片会进行安全审核</view> |
|||
</view> |
|||
</view> |
|||
<scroll-view scroll-y class="form-scroll" :style="{ paddingTop: navHeight + 'px' }"> |
|||
<view class="type-tabs"> |
|||
<view class="type-tab" :class="{ active: form.postType === 'wall' }" @tap="chooseType('wall')">校园墙</view> |
|||
<view class="type-tab" :class="{ active: form.partnerType === 'game' }" @tap="chooseType('game')">游戏搭子</view> |
|||
<view class="type-tab" :class="{ active: form.partnerType === 'daily' }" @tap="chooseType('daily')">日常搭子</view> |
|||
<view class="type-tab" :class="{ active: form.partnerType === 'sport' }" @tap="chooseType('sport')">运动搭子</view> |
|||
</view> |
|||
|
|||
<view class="card"> |
|||
<view class="anonymous-row"> |
|||
<view> |
|||
<view class="anonymous-title">匿名发布</view> |
|||
<view class="anonymous-desc">开启后统一显示默认头像和“半径同学”</view> |
|||
</view> |
|||
<switch :checked="form.anonymous" color="#34c7a2" @change="toggleAnonymous" /> |
|||
</view> |
|||
<view class="field"> |
|||
<view class="label">标题</view> |
|||
<input v-model.trim="form.title" maxlength="30" placeholder="一句话说明你想发布的内容,最多30字" /> |
|||
</view> |
|||
<view class="field"> |
|||
<view class="label">二级分类</view> |
|||
<picker :range="categoryNames" @change="chooseCategory"> |
|||
<view class="picker-value">{{selectedCategoryName || '请选择分类'}} <text>›</text></view> |
|||
</picker> |
|||
</view> |
|||
<view class="field"> |
|||
<view class="label">正文</view> |
|||
<textarea v-model.trim="form.contentText" maxlength="1000" placeholder="友善真实地描述,禁止违规内容" /> |
|||
</view> |
|||
<view class="field" v-if="form.postType === 'partner'"> |
|||
<view class="label">地点 <text>最多30字,可不填</text></view> |
|||
<input v-model.trim="form.location" maxlength="30" placeholder="例如:东操场、图书馆三楼" /> |
|||
</view> |
|||
<view class="field" v-if="form.postType === 'partner'"> |
|||
<view class="label">时间 <text>最多20字,可不填</text></view> |
|||
<input v-model.trim="form.activityTime" maxlength="20" placeholder="例如:今晚8点、周末下午" /> |
|||
</view> |
|||
<view class="field" v-if="form.postType === 'partner'"> |
|||
<view class="label">人数 <text>填写整数,可不填</text></view> |
|||
<input v-model="form.targetCount" type="number" placeholder="期望人数,可不填" @input="onTargetCountInput" /> |
|||
</view> |
|||
<view class="field" v-if="tagOptions.length"> |
|||
<view class="label">热门标签</view> |
|||
<view class="tag-list"> |
|||
<view class="tag" :class="{ active: form.tags.indexOf(item.name) > -1 }" v-for="item in tagOptions" :key="item.key" @tap="toggleTag(item.name)"> |
|||
{{item.name}} |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view class="field"> |
|||
<view class="label">联系方式 <text>微信/手机号可选填</text></view> |
|||
<input v-model.trim="form.contactWechat" maxlength="20" placeholder="微信号,可不填" /> |
|||
<input v-model.trim="form.contactPhone" type="number" maxlength="11" placeholder="手机号,可不填" @input="onContactPhoneInput" /> |
|||
</view> |
|||
<view class="field"> |
|||
<view class="label">图片 <text>最多9张</text></view> |
|||
<view class="image-list"> |
|||
<view class="image-item" v-for="(item,index) in form.images" :key="item"> |
|||
<image :src="item" mode="aspectFill"></image> |
|||
<view class="remove" @tap="removeImage(index)">×</view> |
|||
</view> |
|||
<view class="image-add" v-if="form.images.length < 9" hover-class="none" @tap.stop="chooseImages" @click.stop="chooseImages"> |
|||
<uni-icons type="plusempty" size="28" color="#7d918c"></uni-icons> |
|||
<text>上传</text> |
|||
</view> |
|||
</view> |
|||
<view class="audit-inline" :class="{ fail: imageAuditStatus === 'fail' }" v-if="imageAuditing || imageAuditMessage"> |
|||
<view class="mini-spinner" v-if="imageAuditing"></view> |
|||
<text>{{imageAuditing ? '图片审核中,请稍候...' : imageAuditMessage}}</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view class="safe-tip">发布、评论和聊天都会进行内容审核,违规内容会记录用于审核计费和后台治理。</view> |
|||
<view class="audit-inline form-audit" :class="{ fail: formAuditStatus === 'fail' }" v-if="formAuditMessage && !submitting"> |
|||
<text>{{formAuditMessage}}</text> |
|||
</view> |
|||
<view class="bottom-space"></view> |
|||
</scroll-view> |
|||
|
|||
<view class="submit-bar"> |
|||
<view class="submit-btn" :class="{ disabled: submitting }" @tap="submit">{{submitting ? '提交审核中...' : '提交发布'}}</view> |
|||
</view> |
|||
<view class="audit-mask" v-if="submitting"> |
|||
<view class="audit-card"> |
|||
<view class="spinner"></view> |
|||
<view class="audit-title">表单内容审核中</view> |
|||
<view class="audit-desc">图片、标题和正文正在审核,请勿重复点击或退出页面</view> |
|||
</view> |
|||
</view> |
|||
<ie-auth-dialog /> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { getIeProfile } from '@/common/ieApi.js' |
|||
import { ensureIeVerifiedBeforeAction } from '@/common/ieRealNameAuth.js' |
|||
import { |
|||
listCampusCategories, |
|||
getCampusPostDetail, |
|||
publishCampusPost, |
|||
uploadCampusReleaseImage |
|||
} from '@/common/campusApi.js' |
|||
import IeAuthDialog from '@/package1/components/ie-auth-dialog/ie-auth-dialog.vue' |
|||
|
|||
export default { |
|||
components: { IeAuthDialog }, |
|||
data() { |
|||
return { |
|||
statusBarTop: 24, |
|||
editId: '', |
|||
isEdit: false, |
|||
categories: [], |
|||
tags: [], |
|||
selectedCategoryIndex: -1, |
|||
submitting: false, |
|||
submitLocked: false, |
|||
imageChoosing: false, |
|||
imageAuditing: false, |
|||
imageAuditStatus: '', |
|||
imageAuditMessage: '', |
|||
formAuditStatus: '', |
|||
formAuditMessage: '', |
|||
form: { |
|||
postType: 'wall', |
|||
partnerType: '', |
|||
title: '', |
|||
categoryId: '', |
|||
categoryName: '', |
|||
content: '', |
|||
contentText: '', |
|||
images: [], |
|||
tags: [], |
|||
location: '', |
|||
activityTime: '', |
|||
targetCount: '', |
|||
contactWechat: '', |
|||
contactPhone: '', |
|||
anonymous: false |
|||
} |
|||
} |
|||
}, |
|||
computed: { |
|||
navHeight() { |
|||
return this.statusBarTop + uni.upx2px(120) |
|||
}, |
|||
currentParentCode() { |
|||
return this.form.postType === 'wall' ? 'wall' : this.form.partnerType |
|||
}, |
|||
categoryNames() { |
|||
return this.categories.map(item => item.name) |
|||
}, |
|||
selectedCategoryName() { |
|||
const item = this.categories[this.selectedCategoryIndex] |
|||
return item ? item.name : '' |
|||
}, |
|||
tagOptions() { |
|||
return this.tags |
|||
.filter(item => item.parentCode === this.currentParentCode) |
|||
.map((item, index) => ({ |
|||
...item, |
|||
key: item.id || item.code || item.name || String(index) |
|||
})) |
|||
} |
|||
}, |
|||
onLoad(options = {}) { |
|||
if (options.type && options.type !== 'wall') this.chooseType(options.type) |
|||
if (options.id) { |
|||
this.editId = options.id |
|||
this.isEdit = true |
|||
} |
|||
if (uni.getMenuButtonBoundingClientRect) { |
|||
this.statusBarTop = uni.getMenuButtonBoundingClientRect().top || this.statusBarTop |
|||
} |
|||
this.loadMeta() |
|||
if (this.isEdit) this.loadEditDetail() |
|||
this.ensureVerified(true) |
|||
}, |
|||
methods: { |
|||
back() { |
|||
uni.navigateBack() |
|||
}, |
|||
getArea() { |
|||
const area = uni.getStorageSync('area') |
|||
if (!area) return {} |
|||
if (typeof area === 'string') { |
|||
try { return JSON.parse(area) } catch (e) { return {} } |
|||
} |
|||
return area |
|||
}, |
|||
chooseType(type) { |
|||
if (type === 'wall') { |
|||
this.form.postType = 'wall' |
|||
this.form.partnerType = '' |
|||
} else { |
|||
this.form.postType = 'partner' |
|||
this.form.partnerType = type |
|||
} |
|||
this.selectedCategoryIndex = -1 |
|||
this.form.categoryId = '' |
|||
this.form.categoryName = '' |
|||
this.form.tags = [] |
|||
this.loadMeta() |
|||
}, |
|||
loadMeta() { |
|||
const area = this.getArea() |
|||
Promise.all([ |
|||
listCampusCategories({ regionId: area.id || '', parentCode: this.currentParentCode, categoryType: 'category' }), |
|||
listCampusCategories({ regionId: area.id || '', categoryType: 'tag' }) |
|||
]).then(([categories, tags]) => { |
|||
this.categories = Array.isArray(categories) ? categories : [] |
|||
this.tags = Array.isArray(tags) ? tags : [] |
|||
this.applyEditCategory() |
|||
}) |
|||
}, |
|||
loadEditDetail() { |
|||
getCampusPostDetail(this.editId).then(detail => { |
|||
if (!detail) return |
|||
this.form = { |
|||
...this.form, |
|||
postType: detail.postType || 'wall', |
|||
partnerType: detail.partnerType || '', |
|||
title: detail.title || '', |
|||
categoryId: detail.categoryId || '', |
|||
categoryName: detail.categoryName || '', |
|||
content: detail.content || detail.contentText || '', |
|||
contentText: detail.contentText || '', |
|||
images: detail.imageList || [], |
|||
tags: detail.tagList || [], |
|||
location: detail.location || '', |
|||
activityTime: detail.activityTime || '', |
|||
targetCount: detail.targetCount || '', |
|||
contactWechat: detail.contactWechat || '', |
|||
contactPhone: detail.contactPhone || '', |
|||
anonymous: detail.userName === '半径同学' && !detail.userAvatar |
|||
} |
|||
this.applyEditCategory() |
|||
}) |
|||
}, |
|||
applyEditCategory() { |
|||
if (!this.form.categoryId || !this.categories.length) return |
|||
this.selectedCategoryIndex = this.categories.findIndex(item => item.id === this.form.categoryId) |
|||
}, |
|||
chooseCategory(e) { |
|||
const index = Number(e.detail.value) |
|||
const item = this.categories[index] |
|||
if (!item) return |
|||
this.selectedCategoryIndex = index |
|||
this.form.categoryId = item.id |
|||
this.form.categoryName = item.name |
|||
}, |
|||
toggleTag(tag) { |
|||
const index = this.form.tags.indexOf(tag) |
|||
if (index > -1) this.form.tags.splice(index, 1) |
|||
else this.form.tags.push(tag) |
|||
}, |
|||
toggleAnonymous(e) { |
|||
this.form.anonymous = !!(e && e.detail && e.detail.value) |
|||
}, |
|||
onTargetCountInput(e) { |
|||
const value = String((e && e.detail && e.detail.value) || '').replace(/[^\d]/g, '') |
|||
this.form.targetCount = value |
|||
return value |
|||
}, |
|||
onContactPhoneInput(e) { |
|||
const value = String((e && e.detail && e.detail.value) || '').replace(/[^\d]/g, '').slice(0, 11) |
|||
this.form.contactPhone = value |
|||
return value |
|||
}, |
|||
chooseImages() { |
|||
if (this.imageChoosing || this.imageAuditing) return |
|||
this.imageChoosing = true |
|||
this.imageAuditStatus = '' |
|||
this.imageAuditMessage = '' |
|||
uni.chooseImage({ |
|||
count: 9 - this.form.images.length, |
|||
sizeType: ['compressed'], |
|||
success: async res => { |
|||
const paths = res.tempFilePaths || [] |
|||
if (!paths.length) return |
|||
this.imageAuditing = true |
|||
try { |
|||
for (const path of paths) { |
|||
const audit = await uploadCampusReleaseImage(path) |
|||
if (audit && audit.passed && audit.url) { |
|||
this.form.images.push(audit.url) |
|||
} else { |
|||
this.imageAuditStatus = 'fail' |
|||
this.imageAuditMessage = '图片审核不通过:' + ((audit && audit.message) || '请更换图片后重试') |
|||
uni.showToast({ title: this.imageAuditMessage, icon: 'none', duration: 3000 }) |
|||
break |
|||
} |
|||
} |
|||
if (this.imageAuditStatus !== 'fail') { |
|||
this.imageAuditMessage = '' |
|||
} |
|||
} catch (e) { |
|||
this.imageAuditStatus = 'fail' |
|||
this.imageAuditMessage = '图片审核失败,请稍后重试' |
|||
uni.showToast({ title: this.imageAuditMessage, icon: 'none', duration: 3000 }) |
|||
} finally { |
|||
this.imageAuditing = false |
|||
} |
|||
}, |
|||
fail: err => { |
|||
if (err && err.errMsg && err.errMsg.indexOf('cancel') === -1) { |
|||
uni.showToast({ title: '无法选择图片,请检查相册权限', icon: 'none' }) |
|||
} |
|||
}, |
|||
complete: () => { |
|||
this.imageChoosing = false |
|||
} |
|||
}) |
|||
}, |
|||
removeImage(index) { |
|||
this.form.images.splice(index, 1) |
|||
}, |
|||
validate() { |
|||
this.form.title = (this.form.title || '').trim() |
|||
this.form.contentText = (this.form.contentText || '').trim() |
|||
this.form.location = (this.form.location || '').trim() |
|||
this.form.activityTime = (this.form.activityTime || '').trim() |
|||
this.form.targetCount = String(this.form.targetCount || '').trim() |
|||
this.form.contactWechat = (this.form.contactWechat || '').trim() |
|||
this.form.contactPhone = String(this.form.contactPhone || '').trim() |
|||
if (!this.form.title) return '请填写标题' |
|||
if (this.form.title.length > 30) return '标题不能超过30字' |
|||
if (!this.form.categoryId) return '请选择二级分类' |
|||
if (this.imageAuditing) return '图片正在审核中,请稍候' |
|||
if (this.form.contentText.length > 1000) return '正文不能超过1000字' |
|||
if (this.form.postType === 'partner' && !this.form.partnerType) return '请选择搭子类型' |
|||
if (this.form.location.length > 30) return '地点不能超过30字' |
|||
if (this.form.activityTime.length > 20) return '时间不能超过20字' |
|||
if (this.form.targetCount && (!/^\d+$/.test(this.form.targetCount) || Number(this.form.targetCount) < 1)) return '人数请填写正整数' |
|||
if (this.form.contactWechat && !/^[a-zA-Z][-_a-zA-Z0-9]{5,19}$/.test(this.form.contactWechat)) return '微信号格式不正确' |
|||
if (this.form.contactPhone && !/^1[3-9]\d{9}$/.test(this.form.contactPhone)) return '手机号格式不正确' |
|||
return '' |
|||
}, |
|||
goReleaseHome() { |
|||
uni.redirectTo({ |
|||
url: '/package1/tabbar/release', |
|||
fail: () => uni.navigateBack() |
|||
}) |
|||
}, |
|||
async ensureVerified(redirectOnFail = false) { |
|||
const profile = await getIeProfile().catch(() => null) |
|||
const ok = await ensureIeVerifiedBeforeAction({ |
|||
profile: profile || {}, |
|||
actionText: '发布校园圈内容', |
|||
authDialog: { badge: '校园圈', title: '实名认证', desc: '发布、评论和聊天前需要完成实名认证' }, |
|||
reload: getIeProfile |
|||
}) |
|||
if (!ok && redirectOnFail) { |
|||
setTimeout(() => this.goReleaseHome(), 120) |
|||
} |
|||
return ok |
|||
}, |
|||
async submit() { |
|||
if (this.submitLocked || this.submitting) return |
|||
const error = this.validate() |
|||
if (error) { |
|||
uni.showToast({ title: error, icon: 'none' }) |
|||
return |
|||
} |
|||
const verified = await this.ensureVerified(true) |
|||
if (!verified) return |
|||
this.submitLocked = true |
|||
this.submitting = true |
|||
this.formAuditStatus = '' |
|||
this.formAuditMessage = '' |
|||
const area = this.getArea() |
|||
publishCampusPost({ |
|||
id: this.editId || undefined, |
|||
regionId: area.id || '', |
|||
regionName: area.name || area.title || '', |
|||
...this.getSubmitUserProfile(), |
|||
...this.form, |
|||
content: this.form.contentText, |
|||
targetCount: this.form.targetCount ? Number(this.form.targetCount) : undefined |
|||
}).then(post => { |
|||
if (!post) throw new Error('发布失败') |
|||
const publishedAt = Date.now() |
|||
uni.setStorageSync('campusPostPublishedAt', publishedAt) |
|||
uni.$emit('campusPostPublished', { id: post.id || this.editId || '', publishedAt }) |
|||
uni.$emit('campusUnreadChange') |
|||
uni.showToast({ title: '发布成功', icon: 'success' }) |
|||
setTimeout(() => uni.navigateBack(), 600) |
|||
}).catch(err => { |
|||
this.submitLocked = false |
|||
const message = (err && err.message) || '发布失败,请稍后重试' |
|||
this.formAuditStatus = 'fail' |
|||
this.formAuditMessage = message.indexOf('审核') > -1 || message.indexOf('违规') > -1 |
|||
? '表单审核不通过:' + message |
|||
: message |
|||
uni.showToast({ title: this.formAuditMessage, icon: 'none', duration: 3000 }) |
|||
}).finally(() => { |
|||
this.submitting = false |
|||
}) |
|||
}, |
|||
getSubmitUserProfile() { |
|||
if (this.form.anonymous) { |
|||
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> |
|||
|
|||
<style scoped> |
|||
.release-page { position: relative; min-height: 100vh; overflow: hidden; background: radial-gradient(circle at 12% 8%, rgba(68, 224, 186, .32), transparent 260rpx), radial-gradient(circle at 88% 18%, rgba(93, 175, 255, .18), transparent 300rpx), linear-gradient(180deg, #effff9 0%, #f7fbfa 44%, #ffffff 100%); } |
|||
.release-page::before { content: ''; position: fixed; right: -130rpx; top: 210rpx; z-index: 0; width: 280rpx; height: 280rpx; border-radius: 50%; background: rgba(44, 211, 175, .16); filter: blur(10rpx); pointer-events: none; animation: releaseGlow 4s ease-in-out infinite; } |
|||
.release-page::after { content: ''; position: fixed; left: -150rpx; bottom: 220rpx; z-index: 0; width: 320rpx; height: 320rpx; border-radius: 50%; background: rgba(58, 214, 166, .08); filter: blur(12rpx); pointer-events: none; animation: releaseGlow 5s ease-in-out infinite reverse; } |
|||
.nav { position: fixed; left: 0; right: 0; top: 0; z-index: 10; padding: 20rpx 28rpx 24rpx; display: flex; align-items: center; gap: 18rpx; background: linear-gradient(180deg, rgba(237, 255, 249, .98), rgba(247, 255, 252, .94)); box-shadow: 0 10rpx 28rpx rgba(31, 120, 94, .07); } |
|||
.back { width: 68rpx; height: 68rpx; border-radius: 50%; background: rgba(255,255,255,.88); display: flex; align-items: center; justify-content: center; box-shadow: 0 10rpx 24rpx rgba(34, 100, 82, .10); } |
|||
.nav-title { font-size: 36rpx; font-weight: 900; color: #173b32; letter-spacing: -1rpx; } |
|||
.nav-sub { font-size: 22rpx; color: #6f817c; margin-top: 6rpx; } |
|||
.form-scroll { height: 100vh; box-sizing: border-box; } |
|||
.type-tabs { position: relative; z-index: 1; display: grid; grid-template-columns: repeat(4, 1fr); gap: 14rpx; padding: 30rpx 26rpx 24rpx; } |
|||
.type-tab { padding: 20rpx 8rpx; border-radius: 24rpx; background: rgba(255,255,255,.82); color: #546861; text-align: center; font-size: 24rpx; font-weight: 700; box-shadow: 0 10rpx 24rpx rgba(34, 82, 73, .06); border: 2rpx solid rgba(255,255,255,.8); } |
|||
.type-tab.active { background: linear-gradient(135deg, #34d8ad, #25c6c5); color: #fff; font-weight: 900; box-shadow: 0 14rpx 30rpx rgba(38, 196, 169, .24); } |
|||
.card { position: relative; z-index: 1; margin: 0 24rpx; padding: 28rpx 26rpx 18rpx; background: rgba(255,255,255,.94); border-radius: 34rpx; box-shadow: 0 18rpx 42rpx rgba(40, 84, 75, .10); border: 1rpx solid rgba(255,255,255,.9); backdrop-filter: blur(8rpx); } |
|||
.anonymous-row { display: flex; align-items: center; justify-content: space-between; padding: 0 0 26rpx; border-bottom: 1rpx solid #edf4f1; } |
|||
.anonymous-title { color: #263d38; font-size: 28rpx; font-weight: 900; } |
|||
.anonymous-desc { margin-top: 8rpx; color: #94a39e; font-size: 22rpx; } |
|||
.field { padding: 28rpx 0; border-bottom: 1rpx solid #edf4f1; } |
|||
.field:last-child { border-bottom: 0; } |
|||
.label { font-size: 28rpx; font-weight: 900; color: #263d38; margin-bottom: 18rpx; } |
|||
.label text { color: #94a39e; font-size: 22rpx; font-weight: 400; } |
|||
input, textarea { width: 100%; box-sizing: border-box; font-size: 26rpx; color: #263d38; background: #f6fbf9; border-radius: 22rpx; padding: 0 22rpx; margin-bottom: 16rpx; border: 1rpx solid rgba(225, 238, 234, .9); box-shadow: inset 0 2rpx 0 rgba(255,255,255,.8); } |
|||
input { height: 72rpx; line-height: 72rpx; } |
|||
textarea { height: 260rpx; padding-top: 20rpx; line-height: 1.6; } |
|||
.picker-value { min-height: 72rpx; background: #f6fbf9; border-radius: 22rpx; padding: 0 22rpx; color: #526761; display: flex; align-items: center; justify-content: space-between; border: 1rpx solid rgba(225, 238, 234, .9); } |
|||
.tag-list { display: flex; flex-wrap: wrap; gap: 12rpx; } |
|||
.tag { padding: 12rpx 20rpx; border-radius: 999rpx; background: #eef7f4; color: #526761; font-size: 23rpx; font-weight: 700; } |
|||
.tag.active { background: linear-gradient(135deg, #34d8ad, #25c6c5); color: #fff; box-shadow: 0 8rpx 18rpx rgba(38, 196, 169, .18); } |
|||
.image-list { position: relative; z-index: 2; display: flex; flex-wrap: wrap; gap: 14rpx; } |
|||
.image-item, .image-add { width: 154rpx; height: 154rpx; border-radius: 22rpx; overflow: hidden; position: relative; background: #f3f8f6; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #7d918c; font-size: 22rpx; box-shadow: none; } |
|||
.image-add { border: 2rpx dashed #d8e6e1; background: #f8fbfa; color: #788b86; } |
|||
.image-add:active { background: #eef7f3; } |
|||
.image-add text { margin-top: 6rpx; } |
|||
.image-item image { width: 100%; height: 100%; } |
|||
.remove { position: absolute; right: 8rpx; top: 8rpx; width: 34rpx; height: 34rpx; line-height: 34rpx; text-align: center; background: rgba(0,0,0,.5); color: #fff; border-radius: 50%; } |
|||
.audit-inline { display: flex; align-items: center; gap: 10rpx; margin-top: 18rpx; padding: 16rpx 18rpx; border-radius: 18rpx; background: #effbf7; color: #2c8b72; font-size: 23rpx; line-height: 1.45; } |
|||
.audit-inline.fail { background: #fff1ef; color: #d64b3c; } |
|||
.form-audit { position: relative; z-index: 1; margin: 18rpx 30rpx 0; } |
|||
.mini-spinner { width: 26rpx; height: 26rpx; flex: 0 0 26rpx; border-radius: 50%; border: 4rpx solid #c9f2e8; border-top-color: #2fc49f; animation: spin .8s linear infinite; } |
|||
.safe-tip { position: relative; z-index: 1; margin: 24rpx 30rpx; padding: 18rpx 22rpx; border-radius: 22rpx; background: rgba(235, 250, 246, .86); color: #6f837d; font-size: 23rpx; line-height: 1.6; } |
|||
.bottom-space { height: 240rpx; } |
|||
.submit-bar { position: fixed; left: 0; right: 0; bottom: 0; z-index: 30; padding: 20rpx 28rpx 34rpx; background: rgba(255,255,255,.98); box-shadow: 0 -8rpx 28rpx rgba(31, 67, 59, .08); } |
|||
.submit-btn { height: 92rpx; line-height: 92rpx; text-align: center; border-radius: 999rpx; background: linear-gradient(135deg, #34d8ad, #25c6c5); color: #fff; font-size: 30rpx; font-weight: 900; box-shadow: 0 14rpx 30rpx rgba(38, 196, 169, .28); } |
|||
.submit-btn.disabled { opacity: .65; } |
|||
.audit-mask { position: fixed; inset: 0; z-index: 50; background: rgba(20, 35, 31, .45); display: flex; align-items: center; justify-content: center; } |
|||
.audit-card { width: 440rpx; padding: 44rpx 34rpx; border-radius: 32rpx; background: #fff; text-align: center; } |
|||
.spinner { width: 54rpx; height: 54rpx; margin: 0 auto 20rpx; border-radius: 50%; border: 6rpx solid #d8f5ee; border-top-color: #34c7a2; animation: spin .8s linear infinite; } |
|||
.audit-title { font-size: 30rpx; font-weight: 800; color: #183a32; } |
|||
.audit-desc { margin-top: 10rpx; color: #788883; font-size: 24rpx; } |
|||
@keyframes spin { to { transform: rotate(360deg); } } |
|||
@keyframes releaseGlow { 0%, 100% { transform: scale(.92); opacity: .65; } 50% { transform: scale(1.08); opacity: 1; } } |
|||
</style> |
|||
@ -1,9 +1,424 @@ |
|||
<template> |
|||
<!-- 帖子详情页 --> |
|||
<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> |
|||
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> |
|||
|
|||
<style> |
|||
</style> |
|||
<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> |
|||
|
|||
Loading…
Reference in new issue