feat(前端): 增加账号申请、批量权限设置与分页每页数量
This commit is contained in:
parent
85d28a9bfc
commit
19ac981144
2
components.d.ts
vendored
2
components.d.ts
vendored
@ -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']
|
||||||
|
|||||||
@ -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')
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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 })
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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' })
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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, {
|
||||||
|
|||||||
@ -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()])
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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 前缀
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user