Project

General

Profile

Feature #119 » 設備主檔_0716.html

鎮源 游, 07/31/2026 02:18 AM

 
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<title>設備主檔管理 - 完美匹配截圖 V4</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
'primary': '#1f2937',
'light-blue': '#4c9aff',
'light-blue-hover': '#3b82f6',
'dark-red': '#cc0000',
'red-hover': '#b91c1c',
'gray-header': '#f9fafb',
}
}
}
}
</script>
<style>
body { font-family: sans-serif; color: #4b5563; }
.master-input {
padding: 10px;
border: 1px solid #d1d5db;
border-radius: 6px;
width: 100%;
color: #1f2937;
}
.master-input:focus { border-color: #4c9aff; outline: none; box-shadow: 0 0 0 2px rgba(76, 154, 255, 0.2); }

.required-label::after { content: ' *'; color: #ef4444; margin-left: 2px; }

/* 篩選按鈕樣式 (膠囊形 + 灰色邊框) */
.filter-button {
padding: 8px 18px;
border-radius: 9999px;
font-weight: 500;
border: 1px solid #d1d5db;
transition: all 0.2s;
cursor: pointer;
color: #374151;
background-color: white;
}
.filter-button.active {
background-color: #4c9aff;
color: white;
border-color: #4c9aff;
}
.filter-button.inactive:hover { background-color: #f3f4f6; }

/* 操作按鈕通用樣式 */
.action-button {
padding: 6px 14px;
border-radius: 6px;
font-weight: 500;
display: inline-flex;
items-center: true;
gap: 4px;
transition: all 0.15s;
}
/* 表格欄位寬度比例調校 */
.col-no { width: 13%; }
.col-name { width: 20%; }
.col-type { width: 10%; }
.col-blank { width: 6%; } /* 您的紅框空白欄 */
.col-line { width: 13%; } /* 線別 */
.col-area { width: 13%; } /* 所屬區域 */
.col-action { width: 25%; } /* 操作欄 (修改/刪除) */
</style>
</head>
<body class="bg-gray-100 p-8">

<div id="app">
<div class="main-content-wrapper max-w-7xl mx-auto">
<h1 class="text-3xl font-extrabold text-primary mb-8 tracking-tight">
設備主檔
</h1>
<div class="bg-white p-7 rounded-2xl shadow-xl border border-gray-200">
<h2 class="text-xl font-bold text-gray-800 mb-6 border-b pb-3">
設備清單
</h2>
<div class="flex space-x-3 mb-6 items-center">
<span class="text-sm text-gray-500 mr-2">按類別快速篩選:</span>
<button
@click="activeFilter = '全部'"
:class="['filter-button', activeFilter === '全部' ? 'active' : 'inactive']"
>
全部
</button>
<button
@click="activeFilter = '生產設備'"
:class="['filter-button', activeFilter === '生產設備' ? 'active' : 'inactive']"
>
生產設備
</button>
<button
@click="activeFilter = 'IoT'"
:class="['filter-button', activeFilter === 'IoT' ? 'active' : 'inactive']"
>
IoT
</button>
<button
@click="activeFilter = '車床'"
:class="['filter-button', activeFilter === '車床' ? 'active' : 'inactive']"
>
車床
</button>
</div>

<div class="flex justify-between items-center mb-5 gap-4">
<div class="relative w-72">
<input v-model="searchKeyword" placeholder="搜尋設備編號..." class="p-3 pl-10 border border-gray-300 rounded-xl w-full text-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" /></svg>
</div>
<div class="flex space-x-2 text-sm">
<button @click="search" class="px-5 py-3 border border-gray-300 bg-white text-gray-700 rounded-xl hover:bg-gray-50 transition font-medium">
搜尋
</button>
<button @click="clearSearch" class="px-5 py-3 border border-gray-300 bg-white text-gray-700 rounded-xl hover:bg-gray-50 transition font-medium">
清空
</button>
<button
@click="openDialog('add')"
class="px-6 py-3 bg-light-blue text-white rounded-xl hover:bg-light-blue-hover transition flex items-center space-x-2 font-semibold"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 fill-white" viewBox="0 0 20 20" fill="currentColor"><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>
</div>
</div>

<table class="min-w-full table-auto border border-gray-200 text-sm text-gray-700">
<thead class="bg-gray-header">
<tr class="text-gray-900 border-b">
<th class="col-no px-4 py-3 text-left font-bold text-gray-400">設備編號</th>
<th class="col-name px-4 py-3 text-left font-bold text-gray-400">設備名稱</th>
<th class="col-type px-4 py-3 text-left font-bold text-gray-400">設備類型</th>
<th class="col-blank px-4 py-3"></th> <th class="col-line px-4 py-3 text-left font-bold text-gray-400">線別</th> <th class="col-area px-4 py-3 text-left font-bold text-gray-400">所屬區域</th>
<th class="col-action px-4 py-3 text-left font-bold text-gray-400">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<tr v-for="equipment in filteredEquipment" :key="equipment.no" class="hover:bg-blue-50/50 transition-colors">
<td class="px-4 py-4 font-bold text-gray-900">{{ equipment.no }}</td>
<td class="px-4 py-4 text-gray-600">{{ equipment.name }}</td>
<td class="px-4 py-4 text-gray-600">{{ equipment.type }}</td>
<td class="px-4 py-4"></td> <td class="px-4 py-4 text-gray-600">{{ equipment.line }}</td> <td class="px-4 py-4 text-gray-600">{{ equipment.area }}</td> <td class="px-4 py-4 flex space-x-2 items-center">
<button @click="openDialog('edit', equipment)" class="action-button bg-light-blue text-white hover:bg-light-blue-hover text-xs font-semibold shadow-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 inline" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>
<span>修改</span>
</button>
<button @click="openDialog('delete', equipment)" class="action-button bg-dark-red text-white hover:bg-red-hover text-xs font-semibold shadow-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 inline" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 6v10h6V6H7z" clip-rule="evenodd" /></svg>
<span>刪除</span>
</button>
</td>
</tr>
<tr v-if="filteredEquipment.length === 0">
<td colspan="7" class="px-4 py-8 text-center text-gray-400 border bg-gray-50">找不到符合條件的設備記錄。</td>
</tr>
</tbody>
</table>

<div class="flex justify-between mt-6 items-center text-xs text-gray-400 font-medium">
<span>共 {{ filteredEquipment.length }} 筆資料</span>
<div class="flex space-x-1 items-center">
<button class="px-3 py-1.5 border border-gray-200 rounded bg-white text-gray-500 hover:bg-gray-50">上一頁</button>
<button class="px-3 py-1.5 border border-light-blue bg-light-blue text-white font-bold rounded">1</button>
<button class="px-3 py-1.5 border border-gray-200 rounded bg-white text-gray-500 hover:bg-gray-50">2</button>
<button class="px-3 py-1.5 border border-gray-200 rounded bg-white text-gray-500 hover:bg-gray-50">3</button>
<span class="px-1">...</span>
<button class="px-3 py-1.5 border border-gray-200 rounded bg-white text-gray-500 hover:bg-gray-50">下一頁</button>
</div>
</div>
</div>

</div>
<div v-if="dialog.mode === 'add' || dialog.mode === 'edit'" class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-40 p-4">
<div class="bg-white p-8 rounded-2xl shadow-2xl w-11/12 max-w-2xl border border-gray-100">
<h2 class="text-xl font-bold text-gray-800 mb-6 border-b pb-3">
{{ dialog.mode === 'add' ? '新增設備主檔' : '編輯設備主檔' }}
</h2>
<form @submit.prevent="saveEquipment">
<div class="grid grid-cols-2 gap-x-6 gap-y-4 mb-6 text-sm">
<div>
<label class="block mb-1 font-semibold text-gray-700 required-label">設備編號:</label>
<input
v-model="dialog.data.no"
type="text"
class="master-input"
placeholder="請輸入設備編號 (如: HL-M-13)"
required
:disabled="dialog.mode === 'edit'"
>
</div>
<div>
<label class="block mb-1 font-semibold text-gray-700 required-label">設備名稱:</label>
<input
v-model="dialog.data.name"
type="text"
class="master-input"
placeholder="請輸入設備名稱"
required
>
</div>
<div>
<label class="block mb-1 font-semibold text-gray-700 required-label">設備類型:</label>
<select v-model="dialog.data.type" class="master-input" required>
<option value="">-- 請選擇設備類型 --</option>
<option v-for="type in equipmentTypeFilterPool" :key="type" :value="type">{{ type }}</option>
</select>
</div>

<div>
<label class="block mb-1 font-semibold text-gray-700 required-label">線別:</label>
<input
v-model="dialog.data.line"
type="text"
class="master-input"
placeholder="請輸入線別 (如: 線別 A)"
required
>
</div>

<div class="col-span-2">
<label class="block mb-1 font-semibold text-gray-700 required-label">所屬區域:</label>
<input
v-model="dialog.data.area"
type="text"
class="master-input"
placeholder="請輸入區域 (如: 手動線)"
required
>
</div>

<div class="col-span-2">
<label class="block mb-1 font-semibold text-gray-700">備註:</label>
<textarea
v-model="dialog.data.notes"
class="master-input"
rows="2"
placeholder="請輸入設備相關備註資訊 (選填)"
></textarea>
</div>
</div>
<div class="flex justify-end space-x-3 mt-6">
<button @click="closeDialog" type="button" class="px-6 py-2.5 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200">取消</button>
<button type="submit" class="px-6 py-2.5 bg-light-blue text-white rounded-lg hover:bg-light-blue-hover font-semibold">
{{ dialog.mode === 'add' ? '確認新增' : '確認修改' }}
</button>
</div>
</form>
</div>
</div>

<div v-if="dialog.mode === 'delete'" class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-40 p-4">
<div class="bg-white p-7 rounded-xl shadow-2xl w-11/12 max-w-sm border border-gray-200">
<h2 class="text-lg font-bold text-dark-red mb-3">
⚠️ 確認刪除設備主檔
</h2>
<div class="mb-6 text-sm">
<p class="text-gray-600 mb-2">確定要刪除此設備記錄嗎?</p>
<div class="p-3 bg-red-50 border border-red-200 rounded-lg text-red-800 font-mono font-bold">
{{ dialog.data.no }} - {{ dialog.data.name }}
</div>
</div>
<div class="flex justify-end space-x-3 text-sm">
<button @click="closeDialog" class="px-5 py-2.5 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200">取消</button>
<button @click="confirmDeleteEquipment" class="px-5 py-2.5 bg-dark-red text-white rounded-lg hover:bg-red-hover">確認刪除</button>
</div>
</div>
</div>
</div>

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

createApp({
setup() {
const activeFilter = ref('全部');
const searchKeyword = ref('');
const equipmentTypeFilterPool = ref(['生產設備', 'IoT', '車床']);

// 初始化預設數據 (包含線別、區域和備註)
const equipmentMaster = ref([
{ no: 'HL-M-13', name: '凸面抛光機(4軸)', type: '生產設備', line: '手動 A 線', area: '手動線', notes: '' },
{ no: 'HL-M-14', name: '抛光機(單軸)', type: '生產設備', line: '手動 B 線', area: '手動線', notes: '備品充足' },
{ no: 'HL-M-17', name: '超音波清洗機(小)', type: '生產設備', line: '自動 A 線', area: '手動線', notes: '' },
{ no: 'Keratometer', name: '手動弧度儀', type: '生產設備', line: '手動 A 線', area: '手動線', notes: '定期校正' },
{ no: 'HL-M-18', name: '超音波清洗機(大)', type: '生產設備', line: '自動 B 線', area: '手動線', notes: '' },
{ no: 'HL-M-19', name: '超音波清洗機(大)', type: '生產設備', line: '自動 B 線', area: '手動線', notes: '' },
{ no: 'HL-M-27', name: 'PJ-Plasma II', type: '生產設備', line: '研發線', area: '手動線', notes: '' },
]);
const dialog = reactive({
mode: null,
data: {},
});

// 篩選與搜尋
const filteredEquipment = computed(() => {
let records = equipmentMaster.value;
const keyword = searchKeyword.value.trim().toLowerCase();
const filterType = activeFilter.value;

if (filterType !== '全部') {
records = records.filter(eq => eq.type === filterType);
}

if (keyword) {
records = records.filter(eq =>
eq.no.toLowerCase().includes(keyword) ||
eq.name.toLowerCase().includes(keyword) ||
eq.line.toLowerCase().includes(keyword) ||
eq.area.toLowerCase().includes(keyword)
);
}
return records;
});
// 功能按鈕
const clearSearch = () => {
searchKeyword.value = '';
};

const openDialog = (mode, equipment = {}) => {
dialog.mode = mode;
dialog.data = JSON.parse(JSON.stringify(equipment));
if (mode === 'add') {
dialog.data = { no: '', name: '', type: '', line: '', area: '', notes: '' };
}
};

const closeDialog = () => {
dialog.mode = null;
dialog.data = {};
};

const saveEquipment = () => {
const data = dialog.data;
if (dialog.mode === 'add') {
if(equipmentMaster.value.some(eq => eq.no === data.no)) {
alert(`設備編號 ${data.no} 已存在!`);
return;
}
equipmentMaster.value.push(data);
} else if (dialog.mode === 'edit') {
const index = equipmentMaster.value.findIndex(e => e.no === data.no);
if (index !== -1) Object.assign(equipmentMaster.value[index], data);
}
closeDialog();
};

const confirmDeleteEquipment = () => {
const index = equipmentMaster.value.findIndex(e => e.no === dialog.data.no);
if (index !== -1) equipmentMaster.value.splice(index, 1);
closeDialog();
};

return {
activeFilter,
searchKeyword,
equipmentTypeFilterPool,
equipmentMaster,
filteredEquipment,
dialog,
clearSearch,
openDialog,
closeDialog,
saveEquipment,
confirmDeleteEquipment,
};
}
}).mount('#app');
</script>
</body>
</html>
(2-2/2)