我正在与一个使用 Stepper 组件来控制结帐过程页面的客户合作。流程是购物车 --> 结帐 --> 确认付款 --> 订单摘要。每个页面都有一个带有数字索引的“步骤”,从 0 到 3。在 Checkout 组件中,有一个 CheckoutLayout 组件包装其他屏幕并控制 handleSubmit 和后退按钮功能。后退按钮本身可以正常工作,但我在结帐布局中添加了一个 panResponder,它在向左滑动时触发回调。出于某种原因,滑动触发了重新渲染,它将活动步骤重置为 0(单击自定义后退按钮时不会发生这种情况)。一旦在 panResponder 中触发回调,有谁知道如何在用户滑动时停止手势?
结帐组件:
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Animated, StyleSheet } from 'react-native';
import {
Button,
Footer,
Stepper,
useDeviceType,
useStyles,
} from '@nutrien/bonsai-core';
import { usePlatformNavigation } from '@nutrien/hub-routing';
import { EcommFooter } from '@nutrien/hub-shared';
import { OrderConfirmationProvider } from '../../../contexts/OrderConfirmationContext';
import useTranslation from '../../../hooks/usePackageTranslation';
import CheckoutConfirmation from '../../organisms-br/CheckoutConfirmation';
import CheckoutForm from '../../organisms-br/CheckoutForm';
import CheckoutLayout from '../../organisms-br/CheckoutLayout';
import CheckoutPayment from '../../organisms-br/CheckoutPayment';
export interface CheckoutPageRef {
submit: () => void;
}
export const Checkout = (): JSX.Element => {
const TABLET_WIDTH = 466;
const { push, goBack } = usePlatformNavigation();
const { t } = useTranslation();
const { isHandset, isTablet, isDesktop } = useDeviceType();
const [showOverlay, setOverlay] = useState(false);
const [isSubmitEnable, enableSubmit] = useState(false);
const [isLoading, setLoading] = useState(false);
const checkoutFormRef = useRef<CheckoutPageRef>();
const paymentFormRef = useRef<CheckoutPageRef>();
const confirmationFormRef = useRef<CheckoutPageRef>();
const overlayOpacity = useRef(new Animated.Value(0)).current;
const styles = useStyles((theme) => ({
container: {
flex: 1,
backgroundColor: theme.colors.grey0,
flexGrow: 1,
},
footer: {
justifyContent: isDesktop ? 'flex-end' : 'center',
backgroundColor: theme.colors.white,
paddingBottom: theme.spacing(1),
borderStyle: 'solid',
borderTopWidth: 1,
borderColor: theme.colors.grey[200],
},
button: {
width: isTablet ? TABLET_WIDTH : 'auto',
},
footerWrapper: {
marginHorizontal: isHandset ? theme.spacing(5) : theme.spacing(6),
marginBottom: isDesktop ? theme.spacing(3) : 0,
},
}));
const stepLabels = [
t('checkout.steps.checkout'),
t('checkout.steps.payment'),
t('checkout.steps.confirmation'),
];
const pageTitle = [
{
title: t('checkout.addressTitle'),
submitLabel: t('checkout.goToPayment'),
},
{
title: t('checkout.payment'),
submitLabel: t('checkout.confirmOrder'),
},
{
title: t('checkout.success.title'),
submitLabel: t('checkout.goToMyOrders'),
},
];
useEffect(() => {
Animated.timing(overlayOpacity, {
toValue: showOverlay ? 1 : 0,
duration: 450,
useNativeDriver: true,
}).start();
}, [overlayOpacity, showOverlay]);
const [activeStep, setActiveStep] = useState(0);
const handleSubmit = () => {
if (activeStep === 0) {
checkoutFormRef.current?.submit();
}
if (activeStep === 1) {
paymentFormRef.current?.submit();
}
if (activeStep === 2) {
confirmationFormRef.current?.submit();
}
};
const handleBack = () => {
if (activeStep > 0) {
setActiveStep(activeStep - 1);
} else {
goBack();
}
};
const onSwipeBack = useCallback(() => {
console.log('how many times does this get hit?');
console.log('activeStep: ', activeStep);
setActiveStep((prev) => {
console.log('prev: ', prev);
if (prev === 0) {
goBack();
return 0;
} else {
return prev - 1;
}
});
// if (activeStep === 0) goBack();
}, [activeStep, goBack]);
return (
<>
<CheckoutLayout
title={pageTitle[activeStep].title}
showBackButton={activeStep !== 3}
onBackButtonPress={handleBack}
onSwipeBack={onSwipeBack}
>
<Stepper
activeStep={activeStep}
labels={stepLabels}
orientation="horizontal"
>
<CheckoutForm
ref={checkoutFormRef}
handleSubmit={() => setActiveStep(1)}
onLoading={setLoading}
onEnableSubmit={enableSubmit}
onOverlay={setOverlay}
/>
<CheckoutPayment
ref={paymentFormRef}
handleSubmit={() => setActiveStep(2)}
onLoading={setLoading}
onEnableSubmit={enableSubmit}
/>
<CheckoutConfirmation
ref={confirmationFormRef}
handleSubmit={() => push('accounts.orders')}
onEnableSubmit={enableSubmit}
/>
</Stepper>
<EcommFooter simplified style={styles.footerWrapper} />
</CheckoutLayout>
<Footer style={styles.footer}>
<Button
large
title={pageTitle[activeStep].submitLabel}
disabled={!isSubmitEnable || isLoading}
onPress={handleSubmit}
fullWidth={isHandset}
style={styles.button}
/>
</Footer>
<Animated.View
pointerEvents="none"
style={{
...StyleSheet.absoluteFillObject,
backgroundColor: '#000',
opacity: overlayOpacity.interpolate({
inputRange: [0, 1],
outputRange: [0, 0.2],
}),
}}
/>
</>
);
};
export default (): JSX.Element => (
<OrderConfirmationProvider>
<Checkout />
</OrderConfirmationProvider>
);
CheckoutLayout 组件:
import {
Dimensions,
ScrollView,
TextStyle,
View,
ViewStyle,
} from 'react-native';
import {
ActivityIndicator,
Button,
Text,
useDeviceType,
useStyles,
useTheme,
} from '@nutrien/bonsai-core';
import {
useSelectedRetailAccount,
useSwipeCallback,
} from '@nutrien/hub-shared';
import useTranslation from '../../../hooks/usePackageTranslation';
interface Styles {
container: ViewStyle;
titleStyle: TextStyle;
scrollView: ViewStyle;
header: ViewStyle;
headerContainer: ViewStyle;
backButton: ViewStyle;
backButtonContainer: ViewStyle;
activityIndicator: ViewStyle;
}
interface CheckoutLayoutProps {
title: string;
showBackButton: boolean;
onBackButtonPress: () => void;
onSwipeBack: () => void;
}
const CheckoutLayout: React.FC<CheckoutLayoutProps> = ({
children,
title,
showBackButton,
onBackButtonPress,
onSwipeBack,
}) => {
const TABLE_WIDTH_FOR_TABLETS = 466;
const { height } = Dimensions.get('window');
const { t } = useTranslation();
const { isHandset, isTablet, isDesktop } = useDeviceType();
const theme = useTheme();
const { selectedAccount } = useSelectedRetailAccount();
const swipeCallback = useSwipeCallback({
callback: onSwipeBack,
}).panHandlers;
const styles = useStyles<Styles>((theme) => ({
container: {
flex: 1,
backgroundColor: theme.colors.grey0,
flexGrow: 1,
},
scrollView: {
height: height / 2,
},
titleStyle: {
marginBottom: 0,
},
header: {
position: 'relative',
backgroundColor: theme.colors.grey1,
padding: isHandset ? theme.spacing(2) : theme.spacing(3),
paddingTop: isHandset
? theme.spacing(7)
: isTablet
? theme.spacing(8)
: theme.spacing(9),
},
headerContainer: {
width: '100%',
maxWidth: isTablet ? TABLE_WIDTH_FOR_TABLETS : 'auto',
alignSelf: 'center',
},
backButton: {
paddingHorizontal: 0,
},
backButtonContainer: {
position: 'absolute',
top: isDesktop ? theme.spacing(3) : theme.spacing(2),
left: isHandset ? theme.spacing(1) : theme.spacing(2),
paddingHorizontal: isHandset ? 0 : theme.spacing(0.5),
},
activityIndicator: {
alignSelf: 'flex-start',
},
}));
return (
<View style={styles.container} {...swipeCallback}>
<ScrollView style={styles.scrollView}>
<View style={styles.header}>
{showBackButton && (
<Button
onPress={() => onBackButtonPress()}
icon={{
color: theme.colors.primary,
name: 'chevron-left',
}}
containerStyle={styles.backButtonContainer}
buttonStyle={styles.backButton}
title={t('navigation.back')}
type="clear"
/>
)}
<View style={styles.headerContainer}>
<Text h1 style={styles.titleStyle}>
{title}
</Text>
{selectedAccount ? (
<Text style={styles.titleStyle}>
{selectedAccount?.name || ''}
</Text>
) : (
<ActivityIndicator style={styles.activityIndicator} />
)}
</View>
</View>
{children}
</ScrollView>
</View>
);
};
export default CheckoutLayout;
UseSwipeCallback 钩子:
import { PanResponder, Platform } from 'react-native';
const useSwipeCallback = ({
callback,
direction,
}: {
callback: () => void;
direction?: 'forward' | 'back';
}) => {
return useRef(
PanResponder.create({
/** onPanResponderRelease does not work on android, so using onMoveShouldSetPanResponder, instead */
onMoveShouldSetPanResponder: (evt, gestureState) => {
const swipeDirection =
direction === 'forward' ? gestureState.vx < 0 : gestureState.vx > 0;
const gestureDistanceX = Math.floor(gestureState.dx);
const gestureDistanceYScaled = Math.abs(gestureState.dy) * 2;
const isUniSwipeHorizontal =
direction === 'forward'
? gestureDistanceX < -1 * gestureDistanceYScaled
: gestureDistanceX > gestureDistanceYScaled;
if (Platform.OS !== 'web' && swipeDirection && isUniSwipeHorizontal) {
callback();
}
return false;
},
// onPanResponderTerminationRequest: () => true,
})
).current;
};
export default useSwipeCallback;