diff --git a/clinical-web/README.md b/clinical-web/README.md index ee8dd4f..2b2d3e3 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 接口;导航、路由和页面操作按授权资源树控制,角色数据范围在角色配置中维护,员工页面只负责绑定角色。组织架构页按机构→院区→科室分组展示员工列表,新增院区默认归属“新华院区”,支持节点筛选、员工维护和组织节点维护。系统参数卡片仍保留 Mock,真实签字板/线上签名回调和打印服务仍需设备或后端能力。 文档库管理的真实模式已打通「模板 → 版本 → 正文」三段: diff --git a/clinical-web/src/api/management/organization.ts b/clinical-web/src/api/management/organization.ts index 57b055c..83438e8 100644 --- a/clinical-web/src/api/management/organization.ts +++ b/clinical-web/src/api/management/organization.ts @@ -20,26 +20,35 @@ function getRecords(data: BackendCollection) { return Array.isArray(data) ? data : (data.records ?? data.items ?? data.content ?? []) } -export function getCampuses(): Promise { +export async function getCampuses(): Promise { if (useMockData) { - return Promise.resolve([]) + return [] } - return request - .get>>('/v1/campuses', { - params: { page: 1, size: 200 }, - }) - .then(unwrapApiResponse) - .then((data) => - getRecords(data).map((item) => ({ - id: item.id, - code: item.code ?? item.id, - name: item.name ?? item.campusName ?? item.id, - address: item.address ?? undefined, - status: item.status ?? 'ENABLED', - sortNo: item.sortNo ?? 0, - })), - ) + const firstPage = await getCampusPage(1, 200) + const pageCount = Math.ceil(firstPage.total / firstPage.pageSize) + if (pageCount <= 1) return firstPage.records + + const remainingPages = await Promise.all( + Array.from({ length: pageCount - 1 }, (_, index) => + getCampusPage(index + 2, firstPage.pageSize), + ), + ) + return [firstPage, ...remainingPages].flatMap((page) => page.records) +} + +async function getCampusPage(page: number, pageSize: number) { + const response = await request.get>>( + '/v1/campuses', + { params: { page, size: pageSize } }, + ) + const data = unwrapApiResponse(response) + const records = getRecords(data).map(mapCampus) + return { + records, + total: Array.isArray(data) ? records.length : (data.total ?? records.length), + pageSize: Array.isArray(data) ? pageSize : (data.size ?? pageSize), + } } export function getDepartments(campusId?: string): Promise { @@ -102,6 +111,7 @@ function mapCampus(data: OrganizationResponseDto): CampusRecord { id: data.id, code: data.code ?? data.id, name: data.name ?? data.campusName ?? data.id, + institutionName: data.institutionName?.trim() || '新华院区', address: data.address ?? undefined, status: data.status ?? 'ENABLED', sortNo: data.sortNo ?? 0, diff --git a/clinical-web/src/api/management/types.ts b/clinical-web/src/api/management/types.ts index 7736e82..ea7689f 100644 --- a/clinical-web/src/api/management/types.ts +++ b/clinical-web/src/api/management/types.ts @@ -243,6 +243,7 @@ export interface CampusRecord { id: string code: string name: string + institutionName: string address?: string status: OrganizationStatus sortNo: number @@ -263,6 +264,7 @@ export type OrganizationStatus = 'ENABLED' | 'DISABLED' | string export interface CreateCampusRequest { code: string name: string + institutionName?: string address?: string status?: OrganizationStatus sortNo?: number @@ -285,6 +287,7 @@ export interface OrganizationResponseDto { id: string code?: string | null name?: string | null + institutionName?: string | null address?: string | null status?: OrganizationStatus | null sortNo?: number | null @@ -431,12 +434,14 @@ export type UserStatus = 'enabled' | 'disabled' | 'locked' export interface UserQuery extends PageQuery { keyword?: string department?: string + departmentId?: string status?: UserStatus | 'all' campusId?: string } export interface UserRecord { id: string + employeeId?: number | null name: string account: string department: string @@ -444,6 +449,8 @@ export interface UserRecord { status: UserStatus lastLoginAt: string employeeNo?: string | null + position?: string | null + professionalTitle?: string | null phone?: string | null email?: string | null campusId?: string | null @@ -452,6 +459,7 @@ export interface UserRecord { departmentName?: string roleNames?: string[] roleIds?: string[] + roleDataScopes?: RoleDataScopeType[] version?: number passwordChangeRequired?: boolean } @@ -460,9 +468,12 @@ export type UserListResponse = PageResult export interface UserResponseDto { id: string + employeeId?: number | null username: string displayName: string employeeNo?: string | null + position?: string | null + professionalTitle?: string | null phone?: string | null email?: string | null campusId?: string | null @@ -483,6 +494,7 @@ export interface UserRoleSummaryDto { code: string name: string status?: RoleStatus | null + dataScope?: RoleDataScopeType | null } export interface CreateUserRequest { @@ -490,11 +502,14 @@ export interface CreateUserRequest { password: string displayName: string employeeNo?: string + position?: string + professionalTitle?: string phone?: string email?: string campusId: string departmentId?: string | null status?: BackendUserStatus + passwordChangeRequired?: boolean } export type BackendUserStatus = 'ENABLED' | 'DISABLED' | 'LOCKED' @@ -502,6 +517,8 @@ export type BackendUserStatus = 'ENABLED' | 'DISABLED' | 'LOCKED' export interface UpdateUserRequest { displayName: string employeeNo?: string + position?: string + professionalTitle?: string phone?: string email?: string campusId: string @@ -631,6 +648,7 @@ export interface UpdateDepartmentPermissionPolicyRequest { export interface PasswordResetRequest { newPassword: string forceChangeOnNextLogin?: boolean + reason: string } export type UserImportRowStatus = 'VALID' | 'INVALID' diff --git a/clinical-web/src/api/management/users.ts b/clinical-web/src/api/management/users.ts index ecedc07..2655aa5 100644 --- a/clinical-web/src/api/management/users.ts +++ b/clinical-web/src/api/management/users.ts @@ -90,6 +90,7 @@ function mapUser(dto: UserResponseDto): UserRecord { return { id: dto.id, + employeeId: dto.employeeId, name: dto.displayName, account: dto.username, department: dto.departmentName ?? (dto.departmentId ? '未知科室' : '未指定科室'), @@ -97,6 +98,8 @@ function mapUser(dto: UserResponseDto): UserRecord { status: normalizeStatus(dto.status), lastLoginAt: formatDateTime(dto.lastLoginAt), employeeNo: dto.employeeNo, + position: dto.position, + professionalTitle: dto.professionalTitle, phone: dto.phone, email: dto.email, campusId: dto.campusId, @@ -105,6 +108,7 @@ function mapUser(dto: UserResponseDto): UserRecord { departmentName: dto.departmentName ?? undefined, roleNames, roleIds: roles.map((role) => role.id), + roleDataScopes: roles.map((role) => role.dataScope ?? 'SELF'), version: dto.version ?? undefined, passwordChangeRequired: dto.passwordChangeRequired, } @@ -139,6 +143,10 @@ function toBackendQuery(query: UserQuery) { params.campusId = query.campusId } + if (query.departmentId) { + params.departmentId = query.departmentId + } + return params } @@ -163,10 +171,13 @@ export function getUsers(query: UserQuery): Promise { const matchesKeyword = !keyword || [record.name, record.account].join(' ').toLowerCase().includes(keyword) const matchesDepartment = !query.department || record.department === query.department + const matchesDepartmentId = !query.departmentId || record.departmentId === query.departmentId const matchesStatus = !query.status || query.status === 'all' || record.status === query.status const matchesCampus = !query.campusId || record.campusId === query.campusId - return matchesKeyword && matchesDepartment && matchesStatus && matchesCampus + return ( + matchesKeyword && matchesDepartment && matchesDepartmentId && matchesStatus && matchesCampus + ) }) const page = Math.max(query.page, 1) const pageSize = Math.max(query.pageSize, 1) diff --git a/clinical-web/src/stores/app.ts b/clinical-web/src/stores/app.ts index 73a708e..60c2852 100644 --- a/clinical-web/src/stores/app.ts +++ b/clinical-web/src/stores/app.ts @@ -10,9 +10,30 @@ import type { WorkbenchCampus } from '@/api/workbench/types' const useMockData = mockFlags.organization const mockCampusRecords: CampusRecord[] = [ - { id: 'mock-campus-main', code: 'MAIN', name: '本部院区', status: 'ENABLED', sortNo: 1 }, - { id: 'mock-campus-east', code: 'EAST', name: '东院区', status: 'ENABLED', sortNo: 2 }, - { id: 'mock-campus-west', code: 'WEST', name: '西院区', status: 'ENABLED', sortNo: 3 }, + { + id: 'mock-campus-main', + code: 'MAIN', + name: '本部院区', + institutionName: '新华院区', + status: 'ENABLED', + sortNo: 1, + }, + { + id: 'mock-campus-east', + code: 'EAST', + name: '东院区', + institutionName: '新华院区', + status: 'ENABLED', + sortNo: 2, + }, + { + id: 'mock-campus-west', + code: 'WEST', + name: '西院区', + institutionName: '新华院区', + status: 'ENABLED', + sortNo: 3, + }, ] function getEnabledCampuses(records: CampusRecord[]) { diff --git a/clinical-web/src/utils/mock-document-library.ts b/clinical-web/src/utils/mock-document-library.ts index 08606a0..8e17af8 100644 --- a/clinical-web/src/utils/mock-document-library.ts +++ b/clinical-web/src/utils/mock-document-library.ts @@ -81,11 +81,19 @@ export interface LocalDepartment { * 演示 id 带 mock- 前缀,一眼能看出不是真实 UUID,不会误当成可提交给后端的取值。 */ export const demoCampuses = [ - { id: 'mock-campus-main', code: 'MAIN', name: '本部院区', status: 'ENABLED' as const, sortNo: 1 }, + { + id: 'mock-campus-main', + code: 'MAIN', + name: '本部院区', + institutionName: '新华院区', + status: 'ENABLED' as const, + sortNo: 1, + }, { id: 'mock-campus-east', code: 'EAST', name: '东院区', + institutionName: '新华院区', status: 'ENABLED' as const, sortNo: 2, }, diff --git a/clinical-web/src/views/management/users/components/OrganizationNodeDialog.vue b/clinical-web/src/views/management/users/components/OrganizationNodeDialog.vue new file mode 100644 index 0000000..17f076a --- /dev/null +++ b/clinical-web/src/views/management/users/components/OrganizationNodeDialog.vue @@ -0,0 +1,397 @@ + + + + + diff --git a/clinical-web/src/views/management/users/components/OrganizationTree.vue b/clinical-web/src/views/management/users/components/OrganizationTree.vue new file mode 100644 index 0000000..53476d3 --- /dev/null +++ b/clinical-web/src/views/management/users/components/OrganizationTree.vue @@ -0,0 +1,397 @@ + + + + + diff --git a/clinical-web/src/views/management/users/components/UserEditorDialog.vue b/clinical-web/src/views/management/users/components/UserEditorDialog.vue index c6d6259..4d1009a 100644 --- a/clinical-web/src/views/management/users/components/UserEditorDialog.vue +++ b/clinical-web/src/views/management/users/components/UserEditorDialog.vue @@ -11,6 +11,8 @@ const props = defineProps<{ campuses: CampusRecord[] departments: DepartmentRecord[] roles: RoleRecord[] + defaultCampusId?: string + defaultDepartmentId?: string | null canAssignRoles: boolean saving: boolean }>() @@ -34,12 +36,15 @@ function createEmptyForm(): UserEditorForm { password: '', displayName: '', employeeNo: '', + position: '', + professionalTitle: '', phone: '', email: '', - campusId: props.campuses[0]?.id ?? '', - departmentId: '', + campusId: props.defaultCampusId ?? props.campuses[0]?.id ?? '', + departmentId: props.defaultDepartmentId ?? '', status: 'enabled', roleIds: [], + passwordChangeRequired: true, clearPhone: false, clearEmail: false, } @@ -98,6 +103,11 @@ function submit() { return } + if (!form.employeeNo.trim()) { + error.message = '请输入工号' + return + } + if (!form.campusId) { error.message = '请选择院区' return @@ -152,11 +162,11 @@ watch( >
- {{ isEditing ? '编辑用户' : '新增用户' }} + {{ isEditing ? '编辑员工' : '新增员工' }}

{{ isEditing - ? '更新用户基础资料;权限和数据范围由绑定角色决定' + ? '维护员工资料与组织归属;权限和数据范围由绑定角色决定' : '创建可登录医签通的医护用户' }}

@@ -168,6 +178,11 @@ watch(
+
① 身份与归属
+ + + +
② 角色(权限和数据范围由角色统一配置)
+ + +
③ 账号与安全
-

{{ error.message }}

@@ -352,6 +386,16 @@ watch( grid-column: 1 / -1; } +.form-section { + grid-column: 1 / -1; + padding: 8px 10px; + color: var(--brand-d); + font-size: 12px; + font-weight: 700; + background: #f2f8fa; + border-radius: 6px; +} + .form-grid label > span { color: var(--ink); font-size: 12px; @@ -397,6 +441,18 @@ watch( line-height: 1.5; } +.checkbox-field { + display: flex !important; + flex-direction: row !important; + align-items: center; + gap: 8px !important; + align-self: center; +} + +.checkbox-field input { + width: auto; +} + .form-error { margin: 12px 0 0; color: var(--err); diff --git a/clinical-web/src/views/management/users/components/UserPasswordResetDialog.vue b/clinical-web/src/views/management/users/components/UserPasswordResetDialog.vue index ec9cbf2..6083116 100644 --- a/clinical-web/src/views/management/users/components/UserPasswordResetDialog.vue +++ b/clinical-web/src/views/management/users/components/UserPasswordResetDialog.vue @@ -4,18 +4,21 @@ import { reactive, watch } from 'vue' const props = defineProps<{ visible: boolean userName: string + employeeId?: number | null + employeeNo?: string saving: boolean }>() const emit = defineEmits<{ close: [] - reset: [payload: { newPassword: string; forceChangeOnNextLogin: boolean }] + reset: [payload: { newPassword: string; forceChangeOnNextLogin: boolean; reason: string }] }>() const form = reactive({ newPassword: '', confirmPassword: '', forceChangeOnNextLogin: true, + reason: '', }) const error = reactive({ message: '' }) @@ -23,6 +26,7 @@ function resetForm() { form.newPassword = '' form.confirmPassword = '' form.forceChangeOnNextLogin = true + form.reason = '' error.message = '' } @@ -39,9 +43,15 @@ function submit() { return } + if (!form.reason.trim()) { + error.message = '请填写重置原因' + return + } + emit('reset', { newPassword: form.newPassword, forceChangeOnNextLogin: form.forceChangeOnNextLogin, + reason: form.reason.trim(), }) } @@ -66,8 +76,8 @@ watch( >
- 重置用户密码 -

为“{{ userName }}”设置新的登录密码

+ 重置密码 +

{{ userName }} · ID {{ employeeId ?? '—' }} · 工号 {{ employeeNo || '—' }}

+
+ 重置对象 + {{ userName }}(ID {{ employeeId ?? '—' }} · 工号 {{ employeeNo || '—' }}) +
+
确认后将撤销该员工的全部现有会话。
+