hejianhao
2026-06-29 0a3e3b884072dfaf8d8a0f66bceaeccdbf9e4ad9
refactor: 抽离底部弹窗动画hook并优化样式
1个文件已添加
11个文件已修改
419 ■■■■■ 已修改文件
src/robotaxi/pages/begin-route/components/announcements-popup.tsx 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/auth-bluetooth-popup/index.tsx 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/auth-bluetooth-popup/success-auth/index.tsx 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/temperature-control.tsx 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/cancel-popup.tsx 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/calling-car.less 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/evaluate-popup.tsx 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/hooks/use-bottom-popup-animation.ts 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/no-route-popup.tsx 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/passenger-notice.tsx 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/pickup-exception-popup.tsx 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/route-preference-popup/index.tsx 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/announcements-popup.tsx
@@ -2,6 +2,7 @@
import { View, Image, Video } from '@tarojs/components';
import { useEffect, useState } from 'react';
import './css-folder/announcements-popup.less'
import useBottomPopupAnimation from './hooks/use-bottom-popup-animation';
type Props = {
    show?: boolean;
@@ -9,37 +10,18 @@
}
export default function AnnouncementsPopup({ show = false, onClose }: Props) {
    const [visible, setVisible] = useState(false); // 弹窗显示状态
    const [animationData, setAnimationData] = useState<any>(); // 弹窗开合动画
    const { visible, animationData } = useBottomPopupAnimation(show);
    const [doorImgError, setDoorImgError] = useState(false); // 开门动画图加载失败状态
    const [seatbeltImgError, setSeatbeltImgError] = useState(false); // 系安全带动画图加载失败状态
    const doorImage = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/door-anim.webp'
    const seatbeltImage = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/seatbelt-anim.webp'
    // 控制弹窗开合动画并重置图片兜底状态
    // 打开弹窗时重置图片兜底状态
    useEffect(() => {
        if (show && !visible) {
            setVisible(true);
        if (show) {
            setDoorImgError(false);
            setSeatbeltImgError(false);
            const animation = Taro.createAnimation({
                duration: 300,
                timingFunction: 'ease-out'
            });
            animation.translateY(0).step();
            setAnimationData(animation.export());
        } else if (!show && visible) {
            const animation = Taro.createAnimation({
                duration: 300,
                timingFunction: 'ease-in'
            });
            animation.translateY(500).step();
            setAnimationData(animation.export());
            setTimeout(() => {
                setVisible(false);
            }, 300);
        }
    }, [show]);
src/robotaxi/pages/begin-route/components/auth-bluetooth-popup/index.tsx
@@ -1,7 +1,6 @@
import Taro from '@tarojs/taro';
import { View, Text, Image } from '@tarojs/components';
import { useEffect, useState } from 'react';
import './index.less';
import useBottomPopupAnimation from '../hooks/use-bottom-popup-animation';
type Props = {
  show: boolean;
@@ -10,32 +9,9 @@
};
export default function AuthBluetoothPopup({ show, onClose, onAuthorize }: Props) {
  const [visible, setVisible] = useState(false);
  const [animationData, setAnimationData] = useState<any>();
  const { animationData, shouldRender } = useBottomPopupAnimation(show);
  useEffect(() => {
    if (show && !visible) {
      setVisible(true);
      const animation = Taro.createAnimation({
        duration: 300,
        timingFunction: 'ease-out'
      });
      animation.opacity(1).translateY(0).step();
      setAnimationData(animation.export());
    } else if (!show && visible) {
      const animation = Taro.createAnimation({
        duration: 300,
        timingFunction: 'ease-in'
      });
      animation.opacity(1).translateY('100%').step();
      setAnimationData(animation.export());
      setTimeout(() => {
        setVisible(false);
      }, 300);
    }
  }, [show, visible]);
  if (!visible && !show) return null;
  if (!shouldRender) return null;
  return (
    <View className={`auth-bluetooth-popup ${show ? 'show' : ''}`} catchMove>
src/robotaxi/pages/begin-route/components/auth-bluetooth-popup/success-auth/index.tsx
@@ -1,7 +1,6 @@
import Taro from '@tarojs/taro';
import { View, Text, Image } from '@tarojs/components';
import { useEffect, useState } from 'react';
import './index.less';
import useBottomPopupAnimation from '../../hooks/use-bottom-popup-animation';
type Props = {
  show: boolean;
@@ -10,32 +9,9 @@
};
export default function SuccessAuthPopup({ show, onClose, onConfirm }: Props) {
  const [visible, setVisible] = useState(false);
  const [animationData, setAnimationData] = useState<any>();
  const { animationData, shouldRender } = useBottomPopupAnimation(show);
  useEffect(() => {
    if (show && !visible) {
      setVisible(true);
      const animation = Taro.createAnimation({
        duration: 300,
        timingFunction: 'ease-out'
      });
      animation.opacity(1).translateY(0).step();
      setAnimationData(animation.export());
    } else if (!show && visible) {
      const animation = Taro.createAnimation({
        duration: 300,
        timingFunction: 'ease-in'
      });
      animation.opacity(1).translateY('100%').step();
      setAnimationData(animation.export());
      setTimeout(() => {
        setVisible(false);
      }, 300);
    }
  }, [show, visible]);
  if (!visible && !show) return null;
  if (!shouldRender) return null;
  const handleConfirm = () => {
    onConfirm?.();
src/robotaxi/pages/begin-route/components/calling-car-modules/temperature-control.tsx
@@ -1,5 +1,5 @@
import Taro from "@tarojs/taro";
import { View, Image } from "@tarojs/components";
import { View, Image, Text } from "@tarojs/components";
import { useState, useEffect, useRef } from "react";
// TODO(仅用于测试): 上线前将此处改回从 '../../../../api/begin-route' 导入 './test-case/ac-mock'
import {
@@ -50,9 +50,10 @@
    const canToggleSwitch = tripStatus === TripStatus.VehicleApproaching && !isTogglingAc;
    const canAdjustTemperature = canToggleSwitch && acStatusLoaded && acSwitchOn && typeof tempStatus === 'number';
    const shouldShowTemperatureUnit = typeof tempStatus === 'number' && tempStatus > TEMP_MIN && tempStatus < TEMP_MAX;
    // 温度展示文案:到达最高温显示 HI,到达最低温显示 LO,其余显示具体温度值
    const tempDisplayText = typeof tempStatus === 'number'
        ? (tempStatus >= TEMP_MAX ? 'HI' : (tempStatus <= TEMP_MIN ? 'LO' : `${tempStatus.toFixed(1)}℃`))
        ? (tempStatus >= TEMP_MAX ? 'HI' : (tempStatus <= TEMP_MIN ? 'LO' : tempStatus.toFixed(1)))
        : '--';
    const isAtMinTemperature = typeof tempStatus === 'number' && tempStatus <= TEMP_MIN;
    const isAtMaxTemperature = typeof tempStatus === 'number' && tempStatus >= TEMP_MAX;
@@ -356,7 +357,15 @@
                        }
                    </View>
                    <View className="car-card-temp-num-text">
                        <View className={`car-card-temp-num-text-temp ${tempTextDisabledClassName}`.trim()}>{tempDisplayText}</View>
                        <View className={`car-card-temp-num-text-temp ${tempTextDisabledClassName}`.trim()}>
                            <View className="car-card-temp-num-text-temp-value">{tempDisplayText}</View>
                            {shouldShowTemperatureUnit && (
                                <View className="car-card-temp-num-text-temp-unit">
                                    <View className="car-card-temp-num-text-temp-unit-degree">°</View>
                                    <View className="car-card-temp-num-text-temp-unit-letter">C</View>
                                </View>
                            )}
                        </View>
                        <View className={`car-card-temp-num-text-desc ${tempTextDisabledClassName}`.trim()}>设定温度</View>
                    </View>
                    <View className="car-card-temp-num-btn">
src/robotaxi/pages/begin-route/components/cancel-popup.tsx
@@ -7,6 +7,7 @@
    clearOrderTripStorage
} from '../../../../utils/Robotaxi-public-method'
import { TripStatus } from '../../../constants/trip-status';
import useBottomPopupAnimation from './hooks/use-bottom-popup-animation';
type Props = {
@@ -23,8 +24,7 @@
}
export default function CancelPopup({ show = false, cancelType = 1, startSite, endSite, waitMinutes, tripStatus, routeDurationSeconds, discountAmount, onClose, onConfirm }: Props) {
    const [visible, setVisible] = useState(false); // 弹窗显示状态
    const [animationData, setAnimationData] = useState<any>(); // 弹窗开合动画
    const { visible, animationData } = useBottomPopupAnimation(show);
    const cachedStartSite = startSite || Taro.getStorageSync('startSite') || {};
    const cachedEndSite = endSite || Taro.getStorageSync('endSite') || {};
    const startSiteName = cachedStartSite?.name || cachedStartSite?.address || '-';
@@ -65,30 +65,6 @@
            tip: '当前车辆无法继续服务',
        },
    }
    // 控制弹窗开合动画
    useEffect(() => {
        if (show && !visible) {
            setVisible(true);
            const animation = Taro.createAnimation({
                duration: 300,
                timingFunction: 'ease-out'
            });
            animation.translateY(0).step();
            setAnimationData(animation.export());
        } else if (!show && visible) {
            const animation = Taro.createAnimation({
                duration: 300,
                timingFunction: 'ease-in'
            });
            animation.translateY(500).step();
            setAnimationData(animation.export());
            setTimeout(() => {
                setVisible(false);
            }, 300);
        }
    }, [show]);
    // 关闭弹窗时按类型执行对应流程
    const closePopup = () => {
src/robotaxi/pages/begin-route/components/css-folder/calling-car.less
@@ -174,11 +174,36 @@
          gap: 16px;
          &-temp {
            display: flex;
            align-items: flex-start;
            font-size: 52px;
            font-weight: 500;
            line-height: 48px;
            letter-spacing: normal;
            color: rgba(0, 0, 0, 0.9);
            &-value {
              line-height: 48px;
            }
            &-unit {
              display: flex;
              align-items: flex-start;
              margin-top: 0;
              margin-left: 2px;
              line-height: 1;
              &-degree {
                font-size: 40px;
                line-height: 24px;
              }
              &-letter {
                margin-top: 0;
                margin-left: 1px;
                font-size: 32px;
                line-height: 24px;
              }
            }
          }
          &-desc {
@@ -769,4 +794,4 @@
      height: 17.5px;
    }
  }
}
}
src/robotaxi/pages/begin-route/components/evaluate-popup.tsx
@@ -6,6 +6,7 @@
import { _ROBOTAXI_HOST } from '../../../../constants/api';
import useImageUpload from '../../../hooks/use-image-upload';
import { getOrderId } from '../../../utils/order';
import useBottomPopupAnimation from './hooks/use-bottom-popup-animation';
type Props = {
    show?: boolean;
@@ -32,8 +33,7 @@
    const activeBad = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/active-bad.png';
    const activeNormal = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/active-normal.png';
    const activeGood = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/active-good.png';
    const [visible, setVisible] = useState(false); // 弹窗显示状态
    const [animationData, setAnimationData] = useState<any>(); // 弹窗进出场动画
    const { visible, animationData } = useBottomPopupAnimation(show);
    const [selectedTypeId, setSelectedTypeId] = useState<number | null>(null); // 当前选择的问题类型
    const [feedbackContent, setFeedbackContent] = useState(''); // 反馈文本内容
    const [typeOptions, setTypeOptions] = useState<any[]>([]); // 问题类型选项
@@ -82,30 +82,6 @@
    const evaluateDimension = evaluateDimensionMap[currentOrderRating] || 3; // 接口维度:1 满意,2 一般,3 不满意
    const shouldRequireFeedbackType = !hideFeedbackType;
    const canSubmit = shouldRequireFeedbackType ? !!selectedTypeId && !!feedbackContent.trim() : !!feedbackContent.trim();
    // 控制弹窗开合动画
    useEffect(() => {
        if (show && !visible) {
            setVisible(true);
            const animation = Taro.createAnimation({
                duration: 300,
                timingFunction: 'ease-out'
            });
            animation.translateY(0).step();
            setAnimationData(animation.export());
        } else if (!show && visible) {
            const animation = Taro.createAnimation({
                duration: 300,
                timingFunction: 'ease-in'
            });
            animation.translateY(500).step();
            setAnimationData(animation.export());
            setTimeout(() => {
                setVisible(false);
            }, 300);
        }
    }, [show]);
    // 提交反馈(评价/投诉)
    const submit = () => {
src/robotaxi/pages/begin-route/components/hooks/use-bottom-popup-animation.ts
New file
@@ -0,0 +1,99 @@
import Taro from '@tarojs/taro';
import { useEffect, useRef, useState } from 'react';
type UseBottomPopupAnimationOptions = {
  duration?: number;
  openTimingFunction?: 'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out';
  closeTimingFunction?: 'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out';
  offsetY?: number | string;
};
const ENTER_START_DELAY_MS = 30;
/**
 * 统一管理底部弹窗的开合动画,确保每次打开都能从底部稳定滑入。
 */
export default function useBottomPopupAnimation(
  show: boolean,
  options: UseBottomPopupAnimationOptions = {}
) {
  const {
    duration = 300,
    openTimingFunction = 'ease-out',
    closeTimingFunction = 'ease-in',
    offsetY = '100%'
  } = options;
  const [visible, setVisible] = useState(false);
  const [animationData, setAnimationData] = useState<any>();
  const enterTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const exitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  useEffect(() => {
    if (enterTimerRef.current) {
      clearTimeout(enterTimerRef.current);
      enterTimerRef.current = null;
    }
    if (exitTimerRef.current) {
      clearTimeout(exitTimerRef.current);
      exitTimerRef.current = null;
    }
    if (show) {
      setVisible(true);
      const enterInitialAnimation = Taro.createAnimation({
        duration: 0,
        timingFunction: 'linear'
      });
      enterInitialAnimation.translateY(offsetY).step();
      setAnimationData(enterInitialAnimation.export());
      enterTimerRef.current = setTimeout(() => {
        const enterAnimation = Taro.createAnimation({
          duration,
          timingFunction: openTimingFunction
        });
        enterAnimation.translateY(0).step();
        setAnimationData(enterAnimation.export());
        enterTimerRef.current = null;
      }, ENTER_START_DELAY_MS);
      return;
    }
    if (!visible) {
      return;
    }
    const exitAnimation = Taro.createAnimation({
      duration,
      timingFunction: closeTimingFunction
    });
    exitAnimation.translateY(offsetY).step();
    setAnimationData(exitAnimation.export());
    exitTimerRef.current = setTimeout(() => {
      setVisible(false);
      const resetAnimation = Taro.createAnimation({
        duration: 0,
        timingFunction: 'linear'
      });
      resetAnimation.translateY(offsetY).step();
      setAnimationData(resetAnimation.export());
      exitTimerRef.current = null;
    }, duration);
  }, [show]);
  useEffect(() => () => {
    if (enterTimerRef.current) {
      clearTimeout(enterTimerRef.current);
    }
    if (exitTimerRef.current) {
      clearTimeout(exitTimerRef.current);
    }
  }, []);
  return {
    visible,
    animationData,
    shouldRender: show || visible
  };
}
src/robotaxi/pages/begin-route/components/no-route-popup.tsx
@@ -1,7 +1,6 @@
import Taro from '@tarojs/taro';
import { View } from '@tarojs/components';
import { useEffect, useState } from 'react';
import './css-folder/no-route-popup.less';
import useBottomPopupAnimation from './hooks/use-bottom-popup-animation';
interface NoRoutePopupProps {
  show: boolean;
@@ -11,31 +10,7 @@
export default function NoRoutePopup(props: NoRoutePopupProps) {
  const { show, onClose, onConfirm } = props;
  const [visible, setVisible] = useState(false);
  const [animationData, setAnimationData] = useState<any>();
  useEffect(() => {
    if (show && !visible) {
      setVisible(true);
      const animation = Taro.createAnimation({
        duration: 300,
        timingFunction: 'ease-out'
      });
      animation.translateY(0).step();
      setAnimationData(animation.export());
    } else if (!show && visible) {
      const animation = Taro.createAnimation({
        duration: 300,
        timingFunction: 'ease-in'
      });
      animation.translateY(500).step();
      setAnimationData(animation.export());
      setTimeout(() => {
        setVisible(false);
      }, 300);
    }
  }, [show]);
  const { visible, animationData } = useBottomPopupAnimation(show);
  if (!visible) return null;
src/robotaxi/pages/begin-route/components/passenger-notice.tsx
@@ -3,6 +3,7 @@
import { useEffect, useState } from 'react';
import './css-folder/passenger-notice.less'
import { getPrivacyContent } from '../../../api/home';
import useBottomPopupAnimation from './hooks/use-bottom-popup-animation';
type Props = {
    show?: boolean;
@@ -11,8 +12,7 @@
}
export default function PassengerNotice({ show = false, onClose, onCheckedChange }: Props) {
    const [visible, setVisible] = useState(false); // 弹窗显示状态
    const [animationData, setAnimationData] = useState<any>(); // 弹窗动画数据
    const { visible, animationData } = useBottomPopupAnimation(show);
    const [checked, setChecked] = useState(true); // 近三个月不再显示勾选状态
    const [noticeContent, setNoticeContent] = useState(''); // 须知富文本内容
    const [thumbHeight, setThumbHeight] = useState(0); // 自定义滚动条高度
@@ -32,7 +32,6 @@
    // 文案渲染完成后计算滚动条尺寸
    useEffect(() => {
        if (noticeContent && visible) {
            // Give it a little time to render
            setTimeout(() => {
                const query = Taro.createSelectorQuery();
                query.select('.passenger-content-desc').boundingClientRect();
@@ -55,30 +54,6 @@
            }, 300);
        }
    }, [noticeContent, visible]);
    // 控制弹窗开合动画
    useEffect(() => {
        if (show && !visible) {
            setVisible(true);
            const animation = Taro.createAnimation({
                duration: 300,
                timingFunction: 'ease-out'
            });
            animation.translateY(0).step();
            setAnimationData(animation.export());
        } else if (!show && visible) {
            const animation = Taro.createAnimation({
                duration: 300,
                timingFunction: 'ease-in'
            });
            animation.translateY(500).step();
            setAnimationData(animation.export());
            setTimeout(() => {
                setVisible(false);
            }, 300);
        }
    }, [show]);
    // 每次打开时拉取最新须知内容
    useEffect(() => {
src/robotaxi/pages/begin-route/components/pickup-exception-popup.tsx
@@ -4,6 +4,7 @@
import type { PickupExceptionInfo } from '../pickup-exception';
import { getDistance } from '../../../../utils/Robotaxi-public-method';
import './css-folder/pickup-exception-popup.less';
import useBottomPopupAnimation from './hooks/use-bottom-popup-animation';
type Props = {
  show?: boolean;
@@ -33,15 +34,14 @@
  onClose,
  onConfirm
}: Props) {
  const [visible, setVisible] = useState(false);
  const [animationData, setAnimationData] = useState<any>();
  const { animationData, shouldRender } = useBottomPopupAnimation(show, {
    duration: POPUP_ANIMATION_DURATION_MS
  });
  const [currentUserLocation, setCurrentUserLocation] = useState<LocationPoint | null>(null);
  const [linePoints, setLinePoints] = useState<LocationPoint[]>([]);
  const [distanceToVehicle, setDistanceToVehicle] = useState<number | null>(null);
  const closeTimerRef = useRef<any>(null);
  const locationTimerRef = useRef<any>(null);
  const mapViewportFittedRef = useRef(false);
  const shouldRender = show || visible;
  const mapCenter = useMemo(() => {
    const candidatePoints: LocationPoint[] = [];
@@ -177,42 +177,6 @@
  }, [pickupExceptionInfo?.vehicleLat, pickupExceptionInfo?.vehicleLon]);
  useEffect(() => {
    if (closeTimerRef.current) {
      clearTimeout(closeTimerRef.current);
      closeTimerRef.current = null;
    }
    if (show) {
      setVisible(true);
      setAnimationData(undefined);
      return;
    }
    if (!visible) {
      return;
    }
    const animation = Taro.createAnimation({
      duration: POPUP_ANIMATION_DURATION_MS,
      timingFunction: 'ease-in'
    });
    animation.translateY(500).step();
    setAnimationData(animation.export());
    closeTimerRef.current = setTimeout(() => {
      setVisible(false);
      setAnimationData(undefined);
      closeTimerRef.current = null;
    }, POPUP_ANIMATION_DURATION_MS);
    return () => {
      if (closeTimerRef.current) {
        clearTimeout(closeTimerRef.current);
        closeTimerRef.current = null;
      }
    };
  }, [show, visible]);
  useEffect(() => {
    if (!show) {
      if (locationTimerRef.current) {
        clearInterval(locationTimerRef.current);
@@ -265,7 +229,7 @@
  return (
    <View className='pickup-exception-popup' catchMove>
      <View className='pickup-exception-mask'></View>
      <View className='pickup-exception-content' animation={show ? undefined : animationData}>
      <View className='pickup-exception-content' animation={animationData}>
        <View className='pickup-exception-close' onClick={onClose}>
          <Image
            className='pickup-exception-close-icon'
src/robotaxi/pages/begin-route/components/route-preference-popup/index.tsx
@@ -1,8 +1,8 @@
import Taro from '@tarojs/taro';
import { View, Text, Image, ScrollView } from '@tarojs/components';
import { useEffect, useState } from 'react';
import './index.less';
import { queryRoutePreferences } from '../../../../api/begin-route';
import useBottomPopupAnimation from '../hooks/use-bottom-popup-animation';
export type RoutePreferenceOption = {
  label: string;
@@ -43,32 +43,8 @@
};
export default function RoutePreferencePopup({ show, value, openCityId = 0, onClose, onSelect }: Props) {
  const [visible, setVisible] = useState(false);
  const [animationData, setAnimationData] = useState<any>();
  const { animationData, shouldRender } = useBottomPopupAnimation(show);
  const [options, setOptions] = useState<RoutePreferenceOption[]>(DEFAULT_OPTIONS);
  useEffect(() => {
    if (show && !visible) {
      setVisible(true);
      const animation = Taro.createAnimation({
        duration: 300,
        timingFunction: 'ease-out'
      });
      animation.translateY(0).step();
      setAnimationData(animation.export());
    } else if (!show && visible) {
      const animation = Taro.createAnimation({
        duration: 300,
        timingFunction: 'ease-in'
      });
      animation.translateY('100%').step();
      setAnimationData(animation.export());
      setTimeout(() => {
        setVisible(false);
      }, 300);
    }
  }, [show]);
  useEffect(() => {
    if (!show) return;
@@ -87,7 +63,7 @@
    };
  }, [show, openCityId]);
  if (!visible && !show) return null;
  if (!shouldRender) return null;
  return (
    <View className={`route-preference-popup ${show ? 'show' : ''}`} catchMove>