233 lines
5.3 KiB
Vue
233 lines
5.3 KiB
Vue
|
|
<template>
|
|||
|
|
<view v-if="visible" class="picker-overlay" @click.stop="close">
|
|||
|
|
<view class="picker-container" @click.stop>
|
|||
|
|
<view class="picker-header">
|
|||
|
|
<text class="cancel" @click="close">取消</text>
|
|||
|
|
<text class="title">请选择日期</text>
|
|||
|
|
<text class="confirm" @click="confirm">确定</text>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view class="picker-columns">
|
|||
|
|
<!-- 年 -->
|
|||
|
|
<scroll-view
|
|||
|
|
class="column"
|
|||
|
|
scroll-y
|
|||
|
|
:scroll-top="scrollPos[0]"
|
|||
|
|
@scroll="onScroll(0,$event)"
|
|||
|
|
>
|
|||
|
|
<view
|
|||
|
|
v-for="(y,i) in years"
|
|||
|
|
:key="i"
|
|||
|
|
class="item"
|
|||
|
|
:class="{active:selected[0]===i}"
|
|||
|
|
@click="select(0,i)"
|
|||
|
|
>
|
|||
|
|
{{y}}年
|
|||
|
|
</view>
|
|||
|
|
</scroll-view>
|
|||
|
|
|
|||
|
|
<!-- 月 -->
|
|||
|
|
<scroll-view
|
|||
|
|
class="column"
|
|||
|
|
scroll-y
|
|||
|
|
:scroll-top="scrollPos[1]"
|
|||
|
|
@scroll="onScroll(1,$event)"
|
|||
|
|
>
|
|||
|
|
<view
|
|||
|
|
v-for="(m,i) in months"
|
|||
|
|
:key="i"
|
|||
|
|
class="item"
|
|||
|
|
:class="{active:selected[1]===i}"
|
|||
|
|
@click="select(1,i)"
|
|||
|
|
>
|
|||
|
|
{{m}}月
|
|||
|
|
</view>
|
|||
|
|
</scroll-view>
|
|||
|
|
|
|||
|
|
<!-- 日 -->
|
|||
|
|
<scroll-view
|
|||
|
|
class="column"
|
|||
|
|
scroll-y
|
|||
|
|
:scroll-top="scrollPos[2]"
|
|||
|
|
@scroll="onScroll(2,$event)"
|
|||
|
|
>
|
|||
|
|
<view
|
|||
|
|
v-for="(d,i) in days"
|
|||
|
|
:key="i"
|
|||
|
|
class="item"
|
|||
|
|
:class="{active:selected[2]===i}"
|
|||
|
|
@click="select(2,i)"
|
|||
|
|
>
|
|||
|
|
{{d}}日
|
|||
|
|
</view>
|
|||
|
|
</scroll-view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
export default {
|
|||
|
|
props: { value: String },
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
visible: false,
|
|||
|
|
years: [],
|
|||
|
|
months: Array.from({ length: 12 }, (_, i) => i + 1),
|
|||
|
|
days: [],
|
|||
|
|
selected: [0, 0, 0],
|
|||
|
|
scrollPos: [0, 0, 0], // 小程序 scroll-view 只能用 scroll-top 来滚动
|
|||
|
|
itemH: 40,
|
|||
|
|
scrollTimer: null,
|
|||
|
|
};
|
|||
|
|
},
|
|||
|
|
methods: {
|
|||
|
|
open(val) {
|
|||
|
|
this.visible = true;
|
|||
|
|
const currentYear = new Date().getFullYear();
|
|||
|
|
this.years = Array.from({ length: currentYear - 1980 + 1 }, (_, i) => 1980 + i);
|
|||
|
|
const date = val ? new Date(val) : new Date();
|
|||
|
|
const y = date.getFullYear();
|
|||
|
|
const m = date.getMonth() + 1;
|
|||
|
|
const d = date.getDate();
|
|||
|
|
|
|||
|
|
this.selected = [
|
|||
|
|
this.years.indexOf(y),
|
|||
|
|
m - 1,
|
|||
|
|
0 // 先占位,待 days 生成后再修正
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
this.updateDays(() => {
|
|||
|
|
this.selected[2] = Math.min(d - 1, this.days.length - 1);
|
|||
|
|
this.updateScrollPos();
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
close() {
|
|||
|
|
this.visible = false;
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
confirm() {
|
|||
|
|
const y = this.years[this.selected[0]];
|
|||
|
|
const m = this.selected[1] + 1;
|
|||
|
|
const d = this.selected[2] + 1;
|
|||
|
|
const dateStr = `${y}-${String(m).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
|
|||
|
|
|
|||
|
|
this.$emit("input", dateStr);
|
|||
|
|
this.$emit("confirm", dateStr);
|
|||
|
|
this.close();
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 点击选择项
|
|||
|
|
select(col, idx) {
|
|||
|
|
this.selected.splice(col, 1, idx);
|
|||
|
|
|
|||
|
|
if (col === 0 || col === 1) {
|
|||
|
|
this.updateDays(() => {
|
|||
|
|
if (this.selected[2] >= this.days.length) {
|
|||
|
|
this.selected[2] = this.days.length - 1;
|
|||
|
|
}
|
|||
|
|
this.updateScrollPos();
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
this.updateScrollPos();
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 滚动事件处理(兼容小程序)
|
|||
|
|
onScroll(col, e) {
|
|||
|
|
if (!e.detail) return;
|
|||
|
|
const top = e.detail.scrollTop;
|
|||
|
|
|
|||
|
|
if (this.scrollTimer) clearTimeout(this.scrollTimer);
|
|||
|
|
|
|||
|
|
this.scrollTimer = setTimeout(() => {
|
|||
|
|
const idx = Math.round(top / this.itemH);
|
|||
|
|
|
|||
|
|
this.selected.splice(col, 1, idx);
|
|||
|
|
|
|||
|
|
if (col === 0 || col === 1) {
|
|||
|
|
this.updateDays(() => {
|
|||
|
|
if (this.selected[2] >= this.days.length) {
|
|||
|
|
this.selected[2] = this.days.length - 1;
|
|||
|
|
}
|
|||
|
|
this.updateScrollPos();
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
this.updateScrollPos();
|
|||
|
|
}
|
|||
|
|
}, 120);
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 更新 scroll-top,使选中项居中
|
|||
|
|
updateScrollPos() {
|
|||
|
|
this.scrollPos = [
|
|||
|
|
this.selected[0] * this.itemH,
|
|||
|
|
this.selected[1] * this.itemH,
|
|||
|
|
this.selected[2] * this.itemH,
|
|||
|
|
];
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
updateDays(callback) {
|
|||
|
|
const year = this.years[this.selected[0]];
|
|||
|
|
const month = this.selected[1] + 1;
|
|||
|
|
const max = new Date(year, month, 0).getDate();
|
|||
|
|
|
|||
|
|
this.days = Array.from({ length: max }, (_, i) => i + 1);
|
|||
|
|
|
|||
|
|
this.$nextTick(() => {
|
|||
|
|
if (callback) callback();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.picker-overlay {
|
|||
|
|
position: fixed;
|
|||
|
|
inset: 0;
|
|||
|
|
background: rgba(0,0,0,0.4);
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: center;
|
|||
|
|
align-items: flex-end;
|
|||
|
|
z-index: 999;
|
|||
|
|
}
|
|||
|
|
.picker-container {
|
|||
|
|
background: #fff;
|
|||
|
|
width: 100%;
|
|||
|
|
border-radius: 16px 16px 0 0;
|
|||
|
|
}
|
|||
|
|
.picker-header {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
padding: 12px 20px;
|
|||
|
|
font-size: 16px;
|
|||
|
|
}
|
|||
|
|
.cancel { color: #999; }
|
|||
|
|
.confirm { color:#007aff; }
|
|||
|
|
.title { font-weight:bold; }
|
|||
|
|
|
|||
|
|
.picker-columns {
|
|||
|
|
display: flex;
|
|||
|
|
height: 200px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.column {
|
|||
|
|
flex: 1;
|
|||
|
|
height: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item {
|
|||
|
|
height: 40px;
|
|||
|
|
line-height: 40px;
|
|||
|
|
text-align: center;
|
|||
|
|
color:#666;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item.active {
|
|||
|
|
color:#007aff;
|
|||
|
|
font-weight:bold;
|
|||
|
|
font-size:18px;
|
|||
|
|
}
|
|||
|
|
</style>
|