Files
sbnews/uniapp/src/packages_community/pages/publish.vue
T

528 lines
14 KiB
Vue

<template>
<view class="publish-page">
<view class="publish-header" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="publish-header__left" @tap="handleCancel">取消</view>
<view class="publish-header__title">发帖</view>
<view class="publish-header__right">
<view class="publish-btn" :class="{ 'publish-btn--disabled': !canPublish || publishing }"
@tap="handlePublish">
<text>{{ publishing ? '发布中' : '发布' }}</text>
</view>
</view>
</view>
<scroll-view scroll-y class="publish-scroll" :style="{ height: scrollHeight + 'px' }">
<view class="publish-body">
<textarea v-model="form.content" class="publish-textarea" placeholder="分享你的体育见解..." :maxlength="2000"
auto-height />
<view class="publish-images">
<view v-for="(img, idx) in form.images.slice(0, form.images.length >= 9 ? 8 : form.images.length)"
:key="idx" class="publish-images__item">
<image :src="img" mode="aspectFill" class="publish-images__img" />
<view class="publish-images__del" @tap="removeImage(idx)">
<u-icon name="close" size="24" color="#fff" />
</view>
</view>
<view v-if="form.images.length >= 9" class="publish-images__more" @tap="chooseImage">
<image :src="form.images[8]" mode="aspectFill" class="publish-images__img" />
<view class="publish-images__more-mask">
<text class="publish-images__more-text">+{{ form.images.length - 8 }}</text>
<text class="publish-images__more-hint">点击继续添加</text>
</view>
</view>
<view v-if="form.images.length < 9" class="publish-images__add" @tap="chooseImage">
<u-icon name="plus" size="52" color="#ccc" />
</view>
</view>
</view>
<view class="publish-paid">
<view class="publish-paid__row">
<text class="publish-paid__label">收费阅读</text>
<switch :checked="isPaid" @change="isPaid = $event.detail.value" color="#185dff" />
</view>
<view v-if="isPaid" class="publish-paid__price">
<text class="publish-paid__label">积分价格</text>
<input v-model="pricePoints" type="number" class="publish-paid__input" placeholder="输入积分(1~9999)" />
</view>
</view>
<view class="publish-category">
<view class="publish-section__head">
<view class="publish-section__title-wrap">
<text class="publish-section__title">选择分类</text>
<text class="publish-section__required">必选</text>
</view>
<text class="publish-section__hint">{{ selectedCategory ? '已选择 1 个分类' : '请选择帖子所属分类' }}</text>
</view>
<view v-if="categoryList.length" class="publish-category__list">
<view v-for="category in visibleCategories" :key="category.id" class="publish-category__item"
:class="{ 'publish-category__item--active': selectedCategory === category.id }"
@tap="selectCategory(category.id)">
<text class="publish-category__name">{{ category.name }}</text>
</view>
</view>
<text v-else class="publish-section__empty">暂无可选分类</text>
</view>
<view class="publish-tags">
<view class="publish-section__head">
<view class="publish-section__title-wrap">
<text class="publish-section__title">选择标签</text>
<text class="publish-section__optional">可多选</text>
</view>
<text class="publish-section__hint">已选 {{ selectedTags.length }}/3</text>
</view>
<view class="publish-tags__list">
<view v-for="tag in tagList" :key="tag.id" class="publish-tags__item"
:class="{ 'publish-tags__item--active': selectedTags.includes(tag.id) }"
@tap="toggleTag(tag.id)">
<text>#{{ tag.name }}</text>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup lang="ts">
import { reactive, ref, computed, onMounted } from 'vue'
import { publishPost, getTagLists, getCommunityCategoryLists } from '@/api/community'
import { uploadImage } from '@/api/app'
import { getToken } from '@/utils/auth'
const statusBarHeight = ref(0)
const scrollHeight = ref(500)
const publishing = ref(false)
const tagList = ref<any[]>([])
const selectedTags = ref<number[]>([])
const categoryList = ref<any[]>([])
const selectedCategory = ref(0)
const visibleCategories = computed(() => categoryList.value.slice(0, 4))
const isPaid = ref(false)
const pricePoints = ref('')
const form = reactive({
content: '',
images: [] as string[]
})
const canPublish = computed(() => {
return selectedCategory.value > 0 && (form.content.trim().length > 0 || form.images.length > 0)
})
onMounted(() => {
const sysInfo = uni.getSystemInfoSync()
// #ifdef APP-PLUS || MP
statusBarHeight.value = sysInfo.statusBarHeight || 44
// #endif
// #ifdef H5
statusBarHeight.value = 20
// #endif
scrollHeight.value = sysInfo.windowHeight - statusBarHeight.value - 88
fetchPublishOptions()
})
const fetchPublishOptions = async () => {
try {
const [categories, tags] = await Promise.all([getCommunityCategoryLists(), getTagLists()])
categoryList.value = categories || []
tagList.value = tags || []
} catch (e) { }
}
const handleCancel = () => {
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack()
} else {
uni.switchTab({ url: '/pages/community/community' })
}
}
const handlePublish = async () => {
if (!getToken()) {
uni.navigateTo({ url: '/pages/login/login' })
return
}
if (!selectedCategory.value) {
uni.showToast({ title: '请选择帖子分类', icon: 'none' })
return
}
if (!canPublish.value || publishing.value) return
publishing.value = true
try {
if (isPaid.value) {
const pts = parseInt(pricePoints.value)
if (!pts || pts < 1 || pts > 9999) {
uni.showToast({ title: '积分价格范围1~9999', icon: 'none' })
publishing.value = false
return
}
}
// 上传本地图片到服务器
const uploadedImages: string[] = []
for (const img of form.images) {
if (img.startsWith('http')) {
uploadedImages.push(img)
} else {
const token = getToken()
const res: any = await uploadImage(img, token || '')
if (res?.uri) {
uploadedImages.push(res.uri)
}
}
}
await publishPost({
content: form.content,
images: uploadedImages,
category_id: selectedCategory.value,
tag_ids: selectedTags.value,
post_type: 0,
is_paid: isPaid.value ? 1 : 0,
price_points: isPaid.value ? parseInt(pricePoints.value) : 0
})
uni.showToast({ title: '发布成功', icon: 'success' })
setTimeout(() => {
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack()
} else {
uni.switchTab({ url: '/pages/community/community' })
}
}, 1200)
} catch (e) {
uni.showToast({ title: '发布失败', icon: 'none' })
} finally {
publishing.value = false
}
}
const chooseImage = () => {
const remaining = 18 - form.images.length
if (remaining <= 0) return
uni.chooseImage({
count: remaining,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
form.images.push(...res.tempFilePaths)
}
})
}
const removeImage = (idx: number) => {
form.images.splice(idx, 1)
}
const selectCategory = (id: number) => {
selectedCategory.value = id
}
const toggleTag = (id: number) => {
const idx = selectedTags.value.indexOf(id)
if (idx >= 0) {
selectedTags.value.splice(idx, 1)
} else {
if (selectedTags.value.length >= 3) {
uni.showToast({ title: '最多选择3个标签', icon: 'none' })
return
}
selectedTags.value.push(id)
}
}
</script>
<style lang="scss" scoped>
.publish-page {
min-height: 100vh;
background: #fff;
display: flex;
flex-direction: column;
}
.publish-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
&__left {
font-size: 30rpx;
color: #666;
width: 120rpx;
}
&__title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
&__right {
width: 120rpx;
display: flex;
justify-content: flex-end;
}
}
.publish-btn {
width: 120rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
background: #185dff;
color: #fff;
border-radius: 12rpx;
font-size: 26rpx;
white-space: nowrap;
&--disabled {
opacity: 0.5;
}
}
.publish-scroll {
flex: 1;
}
.publish-body {
padding: 30rpx;
}
.publish-textarea {
width: 100%;
min-height: 300rpx;
font-size: 30rpx;
line-height: 1.7;
color: #333;
}
.publish-images {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-top: 24rpx;
&__item {
position: relative;
width: 200rpx;
height: 200rpx;
}
&__img {
width: 200rpx;
height: 200rpx;
border-radius: 12rpx;
}
&__del {
position: absolute;
top: -12rpx;
right: -12rpx;
width: 40rpx;
height: 40rpx;
background: rgba(0, 0, 0, 0.6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
&__add {
width: 200rpx;
height: 200rpx;
border: 2rpx dashed #ddd;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
}
&__more {
position: relative;
width: 200rpx;
height: 200rpx;
border-radius: 12rpx;
overflow: hidden;
}
&__more-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 12rpx;
}
&__more-text {
font-size: 48rpx;
font-weight: 600;
color: #ffffff;
}
&__more-hint {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.8);
margin-top: 4rpx;
}
}
.publish-paid {
padding: 24rpx 30rpx;
border-top: 16rpx solid #f5f5f5;
&__row {
display: flex;
align-items: center;
justify-content: space-between;
}
&__label {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
&__price {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 20rpx;
}
&__input {
width: 300rpx;
height: 64rpx;
background: #f5f5f5;
border-radius: 12rpx;
padding: 0 20rpx;
font-size: 28rpx;
text-align: right;
}
}
.publish-section__head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
}
.publish-section__title-wrap {
display: flex;
align-items: center;
gap: 10rpx;
}
.publish-section__title {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.publish-section__required,
.publish-section__optional {
padding: 3rpx 10rpx;
border-radius: 10rpx;
font-size: 20rpx;
}
.publish-section__required {
color: #ef4444;
background: #fff1f2;
}
.publish-section__optional {
color: #185dff;
background: #edf4ff;
}
.publish-section__hint,
.publish-section__empty {
color: #9aa3b2;
font-size: 22rpx;
}
.publish-category {
padding: 24rpx 30rpx;
border-top: 16rpx solid #f5f5f5;
&__list {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 16rpx;
max-height: 76rpx;
overflow: hidden;
}
&__item {
display: flex;
align-items: center;
justify-content: center;
min-width: 0;
height: 76rpx;
box-sizing: border-box;
padding: 0 8rpx;
border: 2rpx solid #edf0f5;
border-radius: 14rpx;
background: #fafbfc;
&--active {
border-color: #185dff;
background: #eef4ff;
.publish-category__name {
color: #185dff;
font-weight: 600;
}
}
}
&__name {
overflow: hidden;
color: #4b5563;
font-size: 25rpx;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.publish-tags {
padding: 24rpx 30rpx;
border-top: 16rpx solid #f5f5f5;
&__list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
&__item {
padding: 10rpx 24rpx;
background: #f5f5f5;
border-radius: 12rpx;
font-size: 24rpx;
color: #666;
border: 2rpx solid transparent;
&--active {
background: #e5edff;
color: #185dff;
border-color: #185dff;
}
}
}
</style>