轻松聘
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

import { useEffect, useRef } from 'react';
import type { NativeScrollEvent, NativeSyntheticEvent } from 'react-native';
import { ScrollView, StyleSheet, Text, View } from 'react-native';
import { colors } 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: 14 },
hint: { fontSize: 13, color: colors.textHint, textAlign: 'center', marginTop: 10 },
});