diff --git a/clinical-web/README.md b/clinical-web/README.md index 1348cce..7a8bbf2 100644 --- a/clinical-web/README.md +++ b/clinical-web/README.md @@ -84,7 +84,7 @@ TanStack Vue Query、PDF 预览、报表图表、自动化测试和签字板适 ## 当前状态 -已完成 Vite 基础初始化、路由、Pinia、原型主题 CSS、登录页、首页、签署工作台、签署记录查询、文档库管理页面和「报表分析」四个菜单(签署总览 / 签署量分析 / 签署时效分析 / 患者结构分析)。角色权限与系统设置中的菜单设置已接入 MEDISIGN RBAC 接口;导航、路由和页面操作按授权资源树控制,角色数据范围在角色配置中维护,员工页面只负责绑定角色。组织架构页按机构→院区→科室分组展示员工列表,新增院区默认归属“新华院区”,支持节点筛选、员工维护和组织节点维护。系统参数卡片仍保留 Mock,真实签字板/线上签名回调和打印服务仍需设备或后端能力。 +已完成 Vite 基础初始化、路由、Pinia、原型主题 CSS、登录页、首页、签署工作台、签署记录查询、文档库管理页面和「报表分析」四个菜单(签署总览 / 签署量分析 / 签署时效分析 / 患者结构分析)。角色权限与系统设置中的菜单设置已接入 MEDISIGN RBAC 接口;导航、路由和页面操作按授权资源树控制,角色数据范围在角色配置中维护,员工页面只负责绑定角色。组织架构页按机构→院区→科室分组展示员工列表,新增院区默认归属“新华院区”,支持节点筛选、员工维护和组织节点维护。院区与科室的 `DELETE` 接口表示停用当前节点,不级联停用或删除下级;组织树默认筛选“启用中”,可切换为“已停用”或“全部状态”;停用节点在组织树中标记“已停用”,可通过编辑重新启用。操作成功但列表刷新失败时会提示刷新失败,保留已成功的停用状态。系统参数卡片仍保留 Mock,真实签字板/线上签名回调和打印服务仍需设备或后端能力。 文档库管理的真实模式已打通「模板 → 版本 → 正文」三段: diff --git a/clinical-web/src/api/management/organization.ts b/clinical-web/src/api/management/organization.ts index 83438e8..36c7098 100644 --- a/clinical-web/src/api/management/organization.ts +++ b/clinical-web/src/api/management/organization.ts @@ -154,7 +154,8 @@ export async function updateCampus( return mapCampus(unwrapApiResponse(response)) } -export async function deleteCampus(id: string): Promise { +/** DELETE 接口将院区停用,保留记录及下属科室。 */ +export async function disableCampus(id: string): Promise { if (useMockData) { throw new Error('Mock 模式不执行院区写操作') } @@ -192,7 +193,8 @@ export async function updateDepartment( return mapDepartment(unwrapApiResponse(response)) } -export async function deleteDepartment(id: string): Promise { +/** DELETE 接口将当前科室停用,保留记录及下级科室。 */ +export async function disableDepartment(id: string): Promise { if (useMockData) { throw new Error('Mock 模式不执行科室写操作') } diff --git a/clinical-web/src/views/management/documents/index.vue b/clinical-web/src/views/management/documents/index.vue index 977fed7..5ade7fe 100644 --- a/clinical-web/src/views/management/documents/index.vue +++ b/clinical-web/src/views/management/documents/index.vue @@ -121,7 +121,7 @@ const libraryCampusId = computed(() => (allCampusesSelected.value ? '' : appStor const filters = reactive({ keyword: '', department: null, - status: 'all', + status: '启用中', }) /** 预览弹窗 */ @@ -1190,8 +1190,8 @@ async function resetMockData() { function clearFilters() { filters.keyword = '' filters.department = null - // status 也要一起重置:只按状态页签筛出空结果时,不清它等于「清了个寂寞」 - filters.status = 'all' + // 清除筛选后恢复默认的启用中状态。 + filters.status = '启用中' // 院区不在这里重置:它是页面的取数口径(文书库按院区向服务端取数), // 不是「筛掉一部分已加载数据」的筛选条件,清掉它得重新拉一遍文库 } @@ -1302,7 +1302,7 @@ onMounted(() => { @@ -274,6 +323,24 @@ function isDescendantSelected(department: DepartmentRecord): boolean { border-color: #d3e7ee; } +.org-status-filter { + display: flex; + gap: 8px; + align-items: center; + margin: 10px 0; + color: var(--mut); + font-size: 12px; +} + +.org-status-filter select { + flex: 1; + padding: 5px 8px; + color: var(--ink); + background: #fff; + border: 1px solid var(--line); + border-radius: 6px; +} + .org-tree { display: grid; gap: 4px; @@ -408,7 +475,7 @@ function isDescendantSelected(department: DepartmentRecord): boolean { } .org-tree__edit, -.org-tree__remove { +.org-tree__disable { padding: 3px 7px; font-size: 10.5px; background: #fff; @@ -421,19 +488,39 @@ function isDescendantSelected(department: DepartmentRecord): boolean { border: 1px solid #d3e7ee; } -.org-tree__remove { +.org-tree__disable { margin-right: 6px; color: var(--err); border: 1px solid #f0cdcd; } .org-tree__node:hover .org-tree__edit, -.org-tree__node:hover .org-tree__remove, +.org-tree__node:hover .org-tree__disable, .org-tree__node:focus-within .org-tree__edit, -.org-tree__node:focus-within .org-tree__remove { +.org-tree__node:focus-within .org-tree__disable { opacity: 1; } +.org-tree__node.is-disabled { + color: var(--mut); +} + +.org-tree__status { + flex: 0 0 auto; + padding: 2px 5px; + color: var(--mut); + font-size: 10px; + font-weight: 400; + white-space: nowrap; + background: #f0f2f4; + border: 1px solid var(--line); + border-radius: 4px; +} + +.org-tree__disable:disabled { + cursor: not-allowed; +} + .org-tree__empty { padding: 10px; margin: 0; diff --git a/clinical-web/src/views/management/users/index.vue b/clinical-web/src/views/management/users/index.vue index 5d763c2..824a2ac 100644 --- a/clinical-web/src/views/management/users/index.vue +++ b/clinical-web/src/views/management/users/index.vue @@ -5,8 +5,8 @@ import { ElMessage, ElMessageBox } from 'element-plus' import { createCampus, createDepartment, - deleteCampus, - deleteDepartment, + disableCampus, + disableDepartment, getAllDepartments, getCampuses, updateCampus, @@ -68,6 +68,7 @@ const organizationDialogVisible = ref(false) const organizationDialogKind = ref<'campus' | 'department'>('campus') const organizationDialogInitial = ref(null) const organizationSaving = ref(false) +const organizationDisabling = ref(false) let userRequestId = 0 const canEditUsers = computed(() => isUserMockEnabled || loginStore.hasPermission('system:org:emp')) @@ -302,7 +303,7 @@ function mapMockUsers() { userTotal.value = userRows.value.length } -async function loadUsers() { +async function loadUsers(notifyOnError = true): Promise { const requestId = ++userRequestId loading.value = true try { @@ -321,7 +322,7 @@ async function loadUsers() { return matchesCampus && matchesDepartment && matchesKeyword }) userTotal.value = filtered.length - return + return true } const result = await getUsers({ @@ -332,7 +333,7 @@ async function loadUsers() { campusId: selectedCampusId.value || undefined, departmentId: selectedDepartmentId.value ?? undefined, }) - if (requestId !== userRequestId) return + if (requestId !== userRequestId) return false const campusNames = new Map(campuses.value.map((campus) => [campus.id, campus.name])) const departmentNames = new Map( @@ -340,17 +341,20 @@ async function loadUsers() { ) userRows.value = result.records.map((user) => mapUserRow(user, campusNames, departmentNames)) userTotal.value = result.total + return true } catch { - if (requestId !== userRequestId) return + if (requestId !== userRequestId) return false userRows.value = [] userTotal.value = 0 - ElMessage.error('员工列表加载失败,请稍后重试') + if (notifyOnError) ElMessage.error('员工列表加载失败,请稍后重试') + return false } finally { if (requestId === userRequestId) loading.value = false } } -async function loadOrganization() { +async function loadOrganization(notifyOnError = true): Promise { + let complete = true loading.value = true try { if (isUserMockEnabled) { @@ -363,15 +367,16 @@ async function loadOrganization() { ]) if (campusResult.status === 'rejected') throw campusResult.reason campuses.value = campusResult.value - departments.value = departmentResult.status === 'fulfilled' ? departmentResult.value : [] + if (departmentResult.status === 'fulfilled') departments.value = departmentResult.value + else complete = false roleOptions.value = rolesResult.status === 'fulfilled' ? rolesResult.value : [] roleColorMap.value = Object.fromEntries( roleOptions.value.map((role) => [role.name, role.color ?? '#8a9aa3']), ) - if (canAssignRoles.value && rolesResult.status === 'rejected') { + if (canAssignRoles.value && rolesResult.status === 'rejected' && notifyOnError) { ElMessage.warning('员工角色选项加载失败,角色绑定暂不可用') } - if (departmentResult.status === 'rejected') { + if (departmentResult.status === 'rejected' && notifyOnError) { ElMessage.warning('科室架构加载失败,员工科室筛选暂不可用') } } @@ -390,10 +395,12 @@ async function loadOrganization() { campusCollapsed.value = false if (preferredCampus) appStore.selectCampus(preferredCampus) userPage.value = 1 - await loadUsers() + const usersLoaded = await loadUsers(notifyOnError) + return complete && usersLoaded } catch { loading.value = false - ElMessage.error('组织架构加载失败,请稍后重试') + if (notifyOnError) ElMessage.error('组织架构加载失败,请稍后重试') + return false } } @@ -647,7 +654,6 @@ async function saveOrganizationNode(form: OrganizationNodeSave) { const campus = form.id ? await updateCampus(form.id, form.payload) : await createCampus(form.payload) - await appStore.loadCampuses(true) if (!form.id) selectedCampusId.value = campus.id } else { const department = form.id @@ -657,8 +663,9 @@ async function saveOrganizationNode(form: OrganizationNodeSave) { if (!form.id) selectedDepartmentId.value = department.id } organizationDialogVisible.value = false - await loadOrganization() - ElMessage.success(form.id ? '组织信息已更新' : '组织节点已创建') + const refreshed = await refreshOrganizationAfterMutation(form.kind) + if (refreshed) ElMessage.success(form.id ? '组织信息已更新' : '组织节点已创建') + else ElMessage.warning('组织信息已保存,但页面刷新失败,请刷新页面重试') } catch { ElMessage.error('组织信息保存失败,请检查编码是否重复或所属院区是否有效') } finally { @@ -666,54 +673,77 @@ async function saveOrganizationNode(form: OrganizationNodeSave) { } } -async function removeCampus(campus: CampusRecord) { - if (!canManageCampuses.value) return - const departmentCount = departments.value.filter( - (department) => department.campusId === campus.id, - ).length - const confirmed = await ElMessageBox.confirm( - departmentCount - ? `删除院区“${campus.name}”会同时移除其下 ${departmentCount} 个科室,且无法恢复,是否继续?` - : `删除院区“${campus.name}”后无法恢复,是否继续?`, - '删除院区', - { type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' }, - ) - .then(() => true) - .catch(() => false) - if (!confirmed) return - +async function refreshOrganizationAfterMutation(kind: 'campus' | 'department'): Promise { try { - await deleteCampus(campus.id) - if (selectedCampusId.value === campus.id) selectedDepartmentId.value = null - await appStore.loadCampuses(true) - await loadOrganization() - ElMessage.success('院区已删除') - } catch (error) { - ElMessage.error(toUserMessage(error, '院区删除失败,请稍后重试')) + if (kind === 'campus') await appStore.loadCampuses(true) + const refreshed = await loadOrganization(false) + return refreshed && (kind !== 'campus' || !appStore.campusLoadFailed) + } catch { + return false } } -async function removeDepartment(department: DepartmentRecord) { - if (!canManageDepartments.value) return - const childCount = departments.value.filter((item) => item.parentId === department.id).length - const confirmed = await ElMessageBox.confirm( - childCount - ? `删除科室“${department.name}”会同时移除其下 ${childCount} 个下级科室,且无法恢复,是否继续?` - : `删除科室“${department.name}”后无法恢复,是否继续?`, - '删除科室', - { type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' }, - ) - .then(() => true) - .catch(() => false) - if (!confirmed) return - +async function handleDisableCampus(campus: CampusRecord) { + if (!canManageCampuses.value || campus.status === 'DISABLED' || organizationDisabling.value) + return + organizationDisabling.value = true try { - await deleteDepartment(department.id) + const confirmed = await ElMessageBox.confirm( + `停用院区“${campus.name}”只修改该院区的状态,不会停用或删除下属科室。组织记录和历史数据保留,可通过编辑重新启用。是否继续?`, + '停用院区', + { type: 'warning', confirmButtonText: '停用', cancelButtonText: '取消' }, + ) + .then(() => true) + .catch(() => false) + if (!confirmed) return + + try { + await disableCampus(campus.id) + } catch (error) { + ElMessage.error(toUserMessage(error, '院区停用失败,请稍后重试')) + return + } + campus.status = 'DISABLED' + if (selectedCampusId.value === campus.id) selectedDepartmentId.value = null + const refreshed = await refreshOrganizationAfterMutation('campus') + if (refreshed) ElMessage.success('院区已停用') + else ElMessage.warning('院区已停用,但页面刷新失败,请刷新页面重试') + } finally { + organizationDisabling.value = false + } +} + +async function handleDisableDepartment(department: DepartmentRecord) { + if ( + !canManageDepartments.value || + department.status === 'DISABLED' || + organizationDisabling.value + ) + return + organizationDisabling.value = true + try { + const confirmed = await ElMessageBox.confirm( + `停用科室“${department.name}”只修改当前科室的状态,不会停用或删除下级科室。组织记录和历史数据保留,可通过编辑重新启用。是否继续?`, + '停用科室', + { type: 'warning', confirmButtonText: '停用', cancelButtonText: '取消' }, + ) + .then(() => true) + .catch(() => false) + if (!confirmed) return + + try { + await disableDepartment(department.id) + } catch (error) { + ElMessage.error(toUserMessage(error, '科室停用失败,请稍后重试')) + return + } + department.status = 'DISABLED' if (selectedDepartmentId.value === department.id) selectedDepartmentId.value = null - await loadOrganization() - ElMessage.success('科室已删除') - } catch (error) { - ElMessage.error(toUserMessage(error, '科室删除失败,请稍后重试')) + const refreshed = await refreshOrganizationAfterMutation('department') + if (refreshed) ElMessage.success('科室已停用') + else ElMessage.warning('科室已停用,但页面刷新失败,请刷新页面重试') + } finally { + organizationDisabling.value = false } } @@ -771,14 +801,15 @@ onMounted(loadOrganization) :collapsed="campusCollapsed" :can-manage-campuses="canManageCampuses" :can-manage-departments="canManageDepartments" + :disabling="organizationDisabling" @select-campus="selectCampus" @select-department="selectDepartment" @add-campus="openAddCampus" @add-department="openAddDepartment" @edit-campus="editCampus" @edit-department="editDepartment" - @delete-campus="removeCampus" - @delete-department="removeDepartment" + @disable-campus="handleDisableCampus" + @disable-department="handleDisableDepartment" />