514 lines
14 KiB
Vue
514 lines
14 KiB
Vue
<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>
|