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

514 lines
14 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="departments-container">
<el-card>
<template #header>
<div class="card-header">
<span
>科室管理
{{ currentHospitalName ? `(${currentHospitalName})` : '' }}</span
>
<el-button
v-if="currentHospitalName"
@click="clearHospitalFilter"
type="info"
size="small"
>清除医院筛选</el-button
>
</div>
</template>
<!-- Header / Actions -->
<div class="header-actions">
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item
label="所属医院"
v-if="
userStore.role === 'SYSTEM_ADMIN' && !currentHospitalIdFromQuery
"
>
<el-select
v-model="searchForm.hospitalId"
placeholder="请选择医院"
clearable
@change="fetchData"
>
<el-option
v-for="h in hospitals"
:key="h.id"
:label="h.name"
:value="h.id"
/>
</el-select>
</el-form-item>
<el-form-item label="科室名称">
<el-input
v-model="searchForm.keyword"
placeholder="请输入关键词"
clearable
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="fetchData" icon="Search"
>查询</el-button
>
<el-button @click="resetSearch" icon="Refresh">重置</el-button>
<el-button
v-if="canCreateDepartment"
type="success"
@click="openCreateDialog"
icon="Plus"
>新增科室</el-button
>
</el-form-item>
</el-form>
</div>
<!-- Table -->
<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="name" label="科室名称" min-width="150" />
<el-table-column
prop="hospital.name"
label="所属医院"
min-width="200"
v-if="userStore.role === 'SYSTEM_ADMIN'"
/>
<el-table-column label="科室主任" min-width="180">
<template #default="{ row }">
{{ getDirectorDisplay(row.id) }}
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" width="180">
<template #default="{ row }">
{{ new Date(row.createdAt).toLocaleString() }}
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right" align="center">
<template #default="{ row }">
<el-button size="small" @click="goToGroups(row)"
>管理小组</el-button
>
<el-button
v-if="canEditDepartment"
size="small"
type="primary"
@click="openEditDialog(row)"
>编辑</el-button
>
<el-button
v-if="canDeleteDepartment"
size="small"
type="danger"
@click="handleDelete(row)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<!-- Pagination -->
<div class="pagination-container">
<el-pagination
v-model:current-page="page"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
background
layout="total, sizes, prev, pager, next, jumper"
@size-change="fetchData"
@current-change="fetchData"
/>
</div>
</el-card>
<!-- Dialog for Create / Edit -->
<el-dialog
:title="isEdit ? '编辑科室' : '新增科室'"
v-model="dialogVisible"
width="500px"
@close="resetForm"
>
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<el-form-item
label="所属医院"
prop="hospitalId"
v-if="userStore.role === 'SYSTEM_ADMIN'"
>
<el-select
v-model="form.hospitalId"
placeholder="请选择所属医院"
style="width: 100%"
@change="handleFormHospitalChange"
>
<el-option
v-for="h in hospitals"
:key="h.id"
:label="h.name"
:value="h.id"
/>
</el-select>
</el-form-item>
<el-form-item label="科室名称" prop="name">
<el-input v-model="form.name" placeholder="请输入科室名称" />
</el-form-item>
<el-form-item label="科室主任" v-if="canAssignDirectorInDialog">
<el-select
v-model="form.directorUserId"
placeholder="可选:选择后将任命为科室主任"
clearable
filterable
style="width: 100%"
>
<el-option
v-for="user in directorOptions"
:key="user.id"
:label="`${user.name}${user.phone} / ${getRoleName(user.role)}`"
:value="user.id"
/>
</el-select>
</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>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus';
import {
getDepartments,
createDepartment,
updateDepartment,
deleteDepartment,
getHospitals,
} from '../../api/organization';
import { getUsers, updateUser } from '../../api/users';
import { useUserStore } from '../../store/user';
const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
// --- State ---
const loading = ref(false);
const tableData = ref([]);
const total = ref(0);
const page = ref(1);
const pageSize = ref(10);
const hospitals = ref([]);
const directorNameMap = ref({});
const directorOptions = ref([]);
const roleMap = {
DIRECTOR: '科室主任',
LEADER: '小组组长',
DOCTOR: '医生',
};
const getRoleName = (role) => roleMap[role] || role;
const currentHospitalIdFromQuery = computed(() => {
return route.query.hospitalId ? parseInt(route.query.hospitalId) : null;
});
const currentHospitalName = computed(() => {
return route.query.hospitalName || '';
});
const searchForm = reactive({
keyword: '',
hospitalId: null,
});
// Dialog State
const dialogVisible = ref(false);
const isEdit = ref(false);
const submitLoading = ref(false);
const formRef = ref(null);
const currentId = ref(null);
const form = reactive({
hospitalId: null,
name: '',
directorUserId: null,
});
const rules = computed(() => ({
hospitalId:
userStore.role === 'SYSTEM_ADMIN'
? [{ required: true, message: '请选择所属医院', trigger: 'change' }]
: [],
name: [{ required: true, message: '请输入科室名称', trigger: 'blur' }],
}));
const canCreateDepartment = computed(() =>
['SYSTEM_ADMIN', 'HOSPITAL_ADMIN'].includes(userStore.role),
);
const canEditDepartment = computed(() =>
['SYSTEM_ADMIN', 'HOSPITAL_ADMIN', 'DIRECTOR', 'LEADER'].includes(
userStore.role,
),
);
const canDeleteDepartment = computed(() =>
['SYSTEM_ADMIN', 'HOSPITAL_ADMIN'].includes(userStore.role),
);
const canAssignDirectorInDialog = computed(() =>
['SYSTEM_ADMIN', 'HOSPITAL_ADMIN'].includes(userStore.role),
);
// --- Methods ---
const fetchHospitals = async () => {
try {
const res = await getHospitals({ pageSize: 100 }); // Fetch all or sufficiently large number for dropdown
hospitals.value = res.list || [];
} catch (error) {
console.error('Failed to fetch hospitals', error);
}
};
const fetchData = async () => {
loading.value = true;
try {
const activeHospitalId =
currentHospitalIdFromQuery.value || searchForm.hospitalId;
const [departmentRes, directorRes] = await Promise.all([
getDepartments({
page: page.value,
pageSize: pageSize.value,
keyword: searchForm.keyword || undefined,
hospitalId: activeHospitalId || undefined,
}),
getUsers({ role: 'DIRECTOR' }),
]);
const directorMap = {};
(directorRes.list || []).forEach((user) => {
if (!user.departmentId) {
return;
}
if (!directorMap[user.departmentId]) {
directorMap[user.departmentId] = [];
}
directorMap[user.departmentId].push(user.name);
});
directorNameMap.value = directorMap;
tableData.value = departmentRes.list || [];
total.value = departmentRes.total || 0;
} catch (error) {
console.error('Failed to fetch departments', error);
} finally {
loading.value = false;
}
};
const getDirectorDisplay = (departmentId) => {
const directors = directorNameMap.value[departmentId] || [];
return directors.length > 0 ? directors.join('、') : '未设置';
};
const resetSearch = () => {
searchForm.keyword = '';
if (!currentHospitalIdFromQuery.value) {
searchForm.hospitalId = null;
}
page.value = 1;
fetchData();
};
const clearHospitalFilter = () => {
router.replace({ path: '/organization/departments' });
};
const goToGroups = (row) => {
router.push({
path: '/organization/groups',
query: {
departmentId: row.id,
departmentName: row.name,
hospitalId: row.hospitalId,
},
});
};
const openCreateDialog = () => {
isEdit.value = false;
currentId.value = null;
form.hospitalId =
userStore.role === 'SYSTEM_ADMIN'
? currentHospitalIdFromQuery.value || searchForm.hospitalId || null
: userStore.userInfo?.hospitalId;
form.directorUserId = null;
loadDirectorOptions(form.hospitalId);
dialogVisible.value = true;
};
const openEditDialog = (row) => {
isEdit.value = true;
currentId.value = row.id;
form.name = row.name;
form.hospitalId = row.hospitalId;
form.directorUserId = null;
loadDirectorOptions(row.hospitalId, row.id);
dialogVisible.value = true;
};
const resetForm = () => {
if (formRef.value) {
formRef.value.resetFields();
}
form.name = '';
form.hospitalId = null;
form.directorUserId = null;
};
const handleFormHospitalChange = (hospitalId) => {
form.directorUserId = null;
loadDirectorOptions(hospitalId);
};
const loadDirectorOptions = async (hospitalId, departmentId) => {
if (!canAssignDirectorInDialog.value || !hospitalId) {
directorOptions.value = [];
return;
}
const userRes = await getUsers();
const users = Array.isArray(userRes?.list) ? userRes.list : [];
directorOptions.value = users.filter((user) => {
if (user.role !== 'DIRECTOR') {
return false;
}
if (user.hospitalId !== hospitalId) {
return false;
}
if (departmentId == null) {
return true;
}
return user.departmentId == null || user.departmentId === departmentId;
});
};
const handleSubmit = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid) => {
if (valid) {
submitLoading.value = true;
try {
let targetDepartmentId = null;
let targetHospitalId = form.hospitalId;
if (isEdit.value) {
// 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,
});
targetDepartmentId = updated?.id ?? currentId.value;
targetHospitalId = updated?.hospitalId ?? form.hospitalId;
ElMessage.success('更新成功');
} else {
const created = await createDepartment({
hospitalId: form.hospitalId,
name: form.name,
});
targetDepartmentId = created?.id;
targetHospitalId = created?.hospitalId ?? form.hospitalId;
ElMessage.success('创建成功');
}
if (
canAssignDirectorInDialog.value &&
form.directorUserId &&
targetDepartmentId &&
targetHospitalId
) {
await updateUser(form.directorUserId, {
role: 'DIRECTOR',
hospitalId: targetHospitalId,
departmentId: targetDepartmentId,
groupId: null,
});
ElMessage.success('科室主任已设置');
}
dialogVisible.value = false;
fetchData();
} catch (error) {
console.error('Submit failed', error);
} finally {
submitLoading.value = false;
}
}
});
};
const handleDelete = (row) => {
ElMessageBox.confirm(`确定要删除科室 "${row.name}" 吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(async () => {
try {
await deleteDepartment(row.id);
ElMessage.success('删除成功');
fetchData();
} catch (error) {
console.error('Delete failed', error);
}
})
.catch(() => {});
};
// --- Lifecycle ---
onMounted(async () => {
await fetchHospitals();
fetchData();
});
// Watch for route query changes to refetch if navigating from hospital list again
import { watch } from 'vue';
watch(
() => route.query,
() => {
page.value = 1;
fetchData();
},
);
</script>
<style scoped>
.departments-container {
padding: 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-actions {
margin-bottom: 20px;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
</style>