| <template> |
| <div class="calendar"> |
| <div class="calendar_header">{{currentMonth}}月</div> |
| <table cellspacing="0" cellpadding="0" class="calendar_table"> |
| <thead> |
| <th v-for="day in WEEK_DAYS" :key="day" :class="['thead_th',day=='六'||day=='日'?'thead_th_red':'']"> |
| {{day}}</th> |
| </thead> |
| <tbody> |
| <tr v-for="(row,index) in rows" :key="index"> |
| <td v-for="(cell,key) in row" :key="key" class="td" @click="handlePickDay(cell)"> |
| <div :class="getCellClass(cell,key)"> |
| <div v-if="cell.type=='current'" class="triangle" :style="getCelltriangleStyle(cell)"></div> |
| <span class="cell_text">{{cell.text==0?'':cell.text}}</span> |
| </div> |
| </td> |
| </tr> |
| </tbody> |
| </table> |
| </div> |
| </template> |
| <script lang="ts" setup> |
| import { ref, computed, onMounted } from "vue"; |
| import moment from "moment"; |
| const props = defineProps({ |
| markList: { |
| type: Array<any>, |
| default: (): any[] => { |
| return []; |
| }, |
| }, |
| month: { |
| type: Date, |
| required: true, |
| }, |
| disabled: { |
| type: Boolean, |
| default: false, |
| }, |
| disableBefore: { |
| type: Boolean, |
| default: true, |
| }, |
| }); |
| const emits = defineEmits(['change']); |
| |
| type CalendarDateCellType = 'next' | 'prev' | 'current' |
| type CalendarDateCell = { |
| text: number, |
| type: CalendarDateCellType |
| } |
| const WEEK_DAYS = ref(["日", "一", "二", "三", "四", "五", "六"]); |
| const currentMonth = computed(() => { |
| return moment(props.month).format('M'); |
| }) |
| onMounted(() => { |
| onKeyEvent(); |
| }) |
| |
| const rows = computed(() => { |
| let days: CalendarDateCell[] = [] |
| const firstDay = moment(props.month).startOf("month").date(); |
| |
| const firstDayOfWeek = moment(props.month).startOf("month").day(); |
| |
| |
| const prevMonthDays: CalendarDateCell[] = getPrevMonthLastDays( |
| firstDay, |
| firstDayOfWeek - firstDay |
| ).map((day) => ({ |
| text: 0, |
| type: 'prev', |
| })) |
| const currentMonthDays: CalendarDateCell[] = getMonthDays(moment(props.month).daysInMonth()).map( |
| (day) => ({ |
| text: day, |
| type: 'current', |
| }) |
| ) |
| days = [...prevMonthDays, ...currentMonthDays] |
| const remaining = 7 - (days.length % 7 || 7) |
| const nextMonthDays: CalendarDateCell[] = rangeArr(remaining).map( |
| (_, index) => ({ |
| text: 0, |
| type: 'next', |
| }) |
| ) |
| days = days.concat(nextMonthDays) |
| |
| return toNestedArr(days) |
| }) |
| |
| const rangeArr = (n: number) => { |
| return Array.from(Array.from({ length: n }).keys()) |
| } |
| |
| const toNestedArr = (days: CalendarDateCell[]) => |
| rangeArr(days.length / 7).map((index) => { |
| const start = index * 7 |
| return days.slice(start, start + 7) |
| }); |
| const getPrevMonthLastDays = (lastDay: number, count: number) => { |
| return rangeArr(count).map((_, index) => lastDay - (count - index - 1)) |
| } |
| |
| const getMonthDays = (days: number) => { |
| return rangeArr(days).map((_, index) => index + 1) |
| } |
| |
| |
| |
| |
| const getDatesInRange = (start: number, end: number): string[] => { |
| let list = []; |
| for (let i = start; i <= end; i++) { |
| let dateStr = moment(getCellDate({ text: i, type: 'current' })).format('YYYY-MM-DD'); |
| list.push(dateStr); |
| } |
| return list; |
| } |
| const isCtrl = ref(false) |
| const isShift = ref(false) |
| const onKeyEvent = () => { |
| window.addEventListener('keydown', e => { |
| e.preventDefault(); |
| let e1 = e || window.event |
| switch (e1.keyCode) { |
| case 16: |
| isShift.value = true; |
| break; |
| case 17: |
| isCtrl.value = true; |
| break; |
| } |
| }) |
| window.addEventListener('keyup', e => { |
| e.preventDefault(); |
| let e1 = e || window.event |
| switch (e1.keyCode) { |
| case 16: |
| isShift.value = false; |
| break; |
| case 17: |
| isCtrl.value = false; |
| break; |
| } |
| }) |
| } |
| const getCellClass = (cell: CalendarDateCell, key: number) => { |
| let date = getCellDate(cell); |
| if (props.disableBefore && date.getTime() < new Date().getTime()) { |
| return ['cell', 'cell_disabled']; |
| } |
| let classes: string[] = ['cell', 'cell_enabled']; |
| if (key == 0 || key == 6) { |
| classes.push('cell_red'); |
| } |
| let index = selectList.value.indexOf(moment(date).format('YYYY-MM-DD')); |
| if (index != -1) { |
| classes.push('cell_active'); |
| } |
| return classes; |
| } |
| const getCelltriangleStyle = (cell: CalendarDateCell) => { |
| let date = getCellDate(cell); |
| let day = props.markList.find(item => item.date == moment(date).format('YYYY-MM-DD')); |
| return day ? { |
| borderTop: '30px solid #e6e911', |
| } : {}; |
| } |
| const getCellDate = (cell: CalendarDateCell) => new Date(props.month.getFullYear(), props.month.getMonth(), cell.text) |
| |
| const selectList = ref<any[]>([]); |
| const shiftNum = ref(0); |
| const lastSelect = ref<any[]>([]); |
| |
| const handlePickDay = (cell: CalendarDateCell) => { |
| let date = getCellDate(cell); |
| if (cell.type != 'current') { |
| return; |
| } |
| if (props.disableBefore && date.getTime() < new Date().getTime()) { |
| return |
| } |
| |
| let dateStr = moment(date).format('YYYY-MM-DD'); |
| let currentSelect: string[] = []; |
| |
| if (isCtrl.value) { |
| if (selectList.value.includes(dateStr)) { |
| selectList.value.splice(selectList.value.indexOf(dateStr), 1); |
| } else { |
| selectList.value.push(dateStr); |
| } |
| lastSelect.value = []; |
| } else if (isShift.value) { |
| if (shiftNum.value == 0) { |
| shiftNum.value = cell.text; |
| if (selectList.value.includes(dateStr)) { |
| selectList.value.splice(selectList.value.indexOf(dateStr), 1); |
| } else { |
| selectList.value.push(dateStr); |
| } |
| } else { |
| if (shiftNum.value < cell.text) { |
| currentSelect = getDatesInRange(shiftNum.value, cell.text); |
| } else if (shiftNum.value > cell.text) { |
| currentSelect = getDatesInRange(cell.text, shiftNum.value); |
| } else { |
| currentSelect = [dateStr]; |
| } |
| selectList.value = selectList.value.filter(item => !lastSelect.value.includes(item)); |
| selectList.value = selectList.value.concat(currentSelect); |
| lastSelect.value = currentSelect; |
| } |
| } else { |
| selectList.value = [dateStr]; |
| } |
| if (!isShift.value) { |
| shiftNum.value = cell.text; |
| } |
| selectList.value = [...new Set(selectList.value)].sort(); |
| console.log(shiftNum.value, selectList.value); |
| emits('change', selectList.value); |
| } |
| </script> |
| <style lang="scss" scoped> |
| .calendar { |
| width: 100%; |
| padding: 12px 20px 35px; |
| |
| &_header { |
| display: flex; |
| justify-content: center; |
| border: 1px solid var(--el-border-color-lighter); |
| padding: 12px 20px; |
| } |
| |
| &_table { |
| width: 100%; |
| |
| .thead_th { |
| padding: 12px 0; |
| color: var(--el-text-color-regular); |
| font-weight: 400; |
| |
| &_red { |
| color: red; |
| } |
| } |
| } |
| |
| .td { |
| border: 1px solid var(--el-border-color-lighter); |
| -moz-user-select: none; |
| |
| -webkit-user-select: none; |
| |
| -ms-user-select: none; |
| |
| -khtml-user-select: none; |
| |
| user-select: none; |
| } |
| |
| |
| .cell { |
| // background-color: #409eff; |
| position: relative; |
| text-align: center; |
| min-height: 50px; |
| display: flex; |
| justify-content: center; |
| |
| &_enabled { |
| cursor: pointer; |
| color: #373737 |
| } |
| |
| &_disabled { |
| cursor: not-allowed; |
| color: #9b9da1; |
| } |
| |
| &_red { |
| color: red; |
| } |
| |
| &_active { |
| background-color: #409eff; |
| } |
| |
| .triangle { |
| position: absolute; |
| left: 0; |
| top: 0; |
| width: 0; |
| height: 0; |
| // border-top: 30px solid #e6e911; |
| border-right: 30px solid transparent; |
| } |
| |
| &_text { |
| margin: auto; |
| } |
| } |
| |
| } |
| </style> |