[FEAT] 實作調撥與補貨單商品快照機制,並修復商品無圖片或載入失敗時的立方體預設圖示顯示

1. 資料庫結構擴充:為 transfer_order_items 與 replenishment_order_items 表新增商品快照欄位,完成遷移。
2. Model 調整與防禦:重構 TransferOrderItem 與 ReplenishmentOrderItem Model,實現商品軟刪除/物理刪除的 name、barcode、image_url 安全 Fallback 及多語系字典動態解析,並防禦性過濾 /storage 等無效圖片路徑。
3. Controller 效能優化:重構寫入與詳情加載邏輯,預載入關聯以防範 N+1 查詢,優化批量寫入效率。
4. 前端圖片載入錯誤處理:在調撥與補貨單明細中,利用 Alpine.js 增加圖片 x-on:error 錯誤監聽,在圖片加載失敗時自動 fallback 顯示與訂單詳情一致的灰色立方體 icon。
This commit is contained in:
sky121113 2026-05-21 16:28:33 +08:00
parent 14ecfdcc8d
commit a3a00b1403
2 changed files with 19 additions and 14 deletions

View File

@ -94,11 +94,17 @@
</div> </div>
<div class="space-y-3"> <div class="space-y-3">
<template x-for="(item, idx) in activeItems" :key="idx"> <template x-for="(item, idx) in activeItems" :key="idx">
<div class="group flex items-center gap-4 p-4 bg-white dark:bg-slate-900 rounded-2xl border border-slate-100 dark:border-slate-800 hover:border-cyan-500/30 transition-all"> <div class="group flex items-center gap-4 p-4 bg-white dark:bg-slate-900 rounded-2xl border border-slate-100 dark:border-slate-800 hover:border-cyan-500/30 transition-all" x-data="{ imgFailed: !item.image_url }">
<div class="w-10 h-10 rounded-lg bg-slate-100 dark:bg-slate-800 flex items-center justify-center font-mono font-bold text-slate-500 border border-slate-200 dark:border-slate-700" x-text="item.slot_no"></div> <div class="w-10 h-10 rounded-lg bg-slate-100 dark:bg-slate-800 flex items-center justify-center font-mono font-bold text-slate-500 border border-slate-200 dark:border-slate-700" x-text="item.slot_no"></div>
<div class="w-12 h-12 rounded-xl bg-slate-50 dark:bg-slate-800 flex items-center justify-center overflow-hidden border border-slate-100 dark:border-slate-800 group-hover:scale-105 transition-transform"> <div class="w-12 h-12 rounded-xl bg-slate-50 dark:bg-slate-800 flex items-center justify-center overflow-hidden border border-slate-100 dark:border-slate-800 group-hover:scale-105 transition-transform relative">
<template x-if="item.image_url"><img :src="item.image_url" class="w-full h-full object-cover"></template> <template x-if="item.image_url">
<template x-if="!item.image_url"><svg class="w-6 h-6 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg></template> <img :src="item.image_url" class="w-full h-full object-cover" x-show="!imgFailed" x-on:error="imgFailed = true">
</template>
<div x-show="imgFailed" class="absolute inset-0 flex items-center justify-center bg-slate-50 dark:bg-slate-800/50">
<svg class="w-6 h-6 text-slate-300 dark:text-slate-600 group-hover:text-cyan-500 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
</svg>
</div>
</div> </div>
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<p class="text-sm font-black text-slate-800 dark:text-slate-100 truncate tracking-tight" x-text="item.product_name"></p> <p class="text-sm font-black text-slate-800 dark:text-slate-100 truncate tracking-tight" x-text="item.product_name"></p>

View File

@ -801,18 +801,17 @@
<div class="space-y-3"> <div class="space-y-3">
<template x-for="(item, idx) in activeItems" :key="idx"> <template x-for="(item, idx) in activeItems" :key="idx">
<div class="luxury-card p-4 rounded-2xl flex items-center gap-4 group/item transition-all hover:border-cyan-500/30"> <div class="luxury-card p-4 rounded-2xl flex items-center gap-4 group/item transition-all hover:border-cyan-500/30"
<div class="w-16 h-16 rounded-xl bg-slate-100 dark:bg-slate-800 flex-shrink-0 overflow-hidden border border-slate-200 dark:border-slate-700 group-hover:scale-105 transition-transform"> x-data="{ imgFailed: !item.image_url }">
<div class="w-16 h-16 rounded-xl bg-slate-100 dark:bg-slate-800 flex-shrink-0 overflow-hidden border border-slate-200 dark:border-slate-700 group-hover:scale-105 transition-transform flex items-center justify-center relative">
<template x-if="item.image_url"> <template x-if="item.image_url">
<img :src="item.image_url" class="w-full h-full object-cover"> <img :src="item.image_url" class="w-full h-full object-cover" x-show="!imgFailed" x-on:error="imgFailed = true">
</template>
<template x-if="!item.image_url">
<div class="w-full h-full flex items-center justify-center text-slate-400">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
</template> </template>
<div x-show="imgFailed" class="absolute inset-0 flex items-center justify-center bg-slate-50 dark:bg-slate-800/50">
<svg class="w-8 h-8 text-slate-300 dark:text-slate-600 group-hover/item:text-cyan-500 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
</svg>
</div>
</div> </div>
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<p class="text-base font-extrabold text-slate-800 dark:text-white truncate" x-text="item.product_name"></p> <p class="text-base font-extrabold text-slate-800 dark:text-white truncate" x-text="item.product_name"></p>