feat(前端): 增加账号申请、批量权限设置与分页每页数量

This commit is contained in:
Boen_Shi 2026-05-05 22:01:45 +08:00
parent 85d28a9bfc
commit 19ac981144
12 changed files with 303 additions and 23 deletions

2
components.d.ts vendored
View File

@ -38,8 +38,6 @@ declare module 'vue' {
ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption'] ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination'] ElPagination: typeof import('element-plus/es')['ElPagination']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElSegmented: typeof import('element-plus/es')['ElSegmented'] ElSegmented: typeof import('element-plus/es')['ElSegmented']
ElSelect: typeof import('element-plus/es')['ElSelect'] ElSelect: typeof import('element-plus/es')['ElSelect']
ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElSwitch: typeof import('element-plus/es')['ElSwitch']

View File

@ -12,6 +12,14 @@ interface LoginData {
expires_in: number expires_in: number
} }
interface ApplyAccountPayload {
nickname: string
email: string
phone: string
password: string
password_confirmation: string
}
interface MeData { interface MeData {
user: { user: {
id: number id: number
@ -27,6 +35,9 @@ export const authApi = {
login(data: LoginPayload) { login(data: LoginPayload) {
return request.post<unknown, ApiResponse<LoginData>>('/auth/login', data) return request.post<unknown, ApiResponse<LoginData>>('/auth/login', data)
}, },
applyAccount(data: ApplyAccountPayload) {
return request.post('/auth/apply-account', data)
},
me() { me() {
return request.get<unknown, ApiResponse<MeData>>('/auth/me') return request.get<unknown, ApiResponse<MeData>>('/auth/me')
}, },

View File

@ -19,6 +19,9 @@ export const usersApi = {
syncPermissions(id: number, permissionIds: number[]) { syncPermissions(id: number, permissionIds: number[]) {
return request.put(`/users/${id}/permissions`, { permission_ids: permissionIds }) return request.put(`/users/${id}/permissions`, { permission_ids: permissionIds })
}, },
syncBatchAssignments(payload: { user_ids: number[]; role_ids: number[]; permission_ids: number[] }) {
return request.put('/users/batch-assignments', payload)
},
importUsers(file: File) { importUsers(file: File) {
const formData = new FormData() const formData = new FormData()
formData.append('file', file) formData.append('file', file)

View File

@ -115,7 +115,7 @@
</el-form> </el-form>
<div v-else class='quick-use-server-groups'> <div v-else class='quick-use-server-groups'>
<div v-for='group in quickUseServerGroups' :key='group.server.id' class='group-row'> <div v-for='group in quickUseServerGroups' :key='group.server.id' class='group-row'>
<div class='group-title'>{{ group.server.display_name || group.server.name }}</div> <div class='group-title'>{{ group.server.display_name || group.server.name || `服务器#${group.server.id}` }}</div>
<div class='group-actions btn-gap-8'> <div class='group-actions btn-gap-8'>
<el-button <el-button
v-for='resource in group.resources' v-for='resource in group.resources'
@ -591,7 +591,7 @@ async function fetchQuickUseResources(): Promise<void> {
.filter((item: any) => Boolean(item.parent_id)) .filter((item: any) => Boolean(item.parent_id))
.map((item: any) => { .map((item: any) => {
const server = serverMap.get(item.parent_id) const server = serverMap.get(item.parent_id)
const serverName = server?.display_name || server?.name || '服务器' const serverName = server?.display_name || server?.name || `服务器#${item.parent_id}`
const resourceName = item.display_name || item.name || `资源#${item.id}` const resourceName = item.display_name || item.name || `资源#${item.id}`
return { return {
id: Number(item.id), id: Number(item.id),
@ -599,8 +599,8 @@ async function fetchQuickUseResources(): Promise<void> {
protocols: Array.isArray(item.protocols) ? item.protocols : ['SSH'], protocols: Array.isArray(item.protocols) ? item.protocols : ['SSH'],
parent_id: Number(item.parent_id || 0), parent_id: Number(item.parent_id || 0),
server_id: Number(item.parent_id || 0), server_id: Number(item.parent_id || 0),
server_name: server?.name || '', server_name: server?.name || `server_${item.parent_id}`,
server_display_name: server?.display_name || '', server_display_name: server?.display_name || `服务器#${item.parent_id}`,
display_name: item.display_name || item.name || '', display_name: item.display_name || item.name || '',
name: item.name || '', name: item.name || '',
allow_copy_temp_password: Boolean(item.allow_copy_temp_password), allow_copy_temp_password: Boolean(item.allow_copy_temp_password),

View File

@ -30,6 +30,16 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<el-pagination
class='mt-4'
layout='total, sizes, prev, pager, next'
:total='total'
:current-page='page'
:page-size='perPage'
:page-sizes='[10, 20, 50, 100]'
@current-change='handlePageChange'
@size-change='handleSizeChange'
/>
<el-dialog v-model='dialogVisible' :title='editingId ? "编辑账号" : "新增账号"' width='560px'> <el-dialog v-model='dialogVisible' :title='editingId ? "编辑账号" : "新增账号"' width='560px'>
<el-form :model='form' label-width='100px'> <el-form :model='form' label-width='100px'>
@ -60,6 +70,9 @@ const saving = ref(false)
const dialogVisible = ref(false) const dialogVisible = ref(false)
const editingId = ref<number | null>(null) const editingId = ref<number | null>(null)
const rows = ref<any[]>([]) const rows = ref<any[]>([])
const total = ref(0)
const page = ref(1)
const perPage = ref(20)
const refreshingMap = ref<Record<number, boolean>>({}) const refreshingMap = ref<Record<number, boolean>>({})
const form = reactive<any>({ name: '', username: '', password: '', is_active: true }) const form = reactive<any>({ name: '', username: '', password: '', is_active: true })
const refreshPollAttempts = 90 const refreshPollAttempts = 90
@ -68,13 +81,25 @@ const refreshPollIntervalMs = 1000
async function fetchList(): Promise<void> { async function fetchList(): Promise<void> {
loading.value = true loading.value = true
try { try {
const response: any = await accountsApi.list({ page: 1, per_page: 200 }) const response: any = await accountsApi.list({ page: page.value, per_page: perPage.value })
rows.value = response.data.data || [] rows.value = response.data.data || []
total.value = Number(response.data.total || 0)
} finally { } finally {
loading.value = false loading.value = false
} }
} }
function handlePageChange(nextPage: number): void {
page.value = nextPage
fetchList()
}
function handleSizeChange(nextSize: number): void {
perPage.value = nextSize
page.value = 1
fetchList()
}
function openCreate(): void { function openCreate(): void {
editingId.value = null editingId.value = null
Object.assign(form, { name: '', username: '', password: '', is_active: true }) Object.assign(form, { name: '', username: '', password: '', is_active: true })

View File

@ -18,6 +18,7 @@
</el-form-item> </el-form-item>
<div class='assist-links'> <div class='assist-links'>
<a href='javascript:void(0)'>无法访问您的帐户</a> <a href='javascript:void(0)'>无法访问您的帐户</a>
<a href='javascript:void(0)' @click='applyDialogVisible = true'>账号申请</a>
</div> </div>
<div class='btn-row'> <div class='btn-row'>
<el-button :loading='submitting' type='primary' class='login-btn' @click='handleLogin'>登录</el-button> <el-button :loading='submitting' type='primary' class='login-btn' @click='handleLogin'>登录</el-button>
@ -44,6 +45,30 @@
<el-button type='primary' :loading='forcingPassword' @click='submitForcePasswordChange'>确认修改并进入系统</el-button> <el-button type='primary' :loading='forcingPassword' @click='submitForcePasswordChange'>确认修改并进入系统</el-button>
</template> </template>
</el-dialog> </el-dialog>
<el-dialog v-model='applyDialogVisible' title='账号申请' width='520px'>
<el-form ref='applyFormRef' :model='applyForm' :rules='applyRules' label-width='90px'>
<el-form-item label='昵称' prop='nickname'>
<el-input v-model='applyForm.nickname' placeholder='请输入昵称' />
</el-form-item>
<el-form-item label='邮箱' prop='email'>
<el-input v-model='applyForm.email' placeholder='请输入邮箱' />
</el-form-item>
<el-form-item label='手机号' prop='phone'>
<el-input v-model='applyForm.phone' placeholder='请输入手机号' />
</el-form-item>
<el-form-item label='密码' prop='password'>
<el-input v-model='applyForm.password' type='password' show-password placeholder='请输入密码' />
</el-form-item>
<el-form-item label='确认密码' prop='password_confirmation'>
<el-input v-model='applyForm.password_confirmation' type='password' show-password placeholder='请再次输入密码' />
</el-form-item>
</el-form>
<template #footer>
<el-button @click='applyDialogVisible = false'>取消</el-button>
<el-button type='primary' :loading='applying' @click='submitApplyAccount'>提交申请</el-button>
</template>
</el-dialog>
</div> </div>
</template> </template>
@ -63,9 +88,19 @@ const formRef = ref<FormInstance>()
const submitting = ref(false) const submitting = ref(false)
const forcingPassword = ref(false) const forcingPassword = ref(false)
const forcePasswordDialogVisible = ref(false) const forcePasswordDialogVisible = ref(false)
const applyDialogVisible = ref(false)
const applying = ref(false)
const applyFormRef = ref<FormInstance>()
const forcePasswordForm = reactive({ current_password: '', password: '', password_confirmation: '' }) const forcePasswordForm = reactive({ current_password: '', password: '', password_confirmation: '' })
const form = reactive({ account: '', password: '' }) const form = reactive({ account: '', password: '' })
const applyForm = reactive({
nickname: '',
email: '',
phone: '',
password: '',
password_confirmation: '',
})
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
const phoneRegex = /^1\d{10}$/ const phoneRegex = /^1\d{10}$/
const rules: FormRules = { const rules: FormRules = {
@ -87,6 +122,13 @@ const rules: FormRules = {
}], }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }], password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
} }
const applyRules: FormRules = {
nickname: [{ required: true, message: '请输入昵称', trigger: 'blur' }],
email: [{ required: true, message: '请输入邮箱', trigger: 'blur' }],
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
password_confirmation: [{ required: true, message: '请再次输入密码', trigger: 'blur' }],
}
async function handleLogin(): Promise<void> { async function handleLogin(): Promise<void> {
const valid = await formRef.value?.validate().catch(() => false) const valid = await formRef.value?.validate().catch(() => false)
@ -159,6 +201,36 @@ async function handleForceUserLogout(): Promise<void> {
ElMessage.success('已退出当前账号') ElMessage.success('已退出当前账号')
} }
async function submitApplyAccount(): Promise<void> {
const valid = await applyFormRef.value?.validate().catch(() => false)
if (!valid) {
return
}
if (applyForm.password !== applyForm.password_confirmation) {
ElMessage.warning('两次输入的密码不一致')
return
}
applying.value = true
try {
await authApi.applyAccount(applyForm)
ElMessage.success('申请提交成功,请联系或等待管理员添加权限')
applyDialogVisible.value = false
Object.assign(applyForm, {
nickname: '',
email: '',
phone: '',
password: '',
password_confirmation: '',
})
} catch (error: any) {
const first = error?.errors ? (Object.values(error.errors)[0] as string[])?.[0] : null
ElMessage.error(first || error?.message || '账号申请失败')
} finally {
applying.value = false
}
}
onMounted(async () => { onMounted(async () => {
const token = getToken() const token = getToken()
if (!token) { if (!token) {

View File

@ -57,7 +57,16 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
<el-pagination class='mt-4' layout='total, prev, pager, next' :total='total' :current-page='page' :page-size='perPage' @current-change='handlePageChange' /> <el-pagination
class='mt-4'
layout='total, sizes, prev, pager, next'
:total='total'
:current-page='page'
:page-size='perPage'
:page-sizes='[10, 20, 50, 100]'
@current-change='handlePageChange'
@size-change='handleSizeChange'
/>
</el-card> </el-card>
</template> </template>
@ -112,6 +121,12 @@ function handlePageChange(nextPage: number): void {
fetchList() fetchList()
} }
function handleSizeChange(nextSize: number): void {
perPage.value = nextSize
page.value = 1
fetchList()
}
function handleSortChange(payload: { prop: string, order: 'ascending' | 'descending' | null }): void { function handleSortChange(payload: { prop: string, order: 'ascending' | 'descending' | null }): void {
if (!payload.prop || !payload.order) { if (!payload.prop || !payload.order) {
sortBy.value = 'requested_at' sortBy.value = 'requested_at'

View File

@ -21,6 +21,16 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<el-pagination
class='mt-4'
layout='total, sizes, prev, pager, next'
:total='total'
:current-page='page'
:page-size='perPage'
:page-sizes='[10, 20, 50, 100]'
@current-change='handlePageChange'
@size-change='handleSizeChange'
/>
<el-dialog v-model='dialogVisible' :title='editingId ? "编辑权限" : "新增权限"' width='560px'> <el-dialog v-model='dialogVisible' :title='editingId ? "编辑权限" : "新增权限"' width='560px'>
<el-form :model='form' label-width='95px'> <el-form :model='form' label-width='95px'>
@ -61,6 +71,9 @@ const saving = ref(false)
const dialogVisible = ref(false) const dialogVisible = ref(false)
const editingId = ref<number | null>(null) const editingId = ref<number | null>(null)
const rows = ref<any[]>([]) const rows = ref<any[]>([])
const total = ref(0)
const page = ref(1)
const perPage = ref(20)
const form = reactive({ name: '', category: 'general', description: '', guard_name: 'api' }) const form = reactive({ name: '', category: 'general', description: '', guard_name: 'api' })
const categoryOptions = computed<string[]>(() => { const categoryOptions = computed<string[]>(() => {
const categories = rows.value const categories = rows.value
@ -73,13 +86,25 @@ const categoryOptions = computed<string[]>(() => {
async function fetchList(): Promise<void> { async function fetchList(): Promise<void> {
loading.value = true loading.value = true
try { try {
const response: any = await permissionsApi.list({ per_page: 200 }) const response: any = await permissionsApi.list({ page: page.value, per_page: perPage.value })
rows.value = response.data.data || [] rows.value = response.data.data || []
total.value = Number(response.data.total || 0)
} finally { } finally {
loading.value = false loading.value = false
} }
} }
function handlePageChange(nextPage: number): void {
page.value = nextPage
fetchList()
}
function handleSizeChange(nextSize: number): void {
perPage.value = nextSize
page.value = 1
fetchList()
}
function openCreate(): void { function openCreate(): void {
editingId.value = null editingId.value = null
Object.assign(form, { name: '', category: 'general', description: '', guard_name: 'api' }) Object.assign(form, { name: '', category: 'general', description: '', guard_name: 'api' })

View File

@ -26,6 +26,16 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<el-pagination
class='mt-4'
layout='total, sizes, prev, pager, next'
:total='total'
:current-page='page'
:page-size='perPage'
:page-sizes='[10, 20, 50, 100]'
@current-change='handlePageChange'
@size-change='handleSizeChange'
/>
<el-dialog v-model='dialogVisible' :title='editingId ? "编辑角色" : "新增角色"' width='92vw' top='4vh'> <el-dialog v-model='dialogVisible' :title='editingId ? "编辑角色" : "新增角色"' width='92vw' top='4vh'>
<el-form :model='form' label-width='95px'> <el-form :model='form' label-width='95px'>
@ -60,6 +70,9 @@ const saving = ref(false)
const dialogVisible = ref(false) const dialogVisible = ref(false)
const editingId = ref<number | null>(null) const editingId = ref<number | null>(null)
const rows = ref<any[]>([]) const rows = ref<any[]>([])
const total = ref(0)
const page = ref(1)
const perPage = ref(20)
const permissionOptions = ref<any[]>([]) const permissionOptions = ref<any[]>([])
const selectedPermissionNodes = ref<Array<number | string>>([]) const selectedPermissionNodes = ref<Array<number | string>>([])
@ -70,13 +83,25 @@ const permissionCascader = computed(() => buildPermissionCascader(permissionOpti
async function fetchList(): Promise<void> { async function fetchList(): Promise<void> {
loading.value = true loading.value = true
try { try {
const response: any = await rolesApi.list({ page: 1, per_page: 200 }) const response: any = await rolesApi.list({ page: page.value, per_page: perPage.value })
rows.value = response.data.data || [] rows.value = response.data.data || []
total.value = Number(response.data.total || 0)
} finally { } finally {
loading.value = false loading.value = false
} }
} }
function handlePageChange(nextPage: number): void {
page.value = nextPage
fetchList()
}
function handleSizeChange(nextSize: number): void {
perPage.value = nextSize
page.value = 1
fetchList()
}
async function fetchPermissions(): Promise<void> { async function fetchPermissions(): Promise<void> {
const response: any = await permissionsApi.list({ page: 1, per_page: 500 }) const response: any = await permissionsApi.list({ page: 1, per_page: 500 })
permissionOptions.value = response.data.data || [] permissionOptions.value = response.data.data || []
@ -153,4 +178,3 @@ function sortByPermissions(a: any, b: any): number {
:deep(.permission-panel .el-cascader-node) { width: 100%; } :deep(.permission-panel .el-cascader-node) { width: 100%; }
:deep(.permission-panel .el-cascader-panel) { height: 100%; } :deep(.permission-panel .el-cascader-panel) { height: 100%; }
</style> </style>

View File

@ -3,9 +3,12 @@
<template #header> <template #header>
<div class='flex justify-between items-center'> <div class='flex justify-between items-center'>
<span class='font-700'>服务器资源管理</span> <span class='font-700'>服务器资源管理</span>
<div class='top-actions btn-gap-8' v-if='hasPermission("platform.servers.manage")'> <div class='top-actions btn-gap-8'>
<el-button type='primary' @click='openCreateServer'>新增服务器</el-button> <el-button @click='downloadSsoComponent'>下载单点登录组件</el-button>
<el-button type='success' @click='openCreateResource()'>新增资源</el-button> <template v-if='hasPermission("platform.servers.manage")'>
<el-button type='primary' @click='openCreateServer'>新增服务器</el-button>
<el-button type='success' @click='openCreateResource()'>新增资源</el-button>
</template>
</div> </div>
</div> </div>
</template> </template>
@ -361,6 +364,18 @@ function openCreateResource(parentId?: number): void {
dialogVisible.value = true dialogVisible.value = true
} }
function downloadSsoComponent(): void {
const downloadUrl = `${window.location.origin}/USMSSOsetup.exe`
const anchor = document.createElement('a')
anchor.href = downloadUrl
anchor.download = 'USMSSOsetup.exe'
anchor.target = '_blank'
anchor.rel = 'noopener'
document.body.appendChild(anchor)
anchor.click()
document.body.removeChild(anchor)
}
function openEdit(row: any): void { function openEdit(row: any): void {
editingId.value = row.id editingId.value = row.id
Object.assign(form, { Object.assign(form, {

View File

@ -4,13 +4,15 @@
<div class='flex justify-between items-center'> <div class='flex justify-between items-center'>
<span class='font-700'>用户管理</span> <span class='font-700'>用户管理</span>
<div class='btn-gap-8' v-if='hasPermission("platform.users.manage")'> <div class='btn-gap-8' v-if='hasPermission("platform.users.manage")'>
<el-button type='warning' :disabled='selectedUserIds.length === 0' @click='openBatchAssignDialog'>批量设置用户组和权限</el-button>
<el-button type='success' :loading='importing' @click='openImportDialog'>批量导入</el-button> <el-button type='success' :loading='importing' @click='openImportDialog'>批量导入</el-button>
<el-button type='primary' @click='openCreate'>新增用户</el-button> <el-button type='primary' @click='openCreate'>新增用户</el-button>
</div> </div>
</div> </div>
</template> </template>
<el-table :data='rows' v-loading='loading' @sort-change='handleSortChange'> <el-table :data='rows' v-loading='loading' @sort-change='handleSortChange' @selection-change='handleSelectionChange'>
<el-table-column v-if='hasPermission("platform.users.manage")' type='selection' width='48' />
<el-table-column prop='id' label='ID' width='70' sortable='custom' /> <el-table-column prop='id' label='ID' width='70' sortable='custom' />
<el-table-column prop='nickname' label='昵称' min-width='130' sortable='custom' /> <el-table-column prop='nickname' label='昵称' min-width='130' sortable='custom' />
<el-table-column prop='email' label='邮箱' min-width='180' sortable='custom' /> <el-table-column prop='email' label='邮箱' min-width='180' sortable='custom' />
@ -32,7 +34,16 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
<el-pagination class='mt-4' layout='total, prev, pager, next' :total='total' :current-page='page' :page-size='perPage' @current-change='handlePageChange' /> <el-pagination
class='mt-4'
layout='total, sizes, prev, pager, next'
:total='total'
:current-page='page'
:page-size='perPage'
:page-sizes='[10, 20, 50, 100]'
@current-change='handlePageChange'
@size-change='handleSizeChange'
/>
<el-dialog v-model='dialogVisible' :title='editingId ? "编辑用户" : "新增用户"' width='560px'> <el-dialog v-model='dialogVisible' :title='editingId ? "编辑用户" : "新增用户"' width='560px'>
<el-form :model='form' label-width='95px'> <el-form :model='form' label-width='95px'>
@ -63,6 +74,28 @@
</template> </template>
</el-dialog> </el-dialog>
<el-dialog v-model='batchAssignDialogVisible' title='批量设置用户组和用户权限' width='760px'>
<el-alert type='info' :closable='false' show-icon>
<template #default>已选择 {{ selectedUserIds.length }} 个用户提交后将覆盖这些用户的角色与直授权限</template>
</el-alert>
<el-form :model='batchAssignForm' label-width='90px' class='mt-4'>
<el-form-item label='用户组'>
<el-select v-model='batchAssignForm.role_ids' multiple collapse-tags clearable class='w-full' placeholder='请选择用户组'>
<el-option v-for='item in roleOptions' :key='item.id' :label='item.name' :value='item.id' />
</el-select>
</el-form-item>
<el-form-item label='用户权限'>
<div class='permission-panel-wrap'>
<el-cascader-panel v-model='batchAssignPermissionNodes' :options='permissionCascader' :props='cascaderProps' class='permission-panel' />
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click='batchAssignDialogVisible = false'>取消</el-button>
<el-button type='primary' :loading='batchAssigning' @click='submitBatchAssign'>提交</el-button>
</template>
</el-dialog>
<el-dialog v-model='importDialogVisible' title='批量导入用户' width='620px'> <el-dialog v-model='importDialogVisible' title='批量导入用户' width='620px'>
<el-alert type='info' show-icon :closable='false'> <el-alert type='info' show-icon :closable='false'>
<template #default> <template #default>
@ -123,13 +156,17 @@ const savingPermissions = ref(false)
const dialogVisible = ref(false) const dialogVisible = ref(false)
const permissionDialogVisible = ref(false) const permissionDialogVisible = ref(false)
const importDialogVisible = ref(false) const importDialogVisible = ref(false)
const batchAssignDialogVisible = ref(false)
const editingId = ref<number | null>(null) const editingId = ref<number | null>(null)
const permissionTargetUserId = ref<number | null>(null) const permissionTargetUserId = ref<number | null>(null)
const importing = ref(false) const importing = ref(false)
const batchAssigning = ref(false)
const selectedUserIds = ref<number[]>([])
const rows = ref<any[]>([]) const rows = ref<any[]>([])
const roleOptions = ref<any[]>([]) const roleOptions = ref<any[]>([])
const permissionOptions = ref<any[]>([]) const permissionOptions = ref<any[]>([])
const selectedPermissionNodes = ref<Array<number | string>>([]) const selectedPermissionNodes = ref<Array<number | string>>([])
const batchAssignPermissionNodes = ref<Array<number | string>>([])
const importFile = ref<File | null>(null) const importFile = ref<File | null>(null)
const importResult = ref<{ created_count: number; skipped_count: number; error_count: number; errors: Array<{ line: number; message: string }> } | null>(null) const importResult = ref<{ created_count: number; skipped_count: number; error_count: number; errors: Array<{ line: number; message: string }> } | null>(null)
const total = ref(0) const total = ref(0)
@ -139,6 +176,7 @@ const sortBy = ref('created_at')
const sortOrder = ref<'asc' | 'desc'>('desc') const sortOrder = ref<'asc' | 'desc'>('desc')
const form = reactive<any>({ nickname: '', email: '', phone: '', password: '', force_password_change: false, role_ids: [] }) const form = reactive<any>({ nickname: '', email: '', phone: '', password: '', force_password_change: false, role_ids: [] })
const batchAssignForm = reactive<{ role_ids: number[] }>({ role_ids: [] })
const cascaderProps = { multiple: true, emitPath: false, checkStrictly: false } const cascaderProps = { multiple: true, emitPath: false, checkStrictly: false }
const permissionCascader = computed(() => buildPermissionCascader(permissionOptions.value)) const permissionCascader = computed(() => buildPermissionCascader(permissionOptions.value))
@ -153,6 +191,7 @@ async function fetchList(): Promise<void> {
}) })
const list = response.data.data || [] const list = response.data.data || []
rows.value = await Promise.all(list.map(async (item: any) => (await usersApi.detail(item.id) as any).data)) rows.value = await Promise.all(list.map(async (item: any) => (await usersApi.detail(item.id) as any).data))
selectedUserIds.value = []
total.value = response.data.total || 0 total.value = response.data.total || 0
} finally { } finally {
loading.value = false loading.value = false
@ -160,12 +199,19 @@ async function fetchList(): Promise<void> {
} }
async function fetchOptions(): Promise<void> { async function fetchOptions(): Promise<void> {
const [rolesResponse, permissionsResponse]: any = await Promise.all([ try {
rolesApi.list({ page: 1, per_page: 200 }), const [rolesResponse, permissionsResponse]: any = await Promise.all([
permissionsApi.list({ page: 1, per_page: 500 }), rolesApi.list({ page: 1, per_page: 100 }),
]) permissionsApi.list({ page: 1, per_page: 200 }),
roleOptions.value = rolesResponse.data.data || [] ])
permissionOptions.value = permissionsResponse.data.data || [] roleOptions.value = rolesResponse.data.data || []
permissionOptions.value = permissionsResponse.data.data || []
} catch (error: any) {
roleOptions.value = []
permissionOptions.value = []
const first = error?.errors ? (Object.values(error.errors)[0] as string[])?.[0] : null
ElMessage.error(first || error?.message || '加载用户组选项失败')
}
} }
function handlePageChange(nextPage: number): void { function handlePageChange(nextPage: number): void {
@ -173,6 +219,12 @@ function handlePageChange(nextPage: number): void {
fetchList() fetchList()
} }
function handleSizeChange(nextSize: number): void {
perPage.value = nextSize
page.value = 1
fetchList()
}
function handleSortChange(payload: { prop: string, order: 'ascending' | 'descending' | null }): void { function handleSortChange(payload: { prop: string, order: 'ascending' | 'descending' | null }): void {
if (!payload.prop || !payload.order) { if (!payload.prop || !payload.order) {
sortBy.value = 'created_at' sortBy.value = 'created_at'
@ -269,6 +321,16 @@ function openImportDialog(): void {
importDialogVisible.value = true importDialogVisible.value = true
} }
function openBatchAssignDialog(): void {
if (selectedUserIds.value.length === 0) {
ElMessage.warning('请先勾选用户')
return
}
batchAssignForm.role_ids = []
batchAssignPermissionNodes.value = []
batchAssignDialogVisible.value = true
}
function handleImportFileChange(file: any): void { function handleImportFileChange(file: any): void {
importFile.value = file?.raw || null importFile.value = file?.raw || null
} }
@ -319,6 +381,35 @@ async function downloadImportTemplate(): Promise<void> {
} }
} }
function handleSelectionChange(selection: any[]): void {
selectedUserIds.value = selection
.map((item: any) => Number(item.id || 0))
.filter((id: number) => id > 0)
}
async function submitBatchAssign(): Promise<void> {
if (selectedUserIds.value.length === 0) {
ElMessage.warning('请先勾选用户')
return
}
batchAssigning.value = true
try {
await usersApi.syncBatchAssignments({
user_ids: selectedUserIds.value,
role_ids: batchAssignForm.role_ids,
permission_ids: extractPermissionIds(batchAssignPermissionNodes.value),
})
ElMessage.success('批量设置成功')
batchAssignDialogVisible.value = false
await fetchList()
} catch (error: any) {
const first = error?.errors ? (Object.values(error.errors)[0] as string[])?.[0] : null
ElMessage.error(first || error?.message || '批量设置失败')
} finally {
batchAssigning.value = false
}
}
onMounted(async () => { onMounted(async () => {
await Promise.all([fetchOptions(), fetchList()]) await Promise.all([fetchOptions(), fetchList()])
}) })

View File

@ -33,7 +33,8 @@ export default defineConfig({
server: { server: {
proxy: { proxy: {
'/api': { '/api': {
target: 'https://sso.scirc.top/api', // 后端地址 // target: 'https://sso.scirc.top/api', // 后端地址
target: 'http://localhost:8080', // 后端地址
changeOrigin: true, changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '') // 去掉 /api 前缀 rewrite: path => path.replace(/^\/api/, '') // 去掉 /api 前缀
} }