feat: support full community post editing

This commit is contained in:
hajimi
2026-08-02 20:38:47 +08:00
parent f78580b2b9
commit 2358603eda
9 changed files with 638 additions and 72 deletions
+3 -3
View File
@@ -10,9 +10,9 @@ export function communityPostDetail(params: any) {
return request.get({ url: '/community.communityPost/detail', params })
}
// 编辑帖子内容
export function communityPostEditContent(params: any) {
return request.post({ url: '/community.communityPost/editContent', params })
// 编辑帖子
export function communityPostEdit(params: any) {
return request.post({ url: '/community.communityPost/edit', params })
}
// 帖子审核状态
+319 -26
View File
@@ -68,6 +68,7 @@
<el-table-column label="类型" min-width="80">
<template #default="{ row }">
<el-tag v-if="row.post_type === 1" type="warning">赛事分析</el-tag>
<el-tag v-else-if="row.post_type === 2" type="success">战绩分享</el-tag>
<el-tag v-else>普通</el-tag>
</template>
</el-table-column>
@@ -135,8 +136,8 @@
@click="handleDetail(row.id)">
详情
</el-button>
<el-button v-perms="['community.communityPost/editContent']" type="primary" link
@click="handleEdit(row)">
<el-button v-perms="['community.communityPost/edit']" type="primary" link
@click="handleEdit(row.id)">
编辑
</el-button>
<el-button v-if="row.status === 0" v-perms="['community.communityPost/updateStatus']"
@@ -203,16 +204,161 @@
<el-button @click="showDetail = false">关闭</el-button>
</template>
</el-dialog>
<el-dialog v-model="showEdit" title="编辑帖子内容" width="700px" destroy-on-close>
<el-form label-position="top">
<el-form-item label="帖子内容">
<el-input v-model="editData.content" type="textarea" :rows="14" :maxlength="16000"
show-word-limit resize="vertical" placeholder="请输入帖子内容" />
</el-form-item>
</el-form>
<el-dialog v-model="showEdit" title="编辑帖子" width="920px" destroy-on-close>
<el-scrollbar max-height="68vh">
<el-form ref="editFormRef" v-loading="editLoading" :model="editData" :rules="editRules"
label-width="104px" class="pr-5">
<el-divider content-position="left">基础信息</el-divider>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="帖子ID">
<el-input :model-value="String(editData.id || '')" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="用户ID" prop="user_id">
<el-input-number v-model="editData.user_id" :min="1" :precision="0"
controls-position="right" class="!w-full" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="发布时间" prop="create_time">
<el-date-picker v-model="editData.create_time" type="datetime"
value-format="YYYY-MM-DD HH:mm:ss" placeholder="选择发布时间" class="!w-full" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="来源标识" prop="origin_id">
<el-input v-model="editData.origin_id" maxlength="100" clearable
placeholder="例如 truthsocial:123456" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="更新时间">
<el-input :model-value="editData.update_time || '-'" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="帖子类型" prop="post_type">
<el-select v-model="editData.post_type" class="w-full">
<el-option label="普通帖子" :value="0" />
<el-option label="赛事分析" :value="1" />
<el-option label="战绩分享" :value="2" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="帖子分类" prop="category_id">
<el-select v-model="editData.category_id" filterable class="w-full">
<el-option label="未分类" :value="0" />
<el-option v-for="item in categoryList" :key="item.id"
:label="item.status === 1 ? item.name : `${item.name}(禁用)`"
:value="item.id" :disabled="item.status !== 1 && item.id !== editData.category_id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="关联赛事ID" prop="match_id">
<el-input-number v-model="editData.match_id" :min="0" :precision="0"
controls-position="right" class="!w-full" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="帖子标签" prop="tag_ids">
<el-select v-model="editData.tag_ids" multiple filterable :multiple-limit="3"
collapse-tags collapse-tags-tooltip class="w-full" placeholder="最多选择3个标签">
<el-option v-for="item in tagList" :key="item.id"
:label="item.status === 1 ? item.name : `${item.name}(禁用)`"
:value="item.id" :disabled="item.status !== 1 && !editData.tag_ids.includes(item.id)" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">正文与素材</el-divider>
<el-form-item label="帖子内容" prop="content">
<el-input v-model="editData.content" type="textarea" :rows="8" :maxlength="16000"
show-word-limit resize="vertical" placeholder="帖子内容和图片不能同时为空" />
</el-form-item>
<el-form-item label="帖子图片">
<material-picker v-model="editData.images" :limit="18" />
</el-form-item>
<el-divider content-position="left">付费设置</el-divider>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="收费阅读" prop="is_paid">
<el-switch v-model="editData.is_paid" :active-value="1" :inactive-value="0"
@change="handlePaidChange" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="积分价格" prop="price_points">
<el-input-number v-model="editData.price_points" :min="0" :max="9999" :precision="0"
:disabled="editData.is_paid !== 1" controls-position="right" class="!w-full" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="免费预览" prop="free_content_len">
<el-input-number v-model="editData.free_content_len" :min="20" :max="500"
:precision="0" :disabled="editData.is_paid !== 1" controls-position="right"
class="!w-full" />
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">统计数据手动修正</el-divider>
<el-row :gutter="20">
<el-col v-for="item in countFields" :key="item.key" :span="8">
<el-form-item :label="item.label" :prop="item.key">
<el-input-number v-model="editData[item.key]" :min="0" :precision="0"
controls-position="right" class="!w-full" />
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">展示与状态</el-divider>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="审核状态" prop="status">
<el-select v-model="editData.status" class="w-full">
<el-option label="待审核" :value="0" />
<el-option label="已通过" :value="1" />
<el-option label="已拒绝/隐藏" :value="2" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="推荐排序" prop="sort">
<el-input-number v-model="editData.sort" :min="0" :max="999999" :precision="0"
controls-position="right" class="!w-full" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="展示开关">
<div class="flex flex-wrap gap-x-4 gap-y-2">
<el-checkbox v-model="editData.is_top" :true-value="1" :false-value="0">置顶</el-checkbox>
<el-checkbox v-model="editData.is_recommend" :true-value="1"
:false-value="0">推荐</el-checkbox>
<el-checkbox v-model="editData.is_hot" :true-value="1" :false-value="0">热门</el-checkbox>
<el-checkbox v-model="editData.is_topic" :true-value="1"
:false-value="0">话题</el-checkbox>
</div>
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">扩展数据</el-divider>
<el-form-item label="扩展JSON" prop="ext">
<el-input v-model="editData.ext" type="textarea" :rows="6" resize="vertical"
placeholder='请输入合法 JSON,例如 {"source":"manual"}' />
</el-form-item>
</el-form>
</el-scrollbar>
<template #footer>
<el-button @click="showEdit = false">取消</el-button>
<el-button type="primary" :loading="editSubmitLoading" @click="handleSubmitEdit">
<el-button type="primary" :loading="editSubmitLoading" :disabled="editLoading"
@click="handleSubmitEdit">
保存
</el-button>
</template>
@@ -223,7 +369,7 @@
import {
communityPostLists,
communityPostDetail,
communityPostEditContent,
communityPostEdit,
communityPostStatus,
communityPostSetTop,
communityPostSetHot,
@@ -231,11 +377,13 @@ import {
communityPostSetSort,
communityPostSetTopic,
communityPostSetCategory,
communityTagAll,
communityCategoryAll,
communityPostDelete
} from '@/api/community'
import { usePaging } from '@/hooks/usePaging'
import feedback from '@/utils/feedback'
import type { FormInstance, FormItemRule, FormRules } from 'element-plus'
const queryParams = reactive({
status: '',
@@ -247,6 +395,7 @@ const queryParams = reactive({
})
const categoryList = ref<any[]>([])
const tagList = ref<any[]>([])
const { pager, getLists, resetPage, resetParams } = usePaging({
fetchFun: communityPostLists,
@@ -335,33 +484,167 @@ const handleDetail = async (id: number) => {
}
const showEdit = ref(false)
const editLoading = ref(false)
const editSubmitLoading = ref(false)
const editData = reactive({
const createEditData = () => ({
id: 0,
content: ''
origin_id: '',
user_id: 0,
content: '',
images: [] as string[],
post_type: 0,
category_id: 0,
match_id: 0,
tag_ids: [] as number[],
is_paid: 0,
price_points: 0,
free_content_len: 100,
paid_count: 0,
view_count: 0,
like_count: 0,
comment_count: 0,
share_count: 0,
ext: '',
is_top: 0,
is_hot: 0,
is_recommend: 0,
is_topic: 0,
sort: 0,
status: 1,
create_time: '',
update_time: ''
})
type EditData = ReturnType<typeof createEditData>
type CountFieldKey = 'paid_count' | 'view_count' | 'like_count' | 'comment_count' | 'share_count'
const handleEdit = (post: any) => {
const editData = reactive<EditData>(createEditData())
const editFormRef = ref<FormInstance>()
const countFields: Array<{ key: CountFieldKey; label: string }> = [
{ key: 'paid_count', label: '购买人数' },
{ key: 'view_count', label: '浏览数' },
{ key: 'like_count', label: '点赞数' },
{ key: 'comment_count', label: '评论数' },
{ key: 'share_count', label: '分享数' }
]
const validatePostBody = (_rule: any, _value: any, callback: (error?: Error) => void) => {
if (!editData.content.trim() && editData.images.length === 0) {
callback(new Error('帖子内容和图片不能同时为空'))
return
}
callback()
}
const validatePaidPrice = (_rule: any, value: number, callback: (error?: Error) => void) => {
if (editData.is_paid === 1 && (!value || value < 1 || value > 9999)) {
callback(new Error('付费帖子积分价格范围为1~9999'))
return
}
callback()
}
const validateExt = (_rule: any, value: string, callback: (error?: Error) => void) => {
if (!value.trim()) {
callback()
return
}
try {
JSON.parse(value)
callback()
} catch {
callback(new Error('请输入合法的 JSON'))
}
}
const nonNegativeRule: FormItemRule[] = [
{ required: true, type: 'number', min: 0, message: '请输入不小于0的整数' }
]
const editRules: FormRules = {
user_id: [{ required: true, type: 'number', min: 1, message: '请输入有效的用户ID', trigger: 'blur' }],
content: [{ validator: validatePostBody, trigger: 'blur' }],
post_type: [{ required: true, type: 'number', message: '请选择帖子类型', trigger: 'change' }],
category_id: nonNegativeRule,
match_id: nonNegativeRule,
tag_ids: [{ type: 'array', max: 3, message: '帖子标签最多选择3个', trigger: 'change' }],
price_points: [{ validator: validatePaidPrice, trigger: 'blur' }],
free_content_len: [{ required: true, type: 'number', min: 20, max: 500, message: '免费预览字数为20~500' }],
paid_count: nonNegativeRule,
view_count: nonNegativeRule,
like_count: nonNegativeRule,
comment_count: nonNegativeRule,
share_count: nonNegativeRule,
sort: nonNegativeRule,
status: [{ required: true, type: 'number', message: '请选择审核状态', trigger: 'change' }],
create_time: [{ required: true, message: '请选择发布时间', trigger: 'change' }],
ext: [{ validator: validateExt, trigger: 'blur' }]
}
const normalizeExt = (value: any) => {
if (value === null || value === undefined || value === '') return ''
if (typeof value === 'string') return value
return JSON.stringify(value, null, 2)
}
const handleEdit = async (id: number) => {
showEdit.value = true
editData.id = post.id
editData.content = post.content || ''
editLoading.value = true
Object.assign(editData, createEditData())
try {
const res = await communityPostDetail({ id })
Object.assign(editData, createEditData(), res, {
id: Number(res.id || id),
origin_id: String(res.origin_id || ''),
user_id: Number(res.user_id || 0),
content: String(res.content || ''),
images: Array.isArray(res.images) ? res.images : [],
post_type: Number(res.post_type || 0),
category_id: Number(res.category_id || 0),
match_id: Number(res.match_id || 0),
tag_ids: Array.isArray(res.tag_ids) ? res.tag_ids.map(Number) : [],
is_paid: Number(res.is_paid || 0),
ext: normalizeExt(res.ext),
price_points: Number(res.is_paid) === 1 ? Number(res.price_points || 0) : 0,
free_content_len: Number(res.is_paid) === 1 ? Number(res.free_content_len || 100) : 100,
paid_count: Number(res.paid_count || 0),
view_count: Number(res.view_count || 0),
like_count: Number(res.like_count || 0),
comment_count: Number(res.comment_count || 0),
share_count: Number(res.share_count || 0),
is_top: Number(res.is_top || 0),
is_hot: Number(res.is_hot || 0),
is_recommend: Number(res.is_recommend || 0),
is_topic: Number(res.is_topic || 0),
sort: Number(res.sort || 0),
status: Number(res.status || 0),
create_time: String(res.create_time || ''),
update_time: String(res.update_time || '')
})
} finally {
editLoading.value = false
}
}
const handlePaidChange = (value: any) => {
if (Number(value) !== 1) {
editData.price_points = 0
editData.free_content_len = 100
}
}
const handleSubmitEdit = async () => {
if (!editData.content.trim()) {
feedback.msgError('请输入帖子内容')
return
}
const valid = await editFormRef.value?.validate().catch(() => false)
if (!valid) return
editSubmitLoading.value = true
try {
await communityPostEditContent({
id: editData.id,
content: editData.content
await communityPostEdit({
...editData,
origin_id: editData.origin_id.trim(),
content: editData.content,
images: [...editData.images],
tag_ids: [...editData.tag_ids],
ext: editData.ext.trim()
})
showEdit.value = false
if (detailData.value.id === editData.id) {
detailData.value.content = editData.content
}
getLists()
} finally {
editSubmitLoading.value = false
@@ -377,6 +660,16 @@ const loadCategories = async () => {
}
}
const loadTags = async () => {
try {
const tags = await communityTagAll()
tagList.value = Array.isArray(tags) ? tags : []
} catch {
tagList.value = []
}
}
loadCategories()
loadTags()
getLists()
</script>