871 lines
26 KiB
Vue
871 lines
26 KiB
Vue
<script setup lang="ts">
|
||
import { ref, computed, h, onMounted } from 'vue';
|
||
import { useAppStore } from '@/store/modules/app';
|
||
import { pinyin } from 'pinyin-pro';
|
||
import { Icon } from '@iconify/vue'
|
||
import { useThemeStore } from '@/store/modules/theme';
|
||
import { NButton, NPopconfirm, NTag, NDropdown, useDialog } from 'naive-ui';
|
||
import type { DataTableColumns } from 'naive-ui';
|
||
import { roleListApi } from '@/service/api/role';
|
||
import {
|
||
orgListApi,
|
||
addOrgApi,
|
||
editOrgApi,
|
||
deleteOrgApi,
|
||
userListApi,
|
||
addUserApi,
|
||
editUserApi,
|
||
deleteUserApi,
|
||
resetDataApi,
|
||
downloadTemplateApi,
|
||
importUserApi,
|
||
resetPasswordApi
|
||
} from '@/service/api/user';
|
||
|
||
const appStore = useAppStore();
|
||
const themeStore = useThemeStore();
|
||
const gap = computed(() => (appStore.isMobile ? 0 : 16));
|
||
const dialog = useDialog()
|
||
|
||
const roleList = ref<{ label: string, value: number }[]>([]);
|
||
const restLoading = ref<boolean>(false);
|
||
const treeLoading = ref<boolean>(false);
|
||
const loading = ref<boolean>(false);
|
||
const btnLoading = ref<boolean>(false);
|
||
const currentType = ref<string>('add');
|
||
const currentOrg = ref<number>(1);
|
||
const formRef = ref<any>(null);
|
||
const keyword = ref<string>('');
|
||
// 当前用户
|
||
const currentUser = ref<{ id: number, real_name: string, dept_ids: number[], mobile?: string, email?: string, username: string, role_id: number | null, org_ids: number[], enabled: boolean, gender: number }>({
|
||
id: 0,
|
||
real_name: '',
|
||
dept_ids: [],
|
||
mobile: '',
|
||
email: '',
|
||
username: '',
|
||
role_id: null,
|
||
org_ids: [],
|
||
enabled: true,
|
||
gender: 1,
|
||
});
|
||
// 校验规则
|
||
const rules = {
|
||
real_name: { required: true, message: '姓名不能为空', trigger: ['blur'] },
|
||
dept_ids: {
|
||
type: 'array',
|
||
required: true,
|
||
trigger: ['blur', 'change'],
|
||
message: '请选择所属部门'
|
||
},
|
||
username: { required: true, message: '账号不能为空', trigger: ['blur'] },
|
||
role_id: { type: 'number', required: true, message: '请选择账号角色', trigger: ['blur', 'change'] },
|
||
org_ids: {
|
||
type: 'array',
|
||
required: true,
|
||
trigger: ['blur', 'change'],
|
||
message: '请选择数据权限'
|
||
},
|
||
}
|
||
const userModal = ref<boolean>(false);
|
||
const pagination = ref({
|
||
page: 1,
|
||
pageSize: 3,
|
||
itemCount: 0,
|
||
})
|
||
const list = ref<{ id: number, name: string, dept: string[], mobile: string, email: string, account: string, role: string | null, dataPermission: string[], status: number, gender: number }[]>([])
|
||
|
||
const formNodeRef = ref<any>(null);
|
||
const treeModal = ref<boolean>(false);
|
||
const currentNode = ref<{ id: number, label: string, children: any[], is_bottom: number, org_code: string, parent_id: number, tier: number, sort: number }>({
|
||
id: 0,
|
||
label: '',
|
||
children: [],
|
||
is_bottom: 0,
|
||
org_code: '',
|
||
parent_id: 0,
|
||
tier: 0,
|
||
sort: 0,
|
||
})
|
||
const formNode = ref<{ label: string, is_bottom: number, org_code: string, parent_id: number, sort: number }>({
|
||
label: '',
|
||
is_bottom: 0,
|
||
org_code: '',
|
||
parent_id: 0,
|
||
sort: 0,
|
||
})
|
||
const currentNodeType = ref<string>('edit')
|
||
// 树节点规则
|
||
const treeRules = ref({
|
||
label: [{ required: true, message: '请输入层级名称', trigger: ['blur'] }],
|
||
})
|
||
// 左侧组织树
|
||
const orgTree = ref<any[]>([]);
|
||
const expandedKeys = ref<number[]>([1])
|
||
const selectedKeys = ref<number[]>([1])
|
||
// 右键菜单相关
|
||
const menuVisible = ref(false)
|
||
const menuX = ref(0)
|
||
const menuY = ref(0)
|
||
// 菜单选项
|
||
const menuOptions = [
|
||
{ label: '编辑', key: 'edit' },
|
||
{ label: '添加子级', key: 'add' },
|
||
{ label: '删除', key: 'delete' },
|
||
]
|
||
// 展开节点
|
||
const handleExpandKeys = (keys: number[]) => {
|
||
expandedKeys.value = keys
|
||
}
|
||
// 选中节点
|
||
const handleSelectKeys = (keys: number[]) => {
|
||
selectedKeys.value = keys
|
||
currentOrg.value = keys[0];
|
||
pagination.value.page = 1;
|
||
getList()
|
||
}
|
||
// 处理右键点击树节点
|
||
const treeProps = ({ option }: { option: any }) => {
|
||
return {
|
||
onContextmenu(e: MouseEvent) {
|
||
e.preventDefault()
|
||
currentNode.value = option
|
||
menuX.value = e.clientX
|
||
menuY.value = e.clientY
|
||
menuVisible.value = true
|
||
},
|
||
}
|
||
}
|
||
// 关闭菜单
|
||
const closeMenu = () => {
|
||
menuVisible.value = false
|
||
}
|
||
// 点击菜单项
|
||
const handleMenuSelect = (key: string) => {
|
||
currentNodeType.value = key
|
||
switch (key) {
|
||
case 'edit':
|
||
formNode.value = currentNode.value
|
||
treeModal.value = true
|
||
break
|
||
case 'add':
|
||
formNode.value = {
|
||
label: '',
|
||
is_bottom: 0,
|
||
org_code: '',
|
||
parent_id: currentNode.value.id,
|
||
sort: 0,
|
||
}
|
||
treeModal.value = true
|
||
break
|
||
case 'delete':
|
||
const d = dialog.info({
|
||
title: '删除',
|
||
content: `确定删除层级「${currentNode.value.label}」吗? 该操作不可恢复`,
|
||
positiveText: '确定',
|
||
negativeText: '取消',
|
||
onPositiveClick: () => {
|
||
d.loading = true
|
||
return new Promise(async (resolve) => {
|
||
const {error} = await deleteOrgApi(currentNode.value.id);
|
||
if(!error){
|
||
window.$message?.success('删除成功')
|
||
getOrgList()
|
||
}
|
||
d.loading = false
|
||
resolve(true)
|
||
})
|
||
},
|
||
onNegativeClick: () => {
|
||
d.loading = false
|
||
}
|
||
})
|
||
break
|
||
}
|
||
closeMenu()
|
||
}
|
||
// 自定义渲染展开图标
|
||
const renderSwitcherIcon = (node:any) => {
|
||
// 如果节点没有 children 或 children 为空数组,则不显示图标
|
||
if (!node.option.children || node.option.children.length === 0) {
|
||
return null; // 返回 null 表示不渲染任何图标
|
||
}
|
||
// 有子节点:返回 undefined 表示使用默认展开图标,也可自定义
|
||
return h(Icon,{
|
||
icon: node.option.expanded ? 'bi:caret-down-fill' : 'bi:caret-right-fill',
|
||
class: 'size-12px',
|
||
});
|
||
}
|
||
// 自定义渲染后缀图标
|
||
const renderSuffix = ({ option }: { option: any }) => {
|
||
return h(
|
||
NButton,
|
||
{
|
||
text: true,
|
||
type: 'primary',
|
||
onClick: (e: MouseEvent) => {
|
||
e.stopPropagation();
|
||
currentNode.value = option
|
||
menuX.value = e.clientX
|
||
menuY.value = e.clientY
|
||
menuVisible.value = true
|
||
},
|
||
},
|
||
{
|
||
icon: () =>
|
||
h(Icon, { icon: 'lucide:more-horizontal', class: 'size-14px' }),
|
||
}
|
||
)
|
||
};
|
||
|
||
// 处理数据权限选择变化
|
||
const handleUpdateChecked = (keys: number[]) => {
|
||
currentUser.value.org_ids = keys
|
||
}
|
||
|
||
// 打开添加用户弹框
|
||
const addUser = () => {
|
||
currentType.value = 'add';
|
||
currentUser.value = {
|
||
id: 0,
|
||
real_name: '',
|
||
dept_ids: [],
|
||
mobile: '',
|
||
email: '',
|
||
username: '',
|
||
role_id: null,
|
||
org_ids: [],
|
||
enabled: true,
|
||
gender: 1,
|
||
};
|
||
userModal.value = true;
|
||
}
|
||
// 重置密码
|
||
const resetPassword = async (row: any) => {
|
||
const {error} = await resetPasswordApi(row.id);
|
||
if(!error){
|
||
window.$message?.success('重置密码成功')
|
||
}
|
||
}
|
||
// 编辑用户
|
||
const handleEdit = (row: any) => {
|
||
currentType.value = 'edit';
|
||
currentUser.value = {
|
||
id: row.id,
|
||
real_name: row.real_name,
|
||
username: row.username,
|
||
dept_ids: row.user_departments,
|
||
mobile: row.mobile,
|
||
email: row.email,
|
||
role_id: row.role_id,
|
||
org_ids: row.org_ids,
|
||
enabled: row.status,
|
||
gender: row.gender,
|
||
};
|
||
userModal.value = true;
|
||
}
|
||
// 删除用户
|
||
const handleDelete = async (row: any) => {
|
||
loading.value = true;
|
||
const {error} = await deleteUserApi({id: row.id, type: 3});
|
||
if(!error){
|
||
window.$message?.success('删除成功')
|
||
getList()
|
||
}
|
||
loading.value = false;
|
||
}
|
||
const columns = ref<DataTableColumns>([
|
||
{
|
||
title: '序号',
|
||
key: 'index',
|
||
width: 5,
|
||
align: 'center',
|
||
render: (_row: any, index: number) => {
|
||
return h('span', {}, { default: () => (pagination.value.page - 1) * pagination.value.pageSize + index + 1 })
|
||
}
|
||
},
|
||
{
|
||
title: '姓名',
|
||
key: 'real_name',
|
||
width: 8,
|
||
align: 'center',
|
||
},
|
||
{
|
||
title: '部门',
|
||
key: 'user_departments',
|
||
width: 10,
|
||
align: 'center',
|
||
render: (_row: any) => {
|
||
return h('span', {}, { default: () => getOrgLabel(_row.user_departments) })
|
||
}
|
||
},
|
||
{
|
||
title: '手机号码',
|
||
key: 'mobile',
|
||
width: 12,
|
||
align: 'center',
|
||
},
|
||
{
|
||
title: '账号',
|
||
key: 'username',
|
||
width: 10,
|
||
align: 'center',
|
||
},
|
||
{
|
||
title: '账号角色',
|
||
key: 'role_name',
|
||
width: 10,
|
||
align: 'center',
|
||
},
|
||
{
|
||
title: '状态',
|
||
key: 'status',
|
||
width: 8,
|
||
align: 'center',
|
||
render: (row: any) => {
|
||
return h(NTag, {
|
||
size: 'small',
|
||
type: `${row.status?'success':'error'}`
|
||
}, { default: () => `${row.status?'启用':'禁用'}` })
|
||
}
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'action',
|
||
width: 14,
|
||
align: 'center',
|
||
render: (row: any) => {
|
||
return h('div', { class: 'flex items-center justify-center gap-3' }, {
|
||
default: () => [
|
||
h(NButton, {
|
||
text: true,
|
||
size: 'tiny',
|
||
type: 'primary',
|
||
onClick: () => handleEdit(row)
|
||
}, {
|
||
// 渲染按钮文本
|
||
default: () => '编辑'
|
||
}),
|
||
h(NPopconfirm, {
|
||
// 组件 Props
|
||
positiveText: '确定',
|
||
negativeText: '取消',
|
||
onPositiveClick: () => {
|
||
handleDelete(row)
|
||
},
|
||
// 可选:自定义确认/取消按钮的样式
|
||
positiveButtonProps: { size: 'tiny', type: 'primary' },
|
||
negativeButtonProps: { size: 'tiny' }
|
||
}, {
|
||
// 插槽 (Slots)
|
||
trigger: () => h(NButton,
|
||
{
|
||
text: true,
|
||
size: 'tiny',
|
||
type: 'error',
|
||
},
|
||
{
|
||
default: () => '删除' // 按钮文字
|
||
}
|
||
),
|
||
default: () => '确定要删除吗?' // 弹出框的提示内容
|
||
}),
|
||
h(NPopconfirm, {
|
||
// 组件 Props
|
||
positiveText: '确定',
|
||
negativeText: '取消',
|
||
onPositiveClick: () => {
|
||
resetPassword(row)
|
||
},
|
||
// 可选:自定义确认/取消按钮的样式
|
||
positiveButtonProps: { size: 'tiny', type: 'primary' },
|
||
negativeButtonProps: { size: 'tiny' }
|
||
}, {
|
||
// 插槽 (Slots)
|
||
trigger: () => h(NButton,
|
||
{
|
||
text: true,
|
||
size: 'tiny',
|
||
type: 'primary',
|
||
},
|
||
{
|
||
default: () => '重置密码' // 按钮文字
|
||
}
|
||
),
|
||
default: () => `确定重置该用户密码?` // 弹出框的提示内容
|
||
}),
|
||
]
|
||
})
|
||
}
|
||
},
|
||
])
|
||
// 导入用户
|
||
const importUser = () => {
|
||
const dom = document.createElement('input');
|
||
dom.type = 'file';
|
||
dom.style.display = 'none';
|
||
dom.click();
|
||
dom.onchange = async (e: any) => {
|
||
const formData = new FormData();
|
||
formData.append('file', e.target.files[0]);
|
||
const {error} = await importUserApi(formData);
|
||
if(!error){
|
||
window.$message?.success('导入成功')
|
||
pagination.value.page = 1;
|
||
getList()
|
||
}
|
||
};
|
||
}
|
||
// 下载模板
|
||
const downloadTemplate = async () => {
|
||
const {data,error} = await downloadTemplateApi();
|
||
if(!error){
|
||
// 确保 res 为 Blob 类型(若 API 返回 ArrayBuffer 或原始数据,需适配)
|
||
let blob;
|
||
if (data instanceof Blob) {
|
||
blob = data;
|
||
} else {
|
||
// 假设返回的是二进制数据(如 ArrayBuffer),指定 MIME 类型
|
||
blob = new Blob([data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||
}
|
||
const url = window.URL.createObjectURL(blob);
|
||
const link = document.createElement('a');
|
||
link.href = url;
|
||
link.download = '用户导入模板.xlsx'; // 推荐使用 download 属性
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
// 清理:移除链接并释放 URL 对象
|
||
document.body.removeChild(link);
|
||
window.URL.revokeObjectURL(url);
|
||
}
|
||
};
|
||
// 重置数据权限
|
||
const refreshData = async () => {
|
||
restLoading.value = true;
|
||
const {error} = await resetDataApi();
|
||
if(!error){
|
||
window.$message?.success('重置成功')
|
||
}
|
||
restLoading.value = false;
|
||
}
|
||
// 保存用户
|
||
const saveUser = (e: MouseEvent) => {
|
||
e.preventDefault()
|
||
formRef.value?.validate(async (errors: any) => {
|
||
if (!errors) {
|
||
btnLoading.value = true;
|
||
// 新增
|
||
if(currentType.value === 'add'){
|
||
const { id, ...rest } = currentUser.value;
|
||
const {error} = await addUserApi(rest);
|
||
if(!error){
|
||
window.$message?.success('添加成功')
|
||
userModal.value = false;
|
||
getList();
|
||
}
|
||
}
|
||
// 编辑
|
||
if(currentType.value === 'edit'){
|
||
const {error} = await editUserApi(currentUser.value.id, currentUser.value);
|
||
if(!error){
|
||
window.$message?.success('编辑成功')
|
||
userModal.value = false;
|
||
getList();
|
||
}
|
||
}
|
||
btnLoading.value = false;
|
||
}
|
||
})
|
||
}
|
||
// 保存节点
|
||
const saveNode = (e: MouseEvent) => {
|
||
e.preventDefault()
|
||
formNodeRef.value?.validate(async (errors: any) => {
|
||
if (!errors) {
|
||
btnLoading.value = true;
|
||
// 新增
|
||
if(currentNodeType.value === 'add'){
|
||
const {error} = await addOrgApi(formNode.value);
|
||
if(!error){
|
||
treeModal.value = false;
|
||
window.$message?.success('添加成功')
|
||
getOrgList();
|
||
}
|
||
}
|
||
// 编辑
|
||
if(currentNodeType.value === 'edit'){
|
||
const {error} = await editOrgApi(currentNode.value.id, formNode.value);
|
||
if(!error){
|
||
treeModal.value = false;
|
||
window.$message?.success('编辑成功')
|
||
getOrgList();
|
||
}
|
||
}
|
||
btnLoading.value = false;
|
||
}
|
||
})
|
||
}
|
||
|
||
// 转换为拼音
|
||
const convertToPinyin = async () => {
|
||
formNode.value.org_code = pinyin(formNode.value.label, {
|
||
toneType: 'none',
|
||
type: 'array',
|
||
pattern: 'first',
|
||
}).join('').toUpperCase();
|
||
};
|
||
|
||
// 获取列表
|
||
const getList = async () => {
|
||
loading.value = true;
|
||
const {data,error} = await userListApi({
|
||
org_id: currentOrg.value,
|
||
keyword: keyword.value,
|
||
page: pagination.value.page,
|
||
limit: pagination.value.pageSize
|
||
})
|
||
if(!error){
|
||
list.value = data.list;
|
||
pagination.value.itemCount = data.total;
|
||
}
|
||
loading.value = false;
|
||
}
|
||
// 分页改变
|
||
const pageChange = (page: number) => {
|
||
pagination.value.page = page;
|
||
getList();
|
||
}
|
||
// 搜索用户
|
||
const searchUser = () => {
|
||
pagination.value.page = 1;
|
||
getList();
|
||
}
|
||
|
||
// 获取组织列表
|
||
const getOrgList = async (init: boolean = false) => {
|
||
treeLoading.value = true;
|
||
const {data,error} = await orgListApi();
|
||
if(!error){
|
||
orgTree.value = data;
|
||
if(init){
|
||
if(orgTree.value.length > 0){
|
||
expandedKeys.value = [orgTree.value[0].id];
|
||
selectedKeys.value = [orgTree.value[0].id];
|
||
currentOrg.value = orgTree.value[0].id;
|
||
getList();
|
||
}
|
||
}
|
||
// 删除当前选中节点时,需要更新选中节点
|
||
if(currentNodeType.value==='delete'){
|
||
if(currentNode.value.id===currentOrg.value){
|
||
expandedKeys.value = [orgTree.value[0].id];
|
||
selectedKeys.value = [orgTree.value[0].id];
|
||
currentOrg.value = orgTree.value[0].id;
|
||
}
|
||
getList();
|
||
}
|
||
}
|
||
treeLoading.value = false;
|
||
}
|
||
|
||
// 根据parent_id获取组织名称
|
||
const getParentOrg = (parentId: number): string => {
|
||
const findNode = (nodes: any[]): any => {
|
||
for (const node of nodes) {
|
||
if (node.id === parentId) return node;
|
||
if (node.children) {
|
||
const found = findNode(node.children);
|
||
if (found) return found;
|
||
}
|
||
}
|
||
return null;
|
||
};
|
||
const node = findNode(orgTree.value);
|
||
return node?.label || '';
|
||
};
|
||
|
||
// 获取组织名称(新增/编辑)
|
||
const getOrg = () => {
|
||
if(currentNodeType.value==='edit'){
|
||
return getParentOrg(currentNode.value.parent_id)
|
||
}
|
||
return currentNode.value.label
|
||
}
|
||
|
||
// 获取角色列表
|
||
const getRoleList = async () => {
|
||
const {data,error} = await roleListApi();
|
||
if(!error){
|
||
const newArray = data.map((item:any) => ({
|
||
label: item.name,
|
||
value: item.id
|
||
}));
|
||
roleList.value = newArray;
|
||
}
|
||
}
|
||
|
||
// 通过组织id匹配label(多个)
|
||
const getOrgLabel = (orgIds: number[]) => {
|
||
const labels = orgIds.map((id: number) => getParentOrg(id));
|
||
return labels.join(';');
|
||
}
|
||
|
||
|
||
|
||
onMounted(() => {
|
||
getRoleList();
|
||
getOrgList(true);
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<NSpace vertical :size="16">
|
||
<!-- 用户管理 -->
|
||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||
<div class="border mb-3 py-2 px-3" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive>
|
||
<NGi span="24 s:24 m:16">
|
||
<div class="h-100% flex items-center gap-2">
|
||
<Icon icon="lucide:users-round" class="size-16px" :style="{ color: themeStore.themeColor }" />
|
||
<p class="text-[16px] font-600">架构及用户管理</p>
|
||
</div>
|
||
</NGi>
|
||
<NGi span="24 s:24 m:8" class="flex items-center justify-end gap-12px">
|
||
<NButton type="primary" size="small" @click="addUser">
|
||
<Icon icon="ic:round-plus" class="size-16px" />添加成员
|
||
</NButton>
|
||
<NPopover trigger="hover">
|
||
<template #trigger>
|
||
<NButton type="warning" size="small" @click="importUser">
|
||
<Icon icon="material-symbols:download" class="size-16px" />导入用户
|
||
</NButton>
|
||
</template>
|
||
<NButton type="primary" ghost size="small" @click="downloadTemplate">下载模板</NButton>
|
||
</NPopover>
|
||
<NPopover trigger="hover">
|
||
<template #trigger>
|
||
<NButton text @click="refreshData">
|
||
<Icon icon="tdesign:refresh" class="size-14px" :class="restLoading? 'animate-spin':''" />
|
||
</NButton>
|
||
</template>
|
||
<span>重置数据权限</span>
|
||
</NPopover>
|
||
</NGi>
|
||
</NGrid>
|
||
</div>
|
||
<div class="flex gap-3">
|
||
<NSpin :show="treeLoading" size="small" :stroke-width="18">
|
||
<div class="border p-3 w-300px" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||
<NInput v-model:value="keyword" placeholder="请输入人员" clearable @keyup.enter="searchUser">
|
||
<template #suffix>
|
||
<Icon icon="akar-icons:search" class="size-14px text-slate-400 cursor-pointer" @click="searchUser" />
|
||
</template>
|
||
</NInput>
|
||
<!-- 树 -->
|
||
<div class="mt-2 scroll">
|
||
<NTree
|
||
block-line
|
||
:data="orgTree"
|
||
label-field="label"
|
||
key-field="id"
|
||
:expanded-keys="expandedKeys"
|
||
:selected-keys="selectedKeys"
|
||
:render-switcher-icon="renderSwitcherIcon"
|
||
:render-suffix="renderSuffix"
|
||
selectable
|
||
:cancelable="false"
|
||
:on-update:expanded-keys="handleExpandKeys"
|
||
:on-update:selected-keys="handleSelectKeys"
|
||
:node-props="treeProps"
|
||
/>
|
||
<NDropdown
|
||
:show="menuVisible"
|
||
:options="menuOptions"
|
||
size="small"
|
||
:x="menuX"
|
||
:y="menuY"
|
||
placement="bottom-start"
|
||
trigger="manual"
|
||
@clickoutside="closeMenu"
|
||
@select="handleMenuSelect"
|
||
/>
|
||
</div>
|
||
<p class="mt-2 border-t pt-2 text-[11px] leading-4 text-slate-400">
|
||
点击层级查看该层级全员名录;右键(或点击 ···)可编辑、添加子级、删除。
|
||
</p>
|
||
</div>
|
||
</NSpin>
|
||
<div class="flex-1">
|
||
<NDataTable
|
||
size="small"
|
||
:loading="loading"
|
||
:single-line="false"
|
||
:columns="columns"
|
||
:data="list"
|
||
:pagination="false"
|
||
max-height="calc(100vh - 285px)"
|
||
/>
|
||
<div class="flex justify-end mt-3">
|
||
<NPagination
|
||
v-model:page="pagination.page"
|
||
:page-size="pagination.pageSize"
|
||
:item-count="pagination.itemCount"
|
||
:on-update:page="pageChange"
|
||
>
|
||
<template #prefix="{ itemCount }">
|
||
共 {{ itemCount }} 条
|
||
</template>
|
||
</NPagination>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 新增或编辑抽屉 -->
|
||
<NDrawer v-model:show="userModal" :width="400" placement="right">
|
||
<NDrawerContent :title="currentType === 'add' ? '新增成员' : '编辑成员'" closable>
|
||
<NForm ref="formRef" :model="currentUser" :rules="rules" label-placement="left" label-width="auto">
|
||
<NFormItem label="姓名" path="real_name">
|
||
<NInput
|
||
v-model:value="currentUser.real_name"
|
||
placeholder="请输入姓名"
|
||
/>
|
||
</NFormItem>
|
||
<NFormItem label="所属部门" path="dept_ids">
|
||
<NTreeSelect
|
||
v-model:value="currentUser.dept_ids"
|
||
multiple
|
||
default-expand-all
|
||
label-field="label"
|
||
key-field="id"
|
||
:options="orgTree"
|
||
:render-switcher-icon="renderSwitcherIcon"
|
||
/>
|
||
</NFormItem>
|
||
<NFormItem label="性别">
|
||
<NRadioGroup v-model:value="currentUser.gender" name="gender">
|
||
<NRadio :value="1">男</NRadio>
|
||
<NRadio :value="2">女</NRadio>
|
||
</NRadioGroup>
|
||
</NFormItem>
|
||
<NFormItem label="账号" path="username">
|
||
<NInput
|
||
v-model:value="currentUser.username"
|
||
placeholder="请输入账号"
|
||
/>
|
||
</NFormItem>
|
||
<NFormItem label="手机号码">
|
||
<NInput
|
||
v-model:value="currentUser.mobile"
|
||
placeholder="请输入11位手机号码"
|
||
/>
|
||
</NFormItem>
|
||
<NFormItem label="电子邮箱">
|
||
<NInput
|
||
v-model:value="currentUser.email"
|
||
placeholder="请输入电子邮箱"
|
||
/>
|
||
</NFormItem>
|
||
<NFormItem label="账号角色" path="role_id">
|
||
<NSelect
|
||
v-model:value="currentUser.role_id"
|
||
:options="roleList"
|
||
placeholder="请选择账号角色"
|
||
/>
|
||
</NFormItem>
|
||
<NFormItem label="数据权限" path="org_ids">
|
||
<NTree
|
||
:data="orgTree"
|
||
block-line
|
||
checkable
|
||
cascade
|
||
label-field="label"
|
||
key-field="id"
|
||
:render-switcher-icon="renderSwitcherIcon"
|
||
:checked-keys="currentUser.org_ids"
|
||
:on-update:checked-keys="handleUpdateChecked"
|
||
/>
|
||
</NFormItem>
|
||
<NFormItem label="状态">
|
||
<NRadioGroup v-model:value="currentUser.enabled" name="enabled">
|
||
<NRadio :value="true">启用</NRadio>
|
||
<NRadio :value="false">禁用</NRadio>
|
||
</NRadioGroup>
|
||
</NFormItem>
|
||
</NForm>
|
||
<template #footer>
|
||
<div class="flex justify-end gap-[10px]">
|
||
<NButton @click="userModal = false">取消</NButton>
|
||
<NButton type="primary" :loading="btnLoading" @click="saveUser">保存</NButton>
|
||
</div>
|
||
</template>
|
||
</NDrawerContent>
|
||
</NDrawer>
|
||
<!-- 左侧树添加/编辑弹框 -->
|
||
<NModal
|
||
v-model:show="treeModal"
|
||
preset="card"
|
||
:title="currentNodeType === 'edit' ? '编辑层级' : '添加子级'"
|
||
:auto-focus="false"
|
||
:style="{ width: '450px', height: 'auto' }"
|
||
:segmented="{ content: true, footer: true }"
|
||
>
|
||
<template #default>
|
||
<NForm ref="formNodeRef" :model="formNode" :rules="treeRules" label-placement="left" label-width="auto">
|
||
<NFormItem label="层级名称" path="label">
|
||
<NInput
|
||
v-model:value="formNode.label"
|
||
maxlength="10"
|
||
show-count
|
||
placeholder="请输入层级名称"
|
||
:on-input="convertToPinyin"
|
||
/>
|
||
</NFormItem>
|
||
<NFormItem label="层级简称">
|
||
<NInput
|
||
v-model:value="formNode.org_code"
|
||
maxlength="10"
|
||
show-count
|
||
placeholder="请输入层级简称"
|
||
/>
|
||
</NFormItem>
|
||
<NFormItem label="所属层级">
|
||
<NInput
|
||
:value="getOrg()"
|
||
:disabled="true"
|
||
placeholder=""
|
||
/>
|
||
</NFormItem>
|
||
<NFormItem label="末端层级">
|
||
<NRadioGroup v-model:value="formNode.is_bottom" name="is_bottom">
|
||
<NRadio :value="1">是</NRadio>
|
||
<NRadio :value="0">否</NRadio>
|
||
</NRadioGroup>
|
||
</NFormItem>
|
||
<NFormItem label="排序">
|
||
<NInputNumber
|
||
v-model:value="formNode.sort"
|
||
class="w-full"
|
||
:min="0"
|
||
:show-button="false"
|
||
placeholder="请输入排序"
|
||
/>
|
||
</NFormItem>
|
||
</NForm>
|
||
</template>
|
||
<template #footer>
|
||
<div class="flex justify-end gap-[10px]">
|
||
<NButton @click="treeModal = false">取消</NButton>
|
||
<NButton type="primary" :loading="btnLoading" @click="saveNode">确定</NButton>
|
||
</div>
|
||
</template>
|
||
</NModal>
|
||
</NCard>
|
||
</NSpace>
|
||
</template>
|
||
|
||
<style scoped lang="scss">
|
||
.scroll {
|
||
height: calc(100vh - 322px);
|
||
overflow-y: auto;
|
||
}
|
||
</style>
|