Commit 6360213e by lijiabin

【需求 25171】feat: 实践新增查看权限设置,支持指定部门/人员可见

parent 7d0ad70f
......@@ -261,6 +261,19 @@ export interface ArticleItemDto {
publishTime: number
// 官方账号推送的文章才有这个字段,列表接口直接带出来,不用再单独查一次推送状态
pushInfo?: ArticlePushInfoDto
// 实践的查看权限回显(详情接口才有)。跟保存时传的 valueList(纯 id 数组)不一样,
// 这里是 finalPermissions(带 valueName),方便直接拿来回显名字,不用再反查一次。
typeUpdates?: ArticleTypeUpdateEchoDto[]
}
/**
* 实践查看权限的回显(getArticleDetail 返回),跟保存时的 PracticeTypeUpdateDto 结构不同:
* 保存传 valueList(纯 id 数组),这里返回 finalPermissions(带 valueName,方便直接回显)。
*/
export interface ArticleTypeUpdateEchoDto {
/** 1 部门 / 2 人员 */
valueType: number
finalPermissions: { valueId: string; valueName: string }[]
}
/**
......
......@@ -2,6 +2,15 @@ import { ArticleTypeEnum, ReleaseStatusTypeEnum, BooleanFlag, SendTypeEnum } fro
import type { PageSearchParams } from '@/utils/request/types'
/**
* 保存查看权限时传的一组:valueType 1 部门 / 2 人员,valueList 是纯 id 数组
* (valueType=2 时 valueId 存的是 user_id,不是企微账号)。
*/
export interface PracticeTypeUpdateDto {
valueType: 1 | 2
valueList: string[]
}
/**
* 添加或更新实践DTO
*/
export interface AddOrUpdatePracticeDto {
......@@ -17,6 +26,8 @@ export interface AddOrUpdatePracticeDto {
sendTime: string
deptId?: string
type?: ArticleTypeEnum
// 查看权限:一个都没选就是公开,不传这个字段(不要传空数组硬凑)
typeUpdates?: PracticeTypeUpdateDto[]
}
/**
......
......@@ -144,6 +144,31 @@
<span class="ml-1 text-xs">使用富文本编辑</span>
</el-button>
</el-tooltip>
<!-- 实践可以设置查看权限:不选就是所有人可见,选了部门/人员之后只有被选中的能看到 -->
<DeptUserPicker
v-if="type === ArticleTypeEnum.PRACTICE"
v-model:dept-value="permissionDeptIds"
v-model:user-value="permissionUserIds"
selection-mode="both"
@confirm="(items) => (permissionSelection = items)"
v-slot="{ echoText }"
>
<el-tooltip
:content="echoText ? `仅以下部门/人员可见:${echoText}` : '设置查看权限(不设置则所有人可见)'"
placement="top"
>
<el-button
text
class="w-10 h-10 hover:bg-gray-100 rounded-lg"
:class="echoText ? 'text-blue-500' : 'text-gray-500 hover:text-gray-700'"
>
<el-icon size="18">
<IEpLock />
</el-icon>
</el-button>
</el-tooltip>
</DeptUserPicker>
</div>
<!-- 右侧操作按钮 -->
......@@ -193,6 +218,8 @@ import type { ElButton } from 'element-plus'
import { useAnimate } from '@vueuse/core'
import RichTextarea from '../RichTextarea/index.vue'
import UploadImgIcon from '../UploadImgIcon/index.vue'
import DeptUserPicker from '../DeptUserPicker/index.vue'
import type { DepartmentUserSelection } from '../DepartmentUserModal/types'
import { push } from 'notivue'
import { useTourStore } from '@/stores'
import { useRouter } from 'vue-router'
......@@ -284,6 +311,11 @@ const { imgList, handleFileChange, handleDeleteImg, uploadPercent } = useUploadI
toRef(form.value, 'imgUrl'),
)
// 实践"查看权限":不选就是公开,选了部门/人员之后只有被选中的能看到
const permissionDeptIds = ref<string[]>([])
const permissionUserIds = ref<string[]>([])
const permissionSelection = ref<DepartmentUserSelection[]>([])
const mainTagText = computed(() => {
return tagList.value.find((tag) => tag.id === Number(form.value.mainTagId))?.title
})
......@@ -316,7 +348,18 @@ const validateForm = () => {
return true
}
// 把选中的部门/人员按 valueType 分组,一个都没选就不传 typeUpdates(不要传空数组硬凑)
const buildTypeUpdates = () => {
const depts = permissionSelection.value.filter((item) => item.valueType === 1)
const users = permissionSelection.value.filter((item) => item.valueType === 2)
const typeUpdates: { valueType: 1 | 2; valueList: string[] }[] = []
if (depts.length) typeUpdates.push({ valueType: 1, valueList: depts.map((item) => item.valueId) })
if (users.length) typeUpdates.push({ valueType: 2, valueList: users.map((item) => item.valueId) })
return typeUpdates
}
const transformForm = (releaseStatus: ReleaseStatusTypeEnum): AddOrUpdatePracticeDto => {
const typeUpdates = buildTypeUpdates()
return {
...form.value,
releaseStatus,
......@@ -327,6 +370,7 @@ const transformForm = (releaseStatus: ReleaseStatusTypeEnum): AddOrUpdatePractic
tagId: Number(item),
})),
type: type,
...(typeUpdates.length ? { typeUpdates } : {}),
}
}
const handlePublish = async (releaseStatus: ReleaseStatusTypeEnum) => {
......@@ -339,6 +383,9 @@ const handlePublish = async (releaseStatus: ReleaseStatusTypeEnum) => {
push.success(releaseStatus === ReleaseStatusTypeEnum.PUBLISH ? '发布成功' : '存草稿成功')
// resetForm()
forReset()
permissionDeptIds.value = []
permissionUserIds.value = []
permissionSelection.value = []
// form.value.imgUrl = ''
} catch (error) {
console.error(error)
......
......@@ -2,15 +2,17 @@
import { Search, Upload } from '@element-plus/icons-vue'
import { usePageSearch, useResetData } from '@/hooks'
import { addOrUpdateColumn } from '@/api/backend'
import { getArticleList, updateArticleRecommendAndSort } from '@/api'
import { getArticleList, updateArticleRecommendAndSort, getArticleDetail, addOrUpdatePractice } from '@/api'
import type { FormInstance, FormRules } from 'element-plus'
import type { BackendColumnListItemDto, AddOrUpdateColumnDto } from '@/api/backend'
import dayjs from 'dayjs'
import { ArticleTypeEnum, BooleanFlag } from '@/constants'
import type { ArticleItemDto } from '@/api/article/types'
import type { ArticleItemDto, ArticleTypeUpdateEchoDto } from '@/api/article/types'
import { push } from 'notivue'
import { useMessageBox } from '@/hooks'
import ArticleContentDialog from '@/components/common/ArticleContentDialog/index.vue'
import DeptUserPicker from '@/components/common/DeptUserPicker/index.vue'
import type { DepartmentUserSelection } from '@/components/common/DepartmentUserModal/types'
const { prompt } = useMessageBox()
const contentDialogRef = useTemplateRef<InstanceType<typeof ArticleContentDialog>>('contentDialogRef')
......@@ -105,6 +107,74 @@ const handleSelectionChange = (selection: BackendColumnListItemDto[]) => {
const handleShowContent = (row: ArticleItemDto) => {
contentDialogRef.value?.open(row.content)
}
// 查看权限:管理员在后台也能改,点开先拉一次详情拿到完整数据(保存时要把其他字段原样带回去)
const permissionDialogVisible = ref(false)
const permissionSubmitting = ref(false)
const permissionDetail = ref<ArticleItemDto>()
const permissionDeptIds = ref<string[]>([])
const permissionUserIds = ref<string[]>([])
const permissionSelection = ref<DepartmentUserSelection[]>([])
const permissionInitialSelected = ref<DepartmentUserSelection[]>([])
// 详情接口回显的查看权限是 finalPermissions(带名字),转成 DeptUserPicker 认的 DepartmentUserSelection
const toSelectionFromTypeUpdates = (
typeUpdates?: ArticleTypeUpdateEchoDto[],
): DepartmentUserSelection[] => {
if (!typeUpdates) return []
return typeUpdates.flatMap((group) =>
group.finalPermissions.map((item) => ({
valueType: group.valueType as 1 | 2,
valueId: item.valueId,
valueName: item.valueName,
})),
)
}
const handleEditPermission = async (row: ArticleItemDto) => {
const { data } = await getArticleDetail(row.id)
permissionDetail.value = data
const items = toSelectionFromTypeUpdates(data.typeUpdates)
permissionDeptIds.value = items.filter((item) => item.valueType === 1).map((item) => item.valueId)
permissionUserIds.value = items.filter((item) => item.valueType === 2).map((item) => item.valueId)
permissionSelection.value = items
permissionInitialSelected.value = items
permissionDialogVisible.value = true
}
const handleSubmitPermission = async () => {
if (!permissionDetail.value) return
const depts = permissionSelection.value.filter((item) => item.valueType === 1)
const users = permissionSelection.value.filter((item) => item.valueType === 2)
const typeUpdates: { valueType: 1 | 2; valueList: string[] }[] = []
if (depts.length) typeUpdates.push({ valueType: 1, valueList: depts.map((item) => item.valueId) })
if (users.length) typeUpdates.push({ valueType: 2, valueList: users.map((item) => item.valueId) })
const detail = permissionDetail.value
permissionSubmitting.value = true
try {
await addOrUpdatePractice({
id: detail.id,
title: detail.title,
content: detail.content,
faceUrl: detail.faceUrl,
imgUrl: detail.imgUrl,
releaseStatus: detail.releaseStatus,
mainTagId: detail.tagIdList[0] ?? '',
tagList: detail.tagIdList.map((tagId, index) => ({ tagId, sort: index })),
sendType: detail.sendType,
sendTime: detail.sendTime,
deptId: detail.deptId,
...(typeUpdates.length ? { typeUpdates } : {}),
})
push.success('修改成功')
permissionDialogVisible.value = false
search()
} finally {
permissionSubmitting.value = false
}
}
</script>
<template>
......@@ -180,6 +250,14 @@ const handleShowContent = (row: ArticleItemDto) => {
</template>
</el-table-column>
<el-table-column label="查看权限" min-width="120">
<template #default="{ row }">
<el-link type="primary" :underline="false" @click="handleEditPermission(row)">
设置权限
</el-link>
</template>
</el-table-column>
<el-table-column prop="showName" label="创建人" min-width="200" />
<el-table-column prop="createTime" label="创建时间" min-width="200">
......@@ -235,6 +313,37 @@ const handleShowContent = (row: ArticleItemDto) => {
</el-dialog>
<ArticleContentDialog ref="contentDialogRef" />
<!-- 查看权限:本人和管理员都能改,这里是后台管理员改的入口 -->
<el-dialog
v-model="permissionDialogVisible"
title="设置查看权限"
width="500px"
:close-on-click-modal="false"
>
<DeptUserPicker
v-model:dept-value="permissionDeptIds"
v-model:user-value="permissionUserIds"
selection-mode="both"
:initial-selected="permissionInitialSelected"
@confirm="(items) => (permissionSelection = items)"
v-slot="{ echoText }"
>
<el-input
readonly
class="w-full cursor-pointer"
placeholder="不设置则所有人可见"
:model-value="echoText"
/>
</DeptUserPicker>
<template #footer>
<el-button @click="permissionDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="permissionSubmitting" @click="handleSubmitPermission">
保存
</el-button>
</template>
</el-dialog>
</div>
</template>
......
......@@ -18,6 +18,25 @@
/>
</el-form-item>
<!-- 查看权限:不选就是所有人可见,选了部门/人员之后只有被选中的能看到,本人和管理员都能改 -->
<el-form-item label="查看权限">
<DeptUserPicker
v-model:dept-value="permissionDeptIds"
v-model:user-value="permissionUserIds"
selection-mode="both"
:initial-selected="permissionInitialSelected"
@confirm="(items) => (permissionSelection = items)"
v-slot="{ echoText }"
>
<el-input
readonly
class="w-full cursor-pointer"
placeholder="不设置则所有人可见"
:model-value="echoText"
/>
</DeptUserPicker>
</el-form-item>
<!-- 实践没有推送功能(不管是不是官方账号);官方账号可以选一个所属一级部门,选填 -->
<el-form-item v-if="userInfo.isOfficialAccount" label="所属一级部门">
<el-select v-model="form.deptId" placeholder="请选择一级部门" clearable>
......@@ -71,6 +90,8 @@
* - 主/副标签必填
* - 完全没有推送功能,不管是不是官方账号——这是跟帖子/问吧/专栏/专访最大的区别
* - 官方账号可以选一个"所属一级部门",选填,不是必填项
* - 查看权限:不选部门/人员就是公开,选了之后只有被选中的能看到,作者本人和管理员都能改;
* 这个不区分官方账号,任何人发实践都能设置
* - 提交走的是单独的 addOrUpdatePractice 接口(不是 addOrUpdateArticle),这个在外层
* publishLongArticle/index.vue 里按 type 分支处理,这个组件只管收集字段
*/
......@@ -78,7 +99,9 @@ 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 DeptUserPicker from '@/components/common/DeptUserPicker/index.vue'
import type { DepartmentUserSelection } from '@/components/common/DepartmentUserModal/types'
import type { ArticleItemDto, ArticleTypeUpdateEchoDto } from '@/api/article/types'
import { useResetData } from '@/hooks'
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
......@@ -97,7 +120,15 @@ const [form, resetForm] = useResetData({
regionHide: BooleanFlag.NO,
})
const filterTagsFn = (allTags: any[]) => allTags.filter((tag) => tag.id !== Number(form.value.mainTagId))
// 查看权限选中的部门/人员
const permissionDeptIds = ref<string[]>([])
const permissionUserIds = ref<string[]>([])
const permissionSelection = ref<DepartmentUserSelection[]>([])
// 编辑回显用,给 DeptUserPicker 做首次回显的名字种子
const permissionInitialSelected = ref<DepartmentUserSelection[]>([])
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)
......@@ -123,9 +154,35 @@ const rules: Record<string, FormItemRule[]> = {
regionHide: [{ required: true, message: '请选择是否隐藏IP信息', trigger: 'trigger' }],
}
// 把选中的部门/人员按 valueType 分组,一个都没选就不传 typeUpdates(不要传空数组硬凑)
const buildTypeUpdates = () => {
const depts = permissionSelection.value.filter((item) => item.valueType === 1)
const users = permissionSelection.value.filter((item) => item.valueType === 2)
const typeUpdates: { valueType: 1 | 2; valueList: string[] }[] = []
if (depts.length) typeUpdates.push({ valueType: 1, valueList: depts.map((item) => item.valueId) })
if (users.length) typeUpdates.push({ valueType: 2, valueList: users.map((item) => item.valueId) })
return typeUpdates
}
// 详情接口回显的查看权限是 finalPermissions(带名字),转成 DeptUserPicker 认的 DepartmentUserSelection
const toSelectionFromTypeUpdates = (
typeUpdates?: ArticleTypeUpdateEchoDto[],
): DepartmentUserSelection[] => {
if (!typeUpdates) return []
return typeUpdates.flatMap((group) =>
group.finalPermissions.map((item) => ({
valueType: group.valueType as 1 | 2,
valueId: item.valueId,
valueName: item.valueName,
})),
)
}
defineExpose({
validate: () => formRef.value!.validate(),
getSubmitData: () => ({
getSubmitData: () => {
const typeUpdates = buildTypeUpdates()
return {
imgUrl: form.value.imgUrl,
faceUrl: form.value.imgUrl ? form.value.imgUrl.split(',')[0] || '' : '',
mainTagId: form.value.mainTagId,
......@@ -137,7 +194,9 @@ defineExpose({
sendTime: form.value.sendTime,
regionHide: form.value.regionHide,
...(userInfo.value.isOfficialAccount ? { deptId: form.value.deptId || '' } : {}),
}),
...(typeUpdates.length ? { typeUpdates } : {}),
}
},
setEchoData: (data: ArticleItemDto) => {
form.value.imgUrl = data.imgUrl
form.value.mainTagId = data.tagIdList[0] ? String(data.tagIdList[0]) : ''
......@@ -148,9 +207,23 @@ defineExpose({
if (userInfo.value.isOfficialAccount) {
form.value.deptId = data.deptId === '0' ? '' : data.deptId
}
const items = toSelectionFromTypeUpdates(data.typeUpdates)
permissionDeptIds.value = items
.filter((item) => item.valueType === 1)
.map((item) => item.valueId)
permissionUserIds.value = items
.filter((item) => item.valueType === 2)
.map((item) => item.valueId)
permissionSelection.value = items
permissionInitialSelected.value = items
},
reset: () => {
resetForm()
permissionDeptIds.value = []
permissionUserIds.value = []
permissionSelection.value = []
permissionInitialSelected.value = []
},
})
</script>
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