环境:
"dependencies": {
"@rollup/plugin-alias": "^3.1.9",
"@types/node": "^17.0.43",
"element-plus": "^2.2.15",
"vue": "^3.2.25",
"vue-router": "^4.0.16"
},
"devDependencies": {
"typescript": "^4.5.4",
"vite": "^2.9.9",
"vite-plugin-vue-setup-extend": "^0.4.0",
"vue-tsc": "^0.34.7"
}
效果:(按照element操作方式来写的,颜色自定义的)
组件代码:(composition API的方式)
<template>
<div class="yearPicker" :ref="yearPicker">
<div class="_inner" :style="{ width: props.labelWidth + 'px' }">{{ props.labelText }}</div>
<input class="_inner" :ref="inputLeft" v-model="data.startShowYear" @focus="onFocus" @click="clickInput" type="text"
name="yearInput" @input="checkStartInput()" placeholder="选择年份" />
<span>{{ props.sp }}</span>
<input class="_inner" :ref="inputRight" v-model="data.endShowYear" @focus="onFocus" @click="clickInput" type="text"
name="yearInput" @input="checkEndInput()" placeholder="选择年份" />
<div class="_inner floatPanel" v-if="data.showPanel">
<div class="_inner leftPanel">
<div class="_inner panelHead">
<i class="_inner" @click="onClickLeft"><<</i>
{{ leftYearList[0] + "-" + leftYearList[9] }}
</div>
<div class="_inner panelContent">
<div :class="{
disabled: checkValidYear(item),
oneSelected: item === data.startYear && oneSelected,
startSelected: item === data.startYear,
endSelected: item === data.endYear,
_inner: true,
betweenSelected: item > data.startYear && item < data.endYear,
}" v-for="item in leftYearList" :key="item">
<a :class="{
cell: true,
_inner: true,
selected: item === data.startYear || item === data.endYear,
}" @click="onClickItem(item)" @mouseover="onHoverItem(item)">
{{ item }}
</a>
</div>
</div>
</div>
<div class="_inner rightPanel">
<div class="_inner panelHead">
<i class="_inner" @click="onClickRight">>></i>
{{ rightYearList[0] + "-" + rightYearList[9] }}
</div>
<div class="_inner panelContent">
<div :class="{
disabled: checkValidYear(item),
startSelected: item === data.startYear,
endSelected: item === data.endYear,
betweenSelected: item > data.startYear && item < data.endYear,
}" v-for="item in rightYearList" :key="item">
<a :class="{
cell: true,
_inner: true,
selected: item === data.endYear || item === data.startYear,
}" @click="onClickItem(item)" @mouseover="onHoverItem(item)">
{{ item }}
</a>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { VNodeRef, computed, nextTick, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref } from "vue";
interface Emits {
(e: "updateTimeRange", startYear: string, endYear: string): void;
}
interface Props { labelWidth: number, labelText: string, sp?: string, initYear?: {startYear:number, endYear:number} }
const SELECT_STATE = {
unselect: 0,
selecting: 1,
selected: 2,
};
const data = reactive<any>({
itemBg: {},
startShowYear: null,
endShowYear: null,
yearList: [],
showPanel: false,
startYear: null,
endYear: null,
curYear: 0,
curSelectedYear: 0,
curState: SELECT_STATE.unselect,
})
const yearPicker = ref<VNodeRef>()
const inputLeft = ref<VNodeRef>()
const inputRight = ref<VNodeRef>()
const oneSelected = computed(() => {
return (
data.curState === SELECT_STATE.selecting &&
(data.startYear === data.endYear || data.endYear == null)
);
})
const leftYearList = computed(() => {
return data.yearList.slice(0, 10);
})
const rightYearList = computed(() => {
return data.yearList.slice(10, 20);
})
const emits = defineEmits<Emits>()
const props = withDefaults(defineProps<Props>(), {
labelWidth: 80,
labelText: "时间标签",
sp: "至",
})
const clickInput = (e:any) => {
e.stopPropagation();
return false;
}
const checkValidYear = (iYear:number) => {
if (props.initYear) {
if (iYear > props.initYear.endYear) {
return 1
} else if (iYear < props.initYear.startYear) {
return -1
}
}
return 0
}
const checkStartInput = () => {
if (isNaN(data.startShowYear)) {
data.startShowYear = data.startYear;
} else {
data.startYear = data.startShowYear * 1;
}
}
const checkEndInput = () => {
if (isNaN(data.endShowYear)) {
data.endShowYear = data.endYear;
} else {
data.endYear = data.endShowYear * 1;
}
}
const changeYear = () => {
if (data.startYear > data.endYear) {
let tmp = data.endYear;
data.endYear = data.startYear;
data.startYear = tmp;
}
if (props.initYear) {
data.startYear = Math.max(data.startYear, props.initYear.startYear)
data.endYear = Math.min(data.endYear, props.initYear.endYear)
}
data.startShowYear = data.startYear;
data.endShowYear = data.endYear;
if (data.startYear && data.endYear) {
emits("updateTimeRange",
data.startYear,
data.endYear,
);
} else {
console.warn("WARN:年份不合法", data.startYear, data.endYear);
}
}
const onHoverItem = (iYear:number) => {
if (checkValidYear(iYear) != 0) {
return;
}
if (data.curState === SELECT_STATE.selecting) {
let tmpStart = data.curSelectedYear;
data.endYear = Math.max(tmpStart, iYear);
data.startYear = Math.min(tmpStart, iYear);
}
}
const onClickItem = (iYear:number) => {
if (checkValidYear(iYear) != 0) {
return;
}
if (
data.curState === SELECT_STATE.unselect ||
data.curState === SELECT_STATE.selected
) {
data.startYear = iYear;
data.curSelectedYear = iYear;
data.endYear = null;
data.curState = SELECT_STATE.selecting;
} else if (data.curState === SELECT_STATE.selecting) {
data.endShowYear = data.endYear;
data.startShowYear = data.startYear;
data.curState = SELECT_STATE.selected;
emits("updateTimeRange",
data.startYear,
data.endYear,
);
setTimeout(() => {
//为动画留的时间,可优化
data.showPanel = false;
}, 300);
}
}
const onFocus = () => {
nextTick(() => {
data.showPanel = true;
});
}
const updateYearList = () => {
let iStart = Math.floor(data.curYear / 10) * 10 - 10;
iStart = iStart < 0 ? 0 : iStart;
data.yearList = [];
for (let index = 0; index < 20; index++) {
data.yearList.push(iStart + index);
}
}
const closePanel = (e:any) => {
if (!data.showPanel) {
return;
}
if (typeof e.target.className !== "string" || e.target.className === "") {
nextTick(() => {
changeYear()
data.showPanel = false;
});
return;
}
if (
e.target.className.indexOf("_inner") === -1 ||
(e.target.name === "yearInput" &&
e.target !== inputLeft.value &&
e.target !== inputRight.value)
) {
nextTick(() => {
changeYear()
data.showPanel = false;
});
}
e.stopPropagation();
return false;
}
const onClickLeft = () => {
data.curYear = data.curYear * 1 - 10;
updateYearList();
}
const onClickRight = () => {
data.curYear = data.curYear * 1 + 10;
updateYearList();
}
onBeforeMount(() => {
data.curYear = new Date().getFullYear();
updateYearList();
})
onBeforeUnmount(() => {
document.removeEventListener("click", closePanel.bind(data));
})
onMounted(() => {
document.addEventListener("click", closePanel.bind(data));
})
</script>
<style lang="scss" scoped>
.yearPicker {
font-size: 14px;
display: flex;
position: relative;
transition: all 0.3s;
input:first-child {
text-align: right;
}
background-color: #fff;
span {
padding: 0 8px;
height: 32px;
line-height: 32px;
}
border: 1px solid #eff1f3;
height: 34px;
line-height: 34px;
border-radius: 4px;
padding: 0 8px;
box-sizing: border-box;
.floatPanel {
>div {
width: 50%;
}
padding: 0 16px;
position: absolute;
display: flex;
background-color: #fff;
z-index: 2000;
border-radius: 4px;
width: 650px;
height: 250px;
top: 40px;
left: -50px;
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
.panelContent {
display: flex;
flex-wrap: wrap;
width: 100%;
height: calc(100% - 70px);
.disabled {
color: #ccc;
}
.oneSelected {
border-top-right-radius: 24px;
border-bottom-right-radius: 24px;
}
.startSelected {
background-color: #f6f6f7;
border-top-left-radius: 24px;
border-bottom-left-radius: 24px;
}
.endSelected {
background-color: #f6f6f7;
border-top-right-radius: 24px;
border-bottom-right-radius: 24px;
}
.betweenSelected {
background-color: #f6f6f7;
}
>div {
width: 75px;
height: 48px;
line-height: 48px;
margin: 3px 0;
// border-radius: 24px;
text-align: center;
a {
display: inline-block;
width: 60px;
height: 36px;
cursor: pointer;
line-height: 36px;
border-radius: 18px;
}
.selected {
background-color: #3e77fc;
color: #fff;
}
}
}
.panelHead {
position: relative;
height: 46px;
line-height: 46px;
text-align: center;
i {
position: absolute;
cursor: pointer;
&:hover {
color: #3e77fc;
}
}
}
.rightPanel {
padding-left: 8px;
}
.leftPanel .panelHead i {
left: 20px;
}
.rightPanel .panelHead i {
right: 20px;
}
}
.floatPanel::before {
content: "";
height: 100%;
position: absolute;
left: 50%;
width: 1px;
border-left: 1px solid #e4e4e4;
}
}
input {
width: 60px;
border: none;
height: 32px;
text-align: center;
line-height: 32px;
box-sizing: border-box;
background-color: transparent;
}
input:focus {
outline: none;
background-color: transparent;
}
.yearPicker:hover {
border-color: #3e77fc;
}
.dateIcon {
position: absolute;
right: 16px;
top: 9px;
color: #adb2bc;
}
</style>
外部调用:
<template>
<div>
<yearPicker
style="width:250px"
ref="statisticPicker"
:labelWidth = "50"
labelText="统计期"
:initYear="dateValue"
@updateTimeRange="updateStatisticYear"
/>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import yearPicker from "./component/yearPick.vue"
//可选择区间,initYear传参,不传则所有年份有效,小于0判断一下?
const dateValue= ref<any>({startYear:2000, endYear: new Date().getFullYear()});
//选完/输入完成的回调
const updateStatisticYear:any = (startYear:number, endYear:number)=>{
console.log("选中年份", startYear, endYear)
}
</script>
<style lang="scss" scoped>
</style>
这个是vue2升级上来的,修复了输入的逻辑问题,添加了有效年份区间(区间外的禁止选择,禁止输入),优化了点击关闭页面的逻辑:
vue2版本电梯:http://t.csdnimg.cn/ngmgL