From e5b5412322ba850de33022b18b6a45475054cfb4 Mon Sep 17 00:00:00 2001 From: xy Date: Thu, 24 Sep 2026 08:31:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8E=A5=E5=85=A5=E6=9D=83=E9=99=90?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- clinical-web/README.md | 32 +- clinical-web/src/api/auth.ts | 6 + .../src/api/management/permissions.ts | 238 +---- clinical-web/src/api/management/roles.ts | 197 ++-- clinical-web/src/api/management/types.ts | 142 +-- clinical-web/src/api/management/users.ts | 35 +- clinical-web/src/components/layout/Header.vue | 12 +- clinical-web/src/components/layout/Menu.vue | 90 +- clinical-web/src/layouts/ClinicalLayout.vue | 62 +- clinical-web/src/router/index.ts | 181 ++-- clinical-web/src/stores/login.ts | 110 ++- clinical-web/src/types/auth.ts | 35 +- .../src/views/management/MenuResourceView.vue | 49 + .../management/document-permissions/index.vue | 4 + .../src/views/management/roles/index.vue | 870 ++++++++++++++++++ .../components/MenuResourceSettings.vue | 669 ++++++++++++++ .../src/views/management/settings/index.vue | 58 +- .../components/RoleApiPermissionPanel.vue | 300 ------ .../users/components/RoleSummaryCards.vue | 141 --- .../users/components/UserEditorDialog.vue | 40 +- .../management/users/components/UserTable.vue | 4 +- .../src/views/management/users/index.vue | 799 ++++++---------- .../src/views/management/users/mock.ts | 49 +- .../src/views/management/users/types.ts | 13 +- 24 files changed, 2398 insertions(+), 1738 deletions(-) create mode 100644 clinical-web/src/views/management/MenuResourceView.vue create mode 100644 clinical-web/src/views/management/roles/index.vue create mode 100644 clinical-web/src/views/management/settings/components/MenuResourceSettings.vue delete mode 100644 clinical-web/src/views/management/users/components/RoleApiPermissionPanel.vue delete mode 100644 clinical-web/src/views/management/users/components/RoleSummaryCards.vue diff --git a/clinical-web/README.md b/clinical-web/README.md index 97f89ee..0b7252c 100644 --- a/clinical-web/README.md +++ b/clinical-web/README.md @@ -84,7 +84,7 @@ TanStack Vue Query、PDF 预览、报表图表、自动化测试和签字板适 ## 当前状态 -已完成 Vite 基础初始化、路由、Pinia、原型主题 CSS、登录页、首页、签署工作台和文档库管理页面。登录、首页统计、签署工作台的患者/就诊/模板/任务/审计、签署文件下载、短信重发、模板创建/版本工作流、用户组织查询与用户创建编辑,以及按模板维护文档权限已经接入 MEDISIGN 后端;文档库管理已接入模板与版本接口,系统设置仍保留 Mock,真实签字板/线上签名回调和打印服务仍需设备或后端能力。 +已完成 Vite 基础初始化、路由、Pinia、原型主题 CSS、登录页、首页、签署工作台和文档库管理页面。角色权限与系统设置中的菜单设置已接入 MEDISIGN RBAC 接口;导航、路由和页面操作按授权资源树控制,角色数据范围在角色配置中维护,员工页面只负责绑定角色。系统参数卡片仍保留 Mock,真实签字板/线上签名回调和打印服务仍需设备或后端能力。 文档库管理的真实模式已打通「模板 → 版本 → 正文」三段: @@ -102,16 +102,14 @@ TanStack Vue Query、PDF 预览、报表图表、自动化测试和签字板适 公开页 └─ /login 登录页 -ClinicalLayout -├─ 工作台 -│ ├─ /workbench/home 首页 -│ └─ /workbench/signing 签署工作台 -└─ 管理 - ├─ /management/documents 文档库管理 - ├─ /management/reports 报表分析 - ├─ /management/users 用户与权限 - ├─ /management/document-permissions 文档权限 - └─ /management/settings 系统设置 +ClinicalLayout(实际导航和页面路由由授权菜单资源树提供) +├─ /dashboard 首页 +├─ /sign/workbench 签署工作台 +├─ /system/org 组织架构 / 员工角色绑定 +├─ /system/role 角色权限与数据范围 +├─ /doc/library 文档库管理 +├─ /report/index 报表分析 +└─ /system/setting 系统设置 / 菜单设置 ``` 布局拆分为 `Menu`、`Header` 和 `Container` 三个公共组件,页面内容由各自的 View 负责。 @@ -177,7 +175,7 @@ API 模块与页面按业务域对应。工作台页面使用 `api/workbench` - `POST /api/v1/auth/logout`:注销当前会话; - 后续请求自动携带 `X-Token` 请求头。 -路由和菜单按后端权限码控制可见性,页面中的创建、修改、投递、作废、下载和导出等动作也使用对应权限码单独控制。`READ_ONLY_ALL` 账号即使被误配了写权限码,前端仍不会展示写操作;最终授权结果始终以后端校验为准。系统设置尚无正式后端权限契约,生产默认隐藏,仅在显式开启 `VITE_MOCK_SETTINGS=true` 时用于演示。 +路由、侧栏和页面操作均按后端授权资源控制;导航每 15 秒及窗口重新聚焦时刷新,角色、菜单资源或员工角色绑定变更后也会主动刷新当前授权。角色权限从菜单资源树配置目录、菜单和按钮,员工角色绑定通过独立用户角色接口保存。菜单资源维护使用真实接口;系统参数卡片仍是演示数据,最终授权始终以后端校验为准。 签署工作台已接入的真实接口包括: @@ -198,11 +196,13 @@ API 模块与页面按业务域对应。工作台页面使用 `api/workbench` - `PUT /api/v1/templates/{id}`:更新模板元数据; - `GET/POST /api/v1/templates/{id}/versions`、`GET /api/v1/templates/{id}/versions/{versionId}`:版本列表、创建与版本详情(**正文 `contentHtml` 只在这个详情接口里返回**); - `POST /api/v1/templates/{id}/versions/{versionId}/{action}`(submit-review / reject / approve / publish / disable / archive):版本工作流,模板「删除」即走 archive; -- `GET /api/v1/users`、`GET /api/v1/roles`、`GET /api/v1/campuses`、`GET /api/v1/departments`:用户页真实查询及组织字典; -- `GET /api/v1/users/{id}/roles`、`PUT /api/v1/users/{id}/roles`、`PUT /api/v1/users/{id}/management`:用户角色读取和用户资料/角色聚合保存; +- `GET /api/v1/users`、`POST /api/v1/users`、`GET/PUT /api/v1/users/{id}`:员工列表、资料创建和资料更新; +- `GET /api/v1/users/{id}/roles`、`PUT /api/v1/users/{id}/roles`:单独查询和替换员工角色;用户资料接口不再同时保存角色或个人数据范围; - `POST /api/v1/users/{id}/password/reset`、`POST /api/v1/users/import/preview`、`POST /api/v1/users/import/commit`、`GET /api/v1/users/import/jobs/{jobId}`:管理员重置密码和用户导入流程; -- `GET /api/v1/roles/summary`、`GET/PUT /api/v1/roles/{roleId}/permissions`:角色人数统计和角色 API 权限分配; -- `GET/POST/PUT/DELETE /api/v1/permissions`:API 权限分页查询、详情、新增、修改和停用; +- `GET /api/v1/roles`、`GET /api/v1/roles/assignable`、`POST /api/v1/roles`、`PUT /api/v1/roles/{id}`、`PUT /api/v1/roles/{id}/status`、`DELETE /api/v1/roles/{id}`:角色列表与角色维护; +- `GET /api/v1/roles/permission-tree`、`GET/PUT /api/v1/roles/{id}/permissions`、`GET/PUT /api/v1/roles/{id}/data-scope`:基于菜单资源树配置角色权限和数据范围; +- `GET/POST /api/v1/menu-resources`、`PUT /api/v1/menu-resources/{id}`、`PUT /api/v1/menu-resources/{id}/status`、`PUT /api/v1/menu-resources/{id}/sort`、`DELETE /api/v1/menu-resources/{id}`:菜单资源树维护; +- `GET /api/v1/authorization/me`:当前用户权限、数据范围和可访问导航; - `GET /api/v1/templates/permission-config`:按权限配置范围分页查询模板; - `GET /api/v1/templates/permissions/matrix`、`POST /api/v1/templates/permissions/batch`:跨模板权限矩阵查询和批量新增/撤销; - `GET/POST/DELETE /api/v1/templates/{id}/permissions`:单模板权限兼容接口,其中前端使用 GET 补充用户继承预览; diff --git a/clinical-web/src/api/auth.ts b/clinical-web/src/api/auth.ts index 5776ecb..dd40b20 100644 --- a/clinical-web/src/api/auth.ts +++ b/clinical-web/src/api/auth.ts @@ -9,6 +9,7 @@ import { request } from '@/utils/request' import type { AuthenticatedUser, CaptchaResponse, + CurrentAuthorization, CurrentUserResponse, LoginRequest, LoginResponse, @@ -76,6 +77,11 @@ export async function getCurrentUser(): Promise { } } +export async function getCurrentAuthorization(): Promise { + const response = await request.get>('/v1/authorization/me') + return unwrapApiResponse(response) +} + export async function logout(): Promise { const response = await request.post>>('/v1/auth/logout') unwrapApiResponse(response) diff --git a/clinical-web/src/api/management/permissions.ts b/clinical-web/src/api/management/permissions.ts index f6ff23f..a701fe3 100644 --- a/clinical-web/src/api/management/permissions.ts +++ b/clinical-web/src/api/management/permissions.ts @@ -1,210 +1,56 @@ -import axios from 'axios' - import { unwrapApiResponse, unwrapNullableApiResponse } from '@/utils/api-response' -import { mockFlags } from '@/utils/mock-flags' import { request } from '@/utils/request' -import type { ApiResponse, PageResult } from '@/types/common' +import type { ApiResponse } from '@/types/common' import type { - ApiPermissionListResponse, - ApiPermissionQuery, - ApiPermissionRecord, - ApiPermissionResponseDto, - BackendPage, - CreateApiPermissionRequest, - UpdateApiPermissionRequest, + CreatePermissionResourceRequest, + PermissionResourceNodeDto, + UpdatePermissionResourceRequest, } from './types' -const useMockData = mockFlags.users - -export const isApiPermissionMockEnabled = useMockData - -const mockRecords: ApiPermissionRecord[] = [ - { - id: 'api-permission-001', - parentId: null, - code: 'system:user:query', - name: '查询用户', - resourceType: 'USER', - resourcePath: '/api/v1/users', - action: 'QUERY', - status: 'ENABLED', - createdAt: '2026-08-27 09:00', - updatedAt: '2026-08-27 09:00', - }, - { - id: 'api-permission-002', - parentId: null, - code: 'system:sign-task:create', - name: '创建签署任务', - resourceType: 'SIGN_TASK', - resourcePath: '/api/v1/sign-tasks', - action: 'CREATE', - status: 'ENABLED', - createdAt: '2026-08-27 09:02', - updatedAt: '2026-08-27 09:02', - }, -] - -function formatDateTime(value: string | null | undefined) { - if (!value) { - return '—' - } - - const date = new Date(value) - - if (Number.isNaN(date.getTime())) { - return value - .replace('T', ' ') - .replace(/\.\d+Z$/, '') - .replace(/Z$/, '') - } - - const pad = (part: number) => String(part).padStart(2, '0') - return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad( - date.getHours(), - )}:${pad(date.getMinutes())}` +export async function getPermissionResources(): Promise { + const response = await request.get>('/v1/menu-resources') + return unwrapApiResponse(response) } -function mapPermission(dto: ApiPermissionResponseDto): ApiPermissionRecord { - return { - id: dto.id, - parentId: dto.parentId ?? null, - code: dto.code, - name: dto.name, - resourceType: dto.resourceType, - resourcePath: dto.resourcePath ?? '', - action: dto.action ?? 'QUERY', - status: dto.status ?? 'ENABLED', - createdAt: formatDateTime(dto.createdAt), - updatedAt: formatDateTime(dto.updatedAt ?? dto.createdAt), - } +export async function createPermissionResource( + payload: CreatePermissionResourceRequest, +): Promise { + const response = await request.post>('/v1/menu-resources', payload) + return unwrapApiResponse(response).id } -function normalizePage( - data: BackendPage | T[], - fallbackPage: number, - fallbackPageSize: number, -): PageResult { - const records = Array.isArray(data) ? data : (data.records ?? data.items ?? data.content ?? []) - - return { - records, - total: Array.isArray(data) ? records.length : (data.total ?? records.length), - page: Array.isArray(data) ? fallbackPage : (data.page ?? fallbackPage), - pageSize: Array.isArray(data) ? fallbackPageSize : (data.size ?? fallbackPageSize), - } -} - -function toBackendQuery(query: ApiPermissionQuery) { - const params: Record = { - page: Math.max(query.page, 1), - size: Math.min(Math.max(query.pageSize, 1), 200), - } - - if (query.keyword?.trim()) { - params.keyword = query.keyword.trim() - } - - return params -} - -export function getPermissions(query: ApiPermissionQuery): Promise { - if (!useMockData) { - return request - .get>>('/v1/permissions', { - params: toBackendQuery(query), - }) - .then((response) => { - const page = normalizePage(unwrapApiResponse(response), query.page, query.pageSize) - - return { - ...page, - records: page.records.map(mapPermission), - } - }) - } - - const keyword = query.keyword?.trim().toLowerCase() - const filteredRecords = mockRecords.filter((record) => { - return ( - !keyword || - [record.code, record.name, record.resourceType, record.resourcePath] - .join(' ') - .toLowerCase() - .includes(keyword) - ) - }) - const page = Math.max(query.page, 1) - const pageSize = Math.max(query.pageSize, 1) - const start = (page - 1) * pageSize - - return Promise.resolve({ - records: filteredRecords.slice(start, start + pageSize).map((record) => ({ ...record })), - total: filteredRecords.length, - page, - pageSize, - }) -} - -export async function getPermission(id: string): Promise { - if (useMockData) { - const record = mockRecords.find((item) => item.id === id) - return record ? { ...record } : null - } - - try { - const response = await request.get>( - `/v1/permissions/${encodeURIComponent(id)}`, - ) - return mapPermission(unwrapApiResponse(response)) - } catch (error) { - if (axios.isAxiosError(error) && error.response?.status === 404) { - return null - } - - throw error - } -} - -export function createPermission( - payload: CreateApiPermissionRequest, -): Promise { - if (useMockData) { - return Promise.reject(new Error('Mock 模式不执行 API 权限写操作')) - } - - return request - .post>('/v1/permissions', payload) - .then(unwrapApiResponse) - .then(mapPermission) -} - -export function updatePermission( +export async function updatePermissionResource( id: string, - payload: UpdateApiPermissionRequest, -): Promise { - if (useMockData) { - return Promise.reject(new Error('Mock 模式不执行 API 权限写操作')) - } - - return request - .put>( - `/v1/permissions/${encodeURIComponent(id)}`, - payload, - ) - .then(unwrapApiResponse) - .then(mapPermission) + payload: UpdatePermissionResourceRequest, +): Promise { + const response = await request.put>( + `/v1/menu-resources/${encodeURIComponent(id)}`, + payload, + ) + unwrapNullableApiResponse(response) } -export function deletePermission(id: string): Promise { - if (useMockData) { - return Promise.reject(new Error('Mock 模式不执行 API 权限写操作')) - } - - return request - .delete>(`/v1/permissions/${encodeURIComponent(id)}`) - .then((response) => { - unwrapNullableApiResponse(response) - }) +export async function setPermissionResourceStatus(id: string, enabled: boolean): Promise { + const response = await request.put>( + `/v1/menu-resources/${encodeURIComponent(id)}/status`, + { enabled }, + ) + unwrapNullableApiResponse(response) +} + +export async function sortPermissionResource(id: string, sortNo: number): Promise { + const response = await request.put>( + `/v1/menu-resources/${encodeURIComponent(id)}/sort`, + { sortNo }, + ) + unwrapNullableApiResponse(response) +} + +export async function deletePermissionResource(id: string, cascade = false): Promise { + const response = await request.delete>( + `/v1/menu-resources/${encodeURIComponent(id)}`, + { params: { cascade } }, + ) + unwrapNullableApiResponse(response) } diff --git a/clinical-web/src/api/management/roles.ts b/clinical-web/src/api/management/roles.ts index 76e6634..90397ca 100644 --- a/clinical-web/src/api/management/roles.ts +++ b/clinical-web/src/api/management/roles.ts @@ -1,113 +1,64 @@ -import { unwrapApiResponse } from '@/utils/api-response' -import { mockFlags } from '@/utils/mock-flags' +import { unwrapApiResponse, unwrapNullableApiResponse } from '@/utils/api-response' import { request } from '@/utils/request' import type { ApiResponse } from '@/types/common' import type { BackendCollection, CreateRoleRequest, - RolePermissionsResponseDto, - RolePermissionsUpdateRequest, + PermissionResourceNodeDto, + RoleDataScopeRequest, + RoleDataScopeResponseDto, RoleRecord, - RoleQuery, RoleResponseDto, - RoleSummaryResponseDto, UpdateRoleRequest, } from './types' -const useMockData = mockFlags.users - const fallbackColors = ['#6b5bd2', '#0e6e8c', '#0f9d6c', '#d9821f', '#c65d5d'] -function normalizeRole(dto: RoleResponseDto, index: number, userCount?: number | null): RoleRecord { +function normalizeRole(dto: RoleResponseDto, index: number): RoleRecord { return { id: dto.id, code: dto.code, name: dto.name, - description: dto.description ?? '—', + description: dto.description ?? '', color: fallbackColors[index % fallbackColors.length], - userCount: userCount ?? undefined, - status: dto.status ?? 'ENABLED', + status: dto.status ?? 'DISABLED', + menuPermissionCount: dto.menuPermissionCount ?? 0, + buttonPermissionCount: dto.buttonPermissionCount ?? 0, + dataScope: dto.dataScope ?? 'SELF', + configured: dto.configured ?? false, createdAt: dto.createdAt, updatedAt: dto.updatedAt, version: dto.version ?? undefined, } } -function normalizeRoleSummary(dto: RoleSummaryResponseDto, index: number): RoleRecord { - return normalizeRole( - { - id: dto.roleId, - code: dto.code, - name: dto.name, - description: dto.description, - status: dto.status, - }, - index, - dto.userCount, +function normalizeCollection(collection: BackendCollection): T[] { + return Array.isArray(collection) + ? collection + : (collection.records ?? collection.items ?? collection.content ?? []) +} + +export async function getRoles(): Promise { + const response = await request.get>>('/v1/roles') + return normalizeCollection(unwrapApiResponse(response)).map(normalizeRole) +} + +export async function getAssignableRoles(): Promise { + const response = + await request.get>>('/v1/roles/assignable') + return normalizeCollection(unwrapApiResponse(response)).map(normalizeRole) +} + +export async function getRolePermissionTree(): Promise { + const response = await request.get>( + '/v1/roles/permission-tree', ) + return unwrapApiResponse(response) } -function toBackendQuery(query: RoleQuery) { - const params: Record = { - page: Math.max(query.page, 1), - size: Math.min(Math.max(query.pageSize, 1), 200), - } - - if (query.keyword?.trim()) { - params.keyword = query.keyword.trim() - } - - return params -} - -export function getRoles(query: RoleQuery = { page: 1, pageSize: 200 }): Promise { - if (useMockData) { - return Promise.resolve([]) - } - - return request - .get>>('/v1/roles', { - params: toBackendQuery(query), - }) - .then(unwrapApiResponse) - .then((data) => { - const records = Array.isArray(data) - ? data - : (data.records ?? data.items ?? data.content ?? []) - return records.map((role, index) => normalizeRole(role, index)) - }) -} - -export function getRoleSummaries(campusId?: string): Promise { - if (useMockData) { - return Promise.resolve([]) - } - - return request - .get>>('/v1/roles/summary', { - params: campusId ? { campusId } : undefined, - }) - .then(unwrapApiResponse) - .then((data) => { - const records = Array.isArray(data) - ? data - : (data.records ?? data.items ?? data.content ?? []) - return records.map(normalizeRoleSummary) - }) -} - -export async function getRolePermissions(roleId: string): Promise { - if (useMockData) { - return { - roleId, - permissions: [], - version: 0, - updatedAt: '', - } - } - - const response = await request.get>( +export async function getRolePermissions(roleId: string): Promise { + const response = await request.get>( `/v1/roles/${encodeURIComponent(roleId)}/permissions`, ) return unwrapApiResponse(response) @@ -115,51 +66,55 @@ export async function getRolePermissions(roleId: string): Promise { - if (useMockData) { - throw new Error('Mock 模式不执行角色权限写操作') - } - - const response = await request.put>( + permissionIds: string[], +): Promise { + const response = await request.put>( `/v1/roles/${encodeURIComponent(roleId)}/permissions`, - payload, + { permissionIds }, + ) + unwrapNullableApiResponse(response) +} + +export async function getRoleDataScope(roleId: string): Promise { + const response = await request.get>( + `/v1/roles/${encodeURIComponent(roleId)}/data-scope`, ) return unwrapApiResponse(response) } -export function createRole(payload: CreateRoleRequest): Promise { - if (useMockData) { - return Promise.reject(new Error('Mock 模式不执行角色写操作')) - } - - return request - .post>('/v1/roles', payload) - .then(unwrapApiResponse) - .then((role) => normalizeRole(role, 0)) +export async function updateRoleDataScope( + roleId: string, + payload: RoleDataScopeRequest, +): Promise { + const response = await request.put>( + `/v1/roles/${encodeURIComponent(roleId)}/data-scope`, + payload, + ) + unwrapNullableApiResponse(response) } -export function updateRole(id: string, payload: UpdateRoleRequest): Promise { - if (useMockData) { - return Promise.reject(new Error('Mock 模式不执行角色写操作')) - } - - return request - .put>(`/v1/roles/${encodeURIComponent(id)}`, payload) - .then(unwrapApiResponse) - .then((role) => normalizeRole(role, 0)) +export async function createRole(payload: CreateRoleRequest): Promise { + const response = await request.post>('/v1/roles', payload) + return unwrapApiResponse(response).id } -export function deleteRole(id: string): Promise { - if (useMockData) { - return Promise.reject(new Error('Mock 模式不执行角色写操作')) - } - - return request - .delete>(`/v1/roles/${encodeURIComponent(id)}`) - .then((response) => { - if (response.code !== 0 && response.code !== '0') { - throw new Error(response.message || '角色删除失败') - } - }) +export async function updateRole(id: string, payload: UpdateRoleRequest): Promise { + const response = await request.put>( + `/v1/roles/${encodeURIComponent(id)}`, + payload, + ) + unwrapNullableApiResponse(response) +} + +export async function setRoleStatus(id: string, enabled: boolean): Promise { + const response = await request.put>( + `/v1/roles/${encodeURIComponent(id)}/status`, + { enabled }, + ) + unwrapNullableApiResponse(response) +} + +export async function deleteRole(id: string): Promise { + const response = await request.delete>(`/v1/roles/${encodeURIComponent(id)}`) + unwrapNullableApiResponse(response) } diff --git a/clinical-web/src/api/management/types.ts b/clinical-web/src/api/management/types.ts index 0683151..45b0869 100644 --- a/clinical-web/src/api/management/types.ts +++ b/clinical-web/src/api/management/types.ts @@ -137,6 +137,10 @@ export interface RoleRecord { color?: string userCount?: number status: RoleStatus + menuPermissionCount: number + buttonPermissionCount: number + dataScope: RoleDataScopeType + configured: boolean createdAt?: string | null updatedAt?: string | null version?: number @@ -154,83 +158,78 @@ export interface RoleResponseDto { name: string description?: string | null status?: RoleStatus | null + menuPermissionCount?: number | null + buttonPermissionCount?: number | null + dataScope?: RoleDataScopeType | null + configured?: boolean | null createdAt?: string | null updatedAt?: string | null version?: number | null } -export interface RoleSummaryResponseDto { - roleId: string - code: string - name: string - description?: string | null - status?: RoleStatus | null - userCount?: number | null -} - export interface CreateRoleRequest { code: string name: string description?: string - status?: RoleStatus } -export type UpdateRoleRequest = Partial +export type UpdateRoleRequest = CreateRoleRequest -export type ApiPermissionStatus = 'ENABLED' | 'DISABLED' -export type ApiPermissionAction = 'QUERY' | 'CREATE' | 'UPDATE' | 'DISABLE' +export type RoleDataScopeType = 'ALL' | 'CUSTOM' | 'DEPARTMENT' | 'DEPARTMENT_AND_CHILDREN' | 'SELF' -export interface ApiPermissionQuery extends PageQuery { - keyword?: string +export interface RoleDataScopeResponseDto { + scopeType: RoleDataScopeType + campusIds: string[] + departmentIds: string[] } -export interface ApiPermissionRecord { +export interface RoleDataScopeRequest { + scopeType: RoleDataScopeType + campusIds: string[] + departmentIds: string[] +} + +export interface PermissionResourceNodeDto { id: string parentId: string | null - code: string + code: string | null name: string - resourceType: string - resourcePath: string - action: ApiPermissionAction | string - status: ApiPermissionStatus - createdAt: string - updatedAt: string + resourceType: 'DIR' | 'MENU' | 'BUTTON' + routePath: string | null + componentPath: string | null + icon: string | null + sortNo: number + visible: boolean + cacheable: boolean + status: 'ENABLED' | 'DISABLED' + protectedNode: boolean + children: PermissionResourceNodeDto[] } -export interface ApiPermissionResponseDto { - id: string - parentId?: string | null - code: string +export interface CreatePermissionResourceRequest { + resourceType: 'DIR' | 'MENU' | 'BUTTON' + parentId: string | null + code: string | null name: string - resourceType: string - resourcePath?: string | null - action?: ApiPermissionAction | string | null - status?: ApiPermissionStatus | null - createdAt?: string | null - updatedAt?: string | null + routePath?: string | null + componentPath?: string | null + icon?: string | null + sortNo: number + visible?: boolean + cacheable?: boolean } -export interface CreateApiPermissionRequest { - parentId?: string | null - code: string +export interface UpdatePermissionResourceRequest { + code: string | null name: string - resourceType: string - resourcePath?: string - action?: ApiPermissionAction - status?: ApiPermissionStatus + routePath?: string | null + componentPath?: string | null + icon?: string | null + sortNo: number + visible?: boolean + cacheable?: boolean } -export interface UpdateApiPermissionRequest { - parentId?: string | null - name: string - resourceType: string - resourcePath?: string - action?: ApiPermissionAction - status: ApiPermissionStatus -} - -export type ApiPermissionListResponse = PageResult - export interface CampusRecord { id: string code: string @@ -444,7 +443,6 @@ export interface UserRecord { departmentName?: string roleNames?: string[] roleIds?: string[] - dataScope?: string version?: number passwordChangeRequired?: boolean } @@ -463,7 +461,6 @@ export interface UserResponseDto { departmentId?: string | null departmentName?: string | null roles?: UserRoleSummaryDto[] | null - dataScope?: string | null status?: BackendUserStatus | null lastLoginAt?: string | null createdAt?: string | null @@ -489,12 +486,9 @@ export interface CreateUserRequest { campusId: string departmentId?: string | null status?: BackendUserStatus - dataScope?: BackendDataScope - roleIds?: string[] } export type BackendUserStatus = 'ENABLED' | 'DISABLED' | 'LOCKED' -export type BackendDataScope = 'ALL' | 'CAMPUS' | 'DEPARTMENT' | 'READ_ONLY_ALL' export interface UpdateUserRequest { displayName: string @@ -504,49 +498,13 @@ export interface UpdateUserRequest { campusId: string departmentId?: string | null status: BackendUserStatus - dataScope: BackendDataScope clearPhone?: boolean clearEmail?: boolean expectedVersion?: number } -export interface UserManagementUpdateRequest { - displayName: string - employeeNo?: string - phone?: string - email?: string - clearPhone?: boolean - clearEmail?: boolean - campusId: string - departmentId?: string | null - status: BackendUserStatus - dataScope: BackendDataScope - roleIds: string[] - expectedVersion?: number -} - -export interface UserRolesResponseDto { - userId: string - roles: UserRoleSummaryDto[] - version: number - updatedAt: string -} - export interface UserRolesUpdateRequest { roleIds: string[] - expectedVersion?: number -} - -export interface RolePermissionsResponseDto { - roleId: string - permissions: ApiPermissionResponseDto[] - version: number - updatedAt: string -} - -export interface RolePermissionsUpdateRequest { - permissionIds: string[] - expectedVersion?: number } export type PermissionPolicyLevel = 'NONE' | 'VIEW' | 'USE' | 'MAINTAIN' diff --git a/clinical-web/src/api/management/users.ts b/clinical-web/src/api/management/users.ts index 4b355a8..ecedc07 100644 --- a/clinical-web/src/api/management/users.ts +++ b/clinical-web/src/api/management/users.ts @@ -14,12 +14,11 @@ import type { UserImportCommitRequest, UserImportJobResponseDto, UserImportPreviewResponseDto, - UserManagementUpdateRequest, UserListResponse, UserQuery, UserRecord, UserResponseDto, - UserRolesResponseDto, + UserRoleSummaryDto, UserRolesUpdateRequest, } from './types' @@ -106,7 +105,6 @@ function mapUser(dto: UserResponseDto): UserRecord { departmentName: dto.departmentName ?? undefined, roleNames, roleIds: roles.map((role) => role.id), - dataScope: dto.dataScope ?? '—', version: dto.version ?? undefined, passwordChangeRequired: dto.passwordChangeRequired, } @@ -224,31 +222,12 @@ export function updateUser(id: string, payload: UpdateUserRequest): Promise { +export async function getUserRoles(userId: string): Promise { if (useMockData) { - return Promise.reject(new Error('Mock 模式不执行用户写操作')) + return [] } - return request - .put>(`/v1/users/${encodeURIComponent(id)}/management`, payload) - .then(unwrapApiResponse) - .then(mapUser) -} - -export async function getUserRoles(userId: string): Promise { - if (useMockData) { - return { - userId, - roles: [], - version: 0, - updatedAt: '', - } - } - - const response = await request.get>( + const response = await request.get>( `/v1/users/${encodeURIComponent(userId)}/roles`, ) return unwrapApiResponse(response) @@ -257,16 +236,16 @@ export async function getUserRoles(userId: string): Promise { +): Promise { if (useMockData) { return Promise.reject(new Error('Mock 模式不执行用户角色写操作')) } - const response = await request.put>( + const response = await request.put>( `/v1/users/${encodeURIComponent(userId)}/roles`, payload, ) - return unwrapApiResponse(response) + unwrapNullableApiResponse(response) } export async function resetUserPassword( diff --git a/clinical-web/src/components/layout/Header.vue b/clinical-web/src/components/layout/Header.vue index 6a46b16..3e4ba82 100644 --- a/clinical-web/src/components/layout/Header.vue +++ b/clinical-web/src/components/layout/Header.vue @@ -3,12 +3,20 @@ import { computed, onMounted } from 'vue' import { useRoute } from 'vue-router' import { useAppStore } from '@/stores/app' +import { useLoginStore } from '@/stores/login' const route = useRoute() const appStore = useAppStore() +const loginStore = useLoginStore() -const pageTitle = computed(() => String(route.meta.title ?? '首页')) -const pageGroup = computed(() => String(route.meta.group ?? '工作台')) +const currentResource = computed(() => loginStore.menuForPath(route.path)) +const pageTitle = computed(() => currentResource.value?.name ?? String(route.meta.title ?? '首页')) +const pageGroup = computed(() => { + const directory = loginStore.authorization?.navigation.find((item) => + item.children?.some((child) => child.id === currentResource.value?.id), + ) + return directory?.name ?? String(route.meta.group ?? '工作台') +}) const selectedCampusId = computed({ get: () => appStore.selectedCampusId, set: (id) => appStore.selectCampus(id), diff --git a/clinical-web/src/components/layout/Menu.vue b/clinical-web/src/components/layout/Menu.vue index d7a15cf..6ac7a67 100644 --- a/clinical-web/src/components/layout/Menu.vue +++ b/clinical-web/src/components/layout/Menu.vue @@ -3,64 +3,30 @@ import { computed } from 'vue' import { RouterLink, useRouter } from 'vue-router' import { useLoginStore } from '@/stores/login' -import { mockFlags } from '@/utils/mock-flags' - -interface MenuItem { - path: string - label: string - permission?: string - available?: boolean -} - -interface MenuGroup { - label: string - items: MenuItem[] -} const loginStore = useLoginStore() const router = useRouter() -const menuGroups: MenuGroup[] = [ - { - label: '工作台', - items: [ - { path: '/workbench/home', label: '首页', permission: 'sign:task:query' }, - { path: '/workbench/signing', label: '签署工作台', permission: 'sign:task:query' }, - ], - }, - { - label: '管理', - items: [ - { - path: '/management/documents', - label: '文档库管理', - permission: 'sign:template:query', - }, - { - path: '/management/reports', - label: '报表分析', - permission: 'report:signing:query', - }, - { path: '/management/users', label: '用户与权限', permission: 'system:user:query' }, - { - path: '/management/document-permissions', - label: '文档权限', - permission: 'sign:template:permission:query', - }, - { path: '/management/settings', label: '系统设置', available: mockFlags.settings }, - ], - }, -] - const visibleMenuGroups = computed(() => - menuGroups - .map((group) => ({ - ...group, - items: group.items.filter( - (item) => - item.available !== false && - (!item.permission || loginStore.hasPermission(item.permission)), - ), + (loginStore.authorization?.navigation ?? []) + .filter((directory) => directory.resourceType === 'DIR' && directory.status === 'ENABLED') + .map((directory) => ({ + label: directory.name, + items: (directory.children ?? []) + .filter( + (menu) => + menu.resourceType === 'MENU' && + menu.status === 'ENABLED' && + menu.visible && + Boolean(menu.routePath) && + Boolean(menu.code) && + loginStore.hasPermission(menu.code ?? ''), + ) + .map((menu) => ({ + path: menu.routePath as string, + label: menu.name, + icon: menu.icon, + })), })) .filter((group) => group.items.length), ) @@ -112,7 +78,7 @@ async function handleLogout() { class="menu-item" active-class="is-active" > -