|
<!DOCTYPE html>
|
|
<html lang="zh-TW">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>庫存查詢中心</title>
|
|
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<style>
|
|
/* 自定義聚焦樣式 */
|
|
.focus-ring {
|
|
transition: all 0.2s;
|
|
}
|
|
.focus-ring:focus {
|
|
border-color: #3b82f6; /* blue-500 */
|
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5);
|
|
outline: none;
|
|
}
|
|
/* 篩選按鈕樣式 */
|
|
.filter-btn {
|
|
transition: background-color 0.2s, color 0.2s, transform 0.1s;
|
|
white-space: nowrap;
|
|
font-weight: 600;
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 9999px; /* rounded-full */
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-gray-50 min-h-screen p-8 md:p-12">
|
|
|
|
<div id="app" class="max-w-7xl mx-auto">
|
|
<header class="mb-8 border-b-2 border-orange-100 pb-4 flex justify-between items-center">
|
|
<h1 class="text-4xl font-extrabold text-orange-700">🔍 庫存查詢</h1>
|
|
<span class="text-gray-500 text-lg">即時物料庫存總覽與追溯</span>
|
|
</header>
|
|
|
|
<div class="bg-white p-6 md:p-8 rounded-3xl shadow-2xl mb-10 border border-gray-100">
|
|
|
|
<div class="mb-8 pb-6 border-b border-gray-200">
|
|
<h2 class="text-2xl font-bold text-gray-700 mb-4">按類別快速篩選</h2>
|
|
|
|
<div class="flex flex-wrap gap-3">
|
|
<button
|
|
@click="currentCategoryFilter = 'All'"
|
|
:class="currentCategoryFilter === 'All' ? 'bg-blue-600 text-white shadow-lg' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'"
|
|
class="filter-btn text-sm transition duration-150">
|
|
全部 ({{ inventoryData.length }})
|
|
</button>
|
|
|
|
<button
|
|
v-for="cat in categories"
|
|
:key="cat"
|
|
@click="currentCategoryFilter = cat"
|
|
:class="currentCategoryFilter === cat ? 'bg-green-600 text-white shadow-lg' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'"
|
|
class="filter-btn text-sm transition duration-150">
|
|
{{ cat }} ({{ getCategoryCount(cat) }})
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<h2 class="text-2xl font-bold text-orange-600 mb-6 border-b border-orange-200 pb-3">其他篩選條件</h2>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-5 gap-6 mb-8">
|
|
<div class="space-y-1">
|
|
<label class="block font-medium text-gray-700">倉別 (Warehouse)</label>
|
|
<select v-model="filters.warehouseType" class="w-full p-3 border border-gray-300 rounded-xl focus-ring bg-white">
|
|
<option value="All">所有倉別</option>
|
|
<option v-for="wType in warehouseTypes" :key="wType" :value="wType">{{ wType }}</option>
|
|
</select>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<label class="block font-medium text-gray-700">儲位</label>
|
|
<input v-model="filters.location" class="w-full p-3 border border-gray-300 rounded-xl focus-ring" placeholder="A01-01-01">
|
|
</div>
|
|
<div class="space-y-1">
|
|
<label class="block font-medium text-gray-700">料號 (輸入以看總量)</label>
|
|
<input v-model="filters.partNo" class="w-full p-3 border border-gray-300 rounded-xl focus-ring" :class="{'border-blue-300 ring-2 ring-blue-100': filters.partNo.trim()}" placeholder="RM-STEEL-001">
|
|
</div>
|
|
<div class="space-y-1">
|
|
<label class="block font-medium text-gray-700">批號</label>
|
|
<input v-model="filters.lotNo" class="w-full p-3 border border-gray-300 rounded-xl focus-ring" placeholder="20251023A">
|
|
</div>
|
|
<div class="space-y-1">
|
|
<label class="block font-medium text-gray-700">庫存數量狀態</label>
|
|
<select v-model="filters.status" class="w-full p-3 border border-gray-300 rounded-xl focus-ring bg-white">
|
|
<option value="All">所有庫存</option>
|
|
<option value="Positive">只顯示 > 0 的庫存</option>
|
|
<option value="Zero">只顯示 = 0 的庫存</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 常駐顯示:庫存動態統計面板 -->
|
|
<div class="mb-6 p-4 rounded-2xl border transition-all duration-300 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4"
|
|
:class="filters.partNo.trim() ? 'bg-blue-50 border-blue-200' : 'bg-gray-50 border-gray-200'">
|
|
<div>
|
|
<span class="text-xs font-semibold uppercase tracking-wider block transition-colors duration-300"
|
|
:class="filters.partNo.trim() ? 'text-blue-600' : 'text-gray-500'">
|
|
{{ filters.partNo.trim() ? '💡 料號加總統計中' : '📊 當前畫面庫存摘要' }}
|
|
</span>
|
|
<span class="text-lg font-bold text-gray-800">
|
|
{{ filters.partNo.trim() ? `「${filters.partNo.trim()}」相關批次` : '篩選明細加總' }}
|
|
</span>
|
|
</div>
|
|
<div class="flex items-center gap-6">
|
|
<div class="text-right">
|
|
<span class="text-xs text-gray-400 block">總計項目/批次</span>
|
|
<span class="text-xl font-bold transition-colors duration-300"
|
|
:class="filters.partNo.trim() ? 'text-blue-700' : 'text-gray-700'">
|
|
{{ filteredInventory.length }} 筆
|
|
</span>
|
|
</div>
|
|
<div class="border-l h-10" :class="filters.partNo.trim() ? 'border-blue-200' : 'border-gray-200'"></div>
|
|
<div class="text-right">
|
|
<span class="text-xs text-gray-400 block">加總庫存總量</span>
|
|
<span class="text-3xl font-extrabold transition-colors duration-300"
|
|
:class="filters.partNo.trim() ? 'text-blue-600' : 'text-gray-700'">
|
|
{{ totalStockSum.toLocaleString() }}
|
|
<span v-if="stockUnit" class="text-sm font-normal text-gray-500 ml-1">{{ stockUnit }}</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h2 class="text-2xl font-bold text-orange-600 mb-6 border-b border-orange-200 pb-3">庫存總覽 (共 {{ filteredInventory.length }} 筆資料)</h2>
|
|
|
|
<div class="overflow-x-auto shadow-lg rounded-2xl border border-gray-200">
|
|
<table class="min-w-full table-auto text-sm text-left">
|
|
<thead class="bg-orange-50 text-orange-800 uppercase font-semibold">
|
|
<tr>
|
|
<th class="px-4 py-3 border-b-2 border-orange-200 w-1/12">#</th>
|
|
<th class="px-4 py-3 border-b-2 border-orange-200 w-2/12">料號 / 品名</th>
|
|
<th class="px-4 py-3 border-b-2 border-orange-200 w-1/12">類別</th>
|
|
<th class="px-4 py-3 border-b-2 border-orange-200 w-1/12">倉別</th> <th class="px-4 py-3 border-b-2 border-orange-200 w-1/12">批號 (Lot No.)</th>
|
|
<th class="px-4 py-3 border-b-2 border-orange-200 w-2/12">儲位 (Location)</th>
|
|
<th class="px-4 py-3 border-b-2 border-orange-200 w-2/12 text-right">現有庫存 (Stock)</th>
|
|
<th class="px-4 py-3 border-b-2 border-orange-200 w-1/12">單位</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-100">
|
|
<tr v-for="(item, index) in filteredInventory" :key="item.id" class="bg-white hover:bg-orange-50 transition duration-100"
|
|
:class="{'bg-red-50 text-gray-500': item.stockQty === 0}">
|
|
<td class="px-4 py-3">{{ index + 1 }}</td>
|
|
<td class="px-4 py-3">
|
|
<span class="font-semibold text-gray-900">{{ item.partNo }}</span>
|
|
<br>
|
|
<span class="text-xs text-gray-500">{{ item.partName }}</span>
|
|
</td>
|
|
<td class="px-4 py-3 font-medium text-green-600">{{ item.category }}</td>
|
|
<td class="px-4 py-3">
|
|
<span :class="{'text-blue-600': item.warehouseType === '良品倉', 'text-yellow-600': item.warehouseType === '線邊倉', 'text-red-600': item.warehouseType === '不良品區'}" class="font-semibold">
|
|
{{ item.warehouseType }}
|
|
</span>
|
|
</td> <td class="px-4 py-3 text-gray-600 font-mono">{{ item.lotNo || 'N/A' }}</td>
|
|
<td class="px-4 py-3 font-mono text-blue-600 font-medium">{{ item.location }}</td>
|
|
<td class="px-4 py-3 text-right">
|
|
<span :class="{'text-red-600': item.stockQty === 0, 'text-green-600': item.stockQty > 0}" class="font-mono text-xl font-bold">
|
|
{{ item.stockQty.toLocaleString() }}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3 text-gray-500">{{ item.unit }}</td>
|
|
</tr>
|
|
<tr v-if="inventoryData.length === 0">
|
|
<td colspan="8" class="px-4 py-8 text-center text-lg text-gray-500 bg-gray-50">
|
|
系統中目前沒有庫存資料。
|
|
</td>
|
|
</tr>
|
|
<tr v-else-if="filteredInventory.length === 0">
|
|
<td colspan="8" class="px-4 py-8 text-center text-lg text-red-500 bg-red-50">
|
|
🚨 根據篩選條件,沒有找到符合的庫存資料。
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const { createApp, ref, computed } = Vue;
|
|
|
|
createApp({
|
|
setup() {
|
|
// 類別清單
|
|
const categories = ref(['原料', '半成品', '成品', '包材', '耗材']);
|
|
// 倉別清單
|
|
const warehouseTypes = ref(['良品倉', '線邊倉', '不良品區', '待檢區']);
|
|
|
|
// 控制快速篩選按鈕
|
|
const currentCategoryFilter = ref('All');
|
|
|
|
// 假庫存數據 - 新增 warehouseType 欄位
|
|
const inventoryData = ref([
|
|
{ id: 101, partNo: 'RM-STEEL-001', partName: '中碳鋼板 10mm', unit: 'KG', lotNo: '20251023A', location: 'A01-01-01', stockQty: 500, category: '原料', warehouseType: '良品倉' },
|
|
{ id: 1012, partNo: 'RM-STEEL-001', partName: '中碳鋼板 10mm', unit: 'KG', lotNo: '20251024B', location: 'A01-01-02', stockQty: 350, category: '原料', warehouseType: '良品倉' },
|
|
{ id: 102, partNo: 'RM-OIL-003', partName: '高純度潤滑油', unit: 'L', lotNo: 'O20251101', location: 'L-01-01', stockQty: 75, category: '原料', warehouseType: '線邊倉' },
|
|
{ id: 201, partNo: 'SP-PCB-012', partName: '核心電路板', unit: 'PCS', lotNo: '20251023B', location: 'B01-01-05', stockQty: 1500, category: '半成品', warehouseType: '良品倉' },
|
|
{ id: 301, partNo: 'FG-DEVICE-005', partName: '智能感應器 V2.0', unit: 'PCS', lotNo: 'L20251023A', location: 'FG-A01-01', stockQty: 100, category: '成品', warehouseType: '良品倉' },
|
|
{ id: 401, partNo: 'FG-WIFI-010', partName: '工業級無線模組', unit: 'PCS', lotNo: '', location: 'FG-A01-03', stockQty: 0, category: '成品', warehouseType: '不良品區' },
|
|
{ id: 501, partNo: 'PKG-BOX-L01', partName: '大號紙箱', unit: 'PCS', lotNo: 'PKG20251101', location: 'C03-01-01', stockQty: 2500, category: '包材', warehouseType: '良品倉' },
|
|
{ id: 601, partNo: 'CSM-GLOVE-001', partName: '一次性手套', unit: 'PCS', lotNo: 'CSM20251101', location: 'D01-01-01', stockQty: 1000, category: '耗材', warehouseType: '待檢區' },
|
|
]);
|
|
|
|
// 篩選條件模型 (新增 warehouseType)
|
|
const filters = ref({
|
|
location: '',
|
|
partNo: '',
|
|
lotNo: '',
|
|
status: 'Positive',
|
|
warehouseType: 'All' // 新增倉別篩選
|
|
});
|
|
|
|
// 方法:計算每個類別的庫存項數量 (用於按鈕計數)
|
|
const getCategoryCount = (category) => {
|
|
return inventoryData.value.filter(item => item.category === category).length;
|
|
};
|
|
|
|
// 計算屬性:篩選後的庫存列表
|
|
const filteredInventory = computed(() => {
|
|
let list = inventoryData.value;
|
|
const f = filters.value;
|
|
|
|
// 0. 類別快速篩選 (優先執行)
|
|
if (currentCategoryFilter.value !== 'All') {
|
|
list = list.filter(item => item.category === currentCategoryFilter.value);
|
|
}
|
|
|
|
// 1. 倉別篩選 (新增)
|
|
if (f.warehouseType !== 'All') {
|
|
list = list.filter(item => item.warehouseType === f.warehouseType);
|
|
}
|
|
|
|
// 2. 文字模糊篩選
|
|
const locationFilter = f.location.toUpperCase().trim();
|
|
const partNoFilter = f.partNo.toUpperCase().trim();
|
|
const lotNoFilter = f.lotNo.toUpperCase().trim();
|
|
|
|
if (locationFilter) {
|
|
list = list.filter(item => item.location && item.location.toUpperCase().includes(locationFilter));
|
|
}
|
|
if (partNoFilter) {
|
|
list = list.filter(item => item.partNo && item.partNo.toUpperCase().includes(partNoFilter));
|
|
}
|
|
if (lotNoFilter) {
|
|
list = list.filter(item => item.lotNo && item.lotNo.toUpperCase().includes(lotNoFilter));
|
|
}
|
|
|
|
// 3. 數量狀態篩選
|
|
if (f.status === 'Positive') {
|
|
list = list.filter(item => item.stockQty > 0);
|
|
} else if (f.status === 'Zero') {
|
|
list = list.filter(item => item.stockQty === 0);
|
|
}
|
|
|
|
return list;
|
|
});
|
|
|
|
// 計算屬性:計算搜尋特定料號或目前畫面的加總數量
|
|
const totalStockSum = computed(() => {
|
|
return filteredInventory.value.reduce((sum, item) => sum + item.stockQty, 0);
|
|
});
|
|
|
|
// 計算屬性:動態取得搜尋結果的單位 (如果混合了不同單位則不顯示,避免加總單位錯誤;單一物料搜尋時才會顯示正確單位)
|
|
const stockUnit = computed(() => {
|
|
if (filteredInventory.value.length === 0) return '';
|
|
const firstUnit = filteredInventory.value[0].unit;
|
|
const isSingleUnit = filteredInventory.value.every(item => item.unit === firstUnit);
|
|
return isSingleUnit ? firstUnit : ''; // 只有當所有顯示項目單位相同(如搜單一料號時)才秀單位
|
|
});
|
|
|
|
return {
|
|
filters,
|
|
inventoryData,
|
|
filteredInventory,
|
|
categories,
|
|
warehouseTypes,
|
|
currentCategoryFilter,
|
|
getCategoryCount,
|
|
totalStockSum,
|
|
stockUnit
|
|
};
|
|
}
|
|
}).mount('#app');
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|