Commit c0ef89a5 by lijiabin

【需求 25171】feat: 视频新增分类功能,支持后台管理分类、前台按分类筛选

parent 6360213e
...@@ -16,6 +16,8 @@ export interface ArticleSearchParams extends PageSearchParams { ...@@ -16,6 +16,8 @@ export interface ArticleSearchParams extends PageSearchParams {
title?: string title?: string
questionFocus?: number questionFocus?: number
status?: BooleanFlag status?: BooleanFlag
// 视频分类id,前台分类栏点击筛选用,只对视频类型生效,不传为全部
categoryId?: number
} }
/** /**
...@@ -170,6 +172,8 @@ export interface AddOrUpdateVideoDto { ...@@ -170,6 +172,8 @@ export interface AddOrUpdateVideoDto {
tagList: { tagId: number; sort: number }[] | number[] tagList: { tagId: number; sort: number }[] | number[]
relateColumnId?: number relateColumnId?: number
isShowLatest?: BooleanFlag isShowLatest?: BooleanFlag
// 视频分类,最多5个,选填;不传表示不改动已有分类,传空数组表示清空
categoryIdList?: number[]
} }
/** /**
...@@ -264,6 +268,10 @@ export interface ArticleItemDto { ...@@ -264,6 +268,10 @@ export interface ArticleItemDto {
// 实践的查看权限回显(详情接口才有)。跟保存时传的 valueList(纯 id 数组)不一样, // 实践的查看权限回显(详情接口才有)。跟保存时传的 valueList(纯 id 数组)不一样,
// 这里是 finalPermissions(带 valueName),方便直接拿来回显名字,不用再反查一次。 // 这里是 finalPermissions(带 valueName),方便直接拿来回显名字,不用再反查一次。
typeUpdates?: ArticleTypeUpdateEchoDto[] typeUpdates?: ArticleTypeUpdateEchoDto[]
// 视频分类,只有视频类型才有;categoryIdList/categoryNameList 按下标一一对应,
// 详情接口回显的分类里可能包含已被设为不公开的分类,前端需要把它们并进下拉选项里
categoryIdList?: number[]
categoryNameList?: string[]
} }
/** /**
......
...@@ -18,6 +18,7 @@ export * from './launchCampaign' ...@@ -18,6 +18,7 @@ export * from './launchCampaign'
export * from './selfMessage' export * from './selfMessage'
export * from './luckyWheel' export * from './luckyWheel'
export * from './deptUser' export * from './deptUser'
export * from './videoCategory'
// 导出类型 // 导出类型
export * from './task/types' export * from './task/types'
export * from './shop/types' export * from './shop/types'
...@@ -38,3 +39,4 @@ export * from './launchCampaign/types' ...@@ -38,3 +39,4 @@ export * from './launchCampaign/types'
export * from './selfMessage/types' export * from './selfMessage/types'
export * from './luckyWheel/types' export * from './luckyWheel/types'
export * from './deptUser/types' export * from './deptUser/types'
export * from './videoCategory/types'
import service from '@/utils/request/index'
import type { BooleanFlag } from '@/constants'
import type { VideoCategoryItemDto, AddOrUpdateVideoCategoryDto } from './types'
/**
* 视频分类列表-后台管理:含未公开的,已按排序返回,name 不传为全部
*/
export const listVideoCategoryForManage = (name?: string) =>
service.request<VideoCategoryItemDto[]>({
url: '/api/cultureVideoCategory/listForManage',
method: 'POST',
params: { name },
})
/**
* 视频分类列表-前台:只返回公开的,已按排序返回,前台分类栏与提交/编辑视频的下拉共用
*/
export const listVideoCategoryPublic = () =>
service.request<VideoCategoryItemDto[]>({
url: '/api/cultureVideoCategory/listPublic',
method: 'POST',
})
/**
* 新增或修改分类:不传 id 为新增,传 id 为改名
*/
export const addOrUpdateVideoCategory = (data: AddOrUpdateVideoCategoryDto) =>
service.request<boolean>({
url: '/api/cultureVideoCategory/addOrUpdate',
method: 'POST',
data,
})
/**
* 删除分类:会同时移除该分类下所有视频的关联,视频本身不受影响
*/
export const deleteVideoCategory = (id: number) =>
service.request<boolean>({
url: '/api/cultureVideoCategory/delete',
method: 'POST',
params: { id },
})
/**
* 公开/不公开分类:isPublic 1公开 0不公开
*/
export const changeVideoCategoryPublic = (id: number, isPublic: BooleanFlag) =>
service.request<boolean>({
url: '/api/cultureVideoCategory/changePublic',
method: 'POST',
params: { id, isPublic },
})
/**
* 分类排序:传拖动后排好序的完整 id 列表,后端按下标重排
*/
export const updateVideoCategorySort = (idList: number[]) =>
service.request<boolean>({
url: '/api/cultureVideoCategory/updateSort',
method: 'POST',
data: idList,
})
/**
* 设置某个视频的分类(后台视频列表里改分类用):全量覆盖,传空数组等于清空
*/
export const bindVideoCategoryToArticle = (articleId: number, categoryIdList: number[]) =>
service.request<boolean>({
url: '/api/cultureVideoCategory/bindArticle',
method: 'POST',
data: { articleId, categoryIdList },
})
import type { BooleanFlag } from '@/constants'
/**
* 视频分类
*/
export interface VideoCategoryItemDto {
id: number
name: string
sort: number
isPublic: BooleanFlag
}
/**
* 新增或修改视频分类:不传 id 为新增(后端自动排最后、默认公开),传 id 为改名
*/
export interface AddOrUpdateVideoCategoryDto {
id?: number
name: string
}
<template>
<el-dialog v-model="visible" title="修改视频分类" width="500px" :close-on-click-modal="false">
<el-select
v-model="selectedIds"
multiple
:multiple-limit="5"
collapse-tags
collapse-tags-tooltip
placeholder="最多选择5个分类,不选则不属于任何分类"
class="w-full"
>
<el-option v-for="item in options" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
/**
* 后台视频列表点击「视频分类」列,修改这条视频所属的分类。
*
* 下拉选项统一只展示公开分类(跟前台下拉共用 listPublic):如果在这里能选到不公开的分类,
* 那条视频挂着的分类在前台分类栏里却看不到对应入口,会很confusing,产品那边确认过统一改成
* 只看公开的。但这条视频原本可能已经挂着被设为不公开的分类——那些不在 listPublic 里,直接用
* listPublic 当选项会导致它们的名字显示不出来(变成裸 id)。这里把 row 自带的
* categoryIdList/categoryNameList 里、不在公开列表里的那些也合并进选项,只是为了回显,
* 不代表用户可以新选一个不公开的分类。
*/
import { listVideoCategoryPublic, bindVideoCategoryToArticle } from '@/api'
import type { VideoCategoryItemDto, ArticleItemDto } from '@/api'
import { push } from 'notivue'
const emit = defineEmits<{ changed: [] }>()
const visible = ref(false)
const submitting = ref(false)
const options = ref<VideoCategoryItemDto[]>([])
const selectedIds = ref<number[]>([])
const articleId = ref<number>()
const open = async (row: ArticleItemDto) => {
articleId.value = row.id
selectedIds.value = row.categoryIdList ? [...row.categoryIdList] : []
const { data } = await listVideoCategoryPublic()
const publicOptions = data
const hiddenBound = (row.categoryIdList || [])
.map((id, index) => ({ id, name: row.categoryNameList?.[index] }))
.filter(
(item): item is { id: number; name: string } =>
!!item.name && !publicOptions.some((option) => option.id === item.id),
)
.map((item) => ({ id: item.id, name: item.name, sort: 0, isPublic: 0 }) as VideoCategoryItemDto)
options.value = [...publicOptions, ...hiddenBound]
visible.value = true
}
const handleSubmit = async () => {
if (articleId.value === undefined) return
submitting.value = true
try {
await bindVideoCategoryToArticle(articleId.value, selectedIds.value)
push.success('修改成功')
visible.value = false
emit('changed')
} finally {
submitting.value = false
}
}
defineExpose({ open })
</script>
<template>
<el-dialog v-model="visible" title="视频分类管理" width="600px" :close-on-click-modal="false">
<div class="mb-3 text-13px text-gray-500">排前的分类优先展示,公开后用户才可选择加入该分类</div>
<div class="flex gap-2 mb-3">
<el-input
v-model="searchName"
placeholder="搜索分类名称"
clearable
@keyup.enter="fetchList"
@clear="fetchList"
/>
<el-button type="primary" @click="fetchList">搜索</el-button>
</div>
<div class="flex gap-2 mb-3">
<el-input
v-model="newName"
placeholder="输入分类名称,回车新增"
maxlength="50"
@keyup.enter="handleAdd"
/>
<el-button type="primary" :loading="adding" @click="handleAdd">新增分类</el-button>
</div>
<div v-loading="loading" class="max-h-400px overflow-y-auto border border-gray-100 rounded-lg">
<div
v-for="(item, index) in list"
:key="item.id"
class="flex items-center gap-2 px-3 py-2 border-b border-gray-100 last:border-b-0 bg-white"
:class="{ 'opacity-40': dragIndex === index }"
draggable="true"
@dragstart="handleDragStart(index)"
@dragover.prevent
@drop="handleDrop(index)"
@dragend="dragIndex = undefined"
>
<span class="cursor-move select-none text-gray-400 px-1" title="拖动排序">⠿⠿</span>
<el-input
v-if="editingId === item.id"
v-model="editingName"
size="small"
class="flex-1"
maxlength="50"
@keyup.enter="handleConfirmRename(item)"
@blur="handleConfirmRename(item)"
/>
<span v-else class="flex-1 truncate">{{ item.name }}</span>
<el-button
v-if="editingId !== item.id"
link
type="primary"
@click="handleStartRename(item)"
>
改名
</el-button>
<el-switch
:model-value="item.isPublic"
:active-value="BooleanFlag.YES"
:inactive-value="BooleanFlag.NO"
active-text="公开"
@change="(val) => handleChangePublic(item, val as BooleanFlag)"
/>
<el-button link type="danger" @click="handleDelete(item)">删除</el-button>
</div>
<el-empty v-if="!loading && !list.length" description="暂无分类" />
</div>
<template #footer>
<el-button @click="visible = false">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
/**
* 后台「视频分类管理」弹框:增删改查 + 拖动排序 + 公开开关。
*
* 拖动排序用原生 HTML5 drag & drop(项目里没有引入拖拽库),drop 时把整份
* 拖完序的列表按 id 顺序传给 updateVideoCategorySort,不是只传被拖的那一条。
*/
import {
listVideoCategoryForManage,
addOrUpdateVideoCategory,
deleteVideoCategory,
changeVideoCategoryPublic,
updateVideoCategorySort,
} from '@/api'
import type { VideoCategoryItemDto } from '@/api'
import { BooleanFlag } from '@/constants'
import { push } from 'notivue'
import { useMessageBox } from '@/hooks'
const { confirm } = useMessageBox()
// 分类的增/删/改/排序/公开状态发生变化后通知外层:视频列表里缓存的分类名字可能要跟着刷新
const emit = defineEmits<{ changed: [] }>()
const visible = ref(false)
const loading = ref(false)
const list = ref<VideoCategoryItemDto[]>([])
const searchName = ref('')
const fetchList = async () => {
loading.value = true
try {
const { data } = await listVideoCategoryForManage(searchName.value || undefined)
list.value = data
} finally {
loading.value = false
}
}
const open = () => {
searchName.value = ''
newName.value = ''
editingId.value = undefined
visible.value = true
fetchList()
}
// 新增:新增的分类后端自动排在最后,前端不用传 sort
const newName = ref('')
const adding = ref(false)
const handleAdd = async () => {
const name = newName.value.trim()
if (!name) return
adding.value = true
try {
await addOrUpdateVideoCategory({ name })
newName.value = ''
push.success('新增成功')
await fetchList()
emit('changed')
} finally {
adding.value = false
}
}
// 改名
const editingId = ref<number>()
const editingName = ref('')
const handleStartRename = (item: VideoCategoryItemDto) => {
editingId.value = item.id
editingName.value = item.name
}
const handleConfirmRename = async (item: VideoCategoryItemDto) => {
// enter 和 blur 都会触发这个函数,第一次进来就把 editingId 清空,
// 后触发的那次会因为 id 对不上直接 return,避免重复提交
if (editingId.value !== item.id) return
editingId.value = undefined
const name = editingName.value.trim()
if (!name || name === item.name) return
await addOrUpdateVideoCategory({ id: item.id, name })
push.success('修改成功')
await fetchList()
emit('changed')
}
// 删除:同时移除该分类下所有视频的关联,视频本身不受影响
const handleDelete = async (item: VideoCategoryItemDto) => {
await confirm({
title: '提示',
message: `确定要删除分类"${item.name}"吗?该分类下的视频不受影响,仅会解除关联。`,
type: 'danger',
})
await deleteVideoCategory(item.id)
push.success('删除成功')
await fetchList()
emit('changed')
}
// 公开开关:不公开只是前台不展示这个分类入口,分类下的视频照常可见可搜
const handleChangePublic = async (item: VideoCategoryItemDto, val: BooleanFlag) => {
await changeVideoCategoryPublic(item.id, val)
item.isPublic = val
push.success('修改成功')
emit('changed')
}
// 拖动排序
const dragIndex = ref<number>()
const handleDragStart = (index: number) => {
dragIndex.value = index
}
const handleDrop = async (targetIndex: number) => {
const from = dragIndex.value
dragIndex.value = undefined
if (from === undefined || from === targetIndex) return
const [moved] = list.value.splice(from, 1)
if (!moved) return
list.value.splice(targetIndex, 0, moved)
await updateVideoCategorySort(list.value.map((item) => item.id))
emit('changed')
}
defineExpose({ open })
</script>
...@@ -15,8 +15,15 @@ import { ArticleTypeEnum, BooleanFlag } from '@/constants' ...@@ -15,8 +15,15 @@ import { ArticleTypeEnum, BooleanFlag } from '@/constants'
import type { ArticleItemDto } from '@/api/article/types' import type { ArticleItemDto } from '@/api/article/types'
import { push } from 'notivue' import { push } from 'notivue'
import { useMessageBox } from '@/hooks' import { useMessageBox } from '@/hooks'
import CategoryManageDialog from './CategoryManageDialog.vue'
import BindCategoryDialog from './BindCategoryDialog.vue'
const { prompt, confirm } = useMessageBox() const { prompt, confirm } = useMessageBox()
const categoryManageDialogRef =
useTemplateRef<InstanceType<typeof CategoryManageDialog>>('categoryManageDialogRef')
const bindCategoryDialogRef =
useTemplateRef<InstanceType<typeof BindCategoryDialog>>('bindCategoryDialogRef')
const { loading, list, total, reset, goToPage, changePageSize, refresh, searchParams, search } = const { loading, list, total, reset, goToPage, changePageSize, refresh, searchParams, search } =
usePageSearch(getArticleList, { usePageSearch(getArticleList, {
defaultParams: { defaultParams: {
...@@ -140,6 +147,11 @@ const handleRecallPush = async (row: ArticleItemDto) => { ...@@ -140,6 +147,11 @@ const handleRecallPush = async (row: ArticleItemDto) => {
push.success(isScheduled ? '取消定时推送成功' : '撤回成功') push.success(isScheduled ? '取消定时推送成功' : '撤回成功')
refresh() refresh()
} }
// 点击「视频分类」列,改这条视频的分类
const handleEditCategory = (row: ArticleItemDto) => {
bindCategoryDialogRef.value?.open(row)
}
</script> </script>
<template> <template>
...@@ -153,6 +165,7 @@ const handleRecallPush = async (row: ArticleItemDto) => { ...@@ -153,6 +165,7 @@ const handleRecallPush = async (row: ArticleItemDto) => {
></el-input> ></el-input>
<el-button type="primary" :icon="Search" @click="refresh">搜索</el-button> <el-button type="primary" :icon="Search" @click="refresh">搜索</el-button>
<el-button @click="reset">重置</el-button> <el-button @click="reset">重置</el-button>
<el-button @click="categoryManageDialogRef?.open()">视频分类管理</el-button>
</div> </div>
<!-- 表格区域 --> <!-- 表格区域 -->
...@@ -197,6 +210,28 @@ const handleRecallPush = async (row: ArticleItemDto) => { ...@@ -197,6 +210,28 @@ const handleRecallPush = async (row: ArticleItemDto) => {
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="视频分类" min-width="180">
<template #default="{ row }">
<el-tooltip
v-if="row.categoryNameList?.length"
:content="row.categoryNameList.join('、')"
placement="top"
>
<el-link
type="primary"
:underline="false"
class="truncate max-w-full block"
@click="handleEditCategory(row)"
>
{{ row.categoryNameList.join('、') }}
</el-link>
</el-tooltip>
<el-link v-else type="primary" :underline="false" @click="handleEditCategory(row)">
设置分类
</el-link>
</template>
</el-table-column>
<el-table-column prop="isShowLatest" label="是否在最新出现" min-width="160"> <el-table-column prop="isShowLatest" label="是否在最新出现" min-width="160">
<template #default="{ row }"> <template #default="{ row }">
<el-switch <el-switch
...@@ -316,6 +351,9 @@ const handleRecallPush = async (row: ArticleItemDto) => { ...@@ -316,6 +351,9 @@ const handleRecallPush = async (row: ArticleItemDto) => {
</el-button> </el-button>
</template> </template>
</el-dialog> </el-dialog>
<CategoryManageDialog ref="categoryManageDialogRef" @changed="refresh" />
<BindCategoryDialog ref="bindCategoryDialogRef" @changed="refresh" />
</div> </div>
</template> </template>
......
<template>
<div class="flex items-start gap-2 pb-3">
<div
ref="containerRef"
class="flex flex-wrap gap-2 flex-1 min-w-0"
:style="!expanded && overflowing ? { maxHeight: `${rowHeight}px`, overflow: 'hidden' } : {}"
>
<button
v-for="item in options"
:key="item.id ?? 'all'"
class="px-4 py-1.5 rounded-full text-sm whitespace-nowrap transition-colors duration-200 cursor-pointer"
:class="
modelValue === item.id
? 'bg-orange-400 text-white shadow-md'
: 'bg-gray-50 text-gray-600 hover:text-orange-500 hover:bg-orange-50'
"
@click="$emit('update:modelValue', item.id)"
>
{{ item.name }}
</button>
</div>
<button
v-if="overflowing"
class="px-3 py-1.5 text-sm text-gray-500 hover:text-orange-500 whitespace-nowrap shrink-0 cursor-pointer"
@click="expanded = !expanded"
>
{{ expanded ? '收起' : '更多' }}
</button>
</div>
</template>
<script setup lang="ts">
/**
* 前台视频分类栏:数据用 listPublic(已按后台顺序排好,不用再排),加一个「全部」项
* (选中时不传 categoryId)。超过两行时收起,展示「更多」按钮点开。
*/
import { listVideoCategoryPublic } from '@/api'
defineProps<{ modelValue?: number }>()
defineEmits<{ 'update:modelValue': [value: number | undefined] }>()
const categories = ref<{ id?: number; name: string }[]>([])
const options = computed(() => [{ id: undefined, name: '全部' }, ...categories.value])
const containerRef = useTemplateRef<HTMLDivElement>('containerRef')
const expanded = ref(false)
const overflowing = ref(false)
const rowHeight = ref(0)
const checkOverflow = () => {
const el = containerRef.value
const firstButton = el?.firstElementChild as HTMLElement | undefined
if (!el || !firstButton) return
// gap-2 = 8px:两行的高度是「按钮高度 * 2 + 行间距」,超出这个高度才需要收起
rowHeight.value = firstButton.offsetHeight * 2 + 8
overflowing.value = el.scrollHeight > rowHeight.value + 1
}
let resizeObserver: ResizeObserver | undefined
onMounted(async () => {
const { data } = await listVideoCategoryPublic()
categories.value = data
await nextTick()
checkOverflow()
resizeObserver = new ResizeObserver(checkOverflow)
if (containerRef.value) resizeObserver.observe(containerRef.value)
})
onUnmounted(() => resizeObserver?.disconnect())
</script>
<template> <template>
<div> <div>
<!-- 视频分类栏 -->
<div class="max-w-7xl mx-auto px-4">
<VideoCategoryBar :model-value="searchParams.categoryId" @update:model-value="changeCategory" />
</div>
<!-- tabs --> <!-- tabs -->
<div class="shadow-sm"> <div class="shadow-sm">
<div class="max-w-7xl mx-auto px-4"> <div class="max-w-7xl mx-auto px-4">
...@@ -422,6 +426,7 @@ import { getArticleList } from '@/api' ...@@ -422,6 +426,7 @@ import { getArticleList } from '@/api'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useNavigation } from '@/hooks' import { useNavigation } from '@/hooks'
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core' import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
import VideoCategoryBar from './VideoCategoryBar.vue'
const breakpoints = useBreakpoints(breakpointsTailwind) const breakpoints = useBreakpoints(breakpointsTailwind)
const smallerThanXl = breakpoints.smaller('xl') const smallerThanXl = breakpoints.smaller('xl')
...@@ -448,6 +453,11 @@ const toggleTab = (sortLogic: number) => { ...@@ -448,6 +453,11 @@ const toggleTab = (sortLogic: number) => {
searchParams.value.sortLogic = sortLogic searchParams.value.sortLogic = sortLogic
refresh() refresh()
} }
// 分类栏点击某个分类:跟「最新发布/最多播放」排序是叠加的,两个参数一起传给 listByPage
const changeCategory = (categoryId: number | undefined) => {
searchParams.value.categoryId = categoryId
refresh()
}
defineExpose({ defineExpose({
refresh: () => { refresh: () => {
......
...@@ -100,6 +100,29 @@ ...@@ -100,6 +100,29 @@
</el-form-item> </el-form-item>
</div> </div>
<!-- 视频分类:非必选,最多5个,普通用户和管理员都能选,不受 showSpecialInput 限制 -->
<div class="mb-8">
<el-form-item prop="categoryIdList">
<label class="block text-sm font-semibold text-gray-700 mb-3">视频分类</label>
<el-select
v-model="form.categoryIdList"
multiple
:multiple-limit="5"
collapse-tags
collapse-tags-tooltip
placeholder="选择视频分类(非必选,最多5个)"
class="w-full"
>
<el-option
v-for="item in categoryOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</div>
<template v-if="showSpecialInput"> <template v-if="showSpecialInput">
<div class="mb-8"> <div class="mb-8">
<el-form-item prop="mainTagId"> <el-form-item prop="mainTagId">
...@@ -392,9 +415,15 @@ ...@@ -392,9 +415,15 @@
import UploadVideo from '@/components/common/UploadVideo/index.vue' import UploadVideo from '@/components/common/UploadVideo/index.vue'
import { useResetData } from '@/hooks' import { useResetData } from '@/hooks'
import { ArticleTypeEnum, ReleaseStatusTypeEnum, SendTypeEnum, BooleanFlag } from '@/constants' import { ArticleTypeEnum, ReleaseStatusTypeEnum, SendTypeEnum, BooleanFlag } from '@/constants'
import { addOrUpdateArticle, uploadFile, getArticleDetail,resolveUploadBaseUrl } from '@/api' import {
addOrUpdateArticle,
uploadFile,
getArticleDetail,
resolveUploadBaseUrl,
listVideoCategoryPublic,
} from '@/api'
import SelectTags from '@/components/common/SelectTags/index.vue' import SelectTags from '@/components/common/SelectTags/index.vue'
import type { TagItemDto, AddOrUpdateVideoDto} from '@/api' import type { TagItemDto, AddOrUpdateVideoDto, VideoCategoryItemDto } from '@/api'
import { useVideoStore, useUserStore } from '@/stores' import { useVideoStore, useUserStore } from '@/stores'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import DeptUserPicker from '@/components/common/DeptUserPicker/index.vue' import DeptUserPicker from '@/components/common/DeptUserPicker/index.vue'
...@@ -430,6 +459,7 @@ const [form, resetData] = useResetData({ ...@@ -430,6 +459,7 @@ const [form, resetData] = useResetData({
faceUrl: '', // 封面URL faceUrl: '', // 封面URL
videoDuration: '', videoDuration: '',
relateColumnId: '' as string | number, relateColumnId: '' as string | number,
categoryIdList: [] as number[],
regionHide: BooleanFlag.NO, regionHide: BooleanFlag.NO,
isPushAll: BooleanFlag.YES, isPushAll: BooleanFlag.YES,
// 推送设置 // 推送设置
...@@ -452,6 +482,26 @@ const pushSelection = ref<DepartmentUserSelection[]>([]) ...@@ -452,6 +482,26 @@ const pushSelection = ref<DepartmentUserSelection[]>([])
// 编辑回显用,给 DeptUserPicker 做首次回显的名字种子(不传的话首次打开只能看到 id)。 // 编辑回显用,给 DeptUserPicker 做首次回显的名字种子(不传的话首次打开只能看到 id)。
const pushInitialSelected = ref<DepartmentUserSelection[]>([]) const pushInitialSelected = ref<DepartmentUserSelection[]>([])
// 视频分类下拉:不管是普通用户还是管理员,统一只展示公开分类——管理员如果选了不公开的分类,
// 这个分类在前台分类栏不展示,用户会一脸懵(产品那边确认过,统一改成只看公开的)。
// 编辑回显时,如果这个视频本身挂着已被设为不公开的分类,需要把它们也并进选项里,
// 不然 el-select 只会显示 id 而不是名字
const categoryOptions = ref<VideoCategoryItemDto[]>([])
onMounted(async () => {
const { data } = await listVideoCategoryPublic()
categoryOptions.value = data
})
const mergeHiddenCategoryOptions = (categoryIdList?: number[], categoryNameList?: string[]) => {
const hidden = (categoryIdList || [])
.map((id, index) => ({ id, name: categoryNameList?.[index] }))
.filter(
(item): item is { id: number; name: string } =>
!!item.name && !categoryOptions.value.some((option) => option.id === item.id),
)
.map((item) => ({ id: item.id, name: item.name, sort: 0, isPublic: 0 }) as VideoCategoryItemDto)
if (hidden.length) categoryOptions.value = [...categoryOptions.value, ...hidden]
}
// 封面选择相关 // 封面选择相关
const locationVideoBlolUrl = ref('') const locationVideoBlolUrl = ref('')
...@@ -654,7 +704,9 @@ onActivated(async () => { ...@@ -654,7 +704,9 @@ onActivated(async () => {
...data, ...data,
mainTagId: data.tagIdList?.[0] ? String(data.tagIdList[0]) : '', mainTagId: data.tagIdList?.[0] ? String(data.tagIdList[0]) : '',
tagList: data.tagIdList?.slice(1) || [], tagList: data.tagIdList?.slice(1) || [],
categoryIdList: data.categoryIdList || [],
} }
mergeHiddenCategoryOptions(data.categoryIdList, data.categoryNameList)
// 回显推送的部门 这个后端没有返回相关数据 需要返回 // 回显推送的部门 这个后端没有返回相关数据 需要返回
if (data.pushList?.find((item) => item.valueId == '1')) { if (data.pushList?.find((item) => item.valueId == '1')) {
form.value.isPushAll = BooleanFlag.YES form.value.isPushAll = BooleanFlag.YES
......
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