[FEAT] 完善全站多語系支援、角色權限篩選優化及 UI 元件重構
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m4s
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m4s
- [DOCS] 補齊 en, ja, zh_TW 語系檔翻譯並完善驗證錯誤訊息 (validation.php) - [FEAT] 角色權限頁面新增「所屬單位」篩選功能 (僅限系統管理員) - [STYLE] 優化角色列表顯示,將「類型」變更為具體「所屬單位」名稱 - [STYLE] 修正角色頁面工具列佈局,搜尋框置前並修正下拉箭頭顯示 - [REFACTOR] 統一全站刪除確認視窗,導入新版 <x-delete-confirm-modal /> 組件 - [REFACTOR] 優化 PermissionController 查詢效能 (Eager Loading) - [FIX] 修正 RoleSeeder 角色命名與資料庫同步邏輯
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
roleId: '{{ old('roleId', '') }}',
|
||||
roleName: '{{ old('name', '') }}',
|
||||
rolePermissions: @js(old('permissions', [])),
|
||||
currentUserRoleIds: @js($currentUserRoleIds ?? []),
|
||||
isSystem: {{ old('is_system', '0') }},
|
||||
modalTitle: '{{ $errors->any() && old('_method') == 'PUT' ? __('Edit Role') : ($errors->any() ? __('Create Role') : __('Create Role')) }}',
|
||||
openModal(edit = false, id = '', name = '', permissions = [], isSys = false) {
|
||||
@@ -22,6 +23,18 @@
|
||||
this.isSystem = isSys;
|
||||
this.modalTitle = edit ? '{{ __('Edit Role') }}' : '{{ __('Create Role') }}';
|
||||
this.showModal = true;
|
||||
},
|
||||
isWarningModalOpen: false,
|
||||
deleteWarningMsg: '',
|
||||
triggerDeleteWarning(msg) {
|
||||
this.deleteWarningMsg = msg;
|
||||
this.isWarningModalOpen = true;
|
||||
},
|
||||
isDeleteConfirmOpen: false,
|
||||
deleteFormAction: '',
|
||||
confirmDelete(action) {
|
||||
this.deleteFormAction = action;
|
||||
this.isDeleteConfirmOpen = true;
|
||||
}
|
||||
}">
|
||||
<!-- Header -->
|
||||
@@ -36,18 +49,34 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Roles Content (Integrated Card) -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
||||
<!-- Toolbar -->
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-10">
|
||||
<form action="{{ route($baseRoute) }}" method="GET" class="relative group">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
||||
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text" name="search" value="{{ request('search') }}" class="py-2.5 pl-12 pr-6 block w-full md:w-80 luxury-input" placeholder="{{ __('Search roles...') }}">
|
||||
<form action="{{ route($baseRoute) }}" method="GET" class="flex flex-col md:flex-row md:items-center gap-4">
|
||||
<div class="relative group">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
||||
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text" name="search" value="{{ request('search') }}" class="py-2.5 pl-12 pr-6 block w-full md:w-80 luxury-input" placeholder="{{ __('Search roles...') }}">
|
||||
</div>
|
||||
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<div class="relative">
|
||||
<select name="company_id" onchange="this.form.submit()" class="py-2.5 pl-4 pr-10 block w-full md:w-60 luxury-input">
|
||||
<option value="">{{ __('All Affiliations') }}</option>
|
||||
<option value="system" {{ request('company_id') === 'system' ? 'selected' : '' }}>{{ __('System Level') }}</option>
|
||||
@foreach($companies as $company)
|
||||
<option value="{{ $company->id }}" {{ request('company_id') == $company->id ? 'selected' : '' }}>{{ $company->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
@endif
|
||||
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
||||
</form>
|
||||
</div>
|
||||
@@ -57,7 +86,7 @@
|
||||
<thead>
|
||||
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Role Name') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Type') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Affiliation') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Permissions') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Users') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Actions') }}</th>
|
||||
@@ -85,8 +114,8 @@
|
||||
{{ __('System Level') }}
|
||||
</span>
|
||||
@else
|
||||
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-bold bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20 tracking-wider uppercase">
|
||||
{{ __('Company Level') }}
|
||||
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-bold bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20 tracking-wider">
|
||||
{{ $role->company->name ?? __('Company Level') }}
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
@@ -95,7 +124,7 @@
|
||||
@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>
|
||||
@empty
|
||||
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 italic tracking-widest">{{ __('No permissions') }}</span>
|
||||
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-widest">{{ __('No permissions') }}</span>
|
||||
@endforelse
|
||||
@if($role->permissions->count() > 6)
|
||||
<span class="px-2 py-0.5 text-xs bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 rounded border border-slate-200 dark:border-slate-700 uppercase font-bold tracking-widest">+{{ $role->permissions->count() - 6 }}</span>
|
||||
@@ -111,7 +140,7 @@
|
||||
<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>
|
||||
@if($role->name !== 'super-admin' && (auth()->user()->isSystemAdmin() || !$role->is_system))
|
||||
<form action="{{ route($baseRoute . '.destroy', $role->id) }}" method="POST" @submit.prevent="if(confirm('{{ __('Are you sure you want to delete this role?') }}')) $el.submit()" class="inline text-slate-400">
|
||||
<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
|
||||
@method('DELETE')
|
||||
<button type="submit" class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 hover:bg-rose-500/5 transition-all border border-transparent hover:border-rose-500/20 tooltip" title="{{ __('Delete') }}">
|
||||
@@ -163,6 +192,19 @@
|
||||
<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">
|
||||
@@ -185,7 +227,7 @@
|
||||
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<div class="space-y-4">
|
||||
<label class="text-xs font-black text-slate-400 uppercase tracking-widest pl-1">{{ __('Role Type') }}</label>
|
||||
<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'">
|
||||
@@ -256,5 +298,51 @@
|
||||
</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">
|
||||
<div x-show="isWarningModalOpen" x-transition:enter="ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0" class="fixed inset-0 transition-opacity bg-slate-900/60 backdrop-blur-sm"
|
||||
@click="isWarningModalOpen = false"></div>
|
||||
|
||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen">​</span>
|
||||
|
||||
<div x-show="isWarningModalOpen" x-transition:enter="ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100" x-transition:leave="ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
class="inline-block px-4 pt-5 pb-4 overflow-hidden text-left align-bottom transition-all transform bg-white dark:bg-slate-900 rounded-3xl shadow-2xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-8 border border-slate-100 dark:border-slate-800">
|
||||
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto bg-rose-100 dark:bg-rose-500/10 rounded-2xl sm:mx-0 sm:h-12 sm:w-12 text-rose-600 dark:text-rose-400">
|
||||
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mt-3 text-center sm:mt-0 sm:ml-6 sm:text-left">
|
||||
<h3 class="text-xl font-black text-slate-800 dark:text-white leading-6 tracking-tight outfit-font">
|
||||
{{ __('Cannot Delete Role') }}
|
||||
</h3>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 leading-relaxed" x-text="deleteWarningMsg"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 sm:mt-10 sm:flex sm:flex-row-reverse">
|
||||
<button type="button" @click="isWarningModalOpen = false"
|
||||
class="inline-flex justify-center w-full px-8 py-3 text-sm font-black text-white transition-all bg-slate-800 dark:bg-slate-700 rounded-xl hover:bg-slate-900 dark:hover:bg-slate-600 sm:w-auto tracking-widest uppercase">
|
||||
{{ __('Got it') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Global Delete Confirm Modal -->
|
||||
<x-delete-confirm-modal :message="__('Are you sure you want to delete this role? This action cannot be undone.')" />
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
Reference in New Issue
Block a user