Project

General

Profile

Feature #197 » 開立檢驗單_0908.html

鎮源 游, 09/08/2026 01:57 AM

 
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
outline: none;
}
.required-label::after {
content: ' *';
color: #ef4444;
margin-left: 2px;
}
.disabled-toggle {
opacity: 0.6;
cursor: not-allowed;
}
/* 唯讀表單欄位樣式 */
.readonly-field {
background-color: #f8fafc;
color: #475569;
cursor: not-allowed;
border-color: #e2e8f0;
}
</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 頁面標題區 -->
<header class="mb-8 border-b-2 border-indigo-100 pb-4 flex justify-between items-center">
<h1 class="text-4xl font-extrabold text-indigo-800">📝 檢驗單開立與紀錄管理</h1>
<button @click="openTicketDialog('add')" class="px-6 py-2.5 bg-indigo-600 text-white font-semibold rounded-full shadow-md hover:bg-indigo-700 transition duration-150 transform hover:scale-[1.02] flex items-center space-x-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 fill-white" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd" /></svg>
<span>開立檢驗單</span>
</button>
</header>

<!-- 檢驗單列表 Table -->
<div class="bg-white p-6 rounded-3xl shadow-2xl border border-gray-100">
<div class="overflow-x-auto shadow-sm rounded-2xl border border-gray-200">
<table class="min-w-full table-auto text-sm text-left">
<thead class="bg-indigo-50 text-indigo-800 uppercase font-semibold">
<tr>
<th class="px-4 py-3 border-b-2 border-indigo-200 w-20 text-center">操作</th>
<th class="px-4 py-3 border-b-2 border-indigo-200">檢驗單號</th>
<th class="px-4 py-3 border-b-2 border-indigo-200">檢驗類型</th>
<th class="px-4 py-3 border-b-2 border-indigo-200">來源單號</th>
<th class="px-4 py-3 border-b-2 border-indigo-200">產品名稱</th>
<th class="px-4 py-3 border-b-2 border-indigo-200">批號</th>
<th class="px-4 py-3 border-b-2 border-indigo-200 text-center">抽檢數量</th>
<th class="px-4 py-3 border-b-2 border-indigo-200">檢驗時間</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<tr v-for="ticket in ticketRecords" :key="ticket.id" class="bg-white hover:bg-indigo-50 transition duration-100">
<td class="px-4 py-3 text-center space-x-2">
<button @click="openTicketDialog('view', ticket)" class="text-blue-500 hover:text-blue-700" title="查看">
🔍
</button>
</td>
<td class="px-4 py-3 font-mono font-medium text-indigo-700">{{ ticket.ticketNo }}</td>
<td class="px-4 py-3 font-medium text-teal-600">{{ ticket.ticketType }}</td>
<td class="px-4 py-3 font-mono text-gray-700">{{ ticket.refNo }}</td>
<td class="px-4 py-3 text-gray-800 font-medium">{{ ticket.productName }}</td>
<td class="px-4 py-3 font-mono text-gray-600">{{ ticket.batchNo }}</td>
<td class="px-4 py-3 text-center font-mono font-semibold">{{ ticket.sampleQty }}</td>
<td class="px-4 py-3 text-gray-500 text-xs">{{ ticket.inspectTime }}</td>
</tr>
</tbody>
</table>
</div>
</div>

<!-- Dialog 彈窗:新增 / 查看檢驗單 -->
<div v-if="ticketDialog.isOpen" class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-70 z-40 p-4">
<div class="bg-white p-8 rounded-3xl shadow-2xl w-full max-w-6xl border border-gray-200 transform scale-100 transition-all duration-300 max-h-[92vh] overflow-y-auto">
<h2 class="text-2xl font-bold text-indigo-800 mb-6 border-b border-indigo-200 pb-3 flex justify-between items-center">
<span>{{ ticketDialog.title }}</span>
<span v-if="ticketDialog.data.ticketNo" class="text-sm font-mono text-indigo-500 font-normal">單號:{{ ticketDialog.data.ticketNo }}</span>
</h2>

<!-- 第一區塊:單據與類型 -->
<div class="mb-6 grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- 檢驗類型 -->
<div class="space-y-1">
<label class="block text-sm font-bold required-label text-gray-700">檢驗類型:</label>
<select v-model="ticketDialog.data.ticketType"
:disabled="ticketDialog.mode === 'view'"
class="w-full p-2.5 border border-gray-300 rounded-xl focus-ring bg-white text-sm"
:class="{'bg-gray-100 disabled-toggle': ticketDialog.mode === 'view'}"
>
<option value="" disabled>請選擇檢驗類型</option>
<option value="IQC">IQC (進料檢驗)</option>
<option value="FQC">FQC (成品檢驗)</option>
<option value="OQC">OQC (出貨檢驗)</option>
</select>
</div>

<!-- 來源單號 -->
<div class="space-y-1">
<label class="block text-sm font-bold required-label text-gray-700">綁定來源單號:</label>
<input v-model="ticketDialog.data.refNo"
:readonly="ticketDialog.mode === 'view'"
class="w-full p-2.5 border border-gray-300 rounded-xl focus-ring font-mono text-sm"
:class="{'bg-gray-100': ticketDialog.mode === 'view'}"
placeholder="輸入單號 (如 PO / WO / DO)"
>
</div>

<!-- 套用檢驗表單範本 -->
<div class="space-y-1">
<label class="block text-sm font-bold required-label text-gray-700">套用檢驗表單範本:</label>
<select v-model="ticketDialog.data.formNo"
@change="onFormTemplateChange"
:disabled="ticketDialog.mode === 'view'"
class="w-full p-2.5 border border-gray-300 rounded-xl focus-ring bg-white text-sm"
:class="{'bg-gray-100 disabled-toggle': ticketDialog.mode === 'view'}"
>
<option value="" disabled>請選擇標準範本</option>
<option v-for="form in formTemplates" :key="form.id" :value="form.formNo">
{{ form.formNo }} - {{ form.formName }}
</option>
</select>
</div>
</div>

<!-- 第二區塊:產品選擇(下拉選單)與 手KEY批號、檢驗數量 -->
<div class="mb-8 p-5 bg-indigo-50/50 border border-indigo-100 rounded-2xl grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- 產品選單 -->
<div class="space-y-1">
<label class="block text-sm font-bold required-label text-indigo-900">選擇產品:</label>
<select v-model="ticketDialog.data.productId"
@change="onProductChange"
:disabled="ticketDialog.mode === 'view'"
class="w-full p-2.5 border border-gray-300 rounded-xl focus-ring bg-white text-sm font-medium"
:class="{'bg-gray-100 disabled-toggle': ticketDialog.mode === 'view'}"
>
<option value="" disabled>請選擇產品</option>
<option v-for="prod in productList" :key="prod.id" :value="prod.id">
{{ prod.productNo }} - {{ prod.productName }}
</option>
</select>
</div>

<!-- 手KEY 批號 -->
<div class="space-y-1">
<label class="block text-sm font-bold required-label text-indigo-900">批號:</label>
<input v-model="ticketDialog.data.batchNo"
:readonly="ticketDialog.mode === 'view'"
class="w-full p-2.5 border border-gray-300 rounded-xl focus-ring font-mono text-sm bg-white"
:class="{'bg-gray-100': ticketDialog.mode === 'view'}"
placeholder="請手動輸入生產/進貨批號"
>
</div>

<!-- 手KEY 檢驗數量 -->
<div class="space-y-1">
<label class="block text-sm font-bold required-label text-indigo-900">檢驗數量:</label>
<input v-model.number="ticketDialog.data.sampleQty"
type="number"
min="1"
:readonly="ticketDialog.mode === 'view'"
class="w-full p-2.5 border border-gray-300 rounded-xl focus-ring font-mono text-sm bg-white"
:class="{'bg-gray-100': ticketDialog.mode === 'view'}"
placeholder="請輸入抽檢數量"
>
</div>
</div>

<!-- 第三區塊:檢驗項目明細(選範本後自動帶出,設為唯讀無法編輯) -->
<div v-if="ticketDialog.data.formNo" class="mb-6 transition-all duration-300">
<div class="flex justify-between items-center mb-3">
<h3 class="text-lg font-bold text-blue-600">檢驗項目明細</h3>
<span class="text-xs text-amber-600 font-medium bg-amber-50 px-2.5 py-1 rounded-md border border-amber-200">🔒 範本預設項目(固定不可修改)</span>
</div>

<div class="overflow-x-auto rounded-xl border border-emerald-100 shadow-sm">
<table class="min-w-full table-auto text-sm text-left">
<thead class="bg-[#e6f4f1] text-[#138a72] font-semibold">
<tr>
<th class="px-4 py-3 border-b border-emerald-200 required-label">檢驗項目</th>
<th class="px-4 py-3 border-b border-emerald-200 required-label">條件</th>
<th class="px-4 py-3 border-b border-emerald-200 text-center">預設值</th>
<th class="px-4 py-3 border-b border-emerald-200 text-center">最小值</th>
<th class="px-4 py-3 border-b border-emerald-200 text-center">最大值</th>
<th class="px-4 py-3 border-b border-emerald-200 text-center">IoT</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<tr v-for="(item, index) in ticketDialog.data.details" :key="index" class="bg-white">
<!-- 檢驗項目 (唯讀) -->
<td class="px-2 py-3">
<input :value="item.itemName" readonly class="w-full p-2.5 border rounded-xl text-sm readonly-field font-medium">
</td>
<!-- 條件 (唯讀) -->
<td class="px-2 py-3">
<input :value="item.condition" readonly class="w-full p-2.5 border rounded-xl text-sm readonly-field font-mono">
</td>
<!-- 預設值 (唯讀) -->
<td class="px-2 py-3">
<input :value="item.defaultValue || '☒'" readonly class="w-full p-2.5 border rounded-xl text-center text-sm readonly-field font-mono">
</td>
<!-- 最小值 (唯讀) -->
<td class="px-2 py-3">
<input :value="item.minVal || '☒'" readonly class="w-full p-2.5 border rounded-xl text-center text-sm readonly-field font-mono">
</td>
<!-- 最大值 (唯讀) -->
<td class="px-2 py-3">
<input :value="item.maxVal || '☒'" readonly class="w-full p-2.5 border rounded-xl text-center text-sm readonly-field font-mono">
</td>
<!-- IoT (唯讀) -->
<td class="px-2 py-3">
<input :value="item.iot || '無'" readonly class="w-full p-2.5 border rounded-xl text-center text-sm readonly-field font-mono">
</td>
</tr>
</tbody>
</table>
</div>
</div>

<!-- 備註說明區(與明細同步,在選取範本後顯示) -->
<div v-if="ticketDialog.data.formNo" class="mb-6">
<label class="block text-sm font-bold text-gray-700 mb-1">備註說明:</label>
<textarea v-model="ticketDialog.data.remark"
:readonly="ticketDialog.mode === 'view'"
rows="2"
placeholder="現場紀錄說明..."
class="w-full p-2.5 border border-gray-300 rounded-xl focus-ring text-sm"
></textarea>
</div>

<!-- 未選擇範本時的提示文字 -->
<div v-else class="p-8 text-center text-gray-400 bg-gray-50 border border-dashed border-gray-200 rounded-2xl mb-6">
💡 請先選擇上方「套用檢驗表單範本」,系統將自動載入對應的檢驗項目明細。
</div>

<!-- 彈窗底部按鈕區 -->
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-100">
<button @click="ticketDialog.isOpen = false" class="px-6 py-2 bg-gray-200 text-gray-700 font-semibold rounded-lg hover:bg-gray-300 transition duration-150">
{{ ticketDialog.mode === 'view' ? '關閉' : '✕ 取消' }}
</button>
<button v-if="ticketDialog.mode !== 'view'" @click="saveTicketRecord" class="px-6 py-2 bg-indigo-600 text-white font-semibold rounded-lg shadow-md hover:bg-indigo-700 transition duration-150">
💾 儲存開立
</button>
</div>
</div>
</div>

</div>

<script>
const { createApp, ref, reactive } = Vue;

createApp({
setup() {
// 產品清單下拉選單資料
const productList = ref([
{ id: 1, productNo: 'P-1001', productName: 'T8 LED 燈管' },
{ id: 2, productNo: 'P-2005', productName: '藍牙耳機-標準版' },
{ id: 3, productNo: 'P-3010', productName: '鋰電池模組-A型' }
]);

// 檢驗標準範本
const formTemplates = ref([
{ id: 1, formNo: 'FORM-A01', formName: '第一階段進料檢查' },
{ id: 2, formNo: 'FORM-B02', formName: '耳機終檢 FQC' }
]);

// 檢驗單紀錄
const ticketRecords = ref([
{
id: 1001, ticketNo: 'INSP-20260908-001', ticketType: 'IQC', refNo: 'PO-20260901-01',
productId: 1, productName: 'T8 LED 燈管', batchNo: 'B20260901', sampleQty: 10,
formNo: 'FORM-A01', inspectTime: '2026/09/08 10:30',
details: [
{ itemName: '電壓值', condition: '>', defaultValue: '1', minVal: '0', maxVal: '', iot: '' },
{ itemName: '外觀刮傷', condition: 'AlwaysPass', defaultValue: 'pass', minVal: '', maxVal: '', iot: '' },
{ itemName: '尺寸規格', condition: 'Boolean', defaultValue: '', minVal: '', maxVal: '', iot: '' }
]
}
]);

// Dialog 彈窗狀態
const ticketDialog = reactive({
isOpen: false,
mode: 'add',
title: '',
data: {
ticketNo: '',
ticketType: '',
refNo: '',
formNo: '',
productId: '',
productName: '',
batchNo: '',
sampleQty: null,
remark: '',
details: []
}
});

// 選擇產品變更
const onProductChange = () => {
const p = productList.value.find(item => item.id === ticketDialog.data.productId);
if (p) {
ticketDialog.data.productName = p.productName;
}
};

// 當挑選檢驗標準表單範本時,帶出唯讀的標準明細
const onFormTemplateChange = () => {
if (ticketDialog.data.formNo) {
ticketDialog.data.details = [
{ itemName: '電壓值', condition: '>', defaultValue: '1', minVal: '0', maxVal: '', iot: 'IoT-01' },
{ itemName: '外觀刮傷', condition: 'AlwaysPass', defaultValue: 'pass', minVal: '', maxVal: '', iot: '' },
{ itemName: '尺寸規格', condition: 'Boolean', defaultValue: '', minVal: '', maxVal: '', iot: '' }
];
} else {
ticketDialog.data.details = [];
}
};

// 開啟 Dialog
const openTicketDialog = (mode, ticket = {}) => {
ticketDialog.mode = mode;
if (mode === 'add') {
ticketDialog.title = '新增檢驗單';
const todayStr = new Date().toISOString().slice(0, 10).replace(/-/g, '');
ticketDialog.data = {
ticketNo: `INSP-${todayStr}-${Math.floor(100 + Math.random() * 900)}`,
ticketType: '',
refNo: '',
formNo: '',
productId: '',
productName: '',
batchNo: '',
sampleQty: null,
remark: '',
details: []
};
} else if (mode === 'view') {
ticketDialog.title = `檢驗單詳細資訊:${ticket.ticketNo}`;
ticketDialog.data = JSON.parse(JSON.stringify(ticket));
}
ticketDialog.isOpen = true;
};

const saveTicketRecord = () => {
const d = ticketDialog.data;
if (!d.ticketType || !d.refNo || !d.formNo || !d.productId || !d.batchNo || !d.sampleQty) {
alert('請完整填寫必填欄位:檢驗類型、綁定來源單號、檢驗表單範本、選擇產品、批號 與 檢驗數量!');
return;
}

ticketRecords.value.unshift({
id: Date.now(),
ticketNo: d.ticketNo,
ticketType: d.ticketType,
refNo: d.refNo,
formNo: d.formNo,
productId: d.productId,
productName: d.productName,
batchNo: d.batchNo,
sampleQty: d.sampleQty,
inspectTime: new Date().toLocaleString(),
details: d.details
});

ticketDialog.isOpen = false;
};

return {
productList,
formTemplates,
ticketRecords,
ticketDialog,
onProductChange,
onFormTemplateChange,
openTicketDialog,
saveTicketRecord
};
}
}).mount('#app');
</script>
</body>
</html>
(1-1/2)