组件主要实现四类选择器:日期选择、月份选择、日期范围选择、月份范围选择。
组件主页 index.vue
<script setup>
import DayPicker from '@/components/datepicker/dayPicker'
import MonthPicker from '@/components/datepicker/monthPicker'
import DayRangePicker from '@/components/datepicker/dayRangePicker'
import MonthRangePicker from '@/components/datepicker/monthRangePicker'
const emit = defineEmits();
const datepicker = ref(null);
const v_datepicker = ref(null);
const show = ref(false);
const selectValue = ref(null);
const props = defineProps({
type: {
type: String,
default: 'day'
},
defaultIcon: {
type: String,
default: require('@/assets/images/date.svg')
},
placeholder: {
type: String,
default: '请选择日期'
},
size: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
},
closeable: {
type: Boolean,
default: false
}
});
watch(show, (n, o) => {
datepicker.value.style.border = n ? '1px solid #1890FF' : '1px solid #ebe7e7';
})
const delete_icon = computed(() => {
return props.closeable && selectValue.value ? require('@/assets/images/close.svg') : '';
})
// 点击选择器
const onclick = (e) => {
if (props.disabled) {
e.preventDefault();
} else {
show.value = true;
}
}
// 删除内容
const deleteContent = () => {
if (delete_icon.value) {
selectValue.value = null;
v_datepicker.value.innerText = '';
}
}
const show_data = ref(0);
const showData = (v) => {
show_data.value = v;
show.value = false;
nextTick(() => {
show_data.value = 0;
})
}
import mit from '@/utils/mitt'
mit.on('showData', showData);
const dateCheck = (v) => {
show.value = false;
let option = 'day-check';
switch (props.type) {
case 'day':
option = 'day-check';
v_datepicker.value.innerText = v;
selectValue.value = v;
break;
case 'dayrange':
option = 'day-range-check';
v_datepicker.value.innerText = `${v[0]} ~ ${v[1]}`;
selectValue.value = v;
break;
case 'month':
option = 'month-check';
v_datepicker.value.innerText = v;
selectValue.value = v;
break;
case 'monthrange':
option = 'month-range-check';
v_datepicker.value.innerText = `${v[0]} ~ ${v[1]}`;
selectValue.value = v;
break;
default:
break;
}
emit(option, v);
}
</script>
<template>
<div :class="{'disabled':disabled}" class="date-picker-container date-picker-width" @click="onclick" ref="datepicker">
<div class="datepicker" :placeholder="placeholder" :class="{'margin-right-30': closeable,'mini':size==='mini'}"
ref="v_datepicker">
</div>
<img :class="size==='mini'?'mini-date-icon':'date-icon'" :src="defaultIcon" width="20" height="20" />
<img class="close" :src="delete_icon" width="15" height="15" @click.stop="deleteContent" v-if="delete_icon" />
<div :class="['down-select-panel',{'range-panel-width':type==='dayrange'||type==='monthrange'}]" v-if="show">
<div>
<p>
<day-picker :value="selectValue" @date-check="dateCheck" v-if="type==='day'" />
<month-picker :value="selectValue" @date-check="dateCheck" v-else-if="type==='month'" />
<day-range-picker :value="selectValue" @date-check="dateCheck" v-else-if="type==='dayrange'" />
<month-range-picker :value="selectValue" @date-check="dateCheck" v-else-if="type==='monthrange'" />
</p>
</div>
</div>
<div class="overdelay" v-if="!show_data&&show" @click.stop="show=false"></div>
</div>
</template>
<style lang="scss" scoped>
.date-picker-container {
position: relative;
font-size: 14px;
line-height: 20px;
border: 1px solid $colorLightGray;
border-radius: 3px;
cursor: pointer;
}
.date-picker-width {
width: 240px;
}
.date-icon {
position: absolute;
top: -2px;
left: 5px;
transform: translateY(50%);
}
.mini-date-icon {
position: absolute;
top: -1px;
left: 5px;
transform: translateY(20%);
}
.range-panel-width {
width: 640px;
}
.down-select-panel {
min-width: 320px;
position: absolute;
border-radius: 5px;
border: 1px solid $colorLightGray;
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
background-color: $colorWhite;
margin-top: 15px;
z-index: 999;
}
.down-select-panel > div {
width: 100%;
overflow-y: overlay;
}
.down-select-panel > div > p {
margin: 0;
padding: 12px 0;
}
.down-select-panel::before,
.down-select-panel::after {
content: "";
width: 0;
height: 0;
border-style: solid;
border-width: 8px;
position: absolute;
left: 40px;
}
.down-select-panel::before,
.down-select-panel::after {
top: -16px;
}
.down-select-panel::before {
border-color: transparent transparent $colorLightGray transparent;
}
.down-select-panel::after {
border-color: transparent transparent $colorWhite transparent;
}
.overdelay {
width: 100%;
height: 100%;
opacity: 0;
position: fixed;
top: 0;
left: 0;
z-index: 1;
}
.selected-content {
font-weight: bold;
color: $colorBlue;
background-color: $colorBgText;
}
.datepicker {
text-align: left;
margin: 8px 16px;
padding-left: 15px;
white-space: nowrap;
overflow-x: auto;
overflow-y: hidden;
}
.mini {
font-size: 12px;
line-height: 12px;
}
.datepicker:empty::before {
content: attr(placeholder);
color: $colorLightGray;
}
.close {
position: absolute;
top: 50%;
right: 0;
font-size: 12px;
transform: translate(-10px, -50%);
}
.margin-right-30 {
margin-right: 30px;
}
/* 禁用 */
.disabled {
cursor: no-drop;
opacity: 0.5;
background-color: $colorDisabled;
}
.disabled-event {
pointer-events: none;
}
</style>
日期选择器 dayPicker.vue
<!--
* @Description: 日期选择器
* @Author: dinghao
* @Date: 2022-06-29 15:06:22
* @LastEditTime: 2022-07-22 15:48:05
* @LastEditors: dinghao
-->
<script setup>
const weeks = ['日', '一', '二', '三', '四', '五', '六'];
const emit = defineEmits();
const props = defineProps({
value: {
type: String,
default: null
}
});
// 当前日期
const now_date = new Date();
// 当前格式化日期 yyyy/MM/dd
const now_date_format = now_date.toLocaleDateString();
const year = ref(now_date.getFullYear());
const month = ref(now_date.getMonth());
// 左右年月操作
const operator = (type) => {
switch (type) {
case '+year': // 加年
year.value++;
break;
case '+month': // 加月
if (month.value === 12) { // 跨年年+1,月重置为1、否则月+1
year.value++;
month.value = 1;
} else {
month.value++;
}
break;
case '-year': // 减年
year.value--;
break;
case '-month': // 减月
if (month.value === 1) { // 跨年年-1,月重置为12、否则月-1
year.value--;
month.value = 12;
} else {
month.value--;
}
break;
default:
break;
}
}
// 上一月部分日期
const prev_date = computed(() => {
return calendar('prev', year.value, month.value);
})
// 本月日期
const current_date = computed(() => {
return calendar('current', year.value, month.value);
})
// 下一月部分日期
const next_date = computed(() => {
return calendar('next', year.value, month.value);
})
// 计算显示每一月时得到的数组:包含上一月部分日期、本月日期、下一月部分日期
const calendar = (type, year, month) => {
const lastdays = new Date(year, month, 0).getDate();
const days = new Date(year, month + 1, 0).getDate();
const week = new Date(year, month, 1).getDay();
const prev = Array.from({ length: week }, (el, i) =>
[month == 0 ? year - 1 : year, month == 0 ? 12 : month, lastdays + i - week + 1]
);
const current = Array.from({ length: days }, (el, i) =>
[year, month + 1, i + 1]
);
const next = Array.from({ length: 42 - days - week }, (el, i) =>
[month == 11 ? year + 1 : year, month == 11 ? 1 : month + 2, i + 1]
);
return type === 'prev' ? prev : type === 'current' ? current : next;
}
import { addZero } from '@/utils'
// 选中日期
const checkDate = (item) => {
year.value = item[0];
month.value = item[1] - 1;
emit('date-check', addZero(`${item[0]}/${item[1]}/${item[2]}`));
}
onMounted(() => {
if (props.value) {
year.value = Number(props.value.split('/')[0]);
month.value = Number(props.value.split('/')[1]) - 1;
}
})
</script>
<template>
<div>
<div class="picker-top">
<div>
<span class="left-db-arrow" @click.stop="operator('-year')"></span>
<span class="left-arrow" @click.stop="operator('-month')"></span>
</div>
<div>{{year}} 年 {{month+1}} 月</div>
<div>
<span class="right-arrow" @click.stop="operator('+month')"></span>
<span class="right-db-arrow" @click.stop="operator('+year')"></span>
</div>
</div>
<div class="picker-center">
<span v-for="item in weeks">{{item}}</span>
</div>
<div class="divider"></div>
<div class="picker-bottom">
<!-- 上一月显示日期 -->
<div v-for="item in prev_date">
<span :class="['not-current-date',{'current-selected-date':addZero(`${item[0]}/${item[1]}/${item[2]}`)===value}]"
@click.stop="checkDate(item)">{{item[2]}}</span>
</div>
<!-- 当前月显示日期 -->
<div v-for="item in current_date">
<span
:class="[{'now-date':addZero(`${item[0]}/${item[1]}/${item[2]}`)===addZero(now_date_format)},{'current-selected-date':addZero(`${item[0]}/${item[1]}/${item[2]}`)===value}]"
@click.stop="checkDate(item)">{{item[2]}}</span>
</div>
<!-- 下一月显示日期 -->
<div v-for="item in next_date">
<span :class="['not-current-date',{'current-selected-date':addZero(`${item[0]}/${item[1]}/${item[2]}`)===value}]"
@click.stop="checkDate(item)">{{item[2]}}</span>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.picker-top {
@include flex-row(space-between, center);
height: 40px;
padding: 0 20px;
font-size: 16px;
cursor: default;
}
.picker-top > div {
@include flex-row(center, center);
}
.picker-center {
@include flex-row(space-evenly, center);
padding: 20px 0 10px;
font-size: 12px;
cursor: default;
}
.divider {
height: 0.5px;
margin: 0 20px;
background-color: $colorLightGray;
}
.picker-bottom {
@include flex-row(space-evenly, center);
flex-wrap: wrap;
padding: 0 15px;
font-size: 12px;
cursor: default;
}
.picker-bottom > div {
@include flex-row(center, center);
width: calc(100% / 7);
height: 40px;
}
.picker-bottom > div > span {
@include flex-row(center, center);
@include width-height(25px, 25px);
cursor: pointer;
}
.now-date {
font-weight: bold;
color: $colorBlue;
}
.current-selected-date {
color: $colorWhite !important;
border-radius: 50%;
background-color: $colorBlue;
}
.not-current-date {
color: $colorDullGray;
}
.picker-bottom > div > span:hover {
color: $colorBlue;
}
.left-arrow,
.left-db-arrow,
.right-arrow,
.right-db-arrow {
@include width-height;
display: inline-block;
background-size: 100% 100%;
cursor: pointer;
}
.left-arrow {
margin-left: 10px;
background-image: url("@/assets/images/left_arrow.svg");
}
.left-arrow:hover {
background-image: url("@/assets/images/left_arrow_active.svg");
}
.left-db-arrow {
background-image: url("@/assets/images/left_db_arrow.svg");
}
.left-db-arrow:hover {
background-image: url("@/assets/images/left_db_arrow_active.svg");
}
.right-arrow {
margin-right: 10px;
background-image: url("@/assets/images/right_arrow.svg");
}
.right-arrow:hover {
background-image: url("@/assets/images/right_arrow_active.svg");
}
.right-db-arrow {
background-image: url("@/assets/images/right_db_arrow.svg");
}
.right-db-arrow:hover {
background-image: url("@/assets/images/right_db_arrow_active.svg");
}
</style>
月份选择器 monthPicker.vue
<script setup>
const months = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'];
const emit = defineEmits();
const props = defineProps({
value: {
type: String,
default: null
}
});
// 当前日期
const now_date = new Date();
const now_month = `${now_date.getFullYear()}${now_date.getMonth()}`;
const year = ref(now_date.getFullYear());
// 左右年月操作
const operator = (type) => {
type === '+year' ? year.value++ : year.value--;
}
// 本年月份
const current_month = computed(() => {
return calendar(year.value);
})
// 计算显示每一月时得到的数组:包含上一月部分日期、本月日期、下一月部分日期
const calendar = (year) => {
return [[year], [...months]]
}
import { addZero } from '@/utils'
// 选中日期
const checkMonth = (index) => {
emit('date-check', addZero(`${current_month.value[0]}/${index + 1}`));
}
onMounted(() => {
if (props.value) {
year.value = Number(props.value.split('/')[0]);
}
})
</script>
<template>
<div>
<div class="picker-top">
<div>
<span class="left-db-arrow" @click.stop="operator('-year')"></span>
</div>
<div>{{year}} 年</div>
<div>
<span class="right-db-arrow" @click.stop="operator('+year')"></span>
</div>
</div>
<div class="divider"></div>
<div class="picker-bottom">
<!-- 当前年显示月份 -->
<div v-for="(item,index) in current_month[1]">
<span @click.stop="checkMonth(index)" :class="[{'now-month':addZero(`${current_month[0]}${index}`)===now_month},
{'current-selected-month':addZero(`${current_month[0]}/${index+1}`)===value}]">{{item}}</span>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.picker-top {
@include flex-row(space-between, center);
height: 40px;
padding: 0 20px;
font-size: 16px;
cursor: default;
}
.picker-top > div {
@include flex-row(center, center);
}
.divider {
height: 0.5px;
margin: 0 20px;
background-color: $colorLightGray;
}
.picker-bottom {
@include flex-row(space-evenly, center);
flex-wrap: wrap;
padding: 0 15px;
font-size: 12px;
cursor: default;
}
.picker-bottom > div {
@include flex-row(center, center);
width: calc(100% / 4);
height: 66px;
}
.picker-bottom > div > span {
@include flex-row(center, center);
@include width-height(40px, 40px);
cursor: pointer;
}
.now-month {
font-weight: bold;
color: $colorBlue;
}
.current-selected-month {
color: $colorWhite !important;
border-radius: 50%;
background-color: $colorBlue;
}
.picker-bottom > div > span:hover {
color: $colorBlue;
}
.left-db-arrow,
.right-db-arrow {
@include width-height;
display: inline-block;
background-size: 100% 100%;
cursor: pointer;
}
.left-db-arrow {
background-image: url("@/assets/images/left_db_arrow.svg");
}
.left-db-arrow:hover {
background-image: url("@/assets/images/left_db_arrow_active.svg");
}
.right-db-arrow {
background-image: url("@/assets/images/right_db_arrow.svg");
}
.right-db-arrow:hover {
background-image: url("@/assets/images/right_db_arrow_active.svg");
}
</style>
日期范围选择器 dayRangePicker.vue
<script setup>
const weeks = ['日', '一', '二', '三', '四', '五', '六'];
const emit = defineEmits();
const props = defineProps({
value: {
type: Array,
default: []
}
});
// 当前日期
const now_date = new Date();
// 当前格式化日期 yyyy/MM/dd
const now_date_format = now_date.toLocaleDateString();
const year = ref(now_date.getFullYear());
const month = ref(now_date.getMonth());
// 左右年月操作
const operator = (type) => {
switch (type) {
case '+year': // 加年
year.value++;
break;
case '+month': // 加月
if (month.value === 12) { // 跨年年+1,月重置为1、否则月+1
year.value++;
month.value = 1;
} else {
month.value++;
}
break;
case '-year': // 减年
year.value--;
break;
case '-month': // 减月
if (month.value === 1) { // 跨年年-1,月重置为12、否则月-1
year.value--;
month.value = 12;
} else {
month.value--;
}
break;
default:
break;
}
}
// 上一月部分日期
const prev_date = computed(() => {
return calendar('prev', year.value, month.value);
})
// 本月日期
const current_date = computed(() => {
return calendar('current', year.value, month.value);
})
// 下一月部分日期
const next_date = computed(() => {
return calendar('next', year.value, month.value);
})
// 上一月部分日期
const end_prev_date = computed(() => {
return calendar('prev', year.value, month.value + 1);
})
// 本月日期
const end_current_date = computed(() => {
return calendar('current', year.value, month.value + 1);
})
// 下一月部分日期
const end_next_date = computed(() => {
return calendar('next', year.value, month.value + 1);
})
// 计算显示每一月时得到的数组:包含上一月部分日期、本月日期、下一月部分日期
const calendar = (type, year, month) => {
const lastdays = new Date(year, month, 0).getDate();
const days = new Date(year, month + 1, 0).getDate();
const week = new Date(year, month, 1).getDay();
const prev = Array.from({ length: week }, (el, i) =>
[month == 0 ? year - 1 : year, month == 0 ? 12 : month, lastdays + i - week + 1]
);
const current = Array.from({ length: days }, (el, i) =>
[year, month + 1, i + 1]
);
const next = Array.from({ length: 42 - days - week }, (el, i) =>
[month == 11 ? year + 1 : year, month == 11 ? 1 : month + 2, i + 1]
);
return type === 'prev' ? prev : type === 'current' ? current : next;
}
import { addZero } from '@/utils'
const start_date = ref(null);
const end_date = ref(null);
const flag = ref(0);
// 选中起始日期
const checkStartDate = (item) => {
flag.value++;
if (start_date.value && end_date.value) {
start_date.value = null;
end_date.value = null;
}
if (flag.value !== 2) {
start_date.value = addZero(`${item[0]}/${item[1]}/${item[2]}`);
} else {
if (!start_date.value) {
start_date.value = addZero(`${item[0]}/${item[1]}/${item[2]}`);
}
if (start_date.value && !end_date.value) {
end_date.value = addZero(`${item[0]}/${item[1]}/${item[2]}`);
}
}
let arr = [];
if (end_date.value) {
if (Number(start_date.value.split('/')[1]) > Number(end_date.value.split('/')[1])) {
arr = [start_date.value, end_date.value];
} else {
if (Number(start_date.value.replaceAll('/', '')) > Number(end_date.value.replaceAll('/', ''))) {
arr = [end_date.value, start_date.value];
} else {
arr = [start_date.value, end_date.value];
}
}
emit('date-check', arr);
}
}
// 选中结束日期
const checkEndDate = (item) => {
if (start_date.value && end_date.value) {
start_date.value = null;
end_date.value = null;
}
if (!start_date.value && end_date.value) {
start_date.value = addZero(`${item[0]}/${item[1]}/${item[2]}`);
}
if (!end_date.value) {
end_date.value = addZero(`${item[0]}/${item[1]}/${item[2]}`);
}
let arr = [];
if (start_date.value) {
if (Number(start_date.value.split('/')[1]) > Number(end_date.value.split('/')[1])) {
arr = [start_date.value, end_date.value];
} else {
if (flag.value === 1 || Number(start_date.value.replaceAll('/', '')) <= Number(end_date.value.replaceAll('/', ''))) {
arr = [start_date.value, end_date.value];
} else {
arr = [end_date.value, start_date.value];
}
}
emit('date-check', arr);
}
}
onMounted(() => {
if (props.value && props.value.length > 0) {
year.value = Number(props.value[0].split('/')[0]);
month.value = Number(props.value[0].split('/')[1] - 1);
start_date.value = props.value[0];
end_date.value = props.value[1];
}
})
</script>
<template>
<div class="day-range">
<div>
<div class="picker-start-top">
<div>
<span class="left-db-arrow" @click.stop="operator('-year')"></span>
<span class="left-arrow" @click.stop="operator('-month')"></span>
</div>
<div>{{year}} 年 {{month+1}} 月</div>
</div>
<div class="picker-center">
<span v-for="item in weeks">{{item}}</span>
</div>
<div class="divider"></div>
<div class="picker-bottom">
<!-- 上一月显示日期 -->
<div v-for="item in prev_date">
<span
:class="['not-current-date',{'current-selected-date':addZero(`${item[0]}/${item[1]}/${item[2]}`)===start_date}]"
@click.stop="checkStartDate(item)">{{item[2]}}</span>
</div>
<!-- 当前月显示日期 -->
<div v-for="item in current_date" :class="[
{'select-area': start_date&&end_date&&
addZero(`${item[0]}/${item[1]}/${item[2]}`)>=start_date&&
addZero(`${item[0]}/${item[1]}/${item[2]}`)<=end_date
},
{'start-radius':addZero(`${item[0]}/${item[1]}/${item[2]}`)===start_date},
{'end-radius':addZero(`${item[0]}/${item[1]}/${item[2]}`)===end_date}]">
<span :class="[{'now-date':addZero(`${item[0]}/${item[1]}/${item[2]}`)===addZero(now_date_format)},
{'current-selected-date':addZero(`${item[0]}/${item[1]}/${item[2]}`)===start_date||
addZero(`${item[0]}/${item[1]}/${item[2]}`)===end_date}]"
@click.stop="checkStartDate(item)">{{item[2]}}</span>
</div>
<!-- 下一月显示日期 -->
<div v-for="item in next_date">
<span
:class="['not-current-date',{'current-selected-date':addZero(`${item[0]}/${item[1]}/${item[2]}`)===start_date}]"
@click.stop="checkStartDate(item)">{{item[2]}}</span>
</div>
</div>
</div>
<div>
<div class="picker-end-top">
<div>{{year}} 年 {{month+2}} 月</div>
<div>
<span class="right-arrow" @click.stop="operator('+month')"></span>
<span class="right-db-arrow" @click.stop="operator('+year')"></span>
</div>
</div>
<div class="picker-center">
<span v-for="item in weeks">{{item}}</span>
</div>
<div class="divider"></div>
<div class="picker-bottom">
<!-- 上一月显示日期 -->
<div v-for="item in end_prev_date">
<span class="not-current-date" @click.stop="checkEndDate(item)">{{item[2]}}</span>
</div>
<!-- 当前月显示日期 -->
<div v-for="item in end_current_date" :class="[
{'select-area': start_date&&end_date&&
addZero(`${item[0]}/${item[1]}/${item[2]}`)>=start_date&&
addZero(`${item[0]}/${item[1]}/${item[2]}`)<=end_date
},
{'start-radius':addZero(`${item[0]}/${item[1]}/${item[2]}`)===start_date},
{'end-radius':addZero(`${item[0]}/${item[1]}/${item[2]}`)===end_date}]">
<span
:class="[{'now-date':addZero(`${item[0]}/${item[1]}/${item[2]}`)===addZero(now_date_format)},
{'current-selected-date':addZero(`${item[0]}/${item[1]}/${item[2]}`)===start_date||addZero(`${item[0]}/${item[1]}/${item[2]}`)===end_date}]"
@click.stop="checkEndDate(item)">{{item[2]}}</span>
</div>
<!-- 下一月显示日期 -->
<div v-for="item in end_next_date">
<span class="not-current-date" @click.stop="checkEndDate(item)">{{item[2]}}</span>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.day-range,
.picker-start-top {
@include flex-row(flex-start, center);
}
.picker-start-top,
.picker-end-top {
height: 40px;
padding: 0 20px;
font-size: 16px;
cursor: default;
position: relative;
}
.picker-end-top {
@include flex-row(flex-end, center);
}
.picker-start-top > div,
.picker-end-top > div {
@include flex-row(center, center);
}
.picker-start-top > :last-child {
position: absolute;
left: 50%;
transform: translateX(-50%);
}
.picker-end-top > :first-child {
position: absolute;
right: 50%;
transform: translateX(50%);
}
.picker-center {
@include flex-row(space-evenly, center);
padding: 20px 0 10px;
font-size: 12px;
cursor: default;
}
.divider {
height: 0.5px;
margin: 0 20px;
background-color: $colorLightGray;
}
.picker-bottom {
@include flex-row(space-evenly, center);
flex-wrap: wrap;
padding: 0 15px;
font-size: 12px;
cursor: default;
}
.picker-bottom > div {
@include flex-row(center, center);
width: calc(100% / 7);
padding: 2.5px 0;
margin: 5px 0;
}
.picker-bottom > div > span {
@include flex-row(center, center);
@include width-height(30px, 30px);
cursor: pointer;
}
.now-date {
font-weight: bold;
color: $colorBlue;
}
.current-selected-date {
color: $colorWhite !important;
border-radius: 50%;
background-color: $colorBlue;
}
.not-current-date {
color: $colorDullGray;
}
.picker-bottom > div > span:hover {
color: $colorBlue;
}
.start-radius {
border-top-left-radius: 50px;
border-bottom-left-radius: 50px;
}
.end-radius {
border-top-right-radius: 50px;
border-bottom-right-radius: 50px;
}
.select-area {
background-color: $colorBgText;
}
.left-arrow,
.left-db-arrow,
.right-arrow,
.right-db-arrow {
@include width-height;
display: inline-block;
background-size: 100% 100%;
cursor: pointer;
}
.left-arrow {
margin-left: 10px;
background-image: url("@/assets/images/left_arrow.svg");
}
.left-arrow:hover {
background-image: url("@/assets/images/left_arrow_active.svg");
}
.left-db-arrow {
background-image: url("@/assets/images/left_db_arrow.svg");
}
.left-db-arrow:hover {
background-image: url("@/assets/images/left_db_arrow_active.svg");
}
.right-arrow {
margin-right: 10px;
background-image: url("@/assets/images/right_arrow.svg");
}
.right-arrow:hover {
background-image: url("@/assets/images/right_arrow_active.svg");
}
.right-db-arrow {
background-image: url("@/assets/images/right_db_arrow.svg");
}
.right-db-arrow:hover {
background-image: url("@/assets/images/right_db_arrow_active.svg");
}
</style>
月份范围选择器 monthRangePicker.vue
<script setup>
const months = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'];
const emit = defineEmits();
const props = defineProps({
value: {
type: Array,
default: []
}
});
// 当前日期
const now_date = new Date();
const now_month = `${now_date.getFullYear()}${now_date.getMonth()}`;
const year = ref(now_date.getFullYear());
// 左右年月操作
const operator = (type) => {
type === '+year' ? year.value++ : year.value--;
}
// 本年月份
const current_month = computed(() => {
return calendar(year.value);
})
// 计算显示每一月时得到的数组:包含上一月部分日期、本月日期、下一月部分日期
const calendar = (year) => {
return [[year], [...months]]
}
import { addZero } from '@/utils'
const start_month = ref(null);
const end_month = ref(null);
const flag = ref(0);
// 选择开始日期
const checkStartMonth = (index) => {
flag.value++;
if (start_month.value && end_month.value) {
start_month.value = null;
end_month.value = null;
}
if (flag.value !== 2) {
start_month.value = addZero(`${current_month.value[0]}/${index + 1}`)
} else {
if (!start_month.value) {
start_month.value = addZero(`${current_month.value[0]}/${index + 1}`)
}
if (start_month.value && !end_month.value) {
end_month.value = addZero(`${current_month.value[0]}/${index + 1}`)
}
}
let arr = [];
if (end_month.value) {
if (Number(start_month.value.substr(0, 4)) > Number(end_month.value.substr(0, 4))) {
arr = [start_month.value, end_month.value];
} else {
if (Number(start_month.value.replaceAll('/', '')) > Number(end_month.value.replaceAll('/', ''))) {
arr = [end_month.value, start_month.value];
} else {
arr = [start_month.value, end_month.value];
}
}
emit('date-check', arr);
}
}
// 选择结束日期
const checkEndMonth = (index) => {
if (start_month.value && end_month.value) {
start_month.value = null;
end_month.value = null;
}
if (!start_month.value && end_month.value) {
start_month.value = addZero(`${+current_month.value[0] + 1}/${index + 1}`);
}
if (!end_month.value) {
end_month.value = addZero(`${+current_month.value[0] + 1}/${index + 1}`);
}
let arr = [];
if (start_month.value) {
if (flag.value === 1 || Number(start_month.value.replaceAll('/', '')) <= Number(end_month.value.replaceAll('/', ''))) {
arr = [start_month.value, end_month.value];
} else {
arr = [end_month.value, start_month.value];
}
emit('date-check', arr);
}
}
// 打开月份选择器时展示框内年份与月份范围
onMounted(() => {
if (props.value && props.value.length > 0) {
year.value = Number(props.value[0].split('/')[0]);
start_month.value = props.value[0];
end_month.value = props.value[1];
}
})
</script>
<template>
<div class="month-range">
<div>
<div class="picker-start-top">
<div>
<span class="left-db-arrow" @click.stop="operator('-year')"></span>
</div>
<div>{{year}} 年</div>
</div>
<div class="divider"></div>
<div class="picker-bottom">
<!-- 当前年显示月份 -->
<div v-for="(item,index) in current_month[1]" :class="[
{'select-area': start_month&&end_month&&
addZero(`${current_month[0]}/${index+1}`)>=start_month&&
addZero(`${current_month[0]}/${index+1}`)<=end_month},
{'start-radius':addZero(`${current_month[0]}/${index+1}`)===start_month},
{'end-radius':addZero(`${current_month[0]}/${index+1}`)===end_month}]">
<span @click.stop="checkStartMonth(index)" :class="[
{'now-month':`${year}${index}`===now_month},
{'current-selected-month':addZero(`${current_month[0]}/${index+1}`)===start_month||
addZero(`${current_month[0]}/${index+1}`)===end_month}]">{{item}}</span>
</div>
</div>
</div>
<div>
<div class="picker-end-top">
<div>{{year+1}} 年</div>
<div>
<span class="right-db-arrow" @click.stop="operator('+year')"></span>
</div>
</div>
<div class="divider"></div>
<div class="picker-bottom">
<!-- 当前年显示月份 -->
<div v-for="(item,index) in current_month[1]" :class="[
{'select-area':start_month&&end_month&&
addZero(`${+current_month[0]+1}/${index+1}`)>=start_month&&
addZero(`${+current_month[0]+1}/${index+1}`)<=end_month},
{'start-radius':addZero(`${+current_month[0]+1}/${index+1}`)===start_month},
{'end-radius':addZero(`${+current_month[0]+1}/${index+1}`)===end_month}]">
<span @click.stop="checkEndMonth(index)" :class="[
{'now-month':`${year+1}${index}`===now_month},
{'current-selected-month':addZero(`${+current_month[0]+1}/${index+1}`)===end_month||
addZero(`${+current_month[0]+1}/${index+1}`)===start_month}]">{{item}}</span>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.month-range,
.picker-start-top {
@include flex-row(flex-start, center);
}
.picker-start-top,
.picker-end-top {
height: 40px;
padding: 0 20px;
font-size: 16px;
cursor: default;
position: relative;
}
.picker-end-top {
@include flex-row(flex-end, center);
}
.picker-start-top > div,
.picker-end-top > div {
@include flex-row(center, center);
}
.picker-start-top > :last-child {
position: absolute;
left: 50%;
transform: translateX(-50%);
}
.picker-end-top > :first-child {
position: absolute;
right: 50%;
transform: translateX(50%);
}
.divider {
height: 0.5px;
margin: 0 20px;
background-color: $colorLightGray;
}
.picker-bottom {
@include flex-row(space-evenly, center);
flex-wrap: wrap;
padding: 0 15px;
font-size: 12px;
cursor: default;
}
.picker-bottom > div {
@include flex-row(center, center);
width: calc(100% / 4);
margin: 10px 0;
}
.picker-bottom > div > span {
@include flex-row(center, center);
@include width-height(60px, 35px);
margin: 5px 0;
cursor: pointer;
}
.start-radius {
border-top-left-radius: 50px;
border-bottom-left-radius: 50px;
}
.end-radius {
border-top-right-radius: 50px;
border-bottom-right-radius: 50px;
}
.select-area {
background-color: $colorBgText;
}
.empty-area {
background-color: $colorWhite;
}
.now-month {
font-weight: bold;
color: $colorBlue;
}
.current-selected-month {
color: $colorWhite !important;
border-radius: 20px;
background-color: $colorBlue;
}
.picker-bottom > div > span:hover {
color: $colorBlue;
}
.left-db-arrow,
.right-db-arrow {
@include width-height;
display: inline-block;
background-size: 100% 100%;
cursor: pointer;
}
.left-db-arrow {
background-image: url("@/assets/images/left_db_arrow.svg");
}
.left-db-arrow:hover {
background-image: url("@/assets/images/left_db_arrow_active.svg");
}
.right-db-arrow {
background-image: url("@/assets/images/right_db_arrow.svg");
}
.right-db-arrow:hover {
background-image: url("@/assets/images/right_db_arrow_active.svg");
}
</style>
方法 utils/index.js
export const addZero = function (str) {
if (typeof str !== 'string') {
str = String(str);
}
let arr = str.split('/');
if (arr[1] && arr[1].length === 1) {
arr[1] = `0${arr[1]}`;
}
if (arr[2] && arr[2].length === 1) {
arr[2] = `0${arr[2]}`;
}
return arr.join('/');
}
效果图: