255 lines
5.6 KiB
Vue
255 lines
5.6 KiB
Vue
<template>
|
||
<div class="year-month-picker">
|
||
<!-- 年份选择器 -->
|
||
<div class="picker-group">
|
||
<div class="picker-input" @click="isYearOpen = !isYearOpen" :class="{ 'open': isYearOpen }">
|
||
<span class="value">{{ selectedYear }}年</span>
|
||
<span class="arrow"></span>
|
||
</div>
|
||
<ul class="options" v-show="isYearOpen">
|
||
<li v-for="year in years" :key="year" :class="{ 'selected': year === selectedYear }"
|
||
@click="handleYearSelect(year)">
|
||
{{ year }}年
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<!-- 月份选择器 -->
|
||
<div class="picker-group">
|
||
<div class="picker-input" @click="isMonthOpen = !isMonthOpen" :class="{ 'open': isMonthOpen }">
|
||
<span class="value">{{ selectedMonth }}月</span>
|
||
<span class="arrow"></span>
|
||
</div>
|
||
<ul class="options" v-show="isMonthOpen">
|
||
<li v-for="month in 12" :key="month" :class="{ 'selected': month === selectedMonth }"
|
||
@click="handleMonthSelect(month)">
|
||
{{ month }}月
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue';
|
||
|
||
const props = defineProps({
|
||
year: {
|
||
type: Number,
|
||
required: true,
|
||
},
|
||
month: {
|
||
type: Number,
|
||
required: true,
|
||
},
|
||
startYear: {
|
||
type: Number,
|
||
default: 2000,
|
||
},
|
||
endYear: {
|
||
type: Number,
|
||
default: new Date().getFullYear(),
|
||
},
|
||
disabled: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
});
|
||
|
||
const emit = defineEmits(['update:year', 'update:month', 'change']);
|
||
|
||
// 计算年份列表
|
||
const years = computed(() => {
|
||
const yearList = [];
|
||
for (let y = props.startYear; y <= props.endYear; y++) {
|
||
yearList.push(y);
|
||
}
|
||
return yearList;
|
||
});
|
||
|
||
// 内部状态
|
||
const selectedYear = ref(props.year);
|
||
const selectedMonth = ref(props.month);
|
||
const isYearOpen = ref(false);
|
||
const isMonthOpen = ref(false);
|
||
|
||
// 监听props变化,同步到内部状态
|
||
watch(
|
||
() => [props.year, props.month],
|
||
([newYear, newMonth]) => {
|
||
selectedYear.value = newYear;
|
||
selectedMonth.value = newMonth;
|
||
},
|
||
{ immediate: true }
|
||
);
|
||
|
||
// 当内部值变化时,通知父组件
|
||
const notifyParent = () => {
|
||
emit('update:year', selectedYear.value);
|
||
emit('update:month', selectedMonth.value);
|
||
emit('change', { year: selectedYear.value, month: selectedMonth.value });
|
||
};
|
||
|
||
// 处理年份选择
|
||
const handleYearSelect = (year) => {
|
||
selectedYear.value = year;
|
||
isYearOpen.value = false;
|
||
notifyParent();
|
||
};
|
||
|
||
// 处理月份选择
|
||
const handleMonthSelect = (month) => {
|
||
selectedMonth.value = month;
|
||
isMonthOpen.value = false;
|
||
notifyParent();
|
||
};
|
||
|
||
// 点击外部关闭下拉框
|
||
const handleClickOutside = (event) => {
|
||
if (!event.target.closest('.year-month-picker')) {
|
||
isYearOpen.value = false;
|
||
isMonthOpen.value = false;
|
||
}
|
||
};
|
||
|
||
// 挂载时添加事件监听
|
||
onMounted(() => {
|
||
document.addEventListener('click', handleClickOutside);
|
||
// 验证初始值
|
||
if (!years.value.includes(selectedYear.value)) {
|
||
selectedYear.value = Math.max(props.startYear, Math.min(selectedYear.value, props.endYear));
|
||
}
|
||
selectedMonth.value = Math.max(1, Math.min(selectedMonth.value, 12));
|
||
notifyParent(); // 确保初始值正确通知
|
||
});
|
||
|
||
// 卸载时移除事件监听,防止内存泄漏
|
||
onBeforeUnmount(() => {
|
||
document.removeEventListener('click', handleClickOutside);
|
||
});
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
$vm_base: 1920;
|
||
$vh_base: 1080;
|
||
|
||
// 计算vw
|
||
@function vw($px) {
|
||
@return calc(($px / $vm_base) * 100vw);
|
||
}
|
||
|
||
// 计算vh
|
||
@function vh($px) {
|
||
@return calc(($px / $vh_base) * 100vh);
|
||
}
|
||
|
||
.year-month-picker {
|
||
display: inline-flex;
|
||
border-radius: vw(8);
|
||
font-size: vw(14);
|
||
background-color: transparent;
|
||
box-shadow: 0 vh(2) vh(8) rgba(0, 0, 0, 0.08);
|
||
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||
border: vw(1) solid #c0c4cc;
|
||
}
|
||
|
||
.year-month-picker:hover {
|
||
border-color: #909399;
|
||
box-shadow: 0 vh(4) vh(12) rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.picker-group {
|
||
position: relative;
|
||
}
|
||
|
||
.picker-input {
|
||
width: fit-content;
|
||
display: flex;
|
||
gap: vw(8);
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: vh(8) vw(16);
|
||
cursor: pointer;
|
||
user-select: none;
|
||
border-right: vw(1) solid #e9e9e9;
|
||
}
|
||
|
||
/* 移除最后一个输入框的右边框 */
|
||
.picker-group:last-child .picker-input {
|
||
border-right: none;
|
||
}
|
||
|
||
.picker-input .value {
|
||
color: #fff;
|
||
}
|
||
|
||
.picker-input.open .arrow {
|
||
transform: rotate(180deg);
|
||
}
|
||
|
||
.arrow {
|
||
display: inline-block;
|
||
width: 0;
|
||
height: 0;
|
||
border-style: solid;
|
||
border-width: vw(6) vw(5) 0 vw(5);
|
||
border-color: #909399 transparent transparent transparent;
|
||
transition: transform 0.2s ease-in-out;
|
||
}
|
||
|
||
/* 美化后的下拉选项窗口 */
|
||
.options {
|
||
position: absolute;
|
||
top: 110%;
|
||
left: 0;
|
||
right: 0;
|
||
max-height: vh(200); /* 限制最大高度并可滚动 */
|
||
overflow-y: auto;
|
||
background-color: rgba(0, 0, 0, 0.6);
|
||
border-radius: vw(4);
|
||
box-shadow: 0 vh(4) vh(12) rgba(0, 0, 0, 0.15); /* 更明显的阴影 */
|
||
list-style: none;
|
||
z-index: 10;
|
||
padding: vh(4) 0;
|
||
margin: 0;
|
||
border: vw(1) solid #ebeef5;
|
||
}
|
||
|
||
.options li {
|
||
padding: vh(10) vw(16);
|
||
cursor: pointer;
|
||
transition: background-color 0.2s;
|
||
white-space: nowrap;
|
||
color: #fff;
|
||
}
|
||
|
||
.options li:hover {
|
||
background-color: #f5f7fa;
|
||
color: #1890ff;
|
||
}
|
||
|
||
.options li.selected {
|
||
// background-color: #e6f7ff;
|
||
color: #1890ff;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* 美化滚动条 (WebKit浏览器) */
|
||
.options::-webkit-scrollbar {
|
||
width: vw(6);
|
||
}
|
||
|
||
.options::-webkit-scrollbar-track {
|
||
background: #f1f1f1;
|
||
border-radius: vw(10);
|
||
}
|
||
|
||
.options::-webkit-scrollbar-thumb {
|
||
background: #c9c9c9;
|
||
border-radius: vw(10);
|
||
}
|
||
|
||
.options::-webkit-scrollbar-thumb:hover {
|
||
background: #a8a8a8;
|
||
}
|
||
</style> |