[DOCS] 更新 RBAC 實作規範與角色初始化流程建議
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 55s

This commit is contained in:
2026-03-19 17:18:21 +08:00
parent 5548bb1cc9
commit f00fc940a9
13 changed files with 474 additions and 74 deletions

View File

@@ -322,9 +322,21 @@
placeholder="{{ __('Min 8 characters') }}">
</div>
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Admin Name') }}</label>
<input type="text" name="admin_name" class="luxury-input w-full" placeholder="{{ __('Admin display name') }}">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Admin Name') }}</label>
<input type="text" name="admin_name" class="luxury-input w-full" placeholder="{{ __('Admin display name') }}">
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Initial Role') }}</label>
<select name="admin_role" class="luxury-select w-full">
@foreach($template_roles as $role)
<option value="{{ $role->name }}" {{ $role->name == '通用客戶角色範本' ? 'selected' : '' }}>
{{ $role->name }}
</option>
@endforeach
</select>
</div>
</div>
</div>

View File

@@ -7,28 +7,53 @@
@section('content')
<div class="space-y-6" x-data="{
showModal: false,
editing: false,
showModal: {{ $errors->any() ? 'true' : 'false' }},
editing: {{ old('_method') === 'PUT' || (isset($user) && $errors->any()) ? 'true' : 'false' }},
allRoles: @js($roles),
currentUser: {
id: '',
name: '',
username: '',
email: '',
phone: '',
company_id: '',
role: 'user',
status: 1
id: '{{ old('id') }}',
name: '{{ old('name') }}',
username: '{{ old('username') }}',
email: '{{ old('email') }}',
phone: '{{ old('phone') }}',
company_id: '{{ old('company_id', auth()->user()->isSystemAdmin() ? '' : auth()->user()->company_id) }}',
role: '{{ old('role', '') }}',
status: {{ old('status', 1) }}
},
get filteredRoles() {
if (this.currentUser.company_id === '' || this.currentUser.company_id === null) {
// 系統層級:顯示 is_system = 1 的角色
return this.allRoles.filter(r => r.is_system);
} else {
// 客戶層級:只顯示該公司的角色
let roles = this.allRoles.filter(r => r.company_id == this.currentUser.company_id);
// 如果是系統管理員,額外允許選擇「客戶層級範本」
@if(auth()->user()->isSystemAdmin())
let templates = this.allRoles.filter(r => !r.is_system && (r.company_id === null || r.company_id === ''));
roles = [...roles, ...templates];
@endif
return roles;
}
},
openCreateModal() {
this.editing = false;
this.currentUser = { id: '', name: '', username: '', email: '', phone: '', company_id: '', role: 'user', status: 1 };
this.currentUser = { id: '', name: '', username: '', email: '', phone: '', company_id: '{{ auth()->user()->isSystemAdmin() ? "" : auth()->user()->company_id }}', role: '', status: 1 };
this.showModal = true;
// 預設選取第一個可用的角色
this.$nextTick(() => {
if (this.filteredRoles.length > 0) {
this.currentUser.role = this.filteredRoles[0].name;
}
});
},
openEditModal(user) {
this.editing = true;
this.currentUser = {
...user,
role: user.roles && user.roles.length > 0 ? user.roles[0].name : 'user'
company_id: user.company_id || '',
role: user.roles && user.roles.length > 0 ? user.roles[0].name : ''
};
this.showModal = true;
}
@@ -217,61 +242,97 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Full Name') }}</label>
<input type="text" name="name" x-model="currentUser.name" required class="luxury-input" placeholder="{{ __('e.g. John Doe') }}">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">
{{ __('Full Name') }} <span class="text-rose-500">*</span>
</label>
<input type="text" name="name" x-model="currentUser.name" required class="luxury-input @error('name') border-rose-500 @enderror" placeholder="{{ __('e.g. John Doe') }}">
@error('name')
<p class="text-[10px] font-bold text-rose-500 mt-1 pl-1 uppercase tracking-tight">{{ $message }}</p>
@enderror
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Username') }}</label>
<input type="text" name="username" x-model="currentUser.username" required class="luxury-input" placeholder="{{ __('e.g. johndoe') }}">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">
{{ __('Username') }} <span class="text-rose-500">*</span>
</label>
<input type="text" name="username" x-model="currentUser.username" required class="luxury-input @error('username') border-rose-500 @enderror" placeholder="{{ __('e.g. johndoe') }}">
@error('username')
<p class="text-[10px] font-bold text-rose-500 mt-1 pl-1 uppercase tracking-tight">{{ $message }}</p>
@enderror
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Email') }}</label>
<input type="email" name="email" x-model="currentUser.email" class="luxury-input" placeholder="{{ __('john@example.com') }}">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">
{{ __('Email') }} <span class="text-rose-500">*</span>
</label>
<input type="email" name="email" x-model="currentUser.email" required class="luxury-input @error('email') border-rose-500 @enderror" placeholder="{{ __('john@example.com') }}">
@error('email')
<p class="text-[10px] font-bold text-rose-500 mt-1 pl-1 uppercase tracking-tight">{{ $message }}</p>
@enderror
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Phone') }}</label>
<input type="text" name="phone" x-model="currentUser.phone" class="luxury-input">
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Role') }}</label>
<select name="role" x-model="currentUser.role" class="luxury-select">
@foreach($roles as $role)
<option value="{{ $role->name }}">{{ __($role->name) }}</option>
@endforeach
</select>
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Status') }}</label>
<select name="status" x-model="currentUser.status" class="luxury-select">
<option value="1">{{ __('Active') }}</option>
<option value="0">{{ __('Disabled') }}</option>
</select>
<input type="text" name="phone" x-model="currentUser.phone" class="luxury-input @error('phone') border-rose-500 @enderror">
@error('phone')
<p class="text-[10px] font-bold text-rose-500 mt-1 pl-1 uppercase tracking-tight">{{ $message }}</p>
@enderror
</div>
</div>
@if(auth()->user()->isSystemAdmin())
<div class="space-y-2">
<div class="space-y-2 mb-6">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Company') }}</label>
<select name="company_id" x-model="currentUser.company_id" class="luxury-select">
<select name="company_id" x-model="currentUser.company_id" class="luxury-select @error('company_id') border-rose-500 @enderror"
@change="$nextTick(() => { if (filteredRoles.length > 0 && !filteredRoles.find(r => r.name === currentUser.role)) { currentUser.role = filteredRoles[0].name; } })">
<option value="">{{ __('SYSTEM') }}</option>
@foreach($companies as $company)
<option value="{{ $company->id }}">{{ $company->name }}</option>
@endforeach
</select>
@error('company_id')
<p class="text-[10px] font-bold text-rose-500 mt-1 pl-1 uppercase tracking-tight">{{ $message }}</p>
@enderror
</div>
@endif
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">
{{ __('Role') }} <span class="text-rose-500">*</span>
</label>
<select name="role" x-model="currentUser.role" class="luxury-select @error('role') border-rose-500 @enderror">
<template x-for="role in filteredRoles" :key="role.id">
<option :value="role.name" x-text="role.name"></option>
</template>
</select>
@error('role')
<p class="text-[10px] font-bold text-rose-500 mt-1 pl-1 uppercase tracking-tight">{{ $message }}</p>
@enderror
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Status') }}</label>
<select name="status" x-model="currentUser.status" class="luxury-select @error('status') border-rose-500 @enderror">
<option value="1">{{ __('Active') }}</option>
<option value="0">{{ __('Disabled') }}</option>
</select>
@error('status')
<p class="text-[10px] font-bold text-rose-500 mt-1 pl-1 uppercase tracking-tight">{{ $message }}</p>
@enderror
</div>
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">
<span x-text="editing ? '{{ __('New Password (leave blank to keep current)') }}' : '{{ __('Password') }}'"></span>
<template x-if="!editing">
<span class="text-rose-500">*</span>
</template>
</label>
<input type="password" name="password" :required="!editing" class="luxury-input" placeholder="••••••••">
<input type="password" name="password" :required="!editing" class="luxury-input @error('password') border-rose-500 @enderror" placeholder="••••••••">
@error('password')
<p class="text-[10px] font-bold text-rose-500 mt-1 pl-1 uppercase tracking-tight">{{ $message }}</p>
@enderror
</div>
<div class="flex justify-end gap-x-4 pt-8">

View File

@@ -7,13 +7,13 @@
@section('content')
<div class="space-y-6" x-data="{
showModal: false,
isEdit: false,
roleId: '',
roleName: '',
rolePermissions: [],
isSystem: false,
modalTitle: '{{ __('Create Role') }}',
showModal: {{ $errors->any() ? 'true' : 'false' }},
isEdit: {{ (old('_method') == 'PUT' || request()->has('edit')) ? 'true' : 'false' }},
roleId: '{{ old('roleId', '') }}',
roleName: '{{ old('name', '') }}',
rolePermissions: @js(old('permissions', [])),
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) {
this.isEdit = edit;
this.roleId = id;
@@ -160,6 +160,7 @@
<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">
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
@@ -167,7 +168,13 @@
<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" placeholder="{{ __('Enter role name') }}" :disabled="isEdit && roleName === 'super-admin'">
<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>