You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
145 lines
5.0 KiB
145 lines
5.0 KiB
import { useEffect, useRef } from 'react';
|
|
import type { NativeScrollEvent, NativeSyntheticEvent } from 'react-native';
|
|
import { ScrollView, StyleSheet, Text, View } from 'react-native';
|
|
import { colors, spacing } from '../../../src/theme';
|
|
import { CURRENT_YEAR } from '../../../src/constants';
|
|
|
|
const ITEM_H = 44;
|
|
|
|
function Column({
|
|
items,
|
|
value,
|
|
onValueChange,
|
|
width,
|
|
restingIndex,
|
|
}: {
|
|
items: string[];
|
|
value: string;
|
|
onValueChange: (v: string) => void;
|
|
width: number;
|
|
/** 未选中时滚轮的停留位置(只影响视觉停留,不等于已选中) */
|
|
restingIndex: number;
|
|
}) {
|
|
const scrollRef = useRef<ScrollView>(null);
|
|
// 未选中('')与用户手滑到第 0 项是两回事:这里不再把空值钳到索引 0,
|
|
// 而是保留「无选中项」状态,只在视觉上停在 restingIndex。
|
|
const idx = items.indexOf(value);
|
|
const initialIndex = idx >= 0 ? idx : restingIndex;
|
|
|
|
// contentOffset 只在 iOS 生效,Android 上不定位会让滚轮停在第一项、
|
|
// 与高亮框里的文字对不上,故挂载时用 scrollTo 补一次(两端通用)。
|
|
useEffect(() => {
|
|
scrollRef.current?.scrollTo({ y: initialIndex * ITEM_H, animated: false });
|
|
// 仅挂载时定位:后续滚动由用户驱动,避免每次选值把滚轮拽回去
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
const handleEnd = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
const y = e.nativeEvent.contentOffset.y;
|
|
const i = Math.min(items.length - 1, Math.max(0, Math.round(y / ITEM_H)));
|
|
const v = items[i];
|
|
if (v !== value) onValueChange(v);
|
|
};
|
|
|
|
return (
|
|
<View style={[styles.col, { width }]}>
|
|
<View style={[styles.highlight, idx < 0 && styles.highlightIdle]} pointerEvents="none" />
|
|
<ScrollView
|
|
ref={scrollRef}
|
|
style={styles.scroll}
|
|
showsVerticalScrollIndicator={false}
|
|
snapToInterval={ITEM_H}
|
|
snapToAlignment="center"
|
|
decelerationRate="fast"
|
|
contentOffset={{ x: 0, y: initialIndex * ITEM_H }}
|
|
contentContainerStyle={{ paddingVertical: ITEM_H }}
|
|
onMomentumScrollEnd={handleEnd}
|
|
>
|
|
{items.map((it) => (
|
|
<View key={it} style={styles.item}>
|
|
<Text style={[styles.itemText, it === value && styles.itemTextActive]}>{it}</Text>
|
|
</View>
|
|
))}
|
|
</ScrollView>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
interface DualProps {
|
|
year: string;
|
|
month: string;
|
|
onYearChange: (v: string) => void;
|
|
onMonthChange: (v: string) => void;
|
|
/**
|
|
* 未选择年份时滚轮的停留项。默认停在当年(适合「入职时间」这类近期场景);
|
|
* 「出生年月」应传一个合理的起始年份,否则用户要空滑二十多行。
|
|
*/
|
|
restingYear?: string;
|
|
/**
|
|
* 可选年份跨度:往过去几年、往未来几年。
|
|
* 原实现固定 40 项(当年往前 39 年),只能覆盖 0–39 岁 ——
|
|
* 46 岁的求职者选不了自己的出生年;而「入职时间」也选不到明年。
|
|
*/
|
|
pastYears?: number;
|
|
futureYears?: number;
|
|
}
|
|
|
|
/** 年月双滚轮:用于出生年月 / 首次参加工作 / 入职时间 */
|
|
export function YearMonthWheel({
|
|
year,
|
|
month,
|
|
onYearChange,
|
|
onMonthChange,
|
|
restingYear = `${CURRENT_YEAR}年`,
|
|
pastYears = 60,
|
|
futureYears = 0,
|
|
}: DualProps) {
|
|
const years = Array.from(
|
|
{ length: pastYears + futureYears + 1 },
|
|
(_, i) => `${CURRENT_YEAR + futureYears - i}年`,
|
|
);
|
|
const months = Array.from({ length: 12 }, (_, i) => `${i + 1}月`);
|
|
const unselected = !year || !month;
|
|
const restingYearIndex = Math.max(0, years.indexOf(restingYear));
|
|
|
|
return (
|
|
<View>
|
|
<View style={styles.dual}>
|
|
<Column
|
|
items={years}
|
|
value={year}
|
|
onValueChange={onYearChange}
|
|
width={130}
|
|
restingIndex={restingYearIndex}
|
|
/>
|
|
<Column items={months} value={month} onValueChange={onMonthChange} width={110} restingIndex={0} />
|
|
</View>
|
|
{/* 未选中时滚轮无高亮项,需要文字提示「要滑动才有值」,否则用户会以为已经选好了 */}
|
|
{unselected ? <Text style={styles.hint}>请滑动选择</Text> : null}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
col: { height: ITEM_H * 3, position: 'relative' },
|
|
scroll: { flex: 1 },
|
|
highlight: {
|
|
position: 'absolute',
|
|
top: ITEM_H,
|
|
left: 0,
|
|
right: 0,
|
|
height: ITEM_H,
|
|
borderTopWidth: 1,
|
|
borderBottomWidth: 1,
|
|
borderColor: colors.border,
|
|
backgroundColor: colors.primaryLight,
|
|
opacity: 0.45,
|
|
},
|
|
// 未选中:高亮框退成中性描边,不再用主色底暗示「已经选好了」
|
|
highlightIdle: { backgroundColor: colors.white, opacity: 0.6 },
|
|
item: { height: ITEM_H, alignItems: 'center', justifyContent: 'center' },
|
|
itemText: { fontSize: 18, color: colors.textHint },
|
|
itemTextActive: { fontSize: 20, fontWeight: '700', color: colors.textPrimary },
|
|
dual: { flexDirection: 'row', justifyContent: 'center', gap: spacing.block },
|
|
hint: { fontSize: 13, color: colors.textHint, textAlign: 'center', marginTop: spacing.sm },
|
|
});
|
|
|