memowake-front/components/cascader.tsx
2025-08-08 18:55:18 +08:00

247 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;