|
<!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>
|
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
|
<style>
|
|
.btn-blue { @apply bg-[#1e56a0] hover:bg-[#163e75] text-white px-4 py-1.5 rounded-lg text-sm font-medium flex items-center gap-1 transition shadow-sm; }
|
|
.btn-gray { @apply bg-[#6c757d] hover:bg-[#5a6268] text-white px-4 py-1.5 rounded-lg text-sm font-medium transition shadow-sm; }
|
|
.btn-orange { @apply bg-[#e65c00] hover:bg-[#cc5200] text-white px-3 py-1 rounded-md text-xs font-medium flex items-center gap-1 transition; }
|
|
|
|
input::-webkit-outer-spin-button,
|
|
input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
|
|
input[type=number] { -moz-appearance: textfield; }
|
|
</style>
|
|
</head>
|
|
<body class="bg-[#f4f6f9] min-h-screen p-6">
|
|
|
|
<div id="app" class="max-w-[1600px] mx-auto space-y-4">
|
|
|
|
<h1 class="text-2xl font-bold text-gray-800 tracking-wide px-2">工單管理系統</h1>
|
|
|
|
<main class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-6">
|
|
|
|
<div class="flex justify-between items-center">
|
|
<div>
|
|
<h2 class="text-lg font-bold text-gray-800">工單拆分清單</h2>
|
|
<p class="text-xs text-amber-600 font-medium mt-0.5 flex items-center gap-0.5">
|
|
<span class="material-icons text-sm">info</span> 提示:系統僅顯示未結案工單。且限制僅有【待生產】狀態且數量大於 1 的工單允許執行拆單。
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-2 max-w-4xl">
|
|
<div class="relative flex-1">
|
|
<span class="material-icons absolute left-3 top-3 text-gray-400 text-lg">search</span>
|
|
<input
|
|
type="text"
|
|
placeholder="搜尋工單編號..."
|
|
class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-blue-500 text-sm"
|
|
>
|
|
</div>
|
|
<button class="btn-blue">搜尋</button>
|
|
<button class="btn-gray">清空</button>
|
|
</div>
|
|
|
|
<div class="overflow-x-auto border border-gray-200 rounded-lg">
|
|
<table class="w-full text-left border-collapse">
|
|
<thead>
|
|
<tr class="bg-[#f8f9fa] text-gray-600 text-sm font-medium border-b border-gray-200">
|
|
<th class="p-3.5 pl-6 w-[120px]">狀態</th>
|
|
<th class="p-3.5 w-[140px]">工單類型</th>
|
|
<th class="p-3.5 w-[160px]">工單編號</th>
|
|
<th class="p-3.5">產品名稱</th>
|
|
<th class="p-3.5 text-center w-[150px]">原計畫數量</th>
|
|
<th class="p-3.5 text-center w-[120px]">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-100 text-gray-700 text-sm">
|
|
<tr v-for="order in sortedOrders" :key="order.orderNo" class="hover:bg-gray-50/80 transition">
|
|
<td class="p-3.5 pl-6">
|
|
<span v-if="order.status === '待生產'" class="bg-amber-100 text-amber-800 text-xs px-2.5 py-0.5 rounded-full font-semibold">待生產</span>
|
|
<span v-else-if="order.status === '生產中'" class="bg-green-100 text-green-800 text-xs px-2.5 py-0.5 rounded-full font-medium">生產中</span>
|
|
</td>
|
|
<td class="p-3.5 text-gray-500">
|
|
<span v-if="order.orderNo.includes('-S')" class="text-blue-600 font-medium">衍生子單</span>
|
|
<span v-else class="text-gray-600">原始母單</span>
|
|
</td>
|
|
<td class="p-3.5 font-mono font-bold text-gray-900">
|
|
<span :class="order.orderNo.includes('-S') ? 'text-blue-600 pl-4 border-l-2 border-blue-300 italic' : ''">
|
|
{{ order.orderNo }}
|
|
</span>
|
|
</td>
|
|
<td class="p-3.5 text-gray-600">{{ order.product }}</td>
|
|
<td class="p-3.5 text-center font-mono font-semibold">{{ order.planQty }} PCS</td>
|
|
<td class="p-3.5">
|
|
<div class="flex items-center justify-center">
|
|
<button
|
|
@click="openSplitModal(order)"
|
|
:disabled="order.status !== '待生產' || order.planQty <= 1"
|
|
:class="[
|
|
'btn-orange',
|
|
(order.status !== '待生產' || order.planQty <= 1) ? 'opacity-25 cursor-not-allowed bg-gray-400 hover:bg-gray-400' : ''
|
|
]"
|
|
:title="order.status !== '待生產' ? '生產中狀態,無法拆單' : (order.planQty <= 1 ? '數量不足,無法拆單' : '')"
|
|
>
|
|
<span class="material-icons text-xs">call_split</span> 拆單
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="sortedOrders.length === 0">
|
|
<td colspan="6" class="p-8 text-center text-gray-400 bg-gray-50/50">目前沒有可呈現的工單資料</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="flex justify-between items-center text-xs text-gray-500 pt-2">
|
|
<div>顯示數量: 共 {{ sortedOrders.length }} 筆 (已隱藏已完工工單)</div>
|
|
<div class="flex items-center gap-1">
|
|
<button class="px-2 py-1 bg-gray-100 border rounded hover:bg-gray-200 disabled:opacity-50" disabled>上一頁</button>
|
|
<button class="px-2.5 py-1 bg-[#1e56a0] text-white border rounded font-bold">1</button>
|
|
<button class="px-2 py-1 bg-gray-100 border rounded hover:bg-gray-200 disabled:opacity-50" disabled>下一頁</button>
|
|
</div>
|
|
</div>
|
|
|
|
</main>
|
|
|
|
<div v-if="showModal" class="fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center z-50 transition-opacity">
|
|
<div class="bg-white rounded-xl shadow-xl border border-gray-200 w-full max-w-md overflow-hidden">
|
|
|
|
<div class="bg-[#1e56a0] p-4 text-white flex justify-between items-center">
|
|
<h3 class="font-bold flex items-center gap-1.5 text-base">
|
|
<span class="material-icons text-lg">call_split</span> 工單數量拆分
|
|
</h3>
|
|
<button @click="closeModal" class="hover:text-gray-200 flex items-center">
|
|
<span class="material-icons">close</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="p-6 space-y-4">
|
|
<div class="bg-gray-50 p-3 rounded-lg border text-xs text-gray-600 space-y-1">
|
|
<div><span class="font-medium text-gray-400">當前欲拆工單:</span><span class="font-mono font-bold text-gray-800">{{ activeOrder.orderNo }}</span></div>
|
|
<div class="line-clamp-1"><span class="font-medium text-gray-400">產品名稱:</span>{{ activeOrder.product }}</div>
|
|
</div>
|
|
|
|
<div class="space-y-3">
|
|
<div class="grid grid-cols-2 gap-3 text-center">
|
|
<div class="bg-gray-50 p-2.5 border rounded-lg">
|
|
<span class="block text-[11px] text-gray-400 font-medium">此單保留數 (A)</span>
|
|
<span class="text-lg font-mono font-bold text-teal-700">{{ remainingParentQty }} PCS</span>
|
|
</div>
|
|
<div class="bg-blue-50/50 p-2.5 border border-blue-200 rounded-lg">
|
|
<span class="block text-[11px] text-blue-600 font-medium">新單分出數 (B)</span>
|
|
<span class="text-lg font-mono font-bold text-blue-700">{{ splitQty || 0 }} PCS</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-bold text-gray-700 mb-1">請輸入拆出數量:</label>
|
|
<div class="flex items-center border border-gray-300 rounded-lg overflow-hidden bg-white shadow-sm focus-within:border-blue-500 focus-within:ring-1 focus-within:ring-blue-500">
|
|
<button @click="adjustQty(-1)" class="bg-gray-50 hover:bg-gray-100 px-3 py-2 text-gray-600 font-bold border-r">-</button>
|
|
<input
|
|
type="number"
|
|
v-model.number="splitQty"
|
|
@input="handleSplitQtyChange"
|
|
class="w-full text-center font-mono font-bold text-blue-700 text-base focus:outline-none p-2 bg-yellow-50/40"
|
|
min="1"
|
|
>
|
|
<button @click="adjustQty(1)" class="bg-gray-50 hover:bg-gray-100 px-3 py-2 text-gray-600 font-bold border-l">+</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="text-[11px] text-amber-600 bg-amber-50 p-2 rounded border border-amber-200 font-medium flex items-center gap-1">
|
|
<span class="material-icons text-sm">info</span>
|
|
<span>拆出後將自動生成新【待生產】工單:<strong class="font-mono">{{ nextSubOrderNo }}</strong></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-gray-50 px-6 py-4 border-t flex justify-end gap-2">
|
|
<button @click="closeModal" class="px-4 py-2 border rounded-lg text-sm font-medium text-gray-600 bg-white hover:bg-gray-50">取消</button>
|
|
<button @click="submitSplit" class="px-4 py-2 bg-[#1e56a0] hover:bg-[#163e75] text-white rounded-lg text-sm font-medium shadow-sm">確認拆單</button>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
const { createApp } = Vue;
|
|
createApp({
|
|
data() {
|
|
return {
|
|
showModal: false,
|
|
activeOrder: null,
|
|
splitQty: 0,
|
|
|
|
orders: [
|
|
{ orderNo: 'A001', product: '蔡司邁康Classic角膜塑形用硬性透氣接觸鏡-環曲-紫', planQty: 100, status: '待生產' },
|
|
{ orderNo: 'A001-S1', product: '蔡司邁康Classic角膜塑形用硬性透氣接觸鏡-環曲-紫', planQty: 40, status: '待生產' },
|
|
{ orderNo: 'A002', product: '蔡司邁康Classic角膜塑形用硬性透氣接觸鏡-環曲-藍', planQty: 200, status: '生產中' },
|
|
{ orderNo: 'A003', product: '蔡司邁康Classic角膜塑形用硬性透氣接觸鏡-環曲-綠', planQty: 150, status: '已完工' } // ✨ 這張在畫面上將會消失
|
|
]
|
|
};
|
|
},
|
|
computed: {
|
|
// ✨ 調整排序與過帳邏輯:先用 filter 把狀態是 '已完工' 的工單過濾掉,再進行編號排序
|
|
sortedOrders() {
|
|
return this.orders
|
|
.filter(o => o.status !== '已完工')
|
|
.sort((a, b) => {
|
|
return a.orderNo.localeCompare(b.orderNo, undefined, { numeric: true, sensitivity: 'base' });
|
|
});
|
|
},
|
|
remainingParentQty() {
|
|
if (!this.activeOrder) return 0;
|
|
return Math.max(0, this.activeOrder.planQty - (this.splitQty || 0));
|
|
},
|
|
nextSubOrderNo() {
|
|
if (!this.activeOrder) return '';
|
|
|
|
const currentNo = this.activeOrder.orderNo;
|
|
const baseOrderNo = currentNo.split('-S')[0];
|
|
const subOrders = this.orders.filter(o => o.orderNo.startsWith(baseOrderNo + '-S'));
|
|
|
|
let maxIndex = 0;
|
|
subOrders.forEach(o => {
|
|
const parts = o.orderNo.split('-S');
|
|
if (parts.length > 1) {
|
|
const idx = parseInt(parts[parts.length - 1], 10);
|
|
if (!isNaN(idx) && idx > maxIndex) {
|
|
maxIndex = idx;
|
|
}
|
|
}
|
|
});
|
|
|
|
return `${baseOrderNo}-S${maxIndex + 1}`;
|
|
}
|
|
},
|
|
methods: {
|
|
openSplitModal(order) {
|
|
if (order.status !== '待生產') return;
|
|
|
|
this.activeOrder = order;
|
|
this.splitQty = Math.floor(order.planQty / 2);
|
|
this.showModal = true;
|
|
},
|
|
closeModal() {
|
|
this.showModal = false;
|
|
this.activeOrder = null;
|
|
this.splitQty = 0;
|
|
},
|
|
adjustQty(amount) {
|
|
this.splitQty = (this.splitQty || 0) + amount;
|
|
this.handleSplitQtyChange();
|
|
},
|
|
handleSplitQtyChange() {
|
|
if (!this.activeOrder) return;
|
|
if (this.splitQty < 1 || !this.splitQty) this.splitQty = 1;
|
|
if (this.splitQty >= this.activeOrder.planQty) {
|
|
alert(`⚠️ 拆出數量不能大於或等於當前工單總數!`);
|
|
this.splitQty = this.activeOrder.planQty - 1;
|
|
}
|
|
},
|
|
submitSplit() {
|
|
const currentNo = this.activeOrder.orderNo;
|
|
const newSubNo = this.nextSubOrderNo;
|
|
|
|
if (confirm(`確定執行工單拆分?\n\n當前工單 [${currentNo}] 數量將修改為:${this.remainingParentQty} PCS\n衍生新工單 [${newSubNo}] 建立數量為:${this.splitQty} PCS`)) {
|
|
|
|
this.activeOrder.planQty = this.remainingParentQty;
|
|
|
|
this.orders.push({
|
|
orderNo: newSubNo,
|
|
product: this.activeOrder.product,
|
|
planQty: this.splitQty,
|
|
status: '待生產'
|
|
});
|
|
|
|
alert(`🚀 拆單成功!\n- 工單 ${currentNo} 更新後為:${this.remainingParentQty} PCS\n- 新子工單 ${newSubNo} (${this.splitQty} PCS) 已自動歸類。`);
|
|
|
|
this.closeModal();
|
|
}
|
|
}
|
|
}
|
|
}).mount('#app');
|
|
</script>
|
|
</body>
|
|
</html>
|