star-cloud/resources/views/admin/_shared/account-list.blade.php
sky121113 0332eff9bc [FIX] 修正角色權限顯示邏輯與 UI 組件重構
1. 修正角色編輯頁面、列表頁面與卡片視圖中的權限數量計算邏輯,確保過濾掉無效或隱藏權限。
2. 重構角色權限顯示 UI,將彈窗由不穩定的 Popover 改為基於 Alpine.js x-teleport 的全螢幕置中模窗,解決容器遮擋問題。
3. 抽離並組件化角色清單 (role-list) 與卡片 (role-card) 視圖,提升程式碼複用性。
4. 統一子帳號管理頁面的搜尋與重設按鈕佈局,確保全站視覺一致性。
5. 更新多語系翻譯,修正『租戶』為『客戶』,並新增『全部權限』翻譯項。
6. 完成取貨碼與密碼管理模組的極簡奢華風 UI 遷移。
2026-04-28 16:13:57 +08:00

168 lines
13 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.

{{--
共用帳號列表 Partial
Props:
$users 分頁結果
$baseRoute 路由前綴
$companies 公司選單
$tabHidden 是否嵌入 hidden[name=tab](舊版相容,預設 false
$searchParam search 欄位名稱(預設 accounts_search
$companyParam company_id 欄位名稱(預設 accounts_company_id
$perPageParam per_page 欄位名稱(預設 accounts_per_page
$containerId ajax container id預設 ajax-accounts-container
--}}
@php
$tabHidden ??= false;
$searchParam ??= 'accounts_search';
$companyParam ??= 'accounts_company_id';
$perPageParam ??= 'accounts_per_page';
$containerId ??= 'ajax-accounts-container';
@endphp
<form action="{{ route($baseRoute) }}" method="GET"
class="flex flex-col md:flex-row md:items-center gap-3 mb-8"
@submit.prevent="fetchTabContent($el.action + '?' + new URLSearchParams(new FormData($el)).toString(), '{{ $containerId }}')">
@if($tabHidden)<input type="hidden" name="tab" value="accounts">@endif
<div class="relative group flex-1 md:flex-none">
<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"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
</span>
<input type="text" name="{{ $searchParam }}" value="{{ request($searchParam) }}"
class="py-2.5 pl-12 pr-6 block w-full md:w-80 luxury-input"
placeholder="{{ __('Search users...') }}">
</div>
@if(auth()->user()->isSystemAdmin())
<div class="relative w-full md:w-72">
<x-searchable-select name="{{ $companyParam }}" :options="$companies" :selected="request($companyParam)"
:placeholder="__('All Affiliations')"
onchange="this.form.dispatchEvent(new Event('submit'))" />
</div>
@endif
<input type="hidden" name="{{ $perPageParam }}" value="{{ request($perPageParam, 10) }}">
<div class="flex items-center gap-2 ml-auto md:ml-0 shrink-0">
<button type="submit" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 transition-all" title="{{ __('Search') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
</button>
<button type="button"
@click="$el.closest('form').querySelectorAll('input[type=text]').forEach(i=>i.value=''); $el.closest('form').querySelectorAll('select').forEach(s=>{s.value=' ';window.HSSelect?.getInstance(s)?.setValue(' ');}); $el.closest('form').dispatchEvent(new Event('submit'));"
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 transition-all" title="{{ __('Reset') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
</button>
</div>
</form>
{{-- 桌面表格 --}}
<div class="hidden xl:block overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-y-0">
<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">{{ __('User Info') }}</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">{{ __('Contact Info') }}</th>
@if(auth()->user()->isSystemAdmin())
<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>
@endif
<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">{{ __('Role') }}</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">{{ __('Status') }}</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>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
@forelse($users as $user)
<tr class="group hover:bg-slate-50/50 dark:hover:bg-white/[0.02] transition-colors duration-200">
<td class="px-6 py-6">
<div class="flex items-center gap-4">
<div class="w-10 h-10 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300 overflow-hidden shadow-sm shrink-0">
@if($user->avatar)<img src="{{ Storage::url($user->avatar) }}" class="w-full h-full object-cover">
@else<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z"/></svg>
@endif
</div>
<div class="min-w-0">
<p class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors tracking-tight truncate">{{ $user->name }}</p>
<p class="text-xs font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ $user->username }}</p>
</div>
</div>
</td>
<td class="px-6 py-6">
<div class="flex flex-col gap-0.5">
@if($user->phone)<p class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-widest">{{ $user->phone }}</p>@endif
@if($user->email)<p class="text-xs font-bold text-slate-400 dark:text-slate-500 tracking-wide">{{ $user->email }}</p>@endif
@if(!$user->phone && !$user->email)<span class="text-xs text-slate-300 dark:text-slate-600"></span>@endif
</div>
</td>
@if(auth()->user()->isSystemAdmin())
<td class="px-6 py-6">
@if($user->company)
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-widest uppercase">{{ $user->company->name }}</span>
@else
<span class="px-2.5 py-1 rounded-full text-[10px] font-black bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 uppercase tracking-widest border border-cyan-500/20">{{ __('SYSTEM') }}</span>
@endif
</td>
@endif
<td class="px-6 py-6 text-center">
<div class="flex flex-wrap justify-center gap-1">
@forelse($user->roles as $role)
<span class="px-2.5 py-1 rounded-full text-[10px] font-black bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-slate-700 uppercase tracking-widest">{{ $role->name }}</span>
@empty
<span class="text-xs font-bold text-slate-300 dark:text-slate-600">{{ __('No Role') }}</span>
@endforelse
</div>
</td>
<td class="px-6 py-6 text-center">
<x-status-badge :status="$user->status ? 'active' : 'disabled'" size="sm" />
</td>
<td class="px-6 py-6 text-right">
<div class="flex items-center justify-end gap-2">
@if(!$user->hasRole('super-admin') || auth()->user()->hasRole('super-admin'))
@if($user->status)
<button type="button"
@click="toggleFormAction = '{{ route($baseRoute . '.status.toggle', $user->id) }}'; statusToggleSource = 'list'; isStatusConfirmOpen = true"
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-amber-500 hover:bg-amber-500/5 transition-all border border-transparent hover:border-amber-500/20 tooltip" title="{{ __('Disable') }}">
<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="M15.75 5.25v13.5m-7.5-13.5v13.5"/></svg>
</button>
@else
<button type="button"
@click="toggleFormAction = '{{ route($baseRoute . '.status.toggle', $user->id) }}'; $nextTick(() => $refs.statusToggleForm.submit())"
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-emerald-500 hover:bg-emerald-500/5 transition-all border border-transparent hover:border-emerald-500/20 tooltip" title="{{ __('Enable') }}">
<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="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347c-.75.412-1.667-.13-1.667-.986V5.653z"/></svg>
</button>
@endif
<button @click="openEditModal(@js($user))"
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>
<button type="button" @click="confirmDelete('{{ route($baseRoute . '.destroy', $user->id) }}')"
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') }}">
<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="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"/></svg>
</button>
@else
<span class="text-[10px] font-black text-slate-300 dark:text-slate-600 uppercase tracking-[0.15em] px-2">{{ __('Protected') }}</span>
@endif
</div>
</td>
</tr>
@empty
<x-empty-state mode="table" :colspan="auth()->user()->isSystemAdmin() ? 6 : 5" :message="__('No users found.')" />
@endforelse
</tbody>
</table>
</div>
{{-- Mobile/Tablet Card Grid --}}
<div class="xl:hidden grid grid-cols-1 md:grid-cols-2 gap-6">
@forelse($users as $user)
@include('admin.data-config.partials.account-card', ['user' => $user, 'baseRoute' => $baseRoute])
@empty
<x-empty-state :message="__('No users found.')" />
@endforelse
</div>
{{-- 分頁 --}}
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
{{ $users->appends(request()->query())->links('vendor.pagination.luxury', [
'ajax_navigate_event' => 'ajax:navigate:accounts',
'per_page_param' => $perPageParam,
]) }}
</div>