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>
|