微信小程序如何封装一个可复用的时间筛选组件
以前写小程序统计页的时候,遇到过一个很小但很烦的需求:页面上要按时间筛选数据。
这个需求第一眼看起来没什么技术含量,无非就是两个 picker,一个开始日期,一个结束日期,然后点确定请求接口。但真正写起来会发现,它很容易在项目里变成一堆重复代码:
- A 页面要筛选订单时间。
- B 页面要筛选客户创建时间。
- C 页面要筛选发卡统计时间。
- 有的页面切出去再回来,还希望保留上次筛选条件。
- 有的页面只想临时筛一下,不需要缓存。
如果每个页面都复制一份日期选择和校验逻辑,后面改一次样式或者改一次文案,就要到处找。这个时候就应该把它拆成一个组件。
但我不想把它写成那种“大而全筛选器”。它只做一件事:收集时间条件,然后把结果交给父页面。
我对这个组件的定位
这个组件不是接口请求组件,也不是统计组件。它只是一个“时间条件收集器”。
它应该具备这几个能力:
- 能打开和关闭。
- 能选择开始日期和结束日期。
- 能校验开始日期不能大于结束日期。
- 能点“查看所有”清空筛选。
- 能把筛选结果抛给父页面。
- 如果传了缓存 key,就帮页面记住这次筛选条件。
最后用起来应该尽量短:
<screen
isShow="{{isShow}}"
bind:submit="setScreen"
existKey="statisticsScreen"
/>
这行代码里有三个关键信息:
isShow:父页面控制弹层是否显示。bind:submit:组件点确定以后,把结果交给父页面。existKey:可选,如果传了,就把筛选条件缓存起来。
我比较喜欢这个设计的原因是:父页面看一眼就知道这个筛选器怎么工作,不需要再去组件内部翻半天。
组件的模板
模板里没有什么花哨东西,就是一个遮罩层、一个表单、两个日期选择器、三个按钮。
<view class="mask" hidden="{{isShow}}" bind:tap="cancel" data-cancel="true">
<view class="screen_box">
<view class="screen_title">筛选</view>
<form bindsubmit="define">
<view class="screen_picke_box">
<view class="screen_picke_title">
<text decode="{{true}}">时 间</text>
</view>
<picker mode="date" class="ipt" bindchange="getDate" data-key="0" name="startTime">
<view class="picker">{{startTime}}</view>
</picker>
<view class="text">至</view>
<picker mode="date" class="ipt" bindchange="getDate" data-key="1" name="endTime">
<view class="picker">{{endTime}}</view>
</picker>
</view>
<button class="look" bind:tap="look">查看所有</button>
<view class="bt_box">
<button class="cancel" data-cancel="true">取消</button>
<button class="define" form-type="submit">确定</button>
</view>
</form>
</view>
</view>
这里有个细节我觉得挺实用:组件里留了一个“查看所有”按钮。
很多筛选弹层只考虑“确定”,没考虑“撤销筛选”。但业务页面里用户经常会遇到这种情况:筛了一个时间段以后,想回到全部数据。如果没有这个按钮,就只能重新进入页面或者手动清空两个日期,体验很别扭。
组件内部怎么处理日期
组件内部先定义两个属性:
properties: {
isShow: {
type: Boolean,
value: true
},
existKey: {
type: String
}
}
isShow 是控制弹层显示的,existKey 是决定是否缓存筛选条件的。
日期本身放在 data 里:
data: {
startTime: "起止日期",
endTime: "截止日期"
}
选择日期时,根据 data-key 判断改开始日期还是结束日期:
getDate({ detail, currentTarget }) {
currentTarget.dataset.key == "1"
? this.setData({ endTime: detail.value })
: this.setData({ startTime: detail.value });
}
这段写法很老派,但够直接。两个 picker 共用一个方法,不需要分别写 changeStartTime 和 changeEndTime。
点确定时,组件只做三件事
确定按钮对应的是 define 方法:
define({ detail }) {
let date = new Date(this.data.startTime) - new Date(this.data.endTime);
if (date > 0) {
wx.showToast({
title: "起止日期不能大于截止日期",
icon: "none"
});
return false;
}
this.triggerEvent("submit", detail);
this.data.existKey && wx.setStorageSync(this.data.existKey, detail);
this.setData({ isShow: true });
}
这段代码的重点不是日期校验,而是后面两行:
this.triggerEvent("submit", detail);
this.data.existKey && wx.setStorageSync(this.data.existKey, detail);
组件把结果通过 triggerEvent 抛出去,父页面想请求接口也好,想拼参数也好,都在父页面处理。
缓存也是可选的。传了 existKey 就缓存,不传就不缓存。这样它既能用在统计页,也能用在普通列表页。
我觉得这个点比封装一个漂亮弹层更重要:组件不要替页面做太多决定。
父页面怎么接住这个组件
父页面里先准备一个开关:
data: {
isShow: true
}
点筛选按钮时打开弹层:
screen() {
this.setData({ isShow: false });
}
组件点确定以后,父页面重新请求:
setScreen({ detail }) {
this.requestList();
}
如果这个页面需要跨层级保留筛选条件,比如统计页一层一层点进去,那父页面请求时可以从缓存里取:
requestList() {
const screen = wx.getStorageSync("statisticsScreen");
const params = screen
? { roleId: this.data.roleId, ...screen.value }
: { roleId: this.data.roleId };
this.$http.get("/getTeamInfo.do", params).then((res) => {
this.setData({
list: res.data.data
});
});
}
这样做的好处是:筛选条件不依赖某一个页面实例。用户从总统计点到团队统计,再点到员工统计,只要还在这个业务流里,筛选条件都可以继续生效。
这个场景下,本地缓存不是为了“永久保存用户偏好”,而是为了保存一次业务操作的上下文。
清空筛选别忘了通知父页面
“查看所有”按钮其实也很重要:
look() {
this.setData({
isShow: true,
startTime: "起止日期",
endTime: "截止日期"
});
this.data.existKey && wx.removeStorageSync(this.data.existKey);
this.triggerEvent("submit", null);
}
这里做了三件事:
- 重置组件内部显示。
- 删除本地缓存。
- 通知父页面重新请求全部数据。
很多筛选组件只把“筛选”当成事件,却忘了“取消筛选”也是事件。结果页面上的数据还停留在旧条件里,用户看着会很迷糊。
我现在会改掉的几个地方
这段旧代码能用,但如果现在让我再写一遍,我会改几个细节。
第一,startTime 和 endTime 不要同时承担“展示文案”和“真实日期”两个职责。
旧代码里:
data: {
startTime: "起止日期",
endTime: "截止日期"
}
这样写很省事,但 new Date("起止日期") 这种东西读起来不舒服。更干净的做法是把展示文案和真实值拆开:
data: {
startDate: "",
endDate: ""
}
模板里显示时再兜底:
<view class="picker">{{startDate || '起止日期'}}</view>
<view class="picker">{{endDate || '截止日期'}}</view>
第二,校验时应该先判断两个日期是否都存在:
if (this.data.startDate && this.data.endDate) {
const start = new Date(this.data.startDate).getTime();
const end = new Date(this.data.endDate).getTime();
if (start > end) {
wx.showToast({
title: "开始日期不能大于结束日期",
icon: "none"
});
return;
}
}
第三,事件名可以更语义化一点。比如不叫 submit,叫 change 或者 confirm。
如果是表单弹层,我更倾向于:
confirm:用户点确定。reset:用户点查看所有。close:用户只是关闭弹层。
这样父页面接事件时会更清楚。
一版更干净的写法
如果现在重新封装,我会把核心逻辑写成这样:
Component({
properties: {
visible: {
type: Boolean,
value: false
},
storageKey: {
type: String,
value: ""
}
},
data: {
startDate: "",
endDate: ""
},
methods: {
onDateChange({ detail, currentTarget }) {
const field = currentTarget.dataset.field;
this.setData({
[field]: detail.value
});
},
confirm() {
const { startDate, endDate } = this.data;
if (startDate && endDate && new Date(startDate) > new Date(endDate)) {
wx.showToast({
title: "开始日期不能大于结束日期",
icon: "none"
});
return;
}
const value = {
startTime: startDate,
endTime: endDate
};
if (this.data.storageKey) {
wx.setStorageSync(this.data.storageKey, value);
}
this.triggerEvent("confirm", value);
},
reset() {
this.setData({
startDate: "",
endDate: ""
});
if (this.data.storageKey) {
wx.removeStorageSync(this.data.storageKey);
}
this.triggerEvent("reset");
}
}
});
父页面使用时:
<time-filter
visible="{{filterVisible}}"
storageKey="statisticsScreen"
bind:confirm="onFilterConfirm"
bind:reset="onFilterReset"
/>
父页面逻辑:
onFilterConfirm({ detail }) {
this.setData({
filterVisible: false,
filter: detail
});
this.requestList();
},
onFilterReset() {
this.setData({
filterVisible: false,
filter: {}
});
this.requestList();
}
这样组件就更像一个标准控件:
- 它不关心接口。
- 它不关心列表。
- 它不关心当前页面是订单、客户还是统计。
- 它只负责把时间条件整理好,然后告诉父页面。
这个组件真正复用的不是 UI
很多人一说组件复用,就容易想到“样式复用”。但这个时间筛选组件真正复用的不是那两个日期选择器,而是一套交互规则:
- 用户怎么打开筛选。
- 用户怎么确认筛选。
- 用户怎么清空筛选。
- 非法日期怎么提示。
- 筛选条件要不要跨页面保留。
这些东西如果散在每个页面里,短期看没问题,长期一定会变乱。
所以我现在看这种旧代码,反而觉得它最有价值的地方不是写法多高级,而是它已经开始把“页面里的重复业务动作”拆成组件了。
在真实项目里,能把小功能拆清楚,比一上来写复杂架构更重要。
小结
封装一个可复用的时间筛选组件,我会抓住这几个原则:
- 组件只收集时间条件,不直接请求业务接口。
- 通过事件把结果交给父页面。
- 缓存能力做成可选,不强塞给所有页面。
- “查看所有”也要当成一个正式事件处理。
- 展示文案和真实日期值最好分开。
最后再说一句我自己的判断:一个组件好不好,不是看它能配置多少参数,而是看你下次换个页面用它时,会不会忍不住进去改源码。
不用改源码就能复用,才算真的封装好了。

浙公网安备 33010602011771号