EL b527256874 feat(auth-org): 强化用户权限边界并完善组织负责人配置展示
feat(admin-ui): 医院管理显示医院管理员并限制候选角色
feat(security): 关闭注册入口,新增 system-admin 创建链路与数据脱敏
2026-03-18 17:05:36 +08:00

320 lines
9.4 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="hospitals-container">
<el-card>
<!-- Header / Actions -->
<div class="header-actions">
<el-form :inline="true" :model="searchForm" class="search-form">
<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="userStore.role === 'SYSTEM_ADMIN'" 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="200" />
<el-table-column prop="adminDisplay" label="医院管理员" min-width="220">
<template #default="{ row }">
{{ row.adminDisplay || '未设置' }}
</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="250" fixed="right" align="center">
<template #default="{ row }">
<el-button size="small" @click="goToDepartments(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>
</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="name">
<el-input v-model="form.name" placeholder="请输入医院名称" />
</el-form-item>
<el-form-item label="医院管理员" v-if="userStore.role === 'SYSTEM_ADMIN'">
<el-select
v-model="form.adminUserId"
placeholder="可选:选择后将任命为医院管理员"
clearable
filterable
style="width: 100%;"
>
<el-option
v-for="user in hospitalAdminOptions"
: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 } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus';
import { getHospitals, createHospital, updateHospital, deleteHospital } from '../../api/organization';
import { getUsers, updateUser } from '../../api/users';
import { useUserStore } from '../../store/user';
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 hospitalAdminOptions = ref([]);
const roleMap = {
SYSTEM_ADMIN: '系统管理员',
HOSPITAL_ADMIN: '医院管理员',
DIRECTOR: '科室主任',
LEADER: '小组组长',
DOCTOR: '医生',
ENGINEER: '工程师',
};
const getRoleName = (role) => roleMap[role] || role;
const searchForm = reactive({
keyword: ''
});
// 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({
name: '',
adminUserId: null,
});
const rules = {
name: [{ required: true, message: '请输入医院名称', trigger: 'blur' }]
};
// --- Methods ---
const fetchData = async () => {
loading.value = true;
try {
const res = await getHospitals({
page: page.value,
pageSize: pageSize.value,
keyword: searchForm.keyword || undefined
});
let hospitalAdminNameMap = {};
try {
const userRes = await getUsers();
const users = Array.isArray(userRes?.list) ? userRes.list : [];
hospitalAdminNameMap = users.reduce((acc, user) => {
if (user.role !== 'HOSPITAL_ADMIN' || !user.hospitalId) {
return acc;
}
if (!acc[user.hospitalId]) {
acc[user.hospitalId] = [];
}
acc[user.hospitalId].push(user.name || '-');
return acc;
}, {});
} catch (error) {
console.error('Failed to fetch hospital admins', error);
}
tableData.value = (res.list || []).map((hospital) => ({
...hospital,
adminDisplay: (hospitalAdminNameMap[hospital.id] || []).join('、') || '未设置',
}));
total.value = res.total || 0;
} catch (error) {
console.error('Failed to fetch hospitals', error);
} finally {
loading.value = false;
}
};
const resetSearch = () => {
searchForm.keyword = '';
page.value = 1;
fetchData();
};
const openCreateDialog = () => {
isEdit.value = false;
currentId.value = null;
loadHospitalAdminOptions();
dialogVisible.value = true;
};
const openEditDialog = (row) => {
isEdit.value = true;
currentId.value = row.id;
form.name = row.name;
loadHospitalAdminOptions(row.id);
dialogVisible.value = true;
};
const resetForm = () => {
if (formRef.value) {
formRef.value.resetFields();
}
form.name = '';
form.adminUserId = null;
};
const loadHospitalAdminOptions = async (hospitalId) => {
if (userStore.role !== 'SYSTEM_ADMIN') {
hospitalAdminOptions.value = [];
return;
}
const userRes = await getUsers();
const users = Array.isArray(userRes?.list) ? userRes.list : [];
hospitalAdminOptions.value = users.filter((user) => {
// 仅允许选择“医院管理员”角色,避免误选普通人员。
if (user.role !== 'HOSPITAL_ADMIN') {
return false;
}
if (!hospitalId) {
return true;
}
return user.hospitalId == null || user.hospitalId === hospitalId;
});
};
const handleSubmit = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid) => {
if (valid) {
submitLoading.value = true;
try {
let targetHospitalId = null;
if (isEdit.value) {
const updated = await updateHospital(currentId.value, { name: form.name });
targetHospitalId = updated?.id ?? currentId.value;
ElMessage.success('更新成功');
} else {
const created = await createHospital({ name: form.name });
targetHospitalId = created?.id;
ElMessage.success('创建成功');
}
if (form.adminUserId && targetHospitalId) {
const selectedAdmin = hospitalAdminOptions.value.find(
(user) => user.id === form.adminUserId,
);
if (!selectedAdmin || selectedAdmin.role !== 'HOSPITAL_ADMIN') {
ElMessage.error('仅可选择医院管理员角色人员');
return;
}
await updateUser(form.adminUserId, {
role: 'HOSPITAL_ADMIN',
hospitalId: targetHospitalId,
departmentId: null,
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 deleteHospital(row.id);
ElMessage.success('删除成功');
fetchData();
} catch (error) {
console.error('Delete failed', error);
}
}).catch(() => {});
};
const goToDepartments = (row) => {
router.push({
path: '/organization/departments',
query: { hospitalId: row.id, hospitalName: row.name }
});
};
// --- Lifecycle ---
onMounted(() => {
fetchData();
});
</script>
<style scoped>
.hospitals-container {
padding: 0;
}
.header-actions {
margin-bottom: 20px;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
</style>