卢春龙
2026-08-11 2dd1cdf5938601b3fdc9394b9af1ee76d740efea
Merge branch 'lucl/animation-0810' into 'feature_v2.4.0_ui_animation'

feat: 去掉分层动效

See merge request robotaxi1/xp-robotaxi-miniprogram!262
5个文件已修改
96 ■■■■■ 已修改文件
src/robotaxi/pages/begin-route/components/calling-car-modules/evaluation-card.tsx 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car.tsx 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/calling-car.less 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-dispatch-transition.ts 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/evaluation-card.tsx
@@ -33,6 +33,14 @@
    const [isEvaluated, setIsEvaluated] = useState(false); // 是否已完成评价
    const skipEvaluateResetRef = useRef(false); // 跳过关闭重置的一次性标记
    // 订单切换时重置评价态:去掉 CallingCar 的 key 后本组件不再随状态重挂载,
    // 需显式按 orderId 复位,避免上一单「已评价」状态串到下一单。
    useEffect(() => {
        setActiveIndex(0);
        setIsEvaluated(false);
        skipEvaluateResetRef.current = false;
    }, [orderId]);
    // 外部提交成功后同步评价状态
    useEffect(() => {
        if (typeof evaluateSubmitVersion === 'number' && evaluateSubmitVersion > 0) {
src/robotaxi/pages/begin-route/components/calling-car.tsx
@@ -200,11 +200,29 @@
    const isTripFinished = tripStatus === TripStatus.TripFinished;
    const isCallingCar = tripStatus === TripStatus.CallingCar;
    /**
     * 「直达待评价」入场快照:与 useDispatchTransition 里的 shouldPlayEnter 同理,
     * 父层的 playEvaluateEnter 在下一次渲染即翻 false,直接绑定会把动画类中途卸掉、动效被扼杀,
     * 故在挂载时快照一次,组件生命周期内不变(本组件由 key={tripStatus} 保证每次状态切换都重挂载)。
     * 「直达待评价」入场:与 useDispatchTransition 里的 shouldPlayEnter 同理,
     * 父层的 playEvaluateEnter 在下一次渲染即翻 false,直接绑定会把动画类中途卸掉、动效被扼杀。
     * 过去依赖 key={tripStatus} 重挂载做挂载快照;去 key 后组件不再重挂载,
     * 改为在渲染期同步捕捉 playEvaluateEnter 上升沿置类(避免先渲染一帧静止态再跳到位移起点的闪跳),
     * 播放一次后按动画时长(0.7s)清除类名——最终态与静止态一致无跳变,且使下一次 6→9 能重新触发。
     */
    const [shouldPlayEvaluateEnter] = useState(playEvaluateEnter);
    const [shouldPlayEvaluateEnter, setShouldPlayEvaluateEnter] = useState(false);
    const prevPlayEvaluateRef = useRef(false);
    const evaluateEnterTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
    if (playEvaluateEnter && !prevPlayEvaluateRef.current) {
        prevPlayEvaluateRef.current = true;
        if (!shouldPlayEvaluateEnter) setShouldPlayEvaluateEnter(true);
    } else if (!playEvaluateEnter && prevPlayEvaluateRef.current) {
        prevPlayEvaluateRef.current = false;
    }
    useEffect(() => {
        if (!shouldPlayEvaluateEnter) return;
        if (evaluateEnterTimerRef.current) clearTimeout(evaluateEnterTimerRef.current);
        evaluateEnterTimerRef.current = setTimeout(() => setShouldPlayEvaluateEnter(false), 750);
        return () => {
            if (evaluateEnterTimerRef.current) clearTimeout(evaluateEnterTimerRef.current);
        };
    }, [shouldPlayEvaluateEnter]);
    /**
     * 车控(空调 / 音乐 / 香氛):卡片与弹窗分开挂载。
     * 卡片放进横向滑动的 ScrollView;弹窗是 fixed 全屏浮层,必须挂在 ScrollView 之外,
src/robotaxi/pages/begin-route/components/css-folder/calling-car.less
@@ -154,16 +154,10 @@
    opacity: 0.4;
  }
  to {
    transform: translateY(0);
  // 透明度单独提前收口:0.4 → 1 用 0.2s 走完(0.7s 总时长的 28.57% 处),
  // 之后 transform 继续独立完成剩余位移,不再受 opacity 影响。
  28.57% {
    opacity: 1;
  }
}
// 待评价态新增内容(费用明细卡 / 评价卡)分层浮现:轻微上移淡入,手法对齐 finished-distance-fade。
@keyframes calling-car-evaluate-content-fade {
  from {
    transform: translateY(16px);
    opacity: 0;
  }
  to {
@@ -175,13 +169,6 @@
  // 位移行程约 380px(大于派单入场的 170px),时长相应放到 0.7s,保持速度感与派单入场一致。
  animation: calling-car-evaluate-rise 0.7s cubic-bezier(0.22, 0.61, 0.36, 1) both;
  will-change: transform;
  // 分层:整卡升起过半后,新增的费用明细卡与评价卡再依次浮现,进一步掩盖高度差。
  .fare-box-card {
    animation: calling-car-evaluate-content-fade 600ms ease-out 420ms both;
  }
  .car-card-evaluate {
    animation: calling-car-evaluate-content-fade 600ms ease-out 560ms both;
  }
}
// 派单成功转场期间:蓝牙卡 / 车控卡 / 底部操作栏隐藏但保留占位(opacity),逻辑另在各组件延迟执行;
// 动效完成后移除 .dispatch-defer-content,随 opacity 过渡淡入。用 opacity 而非 visibility,保证淡入过渡生效。
src/robotaxi/pages/begin-route/hooks/use-dispatch-transition.ts
@@ -17,10 +17,21 @@
export function useDispatchTransition(playDispatchTransition: boolean) {
  const [dispatchPhase, setDispatchPhase] = useState<DispatchPhase>('idle');
  const [transitionDone, setTransitionDone] = useState(false);
  // shouldPlayEnter 是挂载快照,在组件生命周期内不变——不能用"两帧 + transition":
  // 起始态在 weapp 可能未被提交导致过渡不触发,且 playDispatchTransition 在父层下一次渲染即翻 false,
  // 直接绑定会把 --enter 类中途卸掉、动画被扼杀。
  const [shouldPlayEnter] = useState(playDispatchTransition);
  // shouldPlayEnter 控制转场期间的内容延迟、背景图门槛与整卡升起 keyframe。
  // 过去依赖 CallingCar 的 key={tripStatus} 重挂载做「挂载快照」,去 key 后组件不再重挂载,
  // 改为在渲染期同步捕捉 playDispatchTransition 的上升沿(false→true)置真——
  // 同步置真而非放到 effect,是为避免整卡升起前先渲染一帧静止态导致的位移闪跳;
  // 又不能直接绑定 playDispatchTransition(其在父层下一次渲染即翻 false,会把 keyframe 类中途卸掉扼杀动画),
  // 故用独立状态 latch,转场完成后回落 false,使同一实例内下一单再次进入派单成功时能重新触发。
  const [shouldPlayEnter, setShouldPlayEnter] = useState(false);
  const prevPlayRef = useRef(false);
  // 渲染期同步捕捉上升沿并置类(React 官方「prop 变化时调整 state」模式,重渲染早于绘制,无中间帧)
  if (playDispatchTransition && !prevPlayRef.current) {
    prevPlayRef.current = true;
    if (!shouldPlayEnter) setShouldPlayEnter(true);
  } else if (!playDispatchTransition && prevPlayRef.current) {
    prevPlayRef.current = false;
  }
  const enterTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const settleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -48,6 +59,9 @@
    settleTimerRef.current = setTimeout(() => {
      setDispatchPhase('idle');
      setTransitionDone(true);
      // 回落转场门槛:--enter keyframe 已在前 0.6s 播完(此时约 3.9s),清除类名不影响观感,
      // 且为同一实例内下一次上升沿重新触发 keyframe 留出空间(false→true 才会重播)。
      setShouldPlayEnter(false);
    }, 600);
  };
@@ -88,15 +102,31 @@
    scheduleShrinkCheck(currentTime);
  };
  // shouldPlayEnter 置真(上升沿)后启动转场状态机;复位所有一次性标记,保证同一实例内可重复播放。
  useEffect(() => {
    if (playDispatchTransition) {
    if (!shouldPlayEnter) return;
    if (enterTimerRef.current) clearTimeout(enterTimerRef.current);
    if (settleTimerRef.current) clearTimeout(settleTimerRef.current);
    if (fallbackTimerRef.current) clearTimeout(fallbackTimerRef.current);
    if (shrinkTimerRef.current) clearTimeout(shrinkTimerRef.current);
    settleTriggeredRef.current = false;
    shrinkTriggeredRef.current = false;
    lastTimeSyncRef.current = { currentTime: 0, wallClock: 0 };
    videoDurationRef.current = 0;
    setTransitionDone(false);
    setDispatchPhase('idle');
      enterTimerRef.current = setTimeout(() => {
        // 视频已在 32ms 内报错/结束(极端)时勿覆盖 settle 阶段
        if (settleTriggeredRef.current) return;
        setDispatchPhase('video');
      }, 32);
      fallbackTimerRef.current = setTimeout(() => enterSettle(), DISPATCH_FALLBACK_MS);
    }
  }, [shouldPlayEnter]);
  // 卸载时清理所有定时器
  useEffect(() => {
    return () => {
      if (enterTimerRef.current) clearTimeout(enterTimerRef.current);
      if (settleTimerRef.current) clearTimeout(settleTimerRef.current);
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
@@ -1042,7 +1042,6 @@
    ) {
      return (
        <CallingCar
          key={deps.tripStatus}
          playDispatchTransition={playDispatchTransition}
          playEvaluateEnter={playEvaluateEnter}
          onDispatchTransitionSettled={deps.onDispatchTransitionSettled}