update: 组织管理
This commit is contained in:
parent
09a3873546
commit
db05307557
|
@ -1,10 +1,18 @@
|
|||
import server from '@/utils/request';
|
||||
|
||||
// 获取tree数据-第一层
|
||||
// 获取部门数据
|
||||
export const getTreeData_api = (data:object) => server.post(`/organization/_all/tree`, data);
|
||||
// 新增部门
|
||||
export const addDepartment_api = (data:object) => server.post(`/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`);
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,13 @@
|
|||
<template>
|
||||
<div>
|
||||
设备
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,13 @@
|
|||
<template>
|
||||
<div>
|
||||
用户
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
Loading…
Reference in New Issue