247 lines
8.0 KiB
TypeScript
247 lines
8.0 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
||
import { ScrollView, StyleProp, StyleSheet, TextStyle, TouchableOpacity, View, ViewStyle } from 'react-native';
|
||
import { ThemedText } from './ThemedText';
|
||
|
||
export type CascaderItem = {
|
||
name: string;
|
||
[key: string]: any; // 允许其他自定义属性
|
||
children?: CascaderItem[];
|
||
};
|
||
|
||
type CascaderProps = {
|
||
data: CascaderItem[]; // 级联数据
|
||
value?: CascaderItem[]; // 选中的值
|
||
onChange?: (value: CascaderItem[]) => void; // 选中项变化时的回调
|
||
displayRender?: (selectedItems: CascaderItem[]) => React.ReactNode; // 自定义显示内容
|
||
style?: StyleProp<ViewStyle>; // 容器样式
|
||
itemStyle?: StyleProp<ViewStyle>; // 选项样式
|
||
activeItemStyle?: StyleProp<ViewStyle>; // 选中项样式
|
||
textStyle?: StyleProp<TextStyle>; // 文字样式
|
||
activeTextStyle?: StyleProp<TextStyle>; // 选中文字样式
|
||
columnWidth?: number; // 列宽
|
||
showDivider?: boolean; // 是否显示分割线
|
||
dividerColor?: string; // 分割线颜色
|
||
showArrow?: boolean; // 是否显示箭头
|
||
};
|
||
|
||
const CascaderComponent: React.FC<CascaderProps> = ({
|
||
data,
|
||
value = [],
|
||
onChange,
|
||
displayRender,
|
||
style,
|
||
activeItemStyle,
|
||
textStyle,
|
||
activeTextStyle,
|
||
columnWidth = 120,
|
||
showDivider = true,
|
||
dividerColor = '#e0e0e0',
|
||
showArrow = false,
|
||
}) => {
|
||
const [selectedItems, setSelectedItems] = useState<CascaderItem[]>(value);
|
||
const [allLevelsData, setAllLevelsData] = useState<CascaderItem[][]>([]);
|
||
|
||
// 初始化数据
|
||
useEffect(() => {
|
||
setAllLevelsData([data]);
|
||
}, [data]);
|
||
|
||
// 处理选择
|
||
const handleSelect = (item: CascaderItem, level: number) => {
|
||
const newSelectedItems = [...selectedItems.slice(0, level), item];
|
||
setSelectedItems(newSelectedItems);
|
||
|
||
// 如果有子项,添加下一级数据
|
||
if (item.children?.length) {
|
||
setAllLevelsData(prev => {
|
||
const newLevels = [...prev.slice(0, level + 1)];
|
||
// 确保 children 存在且是数组
|
||
if (item.children && Array.isArray(item.children)) {
|
||
newLevels.push(item.children);
|
||
}
|
||
return newLevels;
|
||
});
|
||
} else {
|
||
setAllLevelsData(prev => prev.slice(0, level + 1));
|
||
}
|
||
|
||
// 触发onChange回调
|
||
onChange?.(newSelectedItems);
|
||
};
|
||
|
||
// 渲染某一级选项
|
||
const renderLevel = (items: CascaderItem[], level: number) => {
|
||
return (
|
||
<View style={[styles.levelContainer]}>
|
||
{items.map((item, index) => {
|
||
const isActive = selectedItems[level]?.name === item.name;
|
||
return (
|
||
<View key={`${level}-${index}`} style={[
|
||
styles.item,
|
||
isActive && [styles.activeItem, activeItemStyle]
|
||
]}>
|
||
<ScrollView
|
||
horizontal
|
||
showsHorizontalScrollIndicator={false}
|
||
contentContainerStyle={{ flexGrow: 1 }}
|
||
>
|
||
<TouchableOpacity
|
||
style={styles.itemContent}
|
||
onPress={() => handleSelect(item, level)}
|
||
>
|
||
<ThemedText
|
||
style={[
|
||
styles.text,
|
||
textStyle,
|
||
isActive && [styles.activeText, activeTextStyle]
|
||
]}
|
||
type='sfPro'
|
||
>
|
||
{item.name}
|
||
</ThemedText>
|
||
{showArrow && item.children?.length ? (
|
||
<ThemedText style={styles.arrow}>›</ThemedText>
|
||
) : null}
|
||
</TouchableOpacity>
|
||
</ScrollView>
|
||
</View>
|
||
);
|
||
})}
|
||
</View>
|
||
);
|
||
};
|
||
|
||
// 渲染所有级联列
|
||
const renderColumns = () => {
|
||
const totalLevels = allLevelsData.length;
|
||
return allLevelsData.map((items, level) => {
|
||
// 计算每列的宽度
|
||
let width;
|
||
if (totalLevels === 1) {
|
||
width = '100%'; // 只有一级时占满全部宽度
|
||
} else if (totalLevels === 2) {
|
||
width = level === 0 ? '40%' : '60%'; // 两级时第一级40%,第二级60%
|
||
} else {
|
||
// 三级或以上时,前两级各占30%,其余级别平分剩余40%
|
||
if (level < 2) {
|
||
width = '30%';
|
||
} else {
|
||
const remainingLevels = totalLevels - 2;
|
||
width = remainingLevels > 0 ? `${40 / remainingLevels}%` : '40%';
|
||
}
|
||
}
|
||
|
||
return (
|
||
<View
|
||
key={`column-${level}`}
|
||
style={[
|
||
styles.column,
|
||
{ width },
|
||
showDivider && level < totalLevels - 1 && [
|
||
styles.columnWithDivider,
|
||
{ borderRightColor: dividerColor }
|
||
]
|
||
]}
|
||
>
|
||
<ScrollView
|
||
style={{ flex: 1 }}
|
||
showsVerticalScrollIndicator={true}
|
||
contentContainerStyle={{ paddingBottom: 20 }}
|
||
>
|
||
{renderLevel(items, level)}
|
||
</ScrollView>
|
||
</View>
|
||
);
|
||
});
|
||
};
|
||
|
||
// 自定义显示内容
|
||
const renderDisplay = () => {
|
||
if (displayRender) {
|
||
return displayRender(selectedItems);
|
||
}
|
||
return selectedItems.map(item => item.name).join(' / ');
|
||
};
|
||
|
||
return (
|
||
<View style={[styles.container, style]}>
|
||
<ScrollView
|
||
horizontal
|
||
showsHorizontalScrollIndicator={false}
|
||
contentContainerStyle={styles.scrollContent}
|
||
style={{ flex: 1 }}
|
||
>
|
||
{renderColumns()}
|
||
</ScrollView>
|
||
{displayRender && (
|
||
<View style={styles.displayContainer}>
|
||
{renderDisplay()}
|
||
</View>
|
||
)}
|
||
</View>
|
||
);
|
||
};
|
||
|
||
const styles = StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
backgroundColor: '#fff',
|
||
height: 300,
|
||
},
|
||
scrollContent: {
|
||
flexGrow: 1,
|
||
height: '100%',
|
||
flexDirection: 'row',
|
||
},
|
||
column: {
|
||
height: '100%',
|
||
maxHeight: '100%',
|
||
flexShrink: 0,
|
||
},
|
||
columnWithDivider: {
|
||
borderRightWidth: 1,
|
||
},
|
||
levelContainer: {
|
||
height: '100%',
|
||
maxHeight: '100%',
|
||
},
|
||
item: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
paddingVertical: 12,
|
||
paddingHorizontal: 16,
|
||
minWidth: '100%',
|
||
overflow: 'hidden',
|
||
},
|
||
text: {
|
||
fontSize: 15,
|
||
color: '#333',
|
||
flexShrink: 0, // 禁止收缩
|
||
paddingRight: 4,
|
||
},
|
||
itemContent: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
paddingRight: 16, // 确保有足够的右边距
|
||
},
|
||
activeItem: {
|
||
backgroundColor: '#F6F6F6',
|
||
},
|
||
activeText: {
|
||
color: '#AC7E35',
|
||
fontWeight: '500',
|
||
},
|
||
arrow: {
|
||
fontSize: 18,
|
||
color: '#999',
|
||
marginLeft: 8,
|
||
},
|
||
displayContainer: {
|
||
padding: 12,
|
||
borderTopWidth: 1,
|
||
borderTopColor: '#f0f0f0',
|
||
},
|
||
});
|
||
|
||
export default CascaderComponent; |