From d5160e0ba386730fdd6e334cf30eb2316426e06f Mon Sep 17 00:00:00 2001 From: hajimi Date: Mon, 3 Aug 2026 17:27:17 +0800 Subject: [PATCH] feat: add community post category selection --- docs/业务进度管理.md | 15 ++ .../api/controller/CommunityController.php | 23 ++- .../src/packages_community/pages/publish.vue | 184 ++++++++++++++++-- 3 files changed, 207 insertions(+), 15 deletions(-) diff --git a/docs/业务进度管理.md b/docs/业务进度管理.md index 151f3068..19448f6b 100644 --- a/docs/业务进度管理.md +++ b/docs/业务进度管理.md @@ -13,6 +13,21 @@ ## 一、已完成事项 +### 128. 社区发帖分类与多标签选择 + +- 状态:代码已完成,待 H5 验收 - 时间:2026-08-03 + +完成内容: + +- 发帖页新增必选分类卡片,读取后台启用的社区分类并将分类 ID 一同提交。 +- 标签区域明确支持多选,展示已选数量,并限制最多选择 3 个标签。 +- 发布接口校验分类与标签的启用状态,写入帖子分类并同步更新分类、标签的帖子计数。 + +涉及模块: + +- `uniapp/src/packages_community/pages/publish.vue` +- `server/app/api/controller/CommunityController.php` + ### 127. 历史开奖记录页主页风格统一 - 状态:代码已完成,待 H5 验收 - 时间:2026-08-03 diff --git a/server/app/api/controller/CommunityController.php b/server/app/api/controller/CommunityController.php index 88d9ee58..faa9b2d9 100644 --- a/server/app/api/controller/CommunityController.php +++ b/server/app/api/controller/CommunityController.php @@ -168,8 +168,9 @@ class CommunityController extends BaseApiController $content = $this->request->post('content/s', ''); $images = $this->request->post('images/a', []); $postType = $this->request->post('post_type/d', 0); + $categoryId = $this->request->post('category_id/d', 0); $matchId = $this->request->post('match_id/d', 0); - $tagIds = $this->request->post('tag_ids/a', []); + $tagIds = array_values(array_unique(array_filter(array_map('intval', $this->request->post('tag_ids/a', []))))); $isPaid = $this->request->post('is_paid/d', 0); $pricePoints = $this->request->post('price_points/d', 0); $freeContentLen = $this->request->post('free_content_len/d', 100); @@ -178,6 +179,24 @@ class CommunityController extends BaseApiController return $this->fail('请输入内容或上传图片'); } + if ($categoryId <= 0) { + return $this->fail('请选择帖子分类'); + } + $category = CommunityCategory::where('id', $categoryId)->where('status', 1)->findOrEmpty(); + if ($category->isEmpty()) { + return $this->fail('所选分类不可用'); + } + + if (count($tagIds) > 3) { + return $this->fail('最多选择3个标签'); + } + if ($tagIds) { + $validTagCount = CommunityTag::whereIn('id', $tagIds)->where('status', 1)->count(); + if ($validTagCount !== count($tagIds)) { + return $this->fail('存在不可用标签'); + } + } + if ($isPaid && $pricePoints <= 0) { return $this->fail('付费帖子必须设置积分价格'); } @@ -192,6 +211,7 @@ class CommunityController extends BaseApiController 'content' => $content, 'images' => $images, 'post_type' => $postType, + 'category_id' => $categoryId, 'match_id' => $matchId, 'is_paid' => $isPaid ? 1 : 0, 'price_points' => $isPaid ? $pricePoints : 0, @@ -211,6 +231,7 @@ class CommunityController extends BaseApiController // 更新标签帖子数 CommunityTag::whereIn('id', $tagIds)->inc('post_count')->update(); } + CommunityCategory::where('id', $categoryId)->inc('post_count')->update(); Db::commit(); KbSyncService::enqueue('post', 'post', (int) $post->id, 'upsert', 30); diff --git a/uniapp/src/packages_community/pages/publish.vue b/uniapp/src/packages_community/pages/publish.vue index bea82cf6..8cc64cf6 100644 --- a/uniapp/src/packages_community/pages/publish.vue +++ b/uniapp/src/packages_community/pages/publish.vue @@ -48,9 +48,36 @@ + + + + 选择分类 + 必选 + + {{ selectedCategory ? '已选择 1 个分类' : '请选择帖子所属分类' }} + + + + + + {{ category.name.slice(0, 1) }} + + {{ category.name }} + + + + 暂无可选分类 + + - - 选择标签 + + + 选择标签 + 可多选 + + 已选 {{ selectedTags.length }}/3 import { reactive, ref, computed, onMounted } from 'vue' -import { publishPost, getTagLists } from '@/api/community' +import { publishPost, getTagLists, getCommunityCategoryLists } from '@/api/community' import { uploadImage } from '@/api/app' import { getToken } from '@/utils/auth' @@ -75,6 +102,8 @@ const scrollHeight = ref(500) const publishing = ref(false) const tagList = ref([]) const selectedTags = ref([]) +const categoryList = ref([]) +const selectedCategory = ref(0) const isPaid = ref(false) const pricePoints = ref('') @@ -85,7 +114,7 @@ const form = reactive({ }) const canPublish = computed(() => { - return form.content.trim().length > 0 || form.images.length > 0 + return selectedCategory.value > 0 && (form.content.trim().length > 0 || form.images.length > 0) }) onMounted(() => { @@ -97,13 +126,14 @@ onMounted(() => { statusBarHeight.value = 20 // #endif scrollHeight.value = sysInfo.windowHeight - statusBarHeight.value - 88 - fetchTags() + fetchPublishOptions() }) -const fetchTags = async () => { +const fetchPublishOptions = async () => { try { - const res = await getTagLists() - tagList.value = res || [] + const [categories, tags] = await Promise.all([getCommunityCategoryLists(), getTagLists()]) + categoryList.value = categories || [] + tagList.value = tags || [] } catch (e) { } } @@ -121,6 +151,10 @@ const handlePublish = async () => { 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 { @@ -148,6 +182,7 @@ const handlePublish = async () => { await publishPost({ content: form.content, images: uploadedImages, + category_id: selectedCategory.value, tag_ids: selectedTags.value, post_type: 0, is_paid: isPaid.value ? 1 : 0, @@ -186,6 +221,10 @@ 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) { @@ -377,17 +416,134 @@ const toggleTag = (id: number) => { } } -.publish-tags { +.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; - &__label { - font-size: 28rpx; - font-weight: 600; - color: #333; - margin-bottom: 20rpx; + &__list { + display: flex; + flex-wrap: wrap; + gap: 16rpx; } + &__item { + position: relative; + display: flex; + align-items: center; + min-width: calc((100% - 16rpx) / 2); + max-width: 100%; + height: 76rpx; + box-sizing: border-box; + padding: 0 20rpx; + gap: 12rpx; + border: 2rpx solid #edf0f5; + border-radius: 14rpx; + background: #fafbfc; + + &--active { + border-color: #185dff; + background: #eef4ff; + + .publish-category__name { + color: #185dff; + font-weight: 600; + } + + &::after { + position: absolute; + top: 0; + right: 0; + width: 34rpx; + height: 34rpx; + border-radius: 0 10rpx 0 12rpx; + background: #185dff; + content: ''; + } + + .u-icon { + position: absolute; + top: 2rpx; + right: 3rpx; + z-index: 1; + } + } + } + + &__icon { + width: 40rpx; + height: 40rpx; + border-radius: 50%; + flex-shrink: 0; + + &--fallback { + display: flex; + align-items: center; + justify-content: center; + background: #dbe8ff; + + text { + color: #185dff; + font-size: 20rpx; + font-weight: 700; + } + } + } + + &__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;