Tạo bảng chấm công bằng vue2, tailwind css 3
Để tạo một bảng lịch chấm công cho danh sách nhân viên
, mỗi hàng có số ô (đại diện cho số ngày trong tháng đang được chọn) và khi click vào một ô bất kỳ thì hiển thị một popover chứa danh sách các khung giờ làm, chúng ta có thể sử dụng Vue 2 và Tailwind CSS 3 để triển khai. Sau đây là hướng dẫn chi tiết:
Bước 1: Tạo 1 component trong dự án với tên App.vue
<template>
<div class="">
<select v-model="selectedMonth" @change="updateDays">
<option v-for="(month, index) in months" :key="index" :value="index">{{ month }}</option>
</select>
<table class="w-full border border-gray-300 table-fixed">
<thead class="bg-gray-100">
<tr>
<th class="border p-1 text-left font-semibold text-gray-700 text-[8px] md:text-[10px]" colspan="3">Staff</th>
<th v-for="day in days" :key="day" class="border p-3 text-center font-semibold text-gray-700 text-[8px] md:text-[10px]">{{ day }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(staff, index) in staffs" :key="index" class="hover:bg-gray-50">
<td class="border p-1 text-gray-700 font-medium" colspan="3">{{ staff.name }}</td>
<td
v-for="day in days"
:key="day"
class="relative border p-1 cursor-pointer text-center"
:class="{'bg-primary-500 text-white': isPopoverOpen && popoverInfo.staffId === staff.id && popoverInfo.day === day}"
@click="showPopover($event, staff, day)"
>
<span class="text-[8px] md:text-[10px]">{{ getShift(staff.id, day) || '-' }}</span>
<!-- Popover -->
<div
v-if="isPopoverOpen && popoverInfo.staffId === staff.id && popoverInfo.day === day"
:class="popoverPositionClass"
class="absolute z-20 mt-1 w-48 bg-white rounded-lg border border-gray-300"
style="top: 100%;"
>
<div class="bg-gray-50 p-2 rounded-t-lg border-b border-gray-300">
<span class="text-sm font-semibold text-gray-600">Select Shift</span>
</div>
<ul class=" max-h-52 overflow-auto">
<li
v-for="(pattern, index) in timePatterns"
:key="index"
class="p-2 text-sm text-gray-600 hover:bg-primary-50 hover:text-primary-600 border-b border-b-gray-100 hover:border-b-gray-200 cursor-pointer"
@click="selectShift(staff.id, day, pattern)"
>
{{ pattern }}
</li>
<li class="p-2 text-sm text-gray-600 hover:bg-primary-50 hover:text-primary-600 border-b border-b-gray-100 hover:border-b-gray-200 cursor-pointer" @click="clearShift(staff.id, day)">Trash</li>
<li class="p-2 text-sm text-gray-600 hover:bg-primary-50 hover:text-primary-600 border-b border-b-gray-100 hover:border-b-gray-200 cursor-pointer" @click="closePopover">x Close</li>
</ul>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</template>
Vue HTMLBước 2: Viết mã vue2 để thực hiện yêu cầu:
<script>
export default {
data() {
return {
// days: Array.from({ length: 31 }, (_, i) => i + 1), // 31 ngày
selectedMonth: new Date().getMonth(),
months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
days: Array.from({ length: 31 }, (_, i) => i + 1), // Default to 31 days
staffs: [
{ id: 1, name: 'Staff 1', shifts: Array(31).fill(null) },
{ id: 2, name: 'Staff 2', shifts: Array(31).fill(null) },
// Thêm staffs ở đây
],
timePatterns: ['08:00-12:00', '13:00-17:00', '17:00-21:00'], // Các khung giờ làm
shifts: {}, // Object lưu các shifts theo staff và ngày
isPopoverOpen: false,
popoverInfo: { staffId: null, day: null },
popoverPositionClass: 'left-1/2 transform -translate-x-1/2', // Vị trí mặc định của popover
};
},
methods: {
updateDays() {
const month = this.selectedMonth;
const year = new Date().getFullYear();
const daysInMonth = new Date(year, month + 1, 0).getDate();
this.days = Array.from({ length: daysInMonth }, (_, i) => i + 1);
},
showPopover(event, staff, day) {
// Hiển thị popover
this.isPopoverOpen = true;
this.popoverInfo = { staffId: staff.id, day };
// Xác định vị trí của ô để căn chỉnh popover
const cellRect = event.target.getBoundingClientRect();
const windowWidth = window.innerWidth;
const popoverWidth = 200; // Giả sử popover có width khoảng 200px
if (cellRect.right + popoverWidth > windowWidth) {
// Nếu ô nằm sát bên phải, căn về bên trái
this.popoverPositionClass = `right-0 shadow-[-2px_0px_28px_-12px_rgba(0,0,0,0.3)] before:w-4 before:h-4 before:rotate-45 before:bg-white before:absolute before:top-4 before:right-5 before:top-0`;
} else if (cellRect.left + popoverWidth < popoverWidth) {
// Nếu ô nằm sát bên trái, căn về bên phải
this.popoverPositionClass = 'left-0 shadow-[-2px_0px_28px_-12px_rgba(0,0,0,0.3)] before:w-4 before:h-4 before:rotate-45 before:bg-white before:absolute before:top-4 before:-left-2 before:-top-1.5';
} else {
// Mặc định căn giữa ô
this.popoverPositionClass = 'left-1/2 transform -translate-x-1/2 shadow-[0_-4px_24px_-8px_rgba(0,0,0,0.2)] top-14 before:w-4 before:h-4 before:rotate-45 before:bg-white before:absolute before:-top-1.5 before:left-0 before:right-0 before:mx-auto';
}
// Đóng popover nếu click ngoài vùng
const closePopover = (e) => {
if (!event.target.contains(e.target)) {
this.isPopoverOpen = false;
window.removeEventListener('click', closePopover);
}
};
// window.addEventListener('click', closePopover);
},
closePopover() {
const closePopover = (e) => {
this.isPopoverOpen = false;
window.removeEventListener('click', closePopover);
};
window.addEventListener('click', closePopover);
this.popoverInfo = { staffId: null, day: null };
},
selectShift(staffId, day, pattern) {
// Gán shift vào ô tương ứng
this.$set(this.shifts, `${staffId}-${day}`, pattern);
const staff = this.staffs.find(staff => staff.id === staffId);
if (staff) {
this.$set(staff.shifts, day - 1, pattern);
}
this.closePopover(); // Ẩn popover sau khi chọn khung giờ
},
getShift(staffId, day) {
const staff = this.staffs.find(staff => staff.id === staffId);
if (staff) {
return staff.shifts[day - 1];
}
// return this.shifts[`${staffId}-${day}`];
},
// Hàm xóa thông tin shift
clearShift(staffId, day) {
// Xóa shift của ô tương ứng
this.$delete(this.shifts, `${staffId}-${day}`);
this.isPopoverOpen = false; // Ẩn popover sau khi xóa
const staff = this.staffs.find(staff => staff.id === staffId);
if (staff) {
this.$set(staff.shifts, day - 1, null);
}
this.closePopover(); // Ẩn popover sau khi chọn khung giờ
},
},
};
</script>
Vue