周选择日历组件
这是组件代码
下面展示一些 内联代码片
。
// A code block
<template><div class="week-picker"><div class="wp-header"><button class="nav" @click="prevMonth" aria-label="Previous month">‹</button><div class="title">{{ formatMonthYear(viewDate) }}</div><button class="nav" @click="nextMonth" aria-label="Next month">›</button></div><table class="wp-calendar"><thead><tr><th v-for="(d, i) in weekdayLabels" :key="i">{{ d }}</th></tr></thead><tbody><tr v-for="(week, wi) in weeks" :key="wi"@mouseenter="hoverWeekIndex = wi"@mouseleave="hoverWeekIndex = null"><td v-for="(day, di) in week" :key="di":class="cellClass(day, wi)"@click="onDayClick(day, wi)"><div class="date">{{ day.getDate() }}</div></td></tr></tbody></table><div class="wp-footer" v-if="showFooter"><div class="range">选中周:{{ selectedRangeText }}</div></div></div>
</template><script setup>import { ref, computed, watch } from 'vue'const props = defineProps({modelValue: { type: [String, Date], default: null },weekStartsOn: { type: Number, default: 1 }, // 0 = Sunday, 1 = MondayvalueType: { type: String, default: 'string' }, // emitted value type: 'string' | 'date'showFooter: { type: Boolean, default: false },})const emit = defineEmits(['update:modelValue', 'change'])function parseToDate(v) {if (!v) return nullif (v instanceof Date) return new Date(v.getFullYear(), v.getMonth(), v.getDate())const s = String(v)const m = s.match(/^(\d{4})-(\d{2})-(\d{2})/)if (m) return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]))const d = new Date(s)if (!isNaN(d.getTime())) return new Date(d.getFullYear(), d.getMonth(), d.getDate())return null}function formatDateYYYYMMDD(d) {const y = d.getFullYear()const m = String(d.getMonth() + 1).padStart(2, '0')const day = String(d.getDate()).padStart(2, '0')return `${y}-${m}-${day}`}function startOfWeek(date, weekStartsOn) {const d = new Date(date.getFullYear(), date.getMonth(), date.getDate())const day = d.getDay()const diff = (day - weekStartsOn + 7) % 7d.setDate(d.getDate() - diff)d.setHours(0, 0, 0, 0)return d}function isSameDay(a, b) {return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()}function isSameWeek(a, b) {if (!a || !b) return falseconst sa = startOfWeek(a, props.weekStartsOn)const sb = startOfWeek(b, props.weekStartsOn)return isSameDay(sa, sb)}const today = new Date()const viewDate = ref(new Date(today.getFullYear(), today.getMonth(), 1))const internalSelectedStart = ref(props.modelValue? startOfWeek(parseToDate(props.modelValue), props.weekStartsOn): startOfWeek(today, props.weekStartsOn) // 默认选中当前周)// 同时触发一次默认值给父组件if (!props.modelValue) {let emitValue = internalSelectedStart.valueif (props.valueType === 'string') emitValue = formatDateYYYYMMDD(emitValue)emit('update:modelValue', emitValue)emit('change', emitValue)}watch(() => props.modelValue, (nv) => {internalSelectedStart.value = nv ? startOfWeek(parseToDate(nv), props.weekStartsOn) : null})const weekdayLabels = computed(() => {if (props.weekStartsOn === 1) {return ['一', '二', '三', '四', '五', '六', '日']}return ['日', '一', '二', '三', '四', '五', '六']})function getMonthMatrix(baseDate) {const firstOfMonth = new Date(baseDate.getFullYear(), baseDate.getMonth(), 1)const start = startOfWeek(firstOfMonth, props.weekStartsOn)const matrix = []for (let i = 0; i < 6; i++) {const week = []for (let j = 0; j < 7; j++) {const d = new Date(start)d.setDate(start.getDate() + i * 7 + j)week.push(d)}matrix.push(week)}return matrix}const weeks = computed(() => getMonthMatrix(viewDate.value))const hoverWeekIndex = ref(null)function cellClass(day, weekIndex) {const classes = {}classes['muted'] = day.getMonth() !== viewDate.value.getMonth()const weekStart = startOfWeek(weeks.value[weekIndex][0], props.weekStartsOn)classes['selected-week'] = internalSelectedStart.value ? isSameDay(weekStart, internalSelectedStart.value) : falseclasses['hover-week'] = hoverWeekIndex.value === weekIndexclasses['today'] = isSameDay(day, today)return classes}function onDayClick(day, weekIndex) {const weekStart = startOfWeek(day, props.weekStartsOn)internalSelectedStart.value = weekStartlet emitValue = weekStartif (props.valueType === 'string') emitValue = formatDateYYYYMMDD(weekStart)emit('update:modelValue', emitValue)emit('change', emitValue)}function prevMonth() {viewDate.value = new Date(viewDate.value.getFullYear(), viewDate.value.getMonth() - 1, 1)}function nextMonth() {viewDate.value = new Date(viewDate.value.getFullYear(), viewDate.value.getMonth() + 1, 1)}const selectedRangeText = computed(() => {if (!internalSelectedStart.value) return '—'const start = internalSelectedStart.valueconst end = new Date(start)end.setDate(start.getDate() + 6)return `${formatDateYYYYMMDD(start)} 至 ${formatDateYYYYMMDD(end)}`})function clear() {internalSelectedStart.value = nullemit('update:modelValue', null)emit('change', null)}function formatMonthYear(d) {return `${d.getFullYear()} 年 ${d.getMonth() + 1} 月`}</script><style scoped>.week-picker {width: 100%;padding: 8px;font-family: "Helvetica Neue", Arial, sans-serif;user-select: none;}.wp-header {display: flex;align-items: center;justify-content: space-between;margin-bottom: 6px;}.nav {border: none;background: transparent;font-size: 28px;cursor: pointer;padding: 4px 8px;}.title {font-weight: 600;}.wp-calendar {width: 100%;border-collapse: collapse;table-layout: fixed;}.wp-calendar th, .wp-calendar td {width: 14.2857%;text-align: center;padding: 6px 2px;}.wp-calendar thead th {font-weight: 600;font-size: 12px;color: #666;}.wp-calendar td {cursor: pointer;}.wp-calendar td .date {width: 28px;height: 28px;line-height: 28px;margin: 0 auto;}.wp-calendar td.muted {color: #bbb;}.wp-calendar td.today .date {background-color: #4E80EE;color: #fff;border-radius: 50%;}.wp-calendar td.hover-week {background: rgba(0, 122, 255, 0.06);}.wp-calendar td.hover-week:first-child {border-radius: 6px 0 0 6px;}.wp-calendar td.hover-week:last-child {border-radius: 0 6px 6px 0;}.wp-calendar td.selected-week {background: rgba(0, 122, 255, 0.12);}.wp-calendar td.selected-week:first-child {border-radius: 6px 0 0 6px;}.wp-calendar td.selected-week:last-child {border-radius: 0 6px 6px 0;}.wp-footer {display: flex;justify-content: space-between;align-items: center;margin-top: 8px;font-size: 13px;}.actions button {border: 1px solid #e6e6e6;background: #fff;padding: 4px 8px;border-radius: 4px;cursor: pointer;}
</style>
效果图: