184 lines
3.8 KiB
Vue
184 lines
3.8 KiB
Vue
|
|
<template>
|
|||
|
|
<header class="calendar-header">
|
|||
|
|
<!-- 左箭头 -->
|
|||
|
|
<button
|
|||
|
|
class="nav-btn"
|
|||
|
|
aria-label="上一个周期"
|
|||
|
|
@click="emit('prev')"
|
|||
|
|
>
|
|||
|
|
<van-icon name="arrow-left" />
|
|||
|
|
</button>
|
|||
|
|
|
|||
|
|
<!-- 标题内容(可点击跳转) -->
|
|||
|
|
<div
|
|||
|
|
class="header-content"
|
|||
|
|
@click="emit('jump')"
|
|||
|
|
>
|
|||
|
|
<h1 class="header-title">
|
|||
|
|
{{ formattedTitle }}
|
|||
|
|
</h1>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 右箭头 -->
|
|||
|
|
<button
|
|||
|
|
class="nav-btn"
|
|||
|
|
aria-label="下一个周期"
|
|||
|
|
@click="emit('next')"
|
|||
|
|
>
|
|||
|
|
<van-icon name="arrow" />
|
|||
|
|
</button>
|
|||
|
|
|
|||
|
|
<!-- 通知按钮 -->
|
|||
|
|
<button
|
|||
|
|
v-if="showNotification"
|
|||
|
|
class="notif-btn"
|
|||
|
|
aria-label="通知"
|
|||
|
|
@click="emit('notification')"
|
|||
|
|
>
|
|||
|
|
<van-icon name="bell" />
|
|||
|
|
</button>
|
|||
|
|
</header>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { computed } from 'vue'
|
|||
|
|
|
|||
|
|
const props = defineProps({
|
|||
|
|
type: {
|
|||
|
|
type: String,
|
|||
|
|
required: true,
|
|||
|
|
validator: (value) => ['week', 'month', 'year'].includes(value)
|
|||
|
|
},
|
|||
|
|
currentDate: {
|
|||
|
|
type: Date,
|
|||
|
|
required: true
|
|||
|
|
},
|
|||
|
|
showNotification: {
|
|||
|
|
type: Boolean,
|
|||
|
|
default: true
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const emit = defineEmits(['prev', 'next', 'jump', 'notification'])
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 计算 ISO 8601 标准的周数
|
|||
|
|
* @param date 目标日期
|
|||
|
|
* @returns 周数 (1-53)
|
|||
|
|
*/
|
|||
|
|
const getISOWeek = (date) => {
|
|||
|
|
const target = new Date(date.valueOf())
|
|||
|
|
const dayNr = (date.getDay() + 6) % 7 // 周一为0,周日为6
|
|||
|
|
target.setDate(target.getDate() - dayNr + 3) // 本周四
|
|||
|
|
const firstThursday = new Date(target.getFullYear(), 0, 4) // 该年第一个周四
|
|||
|
|
const weekDiff = Math.floor((target.valueOf() - firstThursday.valueOf()) / 86400000)
|
|||
|
|
return 1 + Math.floor(weekDiff / 7)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 计算 ISO 8601 标准的年份(用于周数)
|
|||
|
|
* 注意:年末/年初的周可能属于相邻年份
|
|||
|
|
*/
|
|||
|
|
const getISOYear = (date) => {
|
|||
|
|
const target = new Date(date.valueOf())
|
|||
|
|
const dayNr = (date.getDay() + 6) % 7
|
|||
|
|
target.setDate(target.getDate() - dayNr + 3) // 本周四
|
|||
|
|
return target.getFullYear()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 格式化标题
|
|||
|
|
const formattedTitle = computed(() => {
|
|||
|
|
const date = props.currentDate
|
|||
|
|
const year = date.getFullYear()
|
|||
|
|
const month = date.getMonth() + 1
|
|||
|
|
|
|||
|
|
switch (props.type) {
|
|||
|
|
case 'week': {
|
|||
|
|
const isoYear = getISOYear(date)
|
|||
|
|
const weekNum = getISOWeek(date)
|
|||
|
|
return `${isoYear}年第${weekNum}周`
|
|||
|
|
}
|
|||
|
|
case 'month':
|
|||
|
|
return `${year}年${month}月`
|
|||
|
|
case 'year':
|
|||
|
|
return `${year}年`
|
|||
|
|
default:
|
|||
|
|
return ''
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped lang="scss">
|
|||
|
|
@import '@/assets/theme.css';
|
|||
|
|
|
|||
|
|
/* ========== 头部 ========== */
|
|||
|
|
.calendar-header {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: flex-start;
|
|||
|
|
padding: 8px 24px;
|
|||
|
|
gap: 8px;
|
|||
|
|
background: transparent !important;
|
|||
|
|
position: relative;
|
|||
|
|
z-index: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.header-content {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 4px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
user-select: none;
|
|||
|
|
-webkit-tap-highlight-color: transparent;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.header-title {
|
|||
|
|
font-family: var(--font-primary);
|
|||
|
|
font-size: var(--font-2xl);
|
|||
|
|
font-weight: var(--font-medium);
|
|||
|
|
color: var(--text-primary);
|
|||
|
|
margin: 0;
|
|||
|
|
transition: opacity 0.2s;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.header-content:active .header-title {
|
|||
|
|
opacity: 0.6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notif-btn {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
width: 44px;
|
|||
|
|
height: 44px;
|
|||
|
|
border-radius: var(--radius-full);
|
|||
|
|
background-color: var(--bg-button);
|
|||
|
|
border: none;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: opacity 0.2s;
|
|||
|
|
margin-left: auto;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notif-btn:active {
|
|||
|
|
opacity: 0.7;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.nav-btn {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
width: 36px;
|
|||
|
|
height: 36px;
|
|||
|
|
border-radius: 18px;
|
|||
|
|
background-color: transparent;
|
|||
|
|
border: none;
|
|||
|
|
color: var(--text-secondary);
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.2s;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.nav-btn:active {
|
|||
|
|
background-color: var(--bg-tertiary);
|
|||
|
|
}
|
|||
|
|
</style>
|