ai-coding
2026-08-14 44e790c6649da7abc093d79a41545b854ce071df
feat: 优化派单动效的视频播放效果

Co-Authored-By: 卢春龙 <lucl@xiaopeng.com>
3个文件已修改
44 ■■■■■ 已修改文件
src/robotaxi/pages/begin-route/components/calling-car.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/dispatch-transition-layer.tsx 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-dispatch-transition.ts 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car.tsx
@@ -195,6 +195,7 @@
        dispatchPhase,
        transitionDone,
        shouldPlayEnter,
        endCoverVisible,
        handleDispatchVideoTimeUpdate,
        enterSettle,
    } = useDispatchTransition(playDispatchTransition);
@@ -313,6 +314,7 @@
            shouldPlayEnter={shouldPlayEnter}
            transitionDone={transitionDone}
            dispatchPhase={dispatchPhase}
            endCoverVisible={endCoverVisible}
            vehicleInfo={vehicleInfo}
            onTimeUpdate={handleDispatchVideoTimeUpdate}
            onEnded={enterSettle}
src/robotaxi/pages/begin-route/components/dispatch-transition-layer.tsx
@@ -9,6 +9,8 @@
  shouldPlayEnter: boolean;
  transitionDone: boolean;
  dispatchPhase: DispatchPhase;
  /** 结尾遮挡层是否可见:由 hook 在视频「真正快播放完」时置真(非 settle 起点),避免过早盖住仍在播放的视频。 */
  endCoverVisible: boolean;
  vehicleInfo?: {
    vehiclePictures?: string;
    vehicleCode?: string;
@@ -40,6 +42,7 @@
  shouldPlayEnter,
  transitionDone,
  dispatchPhase,
  endCoverVisible,
  vehicleInfo,
  onTimeUpdate,
  onEnded,
@@ -60,14 +63,15 @@
    prevShouldPlayRef.current = false;
  }
  // 进入 settle(视频卸载前的落定窗口):本次渲染已挂上结尾遮挡层(盖在视频之上),
  // 结尾遮挡层可见(视频真正快播放完)后:本次渲染已挂上遮挡层(盖在视频之上),
  // 延一帧再卸原生视频——遮挡先上屏、视频后拆,原生层销毁的黑帧被遮挡层挡住,杜绝结尾黑帧。
  // 改由 endCoverVisible 驱动(而非 settle):settle 起即收缩但视频仍在播,此时不能卸载视频。
  useEffect(() => {
    if (dispatchPhase === 'settle' && !videoRemoved) {
    if (endCoverVisible && !videoRemoved) {
      const t = setTimeout(() => setVideoRemoved(true), 50);
      return () => clearTimeout(t);
    }
  }, [dispatchPhase, videoRemoved]);
  }, [endCoverVisible, videoRemoved]);
  if (!shouldPlayEnter || transitionDone) return null;
@@ -121,9 +125,12 @@
          src={DISPATCH_VIDEO_POSTER}
        />
      )}
      {/* 结尾遮挡层:settle 阶段用预载尾帧图盖在视频之上(z-index 与 poster 同为 0),先于视频上屏、
           视频卸载后仍在,挡住原生 <video> 卸载黑帧;transitionDone 时随本层卸载,露出已预载的常态背景图。 */}
      {dispatchPhase === 'settle' && (
      {/* 结尾遮挡层:视频「真正快播放完」时(endCoverVisible,非 settle 起点)用预载尾帧图盖在视频之上
           (z-index 与 poster 同为 0),先于视频上屏、视频卸载后仍在,挡住原生 <video> 卸载黑帧;
           transitionDone 时随本层卸载,露出已预载的常态背景图。
           改用 endCoverVisible 而非 dispatchPhase==='settle':settle 起即开始背景收缩但视频仍在播放,
           此时盖上遮挡层会抹掉视频尾段效果;改由 hook 在距结束 END_COVER_LEAD_SEC 时才置真。 */}
      {endCoverVisible && (
        <Image
          className='calling-car-end-cover'
          src={DISPATCH_VIDEO_END_POSTER}
src/robotaxi/pages/begin-route/hooks/use-dispatch-transition.ts
@@ -8,6 +8,11 @@
export const DISPATCH_VIDEO_END_POSTER = VEHICLE_APPROACHING_END_POSTER;
/** 兜底超时(ms):防卡死安全阀——视频加载/播放失败时 onTimeUpdate/onEnded 都不触发,超过此时长强制归位 */
export const DISPATCH_FALLBACK_MS = 10000;
/**
 * 结尾遮挡层提前量(秒):结尾遮挡层只需在视频「真正快播放完」时才盖上(此值即距视频自然结束的秒数)。
 * 必须显著小于 BG_SHRINK_LEAD_SEC——否则遮挡层会在背景刚开始收缩、视频仍在播时就盖住,抹掉视频尾段效果。
 */
export const END_COVER_LEAD_SEC = 0.4;
export type DispatchPhase = 'idle' | 'video' | 'settle';
@@ -19,6 +24,9 @@
export function useDispatchTransition(playDispatchTransition: boolean) {
  const [dispatchPhase, setDispatchPhase] = useState<DispatchPhase>('idle');
  const [transitionDone, setTransitionDone] = useState(false);
  // 结尾遮挡层是否可见。独立于 settle:settle 起即开始背景收缩(视频仍在播),
  // 而遮挡层延后到视频「真正快播放完」(距结束 END_COVER_LEAD_SEC)才置真,避免过早盖住视频尾段。
  const [endCoverVisible, setEndCoverVisible] = useState(false);
  // shouldPlayEnter 控制转场期间的内容延迟、背景图门槛与整卡升起 keyframe。
  // 过去依赖 CallingCar 的 key={tripStatus} 重挂载做「挂载快照」,去 key 后组件不再重挂载,
  // 改为在渲染期同步捕捉 playDispatchTransition 的上升沿(false→true)置真——
@@ -37,6 +45,7 @@
  const enterTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const settleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const endCoverTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const fallbackTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const settleTriggeredRef = useRef(false);
@@ -57,6 +66,16 @@
    settleTriggeredRef.current = true;
    if (fallbackTimerRef.current) clearTimeout(fallbackTimerRef.current);
    setDispatchPhase('settle');
    // 结尾遮挡层延后显示:settle 起先跑背景收缩(视频照常播放),待收缩接近尾声、视频「真正快播放完」
    // (距结束 END_COVER_LEAD_SEC)时才盖上遮挡层。settle 由精确自适应定时器在 duration-BG_SHRINK_LEAD_SEC
    // 触发,故此处再等 (BG_SHRINK_LEAD_SEC-END_COVER_LEAD_SEC) 即精确落在 duration-END_COVER_LEAD_SEC。
    // 该时刻仍早于本层卸载(settleTimer 800ms),遮挡层可先于视频卸载上屏、挡住卸载黑帧。
    // 兜底路径(onError/超时触发的 settle)同样适用:遮挡层在 settle 后固定延时出现,早于 settleTimer。
    if (endCoverTimerRef.current) clearTimeout(endCoverTimerRef.current);
    endCoverTimerRef.current = setTimeout(
      () => setEndCoverVisible(true),
      Math.max((BG_SHRINK_LEAD_SEC - END_COVER_LEAD_SEC) * 1000, 0),
    );
    if (settleTimerRef.current) clearTimeout(settleTimerRef.current);
    settleTimerRef.current = setTimeout(() => {
      setDispatchPhase('idle');
@@ -110,6 +129,7 @@
    if (enterTimerRef.current) clearTimeout(enterTimerRef.current);
    if (settleTimerRef.current) clearTimeout(settleTimerRef.current);
    if (endCoverTimerRef.current) clearTimeout(endCoverTimerRef.current);
    if (fallbackTimerRef.current) clearTimeout(fallbackTimerRef.current);
    if (shrinkTimerRef.current) clearTimeout(shrinkTimerRef.current);
    settleTriggeredRef.current = false;
@@ -117,6 +137,7 @@
    lastTimeSyncRef.current = { currentTime: 0, wallClock: 0 };
    videoDurationRef.current = 0;
    setTransitionDone(false);
    setEndCoverVisible(false);
    setDispatchPhase('idle');
    enterTimerRef.current = setTimeout(() => {
@@ -132,6 +153,7 @@
    return () => {
      if (enterTimerRef.current) clearTimeout(enterTimerRef.current);
      if (settleTimerRef.current) clearTimeout(settleTimerRef.current);
      if (endCoverTimerRef.current) clearTimeout(endCoverTimerRef.current);
      if (fallbackTimerRef.current) clearTimeout(fallbackTimerRef.current);
      if (shrinkTimerRef.current) clearTimeout(shrinkTimerRef.current);
    };
@@ -141,6 +163,7 @@
    dispatchPhase,
    transitionDone,
    shouldPlayEnter,
    endCoverVisible,
    enterSettle,
    handleDispatchVideoTimeUpdate,
  };