lucl
2026-08-10 5705ebbdcd0f89c4962fd529fa374fc4fe5ee3e0
feat: 派单成功动效,弹窗、车辆缩小时间与视频保持一致
3个文件已修改
68 ■■■■■ 已修改文件
src/robotaxi/pages/begin-route/components/calling-car.tsx 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/calling-car.less 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-dispatch-transition.ts 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car.tsx
@@ -182,7 +182,7 @@
        transitionDone,
        shouldPlayEnter,
        handleDispatchVideoTimeUpdate,
        handleDispatchVideoEnded,
        enterSettle,
    } = useDispatchTransition(playDispatchTransition);
    /** 是否为“到达终点/待支付”状态(图一、图二) */
@@ -253,8 +253,8 @@
            dispatchPhase={dispatchPhase}
            vehicleInfo={vehicleInfo}
            onTimeUpdate={handleDispatchVideoTimeUpdate}
            onEnded={handleDispatchVideoEnded}
            onError={handleDispatchVideoEnded}
            onEnded={enterSettle}
            onError={enterSettle}
          />
          {
            // 常驻车图:接驾/行程中/支付成功且有车牌时展示。转场期间车模兜底由 DispatchTransitionLayer 渲染。
src/robotaxi/pages/begin-route/components/css-folder/calling-car.less
@@ -104,14 +104,16 @@
    }
  }
  // 阶段三:视频结束 → 功能卡回升归位、车模缩小回落到最终停靠位、视频切回图片背景
  // --dispatch-settle-duration 与 use-dispatch-transition 中 BG_SHRINK_LEAD_SEC 保持一致
  &.dispatch-settle {
    .calling-car-content-box { transform: translateY(0); transition: transform 0.4s ease; }
    --dispatch-settle-duration: 1.055s;
    .calling-car-content-box { transform: translateY(0); transition: transform var(--dispatch-settle-duration) ease; }
    // 车模从居中态回落到常驻停靠位(translateX 回 0 即基础态 right:116px 位置),并缩小归位。
    // transform 函数列表与 video 阶段对齐(translateY/translateX/scale),保证平滑插值、无瞬跳。
    .calling-car-vehicle-pic {
      transform: translateY(0) translateX(0) scale(1);
      opacity: 1;
      transition: transform 0.4s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.4s ease;
      transition: transform var(--dispatch-settle-duration) cubic-bezier(0.22, 1, 0.36, 1), opacity var(--dispatch-settle-duration) ease;
    }
  }
}
src/robotaxi/pages/begin-route/hooks/use-dispatch-transition.ts
@@ -3,8 +3,6 @@
/** 派单成功转场视频 */
export const DISPATCH_VIDEO_SRC = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/video/vehicle_approaching_v1_file_w_1128@h_1268.mp4';
export const DISPATCH_VIDEO_POSTER = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/video/vehicle_approaching_poster_v1_file_w_1128@h_1268.jpg';
/** 距视频结束多久提前进入 settle 归位阶段(秒) */
export const DISPATCH_SETTLE_LEAD_SECONDS = 0.8;
/** 兜底超时(ms):防卡死安全阀——视频加载/播放失败时 onTimeUpdate/onEnded 都不触发,超过此时长强制归位 */
export const DISPATCH_FALLBACK_MS = 10000;
@@ -28,8 +26,19 @@
  const fallbackTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const settleTriggeredRef = useRef(false);
  // 背景缩小精确计时:微信 video 的 timeupdate 间隔约 265ms,无法直接捕获目标时刻。
  // 方案:timeupdate 记录 {播放时间, 墙上时钟} 作同步锚点,自适应 setTimeout 在两次事件间
  // 通过墙上时钟插值估算播放进度,提前唤醒、递归逼近,最终触发精度约 4ms。
  const lastTimeSyncRef = useRef({ currentTime: 0, wallClock: 0 });
  const videoDurationRef = useRef(0);
  const shrinkTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const shrinkTriggeredRef = useRef(false);
  /** 背景缩小提前量(秒):在视频结束前触发 settle。视频全长 3.333s,即 currentTime ≈ 2.278s 处。 */
  const BG_SHRINK_LEAD_SEC = 1.055;
  // 进入归位落定阶段(幂等,只生效一次)
  const handleDispatchVideoEnded = () => {
  const enterSettle = () => {
    if (settleTriggeredRef.current) return;
    settleTriggeredRef.current = true;
    if (fallbackTimerRef.current) clearTimeout(fallbackTimerRef.current);
@@ -41,13 +50,41 @@
    }, 600);
  };
  // 播放进度更新:距视频结束还剩 DISPATCH_SETTLE_LEAD_SECONDS 秒时提前进入 settle
  /**
   * 自适应缩小定时器。每次 timeupdate 或定时器到期时调用,计算距目标还有多久:
   * - 已到达 → 调用 enterSettle 进入 settle
   * - 未到达 → setTimeout 提前 10ms 唤醒,到期后用墙上时钟插值修正、递归检查
   */
  const scheduleShrinkCheck = (currentTime: number) => {
    if (shrinkTriggeredRef.current) return;
    const targetSec = videoDurationRef.current - BG_SHRINK_LEAD_SEC;
    const remaining = targetSec - currentTime;
    if (remaining <= 0) {
      shrinkTriggeredRef.current = true;
      if (shrinkTimerRef.current) clearTimeout(shrinkTimerRef.current);
      enterSettle();
      return;
    }
    if (shrinkTimerRef.current) clearTimeout(shrinkTimerRef.current);
    shrinkTimerRef.current = setTimeout(() => {
      const { currentTime: lastCT, wallClock: lastWC } = lastTimeSyncRef.current;
      if (lastWC === 0) return;
      const estimated = lastCT + (Date.now() - lastWC) / 1000;
      scheduleShrinkCheck(estimated);
    }, Math.max(remaining * 1000 - 10,30));
  };
  const handleDispatchVideoTimeUpdate = (e: any) => {
    const { currentTime, duration } = e?.detail || {};
    if (typeof currentTime !== 'number' || typeof duration !== 'number' || duration <= 0) return;
    if (duration - currentTime <= DISPATCH_SETTLE_LEAD_SECONDS) {
      handleDispatchVideoEnded();
    }
    lastTimeSyncRef.current = { currentTime, wallClock: Date.now() };
    videoDurationRef.current = duration;
    scheduleShrinkCheck(currentTime);
  };
  useEffect(() => {
@@ -57,12 +94,13 @@
        if (settleTriggeredRef.current) return;
        setDispatchPhase('video');
      }, 32);
      fallbackTimerRef.current = setTimeout(() => handleDispatchVideoEnded(), DISPATCH_FALLBACK_MS);
      fallbackTimerRef.current = setTimeout(() => enterSettle(), DISPATCH_FALLBACK_MS);
    }
    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);
    };
  }, []);
@@ -70,7 +108,7 @@
    dispatchPhase,
    transitionDone,
    shouldPlayEnter,
    enterSettle,
    handleDispatchVideoTimeUpdate,
    handleDispatchVideoEnded,
  };
}