医院管理页新增医院管理员列并支持任命医院管理员

组织架构树展示医院管理员信息
科室与小组弹窗支持设置主任/组长并限制候选角色
患者页优化归属医生选择与字段文案
统一“小组组长”角色文案
This commit is contained in:
EL 2026-03-18 17:07:37 +08:00
parent b527256874
commit 5fdf4c80e6
7 changed files with 691 additions and 275 deletions

View File

@ -4,15 +4,34 @@
<template #header> <template #header>
<h2 class="login-title">调压通管理后台</h2> <h2 class="login-title">调压通管理后台</h2>
</template> </template>
<el-form :model="loginForm" :rules="rules" ref="loginFormRef" @keyup.enter="handleLogin"> <el-form
:model="loginForm"
:rules="rules"
ref="loginFormRef"
@keyup.enter="handleLogin"
>
<el-form-item prop="phone"> <el-form-item prop="phone">
<el-input v-model="loginForm.phone" placeholder="请输入手机号" :prefix-icon="User" /> <el-input
v-model="loginForm.phone"
placeholder="请输入手机号"
:prefix-icon="User"
/>
</el-form-item> </el-form-item>
<el-form-item prop="password"> <el-form-item prop="password">
<el-input v-model="loginForm.password" type="password" placeholder="请输入密码" show-password :prefix-icon="Lock" /> <el-input
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
show-password
:prefix-icon="Lock"
/>
</el-form-item> </el-form-item>
<el-form-item prop="role"> <el-form-item prop="role">
<el-select v-model="loginForm.role" placeholder="请选择登录角色" style="width: 100%;"> <el-select
v-model="loginForm.role"
placeholder="请选择登录角色"
style="width: 100%"
>
<el-option label="系统管理员" value="SYSTEM_ADMIN" /> <el-option label="系统管理员" value="SYSTEM_ADMIN" />
<el-option label="医院管理员" value="HOSPITAL_ADMIN" /> <el-option label="医院管理员" value="HOSPITAL_ADMIN" />
<el-option label="科室主任" value="DIRECTOR" /> <el-option label="科室主任" value="DIRECTOR" />
@ -27,17 +46,23 @@
:min="1" :min="1"
:controls="false" :controls="false"
placeholder="医院 ID多账号场景建议填写" placeholder="医院 ID多账号场景建议填写"
style="width: 100%;" style="width: 100%"
/> />
</el-form-item> </el-form-item>
<el-alert <el-alert
type="info" type="info"
:closable="false" :closable="false"
title="若同一手机号在多个医院有同角色账号,请填写医院 ID。" title="若同一手机号在多个医院有同角色账号,请填写医院 ID。"
style="margin-bottom: 16px;" style="margin-bottom: 16px"
/> />
<el-form-item> <el-form-item>
<el-button type="primary" class="login-btn" :loading="loading" @click="handleLogin">登录</el-button> <el-button
type="primary"
class="login-btn"
:loading="loading"
@click="handleLogin"
>登录</el-button
>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card> </el-card>
@ -68,13 +93,13 @@ const loginForm = reactive({
const rules = { const rules = {
phone: [ phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' }, { required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1\d{10}$/, message: '请输入正确的手机号', trigger: 'blur' } { pattern: /^1\d{10}$/, message: '请输入正确的手机号', trigger: 'blur' },
], ],
password: [ password: [
{ required: true, message: '请输入密码', trigger: 'blur' }, { required: true, message: '请输入密码', trigger: 'blur' },
{ min: 8, message: '密码长度至少为 8 位', trigger: 'blur' } { min: 8, message: '密码长度至少为 8 位', trigger: 'blur' },
], ],
role: [{ required: true, message: '请选择角色', trigger: 'change' }] role: [{ required: true, message: '请选择角色', trigger: 'change' }],
}; };
const handleLogin = async () => { const handleLogin = async () => {

View File

@ -163,7 +163,7 @@
placeholder="可选:选择后将任命为科室主任" placeholder="可选:选择后将任命为科室主任"
clearable clearable
filterable filterable
style="width: 100%;" style="width: 100%"
> >
<el-option <el-option
v-for="user in directorOptions" v-for="user in directorOptions"
@ -416,7 +416,9 @@ const handleSubmit = async () => {
let targetHospitalId = form.hospitalId; let targetHospitalId = form.hospitalId;
if (isEdit.value) { if (isEdit.value) {
// Some backend update APIs don't allow changing hospitalId, but we'll send it if needed, or just name // Some backend update APIs don't allow changing hospitalId, but we'll send it if needed, or just name
const updated = await updateDepartment(currentId.value, { name: form.name }); const updated = await updateDepartment(currentId.value, {
name: form.name,
});
targetDepartmentId = updated?.id ?? currentId.value; targetDepartmentId = updated?.id ?? currentId.value;
targetHospitalId = updated?.hospitalId ?? form.hospitalId; targetHospitalId = updated?.hospitalId ?? form.hospitalId;
ElMessage.success('更新成功'); ElMessage.success('更新成功');
@ -431,10 +433,10 @@ const handleSubmit = async () => {
} }
if ( if (
canAssignDirectorInDialog.value canAssignDirectorInDialog.value &&
&& form.directorUserId form.directorUserId &&
&& targetDepartmentId targetDepartmentId &&
&& targetHospitalId targetHospitalId
) { ) {
await updateUser(form.directorUserId, { await updateUser(form.directorUserId, {
role: 'DIRECTOR', role: 'DIRECTOR',

View File

@ -200,7 +200,7 @@
placeholder="可选:选择后将任命为小组组长" placeholder="可选:选择后将任命为小组组长"
clearable clearable
filterable filterable
style="width: 100%;" style="width: 100%"
> >
<el-option <el-option
v-for="user in leaderOptions" v-for="user in leaderOptions"
@ -497,7 +497,9 @@ const handleSubmit = async () => {
let targetHospitalId = form.hospitalId; let targetHospitalId = form.hospitalId;
if (isEdit.value) { if (isEdit.value) {
// Backend patch dto might just accept name. Sending departmentId may not be allowed or needed. // Backend patch dto might just accept name. Sending departmentId may not be allowed or needed.
const updated = await updateGroup(currentId.value, { name: form.name }); const updated = await updateGroup(currentId.value, {
name: form.name,
});
targetGroupId = updated?.id ?? currentId.value; targetGroupId = updated?.id ?? currentId.value;
targetDepartmentId = updated?.departmentId ?? form.departmentId; targetDepartmentId = updated?.departmentId ?? form.departmentId;
targetHospitalId = updated?.department?.hospitalId ?? form.hospitalId; targetHospitalId = updated?.department?.hospitalId ?? form.hospitalId;
@ -513,11 +515,11 @@ const handleSubmit = async () => {
} }
if ( if (
canAssignLeaderInDialog.value canAssignLeaderInDialog.value &&
&& form.leaderUserId form.leaderUserId &&
&& targetGroupId targetGroupId &&
&& targetDepartmentId targetDepartmentId &&
&& targetHospitalId targetHospitalId
) { ) {
await updateUser(form.leaderUserId, { await updateUser(form.leaderUserId, {
role: 'LEADER', role: 'LEADER',
@ -586,7 +588,11 @@ watch(
if (!dialogVisible.value) { if (!dialogVisible.value) {
return; return;
} }
await loadLeaderOptions(hospitalId, departmentId, isEdit.value ? currentId.value : null); await loadLeaderOptions(
hospitalId,
departmentId,
isEdit.value ? currentId.value : null,
);
}, },
); );
</script> </script>

View File

@ -5,18 +5,36 @@
<div class="header-actions"> <div class="header-actions">
<el-form :inline="true" :model="searchForm" class="search-form"> <el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="医院名称"> <el-form-item label="医院名称">
<el-input v-model="searchForm.keyword" placeholder="请输入关键词" clearable /> <el-input
v-model="searchForm.keyword"
placeholder="请输入关键词"
clearable
/>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="fetchData" icon="Search">查询</el-button> <el-button type="primary" @click="fetchData" icon="Search"
>查询</el-button
>
<el-button @click="resetSearch" icon="Refresh">重置</el-button> <el-button @click="resetSearch" icon="Refresh">重置</el-button>
<el-button v-if="userStore.role === 'SYSTEM_ADMIN'" type="success" @click="openCreateDialog" icon="Plus">新增医院</el-button> <el-button
v-if="userStore.role === 'SYSTEM_ADMIN'"
type="success"
@click="openCreateDialog"
icon="Plus"
>新增医院</el-button
>
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
<!-- Table --> <!-- Table -->
<el-table :data="tableData" v-loading="loading" border stripe style="width: 100%"> <el-table
:data="tableData"
v-loading="loading"
border
stripe
style="width: 100%"
>
<el-table-column prop="id" label="ID" width="80" align="center" /> <el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="name" label="医院名称" min-width="200" /> <el-table-column prop="name" label="医院名称" min-width="200" />
<el-table-column prop="adminDisplay" label="医院管理员" min-width="220"> <el-table-column prop="adminDisplay" label="医院管理员" min-width="220">
@ -31,9 +49,19 @@
</el-table-column> </el-table-column>
<el-table-column label="操作" width="250" fixed="right" align="center"> <el-table-column label="操作" width="250" fixed="right" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-button size="small" @click="goToDepartments(row)">管理科室</el-button> <el-button size="small" @click="goToDepartments(row)"
<el-button size="small" type="primary" @click="openEditDialog(row)">编辑</el-button> >管理科室</el-button
<el-button v-if="userStore.role === 'SYSTEM_ADMIN'" size="small" type="danger" @click="handleDelete(row)">删除</el-button> >
<el-button size="small" type="primary" @click="openEditDialog(row)"
>编辑</el-button
>
<el-button
v-if="userStore.role === 'SYSTEM_ADMIN'"
size="small"
type="danger"
@click="handleDelete(row)"
>删除</el-button
>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -54,18 +82,26 @@
</el-card> </el-card>
<!-- Dialog for Create / Edit --> <!-- Dialog for Create / Edit -->
<el-dialog :title="isEdit ? '编辑医院' : '新增医院'" v-model="dialogVisible" width="500px" @close="resetForm"> <el-dialog
:title="isEdit ? '编辑医院' : '新增医院'"
v-model="dialogVisible"
width="500px"
@close="resetForm"
>
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px"> <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<el-form-item label="医院名称" prop="name"> <el-form-item label="医院名称" prop="name">
<el-input v-model="form.name" placeholder="请输入医院名称" /> <el-input v-model="form.name" placeholder="请输入医院名称" />
</el-form-item> </el-form-item>
<el-form-item label="医院管理员" v-if="userStore.role === 'SYSTEM_ADMIN'"> <el-form-item
label="医院管理员"
v-if="userStore.role === 'SYSTEM_ADMIN'"
>
<el-select <el-select
v-model="form.adminUserId" v-model="form.adminUserId"
placeholder="可选:选择后将任命为医院管理员" placeholder="可选:选择后将任命为医院管理员"
clearable clearable
filterable filterable
style="width: 100%;" style="width: 100%"
> >
<el-option <el-option
v-for="user in hospitalAdminOptions" v-for="user in hospitalAdminOptions"
@ -79,7 +115,12 @@
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button> <el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button> <el-button
type="primary"
@click="handleSubmit"
:loading="submitLoading"
>确定</el-button
>
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
@ -90,7 +131,12 @@
import { ref, reactive, onMounted } from 'vue'; import { ref, reactive, onMounted } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { getHospitals, createHospital, updateHospital, deleteHospital } from '../../api/organization'; import {
getHospitals,
createHospital,
updateHospital,
deleteHospital,
} from '../../api/organization';
import { getUsers, updateUser } from '../../api/users'; import { getUsers, updateUser } from '../../api/users';
import { useUserStore } from '../../store/user'; import { useUserStore } from '../../store/user';
@ -117,7 +163,7 @@ const roleMap = {
const getRoleName = (role) => roleMap[role] || role; const getRoleName = (role) => roleMap[role] || role;
const searchForm = reactive({ const searchForm = reactive({
keyword: '' keyword: '',
}); });
// Dialog State // Dialog State
@ -133,7 +179,7 @@ const form = reactive({
}); });
const rules = { const rules = {
name: [{ required: true, message: '请输入医院名称', trigger: 'blur' }] name: [{ required: true, message: '请输入医院名称', trigger: 'blur' }],
}; };
// --- Methods --- // --- Methods ---
@ -143,7 +189,7 @@ const fetchData = async () => {
const res = await getHospitals({ const res = await getHospitals({
page: page.value, page: page.value,
pageSize: pageSize.value, pageSize: pageSize.value,
keyword: searchForm.keyword || undefined keyword: searchForm.keyword || undefined,
}); });
let hospitalAdminNameMap = {}; let hospitalAdminNameMap = {};
@ -166,7 +212,8 @@ const fetchData = async () => {
tableData.value = (res.list || []).map((hospital) => ({ tableData.value = (res.list || []).map((hospital) => ({
...hospital, ...hospital,
adminDisplay: (hospitalAdminNameMap[hospital.id] || []).join('、') || '未设置', adminDisplay:
(hospitalAdminNameMap[hospital.id] || []).join('、') || '未设置',
})); }));
total.value = res.total || 0; total.value = res.total || 0;
} catch (error) { } catch (error) {
@ -233,7 +280,9 @@ const handleSubmit = async () => {
try { try {
let targetHospitalId = null; let targetHospitalId = null;
if (isEdit.value) { if (isEdit.value) {
const updated = await updateHospital(currentId.value, { name: form.name }); const updated = await updateHospital(currentId.value, {
name: form.name,
});
targetHospitalId = updated?.id ?? currentId.value; targetHospitalId = updated?.id ?? currentId.value;
ElMessage.success('更新成功'); ElMessage.success('更新成功');
} else { } else {
@ -272,29 +321,27 @@ const handleSubmit = async () => {
}; };
const handleDelete = (row) => { const handleDelete = (row) => {
ElMessageBox.confirm( ElMessageBox.confirm(`确定要删除医院 "${row.name}" 吗?`, '警告', {
`确定要删除医院 "${row.name}" 吗?`, confirmButtonText: '确定',
'警告', cancelButtonText: '取消',
{ type: 'warning',
confirmButtonText: '确定', })
cancelButtonText: '取消', .then(async () => {
type: 'warning', try {
} await deleteHospital(row.id);
).then(async () => { ElMessage.success('删除成功');
try { fetchData();
await deleteHospital(row.id); } catch (error) {
ElMessage.success('删除成功'); console.error('Delete failed', error);
fetchData(); }
} catch (error) { })
console.error('Delete failed', error); .catch(() => {});
}
}).catch(() => {});
}; };
const goToDepartments = (row) => { const goToDepartments = (row) => {
router.push({ router.push({
path: '/organization/departments', path: '/organization/departments',
query: { hospitalId: row.id, hospitalName: row.name } query: { hospitalId: row.id, hospitalName: row.name },
}); });
}; };

View File

@ -6,8 +6,17 @@
<el-card shadow="never" class="tree-card"> <el-card shadow="never" class="tree-card">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span class="header-title"><el-icon><Connection /></el-icon> 组织架构全景图</span> <span class="header-title"
<el-button type="primary" @click="fetchTreeData" icon="Refresh" round size="small">刷新</el-button> ><el-icon><Connection /></el-icon> 组织架构全景图</span
>
<el-button
type="primary"
@click="fetchTreeData"
icon="Refresh"
round
size="small"
>刷新</el-button
>
</div> </div>
</template> </template>
@ -26,20 +35,37 @@
<div class="custom-tree-node" :class="`node-${data.type}`"> <div class="custom-tree-node" :class="`node-${data.type}`">
<div class="node-main"> <div class="node-main">
<div class="node-icon-wrapper"> <div class="node-icon-wrapper">
<el-icon v-if="data.type === 'hospital'"><OfficeBuilding /></el-icon> <el-icon v-if="data.type === 'hospital'"
<el-icon v-else-if="data.type === 'department'"><Filter /></el-icon> ><OfficeBuilding
<el-icon v-else-if="data.type === 'group'"><Connection /></el-icon> /></el-icon>
<el-icon v-else-if="data.type === 'user'"><UserFilled /></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>
<div class="node-text"> <div class="node-text">
<span class="node-label">{{ node.label }}</span> <span class="node-label">{{ node.label }}</span>
<span v-if="data.type === 'hospital'" class="node-sub-label"> <span
v-if="data.type === 'hospital'"
class="node-sub-label"
>
医院管理员{{ data.adminDisplay || '未设置' }} 医院管理员{{ data.adminDisplay || '未设置' }}
</span> </span>
<span v-else-if="data.type === 'department'" class="node-sub-label"> <span
v-else-if="data.type === 'department'"
class="node-sub-label"
>
主任{{ data.directorDisplay || '未设置' }} 主任{{ data.directorDisplay || '未设置' }}
</span> </span>
<span v-else-if="data.type === 'group'" class="node-sub-label"> <span
v-else-if="data.type === 'group'"
class="node-sub-label"
>
组长{{ data.leaderDisplay || '未设置' }} 组长{{ data.leaderDisplay || '未设置' }}
</span> </span>
</div> </div>
@ -57,7 +83,10 @@
<div class="node-actions" v-if="data.type !== 'user'"> <div class="node-actions" v-if="data.type !== 'user'">
<el-button <el-button
v-if="canAssignOwner && (data.type === 'department' || data.type === 'group')" v-if="
canAssignOwner &&
(data.type === 'department' || data.type === 'group')
"
type="warning" type="warning"
link link
size="small" size="small"
@ -65,17 +94,34 @@
> >
{{ data.type === 'department' ? '设主任' : '设组长' }} {{ data.type === 'department' ? '设主任' : '设组长' }}
</el-button> </el-button>
<el-button v-if="canEditNode(data)" type="info" link size="small" @click.stop="openEditDialog(data)" icon="EditPen"> <el-button
v-if="canEditNode(data)"
type="info"
link
size="small"
@click.stop="openEditDialog(data)"
icon="EditPen"
>
编辑 编辑
</el-button> </el-button>
<el-button v-if="canDeleteNode(data)" type="danger" link size="small" @click.stop="handleDelete(data)" icon="Delete"> <el-button
v-if="canDeleteNode(data)"
type="danger"
link
size="small"
@click.stop="handleDelete(data)"
icon="Delete"
>
删除 删除
</el-button> </el-button>
</div> </div>
</div> </div>
</template> </template>
</el-tree> </el-tree>
<el-empty v-if="!loading && treeData.length === 0" description="暂无组织架构数据" /> <el-empty
v-if="!loading && treeData.length === 0"
description="暂无组织架构数据"
/>
</div> </div>
</el-card> </el-card>
</el-col> </el-col>
@ -89,7 +135,10 @@
<el-icon><Menu /></el-icon> <el-icon><Menu /></el-icon>
{{ activePanelTitle }} {{ activePanelTitle }}
</span> </span>
<div v-if="activeNode && activeNode.type !== 'user'" class="header-actions"> <div
v-if="activeNode && activeNode.type !== 'user'"
class="header-actions"
>
<el-button <el-button
v-if="canCreateDepartment(activeNode)" v-if="canCreateDepartment(activeNode)"
type="primary" type="primary"
@ -118,18 +167,27 @@
新增人员 新增人员
</el-button> </el-button>
<el-button <el-button
v-if="canAssignOwner && (activeNode.type === 'department' || activeNode.type === 'group')" v-if="
canAssignOwner &&
(activeNode.type === 'department' ||
activeNode.type === 'group')
"
type="primary" type="primary"
size="small" size="small"
@click="openSetOwnerDialog(activeNode)" @click="openSetOwnerDialog(activeNode)"
> >
{{ activeNode.type === 'department' ? '设置主任' : '设置组长' }} {{
activeNode.type === 'department' ? '设置主任' : '设置组长'
}}
</el-button> </el-button>
</div> </div>
</div> </div>
</template> </template>
<div v-if="activeNode && activeNode.type !== 'user'" class="node-detail-panel"> <div
v-if="activeNode && activeNode.type !== 'user'"
class="node-detail-panel"
>
<el-alert <el-alert
v-if="activeNodeMeta" v-if="activeNodeMeta"
:title="activeNodeMeta" :title="activeNodeMeta"
@ -137,31 +195,55 @@
:closable="false" :closable="false"
class="mb-12" class="mb-12"
/> />
<el-table :data="activeNodeChildren" border stripe style="width: 100%" max-height="600"> <el-table
:data="activeNodeChildren"
border
stripe
style="width: 100%"
max-height="600"
>
<el-table-column prop="name" label="名称" /> <el-table-column prop="name" label="名称" />
<el-table-column label="类型" width="100" align="center"> <el-table-column label="类型" width="100" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-tag size="small" :type="getNodeTypeTag(row.type)">{{ getTypeName(row.type) }}</el-tag> <el-tag size="small" :type="getNodeTypeTag(row.type)">{{
getTypeName(row.type)
}}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="负责人" width="180" align="center"> <el-table-column label="负责人" width="180" align="center">
<template #default="{ row }"> <template #default="{ row }">
<span v-if="row.type === 'hospital'">医院管理员{{ row.adminDisplay || '未设置' }}</span> <span v-if="row.type === 'hospital'"
<span v-else-if="row.type === 'department'">主任{{ row.directorDisplay || '未设置' }}</span> >医院管理员{{ row.adminDisplay || '未设置' }}</span
<span v-else-if="row.type === 'group'">组长{{ row.leaderDisplay || '未设置' }}</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> <span v-else>-</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="角色" width="120" align="center"> <el-table-column label="角色" width="120" align="center">
<template #default="{ row }"> <template #default="{ row }">
<span v-if="row.type === 'user'">{{ getRoleName(row.role) }}</span> <span v-if="row.type === 'user'">{{
getRoleName(row.role)
}}</span>
<span v-else>-</span> <span v-else>-</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="220" align="center" fixed="right"> <el-table-column
label="操作"
width="220"
align="center"
fixed="right"
>
<template #default="{ row }"> <template #default="{ row }">
<el-button <el-button
v-if="canAssignOwner && (row.type === 'department' || row.type === 'group')" v-if="
canAssignOwner &&
(row.type === 'department' || row.type === 'group')
"
type="warning" type="warning"
link link
size="small" size="small"
@ -169,8 +251,22 @@
> >
{{ row.type === 'department' ? '设主任' : '设组长' }} {{ row.type === 'department' ? '设主任' : '设组长' }}
</el-button> </el-button>
<el-button v-if="canEditNode(row)" type="primary" link size="small" @click="openEditDialog(row)">编辑</el-button> <el-button
<el-button v-if="canDeleteNode(row)" type="danger" link size="small" @click="handleDelete(row)">删除</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> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -178,12 +274,24 @@
<div v-else-if="selectedUserDetail" class="user-detail-panel"> <div v-else-if="selectedUserDetail" class="user-detail-panel">
<el-descriptions :column="2" border> <el-descriptions :column="2" border>
<el-descriptions-item label="姓名">{{ selectedUserDetail.name || '-' }}</el-descriptions-item> <el-descriptions-item label="姓名">{{
<el-descriptions-item label="角色">{{ getRoleName(selectedUserDetail.role) }}</el-descriptions-item> selectedUserDetail.name || '-'
<el-descriptions-item label="手机号">{{ selectedUserDetail.phone || '-' }}</el-descriptions-item> }}</el-descriptions-item>
<el-descriptions-item label="医院">{{ selectedUserDetail.hospitalName || '-' }}</el-descriptions-item> <el-descriptions-item label="角色">{{
<el-descriptions-item label="科室">{{ selectedUserDetail.departmentName || '-' }}</el-descriptions-item> getRoleName(selectedUserDetail.role)
<el-descriptions-item label="小组">{{ selectedUserDetail.groupName || '-' }}</el-descriptions-item> }}</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-descriptions>
<el-alert <el-alert
title="如需修改人员角色或组织归属,请前往“用户管理”页面操作。" title="如需修改人员角色或组织归属,请前往“用户管理”页面操作。"
@ -198,16 +306,37 @@
</el-row> </el-row>
<!-- Dialog for Create / Edit --> <!-- Dialog for Create / Edit -->
<el-dialog :title="dialogTitle" v-model="dialogVisible" width="450px" @close="resetForm" destroy-on-close> <el-dialog
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px" @submit.prevent> :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-form-item :label="formLabel" prop="name">
<el-input v-model="form.name" :placeholder="`请输入${formLabel}`" clearable /> <el-input
v-model="form.name"
:placeholder="`请输入${formLabel}`"
clearable
/>
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button> <el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button> <el-button
type="primary"
@click="handleSubmit"
:loading="submitLoading"
>确定</el-button
>
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
@ -224,7 +353,7 @@
v-model="selectedOwnerUserId" v-model="selectedOwnerUserId"
filterable filterable
placeholder="请选择人员" placeholder="请选择人员"
style="width: 100%;" style="width: 100%"
> >
<el-option <el-option
v-for="user in ownerCandidates" v-for="user in ownerCandidates"
@ -243,7 +372,11 @@
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button @click="ownerDialogVisible = false">取消</el-button> <el-button @click="ownerDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="ownerSubmitLoading" @click="handleSetOwner"> <el-button
type="primary"
:loading="ownerSubmitLoading"
@click="handleSetOwner"
>
确定 确定
</el-button> </el-button>
</div> </div>
@ -256,10 +389,33 @@
import { ref, reactive, onMounted, computed } from 'vue'; import { ref, reactive, onMounted, computed } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { getHospitals, getDepartments, getGroups, createDepartment, updateDepartment, deleteDepartment, createGroup, updateGroup, deleteGroup, updateHospital, deleteHospital } from '../../api/organization'; import {
getHospitals,
getDepartments,
getGroups,
createDepartment,
updateDepartment,
deleteDepartment,
createGroup,
updateGroup,
deleteGroup,
updateHospital,
deleteHospital,
} from '../../api/organization';
import { getUsers, updateUser } from '../../api/users'; import { getUsers, updateUser } from '../../api/users';
import { useUserStore } from '../../store/user'; import { useUserStore } from '../../store/user';
import { OfficeBuilding, Filter, Connection, UserFilled, Refresh, Plus, EditPen, Delete, Menu, User } from '@element-plus/icons-vue'; import {
OfficeBuilding,
Filter,
Connection,
UserFilled,
Refresh,
Plus,
EditPen,
Delete,
Menu,
User,
} from '@element-plus/icons-vue';
const router = useRouter(); const router = useRouter();
const userStore = useUserStore(); const userStore = useUserStore();
@ -288,7 +444,7 @@ const roleMap = {
DIRECTOR: '科室主任', DIRECTOR: '科室主任',
LEADER: '小组组长', LEADER: '小组组长',
DOCTOR: '医生', DOCTOR: '医生',
ENGINEER: '工程师' ENGINEER: '工程师',
}; };
const getRoleName = (role) => roleMap[role] || role; const getRoleName = (role) => roleMap[role] || role;
@ -302,12 +458,22 @@ const getRoleTagType = (role) => {
}; };
const getTypeName = (type) => { const getTypeName = (type) => {
const map = { hospital: '医院', department: '科室', group: '小组', user: '人员' }; const map = {
hospital: '医院',
department: '科室',
group: '小组',
user: '人员',
};
return map[type] || type; return map[type] || type;
}; };
const getNodeTypeTag = (type) => { const getNodeTypeTag = (type) => {
const map = { hospital: 'primary', department: 'success', group: 'warning', user: 'info' }; const map = {
hospital: 'primary',
department: 'success',
group: 'warning',
user: 'info',
};
return map[type] || 'info'; return map[type] || 'info';
}; };
@ -326,16 +492,16 @@ const canCreateDepartment = (node) =>
const canCreateGroup = (node) => const canCreateGroup = (node) =>
Boolean( Boolean(
node node &&
&& node.type === 'department' node.type === 'department' &&
&& (isOrgAdmin.value || isDirector.value), (isOrgAdmin.value || isDirector.value),
); );
const canAddUser = (node) => const canAddUser = (node) =>
Boolean( Boolean(
node node &&
&& (node.type === 'department' || node.type === 'group') (node.type === 'department' || node.type === 'group') &&
&& isOrgAdmin.value, isOrgAdmin.value,
); );
const canEditNode = (node) => { const canEditNode = (node) => {
@ -382,9 +548,9 @@ const activePanelTitle = computed(() => {
const activeNodeChildren = computed(() => { const activeNodeChildren = computed(() => {
if ( if (
!activeNode.value !activeNode.value ||
|| activeNode.value.type === 'user' activeNode.value.type === 'user' ||
|| !Array.isArray(activeNode.value.children) !Array.isArray(activeNode.value.children)
) { ) {
return []; return [];
} }
@ -410,7 +576,9 @@ const selectedUserDetail = computed(() => {
return null; return null;
} }
const current = allUsers.value.find((user) => user.id === activeNode.value.id); const current = allUsers.value.find(
(user) => user.id === activeNode.value.id,
);
const userData = current || activeNode.value; const userData = current || activeNode.value;
const hospitalId = userData.hospitalId || null; const hospitalId = userData.hospitalId || null;
const departmentId = userData.departmentId || null; const departmentId = userData.departmentId || null;
@ -466,7 +634,7 @@ const fetchTreeData = async () => {
const [deptRes, groupRes, userRes] = await Promise.all([ const [deptRes, groupRes, userRes] = await Promise.all([
getDepartments({ pageSize: 100 }), getDepartments({ pageSize: 100 }),
getGroups({ pageSize: 100 }), getGroups({ pageSize: 100 }),
getUsers() getUsers(),
]); ]);
const departments = deptRes.list || []; const departments = deptRes.list || [];
@ -507,51 +675,86 @@ const fetchTreeData = async () => {
} }
}); });
const tree = hospitals.map(h => { const tree = hospitals.map((h) => {
const hDepts = departments.filter(d => d.hospitalId === h.id); const hDepts = departments.filter((d) => d.hospitalId === h.id);
const adminDisplay = const adminDisplay =
(hospitalAdminNameMap[h.id] || []).join('、') || '未设置'; (hospitalAdminNameMap[h.id] || []).join('、') || '未设置';
const deptNodes = hDepts.map(d => { const deptNodes = hDepts.map((d) => {
const dGroups = groups.filter(g => g.departmentId === d.id); const dGroups = groups.filter((g) => g.departmentId === d.id);
const directorDisplay = const directorDisplay =
(directorNameMap[d.id] || []).join('、') || '未设置'; (directorNameMap[d.id] || []).join('、') || '未设置';
const groupNodes = dGroups.map(g => { const groupNodes = dGroups.map((g) => {
const gUsers = users.filter(u => u.groupId === g.id); const gUsers = users.filter((u) => u.groupId === g.id);
const leaderDisplay = const leaderDisplay =
(leaderNameMap[g.id] || []).join('、') || '未设置'; (leaderNameMap[g.id] || []).join('、') || '未设置';
const userNodes = gUsers.map(u => ({ const userNodes = gUsers.map((u) => ({
key: `u_${u.id}`, id: u.id, name: u.name, type: 'user', role: u.role, key: `u_${u.id}`,
hospitalId: h.id, departmentId: d.id, groupId: g.id id: u.id,
name: u.name,
type: 'user',
role: u.role,
hospitalId: h.id,
departmentId: d.id,
groupId: g.id,
})); }));
return { return {
key: `g_${g.id}`, id: g.id, name: g.name, type: 'group', key: `g_${g.id}`,
departmentId: d.id, hospitalId: h.id, leaderDisplay, children: userNodes 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 dUsers = users.filter(
const dUserNodes = dUsers.map(u => ({ (u) => u.departmentId === d.id && !u.groupId,
key: `u_${u.id}`, id: u.id, name: u.name, type: 'user', role: u.role, );
hospitalId: h.id, departmentId: d.id 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 { return {
key: `d_${d.id}`, id: d.id, name: d.name, type: 'department', key: `d_${d.id}`,
hospitalId: h.id, directorDisplay, children: [...groupNodes, ...dUserNodes] 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 hUsers = users.filter(
const hUserNodes = hUsers.map(u => ({ (u) => u.hospitalId === h.id && !u.departmentId,
key: `u_${u.id}`, id: u.id, name: u.name, type: 'user', role: u.role, );
hospitalId: h.id const hUserNodes = hUsers.map((u) => ({
key: `u_${u.id}`,
id: u.id,
name: u.name,
type: 'user',
role: u.role,
hospitalId: h.id,
})); }));
return { return {
key: `h_${h.id}`, id: h.id, name: h.name, type: 'hospital', adminDisplay, children: [...deptNodes, ...hUserNodes] key: `h_${h.id}`,
id: h.id,
name: h.name,
type: 'hospital',
adminDisplay,
children: [...deptNodes, ...hUserNodes],
}; };
}); });
@ -580,17 +783,19 @@ const openSetOwnerDialog = (node) => {
selectedOwnerUserId.value = null; selectedOwnerUserId.value = null;
if (node.type === 'department') { if (node.type === 'department') {
ownerCandidates.value = allUsers.value.filter((user) => ownerCandidates.value = allUsers.value.filter(
user.hospitalId === node.hospitalId (user) =>
&& user.departmentId === node.id user.hospitalId === node.hospitalId &&
&& user.role === 'DIRECTOR', user.departmentId === node.id &&
user.role === 'DIRECTOR',
); );
} else { } else {
ownerCandidates.value = allUsers.value.filter((user) => ownerCandidates.value = allUsers.value.filter(
user.hospitalId === node.hospitalId (user) =>
&& user.departmentId === node.departmentId user.hospitalId === node.hospitalId &&
&& user.groupId === node.id user.departmentId === node.departmentId &&
&& user.role === 'LEADER', user.groupId === node.id &&
user.role === 'LEADER',
); );
} }
@ -600,7 +805,9 @@ const openSetOwnerDialog = (node) => {
} }
const currentOwner = ownerCandidates.value.find((user) => const currentOwner = ownerCandidates.value.find((user) =>
node.type === 'department' ? user.role === 'DIRECTOR' : user.role === 'LEADER', node.type === 'department'
? user.role === 'DIRECTOR'
: user.role === 'LEADER',
); );
selectedOwnerUserId.value = currentOwner?.id ?? ownerCandidates.value[0].id; selectedOwnerUserId.value = currentOwner?.id ?? ownerCandidates.value[0].id;
ownerDialogVisible.value = true; ownerDialogVisible.value = true;
@ -623,14 +830,14 @@ const handleSetOwner = async () => {
const isDepartment = ownerTargetNode.value.type === 'department'; const isDepartment = ownerTargetNode.value.type === 'department';
const payload = isDepartment const payload = isDepartment
? { ? {
role: 'DIRECTOR', role: 'DIRECTOR',
// DOCTOR / // DOCTOR /
// //
groupId: null, groupId: null,
} }
: { : {
role: 'LEADER', role: 'LEADER',
}; };
ownerSubmitLoading.value = true; ownerSubmitLoading.value = true;
try { try {
@ -651,8 +858,9 @@ const goToAddUser = (nodeData) => {
query: { query: {
action: 'create', action: 'create',
hospitalId: nodeData.hospitalId, hospitalId: nodeData.hospitalId,
departmentId: nodeData.type === 'department' ? nodeData.id : nodeData.departmentId, departmentId:
} nodeData.type === 'department' ? nodeData.id : nodeData.departmentId,
},
}); });
}; };
@ -666,17 +874,45 @@ const parentId = ref(null);
const currentId = ref(null); const currentId = ref(null);
const form = reactive({ name: '' }); const form = reactive({ name: '' });
const rules = { name: [{ required: true, message: '请输入名称', trigger: 'blur' }] }; const rules = {
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
};
const dialogTitle = computed(() => { const dialogTitle = computed(() => {
const typeName = dialogType.value === 'hospital' ? '医院' : (dialogType.value === 'department' ? '科室' : '小组'); const typeName =
dialogType.value === 'hospital'
? '医院'
: dialogType.value === 'department'
? '科室'
: '小组';
return dialogMode.value === 'create' ? `新增${typeName}` : `编辑${typeName}`; return dialogMode.value === 'create' ? `新增${typeName}` : `编辑${typeName}`;
}); });
const formLabel = computed(() => dialogType.value === 'hospital' ? '医院名称' : (dialogType.value === 'department' ? '科室名称' : '小组名称')); 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 openCreateDialog = (type, pId) => {
const openEditDialog = (data) => { dialogType.value = data.type; dialogMode.value = 'edit'; currentId.value = data.id; form.name = data.name; dialogVisible.value = true; }; dialogType.value = type;
const resetForm = () => { if (formRef.value) formRef.value.resetFields(); form.name = ''; }; 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 () => { const handleSubmit = async () => {
if (!formRef.value) return; if (!formRef.value) return;
@ -685,45 +921,78 @@ const handleSubmit = async () => {
submitLoading.value = true; submitLoading.value = true;
try { try {
if (dialogMode.value === 'create') { if (dialogMode.value === 'create') {
if (dialogType.value === 'department') await createDepartment({ name: form.name, hospitalId: parentId.value }); if (dialogType.value === 'department')
else if (dialogType.value === 'group') await createGroup({ name: form.name, departmentId: parentId.value }); await createDepartment({
name: form.name,
hospitalId: parentId.value,
});
else if (dialogType.value === 'group')
await createGroup({
name: form.name,
departmentId: parentId.value,
});
ElMessage.success('创建成功'); ElMessage.success('创建成功');
} else { } else {
if (dialogType.value === 'hospital') await updateHospital(currentId.value, { name: form.name }); if (dialogType.value === 'hospital')
else if (dialogType.value === 'department') await updateDepartment(currentId.value, { name: form.name }); await updateHospital(currentId.value, { name: form.name });
else if (dialogType.value === 'group') await updateGroup(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('更新成功'); ElMessage.success('更新成功');
// Update activeNode locally if it's the one edited // Update activeNode locally if it's the one edited
if (activeNode.value && activeNode.value.id === currentId.value && activeNode.value.type === dialogType.value) { if (
activeNode.value &&
activeNode.value.id === currentId.value &&
activeNode.value.type === dialogType.value
) {
activeNode.value.name = form.name; activeNode.value.name = form.name;
} }
} }
dialogVisible.value = false; dialogVisible.value = false;
fetchTreeData(); fetchTreeData();
} catch (error) { console.error(error); } finally { submitLoading.value = false; } } catch (error) {
console.error(error);
} finally {
submitLoading.value = false;
}
} }
}); });
}; };
const handleDelete = (data) => { const handleDelete = (data) => {
const typeName = data.type === 'hospital' ? '医院' : (data.type === 'department' ? '科室' : '小组'); const typeName =
ElMessageBox.confirm(`确定要删除${typeName} "${data.name}" 吗?`, '警告', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }) data.type === 'hospital'
.then(async () => { ? '医院'
try { : data.type === 'department'
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); ElMessageBox.confirm(`确定要删除${typeName} "${data.name}" 吗?`, '警告', {
ElMessage.success('删除成功'); confirmButtonText: '确定',
if (activeNode.value && activeNode.value.key === data.key) { cancelButtonText: '取消',
activeNode.value = null; // Clear active node if deleted 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);
} }
fetchTreeData(); })
} catch (error) { console.error(error); } .catch(() => {});
}).catch(() => {});
}; };
onMounted(() => { fetchTreeData(); }); onMounted(() => {
fetchTreeData();
});
</script> </script>
<style scoped> <style scoped>
@ -731,7 +1000,8 @@ onMounted(() => { fetchTreeData(); });
padding: 20px; padding: 20px;
} }
.tree-card, .detail-card { .tree-card,
.detail-card {
border-radius: 8px; border-radius: 8px;
height: calc(100vh - 120px); height: calc(100vh - 120px);
display: flex; display: flex;
@ -846,10 +1116,19 @@ onMounted(() => { fetchTreeData(); });
font-size: 16px; font-size: 16px;
} }
.node-hospital .node-icon-wrapper { color: #409EFF; } .node-hospital .node-icon-wrapper {
.node-department .node-icon-wrapper { color: #67C23A; } color: #409eff;
.node-group .node-icon-wrapper { color: #E6A23C; } }
.node-user .node-icon-wrapper { color: #909399; font-size: 14px; } .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 { .node-label {
font-size: 14px; font-size: 14px;

View File

@ -11,7 +11,7 @@
v-model="searchForm.hospitalId" v-model="searchForm.hospitalId"
placeholder="系统管理员必须选择医院" placeholder="系统管理员必须选择医院"
clearable clearable
style="width: 240px;" style="width: 240px"
@change="handleSearchHospitalChange" @change="handleSearchHospitalChange"
> >
<el-option <el-option
@ -30,9 +30,13 @@
/> />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="handleSearch" icon="Search">查询</el-button> <el-button type="primary" @click="handleSearch" icon="Search"
>查询</el-button
>
<el-button @click="resetSearch" icon="Refresh">重置</el-button> <el-button @click="resetSearch" icon="Refresh">重置</el-button>
<el-button type="success" @click="openCreateDialog" icon="Plus">新增患者</el-button> <el-button type="success" @click="openCreateDialog" icon="Plus"
>新增患者</el-button
>
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
@ -42,10 +46,16 @@
type="warning" type="warning"
:closable="false" :closable="false"
title="系统管理员查询患者时必须先选择医院。" title="系统管理员查询患者时必须先选择医院。"
style="margin-bottom: 16px;" style="margin-bottom: 16px"
/> />
<el-table :data="tableData" v-loading="loading" border stripe style="width: 100%"> <el-table
:data="tableData"
v-loading="loading"
border
stripe
style="width: 100%"
>
<el-table-column prop="id" label="ID" width="80" align="center" /> <el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="name" label="姓名" min-width="120" /> <el-table-column prop="name" label="姓名" min-width="120" />
<el-table-column prop="phone" label="手机号" min-width="140" /> <el-table-column prop="phone" label="手机号" min-width="140" />
@ -62,7 +72,11 @@
</el-table-column> </el-table-column>
<el-table-column label="操作" width="260" fixed="right" align="center"> <el-table-column label="操作" width="260" fixed="right" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-button size="small" type="primary" @click="openRecordDialog(row)"> <el-button
size="small"
type="primary"
@click="openRecordDialog(row)"
>
详情 详情
</el-button> </el-button>
<el-button size="small" @click="openEditDialog(row)"> <el-button size="small" @click="openEditDialog(row)">
@ -114,7 +128,7 @@
filterable filterable
clearable clearable
placeholder="请选择归属医生(按科室/小组)" placeholder="请选择归属医生(按科室/小组)"
style="width: 100%;" style="width: 100%"
:disabled="userStore.role === 'DOCTOR'" :disabled="userStore.role === 'DOCTOR'"
/> />
</el-form-item> </el-form-item>
@ -122,7 +136,11 @@
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button> <el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit"> <el-button
type="primary"
:loading="submitLoading"
@click="handleSubmit"
>
确定 确定
</el-button> </el-button>
</div> </div>
@ -131,13 +149,27 @@
<el-dialog title="调压记录详情" v-model="recordDialogVisible" width="860px"> <el-dialog title="调压记录详情" v-model="recordDialogVisible" width="860px">
<el-descriptions :column="4" border class="mb-16"> <el-descriptions :column="4" border class="mb-16">
<el-descriptions-item label="患者">{{ currentPatientName || '-' }}</el-descriptions-item> <el-descriptions-item label="患者">{{
<el-descriptions-item label="手机号">{{ recordSummary.phone || '-' }}</el-descriptions-item> currentPatientName || '-'
<el-descriptions-item label="身份证">{{ recordSummary.idCardHash || '-' }}</el-descriptions-item> }}</el-descriptions-item>
<el-descriptions-item label="记录数">{{ recordList.length }}</el-descriptions-item> <el-descriptions-item label="手机号">{{
recordSummary.phone || '-'
}}</el-descriptions-item>
<el-descriptions-item label="身份证">{{
recordSummary.idCardHash || '-'
}}</el-descriptions-item>
<el-descriptions-item label="记录数">{{
recordList.length
}}</el-descriptions-item>
</el-descriptions> </el-descriptions>
<el-table :data="recordList" v-loading="recordLoading" border stripe max-height="520"> <el-table
:data="recordList"
v-loading="recordLoading"
border
stripe
max-height="520"
>
<el-table-column label="时间" width="180"> <el-table-column label="时间" width="180">
<template #default="{ row }"> <template #default="{ row }">
{{ new Date(row.occurredAt).toLocaleString() }} {{ new Date(row.occurredAt).toLocaleString() }}
@ -155,7 +187,8 @@
</el-table-column> </el-table-column>
<el-table-column label="压力变更" min-width="140"> <el-table-column label="压力变更" min-width="140">
<template #default="{ row }"> <template #default="{ row }">
{{ row.taskItem?.oldPressure ?? '-' }} -> {{ row.taskItem?.targetPressure ?? '-' }} {{ row.taskItem?.oldPressure ?? '-' }} ->
{{ row.taskItem?.targetPressure ?? '-' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="医院" min-width="140"> <el-table-column label="医院" min-width="140">
@ -260,11 +293,15 @@ const doctorTreeProps = {
}; };
const departmentNameMap = computed(() => { const departmentNameMap = computed(() => {
return Object.fromEntries((departments.value || []).map((item) => [item.id, item.name])); return Object.fromEntries(
(departments.value || []).map((item) => [item.id, item.name]),
);
}); });
const groupNameMap = computed(() => { const groupNameMap = computed(() => {
return Object.fromEntries((groups.value || []).map((item) => [item.id, item.name])); return Object.fromEntries(
(groups.value || []).map((item) => [item.id, item.name]),
);
}); });
const doctorTreeOptions = computed(() => { const doctorTreeOptions = computed(() => {
@ -316,16 +353,19 @@ const doctorTreeOptions = computed(() => {
}); });
}); });
return Array.from(deptMap.values()).sort((a, b) => String(a.label).localeCompare(String(b.label), 'zh-Hans-CN')); return Array.from(deptMap.values()).sort((a, b) =>
String(a.label).localeCompare(String(b.label), 'zh-Hans-CN'),
);
}); });
const applyFiltersAndPagination = () => { const applyFiltersAndPagination = () => {
const keyword = searchForm.keyword.trim(); const keyword = searchForm.keyword.trim();
const filtered = allPatients.value.filter((patient) => { const filtered = allPatients.value.filter((patient) => {
const hitKeyword = !keyword const hitKeyword =
|| patient.name?.includes(keyword) !keyword ||
|| patient.phone?.includes(keyword); patient.name?.includes(keyword) ||
patient.phone?.includes(keyword);
return hitKeyword; return hitKeyword;
}); });
@ -481,15 +521,11 @@ const handleSubmit = async () => {
}; };
const handleDelete = (row) => { const handleDelete = (row) => {
ElMessageBox.confirm( ElMessageBox.confirm(`确定要删除患者 "${row.name}" 吗?`, '警告', {
`确定要删除患者 "${row.name}" 吗?`, confirmButtonText: '确定',
'警告', cancelButtonText: '取消',
{ type: 'warning',
confirmButtonText: '确定', })
cancelButtonText: '取消',
type: 'warning',
},
)
.then(async () => { .then(async () => {
await deletePatient(row.id); await deletePatient(row.id);
ElMessage.success('删除成功'); ElMessage.success('删除成功');

View File

@ -36,7 +36,13 @@
</el-form> </el-form>
</div> </div>
<el-table :data="tableData" v-loading="loading" border stripe style="width: 100%"> <el-table
:data="tableData"
v-loading="loading"
border
stripe
style="width: 100%"
>
<el-table-column prop="id" label="ID" width="80" align="center" /> <el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="name" label="姓名" min-width="120" /> <el-table-column prop="name" label="姓名" min-width="120" />
<el-table-column prop="phone" label="手机号" min-width="150" /> <el-table-column prop="phone" label="手机号" min-width="150" />
@ -65,7 +71,9 @@
<el-table-column label="操作" width="260" fixed="right" align="center"> <el-table-column label="操作" width="260" fixed="right" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-button <el-button
v-if="row.role === 'ENGINEER' && userStore.role === 'SYSTEM_ADMIN'" v-if="
row.role === 'ENGINEER' && userStore.role === 'SYSTEM_ADMIN'
"
size="small" size="small"
type="warning" type="warning"
@click="openAssignDialog(row)" @click="openAssignDialog(row)"
@ -123,7 +131,11 @@
/> />
</el-form-item> </el-form-item>
<el-form-item label="角色" prop="role"> <el-form-item label="角色" prop="role">
<el-select v-model="form.role" placeholder="请选择角色" style="width: 100%;"> <el-select
v-model="form.role"
placeholder="请选择角色"
style="width: 100%"
>
<el-option label="系统管理员" value="SYSTEM_ADMIN" /> <el-option label="系统管理员" value="SYSTEM_ADMIN" />
<el-option label="医院管理员" value="HOSPITAL_ADMIN" /> <el-option label="医院管理员" value="HOSPITAL_ADMIN" />
<el-option label="科室主任" value="DIRECTOR" /> <el-option label="科室主任" value="DIRECTOR" />
@ -137,7 +149,7 @@
<el-select <el-select
v-model="form.hospitalId" v-model="form.hospitalId"
placeholder="请选择医院" placeholder="请选择医院"
style="width: 100%;" style="width: 100%"
> >
<el-option <el-option
v-for="hospital in hospitals" v-for="hospital in hospitals"
@ -148,11 +160,15 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="所属科室" prop="departmentId" v-if="needDepartment"> <el-form-item
label="所属科室"
prop="departmentId"
v-if="needDepartment"
>
<el-select <el-select
v-model="form.departmentId" v-model="form.departmentId"
placeholder="请选择科室" placeholder="请选择科室"
style="width: 100%;" style="width: 100%"
> >
<el-option <el-option
v-for="department in formDepartments" v-for="department in formDepartments"
@ -167,7 +183,7 @@
<el-select <el-select
v-model="form.groupId" v-model="form.groupId"
placeholder="请选择小组" placeholder="请选择小组"
style="width: 100%;" style="width: 100%"
> >
<el-option <el-option
v-for="group in formGroups" v-for="group in formGroups"
@ -181,7 +197,11 @@
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button> <el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading"> <el-button
type="primary"
@click="handleSubmit"
:loading="submitLoading"
>
确定 确定
</el-button> </el-button>
</div> </div>
@ -201,7 +221,7 @@
<el-select <el-select
v-model="assignHospitalId" v-model="assignHospitalId"
placeholder="请选择医院" placeholder="请选择医院"
style="width: 100%;" style="width: 100%"
> >
<el-option <el-option
v-for="hospital in hospitals" v-for="hospital in hospitals"
@ -215,7 +235,11 @@
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button @click="assignDialogVisible = false">取消</el-button> <el-button @click="assignDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleAssignSubmit" :loading="submitLoading"> <el-button
type="primary"
@click="handleAssignSubmit"
:loading="submitLoading"
>
确定 确定
</el-button> </el-button>
</div> </div>
@ -286,9 +310,7 @@ const needHospital = computed(() => form.role && form.role !== 'SYSTEM_ADMIN');
const needDepartment = computed(() => const needDepartment = computed(() =>
['DIRECTOR', 'LEADER', 'DOCTOR'].includes(form.role), ['DIRECTOR', 'LEADER', 'DOCTOR'].includes(form.role),
); );
const needGroup = computed(() => const needGroup = computed(() => ['LEADER', 'DOCTOR'].includes(form.role));
['LEADER', 'DOCTOR'].includes(form.role),
);
const rules = computed(() => ({ const rules = computed(() => ({
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }], name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
@ -299,9 +321,9 @@ const rules = computed(() => ({
password: isEdit.value password: isEdit.value
? [] ? []
: [ : [
{ required: true, message: '请输入密码', trigger: 'blur' }, { required: true, message: '请输入密码', trigger: 'blur' },
{ min: 8, message: '密码长度至少为 8 位', trigger: 'blur' }, { min: 8, message: '密码长度至少为 8 位', trigger: 'blur' },
], ],
role: [{ required: true, message: '请选择角色', trigger: 'change' }], role: [{ required: true, message: '请选择角色', trigger: 'change' }],
hospitalId: needHospital.value hospitalId: needHospital.value
? [{ required: true, message: '请选择所属医院', trigger: 'change' }] ? [{ required: true, message: '请选择所属医院', trigger: 'change' }]
@ -332,14 +354,14 @@ const getRoleTagType = (role) => {
return 'info'; return 'info';
}; };
const hospitalMap = computed(() => const hospitalMap = computed(
new Map(hospitals.value.map((item) => [item.id, item.name])), () => new Map(hospitals.value.map((item) => [item.id, item.name])),
); );
const departmentMap = computed(() => const departmentMap = computed(
new Map(departments.value.map((item) => [item.id, item.name])), () => new Map(departments.value.map((item) => [item.id, item.name])),
); );
const groupMap = computed(() => const groupMap = computed(
new Map(groups.value.map((item) => [item.id, item.name])), () => new Map(groups.value.map((item) => [item.id, item.name])),
); );
const resolveHospitalName = (id) => { const resolveHospitalName = (id) => {
@ -570,15 +592,11 @@ const handleSubmit = async () => {
}; };
const handleDelete = (row) => { const handleDelete = (row) => {
ElMessageBox.confirm( ElMessageBox.confirm(`确定要删除用户 "${row.name}" 吗?`, '警告', {
`确定要删除用户 "${row.name}" 吗?`, confirmButtonText: '确定',
'警告', cancelButtonText: '取消',
{ type: 'warning',
confirmButtonText: '确定', })
cancelButtonText: '取消',
type: 'warning',
},
)
.then(async () => { .then(async () => {
await deleteUser(row.id); await deleteUser(row.id);
ElMessage.success('删除成功'); ElMessage.success('删除成功');
@ -601,7 +619,10 @@ const handleAssignSubmit = async () => {
submitLoading.value = true; submitLoading.value = true;
try { try {
await assignEngineerHospital(currentAssignUser.value.id, assignHospitalId.value); await assignEngineerHospital(
currentAssignUser.value.id,
assignHospitalId.value,
);
ElMessage.success('分配成功'); ElMessage.success('分配成功');
assignDialogVisible.value = false; assignDialogVisible.value = false;
await fetchCommonData(); await fetchCommonData();