新增部门管理功能,角色权限支持部门数据权限划分,接口权限认证加入部门数据权限字段
This commit is contained in:
parent
d16382c90c
commit
bade36dd1b
25
kinit-admin/src/api/vadmin/auth/dept.ts
Normal file
25
kinit-admin/src/api/vadmin/auth/dept.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import request from '@/config/axios'
|
||||||
|
|
||||||
|
export const getDeptListApi = (params: any): Promise<IResponse> => {
|
||||||
|
return request.get({ url: '/vadmin/auth/depts', params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const delDeptListApi = (data: any): Promise<IResponse> => {
|
||||||
|
return request.delete({ url: '/vadmin/auth/depts', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addDeptListApi = (data: any): Promise<IResponse> => {
|
||||||
|
return request.post({ url: '/vadmin/auth/depts', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const putDeptListApi = (data: any): Promise<IResponse> => {
|
||||||
|
return request.put({ url: `/vadmin/auth/depts/${data.id}`, data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDeptTreeOptionsApi = (): Promise<IResponse> => {
|
||||||
|
return request.get({ url: '/vadmin/auth/dept/tree/options' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDeptUserTreeOptionsApi = (): Promise<IResponse> => {
|
||||||
|
return request.get({ url: '/vadmin/auth/dept/user/tree/options' })
|
||||||
|
}
|
232
kinit-admin/src/views/Vadmin/Auth/Dept/Dept.vue
Normal file
232
kinit-admin/src/views/Vadmin/Auth/Dept/Dept.vue
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
<script setup lang="tsx">
|
||||||
|
import { reactive, ref, unref } from 'vue'
|
||||||
|
import {
|
||||||
|
getDeptListApi,
|
||||||
|
delDeptListApi,
|
||||||
|
addDeptListApi,
|
||||||
|
putDeptListApi
|
||||||
|
} from '@/api/vadmin/auth/dept'
|
||||||
|
import { useTable } from '@/hooks/web/useTable'
|
||||||
|
import { useI18n } from '@/hooks/web/useI18n'
|
||||||
|
import { Table, TableColumn } from '@/components/Table'
|
||||||
|
import { ElButton, ElSwitch, ElRow, ElCol } from 'element-plus'
|
||||||
|
import { ContentWrap } from '@/components/ContentWrap'
|
||||||
|
import Write from './components/Write.vue'
|
||||||
|
import { Dialog } from '@/components/Dialog'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'AuthDept'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const { tableRegister, tableState, tableMethods } = useTable({
|
||||||
|
fetchDataApi: async () => {
|
||||||
|
const { pageSize, currentPage } = tableState
|
||||||
|
const res = await getDeptListApi({
|
||||||
|
page: unref(currentPage),
|
||||||
|
limit: unref(pageSize)
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
list: res.data || [],
|
||||||
|
total: res.count || 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fetchDelApi: async (value) => {
|
||||||
|
const res = await delDeptListApi(value)
|
||||||
|
return res.code === 200
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { dataList, loading } = tableState
|
||||||
|
const { getList, delList } = tableMethods
|
||||||
|
|
||||||
|
const tableColumns = reactive<TableColumn[]>([
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
label: '部门名称',
|
||||||
|
disabled: true,
|
||||||
|
show: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'dept_key',
|
||||||
|
label: '部门标识',
|
||||||
|
show: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'owner',
|
||||||
|
label: '负责人',
|
||||||
|
show: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'phone',
|
||||||
|
label: '联系电话',
|
||||||
|
show: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'email',
|
||||||
|
label: '邮箱',
|
||||||
|
show: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'desc',
|
||||||
|
label: '描述',
|
||||||
|
show: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'order',
|
||||||
|
label: '排序',
|
||||||
|
width: '120px',
|
||||||
|
show: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'disabled',
|
||||||
|
label: '是否禁用',
|
||||||
|
width: '120px',
|
||||||
|
show: true,
|
||||||
|
slots: {
|
||||||
|
default: (data: any) => {
|
||||||
|
const row = data.row
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ElSwitch value={!row.disabled} disabled />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'action',
|
||||||
|
width: '200px',
|
||||||
|
label: '操作',
|
||||||
|
show: true,
|
||||||
|
slots: {
|
||||||
|
default: (data: any) => {
|
||||||
|
const row = data.row
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ElButton type="primary" link size="small" onClick={() => editAction(row)}>
|
||||||
|
编辑
|
||||||
|
</ElButton>
|
||||||
|
<ElButton type="primary" link size="small" onClick={() => addSonAction(row)}>
|
||||||
|
添加子部门
|
||||||
|
</ElButton>
|
||||||
|
<ElButton
|
||||||
|
type="danger"
|
||||||
|
loading={delLoading.value}
|
||||||
|
link
|
||||||
|
size="small"
|
||||||
|
onClick={() => delData(row)}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</ElButton>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const delLoading = ref(false)
|
||||||
|
|
||||||
|
const delData = async (row: any) => {
|
||||||
|
delLoading.value = true
|
||||||
|
await delList(true, [row.id]).finally(() => {
|
||||||
|
delLoading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const dialogTitle = ref('')
|
||||||
|
|
||||||
|
const currentRow = ref()
|
||||||
|
const parentId = ref(undefined)
|
||||||
|
const actionType = ref('')
|
||||||
|
|
||||||
|
const writeRef = ref<ComponentRef<typeof Write>>()
|
||||||
|
|
||||||
|
const saveLoading = ref(false)
|
||||||
|
|
||||||
|
const editAction = (row: any) => {
|
||||||
|
dialogTitle.value = '编辑'
|
||||||
|
actionType.value = 'edit'
|
||||||
|
currentRow.value = row
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const addAction = () => {
|
||||||
|
dialogTitle.value = '新增'
|
||||||
|
actionType.value = 'add'
|
||||||
|
currentRow.value = undefined
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const addSonAction = (row: any) => {
|
||||||
|
dialogTitle.value = '添加子部门'
|
||||||
|
actionType.value = 'addSon'
|
||||||
|
parentId.value = row.id
|
||||||
|
currentRow.value = undefined
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
const write = unref(writeRef)
|
||||||
|
const formData = await write?.submit()
|
||||||
|
if (formData) {
|
||||||
|
saveLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = ref({})
|
||||||
|
if (actionType.value === 'add' || actionType.value === 'addSon') {
|
||||||
|
res.value = await addDeptListApi(formData)
|
||||||
|
if (res.value) {
|
||||||
|
parentId.value = undefined
|
||||||
|
dialogVisible.value = false
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
} else if (actionType.value === 'edit') {
|
||||||
|
res.value = await putDeptListApi(formData)
|
||||||
|
if (res.value) {
|
||||||
|
dialogVisible.value = false
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
saveLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ContentWrap>
|
||||||
|
<Table
|
||||||
|
:columns="tableColumns"
|
||||||
|
showAction
|
||||||
|
default-expand-all
|
||||||
|
node-key="id"
|
||||||
|
:data="dataList"
|
||||||
|
:loading="loading"
|
||||||
|
@register="tableRegister"
|
||||||
|
@refresh="getList"
|
||||||
|
>
|
||||||
|
<template #toolbar>
|
||||||
|
<ElRow :gutter="10">
|
||||||
|
<ElCol :span="1.5">
|
||||||
|
<ElButton type="primary" @click="addAction">新增部门</ElButton>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</ContentWrap>
|
||||||
|
|
||||||
|
<Dialog v-model="dialogVisible" :title="dialogTitle">
|
||||||
|
<Write ref="writeRef" :current-row="currentRow" :parent-id="parentId" />
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<ElButton v-if="actionType !== 'detail'" type="primary" :loading="saveLoading" @click="save">
|
||||||
|
{{ t('exampleDemo.save') }}
|
||||||
|
</ElButton>
|
||||||
|
<ElButton @click="dialogVisible = false">{{ t('dialogDemo.close') }}</ElButton>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
167
kinit-admin/src/views/Vadmin/Auth/Dept/components/Write.vue
Normal file
167
kinit-admin/src/views/Vadmin/Auth/Dept/components/Write.vue
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
<script setup lang="tsx">
|
||||||
|
import { Form, FormSchema } from '@/components/Form'
|
||||||
|
import { useForm } from '@/hooks/web/useForm'
|
||||||
|
import { PropType, reactive, watch } from 'vue'
|
||||||
|
import { useValidator } from '@/hooks/web/useValidator'
|
||||||
|
import { propTypes } from '@/utils/propTypes'
|
||||||
|
import { getDeptTreeOptionsApi } from '@/api/vadmin/auth/dept'
|
||||||
|
|
||||||
|
const { required } = useValidator()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
currentRow: {
|
||||||
|
type: Object as PropType<any>,
|
||||||
|
default: () => null
|
||||||
|
},
|
||||||
|
parentId: propTypes.number.def(undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
const formSchema = reactive<FormSchema[]>([
|
||||||
|
{
|
||||||
|
field: 'parent_id',
|
||||||
|
label: '上级部门',
|
||||||
|
colProps: {
|
||||||
|
span: 24
|
||||||
|
},
|
||||||
|
component: 'TreeSelect',
|
||||||
|
componentProps: {
|
||||||
|
style: {
|
||||||
|
width: '100%'
|
||||||
|
},
|
||||||
|
checkStrictly: true,
|
||||||
|
placeholder: '请选择上级部门',
|
||||||
|
nodeKey: 'value',
|
||||||
|
defaultExpandAll: true
|
||||||
|
},
|
||||||
|
optionApi: async () => {
|
||||||
|
const res = await getDeptTreeOptionsApi()
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
value: props.parentId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
label: '部门名称',
|
||||||
|
component: 'Input',
|
||||||
|
colProps: {
|
||||||
|
span: 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'dept_key',
|
||||||
|
label: '部门标识',
|
||||||
|
component: 'Input',
|
||||||
|
colProps: {
|
||||||
|
span: 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'owner',
|
||||||
|
label: '负责人',
|
||||||
|
component: 'Input',
|
||||||
|
colProps: {
|
||||||
|
span: 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'phone',
|
||||||
|
label: '联系电话',
|
||||||
|
component: 'Input',
|
||||||
|
colProps: {
|
||||||
|
span: 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'email',
|
||||||
|
label: '邮箱',
|
||||||
|
component: 'Input',
|
||||||
|
colProps: {
|
||||||
|
span: 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'desc',
|
||||||
|
label: '描述',
|
||||||
|
component: 'Input',
|
||||||
|
colProps: {
|
||||||
|
span: 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'order',
|
||||||
|
label: '显示排序',
|
||||||
|
component: 'InputNumber',
|
||||||
|
colProps: {
|
||||||
|
span: 12
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
style: {
|
||||||
|
width: '100%'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'disabled',
|
||||||
|
label: '是否禁用',
|
||||||
|
colProps: {
|
||||||
|
span: 12
|
||||||
|
},
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
style: {
|
||||||
|
width: '100%'
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: '正常',
|
||||||
|
value: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '停用',
|
||||||
|
value: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const rules = reactive({
|
||||||
|
name: [required()],
|
||||||
|
dept_key: [required()],
|
||||||
|
disabled: [required()],
|
||||||
|
order: [required()]
|
||||||
|
})
|
||||||
|
|
||||||
|
const { formRegister, formMethods } = useForm()
|
||||||
|
const { setValues, getFormData, getElFormExpose } = formMethods
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
const elForm = await getElFormExpose()
|
||||||
|
const valid = await elForm?.validate()
|
||||||
|
if (valid) {
|
||||||
|
const formData = await getFormData()
|
||||||
|
return formData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.currentRow,
|
||||||
|
(currentRow) => {
|
||||||
|
if (!currentRow) return
|
||||||
|
setValues(currentRow)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deep: true,
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
submit
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Form :rules="rules" @register="formRegister" :schema="formSchema" :labelWidth="100" />
|
||||||
|
</template>
|
@ -15,7 +15,10 @@ import { Search } from '@/components/Search'
|
|||||||
import { FormSchema } from '@/components/Form'
|
import { FormSchema } from '@/components/Form'
|
||||||
import { ContentWrap } from '@/components/ContentWrap'
|
import { ContentWrap } from '@/components/ContentWrap'
|
||||||
import Write from './components/Write.vue'
|
import Write from './components/Write.vue'
|
||||||
|
import AuthManage from './components/AuthManage.vue'
|
||||||
import { Dialog } from '@/components/Dialog'
|
import { Dialog } from '@/components/Dialog'
|
||||||
|
import { DictDetail, selectDictLabel } from '@/utils/dict'
|
||||||
|
import { useDictStore } from '@/store/modules/dict'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'AuthRole'
|
name: 'AuthRole'
|
||||||
@ -45,12 +48,22 @@ const { tableRegister, tableState, tableMethods } = useTable({
|
|||||||
const { dataList, loading, total, pageSize, currentPage } = tableState
|
const { dataList, loading, total, pageSize, currentPage } = tableState
|
||||||
const { getList, delList } = tableMethods
|
const { getList, delList } = tableMethods
|
||||||
|
|
||||||
|
let dataRangeOptions = ref<DictDetail[]>([])
|
||||||
|
|
||||||
|
const getOptions = async () => {
|
||||||
|
const dictStore = useDictStore()
|
||||||
|
const dictOptions = await dictStore.getDictObj(['sys_vadmin_data_range'])
|
||||||
|
dataRangeOptions.value = dictOptions.sys_vadmin_data_range
|
||||||
|
}
|
||||||
|
|
||||||
|
getOptions()
|
||||||
|
|
||||||
const tableColumns = reactive<TableColumn[]>([
|
const tableColumns = reactive<TableColumn[]>([
|
||||||
{
|
{
|
||||||
field: 'id',
|
field: 'id',
|
||||||
label: '角色编号',
|
label: '角色编号',
|
||||||
show: true,
|
show: false,
|
||||||
disabled: true
|
disabled: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
@ -63,6 +76,21 @@ const tableColumns = reactive<TableColumn[]>([
|
|||||||
label: '权限字符',
|
label: '权限字符',
|
||||||
show: true
|
show: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'data_range',
|
||||||
|
label: '数据范围',
|
||||||
|
show: true,
|
||||||
|
slots: {
|
||||||
|
default: (data: any) => {
|
||||||
|
const row = data.row
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>{selectDictLabel(unref(dataRangeOptions), row.data_range.toString())}</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: 'order',
|
field: 'order',
|
||||||
label: '显示顺序',
|
label: '显示顺序',
|
||||||
@ -92,7 +120,7 @@ const tableColumns = reactive<TableColumn[]>([
|
|||||||
const row = data.row
|
const row = data.row
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ElSwitch value={!row.is_admin} disabled />
|
<ElSwitch value={row.is_admin} disabled />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -105,7 +133,7 @@ const tableColumns = reactive<TableColumn[]>([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'action',
|
field: 'action',
|
||||||
width: '150px',
|
width: '170px',
|
||||||
label: '操作',
|
label: '操作',
|
||||||
show: true,
|
show: true,
|
||||||
slots: {
|
slots: {
|
||||||
@ -125,6 +153,15 @@ const tableColumns = reactive<TableColumn[]>([
|
|||||||
>
|
>
|
||||||
编辑
|
编辑
|
||||||
</ElButton>
|
</ElButton>
|
||||||
|
<ElButton
|
||||||
|
v-show={row.id !== 1}
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
size="small"
|
||||||
|
onClick={() => authManageActive(row)}
|
||||||
|
>
|
||||||
|
权限管理
|
||||||
|
</ElButton>
|
||||||
<ElButton
|
<ElButton
|
||||||
v-show={row.id !== 1}
|
v-show={row.id !== 1}
|
||||||
type="danger"
|
type="danger"
|
||||||
@ -204,6 +241,18 @@ const delData = async (row: any) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const authManageRef = ref<ComponentRef<typeof AuthManage>>()
|
||||||
|
|
||||||
|
// 权限管理
|
||||||
|
const authManageActive = async (row: any) => {
|
||||||
|
const res = await getRoleApi(row.id)
|
||||||
|
if (res) {
|
||||||
|
res.data.data_range = res.data.data_range.toString()
|
||||||
|
currentRow.value = res.data
|
||||||
|
authManageRef.value?.openDrawer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const dialogTitle = ref('')
|
const dialogTitle = ref('')
|
||||||
|
|
||||||
@ -217,15 +266,16 @@ const saveLoading = ref(false)
|
|||||||
const editAction = async (row: any) => {
|
const editAction = async (row: any) => {
|
||||||
const res = await getRoleApi(row.id)
|
const res = await getRoleApi(row.id)
|
||||||
if (res) {
|
if (res) {
|
||||||
dialogTitle.value = '编辑'
|
dialogTitle.value = '编辑角色'
|
||||||
actionType.value = 'edit'
|
actionType.value = 'edit'
|
||||||
|
res.data.data_range = res.data.data_range.toString()
|
||||||
currentRow.value = res.data
|
currentRow.value = res.data
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const addAction = () => {
|
const addAction = () => {
|
||||||
dialogTitle.value = '新增'
|
dialogTitle.value = '新增角色'
|
||||||
actionType.value = 'add'
|
actionType.value = 'add'
|
||||||
currentRow.value = undefined
|
currentRow.value = undefined
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
@ -298,4 +348,6 @@ const save = async () => {
|
|||||||
<ElButton @click="dialogVisible = false">{{ t('dialogDemo.close') }}</ElButton>
|
<ElButton @click="dialogVisible = false">{{ t('dialogDemo.close') }}</ElButton>
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<AuthManage ref="authManageRef" :current-row="currentRow" @get-list="getList" />
|
||||||
</template>
|
</template>
|
||||||
|
255
kinit-admin/src/views/Vadmin/Auth/Role/components/AuthManage.vue
Normal file
255
kinit-admin/src/views/Vadmin/Auth/Role/components/AuthManage.vue
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
ElDrawer,
|
||||||
|
ElButton,
|
||||||
|
ElDivider,
|
||||||
|
ElSelect,
|
||||||
|
ElOption,
|
||||||
|
ElTree,
|
||||||
|
ElContainer,
|
||||||
|
ElHeader,
|
||||||
|
ElAside,
|
||||||
|
ElMain,
|
||||||
|
ElMessage
|
||||||
|
} from 'element-plus'
|
||||||
|
import { ref, nextTick, unref, PropType, watch } from 'vue'
|
||||||
|
import { Icon } from '@/components/Icon'
|
||||||
|
import { useDictStore } from '@/store/modules/dict'
|
||||||
|
import { getMenuRoleTreeOptionsApi } from '@/api/vadmin/auth/menu'
|
||||||
|
import { getDeptUserTreeOptionsApi } from '@/api/vadmin/auth/dept'
|
||||||
|
import { eachTree } from '@/utils/tree'
|
||||||
|
import { isEmptyVal } from '@/utils/is'
|
||||||
|
import { putRoleListApi } from '@/api/vadmin/auth/role'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
currentRow: {
|
||||||
|
type: Object as PropType<any>,
|
||||||
|
default: () => null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['getList'])
|
||||||
|
|
||||||
|
const data = ref({} as Recordable)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.currentRow,
|
||||||
|
(currentRow) => {
|
||||||
|
if (!currentRow) return
|
||||||
|
data.value = JSON.parse(JSON.stringify(currentRow))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deep: true,
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const drawerVisible = ref(false)
|
||||||
|
|
||||||
|
const dataRangeOptions = ref()
|
||||||
|
const getOptions = async () => {
|
||||||
|
const dictStore = useDictStore()
|
||||||
|
const dictOptions = await dictStore.getDictObj(['sys_vadmin_data_range'])
|
||||||
|
dataRangeOptions.value = dictOptions.sys_vadmin_data_range
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
children: 'children',
|
||||||
|
label: 'label'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取部门树
|
||||||
|
let deptTreeData = ref([] as any[])
|
||||||
|
const deptTreeRef = ref<InstanceType<typeof ElTree>>()
|
||||||
|
const getDeptTreeOptions = async () => {
|
||||||
|
const res = await getDeptUserTreeOptionsApi()
|
||||||
|
if (res) {
|
||||||
|
deptTreeData.value = res.data
|
||||||
|
await nextTick()
|
||||||
|
if (props.currentRow) {
|
||||||
|
const dept_ids: number[] = props.currentRow.depts.map((item) => item.id)
|
||||||
|
const checked: number[] = []
|
||||||
|
// 递归按顺序添加选中的菜单项,用于处理半选状态的菜单项
|
||||||
|
eachTree(res.data, (v) => {
|
||||||
|
if (dept_ids.includes(v.value)) {
|
||||||
|
checked.push(v.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
for (const item of checked) {
|
||||||
|
unref(deptTreeRef)?.setChecked(item, true, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取菜单树
|
||||||
|
let menuTreeData = ref([] as any[])
|
||||||
|
const menuTreeRef = ref<InstanceType<typeof ElTree>>()
|
||||||
|
const getMenuRoleTreeOptions = async () => {
|
||||||
|
const res = await getMenuRoleTreeOptionsApi()
|
||||||
|
if (res) {
|
||||||
|
menuTreeData.value = res.data
|
||||||
|
await nextTick()
|
||||||
|
if (props.currentRow) {
|
||||||
|
const menu_ids: number[] = props.currentRow.menus.map((item) => item.id)
|
||||||
|
const checked: number[] = []
|
||||||
|
// 递归按顺序添加选中的菜单项,用于处理半选状态的菜单项
|
||||||
|
eachTree(res.data, (v) => {
|
||||||
|
if (menu_ids.includes(v.value)) {
|
||||||
|
checked.push(v.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
for (const item of checked) {
|
||||||
|
unref(menuTreeRef)?.setChecked(item, true, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
if (loading.value) return
|
||||||
|
if (isEmptyVal(data.value.data_range)) {
|
||||||
|
ElMessage.error('数据范围选择项不能为空!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
const menu_ids = [
|
||||||
|
...(unref(menuTreeRef)?.getCheckedKeys() || []),
|
||||||
|
...(unref(menuTreeRef)?.getHalfCheckedKeys() || [])
|
||||||
|
]
|
||||||
|
data.value.menu_ids = menu_ids
|
||||||
|
data.value.dept_ids = unref(deptTreeRef)?.getCheckedKeys()
|
||||||
|
try {
|
||||||
|
const res = await putRoleListApi(data.value)
|
||||||
|
if (res) {
|
||||||
|
loading.value = false
|
||||||
|
ElMessage.success('保存成功')
|
||||||
|
closeDrawer()
|
||||||
|
emit('getList')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openDrawer = () => {
|
||||||
|
drawerVisible.value = true
|
||||||
|
getMenuRoleTreeOptions()
|
||||||
|
getDeptTreeOptions()
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeDrawer = () => {
|
||||||
|
drawerVisible.value = false
|
||||||
|
data.value = {}
|
||||||
|
unref(menuTreeRef)?.setCheckedKeys([])
|
||||||
|
unref(deptTreeRef)?.setCheckedKeys([])
|
||||||
|
}
|
||||||
|
|
||||||
|
getOptions()
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
openDrawer
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="auth-manage-main-view">
|
||||||
|
<ElDrawer v-model="drawerVisible" :with-header="false" :size="1000" :before-close="closeDrawer">
|
||||||
|
<ElContainer>
|
||||||
|
<ElHeader>
|
||||||
|
<div class="flex justify-between pt-[20px] pb-[20px]">
|
||||||
|
<span>权限管理</span>
|
||||||
|
<span @click="closeDrawer" class="flex cursor-pointer">
|
||||||
|
<Icon icon="iconamoon:close-thin" :size="23" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</ElHeader>
|
||||||
|
<ElDivider />
|
||||||
|
<div class="h-12 flex justify-between mt-3 mr-3 ml-5">
|
||||||
|
<div class="mt-1 text-[#909399]">
|
||||||
|
<span>角色名称:{{ data.name }}</span>
|
||||||
|
</div>
|
||||||
|
<ElButton type="primary" :loading="loading" @click="submit">保存</ElButton>
|
||||||
|
</div>
|
||||||
|
<ElDivider />
|
||||||
|
<ElContainer>
|
||||||
|
<ElAside width="450px">
|
||||||
|
<div class="border-r-1 border-r-[#f0f0f0] b-r-solid h-[100%] p-[20px] box-border">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="yxt-divider"></div>
|
||||||
|
<span>数据权限</span>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4 mt-3">
|
||||||
|
<ElSelect v-model="data.data_range" placeholder="请选择数据范围">
|
||||||
|
<ElOption
|
||||||
|
v-for="item in dataRangeOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</ElSelect>
|
||||||
|
<div
|
||||||
|
v-if="data.data_range === '4'"
|
||||||
|
class="mt-3 max-h-[65vh] b-1 b-solid b-[#e5e7eb] p-10px overflow-auto"
|
||||||
|
>
|
||||||
|
<ElTree
|
||||||
|
ref="deptTreeRef"
|
||||||
|
:data="deptTreeData"
|
||||||
|
show-checkbox
|
||||||
|
node-key="value"
|
||||||
|
:props="defaultProps"
|
||||||
|
:default-expand-all="true"
|
||||||
|
:check-strictly="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ElAside>
|
||||||
|
<ElMain>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="yxt-divider"></div>
|
||||||
|
<span>菜单权限</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 max-h-[70vh] b-1 b-solid b-[#e5e7eb] p-10px overflow-auto box-border">
|
||||||
|
<ElTree
|
||||||
|
ref="menuTreeRef"
|
||||||
|
:data="menuTreeData"
|
||||||
|
show-checkbox
|
||||||
|
node-key="value"
|
||||||
|
:props="defaultProps"
|
||||||
|
:default-expand-all="true"
|
||||||
|
:check-strictly="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ElMain>
|
||||||
|
</ElContainer>
|
||||||
|
</ElContainer>
|
||||||
|
<ElDivider />
|
||||||
|
|
||||||
|
<!-- <ElDivider /> -->
|
||||||
|
</ElDrawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.auth-manage-main-view .el-drawer .el-drawer__body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.auth-manage-main-view .el-divider.el-divider--horizontal {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.yxt-divider {
|
||||||
|
background: #409eff;
|
||||||
|
width: 8px;
|
||||||
|
height: 20px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,11 +1,11 @@
|
|||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { Form, FormSchema } from '@/components/Form'
|
import { Form, FormSchema } from '@/components/Form'
|
||||||
import { useForm } from '@/hooks/web/useForm'
|
import { useForm } from '@/hooks/web/useForm'
|
||||||
import { PropType, nextTick, reactive, ref, unref, watch } from 'vue'
|
import { PropType, reactive, watch } from 'vue'
|
||||||
import { useValidator } from '@/hooks/web/useValidator'
|
import { useValidator } from '@/hooks/web/useValidator'
|
||||||
import { ElCheckbox, ElTree } from 'element-plus'
|
// import { ElTree } from 'element-plus'
|
||||||
import { getMenuRoleTreeOptionsApi } from '@/api/vadmin/auth/menu'
|
// import { getMenuRoleTreeOptionsApi } from '@/api/vadmin/auth/menu'
|
||||||
import { eachTree } from '@/utils/tree'
|
// import { eachTree } from '@/utils/tree'
|
||||||
|
|
||||||
const { required } = useValidator()
|
const { required } = useValidator()
|
||||||
|
|
||||||
@ -16,39 +16,38 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let treeData = ref([] as any[])
|
// let treeData = ref([] as any[])
|
||||||
|
// const treeRef = ref<InstanceType<typeof ElTree>>()
|
||||||
|
|
||||||
const treeRef = ref<InstanceType<typeof ElTree>>()
|
// const getMenuRoleTreeOptions = async () => {
|
||||||
|
// const res = await getMenuRoleTreeOptionsApi()
|
||||||
|
// if (res) {
|
||||||
|
// treeData.value = res.data
|
||||||
|
// await nextTick()
|
||||||
|
// if (props.currentRow) {
|
||||||
|
// const menu_ids: number[] = props.currentRow.menus.map((item) => item.id)
|
||||||
|
// const checked: number[] = []
|
||||||
|
// // 递归按顺序添加选中的菜单项,用于处理半选状态的菜单项
|
||||||
|
// eachTree(res.data, (v) => {
|
||||||
|
// if (menu_ids.includes(v.value)) {
|
||||||
|
// checked.push(v.value)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// for (const item of checked) {
|
||||||
|
// unref(treeRef)?.setChecked(item, true, false)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
const getMenuRoleTreeOptions = async () => {
|
// const defaultProps = {
|
||||||
const res = await getMenuRoleTreeOptionsApi()
|
// children: 'children',
|
||||||
if (res) {
|
// label: 'label'
|
||||||
treeData.value = res.data
|
// }
|
||||||
await nextTick()
|
|
||||||
if (props.currentRow) {
|
|
||||||
const menu_ids: number[] = props.currentRow.menus.map((item) => item.id)
|
|
||||||
const checked: number[] = []
|
|
||||||
// 递归按顺序添加选中的菜单项,用于处理半选状态的菜单项
|
|
||||||
eachTree(res.data, (v) => {
|
|
||||||
if (menu_ids.includes(v.value)) {
|
|
||||||
checked.push(v.value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
for (const item of checked) {
|
|
||||||
unref(treeRef)?.setChecked(item, true, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultProps = {
|
// let selectAll = ref(false)
|
||||||
children: 'children',
|
// let defaultExpandAll = ref(true)
|
||||||
label: 'label'
|
// let checkStrictly = ref(true)
|
||||||
}
|
|
||||||
|
|
||||||
let selectAll = ref(false)
|
|
||||||
let defaultExpandAll = ref(true)
|
|
||||||
let checkStrictly = ref(true)
|
|
||||||
|
|
||||||
// 获取所有节点的key
|
// 获取所有节点的key
|
||||||
const getTreeNodeKeys = (nodes: Recordable[]): number[] => {
|
const getTreeNodeKeys = (nodes: Recordable[]): number[] => {
|
||||||
@ -62,19 +61,19 @@ const getTreeNodeKeys = (nodes: Recordable[]): number[] => {
|
|||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
// 展开/折叠
|
// // 展开/折叠
|
||||||
const handleCheckedTreeExpand = (value: boolean) => {
|
// const handleCheckedTreeExpand = (value: boolean) => {
|
||||||
defaultExpandAll.value = value
|
// defaultExpandAll.value = value
|
||||||
for (let i = 0; i < treeData.value.length; i++) {
|
// for (let i = 0; i < treeData.value.length; i++) {
|
||||||
treeRef.value!.store.nodesMap[treeData.value[i].value].expanded = value
|
// treeRef.value!.store.nodesMap[treeData.value[i].value].expanded = value
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
//全选/全不选
|
// //全选/全不选
|
||||||
const handleCheckedTreeNodeAll = (value: boolean) => {
|
// const handleCheckedTreeNodeAll = (value: boolean) => {
|
||||||
selectAll.value = value
|
// selectAll.value = value
|
||||||
treeRef.value!.setCheckedKeys(value ? getTreeNodeKeys(treeData.value) : [])
|
// treeRef.value!.setCheckedKeys(value ? getTreeNodeKeys(treeData.value) : [])
|
||||||
}
|
// }
|
||||||
|
|
||||||
const formSchema = reactive<FormSchema[]>([
|
const formSchema = reactive<FormSchema[]>([
|
||||||
{
|
{
|
||||||
@ -156,57 +155,64 @@ const formSchema = reactive<FormSchema[]>([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'desc',
|
field: 'desc',
|
||||||
label: '描述',
|
label: '角色描述',
|
||||||
colProps: {
|
|
||||||
span: 12
|
|
||||||
},
|
|
||||||
component: 'Input'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'menu_ids',
|
|
||||||
label: '菜单权限',
|
|
||||||
colProps: {
|
colProps: {
|
||||||
span: 24
|
span: 24
|
||||||
},
|
},
|
||||||
formItemProps: {
|
component: 'Input',
|
||||||
slots: {
|
componentProps: {
|
||||||
default: () => {
|
rows: 4,
|
||||||
return (
|
type: 'textarea',
|
||||||
<>
|
style: {
|
||||||
<div>
|
width: '600px'
|
||||||
<div>
|
|
||||||
<ElCheckbox
|
|
||||||
modelValue={defaultExpandAll.value}
|
|
||||||
onChange={handleCheckedTreeExpand}
|
|
||||||
label="展开/折叠"
|
|
||||||
size="large"
|
|
||||||
/>
|
|
||||||
<ElCheckbox
|
|
||||||
modelValue={selectAll.value}
|
|
||||||
onChange={handleCheckedTreeNodeAll}
|
|
||||||
label="全选/全不选"
|
|
||||||
size="large"
|
|
||||||
/>
|
|
||||||
<ElCheckbox v-model={checkStrictly.value} label="父子联动" size="large" />
|
|
||||||
</div>
|
|
||||||
<div class="max-h-420px b-1 b-solid b-[#e5e7eb] p-10px overflow-auto">
|
|
||||||
<ElTree
|
|
||||||
ref={treeRef}
|
|
||||||
data={treeData.value}
|
|
||||||
show-checkbox
|
|
||||||
node-key="value"
|
|
||||||
props={defaultProps}
|
|
||||||
default-expand-all={defaultExpandAll.value}
|
|
||||||
check-strictly={!checkStrictly.value}
|
|
||||||
></ElTree>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// {
|
||||||
|
// field: 'menu_ids',
|
||||||
|
// label: '菜单权限',
|
||||||
|
// colProps: {
|
||||||
|
// span: 24
|
||||||
|
// },
|
||||||
|
// formItemProps: {
|
||||||
|
// slots: {
|
||||||
|
// default: () => {
|
||||||
|
// return (
|
||||||
|
// <>
|
||||||
|
// <div>
|
||||||
|
// <div>
|
||||||
|
// <ElCheckbox
|
||||||
|
// modelValue={defaultExpandAll.value}
|
||||||
|
// onChange={handleCheckedTreeExpand}
|
||||||
|
// label="展开/折叠"
|
||||||
|
// size="large"
|
||||||
|
// />
|
||||||
|
// <ElCheckbox
|
||||||
|
// modelValue={selectAll.value}
|
||||||
|
// onChange={handleCheckedTreeNodeAll}
|
||||||
|
// label="全选/全不选"
|
||||||
|
// size="large"
|
||||||
|
// />
|
||||||
|
// <ElCheckbox v-model={checkStrictly.value} label="父子联动" size="large" />
|
||||||
|
// </div>
|
||||||
|
// <div class="max-h-420px b-1 b-solid b-[#e5e7eb] p-10px overflow-auto">
|
||||||
|
// <ElTree
|
||||||
|
// ref={treeRef}
|
||||||
|
// data={treeData.value}
|
||||||
|
// show-checkbox
|
||||||
|
// node-key="value"
|
||||||
|
// props={defaultProps}
|
||||||
|
// default-expand-all={defaultExpandAll.value}
|
||||||
|
// check-strictly={!checkStrictly.value}
|
||||||
|
// ></ElTree>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </>
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
])
|
])
|
||||||
|
|
||||||
const rules = reactive({
|
const rules = reactive({
|
||||||
@ -223,10 +229,10 @@ const submit = async () => {
|
|||||||
const valid = await elForm?.validate()
|
const valid = await elForm?.validate()
|
||||||
if (valid) {
|
if (valid) {
|
||||||
const formData = await getFormData()
|
const formData = await getFormData()
|
||||||
formData.menu_ids = [
|
// formData.menu_ids = [
|
||||||
...(unref(treeRef)?.getCheckedKeys() || []),
|
// ...(unref(treeRef)?.getCheckedKeys() || []),
|
||||||
...(unref(treeRef)?.getHalfCheckedKeys() || [])
|
// ...(unref(treeRef)?.getHalfCheckedKeys() || [])
|
||||||
]
|
// ]
|
||||||
return formData
|
return formData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -243,7 +249,7 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
getMenuRoleTreeOptions()
|
// getMenuRoleTreeOptions()
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
submit
|
submit
|
||||||
|
@ -84,8 +84,8 @@ const tableColumns = reactive<TableColumn[]>([
|
|||||||
field: 'id',
|
field: 'id',
|
||||||
label: '用户编号',
|
label: '用户编号',
|
||||||
width: '100px',
|
width: '100px',
|
||||||
show: true,
|
show: false,
|
||||||
disabled: true
|
disabled: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
@ -113,7 +113,7 @@ const tableColumns = reactive<TableColumn[]>([
|
|||||||
{
|
{
|
||||||
field: 'gender',
|
field: 'gender',
|
||||||
label: '性别',
|
label: '性别',
|
||||||
show: true,
|
show: false,
|
||||||
slots: {
|
slots: {
|
||||||
default: (data: any) => {
|
default: (data: any) => {
|
||||||
const row = data.row
|
const row = data.row
|
||||||
@ -140,10 +140,25 @@ const tableColumns = reactive<TableColumn[]>([
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'depts',
|
||||||
|
label: '部门',
|
||||||
|
show: true,
|
||||||
|
slots: {
|
||||||
|
default: (data: any) => {
|
||||||
|
const row = data.row
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="text-truncate">{row.depts.map((item) => item.name).join()}</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: 'is_active',
|
field: 'is_active',
|
||||||
label: '是否可用',
|
label: '是否可用',
|
||||||
show: true,
|
show: false,
|
||||||
slots: {
|
slots: {
|
||||||
default: (data: any) => {
|
default: (data: any) => {
|
||||||
const row = data.row
|
const row = data.row
|
||||||
@ -180,7 +195,7 @@ const tableColumns = reactive<TableColumn[]>([
|
|||||||
field: 'create_datetime',
|
field: 'create_datetime',
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
width: '190px',
|
width: '190px',
|
||||||
show: true
|
show: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'action',
|
field: 'action',
|
||||||
@ -325,6 +340,7 @@ const editAction = async (row: any) => {
|
|||||||
if (res) {
|
if (res) {
|
||||||
dialogTitle.value = '编辑用户'
|
dialogTitle.value = '编辑用户'
|
||||||
res.data.role_ids = res.data.roles.map((item: any) => item.id)
|
res.data.role_ids = res.data.roles.map((item: any) => item.id)
|
||||||
|
res.data.dept_ids = res.data.depts.map((item: any) => item.id)
|
||||||
actionType.value = 'edit'
|
actionType.value = 'edit'
|
||||||
currentRow.value = res.data
|
currentRow.value = res.data
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
|
@ -4,6 +4,7 @@ import { useForm } from '@/hooks/web/useForm'
|
|||||||
import { PropType, reactive, watch } from 'vue'
|
import { PropType, reactive, watch } from 'vue'
|
||||||
import { useValidator } from '@/hooks/web/useValidator'
|
import { useValidator } from '@/hooks/web/useValidator'
|
||||||
import { getRoleOptionsApi } from '@/api/vadmin/auth/role'
|
import { getRoleOptionsApi } from '@/api/vadmin/auth/role'
|
||||||
|
import { getDeptUserTreeOptionsApi } from '@/api/vadmin/auth/dept'
|
||||||
|
|
||||||
const { required, isTelephone, isEmail } = useValidator()
|
const { required, isTelephone, isEmail } = useValidator()
|
||||||
|
|
||||||
@ -173,6 +174,28 @@ const formSchema = reactive<FormSchema[]>([
|
|||||||
},
|
},
|
||||||
value: [],
|
value: [],
|
||||||
ifshow: (values) => values.is_staff
|
ifshow: (values) => values.is_staff
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'dept_ids',
|
||||||
|
label: '部门',
|
||||||
|
colProps: {
|
||||||
|
span: 24
|
||||||
|
},
|
||||||
|
component: 'TreeSelect',
|
||||||
|
componentProps: {
|
||||||
|
style: {
|
||||||
|
width: '100%'
|
||||||
|
},
|
||||||
|
multiple: true,
|
||||||
|
checkStrictly: true,
|
||||||
|
defaultExpandAll: true
|
||||||
|
},
|
||||||
|
optionApi: async () => {
|
||||||
|
const res = await getDeptUserTreeOptionsApi()
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
value: [],
|
||||||
|
ifshow: (values) => values.is_staff
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -181,6 +204,7 @@ const rules = reactive({
|
|||||||
is_active: [required()],
|
is_active: [required()],
|
||||||
is_staff: [required()],
|
is_staff: [required()],
|
||||||
role_ids: [required()],
|
role_ids: [required()],
|
||||||
|
dept_ids: [required()],
|
||||||
telephone: [required(), { validator: isTelephone, trigger: 'blur' }],
|
telephone: [required(), { validator: isTelephone, trigger: 'blur' }],
|
||||||
email: [{ validator: isEmail, trigger: 'blur' }]
|
email: [{ validator: isEmail, trigger: 'blur' }]
|
||||||
})
|
})
|
||||||
|
@ -11,7 +11,7 @@ from fastapi.security import OAuth2PasswordBearer
|
|||||||
"""
|
"""
|
||||||
系统版本
|
系统版本
|
||||||
"""
|
"""
|
||||||
VERSION = "3.4.2"
|
VERSION = "3.5.0"
|
||||||
|
|
||||||
"""安全警告: 不要在生产中打开调试运行!"""
|
"""安全警告: 不要在生产中打开调试运行!"""
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
|
@ -50,6 +50,33 @@ class UserDal(DalBase):
|
|||||||
self.model = models.VadminUser
|
self.model = models.VadminUser
|
||||||
self.schema = schemas.UserSimpleOut
|
self.schema = schemas.UserSimpleOut
|
||||||
|
|
||||||
|
async def recursion_get_dept_ids(
|
||||||
|
self,
|
||||||
|
user: models.VadminUser,
|
||||||
|
depts: list[models.VadminDept] = None,
|
||||||
|
dept_ids: list[int] = None
|
||||||
|
) -> list:
|
||||||
|
"""
|
||||||
|
递归获取所有关联部门 id
|
||||||
|
:param user:
|
||||||
|
:param depts: 所有部门实例
|
||||||
|
:param dept_ids: 父级部门 id 列表
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if not depts:
|
||||||
|
depts = await DeptDal(self.db).get_datas(limit=0, v_return_objs=True)
|
||||||
|
result = []
|
||||||
|
for i in user.depts:
|
||||||
|
result.append(i.id)
|
||||||
|
result.extend(await self.recursion_get_dept_ids(user, depts, result))
|
||||||
|
return list(set(result))
|
||||||
|
elif dept_ids:
|
||||||
|
result = [i.id for i in filter(lambda item: item.parent_id in dept_ids, depts)]
|
||||||
|
result.extend(await self.recursion_get_dept_ids(user, depts, result))
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
async def update_login_info(self, user: models.VadminUser, last_ip: str) -> None:
|
async def update_login_info(self, user: models.VadminUser, last_ip: str) -> None:
|
||||||
"""
|
"""
|
||||||
更新当前登录信息
|
更新当前登录信息
|
||||||
@ -82,11 +109,15 @@ class UserDal(DalBase):
|
|||||||
password = data.telephone[5:12] if settings.DEFAULT_PASSWORD == "0" else settings.DEFAULT_PASSWORD
|
password = data.telephone[5:12] if settings.DEFAULT_PASSWORD == "0" else settings.DEFAULT_PASSWORD
|
||||||
data.password = self.model.get_password_hash(password)
|
data.password = self.model.get_password_hash(password)
|
||||||
data.avatar = data.avatar if data.avatar else settings.DEFAULT_AVATAR
|
data.avatar = data.avatar if data.avatar else settings.DEFAULT_AVATAR
|
||||||
obj = self.model(**data.model_dump(exclude={'role_ids'}))
|
obj = self.model(**data.model_dump(exclude={'role_ids', "dept_ids"}))
|
||||||
if data.role_ids:
|
if data.role_ids:
|
||||||
roles = await RoleDal(self.db).get_datas(limit=0, id=("in", data.role_ids), v_return_objs=True)
|
roles = await RoleDal(self.db).get_datas(limit=0, id=("in", data.role_ids), v_return_objs=True)
|
||||||
for role in roles:
|
for role in roles:
|
||||||
obj.roles.add(role)
|
obj.roles.add(role)
|
||||||
|
if data.dept_ids:
|
||||||
|
depts = await DeptDal(self.db).get_datas(limit=0, id=("in", data.dept_ids), v_return_objs=True)
|
||||||
|
for dept in depts:
|
||||||
|
obj.depts.add(dept)
|
||||||
await self.flush(obj)
|
await self.flush(obj)
|
||||||
return await self.out_dict(obj, v_options, v_return_obj, v_schema)
|
return await self.out_dict(obj, v_options, v_return_obj, v_schema)
|
||||||
|
|
||||||
@ -118,6 +149,14 @@ class UserDal(DalBase):
|
|||||||
for role in roles:
|
for role in roles:
|
||||||
obj.roles.add(role)
|
obj.roles.add(role)
|
||||||
continue
|
continue
|
||||||
|
elif key == "dept_ids":
|
||||||
|
if value:
|
||||||
|
depts = await DeptDal(self.db).get_datas(limit=0, id=("in", value), v_return_objs=True)
|
||||||
|
if obj.depts:
|
||||||
|
obj.depts.clear()
|
||||||
|
for dept in depts:
|
||||||
|
obj.depts.add(dept)
|
||||||
|
continue
|
||||||
setattr(obj, key, value)
|
setattr(obj, key, value)
|
||||||
await self.flush(obj)
|
await self.flush(obj)
|
||||||
return await self.out_dict(obj, None, v_return_obj, v_schema)
|
return await self.out_dict(obj, None, v_return_obj, v_schema)
|
||||||
@ -395,11 +434,15 @@ class RoleDal(DalBase):
|
|||||||
:param v_schema:
|
:param v_schema:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
obj = self.model(**data.model_dump(exclude={'menu_ids'}))
|
obj = self.model(**data.model_dump(exclude={'menu_ids', 'dept_ids'}))
|
||||||
if data.menu_ids:
|
if data.menu_ids:
|
||||||
menus = await MenuDal(db=self.db).get_datas(limit=0, id=("in", data.menu_ids), v_return_objs=True)
|
menus = await MenuDal(db=self.db).get_datas(limit=0, id=("in", data.menu_ids), v_return_objs=True)
|
||||||
for menu in menus:
|
for menu in menus:
|
||||||
obj.menus.add(menu)
|
obj.menus.add(menu)
|
||||||
|
if data.dept_ids:
|
||||||
|
depts = await DeptDal(db=self.db).get_datas(limit=0, id=("in", data.dept_ids), v_return_objs=True)
|
||||||
|
for dept in depts:
|
||||||
|
obj.depts.add(dept)
|
||||||
await self.flush(obj)
|
await self.flush(obj)
|
||||||
return await self.out_dict(obj, v_options, v_return_obj, v_schema)
|
return await self.out_dict(obj, v_options, v_return_obj, v_schema)
|
||||||
|
|
||||||
@ -420,7 +463,7 @@ class RoleDal(DalBase):
|
|||||||
:param v_schema:
|
:param v_schema:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
obj = await self.get_data(data_id, v_options=[joinedload(self.model.menus)])
|
obj = await self.get_data(data_id, v_options=[joinedload(self.model.menus), joinedload(self.model.depts)])
|
||||||
obj_dict = jsonable_encoder(data)
|
obj_dict = jsonable_encoder(data)
|
||||||
for key, value in obj_dict.items():
|
for key, value in obj_dict.items():
|
||||||
if key == "menu_ids":
|
if key == "menu_ids":
|
||||||
@ -431,6 +474,14 @@ class RoleDal(DalBase):
|
|||||||
for menu in menus:
|
for menu in menus:
|
||||||
obj.menus.add(menu)
|
obj.menus.add(menu)
|
||||||
continue
|
continue
|
||||||
|
elif key == "dept_ids":
|
||||||
|
if value:
|
||||||
|
depts = await DeptDal(db=self.db).get_datas(limit=0, id=("in", value), v_return_objs=True)
|
||||||
|
if obj.depts:
|
||||||
|
obj.depts.clear()
|
||||||
|
for dept in depts:
|
||||||
|
obj.depts.add(dept)
|
||||||
|
continue
|
||||||
setattr(obj, key, value)
|
setattr(obj, key, value)
|
||||||
await self.flush(obj)
|
await self.flush(obj)
|
||||||
return await self.out_dict(obj, None, v_return_obj, v_schema)
|
return await self.out_dict(obj, None, v_return_obj, v_schema)
|
||||||
@ -559,7 +610,7 @@ class MenuDal(DalBase):
|
|||||||
"""
|
"""
|
||||||
data = []
|
data = []
|
||||||
for root in nodes:
|
for root in nodes:
|
||||||
router = schemas.TreeListOut.model_validate(root)
|
router = schemas.MenuTreeListOut.model_validate(root)
|
||||||
if root.menu_type == "0" or root.menu_type == "1":
|
if root.menu_type == "0" or root.menu_type == "1":
|
||||||
sons = filter(lambda i: i.parent_id == root.id, menus)
|
sons = filter(lambda i: i.parent_id == root.id, menus)
|
||||||
router.children = self.generate_tree_list(menus, sons)
|
router.children = self.generate_tree_list(menus, sons)
|
||||||
@ -611,3 +662,103 @@ class MenuDal(DalBase):
|
|||||||
raise CustomException("无法删除存在角色关联的菜单", code=400)
|
raise CustomException("无法删除存在角色关联的菜单", code=400)
|
||||||
await super(MenuDal, self).delete_datas(ids, v_soft, **kwargs)
|
await super(MenuDal, self).delete_datas(ids, v_soft, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class DeptDal(DalBase):
|
||||||
|
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
super(DeptDal, self).__init__()
|
||||||
|
self.db = db
|
||||||
|
self.model = models.VadminDept
|
||||||
|
self.schema = schemas.DeptSimpleOut
|
||||||
|
|
||||||
|
async def get_tree_list(self, mode: int) -> list:
|
||||||
|
"""
|
||||||
|
1:获取部门树列表
|
||||||
|
2:获取部门树选择项,添加/修改部门时使用
|
||||||
|
3:获取部门树列表,用户添加部门权限时使用
|
||||||
|
:param mode:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if mode == 3:
|
||||||
|
sql = select(self.model).where(self.model.disabled == 0, self.model.is_delete == false())
|
||||||
|
else:
|
||||||
|
sql = select(self.model).where(self.model.is_delete == false())
|
||||||
|
queryset = await self.db.scalars(sql)
|
||||||
|
datas = list(queryset.all())
|
||||||
|
roots = filter(lambda i: not i.parent_id, datas)
|
||||||
|
if mode == 1:
|
||||||
|
menus = self.generate_tree_list(datas, roots)
|
||||||
|
elif mode == 2 or mode == 3:
|
||||||
|
menus = self.generate_tree_options(datas, roots)
|
||||||
|
else:
|
||||||
|
raise CustomException("获取部门失败,无可用选项", code=400)
|
||||||
|
return self.dept_order(menus)
|
||||||
|
|
||||||
|
def generate_tree_list(self, depts: list[models.VadminDept], nodes: filter) -> list:
|
||||||
|
"""
|
||||||
|
生成部门树列表
|
||||||
|
:param depts: 总部门列表
|
||||||
|
:param nodes: 每层节点部门列表
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
data = []
|
||||||
|
for root in nodes:
|
||||||
|
router = schemas.DeptTreeListOut.model_validate(root)
|
||||||
|
sons = filter(lambda i: i.parent_id == root.id, depts)
|
||||||
|
router.children = self.generate_tree_list(depts, sons)
|
||||||
|
data.append(router.model_dump())
|
||||||
|
return data
|
||||||
|
|
||||||
|
def generate_tree_options(self, depts: list[models.VadminDept], nodes: filter) -> list:
|
||||||
|
"""
|
||||||
|
生成部门树选择项
|
||||||
|
:param depts: 总部门列表
|
||||||
|
:param nodes: 每层节点部门列表
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
data = []
|
||||||
|
for root in nodes:
|
||||||
|
router = {"value": root.id, "label": root.name, "order": root.order}
|
||||||
|
sons = filter(lambda i: i.parent_id == root.id, depts)
|
||||||
|
router["children"] = self.generate_tree_options(depts, sons)
|
||||||
|
data.append(router)
|
||||||
|
return data
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def dept_order(cls, datas: list, order: str = "order", children: str = "children") -> list:
|
||||||
|
"""
|
||||||
|
部门排序
|
||||||
|
:param datas:
|
||||||
|
:param order:
|
||||||
|
:param children:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
result = sorted(datas, key=lambda dept: dept[order])
|
||||||
|
for item in result:
|
||||||
|
if item[children]:
|
||||||
|
item[children] = sorted(item[children], key=lambda dept: dept[order])
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class TestDal(DalBase):
|
||||||
|
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
super(TestDal, self).__init__(db, models.VadminUser, schemas.UserSimpleOut)
|
||||||
|
|
||||||
|
async def test(self):
|
||||||
|
# print("-----------------------开始------------------------")
|
||||||
|
options = [joinedload(self.model.roles)]
|
||||||
|
v_join = [[self.model.roles]]
|
||||||
|
v_where = [self.model.id == 1, models.VadminRole.id == 1]
|
||||||
|
v_start_sql = select(self.model)
|
||||||
|
result, count = await self.get_datas(
|
||||||
|
v_start_sql=v_start_sql,
|
||||||
|
v_join=v_join,
|
||||||
|
v_options=options,
|
||||||
|
v_where=v_where,
|
||||||
|
v_return_count=True
|
||||||
|
)
|
||||||
|
if result:
|
||||||
|
print(result)
|
||||||
|
print(count)
|
||||||
|
# print("-----------------------结束------------------------")
|
||||||
|
@ -7,7 +7,8 @@
|
|||||||
# @desc : 简要说明
|
# @desc : 简要说明
|
||||||
|
|
||||||
|
|
||||||
from .m2m import vadmin_auth_user_roles, vadmin_auth_role_menus
|
from .m2m import vadmin_auth_user_roles, vadmin_auth_role_menus, vadmin_auth_user_depts, vadmin_auth_role_depts
|
||||||
from .menu import VadminMenu
|
from .menu import VadminMenu
|
||||||
from .role import VadminRole
|
from .role import VadminRole
|
||||||
from .user import VadminUser
|
from .user import VadminUser
|
||||||
|
from .dept import VadminDept
|
||||||
|
31
kinit-api/apps/vadmin/auth/models/dept.py
Normal file
31
kinit-api/apps/vadmin/auth/models/dept.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# @version : 1.0
|
||||||
|
# @Create Time : 2023/10/23 13:41
|
||||||
|
# @File : dept.py
|
||||||
|
# @IDE : PyCharm
|
||||||
|
# @desc : 部门模型
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
from db.db_base import BaseModel
|
||||||
|
from sqlalchemy import String, Boolean, Integer, ForeignKey
|
||||||
|
|
||||||
|
|
||||||
|
class VadminDept(BaseModel):
|
||||||
|
__tablename__ = "vadmin_auth_dept"
|
||||||
|
__table_args__ = ({'comment': '部门表'})
|
||||||
|
|
||||||
|
name: Mapped[str] = mapped_column(String(50), index=True, nullable=False, comment="部门名称")
|
||||||
|
dept_key: Mapped[str] = mapped_column(String(50), index=True, nullable=False, comment="部门标识")
|
||||||
|
disabled: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否禁用")
|
||||||
|
order: Mapped[int | None] = mapped_column(Integer, comment="显示排序")
|
||||||
|
desc: Mapped[str | None] = mapped_column(String(255), comment="描述")
|
||||||
|
owner: Mapped[str | None] = mapped_column(String(255), comment="负责人")
|
||||||
|
phone: Mapped[str | None] = mapped_column(String(255), comment="联系电话")
|
||||||
|
email: Mapped[str | None] = mapped_column(String(255), comment="邮箱")
|
||||||
|
|
||||||
|
parent_id: Mapped[int | None] = mapped_column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("vadmin_auth_dept.id", ondelete='CASCADE'),
|
||||||
|
comment="上级部门"
|
||||||
|
)
|
@ -25,3 +25,17 @@ vadmin_auth_role_menus = Table(
|
|||||||
Column("menu_id", Integer, ForeignKey("vadmin_auth_menu.id", ondelete="CASCADE")),
|
Column("menu_id", Integer, ForeignKey("vadmin_auth_menu.id", ondelete="CASCADE")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
vadmin_auth_user_depts = Table(
|
||||||
|
"vadmin_auth_user_depts",
|
||||||
|
Base.metadata,
|
||||||
|
Column("user_id", Integer, ForeignKey("vadmin_auth_user.id", ondelete="CASCADE")),
|
||||||
|
Column("dept_id", Integer, ForeignKey("vadmin_auth_dept.id", ondelete="CASCADE")),
|
||||||
|
)
|
||||||
|
|
||||||
|
vadmin_auth_role_depts = Table(
|
||||||
|
"vadmin_auth_role_depts",
|
||||||
|
Base.metadata,
|
||||||
|
Column("role_id", Integer, ForeignKey("vadmin_auth_role.id", ondelete="CASCADE")),
|
||||||
|
Column("dept_id", Integer, ForeignKey("vadmin_auth_dept.id", ondelete="CASCADE")),
|
||||||
|
)
|
||||||
|
|
||||||
|
@ -10,18 +10,21 @@ from sqlalchemy.orm import relationship, Mapped, mapped_column
|
|||||||
from db.db_base import BaseModel
|
from db.db_base import BaseModel
|
||||||
from sqlalchemy import String, Boolean, Integer
|
from sqlalchemy import String, Boolean, Integer
|
||||||
from .menu import VadminMenu
|
from .menu import VadminMenu
|
||||||
from .m2m import vadmin_auth_role_menus
|
from .dept import VadminDept
|
||||||
|
from .m2m import vadmin_auth_role_menus, vadmin_auth_role_depts
|
||||||
|
|
||||||
|
|
||||||
class VadminRole(BaseModel):
|
class VadminRole(BaseModel):
|
||||||
__tablename__ = "vadmin_auth_role"
|
__tablename__ = "vadmin_auth_role"
|
||||||
__table_args__ = ({'comment': '角色表'})
|
__table_args__ = ({'comment': '角色表'})
|
||||||
|
|
||||||
name: Mapped[str] = mapped_column(String(50), index=True, nullable=False, comment="名称")
|
name: Mapped[str] = mapped_column(String(50), index=True, comment="名称")
|
||||||
role_key: Mapped[str] = mapped_column(String(50), index=True, nullable=False, comment="权限字符")
|
role_key: Mapped[str] = mapped_column(String(50), index=True, comment="权限字符")
|
||||||
|
data_range: Mapped[int] = mapped_column(Integer, default=4, comment="数据权限范围")
|
||||||
disabled: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否禁用")
|
disabled: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否禁用")
|
||||||
order: Mapped[int | None] = mapped_column(Integer, comment="排序")
|
order: Mapped[int | None] = mapped_column(Integer, comment="排序")
|
||||||
desc: Mapped[str | None] = mapped_column(String(255), comment="描述")
|
desc: Mapped[str | None] = mapped_column(String(255), comment="描述")
|
||||||
is_admin: Mapped[bool] = mapped_column(Boolean, comment="是否为超级角色", default=False)
|
is_admin: Mapped[bool] = mapped_column(Boolean, comment="是否为超级角色", default=False)
|
||||||
|
|
||||||
menus: Mapped[set[VadminMenu]] = relationship(secondary=vadmin_auth_role_menus)
|
menus: Mapped[set[VadminMenu]] = relationship(secondary=vadmin_auth_role_menus)
|
||||||
|
depts: Mapped[set[VadminDept]] = relationship(secondary=vadmin_auth_role_depts)
|
||||||
|
@ -12,7 +12,8 @@ from db.db_base import BaseModel
|
|||||||
from sqlalchemy import String, Boolean, DateTime
|
from sqlalchemy import String, Boolean, DateTime
|
||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
from .role import VadminRole
|
from .role import VadminRole
|
||||||
from .m2m import vadmin_auth_user_roles
|
from .dept import VadminDept
|
||||||
|
from .m2m import vadmin_auth_user_roles, vadmin_auth_user_depts
|
||||||
|
|
||||||
pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto')
|
pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto')
|
||||||
|
|
||||||
@ -41,6 +42,7 @@ class VadminUser(BaseModel):
|
|||||||
is_wx_server_openid: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否已有服务端微信平台openid")
|
is_wx_server_openid: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否已有服务端微信平台openid")
|
||||||
|
|
||||||
roles: Mapped[set[VadminRole]] = relationship(secondary=vadmin_auth_user_roles)
|
roles: Mapped[set[VadminRole]] = relationship(secondary=vadmin_auth_user_roles)
|
||||||
|
depts: Mapped[set[VadminDept]] = relationship(secondary=vadmin_auth_user_depts)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_password_hash(password: str) -> str:
|
def get_password_hash(password: str) -> str:
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
from .user import UserParams
|
from .user import UserParams
|
||||||
from .role import RoleParams
|
from .role import RoleParams
|
||||||
|
from .dept import DeptParams
|
||||||
|
31
kinit-api/apps/vadmin/auth/params/dept.py
Normal file
31
kinit-api/apps/vadmin/auth/params/dept.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# @version : 1.0
|
||||||
|
# @Create Time : 2023/12/18 10:19
|
||||||
|
# @File : dept.py
|
||||||
|
# @IDE : PyCharm
|
||||||
|
# @desc : 查询参数-类依赖项
|
||||||
|
|
||||||
|
"""
|
||||||
|
类依赖项-官方文档:https://fastapi.tiangolo.com/zh/tutorial/dependencies/classes-as-dependencies/
|
||||||
|
"""
|
||||||
|
from fastapi import Depends, Query
|
||||||
|
from core.dependencies import Paging, QueryParams
|
||||||
|
|
||||||
|
|
||||||
|
class DeptParams(QueryParams):
|
||||||
|
"""
|
||||||
|
列表分页
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str | None = Query(None, title="部门名称"),
|
||||||
|
dept_key: str | None = Query(None, title="部门标识"),
|
||||||
|
disabled: bool | None = Query(None, title="是否禁用"),
|
||||||
|
params: Paging = Depends()
|
||||||
|
):
|
||||||
|
super().__init__(params)
|
||||||
|
self.name = ("like", name)
|
||||||
|
self.dept_key = ("like", dept_key)
|
||||||
|
self.disabled = disabled
|
@ -1,3 +1,4 @@
|
|||||||
from .user import UserOut, UserUpdate, User, UserIn, UserSimpleOut, ResetPwd, UserUpdateBaseInfo
|
from .user import UserOut, UserUpdate, User, UserIn, UserSimpleOut, ResetPwd, UserUpdateBaseInfo
|
||||||
from .role import Role, RoleOut, RoleIn, RoleOptionsOut, RoleSimpleOut
|
from .role import Role, RoleOut, RoleIn, RoleOptionsOut, RoleSimpleOut
|
||||||
from .menu import Menu, MenuSimpleOut, RouterOut, Meta, TreeListOut
|
from .menu import Menu, MenuSimpleOut, RouterOut, Meta, MenuTreeListOut
|
||||||
|
from .dept import Dept, DeptSimpleOut, DeptTreeListOut
|
||||||
|
40
kinit-api/apps/vadmin/auth/schemas/dept.py
Normal file
40
kinit-api/apps/vadmin/auth/schemas/dept.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# @version : 1.0
|
||||||
|
# @Create Time : 2023/10/25 12:19
|
||||||
|
# @File : dept.py
|
||||||
|
# @IDE : PyCharm
|
||||||
|
# @desc : pydantic 模型,用于数据库序列化操作
|
||||||
|
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
from core.data_types import DatetimeStr
|
||||||
|
from .menu import MenuSimpleOut
|
||||||
|
|
||||||
|
|
||||||
|
class Dept(BaseModel):
|
||||||
|
name: str
|
||||||
|
dept_key: str
|
||||||
|
disabled: bool = False
|
||||||
|
order: int | None = None
|
||||||
|
desc: str | None = None
|
||||||
|
owner: str | None = None
|
||||||
|
phone: str | None = None
|
||||||
|
email: str | None = None
|
||||||
|
|
||||||
|
parent_id: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class DeptSimpleOut(Dept):
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
id: int
|
||||||
|
create_datetime: DatetimeStr
|
||||||
|
update_datetime: DatetimeStr
|
||||||
|
|
||||||
|
|
||||||
|
class DeptTreeListOut(DeptSimpleOut):
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
children: list[dict] = []
|
||||||
|
|
@ -60,7 +60,7 @@ class RouterOut(BaseModel):
|
|||||||
children: list[dict] = []
|
children: list[dict] = []
|
||||||
|
|
||||||
|
|
||||||
class TreeListOut(MenuSimpleOut):
|
class MenuTreeListOut(MenuSimpleOut):
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
children: list[dict] = []
|
children: list[dict] = []
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
from core.data_types import DatetimeStr
|
from core.data_types import DatetimeStr
|
||||||
from .menu import MenuSimpleOut
|
from .menu import MenuSimpleOut
|
||||||
|
from .dept import DeptSimpleOut
|
||||||
|
|
||||||
|
|
||||||
class Role(BaseModel):
|
class Role(BaseModel):
|
||||||
@ -17,6 +18,7 @@ class Role(BaseModel):
|
|||||||
disabled: bool = False
|
disabled: bool = False
|
||||||
order: int | None = None
|
order: int | None = None
|
||||||
desc: str | None = None
|
desc: str | None = None
|
||||||
|
data_range: int = 4
|
||||||
role_key: str
|
role_key: str
|
||||||
is_admin: bool = False
|
is_admin: bool = False
|
||||||
|
|
||||||
@ -33,10 +35,12 @@ class RoleOut(RoleSimpleOut):
|
|||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
menus: list[MenuSimpleOut] = []
|
menus: list[MenuSimpleOut] = []
|
||||||
|
depts: list[DeptSimpleOut] = []
|
||||||
|
|
||||||
|
|
||||||
class RoleIn(Role):
|
class RoleIn(Role):
|
||||||
menu_ids: list[int] = []
|
menu_ids: list[int] = []
|
||||||
|
dept_ids: list[int] = []
|
||||||
|
|
||||||
|
|
||||||
class RoleOptionsOut(BaseModel):
|
class RoleOptionsOut(BaseModel):
|
||||||
|
@ -9,9 +9,9 @@
|
|||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, field_validator
|
from pydantic import BaseModel, ConfigDict, field_validator
|
||||||
from pydantic_core.core_schema import FieldValidationInfo
|
from pydantic_core.core_schema import FieldValidationInfo
|
||||||
|
|
||||||
from core.data_types import Telephone, DatetimeStr, Email
|
from core.data_types import Telephone, DatetimeStr, Email
|
||||||
from .role import RoleSimpleOut
|
from .role import RoleSimpleOut
|
||||||
|
from .dept import DeptSimpleOut
|
||||||
|
|
||||||
|
|
||||||
class User(BaseModel):
|
class User(BaseModel):
|
||||||
@ -31,6 +31,7 @@ class UserIn(User):
|
|||||||
创建用户
|
创建用户
|
||||||
"""
|
"""
|
||||||
role_ids: list[int] = []
|
role_ids: list[int] = []
|
||||||
|
dept_ids: list[int] = []
|
||||||
password: str | None = ""
|
password: str | None = ""
|
||||||
|
|
||||||
|
|
||||||
@ -58,6 +59,7 @@ class UserUpdate(User):
|
|||||||
is_staff: bool | None = False
|
is_staff: bool | None = False
|
||||||
gender: str | None = "0"
|
gender: str | None = "0"
|
||||||
role_ids: list[int] = []
|
role_ids: list[int] = []
|
||||||
|
dept_ids: list[int] = []
|
||||||
|
|
||||||
|
|
||||||
class UserSimpleOut(User):
|
class UserSimpleOut(User):
|
||||||
@ -76,6 +78,7 @@ class UserOut(UserSimpleOut):
|
|||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
roles: list[RoleSimpleOut] = []
|
roles: list[RoleSimpleOut] = []
|
||||||
|
depts: list[DeptSimpleOut] = []
|
||||||
|
|
||||||
|
|
||||||
class ResetPwd(BaseModel):
|
class ResetPwd(BaseModel):
|
||||||
|
@ -40,7 +40,7 @@ class OpenAuth(AuthValidation):
|
|||||||
try:
|
try:
|
||||||
telephone = self.validate_token(request, token)
|
telephone = self.validate_token(request, token)
|
||||||
user = await UserDal(db).get_data(telephone=telephone, v_return_none=True)
|
user = await UserDal(db).get_data(telephone=telephone, v_return_none=True)
|
||||||
return await self.validate_user(request, user, db)
|
return await self.validate_user(request, user, db, is_all=True)
|
||||||
except CustomException:
|
except CustomException:
|
||||||
return Auth(db=db)
|
return Auth(db=db)
|
||||||
|
|
||||||
@ -65,7 +65,7 @@ class AllUserAuth(AuthValidation):
|
|||||||
return Auth(db=db)
|
return Auth(db=db)
|
||||||
telephone = self.validate_token(request, token)
|
telephone = self.validate_token(request, token)
|
||||||
user = await UserDal(db).get_data(telephone=telephone, v_return_none=True)
|
user = await UserDal(db).get_data(telephone=telephone, v_return_none=True)
|
||||||
return await self.validate_user(request, user, db)
|
return await self.validate_user(request, user, db, is_all=True)
|
||||||
|
|
||||||
|
|
||||||
class FullAdminAuth(AuthValidation):
|
class FullAdminAuth(AuthValidation):
|
||||||
@ -94,9 +94,9 @@ class FullAdminAuth(AuthValidation):
|
|||||||
if not settings.OAUTH_ENABLE:
|
if not settings.OAUTH_ENABLE:
|
||||||
return Auth(db=db)
|
return Auth(db=db)
|
||||||
telephone = self.validate_token(request, token)
|
telephone = self.validate_token(request, token)
|
||||||
options = [joinedload(VadminUser.roles).subqueryload(VadminRole.menus)]
|
options = [joinedload(VadminUser.roles).subqueryload(VadminRole.menus), joinedload(VadminUser.depts)]
|
||||||
user = await UserDal(db).get_data(telephone=telephone, v_return_none=True, v_options=options, is_staff=True)
|
user = await UserDal(db).get_data(telephone=telephone, v_return_none=True, v_options=options, is_staff=True)
|
||||||
result = await self.validate_user(request, user, db)
|
result = await self.validate_user(request, user, db, is_all=False)
|
||||||
permissions = self.get_user_permissions(user)
|
permissions = self.get_user_permissions(user)
|
||||||
if permissions != {'*.*.*'} and self.permissions:
|
if permissions != {'*.*.*'} and self.permissions:
|
||||||
if not (self.permissions & permissions):
|
if not (self.permissions & permissions):
|
||||||
|
@ -14,11 +14,14 @@ from apps.vadmin.auth.models import VadminUser
|
|||||||
from core.exception import CustomException
|
from core.exception import CustomException
|
||||||
from utils import status
|
from utils import status
|
||||||
from datetime import timedelta, datetime
|
from datetime import timedelta, datetime
|
||||||
|
from apps.vadmin.auth.crud import UserDal
|
||||||
|
|
||||||
|
|
||||||
class Auth(BaseModel):
|
class Auth(BaseModel):
|
||||||
user: VadminUser = None
|
user: VadminUser = None
|
||||||
db: AsyncSession
|
db: AsyncSession
|
||||||
|
data_range: int | None = None
|
||||||
|
dept_ids: list | None = []
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
# 接收任意类型
|
# 接收任意类型
|
||||||
@ -80,9 +83,14 @@ class AuthValidation:
|
|||||||
return telephone
|
return telephone
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def validate_user(cls, request: Request, user: VadminUser, db: AsyncSession) -> Auth:
|
async def validate_user(cls, request: Request, user: VadminUser, db: AsyncSession, is_all: bool = True) -> Auth:
|
||||||
"""
|
"""
|
||||||
验证用户信息
|
验证用户信息
|
||||||
|
:param request:
|
||||||
|
:param user:
|
||||||
|
:param db:
|
||||||
|
:param is_all: 是否所有人访问,不加权限
|
||||||
|
:return:
|
||||||
"""
|
"""
|
||||||
if user is None:
|
if user is None:
|
||||||
raise CustomException(msg="未认证,请您重新登陆", code=cls.error_code, status_code=cls.error_code)
|
raise CustomException(msg="未认证,请您重新登陆", code=cls.error_code, status_code=cls.error_code)
|
||||||
@ -95,12 +103,17 @@ class AuthValidation:
|
|||||||
request.scope["body"] = await request.body()
|
request.scope["body"] = await request.body()
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
request.scope["body"] = "获取失败"
|
request.scope["body"] = "获取失败"
|
||||||
return Auth(user=user, db=db)
|
if is_all:
|
||||||
|
return Auth(user=user, db=db)
|
||||||
|
data_range, dept_ids = await cls.get_user_data_range(user, db)
|
||||||
|
return Auth(user=user, db=db, data_range=data_range, dept_ids=dept_ids)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_user_permissions(cls, user: VadminUser) -> set:
|
def get_user_permissions(cls, user: VadminUser) -> set:
|
||||||
"""
|
"""
|
||||||
获取员工用户所有权限列表
|
获取员工用户所有权限列表
|
||||||
|
:param user: 用户实例
|
||||||
|
:return:
|
||||||
"""
|
"""
|
||||||
if user.is_admin():
|
if user.is_admin():
|
||||||
return {'*.*.*'}
|
return {'*.*.*'}
|
||||||
@ -110,3 +123,36 @@ class AuthValidation:
|
|||||||
if menu.perms and not menu.disabled:
|
if menu.perms and not menu.disabled:
|
||||||
permissions.add(menu.perms)
|
permissions.add(menu.perms)
|
||||||
return permissions
|
return permissions
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_user_data_range(cls, user: VadminUser, db: AsyncSession) -> tuple:
|
||||||
|
"""
|
||||||
|
获取用户数据范围
|
||||||
|
0 仅本人数据权限 create_user_id 查询
|
||||||
|
1 本部门数据权限 部门 id 左连接查询
|
||||||
|
2 本部门及以下数据权限 部门 id 左连接查询
|
||||||
|
3 自定义数据权限 部门 id 左连接查询
|
||||||
|
4 全部数据权限 无
|
||||||
|
:param user:
|
||||||
|
:param db:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if user.is_admin():
|
||||||
|
return 4, ["*"]
|
||||||
|
data_range = max([i.data_range for i in user.roles])
|
||||||
|
dept_ids = set()
|
||||||
|
if data_range == 0:
|
||||||
|
pass
|
||||||
|
elif data_range == 1:
|
||||||
|
for dept in user.depts:
|
||||||
|
dept_ids.add(dept.id)
|
||||||
|
elif data_range == 2:
|
||||||
|
# 递归获取部门列表
|
||||||
|
dept_ids = await UserDal(db).recursion_get_dept_ids(user)
|
||||||
|
elif data_range == 3:
|
||||||
|
for role_obj in user.roles:
|
||||||
|
for dept in role_obj.depts:
|
||||||
|
dept_ids.add(dept.id)
|
||||||
|
elif data_range == 4:
|
||||||
|
dept_ids.add("*")
|
||||||
|
return data_range, list(dept_ids)
|
||||||
|
@ -13,13 +13,22 @@ from core.database import redis_getter
|
|||||||
from utils.response import SuccessResponse, ErrorResponse
|
from utils.response import SuccessResponse, ErrorResponse
|
||||||
from . import schemas, crud, models
|
from . import schemas, crud, models
|
||||||
from core.dependencies import IdList
|
from core.dependencies import IdList
|
||||||
from apps.vadmin.auth.utils.current import AllUserAuth, FullAdminAuth
|
from apps.vadmin.auth.utils.current import AllUserAuth, FullAdminAuth, OpenAuth
|
||||||
from apps.vadmin.auth.utils.validation.auth import Auth
|
from apps.vadmin.auth.utils.validation.auth import Auth
|
||||||
from .params import UserParams, RoleParams
|
from .params import UserParams, RoleParams, DeptParams
|
||||||
|
|
||||||
app = APIRouter()
|
app = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
###########################################################
|
||||||
|
# 接口测试
|
||||||
|
###########################################################
|
||||||
|
@app.get("/test", summary="接口测试")
|
||||||
|
async def test(auth: Auth = Depends(FullAdminAuth())):
|
||||||
|
print(auth)
|
||||||
|
return SuccessResponse()
|
||||||
|
|
||||||
|
|
||||||
###########################################################
|
###########################################################
|
||||||
# 用户管理
|
# 用户管理
|
||||||
###########################################################
|
###########################################################
|
||||||
@ -29,7 +38,7 @@ async def get_users(
|
|||||||
auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.list"]))
|
auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.list"]))
|
||||||
):
|
):
|
||||||
model = models.VadminUser
|
model = models.VadminUser
|
||||||
options = [joinedload(model.roles)]
|
options = [joinedload(model.roles), joinedload(model.depts)]
|
||||||
schema = schemas.UserOut
|
schema = schemas.UserOut
|
||||||
datas, count = await crud.UserDal(auth.db).get_datas(
|
datas, count = await crud.UserDal(auth.db).get_datas(
|
||||||
**params.dict(),
|
**params.dict(),
|
||||||
@ -70,7 +79,7 @@ async def get_user(
|
|||||||
auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.view", "auth.user.update"]))
|
auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.view", "auth.user.update"]))
|
||||||
):
|
):
|
||||||
model = models.VadminUser
|
model = models.VadminUser
|
||||||
options = [joinedload(model.roles)]
|
options = [joinedload(model.roles), joinedload(model.depts)]
|
||||||
schema = schemas.UserOut
|
schema = schemas.UserOut
|
||||||
return SuccessResponse(await crud.UserDal(auth.db).get_data(data_id, v_options=options, v_schema=schema))
|
return SuccessResponse(await crud.UserDal(auth.db).get_data(data_id, v_options=options, v_schema=schema))
|
||||||
|
|
||||||
@ -189,7 +198,7 @@ async def get_role(
|
|||||||
auth: Auth = Depends(FullAdminAuth(permissions=["auth.role.view", "auth.role.update"]))
|
auth: Auth = Depends(FullAdminAuth(permissions=["auth.role.view", "auth.role.update"]))
|
||||||
):
|
):
|
||||||
model = models.VadminRole
|
model = models.VadminRole
|
||||||
options = [joinedload(model.menus)]
|
options = [joinedload(model.menus), joinedload(model.depts)]
|
||||||
schema = schemas.RoleOut
|
schema = schemas.RoleOut
|
||||||
return SuccessResponse(await crud.RoleDal(auth.db).get_data(data_id, v_options=options, v_schema=schema))
|
return SuccessResponse(await crud.RoleDal(auth.db).get_data(data_id, v_options=options, v_schema=schema))
|
||||||
|
|
||||||
@ -254,3 +263,46 @@ async def get_role_menu_tree(
|
|||||||
tree_data = await crud.MenuDal(auth.db).get_tree_list(mode=3)
|
tree_data = await crud.MenuDal(auth.db).get_tree_list(mode=3)
|
||||||
role_menu_tree = await crud.RoleDal(auth.db).get_role_menu_tree(role_id)
|
role_menu_tree = await crud.RoleDal(auth.db).get_role_menu_tree(role_id)
|
||||||
return SuccessResponse({"role_menu_tree": role_menu_tree, "menus": tree_data})
|
return SuccessResponse({"role_menu_tree": role_menu_tree, "menus": tree_data})
|
||||||
|
|
||||||
|
|
||||||
|
###########################################################
|
||||||
|
# 部门管理
|
||||||
|
###########################################################
|
||||||
|
@app.get("/depts", summary="获取部门列表")
|
||||||
|
async def get_depts(
|
||||||
|
params: DeptParams = Depends(),
|
||||||
|
auth: Auth = Depends(FullAdminAuth())
|
||||||
|
):
|
||||||
|
datas = await crud.DeptDal(auth.db).get_tree_list(1)
|
||||||
|
return SuccessResponse(datas)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/dept/tree/options", summary="获取部门树选择项,添加/修改部门时使用")
|
||||||
|
async def get_dept_options(auth: Auth = Depends(FullAdminAuth())):
|
||||||
|
datas = await crud.DeptDal(auth.db).get_tree_list(mode=2)
|
||||||
|
return SuccessResponse(datas)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/dept/user/tree/options", summary="获取部门树选择项,添加/修改用户时使用")
|
||||||
|
async def get_dept_treeselect(auth: Auth = Depends(FullAdminAuth())):
|
||||||
|
return SuccessResponse(await crud.DeptDal(auth.db).get_tree_list(mode=3))
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/depts", summary="创建部门信息")
|
||||||
|
async def create_dept(data: schemas.Dept, auth: Auth = Depends(FullAdminAuth())):
|
||||||
|
return SuccessResponse(await crud.DeptDal(auth.db).create_data(data=data))
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/depts", summary="批量删除部门", description="硬删除, 如果存在用户关联则无法删除")
|
||||||
|
async def delete_depts(ids: IdList = Depends(), auth: Auth = Depends(FullAdminAuth())):
|
||||||
|
await crud.DeptDal(auth.db).delete_datas(ids.ids, v_soft=False)
|
||||||
|
return SuccessResponse("删除成功")
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/depts/{data_id}", summary="更新部门信息")
|
||||||
|
async def put_dept(
|
||||||
|
data_id: int,
|
||||||
|
data: schemas.Dept,
|
||||||
|
auth: Auth = Depends(FullAdminAuth())
|
||||||
|
):
|
||||||
|
return SuccessResponse(await crud.DeptDal(auth.db).put_data(data_id, data))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user