Files
KnowledgeBase/app/Filament/Resources/RoleResource.php
lizhuoran 3e7083d7c1 fix(权限): 修复权限编辑时的自动勾选显示问题
问题:
- 使用 Tabs 组件时,每个 Tab 中的 CheckboxList 都使用相同的字段名 'permissions'
- 导致字段冲突,无法正确加载和保存已有权限

解决方案:
- 为每个模块使用唯一的字段名(permissions_document, permissions_user 等)
- 添加 afterStateHydrated 钩子,在编辑时自动加载该模块的已有权限
- 添加隐藏字段 all_permissions 收集所有模块的权限
- 在 mutateFormDataBeforeSave/mutateFormDataBeforeCreate 中处理权限数据
- 在 afterSave/afterCreate 中使用 syncPermissions 同步权限到数据库

改进:
- RoleResource: 编辑角色时,每个模块的权限会自动勾选显示
- UserResource: 编辑用户时,直接权限会自动勾选显示
- 保存时正确收集所有模块的权限并同步到数据库
- super-admin 角色的权限字段保持禁用状态

现在编辑角色或用户时,已有的权限会正确显示为勾选状态
2026-03-11 10:25:43 +08:00

319 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\RoleResource\Pages;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
class RoleResource extends Resource
{
protected static ?string $model = Role::class;
protected static ?string $navigationIcon = 'heroicon-o-shield-check';
protected static ?string $navigationLabel = '角色管理';
protected static ?string $modelLabel = '角色';
protected static ?string $pluralModelLabel = '角色';
protected static ?int $navigationSort = 5;
protected static ?string $navigationGroup = '系统管理';
/**
* 控制导航菜单是否显示
*/
public static function shouldRegisterNavigation(): bool
{
return auth()->user()?->can('role.viewAny') ?? false;
}
/**
* 获取权限分组标签页
*/
protected static function getPermissionTabs(): array
{
// 模块名称和图标映射
$moduleConfig = [
'document' => ['name' => '文档管理', 'icon' => 'heroicon-o-document-text'],
'system-setting' => ['name' => '系统设置', 'icon' => 'heroicon-o-cog-6-tooth'],
'activity-log' => ['name' => '操作日志', 'icon' => 'heroicon-o-clipboard-document-list'],
'terminal' => ['name' => '终端管理', 'icon' => 'heroicon-o-computer-desktop'],
'sop-template' => ['name' => 'SOP模板', 'icon' => 'heroicon-o-document-text'],
'group' => ['name' => '分组管理', 'icon' => 'heroicon-o-user-group'],
'user' => ['name' => '用户管理', 'icon' => 'heroicon-o-users'],
'role' => ['name' => '角色管理', 'icon' => 'heroicon-o-shield-check'],
];
// 操作名称映射
$actionNames = [
'viewAny' => '查看列表',
'view' => '查看详情',
'create' => '创建',
'update' => '编辑',
'delete' => '删除',
'download' => '下载',
'export' => '导出',
'sync' => '同步',
'publish' => '发布',
'archive' => '归档',
];
// 按模块分组权限
$groupedPermissions = Permission::all()
->groupBy(function ($permission) {
return explode('.', $permission->name)[0];
});
$tabs = [];
foreach ($groupedPermissions as $module => $permissions) {
$config = $moduleConfig[$module] ?? ['name' => $module, 'icon' => 'heroicon-o-square-3-stack-3d'];
// 构建该模块的权限选项
$options = $permissions->mapWithKeys(function ($permission) use ($actionNames) {
$action = explode('.', $permission->name)[1] ?? '';
$actionName = $actionNames[$action] ?? $action;
return [$permission->name => $actionName];
})->toArray();
$tabs[] = Forms\Components\Tabs\Tab::make($config['name'])
->icon($config['icon'])
->schema([
Forms\Components\CheckboxList::make("permissions_{$module}")
->label('')
->options($options)
->columns(2)
->bulkToggleable()
->disabled(fn (?Role $record): bool => $record?->name === 'super-admin')
->helperText(fn (?Role $record): string =>
$record?->name === 'super-admin'
? 'super-admin 角色拥有所有权限,不可修改'
: '选择该模块的权限'
)
->afterStateHydrated(function ($component, $state, ?Role $record) use ($module) {
if ($record) {
// 获取该角色在当前模块的权限
$modulePermissions = $record->permissions()
->where('name', 'like', "{$module}.%")
->pluck('name')
->toArray();
$component->state($modulePermissions);
}
})
->dehydrated(false), // 不直接保存,在下面统一处理
]);
}
return $tabs;
}
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Section::make('基本信息')
->schema([
Forms\Components\TextInput::make('name')
->label('角色标识')
->required()
->unique(ignoreRecord: true)
->maxLength(255)
->placeholder('例如: content-manager')
->helperText('角色的唯一标识符,使用小写字母和连字符')
->regex('/^[a-z0-9\-]+$/')
->validationMessages([
'regex' => '角色标识只能包含小写字母、数字和连字符',
])
->disabled(fn (?Role $record): bool => $record?->name === 'super-admin'),
Forms\Components\Select::make('guard_name')
->label('守卫')
->options([
'web' => 'Web',
])
->default('web')
->required()
->disabled(),
])
->columns(2),
Forms\Components\Section::make('权限配置')
->schema([
Forms\Components\Hidden::make('all_permissions')
->afterStateHydrated(function ($component, ?Role $record) {
if ($record) {
$component->state($record->permissions->pluck('name')->toArray());
}
})
->dehydrateStateUsing(function ($state, $get) {
// 收集所有模块的权限
$allPermissions = [];
$modules = ['document', 'system-setting', 'activity-log', 'terminal', 'sop-template', 'group', 'user', 'role'];
foreach ($modules as $module) {
$modulePermissions = $get("permissions_{$module}") ?? [];
$allPermissions = array_merge($allPermissions, $modulePermissions);
}
return $allPermissions;
}),
Forms\Components\Tabs::make('权限分组')
->tabs(self::getPermissionTabs())
->columnSpanFull(),
])
->description('配置角色的权限super-admin 角色拥有所有权限且不可修改'),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->label('角色标识')
->searchable()
->sortable()
->weight('bold')
->badge()
->color(fn (string $state): string => match ($state) {
'super-admin' => 'danger',
'admin' => 'warning',
'user' => 'success',
default => 'gray',
}),
Tables\Columns\TextColumn::make('guard_name')
->label('守卫')
->badge()
->sortable()
->toggleable(),
Tables\Columns\TextColumn::make('permissions_count')
->label('权限数量')
->counts('permissions')
->sortable()
->alignCenter()
->badge()
->color('info'),
Tables\Columns\TextColumn::make('users_count')
->label('用户数量')
->counts('users')
->sortable()
->alignCenter()
->badge()
->color('success'),
Tables\Columns\IconColumn::make('is_system')
->label('系统角色')
->boolean()
->trueIcon('heroicon-o-lock-closed')
->falseIcon('heroicon-o-lock-open')
->trueColor('danger')
->falseColor('gray')
->getStateUsing(fn (Role $record): bool => $record->name === 'super-admin')
->alignCenter()
->tooltip(fn (Role $record): string =>
$record->name === 'super-admin'
? '系统角色,不可删除'
: '可以删除'
),
Tables\Columns\TextColumn::make('created_at')
->label('创建时间')
->dateTime('Y-m-d H:i:s')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('updated_at')
->label('更新时间')
->dateTime('Y-m-d H:i:s')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
Tables\Filters\SelectFilter::make('guard_name')
->label('守卫')
->options([
'web' => 'Web',
]),
])
->actions([
Tables\Actions\ViewAction::make()
->label('查看'),
Tables\Actions\EditAction::make()
->label('编辑')
->visible(fn (Role $record): bool => $record->name !== 'super-admin'),
Tables\Actions\DeleteAction::make()
->label('删除')
->visible(fn (Role $record): bool => $record->name !== 'super-admin')
->before(function (Tables\Actions\DeleteAction $action, Role $record) {
// 检查是否有关联用户
if ($record->users()->count() > 0) {
\Filament\Notifications\Notification::make()
->danger()
->title('无法删除')
->body("该角色还有 {$record->users()->count()} 个用户,请先移除用户的角色后再删除。")
->persistent()
->send();
$action->cancel();
}
}),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make()
->label('批量删除')
->before(function (Tables\Actions\DeleteBulkAction $action, $records) {
// 检查是否包含 super-admin
if ($records->contains('name', 'super-admin')) {
\Filament\Notifications\Notification::make()
->danger()
->title('无法删除')
->body('不能删除 super-admin 角色')
->persistent()
->send();
$action->cancel();
return;
}
// 检查是否有关联用户
$rolesWithUsers = $records->filter(fn ($role) => $role->users()->count() > 0);
if ($rolesWithUsers->count() > 0) {
$roleNames = $rolesWithUsers->pluck('name')->join('、');
\Filament\Notifications\Notification::make()
->danger()
->title('无法删除')
->body("以下角色还有关联用户:{$roleNames},请先移除用户的角色后再删除。")
->persistent()
->send();
$action->cancel();
}
}),
]),
])
->defaultSort('created_at', 'desc');
}
public static function getPages(): array
{
return [
'index' => Pages\ListRoles::route('/'),
'create' => Pages\CreateRole::route('/create'),
'edit' => Pages\EditRole::route('/{record}/edit'),
'view' => Pages\ViewRole::route('/{record}'),
];
}
}