This commit is contained in:
2025-12-25 08:26:09 +08:00
parent ab5c01bf5c
commit 964e4f9c33
72 changed files with 2474 additions and 1065 deletions

View File

@@ -0,0 +1,105 @@
<template>
<view class="date-filter">
<!-- 开始日期 -->
<view class="filter-item" @click="$refs.startPicker.open(startDate)">
<text class="label">开始日期</text>
<text class="value">{{ startDate || '请选择' }}</text>
</view>
<view class="divider"></view>
<!-- 结束日期 -->
<view class="filter-item" @click="$refs.endPicker.open(endDate)">
<text class="label">结束日期</text>
<text class="value">{{ endDate || '请选择' }}</text>
</view>
<!-- 开始日期选择器 -->
<DatePicker
v-model="startDate"
ref="startPicker"
@confirm="selectStart"
/>
<!-- 结束日期选择器 -->
<DatePicker
v-model="endDate"
ref="endPicker"
@confirm="selectEnd"
/>
</view>
</template>
<script>
import DatePicker from './DatePicker.vue';
export default {
name: 'DateFilter',
components: { DatePicker },
props: {
start: { type: String, default: '' },
end: { type: String, default: '' }
},
data() {
return {
startDate: this.start,
endDate: this.end
};
},
methods: {
/** 选择开始日期 */
selectStart(e) {
this.startDate = e;
this.$emit('update:start', e);
this.$emit('change', { start: this.startDate, end: this.endDate });
},
/** 选择结束日期 */
selectEnd(e) {
this.endDate = e;
this.$emit('update:end', e);
this.$emit('change', { start: this.startDate, end: this.endDate });
}
}
};
</script>
<style scoped lang="scss">
.date-filter {
display: flex;
align-items: center;
padding: 20rpx;
background: #ffffff;
border-radius: 12rpx;
}
.filter-item {
flex: 1;
display: flex;
flex-direction: column;
}
.label {
font-size: 24rpx;
color: #888;
}
.value {
font-size: 28rpx;
padding-left: 30rpx;
color: #333;
margin-top: 6rpx;
}
.divider {
width: 2rpx;
height: 40rpx;
background: #e5e5e5;
margin: 0 20rpx;
}
</style>

View File

@@ -0,0 +1,232 @@
<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>