From 06c1cbbabf6d001a759cf18640b4fd03a06ff6a3 Mon Sep 17 00:00:00 2001 From: Kritkhanin Anantakul <65160144@go.buu.ac.th> Date: Fri, 28 Feb 2025 19:44:16 +0700 Subject: [PATCH] update gantt --- src/components/GanttChart.vue | 259 +++++++++++++++++++++++++--------- 1 file changed, 191 insertions(+), 68 deletions(-) diff --git a/src/components/GanttChart.vue b/src/components/GanttChart.vue index 84b894e..22de627 100644 --- a/src/components/GanttChart.vue +++ b/src/components/GanttChart.vue @@ -12,63 +12,46 @@ <!-- Rows: เครื่องจักรแต่ละตัว --> <div class="rows"> - <div - v-for="machine in machines" - :key="machine.id" - class="row" - @dragover.prevent - @drop="onDrop($event, machine.name)" - > + <div v-for="machine in machines" :key="machine.id" class="row" @dragover.prevent="onDragOver($event)" + @drop="onDrop($event, machine.name)"> <div class="machine-label"> {{ machine.name }} </div> <div class="row-timeline"> <!-- เส้นแนวตั้ง (Grid Lines) --> - <div - v-for="hour in hours" - :key="'line-' + hour" - class="vertical-line" - :style="getLineStyle(hour)" - ></div> + <div v-for="hour in hours" :key="'line-' + hour" class="vertical-line" :style="getLineStyle(hour)"></div> - <!-- แสดง Order เฉพาะหน้าปัจจุบันและเครื่องจักรตรงกัน --> + <!-- แสดง Order --> <div v-for="order in orders.filter(o => pages.includes(o.page) && o.page === currentPage && o.machine === machine.name)" - :key="order.id" - class="order" - :style="getOrderStyle(order)" - > + :key="order.id" class="order" :class="{ 'faded': draggingOrder && draggingOrder.id === order.id }" + :style="getOrderStyle(order)"> <!-- Handle สำหรับ Resize ด้านซ้าย --> - <div class="resize-handle left" - @mousedown="onResizeStart($event, order, 'left')"> - </div> + <div class="resize-handle left" @mousedown="onResizeStart($event, order, 'left')"></div> <!-- ส่วนกลางของ Order ใช้สำหรับลาก --> - <div class="order-content" - draggable="true" - @dragstart="onDragStart($event, order)" - @dragend="onDragEnd($event, order)" - > + <div class="order-content" draggable="true" @dragstart="onDragStart($event, order)" + @dragend="onDragEnd($event, order)"> {{ order.name }} ({{ order.start }} - {{ order.end }}) </div> <!-- Handle สำหรับ Resize ด้านขวา --> - <div class="resize-handle right" - @mousedown="onResizeStart($event, order, 'right')"> - </div> + <div class="resize-handle right" @mousedown="onResizeStart($event, order, 'right')"></div> </div> </div> </div> </div> - <v-divider :thickness="7" ></v-divider> + + <!-- Ghost Order ขณะลาก --> + <div v-if="ghostOrder" class="drag-ghost" :style="ghostStyle"> + {{ ghostOrder.name }} ({{ ghostOrder.start }} - {{ ghostOrder.end }}) + </div> + + <v-divider :thickness="7"></v-divider> + <!-- Pagination --> <div class="pagination"> - <button - v-for="p in pages" - :key="p" - :class="['page-btn', { active: p === currentPage }]" - @click="currentPage = p" - > + <button v-for="p in pages" :key="p" :class="['page-btn', { active: p === currentPage }]" @click="currentPage = p"> {{ p }} </button> <!-- ปุ่ม + สำหรับเพิ่มหน้าใหม่ --> @@ -95,7 +78,7 @@ export default { { id: 5, name: 'MC5' }, ], - // รายการ Order (ตัวอย่าง Order 1 มีสี blue) + // รายการ Order orders: [ { page: 1, id: 1, name: 'Order 1', start: '13:00', end: '15:00', machine: 'MC1', color: 'blue' }, { page: 1, id: 2, name: 'Order 2', start: '09:00', end: '11:00', machine: 'MC2' }, @@ -109,8 +92,26 @@ export default { resizingOrder: null, resizeDirection: null, + // Ghost Order (ตัวอย่างที่ขยับตามเม้าส์) + ghostOrder: null, + ghostStyle: { + position: 'fixed', + top: '0px', + left: '0px', + display: 'none', + width: 'auto', + height: '40px', + lineHeight: '40px', + padding: '0 10px', + borderRadius: '20px', + pointerEvents: 'none', + backgroundColor: '#4caf50', + color: '#fff', + zIndex: 9999, + }, + // จัดการ Pagination - pages: [1, 2], // เริ่มต้นสองหน้า + pages: [1, 2], currentPage: 1, } }, @@ -144,7 +145,7 @@ export default { return { left, width, - backgroundColor: order.color || '#4caf50' // ใช้สีใน order หรือสี default + backgroundColor: order.color || '#4caf50' }; }, @@ -170,55 +171,142 @@ export default { const minutes = Math.round((decimal - hours) * 60); return (hours < 10 ? '0' + hours : hours) + ':' + (minutes < 10 ? '0' + minutes : minutes); }, + onDragEndGlobal() { + // ลบ Ghost Order ออกจาก UI + this.ghostOrder = null; + this.ghostStyle.display = 'none'; + this.draggingOrder = null; + + // เอา event listener ออก (ไม่งั้นมันจะถูกเรียกทุกครั้งที่ drag) + document.removeEventListener('dragend', this.onDragEndGlobal); + }, // เริ่มลาก Order (DragStart) onDragStart(event, order) { this.draggingOrder = order; const rect = event.target.getBoundingClientRect(); - this.dragOffset = event.clientX - rect.left; - event.dataTransfer.effectAllowed = "move"; + this.dragOffset = event.target.getBoundingClientRect().width / 2; + + // ปิด preview เริ่มต้นของ HTML5 Drag & Drop (หรือใส่เป็นภาพโปร่งใสก็ได้) + const emptyImg = document.createElement('img'); + emptyImg.src = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='; + event.dataTransfer.setDragImage(emptyImg, 0, 0); + + // สร้าง Ghost Order + this.ghostOrder = { ...order }; // clone ข้อมูลของ Order + this.ghostStyle.display = 'block'; + this.ghostStyle.backgroundColor = order.color || '#4caf50'; + + // ฟัง event dragover ทั้งหน้า (หรือจะใช้ใน .rows ก็ได้) + document.addEventListener('dragover', this.onDragOverGlobal); + document.addEventListener('dragend', this.onDragEndGlobal); }, - // จบการลาก Order (DragEnd) => คำนวณตำแหน่งใหม่ - onDragEnd(event, order) { - const rowTimeline = event.target.closest('.row-timeline'); - if (!rowTimeline) return; + // ขณะลาก (DragOver) ที่ผูกใน template (กัน default เพื่อให้ drop ได้) + onDragOver(event) { + // ไม่ได้ทำอะไรมากในที่นี้ เพราะเราจัดการใน onDragOverGlobal + event.preventDefault(); + }, + // onDragOverGlobal - อัปเดตตำแหน่ง Ghost และเวลา + onDragOverGlobal(event) { + event.preventDefault(); + const rowTimeline = document.querySelector('.row-timeline'); + if (!rowTimeline) return; const timelineRect = rowTimeline.getBoundingClientRect(); - const dropX = event.clientX - timelineRect.left - this.dragOffset; const timelineWidth = timelineRect.width; + const offsetX = event.clientX - timelineRect.left; - let ratioStart = dropX / timelineWidth; - ratioStart = Math.min(Math.max(ratioStart, 0), 1); - + // 1️⃣ คำนวณ Snap ตามแนวนอน (เวลา) + let ratio = offsetX / timelineWidth; + ratio = Math.min(Math.max(ratio, 0), 1); + const timelineStart = this.timeToDecimal(this.startTime); const timelineEnd = this.timeToDecimal(this.endTime); - let newStartDecimal = timelineStart + ratioStart * (timelineEnd - timelineStart); + let newStartDecimal = timelineStart + ratio * (timelineEnd - timelineStart); - // ปัดค่าเป็นช่วงครึ่งชั่วโมง (0.5) + // ปัดเป็นช่วงครึ่งชั่วโมง (30 นาที) newStartDecimal = Math.round(newStartDecimal * 2) / 2; - const duration = this.timeToDecimal(order.end) - this.timeToDecimal(order.start); + // คำนวณช่วงเวลาของ Order + const duration = this.timeToDecimal(this.draggingOrder.end) - this.timeToDecimal(this.draggingOrder.start); let newEndDecimal = newStartDecimal + duration; - - // ถ้าเกิน endTime ก็ปรับให้พอดี if (newEndDecimal > timelineEnd) { newEndDecimal = timelineEnd; newStartDecimal = newEndDecimal - duration; } - order.start = this.decimalToTime(newStartDecimal); - order.end = this.decimalToTime(newEndDecimal); - this.draggingOrder = null; + // อัปเดตเวลาใน ghostOrder + this.ghostOrder.start = this.decimalToTime(newStartDecimal); + this.ghostOrder.end = this.decimalToTime(newEndDecimal); + + // คำนวณตำแหน่ง snapped left ตาม grid + const snappedRatioStart = (newStartDecimal - timelineStart) / (timelineEnd - timelineStart); + const snappedLeft = timelineRect.left + snappedRatioStart * timelineWidth; + + // คำนวณความกว้างของ Ghost Order + const snappedRatioEnd = (newEndDecimal - timelineStart) / (timelineEnd - timelineStart); + const snappedWidth = (snappedRatioEnd - snappedRatioStart) * timelineWidth; + + // 2️⃣ คำนวณ Snap ตามแนวตั้ง (เครื่องจักร) + const rows = document.querySelectorAll('.row'); // ดึงทุกแถว + let closestRow = null; + let minDistance = Infinity; + + rows.forEach(row => { + const rect = row.getBoundingClientRect(); + const distance = Math.abs(event.clientY - rect.top); + if (distance < minDistance) { + minDistance = distance; + closestRow = row; + } + }); + + if (closestRow) { + this.ghostOrder.machine = closestRow.querySelector('.machine-label').textContent.trim(); // อัปเดตเครื่องจักรที่ snap + this.ghostStyle.top = closestRow.getBoundingClientRect().top + 'px'; // Snap ไปที่ตำแหน่งของ row นั้น + } + + // 3️⃣ อัปเดตตำแหน่งและขนาดของ Ghost Order + this.ghostStyle.left = snappedLeft + 'px'; + this.ghostStyle.width = snappedWidth + 'px'; }, + + // จบการลาก Order (DragEnd) => คำนวณตำแหน่งใหม่ + onDragEnd(event, order) { + if (!this.ghostOrder) return; + + // ใช้ค่าที่ snap แล้วจาก Ghost Order + order.start = this.ghostOrder.start; + order.end = this.ghostOrder.end; + order.machine = this.ghostOrder.machine; + + // ลบ Ghost Order + document.removeEventListener('dragover', this.onDragOverGlobal); + this.ghostOrder = null; + this.ghostStyle.display = 'none'; + this.draggingOrder = null; +} +, + // เมื่อปล่อย Drag บนเครื่องจักรใหม่ => เปลี่ยน machine ของ Order onDrop(event, newMachine) { - if (this.draggingOrder) { - this.draggingOrder.machine = newMachine; - this.draggingOrder = null; - } - }, + event.preventDefault(); + + if (this.draggingOrder) { + // ใช้ค่าที่ snap แล้วจาก Ghost Order + this.draggingOrder.start = this.ghostOrder.start; + this.draggingOrder.end = this.ghostOrder.end; + this.draggingOrder.machine = newMachine; // ใช้ค่าของเครื่องจักรที่ปล่อยลงไป + } + + // ลบ Ghost Order + this.ghostOrder = null; + this.ghostStyle.display = 'none'; + this.draggingOrder = null; +} +, // เริ่ม Resize (mousedown ที่ handle) onResizeStart(event, order, direction) { @@ -277,6 +365,7 @@ export default { } </script> + <style scoped> .gantt-chart { width: 100%; @@ -284,10 +373,12 @@ export default { display: flex; flex-direction: column; } + .header { display: flex; background: #fff; } + .machine-label { width: 80px; text-align: center; @@ -296,10 +387,12 @@ export default { line-height: 40px; border-bottom: 1px solid #ffffff; } + .time-scale { display: flex; flex: 1; } + .time-cell { flex: 1; text-align: center; @@ -308,20 +401,25 @@ export default { line-height: 40px; font-weight: bold; } + .rows { flex: 1; overflow-y: auto; } + .row { display: flex; min-height: 60px; - border-bottom: 1px solid #ddd; /* เส้นแบ่งระหว่างเครื่องจักร (แนวนอน) */ + border-bottom: 1px solid #ddd; + /* เส้นแบ่งระหว่างเครื่องจักร (แนวนอน) */ } + .row-timeline { position: relative; flex: 1; overflow: hidden; } + .vertical-line { position: absolute; top: 0; @@ -329,6 +427,7 @@ export default { width: 1px; background-color: #ddd; } + .order { position: absolute; top: 10px; @@ -340,6 +439,7 @@ export default { align-items: center; z-index: 1; } + .order-content { flex: 1; text-align: center; @@ -347,31 +447,52 @@ export default { cursor: grab; padding: 0 10px; } + .resize-handle { width: 5px; height: 100%; - background-color: #fff; cursor: ew-resize; position: absolute; } + .resize-handle.left { left: 0; border-top-left-radius: 10px; border-bottom-left-radius: 10px; } + .resize-handle.right { right: 0; border-top-right-radius: 10px; border-bottom-right-radius: 10px; } + +/* เมื่อกำลังลาก Order ตัวจริงให้จางลง */ +.order.faded { + opacity: 0.3; +} + +/* Ghost Order */ +.drag-ghost { + box-sizing: border-box; + text-align: center; + pointer-events: none; + /* กำหนดสไตล์เพิ่มเติมตามต้องการ */ +} + .pagination { display: flex; - justify-content: flex-start; /* จัดเรียงชิดซ้าย */ - align-items: center; /* จัดกึ่งกลางแนวตั้ง */ - gap: 5px; /* เว้นระยะระหว่างปุ่ม */ - width: 100%; /* ทำให้ `.pagination` ขยายเต็มพื้นที่ */ + justify-content: flex-start; + /* จัดเรียงชิดซ้าย */ + align-items: center; + /* จัดกึ่งกลางแนวตั้ง */ + gap: 5px; + /* เว้นระยะระหว่างปุ่ม */ + width: 100%; + /* ทำให้ `.pagination` ขยายเต็มพื้นที่ */ margin-top: 10px; } + .page-btn { background: #ffffff; border: 1px solid #ffffff; @@ -379,10 +500,12 @@ export default { cursor: pointer; font-size: 14px; } + .page-btn.active { background: #007bff; color: #ffffff; } + .page-btn.add-page { font-weight: bold; background: #ffffff; -- GitLab