发布时间:2025/6/26
在移动端应用开发中,日期选择器是一个常见且重要的交互组件。本文将分享我们在 UniApp 项目中实现自定义日期选择器的经验,特别是在样式优化过程中遇到的问题及解决方案。通过这个案例,希望能为大家在 UniApp 组件开发中提供一些参考。
在我们的业务场景中,需要一个支持年、月、日三种维度的日期选择器,具有以下特点:
基于以上需求,我们决定基于 UniApp 的 picker-view 组件进行二次开发,实现一个自定义的日期选择器组件。
时间维度
×
{{ tab.label }}
{{ formatCurrentDate }}
{{ year }}年
{{ month }}月
{{ day }}日
确定
// 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 组件的自定义样式。
问题描述:
在初始实现中,我们发现选中项与指示器(高亮区域)不对齐,导致视觉上的混乱。用户不清楚实际选中的是哪一项。
原因分析:
解决方案:
/* 选中项样式 */
.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-container {
height: 280px;
margin-bottom: 30px;
}
.uni-picker-view-indicator {
height: 52px;
/* 其他样式 */
}
.picker-item {
height: 52px;
line-height: 52px;
/* 其他样式 */
}
问题描述:
在开发过程中,我们发现一些自定义样式被 UniApp 内部样式覆盖,特别是 indicator 的样式。
原因分析:
解决方案:
<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;
}
.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;
}
在早期实现中,我们尝试使用 picker-view 的 indicatorStyle 属性设置样式:
<picker-view :indicator-style="indicatorStyle">
picker-view>
const indicatorStyle = 'height: 48px; background-color: rgba(0, 0, 0, 0.05);';
这种方式导致了多种问题:
改进后,我们完全通过 CSS 类控制样式,提高了代码可维护性。
在日期选择器中,确保以下元素高度一致至关重要:
这不仅影响视觉效果,还会影响滚动计算和选中逻辑。我们通过反复测试确定了 52px 是最佳高度。
在开发过程中,我们遇到了一个棘手的问题:当选择器值变化时,会触发 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();
}
});
picker-view 的可滚动范围与容器高度相关。如果容器高度不足,可能导致某些项无法滚动到选中位置。我们通过增加 picker-container 的高度解决了这个问题:
.picker-container {
height: 280px;
margin-bottom: 30px;
}
经过多次调整和优化,我们的日期选择器组件实现了以下效果:
性能优化方面,我们采取了以下措施:
在不同平台上,UniApp 的 picker-view 组件可能有不同的表现。我们针对主要平台进行了测试和优化:
iOS:
Android:
小程序:
通过这次日期选择器组件的开发,我们积累了丰富的 UniApp 自定义组件开发经验,特别是在处理原生组件样式自定义方面。核心经验包括:
未来,我们计划进一步优化这个组件:
希望本文对大家在 UniApp 开发中实现自定义日期选择器有所帮助。如有任何问题或建议,欢迎在评论区留言讨论。
发布时间:2025/6/26