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.

476 lines
22 KiB

1 day ago
<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>