【UniApp 日期选择器实现与样式优化实践】

发布于:2025-06-27 ⋅ 阅读:(14) ⋅ 点赞:(0)

UniApp 日期选择器实现与样式优化实践

发布时间:2025/6/26

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

前言

在移动端应用开发中,日期选择器是一个常见且重要的交互组件。本文将分享我们在 UniApp 项目中实现自定义日期选择器的经验,特别是在样式优化过程中遇到的问题及解决方案。通过这个案例,希望能为大家在 UniApp 组件开发中提供一些参考。

需求分析

在我们的业务场景中,需要一个支持年、月、日三种维度的日期选择器,具有以下特点:

  1. 多维度选择:支持年、月、日三种维度的切换
  2. 自定义样式:符合设计规范的 UI 样式
  3. 良好交互:滑动流畅,选中项明显
  4. 默认值设置:支持设置默认日期和默认维度

基于以上需求,我们决定基于 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>

核心逻辑

  1. 数据初始化
// 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]);
  1. 动态计算年月日列表
// 年份列表
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;
});
  1. 选择器值初始化
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];
};
  1. 处理选择器变化
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 组件的自定义样式。

问题一:选中项与指示器不对齐

问题描述

在初始实现中,我们发现选中项与指示器(高亮区域)不对齐,导致视觉上的混乱。用户不清楚实际选中的是哪一项。

原因分析

  1. picker-item 的高度与 uni-picker-view-indicator 的高度不一致
  2. 文本在 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 确保文本垂直居中。

问题二:最后一项选不到

问题描述

在某些情况下,列表的最后一项无法滚动到选中位置,导致用户无法选择某些值。

原因分析

  1. picker-view 的内部实现中,滚动计算与项目高度和容器高度相关
  2. 当 picker-item 高度与 uni-picker-view-indicator 不一致时,会导致滚动计算错误

解决方案

  1. 增加 picker-container 的高度,确保有足够的滚动空间:
.picker-container {
  height: 280px;
  margin-bottom: 30px;
}
  1. 确保 picker-item 与 uni-picker-view-indicator 高度一致:
.uni-picker-view-indicator {
  height: 52px;
  /* 其他样式 */
}

.picker-item {
  height: 52px;
  line-height: 52px;
  /* 其他样式 */
}

问题三:自定义样式被覆盖

问题描述

在开发过程中,我们发现一些自定义样式被 UniApp 内部样式覆盖,特别是 indicator 的样式。

原因分析

  1. UniApp 的 picker-view 组件有内置样式,可能会覆盖自定义样式
  2. 某些样式属性被硬编码在组件内部,难以通过外部 CSS 覆盖

解决方案

  1. 使用 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;
}
  1. 避免使用 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;
}

最终效果与性能优化

经过多次调整和优化,我们的日期选择器组件实现了以下效果:

  1. 视觉一致性:选中项与指示器完美对齐
  2. 交互流畅:滚动平滑,所有项都可以选中
  3. 样式美观:符合设计规范,选中项样式明显
  4. 性能良好:避免了不必要的重新渲染

性能优化方面,我们采取了以下措施:

  1. 使用 computed 属性计算年月日列表,避免重复计算
  2. 添加 isUserChanging 标志位,减少不必要的更新
  3. 使用 setTimeout 延迟执行某些操作,确保 DOM 更新完成
  4. 优化 CSS 选择器,减少样式计算复杂度

兼容性考虑

在不同平台上,UniApp 的 picker-view 组件可能有不同的表现。我们针对主要平台进行了测试和优化:

  1. iOS

    • 滚动惯性较强,需要调整选项间距
    • 文本渲染更精细,字体大小需要微调
  2. Android

    • 滚动阻尼不同,可能需要调整滚动参数
    • 不同厂商的 Android 系统可能有不同表现
  3. 小程序

    • 微信小程序中 picker-view 的实现与原生略有不同
    • 需要额外测试确保样式一致

总结与展望

通过这次日期选择器组件的开发,我们积累了丰富的 UniApp 自定义组件开发经验,特别是在处理原生组件样式自定义方面。核心经验包括:

  1. 避免使用内联样式,优先使用 CSS 类控制样式
  2. 确保相关元素的高度一致,特别是在滚动选择器中
  3. 处理好数据流向,避免循环依赖
  4. 考虑不同平台的兼容性问题

未来,我们计划进一步优化这个组件:

  1. 支持更多的日期格式和范围限制
  2. 添加农历日期支持
  3. 优化动画效果和过渡
  4. 提高跨平台兼容性

希望本文对大家在 UniApp 开发中实现自定义日期选择器有所帮助。如有任何问题或建议,欢迎在评论区留言讨论。


发布时间:2025/6/26


网站公告

今日签到

点亮在社区的每一天
去签到