Tạo bảng chấm công bằng vue2, tailwind css 3

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 HTML

Bướ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

Bước 3: Chạy dự án và xem kết quả

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *