update: 组织管理

This commit is contained in:
easy 2023-02-09 18:27:35 +08:00
parent 09a3873546
commit db05307557
7 changed files with 722 additions and 2 deletions

View File

@ -1,10 +1,18 @@
import server from '@/utils/request'; import server from '@/utils/request';
// 获取tree数据-第一层 // 获取部门数据
export const getTreeData_api = (data:object) => server.post(`/organization/_all/tree`, data); export const getTreeData_api = (data:object) => server.post(`/organization/_all/tree`, data);
// 新增部门 // 新增部门
export const addDepartment_api = (data:object) => server.post(`/organization`, data); export const addDepartment_api = (data:object) => server.post(`/organization`, data);
// 更新部门 // 更新部门
export const updateDepartment_api = (data:object) => server.patch(`/organization`, data); export const updateDepartment_api = (data:object) => server.patch(`/organization`, data);
// 删除部门 // 删除部门
export const delDepartment_api = (id:string) => server.remove(`/organization/${id}`); export const delDepartment_api = (id:string) => server.remove(`/organization/${id}`);
// 获取产品列表
export const getDeviceOrProductList_api = (data:object) => server.post(`/device-product/_query`, data);
// 根据产品的id获取产品的权限
export const getPermission_api = (ids:object, id:string) => server.post(`/assets/bindings/product/org/${id}/_query`, ids);
// 获取产品的权限字典
export const getPermissionDict_api = () => server.get(`/assets/bindings/product/permissions`);

View File

@ -0,0 +1,141 @@
<template>
<a-modal
v-model:visible="dialog.visible"
:title="dialog.title"
width="520px"
@ok="dialog.handleOk"
class="edit-dialog-container"
cancelText="取消"
okText="确定"
:confirmLoading="form.loading"
>
<a-form ref="formRef" :model="form.data" layout="vertical">
<a-form-item name="parentId" label="上级组织">
<a-tree-select
v-model:value="form.data.parentId"
style="width: 100%"
placeholder="请选择上级组织"
:tree-data="props.treeData"
:field-names="{ value: 'id' }"
>
<template #title="{ name }"> {{ name }} </template>
</a-tree-select>
</a-form-item>
<a-form-item
name="name"
label="名称"
:rules="[
{ required: true, message: '请输入名称' },
{ max: 64, message: '最多可输入64个字符' },
]"
>
<a-input
v-model:value="form.data.name"
placeholder="请输入名称"
/>
</a-form-item>
<a-form-item
name="sortIndex"
label="排序"
:rules="[{ required: true, message: '请输入排序' }]"
>
<a-input
v-model:value="form.data.sortIndex"
placeholder="请输入排序"
:maxlength="64"
@blur="form.checkSort"
/>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import { FormInstance } from 'ant-design-vue';
import {cloneDeep} from 'lodash-es'
import {
addDepartment_api,
updateDepartment_api,
} from '@/api/system/department';
const emits = defineEmits(['refresh']);
const props = defineProps<{
treeData: any[];
}>();
//
const dialog = reactive({
title: '',
visible: false,
handleOk: () => {
formRef.value?.validate().then(() => {
form.submit();
});
},
//
changeVisible: (status: boolean, row: any = {}) => {
if (row.id) {
dialog.title = '编辑';
form.data = cloneDeep(row);
} else if (row.parentId) {
dialog.title = '新增子组织';
form.data = {
name: '',
sortIndex: ((row.children && row.children.length) || 0) + 1,
parentId: row.parentId,
};
} else {
dialog.title = '新增';
form.data = {
name: '',
sortIndex: props.treeData.length + 1,
};
}
form.beforeSortIndex = form.data.sortIndex;
dialog.visible = status;
nextTick(() => {
formRef.value?.clearValidate();
});
},
});
//
const formRef = ref<FormInstance>();
const form = reactive({
loading: false,
data: {} as formType,
beforeSortIndex: '' as string | number,
checkSort: (e: any) => {
const value = e.target.value.match(/^[0-9]*/)[0];
if (value) {
form.data.sortIndex = value;
form.beforeSortIndex = value;
} else form.data.sortIndex = form.beforeSortIndex;
},
submit: () => {
form.loading = true;
const api = form.data.id ? updateDepartment_api : addDepartment_api;
api(form.data)
.then(() => {
emits('refresh');
dialog.changeVisible(false);
})
.finally(() => (form.loading = false));
},
});
type formType = {
id?: string;
parentId?: string;
name: string;
sortIndex: string | number;
};
//
defineExpose({
openDialog: dialog.changeVisible,
});
</script>
<style scoped></style>

View File

@ -0,0 +1,214 @@
<template>
<div class="left-tree-container">
<a-input
v-model:value="searchValue"
@change="search"
placeholder="请输入组织名称"
class="search-input"
>
<template #suffix>
<search-outlined />
</template>
</a-input>
<a-button type="primary" @click="openDialog" class="add-btn">
新增
</a-button>
<a-tree
:tree-data="treeData"
v-model:selected-keys="selectedKeys"
:fieldNames="{ key: 'id' }"
v-loading="loading"
>
<template #title="{ name, data }">
<span>{{ name }}</span>
<span class="func-btns">
<a-tooltip>
<template #title>编辑</template>
<a-button style="padding: 0" type="link">
<edit-outlined @click="openDialog(data)" />
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>新增子组织</template>
<a-button style="padding: 0" type="link">
<plus-circle-outlined
style="margin: 0 8px"
@click="
openDialog({
...data,
id: '',
parentId: data.id,
})
"
/>
</a-button>
</a-tooltip>
<a-popconfirm
title="确认删除"
ok-text="确定"
cancel-text="取消"
@confirm="delDepartment(data.id)"
>
<a-tooltip>
<template #title>删除</template>
<a-button style="padding: 0" type="link">
<delete-outlined />
</a-button>
</a-tooltip>
</a-popconfirm>
</span>
</template>
</a-tree>
<!-- 编辑弹窗 -->
<EditDepartmentDialog
:tree-data="sourceTree"
ref="editDialogRef"
@refresh="getTree"
/>
</div>
</template>
<script setup lang="ts">
import { getTreeData_api, delDepartment_api } from '@/api/system/department';
import { debounce, cloneDeep, omit } from 'lodash-es';
import { ArrayToTree } from '@/utils/utils';
import EditDepartmentDialog from './EditDepartmentDialog.vue';
import {
SearchOutlined,
EditOutlined,
PlusCircleOutlined,
DeleteOutlined,
} from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
const emits = defineEmits(['change']);
const searchValue = ref('');//
const loading = ref<boolean>(false); //
const sourceTree = ref<any[]>([]); //
const treeMap = new Map(); // map
const treeData = ref<any[]>([]); //
const selectedKeys = ref<string[]>([]); //
getTree();
watch(selectedKeys, (n) => {
emits('change', n[0]);
});
function getTree() {
loading.value = true;
const params = {
paging: false,
sorts: [{ name: 'sortIndex', order: 'asc' }],
} as any;
if (searchValue.value) {
params.terms = [
{ column: 'name$LIKE', value: `%${searchValue.value}%` },
];
}
getTreeData_api(params)
.then((resp: any) => {
selectedKeys.value = [resp.result[0].id];
sourceTree.value = resp.result; //
handleTreeMap(resp.result); // map
treeData.value = resp.result; //
})
.finally(() => {
loading.value = false;
});
};
const search = debounce(() => {
const key = searchValue.value;
const treeArray = new Map();
if (key) {
const searchTree: string[] = [];
treeMap.forEach((item) => {
if (item.name.includes(key)) {
searchTree.push(item.parentId);
treeArray.set(item.id, item);
}
});
dig(searchTree);
treeData.value = ArrayToTree(cloneDeep([...treeArray.values()]));
} else {
treeData.value = ArrayToTree(cloneDeep([...treeMap.values()]));
}
function dig(_data: any[]): any {
const pIds: string[] = [];
if (!_data.length) return;
_data.forEach((item) => {
if (treeMap.has(item)) {
const _item = treeMap.get(item);
pIds.push(_item.parentId);
treeArray.set(item, _item);
}
});
}
}, 500);
// map便
function handleTreeMap(_data: any[]) {
if (_data) {
_data.map((item) => {
treeMap.set(item.id, omit(cloneDeep(item), ['children']));
if (item.children) {
handleTreeMap(item.children);
}
});
}
}
//
function delDepartment(id: string) {
delDepartment_api(id).then(() => {
message.success('操作成功');
getTree();
});
}
//
const editDialogRef = ref(); //
const openDialog = (row: any = {}) => {
editDialogRef.value.openDialog(true, row);
};
</script>
<style lang="less" scoped>
.left-tree-container {
padding-right: 24px;
.add-btn {
margin: 24px 0;
width: 100%;
}
:deep(.ant-tree-treenode) {
width: 100%;
.ant-tree-node-content-wrapper {
flex: 1 1 auto;
.ant-tree-title {
display: flex;
justify-content: space-between;
align-items: center;
.func-btns {
display: none;
font-size: 14px;
.ant-btn {
height: 22px;
}
}
&:hover {
.func-btns {
display: block;
}
}
}
}
}
}
</style>

View File

@ -0,0 +1,13 @@
<template>
<div>
设备
</div>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>

View File

@ -0,0 +1,52 @@
<template>
<div class="department-container">
<a-card class="department-content">
<div class="left">
<LeftTree @change="id=>departmentId = id" />
</div>
<div class="right">
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="product" tab="产品">
<Product :parentId="departmentId" />
</a-tab-pane>
<a-tab-pane key="device" tab="设备">
<Device />
</a-tab-pane>
<a-tab-pane key="user" tab="用户">
<User />
</a-tab-pane>
</a-tabs>
</div>
</a-card>
</div>
</template>
<script setup lang="ts" name="Department">
import LeftTree from './components/LeftTree.vue';
import Product from './product/index.vue';
import Device from './device/index.vue';
import User from './user/index.vue';
const activeKey = ref<'product' | 'device' | 'user'>('product');
const departmentId = ref<string>('')
</script>
<style lang="less" scoped>
.department-container {
padding: 24px;
.department-content {
:deep(.ant-card-body) {
display: flex;
.left {
flex-basis: 300px;
}
.right {
flex: 1 1 auto;
}
}
}
}
</style>

View File

@ -0,0 +1,279 @@
<template>
<div class="product-container">
<Search :columns="query.columns" @search="query.search" />
<JTable
ref="tableRef"
:columns="table.columns"
:request="table.requestFun"
:gridColumn="2"
model="CARD"
:params="query.params.value"
:rowSelection="{
selectedRowKeys: table._selectedRowKeys,
onChange: table.onSelectChange,
}"
>
<template #headerTitle>
<a-space>
<a-button type="primary" @click="table.clickAdd">
新增
</a-button>
</a-space>
</template>
<template #card="slotProps">
<CardBox
:value="slotProps"
:actions="[{ key: 1 }]"
v-bind="slotProps"
:active="
table._selectedRowKeys.value.includes(slotProps.id)
"
:status="slotProps.state?.value"
:statusText="slotProps.state?.text"
:statusNames="{
online: 'success',
offline: 'error',
notActive: 'warning',
}"
>
<template #img>
<slot name="img">
<img :src="getImage('/device-product.png')" />
</slot>
</template>
<template #content>
<h3 class="card-item-content-title">
{{ slotProps.name }}
</h3>
<a-row>
<a-col :span="12">
<div class="card-item-content-text">ID</div>
<div>{{ slotProps.deviceType?.text }}</div>
</a-col>
<a-col :span="12">
<div class="card-item-content-text">
资产权限
</div>
<div>
{{
table.permissionList.value.length &&
table.getPermissLabel(
slotProps.permission,
)
}}
</div>
</a-col>
</a-row>
</template>
<template #actions>
<a-button @click="table.clickEdit(slotProps)">
<AIcon type="EditOutlined" />
</a-button>
<a-popconfirm
title="是否解除绑定"
ok-text="确定"
cancel-text="取消"
@confirm="table.clickUnBind"
><a-button>
<AIcon type="DisconnectOutlined" />
</a-button>
</a-popconfirm>
</template>
</CardBox>
</template>
</JTable>
</div>
</template>
<script setup lang="ts" name="product">
import { ActionsType } from '@/components/Table';
import { getImage } from '@/utils/comm';
import {
getDeviceOrProductList_api,
getPermission_api,
getPermissionDict_api,
} from '@/api/system/department';
const props = defineProps({
parentId: String,
});
const query = {
columns: [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
ellipsis: true,
fixed: 'left',
search: {
type: 'string',
},
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
ellipsis: true,
fixed: 'left',
search: {
type: 'string',
},
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
ellipsis: true,
fixed: 'left',
search: {
type: 'select',
options: [
{
label: '在线',
value: 'online',
},
{
label: '离线',
value: 'offline',
},
{
label: '禁用',
value: 'notActive',
},
],
},
},
],
params: ref({}),
search: (params: any) => {
query.params.value = params;
},
};
const tableRef = ref();
const table = {
columns: [],
_selectedRowKeys: ref<string[]>([]),
permissionList: ref<dictType>([]),
init: () => {
table.getPermissionDict();
watch(
() => props.parentId,
() => {
nextTick(() => {
tableRef.value.reload();
});
},
);
},
getPermissionDict: () => {
getPermissionDict_api().then((resp: any) => {
table.permissionList.value = resp.result;
});
},
getPermissLabel: (values: string[]) => {
const permissionList = table.permissionList.value;
if (permissionList.length < 1 || values.length < 1) return '';
const result = values.map(
(key) => permissionList.find((item) => item.id === key)?.name,
);
return result.join(',');
},
onSelectChange: (keys: string[]) => {
table._selectedRowKeys.value = [...keys];
},
getData: (params: object, parentId: string) =>
new Promise((resolve) => {
getDeviceOrProductList_api(params).then((resp) => {
type resultType = {
data: any[];
total: number;
pageSize: number;
pageIndex: number;
};
const { pageIndex, pageSize, total, data } =
resp.result as resultType;
const ids = data.map((item) => item.id);
getPermission_api(ids, parentId).then((perResp: any) => {
const permissionObj = {};
perResp.result.forEach((item: any) => {
permissionObj[item.assetId] = item.grantedPermissions;
});
data.forEach(
(item) => (item.permission = permissionObj[item.id]),
);
resolve({
code: 200,
result: {
data: data,
pageIndex,
pageSize,
total,
},
status: 200,
});
});
});
}),
requestFun: async (oParams: any) => {
if (props.parentId) {
const params = {
...oParams,
sorts: [{ name: 'createTime', order: 'desc' }],
terms: [
...oParams.terms,
{
column: 'id',
termType: 'dim-assets',
value: {
assetType: 'product',
targets: [
{
type: 'org',
id: props.parentId,
},
],
},
},
],
};
const resp: any = await table.getData(params, props.parentId);
console.log(resp.result);
return {
code: resp.status,
result: resp.result,
status: resp.status,
};
} else {
return {
code: 200,
result: {
data: [],
pageIndex: 0,
pageSize: 0,
total: 0,
},
status: 200,
};
}
},
clickAdd: () => {},
clickEdit: (row: any) => {},
clickUnBind: (row: any) => {},
};
table.init();
type dictType = {
id: string;
name: string;
}[];
</script>
<style scoped></style>

View File

@ -0,0 +1,13 @@
<template>
<div>
用户
</div>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>