You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
767 lines
20 KiB
767 lines
20 KiB
<template> |
|
<div class="role-permission-assignment"> |
|
<div v-if="visible" class="modal-overlay" @click="handleClose"> |
|
<div class="modal large-modal" @click.stop> |
|
<div class="modal-header"> |
|
<h3>角色权限分配 - {{ currentRole?.name }}</h3> |
|
<button class="close-btn" @click="handleClose"> |
|
<i class="fas fa-times"></i> |
|
</button> |
|
</div> |
|
|
|
<div class="modal-body"> |
|
<div v-if="loading" class="loading"> |
|
<i class="fas fa-spinner fa-spin"></i> |
|
<span>加载中...</span> |
|
</div> |
|
|
|
<div v-else class="permission-content"> |
|
<!-- 角色信息 --> |
|
<div class="role-info"> |
|
<div class="role-details"> |
|
<h4>{{ currentRole?.name }}</h4> |
|
<p class="role-description">{{ currentRole?.description }}</p> |
|
<div class="role-stats"> |
|
<span class="stat-item"> |
|
<i class="fas fa-shield-alt"></i> |
|
已分配权限: {{ assignedPermissions.length }} |
|
</span> |
|
<span class="stat-item"> |
|
<i class="fas fa-list"></i> |
|
总权限数: {{ allPermissions.length }} |
|
</span> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<!-- 权限分配区域 --> |
|
<div class="permission-assignment"> |
|
<div class="assignment-tabs"> |
|
<button |
|
class="tab-btn" |
|
:class="{ active: activeTab === 'assigned' }" |
|
@click="activeTab = 'assigned'" |
|
> |
|
<i class="fas fa-check-circle"></i> |
|
已分配权限 ({{ assignedPermissions.length }}) |
|
</button> |
|
<button |
|
class="tab-btn" |
|
:class="{ active: activeTab === 'available' }" |
|
@click="activeTab = 'available'" |
|
> |
|
<i class="fas fa-plus-circle"></i> |
|
可分配权限 ({{ availablePermissions.length }}) |
|
</button> |
|
</div> |
|
|
|
<!-- 已分配权限列表 --> |
|
<div v-if="activeTab === 'assigned'" class="permission-list assigned-permissions"> |
|
<div class="list-header"> |
|
<h5>已分配权限</h5> |
|
<div class="list-actions"> |
|
<button |
|
class="btn btn-sm btn-danger" |
|
@click="removeSelectedPermissions" |
|
:disabled="selectedAssignedPermissions.length === 0" name="rolepermissionassignment-button-sj6qb7" :visible="false"> |
|
<i class="fas fa-minus"></i> |
|
移除选中 ({{ selectedAssignedPermissions.length }}) |
|
</button> |
|
<button |
|
class="btn btn-sm btn-secondary" |
|
@click="clearAssignedSelection" |
|
> |
|
清除选择 |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div v-if="assignedPermissions.length === 0" class="empty-state"> |
|
<i class="fas fa-inbox"></i> |
|
<p>该角色暂无分配权限</p> |
|
</div> |
|
|
|
<div v-else class="permission-items"> |
|
<div |
|
v-for="permission in assignedPermissions" |
|
:key="permission.id" |
|
class="permission-item" |
|
:class="{ selected: selectedAssignedPermissions.includes(permission.id) }" |
|
@click="toggleAssignedPermission(permission.id)" |
|
> |
|
<div class="permission-checkbox"> |
|
<input |
|
type="checkbox" |
|
:checked="selectedAssignedPermissions.includes(permission.id)" |
|
@change="toggleAssignedPermission(permission.id)" |
|
/> |
|
</div> |
|
<div class="permission-info"> |
|
<div class="permission-name">{{ permission.name }}</div> |
|
<div class="permission-details"> |
|
<span class="permission-code">{{ permission.code }}</span> |
|
<span class="permission-resource">{{ permission.resource }}</span> |
|
</div> |
|
<div class="permission-description">{{ permission.description }}</div> |
|
</div> |
|
<div class="permission-actions"> |
|
<button |
|
class="btn btn-sm btn-danger" |
|
@click.stop="removePermission(permission.id)" |
|
title="移除权限" |
|
name="rolepermissionassignment-button-q86qi4" :visible="false"> |
|
<i class="fas fa-minus"></i> |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<!-- 可分配权限列表 --> |
|
<div v-if="activeTab === 'available'" class="permission-list available-permissions"> |
|
<div class="list-header"> |
|
<h5>可分配权限</h5> |
|
<div class="list-actions"> |
|
<button |
|
class="btn btn-sm btn-primary" |
|
@click="assignSelectedPermissions" |
|
:disabled="selectedAvailablePermissions.length === 0" name="rolepermissionassignment-button-a0nyh8" :visible="false"> |
|
<i class="fas fa-plus"></i> |
|
分配选中 ({{ selectedAvailablePermissions.length }}) |
|
</button> |
|
<button |
|
class="btn btn-sm btn-secondary" |
|
@click="clearAvailableSelection" |
|
> |
|
清除选择 |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div v-if="availablePermissions.length === 0" class="empty-state"> |
|
<i class="fas fa-check-circle"></i> |
|
<p>所有权限已分配完毕</p> |
|
</div> |
|
|
|
<div v-else class="permission-items"> |
|
<div |
|
v-for="permission in availablePermissions" |
|
:key="permission.id" |
|
class="permission-item" |
|
:class="{ selected: selectedAvailablePermissions.includes(permission.id) }" |
|
@click="toggleAvailablePermission(permission.id)" |
|
> |
|
<div class="permission-checkbox"> |
|
<input |
|
type="checkbox" |
|
:checked="selectedAvailablePermissions.includes(permission.id)" |
|
@change="toggleAvailablePermission(permission.id)" |
|
/> |
|
</div> |
|
<div class="permission-info"> |
|
<div class="permission-name">{{ permission.name }}</div> |
|
<div class="permission-details"> |
|
<span class="permission-code">{{ permission.code }}</span> |
|
<span class="permission-resource">{{ permission.resource }}</span> |
|
</div> |
|
<div class="permission-description">{{ permission.description }}</div> |
|
</div> |
|
<div class="permission-actions"> |
|
<button |
|
class="btn btn-sm btn-primary" |
|
@click.stop="assignPermission(permission.id)" |
|
title="分配权限" |
|
name="rolepermissionassignment-button-7uizm5" :visible="false"> |
|
<i class="fas fa-plus"></i> |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="modal-footer"> |
|
<button class="btn btn-secondary" @click="handleClose">关闭</button> |
|
<button class="btn btn-primary" @click="refreshPermissions" :disabled="loading"> |
|
<i class="fas fa-sync-alt"></i> |
|
刷新权限 |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</template> |
|
|
|
<script> |
|
import { ref, watch, computed } from 'vue' |
|
import roleService from '../services/roleService.js' |
|
|
|
export default { |
|
name: 'RolePermissionAssignment', |
|
props: { |
|
modelValue: { |
|
type: Boolean, |
|
default: false |
|
}, |
|
role: { |
|
type: Object, |
|
default: null |
|
} |
|
}, |
|
emits: ['update:modelValue', 'permissions-updated'], |
|
setup(props, { emit }) { |
|
const visible = ref(false) |
|
const loading = ref(false) |
|
const currentRole = ref(null) |
|
const allPermissions = ref([]) |
|
const assignedPermissions = ref([]) |
|
const selectedAssignedPermissions = ref([]) |
|
const selectedAvailablePermissions = ref([]) |
|
const activeTab = ref('assigned') |
|
|
|
// 计算可分配权限 |
|
const availablePermissions = computed(() => { |
|
const assignedIds = assignedPermissions.value.map(p => p.id) |
|
return allPermissions.value.filter(p => !assignedIds.includes(p.id)) |
|
}) |
|
|
|
// 监听visible变化 |
|
watch(() => props.modelValue, (newVal) => { |
|
visible.value = newVal |
|
if (newVal && props.role) { |
|
currentRole.value = props.role |
|
loadData() |
|
} |
|
}) |
|
|
|
// 加载数据 |
|
const loadData = async () => { |
|
if (!currentRole.value) return |
|
|
|
loading.value = true |
|
try { |
|
await Promise.all([ |
|
loadAllPermissions(), |
|
loadRolePermissions() |
|
]) |
|
} catch (error) { |
|
console.error('加载权限数据失败:', error) |
|
} finally { |
|
loading.value = false |
|
} |
|
} |
|
|
|
// 加载所有权限 |
|
const loadAllPermissions = async () => { |
|
try { |
|
const response = await roleService.getPermissions() |
|
if (response.code === 200) { |
|
allPermissions.value = response.data || [] |
|
} |
|
} catch (error) { |
|
console.error('加载权限列表失败:', error) |
|
} |
|
} |
|
|
|
// 加载角色权限 |
|
const loadRolePermissions = async () => { |
|
try { |
|
const response = await roleService.getRolePermissions(currentRole.value.id) |
|
if (response.code === 200) { |
|
assignedPermissions.value = response.data || [] |
|
} |
|
} catch (error) { |
|
console.error('加载角色权限失败:', error) |
|
} |
|
} |
|
|
|
// 切换已分配权限选择 |
|
const toggleAssignedPermission = (permissionId) => { |
|
const index = selectedAssignedPermissions.value.indexOf(permissionId) |
|
if (index > -1) { |
|
selectedAssignedPermissions.value.splice(index, 1) |
|
} else { |
|
selectedAssignedPermissions.value.push(permissionId) |
|
} |
|
} |
|
|
|
// 切换可分配权限选择 |
|
const toggleAvailablePermission = (permissionId) => { |
|
const index = selectedAvailablePermissions.value.indexOf(permissionId) |
|
if (index > -1) { |
|
selectedAvailablePermissions.value.splice(index, 1) |
|
} else { |
|
selectedAvailablePermissions.value.push(permissionId) |
|
} |
|
} |
|
|
|
// 清除已分配权限选择 |
|
const clearAssignedSelection = () => { |
|
selectedAssignedPermissions.value = [] |
|
} |
|
|
|
// 清除可分配权限选择 |
|
const clearAvailableSelection = () => { |
|
selectedAvailablePermissions.value = [] |
|
} |
|
|
|
// 分配单个权限 |
|
const assignPermission = async (permissionId) => { |
|
try { |
|
const response = await roleService.assignPermissionsToRole(currentRole.value.id, [permissionId]) |
|
if (response.code === 200) { |
|
await loadRolePermissions() |
|
emit('permissions-updated') |
|
} |
|
} catch (error) { |
|
console.error('分配权限失败:', error) |
|
} |
|
} |
|
|
|
// 移除单个权限 |
|
const removePermission = async (permissionId) => { |
|
try { |
|
const response = await roleService.removePermissionsFromRole(currentRole.value.id, [permissionId]) |
|
if (response.code === 200) { |
|
await loadRolePermissions() |
|
emit('permissions-updated') |
|
} |
|
} catch (error) { |
|
console.error('移除权限失败:', error) |
|
} |
|
} |
|
|
|
// 批量分配权限 |
|
const assignSelectedPermissions = async () => { |
|
if (selectedAvailablePermissions.value.length === 0) return |
|
|
|
try { |
|
const response = await roleService.assignPermissionsToRole( |
|
currentRole.value.id, |
|
selectedAvailablePermissions.value |
|
) |
|
if (response.code === 200) { |
|
selectedAvailablePermissions.value = [] |
|
await loadRolePermissions() |
|
emit('permissions-updated') |
|
} |
|
} catch (error) { |
|
console.error('批量分配权限失败:', error) |
|
} |
|
} |
|
|
|
// 批量移除权限 |
|
const removeSelectedPermissions = async () => { |
|
if (selectedAssignedPermissions.value.length === 0) return |
|
|
|
try { |
|
const response = await roleService.removePermissionsFromRole( |
|
currentRole.value.id, |
|
selectedAssignedPermissions.value |
|
) |
|
if (response.code === 200) { |
|
selectedAssignedPermissions.value = [] |
|
await loadRolePermissions() |
|
emit('permissions-updated') |
|
} |
|
} catch (error) { |
|
console.error('批量移除权限失败:', error) |
|
} |
|
} |
|
|
|
// 刷新权限 |
|
const refreshPermissions = async () => { |
|
await loadData() |
|
} |
|
|
|
// 关闭对话框 |
|
const handleClose = () => { |
|
visible.value = false |
|
emit('update:modelValue', false) |
|
// 重置状态 |
|
selectedAssignedPermissions.value = [] |
|
selectedAvailablePermissions.value = [] |
|
activeTab.value = 'assigned' |
|
} |
|
|
|
return { |
|
visible, |
|
loading, |
|
currentRole, |
|
allPermissions, |
|
assignedPermissions, |
|
availablePermissions, |
|
selectedAssignedPermissions, |
|
selectedAvailablePermissions, |
|
activeTab, |
|
toggleAssignedPermission, |
|
toggleAvailablePermission, |
|
clearAssignedSelection, |
|
clearAvailableSelection, |
|
assignPermission, |
|
removePermission, |
|
assignSelectedPermissions, |
|
removeSelectedPermissions, |
|
refreshPermissions, |
|
handleClose |
|
} |
|
} |
|
} |
|
</script> |
|
|
|
<style scoped> |
|
.role-permission-assignment { |
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
|
} |
|
|
|
.large-modal { |
|
width: 90%; |
|
max-width: 1200px; |
|
height: 80vh; |
|
max-height: 800px; |
|
} |
|
|
|
.modal-overlay { |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
right: 0; |
|
bottom: 0; |
|
background: rgba(0, 0, 0, 0.5); |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
z-index: 1000; |
|
} |
|
|
|
.modal { |
|
background: white; |
|
border-radius: 8px; |
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); |
|
display: flex; |
|
flex-direction: column; |
|
max-height: 90vh; |
|
} |
|
|
|
.modal-header { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
padding: 20px 24px; |
|
border-bottom: 1px solid #e5e7eb; |
|
background: #f9fafb; |
|
border-radius: 8px 8px 0 0; |
|
} |
|
|
|
.modal-header h3 { |
|
margin: 0; |
|
color: #1f2937; |
|
font-size: 18px; |
|
font-weight: 600; |
|
} |
|
|
|
.close-btn { |
|
background: none; |
|
border: none; |
|
font-size: 18px; |
|
cursor: pointer; |
|
color: #6b7280; |
|
padding: 4px; |
|
border-radius: 4px; |
|
transition: all 0.2s; |
|
} |
|
|
|
.close-btn:hover { |
|
background: #e5e7eb; |
|
color: #374151; |
|
} |
|
|
|
.modal-body { |
|
flex: 1; |
|
padding: 24px; |
|
overflow-y: auto; |
|
} |
|
|
|
.modal-footer { |
|
display: flex; |
|
justify-content: flex-end; |
|
gap: 12px; |
|
padding: 20px 24px; |
|
border-top: 1px solid #e5e7eb; |
|
background: #f9fafb; |
|
border-radius: 0 0 8px 8px; |
|
} |
|
|
|
.loading { |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
justify-content: center; |
|
padding: 40px; |
|
color: #6b7280; |
|
} |
|
|
|
.loading i { |
|
font-size: 24px; |
|
margin-bottom: 12px; |
|
} |
|
|
|
.role-info { |
|
margin-bottom: 24px; |
|
padding: 16px; |
|
background: #f8fafc; |
|
border-radius: 8px; |
|
border: 1px solid #e2e8f0; |
|
} |
|
|
|
.role-details h4 { |
|
margin: 0 0 8px 0; |
|
color: #1e293b; |
|
font-size: 16px; |
|
font-weight: 600; |
|
} |
|
|
|
.role-description { |
|
margin: 0 0 12px 0; |
|
color: #64748b; |
|
font-size: 14px; |
|
} |
|
|
|
.role-stats { |
|
display: flex; |
|
gap: 16px; |
|
} |
|
|
|
.stat-item { |
|
display: flex; |
|
align-items: center; |
|
gap: 6px; |
|
color: #475569; |
|
font-size: 14px; |
|
} |
|
|
|
.stat-item i { |
|
color: #3b82f6; |
|
} |
|
|
|
.permission-assignment { |
|
border: 1px solid #e2e8f0; |
|
border-radius: 8px; |
|
overflow: hidden; |
|
} |
|
|
|
.assignment-tabs { |
|
display: flex; |
|
background: #f1f5f9; |
|
border-bottom: 1px solid #e2e8f0; |
|
} |
|
|
|
.tab-btn { |
|
flex: 1; |
|
padding: 12px 16px; |
|
background: none; |
|
border: none; |
|
cursor: pointer; |
|
font-size: 14px; |
|
font-weight: 500; |
|
color: #64748b; |
|
transition: all 0.2s; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
gap: 8px; |
|
} |
|
|
|
.tab-btn.active { |
|
background: white; |
|
color: #3b82f6; |
|
border-bottom: 2px solid #3b82f6; |
|
} |
|
|
|
.tab-btn:hover:not(.active) { |
|
background: #e2e8f0; |
|
color: #475569; |
|
} |
|
|
|
.permission-list { |
|
padding: 20px; |
|
} |
|
|
|
.list-header { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
margin-bottom: 16px; |
|
} |
|
|
|
.list-header h5 { |
|
margin: 0; |
|
color: #1e293b; |
|
font-size: 16px; |
|
font-weight: 600; |
|
} |
|
|
|
.list-actions { |
|
display: flex; |
|
gap: 8px; |
|
} |
|
|
|
.empty-state { |
|
text-align: center; |
|
padding: 40px 20px; |
|
color: #64748b; |
|
} |
|
|
|
.empty-state i { |
|
font-size: 48px; |
|
margin-bottom: 16px; |
|
opacity: 0.5; |
|
} |
|
|
|
.empty-state p { |
|
margin: 0; |
|
font-size: 14px; |
|
} |
|
|
|
.permission-items { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 8px; |
|
} |
|
|
|
.permission-item { |
|
display: flex; |
|
align-items: center; |
|
padding: 12px; |
|
border: 1px solid #e2e8f0; |
|
border-radius: 6px; |
|
cursor: pointer; |
|
transition: all 0.2s; |
|
background: white; |
|
} |
|
|
|
.permission-item:hover { |
|
border-color: #3b82f6; |
|
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.1); |
|
} |
|
|
|
.permission-item.selected { |
|
border-color: #3b82f6; |
|
background: #eff6ff; |
|
} |
|
|
|
.permission-checkbox { |
|
margin-right: 12px; |
|
} |
|
|
|
.permission-checkbox input[type="checkbox"] { |
|
width: 16px; |
|
height: 16px; |
|
cursor: pointer; |
|
} |
|
|
|
.permission-info { |
|
flex: 1; |
|
} |
|
|
|
.permission-name { |
|
font-weight: 500; |
|
color: #1e293b; |
|
margin-bottom: 4px; |
|
} |
|
|
|
.permission-details { |
|
display: flex; |
|
gap: 12px; |
|
margin-bottom: 4px; |
|
} |
|
|
|
.permission-code { |
|
font-family: 'Courier New', monospace; |
|
font-size: 12px; |
|
color: #64748b; |
|
background: #f1f5f9; |
|
padding: 2px 6px; |
|
border-radius: 4px; |
|
} |
|
|
|
.permission-resource { |
|
font-size: 12px; |
|
color: #8b5cf6; |
|
background: #f3f4f6; |
|
padding: 2px 6px; |
|
border-radius: 4px; |
|
} |
|
|
|
.permission-description { |
|
font-size: 12px; |
|
color: #64748b; |
|
line-height: 1.4; |
|
} |
|
|
|
.permission-actions { |
|
margin-left: 12px; |
|
} |
|
|
|
.btn { |
|
padding: 6px 12px; |
|
border: none; |
|
border-radius: 4px; |
|
cursor: pointer; |
|
font-size: 12px; |
|
font-weight: 500; |
|
transition: all 0.2s; |
|
display: inline-flex; |
|
align-items: center; |
|
gap: 4px; |
|
} |
|
|
|
.btn-sm { |
|
padding: 4px 8px; |
|
font-size: 11px; |
|
} |
|
|
|
.btn-primary { |
|
background: #3b82f6; |
|
color: white; |
|
} |
|
|
|
.btn-primary:hover:not(:disabled) { |
|
background: #2563eb; |
|
} |
|
|
|
.btn-primary:disabled { |
|
background: #9ca3af; |
|
cursor: not-allowed; |
|
} |
|
|
|
.btn-danger { |
|
background: #ef4444; |
|
color: white; |
|
} |
|
|
|
.btn-danger:hover:not(:disabled) { |
|
background: #dc2626; |
|
} |
|
|
|
.btn-danger:disabled { |
|
background: #9ca3af; |
|
cursor: not-allowed; |
|
} |
|
|
|
.btn-secondary { |
|
background: #6b7280; |
|
color: white; |
|
} |
|
|
|
.btn-secondary:hover:not(:disabled) { |
|
background: #4b5563; |
|
} |
|
|
|
.btn-secondary:disabled { |
|
background: #9ca3af; |
|
cursor: not-allowed; |
|
} |
|
</style>
|
|
|