Commit 0e5824f6 by lijiabin

【需求 25171】refactor: 发文页按文章类型拆成独立组件

publishLongArticle 之前把专栏/专访/帖子/问吧/实践五种类型的表单全挤在一个文件里,
字段、校验规则、官方账号/管理员身份判断混在一起,改一个类型容易牵连到别的类型。

按类型拆成 components/ 下的 PostForm、ColumnForm、InterviewForm、QuestionForm、
PracticeForm,每个组件独立维护自己完整的抽屉表单(字段、校验、发布时间/IP信息、
提交数据、编辑回显),文件开头写清楚了这个类型的特殊规则(比如帖子封面图什么时候
必填、专栏独有的"同步同事吧"、问吧/实践用图片不用封面图、实践没有推送等)。外层
index.vue 只保留标题/内容编辑器、抽屉壳子和提交/回显的整体编排。

顺带把 api/article/types.ts 里跟实际请求对不上的类型补全了:AddOrUpdatePostDto
之前没声明推送字段,PushSettingBase.pushList 缺了 valueType,新增了
AddOrUpdateQuestionDto。
parent a4df63ff
......@@ -26,11 +26,32 @@ export type AddOrUpdateArticleDto =
| AddOrUpdateColumnDto
| AddOrUpdateInterviewDto
| AddOrUpdateVideoDto
| AddOrUpdateQuestionDto
/**
* 推送对象列表里的一项(部门或人员)
*/
export interface PushListItem {
valueId: string
valueType: number
valueName: string | number
}
/**
* 官方账号才有的推送设置字段,帖子/问吧/专栏/专访都共用这一套
*/
interface OfficialPushFields {
isPushAll?: BooleanFlag
pushType?: SendTypeEnum
pushTime?: string
pushList?: PushListItem[]
summary?: string
}
/**
* 添加帖子的DTO
*/
export interface AddOrUpdatePostDto {
export interface AddOrUpdatePostDto extends OfficialPushFields {
id?: number
title?: string
content?: string
......@@ -44,6 +65,25 @@ export interface AddOrUpdatePostDto {
regionHide?: BooleanFlag
}
/**
* 添加问吧的DTO
*/
export interface AddOrUpdateQuestionDto extends OfficialPushFields {
id?: number
title?: string
content?: string
releaseStatus?: ReleaseStatusTypeEnum
sendType?: SendTypeEnum
// 问吧封面用内容图片的第一张,faceUrl 是前端自己派生后一起传的
imgUrl?: string
faceUrl?: string
sendTime?: string
type?: ArticleTypeEnum.QUESTION
regionHide?: BooleanFlag
mainTagId?: string
tagList?: { tagId: number; sort: number }[]
}
interface AddOrUpdateColumnBase {
id?: number
type: ArticleTypeEnum.COLUMN
......@@ -96,9 +136,10 @@ export interface AddOrUpdateInterviewForm extends AddOrUpdateInterviewBase {
}
interface PushSettingBase {
isPushAll?: BooleanFlag
pushType: SendTypeEnum
pushTime: string
pushList: { valueId: string; valueName: string | number }[]
pushList: PushListItem[]
summary: string
}
// 推送设置相关的字段
......
<template>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
<!-- 封面图:不管什么身份都必填(跟帖子不一样,帖子普通用户可以不传) -->
<el-form-item label="封面图" prop="faceUrl">
<UploadFile class="w-fit" v-model="form.faceUrl" :limit="1" :aspect-ratio="3 / 2" crop />
</el-form-item>
<!-- 主/副标签:专栏必填 -->
<el-form-item label="主标签" prop="mainTagId">
<SelectTags v-model="form.mainTagId" class="w-full" />
</el-form-item>
<el-form-item label="副标签 (最多3个)">
<SelectTags
v-model="form.tagList"
:filter-tags-fn="filterTagsFn"
:max-selected-tags="3"
class="w-full"
/>
</el-form-item>
<!-- 所属栏目 -->
<el-form-item label="所属栏目" prop="relateColumnId">
<el-select v-model="form.relateColumnId" placeholder="请选择专栏栏目" class="w-full">
<el-option
v-for="item in columnList"
:key="item.id"
:value="item.id"
:label="item.title"
/>
</el-select>
</el-form-item>
<el-form-item label="是否推荐" prop="isRecommend">
<el-radio-group v-model="form.isRecommend">
<el-radio :value="BooleanFlag.YES">是</el-radio>
<el-radio :value="BooleanFlag.NO">否</el-radio>
</el-radio-group>
</el-form-item>
<!-- 同步同事吧:专栏独有,专访没有这个字段 -->
<el-form-item label="同步同事吧" prop="isRelateColleague">
<el-radio-group v-model="form.isRelateColleague">
<el-radio :value="BooleanFlag.YES">是</el-radio>
<el-radio :value="BooleanFlag.NO">否</el-radio>
</el-radio-group>
</el-form-item>
<!-- 官方账号才有:推送配置 + 插入视频(问吧/帖子的官方账号只有推送,没有插入视频) -->
<template v-if="userInfo.isOfficialAccount">
<el-form-item label="推送给">
<div class="w-full flex items-center gap-2">
<el-radio-group v-model="form.isPushAll">
<el-radio :value="BooleanFlag.YES">全员</el-radio>
<el-radio :value="BooleanFlag.NO">指定部门/人员</el-radio>
</el-radio-group>
</div>
<div v-show="form.isPushAll === BooleanFlag.NO" class="flex items-center gap-2">
<DeptUserPicker
v-model:dept-value="pushDeptIds"
v-model:user-value="pushUserIds"
selection-mode="both"
:initial-selected="pushInitialSelected"
@confirm="(items) => (pushSelection = items)"
v-slot="{ echoText }"
>
<el-input
readonly
class="w-80 cursor-pointer"
placeholder="请选择部门/人员"
:model-value="echoText"
/>
</DeptUserPicker>
</div>
</el-form-item>
<el-form-item label="推送类型" prop="pushType">
<el-radio-group v-model="form.pushType">
<el-radio :value="SendTypeEnum.IMMEDIATE">立即推送</el-radio>
<el-radio :value="SendTypeEnum.SCHEDULED">定时推送</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.pushType === SendTypeEnum.SCHEDULED" prop="pushTime">
<el-date-picker
v-model="form.pushTime"
type="datetime"
placeholder="选择推送时间"
format="YYYY-MM-DD HH:mm"
time-format="HH:mm"
:disabled-date="(time: Date) => time.getTime() < Date.now() - 1000 * 60 * 60 * 24"
:disabled-minutes="disabledMinutesOnlyTens"
:disabled-seconds="disabledSecondsOnlyZero"
value-format="X"
/>
</el-form-item>
<el-form-item label="推送摘要" prop="summary">
<el-input
v-model="form.summary"
type="textarea"
:maxlength="50"
show-word-limit
placeholder="简单描述推送的信息"
/>
</el-form-item>
<el-form-item label="插入视频" prop="videoLocation">
<el-radio-group v-model="form.videoLocation">
<el-radio :value="VideoPositionEnum.TOP">顶部</el-radio>
<el-radio :value="VideoPositionEnum.BOTTOM">底部</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="视频" prop="articleVideoUrl">
<UploadVideo v-model="form.articleVideoUrl" btnClass="flex-col" />
</el-form-item>
</template>
<!-- 发布时间 + 官方账号的 IP 信息:所有类型都有,规则完全一样,每个类型自己维护一份 -->
<el-form-item label="发布时间" prop="sendType">
<el-radio-group v-model="form.sendType">
<el-radio :value="SendTypeEnum.IMMEDIATE">立即发布</el-radio>
<el-radio :value="SendTypeEnum.SCHEDULED">定时发布</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.sendType === SendTypeEnum.SCHEDULED" prop="sendTime">
<el-date-picker
v-model="form.sendTime"
type="datetime"
placeholder="选择发布时间"
format="YYYY-MM-DD HH:mm"
time-format="HH:mm"
:disabled-date="(time: Date) => time.getTime() < Date.now() - 1000 * 60 * 60 * 24"
:disabled-minutes="disabledMinutesOnlyTens"
:disabled-seconds="disabledSecondsOnlyZero"
value-format="X"
/>
</el-form-item>
<el-form-item v-if="userInfo.isOfficialAccount" label="IP信息" prop="regionHide">
<el-switch
v-model="form.regionHide"
:active-value="BooleanFlag.YES"
:inactive-value="BooleanFlag.NO"
active-text="隐藏地区"
inactive-text="显示地区"
/>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
/**
* 专栏(COLUMN)专属表单——自己管自己的整个抽屉表单,不依赖外层的公共表单。
*
* 专栏的特殊规则:
* - 封面图:不管什么身份都必填(跟帖子不一样,帖子普通用户可以不传)
* - 主/副标签必填,还要选择所属栏目、是否推荐
* - "同步同事吧"是专栏独有的字段,专访没有
* - 官方账号才有:推送配置(推送给谁/推送类型/推送摘要)+ 插入视频(顶部/底部 + 视频文件)——
* 这个"插入视频"能力只有专栏、专访才有,帖子、问吧的官方账号只有推送、没有插入视频
*/
import type { FormInstance, FormItemRule } from 'element-plus'
import { BooleanFlag, SendTypeEnum, VideoPositionEnum } from '@/constants'
import UploadFile from '@/components/common/UploadFile/index.vue'
import UploadVideo from '@/components/common/UploadVideo/index.vue'
import SelectTags from '@/components/common/SelectTags/index.vue'
import DeptUserPicker from '@/components/common/DeptUserPicker/index.vue'
import type { DepartmentUserSelection } from '@/components/common/DepartmentUserModal/types'
import type { ArticleItemDto } from '@/api/article/types'
import { useResetData } from '@/hooks'
import { useUserStore } from '@/stores/user'
import { useColumnStore } from '@/stores/column'
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const columnStore = useColumnStore()
const { columnList } = storeToRefs(columnStore)
const formRef = useTemplateRef<FormInstance>('formRef')
const [form, resetForm] = useResetData({
faceUrl: '',
mainTagId: '',
tagList: [] as number[],
relateColumnId: '' as string | number,
isRecommend: BooleanFlag.NO,
isRelateColleague: BooleanFlag.NO,
isPushAll: BooleanFlag.YES,
pushType: SendTypeEnum.IMMEDIATE,
pushTime: '',
summary: '',
videoLocation: VideoPositionEnum.TOP,
articleVideoUrl: '',
sendType: SendTypeEnum.IMMEDIATE,
sendTime: '',
regionHide: BooleanFlag.NO,
})
const pushDeptIds = ref<string[]>([])
const pushUserIds = ref<string[]>([])
const pushSelection = ref<DepartmentUserSelection[]>([])
const pushInitialSelected = ref<DepartmentUserSelection[]>([])
const disabledSecondsOnlyZero = () => Array.from({ length: 59 }, (_, i) => i + 1)
const disabledMinutesOnlyTens = () =>
Array.from({ length: 60 }, (_, i) => i).filter((m) => m % 10 !== 0)
const filterTagsFn = (allTags: any[]) => allTags.filter((tag) => tag.id !== Number(form.value.mainTagId))
const rules: Record<string, FormItemRule[]> = {
faceUrl: [{ required: true, message: '请上传封面图', trigger: 'blur' }],
mainTagId: [{ required: true, message: '请选择主标签', trigger: 'blur' }],
relateColumnId: [{ required: true, message: '请选择对应的栏目', trigger: 'trigger' }],
isRecommend: [{ required: true, message: '是否推荐', trigger: 'trigger' }],
isRelateColleague: [{ required: true, message: '是否同步同事吧', trigger: 'trigger' }],
pushType: [{ required: true, message: '请选择推送类型', trigger: 'trigger' }],
pushTime: [{ required: true, message: '请选择推送时间', trigger: 'trigger' }],
sendType: [{ required: true, message: '请选择发布类型', trigger: 'blur' }],
sendTime: [
{
validator: (rule, value, callback) => {
if (form.value.sendType === SendTypeEnum.SCHEDULED && !value) {
callback(new Error('请选择发布时间'))
} else {
callback()
}
},
trigger: 'trigger',
},
],
regionHide: [{ required: true, message: '请选择是否隐藏IP信息', trigger: 'trigger' }],
}
const buildPushList = () =>
form.value.isPushAll === BooleanFlag.YES
? [{ valueId: '1', valueType: 1, valueName: '亚声威格' }]
: pushSelection.value.map((item) => ({
valueId: item.valueId,
valueType: item.valueType,
valueName: item.valueName,
}))
defineExpose({
validate: () => formRef.value!.validate(),
getSubmitData: () => ({
faceUrl: form.value.faceUrl,
mainTagId: form.value.mainTagId,
tagList: [form.value.mainTagId, ...form.value.tagList].map((item, index) => ({
tagId: Number(item),
sort: index,
})),
relateColumnId: form.value.relateColumnId,
isRecommend: form.value.isRecommend,
isRelateColleague: form.value.isRelateColleague,
sendType: form.value.sendType,
sendTime: form.value.sendTime,
regionHide: form.value.regionHide,
...(userInfo.value.isOfficialAccount
? {
isPushAll: form.value.isPushAll,
pushType: form.value.pushType,
pushTime: form.value.pushTime,
summary: form.value.summary,
pushList: buildPushList(),
videoLocation: form.value.videoLocation,
articleVideoUrl: form.value.articleVideoUrl,
}
: {}),
}),
setEchoData: (data: ArticleItemDto) => {
form.value.faceUrl = data.faceUrl
form.value.mainTagId = data.tagIdList[0] ? String(data.tagIdList[0]) : ''
form.value.tagList = data.tagIdList.slice(1) || []
form.value.relateColumnId = data.relateColumnId
form.value.isRecommend = data.isRecommend
form.value.isRelateColleague = data.isRelateColleague
form.value.sendType = data.sendType
form.value.sendTime = data.sendTime
form.value.regionHide = data.regionHide
if (data.articleVideoUrl) form.value.articleVideoUrl = data.articleVideoUrl
if (!userInfo.value.isOfficialAccount) return
form.value.summary = data.summary
if (data.pushList?.find((item) => item.valueId == '1')) {
form.value.isPushAll = BooleanFlag.YES
pushDeptIds.value = []
pushUserIds.value = []
pushSelection.value = []
pushInitialSelected.value = []
} else {
form.value.isPushAll = BooleanFlag.NO
const items: DepartmentUserSelection[] = (data.pushList || []).map((item) => ({
valueType: item.valueType as 1 | 2,
valueId: String(item.valueId),
valueName: item.valueName,
}))
pushDeptIds.value = items.filter((item) => item.valueType === 1).map((item) => item.valueId)
pushUserIds.value = items.filter((item) => item.valueType === 2).map((item) => item.valueId)
pushSelection.value = items
pushInitialSelected.value = items
}
},
reset: () => {
resetForm()
pushDeptIds.value = []
pushUserIds.value = []
pushSelection.value = []
pushInitialSelected.value = []
},
})
</script>
<template>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
<!-- 封面图:不管什么身份都必填(跟帖子不一样,帖子普通用户可以不传) -->
<el-form-item label="封面图" prop="faceUrl">
<UploadFile class="w-fit" v-model="form.faceUrl" :limit="1" :aspect-ratio="3 / 2" crop />
</el-form-item>
<!-- 主/副标签:专访必填 -->
<el-form-item label="主标签" prop="mainTagId">
<SelectTags v-model="form.mainTagId" class="w-full" />
</el-form-item>
<el-form-item label="副标签 (最多3个)">
<SelectTags
v-model="form.tagList"
:filter-tags-fn="filterTagsFn"
:max-selected-tags="3"
class="w-full"
/>
</el-form-item>
<!-- 所属栏目 -->
<el-form-item label="所属栏目" prop="relateColumnId">
<el-select v-model="form.relateColumnId" placeholder="请选择专访栏目" class="w-full">
<el-option
v-for="item in interviewList"
:key="item.id"
:value="item.id"
:label="item.title"
/>
</el-select>
</el-form-item>
<el-form-item label="是否推荐" prop="isRecommend">
<el-radio-group v-model="form.isRecommend">
<el-radio :value="BooleanFlag.YES">是</el-radio>
<el-radio :value="BooleanFlag.NO">否</el-radio>
</el-radio-group>
</el-form-item>
<!-- 专访没有"同步同事吧"这个字段,这是专栏独有的 -->
<!-- 官方账号才有:推送配置 + 插入视频 -->
<template v-if="userInfo.isOfficialAccount">
<el-form-item label="推送给">
<div class="w-full flex items-center gap-2">
<el-radio-group v-model="form.isPushAll">
<el-radio :value="BooleanFlag.YES">全员</el-radio>
<el-radio :value="BooleanFlag.NO">指定部门/人员</el-radio>
</el-radio-group>
</div>
<div v-show="form.isPushAll === BooleanFlag.NO" class="flex items-center gap-2">
<DeptUserPicker
v-model:dept-value="pushDeptIds"
v-model:user-value="pushUserIds"
selection-mode="both"
:initial-selected="pushInitialSelected"
@confirm="(items) => (pushSelection = items)"
v-slot="{ echoText }"
>
<el-input
readonly
class="w-80 cursor-pointer"
placeholder="请选择部门/人员"
:model-value="echoText"
/>
</DeptUserPicker>
</div>
</el-form-item>
<el-form-item label="推送类型" prop="pushType">
<el-radio-group v-model="form.pushType">
<el-radio :value="SendTypeEnum.IMMEDIATE">立即推送</el-radio>
<el-radio :value="SendTypeEnum.SCHEDULED">定时推送</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.pushType === SendTypeEnum.SCHEDULED" prop="pushTime">
<el-date-picker
v-model="form.pushTime"
type="datetime"
placeholder="选择推送时间"
format="YYYY-MM-DD HH:mm"
time-format="HH:mm"
:disabled-date="(time: Date) => time.getTime() < Date.now() - 1000 * 60 * 60 * 24"
:disabled-minutes="disabledMinutesOnlyTens"
:disabled-seconds="disabledSecondsOnlyZero"
value-format="X"
/>
</el-form-item>
<el-form-item label="推送摘要" prop="summary">
<el-input
v-model="form.summary"
type="textarea"
:maxlength="50"
show-word-limit
placeholder="简单描述推送的信息"
/>
</el-form-item>
<el-form-item label="插入视频" prop="videoLocation">
<el-radio-group v-model="form.videoLocation">
<el-radio :value="VideoPositionEnum.TOP">顶部</el-radio>
<el-radio :value="VideoPositionEnum.BOTTOM">底部</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="视频" prop="articleVideoUrl">
<UploadVideo v-model="form.articleVideoUrl" btnClass="flex-col" />
</el-form-item>
</template>
<!-- 发布时间 + 官方账号的 IP 信息:所有类型都有,规则完全一样,每个类型自己维护一份 -->
<el-form-item label="发布时间" prop="sendType">
<el-radio-group v-model="form.sendType">
<el-radio :value="SendTypeEnum.IMMEDIATE">立即发布</el-radio>
<el-radio :value="SendTypeEnum.SCHEDULED">定时发布</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.sendType === SendTypeEnum.SCHEDULED" prop="sendTime">
<el-date-picker
v-model="form.sendTime"
type="datetime"
placeholder="选择发布时间"
format="YYYY-MM-DD HH:mm"
time-format="HH:mm"
:disabled-date="(time: Date) => time.getTime() < Date.now() - 1000 * 60 * 60 * 24"
:disabled-minutes="disabledMinutesOnlyTens"
:disabled-seconds="disabledSecondsOnlyZero"
value-format="X"
/>
</el-form-item>
<el-form-item v-if="userInfo.isOfficialAccount" label="IP信息" prop="regionHide">
<el-switch
v-model="form.regionHide"
:active-value="BooleanFlag.YES"
:inactive-value="BooleanFlag.NO"
active-text="隐藏地区"
inactive-text="显示地区"
/>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
/**
* 专访(INTERVIEW)专属表单——自己管自己的整个抽屉表单,不依赖外层的公共表单。
* 跟专栏几乎一样,唯一区别:
* - 没有"同步同事吧"字段(那是专栏独有的)
* 其余规则(封面图必填、标签必填、所属栏目、官方账号推送+插入视频)跟专栏完全一致。
*/
import type { FormInstance, FormItemRule } from 'element-plus'
import { BooleanFlag, SendTypeEnum, VideoPositionEnum } from '@/constants'
import UploadFile from '@/components/common/UploadFile/index.vue'
import UploadVideo from '@/components/common/UploadVideo/index.vue'
import SelectTags from '@/components/common/SelectTags/index.vue'
import DeptUserPicker from '@/components/common/DeptUserPicker/index.vue'
import type { DepartmentUserSelection } from '@/components/common/DepartmentUserModal/types'
import type { ArticleItemDto } from '@/api/article/types'
import { useResetData } from '@/hooks'
import { useUserStore } from '@/stores/user'
import { useInterviewStore } from '@/stores/interview'
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const interviewStore = useInterviewStore()
const { interviewList } = storeToRefs(interviewStore)
const formRef = useTemplateRef<FormInstance>('formRef')
const [form, resetForm] = useResetData({
faceUrl: '',
mainTagId: '',
tagList: [] as number[],
relateColumnId: '' as string | number,
isRecommend: BooleanFlag.NO,
isPushAll: BooleanFlag.YES,
pushType: SendTypeEnum.IMMEDIATE,
pushTime: '',
summary: '',
videoLocation: VideoPositionEnum.TOP,
articleVideoUrl: '',
sendType: SendTypeEnum.IMMEDIATE,
sendTime: '',
regionHide: BooleanFlag.NO,
})
const pushDeptIds = ref<string[]>([])
const pushUserIds = ref<string[]>([])
const pushSelection = ref<DepartmentUserSelection[]>([])
const pushInitialSelected = ref<DepartmentUserSelection[]>([])
const disabledSecondsOnlyZero = () => Array.from({ length: 59 }, (_, i) => i + 1)
const disabledMinutesOnlyTens = () =>
Array.from({ length: 60 }, (_, i) => i).filter((m) => m % 10 !== 0)
const filterTagsFn = (allTags: any[]) => allTags.filter((tag) => tag.id !== Number(form.value.mainTagId))
const rules: Record<string, FormItemRule[]> = {
faceUrl: [{ required: true, message: '请上传封面图', trigger: 'blur' }],
mainTagId: [{ required: true, message: '请选择主标签', trigger: 'blur' }],
relateColumnId: [{ required: true, message: '请选择对应的栏目', trigger: 'trigger' }],
isRecommend: [{ required: true, message: '是否推荐', trigger: 'trigger' }],
pushType: [{ required: true, message: '请选择推送类型', trigger: 'trigger' }],
pushTime: [{ required: true, message: '请选择推送时间', trigger: 'trigger' }],
sendType: [{ required: true, message: '请选择发布类型', trigger: 'blur' }],
sendTime: [
{
validator: (rule, value, callback) => {
if (form.value.sendType === SendTypeEnum.SCHEDULED && !value) {
callback(new Error('请选择发布时间'))
} else {
callback()
}
},
trigger: 'trigger',
},
],
regionHide: [{ required: true, message: '请选择是否隐藏IP信息', trigger: 'trigger' }],
}
const buildPushList = () =>
form.value.isPushAll === BooleanFlag.YES
? [{ valueId: '1', valueType: 1, valueName: '亚声威格' }]
: pushSelection.value.map((item) => ({
valueId: item.valueId,
valueType: item.valueType,
valueName: item.valueName,
}))
defineExpose({
validate: () => formRef.value!.validate(),
getSubmitData: () => ({
faceUrl: form.value.faceUrl,
mainTagId: form.value.mainTagId,
tagList: [form.value.mainTagId, ...form.value.tagList].map((item, index) => ({
tagId: Number(item),
sort: index,
})),
relateColumnId: form.value.relateColumnId,
isRecommend: form.value.isRecommend,
sendType: form.value.sendType,
sendTime: form.value.sendTime,
regionHide: form.value.regionHide,
...(userInfo.value.isOfficialAccount
? {
isPushAll: form.value.isPushAll,
pushType: form.value.pushType,
pushTime: form.value.pushTime,
summary: form.value.summary,
pushList: buildPushList(),
videoLocation: form.value.videoLocation,
articleVideoUrl: form.value.articleVideoUrl,
}
: {}),
}),
setEchoData: (data: ArticleItemDto) => {
form.value.faceUrl = data.faceUrl
form.value.mainTagId = data.tagIdList[0] ? String(data.tagIdList[0]) : ''
form.value.tagList = data.tagIdList.slice(1) || []
form.value.relateColumnId = data.relateColumnId
form.value.isRecommend = data.isRecommend
form.value.sendType = data.sendType
form.value.sendTime = data.sendTime
form.value.regionHide = data.regionHide
if (data.articleVideoUrl) form.value.articleVideoUrl = data.articleVideoUrl
if (!userInfo.value.isOfficialAccount) return
form.value.summary = data.summary
if (data.pushList?.find((item) => item.valueId == '1')) {
form.value.isPushAll = BooleanFlag.YES
pushDeptIds.value = []
pushUserIds.value = []
pushSelection.value = []
pushInitialSelected.value = []
} else {
form.value.isPushAll = BooleanFlag.NO
const items: DepartmentUserSelection[] = (data.pushList || []).map((item) => ({
valueType: item.valueType as 1 | 2,
valueId: String(item.valueId),
valueName: item.valueName,
}))
pushDeptIds.value = items.filter((item) => item.valueType === 1).map((item) => item.valueId)
pushUserIds.value = items.filter((item) => item.valueType === 2).map((item) => item.valueId)
pushSelection.value = items
pushInitialSelected.value = items
}
},
reset: () => {
resetForm()
pushDeptIds.value = []
pushUserIds.value = []
pushSelection.value = []
pushInitialSelected.value = []
},
})
</script>
<template>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
<!-- 封面图:官方账号/管理员必填;普通用户选填(这点跟专栏/专访不一样,那两个不管什么身份都必填) -->
<el-form-item label="封面图" prop="faceUrl" :rules="faceUrlRules">
<UploadFile class="w-fit" v-model="form.faceUrl" :limit="1" :aspect-ratio="3 / 2" crop />
</el-form-item>
<!-- 帖子没有标签、没有关联栏目——只有官方账号才有推送配置 -->
<template v-if="userInfo.isOfficialAccount">
<el-form-item label="推送给">
<div class="w-full flex items-center gap-2">
<el-radio-group v-model="form.isPushAll">
<el-radio :value="BooleanFlag.YES">全员</el-radio>
<el-radio :value="BooleanFlag.NO">指定部门/人员</el-radio>
</el-radio-group>
</div>
<div v-show="form.isPushAll === BooleanFlag.NO" class="flex items-center gap-2">
<DeptUserPicker
v-model:dept-value="pushDeptIds"
v-model:user-value="pushUserIds"
selection-mode="both"
:initial-selected="pushInitialSelected"
@confirm="(items) => (pushSelection = items)"
v-slot="{ echoText }"
>
<el-input
readonly
class="w-80 cursor-pointer"
placeholder="请选择部门/人员"
:model-value="echoText"
/>
</DeptUserPicker>
</div>
</el-form-item>
<el-form-item label="推送类型" prop="pushType">
<el-radio-group v-model="form.pushType">
<el-radio :value="SendTypeEnum.IMMEDIATE">立即推送</el-radio>
<el-radio :value="SendTypeEnum.SCHEDULED">定时推送</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.pushType === SendTypeEnum.SCHEDULED" prop="pushTime">
<el-date-picker
v-model="form.pushTime"
type="datetime"
placeholder="选择推送时间"
format="YYYY-MM-DD HH:mm"
time-format="HH:mm"
:disabled-date="(time: Date) => time.getTime() < Date.now() - 1000 * 60 * 60 * 24"
:disabled-minutes="disabledMinutesOnlyTens"
:disabled-seconds="disabledSecondsOnlyZero"
value-format="X"
/>
</el-form-item>
<el-form-item label="推送摘要" prop="summary">
<el-input
v-model="form.summary"
type="textarea"
:maxlength="50"
show-word-limit
placeholder="简单描述推送的信息"
/>
</el-form-item>
</template>
<!-- 发布时间 + 官方账号的 IP 信息:所有类型都有,规则完全一样,每个类型自己维护一份 -->
<el-form-item label="发布时间" prop="sendType">
<el-radio-group v-model="form.sendType">
<el-radio :value="SendTypeEnum.IMMEDIATE">立即发布</el-radio>
<el-radio :value="SendTypeEnum.SCHEDULED">定时发布</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.sendType === SendTypeEnum.SCHEDULED" prop="sendTime">
<el-date-picker
v-model="form.sendTime"
type="datetime"
placeholder="选择发布时间"
format="YYYY-MM-DD HH:mm"
time-format="HH:mm"
:disabled-date="(time: Date) => time.getTime() < Date.now() - 1000 * 60 * 60 * 24"
:disabled-minutes="disabledMinutesOnlyTens"
:disabled-seconds="disabledSecondsOnlyZero"
value-format="X"
/>
</el-form-item>
<el-form-item v-if="userInfo.isOfficialAccount" label="IP信息" prop="regionHide">
<el-switch
v-model="form.regionHide"
:active-value="BooleanFlag.YES"
:inactive-value="BooleanFlag.NO"
active-text="隐藏地区"
inactive-text="显示地区"
/>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
/**
* 帖子(POST)专属表单——自己管自己的整个抽屉表单(字段、校验、提交数据、编辑回显),
* 不依赖外层拆出去的公共表单,改这个类型的规则只用改这一个文件。
*
* 帖子的特殊规则:
* - 封面图:官方账号 / 管理员必填;普通用户选填(专栏、专访不管身份都必填,问吧、实践用的是"图片"不是"封面图")
* - 不分类:没有主/副标签,也没有关联栏目
* - 只有官方账号才看得到"推送"配置(推送给谁 / 推送类型 / 推送摘要),非官方账号完全没有这些字段
* - 没有"插入视频"功能,这个只有专栏、专访的官方账号才有
* - 发布时间、官方账号的 IP 显示设置:所有类型都一样,这里也自己维护一份
*/
import type { FormInstance, FormItemRule } from 'element-plus'
import { BooleanFlag, SendTypeEnum } from '@/constants'
import UploadFile from '@/components/common/UploadFile/index.vue'
import DeptUserPicker from '@/components/common/DeptUserPicker/index.vue'
import type { DepartmentUserSelection } from '@/components/common/DepartmentUserModal/types'
import type { ArticleItemDto } from '@/api/article/types'
import { useResetData } from '@/hooks'
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const formRef = useTemplateRef<FormInstance>('formRef')
const [form, resetForm] = useResetData({
faceUrl: '',
isPushAll: BooleanFlag.YES,
pushType: SendTypeEnum.IMMEDIATE,
pushTime: '',
summary: '',
sendType: SendTypeEnum.IMMEDIATE,
sendTime: '',
regionHide: BooleanFlag.NO,
})
// “推送给”选中的部门/人员:*Ids 是给 DeptUserPicker 用的 id 数组,
// pushSelection 是 @confirm 拿到的完整详情(带名字),提交时直接用它拼 pushList。
const pushDeptIds = ref<string[]>([])
const pushUserIds = ref<string[]>([])
const pushSelection = ref<DepartmentUserSelection[]>([])
// 编辑回显用,给 DeptUserPicker 做首次回显的名字种子。
const pushInitialSelected = ref<DepartmentUserSelection[]>([])
// 时间选择只允许整十分(:00、:10...:50),秒固定为 0
const disabledSecondsOnlyZero = () => Array.from({ length: 59 }, (_, i) => i + 1)
const disabledMinutesOnlyTens = () =>
Array.from({ length: 60 }, (_, i) => i).filter((m) => m % 10 !== 0)
const faceUrlRules = computed<FormItemRule[]>(() =>
userInfo.value.isOfficialAccount || userInfo.value.isAdmin
? [{ required: true, message: '请上传封面图', trigger: 'blur' }]
: [],
)
const rules: Record<string, FormItemRule[]> = {
pushType: [{ required: true, message: '请选择推送类型', trigger: 'trigger' }],
pushTime: [{ required: true, message: '请选择推送时间', trigger: 'trigger' }],
sendType: [{ required: true, message: '请选择发布类型', trigger: 'blur' }],
sendTime: [
{
validator: (rule, value, callback) => {
if (form.value.sendType === SendTypeEnum.SCHEDULED && !value) {
callback(new Error('请选择发布时间'))
} else {
callback()
}
},
trigger: 'trigger',
},
],
regionHide: [{ required: true, message: '请选择是否隐藏IP信息', trigger: 'trigger' }],
}
const buildPushList = () =>
form.value.isPushAll === BooleanFlag.YES
? [{ valueId: '1', valueType: 1, valueName: '亚声威格' }]
: pushSelection.value.map((item) => ({
valueId: item.valueId,
valueType: item.valueType,
valueName: item.valueName,
}))
defineExpose({
validate: () => formRef.value!.validate(),
getSubmitData: () => ({
faceUrl: form.value.faceUrl,
sendType: form.value.sendType,
sendTime: form.value.sendTime,
regionHide: form.value.regionHide,
...(userInfo.value.isOfficialAccount
? {
isPushAll: form.value.isPushAll,
pushType: form.value.pushType,
pushTime: form.value.pushTime,
summary: form.value.summary,
pushList: buildPushList(),
}
: {}),
}),
setEchoData: (data: ArticleItemDto) => {
form.value.faceUrl = data.faceUrl
form.value.sendType = data.sendType
form.value.sendTime = data.sendTime
form.value.regionHide = data.regionHide
if (!userInfo.value.isOfficialAccount) return
form.value.summary = data.summary
if (data.pushList?.find((item) => item.valueId == '1')) {
form.value.isPushAll = BooleanFlag.YES
pushDeptIds.value = []
pushUserIds.value = []
pushSelection.value = []
pushInitialSelected.value = []
} else {
form.value.isPushAll = BooleanFlag.NO
const items: DepartmentUserSelection[] = (data.pushList || []).map((item) => ({
valueType: item.valueType as 1 | 2,
valueId: String(item.valueId),
valueName: item.valueName,
}))
pushDeptIds.value = items.filter((item) => item.valueType === 1).map((item) => item.valueId)
pushUserIds.value = items.filter((item) => item.valueType === 2).map((item) => item.valueId)
pushSelection.value = items
pushInitialSelected.value = items
}
},
reset: () => {
resetForm()
pushDeptIds.value = []
pushUserIds.value = []
pushSelection.value = []
pushInitialSelected.value = []
},
})
</script>
<template>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
<!-- 实践跟问吧一样,用"图片"(内容图)不是"封面图" -->
<el-form-item label="图片" prop="imgUrl">
<UploadFile v-model="form.imgUrl" :limit="1" class="w-full" />
</el-form-item>
<!-- 主/副标签:实践必填 -->
<el-form-item label="主标签" prop="mainTagId">
<SelectTags v-model="form.mainTagId" class="w-full" />
</el-form-item>
<el-form-item label="副标签 (最多3个)">
<SelectTags
v-model="form.tagList"
:filter-tags-fn="filterTagsFn"
:max-selected-tags="3"
class="w-full"
/>
</el-form-item>
<!-- 实践没有推送功能(不管是不是官方账号);官方账号可以选一个所属一级部门,选填 -->
<el-form-item v-if="userInfo.isOfficialAccount" label="所属一级部门">
<el-select v-model="form.deptId" placeholder="请选择一级部门" clearable>
<el-option
v-for="item in firstDepListOptions"
:key="item.id"
:value="item.id"
:label="item.name"
/>
</el-select>
</el-form-item>
<!-- 发布时间 + 官方账号的 IP 信息:所有类型都有,规则完全一样,每个类型自己维护一份 -->
<el-form-item label="发布时间" prop="sendType">
<el-radio-group v-model="form.sendType">
<el-radio :value="SendTypeEnum.IMMEDIATE">立即发布</el-radio>
<el-radio :value="SendTypeEnum.SCHEDULED">定时发布</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.sendType === SendTypeEnum.SCHEDULED" prop="sendTime">
<el-date-picker
v-model="form.sendTime"
type="datetime"
placeholder="选择发布时间"
format="YYYY-MM-DD HH:mm"
time-format="HH:mm"
:disabled-date="(time: Date) => time.getTime() < Date.now() - 1000 * 60 * 60 * 24"
:disabled-minutes="disabledMinutesOnlyTens"
:disabled-seconds="disabledSecondsOnlyZero"
value-format="X"
/>
</el-form-item>
<el-form-item v-if="userInfo.isOfficialAccount" label="IP信息" prop="regionHide">
<el-switch
v-model="form.regionHide"
:active-value="BooleanFlag.YES"
:inactive-value="BooleanFlag.NO"
active-text="隐藏地区"
inactive-text="显示地区"
/>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
/**
* 实践(PRACTICE)专属表单——自己管自己的整个抽屉表单,不依赖外层的公共表单。
*
* 实践的特殊规则:
* - 用"图片"(imgUrl)不是"封面图",提交时自动取第一张图当 faceUrl,跟问吧一样
* - 主/副标签必填
* - 完全没有推送功能,不管是不是官方账号——这是跟帖子/问吧/专栏/专访最大的区别
* - 官方账号可以选一个"所属一级部门",选填,不是必填项
* - 提交走的是单独的 addOrUpdatePractice 接口(不是 addOrUpdateArticle),这个在外层
* publishLongArticle/index.vue 里按 type 分支处理,这个组件只管收集字段
*/
import type { FormInstance, FormItemRule } from 'element-plus'
import { firstDepListOptions, BooleanFlag, SendTypeEnum } from '@/constants'
import UploadFile from '@/components/common/UploadFile/index.vue'
import SelectTags from '@/components/common/SelectTags/index.vue'
import type { ArticleItemDto } from '@/api/article/types'
import { useResetData } from '@/hooks'
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const formRef = useTemplateRef<FormInstance>('formRef')
const [form, resetForm] = useResetData({
imgUrl: '',
mainTagId: '',
tagList: [] as number[],
deptId: '',
sendType: SendTypeEnum.IMMEDIATE,
sendTime: '',
regionHide: BooleanFlag.NO,
})
const filterTagsFn = (allTags: any[]) => allTags.filter((tag) => tag.id !== Number(form.value.mainTagId))
// 时间选择只允许整十分(:00、:10...:50),秒固定为 0
const disabledSecondsOnlyZero = () => Array.from({ length: 59 }, (_, i) => i + 1)
const disabledMinutesOnlyTens = () =>
Array.from({ length: 60 }, (_, i) => i).filter((m) => m % 10 !== 0)
const rules: Record<string, FormItemRule[]> = {
imgUrl: [{ required: true, message: '请上传内容图片', trigger: 'blur' }],
mainTagId: [{ required: true, message: '请选择主标签', trigger: 'blur' }],
sendType: [{ required: true, message: '请选择发布类型', trigger: 'blur' }],
sendTime: [
{
validator: (rule, value, callback) => {
if (form.value.sendType === SendTypeEnum.SCHEDULED && !value) {
callback(new Error('请选择发布时间'))
} else {
callback()
}
},
trigger: 'trigger',
},
],
regionHide: [{ required: true, message: '请选择是否隐藏IP信息', trigger: 'trigger' }],
}
defineExpose({
validate: () => formRef.value!.validate(),
getSubmitData: () => ({
imgUrl: form.value.imgUrl,
faceUrl: form.value.imgUrl ? form.value.imgUrl.split(',')[0] || '' : '',
mainTagId: form.value.mainTagId,
tagList: [form.value.mainTagId, ...form.value.tagList].map((item, index) => ({
tagId: Number(item),
sort: index,
})),
sendType: form.value.sendType,
sendTime: form.value.sendTime,
regionHide: form.value.regionHide,
...(userInfo.value.isOfficialAccount ? { deptId: form.value.deptId || '' } : {}),
}),
setEchoData: (data: ArticleItemDto) => {
form.value.imgUrl = data.imgUrl
form.value.mainTagId = data.tagIdList[0] ? String(data.tagIdList[0]) : ''
form.value.tagList = data.tagIdList.slice(1) || []
form.value.sendType = data.sendType
form.value.sendTime = data.sendTime
form.value.regionHide = data.regionHide
if (userInfo.value.isOfficialAccount) {
form.value.deptId = data.deptId === '0' ? '' : data.deptId
}
},
reset: () => {
resetForm()
},
})
</script>
<template>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
<!-- 问吧用的是"图片"(内容图),不是"封面图"——提交时会自动把第一张图片当封面图传给后端 -->
<el-form-item label="图片" prop="imgUrl">
<UploadFile v-model="form.imgUrl" :limit="1" class="w-full" />
</el-form-item>
<!-- 主/副标签:问吧必填 -->
<el-form-item label="主标签" prop="mainTagId">
<SelectTags v-model="form.mainTagId" class="w-full" />
</el-form-item>
<el-form-item label="副标签 (最多3个)">
<SelectTags
v-model="form.tagList"
:filter-tags-fn="filterTagsFn"
:max-selected-tags="3"
class="w-full"
/>
</el-form-item>
<!-- 官方账号才有推送配置;跟帖子一样没有"插入视频"(那是专栏/专访独有的) -->
<template v-if="userInfo.isOfficialAccount">
<el-form-item label="推送给">
<div class="w-full flex items-center gap-2">
<el-radio-group v-model="form.isPushAll">
<el-radio :value="BooleanFlag.YES">全员</el-radio>
<el-radio :value="BooleanFlag.NO">指定部门/人员</el-radio>
</el-radio-group>
</div>
<div v-show="form.isPushAll === BooleanFlag.NO" class="flex items-center gap-2">
<DeptUserPicker
v-model:dept-value="pushDeptIds"
v-model:user-value="pushUserIds"
selection-mode="both"
:initial-selected="pushInitialSelected"
@confirm="(items) => (pushSelection = items)"
v-slot="{ echoText }"
>
<el-input
readonly
class="w-80 cursor-pointer"
placeholder="请选择部门/人员"
:model-value="echoText"
/>
</DeptUserPicker>
</div>
</el-form-item>
<el-form-item label="推送类型" prop="pushType">
<el-radio-group v-model="form.pushType">
<el-radio :value="SendTypeEnum.IMMEDIATE">立即推送</el-radio>
<el-radio :value="SendTypeEnum.SCHEDULED">定时推送</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.pushType === SendTypeEnum.SCHEDULED" prop="pushTime">
<el-date-picker
v-model="form.pushTime"
type="datetime"
placeholder="选择推送时间"
format="YYYY-MM-DD HH:mm"
time-format="HH:mm"
:disabled-date="(time: Date) => time.getTime() < Date.now() - 1000 * 60 * 60 * 24"
:disabled-minutes="disabledMinutesOnlyTens"
:disabled-seconds="disabledSecondsOnlyZero"
value-format="X"
/>
</el-form-item>
<el-form-item label="推送摘要" prop="summary">
<el-input
v-model="form.summary"
type="textarea"
:maxlength="50"
show-word-limit
placeholder="简单描述推送的信息"
/>
</el-form-item>
</template>
<!-- 发布时间 + 官方账号的 IP 信息:所有类型都有,规则完全一样,每个类型自己维护一份 -->
<el-form-item label="发布时间" prop="sendType">
<el-radio-group v-model="form.sendType">
<el-radio :value="SendTypeEnum.IMMEDIATE">立即发布</el-radio>
<el-radio :value="SendTypeEnum.SCHEDULED">定时发布</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.sendType === SendTypeEnum.SCHEDULED" prop="sendTime">
<el-date-picker
v-model="form.sendTime"
type="datetime"
placeholder="选择发布时间"
format="YYYY-MM-DD HH:mm"
time-format="HH:mm"
:disabled-date="(time: Date) => time.getTime() < Date.now() - 1000 * 60 * 60 * 24"
:disabled-minutes="disabledMinutesOnlyTens"
:disabled-seconds="disabledSecondsOnlyZero"
value-format="X"
/>
</el-form-item>
<el-form-item v-if="userInfo.isOfficialAccount" label="IP信息" prop="regionHide">
<el-switch
v-model="form.regionHide"
:active-value="BooleanFlag.YES"
:inactive-value="BooleanFlag.NO"
active-text="隐藏地区"
inactive-text="显示地区"
/>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
/**
* 问吧(QUESTION)专属表单——自己管自己的整个抽屉表单,不依赖外层的公共表单。
*
* 问吧的特殊规则:
* - 用"图片"(imgUrl,内容图)而不是"封面图",提交时自动取第一张图当 faceUrl 一起传给后端,
* 编辑回显时也是回显 imgUrl,不是 faceUrl(这点直接影响 setEchoData 怎么写)
* - 主/副标签必填,跟专栏/专访一样,但没有关联栏目
* - 官方账号才有推送配置,跟帖子的推送规则一模一样(没有插入视频)
*/
import type { FormInstance, FormItemRule } from 'element-plus'
import { BooleanFlag, SendTypeEnum } from '@/constants'
import UploadFile from '@/components/common/UploadFile/index.vue'
import SelectTags from '@/components/common/SelectTags/index.vue'
import DeptUserPicker from '@/components/common/DeptUserPicker/index.vue'
import type { DepartmentUserSelection } from '@/components/common/DepartmentUserModal/types'
import type { ArticleItemDto } from '@/api/article/types'
import { useResetData } from '@/hooks'
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const formRef = useTemplateRef<FormInstance>('formRef')
const [form, resetForm] = useResetData({
imgUrl: '',
mainTagId: '',
tagList: [] as number[],
isPushAll: BooleanFlag.YES,
pushType: SendTypeEnum.IMMEDIATE,
pushTime: '',
summary: '',
sendType: SendTypeEnum.IMMEDIATE,
sendTime: '',
regionHide: BooleanFlag.NO,
})
const pushDeptIds = ref<string[]>([])
const pushUserIds = ref<string[]>([])
const pushSelection = ref<DepartmentUserSelection[]>([])
const pushInitialSelected = ref<DepartmentUserSelection[]>([])
const disabledSecondsOnlyZero = () => Array.from({ length: 59 }, (_, i) => i + 1)
const disabledMinutesOnlyTens = () =>
Array.from({ length: 60 }, (_, i) => i).filter((m) => m % 10 !== 0)
const filterTagsFn = (allTags: any[]) => allTags.filter((tag) => tag.id !== Number(form.value.mainTagId))
const rules: Record<string, FormItemRule[]> = {
imgUrl: [{ required: true, message: '请上传内容图片', trigger: 'blur' }],
mainTagId: [{ required: true, message: '请选择主标签', trigger: 'blur' }],
pushType: [{ required: true, message: '请选择推送类型', trigger: 'trigger' }],
pushTime: [{ required: true, message: '请选择推送时间', trigger: 'trigger' }],
sendType: [{ required: true, message: '请选择发布类型', trigger: 'blur' }],
sendTime: [
{
validator: (rule, value, callback) => {
if (form.value.sendType === SendTypeEnum.SCHEDULED && !value) {
callback(new Error('请选择发布时间'))
} else {
callback()
}
},
trigger: 'trigger',
},
],
regionHide: [{ required: true, message: '请选择是否隐藏IP信息', trigger: 'trigger' }],
}
const buildPushList = () =>
form.value.isPushAll === BooleanFlag.YES
? [{ valueId: '1', valueType: 1, valueName: '亚声威格' }]
: pushSelection.value.map((item) => ({
valueId: item.valueId,
valueType: item.valueType,
valueName: item.valueName,
}))
defineExpose({
validate: () => formRef.value!.validate(),
getSubmitData: () => ({
imgUrl: form.value.imgUrl,
// 问吧没有单独的封面图字段,取内容图片第一张当封面图
faceUrl: form.value.imgUrl ? form.value.imgUrl.split(',')[0] || '' : '',
mainTagId: form.value.mainTagId,
tagList: [form.value.mainTagId, ...form.value.tagList].map((item, index) => ({
tagId: Number(item),
sort: index,
})),
sendType: form.value.sendType,
sendTime: form.value.sendTime,
regionHide: form.value.regionHide,
...(userInfo.value.isOfficialAccount
? {
isPushAll: form.value.isPushAll,
pushType: form.value.pushType,
pushTime: form.value.pushTime,
summary: form.value.summary,
pushList: buildPushList(),
}
: {}),
}),
setEchoData: (data: ArticleItemDto) => {
// 回显 imgUrl,不是 faceUrl——问吧详情接口返回的封面图是从 imgUrl 派生的,直接用 imgUrl 才对得上
form.value.imgUrl = data.imgUrl
form.value.mainTagId = data.tagIdList[0] ? String(data.tagIdList[0]) : ''
form.value.tagList = data.tagIdList.slice(1) || []
form.value.sendType = data.sendType
form.value.sendTime = data.sendTime
form.value.regionHide = data.regionHide
if (!userInfo.value.isOfficialAccount) return
form.value.summary = data.summary
if (data.pushList?.find((item) => item.valueId == '1')) {
form.value.isPushAll = BooleanFlag.YES
pushDeptIds.value = []
pushUserIds.value = []
pushSelection.value = []
pushInitialSelected.value = []
} else {
form.value.isPushAll = BooleanFlag.NO
const items: DepartmentUserSelection[] = (data.pushList || []).map((item) => ({
valueType: item.valueType as 1 | 2,
valueId: String(item.valueId),
valueName: item.valueName,
}))
pushDeptIds.value = items.filter((item) => item.valueType === 1).map((item) => item.valueId)
pushUserIds.value = items.filter((item) => item.valueType === 2).map((item) => item.valueId)
pushSelection.value = items
pushInitialSelected.value = items
}
},
reset: () => {
resetForm()
pushDeptIds.value = []
pushUserIds.value = []
pushSelection.value = []
pushInitialSelected.value = []
},
})
</script>
......@@ -5,9 +5,9 @@
<div class="bg-white rounded-lg shadow-lg border border-gray-100/50 overflow-hidden">
<div class="px-10 py-5">
<!-- 标题输入 -->
<el-form-item prop="title" class="mb-8">
<el-form-item class="mb-8">
<el-input
v-model="form.title"
v-model="sharedForm.title"
:placeholder="`在这里输入你的${text}标题...`"
class="title-input text-3xl font-bold"
show-word-limit
......@@ -17,13 +17,14 @@
/>
</el-form-item>
<!-- 富文本编辑器 -->
<!-- 富文本编辑器:只有实践是纯文本,其它类型都是富文本,这个差异跟"官方账号/推送"那些规则
没关系,就是内容编辑器本身的选择,所以没有拆到子组件里,直接在这判断 -->
<div>
<el-form-item prop="content">
<template v-if="form.type === ArticleTypeEnum.PRACTICE">
<el-form-item>
<template v-if="type === ArticleTypeEnum.PRACTICE">
<el-input
:placeholder="`请输入${text}内容`"
v-model="form.content"
v-model="sharedForm.content"
type="textarea"
:rows="30"
:maxlength="2000"
......@@ -32,7 +33,7 @@
</template>
<template v-else>
<!-- 回显回来可能会有bug 所以需要key 重新渲染一下-->
<WangEditor :key v-model="form.content" />
<WangEditor :key="editorKey" v-model="sharedForm.content" />
</template>
</el-form-item>
</div>
......@@ -64,298 +65,25 @@
size="500px"
:before-close="handleDrawerClose"
>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="px-4">
<!-- 基础设置 -->
<div>
<!-- 文章类型 -->
<el-form-item label="文章类型" prop="type">
<div class="px-4">
<!-- 文章类型:只读展示,来自路由参数,不需要表单校验 -->
<div class="mb-6">
<el-button
type="primary"
class="px-6 py-2 bg-blue-500 hover:bg-blue-600 rounded-lg text-white text-sm font-medium shadow-sm hover:shadow-md transition-all duration-200 min-w[120px]"
>
{{ text }}
</el-button>
</el-form-item>
<!-- 内容图片 实践和问吧需要上传内容图片 然后默认第一张是封面图 -->
<template
v-if="form.type === ArticleTypeEnum.PRACTICE || form.type === ArticleTypeEnum.QUESTION"
>
<el-form-item label="图片">
<UploadFile v-model="form.imgUrl" :limit="1" class="w-full" />
</el-form-item>
</template>
<!-- 帖子 专栏专访 是富文本 需要上传封面图 封面图 帖子的封面不必填-->
<template v-else>
<el-form-item
label="封面图"
prop="faceUrl"
:rules="
form.type === ArticleTypeEnum.POST &&
!userInfo.isOfficialAccount &&
!userInfo.isAdmin
? []
: [{ required: true, message: '请上传封面图', trigger: 'blur' }]
"
>
<UploadFile
class="w-fit"
v-model="form.faceUrl"
:limit="1"
:aspect-ratio="3 / 2"
crop
/>
</el-form-item>
</template>
<!-- 标签和主标签 除了帖子 都需要上传 -->
<template v-if="form.type !== ArticleTypeEnum.POST">
<el-form-item label="主标签" prop="mainTagId">
<SelectTags v-model="form.mainTagId" class="w-full" />
</el-form-item>
<el-form-item label="副标签 (最多3个)">
<SelectTags
v-model="form.tagList"
:filter-tags-fn="filterTagsFn"
:max-selected-tags="3"
class="w-full"
/>
</el-form-item>
</template>
</div>
<!-- 专栏配置 (条件显示) -->
<template
v-if="form.type === ArticleTypeEnum.COLUMN || form.type === ArticleTypeEnum.INTERVIEW"
>
<div>
<el-form-item label="所属栏目" prop="relateColumnId">
<el-select
v-model="form.relateColumnId"
:placeholder="`请选择${text}栏目`"
class="w-full"
>
<el-option
v-for="item in relateColumnList"
:key="item.id"
:value="item.id"
:label="item.title"
/>
</el-select>
</el-form-item>
<!-- <el-form-item label="主标签" prop="mainTagId">
<SelectTags v-model="form.mainTagId" class="w-full" />
</el-form-item>
<el-form-item label="副标签 (最多3个)">
<SelectTags
v-model="form.tagList"
:filter-tags-fn="filterTagsFn"
:max-selected-tags="3"
class="w-full"
/>
</el-form-item> -->
<el-form-item label="是否推荐" prop="isRecommend">
<!-- 改为radio单选框 -->
<el-radio-group v-model="form.isRecommend">
<el-radio :value="BooleanFlag.YES">是</el-radio>
<el-radio :value="BooleanFlag.NO">否</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
label="同步同事吧"
prop="isRelateColleague"
v-if="form.type === ArticleTypeEnum.COLUMN"
>
<el-radio-group v-model="form.isRelateColleague">
<el-radio :value="BooleanFlag.YES">是</el-radio>
<el-radio :value="BooleanFlag.NO">否</el-radio>
</el-radio-group>
</el-form-item>
<!-- 如果是官方账号的话还有推送相关的配置 以及能上传视频-->
<template v-if="userInfo.isOfficialAccount">
<el-form-item label="推送给">
<div class="w-full flex items-center gap-2">
<el-radio-group v-model="form.isPushAll">
<el-radio :value="BooleanFlag.YES">全员</el-radio>
<el-radio :value="BooleanFlag.NO">指定部门/人员</el-radio>
</el-radio-group>
</div>
<div v-show="form.isPushAll === BooleanFlag.NO" class="flex items-center gap-2">
<DeptUserPicker
v-model:dept-value="pushDeptIds"
v-model:user-value="pushUserIds"
selection-mode="both"
:initial-selected="pushInitialSelected"
@confirm="(items) => (pushSelection = items)"
v-slot="{ echoText }"
>
<el-input
readonly
class="w-80 cursor-pointer"
placeholder="请选择部门/人员"
:model-value="echoText"
/>
</DeptUserPicker>
<!-- 各文章类型自己的表单:字段、校验规则、发布时间/IP信息、提交/回显逻辑都在各自的组件里维护,
这里不再拆一个"公共表单"出来,每个组件就是一个完整、独立的抽屉表单 -->
<PostForm v-if="type === ArticleTypeEnum.POST" ref="subFormRef" />
<ColumnForm v-else-if="type === ArticleTypeEnum.COLUMN" ref="subFormRef" />
<InterviewForm v-else-if="type === ArticleTypeEnum.INTERVIEW" ref="subFormRef" />
<QuestionForm v-else-if="type === ArticleTypeEnum.QUESTION" ref="subFormRef" />
<PracticeForm v-else-if="type === ArticleTypeEnum.PRACTICE" ref="subFormRef" />
</div>
</el-form-item>
<el-form-item label="推送类型" prop="pushType">
<el-radio-group v-model="form.pushType">
<el-radio :value="SendTypeEnum.IMMEDIATE">立即推送</el-radio>
<el-radio :value="SendTypeEnum.SCHEDULED">定时推送</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.pushType === SendTypeEnum.SCHEDULED" prop="pushTime">
<el-date-picker
v-model="form.pushTime"
type="datetime"
placeholder="选择推送时间"
format="YYYY-MM-DD HH:mm"
time-format="HH:mm"
:disabled-date="(time: Date) => time.getTime() < Date.now() - 1000 * 60 * 60 * 24"
:disabled-minutes="disabledMinutesOnlyTens"
:disabled-seconds="disabledSecondsOnlyZero"
value-format="X"
/>
</el-form-item>
<el-form-item label="推送摘要" prop="summary">
<el-input
v-model="form.summary"
type="textarea"
:maxlength="50"
show-word-limit
placeholder="简单描述推送的信息"
/>
</el-form-item>
<!-- 插入视频 顶部或者底部 radio -->
<el-form-item label="插入视频" prop="videoLocation">
<el-radio-group v-model="form.videoLocation">
<el-radio :value="VideoPositionEnum.TOP">顶部</el-radio>
<el-radio :value="VideoPositionEnum.BOTTOM">底部</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="视频" prop="articleVideoUrl">
<UploadVideo v-model="form.articleVideoUrl" btnClass="flex-col" />
</el-form-item>
</template>
</div>
</template>
<!-- 如果是官方账号的话还有推送相关的配置 帖子 和问吧也要加推送-->
<template
v-if="
userInfo.isOfficialAccount &&
(form.type === ArticleTypeEnum.POST || form.type === ArticleTypeEnum.QUESTION)
"
>
<el-form-item label="推送给">
<div class="w-full flex items-center gap-2">
<el-radio-group v-model="form.isPushAll">
<el-radio :value="BooleanFlag.YES">全员</el-radio>
<el-radio :value="BooleanFlag.NO">指定部门/人员</el-radio>
</el-radio-group>
</div>
<div v-show="form.isPushAll === BooleanFlag.NO" class="flex items-center gap-2">
<DeptUserPicker
v-model:dept-value="pushDeptIds"
v-model:user-value="pushUserIds"
selection-mode="both"
:initial-selected="pushInitialSelected"
@confirm="(items) => (pushSelection = items)"
v-slot="{ echoText }"
>
<el-input
readonly
class="w-80 cursor-pointer"
placeholder="请选择部门/人员"
:model-value="echoText"
/>
</DeptUserPicker>
</div>
</el-form-item>
<el-form-item label="推送类型" prop="pushType">
<el-radio-group v-model="form.pushType">
<el-radio :value="SendTypeEnum.IMMEDIATE">立即推送</el-radio>
<el-radio :value="SendTypeEnum.SCHEDULED">定时推送</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.pushType === SendTypeEnum.SCHEDULED" prop="pushTime">
<el-date-picker
v-model="form.pushTime"
type="datetime"
placeholder="选择推送时间"
format="YYYY-MM-DD HH:mm"
time-format="HH:mm"
:disabled-date="(time: Date) => time.getTime() < Date.now() - 1000 * 60 * 60 * 24"
:disabled-minutes="disabledMinutesOnlyTens"
:disabled-seconds="disabledSecondsOnlyZero"
value-format="X"
/>
</el-form-item>
<el-form-item label="推送摘要" prop="summary">
<el-input
v-model="form.summary"
type="textarea"
:maxlength="50"
show-word-limit
placeholder="简单描述推送的信息"
/>
</el-form-item>
</template>
<!-- 选一级部门 如果是官方账号 并且是 实践 选填 -->
<template v-if="userInfo.isOfficialAccount && form.type === ArticleTypeEnum.PRACTICE">
<el-form-item label="所属一级部门">
<el-select v-model="form.deptId" :placeholder="`请选择一级部门`" clearable>
<el-option
v-for="item in firstDepListOptions"
:key="item.id"
:value="item.id"
:label="item.name"
/>
</el-select>
</el-form-item>
</template>
<!-- 发布设置 -->
<div>
<el-form-item label="发布时间" prop="sendType">
<el-radio-group v-model="form.sendType">
<el-radio :value="SendTypeEnum.IMMEDIATE">立即发布</el-radio>
<el-radio :value="SendTypeEnum.SCHEDULED">定时发布</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.sendType === SendTypeEnum.SCHEDULED" prop="sendTime">
<el-date-picker
v-model="form.sendTime"
type="datetime"
placeholder="选择发布时间"
format="YYYY-MM-DD HH:mm"
time-format="HH:mm"
:disabled-date="(time: Date) => time.getTime() < Date.now() - 1000 * 60 * 60 * 24"
:disabled-minutes="disabledMinutesOnlyTens"
:disabled-seconds="disabledSecondsOnlyZero"
value-format="X"
/>
</el-form-item>
</div>
<!-- 针对官方账号新增是否显示ip -->
<template v-if="userInfo.isOfficialAccount">
<el-form-item label="IP信息" prop="regionHide">
<el-switch
v-model="form.regionHide"
:active-value="BooleanFlag.YES"
:inactive-value="BooleanFlag.NO"
active-text="隐藏地区"
inactive-text="显示地区"
/>
</el-form-item>
</template>
</el-form>
<!-- 抽屉底部按钮 -->
<template #footer>
......@@ -383,37 +111,33 @@
</template>
<script setup lang="ts">
import type { FormInstance } from 'element-plus'
/**
* 发文入口页(专栏/专访/帖子/问吧/实践共用这一个路由 publishLongArticle/:type,视频是单独的 publishVideo 页面)。
*
* 这里只负责标题、内容编辑器、抽屉/按钮壳子、以及提交/编辑回显的整体编排——这几样是真正
* "所有类型完全一样、拆出来也没有独立意义"的部分。除此之外,每个类型的抽屉表单(字段、
* 校验规则、发布时间/IP信息、提交数据、编辑回显)都完整放在 components/ 下对应的
* XxxForm.vue 里自己维护,不再抽一个"公共表单"出来跟子组件拆着管——即使发布时间/IP信息
* 这种规则一样的字段,也是每个类型自己各留一份,改某个类型的东西只用改那一个文件。
*
* 提交流程:校验子组件自己的表单,通过了以后把标题/内容/id 这些公共字段和子组件返回的
* 数据合并起来,按 type 决定调 addOrUpdateArticle 还是 addOrUpdatePractice(实践走的是
* 单独的接口,跟其它类型不一样)。
*/
import WangEditor from '@/components/common/WangEditor/index.vue'
import {
ArticleTypeEnum,
SendTypeEnum,
BooleanFlag,
ReleaseStatusTypeEnum,
articleTypeListOptions,
VideoPositionEnum,
firstDepListOptions,
} from '@/constants'
import UploadFile from '@/components/common/UploadFile/index.vue'
import UploadVideo from '@/components/common/UploadVideo/index.vue'
import { ArticleTypeEnum, ReleaseStatusTypeEnum, articleTypeListOptions } from '@/constants'
import { useResetData } from '@/hooks'
import { useColumnStore } from '@/stores/column'
import { useInterviewStore } from '@/stores/interview'
import { storeToRefs } from 'pinia'
import { addOrUpdateArticle, addOrUpdatePractice, getArticleDetail } from '@/api'
import { useRouter, useRoute } from 'vue-router'
import { useUserStore } from '@/stores/user'
import DeptUserPicker from '@/components/common/DeptUserPicker/index.vue'
import type { DepartmentUserSelection } from '@/components/common/DepartmentUserModal/types'
import type { FormItemRule } from 'element-plus'
import { push } from 'notivue'
import BackButton from '@/components/common/BackButton/index.vue'
const columnStore = useColumnStore()
const { columnList } = storeToRefs(columnStore)
const interviewStore = useInterviewStore()
const { interviewList } = storeToRefs(interviewStore)
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
import type { ArticleTypeFormExpose } from './types'
import PostForm from './components/PostForm.vue'
import ColumnForm from './components/ColumnForm.vue'
import InterviewForm from './components/InterviewForm.vue'
import QuestionForm from './components/QuestionForm.vue'
import PracticeForm from './components/PracticeForm.vue'
const loading = ref(false)
const router = useRouter()
......@@ -422,137 +146,21 @@ const type = route.params.type as ArticleTypeEnum
const text = articleTypeListOptions.find((item) => item.value === type)?.label
const canPublish = computed(() => {
return form.value.title && form.value.content
return sharedForm.value.title && sharedForm.value.content
})
const relateColumnList = computed(() =>
type === ArticleTypeEnum.COLUMN ? columnList.value : interviewList.value,
)
// 抽屉控制
const drawerVisible = ref(false)
const formRef = useTemplateRef<FormInstance>('formRef')
const [form, resetForm] = useResetData({
// 公共字段:只有标题、内容——这两个是主编辑区自己的,不属于抽屉表单,其它字段都在各类型组件自己的表单里
const [sharedForm, resetSharedForm] = useResetData({
id: undefined as number | undefined,
type,
title: '',
content: '',
faceUrl: '',
imgUrl: '',
relateColumnId: '' as string | number,
mainTagId: '',
tagList: [] as number[],
isRelateColleague: BooleanFlag.NO,
isRecommend: BooleanFlag.NO,
sendType: SendTypeEnum.IMMEDIATE,
sendTime: '',
releaseStatus: ReleaseStatusTypeEnum.PUBLISH,
regionHide: BooleanFlag.NO,
isPushAll: BooleanFlag.YES,
// 推送设置
pushType: SendTypeEnum.IMMEDIATE,
pushTime: '',
pushList: [],
summary: '',
// 视频
videoLocation: VideoPositionEnum.TOP,
articleVideoUrl: '',
deptId: '',
})
// 时间选择只允许整十分(:00、:10、:20、:30、:40、:50),秒固定为 0
const disabledSecondsOnlyZero = () => Array.from({ length: 59 }, (_, i) => i + 1)
const disabledMinutesOnlyTens = () =>
Array.from({ length: 60 }, (_, i) => i).filter((m) => m % 10 !== 0)
const rules: Record<string, FormItemRule[]> = {
title: [{ required: true, message: '请输入文章标题', trigger: 'blur' }],
content: [{ required: true, message: '请输入文章内容', trigger: 'blur' }],
type: [{ required: true, message: '请选择文章类型', trigger: 'blur' }],
// faceUrl: [{ required: true, message: '请上传封面图', trigger: 'blur' }],
imgUrl: [{ required: true, message: '请上传内容图片', trigger: 'blur' }],
sendType: [{ required: true, message: '请选择发布类型', trigger: 'blur' }],
sendTime: [
{ required: true, message: '请选择发布时间', trigger: 'trigger' },
{
validator: (rule, value, callback) => {
if (form.value.sendType === SendTypeEnum.SCHEDULED && !value) {
callback(new Error('请选择发布时间'))
} else {
callback()
}
},
trigger: 'trigger',
},
],
releaseStatus: [{ required: true, message: '请选择发布状态', trigger: 'blur' }],
mainTagId: [{ required: true, message: '请选择主标签', trigger: 'blur' }],
isRecommend: [{ required: true, message: '是否推荐', trigger: 'trigger' }],
isRelateColleague: [{ required: true, message: '是否同步同事吧', trigger: 'trigger' }],
relateColumnId: [{ required: true, message: '请选择对应的栏目', trigger: 'trigger' }],
regionHide: [{ required: true, message: '请选择是否隐藏IP信息', trigger: 'trigger' }],
// 推送设置
pushType: [{ required: true, message: '请选择推送类型', trigger: 'trigger' }],
pushTime: [{ required: true, message: '请选择推送时间', trigger: 'trigger' }],
pushList: [{ required: true, message: '请选择推送对象', trigger: 'trigger' }],
}
// “推送给”选中的部门/人员:*Ids 是给 DeptUserPicker 用的 id 数组,
// pushSelection 是 @confirm 拿到的完整详情(带名字),提交时直接用它拼 pushList。
const pushDeptIds = ref<string[]>([])
const pushUserIds = ref<string[]>([])
const pushSelection = ref<DepartmentUserSelection[]>([])
// 编辑回显用,给 DeptUserPicker 做首次回显的名字种子(不传的话首次打开只能看到 id)。
const pushInitialSelected = ref<DepartmentUserSelection[]>([])
const filterTagsFn = (allTags: any[]) => {
return allTags.filter((tag) => tag.id !== Number(form.value.mainTagId))
}
const transFormData = (releaseStatus: ReleaseStatusTypeEnum) => {
const { tagList, ...data } = form.value as any
data.tagList = [data.mainTagId, ...tagList].map((item, index) => ({
tagId: Number(item),
sort: index,
}))
data.releaseStatus = releaseStatus
if (data.type === ArticleTypeEnum.PRACTICE || data.type === ArticleTypeEnum.QUESTION) {
// 手动设置一下封面图
if (data.imgUrl) {
data.faceUrl = data.imgUrl.split(',')[0] || ''
}
}
if (
(data.type === ArticleTypeEnum.COLUMN ||
data.type === ArticleTypeEnum.INTERVIEW ||
data.type === ArticleTypeEnum.POST ||
data.type === ArticleTypeEnum.QUESTION) &&
userInfo.value.isOfficialAccount
) {
if (data.isPushAll === BooleanFlag.YES) {
data.pushList = [
{
valueId: '1',
valueType: 1,
valueName: '亚声威格',
},
]
} else {
data.pushList = pushSelection.value.map((item) => ({
valueId: item.valueId,
valueType: item.valueType,
valueName: item.valueName,
}))
}
}
if (data.type === ArticleTypeEnum.PRACTICE && userInfo.value.isOfficialAccount) {
data.deptId = data.deptId || ''
}
return data
}
// 每个类型自己的表单组件,字段/校验/提交数据都在对应组件里,这里只认 ArticleTypeFormExpose 这个接口
const subFormRef = useTemplateRef<ArticleTypeFormExpose>('subFormRef')
// 打开抽屉
const openDrawer = () => {
......@@ -566,26 +174,37 @@ const handleDrawerClose = () => {
// 取消按钮
const handleClosed = () => {
resetForm()
resetSharedForm()
subFormRef.value?.reset()
router.back()
}
// 提交表单
const handleSubmit = async (releaseStatus: ReleaseStatusTypeEnum) => {
try {
const validateRes = await formRef.value?.validate()
console.log(validateRes, 'validateRes')
await subFormRef.value?.validate()
loading.value = true
if (form.value.type === ArticleTypeEnum.PRACTICE) {
await addOrUpdatePractice(transFormData(releaseStatus))
const data = {
id: sharedForm.value.id,
type,
title: sharedForm.value.title,
content: sharedForm.value.content,
releaseStatus,
...subFormRef.value?.getSubmitData(),
}
if (type === ArticleTypeEnum.PRACTICE) {
await addOrUpdatePractice(data as any)
} else {
await addOrUpdateArticle(transFormData(releaseStatus))
await addOrUpdateArticle(data as any)
}
drawerVisible.value = false
push.success(releaseStatus === ReleaseStatusTypeEnum.PUBLISH ? '发布成功' : '存草稿成功')
resetForm()
resetSharedForm()
subFormRef.value?.reset()
router.back()
// 发布成功后的逻辑...
} catch (error) {
console.log(error)
} finally {
......@@ -593,98 +212,27 @@ const handleSubmit = async (releaseStatus: ReleaseStatusTypeEnum) => {
}
}
const key = ref(0)
// WangEditor 回显时偶尔不刷新,靠 key 强制重新渲染一下
const editorKey = ref(0)
const isEdit = computed(() => !!route.query.id)
onActivated(async () => {
key.value++
resetForm()
pushDeptIds.value = []
pushUserIds.value = []
pushSelection.value = []
pushInitialSelected.value = []
editorKey.value++
resetSharedForm()
subFormRef.value?.reset()
await nextTick()
if (isEdit.value) {
// 要编辑回显
const { data } = await getArticleDetail(route.query.id as string)
// 1首先回显基础的信息
// 标题 内容 主标签 副标签 所属栏目 是否推荐 是否同步同事吧 发布时间 发布状态
const {
id,
title,
content,
relateColumnId,
isRecommend,
isRelateColleague,
sendType,
sendTime,
tagIdList,
regionHide,
} = data
form.value = {
...form.value,
title,
content,
relateColumnId,
isRecommend,
isRelateColleague,
sendType,
sendTime,
id,
regionHide,
}
// 2回显主副标签
form.value.mainTagId = tagIdList[0] ? String(tagIdList[0]) : ''
form.value.tagList = tagIdList.slice(1) || []
// 3回显封面图内容图片 以及顶部 底部 视频等
const { imgUrl, faceUrl, articleVideoUrl } = data
if (type === ArticleTypeEnum.QUESTION || type === ArticleTypeEnum.PRACTICE) {
form.value.imgUrl = imgUrl
} else {
form.value.faceUrl = faceUrl
}
if (articleVideoUrl) {
form.value.articleVideoUrl = articleVideoUrl
}
// 4回显推送人员设置
form.value.summary = data.summary
if (
(type === ArticleTypeEnum.COLUMN ||
type === ArticleTypeEnum.INTERVIEW ||
type === ArticleTypeEnum.POST ||
type === ArticleTypeEnum.QUESTION) &&
userInfo.value.isOfficialAccount
) {
if (data.pushList?.find((item) => item.valueId == '1')) {
form.value.isPushAll = BooleanFlag.YES
pushDeptIds.value = []
pushUserIds.value = []
pushSelection.value = []
pushInitialSelected.value = []
} else {
form.value.isPushAll = BooleanFlag.NO
const items: DepartmentUserSelection[] = (data.pushList || []).map((item) => ({
valueType: item.valueType as 1 | 2,
valueId: String(item.valueId),
valueName: item.valueName,
}))
pushDeptIds.value = items.filter((item) => item.valueType === 1).map((item) => item.valueId)
pushUserIds.value = items.filter((item) => item.valueType === 2).map((item) => item.valueId)
pushSelection.value = items
pushInitialSelected.value = items
}
}
if (!isEdit.value) return
if (userInfo.value.isOfficialAccount && form.value.type === ArticleTypeEnum.PRACTICE) {
form.value.deptId = data.deptId === '0' ? '' : data.deptId
}
// 编辑回显:标题/内容自己填,剩下的(包括发布时间/IP信息)都交给子组件的 setEchoData
const { data } = await getArticleDetail(route.query.id as string)
sharedForm.value = {
...sharedForm.value,
id: data.id,
title: data.title,
content: data.content,
}
subFormRef.value?.setEchoData(data)
})
</script>
......
import type { ArticleItemDto } from '@/api/article/types'
/**
* 每个文章类型子表单组件(PostForm/ColumnForm/InterviewForm/QuestionForm/PracticeForm)
* 都要通过 defineExpose 暴露这一套方法,外层 publishLongArticle/index.vue 只认这个接口,
* 不关心某个类型内部具体有哪些字段——加/改某个类型的专属字段,只改对应的子组件就行。
*/
export interface ArticleTypeFormExpose {
/** 校验自己负责的字段,跟 el-form.validate() 一样,校验不通过会 reject */
validate: () => Promise<unknown>
/** 提交时调用,返回自己负责的字段,外层会跟公共字段(标题/内容/发布时间/IP信息等)合并后一起提交 */
getSubmitData: () => Record<string, unknown>
/** 编辑回显时调用,传入后端返回的文章详情,自己挑需要的字段回显 */
setEchoData: (data: ArticleItemDto) => void
/** 重置自己负责的字段(取消、或者 keep-alive 场景下再次打开同一类型的发布页时调用) */
reset: () => void
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment