feat: 接入权限接口

This commit is contained in:
xy
2026-09-24 08:31:21 +08:00
parent 93fb5fd2c4
commit e5b5412322
24 changed files with 2398 additions and 1738 deletions
+42 -196
View File
@@ -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<PermissionResourceNodeDto[]> {
const response = await request.get<ApiResponse<PermissionResourceNodeDto[]>>('/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<string> {
const response = await request.post<ApiResponse<{ id: string }>>('/v1/menu-resources', payload)
return unwrapApiResponse(response).id
}
function normalizePage<T>(
data: BackendPage<T> | T[],
fallbackPage: number,
fallbackPageSize: number,
): PageResult<T> {
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<string, string | number> = {
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<ApiPermissionListResponse> {
if (!useMockData) {
return request
.get<ApiResponse<BackendPage<ApiPermissionResponseDto>>>('/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<ApiPermissionRecord | null> {
if (useMockData) {
const record = mockRecords.find((item) => item.id === id)
return record ? { ...record } : null
}
try {
const response = await request.get<ApiResponse<ApiPermissionResponseDto>>(
`/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<ApiPermissionRecord> {
if (useMockData) {
return Promise.reject(new Error('Mock 模式不执行 API 权限写操作'))
}
return request
.post<ApiResponse<ApiPermissionResponseDto>>('/v1/permissions', payload)
.then(unwrapApiResponse)
.then(mapPermission)
}
export function updatePermission(
export async function updatePermissionResource(
id: string,
payload: UpdateApiPermissionRequest,
): Promise<ApiPermissionRecord> {
if (useMockData) {
return Promise.reject(new Error('Mock 模式不执行 API 权限写操作'))
}
return request
.put<ApiResponse<ApiPermissionResponseDto>>(
`/v1/permissions/${encodeURIComponent(id)}`,
payload,
)
.then(unwrapApiResponse)
.then(mapPermission)
payload: UpdatePermissionResourceRequest,
): Promise<void> {
const response = await request.put<ApiResponse<null>>(
`/v1/menu-resources/${encodeURIComponent(id)}`,
payload,
)
unwrapNullableApiResponse(response)
}
export function deletePermission(id: string): Promise<void> {
if (useMockData) {
return Promise.reject(new Error('Mock 模式不执行 API 权限写操作'))
}
return request
.delete<ApiResponse<null>>(`/v1/permissions/${encodeURIComponent(id)}`)
.then((response) => {
unwrapNullableApiResponse(response)
})
export async function setPermissionResourceStatus(id: string, enabled: boolean): Promise<void> {
const response = await request.put<ApiResponse<null>>(
`/v1/menu-resources/${encodeURIComponent(id)}/status`,
{ enabled },
)
unwrapNullableApiResponse(response)
}
export async function sortPermissionResource(id: string, sortNo: number): Promise<void> {
const response = await request.put<ApiResponse<null>>(
`/v1/menu-resources/${encodeURIComponent(id)}/sort`,
{ sortNo },
)
unwrapNullableApiResponse(response)
}
export async function deletePermissionResource(id: string, cascade = false): Promise<void> {
const response = await request.delete<ApiResponse<null>>(
`/v1/menu-resources/${encodeURIComponent(id)}`,
{ params: { cascade } },
)
unwrapNullableApiResponse(response)
}
+76 -121
View File
@@ -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<T>(collection: BackendCollection<T>): T[] {
return Array.isArray(collection)
? collection
: (collection.records ?? collection.items ?? collection.content ?? [])
}
export async function getRoles(): Promise<RoleRecord[]> {
const response = await request.get<ApiResponse<BackendCollection<RoleResponseDto>>>('/v1/roles')
return normalizeCollection(unwrapApiResponse(response)).map(normalizeRole)
}
export async function getAssignableRoles(): Promise<RoleRecord[]> {
const response =
await request.get<ApiResponse<BackendCollection<RoleResponseDto>>>('/v1/roles/assignable')
return normalizeCollection(unwrapApiResponse(response)).map(normalizeRole)
}
export async function getRolePermissionTree(): Promise<PermissionResourceNodeDto[]> {
const response = await request.get<ApiResponse<PermissionResourceNodeDto[]>>(
'/v1/roles/permission-tree',
)
return unwrapApiResponse(response)
}
function toBackendQuery(query: RoleQuery) {
const params: Record<string, string | number> = {
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<RoleRecord[]> {
if (useMockData) {
return Promise.resolve([])
}
return request
.get<ApiResponse<BackendCollection<RoleResponseDto>>>('/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<RoleRecord[]> {
if (useMockData) {
return Promise.resolve([])
}
return request
.get<ApiResponse<BackendCollection<RoleSummaryResponseDto>>>('/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<RolePermissionsResponseDto> {
if (useMockData) {
return {
roleId,
permissions: [],
version: 0,
updatedAt: '',
}
}
const response = await request.get<ApiResponse<RolePermissionsResponseDto>>(
export async function getRolePermissions(roleId: string): Promise<string[]> {
const response = await request.get<ApiResponse<string[]>>(
`/v1/roles/${encodeURIComponent(roleId)}/permissions`,
)
return unwrapApiResponse(response)
@@ -115,51 +66,55 @@ export async function getRolePermissions(roleId: string): Promise<RolePermission
export async function updateRolePermissions(
roleId: string,
payload: RolePermissionsUpdateRequest,
): Promise<RolePermissionsResponseDto> {
if (useMockData) {
throw new Error('Mock 模式不执行角色权限写操作')
}
const response = await request.put<ApiResponse<RolePermissionsResponseDto>>(
permissionIds: string[],
): Promise<void> {
const response = await request.put<ApiResponse<null>>(
`/v1/roles/${encodeURIComponent(roleId)}/permissions`,
payload,
{ permissionIds },
)
unwrapNullableApiResponse(response)
}
export async function getRoleDataScope(roleId: string): Promise<RoleDataScopeResponseDto> {
const response = await request.get<ApiResponse<RoleDataScopeResponseDto>>(
`/v1/roles/${encodeURIComponent(roleId)}/data-scope`,
)
return unwrapApiResponse(response)
}
export function createRole(payload: CreateRoleRequest): Promise<RoleRecord> {
if (useMockData) {
return Promise.reject(new Error('Mock 模式不执行角色写操作'))
}
return request
.post<ApiResponse<RoleResponseDto>>('/v1/roles', payload)
.then(unwrapApiResponse)
.then((role) => normalizeRole(role, 0))
export async function updateRoleDataScope(
roleId: string,
payload: RoleDataScopeRequest,
): Promise<void> {
const response = await request.put<ApiResponse<null>>(
`/v1/roles/${encodeURIComponent(roleId)}/data-scope`,
payload,
)
unwrapNullableApiResponse(response)
}
export function updateRole(id: string, payload: UpdateRoleRequest): Promise<RoleRecord> {
if (useMockData) {
return Promise.reject(new Error('Mock 模式不执行角色写操作'))
}
return request
.put<ApiResponse<RoleResponseDto>>(`/v1/roles/${encodeURIComponent(id)}`, payload)
.then(unwrapApiResponse)
.then((role) => normalizeRole(role, 0))
export async function createRole(payload: CreateRoleRequest): Promise<string> {
const response = await request.post<ApiResponse<{ id: string }>>('/v1/roles', payload)
return unwrapApiResponse(response).id
}
export function deleteRole(id: string): Promise<void> {
if (useMockData) {
return Promise.reject(new Error('Mock 模式不执行角色写操作'))
}
return request
.delete<ApiResponse<null>>(`/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<void> {
const response = await request.put<ApiResponse<null>>(
`/v1/roles/${encodeURIComponent(id)}`,
payload,
)
unwrapNullableApiResponse(response)
}
export async function setRoleStatus(id: string, enabled: boolean): Promise<void> {
const response = await request.put<ApiResponse<null>>(
`/v1/roles/${encodeURIComponent(id)}/status`,
{ enabled },
)
unwrapNullableApiResponse(response)
}
export async function deleteRole(id: string): Promise<void> {
const response = await request.delete<ApiResponse<null>>(`/v1/roles/${encodeURIComponent(id)}`)
unwrapNullableApiResponse(response)
}
+50 -92
View File
@@ -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<CreateRoleRequest>
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<ApiPermissionRecord>
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'
+7 -28
View File
@@ -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<User
.then(mapUser)
}
export function updateUserManagement(
id: string,
payload: UserManagementUpdateRequest,
): Promise<UserRecord> {
export async function getUserRoles(userId: string): Promise<UserRoleSummaryDto[]> {
if (useMockData) {
return Promise.reject(new Error('Mock 模式不执行用户写操作'))
return []
}
return request
.put<ApiResponse<UserResponseDto>>(`/v1/users/${encodeURIComponent(id)}/management`, payload)
.then(unwrapApiResponse)
.then(mapUser)
}
export async function getUserRoles(userId: string): Promise<UserRolesResponseDto> {
if (useMockData) {
return {
userId,
roles: [],
version: 0,
updatedAt: '',
}
}
const response = await request.get<ApiResponse<UserRolesResponseDto>>(
const response = await request.get<ApiResponse<UserRoleSummaryDto[]>>(
`/v1/users/${encodeURIComponent(userId)}/roles`,
)
return unwrapApiResponse(response)
@@ -257,16 +236,16 @@ export async function getUserRoles(userId: string): Promise<UserRolesResponseDto
export async function updateUserRoles(
userId: string,
payload: UserRolesUpdateRequest,
): Promise<UserRolesResponseDto> {
): Promise<void> {
if (useMockData) {
return Promise.reject(new Error('Mock 模式不执行用户角色写操作'))
}
const response = await request.put<ApiResponse<UserRolesResponseDto>>(
const response = await request.put<ApiResponse<null>>(
`/v1/users/${encodeURIComponent(userId)}/roles`,
payload,
)
return unwrapApiResponse(response)
unwrapNullableApiResponse(response)
}
export async function resetUserPassword(