EL 5fdf4c80e6 医院管理页新增医院管理员列并支持任命医院管理员
组织架构树展示医院管理员信息
科室与小组弹窗支持设置主任/组长并限制候选角色
患者页优化归属医生选择与字段文案
统一“小组组长”角色文案
2026-03-18 17:07:37 +08:00

1171 lines
32 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="org-tree-container">
<el-row :gutter="20">
<!-- Left Column: Tree -->
<el-col :span="12">
<el-card shadow="never" class="tree-card">
<template #header>
<div class="card-header">
<span class="header-title"
><el-icon><Connection /></el-icon> 组织架构全景图</span
>
<el-button
type="primary"
@click="fetchTreeData"
icon="Refresh"
round
size="small"
>刷新</el-button
>
</div>
</template>
<div class="tree-content" v-loading="loading">
<el-tree
:data="treeData"
:props="defaultProps"
node-key="key"
default-expand-all
:expand-on-click-node="false"
class="beautiful-tree"
@node-click="handleNodeClick"
highlight-current
>
<template #default="{ node, data }">
<div class="custom-tree-node" :class="`node-${data.type}`">
<div class="node-main">
<div class="node-icon-wrapper">
<el-icon v-if="data.type === 'hospital'"
><OfficeBuilding
/></el-icon>
<el-icon v-else-if="data.type === 'department'"
><Filter
/></el-icon>
<el-icon v-else-if="data.type === 'group'"
><Connection
/></el-icon>
<el-icon v-else-if="data.type === 'user'"
><UserFilled
/></el-icon>
</div>
<div class="node-text">
<span class="node-label">{{ node.label }}</span>
<span
v-if="data.type === 'hospital'"
class="node-sub-label"
>
医院管理员:{{ data.adminDisplay || '未设置' }}
</span>
<span
v-else-if="data.type === 'department'"
class="node-sub-label"
>
主任:{{ data.directorDisplay || '未设置' }}
</span>
<span
v-else-if="data.type === 'group'"
class="node-sub-label"
>
组长:{{ data.leaderDisplay || '未设置' }}
</span>
</div>
<el-tag
v-if="data.type === 'user'"
size="small"
:type="getRoleTagType(data.role)"
effect="light"
class="role-tag"
round
>
{{ getRoleName(data.role) }}
</el-tag>
</div>
<div class="node-actions" v-if="data.type !== 'user'">
<el-button
v-if="
canAssignOwner &&
(data.type === 'department' || data.type === 'group')
"
type="warning"
link
size="small"
@click.stop="openSetOwnerDialog(data)"
>
{{ data.type === 'department' ? '设主任' : '设组长' }}
</el-button>
<el-button
v-if="canEditNode(data)"
type="info"
link
size="small"
@click.stop="openEditDialog(data)"
icon="EditPen"
>
编辑
</el-button>
<el-button
v-if="canDeleteNode(data)"
type="danger"
link
size="small"
@click.stop="handleDelete(data)"
icon="Delete"
>
删除
</el-button>
</div>
</div>
</template>
</el-tree>
<el-empty
v-if="!loading && treeData.length === 0"
description="暂无组织架构数据"
/>
</div>
</el-card>
</el-col>
<!-- Right Column: Details & Actions -->
<el-col :span="12">
<el-card shadow="never" class="detail-card">
<template #header>
<div class="card-header">
<span class="header-title">
<el-icon><Menu /></el-icon>
{{ activePanelTitle }}
</span>
<div
v-if="activeNode && activeNode.type !== 'user'"
class="header-actions"
>
<el-button
v-if="canCreateDepartment(activeNode)"
type="primary"
size="small"
icon="Plus"
@click="openCreateDialog('department', activeNode.id)"
>
新增科室
</el-button>
<el-button
v-if="canCreateGroup(activeNode)"
type="success"
size="small"
icon="Plus"
@click="openCreateDialog('group', activeNode.id)"
>
新增小组
</el-button>
<el-button
v-if="canAddUser(activeNode)"
type="warning"
size="small"
icon="User"
@click="goToAddUser(activeNode)"
>
新增人员
</el-button>
<el-button
v-if="
canAssignOwner &&
(activeNode.type === 'department' ||
activeNode.type === 'group')
"
type="primary"
size="small"
@click="openSetOwnerDialog(activeNode)"
>
{{
activeNode.type === 'department' ? '设置主任' : '设置组长'
}}
</el-button>
</div>
</div>
</template>
<div
v-if="activeNode && activeNode.type !== 'user'"
class="node-detail-panel"
>
<el-alert
v-if="activeNodeMeta"
:title="activeNodeMeta"
type="info"
:closable="false"
class="mb-12"
/>
<el-table
:data="activeNodeChildren"
border
stripe
style="width: 100%"
max-height="600"
>
<el-table-column prop="name" label="名称" />
<el-table-column label="类型" width="100" align="center">
<template #default="{ row }">
<el-tag size="small" :type="getNodeTypeTag(row.type)">{{
getTypeName(row.type)
}}</el-tag>
</template>
</el-table-column>
<el-table-column label="负责人" width="180" align="center">
<template #default="{ row }">
<span v-if="row.type === 'hospital'"
>医院管理员:{{ row.adminDisplay || '未设置' }}</span
>
<span v-else-if="row.type === 'department'"
>主任:{{ row.directorDisplay || '未设置' }}</span
>
<span v-else-if="row.type === 'group'"
>组长:{{ row.leaderDisplay || '未设置' }}</span
>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="角色" width="120" align="center">
<template #default="{ row }">
<span v-if="row.type === 'user'">{{
getRoleName(row.role)
}}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column
label="操作"
width="220"
align="center"
fixed="right"
>
<template #default="{ row }">
<el-button
v-if="
canAssignOwner &&
(row.type === 'department' || row.type === 'group')
"
type="warning"
link
size="small"
@click="openSetOwnerDialog(row)"
>
{{ row.type === 'department' ? '设主任' : '设组长' }}
</el-button>
<el-button
v-if="canEditNode(row)"
type="primary"
link
size="small"
@click="openEditDialog(row)"
>编辑</el-button
>
<el-button
v-if="canDeleteNode(row)"
type="danger"
link
size="small"
@click="handleDelete(row)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
</div>
<div v-else-if="selectedUserDetail" class="user-detail-panel">
<el-descriptions :column="2" border>
<el-descriptions-item label="姓名">{{
selectedUserDetail.name || '-'
}}</el-descriptions-item>
<el-descriptions-item label="角色">{{
getRoleName(selectedUserDetail.role)
}}</el-descriptions-item>
<el-descriptions-item label="手机号">{{
selectedUserDetail.phone || '-'
}}</el-descriptions-item>
<el-descriptions-item label="医院">{{
selectedUserDetail.hospitalName || '-'
}}</el-descriptions-item>
<el-descriptions-item label="科室">{{
selectedUserDetail.departmentName || '-'
}}</el-descriptions-item>
<el-descriptions-item label="小组">{{
selectedUserDetail.groupName || '-'
}}</el-descriptions-item>
</el-descriptions>
<el-alert
title="如需修改人员角色或组织归属,请前往“用户管理”页面操作。"
type="info"
:closable="false"
class="mt-12"
/>
</div>
<el-empty v-else description="点击左侧架构树查看下级列表" />
</el-card>
</el-col>
</el-row>
<!-- Dialog for Create / Edit -->
<el-dialog
:title="dialogTitle"
v-model="dialogVisible"
width="450px"
@close="resetForm"
destroy-on-close
>
<el-form
:model="form"
:rules="rules"
ref="formRef"
label-width="100px"
@submit.prevent
>
<el-form-item :label="formLabel" prop="name">
<el-input
v-model="form.name"
:placeholder="`请输入${formLabel}`"
clearable
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button
type="primary"
@click="handleSubmit"
:loading="submitLoading"
>确定</el-button
>
</div>
</template>
</el-dialog>
<el-dialog
:title="ownerDialogTitle"
v-model="ownerDialogVisible"
width="500px"
destroy-on-close
>
<el-form label-width="100px">
<el-form-item :label="ownerDialogLabel">
<el-select
v-model="selectedOwnerUserId"
filterable
placeholder="请选择人员"
style="width: 100%"
>
<el-option
v-for="user in ownerCandidates"
:key="user.id"
:label="`${user.name}${getRoleName(user.role)}`"
:value="user.id"
/>
</el-select>
</el-form-item>
</el-form>
<el-alert
type="info"
:closable="false"
title="仅主任/组长角色可分配;设置后不会自动取消其他同级负责人,请按需在用户管理页调整。"
/>
<template #footer>
<div class="dialog-footer">
<el-button @click="ownerDialogVisible = false">取消</el-button>
<el-button
type="primary"
:loading="ownerSubmitLoading"
@click="handleSetOwner"
>
确定
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus';
import {
getHospitals,
getDepartments,
getGroups,
createDepartment,
updateDepartment,
deleteDepartment,
createGroup,
updateGroup,
deleteGroup,
updateHospital,
deleteHospital,
} from '../../api/organization';
import { getUsers, updateUser } from '../../api/users';
import { useUserStore } from '../../store/user';
import {
OfficeBuilding,
Filter,
Connection,
UserFilled,
Refresh,
Plus,
EditPen,
Delete,
Menu,
User,
} from '@element-plus/icons-vue';
const router = useRouter();
const userStore = useUserStore();
const loading = ref(false);
const treeData = ref([]);
const activeNode = ref(null);
const allUsers = ref([]);
const hospitalNameMap = ref({});
const departmentNameMap = ref({});
const groupNameMap = ref({});
const ownerCandidates = ref([]);
const ownerDialogVisible = ref(false);
const ownerSubmitLoading = ref(false);
const ownerTargetNode = ref(null);
const selectedOwnerUserId = ref(null);
const defaultProps = {
children: 'children',
label: 'name',
};
const roleMap = {
SYSTEM_ADMIN: '系统管理员',
HOSPITAL_ADMIN: '医院管理员',
DIRECTOR: '科室主任',
LEADER: '小组组长',
DOCTOR: '医生',
ENGINEER: '工程师',
};
const getRoleName = (role) => roleMap[role] || role;
const getRoleTagType = (role) => {
if (role === 'HOSPITAL_ADMIN') return 'danger';
if (role === 'DIRECTOR') return 'warning';
if (role === 'LEADER') return 'success';
if (role === 'DOCTOR') return 'primary';
return 'info';
};
const getTypeName = (type) => {
const map = {
hospital: '医院',
department: '科室',
group: '小组',
user: '人员',
};
return map[type] || type;
};
const getNodeTypeTag = (type) => {
const map = {
hospital: 'primary',
department: 'success',
group: 'warning',
user: 'info',
};
return map[type] || 'info';
};
const canAssignOwner = computed(() =>
['SYSTEM_ADMIN', 'HOSPITAL_ADMIN'].includes(userStore.role),
);
const isSystemAdmin = computed(() => userStore.role === 'SYSTEM_ADMIN');
const isOrgAdmin = computed(() =>
['SYSTEM_ADMIN', 'HOSPITAL_ADMIN'].includes(userStore.role),
);
const isDirector = computed(() => userStore.role === 'DIRECTOR');
const isLeader = computed(() => userStore.role === 'LEADER');
const canCreateDepartment = (node) =>
Boolean(node && node.type === 'hospital' && isOrgAdmin.value);
const canCreateGroup = (node) =>
Boolean(
node &&
node.type === 'department' &&
(isOrgAdmin.value || isDirector.value),
);
const canAddUser = (node) =>
Boolean(
node &&
(node.type === 'department' || node.type === 'group') &&
isOrgAdmin.value,
);
const canEditNode = (node) => {
if (!node || node.type === 'user') {
return false;
}
if (node.type === 'hospital') {
return isOrgAdmin.value;
}
if (node.type === 'department') {
return isOrgAdmin.value || isDirector.value || isLeader.value;
}
if (node.type === 'group') {
return isOrgAdmin.value || isDirector.value || isLeader.value;
}
return false;
};
const canDeleteNode = (node) => {
if (!node || node.type === 'user') {
return false;
}
if (node.type === 'hospital') {
return isSystemAdmin.value;
}
if (node.type === 'department') {
return isOrgAdmin.value;
}
if (node.type === 'group') {
return isOrgAdmin.value || isDirector.value;
}
return false;
};
const activePanelTitle = computed(() => {
if (!activeNode.value) {
return '请在左侧选择节点';
}
if (activeNode.value.type === 'user') {
return `人员详情 (${activeNode.value.name})`;
}
return `下级列表 (${activeNode.value.name})`;
});
const activeNodeChildren = computed(() => {
if (
!activeNode.value ||
activeNode.value.type === 'user' ||
!Array.isArray(activeNode.value.children)
) {
return [];
}
const list = [...activeNode.value.children];
if (userStore.role !== 'HOSPITAL_ADMIN') {
return list;
}
// 医院管理员视角下,右侧列表优先显示人员,再显示组织节点。
return list.sort((a, b) => {
const aIsUser = a.type === 'user';
const bIsUser = b.type === 'user';
if (aIsUser !== bIsUser) {
return aIsUser ? -1 : 1;
}
return `${a.name || ''}`.localeCompare(`${b.name || ''}`, 'zh-Hans-CN');
});
});
const selectedUserDetail = computed(() => {
if (!activeNode.value || activeNode.value.type !== 'user') {
return null;
}
const current = allUsers.value.find(
(user) => user.id === activeNode.value.id,
);
const userData = current || activeNode.value;
const hospitalId = userData.hospitalId || null;
const departmentId = userData.departmentId || null;
const groupId = userData.groupId || null;
return {
...userData,
hospitalName: hospitalId ? hospitalNameMap.value[hospitalId] : '',
departmentName: departmentId ? departmentNameMap.value[departmentId] : '',
groupName: groupId ? groupNameMap.value[groupId] : '',
};
});
const activeNodeMeta = computed(() => {
if (!activeNode.value) {
return '';
}
if (activeNode.value.type === 'hospital') {
return `当前医院管理员:${activeNode.value.adminDisplay || '未设置'}`;
}
if (activeNode.value.type === 'department') {
return `当前科室主任:${activeNode.value.directorDisplay || '未设置'}`;
}
if (activeNode.value.type === 'group') {
return `当前小组组长:${activeNode.value.leaderDisplay || '未设置'}`;
}
return '';
});
const ownerDialogTitle = computed(() => {
if (!ownerTargetNode.value) {
return '设置负责人';
}
return ownerTargetNode.value.type === 'department'
? `设置科室主任(${ownerTargetNode.value.name}`
: `设置小组组长(${ownerTargetNode.value.name}`;
});
const ownerDialogLabel = computed(() => {
if (!ownerTargetNode.value) {
return '负责人';
}
return ownerTargetNode.value.type === 'department' ? '科室主任' : '小组组长';
});
const fetchTreeData = async () => {
loading.value = true;
activeNode.value = null; // Reset active node on refresh
try {
const hospRes = await getHospitals({ pageSize: 100 });
const hospitals = hospRes.list || [];
const [deptRes, groupRes, userRes] = await Promise.all([
getDepartments({ pageSize: 100 }),
getGroups({ pageSize: 100 }),
getUsers(),
]);
const departments = deptRes.list || [];
const groups = groupRes.list || [];
const users = userRes.list || [];
allUsers.value = users;
hospitalNameMap.value = Object.fromEntries(
hospitals.map((item) => [item.id, item.name]),
);
departmentNameMap.value = Object.fromEntries(
departments.map((item) => [item.id, item.name]),
);
groupNameMap.value = Object.fromEntries(
groups.map((item) => [item.id, item.name]),
);
const directorNameMap = {};
const leaderNameMap = {};
const hospitalAdminNameMap = {};
users.forEach((user) => {
if (user.role === 'HOSPITAL_ADMIN' && user.hospitalId) {
if (!hospitalAdminNameMap[user.hospitalId]) {
hospitalAdminNameMap[user.hospitalId] = [];
}
hospitalAdminNameMap[user.hospitalId].push(user.name);
}
if (user.role === 'DIRECTOR' && user.departmentId) {
if (!directorNameMap[user.departmentId]) {
directorNameMap[user.departmentId] = [];
}
directorNameMap[user.departmentId].push(user.name);
}
if (user.role === 'LEADER' && user.groupId) {
if (!leaderNameMap[user.groupId]) {
leaderNameMap[user.groupId] = [];
}
leaderNameMap[user.groupId].push(user.name);
}
});
const tree = hospitals.map((h) => {
const hDepts = departments.filter((d) => d.hospitalId === h.id);
const adminDisplay =
(hospitalAdminNameMap[h.id] || []).join('、') || '未设置';
const deptNodes = hDepts.map((d) => {
const dGroups = groups.filter((g) => g.departmentId === d.id);
const directorDisplay =
(directorNameMap[d.id] || []).join('、') || '未设置';
const groupNodes = dGroups.map((g) => {
const gUsers = users.filter((u) => u.groupId === g.id);
const leaderDisplay =
(leaderNameMap[g.id] || []).join('、') || '未设置';
const userNodes = gUsers.map((u) => ({
key: `u_${u.id}`,
id: u.id,
name: u.name,
type: 'user',
role: u.role,
hospitalId: h.id,
departmentId: d.id,
groupId: g.id,
}));
return {
key: `g_${g.id}`,
id: g.id,
name: g.name,
type: 'group',
departmentId: d.id,
hospitalId: h.id,
leaderDisplay,
children: userNodes,
};
});
const dUsers = users.filter(
(u) => u.departmentId === d.id && !u.groupId,
);
const dUserNodes = dUsers.map((u) => ({
key: `u_${u.id}`,
id: u.id,
name: u.name,
type: 'user',
role: u.role,
hospitalId: h.id,
departmentId: d.id,
}));
return {
key: `d_${d.id}`,
id: d.id,
name: d.name,
type: 'department',
hospitalId: h.id,
directorDisplay,
children: [...groupNodes, ...dUserNodes],
};
});
const hUsers = users.filter(
(u) => u.hospitalId === h.id && !u.departmentId,
);
const hUserNodes = hUsers.map((u) => ({
key: `u_${u.id}`,
id: u.id,
name: u.name,
type: 'user',
role: u.role,
hospitalId: h.id,
}));
return {
key: `h_${h.id}`,
id: h.id,
name: h.name,
type: 'hospital',
adminDisplay,
children: [...deptNodes, ...hUserNodes],
};
});
treeData.value = tree;
} catch (error) {
console.error('Failed to fetch tree data', error);
ElMessage.error('获取组织架构树失败');
} finally {
loading.value = false;
}
};
const handleNodeClick = (data) => {
activeNode.value = data;
};
const openSetOwnerDialog = (node) => {
if (!canAssignOwner.value) {
return;
}
if (node.type !== 'department' && node.type !== 'group') {
return;
}
ownerTargetNode.value = node;
selectedOwnerUserId.value = null;
if (node.type === 'department') {
ownerCandidates.value = allUsers.value.filter(
(user) =>
user.hospitalId === node.hospitalId &&
user.departmentId === node.id &&
user.role === 'DIRECTOR',
);
} else {
ownerCandidates.value = allUsers.value.filter(
(user) =>
user.hospitalId === node.hospitalId &&
user.departmentId === node.departmentId &&
user.groupId === node.id &&
user.role === 'LEADER',
);
}
if (ownerCandidates.value.length === 0) {
ElMessage.warning('当前节点下没有可设置候选(仅支持主任/组长角色)');
return;
}
const currentOwner = ownerCandidates.value.find((user) =>
node.type === 'department'
? user.role === 'DIRECTOR'
: user.role === 'LEADER',
);
selectedOwnerUserId.value = currentOwner?.id ?? ownerCandidates.value[0].id;
ownerDialogVisible.value = true;
};
const handleSetOwner = async () => {
if (!ownerTargetNode.value || !selectedOwnerUserId.value) {
ElMessage.warning('请选择人员');
return;
}
const targetUser = ownerCandidates.value.find(
(user) => user.id === selectedOwnerUserId.value,
);
if (!targetUser) {
ElMessage.warning('候选人员不存在');
return;
}
const isDepartment = ownerTargetNode.value.type === 'department';
const payload = isDepartment
? {
role: 'DIRECTOR',
// 后端约束:非 DOCTOR 不允许“调整”科室/小组归属。
// 这里仅做角色变更,并清空小组归属,避免触发该约束。
groupId: null,
}
: {
role: 'LEADER',
};
ownerSubmitLoading.value = true;
try {
await updateUser(targetUser.id, payload);
ElMessage.success(isDepartment ? '设置主任成功' : '设置组长成功');
ownerDialogVisible.value = false;
await fetchTreeData();
} finally {
ownerSubmitLoading.value = false;
}
};
const goToAddUser = (nodeData) => {
// We navigate to the Users list page. In a full implementation, you could
// pass query params to pre-fill a creation form or open a dialog directly.
router.push({
path: '/users',
query: {
action: 'create',
hospitalId: nodeData.hospitalId,
departmentId:
nodeData.type === 'department' ? nodeData.id : nodeData.departmentId,
},
});
};
// --- Dialog Logic ---
const dialogVisible = ref(false);
const submitLoading = ref(false);
const formRef = ref(null);
const dialogType = ref('');
const dialogMode = ref('');
const parentId = ref(null);
const currentId = ref(null);
const form = reactive({ name: '' });
const rules = {
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
};
const dialogTitle = computed(() => {
const typeName =
dialogType.value === 'hospital'
? '医院'
: dialogType.value === 'department'
? '科室'
: '小组';
return dialogMode.value === 'create' ? `新增${typeName}` : `编辑${typeName}`;
});
const formLabel = computed(() =>
dialogType.value === 'hospital'
? '医院名称'
: dialogType.value === 'department'
? '科室名称'
: '小组名称',
);
const openCreateDialog = (type, pId) => {
dialogType.value = type;
dialogMode.value = 'create';
parentId.value = pId;
currentId.value = null;
dialogVisible.value = true;
};
const openEditDialog = (data) => {
dialogType.value = data.type;
dialogMode.value = 'edit';
currentId.value = data.id;
form.name = data.name;
dialogVisible.value = true;
};
const resetForm = () => {
if (formRef.value) formRef.value.resetFields();
form.name = '';
};
const handleSubmit = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid) => {
if (valid) {
submitLoading.value = true;
try {
if (dialogMode.value === 'create') {
if (dialogType.value === 'department')
await createDepartment({
name: form.name,
hospitalId: parentId.value,
});
else if (dialogType.value === 'group')
await createGroup({
name: form.name,
departmentId: parentId.value,
});
ElMessage.success('创建成功');
} else {
if (dialogType.value === 'hospital')
await updateHospital(currentId.value, { name: form.name });
else if (dialogType.value === 'department')
await updateDepartment(currentId.value, { name: form.name });
else if (dialogType.value === 'group')
await updateGroup(currentId.value, { name: form.name });
ElMessage.success('更新成功');
// Update activeNode locally if it's the one edited
if (
activeNode.value &&
activeNode.value.id === currentId.value &&
activeNode.value.type === dialogType.value
) {
activeNode.value.name = form.name;
}
}
dialogVisible.value = false;
fetchTreeData();
} catch (error) {
console.error(error);
} finally {
submitLoading.value = false;
}
}
});
};
const handleDelete = (data) => {
const typeName =
data.type === 'hospital'
? '医院'
: data.type === 'department'
? '科室'
: '小组';
ElMessageBox.confirm(`确定要删除${typeName} "${data.name}" 吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(async () => {
try {
if (data.type === 'hospital') await deleteHospital(data.id);
else if (data.type === 'department') await deleteDepartment(data.id);
else if (data.type === 'group') await deleteGroup(data.id);
ElMessage.success('删除成功');
if (activeNode.value && activeNode.value.key === data.key) {
activeNode.value = null; // Clear active node if deleted
}
fetchTreeData();
} catch (error) {
console.error(error);
}
})
.catch(() => {});
};
onMounted(() => {
fetchTreeData();
});
</script>
<style scoped>
.org-tree-container {
padding: 20px;
}
.tree-card,
.detail-card {
border-radius: 8px;
height: calc(100vh - 120px);
display: flex;
flex-direction: column;
}
:deep(.el-card__body) {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-title {
font-size: 16px;
font-weight: bold;
display: flex;
align-items: center;
gap: 8px;
color: #303133;
}
.tree-content {
flex: 1;
overflow-y: auto;
padding: 10px 0;
}
.node-detail-panel {
padding: 10px;
}
.user-detail-panel {
padding: 10px;
}
.action-title {
margin-top: 20px;
margin-bottom: 15px;
color: #606266;
border-bottom: 1px solid #ebeef5;
padding-bottom: 10px;
}
.quick-actions {
display: flex;
flex-direction: column;
gap: 12px;
align-items: flex-start;
}
.mb-4 {
margin-bottom: 16px;
}
/* Beautiful Tree Styling */
.beautiful-tree {
background: transparent;
}
:deep(.el-tree-node__content) {
height: auto;
padding: 8px 0;
margin-bottom: 4px;
border-radius: 6px;
transition: all 0.3s;
}
:deep(.el-tree-node__content:hover) {
background-color: #f5f7fa;
}
:deep(.el-tree-node.is-current > .el-tree-node__content) {
background-color: #e6f1fc;
}
/* Node specific styles */
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 8px;
border-radius: 4px;
border: 1px solid transparent;
transition: all 0.2s ease;
}
.node-main {
display: flex;
align-items: center;
}
.node-text {
display: flex;
flex-direction: column;
}
.node-icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
margin-right: 8px;
font-size: 16px;
}
.node-hospital .node-icon-wrapper {
color: #409eff;
}
.node-department .node-icon-wrapper {
color: #67c23a;
}
.node-group .node-icon-wrapper {
color: #e6a23c;
}
.node-user .node-icon-wrapper {
color: #909399;
font-size: 14px;
}
.node-label {
font-size: 14px;
color: #303133;
}
.node-user .node-label {
color: #606266;
}
.node-sub-label {
font-size: 12px;
color: #909399;
margin-top: 2px;
}
.role-tag {
margin-left: 10px;
}
.mb-12 {
margin-bottom: 12px;
}
.mt-12 {
margin-top: 12px;
}
/* Hover Actions */
.node-actions {
opacity: 0;
transform: translateX(10px);
transition: all 0.2s ease;
}
:deep(.el-tree-node__content:hover) .node-actions {
opacity: 1;
transform: translateX(0);
}
</style>