【UniApp 日期选择器实现与样式优化实践】
UniApp 日期选择器实现与样式优化实践
发布时间:2025/6/26
前言
在移动端应用开发中,日期选择器是一个常见且重要的交互组件。本文将分享我们在 UniApp 项目中实现自定义日期选择器的经验,特别是在样式优化过程中遇到的问题及解决方案。通过这个案例,希望能为大家在 UniApp 组件开发中提供一些参考。
需求分析
在我们的业务场景中,需要一个支持年、月、日三种维度的日期选择器,具有以下特点:
- 多维度选择:支持年、月、日三种维度的切换
- 自定义样式:符合设计规范的 UI 样式
- 良好交互:滑动流畅,选中项明显
- 默认值设置:支持设置默认日期和默认维度
基于以上需求,我们决定基于 UniApp 的 picker-view 组件进行二次开发,实现一个自定义的日期选择器组件。
基础实现
组件结构
<template><view class="date-picker-drawer"><!-- 遮罩层 --><view v-if="visible" class="drawer-mask" @click="handleClose"></view><!-- 抽屉内容 --><view class="drawer-content" :class="{ show: visible }"><!-- 头部 --><view class="drawer-header"><view class="placeholder-btn"></view><view class="header-title">时间维度</view><view class="close-btn" @click="handleClose">×</view></view><!-- 标签页 --><view class="tab-container"><viewv-for="(tab, index) in tabs":key="tab.value"class="tab-item":class="{ active: currentTab === tab.value }"@click="switchTab(tab.value)">{{ tab.label }}</view></view><!-- 当前选中日期显示 --><view class="current-date"><text class="date-text">{{ formatCurrentDate }}</text></view><!-- 日期选择器 --><view class="picker-container"><picker-viewclass="picker-view":value="pickerValue"@change="handlePickerChange"mask-class="picker-mask"><!-- 年份列 --><picker-view-column><view v-for="year in yearList" :key="year" class="picker-item">{{ year }}年</view></picker-view-column><!-- 月份列 --><picker-view-column v-if="currentTab !== 'year'"><view v-for="month in monthList" :key="month" class="picker-item">{{ month }}月</view></picker-view-column><!-- 日期列 --><picker-view-column v-if="currentTab === 'day'"><view v-for="day in dayList" :key="day" class="picker-item">{{ day }}日</view></picker-view-column></picker-view></view><!-- 确定按钮 --><view class="confirm-btn" @click="handleConfirm">确定</view></view></view>
</template>
核心逻辑
- 数据初始化:
// Props 和 Emits
const props = withDefaults(defineProps<Props>(), {defaultDate: () => new Date(),defaultTab: 'year',minYear: () => new Date().getFullYear() - 3,maxYear: () => new Date().getFullYear() + 3
});// 响应式数据
const currentTab = ref<'day' | 'month' | 'year'>(props.defaultTab);
const selectedDate = ref(new Date(props.defaultDate));
const pickerValue = ref([0, 0, 0]);
- 动态计算年月日列表:
// 年份列表
const yearList = computed(() => {const years = [];const minYear = Math.min(props.minYear, props.maxYear);const maxYear = Math.max(props.minYear, props.maxYear);for (let i = minYear; i <= maxYear; i++) {years.push(i);}return years;
});// 月份列表
const monthList = computed(() => {const months = [];for (let i = 1; i <= 12; i++) {months.push(i);}return months;
});// 日期列表
const dayList = computed(() => {const yearIndex = Math.min(Math.max(0, pickerValue.value[0]), yearList.value.length - 1);const monthIndex = Math.min(Math.max(0, pickerValue.value[1]), monthList.value.length - 1);const year = yearList.value[yearIndex] || new Date().getFullYear();const month = monthList.value[monthIndex] || 1;// 计算该月的天数const daysInMonth = new Date(year, month, 0).getDate();const days = [];for (let i = 1; i <= daysInMonth; i++) {days.push(i);}return days;
});
- 选择器值初始化:
const initPickerValue = () => {const year = selectedDate.value.getFullYear();const month = selectedDate.value.getMonth() + 1;const day = selectedDate.value.getDate();// 确保年份在可选范围内const safeYear = Math.max(props.minYear, Math.min(props.maxYear, year));// 查找年份在列表中的索引const yearIndex = yearList.value.findIndex((y) => y === safeYear);// 月份和日期索引const monthIndex = month - 1;const dayIndex = day - 1;// 确保索引有效const validYearIndex = yearIndex >= 0 ? yearIndex : 0;const validMonthIndex = monthIndex >= 0 && monthIndex < 12 ? monthIndex : 0;const validDayIndex = dayIndex >= 0 && dayIndex < dayList.value.length ? dayIndex : 0;pickerValue.value = [validYearIndex, validMonthIndex, validDayIndex];
};
- 处理选择器变化:
const handlePickerChange = (e: any) => {const values = e.detail.value;// 设置标志位,表示用户正在操作isUserChanging.value = true;// 确保索引有效const validValues = [Math.min(Math.max(0, values[0]), yearList.value.length - 1),Math.min(Math.max(0, values[1] || 0), monthList.value.length - 1),Math.min(Math.max(0, values[2] || 0), dayList.value.length - 1)];pickerValue.value = validValues;// 获取实际选中的值const yearIndex = validValues[0];const year = yearList.value[yearIndex];let month = 1;let day = 1;if (currentTab.value !== 'year' && validValues[1] !== undefined) {const monthIndex = validValues[1];month = monthList.value[monthIndex];}if (currentTab.value === 'day' && validValues[2] !== undefined) {const dayIndex = validValues[2];day = dayList.value[dayIndex] || 1;}// 更新selectedDateselectedDate.value = new Date(year, month - 1, day);// 延迟重置标志位,避免触发watchsetTimeout(() => {isUserChanging.value = false;}, 50);
};
样式优化过程
在实现基本功能后,我们遇到了一系列样式和交互问题,主要围绕 picker-view 组件的自定义样式。
问题一:选中项与指示器不对齐
问题描述:
在初始实现中,我们发现选中项与指示器(高亮区域)不对齐,导致视觉上的混乱。用户不清楚实际选中的是哪一项。
原因分析:
- picker-item 的高度与 uni-picker-view-indicator 的高度不一致
- 文本在 picker-item 中的垂直对齐问题
解决方案:
/* 选中项样式 */
.uni-picker-view-indicator {height: 52px;box-sizing: border-box;border-top: 1px solid rgba(0, 0, 0, 0.1);border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}.picker-item {height: 52px;line-height: 52px;display: flex;align-items: center;justify-content: center;font-size: 32px;color: rgba(0, 0, 0, 0.6);font-family: 'PingFang SC', sans-serif;font-weight: 400;padding: 0;margin: 0;
}/* 选中项文字样式 */
.uni-picker-view-indicator .picker-item {color: rgba(0, 0, 0, 0.9);font-weight: 500;
}
关键点是确保 picker-item 的高度与 uni-picker-view-indicator 的高度一致,并使用 line-height、align-items 和 justify-content 确保文本垂直居中。
问题二:最后一项选不到
问题描述:
在某些情况下,列表的最后一项无法滚动到选中位置,导致用户无法选择某些值。
原因分析:
- picker-view 的内部实现中,滚动计算与项目高度和容器高度相关
- 当 picker-item 高度与 uni-picker-view-indicator 不一致时,会导致滚动计算错误
解决方案:
- 增加 picker-container 的高度,确保有足够的滚动空间:
.picker-container {height: 280px;margin-bottom: 30px;
}
- 确保 picker-item 与 uni-picker-view-indicator 高度一致:
.uni-picker-view-indicator {height: 52px;/* 其他样式 */
}.picker-item {height: 52px;line-height: 52px;/* 其他样式 */
}
问题三:自定义样式被覆盖
问题描述:
在开发过程中,我们发现一些自定义样式被 UniApp 内部样式覆盖,特别是 indicator 的样式。
原因分析:
- UniApp 的 picker-view 组件有内置样式,可能会覆盖自定义样式
- 某些样式属性被硬编码在组件内部,难以通过外部 CSS 覆盖
解决方案:
- 使用 mask-class 属性自定义遮罩层样式:
<picker-viewclass="picker-view":value="pickerValue"@change="handlePickerChange"mask-class="picker-mask"
><!-- 内容 -->
</picker-view>
.picker-mask {background-image: linear-gradient(to bottom, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.6)),linear-gradient(to top, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.6));background-position: top, bottom;background-size: 100% 88px;background-repeat: no-repeat;
}
- 避免使用 indicatorStyle 属性,而是通过 CSS 类选择器控制样式:
.uni-picker-view-indicator {height: 52px;box-sizing: border-box;border-top: 1px solid rgba(0, 0, 0, 0.1);border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}.uni-picker-view-indicator::before,
.uni-picker-view-indicator::after {height: 0px;
}
关键技术点与经验总结
1. 避免使用内联样式
在早期实现中,我们尝试使用 picker-view 的 indicatorStyle 属性设置样式:
<picker-view :indicator-style="indicatorStyle"><!-- 内容 -->
</picker-view>
const indicatorStyle = 'height: 48px; background-color: rgba(0, 0, 0, 0.05);';
这种方式导致了多种问题:
- 样式难以维护和扩展
- 与其他 CSS 规则可能冲突
- 无法使用更复杂的 CSS 选择器
改进后,我们完全通过 CSS 类控制样式,提高了代码可维护性。
2. 同步高度设置的重要性
在日期选择器中,确保以下元素高度一致至关重要:
- uni-picker-view-indicator(选中指示器)
- picker-item(选项项)
这不仅影响视觉效果,还会影响滚动计算和选中逻辑。我们通过反复测试确定了 52px 是最佳高度。
3. 处理循环依赖问题
在开发过程中,我们遇到了一个棘手的问题:当选择器值变化时,会触发 selectedDate 的更新,而 selectedDate 的更新又会触发 pickerValue 的重新计算,形成循环依赖。
解决方案是添加一个标志位,区分用户操作和程序自动更新:
// 添加标志位
const isUserChanging = ref(false);// 处理选择器变化
const handlePickerChange = (e: any) => {// 设置标志位,表示用户正在操作isUserChanging.value = true;// 处理逻辑...// 延迟重置标志位setTimeout(() => {isUserChanging.value = false;}, 50);
};// 监听selectedDate变化
watch(selectedDate, (newDate) => {// 如果是用户操作导致的变化,不需要重新初始化if (!isUserChanging.value) {// 重新初始化pickerValueinitPickerValue();}
});
4. 容器高度与可滚动性
picker-view 的可滚动范围与容器高度相关。如果容器高度不足,可能导致某些项无法滚动到选中位置。我们通过增加 picker-container 的高度解决了这个问题:
.picker-container {height: 280px;margin-bottom: 30px;
}
最终效果与性能优化
经过多次调整和优化,我们的日期选择器组件实现了以下效果:
- 视觉一致性:选中项与指示器完美对齐
- 交互流畅:滚动平滑,所有项都可以选中
- 样式美观:符合设计规范,选中项样式明显
- 性能良好:避免了不必要的重新渲染
性能优化方面,我们采取了以下措施:
- 使用 computed 属性计算年月日列表,避免重复计算
- 添加 isUserChanging 标志位,减少不必要的更新
- 使用 setTimeout 延迟执行某些操作,确保 DOM 更新完成
- 优化 CSS 选择器,减少样式计算复杂度
兼容性考虑
在不同平台上,UniApp 的 picker-view 组件可能有不同的表现。我们针对主要平台进行了测试和优化:
-
iOS:
- 滚动惯性较强,需要调整选项间距
- 文本渲染更精细,字体大小需要微调
-
Android:
- 滚动阻尼不同,可能需要调整滚动参数
- 不同厂商的 Android 系统可能有不同表现
-
小程序:
- 微信小程序中 picker-view 的实现与原生略有不同
- 需要额外测试确保样式一致
总结与展望
通过这次日期选择器组件的开发,我们积累了丰富的 UniApp 自定义组件开发经验,特别是在处理原生组件样式自定义方面。核心经验包括:
- 避免使用内联样式,优先使用 CSS 类控制样式
- 确保相关元素的高度一致,特别是在滚动选择器中
- 处理好数据流向,避免循环依赖
- 考虑不同平台的兼容性问题
未来,我们计划进一步优化这个组件:
- 支持更多的日期格式和范围限制
- 添加农历日期支持
- 优化动画效果和过渡
- 提高跨平台兼容性
希望本文对大家在 UniApp 开发中实现自定义日期选择器有所帮助。如有任何问题或建议,欢迎在评论区留言讨论。
发布时间:2025/6/26