[FIX] 徹底修復商品管理分頁參數洩漏與 UI 狀態不一致問題
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 59s

1. 隔離商品列表與分類管理的搜尋參數(product_search / category_search)。
2. 隔離商品列表與分類管理的公司篩選參數(product_company_id / category_company_id)。
3. 優化分頁切換邏輯,切換 Tab 時自動清理 URL 參數,解決標籤殘留問題。
4. 修復 searchable-select 組件因屬性傳遞錯誤導致的 500 Internal Server Error。
5. 統一各分頁「公司篩選」Placeholder 為「所有公司」。
6. 完成分類管理搜尋框的多語系支援(新增 Search categories... 翻譯鍵值)。
7. 優化分頁器 (Pagination) 樣式以符合極簡奢華風規範。
This commit is contained in:
2026-04-15 16:45:13 +08:00
parent 24553d9b73
commit 1301bf1cb8
10 changed files with 670 additions and 247 deletions

View File

@@ -0,0 +1,84 @@
<div class="relative">
<form @submit.prevent="handleFilterSubmit('categories')" id="categories-filter-form" class="mb-8 flex flex-wrap items-center gap-4">
<!-- Search Input -->
<div class="relative group">
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-slate-400 group-focus-within:text-cyan-500 transition-colors">
<svg class="size-4" 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>
</div>
<input type="text" name="category_search" value="{{ request('category_search', request('tab') === 'categories' ? request('category_search') : '') }}"
class="bg-slate-50 dark:bg-slate-900/50 border-none rounded-xl py-3 pl-10 pr-4 text-sm font-bold text-slate-700 dark:text-slate-200 focus:ring-2 focus:ring-cyan-500/20 w-64 transition-all shadow-sm"
placeholder="{{ __('Search categories...') }}">
</div>
@if(auth()->user()->isSystemAdmin())
<div class="relative min-w-[200px]">
<x-searchable-select name="category_company_id"
:options="$companies"
:selected="request('category_company_id')"
:placeholder="__('All Companies')"
@change="handleFilterSubmit('categories')"
/>
</div>
@endif
<input type="hidden" name="tab" value="categories">
</form>
<div class="overflow-x-auto luxury-scrollbar">
<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">{{ __('Category Name') }}</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 text-center">{{ __('Company') }}</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-right">{{ __('Actions') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
@forelse($categories as $category)
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
<td class="px-6 py-5">
<div class="flex items-center gap-x-4">
<div class="w-10 h-10 rounded-xl 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 group-hover:border-cyan-500 shadow-sm group-hover:shadow-cyan-500/50 transition-all duration-300">
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.568 3H5.25A2.25 2.25 0 003 5.25v13.5A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V9.432a2.25 2.25 0 00-.659-1.591l-4.182-4.182A2.25 2.25 0 0014.568 3h-5z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12h-6m6 4h-6m6-8h-6" />
</svg>
</div>
<span class="text-base font-extrabold text-slate-800 dark:text-slate-100 transition-colors group-hover:text-cyan-600 dark:group-hover:text-cyan-400">
{{ $category->localized_name }}
</span>
</div>
</td>
@if(auth()->user()->isSystemAdmin())
<td class="px-6 py-5 text-center">
<span class="text-xs font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ $category->company->name ?? __('System Default') }}</span>
</td>
@endif
<td class="px-6 py-5 text-right">
<div class="flex justify-end items-center gap-2">
<button type="button" @click="openCategoryModal(@js($category))" class="p-2.5 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" title="{{ __('Edit') }}">
<svg class="size-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('admin.data-config.product-categories.destroy', $category->id) }}')" class="p-2.5 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" title="{{ __('Delete') }}">
<svg class="size-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 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" /></svg>
</button>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="{{ auth()->user()->isSystemAdmin() ? 3 : 2 }}" class="px-6 py-20 text-center text-slate-400 italic font-bold tracking-widest small-caps">
{{ __('No categories found.') }}
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="mt-6 py-6 border-t border-slate-50 dark:border-slate-800/50">
{{ $categories->links('vendor.pagination.luxury') }}
</div>
</div>

View File

@@ -0,0 +1,144 @@
@php
$baseRoute = 'admin.data-config.products';
@endphp
<div class="relative">
<!-- Filters & Search -->
<form @submit.prevent="handleFilterSubmit('products')" id="products-filter-form" class="flex flex-col md:flex-row md:items-center gap-4 mb-6">
<div class="relative group">
<button type="submit" class="absolute inset-y-0 left-0 flex items-center pl-4 z-10 text-slate-400 group-focus-within:text-cyan-500 transition-colors">
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" 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>
</button>
<input type="text" name="product_search" value="{{ request('product_search') }}"
class="py-2.5 pl-12 pr-6 block w-full md:w-80 luxury-input" placeholder="{{ __('Search products...') }}">
<button type="submit" class="hidden"></button>
</div>
@if(auth()->user()->isSystemAdmin())
<div class="relative min-w-[200px]">
<x-searchable-select name="product_company_id" :options="$companies" :selected="request('product_company_id')" :placeholder="__('All Companies')" @change="handleFilterSubmit('products')" />
</div>
@endif
<input type="hidden" name="tab" value="products">
</form>
<!-- Table -->
<div class="overflow-x-auto luxury-scrollbar">
<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">{{ __('Product 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">{{ __('Barcode') }}</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 text-center">{{ __('Company') }}</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">{{ __('Sale Price') }}</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">{{ __('Track Limit (Track/Spring)') }}</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($products as $product)
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
<td class="px-6 py-5 cursor-pointer group/info" @click="viewProductDetail(@js($product))" title="{{ __('View Details') }}">
<div class="flex items-center gap-x-4">
<div class="w-12 h-12 rounded-xl 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/info:bg-cyan-500 group-hover/info:text-white group-hover/info:border-cyan-500 shadow-sm group-hover/info:shadow-cyan-500/50 transition-all duration-300 overflow-hidden relative">
@if($product->image_url)
<img src="{{ $product->image_url }}" class="w-full h-full object-cover">
@else
<svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" /></svg>
@endif
</div>
<div class="flex flex-col">
<span class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover/info:text-cyan-600 dark:group-hover/info:text-cyan-400 transition-colors leading-tight">{{ $product->localized_name }}</span>
<div class="flex flex-wrap items-center gap-1.5 mt-1.5">
@php
$catName = $product->category->localized_name ?? __('Uncategorized');
@endphp
<span class="text-[10px] font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.14em] bg-slate-100/80 dark:bg-slate-800/80 border border-slate-200/60 dark:border-slate-700/60 px-2.5 py-1 rounded-lg backdrop-blur-sm transition-all duration-300 group-hover/info:bg-cyan-500/10 group-hover/info:border-cyan-500/20 group-hover/info:text-cyan-500 group-hover/info:shadow-sm group-hover/info:shadow-cyan-500/10">{{ $catName }}</span>
@if(($companySettings['enable_material_code'] ?? false) && isset($product->metadata['material_code']))
<span class="text-[10px] font-black text-emerald-500/90 uppercase tracking-widest bg-emerald-500/10 px-2 py-0.5 rounded-lg border border-emerald-500/20 shadow-sm shadow-emerald-500/5">#{{ $product->metadata['material_code'] }}</span>
@endif
</div>
</div>
</div>
</td>
<td class="px-6 py-5 whitespace-nowrap">
<span class="text-sm font-mono font-bold text-slate-600 dark:text-slate-300 tracking-tight">{{ $product->barcode ?: '-' }}</span>
</td>
@if(auth()->user()->isSystemAdmin())
<td class="px-6 py-5 text-center">
<span class="text-xs font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest group-hover:text-slate-600 dark:group-hover:text-slate-300 transition-colors">{{ $product->company->name ?? '-' }}</span>
</td>
@endif
<td class="px-6 py-5 text-center whitespace-nowrap">
<span class="text-sm font-black text-slate-800 dark:text-white leading-none">${{ number_format($product->price, 0) }}</span>
</td>
<td class="px-6 py-5">
<div class="flex items-center justify-center gap-2 font-black">
<span class="text-base text-indigo-500 dark:text-indigo-400">
{{ $product->track_limit ?: 0 }}
</span>
<span class="text-xs text-slate-300 dark:text-slate-700">/</span>
<span class="text-base text-amber-500 dark:text-amber-500">
{{ $product->spring_limit ?: 0 }}
</span>
</div>
</td>
<td class="px-6 py-5 text-center">
@if($product->is_active)
<span class="inline-flex items-center px-3 py-1.5 rounded-xl text-[10px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 tracking-widest uppercase shadow-sm shadow-emerald-500/5">{{ __('Active') }}</span>
@else
<span class="inline-flex items-center px-3 py-1.5 rounded-xl text-[10px] font-black bg-slate-400/10 text-slate-400 border border-slate-400/20 tracking-widest uppercase">{{ __('Disabled') }}</span>
@endif
</td>
<td class="px-6 py-5 text-right">
<div class="flex justify-end items-center gap-2">
@if($product->is_active)
<button type="button"
@click="toggleStatus('{{ route($baseRoute . '.status.toggle', $product->id) }}')"
class="p-2.5 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"
title="{{ __('Disable') }}">
<svg class="size-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="toggleStatus('{{ route($baseRoute . '.status.toggle', $product->id) }}')"
class="p-2.5 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"
title="{{ __('Enable') }}">
<svg class="size-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
<a href="{{ route($baseRoute . '.edit', $product->id) }}" class="p-2.5 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" title="{{ __('Edit') }}">
<svg class="size-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>
</a>
<button type="button" @click="confirmDelete('{{ route($baseRoute . '.destroy', $product->id) }}')" class="p-2.5 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" title="{{ __('Delete') }}">
<svg class="size-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 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" /></svg>
</button>
<button type="button" @click="viewProductDetail({{ json_encode($product) }})" class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-indigo-500 hover:bg-indigo-500/5 transition-all border border-transparent hover:border-indigo-500/20" title="{{ __('View') }}">
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.644C3.67 8.5 7.652 6 12 6c4.348 0 8.33 2.5 9.964 5.678.134.263.134.561 0 .824C20.33 15.5 16.348 18 12 18c-4.348 0-8.33-2.5-9.964-5.678Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
</button>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="7" class="px-6 py-20 text-center text-slate-400 italic font-bold tracking-widest small-caps">{{ __('No products found matching your criteria.') }}</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="mt-6 py-6 border-t border-slate-50 dark:border-slate-800/50">
{{ $products->links('vendor.pagination.luxury') }}
</div>
</div>