Commit 883f32cb by lijiabin

【需求 17679】 feat: 完成专栏专访视频 官方账号发布时可以选择推送功能

parent 19d84a7f
......@@ -61,7 +61,7 @@ export interface AddOrUpdateColumnForm extends AddOrUpdateColumnBase {
/**
* 添加专栏的DTO
*/
export interface AddOrUpdateColumnDto extends AddOrUpdateColumnBase {
export interface AddOrUpdateColumnDto extends AddOrUpdateColumnBase, PushSettingBase {
tagList: { tagId: number; sort: number }[]
}
......@@ -85,10 +85,16 @@ export interface AddOrUpdateInterviewForm extends AddOrUpdateInterviewBase {
tagList: number[]
}
interface PushSettingBase {
pushType: SendTypeEnum
pushTime: string
pushList: { valueId: string; valueName: string | number }[]
}
// 推送设置相关的字段
/**
* 添加专访的DTO
*/
export interface AddOrUpdateInterviewDto extends AddOrUpdateInterviewBase {
export interface AddOrUpdateInterviewDto extends AddOrUpdateInterviewBase, PushSettingBase {
tagList: { tagId: number; sort: number }[]
}
......@@ -128,6 +134,7 @@ export interface ArticleItemDto {
createUserId: number
createTime: number
viewCount: number
playCount: number
isRecommend: BooleanFlag
type: ArticleTypeEnum
isRelateColleague: BooleanFlag
......@@ -222,6 +229,7 @@ export interface ColumnItemDto {
videoDuration: string
showName: string
showAvatar: string
playCount: number
}[]
}
......
......@@ -52,12 +52,12 @@ interface IResult {
userList: ISelectUser[]
departmentList: ISelectDept[]
}
interface ISelectUser {
export interface ISelectUser {
id: string
name: string
avatar: string
}
interface ISelectDept {
export interface ISelectDept {
id: string
name: string
}
......
......@@ -53,7 +53,11 @@
<div class="hidden sm:block w-1 h-1 bg-gray-300 rounded-full"></div>
<div class="flex items-center gap-1 hover:text-blue-500 transition-colors">
<el-icon class="text-sm"><View /></el-icon>
<span class="font-medium">{{ item.viewCount }}</span>
<span class="font-medium">{{
item.type === ArticleTypeEnum.VIDEO
? Math.max(item.playCount, item.viewCount)
: item.viewCount
}}</span>
</div>
<div class="flex items-center gap-1 hover:text-red-500 transition-colors">
<el-icon class="text-sm"><ChatDotRound /></el-icon>
......@@ -129,7 +133,7 @@
<script setup lang="ts">
import { usePageSearch } from '@/hooks'
import { getArticleList } from '@/api'
import { TABS_REF_KEY } from '@/constants'
import { TABS_REF_KEY, ArticleTypeEnum } from '@/constants'
import { useScrollTop } from '@/hooks'
import dayjs from 'dayjs'
import { jumpToArticleDetailPage } from '@/utils'
......
......@@ -68,7 +68,7 @@
<el-icon class="text-sm">
<View />
</el-icon>
<span>{{ list[0]?.viewCount }}</span>
<span>{{ Math.max(list[0]?.playCount, list[0]?.viewCount) }}</span>
</div>
<div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-3 py-1.5 rounded-lg"
......@@ -162,7 +162,7 @@
<el-icon class="text-sm">
<View />
</el-icon>
<span>{{ item?.viewCount }}</span>
<span>{{ Math.max(item?.playCount, item?.viewCount) }}</span>
</div>
<div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
......@@ -259,7 +259,7 @@
<el-icon class="text-sm">
<View />
</el-icon>
<span>{{ item.viewCount }}</span>
<span>{{ Math.max(item.playCount, item.viewCount) }}</span>
</div>
<div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
......@@ -355,7 +355,7 @@
<el-icon class="text-sm">
<View />
</el-icon>
<span>{{ item.viewCount }}</span>
<span>{{ Math.max(item.playCount, item.viewCount) }}</span>
</div>
<div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
......
......@@ -68,7 +68,7 @@
<div class="absolute bottom-3 left-3 flex gap-3 text-white text-xs">
<div class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg">
<el-icon class="text-sm"><View /></el-icon>
<span>{{ item.viewCount }}</span>
<span>{{ Math.max(item.playCount, item.viewCount) }}</span>
</div>
<div class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg">
<el-icon class="text-sm"><ChatDotRound /></el-icon>
......@@ -172,7 +172,7 @@
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
>
<el-icon class="text-sm"><View /></el-icon>
<span>{{ i.viewCount }}</span>
<span>{{ Math.max(i.playCount, i.viewCount) }}</span>
</div>
<div
class="flex items-center gap-1 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-lg"
......
......@@ -70,7 +70,7 @@
>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="px-4">
<!-- 基础设置 -->
<div class="mb-6">
<div>
<!-- 文章类型 -->
<el-form-item label="文章类型" prop="type">
<el-button
......@@ -118,7 +118,7 @@
<template
v-if="form.type === ArticleTypeEnum.COLUMN || form.type === ArticleTypeEnum.INTERVIEW"
>
<div class="mb-6">
<div>
<el-form-item label="所属栏目" prop="relateColumnId">
<el-select
v-model="form.relateColumnId"
......@@ -165,11 +165,35 @@
<el-radio :value="BooleanFlag.NO"></el-radio>
</el-radio-group>
</el-form-item>
<!-- 如果是官方账号的话还有推送相关的配置 -->
<template v-if="userInfo.isOfficialAccount">
<el-form-item label="推送给">
<el-button class="button-new-tag" size="small" @click="handleSelectUserAndDept">
+ 添加部门
</el-button>
<span class="ml-2 text-sm text-gray-600"> {{ selectedText }}已选择下属部门</span>
</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="选择推送时间"
:disabled-date="(time: Date) => time.getTime() < Date.now() - 1000 * 60 * 60 * 24"
value-format="X"
/>
</el-form-item>
</template>
</div>
</template>
<!-- 发布设置 -->
<div class="mb-6">
<div>
<el-form-item label="发布时间" prop="sendType">
<el-radio-group v-model="form.sendType">
<el-radio :value="SendTypeEnum.IMMEDIATE">立即发布</el-radio>
......@@ -226,11 +250,15 @@ 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 { selectDepOrUser, type ISelectDept, type ISelectUser } from '@/utils'
const columnStore = useColumnStore()
const { columnList } = storeToRefs(columnStore)
const interviewStore = useInterviewStore()
const { interviewList } = storeToRefs(interviewStore)
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const loading = ref(false)
const router = useRouter()
......@@ -264,6 +292,11 @@ const [form, resetForm] = useResetData({
sendType: SendTypeEnum.IMMEDIATE,
sendTime: '',
releaseStatus: ReleaseStatusTypeEnum.PUBLISH,
// 推送设置
pushType: SendTypeEnum.IMMEDIATE,
pushTime: '',
pushList: [],
})
const rules = {
......@@ -279,6 +312,44 @@ const rules = {
isRecommend: [{ required: true, message: '是否推荐', trigger: 'trigger' }],
isRelateColleague: [{ required: true, message: '是否同步同事吧', trigger: 'trigger' }],
relateColumnId: [{ required: true, message: '请选择对应的栏目', trigger: 'trigger' }],
// 推送设置
pushType: [{ required: true, message: '请选择推送类型', trigger: 'trigger' }],
pushTime: [{ required: true, message: '请选择推送时间', trigger: 'trigger' }],
pushList: [{ required: true, message: '请选择推送对象', trigger: 'trigger' }],
}
// 选中的部门
const selectedDepts = ref<{ id: string; name: string }[]>([])
// 选中的成员
const selectedUsers = ref<{ id: string; name: string }[]>([])
const selectedText = computed(() => {
let text = ''
if (selectedDepts.value.length > 0) {
text += `已选择${selectedDepts.value.length}个部门`
}
if (selectedUsers.value.length > 0) {
text += `已选择${selectedUsers.value.length}个成员`
}
return text
})
const handleSelectUserAndDept = async () => {
const { departmentList, userList } = await selectDepOrUser({
mode: 'multi',
type: ['user', 'department'],
selectedDepartmentIds: selectedDepts.value.map((item) => item.id),
selectedUserIds: selectedUsers.value.map((item) => item.id),
})
console.log(departmentList, userList)
selectedDepts.value = departmentList.map((item) => ({
id: item.id,
name: item.name,
}))
selectedUsers.value = userList.map((item) => ({
id: item.id,
name: item.name,
}))
}
const filterTagsFn = (allTags: any[]) => {
......@@ -299,6 +370,25 @@ const transFormData = (releaseStatus: ReleaseStatusTypeEnum) => {
data.faceUrl = data.imgUrl.split(',')[0]
}
}
if (
(data.type === ArticleTypeEnum.COLUMN || data.type === ArticleTypeEnum.INTERVIEW) &&
userInfo.value.isOfficialAccount
) {
data.pushList = [
...selectedDepts.value.map((item) => ({
valueId: item.id,
valueType: 1,
valueName: item.name,
})),
...selectedUsers.value.map((item) => ({
valueId: item.id,
valueType: 2,
valueName: item.name,
})),
]
}
return data
}
......@@ -389,7 +479,24 @@ onActivated(async () => {
} else {
form.value.faceUrl = faceUrl
}
console.log(form.value, 'form.value')
// 回显推送人员设置
if (
(type === ArticleTypeEnum.COLUMN || type === ArticleTypeEnum.INTERVIEW) &&
userInfo.value.isOfficialAccount
) {
selectedDepts.value = data.pushList
.filter((item) => item.valueType === 1)
.map((item) => ({
id: item.valueId,
name: item.valueName,
}))
selectedUsers.value = data.pushList
.filter((item) => item.valueType === 2)
.map((item) => ({
id: item.valueId,
name: item.valueName,
}))
}
}
})
</script>
......
......@@ -128,6 +128,55 @@
</el-form-item>
</div>
<!-- 新增推送设置 -->
<template v-if="userInfo.isOfficialAccount">
<div class="mb-8">
<el-form-item>
<label class="block text-sm font-semibold text-gray-700 mb-3"> 推送对象 </label>
<div class="w-full">
<el-button
class="button-new-tag"
size="small"
@click="handleSelectUserAndDept"
>
+ 添加部门
</el-button>
<span class="ml-2 text-sm text-gray-600">
{{ selectedText }}已选择下属部门</span
>
</div>
</el-form-item>
</div>
<div class="mb-8">
<el-form-item prop="pushType">
<label class="block text-sm font-semibold text-gray-700 mb-3"> 推送类型 </label>
<div class="w-full">
<el-radio-group v-model="form.pushType">
<el-radio :value="SendTypeEnum.IMMEDIATE">立即推送</el-radio>
<el-radio :value="SendTypeEnum.SCHEDULED">定时推送</el-radio>
</el-radio-group>
</div>
</el-form-item>
</div>
<div class="mb-8">
<el-form-item v-if="form.pushType === SendTypeEnum.SCHEDULED" prop="pushTime">
<label class="block text-sm font-semibold text-gray-700 mb-3"> 推送时间 </label>
<div class="w-full">
<el-date-picker
v-model="form.pushTime"
type="datetime"
placeholder="请选择推送时间"
value-format="X"
:disabled-date="
(time: Date) => {
return time.getTime() < Date.now() - 1000 * 60 * 60 * 24
}
"
/>
</div>
</el-form-item>
</div>
</template>
<div class="mb-8">
<el-form-item prop="relateColumnId">
<label class="block text-sm font-semibold text-gray-700 mb-3">
......@@ -185,6 +234,7 @@
<div class="bg-white/70 backdrop-blur-sm rounded-lg shadow-lg border border-white/20 p-6">
<div class="space-y-3 flex justify-center items-center gap-4">
<el-button
@click="handleSubmit(ReleaseStatusTypeEnum.DRAFT)"
size="large"
class="w-full !border-gray-200 hover:!border-indigo-300 hover:!text-indigo-600 transition-all duration-200"
>
......@@ -197,7 +247,7 @@
type="primary"
size="large"
class="w-full !bg-gradient-to-r !from-indigo-500 !to-purple-600 !border-none shadow-lg hover:shadow-xl transition-all duration-300"
@click="handleSubmit"
@click="handleSubmit(ReleaseStatusTypeEnum.PUBLISH)"
>
<el-icon class="mr-2">
<Upload />
......@@ -295,6 +345,7 @@ import type { TagItemDto, AddOrUpdateVideoDto } from '@/api'
import { Camera, Picture } from '@element-plus/icons-vue'
import { useVideoStore, useUserStore } from '@/stores'
import { storeToRefs } from 'pinia'
import { selectDepOrUser } from '@/utils'
const videoStore = useVideoStore()
const { videoList } = storeToRefs(videoStore)
......@@ -323,7 +374,41 @@ const [form, resetData] = useResetData<AddOrUpdateVideoDto>({
faceUrl: '', // 封面URL
videoDuration: '',
relateColumnId: undefined,
// 推送设置
pushType: SendTypeEnum.IMMEDIATE,
pushTime: '',
pushList: [],
})
const selectedDepts = ref<{ id: string; name: string }[]>([])
const selectedUsers = ref<{ id: string; name: string }[]>([])
const selectedText = computed(() => {
let text = ''
if (selectedDepts.value.length > 0) {
text += `已选择${selectedDepts.value.length}个部门`
}
if (selectedUsers.value.length > 0) {
text += `已选择${selectedUsers.value.length}个成员`
}
return text
})
const handleSelectUserAndDept = async () => {
const { departmentList, userList } = await selectDepOrUser({
mode: 'multi',
type: ['user', 'department'],
selectedDepartmentIds: selectedDepts.value.map((item) => item.id),
selectedUserIds: selectedUsers.value.map((item) => item.id),
})
selectedDepts.value = departmentList.map((item) => ({
id: item.id,
name: item.name,
}))
selectedUsers.value = userList.map((item) => ({
id: item.id,
name: item.name,
}))
}
// 封面选择相关
......@@ -419,17 +504,38 @@ const rules = {
mainTagId: [{ required: true, message: '请选择主标签', trigger: 'change' }],
faceUrl: [{ required: true, message: '请选择视频封面', trigger: 'change' }],
relateColumnId: [{ required: true, message: '请选择视频栏目', trigger: 'change' }],
// 推送设置
pushType: [{ required: true, message: '请选择推送类型', trigger: 'trigger' }],
pushTime: [{ required: true, message: '请选择推送时间', trigger: 'trigger' }],
pushList: [{ required: true, message: '请选择推送对象', trigger: 'trigger' }],
}
const tansformData = () => {
return {
const tansformData = (releaseStatus: ReleaseStatusTypeEnum) => {
const data = {
...form.value,
tagList: [form.value.mainTagId, ...form.value.tagList].map((item, index) => ({
tagId: Number(item),
sort: index,
})),
mainTagId: Number(form.value.mainTagId),
releaseStatus,
}
if (userInfo.value.isOfficialAccount) {
data.pushList = [
...selectedDepts.value.map((item) => ({
valueId: item.id,
valueType: 1,
valueName: item.name,
})),
...selectedUsers.value.map((item) => ({
valueId: item.id,
valueType: 2,
valueName: item.name,
})),
]
}
return data
}
const loading = ref(false)
......@@ -437,12 +543,12 @@ const resetPageData = () => {
resetData()
locationVideoBlolUrl.value = ''
}
const handleSubmit = async () => {
const handleSubmit = async (releaseStatus: ReleaseStatusTypeEnum) => {
await formRef.value?.validate()
loading.value = true
try {
addOrUpdateArticle(tansformData())
ElMessage.success('发布成功')
addOrUpdateArticle(tansformData(releaseStatus))
ElMessage.success(releaseStatus === ReleaseStatusTypeEnum.PUBLISH ? '发布成功' : '存草稿成功')
resetPageData()
router.push('/')
// 重置数据
......
......@@ -132,7 +132,11 @@
<el-icon class="text-sm">
<View />
</el-icon>
<span class="font-medium text-gray-500">{{ item.viewCount }}</span>
<span class="font-medium text-gray-500">{{
item.type === ArticleTypeEnum.VIDEO
? Math.max(item.playCount, item.viewCount)
: item.viewCount
}}</span>
</div>
<div class="flex items-center gap-1">
<el-icon class="text-sm">
......
......@@ -15,7 +15,7 @@
<div class="flex items-center text-14px text-gray-500 gap-4 flex-wrap">
<span class="flex items-center gap-1">
<i class="i-carbon-play-filled text-gray-400"></i>
{{ formatNumber(videoDetail?.viewCount) }}播放
{{ formatNumber(videoDetail?.playCount) }}播放
</span>
<span class="flex items-center gap-1">
<i class="i-carbon-time text-gray-400"></i>
......@@ -266,7 +266,7 @@ const formatNumber = (num: number) => {
// 播放 记录播放量 + 1
const handlePlay = async () => {
await addVideoPlayCount(videoDetail.value.id)
videoDetail.value.viewCount = videoDetail.value.viewCount + 1
videoDetail.value.playCount = videoDetail.value.playCount + 1
}
const handlePause = () => {
......
......@@ -105,7 +105,9 @@
<el-icon class="text-sm">
<View />
</el-icon>
<span class="font-medium text-gray-500">{{ item.viewCount }}</span>
<span class="font-medium text-gray-500">{{
Math.max(item.playCount, item.viewCount)
}}</span>
</div>
<div class="flex items-center gap-1">
<el-icon class="text-sm">
......
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