|
<!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>
|