fix(web): 修正组织停用行为并默认筛选启用中文档
This commit is contained in:
@@ -84,7 +84,7 @@ TanStack Vue Query、PDF 预览、报表图表、自动化测试和签字板适
|
|||||||
|
|
||||||
## 当前状态
|
## 当前状态
|
||||||
|
|
||||||
已完成 Vite 基础初始化、路由、Pinia、原型主题 CSS、登录页、首页、签署工作台、签署记录查询、文档库管理页面和「报表分析」四个菜单(签署总览 / 签署量分析 / 签署时效分析 / 患者结构分析)。角色权限与系统设置中的菜单设置已接入 MEDISIGN RBAC 接口;导航、路由和页面操作按授权资源树控制,角色数据范围在角色配置中维护,员工页面只负责绑定角色。组织架构页按机构→院区→科室分组展示员工列表,新增院区默认归属“新华院区”,支持节点筛选、员工维护和组织节点维护。系统参数卡片仍保留 Mock,真实签字板/线上签名回调和打印服务仍需设备或后端能力。
|
已完成 Vite 基础初始化、路由、Pinia、原型主题 CSS、登录页、首页、签署工作台、签署记录查询、文档库管理页面和「报表分析」四个菜单(签署总览 / 签署量分析 / 签署时效分析 / 患者结构分析)。角色权限与系统设置中的菜单设置已接入 MEDISIGN RBAC 接口;导航、路由和页面操作按授权资源树控制,角色数据范围在角色配置中维护,员工页面只负责绑定角色。组织架构页按机构→院区→科室分组展示员工列表,新增院区默认归属“新华院区”,支持节点筛选、员工维护和组织节点维护。院区与科室的 `DELETE` 接口表示停用当前节点,不级联停用或删除下级;组织树默认筛选“启用中”,可切换为“已停用”或“全部状态”;停用节点在组织树中标记“已停用”,可通过编辑重新启用。操作成功但列表刷新失败时会提示刷新失败,保留已成功的停用状态。系统参数卡片仍保留 Mock,真实签字板/线上签名回调和打印服务仍需设备或后端能力。
|
||||||
|
|
||||||
文档库管理的真实模式已打通「模板 → 版本 → 正文」三段:
|
文档库管理的真实模式已打通「模板 → 版本 → 正文」三段:
|
||||||
|
|
||||||
|
|||||||
@@ -154,7 +154,8 @@ export async function updateCampus(
|
|||||||
return mapCampus(unwrapApiResponse(response))
|
return mapCampus(unwrapApiResponse(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteCampus(id: string): Promise<void> {
|
/** DELETE 接口将院区停用,保留记录及下属科室。 */
|
||||||
|
export async function disableCampus(id: string): Promise<void> {
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
throw new Error('Mock 模式不执行院区写操作')
|
throw new Error('Mock 模式不执行院区写操作')
|
||||||
}
|
}
|
||||||
@@ -192,7 +193,8 @@ export async function updateDepartment(
|
|||||||
return mapDepartment(unwrapApiResponse(response))
|
return mapDepartment(unwrapApiResponse(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteDepartment(id: string): Promise<void> {
|
/** DELETE 接口将当前科室停用,保留记录及下级科室。 */
|
||||||
|
export async function disableDepartment(id: string): Promise<void> {
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
throw new Error('Mock 模式不执行科室写操作')
|
throw new Error('Mock 模式不执行科室写操作')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ const libraryCampusId = computed(() => (allCampusesSelected.value ? '' : appStor
|
|||||||
const filters = reactive<DocumentFilterForm>({
|
const filters = reactive<DocumentFilterForm>({
|
||||||
keyword: '',
|
keyword: '',
|
||||||
department: null,
|
department: null,
|
||||||
status: 'all',
|
status: '启用中',
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 预览弹窗 */
|
/** 预览弹窗 */
|
||||||
@@ -1190,8 +1190,8 @@ async function resetMockData() {
|
|||||||
function clearFilters() {
|
function clearFilters() {
|
||||||
filters.keyword = ''
|
filters.keyword = ''
|
||||||
filters.department = null
|
filters.department = null
|
||||||
// status 也要一起重置:只按状态页签筛出空结果时,不清它等于「清了个寂寞」
|
// 清除筛选后恢复默认的启用中状态。
|
||||||
filters.status = 'all'
|
filters.status = '启用中'
|
||||||
// 院区不在这里重置:它是页面的取数口径(文书库按院区向服务端取数),
|
// 院区不在这里重置:它是页面的取数口径(文书库按院区向服务端取数),
|
||||||
// 不是「筛掉一部分已加载数据」的筛选条件,清掉它得重新拉一遍文库
|
// 不是「筛掉一部分已加载数据」的筛选条件,清掉它得重新拉一遍文库
|
||||||
}
|
}
|
||||||
@@ -1302,7 +1302,7 @@ onMounted(() => {
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<p>没有符合条件的文书</p>
|
<p>没有符合条件的文书</p>
|
||||||
<button
|
<button
|
||||||
v-if="filters.keyword || filters.department || filters.status !== 'all'"
|
v-if="filters.keyword || filters.department || filters.status !== '启用中'"
|
||||||
type="button"
|
type="button"
|
||||||
class="reset-button"
|
class="reset-button"
|
||||||
@click="clearFilters"
|
@click="clearFilters"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
import type { CampusRecord, DepartmentRecord } from '@/api/management/types'
|
import type { CampusRecord, DepartmentRecord } from '@/api/management/types'
|
||||||
|
|
||||||
@@ -11,6 +11,7 @@ const props = defineProps<{
|
|||||||
collapsed: boolean
|
collapsed: boolean
|
||||||
canManageCampuses: boolean
|
canManageCampuses: boolean
|
||||||
canManageDepartments: boolean
|
canManageDepartments: boolean
|
||||||
|
disabling: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -20,13 +21,29 @@ const emit = defineEmits<{
|
|||||||
addDepartment: []
|
addDepartment: []
|
||||||
editCampus: [campus: CampusRecord]
|
editCampus: [campus: CampusRecord]
|
||||||
editDepartment: [department: DepartmentRecord]
|
editDepartment: [department: DepartmentRecord]
|
||||||
deleteCampus: [campus: CampusRecord]
|
disableCampus: [campus: CampusRecord]
|
||||||
deleteDepartment: [department: DepartmentRecord]
|
disableDepartment: [department: DepartmentRecord]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const statusFilter = ref<'ENABLED' | 'DISABLED' | 'all'>('ENABLED')
|
||||||
|
const visibleDepartments = computed(() =>
|
||||||
|
props.departments.filter(
|
||||||
|
(department) => statusFilter.value === 'all' || department.status === statusFilter.value,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
const visibleCampuses = computed(() =>
|
||||||
|
props.campuses.filter(
|
||||||
|
(campus) =>
|
||||||
|
statusFilter.value === 'all' ||
|
||||||
|
campus.status === statusFilter.value ||
|
||||||
|
(statusFilter.value === 'DISABLED' &&
|
||||||
|
visibleDepartments.value.some((department) => department.campusId === campus.id)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
const institutionGroups = computed(() => {
|
const institutionGroups = computed(() => {
|
||||||
const grouped = new Map<string, CampusRecord[]>()
|
const grouped = new Map<string, CampusRecord[]>()
|
||||||
for (const campus of props.campuses) {
|
for (const campus of visibleCampuses.value) {
|
||||||
const institutionName = campus.institutionName?.trim() || '新华院区'
|
const institutionName = campus.institutionName?.trim() || '新华院区'
|
||||||
const group = grouped.get(institutionName) ?? []
|
const group = grouped.get(institutionName) ?? []
|
||||||
group.push(campus)
|
group.push(campus)
|
||||||
@@ -36,7 +53,7 @@ const institutionGroups = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function departmentsForCampus(campusId: string) {
|
function departmentsForCampus(campusId: string) {
|
||||||
return props.departments
|
return visibleDepartments.value
|
||||||
.filter((department) => department.campusId === campusId)
|
.filter((department) => department.campusId === campusId)
|
||||||
.sort(
|
.sort(
|
||||||
(left, right) => left.sortNo - right.sortNo || left.name.localeCompare(right.name, 'zh-CN'),
|
(left, right) => left.sortNo - right.sortNo || left.name.localeCompare(right.name, 'zh-CN'),
|
||||||
@@ -45,13 +62,16 @@ function departmentsForCampus(campusId: string) {
|
|||||||
|
|
||||||
function departmentCountForInstitution(campuses: CampusRecord[]) {
|
function departmentCountForInstitution(campuses: CampusRecord[]) {
|
||||||
const campusIds = new Set(campuses.map((campus) => campus.id))
|
const campusIds = new Set(campuses.map((campus) => campus.id))
|
||||||
return props.departments.filter((department) => campusIds.has(department.campusId)).length
|
return visibleDepartments.value.filter((department) => campusIds.has(department.campusId)).length
|
||||||
}
|
}
|
||||||
|
|
||||||
function childrenFor(parentId: string | null | undefined, campusId: string) {
|
function childrenFor(parentId: string | null | undefined, campusId: string) {
|
||||||
return departmentsForCampus(campusId).filter(
|
const departments = departmentsForCampus(campusId)
|
||||||
(department) => (department.parentId ?? null) === (parentId ?? null),
|
return departments.filter((department) => {
|
||||||
)
|
const visibleParent = departments.some((item) => item.id === department.parentId)
|
||||||
|
const effectiveParentId = visibleParent ? department.parentId : null
|
||||||
|
return (effectiveParentId ?? null) === (parentId ?? null)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDescendantSelected(department: DepartmentRecord): boolean {
|
function isDescendantSelected(department: DepartmentRecord): boolean {
|
||||||
@@ -82,6 +102,15 @@ function isDescendantSelected(department: DepartmentRecord): boolean {
|
|||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<label class="org-status-filter">
|
||||||
|
<span>状态</span>
|
||||||
|
<select v-model="statusFilter" aria-label="组织节点状态">
|
||||||
|
<option value="ENABLED">启用中</option>
|
||||||
|
<option value="DISABLED">已停用</option>
|
||||||
|
<option value="all">全部状态</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
<div class="org-tree">
|
<div class="org-tree">
|
||||||
<div
|
<div
|
||||||
v-for="institution in institutionGroups"
|
v-for="institution in institutionGroups"
|
||||||
@@ -100,7 +129,10 @@ function isDescendantSelected(department: DepartmentRecord): boolean {
|
|||||||
<div v-for="campus in institution.campuses" :key="campus.id" class="org-tree__campus-group">
|
<div v-for="campus in institution.campuses" :key="campus.id" class="org-tree__campus-group">
|
||||||
<div
|
<div
|
||||||
class="org-tree__node org-tree__campus"
|
class="org-tree__node org-tree__campus"
|
||||||
:class="{ 'is-selected': selectedCampusId === campus.id && !selectedDepartmentId }"
|
:class="{
|
||||||
|
'is-selected': selectedCampusId === campus.id && !selectedDepartmentId,
|
||||||
|
'is-disabled': campus.status === 'DISABLED',
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -115,6 +147,7 @@ function isDescendantSelected(department: DepartmentRecord): boolean {
|
|||||||
>
|
>
|
||||||
<span class="org-tree__icon">🏥</span>
|
<span class="org-tree__icon">🏥</span>
|
||||||
<span class="org-tree__name">{{ campus.name }}</span>
|
<span class="org-tree__name">{{ campus.name }}</span>
|
||||||
|
<span v-if="campus.status === 'DISABLED'" class="org-tree__status">已停用</span>
|
||||||
<span class="org-tree__count">{{ departmentsForCampus(campus.id).length }} 科室</span>
|
<span class="org-tree__count">{{ departmentsForCampus(campus.id).length }} 科室</span>
|
||||||
</button>
|
</button>
|
||||||
<div v-if="canManageCampuses" class="org-tree__actions">
|
<div v-if="canManageCampuses" class="org-tree__actions">
|
||||||
@@ -127,12 +160,14 @@ function isDescendantSelected(department: DepartmentRecord): boolean {
|
|||||||
编辑
|
编辑
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
v-if="campus.status !== 'DISABLED'"
|
||||||
type="button"
|
type="button"
|
||||||
class="org-tree__remove"
|
class="org-tree__disable"
|
||||||
:aria-label="`删除院区${campus.name}`"
|
:disabled="disabling"
|
||||||
@click="emit('deleteCampus', campus)"
|
:aria-label="`停用院区${campus.name}`"
|
||||||
|
@click="emit('disableCampus', campus)"
|
||||||
>
|
>
|
||||||
删除
|
停用
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -144,7 +179,10 @@ function isDescendantSelected(department: DepartmentRecord): boolean {
|
|||||||
<template v-for="department in childrenFor(null, campus.id)" :key="department.id">
|
<template v-for="department in childrenFor(null, campus.id)" :key="department.id">
|
||||||
<div
|
<div
|
||||||
class="org-tree__node org-tree__department"
|
class="org-tree__node org-tree__department"
|
||||||
:class="{ 'is-selected': isDescendantSelected(department) }"
|
:class="{
|
||||||
|
'is-selected': isDescendantSelected(department),
|
||||||
|
'is-disabled': department.status === 'DISABLED',
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -152,6 +190,9 @@ function isDescendantSelected(department: DepartmentRecord): boolean {
|
|||||||
@click="emit('selectDepartment', department)"
|
@click="emit('selectDepartment', department)"
|
||||||
>
|
>
|
||||||
<span class="org-tree__name">🗂 {{ department.name }}</span>
|
<span class="org-tree__name">🗂 {{ department.name }}</span>
|
||||||
|
<span v-if="department.status === 'DISABLED'" class="org-tree__status"
|
||||||
|
>已停用</span
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
<div v-if="canManageDepartments" class="org-tree__actions">
|
<div v-if="canManageDepartments" class="org-tree__actions">
|
||||||
<button
|
<button
|
||||||
@@ -163,12 +204,14 @@ function isDescendantSelected(department: DepartmentRecord): boolean {
|
|||||||
编辑
|
编辑
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
v-if="department.status !== 'DISABLED'"
|
||||||
type="button"
|
type="button"
|
||||||
class="org-tree__remove"
|
class="org-tree__disable"
|
||||||
:aria-label="`删除科室${department.name}`"
|
:disabled="disabling"
|
||||||
@click="emit('deleteDepartment', department)"
|
:aria-label="`停用科室${department.name}`"
|
||||||
|
@click="emit('disableDepartment', department)"
|
||||||
>
|
>
|
||||||
删除
|
停用
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -176,7 +219,10 @@ function isDescendantSelected(department: DepartmentRecord): boolean {
|
|||||||
v-for="child in childrenFor(department.id, campus.id)"
|
v-for="child in childrenFor(department.id, campus.id)"
|
||||||
:key="child.id"
|
:key="child.id"
|
||||||
class="org-tree__node org-tree__child-department"
|
class="org-tree__node org-tree__child-department"
|
||||||
:class="{ 'is-selected': isDescendantSelected(child) }"
|
:class="{
|
||||||
|
'is-selected': isDescendantSelected(child),
|
||||||
|
'is-disabled': child.status === 'DISABLED',
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -184,6 +230,7 @@ function isDescendantSelected(department: DepartmentRecord): boolean {
|
|||||||
@click="emit('selectDepartment', child)"
|
@click="emit('selectDepartment', child)"
|
||||||
>
|
>
|
||||||
<span class="org-tree__name">{{ child.name }}</span>
|
<span class="org-tree__name">{{ child.name }}</span>
|
||||||
|
<span v-if="child.status === 'DISABLED'" class="org-tree__status">已停用</span>
|
||||||
</button>
|
</button>
|
||||||
<div v-if="canManageDepartments" class="org-tree__actions">
|
<div v-if="canManageDepartments" class="org-tree__actions">
|
||||||
<button
|
<button
|
||||||
@@ -195,24 +242,26 @@ function isDescendantSelected(department: DepartmentRecord): boolean {
|
|||||||
编辑
|
编辑
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
v-if="child.status !== 'DISABLED'"
|
||||||
type="button"
|
type="button"
|
||||||
class="org-tree__remove"
|
class="org-tree__disable"
|
||||||
:aria-label="`删除科室${child.name}`"
|
:disabled="disabling"
|
||||||
@click="emit('deleteDepartment', child)"
|
:aria-label="`停用科室${child.name}`"
|
||||||
|
@click="emit('disableDepartment', child)"
|
||||||
>
|
>
|
||||||
删除
|
停用
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<p v-if="!childrenFor(null, campus.id).length" class="org-tree__empty">
|
<p v-if="!childrenFor(null, campus.id).length" class="org-tree__empty">
|
||||||
该院区还没有维护科室
|
当前状态下暂无科室
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="!campuses.length" class="org-tree__empty">暂无可用院区</p>
|
<p v-if="!visibleCampuses.length" class="org-tree__empty">当前状态下暂无院区</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@@ -274,6 +323,24 @@ function isDescendantSelected(department: DepartmentRecord): boolean {
|
|||||||
border-color: #d3e7ee;
|
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 {
|
.org-tree {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
@@ -408,7 +475,7 @@ function isDescendantSelected(department: DepartmentRecord): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.org-tree__edit,
|
.org-tree__edit,
|
||||||
.org-tree__remove {
|
.org-tree__disable {
|
||||||
padding: 3px 7px;
|
padding: 3px 7px;
|
||||||
font-size: 10.5px;
|
font-size: 10.5px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
@@ -421,19 +488,39 @@ function isDescendantSelected(department: DepartmentRecord): boolean {
|
|||||||
border: 1px solid #d3e7ee;
|
border: 1px solid #d3e7ee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-tree__remove {
|
.org-tree__disable {
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
color: var(--err);
|
color: var(--err);
|
||||||
border: 1px solid #f0cdcd;
|
border: 1px solid #f0cdcd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-tree__node:hover .org-tree__edit,
|
.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__edit,
|
||||||
.org-tree__node:focus-within .org-tree__remove {
|
.org-tree__node:focus-within .org-tree__disable {
|
||||||
opacity: 1;
|
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 {
|
.org-tree__empty {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
|||||||
import {
|
import {
|
||||||
createCampus,
|
createCampus,
|
||||||
createDepartment,
|
createDepartment,
|
||||||
deleteCampus,
|
disableCampus,
|
||||||
deleteDepartment,
|
disableDepartment,
|
||||||
getAllDepartments,
|
getAllDepartments,
|
||||||
getCampuses,
|
getCampuses,
|
||||||
updateCampus,
|
updateCampus,
|
||||||
@@ -68,6 +68,7 @@ const organizationDialogVisible = ref(false)
|
|||||||
const organizationDialogKind = ref<'campus' | 'department'>('campus')
|
const organizationDialogKind = ref<'campus' | 'department'>('campus')
|
||||||
const organizationDialogInitial = ref<CampusRecord | DepartmentRecord | null>(null)
|
const organizationDialogInitial = ref<CampusRecord | DepartmentRecord | null>(null)
|
||||||
const organizationSaving = ref(false)
|
const organizationSaving = ref(false)
|
||||||
|
const organizationDisabling = ref(false)
|
||||||
let userRequestId = 0
|
let userRequestId = 0
|
||||||
|
|
||||||
const canEditUsers = computed(() => isUserMockEnabled || loginStore.hasPermission('system:org:emp'))
|
const canEditUsers = computed(() => isUserMockEnabled || loginStore.hasPermission('system:org:emp'))
|
||||||
@@ -302,7 +303,7 @@ function mapMockUsers() {
|
|||||||
userTotal.value = userRows.value.length
|
userTotal.value = userRows.value.length
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadUsers() {
|
async function loadUsers(notifyOnError = true): Promise<boolean> {
|
||||||
const requestId = ++userRequestId
|
const requestId = ++userRequestId
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -321,7 +322,7 @@ async function loadUsers() {
|
|||||||
return matchesCampus && matchesDepartment && matchesKeyword
|
return matchesCampus && matchesDepartment && matchesKeyword
|
||||||
})
|
})
|
||||||
userTotal.value = filtered.length
|
userTotal.value = filtered.length
|
||||||
return
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await getUsers({
|
const result = await getUsers({
|
||||||
@@ -332,7 +333,7 @@ async function loadUsers() {
|
|||||||
campusId: selectedCampusId.value || undefined,
|
campusId: selectedCampusId.value || undefined,
|
||||||
departmentId: selectedDepartmentId.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 campusNames = new Map(campuses.value.map((campus) => [campus.id, campus.name]))
|
||||||
const departmentNames = new Map(
|
const departmentNames = new Map(
|
||||||
@@ -340,17 +341,20 @@ async function loadUsers() {
|
|||||||
)
|
)
|
||||||
userRows.value = result.records.map((user) => mapUserRow(user, campusNames, departmentNames))
|
userRows.value = result.records.map((user) => mapUserRow(user, campusNames, departmentNames))
|
||||||
userTotal.value = result.total
|
userTotal.value = result.total
|
||||||
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
if (requestId !== userRequestId) return
|
if (requestId !== userRequestId) return false
|
||||||
userRows.value = []
|
userRows.value = []
|
||||||
userTotal.value = 0
|
userTotal.value = 0
|
||||||
ElMessage.error('员工列表加载失败,请稍后重试')
|
if (notifyOnError) ElMessage.error('员工列表加载失败,请稍后重试')
|
||||||
|
return false
|
||||||
} finally {
|
} finally {
|
||||||
if (requestId === userRequestId) loading.value = false
|
if (requestId === userRequestId) loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadOrganization() {
|
async function loadOrganization(notifyOnError = true): Promise<boolean> {
|
||||||
|
let complete = true
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
if (isUserMockEnabled) {
|
if (isUserMockEnabled) {
|
||||||
@@ -363,15 +367,16 @@ async function loadOrganization() {
|
|||||||
])
|
])
|
||||||
if (campusResult.status === 'rejected') throw campusResult.reason
|
if (campusResult.status === 'rejected') throw campusResult.reason
|
||||||
campuses.value = campusResult.value
|
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 : []
|
roleOptions.value = rolesResult.status === 'fulfilled' ? rolesResult.value : []
|
||||||
roleColorMap.value = Object.fromEntries(
|
roleColorMap.value = Object.fromEntries(
|
||||||
roleOptions.value.map((role) => [role.name, role.color ?? '#8a9aa3']),
|
roleOptions.value.map((role) => [role.name, role.color ?? '#8a9aa3']),
|
||||||
)
|
)
|
||||||
if (canAssignRoles.value && rolesResult.status === 'rejected') {
|
if (canAssignRoles.value && rolesResult.status === 'rejected' && notifyOnError) {
|
||||||
ElMessage.warning('员工角色选项加载失败,角色绑定暂不可用')
|
ElMessage.warning('员工角色选项加载失败,角色绑定暂不可用')
|
||||||
}
|
}
|
||||||
if (departmentResult.status === 'rejected') {
|
if (departmentResult.status === 'rejected' && notifyOnError) {
|
||||||
ElMessage.warning('科室架构加载失败,员工科室筛选暂不可用')
|
ElMessage.warning('科室架构加载失败,员工科室筛选暂不可用')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -390,10 +395,12 @@ async function loadOrganization() {
|
|||||||
campusCollapsed.value = false
|
campusCollapsed.value = false
|
||||||
if (preferredCampus) appStore.selectCampus(preferredCampus)
|
if (preferredCampus) appStore.selectCampus(preferredCampus)
|
||||||
userPage.value = 1
|
userPage.value = 1
|
||||||
await loadUsers()
|
const usersLoaded = await loadUsers(notifyOnError)
|
||||||
|
return complete && usersLoaded
|
||||||
} catch {
|
} catch {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
ElMessage.error('组织架构加载失败,请稍后重试')
|
if (notifyOnError) ElMessage.error('组织架构加载失败,请稍后重试')
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,7 +654,6 @@ async function saveOrganizationNode(form: OrganizationNodeSave) {
|
|||||||
const campus = form.id
|
const campus = form.id
|
||||||
? await updateCampus(form.id, form.payload)
|
? await updateCampus(form.id, form.payload)
|
||||||
: await createCampus(form.payload)
|
: await createCampus(form.payload)
|
||||||
await appStore.loadCampuses(true)
|
|
||||||
if (!form.id) selectedCampusId.value = campus.id
|
if (!form.id) selectedCampusId.value = campus.id
|
||||||
} else {
|
} else {
|
||||||
const department = form.id
|
const department = form.id
|
||||||
@@ -657,8 +663,9 @@ async function saveOrganizationNode(form: OrganizationNodeSave) {
|
|||||||
if (!form.id) selectedDepartmentId.value = department.id
|
if (!form.id) selectedDepartmentId.value = department.id
|
||||||
}
|
}
|
||||||
organizationDialogVisible.value = false
|
organizationDialogVisible.value = false
|
||||||
await loadOrganization()
|
const refreshed = await refreshOrganizationAfterMutation(form.kind)
|
||||||
ElMessage.success(form.id ? '组织信息已更新' : '组织节点已创建')
|
if (refreshed) ElMessage.success(form.id ? '组织信息已更新' : '组织节点已创建')
|
||||||
|
else ElMessage.warning('组织信息已保存,但页面刷新失败,请刷新页面重试')
|
||||||
} catch {
|
} catch {
|
||||||
ElMessage.error('组织信息保存失败,请检查编码是否重复或所属院区是否有效')
|
ElMessage.error('组织信息保存失败,请检查编码是否重复或所属院区是否有效')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -666,54 +673,77 @@ async function saveOrganizationNode(form: OrganizationNodeSave) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeCampus(campus: CampusRecord) {
|
async function refreshOrganizationAfterMutation(kind: 'campus' | 'department'): Promise<boolean> {
|
||||||
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
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await deleteCampus(campus.id)
|
if (kind === 'campus') await appStore.loadCampuses(true)
|
||||||
if (selectedCampusId.value === campus.id) selectedDepartmentId.value = null
|
const refreshed = await loadOrganization(false)
|
||||||
await appStore.loadCampuses(true)
|
return refreshed && (kind !== 'campus' || !appStore.campusLoadFailed)
|
||||||
await loadOrganization()
|
} catch {
|
||||||
ElMessage.success('院区已删除')
|
return false
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error(toUserMessage(error, '院区删除失败,请稍后重试'))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeDepartment(department: DepartmentRecord) {
|
async function handleDisableCampus(campus: CampusRecord) {
|
||||||
if (!canManageDepartments.value) return
|
if (!canManageCampuses.value || campus.status === 'DISABLED' || organizationDisabling.value)
|
||||||
const childCount = departments.value.filter((item) => item.parentId === department.id).length
|
return
|
||||||
const confirmed = await ElMessageBox.confirm(
|
organizationDisabling.value = true
|
||||||
childCount
|
|
||||||
? `删除科室“${department.name}”会同时移除其下 ${childCount} 个下级科室,且无法恢复,是否继续?`
|
|
||||||
: `删除科室“${department.name}”后无法恢复,是否继续?`,
|
|
||||||
'删除科室',
|
|
||||||
{ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' },
|
|
||||||
)
|
|
||||||
.then(() => true)
|
|
||||||
.catch(() => false)
|
|
||||||
if (!confirmed) return
|
|
||||||
|
|
||||||
try {
|
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
|
if (selectedDepartmentId.value === department.id) selectedDepartmentId.value = null
|
||||||
await loadOrganization()
|
const refreshed = await refreshOrganizationAfterMutation('department')
|
||||||
ElMessage.success('科室已删除')
|
if (refreshed) ElMessage.success('科室已停用')
|
||||||
} catch (error) {
|
else ElMessage.warning('科室已停用,但页面刷新失败,请刷新页面重试')
|
||||||
ElMessage.error(toUserMessage(error, '科室删除失败,请稍后重试'))
|
} finally {
|
||||||
|
organizationDisabling.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -771,14 +801,15 @@ onMounted(loadOrganization)
|
|||||||
:collapsed="campusCollapsed"
|
:collapsed="campusCollapsed"
|
||||||
:can-manage-campuses="canManageCampuses"
|
:can-manage-campuses="canManageCampuses"
|
||||||
:can-manage-departments="canManageDepartments"
|
:can-manage-departments="canManageDepartments"
|
||||||
|
:disabling="organizationDisabling"
|
||||||
@select-campus="selectCampus"
|
@select-campus="selectCampus"
|
||||||
@select-department="selectDepartment"
|
@select-department="selectDepartment"
|
||||||
@add-campus="openAddCampus"
|
@add-campus="openAddCampus"
|
||||||
@add-department="openAddDepartment"
|
@add-department="openAddDepartment"
|
||||||
@edit-campus="editCampus"
|
@edit-campus="editCampus"
|
||||||
@edit-department="editDepartment"
|
@edit-department="editDepartment"
|
||||||
@delete-campus="removeCampus"
|
@disable-campus="handleDisableCampus"
|
||||||
@delete-department="removeDepartment"
|
@disable-department="handleDisableDepartment"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<section class="employee-card">
|
<section class="employee-card">
|
||||||
|
|||||||
Reference in New Issue
Block a user