feat(permission): permission manage

This commit is contained in:
dataeaseShu
2025-07-18 18:42:30 +08:00
parent ab93be44d8
commit 9a0d7b0dfe
12 changed files with 1481 additions and 59 deletions
+4
View File
@@ -0,0 +1,4 @@
import { request } from '@/utils/request'
export const getList = () => request.post('/ds_permission/list')
export const savePermissions = (data: any) => request.post('/ds_permission/save', data)
@@ -0,0 +1,5 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.6">
<path d="M5.97112 5.23935L9.32987 1.8806C9.42751 1.78297 9.5858 1.78297 9.68343 1.8806L10.037 2.23415C10.1346 2.33178 10.1346 2.49007 10.037 2.5877L6.67823 5.94646L10.037 9.30522C10.1346 9.40285 10.1346 9.56114 10.037 9.65877L9.68343 10.0123C9.5858 10.11 9.42751 10.11 9.32987 10.0123L5.97112 6.65357L2.61236 10.0123C2.51473 10.11 2.35644 10.11 2.25881 10.0123L1.90525 9.65877C1.80762 9.56114 1.80762 9.40285 1.90525 9.30522L5.26401 5.94646L1.90525 2.5877C1.80762 2.49007 1.80762 2.33178 1.90525 2.23415L2.25881 1.8806C2.35644 1.78297 2.51473 1.78297 2.61236 1.8806L5.97112 5.23935Z" fill="#1F2329"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 727 B

@@ -0,0 +1,5 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.6">
<path d="M5.97112 5.23935L9.32987 1.8806C9.42751 1.78297 9.5858 1.78297 9.68343 1.8806L10.037 2.23415C10.1346 2.33178 10.1346 2.49007 10.037 2.5877L6.67823 5.94646L10.037 9.30522C10.1346 9.40285 10.1346 9.56114 10.037 9.65877L9.68343 10.0123C9.5858 10.11 9.42751 10.11 9.32987 10.0123L5.97112 6.65357L2.61236 10.0123C2.51473 10.11 2.35644 10.11 2.25881 10.0123L1.90525 9.65877C1.80762 9.56114 1.80762 9.40285 1.90525 9.30522L5.26401 5.94646L1.90525 2.5877C1.80762 2.49007 1.80762 2.33178 1.90525 2.23415L2.25881 1.8806C2.35644 1.78297 2.51473 1.78297 2.61236 1.8806L5.97112 5.23935Z" fill="#1F2329"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 727 B

+23 -1
View File
@@ -358,7 +358,29 @@
"edit_row_permission": "Edit row permission",
"add_column_permission": "Add column permission",
"add_row_permission": "Add row permission",
"no_fields_yet": "No fields yet"
"no_fields_yet": "No fields yet",
"filter_eq": "Equal to",
"filter_not_eq": "Not equal to",
"filter_lt": "Less than",
"filter_le": "Less than or equal to",
"filter_gt": "Greater than",
"filter_ge": "Greater than or equal to",
"filter_null": "Empty",
"filter_not_null": "Not empty",
"filter_empty": "Empty string",
"filter_not_empty": "Not empty string",
"filter_include": "Include",
"filter_not_include": "Not included",
"conditional_filtering": "Conditional filtering",
"add_conditions": "Add conditions",
"add_relationships": "Add relationships",
"cannot_be_empty_": "Filter field cannot be empty",
"cannot_be_empty_de_ruler": "Rule condition cannot be empty",
"filter_value_can_null": "Filter value cannot be null",
"filter_like": "Contains",
"filter_not_like": "Does not contain",
"filter_in": "Belongs to",
"filter_not_in": "Does not belong to"
},
"chat": {
"type": "Chart Type",
+23 -1
View File
@@ -374,7 +374,29 @@
"edit_row_permission": "编辑行权限",
"add_column_permission": "添加列权限",
"add_row_permission": "添加行权限",
"no_fields_yet": "暂无字段"
"no_fields_yet": "暂无字段",
"filter_eq": "等于",
"filter_not_eq": "不等于",
"filter_lt": "小于",
"filter_le": "小于等于",
"filter_gt": "大于",
"filter_ge": "大于等于",
"filter_null": "为空",
"filter_not_null": "不为空",
"filter_empty": "空字符串",
"filter_not_empty": "非空字符串",
"filter_include": "包含",
"filter_not_include": "不包含",
"conditional_filtering": "条件筛选",
"add_conditions": "添加条件",
"add_relationships": "添加关系",
"cannot_be_empty_": "过滤字段不能为空",
"cannot_be_empty_de_ruler": "规则条件不能为空",
"filter_value_can_null": "过滤值不能为空",
"filter_like": "包含",
"filter_not_like": "不包含",
"filter_in": "属于",
"filter_not_in": "不属于"
},
"chat": {
"type": "图表类型",
@@ -45,7 +45,7 @@ const setUser = () => {
</div>
<div class="type-value">
<span class="type">{{ $t('permission.permission_rule') }}</span>
<span class="value"> {{ $t('permission.2', { msg: type }) }}</span>
<span class="value"> {{ $t('permission.2', { msg: num }) }}</span>
</div>
<div class="type-value">
<span class="type">{{ $t('permission.restricted_user') }}</span>
@@ -0,0 +1,220 @@
<template>
<div class="select-user_permission">
<p class="lighter-bold">{{ $t('permission.select_restricted_user') }}</p>
<div v-loading="loading" class="flex border" style="height: 428px; border-radius: 6px">
<div class="p-16 border-r">
<el-input
v-model="search"
:validate-event="false"
:placeholder="$t('datasource.search')"
style="width: 364px"
clearable
>
<template #prefix>
<el-icon>
<Search></Search>
</el-icon>
</template>
</el-input>
<div class="mt-8 max-height_workspace">
<el-checkbox
v-if="!search"
v-model="checkAll"
class="mb-8"
:indeterminate="isIndeterminate"
@change="handleCheckAllChange"
>
{{ $t('datasource.select_all') }}
</el-checkbox>
<el-checkbox-group
v-model="checkedWorkspace"
class="checkbox-group-block"
@change="handleCheckedWorkspaceChange"
>
<el-checkbox
v-for="space in workspaceWithKeywords"
:key="space.id"
:label="space.name"
:value="space"
>
<div class="flex">
<el-icon size="28">
<avatar_personal></avatar_personal>
</el-icon>
<span class="ml-4"> {{ space.name }}</span>
<span class="account">({{ space.account }})</span>
</div>
</el-checkbox>
</el-checkbox-group>
</div>
</div>
<div class="p-16 w-full">
<div class="flex-between mb-16">
<span class="lighter">
{{ $t('workspace.selected_2_people', { msg: checkedWorkspace.length }) }}
</span>
<el-button text @click="clearWorkspaceAll">
{{ $t('workspace.clear') }}
</el-button>
</div>
<div v-for="ele in checkedWorkspace" :key="ele.name" class="flex-between">
<div class="flex align-center">
<el-icon size="28">
<avatar_personal></avatar_personal>
</el-icon>
<span class="ml-4 lighter">{{ ele.name }}</span>
<span class="account">({{ ele.account }})</span>
</div>
<el-button text>
<el-icon size="16" @click="clearWorkspace(ele)"><Close /></el-icon>
</el-button>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { workspaceUserList } from '@/api/workspace'
import avatar_personal from '@/assets/svg/avatar_personal.svg'
import Close from '@/assets/svg/icon_close_outlined_w.svg'
import Search from '@/assets/svg/icon_search-outline_outlined.svg'
import type { CheckboxValueType } from 'element-plus-secondary'
const checkAll = ref(false)
const isIndeterminate = ref(false)
const checkedWorkspace = ref<any[]>([])
const workspace = ref<any[]>([])
const search = ref('')
const loading = ref(false)
const centerDialogVisible = ref(false)
const workspaceWithKeywords = computed(() => {
return workspace.value.filter((ele: any) => (ele.name as string).includes(search.value))
})
const handleCheckAllChange = (val: CheckboxValueType) => {
checkedWorkspace.value = val ? workspace.value : []
isIndeterminate.value = false
if (!val) {
clearWorkspaceAll()
}
}
const handleCheckedWorkspaceChange = (value: CheckboxValueType[]) => {
const checkedCount = value.length
checkAll.value = checkedCount === workspace.value.length
isIndeterminate.value = checkedCount > 0 && checkedCount < workspace.value.length
}
const open = async () => {
loading.value = true
checkedWorkspace.value = []
checkAll.value = false
isIndeterminate.value = false
const systemWorkspaceList = await workspaceUserList({}, 1, 1000)
workspace.value = systemWorkspaceList.items.filter((ele: any) => ele.weight !== 1) as any
loading.value = false
centerDialogVisible.value = true
}
const clearWorkspace = (val: any) => {
checkedWorkspace.value = checkedWorkspace.value.filter((ele: any) => ele.id !== val.id)
}
const clearWorkspaceAll = () => {
checkedWorkspace.value = []
handleCheckedWorkspaceChange([])
}
defineExpose({
open,
checkedWorkspace,
})
</script>
<style lang="less">
.select-user_permission {
.mb-8 {
margin-bottom: 8px;
}
.mt-16 {
margin-top: 16px;
}
.p-16 {
padding: 16px;
}
.lighter {
font-weight: 400;
font-size: 14px;
line-height: 22px;
}
.lighter-bold {
font-weight: 500;
font-size: 16px;
line-height: 24px;
margin-bottom: 16px;
}
.checkbox-group-block {
.ed-checkbox,
.ed-checkbox__label,
.flex {
width: 100%;
height: 44px;
}
.flex {
align-items: center;
.account {
color: #8f959e;
}
}
}
.border {
border: 1px solid #dee0e3;
}
.w-full {
width: 100%;
overflow-y: auto;
.flex-between {
height: 44px;
}
}
.mt-8 {
margin-top: 8px;
}
.max-height_workspace {
max-height: calc(100% - 24px);
overflow-y: auto;
}
.align-center {
align-items: center;
}
.flex-between {
display: flex;
justify-content: space-between;
}
.ml-4 {
margin-left: 4px;
}
.flex {
display: flex;
}
.border-r {
border-right: 1px solid #dee0e3;
}
}
</style>
@@ -0,0 +1,249 @@
<script lang="ts">
import icon_down_outlined from '@/assets/svg/arrow-down.svg'
import icon_close_outlined_w from '@/assets/svg/icon_close_outlined_12.svg'
import icon_add_outlined from '@/assets/svg/icon_add_outlined.svg'
export default {
name: 'LogicRelation',
}
</script>
<script lang="ts" setup>
import { useI18n } from 'vue-i18n'
import { type PropType, computed, toRefs } from 'vue'
import FilterFiled from './FilterFiled.vue'
import type { Item } from './FilterFiled.vue'
export type Logic = 'or' | 'and'
export type Relation = {
child?: Relation[]
logic: Logic
x: number
} & Item
const { t } = useI18n()
const props = defineProps({
relationList: {
type: Array as PropType<Relation[]>,
default: () => [],
},
x: {
type: Number,
default: 0,
},
logic: {
type: String as PropType<Logic>,
default: 'or',
},
})
const marginLeft = computed(() => {
return {
marginLeft: props.x ? '20px' : 0,
}
})
const emits = defineEmits([
'addCondReal',
'changeAndOrDfs',
'update:logic',
'removeRelationList',
'del',
])
const { relationList } = toRefs(props)
const handleCommand = (type: any) => {
emits('update:logic', type)
emits('changeAndOrDfs', type)
}
const removeRelationList = (index: any) => {
relationList.value.splice(index, 1)
}
const addCondReal = (type: any) => {
emits('addCondReal', type, props.logic === 'or' ? 'and' : 'or')
}
const add = (type: any, child: any, logic: any) => {
child.push(
type === 'condition'
? {
field_id: '',
value: '',
enum_value: '',
term: '',
filter_type: 'logic',
name: '',
}
: { child: [], logic }
)
}
const del = (index: any, child: any) => {
child.splice(index, 1)
}
</script>
<template>
<div class="logic" :style="marginLeft">
<div class="logic-left">
<div class="operate-title">
<span v-if="x" class="mrg-title">
{{ logic === 'or' ? 'OR' : 'AND' }}
</span>
<el-dropdown v-else trigger="click" @command="handleCommand">
<span class="mrg-title">
{{ logic === 'or' ? 'OR' : 'AND' }}
<el-icon size="12">
<icon_down_outlined />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="and">AND</el-dropdown-item>
<el-dropdown-item command="or">OR</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<span v-if="x" class="operate-icon">
<el-icon size="12" @click="emits('removeRelationList')">
<icon_close_outlined_w />
</el-icon>
</span>
</div>
<div class="logic-right">
<template v-for="(item, index) in relationList" :key="index">
<logic-relation
v-if="item.child"
:x="item.x"
:logic="item.logic"
:relation-list="item.child"
@del="(idx) => del(idx, item.child)"
@add-cond-real="(type, logic) => add(type, item.child, logic)"
@remove-relation-list="removeRelationList(index)"
>
</logic-relation>
<filter-filed v-else :item="item" :index="index" @del="emits('del', index)"></filter-filed>
</template>
<div class="logic-right-add">
<button class="operand-btn" @click="addCondReal('condition')">
<el-icon style="margin-right: 4px" size="16">
<icon_add_outlined></icon_add_outlined>
</el-icon>
{{ t('permission.add_conditions') }}
</button>
<button v-if="x < 2" class="operand-btn" @click="addCondReal('relation')">
<el-icon style="margin-right: 4px" size="16">
<icon_add_outlined></icon_add_outlined>
</el-icon>
{{ t('permission.add_relationships') }}
</button>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.logic {
display: flex;
align-items: center;
position: relative;
z-index: 1;
width: 100%;
.logic-left {
box-sizing: border-box;
width: 48px;
display: flex;
position: relative;
align-items: center;
z-index: 10;
background: #fff;
width: 48px;
align-items: center;
justify-content: center;
cursor: pointer;
&::after {
width: 100%;
height: 100%;
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #1f23291a;
z-index: 1;
border-radius: 4px;
user-select: none;
pointer-events: none;
}
.operate-title {
word-wrap: break-word;
box-sizing: border-box;
color: rgba(0, 0, 0, 0.65);
font-size: 12px;
display: inline-block;
white-space: nowrap;
margin: 0;
padding: 0;
line-height: 20px;
position: relative;
z-index: 1;
height: 20px;
color: #1f2329;
.mrg-title {
text-align: left;
box-sizing: border-box;
position: relative;
display: flex;
height: 20px;
font-size: 12px;
align-items: center;
justify-content: center;
}
}
&:hover {
.operate-icon {
display: inline-block;
}
.mrg-title {
margin-right: 0;
}
}
.operate-icon {
height: 20px;
line-height: 20px;
z-index: 1;
}
}
.logic-right-add {
display: flex;
height: 41.4px;
align-items: center;
padding-left: 26px;
.operand-btn {
box-sizing: border-box;
font-weight: 400;
text-align: center;
outline: 0;
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
cursor: pointer;
height: 32px;
padding: 0 10px;
margin-right: 10px;
font-size: 14px;
color: #1cba90;
background: #fff;
border: 1px solid #1cba90;
border-radius: 6px;
}
}
}
</style>
@@ -0,0 +1,224 @@
<script lang="ts" setup>
import icon_deleteTrash_outlined from '@/assets/svg/icon_delete.svg'
import { ref, inject, computed, onBeforeMount, toRefs, type Ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { allOptions } from '../options'
export interface Item {
term: string
field_id: string
filter_type: string
enum_value: string
name: string
value: any
}
export interface sysVariable {
label: string
value: string
type: string
}
type Props = {
index: number
item: Item
}
const props = withDefaults(defineProps<Props>(), {
index: 0,
item: () => ({
term: '',
field_id: '',
filter_type: '',
enum_value: '',
name: '',
value: null,
}),
})
const { t } = useI18n()
const showDel = ref(false)
const keywords = ref('')
const activeName = ref({})
const checklist = ref<string[]>([])
const filterList = ref<any[]>([])
const { item } = toRefs(props)
const filedList = inject('filedList') as Ref<any[]>
const computedWidth = computed(() => {
const { field_id } = item.value
return {
width: !field_id ? '270px' : '670px',
}
})
const operators = computed(() => {
return allOptions
})
const computedFiledList = computed<any[]>(() => {
return filedList.value || []
})
const dimensions = computed(() => {
if (!keywords.value) return computedFiledList.value
return computedFiledList.value.filter((ele) => ele.name.includes(keywords.value))
})
onBeforeMount(() => {
initNameEnumName()
filterListInit()
})
const initNameEnumName = () => {
const { name, enum_value, field_id } = item.value
dimensions.value.forEach((ele) => {
if (+ele.id === +field_id) {
activeName.value = ele
}
})
const arr = enum_value.trim() ? enum_value.split(',') : []
if (!name && field_id) {
checklist.value = arr
}
if (!name && !field_id) return
initEnumOptions()
checklist.value = arr
}
const filterTypeChange = () => {
item.value.term = ''
item.value.value = null
initEnumOptions()
}
const initEnumOptions = () => {
console.log('initEnumOptions')
}
const selectItem = ({ field_name, id }: any) => {
Object.assign(item.value, {
field_id: id,
name: field_name,
filter_type: 'logic',
value: '',
term: '',
})
filterListInit()
checklist.value = []
}
const filterListInit = () => {
filterList.value = [
{
value: 'logic',
label: t('permission.conditional_filtering'),
},
]
}
const emits = defineEmits(['update:item', 'del'])
</script>
<template>
<div class="white-nowrap">
<div
class="filed"
:style="computedWidth"
@mouseover="showDel = true"
@mouseleave="showDel = false"
>
<el-select
v-model="activeName"
value-key="id"
:placeholder="$t('permission.conditional_filtering')"
style="width: 200px"
@change="selectItem"
>
<el-option v-for="ele in dimensions" :key="ele.id" :label="ele.field_name" :value="ele" />
</el-select>
<template v-if="item.field_id">
<el-select
v-model="item.filter_type"
style="width: 120px; margin-left: 8px"
:placeholder="$t('permission.conditional_filtering')"
@change="filterTypeChange"
>
<el-option
v-for="ele in filterList"
:key="ele.value"
:label="ele.label"
:value="ele.value"
>
</el-option>
</el-select>
<el-input
v-model="item.value"
style="max-width: 280px; margin-left: 8px"
:placeholder="$t('datasource.please_enter')"
class="input-with-select"
>
<template #prepend>
<el-select
v-model="item.term"
style="width: 75px"
:placeholder="t('datasource.Please_select')"
>
<el-option
v-for="ele in operators"
:key="ele.value"
:label="t(ele.label)"
:value="ele.value"
>
</el-option>
</el-select>
</template>
</el-input>
</template>
<el-icon v-if="showDel" class="font16" @click="emits('del')">
<icon_deleteTrash_outlined />
</el-icon>
</div>
</div>
</template>
<style lang="less" scoped>
.white-nowrap {
white-space: nowrap;
}
.filed {
height: 41.4px;
padding: 1px 3px 1px 0;
box-sizing: border-box;
display: flex;
align-items: center;
margin-left: 20px;
min-width: 200px;
justify-content: left;
position: relative;
white-space: nowrap;
.filed-title {
word-wrap: break-word;
line-height: 28px;
color: #7e7e7e;
font-size: 14px;
white-space: nowrap;
box-sizing: border-box;
margin-right: 5px;
display: inline-block;
min-width: 50px;
text-align: right;
}
.font16 {
font-size: 16px;
margin: 0 10px;
cursor: pointer;
}
:deep(.ed-input-group__prepend) {
background-color: #fff;
}
}
</style>
@@ -0,0 +1,313 @@
<script lang="ts" setup>
import { ref, computed } from 'vue'
import AuthTree from './AuthTree.vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const errorMessage = ref('')
const logic = ref<'or' | 'and'>('or')
const relationList = ref<any[]>([])
const svgRealinePath = computed(() => {
const lg = relationList.value.length
let a = { x: 0, y: 0, child: relationList.value }
a.y = Math.floor(dfsXY(a, 0) / 2)
if (!lg) return ''
let path = calculateDepth(a)
return path
})
const svgDashinePath = computed(() => {
const lg = relationList.value.length
let a = { x: 0, y: 0, child: relationList.value }
a.y = Math.floor(dfsXY(a, 0) / 2)
if (!lg) return `M48 20 L68 20`
let path = calculateDepthDash(a)
return path
})
const init = (expressionTree: any) => {
const { logic: lg = 'or', items = [] } = expressionTree
logic.value = lg
relationList.value = dfsInit(items)
}
const submit = () => {
errorMessage.value = ''
emits('save', {
logic: logic.value,
items: dfsSubmit(relationList.value),
errorMessage: errorMessage.value,
})
}
const errorDetected = ({ filter_type, field_id, term, value }: any) => {
if (!field_id) {
errorMessage.value = t('permission.cannot_be_empty_')
return
}
if (filter_type === 'logic') {
if (!term) {
errorMessage.value = t('permission.cannot_be_empty_de_ruler')
return
}
if (!term.includes('null') && !term.includes('empty') && value === '') {
errorMessage.value = t('permission.filter_value_can_null')
return
}
}
}
const dfsInit = (arr: any[]) => {
const elementList: any[] = []
arr.forEach((ele: any) => {
const { subTree } = ele
if (subTree) {
const { items, logic } = subTree
const child = dfsInit(items)
elementList.push({ logic, child })
} else {
const { enum_value, field_id, filter_type, term, value, field } = ele
const { name } = field || {}
elementList.push({
enum_value: enum_value.join(','),
field_id,
filter_type,
term,
value,
name,
})
}
})
return elementList
}
const dfsSubmit = (arr: any[]) => {
const items: any[] = []
arr.forEach((ele) => {
const { child = [] } = ele
if (child.length) {
const { logic } = ele
const subTree = dfsSubmit(child)
items.push({
enum_value: [],
field_id: '',
filter_type: '',
term: '',
type: 'tree',
value: '',
subTree: { logic, items: subTree },
})
} else {
const { enum_value, field_id, filter_type, term, value, name } = ele
errorDetected({ enum_value, field_id, filter_type, term, value, name })
if (field_id) {
items.push({
enum_value: enum_value ? enum_value.split(',') : [],
field_id,
filter_type,
term,
value,
type: 'item',
subTree: null,
})
}
}
})
return items
}
const removeRelationList = () => {
relationList.value = []
}
const getY: any = (arr: any[]) => {
const [a] = arr
if (a.child?.length) {
return getY(a.child)
}
return a.y
}
const calculateDepthDash = (obj: any) => {
const lg = obj.child?.length
let path = ''
if (!lg && Array.isArray(obj.child)) {
const { x, y } = obj
path += `M${48 + x * 68} ${y * 41.4 + 20} L${88 + x * 68} ${y * 41.4 + 20}`
} else if (obj.child?.length) {
let y = Math.max(dfsY(obj, 0), dfs(obj.child, 0) + getY(obj.child) - 1)
let parent = (dfs(obj.child, 0) * 41.4) / 2 + (getY(obj.child) || 0) * 41.4
const { x } = obj
path += `M${24 + x * 68} ${parent} L${24 + x * 68} ${y * 41.4 + 20} L${
64 + x * 68
} ${y * 41.4 + 20}`
obj.child.forEach((item: any) => {
path += calculateDepthDash(item)
})
}
return path
}
const calculateDepth = (obj: any) => {
const lg = obj.child.length
if (!lg) return ''
let path = ''
const { x: depth, y } = obj
obj.child.forEach((item: any, index: any) => {
const { y: sibingLg, z } = item
if (item.child?.length) {
let parent = (dfs(obj.child, 0) * 41.4) / 2 + (getY(obj.child) || 0) * 41.4
let children = (dfs(item.child, 0) * 41.4) / 2 + getY(item.child) * 41.4
let path1 = 0
let path2 = 0
if (parent < children) {
path1 = parent
path2 = children
} else {
;[path1, path2] = [children, parent]
}
if (y >= sibingLg) {
path1 = parent
path2 = children
}
path += `M${24 + depth * 68} ${path1} L${24 + depth * 68} ${path2} L${
68 + depth * 68
} ${path2}`
// path += a;
path += calculateDepth(item)
}
if (!item.child?.length) {
if (sibingLg >= y) {
path += `M${24 + depth * 68} ${y * 40} L${24 + depth * 68} ${
(sibingLg + 1) * 41.4 - 20.69921875
} L${68 + depth * 68} ${(sibingLg + 1) * 41.4 - 20.69921875}`
} else {
path += `M${24 + depth * 68} ${
(sibingLg +
(lg === 1 && index === 0 ? 0 : 1) +
(obj.child[index + 1]?.child?.length ? y - sibingLg - 1 : 0)) *
41.4 +
20 +
(lg === 1 && index === 0 ? 26 : 0)
} L${24 + depth * 68} ${
(sibingLg + 1) * 41.4 - 20.69921875 - (lg === 1 && index === 0 ? (z || 0) * 1.4 : 0)
} L${68 + depth * 68} ${
(sibingLg + 1) * 41.4 - 20.69921875 - (lg === 1 && index === 0 ? (z || 0) * 1.4 : 0)
}`
}
}
})
return path
}
const changeAndOrDfs = (arr: any, logic: any) => {
arr.forEach((ele: any) => {
if (ele.child) {
ele.logic = logic === 'and' ? 'or' : 'and'
changeAndOrDfs(ele.child, ele.logic)
}
})
}
const dfs = (arr: any[], count: any) => {
arr.forEach((ele) => {
if (ele.child?.length) {
count = dfs(ele.child, count)
} else {
count += 1
}
})
count += 1
return count
}
const dfsY = (obj: any, count: any) => {
obj.child.forEach((ele: any) => {
if (ele.child?.length) {
count = dfsY(ele, count)
} else {
count = Math.max(count, ele.y, obj.y)
}
})
return count
}
const dfsXY = (obj: any, count: any) => {
obj.child.forEach((ele: any) => {
ele.x = obj.x + 1
if (ele.child?.length) {
let l = dfs(ele.child, 0)
ele.y = Math.floor(l / 2) + count
count = dfsXY(ele, count)
} else {
count += 1
ele.y = count - 1
}
})
count += 1
return count
}
const addCondReal = (type: any, logic: any) => {
relationList.value.push(
type === 'condition'
? {
field_id: '',
value: '',
enum_value: '',
term: '',
filter_type: 'logic',
name: '',
}
: { child: [], logic }
)
}
const del = (index: any) => {
relationList.value.splice(index, 1)
}
defineExpose({
init,
submit,
})
const emits = defineEmits(['save'])
</script>
<template>
<div class="rowAuth">
<auth-tree
v-model:logic="logic"
:relation-list="relationList"
@del="(idx) => del(idx)"
@add-cond-real="addCondReal"
@remove-relation-list="removeRelationList"
@change-and-or-dfs="(type) => changeAndOrDfs(relationList, type)"
/>
<svg width="388" height="100%" class="real-line">
<path
stroke-linejoin="round"
stroke-linecap="round"
:d="svgRealinePath"
fill="none"
stroke="#D9DCDF"
stroke-width="0.5"
></path>
</svg>
<svg width="388" height="100%" class="dash-line">
<path
stroke-linejoin="round"
stroke-linecap="round"
:d="svgDashinePath"
fill="none"
stroke="#D9DCDF"
stroke-width="0.5"
stroke-dasharray="4,4"
></path>
</svg>
</div>
</template>
<style lang="less" scoped>
.rowAuth {
font-family: Avenir, Helvetica, Arial, sans-serif;
text-align: center;
color: #2c3e50;
position: relative;
}
.real-line,
.dash-line {
position: absolute;
top: 0;
left: 0;
user-select: none;
}
</style>
+362 -56
View File
@@ -1,12 +1,18 @@
<script lang="ts" setup>
import { ref, computed, reactive } from 'vue'
import { ref, computed, reactive, provide, nextTick } from 'vue'
import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlined.svg'
import icon_add_outlined from '@/assets/svg/icon_add_outlined.svg'
import IconOpeEdit from '@/assets/svg/icon_edit_outlined.svg'
import IconOpeDelete from '@/assets/svg/icon_delete.svg'
import icon_close_outlined from '@/assets/svg/operate/ope-close.svg'
import icon_down_outlined from '@/assets/svg/icon_down_outlined.svg'
import Card from './Card.vue'
import SelectPermission from './SelectPermission.vue'
import AuthTree from './auth-tree/RowAuth.vue'
import { getList, savePermissions } from '@/api/permissions'
import { datasourceApi } from '@/api/datasource'
import { useI18n } from 'vue-i18n'
import { cloneDeep } from 'lodash-es'
const { t } = useI18n()
const keywords = ref('')
@@ -18,38 +24,51 @@ const termFormRef = ref()
const columnFormRef = ref()
const drawerTitle = ref('')
const dialogTitle = ref('')
const tableData = ref<any[]>([])
const ruleList = ref<any[]>([
{
id: 1,
num: '10',
type: '90',
name: 'jkjkj',
},
])
const form = reactive({
name: '',
const activeDs = ref({})
const activeTable = ref({})
const ruleList = ref<any[]>([])
const defaultPermission = {
id: '',
})
const tableColumnData = ref<any[]>([])
name: '',
permissions: [],
users: [],
}
const currentPermission = reactive<any>(cloneDeep(defaultPermission))
const searchColumn = ref('')
const columnForm = reactive({
const isCreate = ref(false)
const defaultForm = {
name: '',
id: '',
dataTable: '',
datasource: '',
})
const options = ref<any[]>([])
table_id: '',
type: 'row',
ds_id: '',
table_name: '',
ds_name: '',
permissions: [] as any[],
expression_tree: {},
}
const columnForm = reactive(cloneDeep(defaultForm))
const selectPermissionRef = ref()
const tableListOptions = ref<any[]>([])
const fieldListOptions = ref<any[]>([])
const dsListOptions = ref<any[]>([])
const ruleListWithSearch = computed(() => {
if (!keywords.value) return ruleList.value
return ruleList.value.filter((ele) =>
ele.name.toLowerCase().includes(keywords.value.toLowerCase())
)
})
const tableColumnData = computed<any[]>(() => {
if (!searchColumn.value) return columnForm.permissions
return columnForm.permissions.filter((ele) =>
ele.field_name.toLowerCase().includes(searchColumn.value.toLowerCase())
)
})
provide('filedList', fieldListOptions)
const setDrawerTitle = () => {
if (activeStep.value === 0) {
if (activeStep.value === 0 && isCreate.value) {
drawerTitle.value = t('permission.add_rule_group')
} else {
if (editRule.value === 1) {
@@ -72,39 +91,194 @@ const userTypeList = [
value: 0,
},
]
const ruleType = ref(0)
const handleAddPermission = (val: any) => {
ruleType.value = val
Object.assign(columnForm, cloneDeep(defaultForm))
if (val === 1) {
handleRowPermission()
handleRowPermission(null)
} else {
handleColumnPermission(null)
}
}
const saveAuthTree = (val: any) => {
if (val.errorMessage) {
ElMessage.error(val.errorMessage)
return
}
delete val.errorMessage
columnForm.expression_tree = cloneDeep(val)
const { expression_tree, table_id, ds_id, type, name, ds_name, table_name } = columnForm
if (columnForm.id) {
for (const key in currentPermission.permissions) {
if (currentPermission.permissions[key].id === columnForm.id) {
Object.assign(
currentPermission.permissions[key],
cloneDeep({
expression_tree,
table_id,
ds_id,
type,
name,
ds_name,
table_name,
})
)
}
}
} else {
currentPermission.permissions.push(
cloneDeep({ expression_tree, table_id, ds_id, type, name, ds_name, table_name })
)
}
dialogFormVisible.value = false
}
const getDsList = (row: any) => {
activeDs.value = {}
activeTable.value = {}
datasourceApi
.list()
.then((res: any) => {
dsListOptions.value = res || []
if (!row?.ds_id) return
dsListOptions.value.forEach((ele) => {
if (ele.id === row.ds_id) {
activeDs.value = ele
}
})
})
.finally(() => {
if (!row && columnForm.type === 'row') {
authTreeRef.value.init(columnForm.expression_tree)
}
})
const handleRowPermission = () => {
console.log('handleRowPermission')
if (row) {
handleDsIdChange(row)
handleEditeTable(row.table_id)
}
}
const handleRowPermission = (row: any) => {
columnForm.type = 'row'
getDsList(row)
if (row) {
const { name, ds_id, table_id, tree, id, ds_name, table_name } = row
Object.assign(columnForm, {
id,
name,
ds_id,
table_id,
ds_name,
table_name,
expression_tree: tree,
})
}
dialogFormVisible.value = true
dialogTitle.value = row?.id
? t('permission.edit_row_permission')
: t('permission.add_row_permission')
}
const handleColumnPermission = (row: any) => {
columnForm.type = 'column'
getDsList(row)
if (row) {
const { name, ds_id, table_id, id, permission_list, ds_name, table_name } = row
Object.assign(columnForm, {
id,
name,
ds_id,
ds_name,
table_id,
table_name,
permissions: permission_list,
})
}
dialogFormVisible.value = true
dialogTitle.value = row?.id
? t('permission.edit_column_permission')
: t('permission.add_column_permission')
}
const handleDsIdChange = (val: any) => {
columnForm.ds_id = val.id
columnForm.ds_name = val.name
datasourceApi.tableList(val.id).then((res: any) => {
tableListOptions.value = res || []
if (!columnForm.table_id) return
tableListOptions.value.forEach((ele) => {
if (ele.id === columnForm.table_id) {
activeTable.value = ele
}
})
})
}
const handleTableIdChange = (val: any) => {
columnForm.table_id = val.id
columnForm.table_name = val.name
datasourceApi.fieldList(val.id).then((res: any) => {
fieldListOptions.value = res || []
if (columnForm.type === 'row') return
columnForm.permissions = fieldListOptions.value.map((ele) => {
const { id, field_name, field_comment } = ele
return { field_id: id, field_name, field_comment, enable: true }
})
})
}
const handleEditeTable = (val: any) => {
datasourceApi
.fieldList(val)
.then((res: any) => {
fieldListOptions.value = res || []
if (columnForm.type === 'row') return
const enableMap = columnForm.permissions.reduce((pre, next) => {
pre[next.field_id] = next.enable
return pre
}, {})
columnForm.permissions = fieldListOptions.value.map((ele) => {
const { id, field_name, field_comment } = ele
return { field_id: id, field_name, field_comment, enable: enableMap[id] ?? false }
})
})
.finally(() => {
authTreeRef.value.init(columnForm.expression_tree)
})
}
const beforeClose = () => {
ruleConfigvVisible.value = false
activeStep.value = 0
isCreate.value = false
}
const handleSearch = () => {}
const editHandler = (row: any) => {
const handleSearch = () => {
getList().then((res: any) => {
ruleList.value = res || []
})
}
handleSearch()
const addHandler = () => {
editRule.value = 0
setDrawerTitle()
isCreate.value = true
Object.assign(currentPermission, cloneDeep(defaultPermission))
ruleConfigvVisible.value = true
console.log('row', row)
}
const editForm = (row: any) => {
if (row.type === 'row') {
ruleType.value = 1
handleRowPermission(row)
} else {
ruleType.value = 0
handleColumnPermission(row)
}
}
const handleEditRule = (row: any) => {
editRule.value = 1
isCreate.value = false
setDrawerTitle()
console.log(row)
Object.assign(currentPermission, cloneDeep(row))
ruleConfigvVisible.value = true
}
const deleteHandler = (row: any) => {
console.log(row)
@@ -128,8 +302,81 @@ const rules = {
const closeForm = () => {
dialogFormVisible.value = false
}
const authTreeRef = ref()
const saveHandler = () => {
columnFormRef.value.validate((res: any) => {
if (res) {
if (columnForm.type === 'row') {
authTreeRef.value.submit()
} else {
const { permissions, table_id, ds_id, type, name, ds_name, table_name } = columnForm
if (columnForm.id) {
for (const key in currentPermission.permissions) {
if (currentPermission.permissions[key].id === columnForm.id) {
Object.assign(
currentPermission.permissions[key],
cloneDeep({
permissions,
table_id,
ds_id,
type,
name,
ds_name,
table_name,
})
)
}
}
} else {
currentPermission.permissions.push(
cloneDeep({ permissions, table_id, ds_id, type, name, ds_name, table_name })
)
}
}
dialogFormVisible.value = false
}
})
}
const preview = () => {}
const next = () => {
activeStep.value = 1
nextTick(() => {
selectPermissionRef.value.open()
})
}
const savePermission = () => {
// termFormRef.value.validate((res: any) => {
// if (res) {
const saveHandler = () => {}
// }
// })
const { id, name, permissions } = currentPermission
const permissionsObj = permissions.map((ele: any) => {
return {
...cloneDeep(ele),
permissions: JSON.stringify(ele.permissions || []),
expression_tree: JSON.stringify(ele.expression_tree || {}),
}
})
const obj = {
id,
name,
permissions: permissionsObj,
users: selectPermissionRef.value.checkedWorkspace.map((ele: any) => ele.id),
}
if (!id) {
delete obj.id
}
savePermissions(obj).then(() => {
ElMessage({
type: 'success',
message: t('common.save_success'),
})
ruleConfigvVisible.value = false
handleSearch()
})
}
const columnRules = {
name: [
@@ -139,8 +386,7 @@ const columnRules = {
trigger: 'blur',
},
],
dataTable: [
table_id: [
{
required: true,
message: t('datasource.please_enter') + t('common.empty') + t('permission.rule_group_name'),
@@ -169,7 +415,7 @@ const columnRules = {
</template>
</el-input>
<el-button type="primary" @click="editHandler(null)">
<el-button type="primary" @click="addHandler()">
<template #icon>
<icon_add_outlined></icon_add_outlined>
</template>
@@ -190,8 +436,8 @@ const columnRules = {
:id="ele.id"
:key="ele.id"
:name="ele.name"
:type="ele.type"
:num="ele.num"
:type="ele.users.length"
:num="ele.permissions.length"
@edit="handleEditRule(ele)"
@del="deleteHandler(ele)"
@set-user="setUser(ele)"
@@ -223,14 +469,14 @@ const columnRules = {
</el-icon>
</template>
<div class="drawer-content">
<div v-show="activeStep === 0" class="drawer-content">
<div class="title">
{{ $t('ds.form.base_info') }}
</div>
<el-form
ref="termFormRef"
:model="form"
:model="currentPermission"
label-width="180px"
label-position="top"
:rules="rules"
@@ -238,7 +484,7 @@ const columnRules = {
>
<el-form-item prop="name" :label="t('permission.rule_group_name')">
<el-input
v-model="form.name"
v-model="currentPermission.name"
:placeholder="
$t('datasource.please_enter') +
$t('common.empty') +
@@ -287,14 +533,27 @@ const columnRules = {
<div class="table-content">
<el-table
:empty-text="$t('permission.no_rule')"
:data="tableData"
:data="currentPermission.permissions"
style="width: 100%"
>
<el-table-column prop="name" :label="$t('permission.rule_name')" />
<el-table-column prop="type" :label="$t('permission.type')" />
<el-table-column prop="datasource" :label="$t('permission.data_source')" />
<el-table-column prop="data_table" :label="$t('permission.data_table')" />
<el-table-column fixed="right" width="150" :label="$t('ds.actions')">
<el-table-column prop="type" :label="$t('permission.type')">
<template #default="scope">
{{
scope.row.type === 'row'
? $t('permission.row_permission')
: $t('permission.column_permission')
}}
</template>
</el-table-column>
<el-table-column prop="ds_name" :label="$t('permission.data_source')" />
<el-table-column prop="table_name" :label="$t('permission.data_table')" />
<el-table-column
class-name="actions-methods"
fixed="right"
width="80"
:label="$t('ds.actions')"
>
<template #default="scope">
<el-tooltip
:offset="14"
@@ -302,7 +561,7 @@ const columnRules = {
:content="$t('datasource.edit')"
placement="top"
>
<el-icon class="action-btn" size="16" @click="editHandler(scope.row)">
<el-icon class="action-btn" size="16" @click="editForm(scope.row)">
<IconOpeEdit></IconOpeEdit>
</el-icon>
</el-tooltip>
@@ -324,6 +583,21 @@ const columnRules = {
</el-form-item>
</el-form>
</div>
<div v-show="activeStep !== 0" class="select-permission_content">
<SelectPermission ref="selectPermissionRef"></SelectPermission>
</div>
<template #footer>
<el-button secondary @click="beforeClose"> {{ $t('common.cancel') }} </el-button>
<el-button v-if="activeStep === 1 && isCreate" secondary @click="preview">
{{ t('ds.previous') }}
</el-button>
<el-button v-if="activeStep === 0 && isCreate" type="primary" @click="next">
{{ t('common.next') }}
</el-button>
<el-button v-if="activeStep === 1" type="primary" @click="savePermission">
{{ $t('common.save') }}
</el-button>
</template>
</el-drawer>
<el-drawer
@@ -351,25 +625,39 @@ const columnRules = {
autocomplete="off"
/>
</el-form-item>
<el-form-item prop="dataTable" :label="t('permission.data_table')">
<el-form-item prop="table_id" :label="t('permission.data_table')">
<el-select
v-model="columnForm.datasource"
v-model="activeDs"
style="width: 416px"
value-key="id"
:placeholder="
$t('datasource.Please_select') + $t('common.empty') + $t('permission.data_source')
"
@change="handleDsIdChange"
>
<el-option v-for="item in options" :key="item.id" :label="item.name" :value="item.id" />
<el-option
v-for="item in dsListOptions"
:key="item.id"
:label="item.name"
:value="item"
/>
</el-select>
<el-select
v-model="columnForm.dataTable"
v-model="activeTable"
style="width: 416px; margin-left: auto"
:disabled="!columnForm.datasource"
:disabled="!columnForm.ds_id"
value-key="id"
:placeholder="
$t('datasource.Please_select') + $t('common.empty') + $t('permission.data_table')
"
@change="handleTableIdChange"
>
<el-option v-for="item in options" :key="item.id" :label="item.name" :value="item.id" />
<el-option
v-for="item in tableListOptions"
:key="item.id"
:label="item.table_name"
:value="item"
/>
</el-select>
</el-form-item>
<el-form-item :label="$t('permission.set_rule')">
@@ -377,6 +665,7 @@ const columnRules = {
v-model="searchColumn"
:placeholder="$t('permission.search_rule_group')"
autocomplete="off"
clearable
><template #prefix>
<el-icon>
<icon_searchOutline_outlined />
@@ -384,17 +673,18 @@ const columnRules = {
></el-input>
</el-form-item>
</el-form>
<div class="table-content">
<AuthTree v-if="ruleType === 1" ref="authTreeRef" @save="saveAuthTree"></AuthTree>
<div v-else class="table-content">
<el-table
:empty-text="$t('permission.no_fields_yet')"
:data="tableColumnData"
style="width: 100%"
>
<el-table-column prop="name" :label="$t('datasource.field_name')" />
<el-table-column prop="type" :label="$t('datasource.field_notes')" />
<el-table-column prop="field_name" :label="$t('datasource.field_name')" />
<el-table-column prop="field_comment" :label="$t('datasource.field_notes')" />
<el-table-column fixed="right" width="150" :label="$t('ds.actions')">
<template #default="scope">
<el-switch v-model="scope.row.checked" size="small" />
<el-switch v-model="scope.row.enable" size="small" />
</template>
</el-table-column>
</el-table>
@@ -445,11 +735,14 @@ const columnRules = {
margin-bottom: 16px;
}
.select-permission_content,
.drawer-content {
width: 800px;
margin: 0 auto;
height: calc(100% - 20px);
}
.drawer-content {
.add-btn:hover {
.ed-icon {
transform: rotate(180deg);
@@ -466,6 +759,7 @@ const columnRules = {
margin-top: 16px;
border: 1px solid #1f232926;
border-top: none;
border-bottom: none;
border-radius: 6px;
overflow: hidden;
max-height: calc(100vh - 400px);
@@ -473,6 +767,18 @@ const columnRules = {
padding-top: 0;
}
.actions-methods {
.cell {
height: 24px;
.action-btn {
margin-top: 4px;
&:nth-child(1) {
margin-right: 8px;
}
}
}
}
.ed-icon {
position: relative;
cursor: pointer;
@@ -0,0 +1,52 @@
function formatEnum(ele: any) {
return {
value: ele,
label: `permission.filter_${ele.replace(' ', '_')}`,
}
}
function toLine(name: any) {
return name.replace(/([A-Z])/g, '_$1').toLowerCase()
}
const textEnum = [
'eq',
'not_eq',
'in',
'not in',
'like',
'not like',
'null',
'not_null',
'empty',
'not_empty',
]
const textOptions = textEnum.map(formatEnum)
const dateEnum = ['eq', 'not_eq', 'lt', 'gt', 'le', 'ge']
const dateOptions = dateEnum.map(formatEnum)
const allEnum = [...new Set(textEnum.concat(dateEnum))]
const allOptions = allEnum.map(formatEnum)
const valueEnum = [...dateEnum]
const valueOptions = valueEnum.map(formatEnum)
const sysParams = ['eq', 'not_eq', 'like', 'not like', 'in', 'not in']
const textOptionsForSysParams = sysParams.map(formatEnum)
const sysParamsEnum = ['userId', 'userName', 'userEmail']
const sysParamsIlns = sysParamsEnum.map((_) => {
return { value: `\${sysParams.${_}}`, label: `auth.sysParams_type.${toLine(_)}`, type: 'text' }
})
const fieldEnums = ['text', 'time', 'value', 'value', 'value', 'location']
export {
textOptions,
dateOptions,
valueOptions,
textOptionsForSysParams,
sysParamsIlns,
fieldEnums,
allOptions,
}