删除多余文件

This commit is contained in:
筱野
2025-09-13 10:36:13 +08:00
parent 30d16946d2
commit aef84e76cb
11 changed files with 0 additions and 829 deletions

View File

@@ -1,77 +0,0 @@
# TimePicker 时间选择器组件
## 功能特性
- 使用自定义样式重写PickerViewColumn功能
- 完全还原原生PickerView的样式和动画效果
- 支持年份和月份选择
- 平滑的滚动动画和切换效果
- 响应式设计,支持触摸滚动
- 渐变遮罩效果增强视觉层次
## 技术实现
### 核心特性
- 使用ScrollView替代PickerViewColumn
- 自定义滚动逻辑实现选项对齐
- CSS动画和过渡效果还原原生体验
- 智能滚动位置计算和自动对齐
### 样式还原
- 选中项指示器(高亮背景)
- 渐变遮罩效果(顶部和底部)
- 平滑的过渡动画
- 精确的尺寸和间距
## 使用方法
```tsx
import { TimePicker } from '@/components/TimePicker'
const [visible, setVisible] = useState(false)
<TimePicker
visible={visible}
onClose={() => setVisible(false)}
onConfirm={(year, month) => {
console.log('选择的时间:', year, month)
setVisible(false)
}}
defaultYear={2024}
defaultMonth={6}
minYear={2020}
maxYear={2030}
/>
```
## Props
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| visible | boolean | - | 是否显示选择器 |
| visible | boolean | - | 是否显示选择器 |
| onClose | () => void | - | 关闭回调 |
| onConfirm | (year: number, month: number) => void | - | 确认选择回调 |
| defaultYear | number | 当前年份 | 默认选中的年份 |
| defaultMonth | number | 当前月份 | 默认选中的月份 |
| minYear | number | 2020 | 最小年份 |
| maxYear | number | 2030 | 最大年份 |
## 样式定制
组件使用CSS Modules可以通过修改`index.module.scss`文件来自定义样式:
- `.time-picker-popup`: 弹出层容器
- `.picker-container`: 选择器容器
- `.custom-picker`: 自定义选择器
- `.picker-indicator`: 选中项指示器
- `.picker-column`: 选择列
- `.picker-item`: 选择项
- `.picker-item-active`: 激活状态的选择项
## 测试
运行测试页面:
```tsx
import TimePickerTest from '@/components/TimePicker/test'
```

View File

@@ -1,233 +0,0 @@
import React, { useState, useEffect, useRef } from 'react'
import { View, Text, ScrollView } from '@tarojs/components'
import { CommonPopup } from '../index'
import styles from './index.module.scss'
export interface TimePickerProps {
visible: boolean
onClose: () => void
onConfirm: (year: number, month: number) => void
defaultYear?: number
defaultMonth?: number
minYear?: number
maxYear?: number
}
const TimePicker: React.FC<TimePickerProps> = ({
visible,
onClose,
onConfirm,
defaultYear = new Date().getFullYear(),
defaultMonth = new Date().getMonth() + 1,
minYear = 2020,
maxYear = 2030
}) => {
const [selectedYear, setSelectedYear] = useState(defaultYear)
const [selectedMonth, setSelectedMonth] = useState(defaultMonth)
const [yearScrollTop, setYearScrollTop] = useState(0)
const [monthScrollTop, setMonthScrollTop] = useState(0)
const yearScrollRef = useRef<any>(null)
const monthScrollRef = useRef<any>(null)
// 计算当前选项在数组中的索引
const getYearIndex = (year: number) => year - minYear
const getMonthIndex = (month: number) => month - 1
// 生成选择器的选项数据
const yearOptions = Array.from({ length: maxYear - minYear + 1 }, (_, index) => ({
text: `${minYear + index}`,
value: minYear + index
}))
const monthOptions = Array.from({ length: 12 }, (_, index) => ({
text: `${index + 1}`,
value: index + 1
}))
// 计算滚动位置 - 确保每次只显示一个选项
const calculateScrollTop = (index: number) => {
const itemHeight = 48 // 每个选项的高度
const containerHeight = 216 // 容器高度
const centerOffset = (containerHeight - itemHeight) / 2
return index * itemHeight - centerOffset
}
// 获取当前可见的选项数量
const getVisibleItemCount = () => {
const containerHeight = 216
const itemHeight = 48
return Math.floor(containerHeight / itemHeight)
}
useEffect(() => {
if (visible) {
setSelectedYear(defaultYear)
setSelectedMonth(defaultMonth)
// 设置初始滚动位置
const yearScrollTop = calculateScrollTop(getYearIndex(defaultYear))
const monthScrollTop = calculateScrollTop(getMonthIndex(defaultMonth))
setYearScrollTop(yearScrollTop)
setMonthScrollTop(monthScrollTop)
}
}, [visible, defaultYear, defaultMonth])
// 处理年份滚动
const handleYearScroll = (event: any) => {
const scrollTop = event.detail.scrollTop
const itemHeight = 48
const containerHeight = 216
const centerOffset = (containerHeight - itemHeight) / 2
// 计算当前选中的年份索引
const currentIndex = Math.round((scrollTop + centerOffset) / itemHeight)
const clampedIndex = Math.max(0, Math.min(currentIndex, yearOptions.length - 1))
const newYear = minYear + clampedIndex
if (newYear !== selectedYear) {
setSelectedYear(newYear)
}
}
// 处理年份滚动结束,自动对齐
const handleYearScrollEnd = () => {
const yearIndex = getYearIndex(selectedYear)
const alignedScrollTop = calculateScrollTop(yearIndex)
// 使用setTimeout确保滚动动画完成后再对齐
setTimeout(() => {
setYearScrollTop(alignedScrollTop)
}, 100)
}
// 处理月份滚动
const handleMonthScroll = (event: any) => {
const scrollTop = event.detail.scrollTop
const itemHeight = 48
const containerHeight = 216
const centerOffset = (containerHeight - itemHeight) / 2
// 计算当前选中的月份索引
const currentIndex = Math.round((scrollTop + centerOffset) / itemHeight)
const clampedIndex = Math.max(0, Math.min(currentIndex, monthOptions.length - 1))
const newMonth = clampedIndex + 1
if (newMonth !== selectedMonth) {
setSelectedMonth(newMonth)
}
}
// 处理月份滚动结束,自动对齐
const handleMonthScrollEnd = () => {
const monthIndex = getMonthIndex(selectedMonth)
const alignedScrollTop = calculateScrollTop(monthIndex)
// 使用setTimeout确保滚动动画完成后再对齐
setTimeout(() => {
setMonthScrollTop(alignedScrollTop)
}, 100)
}
const handleConfirm = () => {
onConfirm(selectedYear, selectedMonth)
onClose()
}
if (!visible) return null
return (
<CommonPopup
visible={visible}
onClose={onClose}
onConfirm={handleConfirm}
showHeader={false}
hideFooter={false}
cancelText="返回"
confirmText="完成"
position="bottom"
round={true}
className={styles['time-picker-popup']}
>
{/* 拖拽手柄 */}
<View className={styles['popup-handle']} />
{/* 时间选择器 */}
<View className={styles['picker-container']}>
{/* 自定义多列选择器 */}
<View className={styles['picker-wrapper']}>
<View className={styles['custom-picker']}>
{/* 选中项指示器 */}
<View className={styles['picker-indicator']} />
{/* 年份列 */}
<View className={styles['picker-column']}>
<ScrollView
ref={yearScrollRef}
scrollY
scrollTop={yearScrollTop}
onScroll={handleYearScroll}
onTouchEnd={handleYearScrollEnd}
onScrollToLower={handleYearScrollEnd}
onScrollToUpper={handleYearScrollEnd}
className={styles['picker-scroll']}
scrollWithAnimation={true}
enhanced={true}
showScrollbar={false}
bounces={false}
fastDeceleration={true}
>
<View className={styles['picker-padding']} />
{yearOptions.map((option, index) => (
<View
key={option.value}
className={`${styles['picker-item']} ${
option.value === selectedYear ? styles['picker-item-active'] : ''
}`}
data-value={option.value}
>
<Text className={styles['picker-item-text']}>{option.text}</Text>
</View>
))}
<View className={styles['picker-padding']} />
</ScrollView>
</View>
{/* 月份列 */}
<View className={styles['picker-column']}>
<ScrollView
ref={monthScrollRef}
scrollY
scrollTop={monthScrollTop}
onScroll={handleMonthScroll}
onTouchEnd={handleMonthScrollEnd}
onScrollToLower={handleMonthScrollEnd}
onScrollToUpper={handleMonthScrollEnd}
className={styles['picker-scroll']}
scrollWithAnimation={true}
enhanced={true}
showScrollbar={false}
bounces={false}
fastDeceleration={true}
>
<View className={styles['picker-padding']} />
{monthOptions.map((option, index) => (
<View
key={option.value}
className={`${styles['picker-item']} ${
option.value === selectedMonth ? styles['picker-item-active'] : ''
}`}
data-value={option.value}
>
<Text className={styles['picker-item-text']}>{option.text}</Text>
</View>
))}
<View className={styles['picker-padding']} />
</ScrollView>
</View>
</View>
</View>
</View>
</CommonPopup>
)
}
export default TimePicker

View File

@@ -1,81 +0,0 @@
.demoContainer {
padding: 20px;
text-align: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: white;
}
.title {
font-size: 24px;
font-weight: 700;
margin-bottom: 10px;
display: block;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.subtitle {
font-size: 16px;
margin-bottom: 30px;
display: block;
opacity: 0.9;
}
.demoButton {
margin: 20px 0;
width: 250px;
height: 50px;
border-radius: 25px;
font-size: 18px;
font-weight: 600;
background: rgba(255, 255, 255, 0.2);
border: 2px solid rgba(255, 255, 255, 0.3);
color: white;
&:active {
background: rgba(255, 255, 255, 0.3);
transform: scale(0.98);
}
}
.demoResult {
margin: 30px 0;
padding: 20px;
background: rgba(255, 255, 255, 0.1);
border-radius: 16px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
text {
font-size: 18px;
font-weight: 600;
color: white;
}
}
.demoFeatures {
margin-top: 40px;
padding: 20px;
background: rgba(255, 255, 255, 0.1);
opacity: 0.9;
border-radius: 16px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
text-align: left;
}
.featureTitle {
font-size: 18px;
font-weight: 600;
margin-bottom: 15px;
display: block;
color: white;
}
.featureItem {
font-size: 14px;
margin: 8px 0;
display: block;
color: rgba(255, 255, 255, 0.9);
line-height: 1.5;
}

View File

@@ -1,55 +0,0 @@
import React, { useState } from 'react'
import { View, Button, Text } from '@tarojs/components'
import TimePicker from './TimePicker'
import styles from './demo.module.scss'
const TimePickerDemo: React.FC = () => {
const [visible, setVisible] = useState(false)
const [selectedTime, setSelectedTime] = useState('')
const handleConfirm = (year: number, month: number) => {
setSelectedTime(`${year}${month}`)
setVisible(false)
}
return (
<View className={styles.demoContainer}>
<Text className={styles.title}>TimePicker </Text>
<Text className={styles.subtitle}>"一个一个往下翻"</Text>
<Button
type="primary"
onClick={() => setVisible(true)}
className={styles.demoButton}
>
</Button>
{selectedTime && (
<View className={styles.demoResult}>
<Text>: {selectedTime}</Text>
</View>
)}
<View className={styles.demoFeatures}>
<Text className={styles.featureTitle}>:</Text>
<Text className={styles.featureItem}> </Text>
<Text className={styles.featureItem}> </Text>
<Text className={styles.featureItem}> </Text>
<Text className={styles.featureItem}> </Text>
</View>
<TimePicker
visible={visible}
onClose={() => setVisible(false)}
onConfirm={handleConfirm}
defaultYear={2024}
defaultMonth={6}
minYear={2020}
maxYear={2030}
/>
</View>
)
}
export default TimePickerDemo

View File

@@ -1,187 +0,0 @@
/* 时间选择器弹出层样式 */
.time-picker-popup {
.common-popup-content {
padding: 0;
}
}
.popup-handle {
width: 32px;
height: 4px;
background: #e0e0e0;
border-radius: 2px;
margin: 12px auto;
}
.picker-container {
padding: 26px 16px 0 16px;
background: #fff;
}
.picker-wrapper {
position: relative;
}
.custom-picker {
position: relative;
width: 100%;
height: 216px;
background: #fff;
border-radius: 8px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
/* 确保只显示一个选项 */
perspective: 1000px;
/* 水平布局 */
flex-direction: row;
/* 确保列之间有适当间距 */
gap: 0;
}
/* 选中项指示器 */
.picker-indicator {
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 48px;
background: rgba(22, 24, 35, 0.05);
transform: translateY(-50%);
pointer-events: none;
z-index: 1;
border-radius: 4px;
box-shadow: inset 0 0 0 1px rgba(22, 24, 35, 0.1);
/* 确保指示器完美覆盖选中项 */
margin: 0 20px;
width: calc(100% - 40px);
}
.picker-column {
flex: 1;
height: 100%;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
/* 水平居中布局 */
min-width: 0;
/* 确保列之间有适当间距 */
padding: 0 8px;
&:first-child {
border-right: 1px solid rgba(0, 0, 0, 0.1);
}
/* 确保滚动容器正确显示 */
&::before,
&::after {
content: '';
position: absolute;
left: 0;
right: 0;
height: 84px;
pointer-events: none;
z-index: 2;
}
&::before {
top: 0;
background: linear-gradient(to bottom, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0.8) 50%, rgba(255, 255, 255, 0) 100%);
}
&::after {
bottom: 0;
background: linear-gradient(to top, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0.8) 50%, rgba(255, 255, 255, 0) 100%);
}
}
.picker-scroll {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
will-change: scroll-position;
-webkit-overflow-scrolling: touch;
/* 确保滚动行为 */
scroll-snap-type: y mandatory;
/* 优化滚动性能 */
overscroll-behavior: contain;
}
.picker-padding {
height: 84px; /* (216 - 48) / 2 = 84px用于居中对齐 */
/* 确保padding区域不可见 */
opacity: 0;
pointer-events: none;
}
.picker-item {
display: flex;
align-items: center;
justify-content: center;
height: 48px;
width: 100%;
font-size: 16px;
color: #161823;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
position: relative;
will-change: transform, color;
/* 确保每个选项都能正确对齐 */
scroll-snap-align: center;
/* 水平居中 */
text-align: center;
&.picker-item-active {
color: #161823;
font-weight: 600;
transform: scale(1.02);
.picker-item-text {
color: #161823;
font-weight: 600;
}
}
&:not(.picker-item-active) {
color: rgba(22, 24, 35, 0.6);
.picker-item-text {
color: rgba(22, 24, 35, 0.6);
}
}
}
.picker-item-text {
font-size: 16px;
color: inherit;
text-align: center;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
user-select: none;
width: 100%;
line-height: 48px;
white-space: nowrap;
/* 确保文字完美居中 */
display: block;
overflow: hidden;
text-overflow: ellipsis;
/* 强制居中对齐 */
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
/* 滚动条隐藏 */
.picker-scroll {
::-webkit-scrollbar {
width: 0;
background: transparent;
}
}
/* 移除重复的渐变遮罩代码,已在.picker-column中定义 */

View File

@@ -1,2 +0,0 @@
export { default } from './TimePicker'
export type { TimePickerProps } from './TimePicker'

View File

@@ -1,59 +0,0 @@
.testContainer {
padding: 20px;
text-align: center;
background: #f8f9fa;
min-height: 100vh;
}
.testTitle {
font-size: 22px;
font-weight: 700;
margin-bottom: 10px;
display: block;
color: #333;
}
.testSubtitle {
font-size: 16px;
margin-bottom: 30px;
display: block;
color: #666;
}
.testInfo {
margin: 20px 0;
padding: 20px;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
text-align: left;
text {
font-size: 14px;
margin: 8px 0;
display: block;
color: #555;
line-height: 1.5;
}
}
.testButton {
margin: 20px 0;
width: 200px;
height: 44px;
border-radius: 22px;
font-size: 16px;
background: #007bff;
border: none;
}
.testResult {
margin: 20px 0;
color: white;
border-radius: 8px;
text {
font-size: 16px;
font-weight: 600;
}
}

View File

@@ -1,51 +0,0 @@
import React, { useState } from 'react'
import { View, Text, Button } from '@tarojs/components'
import TimePicker from './TimePicker'
import styles from './layout-test.module.scss'
const LayoutTest: React.FC = () => {
const [visible, setVisible] = useState(false)
const [selectedTime, setSelectedTime] = useState('')
const handleConfirm = (year: number, month: number) => {
setSelectedTime(`${year}${month}`)
setVisible(false)
}
return (
<View className={styles.testContainer}>
<Text className={styles.testTitle}></Text>
<Text className={styles.testSubtitle}></Text>
<View className={styles.testInfo}>
<Text> </Text>
<Text> </Text>
<Text> </Text>
</View>
<Button
type="primary"
onClick={() => setVisible(true)}
className={styles.testButton}
>
</Button>
{selectedTime && (
<View className={styles.testResult}>
<Text>: {selectedTime}</Text>
</View>
)}
<TimePicker
visible={visible}
onClose={() => setVisible(false)}
onConfirm={handleConfirm}
defaultYear={2024}
defaultMonth={6}
minYear={2020}
maxYear={2030}
/>
</View>
)
}

View File

@@ -1,36 +0,0 @@
.container {
padding: 20px;
text-align: center;
background: #f5f5f5;
min-height: 100vh;
}
.title {
font-size: 20px;
font-weight: 600;
color: #333;
margin-bottom: 30px;
display: block;
}
.button {
margin: 20px 0;
width: 200px;
height: 44px;
border-radius: 22px;
font-size: 16px;
}
.result {
margin-top: 30px;
padding: 20px;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
text {
font-size: 16px;
color: #333;
font-weight: 500;
}
}

View File

@@ -1,46 +0,0 @@
import React, { useState } from 'react'
import { View, Button, Text } from '@tarojs/components'
import TimePicker from './TimePicker'
import styles from './test.module.scss'
const TimePickerTest: React.FC = () => {
const [visible, setVisible] = useState(false)
const [selectedTime, setSelectedTime] = useState('')
const handleConfirm = (year: number, month: number) => {
setSelectedTime(`${year}${month}`)
setVisible(false)
}
return (
<View className={styles.container}>
<Text className={styles.title}>TimePicker </Text>
<Button
type="primary"
onClick={() => setVisible(true)}
className={styles.button}
>
</Button>
{selectedTime && (
<View className={styles.result}>
<Text>: {selectedTime}</Text>
</View>
)}
<TimePicker
visible={visible}
onClose={() => setVisible(false)}
onConfirm={handleConfirm}
defaultYear={2024}
defaultMonth={6}
minYear={2020}
maxYear={2030}
/>
</View>
)
}
export default TimePickerTest

View File

@@ -8,7 +8,6 @@ import NumberInterval from "./NumberInterval";
import TimeSelector from "./TimeSelector";
import TitleTextarea from "./TitleTextarea";
import CommonPopup from "./CommonPopup";
import TimePicker from "./TimePicker/TimePicker";
import { CalendarUI, DialogCalendarCard } from "./Picker";
import CommonDialog from "./CommonDialog";
import PublishMenu from "./PublishMenu/PublishMenu";
@@ -28,7 +27,6 @@ export {
TimeSelector,
TitleTextarea,
CommonPopup,
TimePicker,
DialogCalendarCard,
CalendarUI,
CommonDialog,