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">
<view
v-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-view
class="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;
}
// 更新selectedDate
selectedDate.value = new Date(year, month - 1, day);
// 延迟重置标志位,避免触发watch
setTimeout(() => {
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-view
class="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) {
// 重新初始化pickerValue
initPickerValue();
}
});
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