181 lines
6.3 KiB
Vue
181 lines
6.3 KiB
Vue
<template>
|
|
<el-card>
|
|
<template #header>
|
|
<div class='flex justify-between items-center'>
|
|
<span class='font-700'>角色管理</span>
|
|
<el-button v-if='hasPermission("platform.roles.manage")' type='primary' @click='openCreate'>新增角色</el-button>
|
|
</div>
|
|
</template>
|
|
|
|
<el-table :data='rows' v-loading='loading'>
|
|
<el-table-column prop='id' label='ID' width='80' sortable />
|
|
<el-table-column prop='name' label='角色名' min-width='160' sortable />
|
|
<el-table-column label='权限' min-width='420' sortable :sort-method='sortByPermissions'>
|
|
<template #default='{ row }'>
|
|
<div class='perm-wrap'>
|
|
<el-tag v-for='item in row.permissions || []' :key='item.id' size='small' class='perm-tag'>{{ item.name }}</el-tag>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column v-if='hasPermission("platform.roles.manage")' label='操作' width='180'>
|
|
<template #default='{ row }'>
|
|
<div class='btn-gap-8 btn-gap-8--nowrap'>
|
|
<el-button size='small' @click='openEdit(row)'>编辑</el-button>
|
|
<el-button size='small' type='danger' @click='removeRow(row)'>删除</el-button>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
</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-form :model='form' label-width='95px'>
|
|
<el-form-item label='角色名'><el-input v-model='form.name' /></el-form-item>
|
|
<el-form-item label='权限选择'>
|
|
<div class='permission-panel-wrap'>
|
|
<el-cascader-panel v-model='selectedPermissionNodes' :options='permissionCascader' :props='cascaderProps' class='permission-panel' />
|
|
</div>
|
|
</el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<el-button @click='dialogVisible = false'>取消</el-button>
|
|
<el-button type='primary' :loading='saving' @click='submit'>提交</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</el-card>
|
|
</template>
|
|
|
|
<script setup lang='ts'>
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
import { computed, onMounted, reactive, ref } from 'vue'
|
|
import { permissionsApi } from '@/api/permissions'
|
|
import { rolesApi } from '@/api/roles'
|
|
import { buildPermissionCascader, extractPermissionIds } from '@/composables/permission-tree'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
|
|
const authStore = useAuthStore()
|
|
const { hasPermission } = authStore
|
|
|
|
const loading = ref(false)
|
|
const saving = ref(false)
|
|
const dialogVisible = ref(false)
|
|
const editingId = ref<number | null>(null)
|
|
const rows = ref<any[]>([])
|
|
const total = ref(0)
|
|
const page = ref(1)
|
|
const perPage = ref(20)
|
|
const permissionOptions = ref<any[]>([])
|
|
const selectedPermissionNodes = ref<Array<number | string>>([])
|
|
|
|
const form = reactive<any>({ name: '' })
|
|
const cascaderProps = { multiple: true, emitPath: false, checkStrictly: false }
|
|
const permissionCascader = computed(() => buildPermissionCascader(permissionOptions.value))
|
|
|
|
async function fetchList(): Promise<void> {
|
|
loading.value = true
|
|
try {
|
|
const response: any = await rolesApi.list({ page: page.value, per_page: perPage.value })
|
|
rows.value = response.data.data || []
|
|
total.value = Number(response.data.total || 0)
|
|
} finally {
|
|
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> {
|
|
const response: any = await permissionsApi.list({ page: 1, per_page: 500 })
|
|
permissionOptions.value = response.data.data || []
|
|
}
|
|
|
|
function openCreate(): void {
|
|
editingId.value = null
|
|
selectedPermissionNodes.value = []
|
|
Object.assign(form, { name: '' })
|
|
dialogVisible.value = true
|
|
}
|
|
|
|
function openEdit(row: any): void {
|
|
editingId.value = row.id
|
|
selectedPermissionNodes.value = (row.permissions || []).map((item: any) => item.id)
|
|
Object.assign(form, { name: row.name })
|
|
dialogVisible.value = true
|
|
}
|
|
|
|
async function submit(): Promise<void> {
|
|
saving.value = true
|
|
try {
|
|
const payload = { name: form.name, permission_ids: extractPermissionIds(selectedPermissionNodes.value) }
|
|
|
|
if (editingId.value) {
|
|
await rolesApi.update(editingId.value, payload)
|
|
ElMessage.success('更新成功')
|
|
} else {
|
|
await rolesApi.create(payload)
|
|
ElMessage.success('创建成功')
|
|
}
|
|
|
|
dialogVisible.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 {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
async function removeRow(row: any): Promise<void> {
|
|
await ElMessageBox.confirm(`确认删除角色 ${row.name} 吗?`, '提示', { type: 'warning' })
|
|
await rolesApi.remove(row.id)
|
|
ElMessage.success('删除成功')
|
|
await fetchList()
|
|
}
|
|
|
|
onMounted(async () => {
|
|
await Promise.all([fetchPermissions(), fetchList()])
|
|
})
|
|
|
|
function sortByPermissions(a: any, b: any): number {
|
|
return ((a.permissions || []).map((item: any) => item.name).join(',')).localeCompare((b.permissions || []).map((item: any) => item.name).join(','))
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.perm-wrap { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
.perm-tag { margin: 0; }
|
|
:deep(.el-dialog) { max-width: 1000px; }
|
|
.permission-panel-wrap { width: 100%; overflow: hidden; }
|
|
.permission-panel { width: 100%; height: 40vh; min-height: 420px; }
|
|
:deep(.permission-panel .el-cascader-menu) { width: 260px; min-width: 260px; }
|
|
:deep(.permission-panel .el-cascader-menu__wrap) { height: 100%; overflow-y: auto; overflow-x: hidden; }
|
|
:deep(.permission-panel .el-cascader-node__label) {
|
|
white-space: nowrap;
|
|
overflow: visible;
|
|
text-overflow: clip;
|
|
line-height: 1.35;
|
|
}
|
|
:deep(.permission-panel .el-cascader-menu__list) { width: 100%; }
|
|
:deep(.permission-panel .el-cascader-node) { width: 100%; }
|
|
:deep(.permission-panel .el-cascader-panel) { height: 100%; }
|
|
</style>
|