Project

General

Profile

Feature #106 » 工單拆單-0708.html

和絹 傅, 07/13/2026 02:09 AM

 
<!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>
    (1-1/1)