[FEAT] 角色權限編輯頁面重構與多項 UI/翻譯優化
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 44s

- 新增獨立角色權限編輯頁面 (roles-edit.blade.php),採整合式佈局
- 重構 PermissionController 以支援角色建立/編輯/刪除完整 CRUD
- 移除角色手動層級選擇,改為自動判定並顯示所屬單位
- 補齊 20+ 項 menu 權限 Key 的三語系翻譯 (zh_TW/en/ja)
- 修正子項目佈局跑版問題 (min-w-0/flex-shrink-0 防溢出)
- 更新 RoleSeeder 加入巢狀權限結構
- 同步更新側邊欄選單與路由配置
This commit is contained in:
2026-03-20 17:35:06 +08:00
parent d2cefe3f39
commit 72812f9b0b
9 changed files with 509 additions and 170 deletions

View File

@@ -43,14 +43,12 @@
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ $title }}</h1>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Define and manage security roles and permissions.') }}</p>
</div>
<button @click="openModal()" class="btn-luxury-primary text-sm">
<a href="{{ route($baseRoute . '.create') }}" class="btn-luxury-primary text-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
<span>{{ __('Add Role') }}</span>
</button>
</a>
</div>
<!-- Roles Content (Integrated Card) -->
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
<!-- Toolbar -->
@@ -119,7 +117,7 @@
</span>
@endif
</td>
<td class="px-6 py-6">
<td class="px-6 py-6" width="30%">
<div class="flex flex-wrap gap-1 max-w-xs">
@forelse($role->permissions->take(6) as $permission)
<span class="px-2 py-0.5 text-xs bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 rounded border border-slate-200 dark:border-slate-700 uppercase font-bold tracking-widest">{{ __(str_replace('menu.', '', $permission->name)) }}</span>
@@ -136,9 +134,9 @@
</td>
<td class="px-6 py-6 text-right">
<div class="flex items-center justify-end gap-2">
<button @click="openModal(true, @js($role->id), @js($role->name), @js($role->permissions->pluck('name')), {{ $role->is_system ? 'true' : 'false' }})" class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all border border-transparent hover:border-cyan-500/20 tooltip" title="{{ __('Edit') }}">
<a href="{{ route($baseRoute . '.edit', $role->id) }}" class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all border border-transparent hover:border-cyan-500/20 tooltip" title="{{ __('Edit') }}">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"/></svg>
</button>
</a>
@if($role->name !== 'super-admin' && (auth()->user()->isSystemAdmin() || !$role->is_system))
<form action="{{ route($baseRoute . '.destroy', $role->id) }}" method="POST" @submit.prevent="if({{ $role->users()->count() }} > 0) { triggerDeleteWarning('{{ __('Cannot delete role with active users.') }}'); return; } confirmDelete('{{ route($baseRoute . '.destroy', $role->id) }}')" class="inline text-slate-400">
@csrf
@@ -170,134 +168,6 @@
</div>
</div>
<!-- Modal -->
<template x-if="showModal">
<div class="fixed inset-0 z-[60] flex items-center justify-center p-4">
<div @click="showModal = false" class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity"></div>
<div class="relative w-full max-w-3xl bg-white dark:bg-slate-900 rounded-3xl shadow-2xl overflow-hidden animate-luxury-in">
<div class="flex items-center justify-between p-8 border-b border-slate-100 dark:border-slate-800">
<div>
<h3 class="text-2xl font-black text-slate-800 dark:text-white" x-text="modalTitle"></h3>
<p class="text-sm font-bold text-slate-400 mt-1" x-text="isEdit ? '{{ __('Update existing role and permissions.') }}' : '{{ __('Create a new role and assign permissions.') }}'"></p>
</div>
<button @click="showModal = false" class="p-2 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-all">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
</div>
<form :action="isEdit ? '{{ route($baseRoute) }}/' + roleId : '{{ route($baseRoute . '.store') }}'" method="POST">
@csrf
<template x-if="isEdit"><input type="hidden" name="_method" value="PUT"></template>
<input type="hidden" name="roleId" x-model="roleId">
<div class="p-8 max-h-[65vh] overflow-y-auto custom-scrollbar">
<!-- Warning for editing own role -->
<template x-if="isEdit && (currentUserRoleIds || []).map(String).includes(String(roleId))">
<div class="mb-8 p-5 bg-amber-500/10 border border-amber-500/20 text-amber-600 dark:text-amber-400 rounded-2xl font-bold flex items-start gap-4 animate-luxury-in">
<div class="size-10 bg-amber-500/20 rounded-xl flex items-center justify-center flex-shrink-0">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
</div>
<div class="text-sm">
<p class="text-base font-black leading-tight">{{ __('Warning: You are editing your own role!') }}</p>
<p class="font-bold mt-1 opacity-90 leading-relaxed">{{ __('Modifying your own administrative permissions may result in losing access to certain system functions.') }}</p>
</div>
</div>
</template>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- Left: Basic Info -->
<div class="space-y-6">
<div class="space-y-2">
<label class="text-xs font-black text-slate-400 uppercase tracking-widest pl-1">{{ __('Role Name') }}</label>
<input type="text" name="name" x-model="roleName" required class="luxury-input w-full @error('name') border-rose-500 @enderror" placeholder="{{ __('Enter role name') }}" :disabled="isEdit && roleName === 'super-admin'">
@error('name')
<p class="text-[11px] text-rose-500 font-bold mt-1.5 px-1 flex items-center gap-1.5 animate-luxury-in">
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
{{ $message }}
</p>
@enderror
<template x-if="isEdit && roleName === 'super-admin'">
<p class="text-[10px] text-amber-500 font-bold mt-1 px-1 flex items-center gap-1.5">
<svg xmlns="http://www.w3.org/2000/svg" class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
{{ __('The Super Admin role name cannot be modified.') }}
</p>
</template>
</div>
@if(auth()->user()->isSystemAdmin())
<div class="space-y-4">
<label class="text-xs font-black text-slate-400 uppercase tracking-widest pl-1">{{ __('Affiliation') }}</label>
<div class="flex flex-col gap-2">
<label class="flex items-center gap-3 p-3 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-800/50 cursor-pointer transition-all border border-slate-200 dark:border-slate-800 group has-[:checked]:border-cyan-500/50 has-[:checked]:bg-cyan-500/5">
<input type="radio" name="is_system" value="1" x-model="isSystem" class="w-4 h-4 text-cyan-500 bg-transparent border-slate-300 focus:ring-cyan-500" :disabled="isEdit && roleName === 'super-admin'">
<span class="text-sm font-bold text-slate-700 dark:text-slate-200 group-hover:text-cyan-600 dark:group-hover:text-cyan-400">{{ __('System Level') }}</span>
</label>
<label class="flex items-center gap-3 p-3 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-800/50 cursor-pointer transition-all border border-slate-200 dark:border-slate-800 group has-[:checked]:border-cyan-500/50 has-[:checked]:bg-cyan-500/5">
<input type="radio" name="is_system" value="0" x-model="isSystem" class="w-4 h-4 text-cyan-500 bg-transparent border-slate-300 focus:ring-cyan-500" :disabled="isEdit && roleName === 'super-admin'">
<span class="text-sm font-bold text-slate-700 dark:text-slate-200 group-hover:text-cyan-600 dark:group-hover:text-cyan-400">{{ __('Company Level') }}</span>
</label>
</div>
</div>
@endif
</div>
<!-- Right: Permissions -->
<div class="space-y-6">
<label class="text-xs font-black text-slate-400 uppercase tracking-widest pl-1">{{ __('Menu Permissions') }}</label>
<div class="luxury-card border-slate-100 dark:border-slate-800 p-4 rounded-2xl grid grid-cols-1 gap-3">
@php
$icon_map = [
'members' => 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z',
'machines' => 'M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 01-2 2v4a2 2 0 012 2h14a2 2 0 012-2v-4a2 2 0 01-2-2m-2-4h.01M17 16h.01',
'app' => 'M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z',
'warehouses' => 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10',
'sales' => 'M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
'analysis' => 'M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z',
'audit' => 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
'data-config' => 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z',
'remote' => 'M5.636 18.364a9 9 0 010-12.728m12.728 0a9 9 0 010 12.728m-9.9-2.829a5 5 0 010-7.07m7.072 0a5 5 0 010 7.07M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
'line' => 'M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z',
'reservation' => 'M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z',
'special-permission' => 'M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z',
'basic-settings' => 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z',
'permissions' => 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z',
];
@endphp
@foreach($all_permissions->get('menu', []) as $permission)
@php
$pure_name = str_replace('menu.', '', $permission->name);
$is_restricted = in_array($permission->name, ['menu.basic-settings', 'menu.permissions']);
@endphp
<label class="flex items-center gap-3 p-3 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-800/50 cursor-pointer transition-all border border-transparent hover:border-slate-200 dark:hover:border-slate-700 group"
@if($is_restricted) x-show="isSystem == '1'" x-transition @endif>
<div class="relative flex items-center">
<input type="checkbox" name="permissions[]" value="{{ $permission->name }}"
x-model="rolePermissions"
class="peer w-5 h-5 rounded-lg border-2 border-slate-200 dark:border-slate-700 text-cyan-500 focus:ring-cyan-500 focus:ring-offset-0 bg-transparent transition-all checked:border-cyan-500">
<svg class="absolute w-3.5 h-3.5 text-white opacity-0 peer-checked:opacity-100 left-0.5 pointer-events-none transition-opacity" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</div>
<div class="flex items-center gap-2.5">
<div class="w-7 h-7 rounded-lg flex items-center justify-center bg-slate-50 dark:bg-slate-800 text-slate-400 group-hover:bg-cyan-500 group-hover:text-white transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="{{ $icon_map[$pure_name] ?? 'M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z' }}" /></svg>
</div>
<span class="text-sm font-bold text-slate-600 dark:text-slate-300 group-hover:text-slate-900 dark:group-hover:text-white tracking-wide transition-colors">{{ __($pure_name) }}</span>
</div>
</label>
@endforeach
</div>
</div>
</div>
</div>
<div class="p-8 bg-slate-50 dark:bg-slate-800/50 border-t border-slate-100 dark:border-slate-800 flex items-center justify-end gap-4">
<button type="button" @click="showModal = false" class="btn-luxury-ghost px-8 py-3 rounded-2xl font-black text-xs uppercase tracking-widest">{{ __('Cancel') }}</button>
<button type="submit" class="btn-luxury-primary px-10 py-3 rounded-2xl font-black text-xs uppercase tracking-widest shadow-xl shadow-cyan-500/20">{{ __('Save Changes') }}</button>
</div>
</form>
</div>
</div>
</template>
<!-- Global Delete Warning Modal -->
<div x-show="isWarningModalOpen" class="fixed inset-0 z-[200] overflow-y-auto" x-cloak>
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">