fix: 接入用户与权限后端接口

This commit is contained in:
xy
2026-09-03 12:18:57 +08:00
parent e43a118dee
commit 48e5293509
19 changed files with 1982 additions and 375 deletions
+335 -25
View File
@@ -3,13 +3,34 @@ import { computed, onMounted, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { getCampuses, getDepartments } from '@/api/management/organization'
import { getRoles } from '@/api/management/roles'
import { deleteUser, getUserDetail, getUsers, isUserMockEnabled } from '@/api/management/users'
import { createUser, updateUser } from '@/api/management/users'
import { getPermissions } from '@/api/management/permissions'
import {
getRolePermissions,
getRoleSummaries,
getRoles,
updateRolePermissions,
} from '@/api/management/roles'
import {
commitUserImport,
createUser,
deleteUser,
getUserDetail,
getUserImportJob,
getUsers,
isUserMockEnabled,
previewUserImport,
resetUserPassword,
updateUserManagement,
} from '@/api/management/users'
import type {
BackendDataScope,
BackendUserStatus,
ApiPermissionRecord,
CampusRecord,
DepartmentRecord,
RoleRecord,
UserImportJobResponseDto,
UserImportPreviewResponseDto,
UserRecord as ApiUserRecord,
} from '@/api/management/types'
import { useAppStore } from '@/stores/app'
@@ -17,6 +38,9 @@ import { useAppStore } from '@/stores/app'
import RoleSummaryCards from './components/RoleSummaryCards.vue'
import UserTable from './components/UserTable.vue'
import UserEditorDialog from './components/UserEditorDialog.vue'
import UserImportDialog from './components/UserImportDialog.vue'
import UserPasswordResetDialog from './components/UserPasswordResetDialog.vue'
import RoleApiPermissionPanel from './components/RoleApiPermissionPanel.vue'
import { roleColors, roleSummaries, users } from './mock'
import type { RoleSummary, UserEditorForm, UserTableRow } from './types'
@@ -24,6 +48,7 @@ const appStore = useAppStore()
const loading = ref(true)
const keyword = ref('')
const roleRows = ref<RoleSummary[]>([])
const roleOptions = ref<RoleRecord[]>([])
const userRows = ref<UserTableRow[]>([])
const roleColorMap = ref<Record<string, string>>(roleColors)
const userPage = ref(1)
@@ -35,6 +60,20 @@ const editorVisible = ref(false)
const editorInitial = ref<UserEditorForm | null>(null)
const editorSaving = ref(false)
const disablingUserId = ref<string | null>(null)
const passwordResetVisible = ref(false)
const passwordResetTarget = ref<UserTableRow | null>(null)
const passwordResetSaving = ref(false)
const importVisible = ref(false)
const importLoading = ref(false)
const importPreview = ref<UserImportPreviewResponseDto | null>(null)
const importJob = ref<UserImportJobResponseDto | null>(null)
const apiPermissions = ref<ApiPermissionRecord[]>([])
const selectedRoleId = ref('')
const selectedRolePermissionIds = ref<string[]>([])
const rolePermissionVersion = ref<number | undefined>()
const apiPermissionLoading = ref(false)
const apiPermissionSaving = ref(false)
const apiPermissionError = ref('')
let userRequestId = 0
const mockCampuses: CampusRecord[] = [
@@ -84,6 +123,15 @@ async function loadUsers() {
campuses.value = mockCampuses.map((campus) => ({ ...campus }))
departments.value = mockDepartments.map((department) => ({ ...department }))
roleRows.value = roleSummaries.map((role) => ({ ...role }))
roleOptions.value = roleSummaries.map((role) => ({
id: role.id,
code: role.id,
name: role.name,
description: role.description,
color: role.color,
userCount: role.userCount ?? undefined,
status: 'ENABLED' as const,
}))
userRows.value = users.map((user) => ({ ...user }))
roleColorMap.value = roleColors
userTotal.value = userRows.value.length
@@ -100,13 +148,20 @@ async function loadUsers() {
const selectedCampusId = campusRecords.find(
(campus) => campus.name === appStore.selectedCampus,
)?.id
const usersResult = await getUsers({
page: userPage.value,
pageSize: userPageSize.value,
keyword: keyword.value.trim() || undefined,
status: 'all',
campusId: selectedCampusId,
})
const [usersResult, roleSummaryResult] = await Promise.allSettled([
getUsers({
page: userPage.value,
pageSize: userPageSize.value,
keyword: keyword.value.trim() || undefined,
status: 'all',
campusId: selectedCampusId,
}),
getRoleSummaries(selectedCampusId),
])
if (usersResult.status === 'rejected') {
throw usersResult.reason
}
if (requestId !== userRequestId) {
return
@@ -124,22 +179,33 @@ async function loadUsers() {
)
campuses.value = campusesResult.status === 'fulfilled' ? campusesResult.value : []
departments.value = departmentsResult.status === 'fulfilled' ? departmentsResult.value : []
const apiUsers = usersResult.records
const apiUsers = usersResult.value.records
userRows.value = apiUsers.map((user) => mapUserRow(user, campusNames, departmentNames))
userTotal.value = usersResult.total
userTotal.value = usersResult.value.total
const roles = rolesResult.status === 'fulfilled' ? rolesResult.value : []
roleRows.value = roles.map((role) => mapRoleSummary(role))
const summaryRoles = roleSummaryResult.status === 'fulfilled' ? roleSummaryResult.value : roles
roleOptions.value = roles.length ? roles : summaryRoles
roleRows.value = summaryRoles.map((role) => mapRoleSummary(role))
roleColorMap.value = Object.fromEntries(
roles.map((role) => [role.name, role.color ?? '#8a9aa3']),
roleOptions.value.map((role) => [role.name, role.color ?? '#8a9aa3']),
)
if (rolesResult.status === 'rejected' || roleSummaryResult.status === 'rejected') {
ElMessage.warning('角色列表或人数统计加载不完整,部分角色信息可能暂不可用')
}
if (!apiPermissions.value.length) {
void loadApiPermissionCatalog()
}
} catch {
if (requestId !== userRequestId) {
return
}
roleRows.value = []
roleOptions.value = []
userRows.value = []
userTotal.value = 0
ElMessage.error('用户与角色加载失败,请稍后重试')
@@ -150,6 +216,103 @@ async function loadUsers() {
}
}
async function loadSelectedRolePermissions() {
if (!selectedRoleId.value) {
selectedRolePermissionIds.value = []
rolePermissionVersion.value = undefined
return
}
const response = await getRolePermissions(selectedRoleId.value)
selectedRolePermissionIds.value = response.permissions.map((permission) => permission.id)
rolePermissionVersion.value = response.version
}
async function loadApiPermissionCatalog() {
if (isUserMockEnabled || apiPermissionLoading.value) {
return
}
apiPermissionLoading.value = true
apiPermissionError.value = ''
try {
const result = await getPermissions({ page: 1, pageSize: 200 })
apiPermissions.value = result.records
if (!roleOptions.value.some((role) => role.id === selectedRoleId.value)) {
selectedRoleId.value = roleOptions.value[0]?.id ?? ''
}
await loadSelectedRolePermissions()
} catch {
apiPermissionError.value = 'API 权限目录或角色权限加载失败,请检查当前账号权限'
selectedRolePermissionIds.value = []
rolePermissionVersion.value = undefined
} finally {
apiPermissionLoading.value = false
}
}
async function selectRoleForApiPermissions(roleId: string) {
selectedRoleId.value = roleId
apiPermissionLoading.value = true
apiPermissionError.value = ''
try {
await loadSelectedRolePermissions()
} catch {
selectedRolePermissionIds.value = []
rolePermissionVersion.value = undefined
apiPermissionError.value = '该角色的 API 权限加载失败,请稍后重试'
} finally {
apiPermissionLoading.value = false
}
}
function toggleApiPermission(permissionId: string) {
if (selectedRolePermissionIds.value.includes(permissionId)) {
selectedRolePermissionIds.value = selectedRolePermissionIds.value.filter(
(id) => id !== permissionId,
)
return
}
selectedRolePermissionIds.value = [...selectedRolePermissionIds.value, permissionId]
}
async function saveRoleApiPermissions() {
if (!selectedRoleId.value || apiPermissionSaving.value) {
return
}
const disabledPermission = apiPermissions.value.find(
(permission) =>
selectedRolePermissionIds.value.includes(permission.id) && permission.status === 'DISABLED',
)
if (disabledPermission) {
ElMessage.warning(`权限“${disabledPermission.name}”已停用,请取消勾选后再保存`)
return
}
apiPermissionSaving.value = true
try {
const response = await updateRolePermissions(selectedRoleId.value, {
permissionIds: selectedRolePermissionIds.value,
expectedVersion: rolePermissionVersion.value,
})
selectedRolePermissionIds.value = response.permissions.map((permission) => permission.id)
rolePermissionVersion.value = response.version
ElMessage.success('角色 API 权限已保存')
} catch {
ElMessage.error('角色 API 权限保存失败,请稍后重试')
} finally {
apiPermissionSaving.value = false
}
}
function mapUserRow(
user: ApiUserRecord,
campusNames: Map<string, string>,
@@ -177,8 +340,11 @@ function mapUserRow(
department: departmentNames.get(user.departmentId ?? '') ?? user.department ?? '未指定科室',
departmentId: user.departmentId,
role: user.role,
roleNames: user.roleNames,
roleIds: user.roleIds,
dataScope: dataScopeLabels[user.dataScope ?? ''] ?? user.dataScope ?? '—',
status: user.status,
version: user.version,
}
}
@@ -202,7 +368,14 @@ function showAddUserMessage() {
}
function showImportMessage() {
ElMessage.info('当前接口文档未提供批量导入用户能力')
if (isUserMockEnabled) {
ElMessage.info('批量导入需要切换到真实接口模式(VITE_USE_MOCK=false)')
return
}
importPreview.value = null
importJob.value = null
importVisible.value = true
}
async function editUser(row: UserTableRow) {
@@ -234,6 +407,10 @@ async function editUser(row: UserTableRow) {
'',
status: user.status,
dataScope: toBackendDataScope(user.dataScope),
roleIds: user.roleIds ?? [],
clearPhone: false,
clearEmail: false,
version: user.version,
}
editorVisible.value = true
} catch {
@@ -242,7 +419,79 @@ async function editUser(row: UserTableRow) {
}
function resetPassword(user: UserTableRow) {
ElMessage.info(`当前接口文档未提供重置${user.name}密码的能力,请由认证系统或管理员流程处理`)
if (isUserMockEnabled) {
ElMessage.info('密码重置需要切换到真实接口模式(VITE_USE_MOCK=false)')
return
}
passwordResetTarget.value = user
passwordResetVisible.value = true
}
async function handlePasswordReset(payload: {
newPassword: string
forceChangeOnNextLogin: boolean
}) {
const target = passwordResetTarget.value
if (!target) {
return
}
passwordResetSaving.value = true
try {
await resetUserPassword(target.id, payload)
passwordResetVisible.value = false
passwordResetTarget.value = null
ElMessage.success(`用户“${target.name}”的密码已重置`)
} catch {
ElMessage.error('密码重置失败,请稍后重试')
} finally {
passwordResetSaving.value = false
}
}
async function handleImportPreview(payload: { file: File; campusId?: string }) {
importLoading.value = true
importPreview.value = null
importJob.value = null
try {
importPreview.value = await previewUserImport(payload.file, payload.campusId)
} catch {
ElMessage.error('用户文件校验失败,请检查文件格式和内容')
} finally {
importLoading.value = false
}
}
async function handleImportCommit() {
const preview = importPreview.value
if (!preview || preview.invalidRows > 0) {
return
}
importLoading.value = true
try {
const job = await commitUserImport({ importId: preview.importId })
importJob.value = job
try {
importJob.value = await getUserImportJob(job.jobId)
} catch {
// 提交已经成功,任务状态查询失败不影响本次导入结果。
}
ElMessage.success('用户导入任务已提交')
await loadUsers()
} catch {
ElMessage.error('用户导入提交失败,请稍后重试')
} finally {
importLoading.value = false
}
}
function searchUsers() {
@@ -299,7 +548,23 @@ function toBackendDataScope(value: string | undefined) {
全院只读: 'READ_ONLY_ALL',
}
return map[value ?? ''] ?? value ?? 'DEPARTMENT'
return (map[value ?? ''] ?? value ?? 'DEPARTMENT') as BackendDataScope
}
function toBackendUserStatus(value: UserTableRow['status']): BackendUserStatus {
const map: Record<UserTableRow['status'], BackendUserStatus> = {
enabled: 'ENABLED',
disabled: 'DISABLED',
locked: 'LOCKED',
}
return map[value]
}
function getRoleNames(roleIds: string[]) {
return roleIds
.map((roleId) => roleOptions.value.find((role) => role.id === roleId)?.name)
.filter((name): name is string => Boolean(name))
}
function getCampusName(id: string) {
@@ -326,11 +591,12 @@ async function saveUser(form: UserEditorForm) {
campusId: form.campusId,
department: getDepartmentName(form.departmentId),
departmentId: form.departmentId || null,
role: form.id
? (userRows.value.find((item) => item.id === form.id)?.role ?? '未分配角色')
: '未分配角色',
role: getRoleNames(form.roleIds).join('、') || '未分配角色',
roleNames: getRoleNames(form.roleIds),
roleIds: [...form.roleIds],
dataScope: form.dataScope,
status: form.status,
version: form.version,
}
const index = userRows.value.findIndex((item) => item.id === row.id)
if (index === -1) {
@@ -339,15 +605,25 @@ async function saveUser(form: UserEditorForm) {
userRows.value[index] = row
}
} else if (form.id) {
await updateUser(form.id, {
await updateUserManagement(form.id, {
displayName: form.displayName,
employeeNo: form.employeeNo || undefined,
...(form.phoneMasked ? {} : { phone: form.phone || undefined }),
...(form.emailMasked ? {} : { email: form.email || undefined }),
...(form.phoneMasked
? {}
: form.clearPhone
? { clearPhone: true }
: { phone: form.phone || undefined }),
...(form.emailMasked
? {}
: form.clearEmail
? { clearEmail: true }
: { email: form.email || undefined }),
campusId: form.campusId,
departmentId: form.departmentId || null,
status: form.status.toUpperCase(),
status: toBackendUserStatus(form.status),
dataScope: form.dataScope,
roleIds: form.roleIds,
expectedVersion: form.version,
})
await loadUsers()
} else {
@@ -360,8 +636,9 @@ async function saveUser(form: UserEditorForm) {
email: form.email || undefined,
campusId: form.campusId,
departmentId: form.departmentId || null,
status: form.status.toUpperCase(),
status: toBackendUserStatus(form.status),
dataScope: form.dataScope,
roleIds: form.roleIds,
})
await loadUsers()
}
@@ -442,6 +719,20 @@ onMounted(loadUsers)
</button>
</div>
</section>
<RoleApiPermissionPanel
v-if="!isUserMockEnabled"
:roles="roleOptions"
:permissions="apiPermissions"
:selected-role-id="selectedRoleId"
:selected-permission-ids="selectedRolePermissionIds"
:loading="apiPermissionLoading"
:saving="apiPermissionSaving"
:error="apiPermissionError"
@select-role="selectRoleForApiPermissions"
@toggle-permission="toggleApiPermission"
@save="saveRoleApiPermissions"
/>
</template>
<UserEditorDialog
@@ -449,10 +740,29 @@ onMounted(loadUsers)
:initial="editorInitial"
:campuses="campuses"
:departments="departments"
:roles="roleOptions"
:saving="editorSaving"
@close="editorVisible = false"
@save="saveUser"
/>
<UserPasswordResetDialog
:visible="passwordResetVisible"
:user-name="passwordResetTarget?.name ?? ''"
:saving="passwordResetSaving"
@close="passwordResetVisible = false"
@reset="handlePasswordReset"
/>
<UserImportDialog
:visible="importVisible"
:campuses="campuses"
:default-campus-id="campuses.find((campus) => campus.name === appStore.selectedCampus)?.id"
:preview="importPreview"
:job="importJob"
:loading="importLoading"
@close="importVisible = false"
@preview="handleImportPreview"
@commit="handleImportCommit"
/>
</section>
</template>