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