From 429208957f30ab9eedc5533a81aaa284048d09f5 Mon Sep 17 00:00:00 2001
From: zjk <852185829@qq.com>
Date: 星期三, 29 七月 2026 21:51:54 +0800
Subject: [PATCH] Merge branch 'feature_v2.4.0' into fix-0716-blue-dev
---
src/robotaxi/pages/begin-route/hooks/use-android-overlays.ts | 32
src/robotaxi/pages/begin-route/hooks/use-order-payment.ts | 11
src/robotaxi/pages/select-map-site/hooks/use-select-map-site.ts | 57
src/robotaxi/pages/coupon/index.tsx | 19
src/robotaxi/components/coupon-select-popup/index.tsx | 12
src/pages/robotaxi/home/hooks/use-drag-gesture.ts | 5
src/robotaxi/pages/begin-route/components/calling-car-modules/trip-status-header.tsx | 325
src/robotaxi/api/feedback.ts | 23
src/robotaxi/components/demo/index.tsx | 51
src/robotaxi/utils/api-logger.ts | 26
src/pages/robotaxi/home/hooks/use-region-change.ts | 24
src/robotaxi/pages/begin-route/components/calling-car-modules/use-minimum-spin-loading.ts | 57
src/robotaxi/hooks/use-image-upload.ts | 3
src/robotaxi/pages/begin-route/components/calling-car-modules/fragrance-control/index.tsx | 254 +
src/robotaxi/pages/demo/index.tsx | 4
src/robotaxi/pages/personal-center/index.less | 112
src/robotaxi/pages/begin-route/components/begin-route-view.tsx | 67
src/robotaxi/pages/begin-route/components/calling-car-modules/seat-hv-control/index.tsx | 199
src/robotaxi/pages/site-search/components/favorite-popup/index.tsx | 7
src/robotaxi/pages/begin-route/components/calling-car-modules/trunk-confirm-popup/index.less | 75
src/robotaxi/pages/begin-route/components/debug-order-simulator-panel.tsx | 102
src/robotaxi/pages/begin-route/components/modify-destination-review.tsx | 45
src/robotaxi/pages/payment-setting-detail/index.tsx | 10
src/robotaxi/pages/select-map-site/components/select-map-site-view.tsx | 118
src/pages/robotaxi/home/components/privacy-popup/index.tsx | 5
src/robotaxi/pages/begin-route/components/css-folder/fragrance-control.less | 257 +
src/robotaxi/pages/begin-route/components/css-folder/release-be-being.less | 203
src/robotaxi/pages/begin-route/components/css-folder/route-selection-panel.less | 14
src/robotaxi/pages/select-city/index.tsx | 6
src/robotaxi/pages/begin-route/components/route-preference-popup/index.less | 3
src/robotaxi/pages/begin-route/components/modify-destination-fail-popup.tsx | 6
src/robotaxi/pages/share-trip/hooks/use-polyline-render.ts | 10
src/robotaxi/style/ui-variables.less | 26
src/robotaxi/pages/begin-route/components/calling-car-modules/trunk-confirm-popup/index.tsx | 82
src/robotaxi/pages/expense-detail/index.tsx | 6
src/robotaxi/pages/begin-route/components/cancel-popup.tsx | 34
src/robotaxi/components/toast/README.md | 86
src/robotaxi/pages/pricing-rules/index.tsx | 8
src/robotaxi/pages/begin-route/components/css-folder/release-route.less | 250
src/robotaxi/pages/site-search/components/site-list-item.tsx | 7
src/pages/robotaxi/home/components/privacy-popup/index.less | 39
src/robotaxi/components/coupon-select-popup/index.less | 53
src/pages/robotaxi/home/components/city-select/index.less | 75
src/robotaxi/api/types/seat-hv.ts | 47
src/robotaxi/pages/begin-route/components/calling-car-modules/temperature-control.tsx | 280
src/robotaxi/pages/operating-area/index.tsx | 27
src/robotaxi/pages/begin-route/components/evaluate-popup.tsx | 187
docs/座椅通风加热概要设计.md | 378 +
src/robotaxi/pages/begin-route/components/calling-car-modules/trip-action-bar.tsx | 9
src/pages/robotaxi/home/components/home-content-panel.tsx | 109
src/robotaxi/pages/begin-route/components/passenger-notice.tsx | 2
src/robotaxi/pages/begin-route/components/calling-car-modules/bluetooth-operate-card.tsx | 4
src/pages/robotaxi/home/components/home-map-panel.tsx | 48
src/robotaxi/pages/begin-route/hooks/use-bluetooth-access.ts | 19
src/pages/robotaxi/home/hooks/use-navigation.ts | 4
src/robotaxi/pages/order/index.less | 6
src/robotaxi/pages/share-trip/hooks/use-map-markers.ts | 151
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx | 459 +
src/robotaxi/pages/payment-setting/index.tsx | 20
src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx | 97
src/utils/Robotaxi-public-method.ts | 29
src/pages/robotaxi/home/index.tsx | 25
src/robotaxi/pages/begin-route/hooks/use-page-actions.ts | 3
src/robotaxi/pages/begin-route/components/release-be-being.tsx | 66
src/robotaxi/pages/begin-route/hooks/use-route-request.ts | 14
src/robotaxi/pages/begin-route/components/calling-car-modules/evaluation-card.tsx | 7
src/pages/robotaxi/home/components/order-status-card/index.tsx | 4
src/robotaxi/pages/begin-route/components/css-folder/no-route-popup.less | 14
src/robotaxi/components/tip-popup/index.less | 51
src/pages/robotaxi/home/hooks/use-city-switch.ts | 5
src/robotaxi/api/types/begin-route.ts | 10
src/robotaxi/pages/begin-route/hooks/use-preloaded-map-assets.ts | 11
src/robotaxi/pages/select-map-site/index.tsx | 17
src/pages/robotaxi/home/constants.ts | 2
src/components/custom-navbar/index.tsx | 24
src/robotaxi/pages/begin-route/components/route-selection-panel.tsx | 2
src/robotaxi/pages/begin-route/components/calling-car-modules/ac-switch/index.tsx | 31
src/robotaxi/pages/begin-route/components/css-folder/temperature-control.less | 182
src/robotaxi/components/toast/with-toast.tsx | 36
src/robotaxi/pages/begin-route/components/route-preference-popup/index.tsx | 2
src/robotaxi/pages/order/index.tsx | 7
src/robotaxi/pages/begin-route/hooks/use-credit-pay-return.ts | 15
src/robotaxi/api/types/fragrance.ts | 39
src/robotaxi/pages/operating-area/index.less | 81
src/pages/robotaxi/home/hooks/use-home-bootstrap.ts | 10
src/robotaxi/pages/begin-route/hooks/use-bluetooth-car-control.ts | 3
src/robotaxi/pages/feedback/index.tsx | 450 ++
src/robotaxi/pages/site-search/index.tsx | 134
src/robotaxi/pages/begin-route/components/auth-bluetooth-popup/index.less | 34
src/robotaxi/pages/begin-route/components/css-folder/passenger-notice.less | 30
src/robotaxi/pages/begin-route/components/css-folder/evaluate-popup.less | 201
src/pages/not-on-whitelist/index.tsx | 2
src/robotaxi/pages/share-trip/hooks/use-vehicle-tracking.ts | 107
src/robotaxi/pages/begin-route/components/auth-bluetooth-popup/success-auth/index.less | 31
src/robotaxi/pages/begin-route/components/css-folder/seat-hv-control.less | 143
src/robotaxi/pages/favorite-sites/index.tsx | 7
src/robotaxi/pages/begin-route/components/css-folder/modify-destination-review.less | 191
src/robotaxi/components/toast/index.less | 73
src/robotaxi/pages/begin-route/components/calling-car.tsx | 180
src/app.config.ts | 1
src/pages/robotaxi/home/components/map-pointer/index.tsx | 9
src/robotaxi/api/seat-hv.ts | 24
src/robotaxi/pages/expense-detail/index.less | 6
src/robotaxi/pages/begin-route/hooks/use-polyline-render.ts | 14
src/robotaxi/pages/select-map-site/hooks/use-select-map-site-state.ts | 4
src/robotaxi/pages/share-trip/hooks/use-share-trip-runtime.ts | 16
src/pages/robotaxi/home/index.less | 287
src/robotaxi/pages/begin-route/hooks/use-modify-destination-flow.ts | 3
src/robotaxi/pages/feedback/index.less | 382 +
src/components/custom-navbar/index.less | 10
src/robotaxi/pages/begin-route/hooks/use-order-distance-config.ts | 3
src/robotaxi/pages/site-search/index.less | 164
src/robotaxi/pages/begin-route/index.less | 391 +
src/robotaxi/pages/share-trip/components/share-trip-view.tsx | 189
src/robotaxi/pages/begin-route/components/calling-car-modules/ac-switch/index.less | 21
src/robotaxi/pages/feedback/index.config.ts | 4
src/robotaxi/pages/feedback/hooks/use-media-upload.ts | 269 +
src/robotaxi/pages/begin-route/hooks/use-order-submit.ts | 20
src/robotaxi/pages/begin-route/components/calling-car-modules/vehicle-info-card.tsx | 28
src/robotaxi/pages/begin-route/components/release-route.tsx | 126
src/robotaxi/pages/begin-route/components/auth-bluetooth-popup/success-auth/index.tsx | 2
src/robotaxi/pages/begin-route/components/css-folder/calling-car.less | 1686 ++++---
src/pages/robotaxi/home/hooks/use-map-state.ts | 20
src/robotaxi/pages/passenger-notice/index.tsx | 8
src/robotaxi/pages/begin-route/hooks/use-order-payment-result-query.ts | 9
src/robotaxi/pages/personal-center/index.tsx | 90
src/robotaxi/pages/begin-route/components/calling-car-modules/temperature-fragrance-control.tsx | 1
src/robotaxi/pages/share-trip/index.less | 347 +
src/robotaxi/api/begin-route.ts | 39
src/robotaxi/components/demo/index.less | 18
src/robotaxi/api/types/feedback.ts | 9
src/robotaxi/pages/begin-route/components/calling-car-modules/service-route-change.jsx | 2
docs/车载香氛系统概要设计.md | 363 +
src/robotaxi/pages/share-trip/hooks/use-view-delegates.tsx | 86
src/robotaxi/pages/expense-description/index.tsx | 6
src/robotaxi/pages/begin-route/components/css-folder/cancel-popup.less | 105
src/robotaxi/pages/begin-route/components/over-route.tsx | 9
src/robotaxi/pages/select-map-site/index.less | 174
src/robotaxi/api/fragrance.ts | 31
src/robotaxi/pages/begin-route/index.tsx | 142
src/robotaxi/pages/begin-route/hooks/use-order-cancel.ts | 5
/dev/null | 157
src/robotaxi/pages/order/detail/index.tsx | 14
src/utils/weather.ts | 98
src/robotaxi/pages/begin-route/hooks/use-page-restore.ts | 5
src/robotaxi/pages/share-trip/index.tsx | 16
src/pages/robotaxi/home/components/city-select/index.tsx | 97
src/robotaxi/pages/begin-route/components/css-folder/service-route-change.less | 38
src/robotaxi/pages/begin-route/hooks/use-modify-destination-preview-route.ts | 3
src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx | 37
src/robotaxi/components/toast/index.tsx | 161
src/robotaxi/pages/begin-route/hooks/use-map-markers.ts | 146
src/robotaxi/pages/coupon/index.less | 65
src/robotaxi/pages/begin-route/components/css-folder/over-route.less | 64
154 files changed, 9,531 insertions(+), 3,421 deletions(-)
diff --git "a/docs/\345\272\247\346\244\205\351\200\232\351\243\216\345\212\240\347\203\255\346\246\202\350\246\201\350\256\276\350\256\241.md" "b/docs/\345\272\247\346\244\205\351\200\232\351\243\216\345\212\240\347\203\255\346\246\202\350\246\201\350\256\276\350\256\241.md"
new file mode 100644
index 0000000..e2c021a
--- /dev/null
+++ "b/docs/\345\272\247\346\244\205\351\200\232\351\243\216\345\212\240\347\203\255\346\246\202\350\246\201\350\256\276\350\256\241.md"
@@ -0,0 +1,378 @@
+# 概要设计文档:Robotaxi 座椅通风加热
+
+## 0. 文档信息
+
+| 项 | 内容 |
+|----|------|
+| 功能名称 | 座椅通风 / 加热(在空调温控面板内扩展) |
+| 所属模块 | Robotaxi 接驾界面(begin-route / 行程开始页) |
+| 目标分支 | `rt/feature/v2.0_ui_xj_730` |
+| 文档状态 | 概要设计(需求已对齐,待评审后进入编码) |
+| 关联能力 | 空调温控(`temperature-control.tsx`)—— 复用其 `setTemperature` / `airConditionerUnlock` 及温度区间/防抖逻辑 |
+| 参照文档 | `docs/车载香氛系统概要设计.md`(同一套接驾态远控范式) |
+
+---
+
+## 1. 需求背景
+
+Robotaxi 接驾阶段(`TripStatus.VehicleApproaching`)已支持空调温控、车门/后备箱、车载香氛远控。本次在**空调能力基础上扩展座椅通风 / 加热**:
+
+- 交互稿将原空调卡片的内联温控,升级为「卡片展示 + 点击打开『温度调节』面板」
+- 「温度调节」面板内**同时承载**:座椅通风/加热(顶部)+ 空调温度调节与开关(底部)
+- 空调卡片上仍保留**空调总开关**,可直接在卡片上交互
+
+> 采用**方案 A**:重构空调卡片为「展示态 + 面板」,温控迁入面板(滚动温度选择器 + 面板内开关),座椅区位于面板顶部。空调下发逻辑**复用现有接口**。
+
+---
+
+## 2. 需求概述
+
+### 2.1 卡片入口(接驾界面 · 左卡片 = 空调)
+
+```
+┌─────────────┬─────────────┐
+│ ❄ 空调 [开关]│ 智能香氛 │
+│ 23.5 度 │ (右卡片) │
+│ 调节温度|通风加热 │ │
+└─────────────┴─────────────┘
+```
+
+- 左卡片主体展示当前温度(如「23.5 度」),副标题「调节温度|通风加热」
+- 卡片右上角保留**空调总开关 Switch**(复用现有开关下发逻辑,直接在卡片上可交互)
+- **点击卡片主体 → 打开「温度调节」底部弹窗面板**
+
+### 2.2 温度调节面板(底部弹窗)
+
+```
+┌───────────────────────────────┐
+│ 温度调节 [ × ]│ ← 顶部:标题居中 + 关闭
+├───────────────────────────────┤
+│ [车内俯视图 · 座椅区] │
+│ ┌──────────┐ ┌──────────┐ │
+│ │第二排左侧座椅 │ │第二排右侧座椅 │ │
+│ │ ❋通风 ⌇加热 │ │ ❋通风 ⌇加热 │ │ ← 每座椅两个档位按钮
+│ └──────────┘ └──────────┘ │
+├───────────────────────────────┤
+│ 空调 │
+│ …23.5 [24]℃ 24.5… │ ← 横向温度滚动选择器
+│ ——● 空调开关 │ ← 面板内空调开关
+└───────────────────────────────┘
+```
+
+面板自上而下分两区:
+1. **座椅区**(本次新增):车内俯视图 + 按接口返回渲染的座椅,每座椅含「通风」「加热」两个档位按钮
+2. **空调区**(迁移自现有卡片):横向温度滚动选择器 + 空调开关
+
+### 2.3 座椅区渲染逻辑
+
+- **按接口返回的 `seats` 数组渲染**,兼容二排 / 三排(`position` 决定座位标签文案)
+- 每个座椅两个按钮:**通风**(风扇图标)、**加热**(热浪图标)
+- **能力可用性**(按档数字段):
+ - `ventilationLevels === 0` → 该座椅**通风按钮置灰/不可点**(如三排不支持通风),仅保留加热按钮
+ - `heatingLevels === 0` → 加热按钮置灰/不可点
+- 按钮上展示当前档位(如「通风 2」/ 关闭态展示「通风」或档位 0 的视觉)
+
+### 2.4 档位循环与互斥(点击即时下发)
+
+**档位循环**:点击按钮在 `0 → 1 → 2 → … → N → 0` 间循环,上限 `N` 由该座椅对应的 `ventilationLevels` / `heatingLevels` 决定(0 = 关)。
+- 例:`ventilationLevels=3` → 循环 `0→1→2→3→0`
+- 例:`heatingLevels=2` → 循环 `0→1→2→0`
+
+**互斥规则**:同一座椅通风与加热不能同时开启。
+- 用户将**通风**调到非 0 档 → 该座椅**加热自动回退至 0**
+- 反之,将**加热**调到非 0 档 → 该座椅**通风自动回退至 0**
+
+**即时下发**(对齐香氛,无独立"应用"按钮):
+1. 点击某座椅的通风/加热按钮 → 计算该座椅下一档位 + 互斥联动
+2. **仅下发该被改动的座椅**,`ventilationLevel` 与 `heatingLevel` 一起下发
+3. 下发期间该座椅按钮置灰防重复点击
+4. **成功**:按钮档位更新,Toast 提示(文案见 §8)
+5. **失败**:该座椅状态回滚到下发前,Toast「网络异常,请重试」,面板保持
+
+### 2.5 空调区(复用现有能力)
+
+- 温度调节:横向滚动选择器,区间 `15.5 ~ 32.5℃`、步进 `0.5`,**复用现有 `setTemperature` + 防抖(500ms)**
+- 空调开关:面板内开关与**卡片上开关状态同源**,复用 `airConditionerUnlock`
+- 温度/开关的初始化、连环单跳过、本地缓存等逻辑**沿用现有 `temperature-control` 实现**,本次不改其接口契约
+
+---
+
+## 3. 接口契约
+
+服务域 `vehicleConvoy`,路径前缀 `_ROBOTAXI_HOST`,段为 **`base`**(`vehicleConvoy/base/v1/...`)。鉴权:`needLogin=true`、`needSign=false`(对齐香氛/空调)。
+
+> 响应遵循项目统一两层结构(见 `src/utils/request.ts`):`{ code, msg, data }`,`code===CODE_SUCCESS` 视为成功,`request` 默认解包返回第二层 `data`。
+
+### 3.1 查询座椅通风/加热状态
+
+```
+GET /vehicleConvoy/base/v1/mini-program/cabin/seat/hv?orderId={orderId}
+```
+
+**请求参数**(query):
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| orderId | string | 是 | 订单 ID |
+
+**响应示例**:
+
+```json
+{
+ "code": 200,
+ "msg": null,
+ "data": {
+ "seats": [
+ { "position": "SECOND_ROW_LEFT", "ventilation": 1, "ventilationLevels": 3, "heating": 0, "heatingLevels": 3 },
+ { "position": "SECOND_ROW_RIGHT", "ventilation": 0, "ventilationLevels": 3, "heating": 3, "heatingLevels": 3 },
+ { "position": "THIRD_ROW_LEFT", "ventilation": 0, "ventilationLevels": 0, "heating": 0, "heatingLevels": 3 },
+ { "position": "THIRD_ROW_RIGHT", "ventilation": 0, "ventilationLevels": 0, "heating": 0, "heatingLevels": 3 }
+ ]
+ }
+}
+```
+
+**data.seats[] 字段说明**:
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| position | string | 座位标识:`SECOND_ROW_LEFT` / `SECOND_ROW_RIGHT`(目前仅支持二排,前端兼容三排 `THIRD_ROW_*`) |
+| ventilation | int | 通风当前档位 0~3,0=关 |
+| ventilationLevels | int | 通风支持的总档数(如 3 即可设 0~3);0 表示不支持通风 |
+| heating | int | 加热当前档位 0~3,0=关 |
+| heatingLevels | int | 加热支持的总档数(如 3 即可设 0~3);0 表示不支持加热 |
+
+### 3.2 设置座椅通风/加热
+
+```
+POST /vehicleConvoy/base/v1/mini-program/cabin/seat/hv
+```
+
+**请求体**:
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| orderId | string/number | 是 | 订单 ID |
+| seats | SeatSet[] | 是 | 本次改动的座椅集合(**仅传被改动座椅**) |
+| SeatSet.position | string | 是 | 座位标识 |
+| SeatSet.ventilationLevel | int | 是 | 目标通风档位 0~N |
+| SeatSet.heatingLevel | int | 是 | 目标加热档位 0~N(互斥:与 ventilationLevel 至多一个非 0) |
+
+**请求示例**(仅下发改动的座椅,通风与加热一起下发):
+
+```json
+{
+ "orderId": 123456,
+ "seats": [
+ { "position": "SECOND_ROW_LEFT", "ventilationLevel": 2, "heatingLevel": 0 },
+ { "position": "THIRD_ROW_RIGHT", "ventilationLevel": 0, "heatingLevel": 3 }
+ ]
+}
+```
+
+**响应示例**:
+
+```json
+{ "code": 200, "msg": null, "data": null }
+```
+
+**成功判定**:`code===CODE_SUCCESS` 即视为下发成功,`data` 可为 null。
+
+---
+
+## 4. 交互流程
+
+### 4.1 主流程(进入接驾态 → 打开面板 → 点击座椅按钮即时下发)
+
+```
+进入接驾态 (VehicleApproaching)
+ │
+ ├─ 查询座椅状态 GET .../cabin/seat/hv(面板打开时或进入接驾态时)
+ │ ├─ 成功 → 渲染座椅区,按 levels 决定档位上限与按钮可用性
+ │ └─ 失败 → 座椅区兜底(见 8)
+ ├─ 空调温度/开关:沿用现有 temperature-control 初始化
+ │
+用户点击「空调」卡片(主体) → 打开「温度调节」面板
+ │
+用户点击某座椅的 通风/加热 按钮:
+ │
+ ├─ 计算下一档位:curr → (curr+1) % (levels+1)
+ ├─ 互斥联动:通风非0 ⇒ 加热=0;加热非0 ⇒ 通风=0
+ ├─ 该座椅按钮置灰防重复
+ ├─ POST .../cabin/seat/hv
+ │ payload: { orderId, seats: [{ position, ventilationLevel, heatingLevel }] }
+ │
+ ├─ 成功 → 该座椅档位更新 + Toast
+ └─ 失败 → 该座椅状态回滚 + Toast"网络异常,请重试";面板保持
+ │
+(空调温度/开关:复用现有下发逻辑,防抖 500ms)
+```
+
+### 4.2 座椅按钮可用性分支
+
+| 接口字段 | 通风按钮 | 加热按钮 |
+|----------|---------|---------|
+| ventilationLevels>0 & heatingLevels>0 | 可点,循环 0~V | 可点,循环 0~H |
+| ventilationLevels=0 | 置灰/隐藏 | 可点 |
+| heatingLevels=0 | 可点 | 置灰/隐藏 |
+| seats 为空 | 座椅区占位提示(见 8) | — |
+
+---
+
+## 5. 状态机
+
+### 5.1 单座椅档位循环
+
+```
+通风:0 → 1 → 2 → … → V → 0 (V = ventilationLevels)
+加热:0 → 1 → 2 → … → H → 0 (H = heatingLevels)
+```
+
+### 5.2 单座椅互斥联动(点击后、下发前计算)
+
+| 用户操作 | 联动结果 | 下发 payload(该座椅) |
+|----------|---------|------------------------|
+| 通风 0→非0 | 加热强制 0 | `{ ventilationLevel: n, heatingLevel: 0 }` |
+| 加热 0→非0 | 通风强制 0 | `{ ventilationLevel: 0, heatingLevel: n }` |
+| 通风 →0(关) | 加热保持 0 | `{ ventilationLevel: 0, heatingLevel: 0 }` |
+| 加热 →0(关) | 通风保持 0 | `{ ventilationLevel: 0, heatingLevel: 0 }` |
+
+### 5.3 即时下发流程(单座椅)
+
+```
+[座椅按钮] --点击--> [该座椅 applying(置灰,禁点)]
+ │
+ ┌─────────┴─────────┐
+ 成功 失败
+ │ │
+ 档位更新 + Toast 该座椅状态回滚 + Toast"网络异常,请重试"
+ (面板保持) (面板保持)
+```
+
+---
+
+## 6. 组件结构
+
+方案 A:新建独立座椅组件,空调卡片改造为「展示 + 面板」。座椅与空调温控同处「温度调节」面板。
+
+### 6.1 目录结构(建议)
+
+```
+src/robotaxi/pages/begin-route/components/calling-car-modules/
+├── temperature-control.tsx (改造:卡片展示态 + 卡片开关 + 打开面板)
+├── climate-panel/ (新增:温度调节面板容器,可选)
+│ └── index.tsx 面板:座椅区 + 空调温度选择器 + 开关
+├── seat-hv-control/ (新增:座椅通风加热)
+│ └── index.tsx 座椅区渲染 + 档位循环 + 互斥 + 即时下发
+└── ...
+src/robotaxi/pages/begin-route/components/css-folder/
+├── climate-panel.less (新增)
+└── seat-hv-control.less (新增)
+```
+
+> 面板容器 `climate-panel` 是否单独拆分可在编码时按复杂度决定;核心是座椅逻辑独立在 `seat-hv-control`。
+
+### 6.2 组件职责
+
+| 组件/文件 | 职责 |
+|-----------|------|
+| `TemperatureControl`(改造) | 卡片展示当前温度 + 卡片空调开关 + 点击打开面板;温度/开关下发逻辑复用现有 |
+| 「温度调节」面板 | 承载座椅区 + 空调温度滚动选择器 + 面板内开关;底部弹窗动画复用 `use-bottom-popup-animation` |
+| `SeatHvControl`(新增) | 查询座椅状态、渲染座椅、档位循环、互斥联动、点击即时下发(仅改动座椅) |
+| `use-bottom-popup-animation` | 复用现有底部弹窗滑入/滑出动画 |
+
+### 6.3 Props 设计(对齐空调/香氛)
+
+```
+type Props = {
+ tripStatus: TripStatus; // 仅 VehicleApproaching 展示
+ orderId?: string;
+ pageDidShowVersion?: number; // 页面 didShow 版本,触发重查
+ hasPreviousOrder?: boolean; // 连环单时不展示/不下发
+}
+```
+
+### 6.4 API 层
+
+新增 `src/robotaxi/api/seat-hv.ts`:
+
+```
+querySeatHv(orderId) // GET .../cabin/seat/hv
+setSeatHv(payload) // POST .../cabin/seat/hv(seats 仅含改动座椅)
+```
+
+类型新增 `src/robotaxi/api/types/seat-hv.ts`:`SeatHvItem`、`QuerySeatHvResponse`、`SetSeatHvPayload`、`SeatSetItem`、`SeatPosition`。
+
+### 6.5 座椅区视觉
+
+- 车内俯视图:**纯 CSS 布局 + 占位背景图**,座椅按钮绝对定位叠放(设计稿未提供切图,先用占位图)
+- 座位标签文案按 `position` 映射:`SECOND_ROW_LEFT`→「第二排左侧座椅」等
+
+---
+
+## 7. 埋点
+
+沿用项目 xpReport(`this.$app.xpReportSend({ pageId, buttonId, para })`),接驾页 pageId 沿用 begin-route 现有值,buttonId 待与埋点侧确认,建议:
+
+| 事件 | 时机 | 建议 para |
+|------|------|-----------|
+| 打开温度调节面板 | 点击空调卡片 | `{}` |
+| 座椅通风点击下发 | 点击通风按钮 | `{ position, ventilationLevel }` |
+| 座椅加热点击下发 | 点击加热按钮 | `{ position, heatingLevel }` |
+| 座椅下发结果 | 成功/失败回调 | `{ position, ventilationLevel, heatingLevel, success }` |
+
+> 空调温度/开关埋点沿用现有实现,不在本次新增范围。
+
+---
+
+## 8. 异常处理与边界
+
+| 场景 | 处理 |
+|------|------|
+| 查询座椅状态失败 | 座椅区兜底:展示占位提示(如「座椅调节暂不可用」)或空态;不阻塞空调温控 |
+| seats 为空 | 座椅区展示占位提示,不渲染座椅按钮 |
+| ventilationLevels=0 | 该座椅通风按钮置灰/隐藏,仅保留加热 |
+| heatingLevels=0 | 该座椅加热按钮置灰/隐藏,仅保留通风 |
+| 下发接口失败/超时 | 该座椅状态回滚到下发前 + Toast「网络异常,请重试」+ 面板保持 |
+| 下发进行中重复点击 | 该座椅 `applying` 期间按钮置灰禁点,防重复下发 |
+| 连环单(hasPreviousOrder=true) | 空调卡片与座椅区均不展示/不查询/不下发(对齐空调,避免影响上一单乘客) |
+| 非接驾态(tripStatus≠VehicleApproaching) | 组件返回 null,不展示 |
+| 面板打开期间订单切换 | 按 orderId 重新查询并刷新状态,避免串单 |
+
+---
+
+## 9. 落地文件清单
+
+| 文件 | 类型 | 说明 | 落地 |
+|------|------|------|------|
+| `src/robotaxi/api/seat-hv.ts` | 新增 | `querySeatHv` / `setSeatHv` | ✅ |
+| `src/robotaxi/api/types/seat-hv.ts` | 新增 | 座椅通风加热请求/响应类型 | ✅ |
+| `.../calling-car-modules/seat-hv-control/index.tsx` | 新增 | 入口 + 座椅面板 + 档位循环 + 互斥 + 即时下发 | ✅ |
+| `.../css-folder/seat-hv-control.less` | 新增 | 座椅区样式(俯视图 + 按钮) | ✅ |
+| `.../components/calling-car.tsx` | 改 | 挂载 `<SeatHvControl />`(空调卡片下方) | ✅ |
+
+> **本期实现说明**:座椅通风加热作为**独立组件 `SeatHvControl`** 落地(含自带入口 + 底部弹窗面板),未改动已上线的 `temperature-control.tsx`,降低回归风险。温度选择器/空调开关合并进「温度调节」面板(方案 A 完整形态)作为**后续迭代**,待设计切图到位后再合并;当前空调仍沿用现有内联卡片,座椅入口以独立一行挂在其下。
+
+---
+
+## 10. 已决策事项(需求对齐结论)
+
+1. **方案 A**:空调卡片改为「展示 + 点击打开『温度调节』面板」,卡片上保留空调总开关;座椅区在面板内。
+2. **座椅渲染**:按接口返回渲染,兼容二/三排;`ventilationLevels=0` 通风置灰/隐藏仅留加热。
+3. **档位循环**:`0→1→…→N→0`,上限 N 由该座椅 `ventilationLevels`/`heatingLevels` 决定。
+4. **互斥**:同座椅通风与加热至多一个非 0;开一方则另一方自动归 0,一起下发。
+5. **下发范围**:仅下发被改动的座椅(`seats` 数组含该座椅 ventilation+heating 两字段)。
+6. **下发时机**:点击按钮即时下发,无独立"应用"按钮。
+7. **空调下发**:复用现有 `setTemperature` / `airConditionerUnlock` 及温区/防抖逻辑。
+8. **座椅俯视图**:纯 CSS 布局 + 占位背景图 + 绝对定位。
+9. **接口鉴权**:`needLogin=true`、`needSign=false`。
+
+---
+
+## 11. 待确认项(进入编码前建议对齐)
+
+1. 埋点 pageId / buttonId 的具体数值(与埋点侧对齐)。
+2. 座椅区俯视图、通风/加热图标、档位视觉(不同档位的视觉呈现)的正式设计切图与色值。
+3. 下发成功后的 Toast 文案(如「已设置第二排左侧座椅通风2档」是否需要,或统一「设置已应用至车辆」)。
+4. 温度滚动选择器的具体交互(是否复用现有步进逻辑仅换 UI,还是全新滚动组件)——本期倾向复用现有下发逻辑,仅调整展示。
+5. 查询座椅状态失败时座椅区的确切占位文案。
diff --git "a/docs/\350\275\246\350\275\275\351\246\231\346\260\233\347\263\273\347\273\237\346\246\202\350\246\201\350\256\276\350\256\241.md" "b/docs/\350\275\246\350\275\275\351\246\231\346\260\233\347\263\273\347\273\237\346\246\202\350\246\201\350\256\276\350\256\241.md"
new file mode 100644
index 0000000..1319ec0
--- /dev/null
+++ "b/docs/\350\275\246\350\275\275\351\246\231\346\260\233\347\263\273\347\273\237\346\246\202\350\246\201\350\256\276\350\256\241.md"
@@ -0,0 +1,363 @@
+# 概要设计文档:Robotaxi 车载香氛系统
+
+## 0. 文档信息
+
+| 项 | 内容 |
+|----|------|
+| 功能名称 | 车载香氛系统(智能香氛远程控制) |
+| 所属模块 | Robotaxi 接驾界面(begin-route / 行程开始页) |
+| 目标分支 | `rt/feature/v2.0_ui_xj_730` |
+| 文档状态 | 概要设计(需求已对齐,待评审后进入编码) |
+| 关联能力 | 空调温控(`temperature-control.tsx`)—— 本功能与之左右并排,复用同一套远控范式 |
+
+---
+
+## 1. 需求背景
+
+Robotaxi 接驾阶段(车辆即将到达,`TripStatus.VehicleApproaching`)已支持空调温控、车门/后备箱远程控制。本次新增 **车载香氛** 远程控制能力,让乘客在上车前即可通过小程序开启/关闭车内香氛并选择香型,提升乘车体验。
+
+香氛能力与空调对齐:
+- 同为「接驾态可操作」的车机远控
+- 同样抽取为**独立组件**,挂载在接驾卡片区
+- UI 上与空调卡片**左右并排**(左空调、右香氛)
+
+---
+
+## 2. 需求概述
+
+### 2.1 卡片入口(接驾界面)
+
+接驾卡片区采用**左右双卡片**布局:
+
+```
+┌─────────────┬─────────────┐
+│ 空调温控 │ 智能香氛 │
+│ (左卡片) │ (右卡片) │
+│ │ 主标题:智能香氛 │
+│ │ 副标题:木质雪松 / 已关闭 │
+└─────────────┴─────────────┘
+```
+
+- 右卡片为「智能香氛」入口卡片(金色渐变,左上角「香氛」标签 + 图标)
+- **卡片主标题**由查询接口返回的 `switchOn` + `currentScent` 映射:
+ - `switchOn=false` → `已关闭`
+ - `switchOn=true` → 当前香型名称(如 `香杉影木`)
+- 卡片副标题为固定文案(如 `车载香氛·畅享出行`),右下角箭头 `>`
+- **点击右卡片 → 切换面板开合**(再次点击卡片可关闭面板)
+
+### 2.2 控制面板(底部弹窗)
+
+面板以底部弹窗形式呈现,覆盖接驾界面下半部分。**面板无总开关 Switch、无「应用到车辆」按钮**,改为「点击香型即时下发」:
+
+```
+┌───────────────────────────────┐
+│ 车内香氛 [ × ]│ ← 顶部:标题居中 + 关闭
+├───────────────────────────────┤
+│ ┌──────┐ ┌──────┐ ┌──────┐ │
+│ │ 香杉 │ │ 白色 │ │ 琥珀 │ │ ← 香型卡片(竖排名 + 香调描述)
+│ │ 影木 │ │ 清幽 │ │ 光橘 │ │
+│ │沉稳·笃定│ │清新·淡雅│ │愉悦·活力│ │
+│ └──────┘ └──────┘ └──────┘ │
+└───────────────────────────────┘
+```
+
+**香型选择区展示逻辑**:
+- 车端返回的 `availableScents`:正常显示、可点击
+- 车端未返回的香型:不展示
+- `availableScents` 为空:显示占位文案「当前车辆不支持香型选择」
+- 香调描述(如「沉稳·笃定」):接口 `desc` 优先,缺失时按 `id` 走前端本地兜底表
+
+**当前生效态**:
+- `switchOn=true` 时,`currentScent` 命中的香型卡片高亮(米金底 + 金边)
+- `switchOn=false` 时,无高亮
+
+### 2.3 点击香型即时下发(无独立按钮)
+
+用户**点击香型卡片即触发下发**,无需二次确认:
+
+1. 点击**非当前生效**香型 → 开启香氛并切换到该香型(`switchOn=true` + `channelId`)
+2. 点击**当前生效(已高亮)**香型 → 关闭香氛(`switchOn=false`,`channelId` 可不传)
+3. 下发期间禁止重复点击(`applying` 锁 + 香型卡片置灰)
+4. **成功**:
+ - Toast:开启 `设置已应用至车辆` / 关闭 `已关闭香氛`
+ - 面板保持打开,高亮态更新为最新
+ - 「智能香氛」卡片主标题更新(香型名 / `已关闭`)
+5. **失败**:
+ - Toast:`网络异常,请重试`
+ - 状态回滚到下发前,面板保持,用户可重试
+
+> 关闭方式:① 点击已选中香型关闭香氛功能;② 点击关闭 X 或蒙层收起面板;③ 再次点击入口卡片收起面板。
+
+---
+
+## 3. 接口契约
+
+服务域 `vehicleConvoy`,路径前缀 `_ROBOTAXI_HOST`。注意本功能接口为 **`base`** 段(`vehicleConvoy/base/v1/...`),与空调(`vehicleConvoy/api/v1/...`)不同。鉴权:`needLogin=true`、**`needSign=false`**(对齐空调,不做签名)。
+
+> 响应遵循项目统一两层结构(见 `src/utils/request.ts`):`{ code, msg, data }`,`code===CODE_SUCCESS(200/0)` 视为成功,`request` 默认解包返回第二层 `data`。
+
+### 3.1 查询香氛状态
+
+```
+GET /vehicleConvoy/base/v1/mini-program/cabin/fragrance?orderId={orderId}
+```
+
+**请求参数**(query):
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| orderId | string | 是 | 订单 ID |
+
+**响应示例**:
+
+```json
+{
+ "code": 200,
+ "msg": null,
+ "data": {
+ "fragrance": {
+ "switchOn": true,
+ "currentScent": "SANDALWOOD",
+ "availableScents": [
+ { "channelId": "1", "id": "SANDALWOOD", "name": "香杉影木" },
+ { "channelId": "2", "id": "WHITE", "name": "白色清幽" },
+ { "channelId": "3", "id": "AMBER", "name": "琥珀光橘" }
+ ]
+ }
+ }
+}
+```
+
+**data 字段说明**:
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| fragrance.switchOn | boolean | 香氛总开关的真实状态(决定 Switch 初始态) |
+| fragrance.currentScent | string | 当前香型的 `id`(用于匹配默认选中项);关闭时可为空 |
+| fragrance.availableScents | Scent[] | 车端可用香型列表;空数组 = 不支持香型选择 |
+| Scent.channelId | string | 香型通道 ID,**下发时使用此字段** |
+| Scent.id | string | 香型业务标识(与 currentScent 对应) |
+| Scent.name | string | 香型展示名称 |
+
+### 3.2 设置香氛
+
+```
+POST /vehicleConvoy/base/v1/mini-program/cabin/fragrance
+```
+
+**请求体**:
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| orderId | string/number | 是 | 订单 ID |
+| switchOn | boolean | 是 | 目标开关状态 |
+| channelId | string | 否 | 目标香型通道 ID;`switchOn=true` 时必传,`switchOn=false` 时可不传 |
+
+**请求示例**:
+
+```json
+{ "orderId": 123456, "channelId": "1", "switchOn": true }
+```
+
+**响应示例**:
+
+```json
+{ "code": 200, "msg": null, "data": null }
+```
+
+**成功判定**:`code===CODE_SUCCESS`(HTTP 200 且业务码成功)即视为下发成功,`data` 可为 null。
+
+---
+
+## 4. 交互流程
+
+### 4.1 主流程(进入接驾态 → 打开面板 → 点击香型即时下发)
+
+```
+进入接驾态 (VehicleApproaching)
+ │
+ ├─ 查询香氛状态 GET .../cabin/fragrance
+ │ ├─ 成功 → 卡片主标题按 switchOn+currentScent 映射
+ │ │ (关→"已关闭" / 开→香型名);生效香型高亮
+ │ └─ 失败 → 按"已关闭"兜底(见 8.异常处理)
+ │
+用户点击「智能香氛」卡片
+ │
+ ├─ 底部弹窗滑入(复用 use-bottom-popup-animation)
+ ├─ 香型区渲染 availableScents;生效香型高亮
+ │
+用户点击某香型卡片(即时下发):
+ │
+ ├─ 点非生效香型 → nextSwitchOn=true, channelId=该香型
+ ├─ 点已生效(高亮)香型 → nextSwitchOn=false(关闭香氛)
+ ├─ 下发期间置灰防重复点击
+ ├─ POST .../cabin/fragrance
+ │ payload: { orderId, switchOn, channelId? }
+ │
+ ├─ 成功 → 高亮态更新 + 卡片主标题更新
+ │ + Toast(开:"设置已应用至车辆" / 关:"已关闭香氛");面板保持
+ └─ 失败 → 状态回滚 + Toast"网络异常,请重试";面板保持
+```
+
+### 4.2 香型选择区展示分支
+
+| 车端返回 | 展示 |
+|----------|------|
+| availableScents 非空 | 渲染各香型,可点击选择 |
+| availableScents 为空 | 占位文案「当前车辆不支持香型选择」,隐藏香型项 |
+| 未在 availableScents 中的香型 | 不展示 |
+
+---
+
+## 5. 状态机
+
+### 5.1 点击香型的即时下发(无 Switch、无独立按钮)
+
+| 点击对象 | 下发 payload | 结果 |
+|----------|-------------|------|
+| 非当前生效香型 | `{ switchOn: true, channelId }` | 开启香氛并切换到该香型,该卡片高亮 |
+| 当前生效(高亮)香型 | `{ switchOn: false }` | 关闭香氛,取消高亮 |
+| 下发进行中(applying) | — | 忽略点击(置灰防重复) |
+
+### 5.2 即时下发流程
+
+```
+[香型卡片] --点击--> [applying(置灰,禁点)]
+ │
+ ┌─────────┴─────────┐
+ 成功 失败
+ │ │
+ 高亮/标题更新+Toast 状态回滚+Toast"网络异常,请重试"
+ (开:"设置已应用至车辆" (面板保持)
+ 关:"已关闭香氛")
+ 面板保持
+```
+
+### 5.3 卡片主标题状态映射
+
+| 数据来源 | 主标题文案 |
+|----------|-----------|
+| switchOn=false | `已关闭` |
+| switchOn=true | 当前生效香型 name(如 `香杉影木`) |
+| 查询失败/无数据 | 按「已关闭」兜底,主标题展示 `已关闭`(见 8) |
+
+---
+
+## 6. 组件结构
+
+对齐空调组件(`temperature-control.tsx`)的独立组件模式,香氛抽取为**独立可复用组件**。
+
+### 6.1 目录结构(建议)
+
+```
+src/robotaxi/pages/begin-route/components/calling-car-modules/
+├── temperature-control.tsx (现有:空调,左卡片)
+├── fragrance-control/ (新增:香氛,右卡片)
+│ └── index.tsx 主组件(卡片入口 + 面板 + 即时下发编排)
+└── ...
+src/robotaxi/pages/begin-route/components/css-folder/
+└── fragrance-control.less (新增:香氛样式)
+```
+
+### 6.2 组件职责
+
+| 组件/文件 | 职责 |
+|-----------|------|
+| `FragranceControl`(index) | 卡片入口渲染、主标题状态、面板开合、进入接驾态查询、点击香型即时下发 |
+| `use-bottom-popup-animation` | 复用现有底部弹窗滑入/滑出动画 |
+
+### 6.3 Props 设计(对齐空调)
+
+```
+type Props = {
+ tripStatus: TripStatus; // 仅 VehicleApproaching 展示
+ orderId?: string;
+ pageDidShowVersion?: number; // 页面 didShow 版本,触发重查
+ hasPreviousOrder?: boolean; // 连环单时不展示/不下发
+}
+```
+
+### 6.4 挂载点
+
+在 `calling-car.tsx` 车辆信息卡片区,将空调与香氛**左右并排**:
+- 左:`<TemperatureControl />`(现有)
+- 右:`<FragranceControl />`(新增)
+
+> 布局改造需同步调整 `css-folder/calling-car.less` 的卡片容器为左右布局。可参考旁支 `75689edaa` 的 `temperature-fragrance-control` 左右双卡片样式作为视觉参照(该旁支未合入,仅作参考)。
+
+### 6.5 API 层
+
+在 `src/robotaxi/api/` 下新增香氛接口(建议 `api/car-control.ts` 追加,或新建 `api/fragrance.ts`):
+
+```
+queryCabinFragrance(orderId) // GET .../cabin/fragrance
+setCabinFragrance(payload) // POST .../cabin/fragrance
+```
+
+类型定义追加至 `api/types/begin-route.ts`(或新建 `api/types/fragrance.ts`):`CabinFragranceResponse`、`FragranceScent`、`SetFragrancePayload`。
+
+---
+
+## 7. 埋点
+
+沿用项目 xpReport 双系统(`this.$app.xpReportSend({ pageId, buttonId, para })`),接驾页 pageId 沿用 begin-route 现有值,buttonId 待与埋点侧确认,建议:
+
+| 事件 | 时机 | 建议 para |
+|------|------|-----------|
+| 香氛卡片点击 | 打开/关闭面板 | `{ switchOn }` |
+| 香型点击下发 | 点击某香型卡片即时下发 | `{ channelId, switchOn }` |
+| 下发结果 | 成功/失败回调 | `{ switchOn, channelId, success }` |
+
+> 具体 pageId / buttonId 数值需与埋点管理侧对齐后补充。
+
+---
+
+## 8. 异常处理与边界
+
+| 场景 | 处理 |
+|------|------|
+| 查询香氛状态失败 | 按「已关闭」兜底:卡片主标题展示 `已关闭`,无高亮;不阻塞其他卡片 |
+| availableScents 为空 | 香型区展示「当前车辆不支持香型选择」,无可点击香型 |
+| currentScent 不在 availableScents | 开启态回退取列表第一项作为生效香型 |
+| 下发接口失败/超时 | 状态回滚到下发前 + Toast「网络异常,请重试」+ 面板保持 |
+| 连环单(hasPreviousOrder=true) | 不展示香氛卡片、不查询、不下发(对齐空调,避免影响上一单乘客) |
+| 非接驾态(tripStatus≠VehicleApproaching) | 组件返回 null,不展示 |
+| 下发进行中重复点击 | `applying` 期间香型卡片置灰禁点,防重复下发 |
+| 面板打开期间订单切换 | 按 orderId 重新查询并刷新状态,避免串单 |
+
+---
+
+## 9. 落地文件清单
+
+| 文件 | 类型 | 说明 | 落地 |
+|------|------|------|------|
+| `src/robotaxi/api/fragrance.ts` | 新增 | `queryCabinFragrance` / `setCabinFragrance` | ✅ |
+| `src/robotaxi/api/types/fragrance.ts` | 新增 | 香氛请求/响应类型 | ✅ |
+| `.../calling-car-modules/fragrance-control/index.tsx` | 新增 | 香氛卡片入口 + 底部弹窗面板主组件 | ✅ |
+| `.../css-folder/fragrance-control.less` | 新增 | 香氛卡片与面板样式 | ✅ |
+| `.../components/calling-car.tsx` | 改 | 挂载 `<FragranceControl />`,与空调左右并排 | ✅ |
+| `.../css-folder/calling-car.less` | 改 | 新增 `.car-card-comfort` 左右布局容器 | ✅ |
+| `src/robotaxi/components/demo/index.tsx` | 改(可选) | Demo 页增加香氛卡片渲染,便于联调 | 待定 |
+
+> 面板与卡片入口合并在单一 `index.tsx`(面板结构简单,未单拆 `fragrance-panel.tsx`)。
+
+---
+
+## 10. 已决策事项(需求对齐结论)
+
+1. **卡片布局**:接驾卡片区采用左右双卡片,左=空调、右=香氛。
+2. **下发字段**:`channelId`(非 `scentId`/`id`)。请求体 `{ orderId, channelId, switchOn }`。
+3. **香型耗尽逻辑**:本期忽略,默认选中 `availableScents` 第一项。
+4. **关闭态下发**:`switchOn=false`,`channelId` 可不传。
+5. **副标题映射**:关→`已关闭`,开→当前香型名。
+6. **组件化**:抽取为独立组件(对齐空调 `temperature-control` 模式),便于复用与挂载。
+7. **接口鉴权**:`needLogin=true`、`needSign=false`(对齐空调,不做签名)。
+8. **查询失败兜底**:按「已关闭」处理,副标题展示 `已关闭`,面板 Switch 关闭态。
+9. **文档范围**:本次仅概要设计,不含编码。
+
+---
+
+## 11. 待确认项(进入编码前建议对齐)
+
+1. 埋点 pageId / buttonId 的具体数值(与埋点侧对齐)。
+2. 香型选择区的视觉样式(单选圆点 / 卡片选中态)与设计稿对齐。
diff --git a/src/app.config.ts b/src/app.config.ts
index f6e10b4..7bfd9a5 100644
--- a/src/app.config.ts
+++ b/src/app.config.ts
@@ -40,6 +40,7 @@
'pages/operating-area/index',
'pages/share-trip/index',
'pages/personal-center/index',
+ 'pages/feedback/index',
'pages/favorite-sites/index',
'pages/payment-setting/index',
'pages/payment-setting-detail/index',
diff --git a/src/components/custom-navbar/index.less b/src/components/custom-navbar/index.less
index a3ad488..30ab461 100644
--- a/src/components/custom-navbar/index.less
+++ b/src/components/custom-navbar/index.less
@@ -21,6 +21,16 @@
display: block;
width: 64px;
height: 64px;
+
+ &.pure {
+ width: 24px;
+ height: 24px;
+ box-sizing: border-box;
+ border-left: 4px solid #141E27;
+ border-bottom: 4px solid #141E27;
+ transform: rotate(45deg);
+ margin-left: 10px;
+ }
}
}
.btn-app-wrap {
diff --git a/src/components/custom-navbar/index.tsx b/src/components/custom-navbar/index.tsx
index 9adc523..0e1a7ba 100644
--- a/src/components/custom-navbar/index.tsx
+++ b/src/components/custom-navbar/index.tsx
@@ -26,6 +26,8 @@
showAppBtn: false, // 是否显示返回app按钮
appBtnText: '返回APP', // 返回app按钮文案
showTitleColor: false, // 显示页面标题设置颜色
+ backIcon: '', // 自定义返回图标,未传时使用默认图标
+ pureBackArrow: false, // 使用纯样式返回箭头,不使用图片
};
state = {
@@ -82,7 +84,7 @@
render() {
// 这里需要注意:当设置 opacity 时,不是整个导航栏都透明,而是要保留左边区域的操作,比如后退
const { isIOS } = this.state;
- const { fixedtop, seizeSeat, background, opacity, showBack, colorTheme, title, showAppBtn, appBtnText, showTitleColor } = this.props;
+ const { fixedtop, seizeSeat, background, opacity, showBack, colorTheme, title, showAppBtn, appBtnText, showTitleColor, backIcon, pureBackArrow } = this.props;
const appInaverStyle = {
'backgroundColor': hexToRgbA(background, opacity),
height: this.getAppInaverHeight() + 'Px',
@@ -97,18 +99,16 @@
<View className='left'>
{showBack && !showAppBtn && (
<View className='back-wrap' onClick={this.handlergoback.bind(this)}>
- {opacity === 0 ? (
- colorTheme === 'black' ? (
- <Image className='ic-left' mode='heightFix' src={BackBlack2}></Image>
- ) : (
- <Image className='ic-left' mode='heightFix' src={BackWhite2}></Image>
- )
+ {pureBackArrow ? (
+ <View className='ic-left pure' />
) : (
- colorTheme === 'black' ? (
- <Image className='ic-left' mode='heightFix' src={BackBlack}></Image>
- ) : (
- <Image className='ic-left' mode='heightFix' src={BackWhite}></Image>
- )
+ <Image
+ className='ic-left'
+ mode='heightFix'
+ src={backIcon || (opacity === 0
+ ? (colorTheme === 'black' ? BackBlack2 : BackWhite2)
+ : (colorTheme === 'black' ? BackBlack : BackWhite))}
+ ></Image>
)}
</View>
)}
diff --git a/src/pages/not-on-whitelist/index.tsx b/src/pages/not-on-whitelist/index.tsx
index b28bf04..f701545 100644
--- a/src/pages/not-on-whitelist/index.tsx
+++ b/src/pages/not-on-whitelist/index.tsx
@@ -7,7 +7,7 @@
import { jumpTo } from '../../utils/jump';
import './index.less';
-const NOT_ON_WHITELIST_IMAGE_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/not-on-whitelist-v2.png';
+const NOT_ON_WHITELIST_IMAGE_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/not-on-whitelist-v3.jpeg';
const BACK_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/back-icon.png';
const LOGIN_PAGE_URL = '/pages/user/login';
diff --git a/src/pages/robotaxi/home/components/city-select/index.less b/src/pages/robotaxi/home/components/city-select/index.less
new file mode 100644
index 0000000..0467973
--- /dev/null
+++ b/src/pages/robotaxi/home/components/city-select/index.less
@@ -0,0 +1,75 @@
+@import '../../../.././../robotaxi/style/ui-variables.less';
+
+.select-city-wrap {
+ position: relative;
+
+ .select-city {
+ display: flex;
+ align-items: center;
+ font-size: 32px;
+ font-weight: 500;
+ line-height: 48px;
+ letter-spacing: normal;
+ color: @black-text-main;
+
+ .select-city-icon {
+ width: 24px;
+ height: 24px;
+ margin-left: 4px;
+ transition: transform 0.2s ease;
+ }
+
+ .select-city-icon-active {
+ transform: rotate(180deg);
+ }
+ }
+
+ .city-dropdown-mask {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 10;
+ }
+
+ .city-dropdown {
+ position: absolute;
+ top: 64px;
+ left: 0;
+ z-index: 11;
+ min-width: 164px;
+ max-height: 480px;
+ overflow-y: auto;
+ background: #FFFFFF;
+ backdrop-filter: blur(40px);
+ border-radius: 12px;
+ box-shadow: 0px 8px 24px 0px rgba(84, 92, 143, 0.16);
+ padding: 8px 0;
+
+ .city-dropdown-item {
+ position: relative;
+ padding: 0 64px 0 32px;
+ line-height: 80px;
+ font-size: 28px;
+ font-weight: 300;
+ letter-spacing: normal;
+ color: @black-text-main;
+ white-space: nowrap;
+ }
+
+ .city-dropdown-item-active {
+ font-weight: 500;
+ color: @black-text-main;
+
+ .city-dropdown-item-icon {
+ position: absolute;
+ top: 50%;
+ right: 22px;
+ transform: translateY(-50%);
+ width: 22px;
+ height: 16px;
+ }
+ }
+ }
+}
diff --git a/src/pages/robotaxi/home/components/city-select/index.tsx b/src/pages/robotaxi/home/components/city-select/index.tsx
new file mode 100644
index 0000000..5610ba0
--- /dev/null
+++ b/src/pages/robotaxi/home/components/city-select/index.tsx
@@ -0,0 +1,97 @@
+import { useEffect, useState } from 'react';
+import { View, Text, Image } from '@tarojs/components';
+import { getOpenCity } from '../../../../../robotaxi/api/select-city';
+import './index.less';
+
+type Props = {
+ /** 当前选中城市名称。 */
+ selectedCityName?: string;
+ /** 选中城市回调(内联切城)。 */
+ onSelectCity: (city: any) => void;
+};
+
+/**
+ * 首页顶部城市选择:自获取有权限(运营)城市列表,多于一个时点击展开下拉选择,否则仅展示当前城市。
+ */
+export default function CitySelect(props: Props) {
+ const [dropdownVisible, setDropdownVisible] = useState(false); // 下拉是否展开
+ const [openCityList, setOpenCityList] = useState<any[]>([]); // 有权限(运营)城市列表
+ const hasMultipleCities = openCityList.length > 1; // 多于一个城市才允许下拉选择
+ const displayCityName = props.selectedCityName || openCityList[0]?.name || '北京市'; // 当前展示城市名
+
+ /**
+ * 拉取有权限(运营)城市列表。
+ */
+ useEffect(() => {
+ let mounted = true;
+ void getOpenCity().then((response: any) => {
+ if (!mounted) {
+ return;
+ }
+ const cities = Array.isArray(response?.cities) ? response.cities : [];
+ setOpenCityList(cities);
+ }).catch((error) => {
+ console.error('获取运营城市列表失败', error);
+ });
+ return () => {
+ mounted = false;
+ };
+ }, []);
+
+ /**
+ * 点击城市入口:多城市时切换下拉展开,单城市时无操作。
+ */
+ const handleEntryClick = () => {
+ if (!hasMultipleCities) {
+ return;
+ }
+ setDropdownVisible((visible) => !visible);
+ };
+
+ /**
+ * 选中下拉城市后关闭弹窗并回调切城。
+ */
+ const handlePickCity = (city: any) => {
+ setDropdownVisible(false);
+ props.onSelectCity(city);
+ };
+
+ return (
+ <View className="select-city-wrap">
+ <View onClick={handleEntryClick} className="select-city">
+ <Text>{displayCityName}</Text>
+ {hasMultipleCities && (
+ <Image
+ className={`select-city-icon ${dropdownVisible ? 'select-city-icon-active' : ''}`}
+ src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/select-city-icon-20260722.png"
+ />
+ )}
+ </View>
+ {hasMultipleCities && dropdownVisible && (
+ <>
+ <View className="city-dropdown-mask" onClick={() => setDropdownVisible(false)} />
+ <View className="city-dropdown">
+ {openCityList.map((city) => {
+ const isActive = city.name === displayCityName;
+ return (
+ <View
+ key={city.id ?? city.code ?? city.name}
+ className={`city-dropdown-item ${isActive ? 'city-dropdown-item-active' : ''}`}
+ onClick={() => handlePickCity(city)}
+ >
+ {city.name}
+ {isActive && (
+ <Image
+ className="city-dropdown-item-icon"
+ src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/selected-city-20260708.png"
+ />
+ )}
+ </View>
+ );
+ })}
+ </View>
+ </>
+ )}
+ </View>
+ );
+}
diff --git a/src/pages/robotaxi/home/components/home-content-panel.tsx b/src/pages/robotaxi/home/components/home-content-panel.tsx
index 6362541..12ff93c 100644
--- a/src/pages/robotaxi/home/components/home-content-panel.tsx
+++ b/src/pages/robotaxi/home/components/home-content-panel.tsx
@@ -1,4 +1,4 @@
-import { View, Text, Image } from '@tarojs/components';
+import { View, Text, Image, Swiper, SwiperItem } from '@tarojs/components';
import OrderStatusCard from './order-status-card';
import { TripStatus } from '../../../../robotaxi/constants/trip-status';
import { safeParse } from '../../../../utils/Robotaxi-public-method';
@@ -108,57 +108,70 @@
>
<View className="content-body">
<View className="select-site">
- <Image className="site-top-bg" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/site-top-bg-20260507.png' />
- <View className="site-boarding-point">
- <View onClick={() => props.onSelectSite('start')} className="now-site">
- <View className="now-site-dot"></View>
- <View className="now-site-address">
- <View className="now-site-address-content">
- 从
- <Text className="now-site-address-text">
- {startSiteText}
- </Text>
- 出发
- </View>
- <Image className="now-site-icon" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/right-icon.png" />
- </View>
- </View>
- <View onClick={() => props.onSelectSite('end')} className="to-site">
- <View className="to-site-dot"></View>
- <View className="to-site-address">
- <View className="to-site-text">你想去哪里?</View>
- {props.showQuickCapsule && (
- <View
- onClick={(event) => {
- event.stopPropagation();
- props.onQuickCapsuleClick();
- }}
- className="to-site-fast"
- >
- {renderTailPreserveText(props.quickCapsuleText)}
+ <Image className="site-top-bg" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/top-card-bg-20260722.png' />
+ <View className="select-site-content">
+ <View className="site-boarding-point">
+ <View onClick={() => props.onSelectSite('start')} className="now-site">
+ <View className="now-site-dot"></View>
+ <View className="now-site-address">
+ <View className="now-site-address-content">
+ 从
+ <Text className="now-site-address-text">
+ {startSiteText}
+ </Text>
+ 出发
</View>
- )}
+ <Image className="now-site-icon" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/now-site-arrow-20260708.png" />
+ </View>
+ </View>
+ <View onClick={() => props.onSelectSite('end')} className="to-site">
+ <View className="to-site-dot"></View>
+ <View className="to-site-address">
+ <View className="to-site-text">你想去哪?</View>
+ {props.showQuickCapsule && (
+ <View
+ onClick={(event) => {
+ event.stopPropagation();
+ props.onQuickCapsuleClick();
+ }}
+ className="to-site-fast"
+ >
+ {renderTailPreserveText(props.quickCapsuleText)}
+ </View>
+ )}
+ </View>
</View>
</View>
+ {props.shouldShowOrderStatusCard && (
+ <OrderStatusCard
+ tripStatus={props.tripStatus}
+ orderData={props.orderData}
+ onViewDetail={props.onViewOrderDetail}
+ orderStatusMap={props.orderStatusMap}
+ />
+ )}
+
+ {props.bannerList.length > 0 && (
+ <View className="activity">
+ <Swiper
+ className="activity-swiper"
+ autoplay={props.bannerList.length > 1}
+ circular={props.bannerList.length > 1}
+ interval={5000}
+ indicatorDots={props.bannerList.length > 1}
+ indicatorColor="#FFFFFF"
+ indicatorActiveColor="#DFB369"
+ >
+ {props.bannerList.map((item, index) => (
+ <SwiperItem key={item.id || index} className="activity-item" onClick={() => props.onBannerClick(item)}>
+ <Image mode="aspectFill" className="activity-img" src={safeParse(item.imageUrl, [])?.[0]?.url} />
+ </SwiperItem>
+ ))}
+ </Swiper>
+ </View>
+ )}
+ {/* <View onClick={props.onToAgreement} className="product-name">XPENG Robotaxi</View> */}
</View>
- {props.shouldShowOrderStatusCard && (
- <OrderStatusCard
- tripStatus={props.tripStatus}
- orderData={props.orderData}
- onViewDetail={props.onViewOrderDetail}
- orderStatusMap={props.orderStatusMap}
- />
- )}
- {props.bannerList.length > 0 && (
- <View className="activity">
- {props.bannerList.map((item, index) => (
- <View key={item.id || index} className="activity-item" onClick={() => props.onBannerClick(item)}>
- <Image mode="aspectFill" className="activity-img" src={safeParse(item.imageUrl, [])?.[0]?.url} />
- </View>
- ))}
- </View>
- )}
- <View onClick={props.onToAgreement} className="product-name">XPENG Robotaxi</View>
</View>
</View>
</View>
diff --git a/src/pages/robotaxi/home/components/home-map-panel.tsx b/src/pages/robotaxi/home/components/home-map-panel.tsx
index 6e2c01c..b7aaad0 100644
--- a/src/pages/robotaxi/home/components/home-map-panel.tsx
+++ b/src/pages/robotaxi/home/components/home-map-panel.tsx
@@ -3,6 +3,7 @@
import { View, Map, Text, Image } from '@tarojs/components';
import MapOverlayTips from './map-overlay-tips';
import MapPointer from './map-pointer';
+import CitySelect from './city-select';
import { getWeatherIconByDesc } from '../../../../utils/weather';
import { DEFAULT_LATITUDE, DEFAULT_LONGITUDE } from '../../../../robotaxi/constants/map';
import { HOME_MAP_HEIGHT, HOME_MAP_ID } from '../constants';
@@ -66,8 +67,8 @@
onRegionChange: (event: any) => void;
/** Marker 点击回调。 */
onMarkerTap: (event: any) => void;
- /** 去城市选择页。 */
- onSelectCity: () => void;
+ /** 内联切换城市(当前页下拉选择)。 */
+ onSelectCityInline: (city: any) => void;
/** 回到当前位置。 */
onToOrigin: () => void;
/** 展示隐私弹窗。 */
@@ -110,23 +111,29 @@
onMarkerTap={props.onMarkerTap}
style={{ width: '100%', height: HOME_MAP_HEIGHT, flex: 'none' }}
>
+ <View className="home-top-mask">
+ <Image className="home-top-mask-img" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/home-top-bg-20260701.png' />
+ </View>
{props.longitude === null && props.latitude === null && (
<View className="map-placeholder-grid" />
)}
<View className="select-city-container">
<View onClick={() => Taro.switchTab({ url: '/pages/home/index' })} className="home-back-btn">
- <Image className="home-back-icon" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/back-icon.png" />
+ <Image className="home-back-icon" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/back-icon-20260716.png" />
</View>
- <View onClick={props.onSelectCity} className="select-city">
- <Text>{props.selectedCityName || '北京市'}</Text>
- <Image className="select-city-icon" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/select-city.png" />
+ <View className="city-weather-box">
+ <CitySelect
+ selectedCityName={props.selectedCityName}
+ onSelectCity={props.onSelectCityInline}
+ />
+ {props.weatherData && (
+ <View className="weather-info">
+ <Text className="weather-desc">{getWeatherIconByDesc(props.weatherData.weather)}</Text>
+ <Text className="weather-separator">·</Text>
+ <Text>{props.displayTemperature}{props.displayTemperatureUnit}</Text>
+ </View>
+ )}
</View>
- {props.weatherData && (
- <View className="weather-info">
- <Image className="weather-icon" src={getWeatherIconByDesc(props.weatherData.weather)} />
- <Text>{props.displayTemperature}{props.displayTemperatureUnit}</Text>
- </View>
- )}
</View>
<View
className="origin-box"
@@ -139,7 +146,7 @@
bottom: typeof props.overlayAnchorBottom === 'number' ? `${props.overlayAnchorBottom}px` : undefined
}}
>
- <Image onClick={props.onToOrigin} className="origin-icon" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/origin-icon.png' />
+ <Image onClick={props.onToOrigin} className="origin-icon" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/origin-icon-20260716.png' />
</View>
<MapOverlayTips
isGuest={props.shouldShowGuestTip}
@@ -156,11 +163,12 @@
onViewOperatingArea={props.onViewOperatingArea}
/>
<View className="map-entry">
- <Image onClick={props.onToPersonalCenter} className="map-entry-icon" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/personal-center.png' />
- <View className="split-line"></View>
- <Image onClick={props.onOpenService} className="map-entry-icon" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/contact-service.png' />
- <View className="split-line"></View>
- <Image onClick={props.onToCoupon} className="map-entry-icon" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/coupon.png' />
+ <View className="map-entry-box">
+ <Image onClick={props.onToPersonalCenter} className="map-entry-icon" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/personal-center-20260716.png' />
+ <View className="split-line"></View>
+ <Image onClick={props.onOpenService} className="map-entry-icon" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/contact-service-20260716.png' />
+ </View>
+ <Image onClick={props.onToCoupon} className="map-entry-icon entry-coupon" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/coupon-20260723.png' />
</View>
<MapPointer
showToolTip={props.showToolTip}
@@ -171,8 +179,8 @@
isOperatingArea={props.isOperatingArea}
isAdsorbed={props.isAdsorbed}
currentSite={props.currentSite}
- pointerSrc='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/pointer.png'
- rightIconSrc='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/right-icon.png'
+ pointerSrc='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/pointer-20260722.png'
+ rightIconSrc='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/map-white-arrow-20260708.png'
waitingRadarSrc={props.waitingRadarSrc}
onSelectSite={props.onSelectSite}
/>
diff --git a/src/pages/robotaxi/home/components/map-pointer/index.tsx b/src/pages/robotaxi/home/components/map-pointer/index.tsx
index 893b267..8985d75 100644
--- a/src/pages/robotaxi/home/components/map-pointer/index.tsx
+++ b/src/pages/robotaxi/home/components/map-pointer/index.tsx
@@ -47,7 +47,10 @@
<View className="toolTip">
{
(!props.isOperatingArea || (props.isAdsorbed && props.currentSite.time)) && (
- <View className="toolTip-bg">
+ <View className={`toolTip-bg ${!props.isOperatingArea ? 'toolTip-bg-gray' : ''}`}>
+ {props.isOperatingArea && (
+ <Image className="toolTip-bg-icon" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/map-time-icon-20260708.png" />
+ )}
<Text className="toolTip-bg-text">{!props.isOperatingArea ? '不在运营区' : `限停${props.currentSite.time}分钟`}</Text>
</View>
)
@@ -55,6 +58,7 @@
<View onClick={() => props.onSelectSite('start')} className="toolTip-txt" style={{ paddingRight: !(props.isAdsorbed && props.isOperatingArea) ? '10px' : '12px' }}>
<View className="toolTip-txt-content">{!props.isOperatingArea ? '请拖动至运营范围内' : (props.isAdsorbed ? props.currentSite.name : '请拖动至站点')}</View>
{props.isAdsorbed && props.isOperatingArea && <Image className="toolTip-icon" src={props.rightIconSrc} />}
+ <Image className="toolTip-diamond-icon" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/diamond-icon-20260708.png" />
</View>
</View>
)
@@ -64,7 +68,8 @@
<Image className="waiting-radar" src={props.waitingRadarSrc} />
)
}
- <Image className={`pointer-icon ${props.bounce ? 'bounce' : ''}`} src={props.pointerSrc} />
+ {/* 黑色站点名浮窗(toolTip)显示时隐藏中心大头针 */}
+ <Image className={`pointer-icon ${props.bounce ? 'bounce' : ''} ${props.showToolTip && (props.currentSite.name || !props.isOperatingArea) ? 'hidden-pointer' : ''}`} src={props.pointerSrc} />
</View>
);
}
diff --git a/src/pages/robotaxi/home/components/order-status-card/index.tsx b/src/pages/robotaxi/home/components/order-status-card/index.tsx
index 7b43ca5..959683b 100644
--- a/src/pages/robotaxi/home/components/order-status-card/index.tsx
+++ b/src/pages/robotaxi/home/components/order-status-card/index.tsx
@@ -41,8 +41,8 @@
<View className="order-status-left">
{
orderData.vehiclePictures
- ? <Image className="order-status-left-icon" src={safeParse(orderData.vehiclePictures, {})?.url || 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/vehicle-default.png'} />
- : <Image className="order-status-left-icon" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/vehicle-default.png' />
+ ? <Image className="order-status-left-icon" src={safeParse(orderData.vehiclePictures, {})?.url || 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/default-vehicle-img-20260724-v2.png'} />
+ : <Image className="order-status-left-icon" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/default-vehicle-img-20260724-v2.png' />
}
<View className="order-status-left-detail">
<View>{orderData.plateNumber}</View>
diff --git a/src/pages/robotaxi/home/components/privacy-popup/index.less b/src/pages/robotaxi/home/components/privacy-popup/index.less
index fb334e7..503ea42 100644
--- a/src/pages/robotaxi/home/components/privacy-popup/index.less
+++ b/src/pages/robotaxi/home/components/privacy-popup/index.less
@@ -1,3 +1,5 @@
+@import '../../../../../robotaxi/style/ui-variables.less';
+
.rt-privacy-popup {
position: fixed;
top: 0;
@@ -15,24 +17,24 @@
left: 0;
right: 0;
bottom: 0;
- background: rgba(0, 0, 0, 0.5);
+ background: rgba(0, 0, 0, 0.8);
}
.rt-privacy-content {
position: relative;
background: #ffffff;
padding: 56px 48px 48px 48px;
- border-radius: 32px;
+ border-radius: 60px;
margin: 0 63px;
.rt-title {
margin-bottom: 32px;
text-align: center;
font-size: 36px;
- font-weight: 380;
+ font-weight: 500;
line-height: 52px;
letter-spacing: normal;
- color: #2C333E;
+ color: @black-text-main;
}
.rt-privacy-text {
@@ -40,7 +42,7 @@
font-weight: normal;
line-height: 48px;
letter-spacing: normal;
- color: #2C333E;
+ color: @black-text-main;
margin-bottom: 0;
}
@@ -52,8 +54,8 @@
.rt-item-radio {
flex-shrink: 0;
- width: 42px;
- height: 42px;
+ width: 40px;
+ height: 40px;
margin-right: 14px;
}
@@ -63,7 +65,7 @@
font-weight: normal;
line-height: 40px;
letter-spacing: normal;
- color: #6D7FD9;
+ color: @gold-text;
word-break: break-word;
white-space: normal;
}
@@ -81,6 +83,21 @@
.pt-10 {
padding-top: 10px;
}
+ .check-all-box {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-top: 14px;
+ .all-name{
+ font-size: 28px;
+ line-height: 48px;
+ color: @black-text-main;
+ }
+ .rt-item-radio {
+ flex-shrink: 0;
+ margin-right: 8px;
+ }
+ }
.rt-agree-btn {
margin: 0 auto;
@@ -88,8 +105,8 @@
width: 472px;
text-align: center;
line-height: 96px;
- border-radius: 16px;
- background: linear-gradient(98deg, #8475FF 0%, #7DD6FF 103%);
+ border-radius: 2000px;
+ background: @black-text-main;
font-size: 30px;
font-weight: normal;
letter-spacing: normal;
@@ -111,4 +128,4 @@
}
}
-}
\ No newline at end of file
+}
diff --git a/src/pages/robotaxi/home/components/privacy-popup/index.tsx b/src/pages/robotaxi/home/components/privacy-popup/index.tsx
index cf11ec0..f9eb3b2 100644
--- a/src/pages/robotaxi/home/components/privacy-popup/index.tsx
+++ b/src/pages/robotaxi/home/components/privacy-popup/index.tsx
@@ -150,9 +150,8 @@
<Text className='rt-text-content'>
欢迎使用小鹏 Robotaxi小程序!为了更好地保障您的个人权益,我们将通过 {agreementNamesText || ''} 等文档来保护您的个人信息。
</Text>
- <View className='rt-link-item mt-22 pt-10' onClick={checkAll}>
- <Image className='rt-item-radio' src={isAllAgree ? 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/check.png' : 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/uncheck.png'}
- mode='aspectFill' />
+ <View className='rt-link-item mt-22 pt-10 check-all-box' onClick={checkAll}>
+ <Image className='rt-item-radio' src={isAllAgree ? 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/check-20260630.png' : 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/uncheck.png'} mode='aspectFill' />
<View className='rt-item-name all-name'>我已满十四周岁,阅读并同意:</View>
</View>
</View>
diff --git a/src/pages/robotaxi/home/constants.ts b/src/pages/robotaxi/home/constants.ts
index bb8b747..530c279 100644
--- a/src/pages/robotaxi/home/constants.ts
+++ b/src/pages/robotaxi/home/constants.ts
@@ -22,7 +22,7 @@
export const FLOAT_ACTIVITY_DEFAULT_IMAGE = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/coverImage.png';
/** 等待态雷达资源。 */
-export const WAITING_RADAR_SRC = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/waiting-radar.webp';
+export const WAITING_RADAR_SRC = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/waiting-radar-20260728.webp';
/** 地图内容高度。 */
export const HOME_MAP_HEIGHT = '85vh';
diff --git a/src/pages/robotaxi/home/hooks/use-city-switch.ts b/src/pages/robotaxi/home/hooks/use-city-switch.ts
index 30c31a0..2e638c7 100644
--- a/src/pages/robotaxi/home/hooks/use-city-switch.ts
+++ b/src/pages/robotaxi/home/hooks/use-city-switch.ts
@@ -12,7 +12,7 @@
setIsAdsorbed: (value: boolean) => void;
};
-export default function useCitySwitch() {
+export default function useCitySwitch(currentSelectedCityCode?: string) {
/** 跳过下次定位居中 */
const skipNextLocationCenterRef = useRef(false);
/** 跳过 selectedCity 变更副作用 */
@@ -36,7 +36,8 @@
return null;
}
const nextCode = selectedCity.code;
- if (lastSelectedCityCodeRef.current && nextCode && lastSelectedCityCodeRef.current !== nextCode) {
+ const previousCode = lastSelectedCityCodeRef.current || currentSelectedCityCode;
+ if (previousCode && nextCode && previousCode !== nextCode) {
isCitySwitchingRef.current = true;
shouldMoveToSwitchedCityCenterRef.current = true;
Taro.removeStorageSync('startSite');
diff --git a/src/pages/robotaxi/home/hooks/use-drag-gesture.ts b/src/pages/robotaxi/home/hooks/use-drag-gesture.ts
index 0253aa4..d28175c 100644
--- a/src/pages/robotaxi/home/hooks/use-drag-gesture.ts
+++ b/src/pages/robotaxi/home/hooks/use-drag-gesture.ts
@@ -76,7 +76,8 @@
query.select('.content-body').boundingClientRect(rect => {
if (rect) {
const contentHeight = rect.height;
- const baseHeight = 280;
+ // !! 折叠高度由容器内容自动计算,避免写死高度导致内容被截断,先保留原来的逻辑最小280
+ const baseHeight = Math.max(Math.ceil(contentHeight), 280);
const oldFullHeight = fullHeight.current;
fullHeight.current = Math.max(contentHeight, baseHeight);
minHeight.current = baseHeight;
@@ -159,6 +160,8 @@
useEffect(() => {
calculateBoxHeight();
+ console.log('-deps.bannerList, deps.tripStatus, deps.shouldShowOrderStatusCard', deps.bannerList, deps.tripStatus, deps.shouldShowOrderStatusCard);
+
}, [deps.bannerList, deps.tripStatus, deps.shouldShowOrderStatusCard]);
useEffect(() => {
diff --git a/src/pages/robotaxi/home/hooks/use-home-bootstrap.ts b/src/pages/robotaxi/home/hooks/use-home-bootstrap.ts
index 844abfb..5be10cb 100644
--- a/src/pages/robotaxi/home/hooks/use-home-bootstrap.ts
+++ b/src/pages/robotaxi/home/hooks/use-home-bootstrap.ts
@@ -257,6 +257,16 @@
const switchState = deps.consumeCitySwitchFlag();
if (switchState.switched && switchState.shouldMoveToCityCenter) {
const switchedCity = Taro.getStorageSync('selectedCity');
+ const cachedUserLocation = readHomeCachedUserLocation();
+ // 切回用户最近一次定位所在城市时,复用按用户位置吸附最近站点的流程。
+ if (auth !== false && switchedCity?.name && cachedUserLocation?.city === switchedCity.name) {
+ deps.getLocationInfo({
+ updateCenter: true,
+ forceCityRefresh: true,
+ forceNearestSite: true
+ });
+ return;
+ }
if (switchedCity?.name) {
try {
const loc = await deps.getCityLocation(switchedCity.name);
diff --git a/src/pages/robotaxi/home/hooks/use-map-state.ts b/src/pages/robotaxi/home/hooks/use-map-state.ts
index e33e715..027aa22 100644
--- a/src/pages/robotaxi/home/hooks/use-map-state.ts
+++ b/src/pages/robotaxi/home/hooks/use-map-state.ts
@@ -7,6 +7,25 @@
const AUTO_SCALE_MIN = 11; // 自动缩放最小等级
const HOME_MAP_POINT_ON_OFF = 1; // 首页地图固定拉取上车点
+// 站点 marker 原始(紫色)图标,来自 formatSiteToMarker
+const OLD_SITE_MARKER_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/site-poi.png';
+// 首页地图站点金色图标
+const SITE_MARKER_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/golden-site-20260708.png';
+
+/**
+ * 将站点 marker 的图标替换为首页金色图标(仅影响首页地图)。
+ */
+const applySiteMarkerIcon = (markerList: any[]) => {
+ if (!Array.isArray(markerList)) {
+ return markerList;
+ }
+ return markerList.map((marker) => (
+ marker?.iconPath === OLD_SITE_MARKER_ICON
+ ? { ...marker, iconPath: SITE_MARKER_ICON, width: 18, height: 18 }
+ : marker
+ ));
+};
+
/**
* 比较两批站点 marker 的关键展示字段是否一致。
*/
@@ -125,6 +144,7 @@
setNearbyLoading(true);
try {
const result = await fetchMapData(lat, lng, code, HOME_MAP_POINT_ON_OFF);
+ result.newMarkers = applySiteMarkerIcon(result.newMarkers);
setIsOperatingArea(result.isInside);
setElectronicAreaId(result.electronicAreaId);
setNearbySites(Array.isArray(result?.sites) ? result.sites : []);
diff --git a/src/pages/robotaxi/home/hooks/use-navigation.ts b/src/pages/robotaxi/home/hooks/use-navigation.ts
index 085a393..8248310 100644
--- a/src/pages/robotaxi/home/hooks/use-navigation.ts
+++ b/src/pages/robotaxi/home/hooks/use-navigation.ts
@@ -70,7 +70,7 @@
forceNearestSite?: boolean;
}) => void;
/** 查询指针吸附 */
- queryPointerByCenter: (lat: number, lng: number) => Promise<void>;
+ queryPointerByCenter: (lat: number, lng: number, options?: { allowAdsorb?: boolean }) => Promise<void>;
};
/**
@@ -353,7 +353,7 @@
setTimeout(() => {
mapCtx.getCenterLocation({
success: (res) => {
- deps.queryPointerByCenter(res.latitude, res.longitude);
+ deps.queryPointerByCenter(res.latitude, res.longitude, { allowAdsorb: false }); // 查看运营区域仅刷新围栏与站点,不吸附最近点位
}
});
}, 600);
diff --git a/src/pages/robotaxi/home/hooks/use-region-change.ts b/src/pages/robotaxi/home/hooks/use-region-change.ts
index fec4089..4948306 100644
--- a/src/pages/robotaxi/home/hooks/use-region-change.ts
+++ b/src/pages/robotaxi/home/hooks/use-region-change.ts
@@ -52,8 +52,10 @@
/**
* 通过中心点主动触发吸附判断。
+ * @param options.allowAdsorb 是否允许吸附到最近点位,默认允许;查看运营区域等仅浏览场景需传 false 以保持视野不被吸附打断。
*/
- const queryPointerByCenter = async (lat: number, lng: number) => {
+ const queryPointerByCenter = async (lat: number, lng: number, options?: { allowAdsorb?: boolean }) => {
+ const allowAdsorb = options?.allowAdsorb !== false;
siteState.setBounce(true);
siteState.setShowToolTip(false);
try {
@@ -62,21 +64,33 @@
siteState.clearCachedStartSiteSelection();
commitQueriedMarkers(newMarkers, false, false);
settleRegionResult({ lat, lon: lng, isInside: false });
+ siteState.setIsAdsorbed(false);
+ if (allowAdsorb) {
+ siteState.setShowToolTip(true); // 运营区外恢复气泡提示
+ }
siteState.setBounce(false);
return;
}
const nearestCandidates = sites.length > 0 ? sites : newMarkers;
- const { adsorbed, marker } = siteState.tryAdsorb(lat, lng, nearestCandidates);
+ const { adsorbed, marker } = allowAdsorb
+ ? siteState.tryAdsorb(lat, lng, nearestCandidates)
+ : { adsorbed: false, marker: null }; // 仅浏览运营区域时不吸附最近点位
commitQueriedMarkers(newMarkers, true, adsorbed);
const finalLat = adsorbed && marker ? marker.latitude : lat; // 最终纬度
const finalLng = adsorbed && marker ? marker.longitude : lng; // 最终经度
- mapState.setLatitude(finalLat);
- mapState.setLongitude(finalLng);
+ if (allowAdsorb) {
+ // 仅浏览运营区域时不回写地图中心,避免 Map 组件按旧 scale 重绘导致刚展开的运营区视野被收回
+ mapState.setLatitude(finalLat);
+ mapState.setLongitude(finalLng);
+ }
settleRegionResult({ lat: finalLat, lon: finalLng, isInside: true });
setTimeout(async () => {
siteState.setBounce(false);
if (!adsorbed) {
await siteState.resolveAddress(lat, lng);
+ }
+ if (!allowAdsorb) {
+ siteState.setShowToolTip(false); // 查看运营区域期间隐藏气泡,避免遮挡围栏,拖动地图后自动恢复
}
}, 600);
} catch (error) {
@@ -128,6 +142,8 @@
siteState.clearCachedStartSiteSelection();
commitQueriedMarkers(newMarkers, false, false);
settleRegionResult({ lat, lon: lng, isInside: false });
+ siteState.setIsAdsorbed(false);
+ siteState.setShowToolTip(true); // 运营区外恢复气泡提示
siteState.setBounce(false);
return;
}
diff --git a/src/pages/robotaxi/home/index.less b/src/pages/robotaxi/home/index.less
index dd2cebc..118ad8e 100644
--- a/src/pages/robotaxi/home/index.less
+++ b/src/pages/robotaxi/home/index.less
@@ -1,3 +1,5 @@
+@import '../../../robotaxi/style/ui-variables.less';
+
.map-placeholder-grid {
position: absolute;
top: 0;
@@ -11,61 +13,52 @@
background-size: 40px 40px;
}
+.home-top-mask {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 312px;
+ z-index: 2;
+ pointer-events: none;
+ .home-top-mask-img {
+ width: 100%;
+ height: 100%;
+ }
+}
+
.select-city-container {
position: absolute;
top: 104px;
- left: 20px;
+ left: 24px;
display: flex;
align-items: center;
+ justify-content: flex-start;
+ z-index: 3;
.home-back-btn {
display: flex;
align-items: center;
justify-content: center;
- width: 72px;
- height: 72px;
- border-radius: 200px;
- background: rgba(255, 255, 255, 0.9);
- backdrop-filter: blur(40px);
- box-shadow: 0px 4px 16px 0px rgba(84, 92, 143, 0.1);
- margin-right: 16px;
-
+ width: 64px;
+ height: 64px;
+ margin-right: 24px;
.home-back-icon {
- width: 40px;
- height: 40px;
- }
- }
-
- .select-city {
- display: flex;
- align-items: center;
- font-size: 32px;
- font-weight: 500;
- line-height: 48px;
- letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
-
- .select-city-icon {
- width: 28px;
- height: 28px;
- margin-left: 4px;
+ width: 64px;
+ height: 64px;
}
}
.weather-info {
display: flex;
align-items: center;
- margin-left: 20px;
- font-size: 28px;
+ font-size: 24px;
font-weight: 500;
- line-height: 48px;
- color: rgba(0, 0, 0, 0.9);
+ color: @black-text-04;
- .weather-icon {
- width: 40px;
- height: 40px;
- margin-right: 4px;
- }
+ // .weather-separator {
+ // margin: 0 4px;
+ // }
}
}
@@ -98,12 +91,12 @@
.origin-box {
position: fixed;
top: 55%;
- right: 32px;
+ right: 24px;
z-index: 3;
.origin-icon {
- width: 72px;
- height: 72px;
+ width: 80px;
+ height: 80px;
}
}
@@ -125,25 +118,21 @@
letter-spacing: normal;
color: #ffffff;
border-radius: 200px;
- background: rgba(0, 0, 0, 0.4);
- backdrop-filter: blur(40px);
+ background: #6E7378;
padding: 0 44px;
}
.is-maintain {
margin-top: 20px;
border-radius: 16px;
- background: rgba(255, 249, 226, 0.9);
+ background: #F0AC59;
box-sizing: border-box;
- border: 2px solid #ffffff;
- backdrop-filter: blur(40px);
- box-shadow: 0px 4px 16px 0px rgba(84, 92, 143, 0.1);
font-size: 24px;
font-weight: normal;
line-height: 64px;
text-align: center;
letter-spacing: normal;
- color: #f58900;
+ color: #ffffff;
padding: 0 24px;
width: fit-content;
@@ -167,7 +156,7 @@
display: flex;
align-items: center;
border-radius: 16px;
- background: rgba(0, 0, 0, 0.4);
+ background: #6E7378;
backdrop-filter: blur(40px);
padding: 9px 24px;
@@ -185,7 +174,7 @@
font-weight: normal;
line-height: 46px;
letter-spacing: normal;
- color: #afea60;
+ color: #FFD389;
}
}
}
@@ -194,22 +183,31 @@
.map-entry {
position: fixed;
top: 200px;
- right: 32px;
+ right: 24px;
display: flex;
align-items: center;
flex-direction: column;
- gap: 8px;
- border-radius: 24px;
- box-shadow: 0px 4px 16px 0px rgba(84, 92, 143, 0.1);
- background: rgba(255, 255, 255, 1);
overflow: hidden;
- padding: 8px 12px;
-
- .map-entry-icon {
- width: 64px;
- height: 64px;
+ z-index: 10;
+ .map-entry-box {
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ gap: 24px;
+ border-radius: 20px;
+ background: #FFFFFF;
+ padding: 20px 16px;
+ box-shadow: 0px 4px 16px 0px rgba(84, 92, 143, 0.1);
}
-
+ .map-entry-icon {
+ width: 48px;
+ height:48px;
+ }
+ .entry-coupon {
+ width: 80px;
+ height: 80px;
+ margin-top: 16px;
+ }
.split-line {
width: 44px;
height: 1px;
@@ -280,12 +278,29 @@
z-index: 10;
.toolTip-bg {
- background-image: url("https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/tooltip-bg.png");
+ background-image: url("https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/tooltip-bg-20260715.png");
background-size: cover;
- height: 70px;
- min-width: 155px;
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ padding: 3px 20px 32px 20px;
+ height: 80px;
+ box-sizing: border-box;
+ min-width: 186px;
text-align: center;
width: fit-content;
+ &.toolTip-bg-gray {
+ max-width: 157px;
+ background-image:url("https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/tooltip-bg-gray-20260715.png");
+ background-size: cover;
+ }
+ .toolTip-bg-icon {
+ width: 24px;
+ height: 24px;
+ margin-top: 12px;
+ margin-right: 4px;
+ flex-shrink: 0;
+ }
.toolTip-bg-text {
font-size: 22px;
@@ -293,33 +308,45 @@
letter-spacing: normal;
color: #ffffff;
line-height: 48px;
+ padding-right: 20px;
}
}
.toolTip-txt {
- margin-top: -26px;
+ margin-top: -32px;
+ position: relative;
display: flex;
align-items: center;
- border-radius: 16px;
- background: rgba(255, 255, 255, 0.9);
- backdrop-filter: blur(40px);
- box-shadow: 0px 4px 16px 0px rgba(45, 48, 64, 0.2);
- padding: 8px;
- padding-left: 20px;
+ border-radius: 24px;
+ background: @black-text-main;
+ padding: 20px 16px 20px 20px;
+ min-width: 198px;
+ max-width: 400px;
+ box-sizing: border-box;
.toolTip-txt-content {
font-size: 24px;
font-weight: 500;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
- line-height: 40px;
+ color: #FFFFFF;
}
.toolTip-icon {
margin-left: 8px;
- width: 32px;
- height: 32px;
+ width: 16px;
+ height: 16px;
flex-shrink: 0;
+ }
+
+ // 气泡底部中间的钻石指示图标
+ .toolTip-diamond-icon {
+ position: absolute;
+ left: 50%;
+ top: 100%;
+ transform: translate(-50%, -50%);
+ width: 28px;
+ height: 28px;
+ z-index: -1;
}
}
}
@@ -327,13 +354,16 @@
.pointer-icon {
position: relative;
z-index: 9;
- width: 52px;
- height: 76px;
+ width: 64px;
+ height: 86px;
margin-top: -54px;
&.bounce {
animation: pointer-bounce 600ms ease-in-out;
will-change: transform;
+ }
+ &.hidden-pointer {
+ opacity: 0;
}
}
}
@@ -456,26 +486,32 @@
right: 0;
bottom: 0;
z-index: 100;
- background-color: transparent;
+ background: @gary-theme;
margin-top: 0;
- border-radius: 40px 40px 0 0;
+ border-radius: 60px 60px 0 0;
.select-site {
overflow: hidden;
position: relative;
- padding: 32px;
- padding-top: 40px;
- padding-bottom: 64px;
- background: #f2f3f7;
- border-top: 2px solid #fff;
- border-radius: 40px 40px 0 0;
+ padding-top: 94px;
+ padding-bottom: 68px;
+ background: @gary-theme;
+ border-radius: 60px 60px 0 0;
+ .select-site-content {
+ position: relative;
+ z-index: 1;
+ border-radius: 60px 60px 0px 0px;
+ padding: 0 48px;
+ padding-top: 48px ;
+ background: @gary-theme;
+ }
.site-top-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
- height: 326px;
+ height: 276px;
}
.placeholder-box {
@@ -484,95 +520,94 @@
}
.site-boarding-point {
- border-radius: 24px;
- background: rgba(255, 255, 255, 0.9);
- box-sizing: border-box;
- border: 2px solid #ffffff;
- backdrop-filter: blur(20px);
- box-shadow: 0px 40px 60px 0px rgba(87, 99, 147, 0.1);
- padding: 24px;
-
.now-site {
display: flex;
align-items: center;
padding-left: 28px;
.now-site-dot {
- width: 12px;
- height: 12px;
+ width: 16px;
+ height: 16px;
flex-shrink: 0;
border-radius: 50%;
- background: #6d7fd9;
+ background: @gold-light;
margin-right: 24px;
}
.now-site-address {
display: flex;
align-items: center;
+ flex: 1;
+ min-width: 0;
.now-site-address-content {
+ min-width: 0;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
font-size: 28px;
font-weight: 500;
line-height: 44px;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
- overflow: hidden;
- display: -webkit-box;
- -webkit-line-clamp: 1;
- /* 设置最大显示行数 */
- -webkit-box-orient: vertical;
- text-overflow: ellipsis;
- word-break: break-all;
+ color:rgba(20, 30, 39, 0.8);
+ white-space: nowrap;
.now-site-address-text {
- margin: 0 2px;
- color: #6d7fd9;
+ flex: 1;
+ min-width: 0;
+ display: inline-block;
+ margin: 0 8px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ color: #B38F53;
}
}
.now-site-icon {
flex-shrink: 0;
margin-left: 2px;
- width: 32px;
- height: 32px;
+ width: 24px;
+ height: 24px;
}
}
}
.to-site {
- margin-top: 22px;
+ margin-top: 32px;
display: flex;
align-items: center;
- padding: 20px 0 20px 28px;
- border-radius: 20px;
- background: #f1f2f6;
+ padding: 32px;
+ border-radius: 40px;
+ background: #FFFFFF;
.to-site-dot {
- width: 12px;
- height: 12px;
+ width: 16px;
+ height: 16px;
flex-shrink: 0;
border-radius: 50%;
- background: #8cb654;
+ background: #000000;
margin-right: 24px;
}
.to-site-address {
display: flex;
align-items: center;
-
+ justify-content: space-between;
+ flex: 1;
.to-site-text {
flex-shrink: 0;
- font-size: 32px;
+ font-size: 40px;
font-weight: 500;
- line-height: 48px;
+ line-height: 56px;
letter-spacing: normal;
color: rgba(0, 0, 0, 0.9);
margin-right: 22px;
}
.to-site-fast {
- border-radius: 12px;
- background: #ffffff;
+ background: rgba(204, 208, 215, 0.25);
+ border-radius: 60px;
padding: 8px 21px;
display: flex;
align-items: center;
@@ -732,17 +767,23 @@
}
.activity {
- margin-top: 24px;
+ margin-top: 32px;
+ .activity-swiper {
+ width: 100%;
+ height: 190px;
+ }
.activity-item {
- margin-bottom: 24px;
- border-radius: 24px;
+ box-sizing: border-box;
+ border-radius: 32px;
+ overflow: hidden;
height: 190px;
.activity-img {
- border-radius: 24px;
+ border-radius: 32px;
width: 100%;
height: 100%;
+
}
}
}
diff --git a/src/pages/robotaxi/home/index.tsx b/src/pages/robotaxi/home/index.tsx
index 468fad2..d72ff5d 100644
--- a/src/pages/robotaxi/home/index.tsx
+++ b/src/pages/robotaxi/home/index.tsx
@@ -192,7 +192,7 @@
lastSelectedCityCodeRef,
syncSelectedCityFromStorage,
consumeCitySwitchFlag
- } = useCitySwitch();
+ } = useCitySwitch(selectedCity?.code);
const { displayPolygons, handleScaleChange } = useStationScaleFocus({
currentSite: siteState.currentSite,
originalPolygons: mapState.polygons
@@ -329,7 +329,6 @@
toSelectSitePage,
toCoupon,
toOrderDetail,
- toSelectCityPage,
toAgreementPage,
toLogin,
openService,
@@ -406,6 +405,26 @@
*/
const handleAgreePrivacy = () => {
setShowPrivacy(false);
+ };
+
+ /**
+ * 当前页内联切换城市:写入选中城市缓存后复用启动流程完成切城副作用
+ * (清理起终点、移动地图中心、刷新围栏等),与从城市选择页返回保持一致。
+ */
+ const handleSelectCityInline = (city: any) => {
+ if (!city) {
+ return;
+ }
+ const nextCode = city.adcode || city.code;
+ if (nextCode && nextCode === selectedCity?.code) {
+ return; // 选中的是当前城市,无需切换
+ }
+ Taro.setStorageSync('selectedCity', {
+ name: city.name,
+ code: nextCode,
+ id: city.id
+ });
+ bootstrap.onDidShow();
};
/**
@@ -556,7 +575,7 @@
waitingRadarSrc={WAITING_RADAR_SRC}
onRegionChange={handleRegionChange}
onMarkerTap={handleMarkerTap}
- onSelectCity={toSelectCityPage}
+ onSelectCityInline={handleSelectCityInline}
onToOrigin={toOrigin}
onShowPrivacy={() => setShowPrivacy(true)}
onOpenSetting={handleOpenSetting}
diff --git a/src/robotaxi/api/begin-route.ts b/src/robotaxi/api/begin-route.ts
index 597cce3..c678cf4 100644
--- a/src/robotaxi/api/begin-route.ts
+++ b/src/robotaxi/api/begin-route.ts
@@ -34,7 +34,6 @@
ShareQueryOrderInfoPayload,
ShareQueryOrderInfoResponse,
ShareQueryOrderRouteResponse,
- SubmitComplaintPayload,
SubmitFeedbackResponse,
SubmitOrderCommentPayload,
UnlockVehiclePayload,
@@ -43,6 +42,7 @@
QueryOrderPaymentResultResponse,
QueryLedgerCreditOrderResultResponse
} from './types/begin-route';
+import { withApiLog } from '../utils/api-logger';
// 导航-获取路线和费用
export const queryRouteAndFare = async (payload: QueryRouteAndFarePayload): Promise<QueryRouteAndFareResponse> => {
@@ -144,22 +144,23 @@
// 设置温度
export const setTemperature = async (payload: SetTemperaturePayload): Promise<SetTemperatureResponse> => {
- return request({
+ return withApiLog('设置空调温度', payload, request({
url: _ROBOTAXI_HOST + '/vehicleConvoy/api/v1/mini-program/order/ac/set-temperature',
method: 'POST',
- payload
- }, true);
+ payload,
+ showToast: false
+ }, true));
}
// 设置温度并跳过业务异常拦截,供初始化越界温度纠正场景使用
export const setTemperatureIgnoreBusinessError = async (payload: SetTemperaturePayload): Promise<SetTemperatureResponse> => {
- return request({
+ return withApiLog('设置空调温度(忽略业务异常)', payload, request({
url: _ROBOTAXI_HOST + '/vehicleConvoy/api/v1/mini-program/order/ac/set-temperature',
method: 'POST',
payload,
skipException: true,
showToast: false
- }, true);
+ }, true));
}
// 解锁车辆
@@ -177,31 +178,32 @@
export const queryTemperatureByOrderId = async (
payload: OrderIdPayload
): Promise<QueryTemperatureByOrderIdResponse> => {
- return request({
+ return withApiLog('查询车辆温度', payload, request({
url: _ROBOTAXI_HOST + '/vehicleConvoy/api/v1/mini-program/order/queryTemperatureByOrderId',
method: 'GET',
payload
- }, true);
+ }, true));
}
// 查询车辆空调开关状态
export const queryVehicleAirConditionerSwitch = async (
payload: OrderIdPayload
): Promise<QueryVehicleAirConditionerSwitchResponse> => {
- return request({
+ return withApiLog('查询空调开关状态', payload, request({
url: _ROBOTAXI_HOST + '/vehicleConvoy/api/v1/mini-program/order/queryVehicleAirConditionerSwitch',
method: 'GET',
payload
- }, true);
+ }, true));
}
// 车辆空调开关控制
export const airConditionerUnlock = async (payload: AirConditionerUnlockPayload): Promise<void> => {
- return request({
+ return withApiLog('空调开关控制', payload, request({
url: _ROBOTAXI_HOST + '/vehicleConvoy/api/v1/mini-program/order/airConditionerUnlock',
method: 'POST',
- payload
- }, true);
+ payload,
+ showToast: false
+ }, true));
}
// 查询后备箱开启状态
@@ -268,17 +270,6 @@
): Promise<SubmitFeedbackResponse> => {
return request({
url: _ROBOTAXI_HOST + `/order/api/v1/mini-program/order/comment`,
- method: 'POST',
- payload
- }, true);
-}
-
-// 提交订单投诉
-export const submitComplaint = async (
- payload: SubmitComplaintPayload
-): Promise<SubmitFeedbackResponse> => {
- return request({
- url: _ROBOTAXI_HOST + `/operation/base/v1/mini-program/complaint/submit`,
method: 'POST',
payload
}, true);
diff --git a/src/robotaxi/api/feedback.ts b/src/robotaxi/api/feedback.ts
new file mode 100644
index 0000000..8ff3d3e
--- /dev/null
+++ b/src/robotaxi/api/feedback.ts
@@ -0,0 +1,23 @@
+import request from '../../utils/request';
+import { _ROBOTAXI_HOST } from '../../constants/api';
+import type { SubmitComplaintPayload, SubmitFeedbackResponse } from './types/begin-route';
+import type { FeedbackTypeItem } from './types/feedback';
+
+// 提交订单投诉
+export const submitComplaint = async (
+ payload: SubmitComplaintPayload
+): Promise<SubmitFeedbackResponse> => {
+ return request({
+ url: _ROBOTAXI_HOST + `/operation/base/v1/mini-program/complaint/submit`,
+ method: 'POST',
+ payload
+ }, true);
+}
+
+// 获取意见反馈启用中的问题类型列表
+export const getFeedbackTypeList = async (): Promise<FeedbackTypeItem[]> => {
+ return request({
+ url: _ROBOTAXI_HOST + `/operation/base/v1/mini-program/feedback-type/getList`,
+ method: 'GET'
+ }, true);
+}
diff --git a/src/robotaxi/api/fragrance.ts b/src/robotaxi/api/fragrance.ts
new file mode 100644
index 0000000..d12bda1
--- /dev/null
+++ b/src/robotaxi/api/fragrance.ts
@@ -0,0 +1,31 @@
+import request from '../../utils/request';
+import { _ROBOTAXI_HOST } from '../../constants/api';
+import { withApiLog } from '../utils/api-logger';
+import type {
+ QueryCabinFragranceResponse,
+ SetCabinFragrancePayload
+} from './types/fragrance';
+
+// 查询车载香氛状态(总开关、当前香型、可用香型列表)
+export const queryCabinFragrance = async (
+ orderId: string
+): Promise<QueryCabinFragranceResponse> => {
+ const payload = { orderId };
+ return withApiLog('查询车载香氛状态', payload, request({
+ url: _ROBOTAXI_HOST + '/vehicleConvoy/base/v1/mini-program/cabin/fragrance',
+ method: 'GET',
+ payload
+ }, true));
+};
+
+// 设置车载香氛(聚合下发开关状态,开启时同时下发香型通道)
+export const setCabinFragrance = async (
+ payload: SetCabinFragrancePayload
+): Promise<void> => {
+ return withApiLog('设置车载香氛', payload, request({
+ url: _ROBOTAXI_HOST + '/vehicleConvoy/base/v1/mini-program/cabin/fragrance',
+ method: 'POST',
+ payload,
+ skipException: true
+ }, true));
+};
diff --git a/src/robotaxi/api/seat-hv.ts b/src/robotaxi/api/seat-hv.ts
new file mode 100644
index 0000000..785a021
--- /dev/null
+++ b/src/robotaxi/api/seat-hv.ts
@@ -0,0 +1,24 @@
+import request from '../../utils/request';
+import { _ROBOTAXI_HOST } from '../../constants/api';
+import { withApiLog } from '../utils/api-logger';
+import type { QuerySeatHvResponse, SetSeatHvPayload } from './types/seat-hv';
+
+// 查询座椅通风/加热状态
+export const querySeatHv = async (orderId: string): Promise<QuerySeatHvResponse> => {
+ const payload = { orderId };
+ return withApiLog('查询座椅通风加热状态', payload, request({
+ url: _ROBOTAXI_HOST + '/vehicleConvoy/base/v1/mini-program/cabin/seat/hv',
+ method: 'GET',
+ payload
+ }, true));
+};
+
+// 设置座椅通风/加热(seats 仅含被改动的座椅,通风与加热一起下发)
+export const setSeatHv = async (payload: SetSeatHvPayload): Promise<void> => {
+ return withApiLog('设置座椅通风加热', payload, request({
+ url: _ROBOTAXI_HOST + '/vehicleConvoy/base/v1/mini-program/cabin/seat/hv',
+ method: 'POST',
+ payload,
+ skipException: true
+ }, true));
+};
diff --git a/src/robotaxi/api/types/begin-route.ts b/src/robotaxi/api/types/begin-route.ts
index 7beb01e..738a732 100644
--- a/src/robotaxi/api/types/begin-route.ts
+++ b/src/robotaxi/api/types/begin-route.ts
@@ -384,15 +384,19 @@
/** 投诉提交请求。 */
export interface SubmitComplaintPayload {
/** 评价星级。 */
- orderRating: number;
+ orderRating?: number;
/** 反馈类型 ID,多个值使用英文逗号分隔。 */
feedbackTypeId?: string;
/** 反馈文案。 */
feedbackContent?: string;
- /** 反馈图片 JSON。 */
+ /** 反馈图片/视频 URL,多个值使用英文逗号分隔。 */
feedbackPictures?: string;
/** 订单 ID。 */
- orderId: OrderId;
+ orderId?: OrderId;
+ /** 反馈渠道:1=车内二维码,2=小程序"我的"入口。 */
+ feedbackChannel?: number;
+ /** 车辆 VIN,渠道 1(车内二维码)时必填。 */
+ vin?: string;
}
/** 评价/投诉提交响应。 */
diff --git a/src/robotaxi/api/types/feedback.ts b/src/robotaxi/api/types/feedback.ts
new file mode 100644
index 0000000..05ddf21
--- /dev/null
+++ b/src/robotaxi/api/types/feedback.ts
@@ -0,0 +1,9 @@
+import type { ExtendableRecord } from './begin-route';
+
+/** 意见反馈问题类型项。 */
+export interface FeedbackTypeItem extends ExtendableRecord {
+ /** 类型 ID。 */
+ id?: number | string;
+ /** 类型名称。 */
+ feedbackType?: string;
+}
diff --git a/src/robotaxi/api/types/fragrance.ts b/src/robotaxi/api/types/fragrance.ts
new file mode 100644
index 0000000..b09adb0
--- /dev/null
+++ b/src/robotaxi/api/types/fragrance.ts
@@ -0,0 +1,39 @@
+import type { OrderId } from './begin-route';
+
+/** 车载香氛单个香型。 */
+export interface FragranceScent {
+ /** 香型通道 ID,下发时使用此字段。 */
+ channelId: string;
+ /** 香型业务标识,与 currentScent 对应。 */
+ id: string;
+ /** 香型展示名称。 */
+ name: string;
+ /** 香调描述(可选,后端未下发时前端按 id 兜底)。 */
+ desc?: string;
+}
+
+/** 车载香氛状态对象。 */
+export interface CabinFragranceState {
+ /** 香氛总开关状态。 */
+ switchOn: boolean;
+ /** 当前香型的 id(关闭时可为空)。 */
+ currentScent?: string;
+ /** 车端可用香型列表,空数组表示不支持香型选择。 */
+ availableScents?: FragranceScent[];
+}
+
+/** 查询车载香氛状态响应对象。 */
+export interface QueryCabinFragranceResponse {
+ /** 香氛状态。 */
+ fragrance?: CabinFragranceState;
+}
+
+/** 设置车载香氛请求。 */
+export interface SetCabinFragrancePayload {
+ /** 订单 ID。 */
+ orderId: OrderId;
+ /** 目标开关状态。 */
+ switchOn: boolean;
+ /** 目标香型通道 ID;switchOn 为 true 时必传,false 时可不传。 */
+ channelId?: string;
+}
diff --git a/src/robotaxi/api/types/seat-hv.ts b/src/robotaxi/api/types/seat-hv.ts
new file mode 100644
index 0000000..0f8c233
--- /dev/null
+++ b/src/robotaxi/api/types/seat-hv.ts
@@ -0,0 +1,47 @@
+import type { OrderId } from './begin-route';
+
+/** 座椅位置标识(目前仅支持二排,前端兼容三排)。 */
+export type SeatPosition =
+ | 'SECOND_ROW_LEFT'
+ | 'SECOND_ROW_RIGHT'
+ | 'THIRD_ROW_LEFT'
+ | 'THIRD_ROW_RIGHT'
+ | string;
+
+/** 单个座椅的通风/加热状态。 */
+export interface SeatHvItem {
+ /** 座位标识。 */
+ position: SeatPosition;
+ /** 通风当前档位 0~N,0=关。 */
+ ventilation: number;
+ /** 通风支持的总档数;0 表示不支持通风。 */
+ ventilationLevels: number;
+ /** 加热当前档位 0~N,0=关。 */
+ heating: number;
+ /** 加热支持的总档数;0 表示不支持加热。 */
+ heatingLevels: number;
+}
+
+/** 查询座椅通风/加热状态响应对象。 */
+export interface QuerySeatHvResponse {
+ /** 座椅状态列表。 */
+ seats?: SeatHvItem[];
+}
+
+/** 单个座椅的目标设置。 */
+export interface SeatSetItem {
+ /** 座位标识。 */
+ position: SeatPosition;
+ /** 目标通风档位 0~N。 */
+ ventilationLevel: number;
+ /** 目标加热档位 0~N(互斥:与 ventilationLevel 至多一个非 0)。 */
+ heatingLevel: number;
+}
+
+/** 设置座椅通风/加热请求。 */
+export interface SetSeatHvPayload {
+ /** 订单 ID。 */
+ orderId: OrderId;
+ /** 本次改动的座椅集合(仅传被改动座椅)。 */
+ seats: SeatSetItem[];
+}
diff --git a/src/robotaxi/components/coupon-select-popup/index.less b/src/robotaxi/components/coupon-select-popup/index.less
index 0e8f59b..1ee210a 100644
--- a/src/robotaxi/components/coupon-select-popup/index.less
+++ b/src/robotaxi/components/coupon-select-popup/index.less
@@ -28,7 +28,7 @@
position: relative;
z-index: 2;
background: #ffffff;
- border-radius: 40px 40px 0 0;
+ border-radius: 60px 60px 0 0;
width: 100%;
height: 80vh;
display: flex;
@@ -121,13 +121,13 @@
font-family: PingFang SC;
font-size: 32px;
font-weight: 500;
- line-height: 32px;
- color: rgba(0, 0, 0, 0.9);
+ line-height: 48px;
+ color: #141E27;
}
.close-icon {
position: absolute;
- top: 48px;
+ top: 32px;
right: 32px;
width: 40px;
height: 40px;
@@ -138,7 +138,7 @@
display: flex;
justify-content: space-between;
align-items: center;
- padding-left: 42px;
+ padding-left: 32px;
padding-right: 32px;
.coupon-list-head-item {
font-family: PingFang SC;
@@ -148,8 +148,8 @@
display: flex;
align-items: center;
letter-spacing: normal;
- /* 文字色/黑色/重要字 90% */
- color: rgba(0, 0, 0, 0.9);
+ /* 文字色/重要字 */
+ color: #141E27;
}
.my-coupon-item {
font-family: PingFang SC;
@@ -174,7 +174,7 @@
flex: 1;
position: relative;
overflow: hidden;
- margin-top: 40px;
+ margin-top: 24px;
display: flex;
flex-direction: column;
@@ -208,7 +208,7 @@
.unavailable-toggle-text {
font-size: 24px;
- color: rgba(0, 0, 0, 0.4);
+ color: rgba(20, 30, 39, 0.4);
}
.unavailable-toggle-arrow {
@@ -276,11 +276,11 @@
.coupon-item {
position: relative;
width: 100%;
- min-height: 184px;
- border-radius: 24px;
- background: url("https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/coupon-bg.png") center center / 100% 100%
+ min-height: 190px;
+ background: url("https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/coupon-item-bg-0713.png") center center / 100% 100%
no-repeat;
- padding: 28px 24px 28px 28px;
+ padding-left: 28px;
+ padding-right: 24px;
margin-bottom: 0;
display: flex;
align-items: center;
@@ -296,7 +296,12 @@
}
.coupon-price-box {
- padding-right: 24px;
+ flex-shrink: 0;
+ width: 140px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
}
.coupon-price {
@@ -304,14 +309,13 @@
justify-content: center;
align-items: center;
flex-shrink: 0;
- width: 142px;
}
.price {
font-size: 48px;
line-height: 64px;
- font-weight: 600;
- color: #f58900;
+ font-weight: 500;
+ color: #A68042;
}
.unit {
@@ -319,8 +323,8 @@
font-size: 24px;
font-weight: 500;
line-height: 24px;
- color: #f58900;
- margin-left: 2px;
+ color: #A68042;
+ margin-left: 4px;
margin-top: 14px;
}
@@ -330,8 +334,7 @@
font-weight: normal;
line-height: 36px;
letter-spacing: normal;
- /* 功能色/提示色/提示色1 */
- color: #f58900;
+ color: #A68042;
text-align: center;
}
@@ -346,9 +349,9 @@
.coupon-info {
.name {
font-size: 32px;
- font-weight: 600;
+ font-weight: 500;
line-height: 48px;
- color: rgba(0, 0, 0, 0.9);
+ color: #A68042;
margin-bottom: 4px;
}
@@ -359,7 +362,7 @@
display: flex;
align-items: center;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
+ color: #A68042;
}
.more {
@@ -374,7 +377,7 @@
display: flex;
align-items: center;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.4);
+ color: rgba(20, 30, 39, 0.4);
}
.more-img {
diff --git a/src/robotaxi/components/coupon-select-popup/index.tsx b/src/robotaxi/components/coupon-select-popup/index.tsx
index 33591b6..dd58715 100644
--- a/src/robotaxi/components/coupon-select-popup/index.tsx
+++ b/src/robotaxi/components/coupon-select-popup/index.tsx
@@ -212,7 +212,7 @@
<View className="coupon-popup-content" animation={animationData}>
<View className="coupon-header">
<Text className="coupon-title">选择优惠券</Text>
- <Image className="close-icon" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/close-icon.png" onClick={onClose} />
+ <Image className="close-icon" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/close-icon.png" onClick={onClose} />
</View>
<View className="coupon-list-wrap">
<View className="coupon-list-head">
@@ -273,7 +273,7 @@
? "expanded"
: ""
}`}
- src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/coupon-arrow.png"
+ src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/arrow-down-gray.svg"
></Image>
</View>
)}
@@ -283,8 +283,8 @@
className="select-img"
src={
activeCouponId === item.operationCouponId
- ? 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/coupon-selected.png'
- : 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/coupon-select.png'
+ ? 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/coupon-item-selected.svg'
+ : 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/coupon-item-unselect.svg'
}
></Image>
</View>
@@ -382,14 +382,14 @@
? "expanded"
: ""
}`}
- src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/coupon-arrow.png"
+ src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/arrow-down-gray.svg"
></Image>
</View>
</View>
<View className="select-box">
<Image
className="select-img"
- src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/coupon-select.png"
+ src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/coupon-item-unselect.svg"
></Image>
</View>
</View>
diff --git a/src/robotaxi/components/demo/index.less b/src/robotaxi/components/demo/index.less
index 1b67616..98d7fb9 100644
--- a/src/robotaxi/components/demo/index.less
+++ b/src/robotaxi/components/demo/index.less
@@ -12,4 +12,22 @@
font-size: 28px;
color: #bdcbdf;
}
+
+ /* TODO(仅用于测试): 上线前删除以下 Toast Demo 按钮样式 */
+ .toast-demo {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 20rpx;
+ padding: 24rpx;
+
+ &__btn {
+ padding: 16rpx 32rpx;
+ border-radius: 12rpx;
+ background: #141e27;
+ color: #ffffff;
+ font-size: 28rpx;
+ line-height: 40rpx;
+ }
+ }
}
diff --git a/src/robotaxi/components/demo/index.tsx b/src/robotaxi/components/demo/index.tsx
index 60a5029..eb1ff3e 100644
--- a/src/robotaxi/components/demo/index.tsx
+++ b/src/robotaxi/components/demo/index.tsx
@@ -8,6 +8,8 @@
// TODO(仅用于测试): 上线前删除以下修改目的地卡片测试相关引入及 DEMO_MODIFY_* 数据
import ModifyDestinationReview from '../../pages/begin-route/components/modify-destination-review';
import { TripStatus } from '../../constants/trip-status';
+// TODO(仅用于测试): 上线前删除以下通用 Toast 组件的 Demo 引入
+import { showToast, hideToast } from '../toast';
const DEMO_ORDER_ID = '2063814874050772993';
@@ -61,6 +63,55 @@
render () {
return (
<View className='loading'>
+ {/* TODO(仅用于测试): 上线前删除以下通用 Toast 组件 Demo 区块 */}
+ <Text>通用 Toast Demo</Text>
+ <View className='toast-demo'>
+ <View
+ className='toast-demo__btn'
+ onClick={() => showToast({ title: '协议获取失败,请重试', icon: 'none' })}
+ >
+ 无图标
+ </View>
+ <View
+ className='toast-demo__btn'
+ onClick={() => showToast({ title: '切换新路线成功', icon: 'success' })}
+ >
+ 成功
+ </View>
+ <View
+ className='toast-demo__btn'
+ onClick={() => showToast({ title: '提交失败,请检查网络后重试', icon: 'fail' })}
+ >
+ 失败
+ </View>
+ <View
+ className='toast-demo__btn'
+ onClick={() => showToast({ title: '提交失败,请检查网络后重试', icon: 'warning' })}
+ >
+ 告警
+ </View>
+ <View
+ className='toast-demo__btn'
+ onClick={() => showToast({ title: '加载中', icon: 'loading' })}
+ >
+ 加载中
+ </View>
+ <View
+ className='toast-demo__btn'
+ onClick={() =>
+ showToast({
+ title: '提交失败,请检查网络后重试提交失败,请检查网络后重试',
+ icon: 'none'
+ })
+ }
+ >
+ 两行文本
+ </View>
+ <View className='toast-demo__btn' onClick={() => hideToast()}>
+ 关闭 Toast
+ </View>
+ </View>
+
<Text>空调卡片 Demo</Text>
{/* TODO(仅用于测试): 上线前删除 TemperatureControl 测试渲染,.calling-car 包裹仅用于补全样式层级 */}
<View className='calling-car'>
diff --git a/src/robotaxi/components/tip-popup/index.less b/src/robotaxi/components/tip-popup/index.less
index 33a708d..a55817e 100644
--- a/src/robotaxi/components/tip-popup/index.less
+++ b/src/robotaxi/components/tip-popup/index.less
@@ -20,38 +20,55 @@
.tip-content {
position: relative;
- border-radius: 32px;
+ width: 600rpx;
+ height: 388rpx;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 72rpx 40rpx 48rpx 40rpx;
+ gap: 54rpx;
+ border-radius: 48rpx;
background: #FFFFFF;
- padding: 96px 69px 56px 69px;
- margin: 0 75px;
.close-icon {
position: absolute;
- top: 32px;
- right: 32px;
- width: 40px;
- height: 40px;
+ top: 24rpx;
+ right: 24rpx;
+ width: 48rpx;
+ height: 48rpx;
}
.tip-title {
- font-size: 36px;
- font-weight: bold;
- line-height: 52px;
+ font-family: PingFang SC;
+ font-size: 36rpx;
+ font-weight: 500;
+ line-height: 52rpx;
text-align: center;
- color: rgba(0, 0, 0, 0.9);
+ letter-spacing: normal;
+ color: #141E27;
}
.reselect-btn {
- margin-top: 96px;
- height: 96px;
+ width: 404rpx;
+ height: 110rpx;
+ box-sizing: border-box;
display: flex;
+ flex-direction: column;
justify-content: center;
align-items: center;
- border-radius: 16px;
- background: linear-gradient(95deg, #8475FF 0%, #7DD6FF 124%);
- font-size: 28px;
+ padding: 32rpx 80rpx;
+ gap: 16rpx;
+ z-index: 0;
+ border-radius: 1000rpx;
+ border: 4rpx solid #141E27;
+ background: #FFFFFF;
+ font-family: PingFang SC;
+ font-size: 36rpx;
+ font-weight: 500;
+ line-height: 52rpx;
letter-spacing: normal;
- color: #FFFFFF;
+ color: #141E27;
}
}
}
diff --git a/src/robotaxi/components/toast/README.md b/src/robotaxi/components/toast/README.md
new file mode 100644
index 0000000..30220eb
--- /dev/null
+++ b/src/robotaxi/components/toast/README.md
@@ -0,0 +1,86 @@
+# 通用 Toast 组件
+
+轻量级全局提示组件,使用方式与 `Taro.showToast` 类似,支持 **5 种样式** 和 **一行/两行文本**。
+
+## 支持的样式(icon)
+
+| icon 值 | 说明 | 图标 |
+| --- | --- | --- |
+| `success` | 成功 | toast-icon-check |
+| `fail` | 失败 | toast-icon-close |
+| `warning` | 告警 | toast-icon-error |
+| `loading` | 加载中(图标旋转,默认不自动关闭) | toast-icon-loading |
+| `none` | 无图标(纯文本,默认值) | — |
+
+## 使用步骤
+
+> ⚠️ 小程序架构下每个页面是独立渲染树,App 组件不产生常驻 UI,**没有全局根组件**可以只挂载一次供所有页面共用。因此需要在**每个用到 Toast 的页面**挂载一次容器。提供两种方式:
+
+### 方式一(推荐):用 withToast HOC 一行接入
+
+与现有 `withRobotaxiWhitelist` 组合,页面无需在 JSX 里手写 `<Toast />`:
+
+```tsx
+import { withRobotaxiWhitelist } from '../../../utils/whitelist';
+import { withToast } from '../../components/toast/with-toast';
+
+class MyPage extends Component { /* ... */ }
+
+export default withRobotaxiWhitelist(withToast(MyPage as any) as any);
+```
+
+### 方式二:在页面 JSX 中手动挂载容器
+
+```tsx
+import Toast from '@/robotaxi/components/toast';
+
+function Page() {
+ return (
+ <View>
+ {/* 页面其他内容 */}
+ <Toast />
+ </View>
+ );
+}
+```
+
+### 命令式调用(两种方式接入后都一样)
+
+```tsx
+import { showToast, hideToast } from '@/robotaxi/components/toast';
+
+// 无图标(与 Taro.showToast 用法一致)
+showToast({ title: '协议获取失败,请重试', icon: 'none' });
+
+// 成功
+showToast({ title: '切换新路线成功', icon: 'success' });
+
+// 失败
+showToast({ title: '提交失败,请检查网络后重试', icon: 'fail' });
+
+// 告警
+showToast({ title: '提交失败,请检查网络后重试', icon: 'warning' });
+
+// 加载中(默认不自动关闭,需手动隐藏)
+showToast({ title: '加载中', icon: 'loading' });
+// ...异步完成后
+hideToast();
+
+// 两行文本会自动换行
+showToast({ title: '提交失败,请检查网络后重试提交失败,请检查网络后重试', icon: 'none' });
+```
+
+## API
+
+### showToast(options)
+
+| 参数 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| `title` | `string` | — | 提示文本,支持一行 / 两行 |
+| `icon` | `'success' \| 'fail' \| 'warning' \| 'loading' \| 'none'` | `'none'` | 图标类型 |
+| `duration` | `number` | `1500`(loading 为 `0`) | 自动关闭时长(ms),`0` 表示不自动关闭 |
+| `mask` | `boolean` | `false` | 是否显示透明蒙层,阻止穿透点击 |
+
+### hideToast()
+
+手动关闭当前 Toast(loading 场景常用)。
diff --git a/src/robotaxi/components/toast/index.less b/src/robotaxi/components/toast/index.less
new file mode 100644
index 0000000..a4f006b
--- /dev/null
+++ b/src/robotaxi/components/toast/index.less
@@ -0,0 +1,73 @@
+.universal-toast {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 99999;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ // 默认不拦截页面点击,仅在开启 mask 时由蒙层拦截
+ pointer-events: none;
+
+ &__mask {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: transparent;
+ pointer-events: auto;
+ }
+
+ &__box {
+ max-width: 560rpx;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ padding: 24rpx 40rpx;
+ border-radius: 16rpx;
+ background: rgba(0, 0, 0, 0.8);
+ pointer-events: auto;
+
+ // 无图标:纯文本样式,横向内边距略小
+ &.text-only {
+ padding: 24rpx 32rpx;
+ }
+ }
+
+ &__icon {
+ flex-shrink: 0;
+ width: 40rpx;
+ height: 40rpx;
+ margin-right: 12rpx;
+
+ // 加载中图标旋转动画
+ &.is-loading {
+ animation: universal-toast-spin 0.8s linear infinite;
+ }
+ }
+
+ &__title {
+ font-family: PingFang SC;
+ font-size: 28rpx;
+ font-weight: 400;
+ line-height: 40rpx;
+ text-align: center;
+ color: #ffffff;
+ // 支持一行 / 两行文本自动换行
+ word-break: break-all;
+ }
+}
+
+@keyframes universal-toast-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
diff --git a/src/robotaxi/components/toast/index.tsx b/src/robotaxi/components/toast/index.tsx
new file mode 100644
index 0000000..a94e17b
--- /dev/null
+++ b/src/robotaxi/components/toast/index.tsx
@@ -0,0 +1,161 @@
+import { View, Image, Text } from '@tarojs/components';
+import { useEffect, useState } from 'react';
+import './index.less';
+
+/**
+ * 通用 Toast 组件(单例模式)
+ *
+ * 全局仅存在一个 ToastManager 实例统一管理提示状态与计时器,
+ * 无论调用多少次、页面中挂载多少个 <Toast /> 容器,同一时刻只会展示一条提示。
+ *
+ * 使用方式类似 Taro.showToast:
+ * import { showToast, hideToast } from '@/robotaxi/components/toast';
+ * showToast({ title: '协议获取失败,请重试', icon: 'none' });
+ *
+ * ⚠️ 需要在页面(或公共布局根组件)中挂载一次 <Toast /> 容器:
+ * import Toast from '@/robotaxi/components/toast';
+ * <Toast />
+ */
+
+// 5 种图标样式:成功、失败、告警、加载中、无图标
+export type ToastIcon = 'success' | 'fail' | 'warning' | 'loading' | 'none';
+
+export interface ToastOptions {
+ /** 提示文本,支持一行或两行 */
+ title: string;
+ /** 图标类型,默认 none */
+ icon?: ToastIcon;
+ /**
+ * 自动关闭时长(ms)。默认 1500;
+ * loading 默认不自动关闭(0),需手动调用 hideToast()
+ */
+ duration?: number;
+ /** 是否显示透明蒙层,阻止穿透点击,默认 false */
+ mask?: boolean;
+}
+
+/** 内部状态:null 表示当前无提示 */
+type ToastState = Required<Omit<ToastOptions, 'duration'>> | null;
+
+type Listener = (state: ToastState) => void;
+
+// OSS 图标资源
+const ICON_MAP: Record<Exclude<ToastIcon, 'none'>, string> = {
+ success: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/toast-icon-check-0727@2x.png.png',
+ fail: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/toast-icon-close-0727@2x.png.png',
+ warning: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/toast-icon-error-0727@2x.png',
+ loading: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/toast-icon-loading-0727@2x.png'
+};
+
+const DEFAULT_DURATION = 1500;
+
+/**
+ * Toast 单例管理器:全局唯一,统一维护提示状态与自动关闭计时器。
+ * 所有挂载的 <Toast /> 容器订阅同一状态,保证同一时刻只展示一条提示。
+ */
+class ToastManager {
+ private static instance: ToastManager | null = null;
+
+ private listeners = new Set<Listener>();
+ private timer: ReturnType<typeof setTimeout> | null = null;
+ private state: ToastState = null;
+
+ static getInstance(): ToastManager {
+ if (!ToastManager.instance) {
+ ToastManager.instance = new ToastManager();
+ }
+ return ToastManager.instance;
+ }
+
+ subscribe(listener: Listener): () => void {
+ this.listeners.add(listener);
+ // 新容器挂载时同步当前状态
+ listener(this.state);
+ return () => {
+ this.listeners.delete(listener);
+ };
+ }
+
+ private clearTimer() {
+ if (this.timer) {
+ clearTimeout(this.timer);
+ this.timer = null;
+ }
+ }
+
+ private emit() {
+ this.listeners.forEach((l) => l(this.state));
+ }
+
+ show(options: ToastOptions) {
+ this.clearTimer();
+ const icon = options.icon || 'none';
+ this.state = {
+ title: options.title,
+ icon,
+ mask: options.mask ?? false
+ };
+ this.emit();
+
+ // loading 默认不自动关闭,其余默认 1500ms
+ const duration =
+ typeof options.duration === 'number'
+ ? options.duration
+ : icon === 'loading'
+ ? 0
+ : DEFAULT_DURATION;
+
+ if (duration > 0) {
+ this.timer = setTimeout(() => this.hide(), duration);
+ }
+ }
+
+ hide() {
+ this.clearTimer();
+ this.state = null;
+ this.emit();
+ }
+}
+
+const manager = ToastManager.getInstance();
+
+/** 显示 Toast */
+export function showToast(options: ToastOptions) {
+ manager.show(options);
+}
+
+/** 隐藏 Toast */
+export function hideToast() {
+ manager.hide();
+}
+
+export default function Toast() {
+ const [state, setState] = useState<ToastState>(null);
+
+ useEffect(() => {
+ const unsubscribe = manager.subscribe(setState);
+ return unsubscribe;
+ }, []);
+
+ if (!state) {
+ return null;
+ }
+
+ const { title, icon, mask } = state;
+ const hasIcon = icon !== 'none' && !!ICON_MAP[icon];
+
+ return (
+ <View className='universal-toast'>
+ {mask && <View className='universal-toast__mask' catchMove />}
+ <View className={`universal-toast__box ${hasIcon ? 'has-icon' : 'text-only'}`}>
+ {hasIcon && (
+ <Image
+ className={`universal-toast__icon ${icon === 'loading' ? 'is-loading' : ''}`}
+ src={ICON_MAP[icon]}
+ />
+ )}
+ <Text className='universal-toast__title'>{title}</Text>
+ </View>
+ </View>
+ );
+}
diff --git a/src/robotaxi/components/toast/with-toast.tsx b/src/robotaxi/components/toast/with-toast.tsx
new file mode 100644
index 0000000..4de96d4
--- /dev/null
+++ b/src/robotaxi/components/toast/with-toast.tsx
@@ -0,0 +1,36 @@
+import { ComponentType } from 'react';
+import Toast from './index';
+
+/**
+ * withToast —— 页面级 Toast 接入高阶组件。
+ *
+ * 背景:小程序架构下每个页面是独立渲染树,App 组件的 render 不产生常驻 UI,
+ * 因此没有「全局根组件」可以只挂载一次 <Toast /> 供所有页面共用。
+ * 该 HOC 用于让需要 Toast 的页面「一行接入」,自动在页面渲染树里挂载单例容器。
+ *
+ * Toast 的状态由全局单例 ToastManager 统一管理,此处仅负责在当前页面挂载渲染容器,
+ * 不会造成多份状态;不同页面各自挂载互不影响,同屏只展示当前页面的浮层。
+ *
+ * 用法(与现有 withRobotaxiWhitelist 组合):
+ * import { withRobotaxiWhitelist } from '../../../utils/whitelist';
+ * import { withToast } from '../../components/toast/with-toast';
+ *
+ * class MyPage extends Component { ... }
+ * export default withRobotaxiWhitelist(withToast(MyPage as any) as any);
+ *
+ * 之后页面内任意位置直接调用 showToast / hideToast 即可:
+ * import { showToast } from '../../components/toast';
+ * showToast({ title: '协议获取失败,请重试', icon: 'none' });
+ */
+export function withToast<P extends object>(WrappedComponent: ComponentType<P>): ComponentType<P> {
+ return function WithToast(props: P) {
+ return (
+ <>
+ <WrappedComponent {...props} />
+ <Toast />
+ </>
+ );
+ };
+}
+
+export default withToast;
diff --git a/src/robotaxi/hooks/use-image-upload.ts b/src/robotaxi/hooks/use-image-upload.ts
index e82b86d..ff6dc10 100644
--- a/src/robotaxi/hooks/use-image-upload.ts
+++ b/src/robotaxi/hooks/use-image-upload.ts
@@ -1,4 +1,5 @@
import Taro from '@tarojs/taro';
+import { showToast } from '../components/toast';
import { useCallback, useMemo, useState } from 'react';
/**
@@ -158,7 +159,7 @@
const chooseAndUpload = useCallback(async () => {
const remainCount = Math.max(0, options.maxCount - images.length);
if (remainCount <= 0) {
- Taro.showToast({
+ showToast({
title: `最多可上传${options.maxCount}张图片`,
icon: 'none'
});
diff --git a/src/robotaxi/pages/begin-route/components/auth-bluetooth-popup/index.less b/src/robotaxi/pages/begin-route/components/auth-bluetooth-popup/index.less
index 671e612..4dd19ea 100644
--- a/src/robotaxi/pages/begin-route/components/auth-bluetooth-popup/index.less
+++ b/src/robotaxi/pages/begin-route/components/auth-bluetooth-popup/index.less
@@ -1,3 +1,5 @@
+@import url('../../../../style/ui-variables.less');
+
.auth-bluetooth-popup {
position: fixed;
top: 0;
@@ -20,17 +22,15 @@
left: 0;
width: 100%;
height: 100%;
- background: rgba(0, 0, 0, 0.4);
+ background: rgba(0, 0, 0, 0.8);
z-index: 1;
}
.popup-content {
position: relative;
z-index: 2;
- background: #FFFFFF;
- border-radius: 40px 40px 0 0;
- background: linear-gradient(180deg, #D9EBFF 0%, #FFFFFF 35%);
- border-radius: 40px 40px 0 0;
+ background: @gary-theme;
+ border-radius: 60px 60px 0 0;
width: 100%;
transform: translateY(100%);
opacity: 1;
@@ -58,7 +58,7 @@
.content-main {
position: relative;
z-index: 2;
- padding: 80px 58px 80px 40px;
+ padding: 96px 48px;
display: flex;
flex-direction: column;
}
@@ -67,33 +67,35 @@
font-size: 36px;
font-weight: 500;
line-height: 52px;
- color: rgba(0, 0, 0, 0.9);
+ color: @black-text-main;
}
.popup-sub-title {
font-size: 36px;
font-weight: 500;
line-height: 52px;
- color: rgba(0, 0, 0, 0.9);
+ color: @black-text-main;
}
.popup-desc {
font-size: 28px;
line-height: 44px;
- color: rgba(0, 0, 0, 0.6);
+ color: @black-text-04;
}
.authorize-btn {
- margin-top: 82px;
+ margin-top: 80px;
width: 100%;
- height: 88px;
- margin-left: 18px;
- border-radius: 16px;
- background: linear-gradient(90deg, #7b79ff 0%, #68c0ff 100%);
+ height: 106px;
+ box-sizing: border-box;
+ border-radius: 2000px;
+ background: @black-text-main;
display: flex;
align-items: center;
justify-content: center;
- font-size: 28px;
+ text-align: center;
+ font-size: 32px;
+ font-weight: 500;
color: #ffffff;
}
-}
\ No newline at end of file
+}
diff --git a/src/robotaxi/pages/begin-route/components/auth-bluetooth-popup/success-auth/index.less b/src/robotaxi/pages/begin-route/components/auth-bluetooth-popup/success-auth/index.less
index 1a4e235..1f4c1ad 100644
--- a/src/robotaxi/pages/begin-route/components/auth-bluetooth-popup/success-auth/index.less
+++ b/src/robotaxi/pages/begin-route/components/auth-bluetooth-popup/success-auth/index.less
@@ -1,3 +1,5 @@
+@import url('../../../../../style/ui-variables.less');
+
.success-auth-popup {
position: fixed;
top: 0;
@@ -20,7 +22,7 @@
left: 0;
width: 100%;
height: 100%;
- background: rgba(0, 0, 0, 0.4);
+ background: rgba(0, 0, 0, 0.8);
z-index: 1;
}
@@ -28,19 +30,19 @@
position: relative;
z-index: 2;
width: 100%;
- background: linear-gradient(180deg, #D9EBFF 0%, #FFFFFF 35%);
- border-radius: 40px 40px 0 0;
+ background: @gary-theme;
+ border-radius: 60px 60px 0 0;
transform: translateY(100%);
opacity: 1;
- padding: 58px 134px 80px 140px;
+ padding: 96px 120px;
display: flex;
flex-direction: column;
align-items: center;
}
.popup-icon {
- width: 148px;
- height: 148px;
+ width: 144px;
+ height: 144px;
}
.popup-title {
@@ -48,7 +50,7 @@
font-size: 34px;
font-weight: 500;
line-height: 52px;
- color: rgba(0, 0, 0, 0.9);
+ color: @black-text-main;
letter-spacing: normal;
}
@@ -56,20 +58,21 @@
margin-top: 4px;
font-size: 24px;
line-height: 36px;
- color: rgba(0, 0, 0, 0.6);
+ color: @black-text-04;
text-align: center;
}
.popup-confirm-btn {
margin-top: 66px;
width: 100%;
- height: 88px;
- border-radius: 16px;
- border: 2px solid rgba(0, 0, 0, 1);
+ height: 106px;
+ border-radius: 2000px;
+ border: 4px solid @black-text-main;
display: flex;
align-items: center;
justify-content: center;
- font-size: 28px;
- color: rgba(0, 0, 0, 0.9);
+ font-size: 32px;
+ font-weight: 500;
+ color: @black-text-main;
}
-}
\ No newline at end of file
+}
diff --git a/src/robotaxi/pages/begin-route/components/auth-bluetooth-popup/success-auth/index.tsx b/src/robotaxi/pages/begin-route/components/auth-bluetooth-popup/success-auth/index.tsx
index 9c633b1..75ee69e 100644
--- a/src/robotaxi/pages/begin-route/components/auth-bluetooth-popup/success-auth/index.tsx
+++ b/src/robotaxi/pages/begin-route/components/auth-bluetooth-popup/success-auth/index.tsx
@@ -22,7 +22,7 @@
<View className={`success-auth-popup ${show ? 'show' : ''}`} catchMove>
<View className='popup-mask' onClick={onClose}></View>
<View className='popup-content' animation={animationData}>
- <Image className='popup-icon' src={'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/bluetooth-success-auth.png'} />
+ <Image className='popup-icon' src={'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/bluetooth-popup-icon-20260726.png'} />
<Text className='popup-title'>已授权蓝牙,无感自动解锁车门</Text>
<Text className='popup-desc'>授权并打开蓝牙连接,手机靠近车辆即可自动解锁车门</Text>
<View className='popup-confirm-btn' onClick={handleConfirm}>
diff --git a/src/robotaxi/pages/begin-route/components/begin-route-view.tsx b/src/robotaxi/pages/begin-route/components/begin-route-view.tsx
index 02d26cb..e3ff4d5 100644
--- a/src/robotaxi/pages/begin-route/components/begin-route-view.tsx
+++ b/src/robotaxi/pages/begin-route/components/begin-route-view.tsx
@@ -63,23 +63,21 @@
backIcon,
onPersonalCenterEntry,
onServiceEntry,
- onCouponEntry,
personalCenter,
contactService,
- coupon,
} = useBeginRouteViewContext();
return (
<>
- <View onClick={toBack} className='select-city'>
- <Image className='select-city-icon' src={backIcon} />
+ <View onClick={toBack} className='back-icon-box'>
+ <Image className='back-icon' src={backIcon} />
</View>
<View className='map-entry'>
- <Image onClick={onPersonalCenterEntry} className='map-entry-icon' src={personalCenter} />
- <View className='split-line'></View>
- <Image onClick={onServiceEntry} className='map-entry-icon' src={contactService} />
- {/* <View className='split-line'></View>
- <Image onClick={onCouponEntry} className='map-entry-icon' src={coupon} /> */}
+ <View className="map-entry-box">
+ <Image onClick={onPersonalCenterEntry} className='map-entry-icon' src={personalCenter} />
+ <View className='split-line'></View>
+ <Image onClick={onServiceEntry} className='map-entry-icon' src={contactService} />
+ </View>
</View>
</>
);
@@ -109,6 +107,40 @@
return (
<View className='container'>
<View className='container-inner'>
+ {(canToggleRouteSelect || showWalkNavigationButton) && (
+ <View
+ className='select-route-box'
+ style={{
+ opacity: originBoxHidden ? 0 : originOpacity,
+ transition: 'opacity 0.2s ease-out',
+ pointerEvents: originBoxHidden ? 'none' : 'auto'
+ }}
+ >
+ {
+ showWalkNavigationButton && (
+ <Image
+ onClick={handleWalkNavigation}
+ style={{ marginBottom: canToggleRouteSelect ? '8px' : '0px' }}
+ className='walk-icon'
+ src={walkIcon}
+ />
+ )
+ }
+ {
+ canToggleRouteSelect && (
+ <View className='route-icon-box'>
+ {
+ isRouteSelectExpanded ? (
+ <Image onClick={onSelectRouteClick} className='select-route-icon' src={packUpIcon} />
+ ) : (
+ <Image onClick={onSelectRouteClick} className='select-route-icon' src={selectRouteIcon} />
+ )
+ }
+ </View>
+ )
+ }
+ </View>
+ )}
{shouldShowOrigin && (
<View
className='origin-box'
@@ -118,24 +150,7 @@
pointerEvents: originBoxHidden ? 'none' : 'auto'
}}
>
- {showWalkNavigationButton && (
- <Image
- onClick={handleWalkNavigation}
- style={{ marginBottom: '8px' }}
- className='origin-icon'
- src={walkIcon}
- />
- )}
<Image onClick={toOrigin} className='origin-icon' src={originIcon} />
- {canToggleRouteSelect && (
- <View>
- {isRouteSelectExpanded ? (
- <Image onClick={onSelectRouteClick} className='select-route-icon' src={packUpIcon} />
- ) : (
- <Image onClick={onSelectRouteClick} className='select-route-icon' src={selectRouteIcon} />
- )}
- </View>
- )}
</View>
)}
<View catchMove onClick={(event) => event.stopPropagation()}>
diff --git a/src/robotaxi/pages/begin-route/components/calling-car-modules/ac-switch/index.less b/src/robotaxi/pages/begin-route/components/calling-car-modules/ac-switch/index.less
new file mode 100644
index 0000000..29854ff
--- /dev/null
+++ b/src/robotaxi/pages/begin-route/components/calling-car-modules/ac-switch/index.less
@@ -0,0 +1,21 @@
+@keyframes ac-switch-spin {
+ from {
+ transform: rotate(0deg);
+ }
+
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.ac-switch {
+ flex-shrink: 0;
+ width: 104px;
+ height: 64px;
+}
+
+.ac-switch-loading {
+ width: 64px;
+ height: 64px;
+ animation: ac-switch-spin 1s linear infinite;
+}
diff --git a/src/robotaxi/pages/begin-route/components/calling-car-modules/ac-switch/index.tsx b/src/robotaxi/pages/begin-route/components/calling-car-modules/ac-switch/index.tsx
new file mode 100644
index 0000000..242e92f
--- /dev/null
+++ b/src/robotaxi/pages/begin-route/components/calling-car-modules/ac-switch/index.tsx
@@ -0,0 +1,31 @@
+import { Image } from '@tarojs/components';
+import './index.less';
+
+// 图片开关三态图标(复用入口卡片同款)
+const SWITCH_OPEN_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/T-switch-open-20260709.png';
+const SWITCH_CLOSE_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/T-switch-close-20260709.png';
+const SWITCH_LOADING_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/T-switch-loading-20260715.png';
+
+type Props = {
+ /** 开关状态 */
+ checked: boolean;
+ /** 是否处于切换 loading 中 */
+ loading?: boolean;
+ /** 点击回调 */
+ onChange?: () => void;
+ /** 自定义 class */
+ className?: string;
+};
+
+/**
+ * 空调风格图片开关(开/关/loading 三态),供卡片与面板统一复用。
+ */
+export default function AcSwitch({ checked, loading = false, onChange, className = '' }: Props) {
+ return (
+ <Image
+ className={`ac-switch${loading ? ' ac-switch-loading' : ''} ${className}`.trim()}
+ src={loading ? SWITCH_LOADING_ICON : (checked ? SWITCH_OPEN_ICON : SWITCH_CLOSE_ICON)}
+ onClick={(e: any) => { e?.stopPropagation?.(); onChange && onChange(); }}
+ />
+ );
+}
diff --git a/src/robotaxi/pages/begin-route/components/calling-car-modules/bluetooth-operate-card.tsx b/src/robotaxi/pages/begin-route/components/calling-car-modules/bluetooth-operate-card.tsx
index ed770f4..d26ef6c 100644
--- a/src/robotaxi/pages/begin-route/components/calling-car-modules/bluetooth-operate-card.tsx
+++ b/src/robotaxi/pages/begin-route/components/calling-car-modules/bluetooth-operate-card.tsx
@@ -27,8 +27,8 @@
</View>
{shouldRenderRight && (
<View className="bluetooth-operate-card-right">
- {!!actionText && <View>{actionText}</View>}
- <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/blue-right-icon.png" />
+ {/* {!!actionText && <View>{actionText}</View>} */}
+ <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/blue-right-icon-20260706.png" />
</View>
)}
</View>
diff --git a/src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx b/src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx
index f514cfc..56ccecc 100644
--- a/src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx
+++ b/src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx
@@ -4,13 +4,15 @@
import { unlockVehicle, queryVehicleTrunkOpenStatus } from '../../../../api/begin-route';
import { getOrderId } from '../../../../utils/order';
import { TripStatus } from '../../../../constants/trip-status';
+import { showToast } from '../../../../components/toast';
import useMinimumSpinLoading from './use-minimum-spin-loading';
+import TrunkConfirmPopup from './trunk-confirm-popup';
-const LOCK_CLOSE_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/lock-close.png';
-const LOCK_OPEN_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/lock-open.png';
-const TRUNK_CLOSE_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/trunk-close.png';
-const TRUNK_OPEN_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/trunk-open.png';
-const SWITCH_LOADING_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/T-switch-loading-v3.png';
+const LOCK_CLOSE_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/lock-close-20260726.png';
+const LOCK_OPEN_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/lock-open-20260726.png';
+const TRUNK_CLOSE_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/trunk-close-20260727.png';
+const TRUNK_OPEN_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/trunk-open-20260727.png';
+const SWITCH_LOADING_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/T-switch-loading-20260706.png';
type Props = {
tripStatus: TripStatus;
@@ -33,6 +35,7 @@
}: Props) {
const [trunkStatus, setTrunkStatus] = useState(orderTrunkStatus); // 后备箱开关状态
const [lockStatus, setLockStatus] = useState(false); // 车门开关状态
+ const [trunkConfirmVisible, setTrunkConfirmVisible] = useState(false); // 后备箱开启二次确认弹窗
const trunkPollTimerRef = useRef<any>(null); // 后备箱状态轮询定时器
const overRoutePopupDebounceTimerRef = useRef<any>(null); // 结束行程弹窗防抖定时器
const OVER_ROUTE_POPUP_DEBOUNCE_MS = 1000; // 结束行程弹窗点击防抖时长
@@ -127,13 +130,13 @@
return;
}
if (doorType === 'door' && lockStatus) {
- Taro.showToast({ title: '车门已解锁,请开门上车', icon: 'none' });
+ showToast({ title: '车门已解锁,请开门上车', icon: 'none' });
return;
}
const locationPayload = await getUnlockLocationPayload();
if (!locationPayload) {
- Taro.showToast({ title: '获取定位失败,请打开定位开关', icon: 'none' });
+ showToast({ title: '获取定位失败,请打开定位开关', icon: 'none' });
return;
}
startLoading(doorType);
@@ -155,8 +158,10 @@
: !!res?.data;
if (!unlockSucceeded) {
// Taro.hideLoading();
- stopLoading(doorType);
- Taro.showToast({ title: res?.msg || '解锁失败,请重试或联系客服', icon: 'none' });
+ const failMsg = res?.msg || '解锁失败,请重试或联系客服';
+ stopLoading(doorType, () => {
+ showToast({ title: failMsg, icon: 'fail' });
+ });
return;
}
@@ -182,11 +187,12 @@
trunkPollTimerRef.current = setTimeout(doPoll, pollInterval);
} else {
stopPolling();
- stopLoading(doorType);
// Taro.hideLoading();
- Taro.showToast({
- title: failToastText,
- icon: 'none'
+ stopLoading(doorType, () => {
+ showToast({
+ title: failToastText,
+ icon: 'none'
+ });
});
}
};
@@ -215,13 +221,14 @@
stopPolling();
const isOpen = !isCloseAction;
setTrunkStatus(isOpen);
- stopLoading(doorType);
- // Taro.hideLoading();
- Taro.showToast({
- title: successToastText,
- icon: 'none'
- });
onSuccess?.();
+ // Taro.hideLoading();
+ stopLoading(doorType, () => {
+ showToast({
+ title: successToastText,
+ icon: 'none'
+ });
+ });
} else {
handlePollFail('state_mismatch', value);
}
@@ -235,15 +242,40 @@
if (doorType === 'door') {
// Taro.hideLoading();
syncDoorUnlockButtonUI();
- stopLoading(doorType);
- Taro.showToast({ title: '开门指令已发送', icon: 'none' });
onSuccess?.();
+ stopLoading(doorType, () => {
+ showToast({ title: '车门已解锁', icon: 'success' });
+ });
}
}).catch(() => {
// Taro.hideLoading();
- stopLoading(doorType);
- Taro.showToast({ title: '解锁失败,请重试或联系客服', icon: 'none' });
+ stopLoading(doorType, () => {
+ showToast({ title: '解锁失败,请重试或联系客服', icon: 'fail' });
+ });
});
+ };
+
+ // 点击后备箱:当前为关闭态(将执行开启)时弹二次确认,开启态(将执行关闭)直接执行
+ const handleTrunkCardClick = () => {
+ if (loadingControlType) {
+ return;
+ }
+ if (!trunkStatus) {
+ setTrunkConfirmVisible(true);
+ return;
+ }
+ handleUnlock('trunk', () => setTrunkStatus(!trunkStatus));
+ };
+
+ // 二次确认弹窗:确认开启
+ const handleTrunkConfirm = () => {
+ setTrunkConfirmVisible(false);
+ handleUnlock('trunk', () => setTrunkStatus(!trunkStatus));
+ };
+
+ // 二次确认弹窗:取消
+ const handleTrunkCancel = () => {
+ setTrunkConfirmVisible(false);
};
/** 渲染带 loading 效果的控制按钮图标。 */
@@ -265,26 +297,33 @@
if (tripStatus === TripStatus.VehicleArrivedPickup) {
return (
+ <>
<View className="car-card-arrive">
- <View className={`car-card-arrive-item ${trunkStatus && 'close-btn-item'} ${loadingControlType ? 'trip-action-bar-item-disabled' : ''}`.trim()} onClick={() => handleUnlock('trunk', () => setTrunkStatus(!trunkStatus))}>
- {renderControlIcon('trunk', trunkStatus ? TRUNK_OPEN_ICON_URL : TRUNK_CLOSE_ICON_URL, 'car-card-arrive-item-img')}
+ <View className={`car-card-arrive-item ${trunkStatus && 'close-btn-item'} ${loadingControlType ? 'trip-action-bar-item-disabled' : ''}`.trim()} onClick={handleTrunkCardClick}>
<View className="car-card-arrive-item-text">后备箱</View>
+ <View className="car-card-arrive-item-status">{trunkStatus ? '点击关闭' : '点击打开'}</View>
+ {renderControlIcon('trunk', trunkStatus ? TRUNK_OPEN_ICON_URL : TRUNK_CLOSE_ICON_URL, 'car-card-arrive-item-img')}
</View>
- <View className={`car-card-arrive-item ${lockStatus && 'close-btn-item'} ${loadingControlType ? 'trip-action-bar-item-disabled' : ''}`.trim()} onClick={() => handleUnlock('door', onDoorUnlockSuccess)}>
- {renderControlIcon('door', lockStatus ? LOCK_OPEN_ICON_URL : LOCK_CLOSE_ICON_URL, 'car-card-arrive-item-img')}
+ <View className={`car-card-arrive-item ${lockStatus && 'close-btn-item'} ${loadingControlType ? 'trip-action-bar-item-disabled' : ''}`.trim()} onClick={() => handleUnlock('door')}>
<View className="car-card-arrive-item-text">车锁</View>
+ <View className="car-card-arrive-item-status">{lockStatus ? '点击关闭' : '点击打开'}</View>
+ {renderControlIcon('door', lockStatus ? LOCK_OPEN_ICON_URL : LOCK_CLOSE_ICON_URL, 'car-card-arrive-item-img')}
</View>
</View>
+ <TrunkConfirmPopup show={trunkConfirmVisible} onCancel={handleTrunkCancel} onConfirm={handleTrunkConfirm} />
+ </>
);
}
if (tripStatus === TripStatus.ArrivedDestination) {
+ // !! 6.30修改,车端会给我们传信号,说已到达目的地,小程序就会从前往目的地这个页面流转到行程结束待支付;意思是相当于这个不会展示
return (
+ <>
<View className="car-card-reach">
<View className="car-card-reach-text">点击按钮开车门</View>
<Image className='car-card-reach-img' src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/door-anim.webp' />
<View className="car-card-reach-btn">
- <View className={`car-card-reach-btn-item ${trunkStatus && 'close-btn-item'} ${loadingControlType ? 'trip-action-bar-item-disabled' : ''}`.trim()} onClick={() => handleUnlock('trunk', () => setTrunkStatus(!trunkStatus))}>
+ <View className={`car-card-reach-btn-item ${trunkStatus && 'close-btn-item'} ${loadingControlType ? 'trip-action-bar-item-disabled' : ''}`.trim()} onClick={handleTrunkCardClick}>
{renderControlIcon('trunk', trunkStatus ? TRUNK_OPEN_ICON_URL : TRUNK_CLOSE_ICON_URL, 'car-card-reach-btn-item-img')}
<View className="car-card-reach-btn-item-text">后备箱</View>
</View>
@@ -294,6 +333,8 @@
</View>
</View>
</View>
+ <TrunkConfirmPopup show={trunkConfirmVisible} onCancel={handleTrunkCancel} onConfirm={handleTrunkConfirm} />
+ </>
);
}
diff --git a/src/robotaxi/pages/begin-route/components/calling-car-modules/evaluation-card.tsx b/src/robotaxi/pages/begin-route/components/calling-car-modules/evaluation-card.tsx
index b6a02c4..824e7ac 100644
--- a/src/robotaxi/pages/begin-route/components/calling-car-modules/evaluation-card.tsx
+++ b/src/robotaxi/pages/begin-route/components/calling-car-modules/evaluation-card.tsx
@@ -1,4 +1,5 @@
import Taro from "@tarojs/taro";
+import { showToast } from '../../../../components/toast';
import { View, Image, Text } from "@tarojs/components";
import { useState, useEffect, useRef } from "react";
import { submitOrderComment } from '../../../../api/begin-route';
@@ -61,7 +62,7 @@
}
const targetOrderId = getOrderId(orderId);
if (!targetOrderId) {
- Taro.showToast({ title: '订单信息缺失,无法提交评价', icon: 'none' });
+ showToast({ title: '订单信息缺失,无法提交评价', icon: 'none' });
return;
}
submitOrderComment({
@@ -69,12 +70,12 @@
orderId: targetOrderId
}).then(() => {
setIsEvaluated(true);
- Taro.showToast({
+ showToast({
title: ['收到您的评价,我们将努力提升体验', '感谢您的反馈,欢迎下次乘坐'][index - 2],
icon: 'none'
});
}).catch(() => {
- Taro.showToast({ title: '评价提交失败,请稍后重试', icon: 'none' });
+ showToast({ title: '评价提交失败,请稍后重试', icon: 'none' });
});
};
diff --git a/src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx b/src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx
index b57d41a..853f44a 100644
--- a/src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx
+++ b/src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx
@@ -71,7 +71,6 @@
return (
<View>
<View className="car-card-end">
- <View className="car-card-line" style={{ margin: '12px 0' }}></View>
<View className="car-card-end-particulars">
<View className="car-card-end-particulars-label">
行程费用
@@ -79,18 +78,26 @@
{
fareBreakdown?.operateFree == 1 ?
<View className="car-card-end-particulars-free">
- 员工内部测试
+ {fareBreakdown?.freeText || "员工内部测试"}
</View>
:
<View className="car-card-end-particulars-money" onClick={handleToggleDetail}>
+ {
+ fareBreakdown?.redPacketMoney + fareBreakdown?.couponMoney > 0 &&
+ <>
+ <View className="car-card-end-particulars-money-discount-badge">
+ 已优惠
+ <Text className="car-card-end-particulars-money-discount-badge-amount">
+ {formatMoney(fareBreakdown?.redPacketMoney + fareBreakdown?.couponMoney)}元
+ </Text>
+ </View>
+ <View className="car-card-end-particulars-money-through">{formatMoney(fareBreakdown?.orderMoney)}元</View>
+ </>
+ }
<View className="car-card-end-particulars-money-amount">{formatMoney(
fareBreakdown?.payMoney <= 0
? 0 : fareBreakdown?.payMoney)}</View>
- <View>元</View>
- {
- fareBreakdown?.redPacketMoney + fareBreakdown?.couponMoney > 0 &&
- <View className="car-card-end-particulars-money-through">{formatMoney(fareBreakdown?.orderMoney)}元</View>
- }
+ <View className="car-card-end-particulars-money-unit">元</View>
<Image style={{ transform: showDetail ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 0.3s' }} src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/itinerary-cost-icon.png" />
</View>
}
@@ -186,20 +193,22 @@
</View>
<View className="car-card-end-cost-content">
<View className="car-card-end-cost-money" onClick={toMoneyDetail}>
- 全程{formatTripDistance(fareBreakdown?.kilometer)}|{formatTripDurationFromSeconds(fareBreakdown?.time)}
+ 全程{formatTripDistance(fareBreakdown?.kilometer)}<View className="car-card-end-cost-money-line">|</View>{formatTripDurationFromSeconds(fareBreakdown?.time)}
</View>
<Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/ccc-icon-right.png" />
</View>
</View>
{
- (fareBreakdown?.operateFree != 1 && [TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(tripStatus)) && Number(fareBreakdown?.redPacketMoney + fareBreakdown?.couponMoney) > 0 &&
- <View className="car-card-end-through">
- <View className="car-card-end-through-label">优惠活动</View>
- <View className="car-card-end-through-content" onClick={canOpenCouponSelect ? onOpenCouponSelect : undefined}>
+ (fareBreakdown?.operateFree != 1 && [TripStatus.TripFinished].includes(tripStatus)) && Number(fareBreakdown?.redPacketMoney + fareBreakdown?.couponMoney) > 0 &&
+ <View className="car-card-end-through" onClick={canOpenCouponSelect ? onOpenCouponSelect : undefined}>
+ <View className="car-card-end-through-left">
<View className="car-card-end-through-content-title">最佳优惠</View>
+ <View className="car-card-end-through-desc">已为您选择最佳优惠</View>
+ </View>
+ <View className="car-card-end-through-right">
<View className="car-card-end-through-content-money">-{formatMoney(fareBreakdown?.redPacketMoney + fareBreakdown?.couponMoney)}元</View>
{canOpenCouponSelect && (
- <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/ccc-icon-right.png" />
+ <Image className="car-card-end-through-arrow" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/golden-arrow-20260702.png" />
)}
</View>
</View>
@@ -208,7 +217,7 @@
{
tripStatus === TripStatus.TripFinished &&
<View className="pay-box">
- {
+ {/* {
fareBreakdown?.operateFree != 1 ?
<View className="pay-box-money"><View>{formatMoney(
fareBreakdown?.payMoney <= 0
diff --git a/src/robotaxi/pages/begin-route/components/calling-car-modules/fragrance-control/index.tsx b/src/robotaxi/pages/begin-route/components/calling-car-modules/fragrance-control/index.tsx
new file mode 100644
index 0000000..9c3b38b
--- /dev/null
+++ b/src/robotaxi/pages/begin-route/components/calling-car-modules/fragrance-control/index.tsx
@@ -0,0 +1,254 @@
+import Taro from '@tarojs/taro';
+import { View, Text, Image } from '@tarojs/components';
+import { useEffect, useRef, useState } from 'react';
+import { queryCabinFragrance, setCabinFragrance } from '../../../../../api/fragrance';
+import type { FragranceScent } from '../../../../../api/types/fragrance';
+import { showToast } from '../../../../../components/toast';
+import { getOrderId } from '../../../../../utils/order';
+import { TripStatus } from '../../../../../constants/trip-status';
+import useBottomPopupAnimation from '../../hooks/use-bottom-popup-animation';
+import '../../css-folder/fragrance-control.less';
+
+const FC_HEADER_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/fragrance-icon-20260716.png';
+const ARROW_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/temperature-fragrance-arrow-20260706.png';
+const CLOSE_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/close-icon.png';
+const LOADING_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/T-switch-loading-20260715.png';
+const CLOSED_TITLE = '已关闭'; // 关闭态卡片主标题
+const NO_SCENT_TIP = '当前车辆不支持香型选择'; // 无可用香型占位
+const CN_NUMBERS = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十']; // 数字转中文
+
+/**
+ * 卡片副标题:有可用香型时展示"可选N款香型",否则展示不支持文案。
+ */
+function resolveCardSubtitle(scentCount: number): string {
+ if (scentCount <= 0) {
+ return NO_SCENT_TIP;
+ }
+ const cnNum = scentCount <= 10 ? CN_NUMBERS[scentCount] : String(scentCount);
+ return `可选${cnNum}款香型`;
+}
+const FRAGRANCE_OPEN_SUCCESS_TEXT = '已开启香氛'; // 开启香氛成功提示文案
+const CLOSE_SUCCESS_TEXT = '已关闭香氛';
+const APPLY_FAIL_TEXT = '网络异常,请重试';
+
+/** 香调描述本地兜底表:接口未返回 desc 时按 id 取用。 */
+const SCENT_DESC_MAP: Record<string, string> = {
+ SANDALWOOD: '沉稳·笃定',
+ WHITE: '清新·淡雅',
+ AMBER: '愉悦·活力'
+};
+
+type Props = {
+ tripStatus: TripStatus;
+ orderId?: string;
+ pageDidShowVersion?: number;
+ /** 后端推送:当前被指派车辆是否仍在执行上一单(连环单) */
+ hasPreviousOrder?: boolean;
+};
+
+/**
+ * 取香型的香调描述:优先接口 desc,其次本地兜底表。
+ */
+function resolveScentDesc(scent?: FragranceScent): string {
+ if (!scent) {
+ return '';
+ }
+ return scent.desc || SCENT_DESC_MAP[scent.id] || '';
+}
+
+/**
+ * 从香型列表中按通道 ID 找香型。
+ */
+function findScentByChannel(scents: FragranceScent[], channelId: string): FragranceScent | undefined {
+ return scents.find((item) => item.channelId === channelId);
+}
+
+/**
+ * 计算车端开启态下当前生效的香型通道。
+ */
+function resolveActiveChannelId(scents: FragranceScent[], switchOn: boolean, currentScent?: string): string {
+ if (!switchOn || !scents.length) {
+ return '';
+ }
+ const matched = currentScent ? scents.find((item) => item.id === currentScent) : undefined;
+ return (matched || scents[0]).channelId;
+}
+
+export default function FragranceControl({ tripStatus, orderId, pageDidShowVersion = 0, hasPreviousOrder = false }: Props) {
+ // 车端真实状态
+ const [switchOn, setSwitchOn] = useState(false);
+ const [scents, setScents] = useState<FragranceScent[]>([]);
+ // 当前生效香型通道(关闭时为空)
+ const [activeChannelId, setActiveChannelId] = useState('');
+ // 卡片主标题(开启时展示香型名,关闭时展示"已关闭")
+ const [cardTitle, setCardTitle] = useState(CLOSED_TITLE);
+ // 面板显隐 + 下发锁
+ const [panelVisible, setPanelVisible] = useState(false);
+ const [applying, setApplying] = useState(false);
+ // 正在下发的香型通道(该卡片显示 loading),空串表示无
+ const [applyingChannelId, setApplyingChannelId] = useState('');
+ const querySeqRef = useRef(0);
+ // 记录本次接驾态周期内已查询过香氛状态的订单 ID,避免重复请求(同一订单在初始化恢复
+ // 流程中 tripStatus/orderId/hasPreviousOrder 会分批异步更新,触发本 effect 多次)。
+ const queriedOrderIdRef = useRef('');
+
+ const { animationData, shouldRender } = useBottomPopupAnimation(panelVisible);
+ const hasScents = scents.length > 0;
+
+ // 进入接驾态查询香氛状态;连环单不查询、不展示
+ useEffect(() => {
+ if (tripStatus !== TripStatus.VehicleApproaching || hasPreviousOrder) {
+ // 离开接驾态:重置去重标记,允许下次进入接驾态时重新查询一次。
+ queriedOrderIdRef.current = '';
+ return;
+ }
+ const targetOrderId = getOrderId(orderId);
+ if (!targetOrderId) {
+ return;
+ }
+ // 去重:同一订单在本次接驾态周期内只查询一次。
+ if (queriedOrderIdRef.current === targetOrderId) {
+ return;
+ }
+ queriedOrderIdRef.current = targetOrderId;
+ const snapshotSeq = ++querySeqRef.current;
+ queryCabinFragrance(targetOrderId).then((res: any) => {
+ if (querySeqRef.current !== snapshotSeq) {
+ return;
+ }
+ const fragrance = res || {};
+ const nextSwitchOn = fragrance.switchOn === true;
+ const nextScents: FragranceScent[] = Array.isArray(fragrance.availableScents) ? fragrance.availableScents : [];
+ const nextActiveChannelId = resolveActiveChannelId(nextScents, nextSwitchOn, fragrance.currentScent);
+ setSwitchOn(nextSwitchOn);
+ setScents(nextScents);
+ setActiveChannelId(nextActiveChannelId);
+ const activeName = findScentByChannel(nextScents, nextActiveChannelId)?.name || '';
+ setCardTitle(nextSwitchOn && activeName ? activeName : CLOSED_TITLE);
+ }).catch(() => {
+ if (querySeqRef.current !== snapshotSeq) {
+ return;
+ }
+ // 查询失败按「已关闭」兜底
+ setSwitchOn(false);
+ setScents([]);
+ setActiveChannelId('');
+ setCardTitle(CLOSED_TITLE);
+ });
+ }, [tripStatus, orderId, pageDidShowVersion, hasPreviousOrder]);
+
+ // 点击卡片:切换面板开合
+ const togglePanel = () => {
+ setPanelVisible((prev) => !prev);
+ };
+
+ const closePanel = () => {
+ setPanelVisible(false);
+ };
+
+ // 点击香型即时下发:点选中项 → 关闭香氛;点其它项 → 开启并切换香型
+ const handleScentClick = async (channelId: string) => {
+ if (applying) {
+ return;
+ }
+ const targetOrderId = getOrderId(orderId);
+ if (!targetOrderId) {
+ return;
+ }
+ // 点击当前生效香型 → 关闭;否则 → 开启该香型
+ const turningOff = switchOn && channelId === activeChannelId;
+ const nextSwitchOn = !turningOff;
+ setApplying(true);
+ setApplyingChannelId(channelId);
+ try {
+ await setCabinFragrance({
+ orderId: targetOrderId,
+ switchOn: nextSwitchOn,
+ ...(nextSwitchOn ? { channelId } : {})
+ });
+ // 下发成功后同步车端真实状态与卡片主标题
+ setSwitchOn(nextSwitchOn);
+ const nextActiveChannelId = nextSwitchOn ? channelId : '';
+ setActiveChannelId(nextActiveChannelId);
+ const activeName = findScentByChannel(scents, nextActiveChannelId)?.name || '';
+ setCardTitle(nextSwitchOn && activeName ? activeName : CLOSED_TITLE);
+ // 开启/关闭香氛均使用通用 Toast 组件提示(success),并关闭香氛设置弹窗
+ showToast({ title: nextSwitchOn ? FRAGRANCE_OPEN_SUCCESS_TEXT : CLOSE_SUCCESS_TEXT, icon: 'success' });
+ closePanel();
+ } catch (error) {
+ showToast({ title: APPLY_FAIL_TEXT, icon: 'none' });
+ } finally {
+ setApplying(false);
+ setApplyingChannelId('');
+ }
+ };
+
+ // 连环单或非接驾态不展示香氛卡片
+ if (tripStatus !== TripStatus.VehicleApproaching || hasPreviousOrder) {
+ return null;
+ }
+
+ // 香氛卡片背景随开关状态切换(复用 tf-card 视觉)
+ const fragranceCardStateClass = switchOn ? 'is-open' : 'is-close';
+
+ return (
+ <View className='fragrance-control'>
+ {/* 香氛入口卡片(复用 tf-card 视觉):点击打开面板 */}
+ <View
+ className={`tf-card tf-card-fragrance ${fragranceCardStateClass}`}
+ onClick={togglePanel}
+ >
+ <View className='tf-card-header'>
+ <View className='tf-card-header-left'>
+ <Image className='tf-card-header-icon' src={FC_HEADER_ICON} />
+ <Text className='tf-card-header-title'>香氛</Text>
+ </View>
+ </View>
+ <View className='tf-card-bottom'>
+ <View className='tf-card-fragrance-name'>{cardTitle}</View>
+ <View className='tf-card-footer'>
+ <Text className='tf-card-footer-text'>{resolveCardSubtitle(scents.length)}</Text>
+ <Image className='tf-card-footer-arrow' src={ARROW_ICON_URL} />
+ </View>
+ </View>
+ </View>
+
+ {/* 控制面板(底部弹窗) */}
+ {shouldRender && (
+ <View className={`fragrance-popup ${panelVisible ? 'show' : ''}`} catchMove>
+ <View className='popup-mask' onClick={closePanel}></View>
+ <View className='popup-content' animation={animationData}>
+ <View className='popup-header'>
+ <Text className='popup-title'>车内香氛</Text>
+ <Image className='popup-close' src={CLOSE_ICON_URL} onClick={closePanel} />
+ </View>
+
+ <View className='popup-scent-area'>
+ {hasScents ? (
+ <View className='popup-scent-list'>
+ {scents.map((item) => {
+ const isActive = switchOn && item.channelId === activeChannelId;
+ const isLoading = applyingChannelId === item.channelId;
+ const desc = resolveScentDesc(item);
+ return (
+ <View
+ key={item.channelId}
+ className={`popup-scent-item ${isActive ? 'active' : ''} ${applying ? 'disabled' : ''}`}
+ onClick={() => handleScentClick(item.channelId)}
+ >
+ {isLoading && <Image className='popup-scent-loading' src={LOADING_ICON_URL} />}
+ <Text className='popup-scent-name'>{item.name}</Text>
+ </View>
+ );
+ })}
+ </View>
+ ) : (
+ <View className='popup-scent-empty'>{NO_SCENT_TIP}</View>
+ )}
+ </View>
+ </View>
+ </View>
+ )}
+ </View>
+ );
+}
diff --git a/src/robotaxi/pages/begin-route/components/calling-car-modules/seat-hv-control/index.tsx b/src/robotaxi/pages/begin-route/components/calling-car-modules/seat-hv-control/index.tsx
new file mode 100644
index 0000000..4d6e00b
--- /dev/null
+++ b/src/robotaxi/pages/begin-route/components/calling-car-modules/seat-hv-control/index.tsx
@@ -0,0 +1,199 @@
+import Taro from '@tarojs/taro';
+import { showToast } from '../../../../../components/toast';
+import { View, Image } from '@tarojs/components';
+import { useEffect, useRef, useState } from 'react';
+import { querySeatHv, setSeatHv } from '../../../../../api/seat-hv';
+import type { SeatHvItem, SeatSetItem } from '../../../../../api/types/seat-hv';
+import { getOrderId } from '../../../../../utils/order';
+import { TripStatus } from '../../../../../constants/trip-status';
+import '../../css-folder/seat-hv-control.less';
+
+const SEAT_BG_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/seat_bg_0715.png';
+const IMG_BASE = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/';
+const LOADING_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/T-switch-loading-20260715.png';
+const NO_SEAT_TIP = '座椅调节暂不可用';
+const APPLY_FAIL_TEXT = '网络异常,请重试';
+
+type SeatKind = 'ventilation' | 'heating';
+
+/**
+ * 按档位取座椅通风/加热图标:seat_air_gray_level{n} / seat_heat_gray_level{n}(n=0~3)。
+ */
+function resolveSeatIcon(kind: SeatKind, level: number): string {
+ const safeLevel = Math.max(0, Math.min(3, level || 0));
+ const name = kind === 'ventilation' ? 'seat_air_gray_level' : 'seat_heat_gray_level';
+ return `${IMG_BASE}${name}${safeLevel}@2x.png`;
+}
+
+type Props = {
+ tripStatus: TripStatus;
+ orderId?: string;
+ pageDidShowVersion?: number;
+ /** 后端推送:当前被指派车辆是否仍在执行上一单(连环单) */
+ hasPreviousOrder?: boolean;
+ /** 面板是否可见(可见时才查询,避免无谓请求) */
+ active?: boolean;
+};
+
+/**
+ * 计算下一档位:0 → 1 → … → levels → 0。
+ */
+function nextLevel(current: number, levels: number): number {
+ if (levels <= 0) {
+ return 0;
+ }
+ return current >= levels ? 0 : current + 1;
+}
+
+/**
+ * 座椅通风/加热区域(内嵌于「温度调节」面板,无独立入口/弹窗)。
+ */
+export default function SeatHvArea({ tripStatus, orderId, pageDidShowVersion = 0, hasPreviousOrder = false, active = true }: Props) {
+ const [seats, setSeats] = useState<SeatHvItem[]>([]);
+ // 正在下发的按钮标识 `${position}:${kind}`(该按钮显示 loading,其余按钮置灰防重复),空串表示无
+ const [applyingKey, setApplyingKey] = useState('');
+ const querySeqRef = useRef(0);
+ // 记录本次接驾态周期内已查询过座椅状态的订单 ID,避免重复请求(同一订单在初始化恢复
+ // 流程中 tripStatus/orderId/hasPreviousOrder 会分批异步更新,触发本 effect 多次)。
+ const queriedOrderIdRef = useRef('');
+
+ const hasSeats = seats.length > 0;
+
+ // 进入接驾态且面板可见时查询座椅状态;连环单不查询、不展示
+ useEffect(() => {
+ if (!active || tripStatus !== TripStatus.VehicleApproaching || hasPreviousOrder) {
+ // 面板隐藏或离开接驾态:重置去重标记,允许下次可见/进入接驾态时重新查询一次。
+ queriedOrderIdRef.current = '';
+ return;
+ }
+ const targetOrderId = getOrderId(orderId);
+ if (!targetOrderId) {
+ return;
+ }
+ // 去重:同一订单在本次接驾态周期内只查询一次。
+ if (queriedOrderIdRef.current === targetOrderId) {
+ return;
+ }
+ queriedOrderIdRef.current = targetOrderId;
+ const snapshotSeq = ++querySeqRef.current;
+ querySeatHv(targetOrderId).then((res: any) => {
+ if (querySeqRef.current !== snapshotSeq) {
+ return;
+ }
+ const list: SeatHvItem[] = Array.isArray(res?.seats) ? res.seats : [];
+ setSeats(list);
+ }).catch(() => {
+ if (querySeqRef.current !== snapshotSeq) {
+ return;
+ }
+ setSeats([]);
+ });
+ }, [active, tripStatus, orderId, pageDidShowVersion, hasPreviousOrder]);
+
+ // 点击座椅通风/加热按钮:档位循环 + 互斥联动 + 即时下发(仅该座椅)
+ const handleSeatClick = async (seat: SeatHvItem, kind: SeatKind) => {
+ if (applyingKey) {
+ return;
+ }
+ const levels = kind === 'ventilation' ? seat.ventilationLevels : seat.heatingLevels;
+ if (levels <= 0) {
+ return; // 该能力不支持
+ }
+ const targetOrderId = getOrderId(orderId);
+ if (!targetOrderId) {
+ return;
+ }
+
+ // 计算目标档位 + 互斥:开一方则另一方归 0
+ let nextVentilation = seat.ventilation;
+ let nextHeating = seat.heating;
+ if (kind === 'ventilation') {
+ nextVentilation = nextLevel(seat.ventilation, seat.ventilationLevels);
+ if (nextVentilation > 0) {
+ nextHeating = 0;
+ }
+ } else {
+ nextHeating = nextLevel(seat.heating, seat.heatingLevels);
+ if (nextHeating > 0) {
+ nextVentilation = 0;
+ }
+ }
+
+ const payloadSeat: SeatSetItem = {
+ position: seat.position,
+ ventilationLevel: nextVentilation,
+ heatingLevel: nextHeating
+ };
+
+ setApplyingKey(`${seat.position}:${kind}`);
+ try {
+ await setSeatHv({ orderId: targetOrderId, seats: [payloadSeat] });
+ // 下发成功后更新该座椅本地状态
+ setSeats((prev) => prev.map((item) => (
+ item.position === seat.position
+ ? { ...item, ventilation: nextVentilation, heating: nextHeating }
+ : item
+ )));
+ } catch (error) {
+ showToast({ title: APPLY_FAIL_TEXT, icon: 'none' });
+ } finally {
+ setApplyingKey('');
+ }
+ };
+
+ if (!hasSeats) {
+ return <View className='seat-hv-empty'>{NO_SEAT_TIP}</View>;
+ }
+
+ return (
+ <View className='seat-hv-area'>
+ <Image className='seat-hv-bg' src={SEAT_BG_URL} mode='widthFix' />
+ <View className='seat-hv-list'>
+ {seats.map((seat) => {
+ const showVentilation = seat.ventilationLevels > 0;
+ const showHeating = seat.heatingLevels > 0;
+ // 通风、加热都不支持 → 该座椅不显示任何按钮
+ if (!showVentilation && !showHeating) {
+ return null;
+ }
+ const ventilationKey = `${seat.position}:ventilation`;
+ const heatingKey = `${seat.position}:heating`;
+ const ventilationLoading = applyingKey === ventilationKey;
+ const heatingLoading = applyingKey === heatingKey;
+ // 有按钮正在下发时,其余按钮置灰防重复
+ const ventilationDisabled = !!applyingKey && !ventilationLoading;
+ const heatingDisabled = !!applyingKey && !heatingLoading;
+ // 仅加热(不支持通风,如三排)→ 单圆形按钮;支持通风 → 胶囊双图标
+ const isPillLayout = showVentilation && showHeating;
+ return (
+ <View className={`seat-hv-node seat-hv-node-${seat.position}`} key={seat.position}>
+ <View className={`seat-hv-ctrl ${isPillLayout ? 'is-pill' : 'is-single'}`}>
+ {showVentilation && (
+ <View
+ className={`seat-hv-btn ${seat.ventilation > 0 ? 'active' : ''} ${ventilationDisabled ? 'disabled' : ''}`}
+ onClick={() => handleSeatClick(seat, 'ventilation')}
+ >
+ {ventilationLoading
+ ? <Image className='seat-hv-btn-icon seat-hv-btn-loading' src={LOADING_ICON_URL} />
+ : <Image className='seat-hv-btn-icon' src={resolveSeatIcon('ventilation', seat.ventilation)} />}
+ </View>
+ )}
+ {isPillLayout && <View className='seat-hv-divider'></View>}
+ {showHeating && (
+ <View
+ className={`seat-hv-btn ${seat.heating > 0 ? 'active' : ''} ${heatingDisabled ? 'disabled' : ''}`}
+ onClick={() => handleSeatClick(seat, 'heating')}
+ >
+ {heatingLoading
+ ? <Image className='seat-hv-btn-icon seat-hv-btn-loading' src={LOADING_ICON_URL} />
+ : <Image className='seat-hv-btn-icon' src={resolveSeatIcon('heating', seat.heating)} />}
+ </View>
+ )}
+ </View>
+ </View>
+ );
+ })}
+ </View>
+ </View>
+ );
+}
diff --git a/src/robotaxi/pages/begin-route/components/calling-car-modules/service-route-change.jsx b/src/robotaxi/pages/begin-route/components/calling-car-modules/service-route-change.jsx
index 79aa8c7..0bc177a 100644
--- a/src/robotaxi/pages/begin-route/components/calling-car-modules/service-route-change.jsx
+++ b/src/robotaxi/pages/begin-route/components/calling-car-modules/service-route-change.jsx
@@ -33,7 +33,7 @@
<View className='service-route-select-title'>路线选择:</View>
<View className='service-route-select-preference' onClick={() => setShowPreferencePopup(true)}>
<View className='service-route-select-preference-text'>{preference?.label || ''}</View>
- <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/select-line.png" />
+ <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/select-line-20260707.png" />
</View>
</View>
diff --git a/src/robotaxi/pages/begin-route/components/calling-car-modules/temperature-control.tsx b/src/robotaxi/pages/begin-route/components/calling-car-modules/temperature-control.tsx
index b0e26e3..756ea0f 100644
--- a/src/robotaxi/pages/begin-route/components/calling-car-modules/temperature-control.tsx
+++ b/src/robotaxi/pages/begin-route/components/calling-car-modules/temperature-control.tsx
@@ -1,5 +1,6 @@
import Taro from "@tarojs/taro";
-import { View, Image, Text } from "@tarojs/components";
+import { showToast } from '../../../../components/toast';
+import { View, Image, Text, ScrollView } from "@tarojs/components";
import { useState, useEffect, useRef } from "react";
// TODO(仅用于测试): 上线前将此处改回从 '../../../../api/begin-route' 导入 './test-case/ac-mock'
import {
@@ -10,19 +11,26 @@
} from '../../../../api/begin-route';
// TODO(仅用于测试): 上线前删除以下 import
import '../css-folder/calling-car.less';
+import '../css-folder/temperature-control.less';
import { getOrderId } from '../../../../utils/order';
import { TripStatus } from '../../../../constants/trip-status';
import { MODIFY_DESTINATION_SKIP_AC_QUERY_ON_RETURN_KEY } from '../../../../constants/modify-destination';
import { getAcStorage, saveAcStorage } from './ac-storage';
import useMinimumSpinLoading from './use-minimum-spin-loading';
+import useBottomPopupAnimation from '../hooks/use-bottom-popup-animation';
+import SeatHvArea from './seat-hv-control';
+import AcSwitch from './ac-switch';
-const AC_SWITCH_OPEN_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/T-switch-open.png';
-const AC_SWITCH_CLOSE_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/T-switch-close.png';
-const AC_SWITCH_LOADING_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/T-switch-loading-v3.png';
+// 入口卡片图标(复用 temperature-fragrance-control 的 v2 资源)
+const AC_HEADER_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/air-conditioner-20260716.png';
+const AC_ARROW_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/temperature-fragrance-arrow-20260706.png';
+const PANEL_CLOSE_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/close-icon.png';
const AC_TOGGLE_FAIL_TEXT = '空调开关操作失败,请重试';
const AC_TEMP_FAIL_TEXT = '温度设置失败,请重试';
const AC_TEMP_MIN_TIP_TEXT = '已到空调最低设定温度';
const AC_TEMP_MAX_TIP_TEXT = '已到空调最高设定温度';
+const TEMP_ITEM_WIDTH = 130; // 温度滑条单项宽度(所有项等宽)
+const TEMP_SCROLL_SETTLE_MS = 120; // 滑动停止判定延时
type Props = {
tripStatus: TripStatus;
@@ -36,9 +44,15 @@
const [tempStatus, setTempStatus] = useState<number | null>(null);
const [acSwitchOn, setAcSwitchOn] = useState(false);
const [acStatusLoaded, setAcStatusLoaded] = useState(false);
+ // loading 期间正在切换的目标状态(true=开启中,false=关闭中)。
+ // 独立于 acSwitchOn,避免最小 loading 时长未结束前 acSwitchOn 已更新导致文案闪动。
+ const [acTogglingTarget, setAcTogglingTarget] = useState(false);
const tempTimerRef = useRef<any>(null);
const lastConfirmedTempRef = useRef<number | null>(null);
const skipQueryOnThisCycleRef = useRef(false);
+ // 记录本次接驾态周期内已查询过空调状态的订单 ID,避免同一订单在初始化过程中因
+ // tripStatus/orderId/hasPreviousOrder 分批异步更新而重复触发查询。
+ const queriedOrderIdRef = useRef('');
const userActionSeqRef = useRef(0);
const calibrationSeqRef = useRef(0);
const { visibleLoadingKey: acLoadingKey, startLoading, stopLoading } = useMinimumSpinLoading<'ac'>();
@@ -62,6 +76,87 @@
const decreaseButtonIconSuffix = decreaseButtonDisabled ? '-no' : '';
const increaseButtonIconSuffix = increaseButtonDisabled ? '-no' : '';
const tempTextDisabledClassName = acSwitchOn ? '' : 'color-000-25';
+
+ // ============ 温度调节面板(合并座椅通风加热) ============
+ const [panelVisible, setPanelVisible] = useState(false);
+ const { animationData, shouldRender } = useBottomPopupAnimation(panelVisible);
+ // 温度可选项:15.5 ~ 32.5,步进 0.5
+ const TEMP_OPTIONS: number[] = [];
+ for (let t = TEMP_MIN; t <= TEMP_MAX + 1e-6; t += 0.5) {
+ TEMP_OPTIONS.push(Number(t.toFixed(1)));
+ }
+ // 卡片主标题温度(保留一位小数,如 20.5),沿用现有 HI/LO 语义
+ const cardTempText = typeof tempStatus === 'number'
+ ? (tempStatus >= TEMP_MAX ? 'HI' : (tempStatus <= TEMP_MIN ? 'LO' : tempStatus.toFixed(1)))
+ : '--';
+
+ // 温度滑条:滚动停止后吸附居中项并下发
+ const tempScrollSettleTimerRef = useRef<any>(null);
+ const [tempScrollLeft, setTempScrollLeft] = useState(0);
+ // 当前居中项索引(滚动过程中跟随,用于高亮)
+ const [centeredTempIndex, setCenteredTempIndex] = useState(0);
+ // 单项真实像素宽度:设计稿 120px(@designWidth=750)→ 设备真实 px
+ const itemPxRef = useRef(0);
+ if (itemPxRef.current === 0) {
+ try {
+ const sysWidth = Taro.getSystemInfoSync().windowWidth || 375;
+ itemPxRef.current = (TEMP_ITEM_WIDTH / 750) * (sysWidth);
+ } catch {
+ itemPxRef.current = TEMP_ITEM_WIDTH / 2; // 兜底
+ }
+ }
+
+ // 打开面板时,把当前温度滚动到居中
+ useEffect(() => {
+ if (!panelVisible) {
+ return;
+ }
+ const idx = typeof tempStatus === 'number'
+ ? TEMP_OPTIONS.findIndex((v) => Math.abs(v - tempStatus) < 1e-6)
+ : -1;
+ const safeIdx = idx >= 0 ? idx : 0;
+ setCenteredTempIndex(safeIdx);
+ setTempScrollLeft(safeIdx * itemPxRef.current);
+ }, [panelVisible]);
+
+ const openPanel = () => setPanelVisible(true);
+ const closePanel = () => setPanelVisible(false);
+
+ // 滚动过程中实时更新居中高亮,停止后吸附并下发(空调关闭时也可拖动,仅更新本地值)
+ const handleTempScroll = (e: any) => {
+ const scrollLeft = e?.detail?.scrollLeft ?? 0;
+ const idx = Math.round(scrollLeft / itemPxRef.current);
+ const clampedIdx = Math.max(0, Math.min(TEMP_OPTIONS.length - 1, idx));
+ // 滚动中实时跟随居中项高亮
+ setCenteredTempIndex(clampedIdx);
+ if (tempScrollSettleTimerRef.current) {
+ clearTimeout(tempScrollSettleTimerRef.current);
+ }
+ tempScrollSettleTimerRef.current = setTimeout(() => {
+ const value = TEMP_OPTIONS[clampedIdx];
+ // 吸附居中
+ setTempScrollLeft(clampedIdx * itemPxRef.current);
+ if (typeof tempStatus !== 'number' || Math.abs(tempStatus - value) > 1e-6) {
+ handleSelectTemp(value);
+ }
+ }, TEMP_SCROLL_SETTLE_MS);
+ };
+
+ // 点击某温度项:滚动居中并选中(空调关闭时也可选,仅更新本地值)
+ const handleTempItemClick = (index: number, value: number) => {
+ setCenteredTempIndex(index);
+ setTempScrollLeft(index * itemPxRef.current);
+ handleSelectTemp(value);
+ };
+
+ // 面板内滑动/点击选择温度:更新本地值;空调开启时下发,关闭时仅暂存(开启开关时统一下发)
+ const handleSelectTemp = (value: number) => {
+ setTempStatus(value);
+ lastConfirmedTempRef.current = value;
+ if (acSwitchOn) {
+ scheduleTemperatureUpdate(value);
+ }
+ };
useEffect(() => {
@@ -101,6 +196,7 @@
setTempStatus(DEFAULT_FIRST_TEMP);
lastConfirmedTempRef.current = DEFAULT_FIRST_TEMP;
setAcStatusLoaded(true);
+ setAcTogglingTarget(true);
startLoading('ac');
let acSuccess = false;
@@ -118,15 +214,13 @@
}
} catch {}
- stopLoading('ac');
-
- if (!acSuccess) {
- Taro.showToast({ title: AC_TOGGLE_FAIL_TEXT, icon: 'none' });
- } else {
- if (!tempSuccess) {
- Taro.showToast({ title: AC_TEMP_FAIL_TEXT, icon: 'none' });
+ stopLoading('ac', () => {
+ if (!acSuccess) {
+ showToast({ title: AC_TOGGLE_FAIL_TEXT, icon: 'none' });
+ } else if (!tempSuccess) {
+ showToast({ title: AC_TEMP_FAIL_TEXT, icon: 'none' });
}
- }
+ });
saveAcStorage(targetOrderId, { acOpen: true, temperature: DEFAULT_FIRST_TEMP });
};
@@ -189,6 +283,8 @@
useEffect(() => {
if (tripStatus !== TripStatus.VehicleApproaching) {
+ // 离开接驾态:重置去重标记,允许下次进入接驾态时重新查询一次。
+ queriedOrderIdRef.current = '';
return;
}
// 连环派单场景:后端明确告知车辆仍在执行上一单(hasPreviousOrder 为 true)时,
@@ -204,6 +300,12 @@
if (!targetOrderId) {
return;
}
+ // 去重:同一订单在本次接驾态周期内只查询一次。tripStatus/pageDidShowVersion/hasPreviousOrder
+ // 在页面初始化恢复流程中会分多次异步更新,但 orderId 不变时无需重复查询。
+ if (queriedOrderIdRef.current === targetOrderId) {
+ return;
+ }
+ queriedOrderIdRef.current = targetOrderId;
const storageData = getAcStorage(targetOrderId);
@@ -244,11 +346,11 @@
// Taro.showToast({ title: '设置成功', icon: 'none' });
} else {
setTempStatus(lastConfirmedTempRef.current ?? null);
- Taro.showToast({ title: AC_TEMP_FAIL_TEXT, icon: 'none' });
+ showToast({ title: AC_TEMP_FAIL_TEXT, icon: 'none' });
}
}).catch(() => {
setTempStatus(lastConfirmedTempRef.current ?? null);
- Taro.showToast({ title: AC_TEMP_FAIL_TEXT, icon: 'none' });
+ showToast({ title: AC_TEMP_FAIL_TEXT, icon: 'none' });
});
}, TEMP_UPDATE_DEBOUNCE_MS);
};
@@ -263,11 +365,11 @@
}
const next = prev + delta;
if (delta < 0 && next < TEMP_MIN) {
- Taro.showToast({ title: '当前已是空调最低温度', icon: 'none' });
+ showToast({ title: '当前已是空调最低温度', icon: 'none' });
return prev;
}
if (delta > 0 && next > TEMP_MAX) {
- Taro.showToast({ title: '当前已是空调最高温度', icon: 'none' });
+ showToast({ title: '当前已是空调最高温度', icon: 'none' });
return prev;
}
scheduleTemperatureUpdate(next);
@@ -280,7 +382,7 @@
return;
}
if (isAtMinTemperature) {
- Taro.showToast({ title: AC_TEMP_MIN_TIP_TEXT, icon: 'none' });
+ showToast({ title: AC_TEMP_MIN_TIP_TEXT, icon: 'none' });
return;
}
handleTempChange(-0.5);
@@ -291,7 +393,7 @@
return;
}
if (isAtMaxTemperature) {
- Taro.showToast({ title: AC_TEMP_MAX_TIP_TEXT, icon: 'none' });
+ showToast({ title: AC_TEMP_MAX_TIP_TEXT, icon: 'none' });
return;
}
handleTempChange(0.5);
@@ -306,7 +408,9 @@
return;
}
const targetOpenState = !acSwitchOn;
+ setAcTogglingTarget(targetOpenState);
startLoading('ac');
+ let toastText = '';
try {
// 200就认为是指令下发成功了
await airConditionerUnlock({
@@ -327,15 +431,20 @@
userActionSeqRef.current++;
const currentTemp = typeof tempStatus === 'number' ? tempStatus : DEFAULT_FIRST_TEMP;
saveAcStorage(targetOrderId, { acOpen: targetOpenState, temperature: currentTemp });
- Taro.showToast({ title: targetOpenState ? '已开启空调' : '已关闭空调', icon: 'none' });
+ toastText = targetOpenState ? '已开启空调' : '已关闭空调';
if (targetOpenState && typeof tempStatus !== 'number') {
setTempStatus(DEFAULT_FIRST_TEMP);
lastConfirmedTempRef.current = DEFAULT_FIRST_TEMP;
}
} catch (error) {
- Taro.showToast({ title: AC_TOGGLE_FAIL_TEXT, icon: 'none' });
+ toastText = AC_TOGGLE_FAIL_TEXT;
} finally {
- stopLoading('ac');
+ // 等 loading 动画真正结束后再弹 toast,避免异常提示已弹出但 loading 仍在转
+ stopLoading('ac', () => {
+ if (toastText) {
+ showToast({ title: toastText, icon: 'none' });
+ }
+ });
}
};
@@ -345,60 +454,89 @@
}
return (
- <View>
- <View className="car-card-temp">
- <View className="car-card-temp-num">
- <View className="car-card-temp-num-btn">
- {
- canAdjustTemperature ?
- <Image className="car-card-temp-num-btn-img" src={`https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/T-down${decreaseButtonIconSuffix}.png`} onClick={handleDecreaseTemperature} />
- :
- <Image className="car-card-temp-num-btn-img" src={`https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/T-down${decreaseButtonIconSuffix}.png`} />
- }
+ <View className='ac-control'>
+ {/* 空调入口卡片(复用 tf-card 视觉):点击打开温度调节面板;右上开关直接切空调 */}
+ <View
+ className={`tf-card tf-card-ac ${acSwitchOn ? 'is-open' : 'is-close'}`}
+ onClick={openPanel}
+ >
+ <View className='tf-card-header'>
+ <View className='tf-card-header-left'>
+ <Image className='tf-card-header-icon' src={AC_HEADER_ICON} />
+ <Text className='tf-card-header-title'>空调</Text>
</View>
- <View className="car-card-temp-num-text">
- <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">
- {
- canAdjustTemperature ?
- <Image className="car-card-temp-num-btn-img" src={`https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/T-up${increaseButtonIconSuffix}.png`} onClick={handleIncreaseTemperature} />
- :
- <Image className="car-card-temp-num-btn-img" src={`https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/T-up${increaseButtonIconSuffix}.png`} />
- }
- </View>
+ <AcSwitch checked={acSwitchOn} loading={isTogglingAc} onChange={handleToggleAcSwitch} />
</View>
- <View className={`car-card-temp-switch ${acSwitchOn ? 'car-card-temp-switch-open' : ''}`.trim()} onClick={handleToggleAcSwitch}>
- <View className="car-card-temp-switch-icon">
- {
- isTogglingAc ?
- <>
- <Image className="car-card-temp-switch-img" style={{ visibility: 'hidden' }} src={AC_SWITCH_OPEN_ICON} />
- <Image
- className="car-card-temp-switch-img car-card-temp-switch-img-spinning"
- style={{ position: 'absolute', left: 0, top: 0 }}
- src={AC_SWITCH_LOADING_ICON}
- />
- </>
- :
- acSwitchOn ?
- <Image className="car-card-temp-switch-img" src={AC_SWITCH_CLOSE_ICON} />
- :
- <Image className="car-card-temp-switch-img" src={AC_SWITCH_OPEN_ICON} />
- }
+ <View className='tf-card-bottom'>
+ {acStatusLoaded && (
+ <View className='tf-card-ac-temp'>
+ <Text className='tf-card-ac-temp-value'>{cardTempText}</Text>
+ {shouldShowTemperatureUnit && <Text className='tf-card-ac-temp-unit'>°C</Text>}
+ </View>
+ )}
+ <View className='tf-card-footer'>
+ <Text className='tf-card-footer-text'>调节温度|通风加热</Text>
+ <Image className='tf-card-footer-arrow' src={AC_ARROW_ICON} />
</View>
- <View className="car-card-temp-switch-text">{acSwitchOn ? '点击关闭' : '点击开启'}</View>
</View>
</View>
- </View>
+
+ {/* 温度调节面板(底部弹窗,fixed 定位覆盖全屏) */}
+ {shouldRender && (
+ <View className={`ac-popup ${panelVisible ? 'show' : ''}`} catchMove>
+ <View className='popup-mask' onClick={closePanel}></View>
+ <View className='popup-content' animation={animationData}>
+ {/* 座椅通风/加热区(顶部满宽,关闭按钮浮于右上角) */}
+ <View className='popup-seat'>
+ <Image className='popup-close' src={PANEL_CLOSE_ICON} onClick={closePanel} />
+ <SeatHvArea
+ tripStatus={tripStatus}
+ orderId={orderId}
+ pageDidShowVersion={pageDidShowVersion}
+ hasPreviousOrder={hasPreviousOrder}
+ active={panelVisible}
+ />
+ </View>
+
+ {/* 空调温度滑动选择(底部白色圆角卡片) */}
+ <View className='popup-ac'>
+ <Text className='popup-ac-title'>温度调节</Text>
+ <View className='popup-temp-wrap'>
+ <ScrollView
+ className='popup-temp-scroll'
+ scrollX
+ showScrollbar={false}
+ scrollWithAnimation
+ scrollLeft={tempScrollLeft}
+ onScroll={handleTempScroll}
+ >
+ <View className='popup-temp-track'>
+ {TEMP_OPTIONS.map((value, index) => {
+ const selected = index === centeredTempIndex;
+ return (
+ <View
+ key={value}
+ className={`popup-temp-item ${selected ? 'active' : ''} ${selected && !acSwitchOn ? 'active-off' : ''}`}
+ onClick={() => handleTempItemClick(index, value)}
+ >
+ <Text className='popup-temp-item-value'>{value.toFixed(1)}</Text>
+ {selected && <Text className='popup-temp-item-unit'>℃</Text>}
+ </View>
+ );
+ })}
+ </View>
+ </ScrollView>
+ </View>
+ <View className='popup-ac-switch-row'>
+ <AcSwitch checked={acSwitchOn} loading={isTogglingAc} onChange={handleToggleAcSwitch} />
+ <Text className='popup-ac-switch-text'>
+ {isTogglingAc ? (acTogglingTarget ? '开启中' : '关闭中') : '空调开关'}
+ </Text>
+ </View>
+ </View>
+ </View>
+ </View>
+ )}
+ </View>
);
}
diff --git a/src/robotaxi/pages/begin-route/components/calling-car-modules/temperature-fragrance-control.tsx b/src/robotaxi/pages/begin-route/components/calling-car-modules/temperature-fragrance-control.tsx
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/robotaxi/pages/begin-route/components/calling-car-modules/temperature-fragrance-control.tsx
@@ -0,0 +1 @@
+
diff --git a/src/robotaxi/pages/begin-route/components/calling-car-modules/trip-action-bar.tsx b/src/robotaxi/pages/begin-route/components/calling-car-modules/trip-action-bar.tsx
index b97610e..cd3a716 100644
--- a/src/robotaxi/pages/begin-route/components/calling-car-modules/trip-action-bar.tsx
+++ b/src/robotaxi/pages/begin-route/components/calling-car-modules/trip-action-bar.tsx
@@ -29,14 +29,14 @@
{
key: "cancel",
label: "取消行程",
- icon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/cancel-trip.png',
+ icon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/cancel-trip-20260716.png',
visible: showCancelTrip,
onClick: handleCancelTrip
},
{
key: "destination",
label: "修改目的地",
- icon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/change-destination.png',
+ icon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/change-destination-20260716.png',
visible: canShowTripAction,
disabled: !showModifyDestination,
onClick: handleModifyDestination
@@ -44,7 +44,7 @@
{
key: "share",
label: "分享行程",
- icon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/share-trip.png',
+ icon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/share-trip-20260716.png',
visible: canShowTripAction && showShareTrip,
onClick: handleShareTrip
}
@@ -57,7 +57,7 @@
return (
<View className={`trip-action-bar${hasCancelTripAction ? '' : ' trip-action-bar-without-cancel'}`}>
- {actionList.map((item, index) => (
+ {actionList.map((item) => (
<Fragment key={item.key}>
{item.key === 'share' ? (
<Button
@@ -87,7 +87,6 @@
<Text className="trip-action-bar-item-text">{item.label}</Text>
</View>
)}
- {index < actionList.length - 1 && <View className="trip-action-bar-divider" />}
</Fragment>
))}
</View>
diff --git a/src/robotaxi/pages/begin-route/components/calling-car-modules/trip-status-header.tsx b/src/robotaxi/pages/begin-route/components/calling-car-modules/trip-status-header.tsx
index 1f64e01..cb1e294 100644
--- a/src/robotaxi/pages/begin-route/components/calling-car-modules/trip-status-header.tsx
+++ b/src/robotaxi/pages/begin-route/components/calling-car-modules/trip-status-header.tsx
@@ -1,151 +1,222 @@
-import { View, Image } from "@tarojs/components";
-import { useEffect, useState } from "react";
-import { formatCountdown } from '../../../../../utils/Robotaxi-public-method';
+import Taro from '@tarojs/taro';
+import { View, Image, Text } from '@tarojs/components';
+import { useEffect, useState } from 'react';
import { queryOrderQueueStatus } from '../../../../api/begin-route';
import { TripStatus } from '../../../../constants/trip-status';
type QueueStatus = {
- currentPosition: number;
- estimatedWaitTime: number;
- status: number;
- totalCount: number;
+ currentPosition: number;
+ estimatedWaitTime: number | null;
+ status: number;
+ totalCount: number;
} | null;
type Props = {
- orderId?: string;
- tripStatus: TripStatus;
- waitMinutes: number | null;
- durationDisplay: { value: string | number; unit: string };
- isArriveSoon: boolean;
- isPickupCountdownSoon: boolean;
- pickupCountdownSeconds: number | null;
- doorUnlockedInCurrentPage: boolean;
- finishCountdownEnabled: boolean;
- finishCountdownSeconds: number | null;
- limitMinutes: number | null;
- handleOpenCancelPopup?: (displayInfo: { durationDisplay: { value: string | number; unit: string }; waitMinutes: number | null }) => void;
- queuePollingTimeSeconds: number;
+ orderId?: string;
+ tripStatus: TripStatus;
+ waitMinutes: number | null;
+ durationDisplay: { value: string | number; unit: string };
+ isArriveSoon: boolean;
+ isPickupCountdownSoon: boolean;
+ pickupCountdownSeconds: number | null;
+ doorUnlockedInCurrentPage: boolean;
+ finishCountdownEnabled: boolean;
+ finishCountdownSeconds: number | null;
+ limitMinutes: number | null;
+ routeDistance?: { value: number; unit: 'km' | 'm' } | null;
+ handleOpenCancelPopup?: (displayInfo: { durationDisplay: { value: string | number; unit: string }; waitMinutes: number | null }) => void;
+ queuePollingTimeSeconds: number;
};
export default function TripStatusHeader({
- orderId,
- tripStatus,
- waitMinutes,
- durationDisplay,
- isArriveSoon,
- isPickupCountdownSoon,
- pickupCountdownSeconds,
- doorUnlockedInCurrentPage,
- finishCountdownEnabled,
- finishCountdownSeconds,
- limitMinutes,
- handleOpenCancelPopup,
- queuePollingTimeSeconds
+ orderId,
+ tripStatus,
+ waitMinutes,
+ durationDisplay,
+ isArriveSoon,
+ isPickupCountdownSoon,
+ pickupCountdownSeconds,
+ doorUnlockedInCurrentPage,
+ finishCountdownEnabled,
+ finishCountdownSeconds,
+ limitMinutes,
+ routeDistance = null,
+ handleOpenCancelPopup,
+ queuePollingTimeSeconds
}: Props) {
- const [queueStatus, setQueueStatus] = useState<QueueStatus>(null);
- const pollingIntervalMs = Math.max(1000, Number(queuePollingTimeSeconds || 10) * 1000); // 排队位次轮询间隔
- const inServiceDurationDisplay = durationDisplay.unit === '秒'
- ? { value: 1, unit: '分钟' as const }
- : durationDisplay; // 行程中剩余时间小于 1 分钟时按 1 分钟展示
+ const [queueStatus, setQueueStatus] = useState<QueueStatus>(null);
+ const pollingIntervalMs = Math.max(1000, Number(queuePollingTimeSeconds || 10) * 1000); // 排队位次轮询间隔
+ const normalizedDurationDisplay = durationDisplay.unit === '秒'
+ ? { value: 1, unit: '分钟' as const }
+ : durationDisplay; // 剩余时间小于 1 分钟时按 1 分钟展示,不展示秒
+ const inServiceDurationDisplay = normalizedDurationDisplay; // 行程中沿用统一的归一化时长
+ // 排队接口返回实时预计等待分钟数;无有效值时回退下单时的初始预估。
+ const displayedWaitMinutes = queueStatus?.estimatedWaitTime ?? waitMinutes;
- useEffect(() => {
- if (tripStatus !== TripStatus.CallingCar || !orderId) {
- setQueueStatus(null);
- return;
+ useEffect(() => {
+ if (tripStatus !== TripStatus.CallingCar || !orderId) {
+ setQueueStatus(null);
+ return;
+ }
+
+ let timer: ReturnType<typeof setInterval> | null = null;
+ let unmounted = false;
+
+ // 排队位次是动态变化的,状态为 3 时定时刷新
+ const fetchQueueStatus = async () => {
+ try {
+ const res = await queryOrderQueueStatus({ orderId });
+ if (unmounted) {
+ return;
}
+ const queueData = res?.data ?? res;
+ const rawEstimatedWaitTime = queueData?.estimatedWaitTime;
+ const estimatedWaitTime = rawEstimatedWaitTime === null || rawEstimatedWaitTime === undefined || rawEstimatedWaitTime === ''
+ ? null
+ : Number(rawEstimatedWaitTime);
+ if (
+ Number(queueData?.currentPosition) > 0 &&
+ Number(queueData?.totalCount) > 0
+ ) {
+ setQueueStatus({
+ currentPosition: Number(queueData.currentPosition),
+ estimatedWaitTime: typeof estimatedWaitTime === 'number' && Number.isFinite(estimatedWaitTime) && estimatedWaitTime >= 0 ? estimatedWaitTime : null,
+ status: Number(queueData.status) || 0,
+ totalCount: Number(queueData.totalCount)
+ });
+ return;
+ }
+ setQueueStatus(null);
+ } catch (_) {
+ if (!unmounted) {
+ setQueueStatus(null);
+ }
+ }
+ };
- let timer: ReturnType<typeof setInterval> | null = null;
- let unmounted = false;
+ fetchQueueStatus();
+ timer = setInterval(fetchQueueStatus, pollingIntervalMs);
- // 排队位次是动态变化的,状态为 3 时定时刷新
- const fetchQueueStatus = async () => {
- try {
- const res = await queryOrderQueueStatus({ orderId });
- if (unmounted) {
- return;
- }
- const queueData = res?.data ?? res;
- if (
- Number(queueData?.currentPosition) > 0 &&
- Number(queueData?.totalCount) > 0
- ) {
- setQueueStatus({
- currentPosition: Number(queueData.currentPosition),
- estimatedWaitTime: Number(queueData.estimatedWaitTime) || 0,
- status: Number(queueData.status) || 0,
- totalCount: Number(queueData.totalCount)
- });
- return;
- }
- setQueueStatus(null);
- } catch (_) {
- if (!unmounted) {
- setQueueStatus(null);
- }
- }
- };
+ return () => {
+ unmounted = true;
+ if (timer) {
+ clearInterval(timer);
+ }
+ };
+ }, [tripStatus, orderId, pollingIntervalMs]);
- fetchQueueStatus();
- timer = setInterval(fetchQueueStatus, pollingIntervalMs);
+ // 标题行文案映射(已改为按状态直接渲染 DOM,保留注释供参考)
+ // const titleMap = {
+ // [TripStatus.CallingCar]: <>预计<View className='time'>{waitMinutes}</View>分钟内有车应答</>,
+ // [TripStatus.VehicleApproaching]: isArriveSoon
+ // ? <>车辆即将到达,请准备上车</>
+ // : <>车辆还有<View className='time'>{durationDisplay.value}</View>{durationDisplay.unit}到达</>,
+ // [TripStatus.VehicleArrivedPickup]: doorUnlockedInCurrentPage
+ // ? <>车辆已到达,请尽快上车</>
+ // : <>{isPickupCountdownSoon ? '车辆已到达,请尽快上车' : '车辆已到达上车点'}<View className='time'>{pickupCountdownSeconds}</View></>,
+ // [TripStatus.InService]: <>预计<View className='time'>{inServiceDurationDisplay.value}</View>{inServiceDurationDisplay.unit}抵达目的地</>,
+ // [TripStatus.ArrivedDestination]: (finishCountdownEnabled || finishCountdownSeconds !== null)
+ // ? <>已到达目的地<View className='time'>{finishCountdownSeconds}</View></>
+ // : <>已到达目的地</>,
+ // [TripStatus.TripFinished]: '行程已结束',
+ // [TripStatus.PaymentSuccess]: '支付成功'
+ // }
- return () => {
- unmounted = true;
- if (timer) {
- clearInterval(timer);
- }
- };
- }, [tripStatus, orderId, pollingIntervalMs]);
+ // 说明文案映射(已注释)
+ // const descMap = {
+ // [TripStatus.CallingCar]: { text: '正全力为您叫车中,请耐心等待...', className: 'calling-car-content-desc' },
+ // [TripStatus.VehicleApproaching]: limitMinutes ? { text: `此处限停${limitMinutes}分钟,请尽快前往上车点`, className: 'calling-car-content-warning' } : null,
+ // [TripStatus.VehicleArrivedPickup]: doorUnlockedInCurrentPage
+ // ? { text: '车门已解锁,请尽快上车', className: 'calling-car-content-desc' }
+ // : { text: isPickupCountdownSoon ? `车辆到达后最多等待${limitMinutes}分钟,超时自动取消` : '此处限停,请在倒计时结束前到达上车点', className: 'calling-car-content-desc' },
+ // [TripStatus.InService]: { text: '行程已开始,请全程系好安全带', className: 'calling-car-content-desc' },
+ // [TripStatus.ArrivedDestination]: { text: '倒计时结束时车辆将自动驶离,请带好物品安全下车', className: 'calling-car-content-desc' },
+ // [TripStatus.TripFinished]: { text: '订单待支付,请核对后完成支付', className: 'calling-car-content-desc' }
+ // }
- // 标题行文案映射
- const titleMap = {
- [TripStatus.CallingCar]: <>预计<View className="time">{waitMinutes}</View>分钟内有车应答</>,
- [TripStatus.VehicleApproaching]: isArriveSoon
- ? <>车辆即将到达,请准备上车</>
- : <>车辆还有<View className="time">{durationDisplay.value}</View>{durationDisplay.unit}到达</>,
- [TripStatus.VehicleArrivedPickup]: doorUnlockedInCurrentPage
- ? <>车辆已到达,请尽快上车</>
- : <>{isPickupCountdownSoon ? '车辆已到达,请尽快上车' : '车辆已到达上车点'}<View className="time" style={isPickupCountdownSoon ? { color: '#f58900' } : undefined}>{pickupCountdownSeconds !== null ? formatCountdown(pickupCountdownSeconds) : '--:--'}</View></>,
- [TripStatus.InService]: <>预计<View className="time">{inServiceDurationDisplay.value}</View>{inServiceDurationDisplay.unit}抵达目的地</>,
- [TripStatus.ArrivedDestination]: (finishCountdownEnabled || finishCountdownSeconds !== null)
- ? <>已到达目的地<View className="time">{finishCountdownSeconds !== null ? formatCountdown(finishCountdownSeconds) : '--:--'}</View></>
- : <>已到达目的地</>,
- [TripStatus.TripFinished]: '行程已结束',
- [TripStatus.PaymentSuccess]: '支付成功'
- }
+ // 站点信息(图二):与 cancel-popup 一致,优先取缓存的起终点
+ const cachedStartSite = Taro.getStorageSync('startSite') || {};
+ const cachedEndSite = Taro.getStorageSync('endSite') || {};
+ const startSiteName = cachedStartSite?.name || cachedStartSite?.address || '-';
+ const endSiteName = cachedEndSite?.name || cachedEndSite?.address || '-';
- // 说明文案映射
- const descMap = {
- [TripStatus.CallingCar]: { text: '正全力为您叫车中,请耐心等待...', className: 'calling-car-content-desc' },
- [TripStatus.VehicleApproaching]: limitMinutes ? { text: `此处限停${limitMinutes}分钟,请尽快前往上车点`, className: 'calling-car-content-warning' } : null,
- [TripStatus.VehicleArrivedPickup]: doorUnlockedInCurrentPage
- ? { text: '车门已解锁,请尽快上车', className: 'calling-car-content-desc' }
- : { text: isPickupCountdownSoon ? `车辆到达后最多等待${limitMinutes}分钟,超时自动取消` : '此处限停,请在倒计时结束前到达上车点', className: 'calling-car-content-desc' },
- [TripStatus.InService]: { text: '行程已开始,请全程系好安全带', className: 'calling-car-content-desc' },
- [TripStatus.ArrivedDestination]: { text: '倒计时结束时车辆将自动驶离,请带好物品安全下车', className: 'calling-car-content-desc' },
- [TripStatus.TripFinished]: { text: '订单待支付,请核对后完成支付', className: 'calling-car-content-desc' }
- }
+ // 状态栏右侧的距离/时间文案
+ const distanceText = routeDistance ? `${routeDistance.value}${routeDistance.unit === 'm' ? 'M' : 'KM'}` : '';
+ const approachingTimeText = `${normalizedDurationDisplay.value}MIN`;
+ const inServiceTimeText = `${normalizedDurationDisplay.value}MIN`;
- return (
- <View className='calling-car-content'>
- <View>
- {titleMap[tripStatus] && (
- <View className='calling-car-content-title'>{titleMap[tripStatus]}</View>
+ return (
+ <>
+ {/* 呼叫车中:DOM 结构与样式保持不变(图一 + 站点卡片) */}
+ {tripStatus === TripStatus.CallingCar && (
+ <>
+ <View className='calling-car-content'>
+ <View className='calling-car-content-main'>
+ <View className='calling-car-content-title calling-car-content-title--strong'>
+ 预计{displayedWaitMinutes ?? '-'}分钟内有车应答
+ </View>
+ <View className='calling-car-content-desc'>
+ 正全力为您叫车中...
+ {queueStatus && (
+ <>
+ <Text className='calling-car-content-desc-divider'>|</Text>
+ 第{queueStatus.currentPosition}位/共{queueStatus.totalCount}位
+ </>
)}
- {descMap[tripStatus] && (
- <View style={{ paddingBottom: tripStatus === TripStatus.CallingCar && !queueStatus ? '20px' : '0px' }} className={descMap[tripStatus]?.className}>{descMap[tripStatus]?.text}</View>
- )}
- {tripStatus === TripStatus.CallingCar && queueStatus && (
- <View className='calling-car-content-desc'>
- 第{queueStatus.currentPosition}位/共{queueStatus.totalCount}位
- </View>
- )}
+ </View>
</View>
- {
- [TripStatus.CallingCar].includes(tripStatus) &&
- <View className="cancel-btn">
- <Image onClick={() => handleOpenCancelPopup?.({ durationDisplay, waitMinutes })} src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/cancel-btn.png' className="cancel" />
- </View>
- }
+ <View
+ className='calling-car-cancel-btn'
+ onClick={() => handleOpenCancelPopup?.({ durationDisplay, waitMinutes: displayedWaitMinutes })}
+ >
+ 取消行程
+ </View>
+ </View>
+ <View className='calling-car-site-box'>
+ <View className='calling-car-site-box-icon'>
+ <Image className='calling-car-site-check'src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/route-state-icon-golden-20260702.png' ></Image>
+ <Image className='dotted-line' src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/dotted-line.png' />
+ <Image className='calling-car-site-check' src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/route-state-icon-black-20260702.png'></Image>
+ </View>
+ <View className='calling-car-site-box-content'>
+ <View>{startSiteName}</View>
+ <View className='calling-car-site-box-divider'></View>
+ <View>{endSiteName}</View>
+ </View>
+ </View>
+ </>
+ )}
+
+ {/* 车辆即将到达:左右布局状态栏(图一) */}
+ {tripStatus === TripStatus.VehicleApproaching && (
+ <View className='calling-car-status-box'>
+ <Image className='calling-car-status-box-come-icon' mode='widthFix' src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/come-icon-20260728.webp'></Image>
+ <View className='calling-car-status-box-left'>{isArriveSoon ? '即将抵达' : '正在赶来'}</View>
+ <View className='calling-car-status-box-right'>
+ {distanceText && <View className='distance'>{distanceText}</View>}
+ <View className='time'>{approachingTimeText}</View>
+ </View>
</View>
- );
+ )}
+
+ {/* 车辆到达上车点:直接展示图片 */}
+ {tripStatus === TripStatus.VehicleArrivedPickup && (
+ <View className='calling-car-status-box calling-car-arrived-img'>
+ <View className='calling-car-status-box-left'>车辆已到达上车点,倒计时结束将自动取消订单</View>
+ </View>
+ )}
+
+ {/* 行程中:左右布局状态栏 */}
+ {tripStatus === TripStatus.InService && (
+ <View className='calling-car-status-box'>
+ <Image className='calling-car-status-box-come-icon' mode='widthFix' src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/come-icon-20260728.webp'></Image>
+ <View className='calling-car-status-box-left'>正在前往目的地</View>
+ <View className='calling-car-status-box-right'>
+ {distanceText && <View className='distance'>{distanceText}</View>}
+ <View className='time'>{inServiceTimeText}</View>
+ </View>
+ </View>
+ )}
+ </>
+ );
}
diff --git a/src/robotaxi/pages/begin-route/components/calling-car-modules/trunk-confirm-popup/index.less b/src/robotaxi/pages/begin-route/components/calling-car-modules/trunk-confirm-popup/index.less
new file mode 100644
index 0000000..d6b2ca1
--- /dev/null
+++ b/src/robotaxi/pages/begin-route/components/calling-car-modules/trunk-confirm-popup/index.less
@@ -0,0 +1,75 @@
+.trunk-confirm-popup {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 99999;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .trunk-confirm-mask {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.8);
+ }
+
+ .trunk-confirm-content {
+ position: relative;
+ width: 600rpx;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 72rpx 40rpx 48rpx 40rpx;
+ gap: 64rpx;
+ border-radius: 48rpx;
+ background: #FFFFFF;
+ text-align: center;
+ }
+
+ .trunk-confirm-title {
+ font-family: PingFang SC;
+ font-size: 36rpx;
+ font-weight: 500;
+ line-height: 52rpx;
+ color: rgba(0, 0, 0, 0.9);
+ }
+
+ .trunk-confirm-btns {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ width: 100%;
+ gap: 20rpx;
+ }
+
+ .trunk-confirm-btn {
+ flex: 1;
+ height: 110rpx;
+ box-sizing: border-box;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: 110rpx;
+ font-family: PingFang SC;
+ font-size: 36rpx;
+ font-weight: 500;
+ line-height: 52rpx;
+ }
+
+ .trunk-confirm-btn-cancel {
+ background: #FFFFFF;
+ border: 4rpx solid #141E27;
+ color: #141E27;
+ }
+
+ .trunk-confirm-btn-primary {
+ background: #141E27;
+ color: #FFFFFF;
+ }
+}
diff --git a/src/robotaxi/pages/begin-route/components/calling-car-modules/trunk-confirm-popup/index.tsx b/src/robotaxi/pages/begin-route/components/calling-car-modules/trunk-confirm-popup/index.tsx
new file mode 100644
index 0000000..68d4a40
--- /dev/null
+++ b/src/robotaxi/pages/begin-route/components/calling-car-modules/trunk-confirm-popup/index.tsx
@@ -0,0 +1,82 @@
+import Taro from '@tarojs/taro';
+import { View } from '@tarojs/components';
+import { useEffect, useRef, useState } from 'react';
+import './index.less';
+
+type Props = {
+ /** 是否展示弹窗 */
+ show?: boolean;
+ /** 点击「取消」回调 */
+ onCancel?: () => void;
+ /** 点击「确认」回调 */
+ onConfirm?: () => void;
+};
+
+const ANIMATION_DURATION_MS = 300; // 弹窗开合动画时长
+
+/**
+ * 后备箱开启二次确认弹窗(居中,取消/确认双按钮)。
+ * 容器/遮罩/标题沿用项目居中弹窗规范(over-popup),新增横向双按钮 + 实心主按钮。
+ */
+export default function TrunkConfirmPopup({ show = false, onCancel, onConfirm }: Props) {
+ const [visible, setVisible] = useState(false); // 关闭动画保活标记
+ const [animationData, setAnimationData] = useState<any>();
+ const closeTimerRef = useRef<any>(null);
+
+ // 控制弹窗开合:打开时立即渲染并入场,关闭时保留退场动画
+ useEffect(() => {
+ if (closeTimerRef.current) {
+ clearTimeout(closeTimerRef.current);
+ closeTimerRef.current = null;
+ }
+
+ if (show) {
+ setVisible(true);
+ const animation = Taro.createAnimation({ duration: ANIMATION_DURATION_MS, timingFunction: 'ease-out' });
+ animation.opacity(1).scale(1).step();
+ setAnimationData(animation.export());
+ return;
+ }
+
+ if (!visible) {
+ return;
+ }
+
+ const animation = Taro.createAnimation({ duration: ANIMATION_DURATION_MS, timingFunction: 'ease-in' });
+ animation.opacity(0).scale(0.8).step();
+ setAnimationData(animation.export());
+ closeTimerRef.current = setTimeout(() => {
+ setVisible(false);
+ closeTimerRef.current = null;
+ }, ANIMATION_DURATION_MS);
+ }, [show, visible]);
+
+ // 卸载时清理定时器
+ useEffect(() => {
+ return () => {
+ if (closeTimerRef.current) {
+ clearTimeout(closeTimerRef.current);
+ }
+ };
+ }, []);
+
+ if (!visible) {
+ return null;
+ }
+
+ return (
+ <View className='trunk-confirm-popup' catchMove>
+ <View className='trunk-confirm-mask'></View>
+ <View className='trunk-confirm-content' animation={animationData}>
+ <View className='trunk-confirm-title'>
+ <View>请确认后备箱周边安全</View>
+ <View>确认开启吗?</View>
+ </View>
+ <View className='trunk-confirm-btns'>
+ <View className='trunk-confirm-btn trunk-confirm-btn-cancel' onClick={onCancel}>取消</View>
+ <View className='trunk-confirm-btn trunk-confirm-btn-primary' onClick={onConfirm}>确认</View>
+ </View>
+ </View>
+ </View>
+ );
+}
diff --git a/src/robotaxi/pages/begin-route/components/calling-car-modules/use-minimum-spin-loading.ts b/src/robotaxi/pages/begin-route/components/calling-car-modules/use-minimum-spin-loading.ts
index dfb872d..45a4fb4 100644
--- a/src/robotaxi/pages/begin-route/components/calling-car-modules/use-minimum-spin-loading.ts
+++ b/src/robotaxi/pages/begin-route/components/calling-car-modules/use-minimum-spin-loading.ts
@@ -9,10 +9,20 @@
export default function useMinimumSpinLoading<T extends string>() {
/** 当前处于 loading 展示中的控件标识。 */
const [visibleLoadingKey, setVisibleLoadingKey] = useState<T | null>(null);
+ /** visibleLoadingKey 的同步镜像,供 stopLoading 内即时读取,避免闭包拿到旧值。 */
+ const visibleLoadingKeyRef = useRef<T | null>(null);
/** 当前 loading 开始时间戳。 */
const loadingStartedAtRef = useRef(0);
/** 延迟关闭 loading 的定时器。 */
const loadingFinishTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+
+ /**
+ * 同步更新 state 与 ref,保证两者一致。
+ */
+ const setKey = useCallback((key: T | null) => {
+ visibleLoadingKeyRef.current = key;
+ setVisibleLoadingKey(key);
+ }, []);
/**
* 清理延迟关闭定时器。
@@ -36,33 +46,34 @@
const startLoading = useCallback((key: T) => {
clearLoadingFinishTimer();
loadingStartedAtRef.current = Date.now();
- setVisibleLoadingKey(key);
- }, [clearLoadingFinishTimer]);
+ setKey(key);
+ }, [clearLoadingFinishTimer, setKey]);
/**
* 结束 loading 动画;若未满一圈则延迟到一圈完成后再关闭。
+ * @param onStopped loading 真正关闭(含最短时长补足)后触发,用于把 toast 等提示对齐到动画结束,避免 loading 仍在转就弹出提示。
*/
- const stopLoading = useCallback((key?: T | null) => {
- setVisibleLoadingKey((currentKey) => {
- if (!currentKey) {
- return currentKey;
- }
- if (key && currentKey !== key) {
- return currentKey;
- }
- const elapsed = Date.now() - loadingStartedAtRef.current;
- const remaining = Math.max(0, MINIMUM_SPIN_DURATION_MS - elapsed);
- clearLoadingFinishTimer();
- if (remaining === 0) {
- return null;
- }
- loadingFinishTimerRef.current = setTimeout(() => {
- setVisibleLoadingKey((latestKey) => (latestKey === currentKey ? null : latestKey));
- loadingFinishTimerRef.current = null;
- }, remaining);
- return currentKey;
- });
- }, [clearLoadingFinishTimer]);
+ const stopLoading = useCallback((key?: T | null, onStopped?: () => void) => {
+ const currentKey = visibleLoadingKeyRef.current;
+ // 无 loading 或指定 key 不匹配:不关闭,但仍执行回调作为兜底,保证提示不丢失。
+ if (!currentKey || (key && currentKey !== key)) {
+ onStopped?.();
+ return;
+ }
+ const elapsed = Date.now() - loadingStartedAtRef.current;
+ const remaining = Math.max(0, MINIMUM_SPIN_DURATION_MS - elapsed);
+ clearLoadingFinishTimer();
+ if (remaining === 0) {
+ setKey(null);
+ onStopped?.();
+ return;
+ }
+ loadingFinishTimerRef.current = setTimeout(() => {
+ setKey(null);
+ loadingFinishTimerRef.current = null;
+ onStopped?.();
+ }, remaining);
+ }, [clearLoadingFinishTimer, setKey]);
return {
visibleLoadingKey,
diff --git a/src/robotaxi/pages/begin-route/components/calling-car-modules/vehicle-info-card.tsx b/src/robotaxi/pages/begin-route/components/calling-car-modules/vehicle-info-card.tsx
index fced141..65615e1 100644
--- a/src/robotaxi/pages/begin-route/components/calling-car-modules/vehicle-info-card.tsx
+++ b/src/robotaxi/pages/begin-route/components/calling-car-modules/vehicle-info-card.tsx
@@ -1,6 +1,4 @@
-import Taro from "@tarojs/taro";
-import { View, Image } from "@tarojs/components";
-import { safeParse } from '../../../../../utils/Robotaxi-public-method';
+import { View } from "@tarojs/components";
type Props = {
vehicleInfo?: {
@@ -12,25 +10,19 @@
};
export default function VehicleInfoCard({ vehicleInfo }: Props) {
- // 展示车辆基础信息与车辆图片
+ // 展示车辆基础信息:左侧车牌·车身颜色,右侧车型
return (
<View className="car-card-detail">
- <View>
+ <View className="car-card-detail-plate">
<View className="car-card-detail-num">{vehicleInfo?.plateNumber}</View>
- <View className="car-card-detail-color">
- <View className="car-card-detail-color-text">小鹏GX</View>
- <View className="car-card-detail-color-line"></View>
- <View className="car-card-detail-color-text">{vehicleInfo?.vehicleColor}</View>
- {/* 车身编码: {vehicleInfo?.vehicleCode} */}
- </View>
+ {vehicleInfo?.vehicleColor && (
+ <>
+ <View className="car-card-detail-dot"></View>
+ <View className="car-card-detail-color-text">{vehicleInfo?.vehicleColor}</View>
+ </>
+ )}
</View>
- {
- vehicleInfo?.vehiclePictures &&
- <Image
- className="car-card-detail-img"
- src={safeParse(vehicleInfo?.vehiclePictures, {})?.url || 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/vehicle-default.png'}
- />
- }
+ <View className="car-card-detail-model">小鹏GX</View>
</View>
);
}
diff --git a/src/robotaxi/pages/begin-route/components/calling-car.tsx b/src/robotaxi/pages/begin-route/components/calling-car.tsx
index 210bdf3..fbd32f1 100644
--- a/src/robotaxi/pages/begin-route/components/calling-car.tsx
+++ b/src/robotaxi/pages/begin-route/components/calling-car.tsx
@@ -1,10 +1,14 @@
import { View, Image } from "@tarojs/components";
+import { useState, useEffect } from 'react';
import './css-folder/calling-car.less'
import { formatRemainingDurationDisplay } from '../../../utils/format';
+import { safeParse, formatTripDistance } from '../../../../utils/Robotaxi-public-method';
import { TripStatus } from '../../../constants/trip-status';
+import { BLUETOOTH_ICON_OFF } from '../hooks/use-bluetooth-access'; // 蓝牙未开/未授权图标
import TripStatusHeader from './calling-car-modules/trip-status-header'; // 行程状态头
import VehicleInfoCard from './calling-car-modules/vehicle-info-card'; // 车辆信息卡片
-import TemperatureControl from './calling-car-modules/temperature-control'; // 温度控制
+import TemperatureControl from './calling-car-modules/temperature-control'; // 空调温控(含温度调节面板 + 座椅通风加热)
+import FragranceControl from './calling-car-modules/fragrance-control'; // 车载香氛控制
import TripActionBar from './calling-car-modules/trip-action-bar'; // 行程操作栏
import DoorTrunkControl from './calling-car-modules/door-trunk-control'; // 门箱控制
import FareDetailCard from './calling-car-modules/fare-detail-card'; // 费用明细卡片
@@ -50,6 +54,8 @@
handleShareTrip?: () => void;
/** 预估行程秒数 */
routeDurationSeconds?: number | null;
+ /** 剩余距离(来自 ORDER_ROUTE.route.distance) */
+ routeDistance?: { value: number; unit: 'km' | 'm' } | null;
/** 是否启用到达终点后的离车倒计时 */
finishCountdownEnabled?: boolean;
/** 重置费用明细展开态的版本号 */
@@ -119,6 +125,7 @@
showShareTrip = true,
handleShareTrip,
routeDurationSeconds,
+ routeDistance = null,
finishCountdownEnabled = false,
resetDetailVersion,
waitMinutes = null,
@@ -136,7 +143,7 @@
bluetoothStatusReady = false,
bluetoothCardDescription = '授权并打开蓝牙链接,享受无感出行',
bluetoothCardActionText = '去开启',
- bluetoothCardIconSrc = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/close-bluetooth.png',
+ bluetoothCardIconSrc = BLUETOOTH_ICON_OFF,
bluetoothCardDescriptionColor = '',
bluetoothCardClickable = true,
handleBluetoothCardClick,
@@ -160,62 +167,109 @@
/** 开门后,接驾态不再展示取消行程。 */
const showCancelTrip = [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus) && !doorUnlockedInCurrentPage;
+ /** 是否为“到达终点/待支付”状态(图一、图二) */
+ const isTripFinished = tripStatus === TripStatus.TripFinished;
+ /** 费用明细是否展开(拦截 FareDetailCard 的回调本地记录,用于切换背景与卡片布局) */
+ const [detailExpanded, setDetailExpanded] = useState(false);
+ /** 大号里程文案:取全程里程,单位参考 formatTripDistance(米→M,公里→KM) */
+ const finishDistanceText = formatTripDistance(fareBreakdown?.kilometer).replace('公里', 'KM').replace('米', 'M');
+
+ // 外部重置版本变化或离开待支付态时收起,保持与 FareDetailCard 同步
+ useEffect(() => {
+ setDetailExpanded(false);
+ }, [resetDetailVersion, tripStatus]);
+
+ // 拦截费用明细展开/收起回调:本地记录展开态后再透传给外部
+ const handleFareToggle = (expanded: boolean, hasFare: boolean) => {
+ setDetailExpanded(expanded);
+ if (typeof onToggleDetailFade === 'function') {
+ onToggleDetailFade(expanded, hasFare);
+ }
+ };
+
+ // 根节点状态类:待支付态 + 明细展开态
+ const rootClassName = `calling-car${isTripFinished ? ' calling-car--finished' : ''}${isTripFinished && detailExpanded ? ' is-detail-expanded' : ''}`;
+ // 待支付态使用专属背景图,其余状态沿用叫车成功背景图
+ const bgImageSrc = isTripFinished
+ ? 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/trip-finished-20260703.png'
+ : 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/card-bg-20260714.png';
+
// 按状态组合渲染行程卡片模块
return (
- <View className='calling-car'>
- <Image className="calling-car-bg" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/site-top-bg-20260507.png' />
-
+ <View className={rootClassName}>
+ <Image className={`calling-car-bg ${isTripFinished ? 'finished-bg' : ''}`} src={bgImageSrc} />
+ {
+ [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService].includes(tripStatus) && vehicleInfo?.vehiclePictures &&
+ <Image
+ className='calling-car-vehicle-pic'
+ src={safeParse(vehicleInfo?.vehiclePictures, {})?.url || 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/default-vehicle-img-20260724-v2.png'}
+ />
+ }
+ <View className='calling-car-content-box'>
+ {
+ isTripFinished && !detailExpanded &&
+ <View className='trip-finished-header'>
+ <View className='trip-finished-header-title'>到达目的地</View>
+ <View className='trip-finished-header-distance'>{finishDistanceText}</View>
+ </View>
+ }
+ {
+ [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService, TripStatus.PaymentSuccess].includes(tripStatus) &&
+ <VehicleInfoCard vehicleInfo={vehicleInfo} />
+ }
<TripStatusHeader
- orderId={orderId}
- tripStatus={tripStatus}
- waitMinutes={waitMinutes}
- durationDisplay={durationDisplay}
- isArriveSoon={isArriveSoon}
- isPickupCountdownSoon={isPickupCountdownSoon}
- pickupCountdownSeconds={pickupCountdownSeconds}
- doorUnlockedInCurrentPage={doorUnlockedInCurrentPage}
- finishCountdownEnabled={finishCountdownEnabled}
- finishCountdownSeconds={finishCountdownSeconds}
- limitMinutes={limitMinutes}
- handleOpenCancelPopup={handleOpenCancelPopup}
- queuePollingTimeSeconds={queuePollingTimeSeconds}
+ orderId={orderId}
+ tripStatus={tripStatus}
+ waitMinutes={waitMinutes}
+ durationDisplay={durationDisplay}
+ isArriveSoon={isArriveSoon}
+ isPickupCountdownSoon={isPickupCountdownSoon}
+ pickupCountdownSeconds={pickupCountdownSeconds}
+ doorUnlockedInCurrentPage={doorUnlockedInCurrentPage}
+ finishCountdownEnabled={finishCountdownEnabled}
+ finishCountdownSeconds={finishCountdownSeconds}
+ limitMinutes={limitMinutes}
+ routeDistance={routeDistance}
+ handleOpenCancelPopup={handleOpenCancelPopup}
+ queuePollingTimeSeconds={queuePollingTimeSeconds}
/>
{
[TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus) && bluetoothStatusReady &&
<BluetoothOperateCard
- description={bluetoothCardDescription}
- actionText={bluetoothCardActionText}
- onClick={handleBluetoothCardClick || (() => { })}
- iconSrc={bluetoothCardIconSrc}
- descriptionColor={bluetoothCardDescriptionColor}
- clickable={bluetoothCardClickable}
+ description={bluetoothCardDescription}
+ actionText={bluetoothCardActionText}
+ onClick={handleBluetoothCardClick || (() => { })}
+ iconSrc={bluetoothCardIconSrc}
+ descriptionColor={bluetoothCardDescriptionColor}
+ clickable={bluetoothCardClickable}
/>
}
{
[TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus) &&
<AuthBluetoothPopup
- show={showAuthBluetoothPopup}
- onClose={handleCloseAuthBluetoothPopup}
- onAuthorize={handleAuthorizeBluetooth}
+ show={showAuthBluetoothPopup}
+ onClose={handleCloseAuthBluetoothPopup}
+ onAuthorize={handleAuthorizeBluetooth}
/>
}
{
[TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus) &&
<SuccessAuthPopup
- show={showSuccessAuthPopup}
- onClose={handleCloseSuccessAuthPopup || (() => { })}
+ show={showSuccessAuthPopup}
+ onClose={handleCloseSuccessAuthPopup || (() => { })}
/>
}
{
- [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService, TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(tripStatus) &&
- <View className="car-card" style={{ marginBottom: tripStatus === TripStatus.TripFinished ? '70px' : '0px' }}>
- <VehicleInfoCard vehicleInfo={vehicleInfo} />
-
- <TemperatureControl tripStatus={tripStatus} orderId={orderId} pageDidShowVersion={pageDidShowVersion} hasPreviousOrder={hasPreviousOrder} />
+ [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService].includes(tripStatus) &&
+ <View className='car-card'>
+ <View className='temp-fragrance-control'>
+ <TemperatureControl tripStatus={tripStatus} orderId={orderId} pageDidShowVersion={pageDidShowVersion} hasPreviousOrder={hasPreviousOrder} />
+ <FragranceControl tripStatus={tripStatus} orderId={orderId} pageDidShowVersion={pageDidShowVersion} hasPreviousOrder={hasPreviousOrder} />
+ </View>
<DoorTrunkControl
tripStatus={tripStatus}
@@ -228,27 +282,41 @@
/>
<FareDetailCard
- tripStatus={tripStatus}
- fareBreakdown={fareBreakdown}
- orderId={orderId}
- resetDetailVersion={resetDetailVersion}
- onToggleDetailFade={onToggleDetailFade}
- handlePay={handlePay}
- onOpenCouponSelect={onOpenCouponSelect}
+ tripStatus={tripStatus}
+ fareBreakdown={fareBreakdown}
+ orderId={orderId}
+ resetDetailVersion={resetDetailVersion}
+ onToggleDetailFade={onToggleDetailFade}
+ handlePay={handlePay}
+ onOpenCouponSelect={onOpenCouponSelect}
+ />
+ </View>
+ }
+ {
+ [TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(tripStatus) &&
+ <View className='car-card fare-box-card'>
+ <FareDetailCard
+ tripStatus={tripStatus}
+ fareBreakdown={fareBreakdown}
+ orderId={orderId}
+ resetDetailVersion={resetDetailVersion}
+ onToggleDetailFade={handleFareToggle}
+ handlePay={handlePay}
+ onOpenCouponSelect={onOpenCouponSelect}
/>
</View>
}
{
[TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService, TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(tripStatus) &&
<TripActionBar
- tripStatus={tripStatus}
- handleCancelTrip={handleCancelTrip}
- showCancelTrip={showCancelTrip}
- showShareTrip={showShareTrip}
- handleModifyDestination={handleModifyDestination}
- handleModifyDestinationDisabledClick={handleModifyDestinationDisabledClick}
- showModifyDestination={showModifyDestination}
- handleShareTrip={handleShareTrip}
+ tripStatus={tripStatus}
+ handleCancelTrip={handleCancelTrip}
+ showCancelTrip={showCancelTrip}
+ showShareTrip={showShareTrip}
+ handleModifyDestination={handleModifyDestination}
+ handleModifyDestinationDisabledClick={handleModifyDestinationDisabledClick}
+ showModifyDestination={showModifyDestination}
+ handleShareTrip={handleShareTrip}
/>
}
@@ -263,13 +331,15 @@
/>
<EvaluationCard
- tripStatus={tripStatus}
- orderId={orderId}
- handleOpenEvaluatePopup={handleOpenEvaluatePopup}
- evaluateSubmitVersion={evaluateSubmitVersion}
- evaluateCloseVersion={evaluateCloseVersion}
- evaluatePopupStatus={evaluatePopupStatus}
+ tripStatus={tripStatus}
+ orderId={orderId}
+ handleOpenEvaluatePopup={handleOpenEvaluatePopup}
+ evaluateSubmitVersion={evaluateSubmitVersion}
+ evaluateCloseVersion={evaluateCloseVersion}
+ evaluatePopupStatus={evaluatePopupStatus}
/>
+ </View>
+
</View>
);
}
diff --git a/src/robotaxi/pages/begin-route/components/cancel-popup.tsx b/src/robotaxi/pages/begin-route/components/cancel-popup.tsx
index 47100c6..3af9ee2 100644
--- a/src/robotaxi/pages/begin-route/components/cancel-popup.tsx
+++ b/src/robotaxi/pages/begin-route/components/cancel-popup.tsx
@@ -23,8 +23,21 @@
onConfirm?: () => void;
}
+// 占位动画图片地址:优先加载 webp,加载失败回退 png
+const CANCEL_ANIMATION_WEBP = 'https://xps01.xiaopeng.com/content/commomother/2026/07-09/14-45-520155-955631373.png?x-oss-process=image/format,webp';
+const CANCEL_ANIMATION_PNG = 'https://xps01.xiaopeng.com/content/commomother/2026/07-09/14-45-520155-955631373.png';
+
export default function CancelPopup({ show = false, cancelType = 1, startSite, endSite, waitMinutes, tripStatus, routeDurationSeconds, discountAmount, onClose, onConfirm }: Props) {
const { visible, animationData } = useBottomPopupAnimation(show);
+ // 占位动画当前使用的图片地址,默认 webp,加载失败时回退到 png
+ const [animationSrc, setAnimationSrc] = useState(CANCEL_ANIMATION_WEBP);
+
+ // webp 加载失败时回退到 png
+ const handleAnimationError = () => {
+ if (animationSrc !== CANCEL_ANIMATION_PNG) {
+ setAnimationSrc(CANCEL_ANIMATION_PNG);
+ }
+ };
const cachedStartSite = startSite || Taro.getStorageSync('startSite') || {};
const cachedEndSite = endSite || Taro.getStorageSync('endSite') || {};
const startSiteName = cachedStartSite?.name || cachedStartSite?.address || '-';
@@ -91,11 +104,13 @@
<View className='cancel-mask'></View>
<View className='cancel-content' animation={animationData}>
<View className='cancel-box'>
- <Image onClick={closePopup} className='cancel-icon' src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/close-icon.png" />
+ <Image onClick={closePopup} className='cancel-icon' src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/close-icon.png" />
</View>
<View className='cancel-tip-box'>
- <View className='cancel-tip-box-title'>{STATUS_MAP[cancelType].title}</View>
- <View className={`cancel-tip-box-desc ${cancelType === 4 && 'color1'}`}>{STATUS_MAP[cancelType].tip}</View>
+ <View className='cancel-tip-box-title'>{STATUS_MAP[cancelType]?.title}</View>
+ {
+ STATUS_MAP[cancelType]?.tip && <View className={`cancel-tip-box-desc ${cancelType === 4 && 'color1'}`}>{STATUS_MAP[cancelType].tip}</View>
+ }
</View>
{
[4, 5].includes(cancelType) ?
@@ -108,6 +123,7 @@
</View>
<View className='cancel-site-box-content'>
<View>{startSiteName}</View>
+ <View className='cancel-site-box-divider'></View>
<View>{endSiteName}</View>
</View>
</View>
@@ -116,10 +132,14 @@
</View>
</View>
:
- <View className='cancel-btn-box'>
- <Image className='cancel-btn-cancel' onClick={onConfirm} src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/submit-cancel-btn.png' />
- <View className='cancel-btn-again wait-btn' onClick={onClose}>再等等</View>
- </View>
+ <>
+ {/* 占位动画:优先 webp,加载失败回退 png */}
+ <Image className='cancel-animation' src={animationSrc} onError={handleAnimationError} />
+ <View className='cancel-btn-box'>
+ <View className=' cancel-btn-cancel' onClick={onConfirm}>确认取消</View>
+ <View className='cancel-btn-again wait-btn' onClick={onClose}>再等等</View>
+ </View>
+ </>
}
</View>
</View>
diff --git a/src/robotaxi/pages/begin-route/components/css-folder/calling-car.less b/src/robotaxi/pages/begin-route/components/css-folder/calling-car.less
index 3a82680..422c9cd 100644
--- a/src/robotaxi/pages/begin-route/components/css-folder/calling-car.less
+++ b/src/robotaxi/pages/begin-route/components/css-folder/calling-car.less
@@ -1,3 +1,5 @@
+@import '../../../../style/ui-variables.less';
+
@keyframes ac-switch-spin {
from {
transform: rotate(0deg);
@@ -10,675 +12,728 @@
.calling-car {
position: relative;
- padding: 40px 42px 68px 34px;
- background: #f2f3f7;
- border-top: 2px solid #fff;
- border-radius: 40px 40px 0 0;
+ padding: 148px 0px 0px 0px;
+ background: @gary-theme;
+ border-radius: 60px 60px 0 0;
overflow: hidden;
-
- &-bg {
+ .calling-car-content-box {
+ position: relative;
+ padding: 48px 48px 82px 48px;
+ background: @gary-theme;
+ border-radius: 60px 60px 0 0;
+ }
+ .calling-car-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
- height: 326px;
+ height: 282px;
+ &.finished-bg {
+ height: 844px;
+ }
}
-
- &-content {
- position: relative;
+ .calling-car-vehicle-pic {
+ position: absolute;
+ top: -4px;
+ right: 116px;
+ width: 338px;
+ height: 236px;
z-index: 1;
- display: flex;
- justify-content: space-between;
- margin-bottom: 40px;
-
- &-title {
- font-size: 32px;
- font-weight: 500;
- line-height: 36px;
- letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
- display: flex;
- align-items: center;
-
- .time {
- margin: 0 4px;
- font-size: 40px;
- line-height: 40px;
- color: #768aff;
- }
- }
-
- &-desc {
- margin-top: 8px;
- font-size: 24px;
- line-height: 34px;
- color: rgba(0, 0, 0, 0.4);
- }
-
- &-warning {
- margin-top: 8px;
- font-size: 24px;
- line-height: 34px;
- color: #f58900;
- }
-
- &-desc-red {
- margin-top: 8px;
- font-size: 24px;
- line-height: 34px;
- color: #f53f3f;
- }
-
- .cancel-btn {
- margin-top: 6px;
- height: 50px;
- display: flex;
- align-items: center;
- justify-content: center;
-
- .cancel {
- flex-shrink: 0;
- width: 144px;
- height: 48px;
- }
+ }
+}
+// 到达终点/待支付态(图一、图二):背景透明化 + 到达目的地头部 + 白色费用卡
+.calling-car--finished {
+ padding-top: 0;
+ .calling-car-content-box {
+ background: transparent;
+ padding: 0;
+ }
+ // 图一:到达目的地 + 大号里程,距顶部 172px
+ .trip-finished-header {
+ padding: 172px 48px 0;
+ text-align: center;
+ }
+ .trip-finished-header-title {
+ font-size: 24px;
+ line-height: 44px;
+ letter-spacing: normal;
+ color: #FFFFFF;
+ }
+ .trip-finished-header-distance {
+ margin-top: 0;
+ margin-bottom: 120px;
+ font-size: 88px;
+ line-height: 108px;
+ font-weight: 600;
+ font-family: @font-xpsans-bold;
+ letter-spacing: normal;
+ color: #FFFFFF;
+ }
+ // 白色费用卡(横向内边距由卡片自身承担,因 content-box 已透明化)
+ // 提升特异性:与 .calling-car .fare-box-card { padding-bottom: 0 } 同特异性且在其之前,
+ // 用 &.calling-car 组合两个类名避免下方规则覆盖 padding-bottom
+ &.calling-car .fare-box-card {
+ padding: 24px 48px 208px 48px;
+ background: @gary-theme;
+ border-radius: 60px 60px 0 0;
+ }
+ // 图二:明细展开后,费用卡内容顶部距背景 112px,隐藏到达目的地头部
+ &.is-detail-expanded {
+ .calling-car-content-box {
+ padding-top: 112px;
}
}
-
+}
+.calling-car-content {
+ position: relative;
+ z-index: 1;
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 40px;
+}
+.calling-car-content-title {
+ font-size: 32px;
+ font-weight: 500;
+ line-height: 36px;
+ letter-spacing: normal;
+ color: @black-text-main;
+ display: flex;
+ align-items: center;
+ .time {
+ margin: 0 4px;
+ font-size: 40px;
+ line-height: 40px;
+ color: #768aff;
+ }
+}
+.calling-car-content-desc {
+ margin-top: 8px;
+ font-size: 24px;
+ line-height: 34px;
+ color: @black-text-04;
+}
+.calling-car-content-warning {
+ margin-top: 8px;
+ font-size: 24px;
+ line-height: 34px;
+ color: #f58900;
+}
+.calling-car-content-desc-red {
+ margin-top: 8px;
+ font-size: 24px;
+ line-height: 34px;
+ color: #f53f3f;
+}
+.calling-car-content {
+ .cancel-btn {
+ margin-top: 6px;
+ height: 50px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ .cancel {
+ flex-shrink: 0;
+ width: 144px;
+ height: 48px;
+ }
+ }
+}
+.calling-car {
.car-card {
position: relative;
- z-index: 1;
- margin-top: 16px;
- border-radius: 24px;
- background: #ffffff;
- padding: 31.29px 32px 36px 33px;
-
- .car-card-line {
- margin-top: 29.29px;
- margin-bottom: 36px;
- height: 1px;
- background-color: rgba(0, 0, 0, 0.07);
- }
-
- &-detail {
- display: flex;
- align-items: center;
- justify-content: space-between;
-
- &-num {
- font-size: 48px;
- line-height: 64px;
- font-weight: 500;
- letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
- }
-
- &-color {
- margin-top: 4px;
- display: flex;
- align-items: center;
-
- &-text {
- font-size: 24px;
- line-height: 44px;
- color: rgba(0, 0, 0, 0.6);
- letter-spacing: normal;
- }
-
- &-line {
- width: 1.4px;
- height: 20px;
- background: #d8d8d8;
- margin: 0 14px;
- }
- }
-
- &-img {
- flex-shrink: 0;
- width: 300px;
- height: 141.43px;
- }
- }
-
- &-temp {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 16px;
- margin-top: 21px;
-
- &-num {
- flex: 1;
- border-radius: 16px;
- background: #F5F6FA;
- padding: 34px 0;
- display: flex;
- align-items: center;
- justify-content: space-between;
-
- &-btn {
- flex-shrink: 0;
- padding: 24px;
- display: flex;
- align-items: center;
-
- &-img {
- width: 40px;
- height: 40px;
- }
- }
-
- &-text {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 16px;
-
- &-temp {
- display: flex;
- align-items: flex-start;
- margin-left: -6px;
- font-size: 52px;
- 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 {
- font-size: 24px;
- font-weight: normal;
- line-height: 24px;
- letter-spacing: normal;
- color: rgba(0, 0, 0, 0.4);
- }
-
- .color-000-25 {
- color: rgba(0, 0, 0, 0.25);
- }
- }
- }
-
- &-switch {
- position: relative;
- padding: 24px 52px;
- border-radius: 16px;
- background: #F5F6FA;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
-
- &-icon {
- position: relative;
- width: 68px;
- height: 68px;
- line-height: 0;
- flex-shrink: 0;
- }
-
- &-img {
- width: 68px;
- height: 68px;
-
- &-spinning {
- animation: ac-switch-spin 1s linear infinite;
- transform-origin: center center;
- }
- }
-
- &-text {
- font-size: 24px;
- font-weight: normal;
- line-height: 40px;
- letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
- }
- }
-
- &-switch-open {
- background: linear-gradient(118deg, rgba(132, 117, 255, 0.15) 0%, rgba(125, 214, 255, 0.15) 94%);
- }
- }
-
- &-arrive {
- margin-top: 36px;
- display: flex;
- align-items: center;
- gap: 30px;
- height: 156px;
- text-align: center;
- font-size: 24px;
- line-height: 40px;
- color: rgba(0, 0, 0, 0.9);
-
- &-item {
- flex: 1;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 8px;
- padding: 22px 0;
-
- &:first-child {
- position: relative;
- border-radius: 16px;
- background: #ffffff;
- box-sizing: border-box;
- border: 2px solid rgba(0, 0, 0, 0.15);
- }
-
- &:last-child {
- position: relative;
- border-radius: 16px;
- background: #ffffff;
- box-sizing: border-box;
- border: 2px solid rgba(0, 0, 0, 0.15);
- }
-
- &-img {
- width: 64px;
- height: 64px;
-
- &-spinning {
- animation: ac-switch-spin 1s linear infinite;
- transform-origin: center center;
- }
- }
- }
- }
-
- &-end {
- &-particulars {
- display: flex;
- align-items: center;
- justify-content: space-between;
-
- &-label {
- font-size: 24px;
- letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
- }
-
- &-free {
- font-size: 28px;
- letter-spacing: normal;
- color: #F58900;
- }
-
- &-money {
- display: flex;
- align-items: center;
-
- &-amount {
- font-size: 36px;
- font-weight: 500;
- letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
- }
-
- &-through {
- margin-left: 4px;
- font-size: 28px;
- line-height: 36px;
- letter-spacing: normal;
- text-decoration: line-through;
- color: rgba(0, 0, 0, 0.4);
- }
-
- image {
- margin-left: 4px;
- width: 32px;
- height: 32px;
- }
- }
-
- }
-
- .particulars-detail {
- border-radius: 16px;
- background: #f5f6fa;
- display: flex;
- flex-direction: column;
- gap: 16px;
- overflow: auto;
-
- max-height: 0;
- margin-top: 0;
- margin-bottom: 0;
- padding: 0 32px;
- opacity: 0;
- transition: all 0.3s ease-in-out;
-
- &.expand {
- max-height: 600px;
- margin-top: 24px;
- margin-bottom: 16px;
- padding: 24px 32px;
- opacity: 1;
- }
-
- &-item {
- width: 100%;
-
- &-title {
- display: flex;
- align-items: center;
- justify-content: space-between;
- font-size: 24px;
- line-height: 40px;
- letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
-
- .color1 {
- color: #f58900;
- }
- }
-
- &-content {
- display: flex;
- align-items: center;
- justify-content: space-between;
- font-size: 20px;
- line-height: 36px;
- letter-spacing: normal;
- color: rgba(0, 0, 0, 0.4);
- }
- }
- }
-
- &-cost {
- margin-top: 16px;
- display: flex;
- align-items: center;
- justify-content: space-between;
-
- &-label {
- font-size: 24px;
- letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
- }
-
- &-content {
- display: flex;
- align-items: center;
- gap: 4px;
-
- &-money {
- font-size: 24px;
- line-height: 44px;
- letter-spacing: normal;
- color: rgba(0, 0, 0, 0.4);
- }
-
- image {
- margin-left: 4px;
- width: 32px;
- height: 32px;
- }
- }
- }
-
- &-through {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-top: 40px;
-
- &-label {
- font-size: 24px;
- letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
- }
-
- &-content {
- display: flex;
- align-items: center;
-
- &-title {
- border-radius: 7.19px;
- background: rgba(245, 137, 0, 0.06);
- border: 1.2px solid rgba(245, 137, 0, 0.2);
- width: 92.38px;
- height: 36.38px;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 19.18px;
- letter-spacing: normal;
- color: #F58900;
- }
-
- &-money {
- font-size: 24px;
- color: #F58900;
- margin-left: 8px;
- }
-
- image {
- margin-left: 4px;
- width: 32px;
- height: 32px;
- }
- }
- }
- }
+ z-index: 3;
+ border-radius: 60rpx;
}
-
- .trip-action-bar {
- border-radius: 24px;
- background: #FFFFFF;
- margin-top: 16px;
+ .fare-box-card {
+ padding-bottom: 0;
+ }
+ .car-card-detail {
display: flex;
align-items: center;
justify-content: space-between;
- padding: 24px 42px 24px 32px;
-
- &-item {
- display: flex;
- align-items: center;
- box-sizing: border-box;
-
- &-disabled {
- opacity: 0.4;
- }
-
- &-icon {
- flex-shrink: 0;
- width: 48px;
- height: 48px;
- }
-
- &-destination {
-
- .trip-action-bar-item-icon {
- width: 44px;
- height: 44px;
- }
- }
-
- &-text {
- flex-shrink: 0;
- font-size: 24px;
- line-height: 24px;
- color: rgba(0, 0, 0, 0.9);
- }
- }
-
- &-share-btn {
- padding: 0;
- margin: 0;
- min-width: auto;
- line-height: normal;
- background: transparent;
- border: none;
-
- &::after {
- border: none;
- }
- }
-
- &-divider {
- width: 2px;
- height: 32px;
- background: rgba(0, 0, 0, 0.1);
- flex-shrink: 0;
- }
-
- &-without-cancel {
- padding-left: 102px;
- padding-right: 112px;
+ margin-bottom: 24px;
+ }
+ .car-card-detail-plate {
+ display: flex;
+ align-items: center;
+ font-size: 40px;
+ font-weight: 500;
+ line-height: 56px;
+ color: @black-text-main;
+ }
+ .car-card-detail-num {
+ font-size: 44px;
+ font-weight: 600;
+ line-height: 56px;
+ letter-spacing: normal;
+ color: @black-text-main;
+ }
+ .car-card-detail-dot {
+ flex-shrink: 0;
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: @black-text-main;
+ margin: 0 8px;
+ font-size: 44px;
+ font-weight: 500;
+ line-height: 56px;
+ letter-spacing: normal;
+ color: @black-text-main;
+ }
+ .car-card-detail-color-text {
+ font-size: 40px;
+ font-weight: 500;
+ line-height: 56px;
+ letter-spacing: normal;
+ color: @black-text-main;
+ }
+ .car-card-detail-model {
+ font-size: 24px;
+ line-height: 44px;
+ color: rgba(0, 0, 0, 0.6);
+ letter-spacing: normal;
+ }
+ .car-card-temp {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ margin-top: 21px;
+ }
+ .car-card-temp-num {
+ flex: 1;
+ border-radius: 16px;
+ background: #F5F6FA;
+ padding: 34px 0;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+ .car-card-temp-num-btn {
+ flex-shrink: 0;
+ padding: 24px;
+ display: flex;
+ align-items: center;
+ }
+ .car-card-temp-num-btn-img {
+ width: 40px;
+ height: 40px;
+ }
+ .car-card-temp-num-text {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 16px;
+ }
+ .car-card-temp-num-text-temp {
+ display: flex;
+ align-items: flex-start;
+ margin-left: -6px;
+ font-size: 52px;
+ line-height: 48px;
+ letter-spacing: normal;
+ color: @black-text-main;
+ }
+ .car-card-temp-num-text-temp-value {
+ line-height: 48px;
+ }
+ .car-card-temp-num-text-temp-unit {
+ display: flex;
+ align-items: flex-start;
+ margin-top: 0;
+ margin-left: 2px;
+ line-height: 1;
+ }
+ .car-card-temp-num-text-temp-unit-degree {
+ font-size: 40px;
+ line-height: 24px;
+ }
+ .car-card-temp-num-text-temp-unit-letter {
+ margin-top: 0;
+ margin-left: 1px;
+ font-size: 32px;
+ line-height: 24px;
+ }
+ .car-card-temp-num-text-desc {
+ font-size: 24px;
+ font-weight: normal;
+ line-height: 24px;
+ letter-spacing: normal;
+ color: @black-text-04;
+ }
+ .car-card-temp-num-text {
+ .color-000-25 {
+ color: rgba(0, 0, 0, 0.25);
}
}
+ .car-card-temp-switch {
+ position: relative;
+ padding: 24px 52px;
+ border-radius: 16px;
+ background: #F5F6FA;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ }
+ .car-card-temp-switch-icon {
+ position: relative;
+ width: 68px;
+ height: 68px;
+ line-height: 0;
+ flex-shrink: 0;
+ }
+ .car-card-temp-switch-img {
+ width: 68px;
+ height: 68px;
+ }
+ .car-card-temp-switch-img-spinning {
+ animation: ac-switch-spin 1s linear infinite;
+ transform-origin: center center;
+ }
+ .car-card-temp-switch-text {
+ font-size: 24px;
+ font-weight: normal;
+ line-height: 40px;
+ letter-spacing: normal;
+ color: @black-text-main;
+ }
+ .car-card-temp-switch-open {
+ background: linear-gradient(118deg, rgba(132, 117, 255, 0.15) 0%, rgba(125, 214, 255, 0.15) 94%);
+ }
+ .car-card-arrive {
+ display: flex;
+ align-items: center;
+ gap: 30px;
+ text-align: center;
+ font-size: 24px;
+ line-height: 40px;
+ color: @black-text-main;
+ }
+ .car-card-arrive-item {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: center;
+ gap: 8px;
+ width: 312px;
+ height: 240px;
+ padding: 32px;
+ background: rgba(204, 208, 215, 0.25);
+ &:first-child {
+ position: relative;
+ border-radius: 32px;
+ box-sizing: border-box;
+ }
+ &:last-child {
+ position: relative;
+ border-radius: 32px;
+ box-sizing: border-box;
+ }
+ &.close-btn-item {
+ position: relative;
+ border-radius: 32px;
+ background: url(https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/door-trunk-item-bg-20260729.webp);
+ background-repeat: no-repeat;
+ background-size: 100% 100%;
+ .car-card-arrive-item-text {
+ color: #ffffff;
+ }
+ .car-card-arrive-item-status {
+ color: rgba(255, 255, 255, 0.4);
+ }
+ }
+ }
+ .car-card-arrive-item-status {
+ font-size: 24px;
+ line-height: normal;
+ letter-spacing: normal;
+ color: @black-text-04;
+ }
+ .car-card-arrive-item-text {
+ font-size: 28px;
+ font-weight: 500;
+ color: @black-text-main;
+ }
+ .car-card-arrive-item-img {
+ width: 64px;
+ height: 64px;
+ margin-top: 30px;
+ }
+ .car-card-arrive-item-img-spinning {
+ animation: ac-switch-spin 1s linear infinite;
+ transform-origin: center center;
+ }
+ .car-card-end-particulars {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+ .car-card-end-particulars-label {
+ font-size: 28px;
+ letter-spacing: normal;
+ color: @black-text-main;
+ }
+ .car-card-end-particulars-free {
+ font-size: 28px;
+ font-weight: 500;
+ line-height: normal;
+ letter-spacing: normal;
+ color: @gold-text;
+ }
+ .car-card-end-particulars-money {
+ display: flex;
+ align-items: flex-end;
+ }
+ .car-card-end-particulars-money-amount {
+ font-size: 56px;
+ font-weight: 500;
+ letter-spacing: normal;
+ color: @black-text-main;
+ }
+ .car-card-end-particulars-money-unit {
+ margin-bottom: 10px;
+ font-size: 24px;
+ color: @black-text-main;
+ }
+ .car-card-end-particulars-money-through {
+ margin-left: 0;
+ margin-right: 4px;
+ margin-bottom: 10px;
+ font-size: 24px;
+ line-height: 36px;
+ letter-spacing: normal;
+ text-decoration: line-through;
+ color: @black-text-04;
+ }
+ .car-card-end-particulars-money-discount-badge {
+ display: inline-flex;
+ align-items: center;
+ height: 36px;
+ padding: 4px 10px;
+ margin-right: 8px;
+ margin-bottom: 10px;
+ background: rgba(223, 179, 105, 0.2);
+ border-radius: 8px;
+ font-size: 20px;
+ font-weight: 500;
+ color: @gold-text;
+ &-amount {
+ color: @gold-text;
+ }
+ }
+ .car-card-end-particulars-money {
+ image {
+ margin-left: 4px;
+ margin-bottom: 10px;
+ width: 32px;
+ height: 32px;
+ }
+ }
+ .car-card-end {
+ .particulars-detail {
+ border-radius: 24px;
+ background: rgba(204, 208, 215, 0.25);
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ overflow: auto;
+ max-height: 0;
+ margin-top: 0;
+ margin-bottom: 0;
+ padding: 0 20px;
+ opacity: 0;
+ transition: all 0.3s ease-in-out;
+ &.expand {
+ max-height: 400px;
+ margin-top: 24px;
+ margin-bottom: 16px;
+ padding: 24px 20px;
+ opacity: 1;
+ overflow-y: scroll;
+ }
+ }
+ .particulars-detail-item {
+ width: 100%;
+ }
+ .particulars-detail-item-title {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 24px;
+ line-height: normal;
+ letter-spacing: normal;
+ color: @black-text-main;
+ .color1 {
+ color: @gold-text;
+ }
+ }
+ .particulars-detail-item-content {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 24px;
+ line-height: normal;
+ letter-spacing: normal;
+ color: @black-text-04;
+ }
+ }
+ .car-card-end-cost {
+ margin-top: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+ .car-card-end-cost-label {
+ font-size: 28px;
+ letter-spacing: normal;
+ color: @black-text-main;
+ }
+ .car-card-end-cost-money {
+ color: @black-text-04;
+ font-size: 28px;
+ }
+ .car-card-end-cost-money-line {
+ display: inline-block;
+ margin: 0 20px;
+ color: @black-text-04;
+ }
+ .car-card-end-cost-content {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ }
+ .car-card-end-cost-content-money {
+ font-size: 24px;
+ line-height: 44px;
+ letter-spacing: normal;
+ color: @black-text-04;
+ }
+ .car-card-end-cost-content {
+ image {
+ margin-left: 4px;
+ width: 32px;
+ height: 32px;
+ }
+ }
+ .car-card-end-through {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 24px;
+ padding: 8px 16px 8px 24px;
+ height: 56px;
+ border-radius: 16px;
+ background: rgba(208, 151, 55, 0.15);
+ }
+ .car-card-end-through-left {
+ display: flex;
+ align-items: center;
+ }
+ .car-card-end-through-right {
+ display: flex;
+ align-items: center;
+ }
+ .car-card-end-through-desc {
+ margin-left: 10px;
+ font-size: 24px;
+ line-height: 40px;
+ letter-spacing: normal;
+ color: @gold-text;
+ }
+ .car-card-end-through-content-title {
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0);
+ border: 1.4px solid @gold-text;
+ width: 84px;
+ height: 29px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 16px;
+ font-weight: 500;
+ letter-spacing: normal;
+ color: @gold-text;
+ }
+ .car-card-end-through-content-money {
+ margin-right: 4px;
+ font-size: 24px;
+ font-weight: 500;
+ color: @gold-text;
+ }
+ .car-card-end-through-arrow {
+ flex-shrink: 0;
+ width: 24px;
+ height: 24px;
+ }
+ .trip-action-bar {
+ border-radius: 24px;
+ background: @gary-theme;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 32px 8px 0 8px;
+ }
+ .trip-action-bar-item {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ }
+ .trip-action-bar-item-disabled {
+ opacity: 0.4;
+ }
+ .trip-action-bar-item-icon {
+ flex-shrink: 0;
+ width: 28px;
+ height: 28px;
+ margin-right: 8px;
+ }
+ .trip-action-bar-item-destination {
+ .trip-action-bar-item-icon {
+ width: 28px;
+ height: 28px;
+ }
+ }
+ .trip-action-bar-item-text {
+ flex-shrink: 0;
+ font-size: 24px;
+ line-height: 24px;
+ color: @black-text-main;
+ }
+ .trip-action-bar-share-btn {
+ padding: 0;
+ margin: 0;
+ min-width: auto;
+ line-height: normal;
+ background: transparent;
+ border: none;
+ &::after {
+ border: none;
+ }
+ }
+ .trip-action-bar-divider {
+ width: 2px;
+ height: 32px;
+ background: rgba(0, 0, 0, 0.1);
+ flex-shrink: 0;
+ }
+ .trip-action-bar-without-cancel {
+ padding-left: 102px;
+ padding-right: 112px;
+ }
.car-card-reach {
margin-top: 22px;
border-radius: 24px;
background: #ffffff;
padding: 32px;
padding-top: 22px;
-
- &-text {
- font-size: 28px;
- line-height: 44px;
- letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
+ }
+ .car-card-reach-text {
+ font-size: 28px;
+ line-height: 44px;
+ letter-spacing: normal;
+ color: @black-text-main;
+ }
+ .car-card-reach-img {
+ border-radius: 16px;
+ margin-top: 12px;
+ width: 100%;
+ height: 313px;
+ }
+ .car-card-reach-btn {
+ margin-top: 28px;
+ display: flex;
+ align-items: center;
+ margin-bottom: 16px;
+ gap: 30px;
+ height: 156px;
+ text-align: center;
+ font-size: 24px;
+ line-height: 40px;
+ color: @black-text-main;
+ }
+ .car-card-reach-btn-item {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ padding: 22px 0;
+ &:first-child {
+ position: relative;
+ border-radius: 32px;
+ background: rgba(204, 208, 215, 0.25);
+ box-sizing: border-box;
}
-
- &-img {
- border-radius: 16px;
- margin-top: 12px;
- width: 100%;
- height: 313px;
- }
-
- &-btn {
- margin-top: 28px;
- display: flex;
- align-items: center;
- margin-bottom: 16px;
- gap: 30px;
- height: 156px;
- text-align: center;
- font-size: 24px;
- line-height: 40px;
- color: rgba(0, 0, 0, 0.9);
-
- &-item {
- flex: 1;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 8px;
- padding: 22px 0;
-
- &:first-child {
- position: relative;
- border-radius: 16px;
- background: #ffffff;
- box-sizing: border-box;
- border: 2px solid rgba(0, 0, 0, 0.15);
- }
-
- &:last-child {
- border-radius: 16px;
- background: linear-gradient(110deg, #8475ff 0%, #7dd6ff 99%);
- color: #fff;
- }
-
- &-img {
- width: 64px;
- height: 64px;
-
- &-spinning {
- animation: ac-switch-spin 1s linear infinite;
- transform-origin: center center;
- }
- }
- }
+ &:last-child {
+ border-radius: 32px;
+ background: rgba(204, 208, 215, 0.25);
}
}
-
+ .car-card-reach-btn-item-img {
+ width: 64px;
+ height: 64px;
+ }
+ .car-card-reach-btn-item-img-spinning {
+ animation: ac-switch-spin 1s linear infinite;
+ transform-origin: center center;
+ }
.car-card-evaluate {
margin-top: 24px;
border-radius: 16px;
padding: 36px 0 32px 0;
- border-radius: 24px;
- background: linear-gradient(180deg, #fff8e6 0%, #ffffff 46%);
+ border-radius: 40px;
+ background: #FFFFFF;
box-sizing: border-box;
border: 3px solid #ffffff;
-
- &-title {
- font-weight: 500;
- padding-left: 32px;
- font-size: 28px;
- line-height: 40px;
- letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
- margin: 0px 0px 24px 2px;
- }
-
- &-content {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-top: 32px;
- margin-left: 60px;
- margin-right: 62px;
-
- &-item {
- width: 140px;
- height: 128px;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- align-items: center;
-
- &-img {
- width: 80px;
- height: 80px;
- }
-
- &-text {
- font-size: 24px;
- line-height: 40px;
- letter-spacing: normal;
- color: rgba(0, 0, 0, 0.4);
- }
-
- .active-text {
- color: rgba(0, 0, 0, 0.9);
- }
- }
+ }
+ .car-card-evaluate-title {
+ font-weight: 500;
+ padding-left: 60px;
+ font-size: 28px;
+ line-height: 40px;
+ letter-spacing: normal;
+ color: @black-text-main;
+ margin: 0px 0px 24px 2px;
+ }
+ .car-card-evaluate-content {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 32px;
+ margin-left: 60px;
+ margin-right: 62px;
+ }
+ .car-card-evaluate-content-item {
+ width: 140px;
+ height: 128px;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: center;
+ }
+ .car-card-evaluate-content-item-img {
+ width: 80px;
+ height: 80px;
+ }
+ .car-card-evaluate-content-item-text {
+ font-size: 24px;
+ line-height: 40px;
+ letter-spacing: normal;
+ color: @black-text-04;
+ }
+ .car-card-evaluate-content-item {
+ .active-text {
+ color: @black-text-main;
}
}
}
-
.pay-box {
position: fixed;
bottom: 0;
@@ -686,14 +741,22 @@
z-index: 11;
width: 100%;
box-sizing: border-box;
- padding: 24px 38px 62px 32px;
- background: #FEFEFF;
+ padding: 0 38px 80px 32px;
+ background: @gary-theme;
display: flex;
- justify-content: space-between;
-
- &-money {
- font-size: 48px;
- font-weight: 500;
+ justify-content: center;
+}
+.pay-box-money {
+ font-size: 48px;
+ font-weight: 500;
+ letter-spacing: normal;
+ color: @black-text-main;
+ display: flex;
+ align-items: center;
+ .unit {
+ margin-left: 4px;
+ font-size: 24px;
+ line-height: 40px;
letter-spacing: normal;
color: rgba(0, 0, 0, 0.9);
display: flex;
@@ -737,81 +800,334 @@
background: rgba(255, 255, 255, 0.4);
}
}
-
+.pay-box-btn {
+ width: 654px;
+ height: 104px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: 2000px;
+ background: @black-text-main;
+ font-size: 32px;
+ letter-spacing: normal;
+ color: #FFFFFF;
+}
.trip-action-bar-item-hover {
flex-shrink: 0;
opacity: 0.75;
}
-
-.close-btn-item {
- position: relative;
- border-radius: 16px;
- background: linear-gradient(110deg,
- rgba(132, 117, 255, 0.2) 0%,
- rgba(125, 214, 255, 0.2) 99%) !important;
-
- &::after {
- content: "";
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- border-radius: 16px;
- padding: 1px;
- background: linear-gradient(180deg,
- #aaa8ff -10%,
- rgba(41, 159, 255, 0.0469) 108%,
- rgba(0, 0, 0, 0) 108%);
- -webkit-mask: linear-gradient(#fff 0 0) content-box,
- linear-gradient(#fff 0 0);
- -webkit-mask-composite: xor;
- mask-composite: exclude;
- pointer-events: none;
- }
-}
-
.bluetooth-operate-card {
position: relative;
z-index: 1;
display: flex;
align-items: center;
- justify-content: space-between;
- padding: 16px 24px 16px 16px;
+ justify-content: flex-start;
+ padding: 24px 0px;
border-radius: 16px;
- background: #ffffff;
-
+ background: @gary-theme;
.bluetooth-operate-card-left {
display: flex;
align-items: center;
- gap: 12px;
+ gap: 4px;
font-size: 24px;
font-weight: normal;
- line-height: 40px;
+ line-height: normal;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
-
+ color: @black-text-main;
image {
flex-shrink: 0;
- width: 24px;
- height: 28px;
+ width: 36px;
+ height: 36px;
}
}
-
.bluetooth-operate-card-right {
display: flex;
align-items: center;
- gap: 11px;
+ margin-left: 4px;
font-size: 24px;
font-weight: normal;
line-height: 40px;
letter-spacing: normal;
color: #6d7fd9;
-
image {
flex-shrink: 0;
- width: 11.97px;
- height: 17.5px;
+ width: 24px;
+ height: 24px;
+ }
+ }
+}
+.calling-car {
+ .calling-car-content {
+ align-items: flex-start;
+ .calling-car-content-main {
+ flex: 1;
+ min-width: 0;
+ }
+ .calling-car-content-title--strong {
+ font-size: 40px;
+ font-weight: 600;
+ line-height: 48px;
+ color: @black-text-main;
+ }
+ .calling-car-content-desc {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ }
+ .calling-car-content-desc-divider {
+ margin: 0 12px;
+ color: rgba(20, 30, 39, 0.2);
+ }
+ }
+ .calling-car-cancel-btn {
+ flex-shrink: 0;
+ margin-left: 24px;
+ align-self: flex-start;
+ padding: 0 28px;
+ height: 60px;
+ line-height: 60px;
+ border-radius: 1000px;
+ background: #ffffff;
+ font-size: 26px;
+ color: @black-text-main;
+ }
+ .calling-car-site-box {
+ position: relative;
+ z-index: 2;
+ border-radius: 40px;
+ background: rgba(204, 208, 215, 0.25);
+ padding: 32px;
+ display: flex;
+ gap: 16px;
+ height: fit-content;
+ .calling-car-site-box-icon {
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ .calling-car-site-check {
+ width: 32px;
+ height: 32px;
+ }
+ .dotted-line {
+ flex: 1;
+ width: 2px;
+ margin: 6px 0;
+ height: fit-content;
+ }
+ }
+ .calling-car-site-box-content {
+ flex: 1;
+ font-size: 28px;
+ line-height: 44px;
+ letter-spacing: normal;
+ color: @black-text-main;
+ .calling-car-site-box-divider {
+ margin: 40px 24px 40px 0px;
+ border-top: 1px solid rgba(20, 30, 39, 0.08);
+ }
+ }
+ }
+ .calling-car-status-box {
+ position: relative;
+ z-index: 2;
+ box-sizing: border-box;
+ width: 100%;
+ height: 74px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 32px;
+ border-radius: 24px;
+ overflow: hidden;
+ background-image: url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/start-status-20260714.png');
+ background-repeat: no-repeat;
+ background-size: 100% 100%;
+ .calling-car-status-box-come-icon {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 245px;
+ height: 10px;
+ }
+ .calling-car-status-box-left {
+ flex-shrink: 0;
+ font-size: 24px;
+ line-height: 40px;
+ color: #ffffff;
+ }
+ .calling-car-status-box-right {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ .distance {
+ font-size: 24px;
+ font-weight: 500;
+ font-family: @font-xpsans-bold;
+ color: #ffffff;
+ }
+ .time {
+ margin-left: 16px;
+ padding: 0;
+ font-size: 24px;
+ font-weight: 500;
+ font-family: @font-xpsans-bold;
+ color: #ffffff;
+ }
+ }
+ }
+ .calling-car-arrived-img {
+ background-image: url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/arrived-pickup-20260714.png');
+ background-repeat: no-repeat;
+ background-size: 100% 100%;
+ }
+}
+
+/* 空调 + 香氛 左右布局容器(迁移自 temperature-fragrance-control.less) */
+.temp-fragrance-control {
+ position: relative;
+ z-index: 2;
+ display: flex;
+ justify-content: space-between;
+ gap: 16px;
+ margin-top: 16px;
+}
+
+/* 空调/香氛入口卡片开关 loading 旋转动画 */
+@keyframes tf-switch-spin {
+ from {
+ transform: rotate(0deg);
+ }
+
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+/* tf-card 卡片基础样式(空调入口卡片使用;香氛入口卡片在 fragrance-control.less 内自包含一份) */
+.tf-card {
+ flex-shrink: 0;
+ position: relative;
+ box-sizing: border-box;
+ width: 312px;
+ height: 240px;
+ padding: 24px;
+ border-radius: 24px;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: 100% 100%;
+
+ /* 底部内容组:温度 + footer,整体靠底部 */
+ .tf-card-bottom {
+ display: flex;
+ flex-direction: column;
+ }
+
+ /* 卡片头部:左右布局 */
+ .tf-card-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ .tf-card-header-left {
+ display: flex;
+ align-items: center;
+ }
+
+ .tf-card-header-icon {
+ flex-shrink: 0;
+ width: 28px;
+ height: 28px;
+ margin-right: 8px;
+ }
+
+ .tf-card-header-title {
+ font-size: 24px;
+ font-weight: 500;
+ line-height: normal;
+ letter-spacing: normal;
+ color: @black-text-main;
+ }
+
+ .tf-card-header-switch {
+ flex-shrink: 0;
+ width: 104px;
+ height: 64px;
+ }
+
+ .tf-card-header-switch-loading {
+ width: 64px;
+ height: 64px;
+ animation: tf-switch-spin 1s linear infinite;
+ }
+
+ /* 空调温度显示 */
+ .tf-card-ac-temp {
+ display: flex;
+ align-items: flex-start;
+ }
+
+ .tf-card-ac-temp-value {
+ font-size: 48px;
+ font-weight: normal;
+ line-height: 64px;
+ font-family: @font-xpsans-bold;
+ color: @black-text-main;
+ }
+
+ .tf-card-ac-temp-unit {
+ margin-top: 8px;
+ margin-left: 6px;
+ font-size: 28px;
+ font-weight: 500;
+ line-height: 32px;
+ color: @black-text-main;
+ }
+
+ /* 卡片底部:左右布局 */
+ .tf-card-footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ .tf-card-footer-text {
+ font-size: 20px;
+ line-height: normal;
+ letter-spacing: normal;
+ color: rgba(20, 30, 39, 0.5);
+ }
+
+ .tf-card-footer-arrow {
+ flex-shrink: 0;
+ width: 20px;
+ height: 20px;
+ }
+}
+
+/* 空调入口卡片:背景随开关状态切换 */
+.tf-card-ac {
+ padding-top: 8px;
+
+ &.is-open {
+ background-image: url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/aircondition-open-20260703.png');
+ }
+
+ &.is-close {
+ background-image: url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/aircondition-close-20260703.png');
+
+ // 关闭态卡片文字统一弱化
+ .tf-card-header-title,
+ .tf-card-ac-temp-value,
+ .tf-card-ac-temp-unit,
+ .tf-card-footer-text {
+ color: rgba(20, 30, 39, 0.3);
}
}
}
diff --git a/src/robotaxi/pages/begin-route/components/css-folder/cancel-popup.less b/src/robotaxi/pages/begin-route/components/css-folder/cancel-popup.less
index 027d402..10b5677 100644
--- a/src/robotaxi/pages/begin-route/components/css-folder/cancel-popup.less
+++ b/src/robotaxi/pages/begin-route/components/css-folder/cancel-popup.less
@@ -20,20 +20,20 @@
bottom: 0;
left: 0;
right: 0;
- background: #ffffff;
- border-radius: 16px 16px 0 0;
- padding: 64px 40px 88px 40px;
+ border-radius: 60px 60px 0px 0px;
+ background: #F1F5FA;
+ padding: 48px 48px 80px 48px;
.cancel-box {
position: absolute;
- top: 40px;
- right: 40px;
+ top: 32px;
+ right: 32px;
display: flex;
justify-content: flex-end;
.cancel-icon {
- width: 48px;
- height: 48px;
+ width: 40px;
+ height: 40px;
}
}
@@ -44,16 +44,16 @@
font-weight: 500;
line-height: normal;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
+ color: #141E27;
}
- &-desc {
- margin-top: 8px;
- font-size: 24px;
+ .cancel-tip-box-desc {
+ margin-top: 14px;
+ font-size: 28px;
font-weight: normal;
- line-height: 34px;
+ line-height: 32px;
letter-spacing: normal;
- color: #F58900;
+ color: rgba(20, 30, 39, 0.4);
}
.color1 {
@@ -62,32 +62,32 @@
}
.cancel-site-box {
- margin-top: 32px;
- border-radius: 20px;
- background: #F5F6FA;
- padding: 32px;
+ margin-top: 48px;
+ border-radius: 40px;
+ background: rgba(204, 208, 215, 0.25);
+ padding: 48px 32px 42px 32px;
display: flex;
gap: 16px;
height: fit-content;
- &-icon {
- padding: 17px 0;
+ .cancel-site-box-icon {
+ padding: 16px 0;
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;
- &-start {
- width: 10px;
- height: 10px;
- background: #768AFF;
+ .cancel-site-box-icon-start {
+ width: 16px;
+ height: 16px;
+ background: #DFB369;
border-radius: 50%;
}
- &-end {
- width: 10px;
- height: 10px;
- background: #8BB654;
+ .cancel-site-box-icon-end {
+ width: 16px;
+ height: 16px;
+ background: #141E27;
border-radius: 50%;
}
@@ -99,41 +99,58 @@
}
}
- &-content {
+ .cancel-site-box-content {
flex: 1;
- font-size: 28px;
- line-height: 44px;
+ font-size: 32px;
+ line-height: 48px;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
-
- view:first-child {
- margin-bottom: 40px;
+ color: #141E27;
+ .cancel-site-box-divider {
+ margin: 34px 32px 40px 0px;
+ border-top: 1px solid rgba(204, 208, 215, 0.25);
}
}
}
+ .cancel-animation {
+ width: 100%;
+ height: 262px;
+ margin-top: 48px;
+ }
+
.cancel-btn-box {
display: flex;
- margin-top: 58px;
+ margin-top: 48px;
.cancel-btn-cancel {
flex: 1;
- height: 96px;
+ border: 4px solid #141E27;
+ border-radius: 1000px;
+ font-size: 32px;
+ line-height: 104px;
+ text-align: center;
+ letter-spacing: normal;
+ color: #141E27;
+ height: 104px;
+ width: 310px;
}
.cancel-btn-again {
flex: 1;
- line-height: 96px;
text-align: center;
- border-radius: 16px;
- border-radius: 16px;
- background: linear-gradient(102deg, #8475FF 1%, #7DD6FF 102%);
- font-size: 30px;
- letter-spacing: normal;
- color: #FFFFFF;
+ border-radius: 1000px;
+ border: 4px solid #141E27;
+ font-size: 32px;
+ line-height: 104px;
+ text-align: center;
+ color: #141E27;
+ height: 104px;
+ width: 310px;
}
.wait-btn {
- margin-left: 32px;
+ margin-left: 22px;
+ color: #FFFFFF;
+ background: #141E27;
}
}
}
diff --git a/src/robotaxi/pages/begin-route/components/css-folder/evaluate-popup.less b/src/robotaxi/pages/begin-route/components/css-folder/evaluate-popup.less
index 40fa7b5..56081f8 100644
--- a/src/robotaxi/pages/begin-route/components/css-folder/evaluate-popup.less
+++ b/src/robotaxi/pages/begin-route/components/css-folder/evaluate-popup.less
@@ -16,22 +16,37 @@
}
.evaluate-content {
+ height: 1336px;
position: absolute;
bottom: 0;
left: 0;
right: 0;
- background: #ffffff;
- border-radius: 16px 16px 0 0;
- padding: 42px 0 88px 0;
+ background: #F1F5FA;
+ border-radius: 60px 60px 0 0;
+ padding: 64px 0 0 0;
+
+ .evaluate-rating-switcher,
+ .content-box {
+ transition: opacity 180ms ease, transform 180ms ease;
+ will-change: opacity, transform;
+ }
+
+ &.is-rating-switching {
+ .evaluate-rating-switcher,
+ .content-box {
+ opacity: 0;
+ transform: translateY(12px) scale(0.985);
+ pointer-events: none;
+ }
+ }
&-close {
- display: flex;
- justify-content: flex-end;
- padding-right: 34px;
-
+ position: absolute;
+ top: 34px;
+ right: 34px;
image {
- width: 36px;
- height: 36px;
+ width: 40px;
+ height: 40px;
}
}
@@ -52,9 +67,76 @@
}
}
- .content-box {
- margin-top: 54px;
+ .evaluate-rating-switcher {
+
padding: 0 49px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ &-title {
+ font-size: 32px;
+ font-weight: 600;
+ line-height: 44px;
+ letter-spacing: normal;
+ color: rgba(0, 0, 0, 0.9);
+ text-align: left;
+ align-self: stretch;
+ padding-left: 30px;
+ box-sizing: border-box;
+ }
+
+ &-content {
+ width: 100%;
+ margin-top: 32px;
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+
+ &-item {
+ width: 180px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ &-img {
+ width: 80px;
+ height: 80px;
+ flex-shrink: 0;
+ }
+
+ &-text {
+ margin-top: 12px;
+ font-size: 24px;
+ line-height: 34px;
+ letter-spacing: normal;
+ color: rgba(0, 0, 0, 0.4);
+ }
+
+ .active-text {
+ color: #141E27;
+ font-weight: 600;
+ }
+ }
+ }
+
+ &-summary {
+ margin-top: 28px;
+ font-size: 32px;
+ font-weight: 600;
+ line-height: 44px;
+ letter-spacing: normal;
+ color: rgba(0, 0, 0, 0.9);
+ text-align: center;
+ }
+ }
+
+ .content-box {
+ margin-top: 40px;
+ padding: 0 49px;
+ min-height: 420px;
+ box-sizing: border-box;
+ padding-bottom: 224px;
.evaluate-problem-type {
@@ -77,13 +159,12 @@
display: flex;
justify-content: center;
align-items: center;
- border-radius: 8px;
- background: #FFFFFF;
+ border-radius: 20px;
+ background: #E8ECF1;
box-sizing: border-box;
- border: 2px solid rgba(0, 0, 0, 0.2);
font-size: 28px;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.6);
+ color: rgba(20, 30, 39, 0.5);
min-width: 0;
&-text {
@@ -96,10 +177,10 @@
}
.active-tag {
- border-radius: 12px;
+ border-radius: 20px;
box-sizing: border-box;
- border: 2px solid #000000;
- color: rgba(0, 0, 0, 0.9);
+ background: rgba(223, 179, 105, 0.2);
+ color: #B38F53;
}
}
}
@@ -110,19 +191,19 @@
.evaluate-problem-desc {
width: calc(100% - 48px);
margin-top: 48px;
- border-radius: 16px;
- background: rgba(0, 0, 0, 0.03);
+ border-radius: 32px;
+ background: #E8ECF1;
padding: 24px;
padding-bottom: 16px;
font-size: 28px;
- color: rgba(0, 0, 0, 0.9);
+ color: rgba(20, 30, 39, 0.4);
height: 196px;
}
.placeholder-class {
font-size: 28px;
line-height: 44px;
- color: rgba(0, 0, 0, 0.25);
+ color: rgba(20, 30, 39, 0.4);
}
.evaluate-problem-desc-count {
@@ -132,7 +213,7 @@
font-size: 20px;
line-height: 20px;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.25);
+ color: rgba(20, 30, 39, 0.4);
}
}
@@ -144,17 +225,17 @@
&-item {
position: relative;
- width: 208px;
- height: 208px;
- border-radius: 16px;
- // overflow: hidden;
+ width: 196px;
+ height: 196px;
+ border-radius: 32px;
+ overflow: hidden;
background: rgba(0, 0, 0, 0.03);
}
&-img {
flex-shrink: 0;
- width: 208px;
- height: 208px;
+ width: 196px;
+ height: 196px;
object-fit: cover;
}
@@ -198,8 +279,8 @@
&-delete {
position: absolute;
- top: -10px;
- right: -10px;
+ top: 4px;
+ right: 4px;
z-index: 2;
.delete-icon {
@@ -220,46 +301,66 @@
&-upload {
flex-shrink: 0;
- width: 208px;
- height: 208px;
+ width: 196px;
+ height: 196px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
- border-radius: 16px;
- background: rgba(0, 0, 0, 0.03);
+ border-radius: 32px;
+ background: #E8ECF1;
&-img {
width: 48px;
height: 48px;
}
- &-count {
- font-size: 24px;
- line-height: 40px;
+ &-text {
+ font-family: PingFang SC;
+ font-size: 20px;
+ font-weight: normal;
+ line-height: 32px;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.25);
+ color: rgba(20, 30, 39, 0.5);
}
}
}
}
&-confirm {
- margin: 0 94px;
- margin-top: 40px;
- font-size: 30px;
- letter-spacing: normal;
- color: #FFFFFF;
- text-align: center;
- line-height: 96px;
- border-radius: 16px;
- background: rgba(0, 0, 0, 0.4);
+ position: absolute;
+ left: 49px;
+ right: 49px;
+ bottom: 80px;
+ height: 104px;
+ margin: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &-btn {
+ width: 100%;
+ height: 104px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 32px 80px;
+ gap: 16px;
+ box-sizing: border-box;
+ z-index: 2;
+ font-size: 30px;
+ letter-spacing: normal;
+ color: #FFFFFF;
+ text-align: center;
+ border-radius: 1000px;
+ background: #D6D9DC;
+ }
}
.ok-btn {
- border-radius: 16px;
- background: linear-gradient(97deg, #8475FF 0%, #7DD6FF 103%);
+ border-radius: 1000px;
+ background: #141E27;
}
}
}
diff --git a/src/robotaxi/pages/begin-route/components/css-folder/fragrance-control.less b/src/robotaxi/pages/begin-route/components/css-folder/fragrance-control.less
new file mode 100644
index 0000000..1c499e5
--- /dev/null
+++ b/src/robotaxi/pages/begin-route/components/css-folder/fragrance-control.less
@@ -0,0 +1,257 @@
+@import '../../../../style/ui-variables.less';
+
+// 香型下发 loading 旋转动画
+@keyframes fragrance-spin {
+ from {
+ transform: rotate(0deg);
+ }
+
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+/* 香氛入口卡片 tf-card 样式(自包含,迁移自 temperature-fragrance-control.less) */
+.fragrance-control {
+ .tf-card {
+ flex-shrink: 0;
+ position: relative;
+ box-sizing: border-box;
+ width: 312px;
+ height: 240px;
+ padding: 24px;
+ border-radius: 24px;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: 100% 100%;
+ }
+
+ .tf-card-bottom {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .tf-card-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ .tf-card-header-left {
+ display: flex;
+ align-items: center;
+ }
+
+ .tf-card-header-icon {
+ flex-shrink: 0;
+ width: 28px;
+ height: 28px;
+ margin-right: 8px;
+ }
+
+ .tf-card-header-title {
+ font-size: 24px;
+ font-weight: 500;
+ line-height: normal;
+ letter-spacing: normal;
+ color: @black-text-main;
+ }
+
+ /* 香氛名称,大小类似温度显示 */
+ .tf-card-fragrance-name {
+ font-size: 40px;
+ font-weight: 500;
+ line-height: 64px;
+ color: @black-text-main;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ text-align: left;
+ }
+
+ .tf-card-footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ .tf-card-footer-text {
+ font-size: 20px;
+ line-height: normal;
+ letter-spacing: normal;
+ color: rgba(20, 30, 39, 0.5);
+ }
+
+ .tf-card-footer-arrow {
+ flex-shrink: 0;
+ width: 20px;
+ height: 20px;
+ }
+
+ /* 香氛入口卡片:背景随开关状态切换 */
+ .tf-card-fragrance {
+ &.is-open {
+ background-image: url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/fragrance-open-20260703.png');
+ }
+
+ &.is-close {
+ background-image: url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/fragrance-close-20260703.png');
+
+ // 关闭态卡片文字统一弱化
+ .tf-card-fragrance-name,
+ .tf-card-footer-text {
+ color: rgba(20, 30, 39, 0.3);
+ }
+ }
+ }
+
+ // 底部弹窗面板
+ .fragrance-popup {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 1000;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ pointer-events: none;
+
+ &.show {
+ pointer-events: auto;
+ }
+
+ .popup-mask {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.4);
+ z-index: 102;
+ }
+
+ .popup-content {
+ position: relative;
+ z-index: 103;
+ background: #FFFFFF;
+ border-radius: 60px 60px 0 0;
+ width: 100%;
+ // 设计稿左右边距 24px
+ padding: 0 0 calc(48px + env(safe-area-inset-bottom));
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ transform: translateY(100%);
+ }
+
+ .popup-header {
+ height: 112px;
+ padding-top: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ flex-shrink: 0;
+
+ .popup-title {
+ font-size: 32px;
+ font-weight: 500;
+ line-height: 32px;
+ color: rgba(0, 0, 0, 0.9);
+ }
+
+ .popup-close {
+ position: absolute;
+ right: 32px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 40px;
+ height: 40px;
+ }
+ }
+
+ // 香型选择区
+ .popup-scent-area {
+ margin-top: 48px;
+ padding: 0 56px;
+ }
+
+ .popup-scent-list {
+ display: flex;
+ align-items: stretch;
+ justify-content: space-between;
+
+ // 香型卡片:竖排香型名 + 香调描述,背景使用设计切图(选中/未选)
+ .popup-scent-item {
+ position: relative;
+ flex-shrink: 0;
+ // 设计稿卡片尺寸 90px × 208px
+ width: 180px;
+ height: 416px;
+ padding: 24px 0 20px;
+ box-sizing: border-box;
+ border-radius: 64px;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: 100% 100%;
+ // 未选中背景图
+ background-image: url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/fragrance_item_unselect@2x.png');
+
+ // 下发中禁止重复点击
+ &.disabled {
+ opacity: 0.6;
+ }
+
+ // 下发中 loading 图标(叠加于卡片上部,水平居中,旋转)
+ .popup-scent-loading {
+ position: absolute;
+ top: 40px;
+ left: 50%;
+ width: 64px;
+ height: 64px;
+ margin: 0 0 0 -32px;
+ z-index: 1;
+ animation: fragrance-spin 1s linear infinite;
+ }
+
+ .popup-scent-name {
+ // 香型名竖排
+ writing-mode: vertical-rl;
+ letter-spacing: 16px;
+ font-size: 32px;
+ line-height: 48px;
+ font-weight: 500;
+ color: rgba(20, 30, 39, 0.3);
+ }
+
+ // 选中背景图 + 白色文字
+ &.active {
+ background-image: url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/fragrance_item_select@2x.png');
+
+ .popup-scent-name {
+ color: #FFFFFF;
+ }
+ }
+ }
+ }
+
+ .popup-scent-empty {
+ padding: 96px 0;
+ text-align: center;
+ font-size: 28px;
+ line-height: 40px;
+ color: rgba(0, 0, 0, 0.4);
+ }
+ }
+}
diff --git a/src/robotaxi/pages/begin-route/components/css-folder/modify-destination-review.less b/src/robotaxi/pages/begin-route/components/css-folder/modify-destination-review.less
index 29d00c1..ca99d1e 100644
--- a/src/robotaxi/pages/begin-route/components/css-folder/modify-destination-review.less
+++ b/src/robotaxi/pages/begin-route/components/css-folder/modify-destination-review.less
@@ -1,3 +1,6 @@
+@import '../../../../style/ui-variables.less';
+
+
.modify-destination-review-container {
position: relative;
width: 100vw;
@@ -6,7 +9,7 @@
.modify-destination-review {
position: relative;
width: 100%;
- border-radius: 40px 40px 0 0;
+ border-radius: 60px 60px 0 0;
background: transparent;
z-index: 10;
overflow: hidden;
@@ -25,26 +28,24 @@
&-content-wrapper {
position: relative;
- padding-bottom: 180px;
- background: #f2f3f7 url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/site-top-bg-20260507.png') no-repeat top center;
- background-size: 100% auto;
- border-top: 2px solid #ffffff;
- border-radius: 40px 40px 0 0;
+ padding-bottom: 204px;
+ background: @gary-theme;
+ border-radius: 60px 60px 0 0;
}
&-content {
position: relative;
- padding: 40px 32px 40px;
+ padding: 48px;
z-index: 10;
}
&-title {
position: relative;
z-index: 1;
- font-size: 28px;
+ font-size: 36px;
font-weight: 500;
- line-height: 40px;
- color: rgba(0, 0, 0, 0.9);
+ line-height: 48px;
+ color: @black-text-main;
}
.modify-destination-review-tip {
@@ -54,21 +55,21 @@
font-size: 24px;
line-height: 36px;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.4);
+ color: @black-text-04;
margin-bottom: 32px;
}
- &-sites,
- &-sites-card {
+ // 站点样式参考 cancel-popup 的 cancel-site-box:灰底盒子 + 左侧起终点圆点虚线列
+ .modify-destination-review-select-site {
position: relative;
z-index: 1;
- padding: 24px;
- border-radius: 24px;
- background: #ffffff;
+ display: flex;
+ gap: 16px;
+ padding: 40px 32px 36px 24px;
+ border-radius: 40px;
+ background: rgba(204, 208, 215, 0.25);
box-sizing: border-box;
- border: 2px solid #ffffff;
- backdrop-filter: blur(20px);
- box-shadow: 0 40px 60px 0 rgba(87, 99, 147, 0.1);
+ height: fit-content;
}
&-trip-card {
@@ -81,48 +82,56 @@
box-shadow: 0 20px 40px 0 rgba(87, 99, 147, 0.08);
}
- &-site-item {
- display: flex;
- align-items: center;
- min-height: 92px;
- padding: 0 24px;
- border-radius: 20px;
-
- &+& {
- margin-top: 12px;
- }
+ .modify-destination-review-select-site-start {
+ color: @black-text-04;
}
- &-site-item-active {
- background: #F5F6FA;
- }
-
- &-site-dot {
- width: 12px;
- height: 12px;
- margin-right: 20px;
- border-radius: 50%;
+ .modify-destination-review-select-site-icon {
+ padding: 16px 0;
flex-shrink: 0;
-
- &-old {
- background: #768aff;
- }
-
- &-new {
- background: #9cc14e;
- }
+ display: flex;
+ flex-direction: column;
+ align-items: center;
}
- .start-txt {
- color: rgba(0, 0, 0, 0.4);
+ .modify-destination-review-select-site-icon-start {
+ width: 16px;
+ height: 16px;
+ background: #DFB369;
+ border-radius: 50%;
}
- &-site-text {
+ .modify-destination-review-select-site-icon-end {
+ width: 16px;
+ height: 16px;
+ background: @black-text-main;
+ border-radius: 50%;
+ }
+
+ .modify-destination-review-select-site-icon image {
flex: 1;
- font-size: 30px;
- line-height: 42px;
- color: rgba(0, 0, 0, 0.9);
+ width: 2px;
+ margin: 7px 0;
+ height: fit-content;
+ }
+
+ .modify-destination-review-select-site-content {
+ flex: 1;
+ min-width: 0;
+ font-size: 32px;
+ line-height: 48px;
+ letter-spacing: normal;
+ color: @black-text-main;
+ }
+
+ .modify-destination-review-select-site-start,
+ .modify-destination-review-select-site-end {
word-break: break-all;
+ }
+
+ .modify-destination-review-select-site-divider {
+ margin: 34px 32px 40px 0px;
+ border-top: 1px solid rgba(204, 208, 215, 0.25);
}
&-footer {
@@ -131,10 +140,10 @@
right: 0;
bottom: 0;
z-index: 11;
- height: 180px;
+ height: 204px;
box-sizing: border-box;
- padding: 24px 38px 0 32px;
- background: #fefeff;
+ padding: 28px 48px 72px 48px;
+ background: @gary-theme;
display: flex;
align-items: center;
justify-content: space-between;
@@ -145,12 +154,21 @@
min-width: 0;
}
- &-fare-main {
+ // 预估价样式参考 release-route 的 money-box(route-content-top-money)
+ .modify-destination-review-fare-main {
+ display: flex;
+ align-items: first baseline;
+ font-size: 20px;
+ line-height: normal;
+ letter-spacing: normal;
+ color: rgba(20, 30, 39, 0.4);
+ }
+
+ &-fare-forecast {
+ margin-top: 28px;
display: flex;
align-items: center;
- font-size: 24px;
- line-height: 36px;
- color: rgba(0, 0, 0, 0.9);
+ gap: 4px;
}
&-fare-original {
@@ -166,49 +184,63 @@
}
}
- &-tip-icon {
- width: 22px;
- height: 22px;
- margin-right: 4px;
+ .modify-destination-review-tip-icon {
+ width: 20px;
+ height: 20px;
flex-shrink: 0;
}
&-operate-free {
font-size: 28px;
+ font-weight: 500;
line-height: 40px;
- color: #f58900;
+ color: #A68042;
}
- &-fare-num {
- margin: 0 6px;
- font-size: 56px;
- line-height: 56px;
- color: rgba(0, 0, 0, 0.9);
+ .modify-destination-review-fare-num {
+ margin: 0 4px;
+ font-size: 60px;
+ line-height: normal;
+ letter-spacing: normal;
+ font-family: "XPSANS-Bold";
+ color: #141E27;
+ }
+
+ &-fare-unit {
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ gap: 8px;
}
&-fare-discount {
+ display: inline-flex;
+ align-items: center;
+ height: 36px;
+ padding: 4px 10px;
margin-left: 30px;
margin-top: 8px;
+ background: rgba(223, 179, 105, 0.2);
+ border-radius: 8px;
font-size: 20px;
- line-height: 20px;
- color: rgba(0, 0, 0, 0.4);
+ color: #A68042;
&-amount {
- color: rgba(245, 137, 0, 1);
+ color: #A68042;
}
}
&-confirm {
flex-shrink: 0;
width: 296px;
- height: 94px;
- border-radius: 20px;
- background: linear-gradient(102deg, #8475ff 0%, #7dd6ff 102%);
+ height: 104px;
+ border-radius: 2000px;
+ background: @black-text-main;
display: flex;
align-items: center;
justify-content: center;
- font-size: 28px;
- line-height: 40px;
+ font-size: 32px;
+ line-height: 44px;
color: #ffffff;
}
@@ -221,7 +253,8 @@
position: relative;
flex-shrink: 0;
width: 296px;
- height: 94px;
+ height: 104px;
+ border-radius: 2000px;
}
&-confirm-mask {
@@ -230,7 +263,7 @@
left: 0;
right: 0;
bottom: 0;
- border-radius: 20px;
+ border-radius: 2000px;
background: rgba(255, 255, 255, 0.45);
z-index: 2;
}
diff --git a/src/robotaxi/pages/begin-route/components/css-folder/no-route-popup.less b/src/robotaxi/pages/begin-route/components/css-folder/no-route-popup.less
index 1341b99..97e1c96 100644
--- a/src/robotaxi/pages/begin-route/components/css-folder/no-route-popup.less
+++ b/src/robotaxi/pages/begin-route/components/css-folder/no-route-popup.less
@@ -1,3 +1,5 @@
+@import '../../../../style/ui-variables.less';
+
.no-route-popup {
position: fixed;
top: 0;
@@ -20,8 +22,8 @@
bottom: 0;
left: 0;
right: 0;
- background: #ffffff;
- border-radius: 32px 32px 0 0;
+ background: @gary-theme;
+ border-radius: 60px 60px 0 0;
padding: 40px 40px 82px 40px;
box-sizing: border-box;
transform: translateY(500px);
@@ -46,12 +48,12 @@
.confirm-btn {
width: 100%;
line-height: 88px;
- border-radius: 16px;
- background: linear-gradient(95deg, #8475FF 0%, #7DD6FF 124%);
+ border-radius: 2000px;
font-size: 28px;
font-weight: 500;
- color: #FFFFFF;
+ color: @black-text-main;
text-align: center;
+ border: 2px solid @black-text-main;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/robotaxi/pages/begin-route/components/css-folder/over-route.less b/src/robotaxi/pages/begin-route/components/css-folder/over-route.less
index 5a4567f..ba7f3bc 100644
--- a/src/robotaxi/pages/begin-route/components/css-folder/over-route.less
+++ b/src/robotaxi/pages/begin-route/components/css-folder/over-route.less
@@ -20,48 +20,68 @@
.over-content {
position: relative;
- width: calc(100% - 150px);
+ width: 600rpx;
+ height: 380rpx;
box-sizing: border-box;
- border-radius: 32px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 56rpx 40rpx 48rpx 40rpx;
+ gap: 54rpx;
+ border-radius: 48rpx;
background: #FFFFFF;
- padding: 84px 0 56px 0;
- margin: 0 75px;
text-align: center;
+
+ .over-text-group {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 16rpx;
+ width: 100%;
+ }
.close-icon {
position: absolute;
- top: 32px;
- right: 32px;
- width: 40px;
- height: 40px;
+ top: 24rpx;
+ right: 24rpx;
+ width: 48rpx;
+ height: 48rpx;
}
.over-title {
- font-size: 36px;
- font-weight: 500;
- line-height: 52px;
+ font-size: 36rpx;
+ font-weight: 600;
+ line-height: 52rpx;
color: rgba(0, 0, 0, 0.9);
}
.over-txt {
- margin-top: 16px;
- font-size: 28px;
- line-height: 52px;
- color: rgba(0, 0, 0, 0.6);
+ margin-top: 0;
+ font-size: 28rpx;
+ line-height: 42rpx;
+ color: rgba(0, 0, 0, 0.4);
}
.reselect-btn {
- width: 404px;
- margin: 60px auto 0;
- height: 96px;
+ width: 404rpx;
+ height: 110rpx;
+ box-sizing: border-box;
+ margin-top: 0;
display: flex;
+ flex-direction: column;
justify-content: center;
align-items: center;
- border-radius: 16px;
- background: linear-gradient(95deg, #8475FF 0%, #7DD6FF 124%);
- font-size: 28px;
+ padding: 32rpx 80rpx;
+ gap: 16rpx;
+ border-radius: 1000rpx;
+ border: 2rpx solid #141E27;
+ font-size: 36rpx;
+ font-weight: 500;
+ line-height: 52rpx;
letter-spacing: normal;
- color: #FFFFFF;
+ color: #141E27;
+ background: transparent;
+ z-index: 0;
}
}
}
diff --git a/src/robotaxi/pages/begin-route/components/css-folder/passenger-notice.less b/src/robotaxi/pages/begin-route/components/css-folder/passenger-notice.less
index 3ea6872..0c5381a 100644
--- a/src/robotaxi/pages/begin-route/components/css-folder/passenger-notice.less
+++ b/src/robotaxi/pages/begin-route/components/css-folder/passenger-notice.less
@@ -1,3 +1,5 @@
+@import '../../../../style/ui-variables.less';
+
.passenger-popup {
position: fixed;
top: 0;
@@ -12,7 +14,7 @@
left: 0;
right: 0;
bottom: 0;
- background: rgba(0, 0, 0, 0.5);
+ background: rgba(0, 0, 0, 0.8);
}
.passenger-content {
@@ -20,9 +22,9 @@
bottom: 0;
left: 0;
right: 0;
- background: #ffffff;
- border-radius: 40px 40px 0 0;
- padding: 48px 18px 68px 48px;
+ background: @gary-theme;
+ border-radius: 60px 60px 0px 0px;
+ padding: 48px 48px 80px 48px;
height: 75vh;
overflow: hidden;
.passenger-content-bg {
@@ -31,9 +33,9 @@
right: 0;
left: 0;
width: 100%;
- min-height: 744px;
+ min-height: 666px;
z-index: -1;
- border-radius: 16px 16px 0 0;
+ border-radius: 60px 60px 0px 0px;
overflow: hidden;
.passenger-content-bg-img {
width: 100%;
@@ -41,19 +43,18 @@
}
&-title {
- font-size: 32px;
+ font-size: 36px;
font-weight: 500;
line-height: 48px;
text-align: center;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
+ color: @black-text-main;
}
&-desc-wrap {
position: relative;
margin-top: 34px;
height: calc(75vh - 400px);
- padding-right: 1em;
z-index: 1;
.passenger-content-desc {
height: 100%;
@@ -107,13 +108,16 @@
&-btn {
margin-top: 30px;
margin-right: 32px;
- border-radius: 16px;
- background: linear-gradient(96deg, #8475FF -1%, #7DD6FF 103%);
+ border-radius: 2000px;
+ box-sizing: border-box;
+ border: 4px solid #141E27;
+ background: @gary-theme;
line-height: 96px;
text-align: center;
- font-size: 30px;
+ font-size: 32px;
+ font-weight: 500;
letter-spacing: normal;
- color: #FFFFFF;
+ color: @black-text-main;
}
}
}
diff --git a/src/robotaxi/pages/begin-route/components/css-folder/release-be-being.less b/src/robotaxi/pages/begin-route/components/css-folder/release-be-being.less
index 13e152b..1307acf 100644
--- a/src/robotaxi/pages/begin-route/components/css-folder/release-be-being.less
+++ b/src/robotaxi/pages/begin-route/components/css-folder/release-be-being.less
@@ -1,77 +1,138 @@
+@import '../../../../style/ui-variables.less';
+
.release-be-being {
- border-radius: 32px 32px 0 0;
- padding: 37px 40px 94px 40px;
- background: #FFFFFF;
+ position: relative;
+ background: @gary-theme;
+ border-radius: 60px 60px 0px 0px;
+ overflow: hidden;
+ padding-top: 84px;
+ .release-be-being-content {
+ position: relative;
+ z-index: 1;
+ border-radius: 60px 60px 0px 0px;
+ padding: 48px 48px 112px 48px;
+ background: @gary-theme;
+ }
+ .release-be-being-top-bg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 276px;
+ }
+ .top-box {
+ .top-box-title {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
- .top-box {
+ .title {
+ margin-top: 3px;
+ font-size: 40px;
+ font-weight: bold;
+ letter-spacing: normal;
+ color: #141e27;
+ }
- .top-box-title {
- display: flex;
- align-items: center;
- justify-content: space-between;
-
- .title {
- margin-top: 3px;
- font-size: 36px;
- font-weight: bold;
- line-height: 52px;
- letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
- }
-
- .cancel-btn {
- flex-shrink: 0;
- width: 144px;
- height: 48px;
- }
- }
-
- .progress-box {
- margin-top: 24px;
- height: 8px;
- background: #F2F3F7;
- border-radius: 16px;
-
- .progress {
- height: 8px;
- border-radius: 16px;
- background: linear-gradient(91deg, #8475FF 0%, #7DD6FF 104%);
- transition: width 2s ease-out;
- }
- }
+ .cancel-btn {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 162px;
+ height: 58px;
+ background: rgba(204, 208, 215, 0.25);
+ border-radius: 58px;
+ font-size: 24px;
+ color: #141e27;
+ }
}
- .route-state-box {
- margin-top: 64px;
-
- &-item {
- display: flex;
- align-items: center;
- gap: 16px;
- margin-bottom: 24px;
-
- &-dot {}
-
- &-icon {
- width: 32px;
- height: 32px;
- flex-shrink: 0;
- }
-
- &-text {
- font-size: 28px;
- line-height: 44px;
- letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
- }
-
- &-text-strong {
- font-size: 32px;
- font-weight: bold;
- line-height: 48px;
- letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
- }
- }
+ .progress-box {
+ position: relative;
+ margin-top: 24px;
+ height: 32px;
+ .progress-icon {
+ position: absolute;
+ top: 0;
+ left: -22px;
+ width: 678px;
+ height: 32px;
+ }
}
-}
\ No newline at end of file
+ }
+
+ .route-card {
+ margin-top: 48px;
+ padding: 48px 40px;
+ background: rgba(204, 208, 215, 0.25);
+ border-radius: 40px;
+
+ .route-card-item {
+ position: relative;
+
+ display: flex;
+ align-items: flex-start;
+ gap: 16px;
+
+ &.route-card-item-first {
+ padding-bottom: 80px;
+ }
+ // 虚线连接线:以 item 盒子为基准,用 top/bottom 锚定首尾 marker 中心,
+ // 文字换行导致 item 变高时自动拉伸,无需固定高度。
+ .route-card-item-line {
+ position: absolute;
+ left: 14px;
+ top: 40px;
+ bottom: -8px;
+ transform: translateX(-50%);
+ width: 0;
+ border-left: 2px dashed #CCD0D7;
+ }
+ // 两项之间的水平分割线:与文字列左对齐,居中于 48px 行间距内。
+ .route-card-item-divider {
+ position: absolute;
+ left: 48px;
+ right: 0;
+ top: 88px;
+ height: 0;
+ border-top: 1px solid rgba(204, 208, 215, 0.25);
+ }
+ .route-card-item-marker {
+ position: relative;
+ flex-shrink: 0;
+ width: 28px;
+ height: 44px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .route-card-item-check {
+ position: relative;
+ width: 28px;
+ height: 28px;
+ }
+
+ .route-card-item-dot {
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ background: #141e27;
+ &.route-card-item-dot-first {
+ background: #dfb369;
+ }
+ }
+ }
+
+ .route-card-item-text {
+ flex: 1;
+ font-size: 32px;
+ font-weight: 500;
+ line-height: 48px;
+ letter-spacing: normal;
+ color: #141e27;
+ word-break: break-all;
+ }
+ }
+ }
+}
diff --git a/src/robotaxi/pages/begin-route/components/css-folder/release-route.less b/src/robotaxi/pages/begin-route/components/css-folder/release-route.less
index 2896831..2593e95 100644
--- a/src/robotaxi/pages/begin-route/components/css-folder/release-route.less
+++ b/src/robotaxi/pages/begin-route/components/css-folder/release-route.less
@@ -1,6 +1,8 @@
+@import '../../../../style/ui-variables.less';
+
.release-route-wrapper {
position: relative;
- border-radius: 40px 40px 0 0;
+ border-radius: 60px 60px 0 0;
background: transparent;
z-index: 10;
width: 100vw;
@@ -35,7 +37,7 @@
font-weight: 500;
line-height: 44px;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
+ color: @black-text-main;
}
.route-select-preference {
@@ -47,7 +49,7 @@
font-weight: 500;
line-height: 40px;
letter-spacing: normal;
- color: #6D7FD9;
+ color: @gold-text;
}
image {
@@ -76,7 +78,7 @@
font-size: 20px;
line-height: 20px;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.4);
+ color: @black-text-main;
}
.route-select-content-item-time {
@@ -85,7 +87,7 @@
font-weight: 500;
line-height: 40px;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
+ color: @black-text-main;
}
.route-select-content-item-distance {
@@ -96,7 +98,7 @@
font-size: 20px;
line-height: 20px;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.4);
+ color: @black-text-main;
.traffic-signal-item {
display: flex;
@@ -113,12 +115,12 @@
}
.active-item {
- background: linear-gradient(97deg, rgba(132, 117, 255, 0.2) 0%, rgba(125, 214, 255, 0.2) 103%) !important;
+ background: linear-gradient(97deg, rgba(255, 209, 129, 0.2) 0%, rgba(223, 179, 105, 0.2) 103%) !important;
box-sizing: border-box;
position: relative;
.route-select-content-item-title {
- color: #6D7FD9;
+ color: @gold-text;
}
&::after {
@@ -130,7 +132,7 @@
bottom: 0;
border-radius: 16px;
padding: 1px;
- background: linear-gradient(180deg, #AAA8FF -10%, rgba(41, 159, 255, 0.0469) 108%, rgba(0, 0, 0, 0) 108%);
+ background: linear-gradient(180deg, @gold-light -10%, rgba(223, 179, 105, 0.0469) 108%, rgba(0, 0, 0, 0) 108%);
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
@@ -142,28 +144,37 @@
.select-site {
position: relative;
- padding: 32px;
- padding-top: 40px;
- background: #F2F3F7;
- border-top: 2px solid #fff;
- border-radius: 40px 40px 0 0;
+ padding-bottom: 184px;
+ padding-top: 94px;
+ background: @gary-theme;
+ border-radius: 60px 60px 0 0;
+ box-sizing: border-box;
+ .select-site-content {
+ position: relative;
+ z-index: 1;
+ border-radius: 60px 60px 0px 0px;
+ padding: 0 48px;
+ padding-top: 32px ;
+ background: @gary-theme;
+ }
+
.site-top-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
- height: 326px;
+ height: 276px;
}
.site-boarding-point {
- border-radius: 24px;
- background: rgba(255, 255, 255, 0.9);
- box-sizing: border-box;
- border: 2px solid #FFFFFF;
- backdrop-filter: blur(20px);
- box-shadow: 0px 40px 60px 0px rgba(87, 99, 147, 0.1);
- padding: 24px;
+ // border-radius: 24px;
+ // background: rgba(255, 255, 255, 0.9);
+ // box-sizing: border-box;
+ // border: 2px solid #FFFFFF;
+ // backdrop-filter: blur(20px);
+ // box-shadow: 0px 40px 60px 0px rgba(87, 99, 147, 0.1);
+ // padding: 24px;
.now-site {
display: flex;
@@ -172,34 +183,49 @@
.now-site-dot {
flex-shrink: 0;
- width: 12px;
- height: 12px;
+ width: 16px;
+ height: 16px;
+ flex-shrink: 0;
border-radius: 50%;
- background: #6D7FD9;
+ background: @gold-light;
margin-right: 24px;
}
.now-site-address {
display: flex;
align-items: center;
+ flex: 1;
+ min-width: 0;
.now-site-address-content {
+ min-width: 0;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
font-size: 28px;
font-weight: 500;
line-height: 44px;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
+ color:rgba(20, 30, 39, 0.8);
+ white-space: nowrap;
.now-site-address-text {
- margin: 0 2px;
- color: #6D7FD9;
+ flex: 1;
+ min-width: 0;
+ display: inline-block;
+ margin: 0 8px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ color: @gold-text;
}
}
.now-site-icon {
+ flex-shrink: 0;
margin-left: 2px;
- width: 32px;
- height: 32px;
+ width: 24px;
+ height: 24px;
}
}
}
@@ -209,7 +235,7 @@
font-weight: normal;
line-height: normal;
letter-spacing: normal;
- color: #F58900;
+ color: @black-text-main;
margin-left: 64px;
}
@@ -217,30 +243,37 @@
margin-top: 32px;
display: flex;
align-items: center;
- padding: 20px 0 20px 28px;
- border-radius: 20px;
- background: #F1F2F6;
+ padding: 32px;
+ border-radius: 40px;
+ background: #FFFFFF;
.to-site-dot {
flex-shrink: 0;
- width: 12px;
- height: 12px;
+ width: 16px;
+ height: 16px;
border-radius: 50%;
- background: #8CB654;
+ background: #000000;
margin-right: 24px;
}
.to-site-address {
display: flex;
align-items: center;
+ flex: 1;
+ min-width: 0;
.to-site-text {
- font-size: 28px;
- font-weight: 500;
- line-height: 48px;
+ flex: 1;
+ min-width: 0;
+ font-size: 40px;
+ font-weight: 500;
+ line-height: 56px;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
+ color: @black-text-main;
margin-right: 22px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
}
}
@@ -248,8 +281,9 @@
.route-content {
position: relative;
- margin-top: 28px;
- margin-bottom: 24px;
+ margin-top: 16px;
+ padding-bottom: 36px;
+ box-sizing: border-box;
z-index: 10;
display: flex;
align-items: center;
@@ -268,9 +302,8 @@
.route-content-txt {
font-size: 24px;
- line-height: 40px;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.4);
+ color: @black-text-04;
}
.route-content-bottom {
@@ -279,12 +312,13 @@
display: flex;
align-items: center;
font-size: 24px;
- line-height: 40px;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.6);
+ color: @black-text-04;
.tip-icon {
- margin-right: 6px;
+ width: 24px;
+ height: 24px;
+ margin-right: 6px;
}
}
}
@@ -303,8 +337,9 @@
display: flex;
align-items: center;
justify-content: space-between;
+ height: 56px;
padding: 8px 32px 8px 24px;
- background: #FFF3E6;
+ background: rgba(208, 151, 55, 0.15);
.flex-center {
display: flex;
@@ -313,24 +348,23 @@
.discount-amount-text {
flex-shrink: 0;
- border-radius: 6px;
- background: rgba(245, 137, 0, 0.06);
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0);
box-sizing: border-box;
- border: 2px solid #F58900;
- line-height: 30px;
+ border: 1.4px solid @gold-text;
text-align: center;
- padding: 0px 6px;
+ padding: 6px 10px;
font-size: 16px;
font-weight: 500;
- color: #F58900;
+ color: @gold-text;
}
.discount-amount-desc {
- margin-left: 10px;
+ margin-left: 8px;
font-size: 24px;
line-height: 40px;
letter-spacing: normal;
- color: #F58900;
+ color: @gold-text;
}
.discount-amount-num {
@@ -339,7 +373,7 @@
font-weight: 500;
line-height: 40px;
letter-spacing: normal;
- color: #F58900;
+ color: @gold-text;
}
.change-coupon-icon {
@@ -353,8 +387,8 @@
display: flex;
justify-content: space-between;
align-items: center;
- padding: 24px 38px 62px 32px;
- background: #FEFEFF;
+ padding: 8px 38px 62px 32px;
+ background: @gary-theme;
.money-box {
display: flex;
@@ -362,64 +396,83 @@
.route-operate-free {
font-size: 28px;
+ font-weight: 500;
line-height: 40px;
- color: #F58900;
+ color: #A68042;
}
.route-content-top-money {
display: flex;
- align-items: center;
- font-size: 24px;
+ align-items: first baseline;
+ font-size: 20px;
+ line-height: normal;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
-
- .price-box {
- display: flex;
- align-items: center;
-
- .price-box-text {
- font-size: 24px;
- line-height: 24px;
- }
+ color: @black-text-04;
+ .tip-icon-box {
+ display: flex;
+ justify-content: first baseline;
}
-
+ .forecast-text {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ }
.tip-icon {
- width: 22px;
- height: 22px;
- flex-shrink: 0;
- margin-right: 4px;
+ width: 20px;
+ height: 20px;
+ flex-shrink: 0;
+ }
+ .unit-text {
+ display: flex;
+ align-items: center;
}
.money-num {
margin: 0 4px;
- font-size: 48px;
- line-height: 40px;
+ font-size: 60px;
+ line-height: normal;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
- margin-bottom: 10px;
- font-weight: 500;
+ font-family: @font-xpsans-bold;
+ color: @black-text-main;
}
}
}
.confirm-btn-wrapper {
- width: 296px;
+ width: 328px;
+ height: 104px;
position: relative;
- height: 94px;
+ }
+
+ .confirm-btn-bg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ width: 328px;
+ height: 104px;
+ z-index: 1;
}
.confirm-btn {
- width: 296px;
- height: 94px;
- display: flex;
- justify-content: center;
- align-items: center;
- border-radius: 20px;
- background: linear-gradient(102deg, #8475FF 0%, #7DD6FF 102%);
- font-size: 28px;
- font-weight: 500;
- letter-spacing: normal;
- color: #FFFFFF;
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 328px;
+ height: 104px;
+ border-radius: 2000px;
+ font-size: 32px;
+ font-weight: 500;
+ letter-spacing: normal;
+ color: #FFFFFF;
+ z-index: 2;
+ .confirm-btn-icon {
+ width: 40px;
+ height: 40px;
+ margin-right: 8px;
+ }
}
.confirm-btn-mask {
@@ -428,9 +481,10 @@
left: 0;
right: 0;
bottom: 0;
- border-radius: 20px;
+ border-radius: 2000px;
background: rgba(255, 255, 255, 0.4);
+ z-index: 3;
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/robotaxi/pages/begin-route/components/css-folder/route-selection-panel.less b/src/robotaxi/pages/begin-route/components/css-folder/route-selection-panel.less
index 914638f..13b59d4 100644
--- a/src/robotaxi/pages/begin-route/components/css-folder/route-selection-panel.less
+++ b/src/robotaxi/pages/begin-route/components/css-folder/route-selection-panel.less
@@ -1,3 +1,4 @@
+@import '../../../../style/ui-variables.less';
.route-select {
border-radius: 24px;
background: #FFFFFF;
@@ -25,7 +26,7 @@
font-size: 24px;
font-weight: 500;
line-height: 40px;
- color: #6D7FD9;
+ color: @gold-text;
font-weight: 500;
}
@@ -54,7 +55,7 @@
.route-select-content-item-title {
font-size: 20px;
line-height: 20px;
- color: rgba(0, 0, 0, 0.4);
+ color: rgba(0, 0, 0, 0.9);
}
.route-select-content-item-time {
@@ -72,7 +73,7 @@
margin-top: 12px;
font-size: 20px;
line-height: 20px;
- color: rgba(0, 0, 0, 0.4);
+ color: rgba(0, 0, 0, 0.9);
.traffic-signal-item {
display: flex;
@@ -89,12 +90,11 @@
}
.active-item {
- background: linear-gradient(97deg, rgba(132, 117, 255, 0.2) 0%, rgba(125, 214, 255, 0.2) 103%) !important;
+ background: linear-gradient(97deg, rgba(255, 209, 129, 0.2) 0%, rgba(223, 179, 105, 0.2) 103%) !important;
box-sizing: border-box;
position: relative;
-
.route-select-content-item-title {
- color: #6D7FD9;
+ color: @gold-text;
}
&::after {
@@ -106,7 +106,7 @@
bottom: 0;
border-radius: 16px;
padding: 1px;
- background: linear-gradient(180deg, #AAA8FF -10%, rgba(41, 159, 255, 0.0469) 108%, rgba(0, 0, 0, 0) 108%);
+ background: linear-gradient(180deg, @gold-light -10%, rgba(223, 179, 105, 0.0469) 108%, rgba(0, 0, 0, 0) 108%);
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
diff --git a/src/robotaxi/pages/begin-route/components/css-folder/seat-hv-control.less b/src/robotaxi/pages/begin-route/components/css-folder/seat-hv-control.less
new file mode 100644
index 0000000..ed7710f
--- /dev/null
+++ b/src/robotaxi/pages/begin-route/components/css-folder/seat-hv-control.less
@@ -0,0 +1,143 @@
+// loading 图标旋转动画
+@keyframes seat-hv-spin {
+ from {
+ transform: rotate(0deg);
+ }
+
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+// 座椅通风加热区(内嵌于「温度调节」面板)
+.seat-hv-area {
+ position: relative;
+ width: 100%;
+ overflow: hidden;
+
+ // 座舱俯视图背景(widthFix,高度按图片比例自适应,撑起区域高度)
+ .seat-hv-bg {
+ width: 100%;
+ display: block;
+ }
+
+ // 座椅节点叠放层
+ .seat-hv-list {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ }
+
+ // 每个座椅控制节点,以中心点百分比定位(对照 seat_bg 座椅位置)
+ .seat-hv-node {
+ position: absolute;
+ transform: translate(-50%, -50%);
+ }
+
+ // 二排(目前唯一可控排):按钮位于座舱图中部横排,左右座椅中心分别约 28% / 72%
+ .seat-hv-node-SECOND_ROW_LEFT {
+ top: 45%;
+ left: 28.5%;
+ }
+
+ .seat-hv-node-SECOND_ROW_RIGHT {
+ top: 45%;
+ left: 71.5%;
+ }
+
+ // 三排:纵向约 60%,左右座椅中心分别约 28% / 72%
+ .seat-hv-node-THIRD_ROW_LEFT {
+ top: 54%;
+ left: 28.5%;
+ }
+
+ .seat-hv-node-THIRD_ROW_RIGHT {
+ top: 54%;
+ left: 71.5%;
+ }
+
+ // 控制器:胶囊(通风+加热双图标)/ 单圆按钮
+ .seat-hv-ctrl {
+ display: flex;
+ align-items: center;
+ background: rgba(255, 255, 255, 0.92);
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
+
+ &.is-pill {
+ height: 88px;
+ padding: 0 28px;
+ border-radius: 44px;
+ }
+
+ &.is-single {
+ width: 96px;
+ height: 96px;
+ border-radius: 50%;
+ justify-content: center;
+ }
+ }
+
+ // 胶囊内通风/加热分隔线
+ .seat-hv-divider {
+ width: 2px;
+ height: 40px;
+ margin: 0 24px;
+ background: rgba(0, 0, 0, 0.1);
+ }
+
+ .seat-hv-btn {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &-icon {
+ width: 48px;
+ height: 48px;
+ }
+
+ // 下发中 loading 图标旋转(点击后至指令返回)
+ &-loading {
+ opacity: 1;
+ animation: seat-hv-spin 1s linear infinite;
+ }
+
+ &-level {
+ margin-left: 4px;
+ font-size: 24px;
+ line-height: 24px;
+ color: #45A5FF;
+ font-weight: 500;
+ }
+
+ // 开启态高亮
+ &.active {
+ .seat-hv-btn-icon {
+ opacity: 1;
+ }
+ }
+
+ // 未开启态图标弱化
+ &:not(.active) .seat-hv-btn-icon {
+ opacity: 0.55;
+ }
+
+ // 下发中置灰防重复
+ &.disabled {
+ opacity: 0.5;
+ }
+ }
+}
+
+// 无座椅占位
+.seat-hv-empty {
+ height: 320px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: #E9EBF0;
+ font-size: 28px;
+ color: rgba(0, 0, 0, 0.4);
+}
diff --git a/src/robotaxi/pages/begin-route/components/css-folder/service-route-change.less b/src/robotaxi/pages/begin-route/components/css-folder/service-route-change.less
index 12c0c85..6a08133 100644
--- a/src/robotaxi/pages/begin-route/components/css-folder/service-route-change.less
+++ b/src/robotaxi/pages/begin-route/components/css-folder/service-route-change.less
@@ -1,3 +1,4 @@
+@import '../../../../style/ui-variables.less';
.service-route-change-container {
background: #FFFFFF;
border-radius: 40px 40px 0 0;
@@ -22,7 +23,7 @@
font-size: 24px;
font-weight: 500;
line-height: 40px;
- color: #6D7FD9;
+ color: @gold-text;
font-weight: 500;
}
@@ -53,7 +54,7 @@
.service-route-select-content-item-title {
font-size: 20px;
line-height: 20px;
- color: rgba(0, 0, 0, 0.4);
+ color: rgba(0, 0, 0, 0.9);
}
.service-route-select-content-item-time {
@@ -71,7 +72,7 @@
margin-top: 12px;
font-size: 20px;
line-height: 20px;
- color: rgba(0, 0, 0, 0.4);
+ color: rgba(0, 0, 0, 0.9);
.service-traffic-signal-item {
display: flex;
@@ -95,12 +96,12 @@
}
.active {
- background: linear-gradient(97deg, rgba(132, 117, 255, 0.2) 0%, rgba(125, 214, 255, 0.2) 103%) !important;
+ background: linear-gradient(97deg, rgba(255, 209, 129, 0.2) 0%, rgba(223, 179, 105, 0.2) 103%) !important;
box-sizing: border-box;
position: relative;
.service-route-select-content-item-title {
- color: #6D7FD9;
+ color: @gold-text;
}
&::after {
@@ -112,7 +113,7 @@
bottom: 0;
border-radius: 16px;
padding: 1px;
- background: linear-gradient(180deg, #AAA8FF -10%, rgba(41, 159, 255, 0.0469) 108%, rgba(0, 0, 0, 0) 108%);
+ background: linear-gradient(180deg, @gold-light -10%, rgba(223, 179, 105, 0.0469) 108%, rgba(0, 0, 0, 0) 108%);
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
@@ -124,28 +125,31 @@
.service-route-select-footer {
display: flex;
justify-content: space-between;
- gap: 24px;
+ gap: 22px;
margin-top: 48px;
- font-size: 28px;
- font-weight: normal;
+ font-size: 32px;
+ font-weight: 500;
text-align: center;
.service-route-select-footer-item-cancel {
flex: 1;
- color: rgba(0, 0, 0, 0.9);
- border-radius: 16px;
+ color: #141E27;
+ border-radius: 1000px;
box-sizing: border-box;
- border: 2px solid rgba(0, 0, 0, 0.9);
- line-height: 92px;
+ border: 4px solid #141E27;
+ line-height: 104px;
+ height: 104px;
}
.service-route-select-footer-item-confirm {
flex: 1;
color: #FFFFFF;
- border-radius: 16px;
- line-height: 92px;
- border-radius: 20px;
- background: linear-gradient(102deg, #8475FF 0%, #7DD6FF 102%);
+ border-radius: 1000px;
+ line-height: 104px;
+ border: 4px solid #141E27;
+ box-sizing: border-box;
+ height: 104px;
+ background: #141E27;
}
}
}
\ No newline at end of file
diff --git a/src/robotaxi/pages/begin-route/components/css-folder/temperature-control.less b/src/robotaxi/pages/begin-route/components/css-folder/temperature-control.less
new file mode 100644
index 0000000..92c5959
--- /dev/null
+++ b/src/robotaxi/pages/begin-route/components/css-folder/temperature-control.less
@@ -0,0 +1,182 @@
+@import '../../../../style/ui-variables.less';
+
+.ac-control {
+ flex-shrink: 0;
+
+ // 温度调节面板(底部弹窗):座椅通风加热 + 空调温度滑动 + 空调开关
+ .ac-popup {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 1000;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ pointer-events: none;
+
+ &.show {
+ pointer-events: auto;
+ }
+
+ .popup-mask {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.4);
+ z-index: 102;
+ }
+
+ // 弹窗主体:顶部座椅区满宽,底部空调白卡
+ .popup-content {
+ position: relative;
+ z-index: 103;
+ width: 100%;
+ height: 1232px;
+ border-radius: 60px 60px 0 0;
+ overflow: hidden;
+ background: #FFFFFF;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ transform: translateY(100%);
+ }
+
+ // 座椅区容器(满宽,无侧边距)
+ .popup-seat {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+
+ .popup-close {
+ position: absolute;
+ top: 32px;
+ right: 32px;
+ z-index: 2;
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.85);
+ padding: 8px;
+ box-sizing: border-box;
+ }
+ }
+
+ // 空调区:底部白色圆角卡片,覆盖在座椅区之上
+ .popup-ac {
+ position: relative;
+ z-index: 1;
+ margin: 772px 32px calc(48px + env(safe-area-inset-bottom));
+ padding: 40px 20px;
+ background: #F3F7FC;
+ border-radius: 40px;
+ box-sizing: border-box;
+
+ .popup-ac-title {
+ display: block;
+ text-align: center;
+ font-size: 32px;
+ font-weight: 500;
+ line-height: 40px;
+ color: rgba(0, 0, 0, 0.9);
+ }
+ }
+
+ // 温度横向滑动选择器
+ .popup-temp-scroll {
+ margin-top: 24px;
+ width: 100%;
+ height: 120px;
+ white-space: nowrap;
+
+ &.disabled {
+ opacity: 0.4;
+ }
+ }
+
+ .popup-temp-track {
+ display: inline-flex;
+ align-items: center;
+ // 左右留白 = (视口宽 - 单项宽)/2,使任意项都能滚动到正中;单项 130px → 半宽 65px
+ padding-left: calc(50% - 65px);
+ padding-right: calc(50% - 65px);
+ box-sizing: border-box;
+ }
+
+ .popup-temp-item {
+ flex-shrink: 0;
+ // 所有项等宽(保证滚动居中正确、边缘项可见)
+ width: 130px;
+ height: 120px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .popup-temp-item-value {
+ font-family: @font-xpsans-bold;
+ font-size: 40px;
+ line-height: 44px;
+ // 未选中温度:#141E27 透明度 0.2(对齐设计标注)
+ color: #141E27;
+ opacity: 0.2;
+ transition: all 0.15s ease;
+ }
+
+ // 居中选中项:放大加粗(空调开启时高亮)
+ &.active {
+ width: 180px;
+ .popup-temp-item-value {
+ font-size: 64px;
+ line-height: 72px;
+ font-weight: 600;
+ color: @black-text-main;
+ opacity: 1;
+ }
+
+ .popup-temp-item-unit {
+ margin-left: 4px;
+ font-family: @font-xpsans-bold;
+ font-size: 28px;
+ line-height: 40px;
+ font-weight: 500;
+ color: @black-text-main;
+ }
+ }
+
+ // 空调关闭时的选中项:仍放大,但置灰(不高亮)
+ &.active-off {
+ .popup-temp-item-value,
+ .popup-temp-item-unit {
+ opacity: 0.2;
+ }
+ }
+ }
+
+ // 居中高亮引导(中间选中区)
+ .popup-temp-wrap {
+ position: relative;
+ }
+
+ // 空调开关行(居中,使用统一 AcSwitch 图片开关)
+ .popup-ac-switch-row {
+ margin-top: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .ac-switch {
+ margin-right: 12px;
+ }
+
+ .popup-ac-switch-text {
+ font-size: 28px;
+ line-height: 40px;
+ color: @black-text-main;
+ }
+ }
+ }
+}
diff --git a/src/robotaxi/pages/begin-route/components/debug-order-simulator-panel.tsx b/src/robotaxi/pages/begin-route/components/debug-order-simulator-panel.tsx
index ef99638..5c39a83 100644
--- a/src/robotaxi/pages/begin-route/components/debug-order-simulator-panel.tsx
+++ b/src/robotaxi/pages/begin-route/components/debug-order-simulator-panel.tsx
@@ -28,6 +28,14 @@
startSite: SitePoint;
/** 下车点坐标。 */
endSite: SitePoint;
+ /** 设置取消弹窗类型(用于本地手动触发各类取消弹窗文案)。 */
+ setCancelType?: (type: number) => void;
+ /** 设置取消弹窗展示状态。 */
+ setShowCancelPopup?: (show: boolean) => void;
+ /** 设置修改目的地失败弹窗展示状态(用于本地手动触发)。 */
+ setShowModifyDestinationFailPopup?: (show: boolean) => void;
+ /** 设置修改目的地失败弹窗文案。 */
+ setModifyDestinationFailMessage?: (message: string) => void;
};
/** 各阶段速度档位 → tick 间隔(毫秒)。 */
@@ -177,6 +185,41 @@
props.setTripStatus(TripStatus.PublishRoute);
};
+ /**
+ * 直达评价页:本地跳过「结束行程/支付」真实接口调用,直接把行程状态置为已支付(9),
+ * 用于测试满意度评价卡片(EvaluationCard)。
+ * 注意:费用明细(FareDetailCard)仍会尝试真实查询,模拟订单号(SIM-xxx)后端不存在,
+ * 会静默失败并展示为 0 元,不影响评价卡片的调试。
+ */
+ const handleJumpToEvaluatePage = () => {
+ simulatorRef.current?.stop();
+ replayRef.current?.stop();
+ setRunning(false);
+ setReplaying(false);
+ const orderId = resolveOrderId();
+ ensureSimulationOrderId(orderId);
+ props.setOrderId(orderId);
+ props.bindSocketListeners();
+ props.setTripStatus(TripStatus.PaymentSuccess);
+ };
+
+ /**
+ * 直达「行程结束未支付」(TripFinished=8):本地跳过「结束行程」真实接口,直接把行程状态置为待支付,
+ * 用于测试行程结束未支付页(费用明细 FareDetailCard、优惠券切换、支付按钮)。
+ * 注意:费用明细仍会尝试真实查询,模拟订单号(SIM-xxx)后端不存在会静默失败并展示为 0 元。
+ */
+ const handleJumpToTripFinished = () => {
+ simulatorRef.current?.stop();
+ replayRef.current?.stop();
+ setRunning(false);
+ setReplaying(false);
+ const orderId = resolveOrderId();
+ ensureSimulationOrderId(orderId);
+ props.setOrderId(orderId);
+ props.bindSocketListeners();
+ props.setTripStatus(TripStatus.TripFinished);
+ };
+
/** 各速度档位 → 回放时间缩放因子(越大越快)。 */
const REPLAY_SPEED_FACTOR: Record<keyof typeof SPEED_PRESETS, number> = {
slow: 8,
@@ -220,6 +263,23 @@
replayRef.current?.stop();
setReplaying(false);
};
+
+ /** 手动触发「附近没有空余车辆,已为您取消订单」弹窗(cancelType 5)。 */
+ const handleTriggerNoVehicleCancelPopup = () => {
+ props.setCancelType?.(5);
+ props.setShowCancelPopup?.(true);
+ };
+
+ /** 手动触发「修改目的地失败」弹窗。 */
+ const handleTriggerModifyDestinationFailPopup = () => {
+ console.log('[DebugPanel] 点击「修改目的地失败」,setter 是否存在:', {
+ hasSetMessage: typeof props.setModifyDestinationFailMessage === 'function',
+ hasSetShow: typeof props.setShowModifyDestinationFailPopup === 'function',
+ });
+ props.setModifyDestinationFailMessage?.('即将到达终点,已无法修改目的地');
+ props.setShowModifyDestinationFailPopup?.(true);
+ };
+
const buttonStyle = (background: string) => ({
flex: 1,
textAlign: 'center' as const,
@@ -291,7 +351,7 @@
}}
>
<View style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
- <Text style={{ fontSize: '14px', fontWeight: 'bold' }}>订单状态模拟器</Text>
+ <Text style={{ fontSize: '14px', fontWeight: 'bold' }}>订单状态模拟器 TEST123</Text>
<View
onClick={() => setExpanded(false)}
style={{ padding: '2px 10px', fontSize: '13px', background: 'rgba(255,255,255,0.15)', borderRadius: '8px' }}
@@ -389,6 +449,46 @@
<Text style={{ display: 'block' }}>提示:通过注入 socket 事件驱动,与线上数据通路一致。</Text>
<Text style={{ display: 'block' }}>「开始模拟」按当前上/下车点合成路线;「回放真实日志」按录制顺序复刻线上推送。</Text>
</View>
+
+ <View style={{ height: '1px', background: 'rgba(255,255,255,0.15)', margin: '10px 0 8px' }} />
+
+ <View style={{ fontSize: '12px', lineHeight: '1.8', marginBottom: '6px', opacity: 0.92 }}>
+ <Text style={{ display: 'block', fontWeight: 'bold' }}>页面态快捷跳转</Text>
+ </View>
+ <View style={{ display: 'flex', marginBottom: '4px' }}>
+ <View
+ onClick={handleJumpToTripFinished}
+ style={buttonStyle('#0e9f6e')}
+ >
+ 行程结束未支付
+ </View>
+ <View
+ onClick={handleJumpToEvaluatePage}
+ style={buttonStyle('#7c5cff')}
+ >
+ 直达评价页
+ </View>
+ </View>
+
+ <View style={{ height: '1px', background: 'rgba(255,255,255,0.15)', margin: '10px 0 8px' }} />
+
+ <View style={{ fontSize: '12px', lineHeight: '1.8', marginBottom: '6px', opacity: 0.92 }}>
+ <Text style={{ display: 'block', fontWeight: 'bold' }}>取消 / 失败弹窗手动触发</Text>
+ </View>
+ <View style={{ display: 'flex', marginBottom: '4px' }}>
+ <View
+ onClick={handleTriggerNoVehicleCancelPopup}
+ style={buttonStyle('#e0574a')}
+ >
+ 附近无车弹窗
+ </View>
+ <View
+ onClick={handleTriggerModifyDestinationFailPopup}
+ style={buttonStyle('#a5673f')}
+ >
+ 修改目的地失败
+ </View>
+ </View>
</View>
);
}
diff --git a/src/robotaxi/pages/begin-route/components/evaluate-popup.tsx b/src/robotaxi/pages/begin-route/components/evaluate-popup.tsx
index 9af8561..0f095c3 100644
--- a/src/robotaxi/pages/begin-route/components/evaluate-popup.tsx
+++ b/src/robotaxi/pages/begin-route/components/evaluate-popup.tsx
@@ -1,8 +1,10 @@
import Taro from '@tarojs/taro';
+import { showToast } from '../../../components/toast';
import { View, Text, Image, Textarea } from '@tarojs/components';
import { useEffect, useRef, useState } from 'react';
import './css-folder/evaluate-popup.less'
-import { getOrderCommentType, submitComplaint, submitOrderComment } from '../../../api/begin-route';
+import { getOrderCommentType, submitOrderComment } from '../../../api/begin-route';
+import { submitComplaint } from '../../../api/feedback';
import { _ROBOTAXI_HOST } from '../../../../constants/api';
import useImageUpload from '../../../hooks/use-image-upload';
import { getOrderId } from '../../../utils/order';
@@ -30,14 +32,23 @@
submitMode = 'comment',
hideFeedbackType = false
}: Props) {
+ const bad = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/bad.png';
+ const normal = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/normal.png';
+ const good = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/good.png';
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, animationData } = useBottomPopupAnimation(show);
+ const [selectedRating, setSelectedRating] = useState(() => Number(orderRating) || 1);
+ const [isRatingSwitching, setIsRatingSwitching] = useState(false);
const [selectedTypeIds, setSelectedTypeIds] = useState<string[]>([]); // 当前选择的问题类型列表
const [feedbackContent, setFeedbackContent] = useState(''); // 反馈文本内容
const [typeOptions, setTypeOptions] = useState<any[]>([]); // 问题类型选项
const isSubmittingRef = useRef(false); // 同步提交锁,防止快速重复点击导致的并发提交
+ const ratingSwitchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); // 评分切换过渡计时器
+ const ratingOptionsRequestIdRef = useRef(0); // 拉取问题类型请求序号,避免异步串台
+ const ratingTouchStartRef = useRef<{ x: number; y: number } | null>(null); // 弹窗区域滑动起点
+ const ratingTouchMoveRef = useRef<{ x: number; y: number } | null>(null); // 弹窗区域滑动过程点
const {
images: pictures,
uploading,
@@ -51,28 +62,28 @@
uploadUrl: _ROBOTAXI_HOST + '/operation/base/v1/mini-program/oss/upload'
}); // 图片上传状态与操作
- // 根据外部点击的评分,动态展示评价弹窗的 icon 和文案
- const currentOrderRating = Number(orderRating) || 1;
+ const currentOrderRating = Number(selectedRating) || 1;
+ // 根据当前选择的评分,动态展示评价弹窗的文案
const popupDisplayConfig = {
1: {
- icon: activeBad,
title: '“不满意,我要吐槽”',
- placeholder: '请详描述您遇到的问题(可添加相关截图辅助说明)'
+ summary: '不满意,我要吐槽',
+ placeholder: '请描述您的建议或问题'
},
2: {
- icon: activeNormal,
title: '“一般般,还需努力”',
+ summary: '一般般,还需努力',
placeholder: '告诉我们哪些方面还可以继续改进'
},
3: {
- icon: activeGood,
title: '“很满意,表扬一下”',
+ summary: '很满意!很满意!',
placeholder: '告诉我哪里做的不错,我们继续保持!'
}
}[currentOrderRating] || {
- icon: activeBad,
title: '“不满意,我要吐槽”',
- placeholder: '请详描述您遇到的问题(可添加相关截图辅助说明)'
+ summary: '不满意,我要吐槽',
+ placeholder: '请描述您的建议或问题'
};
const evaluateDimensionMap: Record<number, number> = {
3: 1,
@@ -92,6 +103,81 @@
}
return String(typeId);
};
+
+ const handleSelectRating = (rating: number) => {
+ if (rating === selectedRating) {
+ return;
+ }
+ if (ratingSwitchTimerRef.current) {
+ clearTimeout(ratingSwitchTimerRef.current);
+ ratingSwitchTimerRef.current = null;
+ }
+ setIsRatingSwitching(true);
+ ratingSwitchTimerRef.current = setTimeout(() => {
+ setSelectedRating(rating);
+ ratingSwitchTimerRef.current = null;
+ }, 120);
+ };
+
+ const getTouchPoint = (touch: any) => ({
+ x: Number(touch?.pageX ?? touch?.clientX ?? touch?.x ?? 0),
+ y: Number(touch?.pageY ?? touch?.clientY ?? touch?.y ?? 0)
+ });
+
+ /**
+ * 评分区域:记录滑动起点。
+ */
+ const handleRatingTouchStart = (e: any) => {
+ const touch = e?.touches?.[0];
+ if (!touch) {
+ return;
+ }
+ const point = getTouchPoint(touch);
+ ratingTouchStartRef.current = point;
+ ratingTouchMoveRef.current = point;
+ };
+
+ /**
+ * 弹窗区域:实时记录滑动坐标,兼容部分端 changedTouches 缺失。
+ */
+ const handleRatingTouchMove = (e: any) => {
+ const touch = e?.touches?.[0];
+ if (!touch) {
+ return;
+ }
+ ratingTouchMoveRef.current = getTouchPoint(touch);
+ };
+
+ /**
+ * 评分区域:左右滑动切换满意度。
+ */
+ const handleRatingTouchEnd = (e: any) => {
+ const start = ratingTouchStartRef.current;
+ ratingTouchStartRef.current = null;
+ if (!start || isRatingSwitching) {
+ return;
+ }
+ const touch = e?.changedTouches?.[0];
+ const point = touch ? getTouchPoint(touch) : ratingTouchMoveRef.current;
+ if (!point) {
+ return;
+ }
+ const endX = point.x;
+ const endY = point.y;
+ const deltaX = endX - start.x;
+ const deltaY = endY - start.y;
+ const minSwipeDistance = 24;
+
+ // 只响应明显的水平滑动,避免与纵向滚动或点击冲突。
+ if (Math.abs(deltaX) < minSwipeDistance || Math.abs(deltaX) <= Math.abs(deltaY)) {
+ return;
+ }
+
+ const direction = deltaX < 0 ? 1 : -1; // 左滑=下一档,右滑=上一档
+ const nextRating = Math.min(3, Math.max(1, selectedRating + direction));
+ handleSelectRating(nextRating);
+ };
+
const toggleFeedbackType = (typeId: unknown) => {
const normalizedTypeId = normalizeFeedbackTypeId(typeId);
if (!normalizedTypeId) {
@@ -110,7 +196,7 @@
return;
}
if (shouldRequireFeedbackType && selectedTypeIds.length === 0) {
- Taro.showToast({
+ showToast({
title: '请选择问题类型',
icon: 'none'
});
@@ -118,21 +204,21 @@
}
const targetOrderId = getOrderId(orderId);
if (!targetOrderId) {
- Taro.showToast({
+ showToast({
title: '订单信息缺失,无法提交评价',
icon: 'none'
});
return;
}
if (!shouldHideFeedbackTextarea && !feedbackContent.trim()) {
- Taro.showToast({
+ showToast({
title: '请输入问题描述',
icon: 'none'
});
return;
}
if (uploading) {
- Taro.showToast({
+ showToast({
title: '图片上传中,请稍后提交',
icon: 'none'
});
@@ -146,7 +232,7 @@
.map(p => ({ id: p.server!.id, name: p.server!.name, url: p.server!.url }))
);
const payload: any = {
- orderRating: orderRating ?? 1,
+ orderRating: selectedRating ?? 1,
feedbackContent,
feedbackPictures: picturePayload,
orderId: targetOrderId
@@ -157,20 +243,20 @@
submitter(payload).then((res: any) => {
const isFailure = res?.success === false || (typeof res?.code === 'number' && res.code !== 0);
if (isFailure) {
- Taro.showToast({
+ showToast({
title: '评价提交失败,请稍后重试',
icon: 'none'
});
return;
}
const successTitle = submitMode === 'complaint' ? '投诉已提交' : '评价已提交';
- Taro.showToast({
+ showToast({
title: successTitle,
icon: 'none'
});
onConfirm?.();
}).catch(() => {
- Taro.showToast({
+ showToast({
title: '评价提交失败,请稍后重试',
icon: 'none'
});
@@ -179,15 +265,27 @@
});
}
- // 拉取问题类型并重置本次填写内容
+ // 弹窗打开时同步当前评分
useEffect(() => {
if (!show) {
return;
}
+ setSelectedRating(Number(orderRating) || 1);
+ }, [show, orderRating]);
+
+ // 评分切换时重置本次填写内容并拉取对应问题类型
+ useEffect(() => {
+ if (!show) {
+ return;
+ }
+ const requestId = ++ratingOptionsRequestIdRef.current;
setSelectedTypeIds([]);
setFeedbackContent('');
resetImages();
getOrderCommentType(evaluateDimension).then((res: any) => {
+ if (requestId !== ratingOptionsRequestIdRef.current) {
+ return;
+ }
const list = res;
if (Array.isArray(list)) {
setTypeOptions(list);
@@ -195,27 +293,64 @@
setTypeOptions([]);
}
}).catch(() => {
+ if (requestId !== ratingOptionsRequestIdRef.current) {
+ return;
+ }
setTypeOptions([]);
+ }).finally(() => {
+ if (requestId !== ratingOptionsRequestIdRef.current) {
+ return;
+ }
+ setTimeout(() => {
+ setIsRatingSwitching(false);
+ }, 120);
});
}, [evaluateDimension, resetImages, show]);
+
+ useEffect(() => () => {
+ if (ratingSwitchTimerRef.current) {
+ clearTimeout(ratingSwitchTimerRef.current);
+ ratingSwitchTimerRef.current = null;
+ }
+ }, []);
if (!visible) return null;
return (
<View className='evaluate-popup' catchMove onClick={(e) => e.stopPropagation()}>
<View className='evaluate-mask' onClick={onClose}></View>
- <View className='evaluate-content' animation={animationData}>
+ <View
+ className={`evaluate-content ${isRatingSwitching ? 'is-rating-switching' : ''}`}
+ animation={animationData}
+ onTouchStart={handleRatingTouchStart}
+ onTouchMove={handleRatingTouchMove}
+ onTouchEnd={handleRatingTouchEnd}
+ >
<View className='evaluate-content-close'>
- <Image onClick={onClose} src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/close-icon.png" />
+ <Image onClick={onClose} src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/close-fill-20260726.svg" />
</View>
- <View className='evaluate-content-title'>
- <Image src={popupDisplayConfig.icon} />
- <View>{popupDisplayConfig.title}</View>
+ <View className='evaluate-rating-switcher'>
+ <View className='evaluate-rating-switcher-title'>您对本次行程满意吗?</View>
+ <View className='evaluate-rating-switcher-content'>
+ <View className='evaluate-rating-switcher-content-item' onClick={() => handleSelectRating(1)}>
+ <Image src={selectedRating === 1 ? activeBad : bad} className='evaluate-rating-switcher-content-item-img' />
+ <Text className={`evaluate-rating-switcher-content-item-text ${selectedRating === 1 ? 'active-text' : ''}`}>不满意</Text>
+ </View>
+ <View className='evaluate-rating-switcher-content-item' onClick={() => handleSelectRating(2)}>
+ <Image src={selectedRating === 2 ? activeNormal : normal} className='evaluate-rating-switcher-content-item-img' />
+ <Text className={`evaluate-rating-switcher-content-item-text ${selectedRating === 2 ? 'active-text' : ''}`}>一般般</Text>
+ </View>
+ <View className='evaluate-rating-switcher-content-item' onClick={() => handleSelectRating(3)}>
+ <Image src={selectedRating === 3 ? activeGood : good} className='evaluate-rating-switcher-content-item-img' />
+ <Text className={`evaluate-rating-switcher-content-item-text ${selectedRating === 3 ? 'active-text' : ''}`}>很满意</Text>
+ </View>
+ </View>
+ <View className='evaluate-rating-switcher-summary'>{popupDisplayConfig.summary}</View>
</View>
<View className='content-box'>
{!hideFeedbackType && (
<View className='evaluate-problem-type'>
- <View className='evaluate-problem-type-title'>反馈的问题类型:</View>
+
<View className='evaluate-problem-type-content'>
{typeOptions.map((item) => (
<View key={item?.id} className={`evaluate-problem-type-content-item ${selectedTypeIds.includes(normalizeFeedbackTypeId(item?.id)) && 'active-tag'}`} onClick={() => toggleFeedbackType(item?.id)}>
@@ -269,8 +404,8 @@
{
pictures.length < 3 && (
<View className='evaluate-problem-pic-box-upload' onClick={chooseAndUpload}>
- <Image className='evaluate-problem-pic-box-upload-img' src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/upload-icon.png" />
- <View className='evaluate-problem-pic-box-upload-count'>{pictures.length}/3</View>
+ <Image className='evaluate-problem-pic-box-upload-img' src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/add-icon-20260726.svg" />
+ <View className='evaluate-problem-pic-box-upload-text'>上传图片/视频</View>
</View>
)
}
diff --git a/src/robotaxi/pages/begin-route/components/modify-destination-fail-popup.tsx b/src/robotaxi/pages/begin-route/components/modify-destination-fail-popup.tsx
index 155b433..2846407 100644
--- a/src/robotaxi/pages/begin-route/components/modify-destination-fail-popup.tsx
+++ b/src/robotaxi/pages/begin-route/components/modify-destination-fail-popup.tsx
@@ -69,8 +69,10 @@
<View className='over-popup' catchMove>
<View className='over-mask'></View>
<View className='over-content' animation={show ? undefined : animationData}>
- <View className='over-title'>修改目的地失败</View>
- {content ? <View className='over-txt'>{content}</View> : null}
+ <View className='over-text-group'>
+ <View className='over-title'>修改目的地失败</View>
+ {content ? <View className='over-txt'>{content}</View> : null}
+ </View>
<View onClick={handleConfirm} className='reselect-btn'>我知道了</View>
</View>
</View>
diff --git a/src/robotaxi/pages/begin-route/components/modify-destination-review.tsx b/src/robotaxi/pages/begin-route/components/modify-destination-review.tsx
index c063da9..b0e218e 100644
--- a/src/robotaxi/pages/begin-route/components/modify-destination-review.tsx
+++ b/src/robotaxi/pages/begin-route/components/modify-destination-review.tsx
@@ -1,5 +1,6 @@
import { View, Text, Image } from "@tarojs/components";
import Taro from "@tarojs/taro";
+import { showToast } from '../../../components/toast';
import { useEffect, useState, useRef } from "react";
import { getUpdateAddressNum } from "../../../api/begin-route";
import "./css-folder/modify-destination-review.less";
@@ -112,7 +113,7 @@
* 置灰态点击确认按钮时给出明确提示。
*/
const handleDisabledConfirmToast = () => {
- Taro.showToast({
+ showToast({
title: MODIFY_DESTINATION_DISABLED_TOAST,
icon: "none"
});
@@ -361,21 +362,21 @@
</View>
) : null}
<View className="modify-destination-review-select-site">
- <View className="modify-destination-review-sites-card">
- <View className="modify-destination-review-site-item">
- <View className="modify-destination-review-site-dot modify-destination-review-site-dot-old" />
- <View className="modify-destination-review-site-text start-txt">
- {startSite?.name || "-"}
- </View>
+ <View className="modify-destination-review-select-site-icon">
+ <View className="modify-destination-review-select-site-icon-start"></View>
+ <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/dotted-line.png" />
+ <View className="modify-destination-review-select-site-icon-end"></View>
+ </View>
+ <View className="modify-destination-review-select-site-content">
+ <View className="modify-destination-review-select-site-start">
+ {startSite?.name || "-"}
</View>
+ <View className="modify-destination-review-select-site-divider"></View>
<View
- className="modify-destination-review-site-item modify-destination-review-site-item-active"
+ className="modify-destination-review-select-site-end"
onClick={() => onEditDestination?.()}
>
- <View className="modify-destination-review-site-dot modify-destination-review-site-dot-new" />
- <View className="modify-destination-review-site-text">
- {nextEndSite?.name || "-"}
- </View>
+ {nextEndSite?.name || "-"}
</View>
</View>
</View>
@@ -383,7 +384,7 @@
<View className="modify-destination-review-fare">
{isOperateFree ? (
<View className="modify-destination-review-operate-free">
- 员工内部测试
+ {selectedRoute?.freeText || "员工内部测试"}
</View>
) : (
<View>
@@ -391,15 +392,17 @@
className="modify-destination-review-fare-main"
onClick={toPriceDescription}
>
- <Image
- className="modify-destination-review-tip-icon"
- src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/tip-default-icon.png"
- />
- 预估
+ <View className="modify-destination-review-fare-forecast">
+ <Image
+ className="modify-destination-review-tip-icon"
+ src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/tip-default-icon.png"
+ />
+ <Text>预估</Text>
+ </View>
<Text className="modify-destination-review-fare-num">
{fareEstimate?.finalFareStr || "-"}
</Text>
- 元
+ <View className="modify-destination-review-fare-unit">元</View>
</View>
<View
className={`modify-destination-review-fare-original${
@@ -412,7 +415,7 @@
</View>
</View>
)}
- {isOperateFree
+ {/* {isOperateFree
? null
: hasFareDiscount && (
<View className="modify-destination-review-fare-discount">
@@ -421,7 +424,7 @@
{fareEstimate.discountAmount}元
</Text>
</View>
- )}
+ )} */}
</View>
<View className="modify-destination-review-confirm-wrapper">
<View
diff --git a/src/robotaxi/pages/begin-route/components/over-route.tsx b/src/robotaxi/pages/begin-route/components/over-route.tsx
index 3f3ea6d..112fa7f 100644
--- a/src/robotaxi/pages/begin-route/components/over-route.tsx
+++ b/src/robotaxi/pages/begin-route/components/over-route.tsx
@@ -1,4 +1,5 @@
import Taro from '@tarojs/taro';
+import { showToast } from '../../../components/toast';
import { View, Image } from '@tarojs/components';
import { useEffect, useState } from 'react';
import './css-folder/over-route.less'
@@ -64,7 +65,7 @@
setAnimationData(animation.export());
}).catch(() => {
if (!active) return;
- Taro.showToast({
+ showToast({
title: '获取结束时间失败,请稍后重试',
icon: 'none'
});
@@ -78,8 +79,10 @@
<View className='over-mask' onClick={onClose}></View>
<View className='over-content' animation={animationData}>
<Image onClick={onClose} className='close-icon' src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/close-icon.png" />
- <View className='over-title'>确认结束行程?</View>
- <View className='over-txt'>行程结束{finishMinutes}分钟后车辆将自动驶离</View>
+ <View className='over-text-group'>
+ <View className='over-title'>确认结束行程?</View>
+ <View className='over-txt'>行程结束{finishMinutes}分钟后车辆将自动驶离</View>
+ </View>
<View onClick={handleConfirm} className='reselect-btn'>确认结束</View>
</View>
</View>
diff --git a/src/robotaxi/pages/begin-route/components/passenger-notice.tsx b/src/robotaxi/pages/begin-route/components/passenger-notice.tsx
index 2c4d6cc..60d9fbd 100644
--- a/src/robotaxi/pages/begin-route/components/passenger-notice.tsx
+++ b/src/robotaxi/pages/begin-route/components/passenger-notice.tsx
@@ -82,7 +82,7 @@
<View className='passenger-mask' catchMove></View>
<View className='passenger-content' animation={animationData}>
<View className='passenger-content-bg'>
- <Image className='passenger-content-bg-img' mode='widthFix' src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/passenger-content-bg.png'></Image>
+ <Image className='passenger-content-bg-img' mode='widthFix' src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/passenger-content-20260709.png'></Image>
</View>
<View className='passenger-content-title'>乘车需知</View>
<View className='passenger-content-desc-wrap'>
diff --git a/src/robotaxi/pages/begin-route/components/release-be-being.tsx b/src/robotaxi/pages/begin-route/components/release-be-being.tsx
index 43d6ad6..e5c9aa6 100644
--- a/src/robotaxi/pages/begin-route/components/release-be-being.tsx
+++ b/src/robotaxi/pages/begin-route/components/release-be-being.tsx
@@ -10,7 +10,6 @@
}
export default function ReleaseBeBeing({ handleOpenCancelPopup, onFinish, finishBlocked = false }: Props) {
- const [progress, setProgress] = useState(0); // 发布进度条
const [completedCount, setCompletedCount] = useState(0); // 已完成步骤数
const [finishReady, setFinishReady] = useState(false); // 发布动画是否已走完
const finishTriggeredRef = useRef(false); // 防止 onFinish 重复触发
@@ -18,13 +17,11 @@
const endSite = Taro.getStorageSync('endSite') || {};
const startSiteName = startSite?.name || startSite?.address || '-';
const endSiteName = endSite?.name || endSite?.address || '-';
+ const siteNames = [startSiteName, endSiteName];
+
// 模拟发布流程进度和状态推进
useEffect(() => {
- // 使用 setTimeout 触发 CSS transition 动画
- const timer = setTimeout(() => {
- setProgress(90);
- }, 100);
// 模拟步骤完成:1秒后完成第一步,2秒后完成第二步
const step1 = setTimeout(() => setCompletedCount(1), 1000);
@@ -36,7 +33,6 @@
}, 3000);
return () => {
- clearTimeout(timer);
clearTimeout(step1);
clearTimeout(step2);
clearTimeout(step3);
@@ -52,32 +48,56 @@
onFinish?.();
}, [finishBlocked, finishReady, onFinish]);
+ const renderSiteItemMarker = (index, count) => {
+ /* 根据 completedCount 动态显示完成图标 */
+ if (index < count) {
+ return (
+ <Image
+ className='route-card-item-check'
+ src={
+ index === 0
+ ? 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/route-state-icon-golden-20260702.png'
+ : 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/route-state-icon-black-20260702.png'
+ }
+ />
+ );
+ }
+ return (
+ <View
+ className={`route-card-item-dot${
+ index === 0 ? " route-card-item-dot-first" : ""
+ }`}
+ ></View>
+ );
+ }
+
return (
<View className='release-be-being'>
+ <Image className="release-be-being-top-bg" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/top-card-bg-20260722.png' />
+ <View className="release-be-being-content">
<View className="top-box">
<View className="top-box-title">
<View className="title">正在发布行程...</View>
- <Image onClick={handleOpenCancelPopup} src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/cancel-btn.png' className="cancel-btn" />
+ <View onClick={handleOpenCancelPopup} className="cancel-btn">取消行程</View>
</View>
<View className="progress-box">
- <View className="progress" style={{ width: `${progress}%` }}></View>
+ <Image className='progress-icon' mode='widthFix' src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/publish-progress-20260715.webp'></Image>
</View>
- </View>
- <View className="route-state-box">
+ </View>
+ <View className="route-card">
{
- [0, 1, 2].map((item) => (
- <View key={item} className="route-state-box-item">
- {/* 根据 completedCount 动态显示完成图标 */}
- {item < completedCount ? (
- <Image className="route-state-box-item-icon" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/route-state-icon.png" />
- ) : (
- <Image className="route-state-box-item-icon" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/route-state-dot.png" />
- )}
- <View className={`route-state-box-item-text ${item == 2 && 'route-state-box-item-text-strong'}`}>{['上车点:', '目的地:', ''][item]}{[startSiteName, endSiteName, '正在发布行程'][item]}</View>
- </View>
- ))
- }
- </View>
+ siteNames.map((name, item) => (
+ <View key={item} className={`route-card-item${item === 0 ? ' route-card-item-first' : ''}`}>
+ {item === 0 && <View className="route-card-item-line"></View>}
+ {item === 0 && <View className="route-card-item-divider"></View>}
+ <View className="route-card-item-marker">
+ {renderSiteItemMarker(item, completedCount)}
+ </View>
+ <View className="route-card-item-text">{name}</View>
+ </View>
+ ))}
+ </View>
+ </View>
</View>
)
}
diff --git a/src/robotaxi/pages/begin-route/components/release-route.tsx b/src/robotaxi/pages/begin-route/components/release-route.tsx
index 5a258c7..3c964fe 100644
--- a/src/robotaxi/pages/begin-route/components/release-route.tsx
+++ b/src/robotaxi/pages/begin-route/components/release-route.tsx
@@ -41,7 +41,7 @@
if (!Number.isFinite(discountAmount) || discountAmount <= 0) {
return '0元';
}
- return `${parseFloat(discountAmount.toFixed(2))}元`;
+ return `-${parseFloat(discountAmount.toFixed(2))}元`;
};
useEffect(() => {
@@ -178,49 +178,50 @@
return (
<View className="release-route-container">
<View
- className='release-route-wrapper'
- style={{ height: typeof boxHeight === 'number' ? `${boxHeight}px` : 'auto' }}
- catchMove={isRouteSelectExpanded}
- onTouchStart={(e) => {
- // 如果没有展开,禁止滑动
- if (!isRouteSelectExpanded) return;
- e.stopPropagation();
- handleTouchStart(e);
- }}
- onTouchMove={(e) => {
- if (!isRouteSelectExpanded) return;
- e.stopPropagation();
- handleTouchMove(e);
- }}
- onTouchEnd={(e) => {
- if (!isRouteSelectExpanded) return;
- e.stopPropagation();
- handleTouchEnd();
- }}
+ className='release-route-wrapper'
+ style={{ height: typeof boxHeight === 'number' ? `${boxHeight}px` : 'auto' }}
+ catchMove={isRouteSelectExpanded}
+ onTouchStart={(e) => {
+ // 如果没有展开,禁止滑动
+ if (!isRouteSelectExpanded) return;
+ e.stopPropagation();
+ handleTouchStart(e);
+ }}
+ onTouchMove={(e) => {
+ if (!isRouteSelectExpanded) return;
+ e.stopPropagation();
+ handleTouchMove(e);
+ }}
+ onTouchEnd={(e) => {
+ if (!isRouteSelectExpanded) return;
+ e.stopPropagation();
+ handleTouchEnd();
+ }}
>
<View className='release-route-content'>
{isRouteSelectExpanded && (
<RouteSelectionPanel
- routeOptions={routeOptions}
- selectedRouteIndex={selectedRouteIndex}
- handleSelectRoute={handleSelectRoute}
+ routeOptions={routeOptions}
+ selectedRouteIndex={selectedRouteIndex}
+ handleSelectRoute={handleSelectRoute}
/>
)}
<View className="select-site" style={{ paddingBottom: typeof bottomPaddingHeight === 'number' ? `${bottomPaddingHeight}px` : undefined }}>
- <Image className="site-top-bg" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/site-top-bg-20260507.png' />
- <View className="site-boarding-point">
+ <Image className="site-top-bg" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/top-card-bg-20260722.png' />
+ <View className="select-site-content">
+ <View className="site-boarding-point">
{/* 起点 */}
<View>
- <View onClick={() => toSeleteSitePage('start')} className="now-site">
- <View className="now-site-dot"></View>
- <View className="now-site-address">
- <View className="now-site-address-content">
- 从<Text className="now-site-address-text">{startSite.name}</Text>上车
- </View>
- <Image className="now-site-icon" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/right-icon.png" />
- </View>
- </View>
- {walkingTime > 0 && <View className="now-site-walk">步行{formatWalkingTime(walkingTime)}</View>}
+ <View onClick={() => toSeleteSitePage('start')} className="now-site">
+ <View className="now-site-dot"></View>
+ <View className="now-site-address">
+ <View className="now-site-address-content">
+ 从<Text className="now-site-address-text">{startSite.name}</Text>上车
+ </View>
+ <Image className="now-site-icon" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/now-site-arrow-20260708.png" />
+ </View>
+ </View>
+ {walkingTime > 0 && <View className="now-site-walk">步行{formatWalkingTime(walkingTime)}</View>}
</View>
{/* 终点 */}
<View onClick={() => toSeleteSitePage('end')} className="to-site">
@@ -229,54 +230,65 @@
<View className="to-site-text">{endSite.name}</View>
</View>
</View>
- </View>
- <View className="route-content">
- <View className="route-content-top">
- <View className="route-content-txt">预估等待{fareEstimate.waitingTime || '-'}分钟</View>
- <View className="route-content-txt" style={{ margin: '0 4px' }}>|</View>
- <View className="route-content-txt">限乘{fareEstimate.maxPeopleNumber || '-'}人</View>
- </View>
- <View className="route-content-bottom">
- <View onClick={handleOpenPassengerNotice} className="route-content-bottom-left">
- <Image className="tip-icon" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/tip-default-icon.png" />
- <View>乘车须知</View>
- </View>
+ <View className="route-content">
+ <View className="route-content-top">
+ <View className="route-content-txt">预估等待{fareEstimate.waitingTime || '-'}分钟</View>
+ <View className="route-content-txt" style={{ margin: '0 4px' }}>|</View>
+ <View className="route-content-txt">限乘{fareEstimate.maxPeopleNumber || '-'}人</View>
+ </View>
+ <View className="route-content-bottom">
+ <View onClick={handleOpenPassengerNotice} className="route-content-bottom-left">
+ <Image className="tip-icon" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/tip-default-icon-20260726.png" />
+ <View>乘车须知</View>
+ </View>
+ </View>
</View>
</View>
</View>
+ </View>
</View>
<View className="confirm-btn-box">
{
hasDiscountAmount &&
- <View className="discount-amount" onClick={onOpenCouponSelect}>
+ <View
+ className="discount-amount"
+ style={{ visibility: isRouteSelectExpanded ? 'hidden' : 'visible' }}
+ onClick={onOpenCouponSelect}
+ >
<View className="flex-center">
<View className="discount-amount-text">最佳优惠</View>
<View className="discount-amount-desc" >已为您选择最佳优惠</View>
</View>
<View className="flex-center">
<View className="discount-amount-num">{formatDiscountAmountText()}</View>
- <Image className="change-coupon-icon" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/change-coupon.png' />
+ <Image className="change-coupon-icon" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/golden-arrow-20260702.png' />
</View>
</View>
}
<View className="confirm-btn-box-content">
<View className="money-box">
{isOperateFree ? (
- <View className="route-operate-free">员工内部测试</View>
+ <View className="route-operate-free">{selectedRoute?.freeText || "员工内部测试"}</View>
) : (
<View className="route-content-top-money" onClick={() => toPriceDescription()} >
- <Image className="tip-icon" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/tip-default-icon.png" />
- <View className="price-box">
- <View className="price-box-text">预估</View>
- <Text className="money-num">{fareEstimate.finalFareStr || '-'}</Text>
- <View className="price-box-text">元</View>
+ <View className="forecast-text">
+ <Image className="tip-icon" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/tip-default-icon.png" />
+ <Text>预估</Text>
+ </View>
+ <Text className="money-num">{fareEstimate.finalFareStr || '-'}</Text>
+ <View className="unit-text">
+ 元
</View>
</View>
)}
</View>
<View className="confirm-btn-wrapper">
- <View onClick={() => !isCallDisabled && callImmediately()} className="confirm-btn">立即呼叫</View>
- {isCallDisabled && <View className="confirm-btn-mask" />}
+ <Image className="confirm-btn-bg" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/confirm-btn-bg-20260726.png" />
+ <View onClick={() => !isCallDisabled && callImmediately()} className="confirm-btn">
+ <Image className="confirm-btn-icon" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/confirm-btn-icon-20260629.png" />
+ 立即呼叫
+ </View>
+ {isCallDisabled && <View className="confirm-btn-mask" />}
</View>
</View>
</View>
diff --git a/src/robotaxi/pages/begin-route/components/route-preference-popup/index.less b/src/robotaxi/pages/begin-route/components/route-preference-popup/index.less
index 772784d..03523ba 100644
--- a/src/robotaxi/pages/begin-route/components/route-preference-popup/index.less
+++ b/src/robotaxi/pages/begin-route/components/route-preference-popup/index.less
@@ -1,3 +1,4 @@
+@import '../../../../style/ui-variables.less';
.route-preference-popup {
position: fixed;
top: 0;
@@ -90,7 +91,7 @@
&.active {
.popup-name {
- color: #6D7FD9;
+ color: @gold-text;
}
}
}
diff --git a/src/robotaxi/pages/begin-route/components/route-preference-popup/index.tsx b/src/robotaxi/pages/begin-route/components/route-preference-popup/index.tsx
index 70157d4..8a9e91a 100644
--- a/src/robotaxi/pages/begin-route/components/route-preference-popup/index.tsx
+++ b/src/robotaxi/pages/begin-route/components/route-preference-popup/index.tsx
@@ -88,7 +88,7 @@
>
<View className='popup-name'>{item.label}</View>
{isSelected && (
- <Image className='select-icon' src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/select-type-line.png" />
+ <Image className='select-icon' src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/select-type-line-20260707.png" />
)}
</View>
);
diff --git a/src/robotaxi/pages/begin-route/components/route-selection-panel.tsx b/src/robotaxi/pages/begin-route/components/route-selection-panel.tsx
index 0c021ba..5dd2066 100644
--- a/src/robotaxi/pages/begin-route/components/route-selection-panel.tsx
+++ b/src/robotaxi/pages/begin-route/components/route-selection-panel.tsx
@@ -33,7 +33,7 @@
<View className='route-select-title'>路线选择:</View>
<View className='route-select-preference' onClick={() => setShowPreferencePopup(true)}>
<View className='route-select-preference-text'>{preference?.label || ''}</View>
- <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/select-line.png" />
+ <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/select-line-20260707.png" />
</View>
</View>
<View className='route-select-content'>
diff --git a/src/robotaxi/pages/begin-route/hooks/route-summary-anchor.ts b/src/robotaxi/pages/begin-route/hooks/route-summary-anchor.ts
deleted file mode 100644
index 5846d20..0000000
--- a/src/robotaxi/pages/begin-route/hooks/route-summary-anchor.ts
+++ /dev/null
@@ -1,120 +0,0 @@
-/** 路线经纬度点。 */
-export type RouteSummaryGeoPoint = {
- latitude: number;
- longitude: number;
-};
-
-/** 路线摘要气泡预估宽度。 */
-export const ROUTE_SUMMARY_CALLOUT_WIDTH = 190;
-/** 路线摘要气泡预估高度。 */
-export const ROUTE_SUMMARY_CALLOUT_HEIGHT = 45;
-/** 路线摘要气泡左右安全边距。 */
-export const ROUTE_SUMMARY_SAFE_SIDE_MARGIN = 12;
-/** 路线摘要气泡顶部安全边距。 */
-export const ROUTE_SUMMARY_SAFE_TOP_MARGIN = 12;
-/** 路线摘要气泡底部安全边距。 */
-export const ROUTE_SUMMARY_SAFE_BOTTOM_MARGIN = 12;
-
-/** 屏幕坐标点。 */
-export type RouteSummaryScreenPoint = {
- x: number;
- y: number;
-};
-
-/** 路线摘要气泡安全边界。 */
-export type RouteSummaryBubbleBounds = {
- safeLeft: number;
- safeRight: number;
- safeTop: number;
- safeBottom: number;
- bubbleWidth: number;
- bubbleHeight: number;
-};
-
-/** 路线摘要锚点解析入参。 */
-export type ResolveRouteSummaryAnchorInput = {
- routePoints: RouteSummaryGeoPoint[];
- fallbackPoint: RouteSummaryGeoPoint | null;
- bounds: RouteSummaryBubbleBounds;
- projectToScreen: (point: RouteSummaryGeoPoint) => RouteSummaryScreenPoint | null;
-};
-
-/**
- * 判断候选路线点是否有效。
- */
-const isValidRoutePoint = (point: RouteSummaryGeoPoint | null | undefined) => (
- Number.isFinite(Number(point?.latitude)) && Number.isFinite(Number(point?.longitude))
-);
-
-/**
- * 判断气泡在当前候选锚点下是否完全位于安全区内。
- */
-const isBubbleInsideSafeBounds = (
- point: RouteSummaryScreenPoint,
- bounds: RouteSummaryBubbleBounds
-) => (
- point.x >= bounds.safeLeft
- && point.x + bounds.bubbleWidth <= bounds.safeRight
- && point.y >= bounds.safeTop
- && point.y + bounds.bubbleHeight <= bounds.safeBottom
-);
-
-/**
- * 以路线中点为中心向两侧扩散,生成候选索引顺序。
- */
-const buildCandidateIndices = (pointCount: number) => {
- if (pointCount <= 0) {
- return [];
- }
- const midpointIndex = Math.max(0, Math.floor(pointCount / 2));
- const sampleStep = pointCount > 120 ? 3 : pointCount > 60 ? 2 : 1;
- const visited = new Set<number>();
- const indices: number[] = [];
-
- for (let offset = 0; offset < pointCount; offset += sampleStep) {
- const rightIndex = midpointIndex + offset;
- if (rightIndex >= 0 && rightIndex < pointCount && !visited.has(rightIndex)) {
- visited.add(rightIndex);
- indices.push(rightIndex);
- }
- if (offset === 0) {
- continue;
- }
- const leftIndex = midpointIndex - offset;
- if (leftIndex >= 0 && leftIndex < pointCount && !visited.has(leftIndex)) {
- visited.add(leftIndex);
- indices.push(leftIndex);
- }
- }
-
- if (!visited.has(midpointIndex)) {
- indices.unshift(midpointIndex);
- }
- return indices;
-};
-
-/**
- * 解析路线摘要气泡应挂载的最佳路线点。
- */
-export const resolveRouteSummaryAnchorPoint = (
- input: ResolveRouteSummaryAnchorInput
-) => {
- const { routePoints, fallbackPoint, bounds, projectToScreen } = input;
- const candidateIndices = buildCandidateIndices(routePoints.length);
-
- for (const candidateIndex of candidateIndices) {
- const candidatePoint = routePoints[candidateIndex];
- if (!isValidRoutePoint(candidatePoint)) {
- continue;
- }
- const screenPoint = projectToScreen(candidatePoint);
- if (!screenPoint) {
- continue;
- }
- if (isBubbleInsideSafeBounds(screenPoint, bounds)) {
- return candidatePoint;
- }
- }
-
- return isValidRoutePoint(fallbackPoint) ? fallbackPoint : null;
-};
diff --git a/src/robotaxi/pages/begin-route/hooks/use-android-overlays.ts b/src/robotaxi/pages/begin-route/hooks/use-android-overlays.ts
index d42f471..04bb97a 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-android-overlays.ts
+++ b/src/robotaxi/pages/begin-route/hooks/use-android-overlays.ts
@@ -9,7 +9,6 @@
mapScale: number;
/** 修改目的地预览态下隐藏车辆气泡。 */
isModifyDestinationReviewVisible: boolean;
- routeSummaryLocation: { latitude: number; longitude: number } | null;
startSite: any;
endSite: any;
vehicleLocation: { latitude: number; longitude: number } | null;
@@ -38,9 +37,6 @@
// 到达气泡样式与显示状态
const [arriveCalloutStyle, setArriveCalloutStyle] = useState<any>({});
const [arriveCalloutVisible, setArriveCalloutVisible] = useState(false);
- // 路线摘要气泡样式与显示状态
- const [routeSummaryCalloutStyle, setRouteSummaryCalloutStyle] = useState<any>({});
- const [routeSummaryCalloutVisible, setRouteSummaryCalloutVisible] = useState(false);
const mapRectRef = useRef<any>(null);
// 读取地图投影上下文,供路线点批量转换屏幕坐标使用。
@@ -181,25 +177,6 @@
setArriveCalloutVisible(true);
};
- // 更新路线摘要气泡位置
- const updateRouteSummaryCalloutPosition = async () => {
- if (!deps.isAndroid || !deps.routeSummaryLocation) {
- setRouteSummaryCalloutVisible(false);
- return;
- }
- const rect = await ensureMapRect();
- const pos = await computeScreenPosition(deps.routeSummaryLocation, rect, 0);
- if (!pos) {
- setRouteSummaryCalloutVisible(false);
- return;
- }
- setRouteSummaryCalloutStyle(buildTransformOverlayStyle({
- x: pos.x,
- y: pos.y
- }, ''));
- setRouteSummaryCalloutVisible(true);
- };
-
// 地图区域变化时同步覆盖物显隐与重算
// 注意:这里把「begin 隐藏」与「end 重算」解耦,避免历史上的两类问题:
// 1) 若 begin 对所有区域变化都隐藏,则 fitRouteIntoView/includePoints 等程序化视口调整会
@@ -219,7 +196,6 @@
setWaitingRadarVisible(false);
setCarCalloutVisible(false);
setArriveCalloutVisible(false);
- setRouteSummaryCalloutVisible(false);
}
return;
}
@@ -230,7 +206,6 @@
updateWaitingRadarPosition();
updateCarCalloutPosition();
updateArriveCalloutPosition();
- updateRouteSummaryCalloutPosition();
}
};
@@ -256,10 +231,6 @@
}, [deps.isAndroid, deps.tripStatus, deps.endSite?.latitude, deps.endSite?.longitude]);
useEffect(() => {
- updateRouteSummaryCalloutPosition();
- }, [deps.isAndroid, deps.routeSummaryLocation?.latitude, deps.routeSummaryLocation?.longitude, deps.mapScale, deps.tripStatus]);
-
- useEffect(() => {
if (!deps.isAndroid || deps.tripStatus !== TripStatus.ArrivedDestination) return;
if (deps.mapScale < 20) return;
const timer = setTimeout(() => {
@@ -275,12 +246,9 @@
carCalloutVisible,
arriveCalloutStyle,
arriveCalloutVisible,
- routeSummaryCalloutStyle,
- routeSummaryCalloutVisible,
updateWaitingRadarPosition,
updateCarCalloutPosition,
updateArriveCalloutPosition,
- updateRouteSummaryCalloutPosition,
handleRegionChange
};
}
diff --git a/src/robotaxi/pages/begin-route/hooks/use-bluetooth-access.ts b/src/robotaxi/pages/begin-route/hooks/use-bluetooth-access.ts
index 1b5cd7c..da79801 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-bluetooth-access.ts
+++ b/src/robotaxi/pages/begin-route/hooks/use-bluetooth-access.ts
@@ -1,4 +1,5 @@
import Taro from '@tarojs/taro';
+import { showToast } from '../../../components/toast';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { TripStatus } from '../../../constants/trip-status';
import { waitForOrderSuccessSubscribePopup } from '../../../../utils/subscribe';
@@ -39,8 +40,8 @@
const BLUETOOTH_AUTH_PROTECTION_WINDOW_MS = 5000; // 授权成功后的保护窗口,避免瞬时 permission_deny 覆盖 UI
const SUBSCRIBE_POPUP_WAIT_TIMEOUT_MS = 8000; // 进入接驾态时等待订阅弹窗结束的最长兜底时间,超时后仍放行蓝牙初始化
// 蓝牙图标资源:点亮(已授权)与熄灭(未开/未授权)两态
-const BLUETOOTH_ICON_ON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/bluetooth-icon.png';
-const BLUETOOTH_ICON_OFF = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/close-bluetooth.png';
+const BLUETOOTH_ICON_ON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/bluetooth-icon-20260706.png';
+export const BLUETOOTH_ICON_OFF = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/close-bluetooth-20260716.png';
// 车端钥匙下发失败时的描述文案与文字颜色
const UNLOCK_TOKEN_FAILED_TEXT = '蓝牙解锁失败, 请点击右下方按键解锁车门';
const UNLOCK_TOKEN_FAILED_COLOR = '#FF4D4F';
@@ -321,11 +322,11 @@
recentAuthorizedAtRef.current = 0;
stopOpenSettingPollingRef.current();
setStatus('system_off');
- Taro.showToast({ title: '请先开启系统蓝牙', icon: 'none' });
+ showToast({ title: '请先开启系统蓝牙', icon: 'none' });
return;
}
- Taro.showToast({ title: '蓝牙初始化失败,请稍后重试', icon: 'none' });
+ showToast({ title: '蓝牙初始化失败,请稍后重试', icon: 'none' });
setTimeout(() => {
refreshBluetoothStateRef.current();
}, BLUETOOTH_AUTH_SYNC_DELAY_MS);
@@ -607,7 +608,7 @@
error,
systemInfo: systemInfoSnapshot
});
- Taro.showToast({ title: '请手动开启系统蓝牙', icon: 'none' });
+ showToast({ title: '请手动开启系统蓝牙', icon: 'none' });
} finally {
logBluetoothAccess('系统蓝牙设置流程结束,准备刷新状态', {
systemInfo: systemInfoSnapshot
@@ -648,7 +649,7 @@
openSettingError,
...permissionSnapshot
});
- Taro.showToast({ title: getBluetoothPermissionDeniedMessage(permissionSnapshot), icon: 'none' });
+ showToast({ title: getBluetoothPermissionDeniedMessage(permissionSnapshot), icon: 'none' });
return;
}
const latestAuthState = await getMiniappBluetoothAuthState();
@@ -658,7 +659,7 @@
...latestSnapshot
});
if (latestAuthState !== 'authorized') {
- Taro.showToast({ title: getBluetoothPermissionDeniedMessage(latestSnapshot), icon: 'none' });
+ showToast({ title: getBluetoothPermissionDeniedMessage(latestSnapshot), icon: 'none' });
return;
}
await finishBluetoothAuthorization();
@@ -685,7 +686,7 @@
openSettingError,
...permissionSnapshot
});
- Taro.showToast({ title: getBluetoothPermissionDeniedMessage(permissionSnapshot), icon: 'none' });
+ showToast({ title: getBluetoothPermissionDeniedMessage(permissionSnapshot), icon: 'none' });
return;
}
const latestAuthState = await getMiniappBluetoothAuthState();
@@ -695,7 +696,7 @@
...latestSnapshot
});
if (latestAuthState !== 'authorized') {
- Taro.showToast({ title: getBluetoothPermissionDeniedMessage(latestSnapshot), icon: 'none' });
+ showToast({ title: getBluetoothPermissionDeniedMessage(latestSnapshot), icon: 'none' });
return;
}
await finishBluetoothAuthorization();
diff --git a/src/robotaxi/pages/begin-route/hooks/use-bluetooth-car-control.ts b/src/robotaxi/pages/begin-route/hooks/use-bluetooth-car-control.ts
index f013ae4..58d6775 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-bluetooth-car-control.ts
+++ b/src/robotaxi/pages/begin-route/hooks/use-bluetooth-car-control.ts
@@ -1,4 +1,5 @@
import Taro from '@tarojs/taro';
+import { showToast } from '../../../components/toast';
import { useCallback, useEffect, useRef, useState } from 'react';
import RtAuthBluetoothManager from '../../../utils/car-control/RtAuthBluetoothManager';
import { getBluetoothUnlockToken } from '../../../api/car-control';
@@ -71,7 +72,7 @@
const restartAuthWithFreshTokenRef = useRef<() => void>(() => {}); // 异常断连后重新拉取最新凭证并重连
const closeConnectionRef = useRef<() => void>(() => {}); // 断开连接动作引用
const showUnlockToast = (title: string) => {
- Taro.showToast({
+ showToast({
title,
icon: 'none'
});
diff --git a/src/robotaxi/pages/begin-route/hooks/use-credit-pay-return.ts b/src/robotaxi/pages/begin-route/hooks/use-credit-pay-return.ts
index b6871f6..8f3b66d 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-credit-pay-return.ts
+++ b/src/robotaxi/pages/begin-route/hooks/use-credit-pay-return.ts
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useRef } from 'react';
import Taro from '@tarojs/taro';
import { queryLedgerCreditOrderResult } from '../../../api/begin-route';
+import { showToast } from '../../../components/toast';
type Deps = {
/** 当前正在下单的订单 ID,用于查询授权结果。 */
@@ -38,16 +39,14 @@
const openCreditPayScoreUse = useCallback((packageValue: string, targetOrderId?: string) => {
const queryOrderId = targetOrderId || deps.orderId;
if (!packageValue) {
- Taro.showToast({ title: '支付参数缺失,请稍后重试', icon: 'none' });
- void deps.onLaunchFailed(queryOrderId);
- return true;
+ showToast({ title: '支付参数缺失,请稍后重试', icon: 'none' });
+ return false;
}
// @ts-ignore
if (!wx.openBusinessView) {
- Taro.showToast({ title: '请升级微信版本后重试', icon: 'none' });
- void deps.onLaunchFailed(queryOrderId);
- return true;
+ showToast({ title: '请升级微信版本后重试', icon: 'none' });
+ return false;
}
clearAppShowListener();
@@ -161,9 +160,7 @@
},
fail: () => {
clearAppShowListener();
- console.log('[信用分授权拉起失败][openBusinessView]');
- Taro.showToast({ title: '拉起支付分订单失败', icon: 'none' });
- void deps.onLaunchFailed(queryOrderId);
+ showToast({ title: '拉起支付分订单失败', icon: 'none' });
}
});
diff --git a/src/robotaxi/pages/begin-route/hooks/use-map-markers.ts b/src/robotaxi/pages/begin-route/hooks/use-map-markers.ts
index 107b751..3fb8726 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-map-markers.ts
+++ b/src/robotaxi/pages/begin-route/hooks/use-map-markers.ts
@@ -4,6 +4,38 @@
const CHANGE_SITE_UP_ICON_URL =
"https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/change-site-up.png"; // 行程中修改目的地时的上车点图标
+// 接驾态(前往上车点)上车点三角图标
+const PICKUP_APPROACHING_ICON_URL =
+ "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/up-triangle-20260722.png";
+// 送驾态(前往目的地)下车点三角图标
+const DROPOFF_INSERVICE_ICON_URL =
+ "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/down-triangle-20260722.png";
+// 站点名称 label(图标下方)统一配置
+const SITE_NAME_LABEL_BASE = {
+ color: "#141E27",
+ fontSize: 12,
+ anchorX: 0,
+ anchorY: 9, // 图标下方 9px
+ textAlign: "center" as const,
+ bgColor: "#00000000",
+ fontWeight: 'bold',
+ borderWidth: 0,
+ borderRadius: 0,
+ padding: 0
+};
+// 到达上车点/待支付/完成态:站点名称在图标下方 8px,11px #141E27 居中
+const SITE_NAME_LABEL_BELOW_LG = {
+ color: "#141E27",
+ fontSize: 12,
+ anchorX: 0,
+ anchorY: 8,
+ textAlign: "center" as const,
+ bgColor: "#00000000",
+ fontWeight: 'bold',
+ borderWidth: 0,
+ borderRadius: 0,
+ padding: 0
+};
/**
* 生成 marker 签名,供平移态判断结构是否实际变化。
@@ -167,37 +199,63 @@
useAndroidNativeTrip1Callout: boolean
) => {
if (!(pickupSite?.latitude && pickupSite?.longitude)) return null;
- const shouldUseChangePickupIcon = deps.isModifyDestinationReviewVisible && deps.tripStatus >= TripStatus.VehicleArrivedPickup; // 修改目的地确认态且进入行程阶段时使用换点上车图标
- const shouldHidePickupName = deps.tripStatus === TripStatus.InService && deps.isModifyDestinationReviewVisible; // 行程中修改目的地时只展示换点图标,不展示上车点名称
- // iOS 接驾态(VehicleApproaching):车辆由 translateMarker 平移,会把「另一个静止 marker」的 customCallout
- // 丢弃(必现,上车点气泡消失而车辆自身气泡不受影响)。这里让上车点改用原生 callout(与送驾态下车点同款),
- // 原生 callout 不受 translateMarker 影响,从而规避气泡丢失。
- const usePickupIosNativeCallout = !deps.isAndroid && deps.tripStatus === TripStatus.VehicleApproaching;
- const usePickupNativeCallout = useAndroidNativeTrip1Callout || usePickupIosNativeCallout;
+ // 修改目的地预览:起点改为 up-triangle 三角图标 + 名称显示在图标下方,不再用换点图标、不隐藏名称
+ const shouldUseChangePickupIcon = false;
+ const shouldHidePickupName = false;
+ // 接驾态(前往上车点):上车点改用三角图标,名称移到图标下方,浮窗展示车图标+限停时间;
+ // 待支付/完成态:上车点同样三角图标,浮窗展示 end-site 图片,名称移到图标左侧。
+ const isPickupApproaching =
+ deps.tripStatus === TripStatus.VehicleApproaching && !shouldUseChangePickupIcon;
+ const isPickupArrived =
+ deps.tripStatus === TripStatus.VehicleArrivedPickup && !shouldUseChangePickupIcon;
+ const isPickupSettled =
+ [TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(deps.tripStatus) &&
+ !shouldUseChangePickupIcon;
+ // 修改目的地预览:起点(上车点)统一用三角图标 up-triangle + 名称显示在图标下方
+ const isPickupModifyPreview = deps.isModifyDestinationReviewVisible;
+ // 接驾态路线切换卡片展示中:地图状态被切换为 PublishRoute 展示态,但上车点名称仍需按三角图标态展示
+ const isPickupTriangle = isPickupApproaching || isPickupArrived || isPickupSettled || isPickupModifyPreview || deps.showTrip1StyleEndpoint;
+ // 名称在图标下方:接驾态 11px,到达/待支付/完成态 22px
+ const pickupNameLabelConfig = isPickupApproaching
+ ? SITE_NAME_LABEL_BASE
+ : SITE_NAME_LABEL_BELOW_LG;
+ const pickupName = pickupSite?.name || "上车点";
+ // 三角态浮窗为富内容(图片/限停),必须走 cover-view customCallout,不能用原生 callout(原生仅支持纯文本)。
+ const usePickupRichCallout = isPickupTriangle && !shouldHidePickupName;
+ const usePickupNativeCallout = useAndroidNativeTrip1Callout && !usePickupRichCallout;
return {
id: 1,
latitude: pickupSite.latitude,
longitude: pickupSite.longitude,
iconPath: shouldUseChangePickupIcon
? CHANGE_SITE_UP_ICON_URL
- : "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/start-icon.png",
- width: 21,
- height: 31.5,
- anchor: { x: 0.5, y: 1 },
+ : isPickupTriangle
+ ? PICKUP_APPROACHING_ICON_URL
+ : "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/golden-site-20260708.png",
+ width: isPickupTriangle ? 40 : 18,
+ height: isPickupTriangle ? 40 : 18,
+ // 三角图标(up-triangle)实心尖端位于图片高度约 63% 处(下方 37% 为渐变阴影+透明留白),
+ // 锚点上移到 0.5 让尖端贴合并略微压入坐标点(路线末端);其余图标仍用底边锚点 1。
+ anchor: { x: 0.5, y: isPickupTriangle ? 0.5 : 1 },
zIndex: 1,
- name: pickupSite?.name || '上车点',
- hideNameCallout: deps.isModifyDestinationReviewVisible || shouldHidePickupName, // 修改目的地确认态隐藏上车点名称气泡
+ name: pickupName,
+ hideNameCallout: shouldHidePickupName, // 名称通过图标下方 label 展示;修改态的名称气泡/限停/倒计时由各气泡分支单独控制
+ // 三角态在图标下方展示站点名称
+ ...(isPickupTriangle && !shouldHidePickupName
+ ? { label: { content: pickupName, ...pickupNameLabelConfig } }
+ : {}),
...(!shouldHidePickupName && usePickupNativeCallout
? {
callout: {
content:
deps.tripStatus === TripStatus.PublishRoute ||
deps.showTrip1StyleEndpoint
- ? `${pickupSite?.name || "上车点"} >`
- : pickupSite?.name || "上车点",
- color: "#000000",
+ ? `${pickupName} >`
+ : pickupName,
+ color: "#FFFFFF",
fontSize: 12,
- bgColor: "#FFFFFF",
+ fontWeight: 500,
+ bgColor: "#000000",
borderRadius: 8,
padding: 8,
display: "ALWAYS",
@@ -211,7 +269,9 @@
deps.tripStatus
)
? 150
- : -5,
+ : isPickupArrived
+ ? -10
+ : -2,
anchorX: 0,
display: "ALWAYS"
}
@@ -231,30 +291,46 @@
)
? "已到达"
: dropoffSite.name || "目的地";
+ // 送驾态(前往目的地):下车点改用三角图标,名称移到图标下方,浮窗展示 end-site 图片;
+ // 待支付/完成态:下车点同样三角图标 + end-site 浮窗,名称移到图标左侧。
+ const isDropoffInService = deps.tripStatus === TripStatus.InService;
+ const isDropoffSettled = [
+ TripStatus.TripFinished,
+ TripStatus.PaymentSuccess
+ ].includes(deps.tripStatus);
+ const isDropoffTriangle = isDropoffInService || isDropoffSettled;
+ const useDropoffRichCallout = isDropoffTriangle; // cover-view 富浮窗
return {
id: 2,
latitude: dropoffSite.latitude,
longitude: dropoffSite.longitude,
- iconPath: "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/end-icon.png",
- width: 21,
- height: 31.5,
- anchor: { x: 0.5, y: 1 },
+ iconPath: isDropoffTriangle
+ ? DROPOFF_INSERVICE_ICON_URL
+ : "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/black-site-20260708.png",
+ width: isDropoffTriangle ? 40 : 18,
+ height: isDropoffTriangle ? 40 : 18,
+ // 三角图标(down-triangle)实心尖端位于图片高度约 63% 处(下方 37% 为渐变阴影+透明留白),
+ // 锚点上移到 0.5 让尖端贴合并略微压入坐标点(路线末端);其余图标仍用底边锚点 1。
+ anchor: { x: 0.5, y: isDropoffTriangle ? 0.5 : 1 },
zIndex: 1,
name: dropoffName,
- ...(useAndroidNativeTrip1Callout ||
- deps.tripStatus === TripStatus.InService
+ // 三角态在图标下方展示目的地名称:送驾态 11px、待支付/完成态 22px
+ // 修改目的地预览态下,站点名已在黑色浮窗(endpoint-summary)展示,隐藏图标下方的重复名称
+ ...(isDropoffTriangle && !deps.isModifyDestinationReviewVisible
+ ? {
+ label: {
+ content: dropoffName,
+ ...(isDropoffSettled ? SITE_NAME_LABEL_BELOW_LG : SITE_NAME_LABEL_BASE)
+ }
+ }
+ : {}),
+ ...(useAndroidNativeTrip1Callout && !useDropoffRichCallout
? {
callout: {
- content:
- deps.tripStatus === TripStatus.PublishRoute ||
- deps.isModifyDestinationReviewVisible ||
- (deps.showTrip1StyleEndpoint &&
- deps.tripStatus !== TripStatus.InService)
- ? dropoffName
- : dropoffName,
- color: "#000000",
+ content: dropoffName,
+ color: "#FFFFFF",
fontSize: 12,
- bgColor: "#FFFFFF",
+ bgColor: "#000000",
borderRadius: 8,
padding: 8,
display: "ALWAYS",
@@ -262,7 +338,7 @@
}
}
: {
- customCallout: { anchorY: -5, anchorX: 0, display: "ALWAYS" }
+ customCallout: { anchorY: -2, anchorX: 0, display: "ALWAYS" }
})
};
};
@@ -299,7 +375,7 @@
const verticalExtraOffset = hasBeforeUserOrderTip ? 22 : 0;
const anchorY = 80 + verticalExtraOffset;
const anchorX = 0;
-
+
return {
id: deps.vehicleMarkerId,
diff --git a/src/robotaxi/pages/begin-route/hooks/use-modify-destination-flow.ts b/src/robotaxi/pages/begin-route/hooks/use-modify-destination-flow.ts
index c1f303a..1907346 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-modify-destination-flow.ts
+++ b/src/robotaxi/pages/begin-route/hooks/use-modify-destination-flow.ts
@@ -1,4 +1,5 @@
import Taro from '@tarojs/taro';
+import { showToast } from '../../../components/toast';
import { useEffect, useMemo, useState } from 'react';
import { orderChange } from '../../../api/begin-route';
import { TripStatus } from '../../../constants/trip-status';
@@ -416,7 +417,7 @@
exitModifyDestinationReview({ shouldRestorePreviousEndSite: false });
Taro.hideLoading();
- Taro.showToast({
+ showToast({
title: '行程已更新',
icon: 'none'
});
diff --git a/src/robotaxi/pages/begin-route/hooks/use-modify-destination-preview-route.ts b/src/robotaxi/pages/begin-route/hooks/use-modify-destination-preview-route.ts
index acb8935..83b7c47 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-modify-destination-preview-route.ts
+++ b/src/robotaxi/pages/begin-route/hooks/use-modify-destination-preview-route.ts
@@ -1,4 +1,5 @@
import Taro from '@tarojs/taro';
+import { showToast } from '../../../components/toast';
import { useCallback, useEffect, useRef, useState } from 'react';
import { TripStatus } from '../../../constants/trip-status';
import { getOrderId } from '../../../utils/order';
@@ -238,7 +239,7 @@
syncOrderRouteMetrics: false
},
);
- Taro.showToast({
+ showToast({
title: '报价已更新,请再次确认修改',
icon: 'none'
});
diff --git a/src/robotaxi/pages/begin-route/hooks/use-order-cancel.ts b/src/robotaxi/pages/begin-route/hooks/use-order-cancel.ts
index b1e9ce3..d5ff7d0 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-order-cancel.ts
+++ b/src/robotaxi/pages/begin-route/hooks/use-order-cancel.ts
@@ -1,4 +1,5 @@
import Taro from '@tarojs/taro';
+import { showToast } from '../../../components/toast';
import { clearOrderTripStorage, reportEvent } from '../../../../utils/Robotaxi-public-method';
import { cancelOrder } from '../../../api/begin-route';
import { getOrderId } from '../../../utils/order';
@@ -247,12 +248,12 @@
if (shouldReportPickupArrivedCancelTrip) {
reportPickupArrivedCancelTripEvent();
}
- Taro.showToast({ title: '行程已取消', icon: 'success' });
+ showToast({ title: '行程已取消', icon: 'success' });
clearActiveOrderRuntime();
deps.setTripStatus(TripStatus.CancelOnlyPickup);
deps.setCancelType(4);
} catch (error) {
- Taro.showToast({ title: '取消订单失败,请重试', icon: 'none' });
+ showToast({ title: '取消订单失败,请重试', icon: 'none' });
}
};
diff --git a/src/robotaxi/pages/begin-route/hooks/use-order-distance-config.ts b/src/robotaxi/pages/begin-route/hooks/use-order-distance-config.ts
index 48d4c02..05b7325 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-order-distance-config.ts
+++ b/src/robotaxi/pages/begin-route/hooks/use-order-distance-config.ts
@@ -1,4 +1,5 @@
import Taro from '@tarojs/taro';
+import { showToast } from '../../../components/toast';
import { useCallback, useState } from 'react';
import { getOrderDistance } from '../../../api/select-map-site';
import { getDistance } from '../../../../utils/Robotaxi-public-method';
@@ -132,7 +133,7 @@
}
const openCityId = await resolveOpenCityId();
if (openCityId) return openCityId;
- Taro.showToast({ title: '未能获取所在城市信息,请重试', icon: 'none' });
+ showToast({ title: '未能获取所在城市信息,请重试', icon: 'none' });
deps.onCheckFailed();
return null;
};
diff --git a/src/robotaxi/pages/begin-route/hooks/use-order-payment-result-query.ts b/src/robotaxi/pages/begin-route/hooks/use-order-payment-result-query.ts
index 820de01..b4ee7be 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-order-payment-result-query.ts
+++ b/src/robotaxi/pages/begin-route/hooks/use-order-payment-result-query.ts
@@ -1,5 +1,6 @@
import { useCallback } from 'react';
import Taro from '@tarojs/taro';
+import { showToast } from '../../../components/toast';
import { queryOrderPaymentResultFromOrderId } from '../../../api/begin-route';
import { RobotaxiOrderStatus } from '../../../api/types/begin-route';
@@ -19,7 +20,7 @@
*/
const queryPaymentResult = useCallback(async () => {
if (!deps.orderId) {
- Taro.showToast({ title: '订单信息缺失,无法确认支付结果', icon: 'none' });
+ showToast({ title: '订单信息缺失,无法确认支付结果', icon: 'none' });
return;
}
@@ -34,13 +35,13 @@
}
if (nextStatus === RobotaxiOrderStatus.Cancelled) {
- Taro.showToast({ title: '订单已取消', icon: 'none' });
+ showToast({ title: '订单已取消', icon: 'none' });
return;
}
- Taro.showToast({ title: '订单状态异常,请稍后查看', icon: 'none' });
+ showToast({ title: '订单状态异常,请稍后查看', icon: 'none' });
} catch (error) {
- Taro.showToast({ title: '支付结果查询失败,请稍后重试', icon: 'none' });
+ showToast({ title: '支付结果查询失败,请稍后重试', icon: 'none' });
} finally {
Taro.hideLoading();
}
diff --git a/src/robotaxi/pages/begin-route/hooks/use-order-payment.ts b/src/robotaxi/pages/begin-route/hooks/use-order-payment.ts
index 41ec882..42a9c03 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-order-payment.ts
+++ b/src/robotaxi/pages/begin-route/hooks/use-order-payment.ts
@@ -1,4 +1,5 @@
import Taro from '@tarojs/taro';
+import { showToast } from '../../../components/toast';
import { useRef } from 'react';
import { reportEvent } from '../../../../utils/Robotaxi-public-method';
import {
@@ -129,7 +130,7 @@
deps.setShowOverRoutePopup(false);
const targetOrderId = getTargetOrderId();
if (!targetOrderId) {
- Taro.showToast({ title: '订单信息缺失,无法结束行程', icon: 'none' });
+ showToast({ title: '订单信息缺失,无法结束行程', icon: 'none' });
return;
}
@@ -155,14 +156,14 @@
}
reportTripPayStartEvent(orderDetail);
if (endOrderResult) {
- Taro.showToast({
+ showToast({
title: '当前选择优惠券不满足,已自动切换为满足条件优惠券',
icon: 'none',
duration: 3000
});
}
} catch (error) {
- Taro.showToast({ title: '结束行程失败,请重试', icon: 'none' });
+ showToast({ title: '结束行程失败,请重试', icon: 'none' });
}
};
@@ -194,7 +195,7 @@
}
const targetOrderId = getTargetOrderId();
if (!targetOrderId) {
- Taro.showToast({ title: '订单信息缺失,无法支付', icon: 'none' });
+ showToast({ title: '订单信息缺失,无法支付', icon: 'none' });
return;
}
paySubmittingRef.current = true;
@@ -232,7 +233,7 @@
Taro.showToast({ title: '暂不支持当前支付方式', icon: 'none' });
} catch (error) {
- Taro.showToast({ title: '支付失败,请重试', icon: 'none' });
+ showToast({ title: '支付失败,请重试', icon: 'none' });
} finally {
Taro.hideLoading();
paySubmittingRef.current = false;
diff --git a/src/robotaxi/pages/begin-route/hooks/use-order-submit.ts b/src/robotaxi/pages/begin-route/hooks/use-order-submit.ts
index 3adbac8..0204f21 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-order-submit.ts
+++ b/src/robotaxi/pages/begin-route/hooks/use-order-submit.ts
@@ -1,4 +1,5 @@
import Taro from '@tarojs/taro';
+import { showToast } from '../../../components/toast';
import { useCallback, useRef } from 'react';
import { formatDateTime, debounce, reportEvent } from '../../../../utils/Robotaxi-public-method';
import { createCreditPayOrder, saveOrder, routeStaleCheck } from '../../../api/begin-route';
@@ -172,7 +173,7 @@
*/
const refreshRouteAndStopSubmitting = (startSite: any, message: string) => {
reportEvent(CONFIRM_RIDE_EXPIRE_BUTTON_EVENT_ID, createConfirmRideEventPayload('expire'));
- Taro.showToast({ title: message, icon: 'none' });
+ showToast({ title: message, icon: 'none' });
deps.routeRequestRouteAndFare(
Number(startSite?.latitude),
Number(startSite?.longitude),
@@ -237,7 +238,7 @@
if (typeof response === 'string') {
reportPayResultEvent(resolvePayResultStatus(response));
- Taro.showToast({ title: response, icon: 'none', mask: true });
+ showToast({ title: response, icon: 'none', mask: true });
setTimeout(() => {
Taro.reLaunch({ url: HOME_PAGE_URL });
Taro.removeStorageSync('selectedCouponId');
@@ -247,7 +248,7 @@
if (!resolvedOrderId) {
reportPayResultEvent(resolvePayResultStatus(response?.msg || response?.data || response));
- Taro.showToast({ title: '下单失败,请重试', icon: 'none' });
+ showToast({ title: '下单失败,请重试', icon: 'none' });
return null;
}
@@ -343,7 +344,7 @@
stopCallSubmitting();
return false;
} catch (error) {
- Taro.showToast({ title: '协议校验失败,请重试', icon: 'none' });
+ showToast({ title: '协议校验失败,请重试', icon: 'none' });
stopCallSubmitting();
return false;
}
@@ -357,7 +358,7 @@
return true;
}
if (!deps.endSite.name) {
- Taro.showToast({ title: '请选择站点', icon: 'none' });
+ showToast({ title: '请选择站点', icon: 'none' });
}
stopCallSubmitting();
return false;
@@ -386,7 +387,7 @@
) {
return startSite;
}
- Taro.showToast({ title: '请先选择起终点', icon: 'none' });
+ showToast({ title: '请先选择起终点', icon: 'none' });
stopCallSubmitting();
return null;
};
@@ -425,7 +426,7 @@
refreshRouteAndStopSubmitting(startSite, ROUTE_STALE_NOTICE_TEXT);
return false;
} catch (error) {
- Taro.showToast({ title: '路线校验失败,请重试', icon: 'none' });
+ showToast({ title: '路线校验失败,请重试', icon: 'none' });
stopCallSubmitting();
return false;
}
@@ -555,7 +556,7 @@
const selectedRoute = getSelectedRoute();
if (!selectedRoute) {
- Taro.showToast({ title: '路线信息缺失,请重试', icon: 'none' });
+ showToast({ title: '路线信息缺失,请重试', icon: 'none' });
stopCallSubmitting();
return;
}
@@ -607,8 +608,7 @@
Taro.setStorageSync('creditPayOrderId', submitResult.orderId);
const creditPayPackageValue = String(submitResult.creditPayPackageValue || '').trim();
if (!creditPayPackageValue) {
- Taro.hideLoading();
- Taro.showToast({ title: '支付参数缺失,请稍后重试', icon: 'none' });
+ showToast({ title: '支付参数缺失,请稍后重试', icon: 'none' });
return;
}
// 复用现有支付分确认页拉起与回跳查询链路。
diff --git a/src/robotaxi/pages/begin-route/hooks/use-page-actions.ts b/src/robotaxi/pages/begin-route/hooks/use-page-actions.ts
index 5ec245d..4242286 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-page-actions.ts
+++ b/src/robotaxi/pages/begin-route/hooks/use-page-actions.ts
@@ -1,4 +1,5 @@
import Taro from '@tarojs/taro';
+import { showToast } from '../../../components/toast';
import { clearOrderTripStorage, safeMakePhoneCall } from '../../../../utils/Robotaxi-public-method';
import { TripStatus } from '../../../constants/trip-status';
import {
@@ -69,7 +70,7 @@
*/
const guardPrivacyThenNavigate = (url: string) => {
if (Taro.getStorageSync('has_show_privicy_Robotaxi') !== 'true') {
- Taro.showToast({
+ showToast({
title: '请先同意协议',
icon: 'none'
});
diff --git a/src/robotaxi/pages/begin-route/hooks/use-page-restore.ts b/src/robotaxi/pages/begin-route/hooks/use-page-restore.ts
index 7ced16d..ee1cba6 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-page-restore.ts
+++ b/src/robotaxi/pages/begin-route/hooks/use-page-restore.ts
@@ -1,4 +1,5 @@
import Taro from '@tarojs/taro';
+import { showToast } from '../../../components/toast';
import wsManager from '../../../../utils/websocket';
import { mapOrderStatusToTripStatus, clearOrderTripStorage } from '../../../../utils/Robotaxi-public-method';
import { getOrderActive, } from '../../../api/home';
@@ -440,14 +441,14 @@
const isModifyDestinationReturn = String(fromModifyDestination || '') === '1';
const shouldShowPendingSameSiteToast = isModifyDestinationReturn && !!Taro.getStorageSync(MODIFY_DESTINATION_PENDING_SAME_SITE_TOAST_KEY);
if (skipPageFlag && hasActiveTripOrderInStorage() && !isModifyDestinationReturn && !isModifyDestinationSessionActive) {
- Taro.showToast({
+ showToast({
title: getActiveTripOrderToastTitle(Taro.getStorageSync('tripStatus')),
icon: 'none',
});
}
if (shouldShowPendingSameSiteToast) {
Taro.removeStorageSync(MODIFY_DESTINATION_PENDING_SAME_SITE_TOAST_KEY);
- Taro.showToast({
+ showToast({
title: MODIFY_DESTINATION_SAME_SITE_TOAST,
icon: 'none'
});
diff --git a/src/robotaxi/pages/begin-route/hooks/use-polyline-render.ts b/src/robotaxi/pages/begin-route/hooks/use-polyline-render.ts
index c9edcfc..d5d23b1 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-polyline-render.ts
+++ b/src/robotaxi/pages/begin-route/hooks/use-polyline-render.ts
@@ -334,7 +334,7 @@
lines.push({
id: PASSED_ROUTE_POLYLINE_ID,
points: passedRoutePoints,
- color: '#D7DDEF',
+ color: '#D6D9DC',
width: 10,
clickable: false,
dottedLine: false,
@@ -350,7 +350,7 @@
lines.push({
id: ROUTE_POLYLINE_BASE_ID + index,
points: reducePoints(points, resolveRoutePointLimit(deps.isAndroid)),
- color: '#C2CAF1',
+ color: '#BED6CA',
width: 10,
clickable: true,
dottedLine: false,
@@ -364,9 +364,9 @@
lines.push({
id: ROUTE_POLYLINE_BASE_ID + selectedRouteIndex,
points: displayRoutePoints,
- color: '#7B91FF',
+ color: '#59B688',
width: 8,
- borderColor: '#6D7BC1',
+ borderColor: '#3D8F66',
borderWidth: 1,
clickable: true,
dottedLine: false,
@@ -377,9 +377,9 @@
lines.push({
id: ROUTE_POLYLINE_BASE_ID,
points: displayRoutePoints,
- color: '#7B91FF',
+ color: '#59B688',
width: 8,
- borderColor: '#6D7BC1',
+ borderColor: '#3D8F66',
borderWidth: 1,
clickable: true,
dottedLine: false,
@@ -391,7 +391,7 @@
lines.push({
id: WALKING_POLYLINE_ID_OFFSET + allRoutePoints.length,
points: walkingPoints,
- color: '#45A5FF',
+ color: '#DFB369',
width: deps.isAndroid ? 5 : 8,
dottedLine: true,
arrowLine: false,
diff --git a/src/robotaxi/pages/begin-route/hooks/use-preloaded-map-assets.ts b/src/robotaxi/pages/begin-route/hooks/use-preloaded-map-assets.ts
index 7d87a1e..684a190 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-preloaded-map-assets.ts
+++ b/src/robotaxi/pages/begin-route/hooks/use-preloaded-map-assets.ts
@@ -10,8 +10,6 @@
waitingCalloutBgUrl: string;
/** 等待态雷达图地址。 */
waitingRadarUrl: string;
- /** 路线概览气泡背景图地址。 */
- routeSummaryBgUrl: string;
};
/** Hook 入参。 */
@@ -30,8 +28,6 @@
waitingCalloutBgSrc: string;
/** 等待态雷达图本地地址。 */
waitingRadarSrc: string;
- /** 路线概览气泡背景图本地地址。 */
- routeSummaryBgSrc: string;
};
/**
@@ -41,7 +37,6 @@
const [customCalloutIconSrc, setCustomCalloutIconSrc] = useState(params.assetUrls.customCalloutIconUrl); // 自定义气泡箭头图本地地址
const [waitingCalloutBgSrc, setWaitingCalloutBgSrc] = useState(params.assetUrls.waitingCalloutBgUrl); // 等待态气泡背景图本地地址
const [waitingRadarSrc, setWaitingRadarSrc] = useState(params.assetUrls.waitingRadarUrl); // 等待态雷达图本地地址
- const [routeSummaryBgSrc, setRouteSummaryBgSrc] = useState(params.assetUrls.routeSummaryBgUrl); // 路线概览气泡背景图本地地址
/**
* 预加载单张图片并在组件仍挂载时回写结果。
@@ -56,11 +51,10 @@
useEffect(() => {
const cancelledRef = { current: false };
preloadSingleAsset(params.assetUrls.customCalloutIconUrl, setCustomCalloutIconSrc, cancelledRef);
- preloadSingleAsset(params.assetUrls.routeSummaryBgUrl, setRouteSummaryBgSrc, cancelledRef);
return () => {
cancelledRef.current = true;
};
- }, [params.assetUrls.customCalloutIconUrl, params.assetUrls.routeSummaryBgUrl]);
+ }, [params.assetUrls.customCalloutIconUrl]);
useEffect(() => {
if (![TripStatus.Publishing, TripStatus.CallingCar].includes(params.tripStatus)) {
@@ -77,7 +71,6 @@
return {
customCalloutIconSrc,
waitingCalloutBgSrc,
- waitingRadarSrc,
- routeSummaryBgSrc
+ waitingRadarSrc
};
}
diff --git a/src/robotaxi/pages/begin-route/hooks/use-route-request.ts b/src/robotaxi/pages/begin-route/hooks/use-route-request.ts
index 037d00c..d37c3b4 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-route-request.ts
+++ b/src/robotaxi/pages/begin-route/hooks/use-route-request.ts
@@ -119,11 +119,14 @@
item: any,
snapshotId: any,
generatedAtMillis: any,
- responseOriginalFare?: any
+ responseOriginalFare?: any,
+ responseFreeText?: any
) => {
const normalizedTopLevelOriginalFare = Number(responseOriginalFare);
return {
...(item || {}),
+ // 顶层免费文案(与顶层 operateFree 同级)兜底透传到每条路线,供免费态展示;路线自带 freeText 时优先使用
+ freeText: item?.freeText ?? responseFreeText,
fareEstimate: {
...(item?.fareEstimate || {}),
...(Number.isFinite(normalizedTopLevelOriginalFare)
@@ -436,6 +439,7 @@
const responseSnapshotId = response?.snapshotId;
const responseGeneratedAtMillis = response?.generatedAtMillis;
const responseOriginalFare = response?.originalFare;
+ const responseFreeText = response?.freeText;
let options: any[] = [];
if (responseRoutes.length > 0) {
@@ -443,17 +447,19 @@
item,
responseSnapshotId,
responseGeneratedAtMillis,
- responseOriginalFare
+ responseOriginalFare,
+ responseFreeText
));
} else if (Array.isArray(response) && response.length > 0) {
options = response.map((item: any) => normalizeRouteOption(
item,
responseSnapshotId,
responseGeneratedAtMillis,
- responseOriginalFare
+ responseOriginalFare,
+ responseFreeText
));
} else if (response) {
- options = [normalizeRouteOption(response, responseSnapshotId, responseGeneratedAtMillis, responseOriginalFare)];
+ options = [normalizeRouteOption(response, responseSnapshotId, responseGeneratedAtMillis, responseOriginalFare, responseFreeText)];
}
let routeData = options[0] || null;
diff --git a/src/robotaxi/pages/begin-route/hooks/use-route-summary-anchor-location.ts b/src/robotaxi/pages/begin-route/hooks/use-route-summary-anchor-location.ts
deleted file mode 100644
index ddc4491..0000000
--- a/src/robotaxi/pages/begin-route/hooks/use-route-summary-anchor-location.ts
+++ /dev/null
@@ -1,157 +0,0 @@
-import Taro from '@tarojs/taro';
-import { useCallback, useEffect, useRef, useState } from 'react';
-import {
- resolveRouteSummaryAnchorPoint,
- ROUTE_SUMMARY_CALLOUT_HEIGHT,
- ROUTE_SUMMARY_SAFE_BOTTOM_MARGIN,
- ROUTE_SUMMARY_SAFE_SIDE_MARGIN,
- ROUTE_SUMMARY_SAFE_TOP_MARGIN,
- ROUTE_SUMMARY_CALLOUT_WIDTH,
- type RouteSummaryGeoPoint
-} from './route-summary-anchor';
-
-type Deps = {
- enabled: boolean;
- routeSummaryLocation: RouteSummaryGeoPoint | null;
- routePoints?: RouteSummaryGeoPoint[];
-};
-
-type MapProjectionContext = {
- rect: any;
- southwest: { latitude: number; longitude: number };
- northeast: { latitude: number; longitude: number };
-};
-
-/**
- * 路线摘要气泡锚点解析:优先选择屏幕内可见且不碰撞底部卡片的路线点。
- */
-export default function useRouteSummaryAnchorLocation(deps: Deps) {
- /** 当前解析出的路线摘要锚点。 */
- const [routeSummaryAnchorLocation, setRouteSummaryAnchorLocation] = useState<RouteSummaryGeoPoint | null>(null);
- /** 地图容器矩形缓存。 */
- const mapRectRef = useRef<any>(null);
-
- /**
- * 同时量取地图区域与底部卡片区域。
- */
- const measureLayout = useCallback(() => new Promise<any>((resolve) => {
- Taro.createSelectorQuery()
- .select('#map')
- .boundingClientRect()
- .select('.container')
- .boundingClientRect()
- .exec((res) => {
- const mapRect = res?.[0];
- const containerRect = res?.[1];
- if (mapRect) {
- mapRectRef.current = mapRect;
- }
- resolve({
- mapRect,
- containerRect
- });
- });
- }), []);
-
- /**
- * 获取当前地图投影上下文。
- */
- const resolveMapProjectionContext = useCallback((rect: any) => {
- if (!rect) {
- return Promise.resolve<MapProjectionContext | null>(null);
- }
- return new Promise<MapProjectionContext | null>((resolve) => {
- Taro.createMapContext('map').getRegion({
- success: (res) => {
- const southwest = res?.southwest;
- const northeast = res?.northeast;
- if (!southwest || !northeast) {
- resolve(null);
- return;
- }
- resolve({
- rect,
- southwest,
- northeast
- });
- },
- fail: () => resolve(null)
- });
- });
- }, []);
-
- /**
- * 将路线点投影到屏幕坐标。
- */
- const projectToScreen = useCallback((point: RouteSummaryGeoPoint, projection: MapProjectionContext | null) => {
- if (!point || !projection) {
- return null;
- }
- const latRange = projection.northeast.latitude - projection.southwest.latitude;
- const lngRange = projection.northeast.longitude - projection.southwest.longitude;
- if (!latRange || !lngRange) {
- return null;
- }
- return {
- x: projection.rect.left + ((point.longitude - projection.southwest.longitude) / lngRange) * projection.rect.width,
- y: projection.rect.top + ((projection.northeast.latitude - point.latitude) / latRange) * projection.rect.height
- };
- }, []);
-
- /**
- * 重新计算路线摘要锚点。
- */
- const updateRouteSummaryAnchorLocation = useCallback(async () => {
- if (!deps.enabled || !deps.routeSummaryLocation) {
- setRouteSummaryAnchorLocation(null);
- return;
- }
- const layout = await measureLayout();
- const rect = layout?.mapRect || mapRectRef.current;
- const projection = await resolveMapProjectionContext(rect);
- if (!rect || !projection) {
- setRouteSummaryAnchorLocation(deps.routeSummaryLocation);
- return;
- }
- const mapRight = rect.left + rect.width;
- const mapBottom = rect.top + rect.height;
- const containerTop = Number(layout?.containerRect?.top);
- const safeBottomBoundary = Number.isFinite(containerTop)
- ? Math.min(mapBottom - ROUTE_SUMMARY_SAFE_BOTTOM_MARGIN, containerTop - ROUTE_SUMMARY_SAFE_BOTTOM_MARGIN)
- : mapBottom - ROUTE_SUMMARY_SAFE_BOTTOM_MARGIN;
- const nextResolvedAnchor = resolveRouteSummaryAnchorPoint({
- routePoints: Array.isArray(deps.routePoints) ? deps.routePoints : [],
- fallbackPoint: deps.routeSummaryLocation,
- bounds: {
- safeLeft: rect.left + ROUTE_SUMMARY_SAFE_SIDE_MARGIN,
- safeRight: mapRight - ROUTE_SUMMARY_SAFE_SIDE_MARGIN,
- safeTop: rect.top + ROUTE_SUMMARY_SAFE_TOP_MARGIN,
- safeBottom: safeBottomBoundary,
- bubbleWidth: ROUTE_SUMMARY_CALLOUT_WIDTH,
- bubbleHeight: ROUTE_SUMMARY_CALLOUT_HEIGHT
- },
- projectToScreen: (point) => projectToScreen(point, projection)
- });
- setRouteSummaryAnchorLocation(nextResolvedAnchor || deps.routeSummaryLocation);
- }, [deps.enabled, deps.routePoints, deps.routeSummaryLocation, measureLayout, projectToScreen, resolveMapProjectionContext]);
-
- useEffect(() => {
- updateRouteSummaryAnchorLocation();
- }, [updateRouteSummaryAnchorLocation]);
-
- /**
- * 地图视野变化结束后,重新解析路线摘要锚点。
- */
- const handleRegionChange = useCallback((event: any) => {
- const eventType = String(event?.type || event?.detail?.type || '').toLowerCase();
- if (eventType === 'end') {
- updateRouteSummaryAnchorLocation();
- }
- }, [updateRouteSummaryAnchorLocation]);
-
- return {
- routeSummaryAnchorLocation,
- updateRouteSummaryAnchorLocation,
- handleRegionChange
- };
-}
diff --git a/src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx b/src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
index db4f5c6..9997325 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
+++ b/src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
@@ -10,6 +10,14 @@
import { TripStatus } from "../../../constants/trip-status";
import { formatRemainingDurationDisplay } from "../../../utils/format";
+/** 将秒数格式化为 MM:SS(不足两位补零)。 */
+const formatCountdownDisplay = (seconds: unknown) => {
+ const total = Math.max(0, Math.floor(Number(seconds) || 0));
+ const minutes = Math.floor(total / 60);
+ const rest = total % 60;
+ return `${String(minutes).padStart(2, "0")}:${String(rest).padStart(2, "0")}`;
+};
+
type Deps = {
/** 是否安卓 */
isAndroid: boolean;
@@ -35,10 +43,6 @@
arriveCalloutVisible: boolean;
/** 到达气泡样式 */
arriveCalloutStyle: any;
- /** 路线摘要气泡可见 */
- routeSummaryCalloutVisible: boolean;
- /** 路线摘要气泡样式 */
- routeSummaryCalloutStyle: any;
/** 到达态气泡渲染就绪(角度与缩放均就绪) */
arrivedTooltipReady: boolean;
/** 路线距离 */
@@ -51,12 +55,12 @@
hasPreviousOrder: boolean;
/** 自定义气泡图标 */
customCalloutIconSrc: string;
- /** 路线概览气泡背景图 */
- routeSummaryBgSrc: string;
/** 是否使用下单前端点气泡样式 */
showTrip1StyleEndpoint: boolean;
/** 上车点 */
startSite: any;
+ /** 呼单等待计时秒数(按秒递增,接单后重置为 0) */
+ elapsedSeconds: number;
/** 下车点 */
endSite: any;
/** 发单 */
@@ -209,6 +213,8 @@
hasBeforeUserEndSite: boolean;
routeDistance: { value: number; unit: "km" | "m" } | null;
durationDisplay: DurationDisplay;
+ /** 是否接驾态(前往上车点):金色药丸样式,仅展示里程(KM/M),不展示分钟 */
+ isPickup: boolean;
};
/**
@@ -216,36 +222,60 @@
*/
const AndroidCarCalloutContent = memo(
function AndroidCarCalloutContent(props: AndroidCarCalloutContentProps) {
- const { hasBeforeUserEndSite, routeDistance, durationDisplay } = props;
+ const { hasBeforeUserEndSite, routeDistance, durationDisplay, isPickup } = props;
return (
<View
- className={`custom-callout-content ${hasBeforeUserEndSite
+ className={`custom-callout-content ${isPickup ? "car-metric-callout " : ""}${hasBeforeUserEndSite
? "car-callout-with-before car-callout-double-line"
: ""
}`.trim()}
- style={{ padding: "6px 10px" }}
+ style={isPickup ? undefined : { padding: "6px 10px" }}
>
{hasBeforeUserEndSite && (
<View className="car-time car-before-order-tip">即将完成上一单</View>
)}
- <View
- className="car-time"
- style={{ alignItems: "baseline", paddingBottom: 0 }}
- >
- <View className="car-time-num">
- {routeDistance?.value ?? 0}
+ {hasBeforeUserEndSite ? (
+ <View className="car-before-order-tip-text">
+ <View>全程</View>
+ <View className="car-before-order-tip-value">
+ {routeDistance?.value ?? 0}
+ </View>
+ <View className="car-before-order-tip-unit">
+ {routeDistance?.unit === "m" ? "米" : "公里"}
+ </View>
+ <View className="car-before-order-tip-value">
+ {durationDisplay.value}
+ </View>
+ <View className="car-before-order-tip-unit">
+ {durationDisplay.unit}
+ </View>
</View>
- <View style={{ flexShrink: 0 }}>
- {routeDistance?.unit === "m" ? "米" : "公里"}
- </View>
+ ) : (
<View
- style={{ marginLeft: "2px" }}
- className="car-time-num"
+ className="car-time"
+ style={{ alignItems: "baseline", paddingBottom: 0 }}
>
- {durationDisplay.value}
+ <View className="car-time-num">
+ {routeDistance?.value ?? 0}
+ </View>
+ <View style={{ flexShrink: 0 }}>
+ {isPickup
+ ? routeDistance?.unit === "m" ? "M" : "KM"
+ : routeDistance?.unit === "m" ? "米" : "公里"}
+ </View>
+ {!isPickup && (
+ <>
+ <View
+ style={{ marginLeft: "2px" }}
+ className="car-time-num"
+ >
+ {durationDisplay.value}
+ </View>
+ <View style={{ flexShrink: 0 }}>{durationDisplay.unit}</View>
+ </>
+ )}
</View>
- <View style={{ flexShrink: 0 }}>{durationDisplay.unit}</View>
- </View>
+ )}
</View>
);
},
@@ -254,7 +284,8 @@
prevProps.routeDistance?.value === nextProps.routeDistance?.value &&
prevProps.routeDistance?.unit === nextProps.routeDistance?.unit &&
prevProps.durationDisplay.value === nextProps.durationDisplay.value &&
- prevProps.durationDisplay.unit === nextProps.durationDisplay.unit
+ prevProps.durationDisplay.unit === nextProps.durationDisplay.unit &&
+ prevProps.isPickup === nextProps.isPickup
);
/**
@@ -294,15 +325,6 @@
/** 修改目的地预览态和接驾态切路线期间隐藏车辆公里/分钟气泡。 */
const shouldShowVehicleMetricCallout =
!deps.isModifyDestinationReviewVisible && !deps.isPickupRouteChangeVisible;
- const routeSummaryText = useMemo(
- () =>
- deps.routeDistance && deps.routeDuration !== null
- ? `${vehicleMetricDurationDisplay.value}${vehicleMetricDurationDisplay.unit
- } ${deps.routeDistance.value}${deps.routeDistance.unit === "m" ? "米" : "公里"
- }`
- : "",
- [deps.routeDistance, deps.routeDuration, vehicleMetricDurationDisplay]
- );
const shouldHideAndroidMarkerCallout =
deps.isAndroid &&
([
@@ -319,12 +341,14 @@
hasBeforeUserEndSite={deps.hasBeforeUserEndSite}
routeDistance={deps.routeDistance}
durationDisplay={vehicleMetricDurationDisplay}
+ isPickup={[TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus)}
/>
),
[
deps.hasBeforeUserEndSite,
deps.routeDistance,
- vehicleMetricDurationDisplay
+ vehicleMetricDurationDisplay,
+ deps.tripStatus
]
);
@@ -381,22 +405,41 @@
slot="callout"
marker-id={item.id}
key={calloutKey}
- className="waiting-callout"
+ className={`waiting-callout ${deps.tripStatus === TripStatus.Publishing ? 'waiting-callout-publishing' : ''}`}
>
<CoverView className="waiting-callout-content">
+ {deps.tripStatus === TripStatus.CallingCar && (
+ <CoverView className="waiting-callout-bg waiting-timedown-bg">
+ <CoverImage
+ className="waiting-callout-bg-img"
+ src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/tooltip-bg-mini-20260715.png"
+ />
+ <CoverView className="waiting-callout-bg-text">
+ {formatCountdownDisplay(deps.elapsedSeconds)}
+ </CoverView>
+ </CoverView>
+ )}
<CoverView
- className={`waiting-callout-txt ${!deps.isAndroid && "pr-6"}`}
+ className={`waiting-callout-txt ${!deps.isAndroid && "pr-6"}${
+ deps.tripStatus === TripStatus.CallingCar
+ ? " waiting-callout-txt--overlap"
+ : ""
+ }`}
>
{item.name}
{!deps.isAndroid && (
<CoverView style={{ color: "transparent" }}>占</CoverView>
)}
</CoverView>
+ <CoverImage
+ className="callout-diamond-icon"
+ src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/diamond-icon-20260708.png"
+ />
</CoverView>
<CoverImage className="waiting-radar" src={deps.waitingRadarSrc} />
</CoverView>
),
- [deps.isAndroid, deps.waitingRadarSrc]
+ [deps.isAndroid, deps.waitingRadarSrc, deps.tripStatus, deps.elapsedSeconds]
);
/**
@@ -408,25 +451,18 @@
slot="callout"
marker-id={item.id}
key={calloutKey}
- className="custom-callout"
+ className={`
+ custom-callout
+ ${
+ [TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus)
+ ? 'site-status-callout-box'
+ : ''
+ }
+ ${deps.tripStatus === TripStatus.VehicleArrivedPickup ? 'arrived-pickup-callout-box' : ''}`
+ }
style={item.id === 1 ? calloutRepaintStyle : undefined}
>
- {item.isRouteSummaryBubble ? (
- routeSummaryText ? (
- // 路由摘要气泡
- <CoverView className="route-summary-callout">
- <CoverImage
- className="route-summary-callout-bg"
- src={deps.routeSummaryBgSrc}
- />
- <CoverView className="route-summary-callout-text">
- {routeSummaryText}
- {/* 占位 */}
- <CoverView style={{ color: "transparent" }}>占</CoverView>
- </CoverView>
- </CoverView>
- ) : null
- ) : item.isBeforeUserSite ? (
+ {item.isBeforeUserSite ? (
<CoverView className="before-user-site-callout">
<CoverView className="before-user-site-txt">上一站终点</CoverView>
</CoverView>
@@ -437,7 +473,41 @@
item.id === 2 &&
[TripStatus.ArrivedDestination].includes(deps.tripStatus)
) &&
+ !(
+ item.id === 2 &&
+ (deps.tripStatus === TripStatus.PublishRoute || deps.isPickupRouteChangeVisible) &&
+ !!deps.routeDistance &&
+ deps.routeDuration !== null
+ ) &&
+ !(
+ item.id === 1 &&
+ deps.tripStatus === TripStatus.PublishRoute &&
+ !!deps.routeDistance &&
+ deps.routeDuration !== null
+ ) &&
+ !(
+ item.id === 1 &&
+ [
+ TripStatus.VehicleApproaching,
+ TripStatus.VehicleArrivedPickup,
+ TripStatus.TripFinished,
+ TripStatus.PaymentSuccess
+ ].includes(deps.tripStatus)
+ ) &&
+ !(
+ item.id === 2 &&
+ [
+ TripStatus.InService,
+ TripStatus.TripFinished,
+ TripStatus.PaymentSuccess
+ ].includes(deps.tripStatus)
+ ) &&
+ !(
+ (item.id === 1 || item.id === 2) &&
+ deps.isModifyDestinationReviewVisible
+ ) &&
!item.hideNameCallout && (
+ <>
<CoverView
className={`custom-callout-content ${deps.isAndroid &&
"txt-center "}`}
@@ -470,6 +540,191 @@
/>
)}
</CoverView>
+ <CoverImage
+ className="callout-diamond-icon"
+ src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/diamond-icon-20260708.png"
+ />
+ </>
+ )}
+ {item.id === 1 &&
+ deps.tripStatus === TripStatus.PublishRoute &&
+ !!deps.routeDistance &&
+ deps.routeDuration !== null &&
+ !item.hideNameCallout && (
+ <>
+ {Number(deps.startSite?.time) > 0 && (
+ <CoverView className="callout-parking-tip">
+ <CoverImage
+ className="callout-parking-tip-bg"
+ src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/tooltip-bg-20260715.png"
+ />
+ <CoverImage
+ className="callout-parking-tip-icon"
+ src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/map-time-icon-20260708.png"
+ />
+ <CoverView className="callout-parking-tip-text">
+ 限停{deps.startSite.time}分钟
+ {!deps.isAndroid && (
+ <CoverView
+ style={{ color: "transparent", width: "4px" }}
+ >
+ 占
+ </CoverView>
+ )}
+ </CoverView>
+ </CoverView>
+ )}
+ <CoverView
+ className={`custom-callout-content endpoint-summary-callout${
+ Number(deps.startSite?.time) > 0
+ ? " endpoint-summary-callout--overlap"
+ : ""
+ }`}
+ style={{ zIndex: 1 }}
+ >
+ <CoverView className="endpoint-summary-main">
+ <CoverView className="endpoint-summary-name">
+ {item.name}
+ {!deps.isAndroid &&
+ /[^\u4e00-\u9fa5]/.test(item.name) && (
+ <CoverView
+ style={{ color: "transparent", width: "3px" }}
+ >
+ 占
+ </CoverView>
+ )}
+ </CoverView>
+ </CoverView>
+ <CoverImage
+ className="endpoint-summary-icon"
+ src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/map-white-arrow-20260708.png"
+ />
+ </CoverView>
+ <CoverImage
+ className="callout-diamond-icon"
+ src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/diamond-icon-20260708.png"
+ />
+ </>
+ )}
+ {item.id === 2 &&
+ (deps.tripStatus === TripStatus.PublishRoute
+ || deps.isModifyDestinationReviewVisible
+ || deps.isPickupRouteChangeVisible
+ ) &&
+ !!deps.routeDistance &&
+ deps.routeDuration !== null &&
+ !item.hideNameCallout && (
+ <>
+ <CoverView
+ className="custom-callout-content endpoint-summary-callout"
+ style={{ zIndex: 1 }}
+ >
+ <CoverView className="endpoint-summary-main">
+ <CoverView className="endpoint-summary-name">
+ {item.name}
+ {!deps.isAndroid &&
+ /[^\u4e00-\u9fa5]/.test(item.name) && (
+ <CoverView
+ style={{ color: "transparent", width: "3px" }}
+ >
+ 占
+ </CoverView>
+ )}
+ </CoverView>
+ <CoverView className="endpoint-summary-metric">
+ <CoverView className="endpoint-summary-label">
+ 全程
+ </CoverView>
+ <CoverView className="endpoint-summary-golden-value">
+ {deps.routeDistance.value}
+ </CoverView>
+ <CoverView className="endpoint-summary-value-unit">
+ {deps.routeDistance.unit === "m" ? "米" : "公里"}
+ </CoverView>
+ <CoverView className="endpoint-summary-golden-value">
+ {vehicleMetricDurationDisplay.value}
+ </CoverView>
+ <CoverView className="endpoint-summary-value-unit">
+ {vehicleMetricDurationDisplay.unit}
+ </CoverView>
+ </CoverView>
+ </CoverView>
+ {!deps.isModifyDestinationReviewVisible && (
+ <CoverImage
+ className="endpoint-summary-icon"
+ src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/map-white-arrow-20260708.png"
+ />
+ )}
+ </CoverView>
+ <CoverImage
+ className="callout-diamond-icon"
+ src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/diamond-icon-20260708.png"
+ />
+ </>
+ )}
+ {item.id === 1 &&
+ deps.tripStatus === TripStatus.VehicleApproaching &&
+ !deps.isModifyDestinationReviewVisible &&
+ !item.hideNameCallout && (
+ <>
+ <CoverView
+ className="custom-callout-content site-status-callout"
+ style={{ zIndex: 1 }}
+ >
+ <CoverImage
+ className="site-status-car-icon"
+ src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/golden-car-icon-20260708.png"
+ />
+ <CoverView className="site-status-time">
+ 限停{deps.startSite?.time}MIN
+ </CoverView>
+ </CoverView>
+ <CoverImage
+ className="callout-diamond-icon"
+ src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/diamond-icon-20260708.png"
+ />
+ </>
+ )}
+ {item.id === 1 &&
+ deps.tripStatus === TripStatus.VehicleArrivedPickup &&
+ !deps.isModifyDestinationReviewVisible &&
+ !item.hideNameCallout &&
+ Number(pickupCountdownSeconds) > 0 && (
+ <CoverView
+ className="custom-callout-content site-countdown-callout"
+ style={{ zIndex: 1 }}
+ >
+ <CoverImage
+ className="site-countdown-icon"
+ src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/golden-time-icon-20260709.png"
+ />
+ <CoverView className="site-countdown-time">
+ {formatCountdownDisplay(pickupCountdownSeconds)}
+ </CoverView>
+ </CoverView>
+ )}
+ {item.id === 1 &&
+ ([TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(
+ deps.tripStatus
+ ) || deps.isModifyDestinationReviewVisible) &&
+ !item.hideNameCallout && (
+ <CoverImage
+ className="site-status-end-img"
+ src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/start-site-20260722.png"
+ />
+ )}
+ {item.id === 2 &&
+ [
+ TripStatus.InService,
+ TripStatus.TripFinished,
+ TripStatus.PaymentSuccess
+ ].includes(deps.tripStatus) &&
+ !deps.isModifyDestinationReviewVisible &&
+ !item.hideNameCallout && (
+ <CoverImage
+ className="site-status-end-img"
+ src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/end-site-20260722.png"
+ />
)}
{item.name === "车辆位置" &&
[TripStatus.VehicleApproaching, TripStatus.InService].includes(
@@ -478,37 +733,46 @@
!deps.isAndroid &&
shouldShowVehicleMetricCallout && (
<CoverView
- className={`custom-callout-content ${deps.hasBeforeUserEndSite
+ className={`custom-callout-content car-metric-callout ${deps.hasBeforeUserEndSite
? "car-callout-with-before car-callout-double-line"
: ""
}`.trim()}
- style={{
- padding: "6px 10px",
- minWidth: "40px",
- justifyContent: "center",
- zIndex: 2
- }}
+ style={{ minWidth: "40px", justifyContent: "center", zIndex: 2 }}
>
- {deps.hasBeforeUserEndSite && (
+ {!deps.hasBeforeUserEndSite && (
<CoverView className="car-time car-before-order-tip">
即将完成上一单
</CoverView>
)}
- <CoverView className="car-time">
- <CoverView className="car-time-num">
- {deps.routeDistance?.value ?? 0}
+ {!deps.hasBeforeUserEndSite ? (
+ <CoverView className="car-before-order-tip-text">
+ <CoverView>
+ 全程
+ </CoverView>
+ <CoverView className="car-before-order-tip-value">
+ {deps.routeDistance?.value ?? 0}
+ </CoverView>
+ <CoverView className="car-before-order-tip-unit">
+ {deps.routeDistance?.unit === "m" ? "M" : "KM"}
+ </CoverView>
+ <CoverView className="car-before-order-tip-value">
+ {vehicleMetricDurationDisplay.value}
+ </CoverView>
+ <CoverView className="car-before-order-tip-unit">
+ {vehicleMetricDurationDisplay.unit}
+ </CoverView>
</CoverView>
- <CoverView>
- {deps.routeDistance?.unit === "m" ? "米" : "公里"}
+ ) : (
+ <CoverView className="car-time">
+ <CoverView className="car-time-num">
+ {deps.routeDistance?.value ?? 0}
+ </CoverView>
+ <CoverView>
+ {deps.routeDistance?.unit === "m" ? "M" : "KM"}
+ </CoverView>
</CoverView>
- <CoverView
- style={{ marginLeft: "2px" }}
- className="car-time-num"
- >
- {vehicleMetricDurationDisplay.value}
- </CoverView>
- <CoverView>{vehicleMetricDurationDisplay.unit}</CoverView>
- </CoverView>
+ )
+ }
</CoverView>
)}
{item.id === 2 &&
@@ -527,11 +791,13 @@
deps.hasBeforeUserEndSite,
deps.isAndroid,
deps.isModifyDestinationReviewVisible,
+ deps.isPickupRouteChangeVisible,
deps.routeDistance,
- deps.routeSummaryBgSrc,
+ deps.routeDuration,
deps.showTrip1StyleEndpoint,
deps.tripStatus,
- routeSummaryText,
+ deps.startSite?.time,
+ pickupCountdownSeconds,
vehicleMetricDurationDisplay,
shouldShowVehicleMetricCallout,
calloutRepaintStyle
@@ -542,21 +808,23 @@
* 渲染单个 marker 气泡。
*/
const renderMarkerCallout = useCallback((item: any, index: number) => {
- const calloutKey = `callout-${item.id}-${item.name || ''}`;
- // 路线摘要气泡需跨平台强制显示,避免被 Android 普通气泡隐藏规则误伤
- if (item.isRouteSummaryBubble) {
- return deps.isAndroid ? null : renderDefaultCallout(item, calloutKey);
- }
- if (shouldHideAndroidMarkerCallout && !item.isBeforeUserSite) return null;
+ // iOS 上 CoverView 内多个文本节点(里程/时长数字)原地更新会偶发数字错乱,
+ // 故将会变化的里程/时长/限停时长纳入 key,数值变化时强制整体重新挂载而非原地 patch。
+ const calloutKey = `callout-${item.id}-${item.name || ''}-${deps.routeDistance?.value ?? ''}-${deps.routeDistance?.unit ?? ''}-${deps.routeDuration ?? ''}-${deps.startSite?.time ?? ''}`;
+ // 待支付/完成态的上/下车点浮窗为富内容(end-site 图片),需走 cover-view,不能被 Android 常规隐藏规则误伤
+ const isSettledSiteMarker =
+ (item.id === 1 || item.id === 2) &&
+ [TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(deps.tripStatus);
+ // 到达上车点态的上车点倒计时浮窗(cover-view),同样需放开
+ const isArrivedPickupMarker =
+ item.id === 1 && deps.tripStatus === TripStatus.VehicleArrivedPickup;
+ if (shouldHideAndroidMarkerCallout && !item.isBeforeUserSite && !isSettledSiteMarker && !isArrivedPickupMarker) return null;
const isWaiting = [TripStatus.Publishing, TripStatus.CallingCar].includes(deps.tripStatus) && index === 0;
if (isWaiting) return renderWaitingCallout(item, calloutKey);
// 车辆里程气泡:两端都不再用 marker customCallout 渲染。
// Android 用 <View> 浮层;iOS 用「自由定位的 CoverView」浮层(见 renderMapOverlays),
// 均规避被 translateMarker 丢弃(customCallout 在 iOS translate 下接驾/送驾都会偶发丢失)。
if (item.name === '车辆位置' && [TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus)) return null;
- // iOS 接驾态上车点已改用原生 callout(marker.callout),不再渲染 cover-view customCallout,
- // 既避免与原生 callout 重复,也彻底规避被车辆 translateMarker 丢弃。
- if (!deps.isAndroid && item.id === 1 && deps.tripStatus === TripStatus.VehicleApproaching) return null;
return renderDefaultCallout(item, calloutKey);
}, [deps.isAndroid, deps.tripStatus, renderDefaultCallout, renderWaitingCallout, shouldHideAndroidMarkerCallout]);
@@ -574,20 +842,6 @@
webp={true}
style={deps.waitingRadarStyle}
/>
- )}
- {deps.isAndroid && deps.routeSummaryCalloutVisible && routeSummaryText && (
- <View
- className="route-summary-callout"
- style={deps.routeSummaryCalloutStyle}
- >
- <Image
- className="route-summary-callout-bg"
- src={deps.routeSummaryBgSrc}
- />
- <View className="route-summary-callout-text">
- {routeSummaryText}
- </View>
- </View>
)}
{deps.isAndroid && deps.carCalloutVisible && shouldShowVehicleMetricCallout && (
<View className="car-callout-android" style={deps.carCalloutStyle}>
@@ -614,15 +868,11 @@
deps.isModifyDestinationReviewVisible,
deps.markers,
deps.routeDistance,
- deps.routeSummaryBgSrc,
- deps.routeSummaryCalloutStyle,
- deps.routeSummaryCalloutVisible,
deps.shouldShowWaitingRadar,
deps.waitingRadarSrc,
deps.waitingRadarStyle,
deps.waitingRadarVisible,
renderMarkerCallout,
- routeSummaryText,
shouldShowVehicleMetricCallout,
vehicleMetricDurationDisplay
]);
@@ -768,6 +1018,7 @@
onOpenCouponSelect={deps.handleOpenCouponSelect}
handleOpenEvaluatePopup={deps.handleOpenEvaluatePopup}
routeDurationSeconds={deps.routeDuration}
+ routeDistance={deps.routeDistance}
onToggleDetailFade={deps.handleToggleDetailFade}
orderTrunkStatus={deps.trunkStatus}
doorUnlockedInCurrentPage={deps.doorUnlockedInCurrentPage}
diff --git a/src/robotaxi/pages/begin-route/index.less b/src/robotaxi/pages/begin-route/index.less
index a2ab210..14d9987 100644
--- a/src/robotaxi/pages/begin-route/index.less
+++ b/src/robotaxi/pages/begin-route/index.less
@@ -1,40 +1,46 @@
-.select-city {
+@import '../../../robotaxi/style/ui-variables.less';
+
+.back-icon-box {
position: fixed;
top: 110px;
- left: 31px;
+ left: 24px;
display: flex;
align-items: center;
justify-content: center;
- width: 72px;
- height: 72px;
- border-radius: 200px;
+ width: 64px;
+ height: 64px;
background: rgba(255, 255, 255, 0.9);
- backdrop-filter: blur(40px);
+ // backdrop-filter: blur(40px);
box-shadow: 0px 4px 16px 0px rgba(84, 92, 143, 0.1);
-
- .select-city-icon {
- width: 40px;
- height: 40px;
+ border-radius: 50%;
+ .back-icon {
+ width: 64px;
+ height: 64px;
}
}
.map-entry {
position: fixed;
top: 200px;
- right: 32px;
+ right: 24px;
display: flex;
align-items: center;
flex-direction: column;
gap: 8px;
- border-radius: 24px;
+ .map-entry-box {
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ gap: 24px;
+ border-radius: 20px;
+ background: #FFFFFF;
+ padding: 20px 16px;
box-shadow: 0px 4px 16px 0px rgba(84, 92, 143, 0.1);
- background: rgba(255, 255, 255, 1);
- overflow: hidden;
- padding: 8px 12px;
+ }
.map-entry-icon {
- width: 64px;
- height: 64px;
+ width: 48px;
+ height:48px;
}
.split-line {
@@ -67,21 +73,40 @@
/* 相对于父级(底部卡片容器)顶部 */
margin-bottom: 24px;
/* 距离底部卡片24px */
- right: 32px;
+ right: 24px;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
.origin-icon {
- width: 72px;
- height: 72px;
+ width: 80px;
+ height: 80px;
}
+}
- .select-route-icon {
- margin-top: 16px;
- width: 72px;
- height: 72px;
+ .select-route-box {
+ position: absolute;
+ bottom: 100%;
+ /* 相对于父级(底部卡片容器)顶部 */
+ margin-bottom: 24px;
+ /* 距离底部卡片24px */
+ left: 24px;
+ z-index: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ .route-icon-box {
+ width: 80px;
+ height: 80px;
+ .select-route-icon {
+ width: 80px;
+ height: 80px;
+ }
+ }
+ .walk-icon {
+ width: 80px;
+ height: 80px;
}
}
}
@@ -94,6 +119,13 @@
align-items: center;
justify-content: center;
flex-direction: column;
+ transform: translateY(6px);
+ &.site-status-callout-box {
+ transform: translateY(13px);
+ }
+ &.arrived-pickup-callout-box {
+ transform: translateY(24px);
+ }
}
.pd-6-10 {
@@ -103,55 +135,250 @@
.waiting-callout-content {
z-index: 2;
position: absolute;
- top: 240px;
+ top: 218px;
left: 50%;
transform: translateX(-50%);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
}
.custom-callout-content {
display: flex;
+ position: relative;
align-items: center;
white-space: nowrap;
- border-radius: 16px;
- background: rgba(255, 255, 255, 1);
+ border-radius: 24px;
+ background: #000000;
font-size: 24px;
font-weight: 400;
- color: rgba(0, 0, 0, 0.9);
- line-height: 38px;
+ color: #FFFFFF;
+ line-height: normal;
box-shadow: 0px 4px 16px 0px rgba(45, 48, 64, 0.2);
- padding: 8px 20px;
+ padding: 20px;
}
-.route-summary-callout {
- position: relative;
- display: inline-flex;
+// 气泡底部中间的钻石指示图标
+.callout-diamond-icon {
+ width: 28px;
+ height: 28px;
+ margin-top: -14px;
+ flex-shrink: 0;
+ z-index: 0;
+}
+
+// 接驾态车辆气泡:金色圆角药丸,仅展示里程(KM/M)
+.car-metric-callout {
+ background: #DFB369;
+ color: #FFFFFF;
+ border-radius: 58px;
+ padding: 12px 20px;
+}
+
+.car-metric-callout .car-time {
+ color: #FFFFFF;
+ padding-bottom: 0;
+}
+
+.car-metric-callout .car-time-num {
+ color: #FFFFFF;
+}
+
+// 接驾/送驾态站点富浮窗(车图标+限停 / end-site 图片)
+.custom-callout-content.site-status-callout {
+ flex-direction: column;
align-items: center;
justify-content: center;
- min-width: 169px;
- height: 45px;
- padding-left: 15px;
- padding-right: 4px;
+ background: #000000;
+ border-radius: 32px;
+ padding: 12px 20px;
+}
+
+.site-status-car-icon {
+ width: 32px;
+ height: 32px;
+ display: block;
+}
+
+.site-status-time {
+ font-size: 18px;
+ font-weight: 500;
+ line-height: normal;
+ text-align: center;
+ letter-spacing: 0.04em;
+ color: #FFD181;
+ margin-top: 6px;
+}
+
+.site-status-end-img {
+ width: 96px;
+ height: 78px;
+ display: block;
+}
+
+// 到达上车点:倒计时黑色药丸浮窗(金色时钟图标 + MM:SS)
+.custom-callout-content.site-countdown-callout {
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ background: @black-text-main;
+ border-radius: 100px;
+ padding: 16px 20px;
+}
+
+.site-countdown-icon {
+ width: 24px;
+ height: 24px;
+ display: block;
+ margin-right: 8px;
+}
+
+.site-countdown-time {
+ font-family: @font-xpsans-bold;
+ font-size: 24px;
+ font-weight: normal;
+ line-height: 32px;
+ text-align: center;
+ letter-spacing: 0.04em;
+ color: #FFD181;
+}
+
+// 终点浮窗:站点名 + 全程里程/时长合并展示
+.custom-callout-content.endpoint-summary-callout {
+ align-items: center;
+ background: #141E27;
+ border-radius: 24px;
+ padding: 20px 20px 20px 16px;
+ // 外层浮窗需同步限宽,否则 white-space: nowrap 会让 flex 子项按内容撑开,
+ // 导致内部 .endpoint-summary-main 的 max-width: 400px 不生效
+ max-width: 400px;
box-sizing: border-box;
}
-.route-summary-callout-bg {
+// 起点浮窗存在限停气泡时,黑窗上移与限停气泡衔接(对齐 map-pointer 的 toolTip-txt margin-top:-32px)
+.custom-callout-content.endpoint-summary-callout.endpoint-summary-callout--overlap {
+ margin-top: -32px;
+}
+
+.endpoint-summary-main {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: center;
+ min-width: 198px;
+ max-width: 400px;
+ // 作为父级 flex(row) 容器的子项,默认 min-width: auto 会按内容撑开忽略 max-width,
+ // 需显式覆盖为 0 才能真正收缩到限定宽度内
+ flex-shrink: 1;
+}
+
+.endpoint-summary-name {
+ display: flex;
+ align-items: center;
+ max-width: 100%;
+ font-size: 24px;
+ font-weight: 500;
+ color: #FFFFFF;
+ white-space: normal;
+ word-break: break-all;
+}
+
+.endpoint-summary-metric {
+ display: flex;
+ align-items: center;
+ margin-top: 8px;
+ font-size: 20px;
+ line-height: normal;
+ white-space: normal;
+ // word-break: break-all;
+ // white-space: nowrap;
+}
+
+.endpoint-summary-label {
+ font-size: 20px;
+ line-height: normal;
+ flex-shrink: 0;
+ margin-right: 2px;
+ color: rgba(255, 255, 255, 0.5);
+}
+
+.endpoint-summary-value-unit {
+ display: flex;
+ align-items: center;
+ font-weight: 500;
+ line-height: normal;
+ font-size: 20px;
+ color: rgba(255, 255, 255, 0.5);
+ margin-left: 2px;
+}
+.endpoint-summary-golden-value {
+ display: flex;
+ align-items: center;
+ font-weight: 500;
+ line-height: normal;
+ font-size: 20px;
+ margin:0 2px;
+ color: rgba(255, 209, 129, 1);
+}
+
+.endpoint-summary-icon {
+ width: 16px;
+ height: 16px;
+ flex-shrink: 0;
+ margin-left: 8px;
+ display: block;
+}
+
+// 起点浮窗上方限停气泡(样式参考 map-pointer 的 toolTip-bg:带背景图,位于黑色浮窗上方,与黑窗左对齐)
+.callout-parking-tip {
+ position: relative;
+ align-self: flex-start;
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ justify-content: center;
+ padding: 3px 20px 32px 20px;
+ height: 80px;
+ min-width: 200px;
+ max-width: 400px;
+ box-sizing: border-box;
+ text-align: center;
+}
+
+.callout-parking-tip-bg {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
+ z-index: 0;
}
-.route-summary-callout-text {
+.callout-parking-tip-icon {
position: relative;
- z-index: 2;
- font-size: 20px;
- font-weight: 500;
- line-height: 39px;
- color: #FFFFFF;
- white-space: nowrap;
- text-align: center;
+ z-index: 1;
+ width: 24px;
+ height: 24px;
+ margin-top: 14px;
+ padding-right: 4px;
+ flex-shrink: 0;
+ display: block;
+}
+
+.callout-parking-tip-text {
+ position: relative;
display: flex;
+ align-items: center;
+ z-index: 1;
+ font-size: 22px;
+ font-weight: 500;
+ letter-spacing: normal;
+ color: #FFFFFF;
+ line-height: 48px;
+ white-space: nowrap;
+ min-width: 120px;
+ padding-right: 24px;
+ box-sizing: border-box;
}
.txt-center {
@@ -160,6 +387,7 @@
.car-time {
font-size: 20px;
+ font-family: @font-xpsans-bold;
letter-spacing: normal;
display: flex;
align-items: flex-end;
@@ -170,6 +398,7 @@
.car-callout-with-before {
flex-direction: column;
align-items: flex-start;
+ border-radius: 24px;
}
.car-callout-double-line {
@@ -182,20 +411,43 @@
}
.car-callout-double-line .car-time {
- line-height: 28px;
+ line-height: 32px;
}
.car-before-order-tip {
- margin-bottom: 4px;
- line-height: 28px;
+ font-size: 24px;
+ margin-bottom: 4px;
+ line-height: normal;
}
+.car-before-order-tip-text {
+ display: flex;
+ align-items: center;
+ font-size: 20px;
+ line-height: normal;
+ text-align: center;
+ letter-spacing: 0.04em;
+ color: #FFFFFF;
+ .car-before-order-tip-value {
+ margin: 0 2px;
+ font-weight: 500;
+ }
+ .car-before-order-tip-unit {
+ font-size: 20px;
+ line-height: normal;
+ text-align: center;
+ letter-spacing: 0.04em;
+ color: #FFFFFF;
+ }
+}
+
+
.car-time-num {
- font-size: 28px;
- line-height: 28rpx;
+ font-size: 24px;
+ line-height: 32px;
font-weight: 500;
letter-spacing: normal;
- color: #6D7FD9;
+ color: #FFFFFF;
margin-right: 2px;
}
@@ -236,6 +488,10 @@
overflow: visible; // 确保辐射动画不被裁剪
min-width: 700px;
height: 600px;
+ transform: translateY(-34px);
+ &.waiting-callout-publishing {
+ transform: translateY(16px);
+ }
}
.waiting-radar {
@@ -259,12 +515,14 @@
.waiting-callout-bg {
position: relative;
- height: 70px;
- min-width: 155px;
+ align-self: flex-start;
+ padding: 3px 40px 32px 20px;
+ box-sizing: border-box;
+ height: 80px;
+ min-width: 186px;
text-align: center;
display: flex;
align-items: flex-start;
- justify-content: center;
width: fit-content;
}
@@ -292,16 +550,29 @@
.waiting-callout-txt {
z-index: 200;
- border-radius: 16px;
- color: rgba(0, 0, 0, 0.9);
+ border-radius: 24px;
+ color: #FFFFFF;
font-size: 24px;
font-weight: 400;
- background: rgba(255, 255, 255, 1);
- padding: 8px 20px;
- line-height: 48px;
+ background: @black-text-main;
+ padding: 20px;
display: flex;
}
+.waiting-timedown-bg {
+ padding: 3px 0 0 20px;
+ min-width: 112px;
+ height: 80px;
+ .waiting-callout-bg-text {
+ padding: 0;
+ }
+}
+
+// 呼单等待态:黑色名称浮窗上移与计时气泡衔接(对齐 toolTip-txt margin-top:-32px)
+.waiting-callout-txt.waiting-callout-txt--overlap {
+ margin-top: -32px;
+}
+
.pr-6 {
padding-right: 6px;
}
diff --git a/src/robotaxi/pages/begin-route/index.tsx b/src/robotaxi/pages/begin-route/index.tsx
index 1b766ef..d9b9594 100644
--- a/src/robotaxi/pages/begin-route/index.tsx
+++ b/src/robotaxi/pages/begin-route/index.tsx
@@ -25,7 +25,6 @@
import useSmoothRouteMotion from './hooks/use-smooth-route-motion';
// Android 覆盖物状态管理
import useAndroidOverlays from './hooks/use-android-overlays';
-import useRouteSummaryAnchorLocation from './hooks/use-route-summary-anchor-location';
// 地图视口状态管理
import useMapViewport from './hooks/use-map-viewport';
import useActiveTripScale from './hooks/use-active-trip-scale';
@@ -60,7 +59,7 @@
import useBluetoothCarControl from './hooks/use-bluetooth-car-control';
import { toTripStatus, TripStatus } from '../../constants/trip-status';
import { MODIFY_DESTINATION_SOCKET_STATUS_KEY } from '../../constants/modify-destination';
-import { cancelOrder, selectNavigationRoute } from '../../api/begin-route';
+import { cancelOrder, selectNavigationRoute ,getUpdateAddressNum} from '../../api/begin-route';
import { getAddressByLocation, getDistance, reportEvent } from '../../../utils/Robotaxi-public-method';
import { CurrentUserLocation, RouteDistanceValue } from './hooks/use-route-manager.shared';
import { MODIFY_DESTINATION_SESSION_KEY } from '../../constants/modify-destination';
@@ -87,9 +86,10 @@
resetConfirmRideSelectionContext
} from '../../constants/confirm-ride-report';
import { withRobotaxiWhitelist } from '../../../utils/whitelist';
+import { withToast } from '../../components/toast/with-toast';
+import { showToast } from '../../components/toast';
import type { PickupExceptionInfo } from './pickup-exception';
-const ROUTE_SUMMARY_MARKER_ID = 999001;
const MODIFY_DESTINATION_PICKUP_STATUS_GUARD_MS = 5000;
const COUPON_POPUP_CLOSE_VIEWPORT_REFRESH_DELAY_MS = 320; // 优惠券弹窗退场动画结束后,再刷新完成态视口
const ROUTE_SELECTABLE_TRIP_STATUSES = [TripStatus.PublishRoute, TripStatus.VehicleApproaching] as const; // 允许展示并切换候选路线的行程状态
@@ -297,9 +297,8 @@
const [doorUnlockedInCurrentPage, setDoorUnlockedInCurrentPage] = useState(false); // 当前页面内是否已解锁过车门
const [finishCountdownEnabled, setFinishCountdownEnabled] = useState(false); // 完成倒计时是否启用
const customCalloutIconUrl = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/maker-tooltip-right.png'; // 自定义气泡图标URL
- const waitingCalloutBgUrl = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/maker-tooltip-bg.png'; // 等待气泡背景图URL
- const waitingRadarUrl = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/waiting-radar.webp'; // 等待雷达动画图URL
- const routeSummaryBgUrl = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/summary-icon.png'; // 路线概览气泡背景图URL
+ const waitingCalloutBgUrl = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/tooltip-bg-20260715.png'; // 等待气泡背景图URL
+ const waitingRadarUrl = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/waiting-radar-20260728.webp'; // 等待雷达动画图URL
const [isRouteSelectExpanded, setIsRouteSelectExpanded] = useState(false); // 路线选择展开状态
const [isPickupRouteChangeVisible, setIsPickupRouteChangeVisible] = useState(false); // 接驾态路线切换卡片展示状态
const [isConfirmingPickupRouteChange, setIsConfirmingPickupRouteChange] = useState(false); // 接驾态确认切换路线提交中
@@ -315,6 +314,8 @@
const [orderDetailViewportRefreshVersion, setOrderDetailViewportRefreshVersion] = useState(0); // 完成态视口刷新版本,关闭优惠券弹窗后触发 includePoints 纠偏
const [showModifyDestinationFailPopup, setShowModifyDestinationFailPopup] = useState(false); // 修改目的地失败弹窗
const [modifyDestinationFailMessage, setModifyDestinationFailMessage] = useState(''); // 修改目的地失败文案(默认空,避免闪文案)
+ const [entryRemainingModifyCount, setEntryRemainingModifyCount] = useState<number | null>(null); // 入口按钮用剩余修改次数,null 表示未就绪/未查询,-1 表示不限次
+ const [modifyCountRefreshVersion, setModifyCountRefreshVersion] = useState(0); // 剩余修改次数刷新版本,修改成功后自增触发重查
const [pickupExceptionInfo, setPickupExceptionInfo] = useState<PickupExceptionInfo | null>(null); // 接驾异常信息
const [showPickupExceptionPopup, setShowPickupExceptionPopup] = useState(false); // 接驾异常弹窗展示状态
const [showCreditPayPopup, setShowCreditPayPopup] = useState(false); // 免密支付开通弹窗
@@ -445,15 +446,13 @@
const vehicleMarkerId = !isAndroid && tripStatus === TripStatus.InService ? 30 : 3; // 车辆标记 ID,iOS 行驶中阶段使用不同 ID
const {
customCalloutIconSrc,
- waitingRadarSrc,
- routeSummaryBgSrc
+ waitingRadarSrc
} = usePreloadedMapAssets({
tripStatus,
assetUrls: {
customCalloutIconUrl,
waitingCalloutBgUrl,
- waitingRadarUrl,
- routeSummaryBgUrl
+ waitingRadarUrl
}
}); // 地图气泡资源预加载
const {
@@ -627,7 +626,7 @@
const isTripBeforePickupArrived = [TripStatus.Publishing, TripStatus.CallingCar, TripStatus.VehicleApproaching].includes(tripStatus); // 车辆未到达上车点前视为“行程未开始”
return {
...baseRule,
- showPickup: baseRule.showPickup, // 行程中(车辆已过上车点)即使在修改目的地态也不展示上车点,沿用基础规则
+ showPickup: isModifyDestinationReviewVisible ? true : baseRule.showPickup, // 修改目的地预览强制展示起点(上车点)
// 预览态强制展示下车点;确认修改后(本地覆盖态)回退至基础规则,避免接驾态错误渲染目的地,且不影响送驾态的正常显示
showDropoff: isModifyDestinationReviewVisible ? true : baseRule.showDropoff,
showCar: !isTripBeforePickupArrived && baseRule.showCar,
@@ -660,7 +659,6 @@
|| (tripStatus === TripStatus.VehicleApproaching
? isPickupRouteChangeVisible
: ROUTE_SELECTABLE_TRIP_STATUSES.includes(tripStatus)); // 接驾态仅在展示路线切换卡片时展示多条候选路线
- const shouldShowRouteSummaryBubble = allowSelectableRoutes; // 路线概览气泡与多路线展示条件保持一致
const showTrip1StyleEndpoint = isModifyDestinationReviewVisible || hasLocalDestinationOverride || isPickupRouteChangeMapMode;
const vehicleState = useVehicleTracking({
statusRule: effectiveStatusRule,
@@ -837,7 +835,7 @@
const handleOpenPickupRouteChange = useCallback(async () => {
const routeContext = resolvePickupRouteChangeRequestContext();
if (!routeContext) {
- Taro.showToast({
+ showToast({
title: '当前无法切换路线',
icon: 'none'
});
@@ -903,7 +901,7 @@
const pathFingerprint = selectedRoute?.pathFingerprint ?? selectedRoute?.route?.pathFingerprint;
if (!orderId || !snapshotId || !pathFingerprint) {
- Taro.showToast({
+ showToast({
title: '路线信息不完整,请刷新后重试',
icon: 'none'
});
@@ -926,7 +924,7 @@
routeRequestOrderRoute(orderId, tripStatus);
Taro.hideLoading();
setTimeout(() => {
- Taro.showToast({
+ showToast({
title: '切换成功,将按新路线行驶',
icon: 'none'
});
@@ -1008,45 +1006,6 @@
resetRouteRuntimeState();
};
const displayPolyline = polyline;
- const routeSummaryBaseLocation = useMemo(() => {
- if (!shouldShowRouteSummaryBubble || !routeDistance || routeDuration === null || !Array.isArray(routePoints) || routePoints.length === 0) {
- return null;
- }
- const midpoint = routePoints[Math.max(0, Math.floor(routePoints.length / 2))];
- if (!midpoint?.latitude || !midpoint?.longitude) {
- return null;
- }
- return {
- latitude: midpoint.latitude,
- longitude: midpoint.longitude
- };
- }, [routeDistance, routeDuration, routePoints, shouldShowRouteSummaryBubble]);
- const {
- routeSummaryAnchorLocation,
- handleRegionChange: handleRouteSummaryRegionChange
- } = useRouteSummaryAnchorLocation({
- enabled: shouldShowRouteSummaryBubble && !!routeDistance && routeDuration !== null,
- routeSummaryLocation: routeSummaryBaseLocation,
- routePoints
- });
- const routeSummaryMarker = useMemo(() => {
- const summaryLocation = routeSummaryAnchorLocation || routeSummaryBaseLocation;
- if (!summaryLocation?.latitude || !summaryLocation?.longitude) {
- return null;
- }
- return {
- id: ROUTE_SUMMARY_MARKER_ID,
- latitude: summaryLocation.latitude,
- longitude: summaryLocation.longitude,
- iconPath: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/marker-car-v2.png',
- width: 1,
- height: 1,
- alpha: 0,
- zIndex: 0,
- isRouteSummaryBubble: true,
- customCallout: { anchorY: 22, anchorX: 42, display: 'ALWAYS' }
- };
- }, [routeSummaryAnchorLocation, routeSummaryBaseLocation]);
const viewportState = useMapViewport({
tripStatus: mapPresentationTripStatus,
statusRule: effectiveStatusRule,
@@ -1130,8 +1089,6 @@
carCalloutVisible,
arriveCalloutStyle,
arriveCalloutVisible,
- routeSummaryCalloutStyle,
- routeSummaryCalloutVisible,
handleRegionChange: handleAndroidOverlayRegionChange
} = useAndroidOverlays({
shouldShowWaitingRadar,
@@ -1139,7 +1096,6 @@
tripStatus: mapPresentationTripStatus,
mapScale,
isModifyDestinationReviewVisible,
- routeSummaryLocation: routeSummaryAnchorLocation || routeSummaryBaseLocation,
startSite,
endSite: effectiveEndSite,
vehicleLocation,
@@ -1149,10 +1105,6 @@
longitude,
latitude
}); // Android 覆盖物状态编排
- const markersWithRouteSummary = useMemo(() => {
- if (!routeSummaryMarker) return markers;
- return [...markers, routeSummaryMarker];
- }, [markers, routeSummaryMarker]);
const hasValidBeforeUserEndSite = useMemo(() => (
Number.isFinite(Number(beforeUserEndSite?.latitude))
&& Number.isFinite(Number(beforeUserEndSite?.longitude))
@@ -1190,12 +1142,39 @@
return Number.isFinite(cachedStatus) ? cachedStatus : null;
}, [modifyDestinationSocketStatus, orderId]);
const isTripStatusAllowModifyEntry = [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService].includes(tripStatus);
+ // 查询本次订单的剩余修改目的地次数,用于入口按钮置灰判定(0 次时置灰)
+ useEffect(() => {
+ let isUnmounted = false;
+ if (!orderId || !isTripStatusAllowModifyEntry) {
+ setEntryRemainingModifyCount(null);
+ return () => {
+ isUnmounted = true;
+ };
+ }
+ (async () => {
+ try {
+ const response = await getUpdateAddressNum(String(orderId));
+ if (isUnmounted) return;
+ const nextRemainingCount = Number(response);
+ setEntryRemainingModifyCount(Number.isFinite(nextRemainingCount) ? nextRemainingCount : null);
+ } catch (error) {
+ if (isUnmounted) return;
+ setEntryRemainingModifyCount(null); // 查询失败不置灰,交由后续流程兜底
+ }
+ })();
+ return () => {
+ isUnmounted = true;
+ };
+ }, [orderId, isTripStatusAllowModifyEntry, modifyCountRefreshVersion]);
const modifyDestinationDisabledToastText = useMemo(() => {
if (!isTripStatusAllowModifyEntry) {
return '车辆临近终点,不支持修改目的地';
}
if (effectiveModifyDestinationSocketStatus === 0) {
return '即将到达终点,已无法修改目的地';
+ }
+ if (entryRemainingModifyCount === 0) {
+ return '当前剩余修改次数为 0';
}
if (tripStatus !== TripStatus.InService) {
return '';
@@ -1223,6 +1202,7 @@
isTripStatusAllowModifyEntry,
tripStatus,
effectiveModifyDestinationSocketStatus,
+ entryRemainingModifyCount,
modifyDistanceLimitMeters,
modifyDurationLimitMinutes,
orderRouteMetrics?.distanceMeters,
@@ -1760,8 +1740,8 @@
*/
const handleSignedCreditPayment = useCallback((value: SignedOrderPaymentValue, targetOrderId?: string) => {
if (Number(value?.payType) !== RobotaxiSignedPayType.WechatCredit) {
- Taro.showToast({ title: '暂不支持当前支付方式', icon: 'none' });
- return false;
+ showToast({ title: '暂不支持当前支付方式', icon: 'none' });
+ return;
}
pendingPublishingAfterSignedCreditPayRef.current = true;
@@ -1885,7 +1865,7 @@
const shareOrderId = getOrderId(orderId);
const onShareAppMessage = () => ({
- title: `已乘坐小鹏Robotaxi出发,${vehicleInfo?.plateNumber || ''},实时伴我出行~`,
+ title: `已乘坐小鹏Robotaxi出发,${!!vehicleInfo?.plateNumber ? `${vehicleInfo?.plateNumber },` : ''}实时伴我出行~`,
path: `/robotaxi/pages/share-trip/index?orderId=${shareOrderId}`
});
@@ -2022,7 +2002,6 @@
return;
}
activeTripScale.handleRegionChange(e);
- handleRouteSummaryRegionChange(e);
handleAndroidOverlayRegionChange(e);
// 用户手势导致的缩放/拖拽结束后,把原生地图的实际 scale 同步回 React 状态,
@@ -2110,7 +2089,7 @@
if (!modifyDestinationDisabledToastText) {
return;
}
- Taro.showToast({
+ showToast({
title: modifyDestinationDisabledToastText,
icon: 'none'
});
@@ -2210,6 +2189,7 @@
}
try {
await handleConfirmModifyDestination(selectedRoute);
+ setModifyCountRefreshVersion((version) => version + 1); // 修改成功后刷新剩余次数,及时置灰入口
} catch (error) {
openModifyDestinationFailPopup('修改目的地失败,请返回重试');
}
@@ -2386,7 +2366,7 @@
isAndroid,
tripStatus,
mapScale,
- markers: markersWithRouteSummary,
+ markers: markers,
isRouteSelectExpanded,
isPickupRouteChangeVisible,
isConfirmingPickupRouteChange,
@@ -2401,17 +2381,15 @@
carCalloutStyle,
arriveCalloutVisible,
arriveCalloutStyle,
- routeSummaryCalloutVisible,
- routeSummaryCalloutStyle,
arrivedTooltipReady,
routeDistance,
routeDuration,
hasBeforeUserEndSite: hasValidBeforeUserEndSite,
hasPreviousOrder,
customCalloutIconSrc,
- routeSummaryBgSrc,
showTrip1StyleEndpoint,
startSite,
+ elapsedSeconds,
endSite: effectiveEndSite,
callImmediately: handleCallImmediately,
isWaitingSignedCreditPayResult,
@@ -2546,7 +2524,7 @@
mapLongitude,
mapLatitude,
mapScale: mapScaleRef.current ?? mapScale,
- markers: markersWithRouteSummary,
+ markers: markers,
polyline: displayPolyline,
showLocation: !!longitude && showLocationFlag,
handleCalloutTap,
@@ -2556,21 +2534,21 @@
renderMapOverlays,
renderCarCalloutOverlay,
toBack: handleBack,
- backIcon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/back-icon.png',
+ backIcon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/back-icon-20260716.png',
onServiceEntry: handleServiceEntry,
onCouponEntry: handleCouponEntry,
onPersonalCenterEntry: handlePersonalCenterEntry,
- personalCenter: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/personal-center.png',
- coupon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/coupon.png',
- contactService: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/contact-service.png',
+ personalCenter: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/personal-center-20260716.png',
+ coupon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/coupon-20260723.png',
+ contactService: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/contact-service-20260716.png',
shouldShowOrigin,
originBoxHidden,
originOpacity,
toOrigin,
handleWalkNavigation,
showWalkNavigationButton,
- selectRouteIcon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/select-route.png',
- packUpIcon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/pack-up.png',
+ selectRouteIcon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/select-route-20260716-2.png',
+ packUpIcon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/pack-up-20260716.png',
onSelectRouteClick: () => {
if (isPickupRouteChangeMode) {
if (isPickupRouteChangeVisible) {
@@ -2584,8 +2562,8 @@
},
isRouteSelectExpanded: routeSelectExpandedForView,
canToggleRouteSelect,
- walkIcon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/walk-icon.png',
- originIcon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/origin-icon.png',
+ walkIcon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/walk-icon-20260716.png',
+ originIcon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/origin-icon-20260716.png',
renderTripCard,
showCustomerService,
handleCloseCustomerService,
@@ -2657,9 +2635,13 @@
bindSocketListeners={bindSocketListeners}
startSite={startSite}
endSite={endSite}
+ setCancelType={setCancelType}
+ setShowCancelPopup={setShowCancelPopup}
+ setShowModifyDestinationFailPopup={setShowModifyDestinationFailPopup}
+ setModifyDestinationFailMessage={setModifyDestinationFailMessage}
/> */}
</>
)
}
-export default withRobotaxiWhitelist(BeginRoute);
+export default withRobotaxiWhitelist(withToast(BeginRoute));
diff --git a/src/robotaxi/pages/coupon/index.less b/src/robotaxi/pages/coupon/index.less
index 6025ee7..9585346 100644
--- a/src/robotaxi/pages/coupon/index.less
+++ b/src/robotaxi/pages/coupon/index.less
@@ -6,23 +6,24 @@
flex-direction: column;
.tab-card {
- padding: 0 0 32px;
+ padding: 0 0 48px;
display: flex;
align-items: center;
justify-content: flex-start;
.tab-item {
- width: 118px;
+ flex: 1;
+ max-width: 118px;
height: 60px;
border-radius: 12px;
- background: #f5f6fa;
+ background: #F1F5FA;
margin-right: 20px;
font-family: PingFang SC;
font-size: 24px;
font-weight: normal;
line-height: 60px;
text-align: center;
- color: rgba(0, 0, 0, 0.6);
+ color: rgba(20, 30, 39, 0.4);
&:last-child {
margin-right: 0;
@@ -30,9 +31,10 @@
}
.active-status {
- background: #f5f6fa;
- color: rgba(0, 0, 0, 0.9);
- font-weight: 600;
+ background: rgba(223, 179, 105, 0.2);
+ color: #A68042;
+ font-weight: 500;
+ border-radius: 20px;
}
}
@@ -54,11 +56,11 @@
.coupon-item {
position: relative;
width: 100%;
- min-height: 184px;
- border-radius: 24px;
- background: url("https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/coupon-bg.png") center center / 100% 100%
+ min-height: 190px;
+ background: url("https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/coupon-item-bg-0713.png") center center / 100% 100%
no-repeat;
- padding: 28px 24px 28px 28px;
+ padding-left: 28px;
+ padding-right: 36px;
margin-bottom: 24px;
display: flex;
align-items: center;
@@ -74,7 +76,12 @@
}
.coupon-price-box {
- padding-right: 24px;
+ flex-shrink: 0;
+ width: 140px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
}
.coupon-price {
@@ -82,14 +89,13 @@
justify-content: center;
align-items: center;
flex-shrink: 0;
- width: 142px;
}
.price {
font-size: 48px;
line-height: 64px;
- font-weight: 600;
- color: #f58900;
+ font-weight: 500;
+ color: #A68042;
}
.unit {
@@ -97,8 +103,8 @@
font-size: 24px;
font-weight: 500;
line-height: 24px;
- color: #f58900;
- margin-left: 2px;
+ color: #A68042;
+ margin-left: 4px;
margin-top: 14px;
}
@@ -109,7 +115,7 @@
line-height: 36px;
letter-spacing: normal;
/* 功能色/提示色/提示色1 */
- color: #f58900;
+ color: #A68042;
text-align: center;
}
@@ -120,9 +126,9 @@
.name {
font-size: 32px;
- font-weight: 600;
+ font-weight: 500;
line-height: 48px;
- color: rgba(0, 0, 0, 0.9);
+ color: #A68042;
margin-bottom: 4px;
}
@@ -133,7 +139,7 @@
display: flex;
align-items: center;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
+ color: #A68042;
}
.more {
@@ -148,7 +154,7 @@
display: flex;
align-items: center;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.4);
+ color: rgba(20, 30, 39, 0.4);
}
.more-img {
@@ -161,26 +167,27 @@
.receive {
flex-shrink: 0;
- width: 128px;
+ min-width: 112px;
height: 56px;
+ padding: 0 20px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 110px;
- background: #f58900;
+ background: #DFB369;
font-size: 24px;
- font-weight: 500;
+ font-weight: normal;
line-height: 56px;
text-align: center;
color: #ffffff;
}
.status-img {
- width: 132px;
- height: 132px;
+ width: 120px;
+ height: 120px;
position: absolute;
- right: 16px;
- bottom: 16px;
+ right: 24px;
+ bottom: 24px;
}
}
}
diff --git a/src/robotaxi/pages/coupon/index.tsx b/src/robotaxi/pages/coupon/index.tsx
index a0c95dc..f1da2d3 100644
--- a/src/robotaxi/pages/coupon/index.tsx
+++ b/src/robotaxi/pages/coupon/index.tsx
@@ -10,6 +10,8 @@
CouponDetail
} from "../../../robotaxi/api/coupon";
import { withRobotaxiWhitelist } from "../../../utils/whitelist";
+import { showToast } from "../../components/toast";
+import { withToast } from "../../components/toast/with-toast";
function CouponPage() {
const [couponStatus] = useState([
@@ -144,7 +146,7 @@
setVisible(true);
} catch (error) {
Taro.hideLoading();
- Taro.showToast({ title: "获取详情失败", icon: "none" });
+ showToast({ title: "获取详情失败", icon: "none" });
}
};
@@ -162,12 +164,12 @@
e.stopPropagation();
try {
await receiveCoupon({ couponId: item.couponId });
- Taro.showToast({ title: "领取成功", icon: "success" });
+ showToast({ title: "领取成功", icon: "success" });
// 领取成功后刷新列表
getList(1, activeStatus, true);
setPageNum(1);
} catch (error) {
- Taro.showToast({ title: "领取失败", icon: "none" });
+ showToast({ title: "领取失败", icon: "none" });
}
};
@@ -208,7 +210,7 @@
<View
className="coupon-item"
key={item.couponId || index}
- style={{ opacity: ![0, 1].includes(item.status) ? 0.5 : 1 }}
+ style={{ opacity: ![0, 1].includes(item.status) ? 0.6 : 1 }}
>
<View className="coupon-content">
<View className="coupon-price-box">
@@ -227,7 +229,7 @@
</Text>
<View className="more" onClick={() => openPopup(item)}>
<Text className="more-detail">查看详情</Text>
- <Image className="more-img" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/coupon-arrow.png"></Image>
+ <Image className="more-img" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/arrow-down-gray.svg"></Image>
</View>
</View>
</View>
@@ -243,19 +245,18 @@
<View
onClick={() => toHome(item.couponId)}
className="receive"
- style={{ backgroundColor: "#F58900" }}
>
去使用
</View>
) : item.status === 2 ? (
<Image
className="status-img"
- src={'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/coupon-used.png'}
+ src={'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/coupon-item-tag-used.svg'}
/>
) : item.status === 3 ? (
<Image
className="status-img"
- src={'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/coupon-lost-efficacy.png'}
+ src={'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/coupon-item-tag-expired.svg'}
/>
) : (
""
@@ -315,4 +316,4 @@
);
}
-export default withRobotaxiWhitelist(CouponPage);
+export default withRobotaxiWhitelist(withToast(CouponPage));
diff --git a/src/robotaxi/pages/demo/index.tsx b/src/robotaxi/pages/demo/index.tsx
index 57ea5f5..2bae60a 100644
--- a/src/robotaxi/pages/demo/index.tsx
+++ b/src/robotaxi/pages/demo/index.tsx
@@ -7,6 +7,8 @@
import { checkOtp } from '../../../utils/tools';
import { withRobotaxiWhitelist } from '../../../utils/whitelist';
+// 通过 withToast HOC 在页面挂载一次 Toast 单例容器(推荐的接入方式)
+import { withToast } from '../../components/toast/with-toast';
// @ts-ignore
@createPage()
@@ -43,4 +45,4 @@
}
}
-export default withRobotaxiWhitelist(ToolboxBase as any);
+export default withRobotaxiWhitelist(withToast(ToolboxBase as any) as any);
diff --git a/src/robotaxi/pages/expense-description/index.tsx b/src/robotaxi/pages/expense-description/index.tsx
index 5ae5afb..37af3a5 100644
--- a/src/robotaxi/pages/expense-description/index.tsx
+++ b/src/robotaxi/pages/expense-description/index.tsx
@@ -8,6 +8,8 @@
} from '../../../utils/Robotaxi-public-method';
import EvaluatePopup from '../begin-route/components/evaluate-popup';
import { withRobotaxiWhitelist } from '../../../utils/whitelist';
+import { showToast } from '../../components/toast';
+import { withToast } from '../../components/toast/with-toast';
import './index.less';
function OrderPage() {
@@ -36,7 +38,7 @@
setFareData(res);
} catch (error) {
console.error('获取费用明细失败', error);
- Taro.showToast({ title: '获取费用明细失败', icon: 'none' });
+ showToast({ title: '获取费用明细失败', icon: 'none' });
}
};
@@ -274,4 +276,4 @@
)
}
-export default withRobotaxiWhitelist(OrderPage);
+export default withRobotaxiWhitelist(withToast(OrderPage));
diff --git a/src/robotaxi/pages/expense-detail/index.less b/src/robotaxi/pages/expense-detail/index.less
index 6f9dc2a..1827e48 100644
--- a/src/robotaxi/pages/expense-detail/index.less
+++ b/src/robotaxi/pages/expense-detail/index.less
@@ -1,3 +1,5 @@
+@import '../../style/ui-variables.less';
+
.expense-detail-page {
min-height: 100vh;
display: flex;
@@ -36,7 +38,7 @@
.start-icon {
width: 8px;
height: 8px;
- background: #768AFF;
+ background: @gold-light;
border-radius: 50%;
flex-shrink: 0;
}
@@ -53,7 +55,7 @@
width: 8px;
height: 8px;
opacity: 1;
- background: #65C3FC;
+ background: @black-text-main;
z-index: 0;
border-radius: 50%;
flex-shrink: 0;
diff --git a/src/robotaxi/pages/expense-detail/index.tsx b/src/robotaxi/pages/expense-detail/index.tsx
index a1d3443..25583f2 100644
--- a/src/robotaxi/pages/expense-detail/index.tsx
+++ b/src/robotaxi/pages/expense-detail/index.tsx
@@ -7,6 +7,8 @@
formatTripDurationFromSeconds
} from '../../../utils/Robotaxi-public-method';
import { withRobotaxiWhitelist } from '../../../utils/whitelist';
+import { showToast } from '../../components/toast';
+import { withToast } from '../../components/toast/with-toast';
/** 城市缓存 key */
const SELECTED_CITY_STORAGE_KEY = 'selectedCity';
@@ -56,7 +58,7 @@
const cityID = getSelectedCityId();
if (!cityID) {
- Taro.showToast({
+ showToast({
title: RESELECT_CITY_TOAST_TEXT,
icon: 'none'
});
@@ -189,4 +191,4 @@
)
}
-export default withRobotaxiWhitelist(OrderPage);
+export default withRobotaxiWhitelist(withToast(OrderPage));
diff --git a/src/robotaxi/pages/favorite-sites/index.tsx b/src/robotaxi/pages/favorite-sites/index.tsx
index a5920f2..5253a19 100644
--- a/src/robotaxi/pages/favorite-sites/index.tsx
+++ b/src/robotaxi/pages/favorite-sites/index.tsx
@@ -1,4 +1,4 @@
-import Taro from '@tarojs/taro';
+import Toast, { showToast } from '../../components/toast';
import { View, Image, ScrollView } from '@tarojs/components';
import { useEffect, useState } from 'react';
import './index.less';
@@ -55,10 +55,10 @@
await collectPoint(payload);
setList(prev => prev.filter(f => (f.id || f.pointId || f.stationId) !== (item.id || item.pointId || item.stationId)));
- Taro.showToast({ title: '已取消收藏', icon: 'none' });
+ showToast({ title: '已取消收藏', icon: 'none' });
} catch (e) {
console.log('取消收藏失败', e);
- Taro.showToast({ title: '取消收藏失败', icon: 'none' });
+ showToast({ title: '取消收藏失败', icon: 'none' });
}
};
@@ -97,6 +97,7 @@
)}
</View>
</ScrollView>
+ <Toast />
</View>
);
}
diff --git a/src/robotaxi/pages/feedback/hooks/use-media-upload.ts b/src/robotaxi/pages/feedback/hooks/use-media-upload.ts
new file mode 100644
index 0000000..afbba91
--- /dev/null
+++ b/src/robotaxi/pages/feedback/hooks/use-media-upload.ts
@@ -0,0 +1,269 @@
+import Taro from '@tarojs/taro';
+import { useCallback, useMemo, useState } from 'react';
+import { getStorage } from '../../../../utils/tools';
+
+/**
+ * 上传成功后的服务端文件信息。
+ */
+export type UploadedServerFile = {
+ id: string;
+ name: string;
+ url: string;
+};
+
+/**
+ * 媒体类型:图片或视频。
+ */
+export type MediaType = 'image' | 'video';
+
+/**
+ * 媒体上传项状态。
+ */
+export type MediaUploadItem = {
+ id: string;
+ localUrl: string;
+ mediaType: MediaType;
+ server: UploadedServerFile | null;
+ status: 'uploading' | 'success' | 'failed';
+};
+
+/**
+ * 图片/视频混合上传 Hook 配置。
+ */
+type UseMediaUploadOptions = {
+ maxCount: number;
+ uploadUrl: string;
+ imageSizeLimitBytes?: number;
+ videoSizeLimitBytes?: number;
+};
+
+const DEFAULT_IMAGE_SIZE_LIMIT_BYTES = 10 * 1024 * 1024; // 图片 10MB
+const DEFAULT_VIDEO_SIZE_LIMIT_BYTES = 20 * 1024 * 1024; // 视频 20MB
+const IMAGE_COMPRESS_QUALITIES = [80, 60, 40];
+
+/**
+ * 图片/视频混合上传:封装选择、图片压缩、视频大小校验、上传、重试、删除与重置。
+ */
+export default function useMediaUpload(options: UseMediaUploadOptions) {
+ const [items, setItems] = useState<MediaUploadItem[]>([]);
+
+ /**
+ * 更新指定媒体项。
+ */
+ const updateItemById = useCallback((itemId: string, updater: (item: MediaUploadItem) => MediaUploadItem) => {
+ setItems((prev) => prev.map((item) => (item.id === itemId ? updater(item) : item)));
+ }, []);
+
+ /**
+ * 获取文件大小。
+ */
+ const getFileSize = useCallback(async (filePath: string) => {
+ const fs = Taro.getFileSystemManager();
+ const info: any = await new Promise((resolve, reject) => {
+ fs.getFileInfo({
+ filePath,
+ success: resolve,
+ fail: reject
+ });
+ });
+ return Number(info?.size || 0);
+ }, []);
+
+ /**
+ * 压缩图片到体积限制内。
+ */
+ const ensureImageSizeUnderLimit = useCallback(async (filePath: string) => {
+ // iOS 端 Taro.compressImage 仅支持 JPG/JPEG,其他格式直接使用原图上传。
+ if (!/\.(?:jpe?g)(?:[?#].*)?$/i.test(filePath)) {
+ return filePath;
+ }
+ const sizeLimitBytes = options.imageSizeLimitBytes || DEFAULT_IMAGE_SIZE_LIMIT_BYTES;
+ try {
+ const originalFilePath = filePath;
+ let lastCompressedFilePath = filePath;
+ const currentSize = await getFileSize(originalFilePath);
+ if (currentSize <= sizeLimitBytes) {
+ return originalFilePath;
+ }
+ for (const quality of IMAGE_COMPRESS_QUALITIES) {
+ try {
+ // 每次都基于原图压缩,避免 80 -> 60 -> 40 的代际压缩导致画质累积劣化。
+ const compressResult: any = await Taro.compressImage({
+ src: originalFilePath,
+ quality
+ });
+ const compressedPath = compressResult?.tempFilePath;
+ if (!compressedPath) {
+ continue;
+ }
+ lastCompressedFilePath = compressedPath;
+ const compressedSize = await getFileSize(compressedPath);
+ if (compressedSize <= sizeLimitBytes) {
+ return compressedPath;
+ }
+ } catch (error) {
+ // 单次压缩失败时继续基于原图尝试更低质量。
+ }
+ }
+ return lastCompressedFilePath;
+ } catch (error) {
+ return filePath;
+ }
+ }, [getFileSize, options.imageSizeLimitBytes]);
+
+ /**
+ * 上传单个媒体文件。
+ */
+ const uploadSingle = useCallback(async (itemId: string, filePath: string, mediaType: MediaType) => {
+ updateItemById(itemId, (item) => ({
+ ...item,
+ status: 'uploading'
+ }));
+ try {
+ const finalFilePath = mediaType === 'image' ? await ensureImageSizeUnderLimit(filePath) : filePath;
+ const otpToken = await getStorage('otp').catch(() => '');
+ const uploadResult = await Taro.uploadFile({
+ url: options.uploadUrl,
+ filePath: finalFilePath,
+ name: 'file',
+ header: otpToken ? { 'Xp-Otp-Token': String(otpToken) } : undefined
+ });
+ let responseData: any = uploadResult.data;
+ try {
+ if (typeof responseData === 'string') {
+ responseData = JSON.parse(responseData);
+ }
+ } catch (error) {
+ responseData = null;
+ }
+ console.log('[feedback upload] raw response:', uploadResult);
+ console.log('[feedback upload] parsed response data:', responseData);
+ if (uploadResult.statusCode !== 200 || responseData?.code !== 200 || !responseData?.data) {
+ throw new Error(responseData?.msg || '上传失败');
+ }
+ const payload = responseData?.data;
+ const serverFile = {
+ id: String(payload?.id || ''),
+ name: String(payload?.name || ''),
+ url: String(payload?.url || '')
+ };
+ console.log('[feedback upload] normalized server file:', serverFile);
+ if (!serverFile.id || !serverFile.url) {
+ throw new Error('invalid upload response');
+ }
+ updateItemById(itemId, (item) => ({
+ ...item,
+ server: serverFile,
+ status: 'success'
+ }));
+ } catch (error: any) {
+ console.log('[feedback upload] upload failed:', error);
+ updateItemById(itemId, (item) => ({
+ ...item,
+ status: 'failed'
+ }));
+ Taro.showToast({
+ title: error?.message || '上传失败,请重试',
+ icon: 'none'
+ });
+ }
+ }, [ensureImageSizeUnderLimit, options.uploadUrl, updateItemById]);
+
+ /**
+ * 生成本地媒体项。
+ */
+ const createMediaItem = useCallback((localUrl: string, mediaType: MediaType, index: number): MediaUploadItem => ({
+ id: `${Date.now()}-${index}-${localUrl}`,
+ localUrl,
+ mediaType,
+ server: null,
+ status: 'uploading'
+ }), []);
+
+ /**
+ * 选择图片/视频并立即上传,选择前校验数量与视频体积上限。
+ */
+ const chooseAndUpload = useCallback(async () => {
+ const remainCount = Math.max(0, options.maxCount - items.length);
+ if (remainCount <= 0) {
+ Taro.showToast({
+ title: `最多可上传${options.maxCount}个图片或视频`,
+ icon: 'none'
+ });
+ return;
+ }
+ try {
+ const chooseResult: any = await Taro.chooseMedia({
+ count: remainCount,
+ mediaType: ['image', 'video'],
+ sourceType: ['album', 'camera']
+ });
+ const files: any[] = chooseResult?.tempFiles || [];
+ if (!files.length) {
+ return;
+ }
+ const videoSizeLimitBytes = options.videoSizeLimitBytes || DEFAULT_VIDEO_SIZE_LIMIT_BYTES;
+ const acceptedFiles: { path: string; mediaType: MediaType }[] = [];
+ for (const file of files) {
+ const mediaType: MediaType = file.fileType === 'video' ? 'video' : 'image';
+ if (mediaType === 'video' && Number(file.size || 0) > videoSizeLimitBytes) {
+ Taro.showToast({
+ title: '视频不能超过20MB',
+ icon: 'none'
+ });
+ continue;
+ }
+ acceptedFiles.push({ path: file.tempFilePath, mediaType });
+ }
+ if (!acceptedFiles.length) {
+ return;
+ }
+ const nextItems = acceptedFiles.map((file, index) => createMediaItem(file.path, file.mediaType, index));
+ setItems((prev) => [...prev, ...nextItems].slice(0, options.maxCount));
+ nextItems.forEach((item) => {
+ void uploadSingle(item.id, item.localUrl, item.mediaType);
+ });
+ } catch (error) {
+ console.log('拉起上传图片/视频', error);
+ }
+ }, [createMediaItem, items.length, options.maxCount, options.videoSizeLimitBytes, uploadSingle]);
+
+ /**
+ * 重试上传失败媒体。
+ */
+ const retryUpload = useCallback((index: number) => {
+ const mediaItem = items[index];
+ if (!mediaItem) {
+ return;
+ }
+ void uploadSingle(mediaItem.id, mediaItem.localUrl, mediaItem.mediaType);
+ }, [items, uploadSingle]);
+
+ /**
+ * 删除媒体。
+ */
+ const removeItem = useCallback((index: number) => {
+ setItems((prev) => prev.filter((_, currentIndex) => currentIndex !== index));
+ }, []);
+
+ /**
+ * 重置全部媒体状态。
+ */
+ const resetItems = useCallback(() => {
+ setItems([]);
+ }, []);
+
+ /**
+ * 当前是否存在媒体上传中。
+ */
+ const uploading = useMemo(() => items.some((item) => item.status === 'uploading'), [items]);
+
+ return {
+ items,
+ uploading,
+ chooseAndUpload,
+ retryUpload,
+ removeItem,
+ resetItems
+ };
+}
diff --git a/src/robotaxi/pages/feedback/index.config.ts b/src/robotaxi/pages/feedback/index.config.ts
new file mode 100644
index 0000000..06a7fda
--- /dev/null
+++ b/src/robotaxi/pages/feedback/index.config.ts
@@ -0,0 +1,4 @@
+export default {
+ navigationBarTitleText: '意见反馈',
+ navigationStyle: 'custom'
+};
diff --git a/src/robotaxi/pages/feedback/index.less b/src/robotaxi/pages/feedback/index.less
new file mode 100644
index 0000000..165e784
--- /dev/null
+++ b/src/robotaxi/pages/feedback/index.less
@@ -0,0 +1,382 @@
+@import "../../style/ui-variables.less";
+
+.feedback-page {
+ position: relative;
+ min-height: 100vh;
+ box-sizing: border-box;
+ background: #FFFFFF;
+
+ .custom-navbar .app-inaver .left {
+ width: 88px;
+ justify-content: flex-start;
+
+ .back-wrap {
+ padding-right: 0;
+ padding-left: 32px;
+ display: flex;
+ align-items: center;
+
+ .ic-left.pure {
+ margin-left: 0;
+ margin-top: -2px;
+ }
+ }
+ }
+
+ .custom-navbar .app-inaver .center {
+ font-family: MiSans;
+ font-size: 32px;
+ font-weight: 500;
+ line-height: 48px;
+ letter-spacing: normal;
+ color: #141E27;
+
+ &.ioscenter {
+ margin-left: 114px;
+ }
+ }
+}
+
+.feedback-toast {
+ position: fixed;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 9999;
+ height: 112px;
+ display: flex;
+ align-items: center;
+ padding: 32px 64px;
+ gap: 16px;
+ border-radius: 64px;
+ background: #2A2D30;
+ box-shadow: 0px 16px 32px 0px rgba(23, 31, 38, 0.1);
+ max-width: calc(100vw - 48px);
+
+ &-icon {
+ width: 40px;
+ height: 40px;
+ flex-shrink: 0;
+ }
+
+ &-text {
+ font-family: PingFang SC;
+ font-size: 32px;
+ font-weight: normal;
+ line-height: normal;
+ letter-spacing: normal;
+ color: #FFFFFF;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+}
+
+.feedback-content {
+ min-height: calc(100vh - 176px);
+ padding: 0 44px;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+}
+
+.feedback-type {
+ &-title {
+ font-size: 28px;
+ line-height: 40px;
+ font-weight: 500;
+ margin-top: 50px;
+ color: #141E27;
+
+ .required-mark {
+ color: #F53F3F;
+ margin-left: 4px;
+ }
+ }
+
+ &-list {
+ margin-top: 28px;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 20px 20px;
+ }
+
+ &-item {
+ height: 72px;
+ padding: 0 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 20px;
+ background: #F1F5FA;
+ box-sizing: border-box;
+ min-width: 0;
+
+ &-text {
+ display: block;
+ max-width: 100%;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ font-size: 26px;
+ line-height: 36px;
+ color: rgba(20, 30, 39, 0.5);
+ }
+
+ &.active {
+ background: rgba(223, 179, 105, 0.2);
+
+ .feedback-type-item-text {
+ color: #B38F53;
+ }
+ }
+ }
+}
+
+.feedback-desc {
+ margin-top: 48px;
+
+ &-title {
+ font-size: 28px;
+ line-height: 40px;
+ font-weight: 500;
+ color: #141E27;
+
+ .required-mark {
+ color: #F53F3F;
+ margin-left: 4px;
+ }
+ }
+
+ &-box {
+ position: relative;
+ margin-top: 24px;
+ }
+
+ &-textarea {
+ width: 100%;
+ box-sizing: border-box;
+ height: 196px;
+ padding: 24px;
+ padding-bottom: 48px;
+ border-radius: 32px;
+ background: #F1F5FA;
+ font-size: 28px;
+ line-height: 40px;
+ color: #141E27;
+ }
+
+ &-placeholder {
+ font-size: 28px;
+ line-height: 40px;
+ color: rgba(20, 30, 39, 0.3);
+ }
+
+ &-count {
+ position: absolute;
+ right: 24px;
+ bottom: 16px;
+ font-size: 22px;
+ line-height: 32px;
+ color: rgba(20, 30, 39, 0.3);
+ }
+}
+
+.feedback-media {
+ margin-top: 24px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 16px;
+
+ &-item {
+ position: relative;
+ width: 208px;
+ height: 208px;
+ border-radius: 32px;
+ overflow: hidden;
+ background: #F1F5FA;
+ }
+
+ &-thumb {
+ width: 208px;
+ height: 208px;
+ object-fit: cover;
+ }
+
+ &-mask {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ background: rgba(0, 0, 0, 0.4);
+ color: #FFFFFF;
+
+ &-text {
+ font-size: 22px;
+ color: #FFFFFF;
+ }
+ }
+
+ &-spinner {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ border: 3px solid rgba(255, 255, 255, 0.4);
+ border-top-color: #FFFFFF;
+ animation: feedback-media-spin 700ms linear infinite;
+ }
+
+ &-delete {
+ position: absolute;
+ top: 4px;
+ right: 4px;
+ width: 40px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ background: rgba(0, 0, 0, 0.4);
+
+ &-text {
+ font-size: 24px;
+ line-height: 24px;
+ color: #FFFFFF;
+ }
+ }
+
+ &-upload {
+ width: 208px;
+ height: 208px;
+ border-radius: 32px;
+ background: #F1F5FA;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+
+ &-icon {
+ width: 48px;
+ height: 48px;
+ }
+
+ &-text {
+ font-size: 22px;
+ line-height: 32px;
+ color: rgba(20, 30, 39, 0.4);
+ }
+ }
+}
+
+.feedback-tip {
+ text-align: center;
+ font-size: 24px;
+ line-height: 34px;
+ color: rgba(20, 30, 39, 0.4);
+}
+
+.feedback-submit-btn {
+ margin-top: 24px;
+ height: 104px;
+ border-radius: 1000px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 32px;
+ font-weight: 500;
+ color: rgba(255, 255, 255, 0.8);
+ background: rgba(20, 30, 39, 0.2);
+
+ &.active {
+ background: #141E27;
+ color: #FFFFFF;
+ }
+}
+
+.feedback-footer {
+ margin-top: auto;
+ padding-top: 48px;
+ padding-bottom: calc(48px + env(safe-area-inset-bottom));
+}
+
+.feedback-success {
+ min-height: calc(100vh - 176px);
+ padding: 237px 44px calc(48px + env(safe-area-inset-bottom));
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-start;
+
+ &-main {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ &-icon {
+ width: 114px;
+ height: 114px;
+ }
+
+ &-title {
+ margin-top: 32px;
+ font-size: 44px;
+ line-height: 60px;
+ font-weight: 500;
+ color: #141E27;
+ }
+
+ &-desc {
+ margin-top: 8px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ font-family: PingFang SC;
+ font-size: 28px;
+ font-weight: normal;
+ line-height: 44px;
+ text-align: center;
+ letter-spacing: normal;
+ color: rgba(20, 30, 39, 0.5);
+ }
+
+ &-btn {
+ width: 460px;
+ height: 98px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ padding: 25px 8px;
+ z-index: 1;
+ border-radius: 200px;
+ box-sizing: border-box;
+ border: 4px solid #141E27;
+ background: transparent;
+ font-family: PingFang SC;
+ font-size: 32px;
+ font-weight: 500;
+ line-height: 48px;
+ text-align: center;
+ letter-spacing: normal;
+ color: #141E27;
+ margin-top: 112px;
+ }
+}
+
+@keyframes feedback-media-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
diff --git a/src/robotaxi/pages/feedback/index.tsx b/src/robotaxi/pages/feedback/index.tsx
new file mode 100644
index 0000000..6bc49b8
--- /dev/null
+++ b/src/robotaxi/pages/feedback/index.tsx
@@ -0,0 +1,450 @@
+import Taro from '@tarojs/taro';
+import { View, Text, Image, Textarea, Video } from '@tarojs/components';
+import { useEffect, useMemo, useRef, useState } from 'react';
+import CustomNavbar from '../../../components/custom-navbar';
+import { withRobotaxiWhitelist } from '../../../utils/whitelist';
+import { getFeedbackTypeList, submitComplaint } from '../../api/feedback';
+import { _ROBOTAXI_HOST } from '../../../constants/api';
+import useMediaUpload from './hooks/use-media-upload';
+import './index.less';
+
+const FEEDBACK_CONTENT_MAX_LENGTH = 500; // 问题描述最大字数
+const FEEDBACK_MEDIA_MAX_COUNT = 3; // 图片/视频合计最多数量
+const FEEDBACK_TOAST_DURATION = 2000;
+const FEEDBACK_TOAST_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/error-circle-filled-20260727.png';
+const FEEDBACK_TOAST_ICON_CACHE_KEY = 'robotaxiFeedbackToastIconPath';
+
+/** 反馈渠道:1=车内二维码,2=小程序"我的"入口。 */
+const FEEDBACK_CHANNEL_SCAN = 1;
+const FEEDBACK_CHANNEL_MINE = 2;
+
+/**
+ * 归一化问题类型 ID,过滤空值。
+ */
+const normalizeFeedbackTypeId = (typeId: unknown) => {
+ if (typeId === null || typeId === undefined || typeId === '') {
+ return '';
+ }
+ return String(typeId);
+};
+
+/**
+ * 从扫码场景值中解析车辆 VIN,兼容 vin=xxx、VIN=xxx、querystring 以及纯 VIN 文本。
+ */
+const parseVinFromScene = (scene: unknown) => {
+ const decodedScene = decodeURIComponent(String(scene || ''));
+ if (!decodedScene) {
+ return '';
+ }
+ const matched = decodedScene.match(/(?:^|[?&])vin=([^&]+)/i);
+ if (matched?.[1]) {
+ return matched[1];
+ }
+ // 部分扫码场景会直接透传 VIN。
+ return decodedScene.trim();
+}
+
+function FeedbackPage() {
+ const router = Taro.useRouter();
+ const isScanChannel = String(router.params?.source || '') !== 'mine'; // 除“我的”入口外均按扫码渠道处理
+ const vin = useMemo(() => parseVinFromScene(router.params?.scene), [router.params?.scene]);
+ const feedbackChannel = isScanChannel ? FEEDBACK_CHANNEL_SCAN : FEEDBACK_CHANNEL_MINE;
+
+ const [typeOptions, setTypeOptions] = useState<any[]>([]); // 问题类型选项
+ const [selectedTypeIds, setSelectedTypeIds] = useState<string[]>([]); // 已选问题类型
+ const [feedbackContent, setFeedbackContent] = useState(''); // 问题描述文案
+ const [isSubmitSuccess, setIsSubmitSuccess] = useState(false); // 是否展示提交成功态
+ const [toastMessage, setToastMessage] = useState('');
+ const [feedbackToastIconSrc, setFeedbackToastIconSrc] = useState(() => {
+ const cachedIconPath = Taro.getStorageSync(FEEDBACK_TOAST_ICON_CACHE_KEY);
+ return typeof cachedIconPath === 'string' && cachedIconPath ? cachedIconPath : FEEDBACK_TOAST_ICON_URL;
+ });
+ const isSubmittingRef = useRef(false); // 同步提交锁,防止重复点击
+ const toastTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+
+ const {
+ items: mediaItems,
+ uploading,
+ chooseAndUpload,
+ retryUpload,
+ removeItem
+ } = useMediaUpload({
+ maxCount: FEEDBACK_MEDIA_MAX_COUNT,
+ uploadUrl: _ROBOTAXI_HOST + '/operation/base/v1/mini-program/oss/upload'
+ }); // 图片/视频上传状态与操作
+
+ const hasFeedbackContent = !!feedbackContent.trim();
+ const isVinMissing = isScanChannel && !vin;
+ const canSubmit = selectedTypeIds.length > 0;
+ const hasUnsavedFeedback = selectedTypeIds.length > 0 || hasFeedbackContent || mediaItems.length > 0;
+
+ const showFeedbackToast = (message: string) => {
+ if (!message) {
+ return;
+ }
+ if (toastTimerRef.current) {
+ clearTimeout(toastTimerRef.current);
+ }
+ setToastMessage(message);
+ toastTimerRef.current = setTimeout(() => {
+ setToastMessage('');
+ toastTimerRef.current = null;
+ }, FEEDBACK_TOAST_DURATION);
+ };
+
+ const handlePreviewImage = (currentUrl: string) => {
+ if (!currentUrl) {
+ return;
+ }
+ const imageUrls = mediaItems
+ .filter((item) => item.mediaType !== 'video')
+ .map((item) => item.server?.url || item.localUrl)
+ .filter((url): url is string => !!url);
+ if (imageUrls.length === 0) {
+ return;
+ }
+ Taro.previewImage({
+ current: currentUrl,
+ urls: imageUrls
+ });
+ };
+
+ /**
+ * 切换问题类型多选状态。
+ */
+ const toggleFeedbackType = (typeId: unknown) => {
+ const normalizedTypeId = normalizeFeedbackTypeId(typeId);
+ if (!normalizedTypeId) {
+ return;
+ }
+ setSelectedTypeIds((prev) => (
+ prev.includes(normalizedTypeId)
+ ? prev.filter((id) => id !== normalizedTypeId)
+ : [...prev, normalizedTypeId]
+ ));
+ };
+
+ /**
+ * 提交意见反馈。
+ */
+ const handleSubmit = async () => {
+ if (isSubmittingRef.current) {
+ return;
+ }
+ if (selectedTypeIds.length === 0) {
+ showFeedbackToast('请选择问题类型');
+ return;
+ }
+ if (!hasFeedbackContent) {
+ showFeedbackToast('请输入问题描述');
+ return;
+ }
+ if (isVinMissing) {
+ showFeedbackToast('二维码信息无效,请重新扫码');
+ return;
+ }
+ if (uploading) {
+ showFeedbackToast('图片/视频上传中,请稍后提交');
+ return;
+ }
+
+ isSubmittingRef.current = true;
+ Taro.showLoading({ title: '提交中...', mask: true });
+ try {
+ const networkTypeRes = await Taro.getNetworkType();
+ if (networkTypeRes.networkType === 'none') {
+ showFeedbackToast('网络不可用');
+ return;
+ }
+
+ const uploadedMediaItems = mediaItems.filter((item) => item.status === 'success' && item.server?.id && item.server?.url);
+ const feedbackPictures = JSON.stringify(
+ uploadedMediaItems.map((item) => ({
+ id: String(item.server?.id || ''),
+ name: String(item.server?.name || ''),
+ url: String(item.server?.url || '')
+ }))
+ );
+
+ const payload: any = {
+ feedbackTypeId: selectedTypeIds.join(','),
+ feedbackContent: feedbackContent.trim(),
+ feedbackChannel
+ };
+ if (uploadedMediaItems.length > 0) {
+ payload.feedbackPictures = feedbackPictures;
+ }
+ if (isScanChannel) {
+ payload.vin = vin;
+ }
+
+ console.log('[feedback submit] media items:', mediaItems);
+ console.log('[feedback submit] feedbackPictures:', feedbackPictures);
+ console.log('[feedback submit] payload:', payload);
+
+ const res = await submitComplaint(payload);
+ console.log('[feedback submit] success response:', res);
+ setIsSubmitSuccess(true);
+ } catch (error: any) {
+ console.log('[feedback submit] error response:', error);
+ const errorTitle = error?.msg || (error?.code ? `提交失败(${error.code})` : '提交失败,请稍后重试');
+ showFeedbackToast(errorTitle);
+ } finally {
+ isSubmittingRef.current = false;
+ Taro.hideLoading();
+ }
+ };
+
+ /**
+ * 成功态确认按钮,沿用原有提交成功后的跳转逻辑。
+ */
+ const handleSuccessConfirm = () => {
+ if (isScanChannel) {
+ Taro.reLaunch({ url: '/robotaxi/pages/begin-route/index' });
+ return;
+ }
+ Taro.navigateBack({
+ fail: () => Taro.navigateTo({ url: '/robotaxi/pages/personal-center/index' })
+ });
+ };
+
+ const navigateBack = () => {
+ const pages = Taro.getCurrentPages();
+ if (isScanChannel && pages.length <= 1) {
+ Taro.reLaunch({ url: '/robotaxi/pages/begin-route/index' });
+ return;
+ }
+ Taro.navigateBack({
+ fail: () => Taro.reLaunch({ url: '/robotaxi/pages/begin-route/index' })
+ });
+ };
+
+ /**
+ * 导航栏返回按钮点击。存在未提交内容时先二次确认,避免误操作丢失反馈内容。
+ */
+ const handleGoBack = async () => {
+ if (isSubmittingRef.current) {
+ return;
+ }
+ if (isSubmitSuccess || !hasUnsavedFeedback) {
+ navigateBack();
+ return;
+ }
+
+ const modalResult = await Taro.showModal({
+ title: '',
+ content: '当前反馈尚未提交,确定退出吗?',
+ cancelText: '取消',
+ confirmText: '确定'
+ });
+
+ if (modalResult.confirm) {
+ navigateBack();
+ }
+ };
+
+ useEffect(() => {
+ getFeedbackTypeList().then((res: any) => {
+ setTypeOptions(Array.isArray(res) ? res : []);
+ }).catch((error) => {
+ console.error('[feedback] failed to load feedback types:', error);
+ setTypeOptions([]);
+ showFeedbackToast('问题类型加载失败,请稍后重试');
+ });
+ }, []);
+
+ useEffect(() => {
+ let cancelled = false;
+
+ const loadFeedbackToastIcon = async () => {
+ const cachedIconPath = Taro.getStorageSync(FEEDBACK_TOAST_ICON_CACHE_KEY);
+ if (typeof cachedIconPath === 'string' && cachedIconPath) {
+ try {
+ await Taro.getFileInfo({ filePath: cachedIconPath });
+ if (!cancelled) {
+ setFeedbackToastIconSrc(cachedIconPath);
+ }
+ return;
+ } catch (error) {
+ Taro.removeStorageSync(FEEDBACK_TOAST_ICON_CACHE_KEY);
+ }
+ }
+
+ if (!cancelled) {
+ setFeedbackToastIconSrc(FEEDBACK_TOAST_ICON_URL);
+ }
+
+ try {
+ const downloadResult = await Taro.downloadFile({ url: FEEDBACK_TOAST_ICON_URL });
+ if (downloadResult.statusCode !== 200 || !downloadResult.tempFilePath) {
+ return;
+ }
+
+ const saveResult = await Taro.saveFile({ tempFilePath: downloadResult.tempFilePath });
+ if (!saveResult.savedFilePath) {
+ return;
+ }
+
+ Taro.setStorageSync(FEEDBACK_TOAST_ICON_CACHE_KEY, saveResult.savedFilePath);
+ if (!cancelled) {
+ setFeedbackToastIconSrc(saveResult.savedFilePath);
+ }
+ } catch (error) {
+ console.warn('[feedback toast] icon cache failed:', error);
+ }
+ };
+
+ loadFeedbackToastIcon();
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ useEffect(() => {
+ return () => {
+ if (toastTimerRef.current) {
+ clearTimeout(toastTimerRef.current);
+ }
+ };
+ }, []);
+
+ return (
+ <View className='feedback-page'>
+ <CustomNavbar
+ title='意见反馈'
+ colorTheme='#141E27'
+ pureBackArrow
+ opacity={0}
+ showTitleColor
+ seizeSeat
+ toBack={false}
+ onGoBackClick={handleGoBack}
+ />
+
+ {isSubmitSuccess ? (
+ <View className='feedback-success'>
+ <View className='feedback-success-main'>
+ <Image
+ className='feedback-success-icon'
+ src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/selected-golden-20260729.png'
+ mode='aspectFit'
+ />
+ <View className='feedback-success-title'>反馈成功</View>
+ <View className='feedback-success-desc'>
+ <Text>我们将会尽快修复和完善相关功能,</Text>
+ <Text>感谢您的关注与支持!</Text>
+ </View>
+ </View>
+ <View className='feedback-success-btn' onClick={handleSuccessConfirm}>我知道了</View>
+ </View>
+ ) : (
+ <View className='feedback-content'>
+ <View className='feedback-type'>
+ <View className='feedback-type-title'>问题类型<Text className='required-mark'>*</Text></View>
+ <View className='feedback-type-list'>
+ {typeOptions.map((item) => (
+ <View
+ key={item?.id}
+ className={`feedback-type-item ${selectedTypeIds.includes(normalizeFeedbackTypeId(item?.id)) ? 'active' : ''}`}
+ onClick={() => toggleFeedbackType(item?.id)}
+ >
+ <Text className='feedback-type-item-text'>{item?.feedbackType}</Text>
+ </View>
+ ))}
+ </View>
+ </View>
+
+ <View className='feedback-desc'>
+ <View className='feedback-desc-title'>问题描述<Text className='required-mark'>*</Text></View>
+ <View className='feedback-desc-box'>
+ <Textarea
+ className='feedback-desc-textarea'
+ maxlength={FEEDBACK_CONTENT_MAX_LENGTH}
+ placeholderClass='feedback-desc-placeholder'
+ placeholder='请描述您的建议或问题(可添加相关图片或视频)'
+ value={feedbackContent}
+ onInput={(e) => setFeedbackContent(e.detail.value)}
+ />
+ <View className='feedback-desc-count'>{feedbackContent.length}/{FEEDBACK_CONTENT_MAX_LENGTH}</View>
+ </View>
+ </View>
+
+ <View className='feedback-media'>
+ {mediaItems.map((item, index) => (
+ <View key={item.id} className='feedback-media-item'>
+ {item.mediaType === 'video' ? (
+ <Video
+ className='feedback-media-thumb'
+ src={item.server?.url || item.localUrl}
+ showFullscreenBtn={false}
+ showPlayBtn
+ controls={false}
+ />
+ ) : (
+ <Image
+ className='feedback-media-thumb'
+ src={item.server?.url || item.localUrl}
+ mode='aspectFill'
+ onClick={() => handlePreviewImage(item.server?.url || item.localUrl)}
+ />
+ )}
+ {item.status === 'uploading' && (
+ <View className='feedback-media-mask'>
+ <View className='feedback-media-spinner' />
+ <Text className='feedback-media-mask-text'>上传中...</Text>
+ </View>
+ )}
+ {item.status === 'failed' && (
+ <View className='feedback-media-mask failed' onClick={() => retryUpload(index)}>
+ <Text className='feedback-media-mask-text'>请重试</Text>
+ <View
+ className='feedback-media-delete'
+ onClick={(event) => {
+ event.stopPropagation();
+ removeItem(index);
+ }}
+ >
+ <Text className='feedback-media-delete-text'>×</Text>
+ </View>
+ </View>
+ )}
+ {item.status === 'success' && (
+ <View className='feedback-media-delete' onClick={() => removeItem(index)}>
+ <Text className='feedback-media-delete-text'>×</Text>
+ </View>
+ )}
+ </View>
+ ))}
+ {mediaItems.length < FEEDBACK_MEDIA_MAX_COUNT && (
+ <View className='feedback-media-upload' onClick={chooseAndUpload}>
+ <Image
+ className='feedback-media-upload-icon'
+ src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/add-feedback-20260727.png'
+ mode='aspectFit'
+ />
+ <Text className='feedback-media-upload-text'>上传图片/视频</Text>
+ </View>
+ )}
+ </View>
+
+ <View className='feedback-footer'>
+ <View className='feedback-tip'>您的反馈将帮助我们持续性优化</View>
+
+ <View className={`feedback-submit-btn ${canSubmit ? 'active' : ''}`} onClick={handleSubmit}>
+ 提交反馈
+ </View>
+ </View>
+ </View>
+ )}
+
+ {!!toastMessage && (
+ <View className='feedback-toast'>
+ <Image className='feedback-toast-icon' src={feedbackToastIconSrc} mode='aspectFit' />
+ <Text className='feedback-toast-text'>{toastMessage}</Text>
+ </View>
+ )}
+ </View>
+ );
+}
+
+export default withRobotaxiWhitelist(FeedbackPage);
diff --git a/src/robotaxi/pages/operating-area/index.less b/src/robotaxi/pages/operating-area/index.less
index 0b3b811..06d98c7 100644
--- a/src/robotaxi/pages/operating-area/index.less
+++ b/src/robotaxi/pages/operating-area/index.less
@@ -1,3 +1,5 @@
+@import '../../style/ui-variables.less';
+
.operating-area-container {
width: 100vw;
height: 100vh;
@@ -19,9 +21,9 @@
.top-bar-container {
position: absolute;
top: 92px;
- left: 31px;
+ left: 24px;
display: flex;
- flex-direction: column;
+ justify-content: flex-start;
align-items: center;
z-index: 100;
@@ -29,40 +31,16 @@
display: flex;
align-items: center;
justify-content: center;
- width: 72px;
- height: 72px;
- border-radius: 200px;
+ width: 64px;
+ height: 64px;
+ margin-right: 24px;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(40px);
box-shadow: 0px 4px 16px 0px rgba(84, 92, 143, 0.1);
- margin-right: 16px;
-
+ border-radius: 50%;
.back-icon {
- width: 40px;
- height: 40px;
- }
- }
-
- .select-city {
- display: flex;
- align-items: center;
- font-size: 32px;
- font-weight: 500;
- line-height: 48px;
- color: rgba(0, 0, 0, 0.9);
- margin-top: 32px;
-
- .city-name {
- max-width: 200px;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- }
-
- .select-city-icon {
- width: 28px;
- height: 28px;
- margin-left: 4px;
+ width: 64px;
+ height: 64px;
}
}
}
@@ -72,8 +50,8 @@
bottom: 0;
left: 0;
width: 100%;
- background: #FFFFFF;
- border-radius: 40rpx 40rpx 0 0;
+ background: @gary-theme;
+ border-radius: 60px 60px 0px 0px;
padding: 42rpx 40rpx 80rpx 40rpx;
box-sizing: border-box;
z-index: 100;
@@ -82,9 +60,9 @@
.origin-icon {
position: absolute;
top: -104rpx;
- right: 32px;
- width: 72rpx;
- height: 72rpx;
+ right: 24px;
+ width: 80px;
+ height: 80px;
}
.header {
@@ -92,11 +70,13 @@
justify-content: center;
align-items: center;
position: relative;
- margin-bottom: 56rpx;
+ margin-bottom: 40px;
.title {
- font-size: 32rpx;
+ font-size: 32px;
font-weight: 500;
+ line-height: 48px;
+ text-align: center;
color: rgba(0, 0, 0, 0.9);
}
@@ -122,12 +102,12 @@
display: inline-block;
font-size: 28rpx;
font-weight: 500;
- color: rgba(0, 0, 0, 0.4);
+ color: @black-text-04;
margin-right: 56rpx;
transition: color 0.3s;
&.active {
- color: rgba(0, 0, 0, 0.9);
+ color: @black-text-main;
}
&:last-child {
@@ -138,26 +118,27 @@
}
.content-box {
- background: linear-gradient(122deg, rgba(165, 226, 255, 0.12) 0%, rgba(117, 136, 255, 0.12) 99%);
+ background: #FFFFFF;
// height: 420rpx;
- border-radius: 16rpx;
+ border-radius: 24px;
padding: 32rpx 32rpx 36rpx 32rpx;
box-sizing: border-box;
height: 410rpx;
overflow: auto;
.content-title {
- font-size: 24rpx;
- line-height: 40rpx;
+ font-size: 24px;
+ line-height: 40px;
font-weight: 500;
- color: rgba(0, 0, 0, 0.9);
+ letter-spacing: normal;
+ color: @black-text-main;
margin-bottom: 8rpx;
}
.content-time {
font-size: 24rpx;
line-height: 40rpx;
- color: rgba(0, 0, 0, 0.7);
+ color: rgba(20, 30, 39, 0.6);
margin-bottom: 40rpx;
white-space: pre-wrap;
}
@@ -166,15 +147,15 @@
.desc-item {
font-size: 24rpx;
line-height: 40rpx;
- color: rgba(0, 0, 0, 0.7);
+ color: rgba(20, 30, 39, 0.6);
margin-bottom: 8rpx;
display: flex;
align-items: flex-start;
-
+
.index {
margin-right: 8rpx;
}
-
+
.text {
flex: 1;
white-space: normal;
diff --git a/src/robotaxi/pages/operating-area/index.tsx b/src/robotaxi/pages/operating-area/index.tsx
index 65ab52d..e2cd741 100644
--- a/src/robotaxi/pages/operating-area/index.tsx
+++ b/src/robotaxi/pages/operating-area/index.tsx
@@ -7,16 +7,17 @@
import { getGeofence } from '../../api/home';
import { extractPointsFromPolygons, findMatchedPolygon } from '../../../utils/Robotaxi-public-method';
import { withRobotaxiWhitelist } from '../../../utils/whitelist';
+import CitySelect from '../../../pages/robotaxi/home/components/city-select';
const DEFAULT_MAP_LONGITUDE = 116.397128;
const DEFAULT_MAP_LATITUDE = 39.916527;
const MAP_INCLUDE_POINTS_PADDING = [10, 10, 10, 10];
// includePoints 缩放动画的预估时长,动画结束后再显示地图,避免“先展示再放大”的跳动
const MAP_FIT_ANIMATION_DURATION = 700;
-const POLYGON_STROKE_COLOR = '#A1B1FF';
-const SELECTED_POLYGON_FILL_COLOR = '#6d7fd933';
-const UNSELECTED_POLYGON_FILL_COLOR = '#6d7fd91a';
-const POLYGON_STROKE_WIDTH = 2;
+const POLYGON_STROKE_COLOR = '#DFB369'; // rgb(223, 179, 105)
+const SELECTED_POLYGON_FILL_COLOR = '#DFB36929'; // rgba(223, 179, 105, 0.16)
+const UNSELECTED_POLYGON_FILL_COLOR = '#DFB36929'; // rgba(223, 179, 105, 0.16)
+const POLYGON_STROKE_WIDTH = 1.5;
const SELECTED_POLYGON_DASH_ARRAY = [0, 0];
const UNSELECTED_POLYGON_DASH_ARRAY = [10, 10];
@@ -140,6 +141,13 @@
const handleSelectCity = (city: any) => {
const cityCode = city.code || city.adcode;
if (!cityCode) return;
+ if (selectedCity?.code && selectedCity.code === cityCode) return; // 选中的是当前城市,无需切换
+ // 与首页保持一致:内联切城后写入缓存,供本页 useDidShow 及其他页面复用
+ Taro.setStorageSync('selectedCity', {
+ name: city.name,
+ code: cityCode,
+ id: city.id
+ });
// 进入/切换城市:先用占位图盖住地图,避免定位与缩放过程的跳动被看到
setIsMapLoading(true);
if (revealTimerRef.current) {
@@ -210,7 +218,7 @@
}
setScrollIntoViewId(`fence-${polygonId}`);
selectPolygon(polygonId, rawGeofenceData);
-
+
const targetPolygon = rawGeofenceData.find(item => item.mapPolygonId === polygonId);
if (targetPolygon && targetPolygon.polygonPoints && targetPolygon.polygonPoints.length > 0) {
const mapCtx = Taro.createMapContext('map');
@@ -349,19 +357,16 @@
{/* 顶部返回和城市选择 */}
<View className="top-bar-container">
<View onClick={toBack} className="back-btn">
- <Image className="back-icon" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/back-icon.png" />
+ <Image className="back-icon" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/back-icon-20260716.png" />
</View>
- <View className="select-city" onClick={() => Taro.navigateTo({ url: '/robotaxi/pages/select-city/index' })}>
- <Text className="city-name">{selectedCity?.name || '选择城市'}</Text>
- <Image className="select-city-icon" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/select-city.png" />
- </View>
+ <CitySelect selectedCityName={selectedCity?.name} onSelectCity={handleSelectCity} />
</View>
{/* 底部面板 */}
{
rawGeofenceData.length !== 0 &&
<View className="bottom-sheet">
- <Image className="origin-icon" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/origin-icon.png' onClick={toOrigin} />
+ <Image className="origin-icon" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/origin-icon-20260716.png' onClick={toOrigin} />
<View className="header">
<Text className="title">查看运营区域</Text>
</View>
diff --git a/src/robotaxi/pages/order/detail/index.tsx b/src/robotaxi/pages/order/detail/index.tsx
index d4d593b..98eae4c 100644
--- a/src/robotaxi/pages/order/detail/index.tsx
+++ b/src/robotaxi/pages/order/detail/index.tsx
@@ -9,6 +9,8 @@
import { formatTripDistance, formatTripDurationFromSeconds, safeParse } from '../../../../utils/Robotaxi-public-method';
import { getOrderId } from '../../../utils/order';
import { withRobotaxiWhitelist } from '../../../../utils/whitelist';
+import { showToast } from '../../../components/toast';
+import { withToast } from '../../../components/toast/with-toast';
function OrderDetailPage() {
const router = useRouter();
@@ -124,7 +126,7 @@
setActiveIndex(res.orderRating);
} catch (error) {
console.error('获取订单详情失败', error);
- Taro.showToast({ title: '获取详情失败', icon: 'none' });
+ showToast({ title: '获取详情失败', icon: 'none' });
}
};
@@ -249,7 +251,7 @@
const submitSimpleEvaluate = (index: number) => {
const targetOrderId = getOrderId(orderId);
if (!targetOrderId) {
- Taro.showToast({ title: '订单信息缺失,无法提交评价', icon: 'none' });
+ showToast({ title: '订单信息缺失,无法提交评价', icon: 'none' });
return;
}
const successToastMap: Record<number, string> = {
@@ -260,13 +262,13 @@
submitOrderComment({ orderRating: index, orderId: targetOrderId })
.then(() => {
setActiveIndex(index);
- Taro.showToast({
+ showToast({
title: successToastMap[index] || '评价提交成功',
icon: 'none'
});
})
.catch(() => {
- Taro.showToast({ title: '评价提交失败,请稍后重试', icon: 'none' });
+ showToast({ title: '评价提交失败,请稍后重试', icon: 'none' });
});
}
@@ -302,7 +304,7 @@
<Text className='code'>{orderData.vehicleCode}</Text> */}
</View>
</View>
- <Image src={safeParse(orderData.vehiclePictures, {})?.url || 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/vehicle-default.png'} className='car-img' />
+ <Image src={safeParse(orderData.vehiclePictures, {})?.url || 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/default-vehicle-img-20260724-v2.png'} className='car-img' />
</View>
}
{
@@ -386,4 +388,4 @@
)
}
-export default withRobotaxiWhitelist(OrderDetailPage);
+export default withRobotaxiWhitelist(withToast(OrderDetailPage));
diff --git a/src/robotaxi/pages/order/index.less b/src/robotaxi/pages/order/index.less
index 0dd5c33..6096238 100644
--- a/src/robotaxi/pages/order/index.less
+++ b/src/robotaxi/pages/order/index.less
@@ -25,13 +25,15 @@
box-sizing: border-box;
&.active {
+ border-radius: 20px;
+ background: rgba(223, 179, 105, 0.2);
font-weight: 700;
- color: rgba(0, 0, 0, 0.9);
+ color: #A68042;
}
}
.order-item {
- border-radius: 24px;
+ border-radius: 40px;
background: #FFFFFF;
margin-bottom: 24px;
padding: 28px 40px 24px 32px;
diff --git a/src/robotaxi/pages/order/index.tsx b/src/robotaxi/pages/order/index.tsx
index 577c6a5..435ff22 100644
--- a/src/robotaxi/pages/order/index.tsx
+++ b/src/robotaxi/pages/order/index.tsx
@@ -32,6 +32,8 @@
const XP_INVOICE_APP_ID = "xp_invoice";
import { withRobotaxiWhitelist } from "../../../utils/whitelist";
+import { showToast } from "../../components/toast";
+import { withToast } from "../../components/toast/with-toast";
function OrderPage() {
const router = useRouter();
@@ -183,7 +185,7 @@
// 根据当前订单编号跳转到发票开具页面。
const toInvoicePage = (orderNo?: string) => {
if (!orderNo) {
- Taro.showToast({
+ showToast({
title: "订单编号缺失",
icon: "none"
});
@@ -258,7 +260,6 @@
className={`order-filter-tab ${
activeFilter === item.key ? "active" : ""
}`}
- hoverClass="common-hover-class"
onClick={() => handleChangeTab(item.key)}
>
{item.label}
@@ -378,4 +379,4 @@
);
}
-export default withRobotaxiWhitelist(OrderPage);
+export default withRobotaxiWhitelist(withToast(OrderPage));
diff --git a/src/robotaxi/pages/passenger-notice/index.tsx b/src/robotaxi/pages/passenger-notice/index.tsx
index 901e22a..42f2ab8 100644
--- a/src/robotaxi/pages/passenger-notice/index.tsx
+++ b/src/robotaxi/pages/passenger-notice/index.tsx
@@ -4,6 +4,8 @@
import './index.less'
import { getLoginAgreementContentInfo, getPrivacyContent } from '../../api/home';
import { withRobotaxiWhitelist } from '../../../utils/whitelist';
+import { showToast } from '../../components/toast';
+import { withToast } from '../../components/toast/with-toast';
type AgreementItem = {
id: number | string;
@@ -56,9 +58,9 @@
console.log('normalizeContent(target.content ||', normalizeContent(target.content || ''));
return;
}
- Taro.showToast({ title: '协议内容获取失败', icon: 'none' });
+ showToast({ title: '协议内容获取失败', icon: 'none' });
}).catch(() => {
- Taro.showToast({ title: '协议内容获取失败', icon: 'none' });
+ showToast({ title: '协议内容获取失败', icon: 'none' });
});
}, []);
@@ -72,4 +74,4 @@
)
}
-export default withRobotaxiWhitelist(PassengerNotice);
+export default withRobotaxiWhitelist(withToast(PassengerNotice));
diff --git a/src/robotaxi/pages/payment-setting-detail/index.tsx b/src/robotaxi/pages/payment-setting-detail/index.tsx
index a1042f3..8e284d8 100644
--- a/src/robotaxi/pages/payment-setting-detail/index.tsx
+++ b/src/robotaxi/pages/payment-setting-detail/index.tsx
@@ -3,6 +3,8 @@
import { useCallback, useState } from 'react';
import { getPayPwd, RobotaxiPaySettingStatus, setPayPwd } from '../../api/payment';
import { withRobotaxiWhitelist } from '../../../utils/whitelist';
+import { showToast } from '../../components/toast';
+import { withToast } from '../../components/toast/with-toast';
import './index.less';
@@ -54,7 +56,7 @@
try {
setIsOperating(true);
await setPayPwd(RobotaxiPaySettingStatus.Disable);
- Taro.showToast({
+ showToast({
title: '微信免密支付已关闭',
icon: 'none'
});
@@ -62,7 +64,7 @@
navigateBackToList();
}, 300);
} catch (error) {
- Taro.showToast({
+ showToast({
title: '关闭失败',
icon: 'none'
});
@@ -79,7 +81,7 @@
}
})
.catch(() => {
- Taro.showToast({
+ showToast({
title: '查询支付设置失败',
icon: 'none'
});
@@ -109,4 +111,4 @@
);
}
-export default withRobotaxiWhitelist(PaymentSettingDetail);
+export default withRobotaxiWhitelist(withToast(PaymentSettingDetail));
diff --git a/src/robotaxi/pages/payment-setting/index.tsx b/src/robotaxi/pages/payment-setting/index.tsx
index b3ac6e8..ec35c9a 100644
--- a/src/robotaxi/pages/payment-setting/index.tsx
+++ b/src/robotaxi/pages/payment-setting/index.tsx
@@ -3,6 +3,8 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { getPayPwd, RobotaxiPaySettingStatus, setPayPwd } from '../../api/payment';
import { withRobotaxiWhitelist } from '../../../utils/whitelist';
+import { showToast } from '../../components/toast';
+import { withToast } from '../../components/toast/with-toast';
import './index.less';
@@ -53,7 +55,7 @@
return nextStatus;
} catch (error) {
if (showErrorToast) {
- Taro.showToast({ title: '查询支付设置失败', icon: 'none' });
+ showToast({ title: '查询支付设置失败', icon: 'none' });
}
throw error;
}
@@ -72,12 +74,12 @@
try {
const nextStatus = await refreshPayStatus(false);
- Taro.showToast({
+ showToast({
title: nextStatus ? '开通成功' : '开通未完成',
icon: nextStatus ? 'success' : 'none'
});
} catch (error) {
- Taro.showToast({ title: '查询签约结果失败', icon: 'none' });
+ showToast({ title: '查询签约结果失败', icon: 'none' });
} finally {
setIsOperating(false);
}
@@ -101,7 +103,7 @@
const openCreditPaySign = useCallback((signStr: string) => {
const weappGlobal = (globalThis as any).wx;
if (typeof weappGlobal?.openBusinessView !== 'function') {
- Taro.showToast({ title: '请升级微信版本后重试', icon: 'none' });
+ showToast({ title: '请升级微信版本后重试', icon: 'none' });
return false;
}
@@ -116,7 +118,7 @@
waitingCreditPayReturnRef.current = false;
clearCreditPayReturnListener();
setIsOperating(false);
- Taro.showToast({ title: '拉起支付分签约失败', icon: 'none' });
+ showToast({ title: '拉起支付分签约失败', icon: 'none' });
}
});
return true;
@@ -147,7 +149,7 @@
if (nextOperateStatus === RobotaxiPaySettingStatus.Disable) {
const nextStatus = await refreshPayStatus(false);
- Taro.showToast({
+ showToast({
title: nextStatus ? '关闭未完成' : '已关闭',
icon: nextStatus ? 'none' : 'success'
});
@@ -156,7 +158,7 @@
const signStr = String(response?.signStr || '');
if (!signStr) {
- Taro.showToast({ title: '获取签约信息失败', icon: 'none' });
+ showToast({ title: '获取签约信息失败', icon: 'none' });
return;
}
@@ -165,7 +167,7 @@
return;
}
} catch (error) {
- Taro.showToast({
+ showToast({
title: isPayEnabled ? '关闭失败' : '开通失败',
icon: 'none'
});
@@ -212,4 +214,4 @@
);
}
-export default withRobotaxiWhitelist(PaymentSetting);
+export default withRobotaxiWhitelist(withToast(PaymentSetting));
diff --git a/src/robotaxi/pages/personal-center/index.less b/src/robotaxi/pages/personal-center/index.less
index e8483fe..152b497 100644
--- a/src/robotaxi/pages/personal-center/index.less
+++ b/src/robotaxi/pages/personal-center/index.less
@@ -1,10 +1,13 @@
+@import "../../style/ui-variables.less";
+
.personal-center-page {
min-height: 100vh;
box-sizing: border-box;
- background-image: url("https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/mine-bg.png");
- background-size: 100% 804px;
+ background-image: url("https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/mine-bg-20260630.png");
+ background-size: 100% 788px;
background-repeat: no-repeat;
- background-color: #f5f6fa;
+ background-color: @gary-theme;
+ padding-bottom: 40px;
}
.personal-center-header {
@@ -28,31 +31,38 @@
padding: 52px 22px 0 24px;
}
+.user-info {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ .head-img {
+ width: 108px;
+ height: 108px;
+ border: 5px solid #fff;
+ border-radius: 50%;
+ overflow: hidden;
+ }
+}
.user-name {
font-size: 36px;
line-height: 48px;
font-weight: 500;
- color: rgba(0, 0, 0, 0.9);
- margin-bottom: 48px;
- padding-left: 8px;
+ color: @black-text-main;
+ padding-left: 16px;
}
-
.coupon-card {
- height: 112px;
- border-radius: 16px;
- box-sizing: border-box;
- border: 2px solid #ffffff;
- margin-bottom: 24px;
+ position: relative;
+ z-index: 2;
display: flex;
align-items: center;
- justify-content: space-between;
- padding: 0 24px 0 42px;
-background: linear-gradient(137deg, #FFF8EE 22%, rgba(255, 255, 255, 0.55) 81%);
+ justify-content: flex-start;
+ padding: 32px 48px 20px 48px;
.coupon-card-title {
font-size: 28px;
- line-height: 44px;
- color: rgba(0, 0, 0, 0.9);
+ line-height: normal;
+ color: @gold-light;
+ margin-right: 32px;
}
.coupon-card-right {
@@ -61,10 +71,9 @@
}
.coupon-card-count {
- font-size: 28px;
- line-height: 44px;
- color: rgba(0, 0, 0, 0.4);
- margin-right: 8px;
+ font-size: 24px;
+ line-height: normal;
+ color: @gold-light;
}
.coupon-card-arrow {
width: 32px;
@@ -74,35 +83,58 @@
.section-card {
background: #ffffff;
- border-radius: 24px;
- padding: 24px 0;
+ border-radius:60px;
+ padding: 32px 24px 24px 48px;
margin-bottom: 24px;
- &:last-child {
- padding: 32px 0;
- padding-bottom: 8px;
+}
+
+
+.coupon-section-card {
+ position: relative;
+ margin-top: 70px;
+ border-radius: 60px;
+ overflow: hidden;
+ .card-bg-box {
+ position: absolute;
+ top: 0;
+ right: 0;
+ left: 0;
+ z-index: 1;
+ width: 698px;
+ overflow: hidden;
+ .coupon-section-card-bg {
+ width: 698px;
+ height: 260px;
+ }
+ }
+
+ .order-menu-card {
+ position: relative;
+ z-index: 2;
+ border-radius: 60px;
+ background: linear-gradient(180deg, #CCD0D7 0%, #FFFFFF 54%);
+ padding-left: 0;
+ padding-bottom: 40px;
+ .section-title {
+ padding-left: 48px;
+ margin-bottom: 20px;
+ }
}
}
.section-title {
font-size: 28px;
- font-weight: 600;
+ font-weight: 500;
line-height: 44px;
/* 文字色/黑色/重要字 90% */
- color: rgba(0, 0, 0, 0.9);
- text-indent: 32px;
- margin-bottom: 20px;
-}
-
-.section-card:last-child {
- .section-title {
- margin-bottom: 8px;
- }
+ color: @black-text-main;
+ margin-bottom: 16px;
}
.trip-menu {
display: flex;
align-items: center;
- padding-left: 8px;
+ padding-left: 12px;
}
.trip-menu-item {
@@ -145,7 +177,7 @@
.trip-menu-label {
font-size: 24px;
line-height: 40px;
- color: rgba(0, 0, 0, 0.9);
+ color: @black-text-main;
}
.tool-menu-item {
@@ -153,8 +185,6 @@
display: flex;
align-items: center;
justify-content: space-between;
- padding-left: 32px;
- padding-right: 24px;
transition: background-color 0.2s;
}
@@ -165,7 +195,7 @@
.tool-menu-label {
font-size: 28px;
line-height: 44px;
- color: rgba(0, 0, 0, 0.9);
+ color: @black-text-main;
}
.tool-menu-arrow {
diff --git a/src/robotaxi/pages/personal-center/index.tsx b/src/robotaxi/pages/personal-center/index.tsx
index 6738b45..8ab905d 100644
--- a/src/robotaxi/pages/personal-center/index.tsx
+++ b/src/robotaxi/pages/personal-center/index.tsx
@@ -33,15 +33,19 @@
const toolMenuList: ToolMenuItem[] = [
{ key: "service", label: "联系客服" },
- { key: "invoice", label: "开发票" }, // 暂未上线,屏蔽入口
- // { key: "payment", label: "支付设置" },
- { key: "favorite", label: "收藏站点" }
+ { key: "invoice", label: "开发票" },
+ { key: "payment", label: "支付设置" },
+ { key: "favorite", label: "收藏站点" },
+ { key: "feedback", label: "意见反馈" }
];
import { withRobotaxiWhitelist } from '../../../utils/whitelist';
+import { showToast } from '../../components/toast';
+import { withToast } from '../../components/toast/with-toast';
function PersonalCenter() {
const nickname = useSelector((state: any) => state.user?.userInfo?.nickName) || "小鹏艺术家";
+ const headImg = useSelector((state: any) => state.user?.userInfo?.headImg) || "";
const reduxDispatch = useDispatch();
const [hasPendingPayOrder, setHasPendingPayOrder] = useState(false);
@@ -136,6 +140,11 @@
return;
}
+ if (key === "feedback") {
+ Taro.navigateTo({ url: "/robotaxi/pages/feedback/index?source=mine" });
+ return;
+ }
+
Taro.showToast({ title: "功能建设中", icon: "none" });
};
@@ -149,40 +158,49 @@
</View>
<View className="personal-center-content">
- <View className="user-name">{nickname}</View>
-
- <View
- className="coupon-card"
- onClick={() =>
- Taro.navigateTo({ url: "/robotaxi/pages/coupon/index" })
- }
- >
- <Text className="coupon-card-title">我的优惠券</Text>
- <View className="coupon-card-right">
- <Text className="coupon-card-count">{couponCount}张</Text>
- <Image className="coupon-card-arrow" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/mine-arrow.png" />
- </View>
+ <View className="user-info">
+ { headImg && <Image src={headImg} className="head-img"></Image> }
+ <View className="user-name">{nickname}</View>
</View>
- <View className="section-card">
- <View className="section-title">我的行程</View>
- <View className="trip-menu">
- {tripMenuList.map(item => (
- <View
- key={item.key}
- className={item.key === "all" ? "trip-menu-item all" : "trip-menu-item"}
- onClick={() => handleTripClick(item.key)}
- >
- {item.key === "all" && (
- <Image className="mine-line" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/mine-line.png" />
- )}
- {item.key === "pendingPay" && hasPendingPayOrder && (
- <View className="trip-menu-badge" />
- )}
- <Image className="trip-menu-icon" src={item.icon} />
- <Text className="trip-menu-label">{item.label}</Text>
- </View>
- ))}
+ <View className="coupon-section-card">
+ <View className="card-bg-box">
+ <Image className="coupon-section-card-bg" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/order-menu-bg-20260630.png" />
+ </View>
+
+ <View
+ className="coupon-card"
+ onClick={() =>
+ Taro.navigateTo({ url: "/robotaxi/pages/coupon/index" })
+ }
+ >
+ <Text className="coupon-card-title">我的优惠券</Text>
+ <View className="coupon-card-right">
+ <Text className="coupon-card-count">{couponCount}张可用</Text>
+ <Image className="coupon-card-arrow" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/golden-arrow-20260629.png" />
+ </View>
+ </View>
+
+ <View className="section-card order-menu-card">
+ <View className="section-title">我的行程</View>
+ <View className="trip-menu">
+ {tripMenuList.map(item => (
+ <View
+ key={item.key}
+ className={item.key === "all" ? "trip-menu-item all" : "trip-menu-item"}
+ onClick={() => handleTripClick(item.key)}
+ >
+ {item.key === "all" && (
+ <Image className="mine-line" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/mine-line.png" />
+ )}
+ {item.key === "pendingPay" && hasPendingPayOrder && (
+ <View className="trip-menu-badge" />
+ )}
+ <Image className="trip-menu-icon" src={item.icon} />
+ <Text className="trip-menu-label">{item.label}</Text>
+ </View>
+ ))}
+ </View>
</View>
</View>
@@ -208,4 +226,4 @@
);
}
-export default withRobotaxiWhitelist(PersonalCenter);
+export default withRobotaxiWhitelist(withToast(PersonalCenter));
diff --git a/src/robotaxi/pages/pricing-rules/index.tsx b/src/robotaxi/pages/pricing-rules/index.tsx
index 37728f0..6fc4067 100644
--- a/src/robotaxi/pages/pricing-rules/index.tsx
+++ b/src/robotaxi/pages/pricing-rules/index.tsx
@@ -3,6 +3,8 @@
import { View, Text } from '@tarojs/components';
import { getPricingRules } from '../../api/pricing';
import { withRobotaxiWhitelist } from '../../../utils/whitelist';
+import { showToast } from '../../components/toast';
+import { withToast } from '../../components/toast/with-toast';
import './index.less';
type AnyRecord = Record<string, any>;
@@ -147,10 +149,10 @@
setMockJson(res || {});
} catch (error) {
setMockJson({});
- Taro.showToast({
+ showToast({
title: '获取定价规则失败',
icon: 'none'
- });
+ });
} finally {
setLoading(false);
}
@@ -584,5 +586,5 @@
);
}
-export default withRobotaxiWhitelist(PricingRulesPage);
+export default withRobotaxiWhitelist(withToast(PricingRulesPage));
diff --git a/src/robotaxi/pages/select-city/index.tsx b/src/robotaxi/pages/select-city/index.tsx
index f915d42..844d53e 100644
--- a/src/robotaxi/pages/select-city/index.tsx
+++ b/src/robotaxi/pages/select-city/index.tsx
@@ -4,6 +4,8 @@
import './index.less';
import { getOpenCity, getCityList } from '../../api/select-city';
import { withRobotaxiWhitelist } from '../../../utils/whitelist';
+import { showToast } from '../../components/toast';
+import { withToast } from '../../components/toast/with-toast';
@@ -103,7 +105,7 @@
const handleSelectCity = (city, isOpen) => {
if (!isOpen) {
- Taro.showToast({
+ showToast({
title: '该城市暂未开通服务',
icon: 'none'
});
@@ -295,4 +297,4 @@
)
}
-export default withRobotaxiWhitelist(SelectCityPage);
+export default withRobotaxiWhitelist(withToast(SelectCityPage));
diff --git a/src/robotaxi/pages/select-map-site/components/select-map-site-view.tsx b/src/robotaxi/pages/select-map-site/components/select-map-site-view.tsx
index 3cba50b..842aed2 100644
--- a/src/robotaxi/pages/select-map-site/components/select-map-site-view.tsx
+++ b/src/robotaxi/pages/select-map-site/components/select-map-site-view.tsx
@@ -67,6 +67,7 @@
const showRangeWarning = isOutOfOperatingArea || isCrossOperatingArea;
const shouldShowSelectedSiteEcho = !showRangeWarning && Boolean(deps.selectedSite.name);
const shouldShowStationSelection = deps.selectionMode === 'station' && !showRangeWarning;
+ const garyToolTipBg = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/tooltip-bg-gray-20260714.png';
return (
<View className="home-container">
@@ -109,7 +110,10 @@
<View className="toolTip">
{(showRangeWarning || deps.selectedSite.time) && (
<View className="toolTip-bg">
- <Image className="toolTip-bg-img" src={deps.tooltipBgSrc} />
+ <Image className="toolTip-bg-img" src={(isOutOfOperatingArea || isCrossOperatingArea) ? garyToolTipBg : deps.tooltipBgSrc } />
+ {!showRangeWarning && (
+ <Image className="toolTip-bg-icon" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/map-time-icon-20260708.png" />
+ )}
<Text className="toolTip-bg-text">
{isOutOfOperatingArea ? '不在运营区' : isCrossOperatingArea ? '不在同一运营区' : `限停${deps.selectedSite.time}分钟`}
</Text>
@@ -123,13 +127,12 @@
? '请在同一运营区内选择'
: deps.selectedSite.name}
</View>
+ <Image className="toolTip-diamond-icon" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/diamond-icon-20260708.png" />
</View>
</View>
)}
- <Image
- className={`pointer-icon ${deps.bounce ? 'bounce' : ''}`}
- src={deps.pointer}
- />
+ {/* 黑色站点名浮窗(toolTip)显示时隐藏中心大头针 */}
+ <Image className={`pointer-icon ${deps.bounce ? 'bounce' : ''} ${(deps.showToolTip && shouldShowSelectedSiteEcho) || showRangeWarning ? 'hidden-pointer' : ''}`} src={deps.pointer} />
</View>
<View className="container">
<View className="container-inner">
@@ -145,60 +148,63 @@
)}
{/* 确认站点 */}
<View className="content-box">
- <View className="content-box-item">
- <View className='content-box-item-title'>
- <View className="content-title">{shouldShowStationSelection ? deps.selectedStation?.name : (deps.pageType === 'start' ? '确认上车站点' : '确认目的地')}</View>
- {deps.pageType === 'start' && deps.walkingDuration !== null && deps.walkingDistance !== null && (
- <View className="limit-tip">
- 从当前位置出发步行{formatTripDurationFromSeconds(deps.walkingDuration)}({formatTripDistance(deps.walkingDistance)})上车
- </View>
- )}
- {(deps.nearbySites.length > 0 && deps.pageType === 'end') && (
- <View className="limit-tip">点击紫色圆点或拖动大头针,可修改{deps.pageType === 'start' ? '上车点' : '目的地'}</View>
+ <Image className="content-box-top-bg" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/top-card-bg-20260722.png' />
+ <View className="content-box-coantainer">
+ <View className="content-box-item">
+ <View className='content-box-item-title'>
+ <View className="content-title">{shouldShowStationSelection ? deps.selectedStation?.name : (deps.pageType === 'start' ? '确认上车点' : '确认目的地')}</View>
+ {deps.pageType === 'start' && deps.walkingDuration !== null && deps.walkingDistance !== null && (
+ <View className="limit-tip">
+ 从当前位置出发步行{formatTripDurationFromSeconds(deps.walkingDuration)}({formatTripDistance(deps.walkingDistance)})上车
+ </View>
+ )}
+ {(deps.nearbySites.length > 0 && deps.pageType === 'end') && (
+ <View className="limit-tip">点击金色圆点或拖动大头针,可修改目的地</View>
+ )}
+ </View>
+ {shouldShowStationSelection && (
+ <Image
+ onClick={deps.handleCollect}
+ className='collect-icon'
+ src={deps.selectedStation?.isCollect ? deps.activeStar : deps.noStar}
+ />
)}
</View>
- {shouldShowStationSelection && (
- <Image
- onClick={deps.handleCollect}
- className='collect-icon'
- src={deps.selectedStation?.isCollect ? deps.activeStar : deps.noStar}
- />
- )}
- </View>
- <View className='site-list-wrap'>
- <ScrollView
- className="site-list"
- scrollY
- enhanced
- showScrollbar={true}
- >
- <View className='site-list-inner'>
- {shouldShowStationSelection ? (
- deps.stationSiteList.length > 0 ? deps.stationSiteList.map((item) => (
- <View
- key={item.id || item.pointId || `${item.latitude}_${item.longitude}`}
- className={`site-item ${String(item.id || item.pointId || '') === String(deps.selectedSite?.id || deps.selectedSite?.pointId || '') ? 'site-active' : ''}`}
- onClick={() => deps.changeStationSite(item)}
- >
- <View>{item.name}</View>
+ <View className='site-list-wrap'>
+ <ScrollView
+ className="site-list"
+ scrollY
+ enhanced
+ showScrollbar={true}
+ >
+ <View className='site-list-inner'>
+ {shouldShowStationSelection ? (
+ deps.stationSiteList.length > 0 ? deps.stationSiteList.map((item) => (
+ <View
+ key={item.id || item.pointId || `${item.latitude}_${item.longitude}`}
+ className={`site-item ${String(item.id || item.pointId || '') === String(deps.selectedSite?.id || deps.selectedSite?.pointId || '') ? 'site-active' : ''}`}
+ onClick={() => deps.changeStationSite(item)}
+ >
+ <View>{item.name}</View>
+ </View>
+ )) : <View className="no-service">当前场站暂无可选站点</View>
+ ) : shouldShowSelectedSiteEcho ? (
+ <View className='site-item site-active'>
+ <View>{deps.selectedSite.name}</View>
+ {deps.selectionMode === 'point' && (
+ <Image onClick={deps.handleCollect} className='collect-icon' src={deps.selectedSite.isCollect === 1 ? deps.activeStar : deps.noStar} />
+ )}
</View>
- )) : <View className="no-service">当前场站暂无可选站点</View>
- ) : shouldShowSelectedSiteEcho ? (
- <View className='site-item site-active'>
- <View>{deps.selectedSite.name}</View>
- {deps.selectionMode === 'point' && (
- <Image onClick={deps.handleCollect} className='collect-icon' src={deps.selectedSite.isCollect === 1 ? deps.activeStar : deps.noStar} />
- )}
- </View>
- ) : showRangeWarning ? (
- <View className="no-service">{isCrossOperatingArea ? deps.crossOperatingAreaHintText : '超出服务范围,请在运营区内选择'}</View>
- ) : <View className="no-service">附近无可选站点</View>
- }
- </View>
- </ScrollView>
- </View>
- <View onClick={deps.confirmButtonDisabled ? undefined : deps.confirmSite} className={`site-confirm ${deps.confirmButtonDisabled ? 'no-confirm' : ''}`}>
- {deps.confirmButtonText}
+ ) : showRangeWarning ? (
+ <View className="no-service">{isCrossOperatingArea ? deps.crossOperatingAreaHintText : '超出服务范围,请在运营区内选择'}</View>
+ ) : <View className="no-service">附近无可选站点</View>
+ }
+ </View>
+ </ScrollView>
+ </View>
+ <View onClick={deps.confirmButtonDisabled ? undefined : deps.confirmSite} className={`site-confirm ${deps.confirmButtonDisabled ? 'no-confirm' : ''}`}>
+ {deps.confirmButtonText}
+ </View>
</View>
</View>
</View>
diff --git a/src/robotaxi/pages/select-map-site/hooks/use-select-map-site-state.ts b/src/robotaxi/pages/select-map-site/hooks/use-select-map-site-state.ts
index 0fba06a..b808e13 100644
--- a/src/robotaxi/pages/select-map-site/hooks/use-select-map-site-state.ts
+++ b/src/robotaxi/pages/select-map-site/hooks/use-select-map-site-state.ts
@@ -39,10 +39,10 @@
const [rangeStatus, setRangeStatus] = useState<SelectSiteRangeStatus>(SELECT_SITE_RANGE_STATUS.normal); // 当前选点范围状态
const [walkingDistance, setWalkingDistance] = useState<number | null>(null); // 步行距离
const [walkingDuration, setWalkingDuration] = useState<number | null>(null); // 步行时长
- const tooltipBgUrl = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/tooltip-bg.png'; // 提示背景图资源
+ const tooltipBgUrl = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/tooltip-bg-20260715.png'; // 提示背景图资源
const [tooltipBgSrc, setTooltipBgSrc] = useState(tooltipBgUrl); // 提示背景图路径
const [isAndroid, setIsAndroid] = useState(false); // 是否安卓机型
- const waitingRadarSrc = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/waiting-radar.webp'; // 加载雷达图
+ const waitingRadarSrc = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/waiting-radar-20260728.webp'; // 加载雷达图
const [nearbyLoading, setNearbyLoading] = useState(false); // 附近站点加载态
const pendingNearbyRequestCountRef = useRef(0); // 待完成附近站点请求数
diff --git a/src/robotaxi/pages/select-map-site/hooks/use-select-map-site.ts b/src/robotaxi/pages/select-map-site/hooks/use-select-map-site.ts
index 1345509..db3c02f 100644
--- a/src/robotaxi/pages/select-map-site/hooks/use-select-map-site.ts
+++ b/src/robotaxi/pages/select-map-site/hooks/use-select-map-site.ts
@@ -1,4 +1,5 @@
import Taro, { useDidShow } from "@tarojs/taro";
+import { showToast } from '../../../components/toast';
import { useEffect, useMemo } from 'react';
import {
buildStationMetaMap,
@@ -28,12 +29,13 @@
import { SELECT_SITE_RANGE_STATUS } from './use-select-map-site-state';
import useSelectMapSiteWalkingRoute from './use-select-map-site-walking-route';
import {
- MODIFY_DESTINATION_CANDIDATE_KEY,
- MODIFY_DESTINATION_PENDING_SAME_SITE_TOAST_KEY,
- MODIFY_DESTINATION_SKIP_AC_QUERY_ON_RETURN_KEY,
MODIFY_DESTINATION_SESSION_KEY,
+ MODIFY_DESTINATION_CANDIDATE_KEY,
+ MODIFY_DESTINATION_SKIP_AC_QUERY_ON_RETURN_KEY,
+ MODIFY_DESTINATION_PENDING_SAME_SITE_TOAST_KEY,
isSameModifyDestinationSite
} from '../../../constants/modify-destination';
+import { hasActiveTripOrderInStorage } from '../../../utils/order';
import {
CONFIRM_MAP_DROPOFF_BUTTON_EVENT_ID,
CONFIRM_MAP_PICKUP_BUTTON_EVENT_ID
@@ -58,13 +60,32 @@
const AUTO_SCALE_MIN = 11; // 自动缩放最小等级
const INCLUDE_POINTS_PADDING = [100, 100, 100, 100] as [number, number, number, number]; // 地图回视图内边距
const CROSS_OPERATING_AREA_FILL_COLOR = '#0000001a'; // 非可选运营区围栏填充色
-const CROSS_OPERATING_AREA_STROKE_COLOR = '#00000033'; // 非可选运营区围栏描边色
-const CROSS_OPERATING_AREA_STROKE_WIDTH = 2; // 非可选运营区围栏描边宽度
+const CROSS_OPERATING_AREA_STROKE_COLOR = '#DFB369'; // 非可选运营区围栏描边色
+const CROSS_OPERATING_AREA_STROKE_WIDTH = 1.5; // 非可选运营区围栏描边宽度
const CROSS_OPERATING_AREA_DASH_ARRAY = [10, 10] as [number, number]; // 非可选运营区 polygon 虚线边框
const SELECT_MAP_POINT_ON_OFF = {
start: 1,
end: 2
} as const; // 选点地图站点开关:1=上车点,2=下车点
+
+// 站点 marker 原始(紫色)图标,来自 formatSiteToMarker
+const OLD_SITE_MARKER_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/site-poi.png';
+// 选点页站点金色图标(与首页保持一致)
+const SITE_MARKER_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/golden-site-20260708.png';
+
+/**
+ * 将站点 marker 的紫色图标替换为金色图标(与首页一致)。
+ */
+const applySiteMarkerIcon = (markerList: any[]) => {
+ if (!Array.isArray(markerList)) {
+ return markerList;
+ }
+ return markerList.map((marker) => (
+ marker?.iconPath === OLD_SITE_MARKER_ICON
+ ? { ...marker, iconPath: SITE_MARKER_ICON, width: 18, height: 18 }
+ : marker
+ ));
+};
/**
* 选点页业务编排:聚合状态容器与副作用 hooks,并对外提供稳定页面接口。
@@ -389,13 +410,15 @@
state.pendingNearbyRequestCountRef.current += 1;
state.setNearbyLoading(true);
try {
- const { isInside, newMarkers, sites, electronicAreaId } = await fetchMapData(
+ const { isInside, newMarkers: rawMarkers, sites, electronicAreaId } = await fetchMapData(
lat,
lng,
selectedCity?.code,
pointOnOff,
{ allowedElectronicAreaId: referenceElectronicAreaId }
);
+ // 站点紫色图标替换为金色(与首页一致)
+ const newMarkers = applySiteMarkerIcon(rawMarkers);
// 仅最新一次拖动请求才回写状态;过期请求只返回数据、不污染 markers/nearbySites 等状态,
// 防止过期 nearbySites 触发场站副作用回填 selectedSite,导致大头针离开点位后旧信息滞留。
const isStaleRequest = typeof seq === 'number' && seq !== state.regionSeqRef.current;
@@ -473,7 +496,7 @@
const handleViewOperatingArea = () => {
const points = extractPointsFromPolygons(selectablePolygons);
if (points.length === 0) {
- Taro.showToast({ title: '暂无可选运营区域', icon: 'none' });
+ showToast({ title: '暂无可选运营区域', icon: 'none' });
return;
}
const mapCtx = Taro.createMapContext('select-site-map');
@@ -654,18 +677,18 @@
*/
const confirmSite = async () => {
if (!state.selectedSite.name) {
- Taro.showToast({ title: '请选择站点', icon: 'none' });
+ showToast({ title: '请选择站点', icon: 'none' });
return;
}
if (state.rangeStatus === SELECT_SITE_RANGE_STATUS.crossOperatingArea) {
- Taro.showToast({ title: '请在同一运营区内选择', icon: 'none' });
+ showToast({ title: '请在同一运营区内选择', icon: 'none' });
return;
}
if (state.pageType === 'start') {
const distanceConfig = await ensureDistanceConfigReady();
const endSiteCache = Taro.getStorageSync('endSite');
if (endSiteCache?.electronicAreaId && state.selectedSite?.electronicAreaId && endSiteCache.electronicAreaId !== state.selectedSite.electronicAreaId) {
- Taro.showToast({ title: '请在同一运营区内选择', icon: 'none' });
+ showToast({ title: '请在同一运营区内选择', icon: 'none' });
return;
}
if (!isWithinPickupMaxDistance({
@@ -707,7 +730,7 @@
const distanceConfig = await ensureDistanceConfigReady();
const startSiteCache = Taro.getStorageSync('startSite');
if (startSiteCache?.electronicAreaId && state.selectedSite?.electronicAreaId && startSiteCache.electronicAreaId !== state.selectedSite.electronicAreaId) {
- Taro.showToast({ title: '请在同一运营区内选择', icon: 'none' });
+ showToast({ title: '请在同一运营区内选择', icon: 'none' });
return;
}
if (!isWithinStartEndMinDistance({
@@ -721,9 +744,11 @@
}
reportConfirmMapSiteEvent();
const modifySession = Taro.getStorageSync(MODIFY_DESTINATION_SESSION_KEY); // 修改目的地会话中将终点写入候选缓存,返回行程页进入预览态
+ // 增加 hasActiveTripOrderInStorage 联合判断,防止上一单异常终止导致 modifySession 缓存残留,污染新下单流程
+ const isModifyDestinationActive = modifySession?.active && hasActiveTripOrderInStorage();
// 【修改目的地拦截补充】接驾中选点相同或距离过近拦截
- if (modifySession?.active) {
+ if (isModifyDestinationActive) {
const isPickupTripStatus = [4, 5].includes(modifySession.tripStatus); // 4: VehicleApproaching, 5: VehicleArrivedPickup
if (isPickupTripStatus) {
const isSameSite = isSameModifyDestinationSite(startSiteCache, state.selectedSite);
@@ -741,10 +766,10 @@
}
}
- if (modifySession?.active && isSameModifyDestinationSite(modifySession?.previousEndSite, state.selectedSite)) {
+ if (isModifyDestinationActive && isSameModifyDestinationSite(modifySession?.previousEndSite, state.selectedSite)) {
Taro.setStorageSync(MODIFY_DESTINATION_PENDING_SAME_SITE_TOAST_KEY, 1);
}
- if (modifySession?.active) {
+ if (isModifyDestinationActive) {
Taro.setStorageSync(MODIFY_DESTINATION_CANDIDATE_KEY, state.selectedSite);
Taro.setStorageSync(MODIFY_DESTINATION_SKIP_AC_QUERY_ON_RETURN_KEY, 1); // 修改目的地返回行程页时跳过一次空调/温度状态查询
} else {
@@ -752,7 +777,7 @@
}
writeConfirmRideSiteSourceType('end', MAP_CONFIRM_RIDE_SOURCE_TYPE);
completeConfirmRideSelectionSession(startSiteCache, state.selectedSite);
- const returnToBeginRouteUrl = modifySession?.active
+ const returnToBeginRouteUrl = isModifyDestinationActive
? '/robotaxi/pages/begin-route/index?skipPageFlag=1&fromModifyDestination=1'
: (Taro.getStorageSync('orderId') ? '/robotaxi/pages/begin-route/index?skipPageFlag=1' : '/robotaxi/pages/begin-route/index');
Taro.reLaunch({
@@ -823,7 +848,7 @@
Taro.setStorageSync('endSite', nextSite);
}
}
- Taro.showToast({
+ showToast({
title: type === 2 ? '已取消收藏' : '收藏成功',
icon: 'none'
});
diff --git a/src/robotaxi/pages/select-map-site/index.less b/src/robotaxi/pages/select-map-site/index.less
index 6a9f885..c3b8ac8 100644
--- a/src/robotaxi/pages/select-map-site/index.less
+++ b/src/robotaxi/pages/select-map-site/index.less
@@ -1,3 +1,5 @@
+@import '../../../robotaxi/style/ui-variables.less';
+
.select-city {
position: fixed;
top: 92px;
@@ -5,16 +7,16 @@
display: flex;
align-items: center;
justify-content: center;
- width: 72px;
- height: 72px;
+ width: 64px;
+ height: 64px;
border-radius: 200px;
background: rgba(255, 255, 255, 0.9);
- backdrop-filter: blur(40px);
+ // backdrop-filter: blur(40px);
box-shadow: 0px 4px 16px 0px rgba(84, 92, 143, 0.1);
.select-city-icon {
- width: 40px;
- height: 40px;
+ width: 64px;
+ height: 64px;
}
}
@@ -35,16 +37,16 @@
z-index: 11;
.search-icon {
- width: 10px;
- height: 10px;
- background: #768AFF;
+ width: 16px;
+ height: 16px;
+ background: @gold-light;
border-radius: 50%;
margin-right: 16px;
flex-shrink: 0;
}
.end-icon {
- background: #8BB654;
+ background: @black-text-main;
}
.search-input-box {
@@ -61,6 +63,9 @@
color: rgba(0, 0, 0, 0.3);
text-align: left;
}
+ .search-input {
+ color: @black-text-main;
+ }
}
}
@@ -71,7 +76,7 @@
display: flex;
align-items: center;
flex-direction: column;
- gap: 8px;
+ gap: 24px;
border-radius: 24px;
box-shadow: 0px 4px 16px 0px rgba(84, 92, 143, 0.1);
background: rgba(255, 255, 255, 1);
@@ -124,12 +129,15 @@
.toolTip-bg {
position: relative;
- height: 70px;
- min-width: 155px;
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ padding: 3px 20px 32px 20px;
+ height: 80px;
+ box-sizing: border-box;
+ min-width: 186px;
text-align: center;
width: fit-content;
- padding: 0 15px 0 3px;
-
.toolTip-bg-img {
position: absolute;
top: 0;
@@ -137,6 +145,16 @@
width: 100%;
height: 100%;
z-index: -1;
+ }
+
+ .toolTip-bg-icon {
+ position: relative;
+ z-index: 1;
+ width: 24px;
+ height: 24px;
+ margin-top: 12px;
+ margin-right: 4px;
+ flex-shrink: 0;
}
.toolTip-bg-text {
@@ -147,32 +165,45 @@
letter-spacing: normal;
color: #FFFFFF;
line-height: 48px;
+ padding-right: 20px;
}
}
.toolTip-txt {
- margin-top: -26px;
+ margin-top: -32px;
+ position: relative;
display: flex;
align-items: center;
- border-radius: 16px;
- background: rgba(255, 255, 255, 0.9);
- backdrop-filter: blur(40px);
- box-shadow: 0px 4px 16px 0px rgba(45, 48, 64, 0.2);
- padding: 12px 20px;
+ border-radius: 24px;
+ background: @black-text-main;
+ padding: 20px 16px 20px 20px;
+ min-width: 198px;
+ max-width: 400px;
+ box-sizing: border-box;
.toolTip-txt-content {
font-size: 24px;
- font-weight: bold;
+ font-weight: 500;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
- line-height: 48px;
+ color: #FFFFFF;
}
.toolTip-icon {
margin-left: 8px;
- width: 32px;
- height: 32px;
+ width: 16px;
+ height: 16px;
flex-shrink: 0;
+ }
+
+ // 气泡底部中间的钻石指示图标
+ .toolTip-diamond-icon {
+ position: absolute;
+ left: 50%;
+ top: 100%;
+ transform: translate(-50%, -50%);
+ width: 28px;
+ height: 28px;
+ z-index: -1;
}
}
@@ -181,13 +212,16 @@
.pointer-icon {
position: relative;
z-index: 9;
- width: 52px;
- height: 76px;
+ width: 64px;
+ height: 86px;
margin-top: -54px;
&.bounce {
animation: pointer-bounce 600ms ease-in-out;
will-change: transform;
+ }
+ &.hidden-pointer {
+ opacity: 0;
}
}
}
@@ -259,18 +293,33 @@
padding-right: 32px;
.origin-icon {
- width: 88px;
- height: 88px;
+ width: 80px;
+ height: 80px;
}
}
.content-box {
position: relative;
overflow: hidden;
- border-radius: 32px 32px 0 0;
- padding: 40px;
- padding-bottom: 82px;
- background: #FFFFFF;
+ border-radius: 60px 60px 0px 0px;
+ background: @gary-theme;
+ padding-bottom: 64px;
+ padding-top: 96px;
+ .content-box-coantainer {
+ position: relative;
+ z-index: 1;
+ border-radius: 60px 60px 0px 0px;
+ padding: 0 48px;
+ padding-top: 32px ;
+ background: @gary-theme;
+ }
+ .content-box-top-bg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 276px;
+ }
.content-box-item {
display: flex;
@@ -299,11 +348,10 @@
}
.content-title {
- font-size: 36px;
+ font-size: 40px;
font-weight: 500;
- line-height: 50px;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
+ color: #141E27;
display: flex;
align-items: center;
justify-content: space-between;
@@ -316,12 +364,12 @@
}
.limit-tip {
- margin-top: 8px;
- font-size: 24px;
+ margin-top: 16px;
+ font-size: 28px;
font-weight: normal;
- line-height: 40px;
+ line-height: 32px;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.4);
+ color: rgba(20, 30, 39, 0.4);
.emphasis-txt {
color: #F58900
@@ -347,16 +395,16 @@
align-items: center;
justify-content: space-between;
margin-top: 32px;
- font-size: 28px;
+ font-size: 32px;
font-weight: 500;
- color: #3D3D3D;
+ color: @gold-text;
display: flex;
align-items: center;
- height: 112px;
- padding-left: 32px;
- padding-right: 37px;
- border-radius: 16px;
- background: #F1F2F6;
+ height: 120px;
+ padding-left: 40px;
+ padding-right: 40px;
+ border-radius: 40px;
+ background: rgba(204, 208, 215, 0.25);
.collect-icon {
flex-shrink: 0;
@@ -367,25 +415,7 @@
}
.site-active {
- background: linear-gradient(97deg, rgba(132, 117, 255, 0.2) 0%, rgba(125, 214, 255, 0.2) 103%) !important;
- box-sizing: border-box;
- position: relative;
-
- &::after {
- content: "";
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- border-radius: 16px;
- padding: 1px;
- background: linear-gradient(180deg, #AAA8FF -10%, rgba(41, 159, 255, 0.0469) 108%, rgba(0, 0, 0, 0) 108%);
- -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
- -webkit-mask-composite: xor;
- mask-composite: exclude;
- pointer-events: none;
- }
+ background: rgba(223, 179, 105, 0.2);
}
.no-service {
@@ -399,15 +429,17 @@
.site-confirm {
margin-top: 48px;
- height: 88px;
+ height: 104px;
display: flex;
justify-content: center;
align-items: center;
- border-radius: 16px;
- background: linear-gradient(95deg, #8475FF 0%, #7DD6FF 124%);
- font-size: 28px;
+ border-radius: 1000px;
+ box-sizing: border-box;
+ border: 4px solid #141E27;
+ font-size: 32px;
letter-spacing: normal;
- color: #FFFFFF;
+ color: #141E27;
+ font-weight: 760;
}
.no-confirm {
diff --git a/src/robotaxi/pages/select-map-site/index.tsx b/src/robotaxi/pages/select-map-site/index.tsx
index 3be1518..5a80e79 100644
--- a/src/robotaxi/pages/select-map-site/index.tsx
+++ b/src/robotaxi/pages/select-map-site/index.tsx
@@ -10,6 +10,7 @@
PIN_PICKUP_LOCATION_PAGE_VIEW_EVENT_ID
} from '../../constants/report-events';
import { withRobotaxiWhitelist } from '../../../utils/whitelist';
+import { withToast } from '../../components/toast/with-toast';
/**
* 地图选点页入口:负责 hook 编排与视图组装。
@@ -33,7 +34,7 @@
}, [handleScaleChange]);
const hasReportedPickupPageViewRef = useRef(false);
- const hasReportedDropoffPageViewRef = useRef(false);
+ const hasReportedDropoffPageViewRef = useRef(false);
useEffect(() => {
if (selectMapSite.pageType === 'start' && !hasReportedPickupPageViewRef.current) {
@@ -58,13 +59,13 @@
handleRegionChange={selectMapSite.handleRegionChange}
handleMarkerTap={selectMapSite.handleMarkerTap}
toBack={selectMapSite.toBack}
- backIcon='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/back-icon.png'
+ backIcon='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/back-icon-20260716.png'
openService={selectMapSite.openService}
toCoupon={selectMapSite.toCoupon}
toPersonalCenter={selectMapSite.toPersonalCenter}
- personalCenter='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/personal-center.png'
- coupon='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/coupon.png'
- contactService='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/contact-service.png'
+ personalCenter='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/personal-center-20260716.png'
+ coupon='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/coupon-20260723.png'
+ contactService='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/contact-service-20260716.png'
nearbyLoading={selectMapSite.nearbyLoading}
isAndroid={selectMapSite.isAndroid}
waitingRadarSrc={selectMapSite.waitingRadarSrc}
@@ -77,9 +78,9 @@
tooltipBgSrc={selectMapSite.tooltipBgSrc}
pageType={selectMapSite.pageType}
bounce={selectMapSite.bounce}
- pointer='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/pointer.png'
+ pointer='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/pointer-20260722.png'
toOrigin={selectMapSite.toOrigin}
- originIcon='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/origin-icon.png'
+ originIcon='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/origin-icon-20260716.png'
walkingDuration={selectMapSite.walkingDuration}
walkingDistance={selectMapSite.walkingDistance}
nearbySites={selectMapSite.nearbySites}
@@ -116,4 +117,4 @@
);
}
-export default withRobotaxiWhitelist(SelectMapSite);
+export default withRobotaxiWhitelist(withToast(SelectMapSite));
diff --git a/src/robotaxi/pages/share-trip/components/share-trip-view.tsx b/src/robotaxi/pages/share-trip/components/share-trip-view.tsx
index fd8bfe1..edd8155 100644
--- a/src/robotaxi/pages/share-trip/components/share-trip-view.tsx
+++ b/src/robotaxi/pages/share-trip/components/share-trip-view.tsx
@@ -45,6 +45,18 @@
// 与 begin-route 对齐:分钟按向下取整,秒按四舍五入。
const durationDisplay = hasRouteDuration ? formatRemainingDurationDisplay(routeDuration) : null; // 预计耗时展示文案
const isCanceledTrip = tripData?.tripStatus === TripStatus.CancelOnlyPickup || Number(tripData?.rawOrderStatus) === 0;
+ const tripStatusValue = tripData?.tripStatus;
+ // 起点置灰:已上车前往目的地(含之后)或已取消
+ const isStartSiteGray = isCanceledTrip || tripStatusValue >= TripStatus.InService;
+ // 终点置灰:已到达目的地(含之后)或已取消
+ const isEndSiteGray = isCanceledTrip || tripStatusValue >= TripStatus.ArrivedDestination;
+ // 起点右侧「XX:XX 上车」:已上车前往目的地起展示(非取消)
+ const showBoardingTime = !isCanceledTrip && tripStatusValue >= TripStatus.InService && !!tripData?.boardingTime;
+ // 终点右侧「XX:XX 到达」:已到达目的地起展示(非取消)
+ const showGetoffTime = !isCanceledTrip && tripStatusValue >= TripStatus.ArrivedDestination && !!tripData?.getoffTime;
+ const normalizedDurationDisplay = durationDisplay && durationDisplay.unit === '秒'
+ ? { value: 1, unit: '分钟' as const }
+ : durationDisplay; // 剩余时间小于 1 分钟时按 1 分钟展示,不展示秒
if (isExpired) {
return (
@@ -85,114 +97,97 @@
className={`map-entry ${showReminder ? 'has-tip' : ''}`}
onClick={handleContactService}
>
- <Image className='map-entry-icon' src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/contact-service.png' />
+ <Image className='map-entry-icon' src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/contact-service-20260716.png' />
</View>
<View className='container-inner'>
<View catchMove onClick={(event) => event.stopPropagation()}>
<View className='calling-car'>
<Image
className='calling-car-bg'
- src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/site-top-bg-20260507.png'
+ src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/card-bg-20260714.png'
/>
- <View className='calling-car-head'>
- {tripData?.tripStatus === TripStatus.VehicleApproaching && (
- <View className='calling-car-head-title'>
- {durationDisplay ? (
- <>
- 车辆还有
- <View className='calling-car-head-title-eta'>{durationDisplay.value}</View>
- {durationDisplay.unit}到达上车点
- </>
- ) : (
- '车辆正在前往上车点'
- )}
- </View>
- )}
- {tripData?.tripStatus === TripStatus.VehicleArrivedPickup && (
- <View className='calling-car-head-title'>车辆已到达上车点</View>
- )}
- {tripData?.tripStatus === TripStatus.InService && (
- <View className='calling-car-head-title'>
- {/* 预计
- <View className='calling-car-head-title-eta'>{durationDisplay.value}</View>
- {durationDisplay.unit}抵达目的地 */}
- 乘客已上车,正在前往目的地
- </View>
- )}
- {isCanceledTrip && (
- <View className='calling-car-head-title'>乘客已取消行程</View>
- )}
- {tripData?.tripStatus >= TripStatus.ArrivedDestination && (
- !isCanceledTrip && <View className='calling-car-head-title'>已到达目的地</View>
- )}
- </View>
- <View className='car-card'>
- <View className='car-card-detail'>
- <View>
- <View className='car-card-detail-num'>{tripData?.plateNumber || '--'}</View>
- <View className='car-card-detail-color'>
- {tripData?.vehicleColor || '--'}
- {/* <View className='car-card-detail-color-line'></View>
- 车身编码: {tripData?.vehicleCode || '--'} */}
- </View>
- </View>
- {tripData?.vehiclePictures && (
- <Image
- className='car-card-detail-img'
- src={
- safeParse(tripData?.vehiclePictures, {})?.url
- || 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/vehicle-default.png'
- }
- />
- )}
- </View>
- <View className='car-card-line'></View>
- <View className='car-card-temp'>
- <View className='car-card-temp-list'>
- <Image className='car-card-temp-img' src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/share-line.png" />
- <View className='car-card-temp-row'>
- <View className='car-card-temp-row-left'>
- <View className='dot start'></View>
- <View
- className={`car-card-temp-title ${
- tripData?.tripStatus === TripStatus.InService
- || (tripData?.tripStatus >= TripStatus.ArrivedDestination && !isCanceledTrip)
- ? 'disabled-text'
- : ''
- }`}
- >
- 起点:{tripData?.startPointName}
- </View>
- </View>
- {(tripData?.tripStatus === TripStatus.InService
- || (tripData?.tripStatus >= TripStatus.ArrivedDestination && !isCanceledTrip))
- && tripData?.boardingTime && (
- <View className='car-card-temp-time disabled-text'>
- {tripData.boardingTime} 上车
- </View>
+ {tripData?.vehiclePictures && (
+ <Image
+ className='calling-car-vehicle-pic'
+ src={
+ safeParse(tripData?.vehiclePictures, {})?.url
+ || 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/default-vehicle-img-20260724-v2.png'
+ }
+ />
+ )}
+ <View className='car-card-content'>
+ <View className='car-card'>
+ {/* 车辆信息:参考 VehicleInfoCard,左侧车牌·车身色,右侧车型 */}
+ <View className='car-card-detail'>
+ <View className='car-card-detail-plate'>
+ <View className='car-card-detail-num'>{tripData?.plateNumber || '--'}</View>
+ {tripData?.vehicleColor && (
+ <>
+ <View className='car-card-detail-dot'></View>
+ <View className='car-card-detail-color-text'>{tripData?.vehicleColor}</View>
+ </>
)}
</View>
- <View className='car-card-temp-row' style={{ marginTop: '12px' }}>
- <View className='car-card-temp-row-left'>
- <View className='dot end'></View>
- <View
- className={`car-card-temp-title ${
- tripData?.tripStatus >= TripStatus.ArrivedDestination && !isCanceledTrip
- ? 'disabled-text'
- : ''
- }`}
- >
- 终点:{tripData?.endPointName}
- </View>
- </View>
- {tripData?.tripStatus >= TripStatus.ArrivedDestination
- && !isCanceledTrip
- && tripData?.getoffTime && (
- <View className='car-card-temp-time disabled-text'>
- {tripData.getoffTime} 到达
- </View>
+ <View className='car-card-detail-model'>小鹏GX</View>
+ </View>
+ {/* 状态往下放:车辆信息下方展示行程状态,对齐前往目的地态结构 */}
+ <View className='calling-car-head'>
+ {tripData?.tripStatus === TripStatus.VehicleApproaching && (
+ <View className='calling-car-head-title'>
+ {durationDisplay && normalizedDurationDisplay && normalizedDurationDisplay.value === 1 ? (
+ '车辆即将到达上车点'
+ ) : (
+ '车辆正在前往上车点'
)}
+ {
+ durationDisplay && normalizedDurationDisplay && <View>{`${normalizedDurationDisplay.value}MIN`}</View>
+ }
</View>
+ )}
+ {tripData?.tripStatus === TripStatus.VehicleArrivedPickup && (
+ <View className='calling-car-head-title'>车辆已到达上车点</View>
+ )}
+ {tripData?.tripStatus === TripStatus.InService && (
+ <View className='calling-car-head-title'>
+ 乘客已上车,正在前往目的地
+ {
+ durationDisplay && normalizedDurationDisplay && <View>{`${normalizedDurationDisplay.value}MIN`}</View>
+ }
+ </View>
+ )}
+ {isCanceledTrip && (
+ <View className='calling-car-head-title'>乘客已取消行程</View>
+ )}
+ {tripData?.tripStatus >= TripStatus.ArrivedDestination && (
+ !isCanceledTrip && <View className='calling-car-head-title'>已到达目的地</View>
+ )}
+ </View>
+ </View>
+ {/* 站点信息卡:参考 cancel-site-box 布局与背景卡片样式 */}
+ <View className='share-site-box'>
+ <View className='share-site-box-icon'>
+ <View className='share-site-box-icon-start'></View>
+ <Image src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/dotted-line.png' />
+ <View className='share-site-box-icon-end'></View>
+ </View>
+ <View className='share-site-box-content'>
+ <View className='share-site-row'>
+ <View className={`share-site-name ${isStartSiteGray ? 'gray-text' : ''}`}>
+ {tripData?.startPointName}
+ </View>
+ {showBoardingTime && (
+ <View className='share-site-time'>{tripData.boardingTime} 上车</View>
+ )}
+ </View>
+ <View className='share-site-box-divider'></View>
+ <View className='share-site-row'>
+ <View className={`share-site-name ${isEndSiteGray ? 'gray-text' : ''}`}>
+ {tripData?.endPointName}
+ </View>
+ {showGetoffTime && (
+ <View className='share-site-time'>{tripData.getoffTime} 到达</View>
+ )}
+ </View>
</View>
</View>
</View>
diff --git a/src/robotaxi/pages/share-trip/hooks/use-map-markers.ts b/src/robotaxi/pages/share-trip/hooks/use-map-markers.ts
index 27c031a..d9680b4 100644
--- a/src/robotaxi/pages/share-trip/hooks/use-map-markers.ts
+++ b/src/robotaxi/pages/share-trip/hooks/use-map-markers.ts
@@ -2,6 +2,39 @@
import { useEffect, useRef, useState } from 'react';
import { TripStatus } from '../../../constants/trip-status';
+// 接驾态(前往上车点)/到达终点态 上车点三角图标(与 begin-route 对齐)
+const PICKUP_APPROACHING_ICON_URL =
+ 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/up-triangle-2026022.png';
+// 送驾态(前往目的地)/到达终点态 下车点三角图标(与 begin-route 对齐)
+const DROPOFF_INSERVICE_ICON_URL =
+ 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/down-triangle-20260722.png';
+// 站点名称 label(图标下方)统一配置,对齐 begin-route SITE_NAME_LABEL_BASE
+const SITE_NAME_LABEL_BASE = {
+ color: '#141E27',
+ fontSize: 12,
+ anchorX: 0,
+ anchorY: 9, // 图标下方 9px
+ textAlign: 'center' as const,
+ bgColor: '#00000000',
+ fontWeight: 'bold',
+ borderWidth: 0,
+ borderRadius: 0,
+ padding: 0
+};
+// 到达终点态:站点名称在图标下方 8px
+const SITE_NAME_LABEL_BELOW_LG = {
+ color: '#141E27',
+ fontSize: 12,
+ anchorX: 0,
+ anchorY: 8,
+ textAlign: 'center' as const,
+ bgColor: '#00000000',
+ fontWeight: 'bold',
+ borderWidth: 0,
+ borderRadius: 0,
+ padding: 0
+};
+
// 生成 marker 签名,供平移态判断结构是否实际变化。
const buildMarkersSignature = (
markers: any[],
@@ -74,59 +107,16 @@
export default function useMapMarkers(rawDeps?: Partial<Deps>) {
const deps = { ...defaultDeps, ...(rawDeps || {}) } as Deps;
// 内部"逻辑 markers"——真相态,所有内部判断(签名、是否含车标等)以此为准
+ // 注意:已废弃早期的 "Rebuild Dance" (两阶段移除恢复以派生新 ID 的做法)
+ // 原因是:如果在 translateMarker 动画执行期间,直接从声明式 markers 中删除该 ID,
+ // 原生层可能会因为找不到对应节点而无法正确销毁,导致地图上出现车标的"虚影"。
+ // 现在的做法是:保持车标 ID 永远不变(保证 React Diff 映射稳定),
+ // 遇到需要重置位置或旋转矩阵时,交由 use-vehicle-tracking 通过发送极短时间的 translateMarker 动画来强行覆盖原生状态。
const [markers, setMarkers] = useState<any[]>([]);
- // 对外"展示 markers"——返回给页面,通过 rebuild dance 制造短暂"无车标"窗口以重建原生 marker
- const [displayMarkers, setDisplayMarkers] = useState<any[]>([]);
const markersRef = useRef<any[]>([]);
const markersSignatureRef = useRef<string>("");
const carMarkerInitialized = useRef(false);
const tripStatus6MarkerRefreshedRef = useRef(false);
- const rebuildCarMarkerTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
- // 每次 rebuild 递增,用于派生新的 car marker id,避免原生层因 id 相同跳过销毁/重建导致旋转矩阵残留
- const carMarkerEpochRef = useRef(0);
-
- /**
- * 派生新的 car marker id:base + ((epoch % 1000) + 1) * 1000
- * - 与业务 base id(3/30)和其他 marker(pickup=1, dropoff=2)不冲突
- * - epoch 取模 1000 防止无限增长
- */
- const deriveNewCarMarkerId = (baseId: number) => {
- return baseId + ((carMarkerEpochRef.current % 1000) + 1) * 1000;
- };
-
- /**
- * 当逻辑 markers 变化时,对展示 markers 做"先移除车标 → 100ms 后用新 id 恢复"的两阶段提交。
- * 关键:恢复时给车标分配一个全新 markerId(base + epoch 派生),强制原生层创建新 marker 实例,
- * 不会因 id 相同被原生层复用,translateMarker 残留的旋转矩阵随之清除。
- */
- useEffect(() => {
- if (rebuildCarMarkerTimerRef.current) {
- clearTimeout(rebuildCarMarkerTimerRef.current);
- rebuildCarMarkerTimerRef.current = null;
- }
- const hasCarMarker = markers.some((m: any) => m.id === deps.vehicleMarkerId);
- if (!hasCarMarker) {
- setDisplayMarkers(markers);
- return;
- }
- carMarkerEpochRef.current += 1;
- const newCarId = deriveNewCarMarkerId(deps.vehicleMarkerId);
- // 通知外部:CarMarker 即将以 newCarId 重建,暂停 translateMarker 并切换到新 id
- deps.onCarMarkerWillRebuild(newCarId);
- // 第一步:展示层先去掉车标(按业务 base id 过滤),原生层销毁旧 car marker
- setDisplayMarkers(markers.filter((m: any) => m.id !== deps.vehicleMarkerId));
- // 第二步:100ms 后用 newCarId 重新挂载车标,原生层创建全新 marker 实例
- rebuildCarMarkerTimerRef.current = setTimeout(() => {
- rebuildCarMarkerTimerRef.current = null;
- setDisplayMarkers(markers.map((m: any) => m.id === deps.vehicleMarkerId ? { ...m, id: newCarId } : m));
- }, 100);
- return () => {
- if (rebuildCarMarkerTimerRef.current) {
- clearTimeout(rebuildCarMarkerTimerRef.current);
- rebuildCarMarkerTimerRef.current = null;
- }
- };
- }, [markers, deps.vehicleMarkerId]);
const shouldUsePureTranslateCarMarker =
[TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus)
@@ -140,17 +130,31 @@
if (deps.isAndroid && deps.tripStatus === TripStatus.InService) {
return null;
}
+ const pickupName = pickupSite?.name || '上车点';
+ // 接驾态 / 到达终点态:上车点改用 up-triangle 图标,名称移到图标下方(对齐 begin-route)
+ const isPickupApproaching = deps.tripStatus === TripStatus.VehicleApproaching;
+ const isPickupArrivedDest = [TripStatus.ArrivedDestination, TripStatus.TripFinished, TripStatus.CancelOnlyPickup].includes(deps.tripStatus);
+ const isPickupTriangle = isPickupApproaching || isPickupArrivedDest;
+ const pickupNameLabelConfig = isPickupApproaching ? SITE_NAME_LABEL_BASE : SITE_NAME_LABEL_BELOW_LG;
return {
id: 1,
latitude: pickupSite.latitude,
longitude: pickupSite.longitude,
- iconPath: "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/start-icon.png",
- width: 21,
- height: 31.5,
- anchor: { x: 0.5, y: 1 },
- zIndex: 1,
- name: pickupSite?.name || '上车点',
- ...(useAndroidNativeTrip1Callout
+ iconPath: isPickupTriangle
+ ? PICKUP_APPROACHING_ICON_URL
+ : "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/golden-site-20260708.png",
+ width: isPickupTriangle ? 40 : 18,
+ height: isPickupTriangle ? 40 : 18,
+ // 三角图标(up-triangle)实心尖端位于图片高度约 63% 处(下方为渐变阴影+透明留白),
+ // 锚点上移到 0.5 让尖端贴合并略微压入坐标点(路线末端);其余图标仍用底边锚点 1。
+ anchor: { x: 0.5, y: isPickupTriangle ? 0.5 : 1 },
+ zIndex: 1,
+ name: pickupName,
+ // 三角态:站点名称在图标下方展示
+ ...(isPickupTriangle
+ ? { label: { content: pickupName, ...pickupNameLabelConfig } }
+ : {}),
+ ...(useAndroidNativeTrip1Callout && !isPickupTriangle
? {
callout: {
content: deps.tripStatus === TripStatus.PublishRoute
@@ -183,18 +187,32 @@
// 分享行程只有从 5/6 及之后的状态才渲染下车点,避免 4 态提前入列引发地图多次缩放。
if (deps.tripStatus <= TripStatus.VehicleArrivedPickup) return null;
const dropoffName =
- [TripStatus.ArrivedDestination].includes(deps.tripStatus) ? '已到达' : dropoffSite.name || '目的地';
+ [TripStatus.ArrivedDestination, TripStatus.TripFinished, TripStatus.CancelOnlyPickup].includes(deps.tripStatus) ? '已到达' : dropoffSite.name || '目的地';
+ // 送驾态 / 到达终点态:下车点改用 down-triangle 图标,名称移到图标下方(对齐 begin-route)
+ const isDropoffInService = deps.tripStatus === TripStatus.InService;
+ const isDropoffArrivedDest = [TripStatus.ArrivedDestination, TripStatus.TripFinished, TripStatus.CancelOnlyPickup].includes(deps.tripStatus);
+ const isDropoffTriangle = isDropoffInService || isDropoffArrivedDest;
+ const dropoffLabelName = dropoffSite?.name || '目的地';
+ const dropoffNameLabelConfig = isDropoffInService ? SITE_NAME_LABEL_BASE : SITE_NAME_LABEL_BELOW_LG;
return {
id: 2,
latitude: dropoffSite.latitude,
longitude: dropoffSite.longitude,
- iconPath: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/end-icon.png',
- width: 21,
- height: 31.5,
- anchor: { x: 0.5, y: 1 },
- zIndex: 1,
+ iconPath: isDropoffTriangle
+ ? DROPOFF_INSERVICE_ICON_URL
+ : 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/black-site-20260708.png',
+ width: isDropoffTriangle ? 40 : 18,
+ height: isDropoffTriangle ? 40 : 18,
+ // 三角图标(down-triangle)实心尖端位于图片高度约 63% 处(下方为渐变阴影+透明留白),
+ // 锚点上移到 0.5 让尖端贴合并略微压入坐标点(路线末端);其余图标仍用底边锚点 1。
+ anchor: { x: 0.5, y: isDropoffTriangle ? 0.5 : 1 },
+ zIndex: 1,
name: dropoffName,
- ...(useAndroidNativeTrip1Callout || deps.tripStatus === TripStatus.InService
+ // 三角态:站点名称在图标下方展示
+ ...(isDropoffTriangle
+ ? { label: { content: dropoffLabelName, ...dropoffNameLabelConfig } }
+ : {}),
+ ...(useAndroidNativeTrip1Callout && !isDropoffTriangle
? {
callout: {
content: deps.tripStatus === TripStatus.PublishRoute
@@ -316,15 +334,14 @@
}, [markers]);
useEffect(() => {
- // 这里使用 displayMarkers + name 匹配(而非 id),因为 displayMarkers 中 car id 每次 rebuild 都会变成新 id
- const hasVehicleMarker = deps.statusRule.showCar && displayMarkers.some((item: any) => item.name === '车辆位置');
+ const hasVehicleMarker = deps.statusRule.showCar && markers.some((item: any) => item.id === deps.vehicleMarkerId);
if (!hasVehicleMarker) {
return;
}
Taro.nextTick(() => {
deps.onVehicleMarkerReady();
});
- }, [displayMarkers, deps.statusRule.showCar, deps.onVehicleMarkerReady]);
+ }, [markers, deps.statusRule.showCar, deps.onVehicleMarkerReady, deps.vehicleMarkerId]);
useEffect(() => {
const pickupSite =
@@ -402,8 +419,8 @@
]);
return {
- // 对外返回经过 rebuild dance 的展示态 markers;内部逻辑(applyMarkerState 等)仍读取 markers(逻辑态)
- markers: displayMarkers,
+ // 对外返回逻辑 markers,不再使用 rebuild dance
+ markers,
setMarkers,
markersRef,
markersSignatureRef,
diff --git a/src/robotaxi/pages/share-trip/hooks/use-polyline-render.ts b/src/robotaxi/pages/share-trip/hooks/use-polyline-render.ts
index f189f2c..7cb3039 100644
--- a/src/robotaxi/pages/share-trip/hooks/use-polyline-render.ts
+++ b/src/robotaxi/pages/share-trip/hooks/use-polyline-render.ts
@@ -259,7 +259,7 @@
lines.push({
id: PASSED_ROUTE_POLYLINE_ID,
points: passedRoutePoints,
- color: '#D7DDEF',
+ color: '#D6D9DC',
width: 10,
clickable: false,
dottedLine: false,
@@ -270,8 +270,10 @@
lines.push({
id: ROUTE_POLYLINE_BASE_ID,
points: displayRoutePoints,
- color: '#7B91FF',
- width: 10,
+ color: '#59B688',
+ width: 8,
+ borderColor: '#3D8F66',
+ borderWidth: 1,
clickable: true,
dottedLine: false,
arrowLine: true
@@ -281,7 +283,7 @@
lines.push({
id: WALKING_POLYLINE_ID_OFFSET,
points: walkingPoints,
- color: '#45A5FF',
+ color: '#DFB369',
width: deps.isAndroid ? 4 : 6,
dottedLine: true,
arrowLine: false
diff --git a/src/robotaxi/pages/share-trip/hooks/use-share-trip-runtime.ts b/src/robotaxi/pages/share-trip/hooks/use-share-trip-runtime.ts
index c5d93e7..7ca8e33 100644
--- a/src/robotaxi/pages/share-trip/hooks/use-share-trip-runtime.ts
+++ b/src/robotaxi/pages/share-trip/hooks/use-share-trip-runtime.ts
@@ -1,4 +1,5 @@
import Taro from '@tarojs/taro';
+import { showToast } from '../../../components/toast';
import { useEffect, useRef } from 'react';
import type { MutableRefObject } from 'react';
import { getDistance, mapOrderStatusToTripStatus } from '../../../../utils/Robotaxi-public-method';
@@ -173,6 +174,15 @@
source,
isStart ? ['startAddress', 'startSiteAddress', 'upAddress', 'originAddress'] : ['endAddress', 'endSiteAddress', 'downAddress', 'destinationAddress'],
) || '',
+ // 限停时长(分钟):接驾态「限停 X MIN」浮窗的数据源,与 begin-route startParkingTimeLimit 对齐
+ time: Number(
+ getFieldValue(
+ source,
+ isStart
+ ? ['startParkingTimeLimit', 'startStopTime', 'startParkingTime']
+ : ['endParkingTimeLimit', 'endStopTime', 'endParkingTime'],
+ ),
+ ) || 0,
};
};
@@ -446,7 +456,7 @@
return;
}
if (!deps.orderId) {
- Taro.showToast({ title: '分享订单不存在', icon: 'none' });
+ showToast({ title: '分享订单不存在', icon: 'none' });
return;
}
@@ -465,7 +475,7 @@
console.error('获取分享行程失败', error);
if (!errorToastShownRef.current) {
errorToastShownRef.current = true;
- Taro.showToast({ title: '获取分享行程失败', icon: 'none' });
+ showToast({ title: '获取分享行程失败', icon: 'none' });
}
};
@@ -553,7 +563,7 @@
pollCount += 1;
if (pollCount >= MAX_SHARE_TRIP_POLL_COUNT) {
stopPolling();
- Taro.showToast({ title: '分享行程数据可能不是最新,请手动刷新', icon: 'none' });
+ showToast({ title: '分享行程数据可能不是最新,请手动刷新', icon: 'none' });
return;
}
void fetchShareTripData();
diff --git a/src/robotaxi/pages/share-trip/hooks/use-vehicle-tracking.ts b/src/robotaxi/pages/share-trip/hooks/use-vehicle-tracking.ts
index 04082c1..23c5abc 100644
--- a/src/robotaxi/pages/share-trip/hooks/use-vehicle-tracking.ts
+++ b/src/robotaxi/pages/share-trip/hooks/use-vehicle-tracking.ts
@@ -128,48 +128,50 @@
}
};
- // 兜底定时器,防止回调丢失导致动画队列卡死
- fallbackTimer = setTimeout(() => {
- finishTranslate();
- }, TRANSLATE_DURATION_MS + 50);
+ // 兜底定时器,防止回调丢失导致动画队列卡死
+ fallbackTimer = setTimeout(() => {
+ finishTranslate();
+ }, task.duration + 50);
- Taro.nextTick(() => {
- const mapCtx = Taro.createMapContext('map');
- mapCtx.translateMarker({
- markerId: task.markerId,
- destination: {
- latitude: task.latitude,
- longitude: task.longitude,
- },
- autoRotate: false,
- moveWithRotate: true,
- rotate: task.rotate,
- duration: Math.max(task.duration, 100),
- animationEnd: () => {
- if (translateSessionId !== translateSessionIdRef.current) {
- return;
- }
- lastAppliedTranslateTaskRef.current = {
- latitude: task.latitude,
- longitude: task.longitude,
- rotate: task.rotate
- };
- finishTranslate();
- },
- fail: () => {
- if (translateSessionId !== translateSessionIdRef.current) {
- return;
- }
- lastAppliedTranslateTaskRef.current = {
- latitude: task.latitude,
- longitude: task.longitude,
- rotate: task.rotate
- };
- finishTranslate();
- }
- });
- });
- };
+ Taro.nextTick(() => {
+ const mapCtx = Taro.createMapContext("map");
+ mapCtx.translateMarker({
+ markerId: task.markerId,
+ destination: {
+ latitude: task.latitude,
+ longitude: task.longitude,
+ },
+ autoRotate: false,
+ moveWithRotate: true,
+ rotate: task.rotate,
+ // duration 为 0 时,微信地图在 iOS 存在 Bug(回调永远不触发或引发死锁)。
+ // 兜底为 1ms,既能实现视觉上的"瞬移"(解决重置旋转矩阵的需求),又能保证动画队列正常结束。
+ duration: Math.max(task.duration, 1),
+ animationEnd: () => {
+ if (translateSessionId !== translateSessionIdRef.current) {
+ return;
+ }
+ lastAppliedTranslateTaskRef.current = {
+ latitude: task.latitude,
+ longitude: task.longitude,
+ rotate: task.rotate,
+ };
+ finishTranslate();
+ },
+ fail: () => {
+ if (translateSessionId !== translateSessionIdRef.current) {
+ return;
+ }
+ lastAppliedTranslateTaskRef.current = {
+ latitude: task.latitude,
+ longitude: task.longitude,
+ rotate: task.rotate,
+ };
+ finishTranslate();
+ },
+ });
+ });
+ };
// 更新车辆位置并维护最近位置引用
const updateVehicleLocationThrottled = (
@@ -234,26 +236,27 @@
}, []);
/**
- * 暂停 translateMarker 并切换到新的 markerId。
- * 在 CarMarker 重建期间调用:原生层即将销毁旧 marker、用 newMarkerId 创建新 marker,
- * 这里同步把 vehicleMarkerIdRef 切到新 id,pending 任务的 markerId 也一并更新,
- * 确保 ready 恢复后 translateMarker 作用于新 marker。
- *
- * 关键:保留 pendingTranslateRef.current(只改 markerId)。
- * - 若 suspend 前已有 pending(最近一次推送写入但尚未 flush),ready 恢复后会直接消费它。
- * - 若 suspend 后再有新推送,translateCar useEffect 会用最新值覆盖 pending(markerId 用 ref 取最新)。
+ * 重置 translateMarker 并准备新状态。
+ * 背景:为了解决旧版方案(修改 marker ID)导致的车标虚影问题,我们现已将 marker ID 固定。
+ * 当需要强行纠正位置或清除原生层残留的旋转矩阵(rotate)时:
+ * 我们不再销毁/重建 marker,而是将 pending 动画的 duration 设为极小值(0,实际运行为 1ms),
+ * 让原生引擎立即"瞬移"到目标状态,从而强制覆盖掉内部的旧状态。
*/
- const suspendVehicleMarker = useCallback((newMarkerId: number) => {
+ const suspendVehicleMarker = useCallback((_newMarkerId: number) => {
isTranslatingRef.current = false;
+ // 清除上一次的记录,让下次 translate 时认为发生了位移和角度变化,必须派发指令
lastAppliedTranslateTaskRef.current = null;
vehicleMarkerReadyRef.current = false;
clearVehicleMarkerReadyTimer();
translateSessionIdRef.current += 1;
- vehicleMarkerIdRef.current = newMarkerId;
+
+ // 注意:不更新 vehicleMarkerIdRef.current,因为不再派生新 ID
+
+ // 保留 pending,并将 duration 设为 0 以强制原生层立即归位(重置旋转矩阵和位置)
if (pendingTranslateRef.current) {
pendingTranslateRef.current = {
...pendingTranslateRef.current,
- markerId: newMarkerId
+ duration: ROTATE_ONLY_DURATION_MS
};
}
}, []);
diff --git a/src/robotaxi/pages/share-trip/hooks/use-view-delegates.tsx b/src/robotaxi/pages/share-trip/hooks/use-view-delegates.tsx
index 08ab97f..53070d3 100644
--- a/src/robotaxi/pages/share-trip/hooks/use-view-delegates.tsx
+++ b/src/robotaxi/pages/share-trip/hooks/use-view-delegates.tsx
@@ -22,6 +22,8 @@
routeDuration: number | null;
customCalloutIconSrc: string;
arrivedTooltipReady: boolean;
+ /** 上车点(含限停时长 time,用于接驾态限停浮窗) */
+ startSite?: any;
};
type DurationDisplay = {
@@ -32,17 +34,30 @@
type AndroidCarCalloutContentProps = {
routeDistance: { value: number; unit: 'km' | 'm' } | null;
durationDisplay: DurationDisplay;
+ /** 是否接驾/送驾态(VehicleApproaching / InService):金色药丸样式,仅展示里程(KM/M),不展示分钟 */
+ isPickup: boolean;
};
const AndroidCarCalloutContent = memo(function AndroidCarCalloutContent(props: AndroidCarCalloutContentProps) {
- const { routeDistance, durationDisplay } = props;
+ const { routeDistance, durationDisplay, isPickup } = props;
return (
- <View className='custom-callout-content' style={{ padding: '6px 10px' }}>
- <View className='car-time' style={{ alignItems: 'baseline' }}>
+ <View
+ className={`custom-callout-content ${isPickup ? 'car-metric-callout' : ''}`.trim()}
+ style={isPickup ? undefined : { padding: '6px 10px' }}
+ >
+ <View className='car-time' style={{ alignItems: 'baseline', paddingBottom: 0 }}>
<View className='car-time-num'>{routeDistance?.value ?? 0}</View>
- <View style={{ flexShrink: 0 }}>{routeDistance?.unit === 'm' ? '米' : '公里'}</View>
- <View style={{ marginLeft: '2.5px' }} className='car-time-num'>{durationDisplay.value}</View>
- <View style={{ flexShrink: 0 }}>{durationDisplay.unit}</View>
+ <View style={{ flexShrink: 0 }}>
+ {isPickup
+ ? (routeDistance?.unit === 'm' ? 'M' : 'KM')
+ : (routeDistance?.unit === 'm' ? '米' : '公里')}
+ </View>
+ {!isPickup && (
+ <>
+ <View style={{ marginLeft: '2.5px' }} className='car-time-num'>{durationDisplay.value}</View>
+ <View style={{ flexShrink: 0 }}>{durationDisplay.unit}</View>
+ </>
+ )}
</View>
</View>
);
@@ -51,6 +66,7 @@
&& prevProps.routeDistance?.unit === nextProps.routeDistance?.unit
&& prevProps.durationDisplay.value === nextProps.durationDisplay.value
&& prevProps.durationDisplay.unit === nextProps.durationDisplay.unit
+ && prevProps.isPickup === nextProps.isPickup
));
export default function useViewDelegates(deps: Deps) {
@@ -73,8 +89,9 @@
<AndroidCarCalloutContent
routeDistance={deps.routeDistance}
durationDisplay={vehicleMetricDurationDisplay}
+ isPickup={[TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus)}
/>
- ), [deps.routeDistance, vehicleMetricDurationDisplay]);
+ ), [deps.routeDistance, vehicleMetricDurationDisplay, deps.tripStatus]);
const routeSummaryText = useMemo(() => (
deps.routeDistance && hasRouteDuration
? `${vehicleMetricDurationDisplay.value}${vehicleMetricDurationDisplay.unit} ${deps.routeDistance.value}${deps.routeDistance.unit === 'm' ? '米' : '公里'}`
@@ -107,9 +124,10 @@
) : null
) : (
<>
- {(item.name !== '车辆位置' && !(item.id === 2 && [TripStatus.ArrivedDestination].includes(deps.tripStatus)) && !item.hideNameCallout) && (
+ {(item.name !== '车辆位置' && !(item.id === 2 && [TripStatus.ArrivedDestination, TripStatus.TripFinished, TripStatus.CancelOnlyPickup].includes(deps.tripStatus)) && !(item.id === 2 && [TripStatus.InService].includes(deps.tripStatus)) && !(item.id === 1 && [TripStatus.VehicleApproaching, TripStatus.ArrivedDestination, TripStatus.TripFinished, TripStatus.CancelOnlyPickup].includes(deps.tripStatus)) && !item.hideNameCallout) && (
+ <>
<CoverView className={`custom-callout-content ${deps.isAndroid && 'txt-center '}`} style={{ zIndex: 1 }}>
- <CoverView style={{ lineHeight: '22px', whiteSpace: 'nowrap', display: 'flex' }}>
+ <CoverView style={{ whiteSpace: 'nowrap', display: 'flex' }}>
{item.name}
{!deps.isAndroid && item.name.length > 16 && <CoverView style={{ color: "transparent",width: "3px" }}>占</CoverView>}
</CoverView>
@@ -118,24 +136,51 @@
<CoverImage className='custom-callout-icon' src={deps.customCalloutIconSrc} />
)}
</CoverView>
+ {deps.tripStatus === TripStatus.VehicleArrivedPickup && (
+ <CoverImage
+ className='callout-diamond-icon'
+ src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/diamond-icon-20260708.png'
+ />
+ )}
+ </>
)}
{(item.name === '车辆位置' && [TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus) && !deps.isAndroid) && (
<CoverView
- className='custom-callout-content'
- style={{ padding: '6px 10px', minWidth: '40px', justifyContent: 'center', zIndex: 2 }}
+ className='custom-callout-content car-metric-callout'
+ style={{ minWidth: '40px', justifyContent: 'center', zIndex: 2 }}
>
<CoverView className='car-time'>
<CoverView className='car-time-num'>{deps.routeDistance?.value ?? 0}</CoverView>
- <CoverView>{deps.routeDistance?.unit === 'm' ? '米' : '公里'}</CoverView>
- <CoverView style={{ marginLeft: '2.5px' }} className='car-time-num'>{vehicleMetricDurationDisplay.value}</CoverView>
- <CoverView>{vehicleMetricDurationDisplay.unit}</CoverView>
+ <CoverView>{deps.routeDistance?.unit === 'm' ? 'M' : 'KM'}</CoverView>
</CoverView>
</CoverView>
)}
- {(item.id === 2 && [TripStatus.ArrivedDestination].includes(deps.tripStatus) && !deps.isAndroid && deps.mapScale >= 20) && (
- <CoverView className='custom-callout-content'>
- 已到达
- </CoverView>
+ {(item.id === 1 && deps.tripStatus === TripStatus.VehicleApproaching && !item.hideNameCallout) && (
+ <>
+ <CoverView className='custom-callout-content site-status-callout' style={{ zIndex: 1 }}>
+ <CoverImage
+ className='site-status-car-icon'
+ src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/golden-car-icon-20260708.png'
+ />
+ <CoverView className='site-status-time'>限停{deps.startSite?.time}MIN</CoverView>
+ </CoverView>
+ <CoverImage
+ className='callout-diamond-icon'
+ src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/diamond-icon-20260708.png'
+ />
+ </>
+ )}
+ {(item.id === 1 && [TripStatus.ArrivedDestination, TripStatus.TripFinished, TripStatus.CancelOnlyPickup].includes(deps.tripStatus) && !item.hideNameCallout) && (
+ <CoverImage
+ className='site-status-end-img'
+ src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/start-site-20260722.png'
+ />
+ )}
+ {(item.id === 2 && [TripStatus.InService, TripStatus.ArrivedDestination, TripStatus.TripFinished, TripStatus.CancelOnlyPickup].includes(deps.tripStatus) && !item.hideNameCallout) && (
+ <CoverImage
+ className='site-status-end-img'
+ src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/end-site-20260722.png'
+ />
)}
</>
)}
@@ -147,6 +192,7 @@
deps.isAndroid,
deps.routeDistance,
deps.tripStatus,
+ deps.startSite?.time,
routeSummaryText,
vehicleMetricDurationDisplay,
]);
@@ -159,7 +205,9 @@
if (deps.isAndroid && item.callout) {
return null;
}
- if (shouldHideAndroidMarkerCallout) {
+ // 完单/到达态起终点富浮窗(start-site/end-site 图片)在 Android 也需渲染,放开常规隐藏规则
+ const isSettledSiteMarker = (item.id === 1 || item.id === 2) && [TripStatus.TripFinished, TripStatus.CancelOnlyPickup].includes(deps.tripStatus);
+ if (shouldHideAndroidMarkerCallout && !isSettledSiteMarker) {
return null;
}
const isWaiting = [TripStatus.Publishing, TripStatus.CallingCar].includes(deps.tripStatus) && index === 0;
diff --git a/src/robotaxi/pages/share-trip/index.less b/src/robotaxi/pages/share-trip/index.less
index 856ce7c..e404312 100644
--- a/src/robotaxi/pages/share-trip/index.less
+++ b/src/robotaxi/pages/share-trip/index.less
@@ -1,3 +1,5 @@
+@import '../../style/ui-variables.less';
+
.home-container {
display: flex;
flex-direction: column;
@@ -29,24 +31,24 @@
.expired-text {
font-size: 30px;
line-height: 46px;
- color: rgba(0, 0, 0, 0.4);
+ color: @black-text-04;
}
.expired-sub-text {
font-size: 30px;
line-height: 46px;
- color: rgba(0, 0, 0, 0.4);
+ color: @black-text-04;
}
.expired-btn {
- width: 264px;
- height: 96px;
+ width: 358px;
+ height: 98px;
/* 自动布局 */
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
- border-radius: 16px;
+ border-radius: 100px;
box-sizing: border-box;
/* 按钮色/面性线性按钮 */
border: 2px solid #181916;
@@ -94,11 +96,11 @@
display: flex;
align-items: center;
justify-content: center;
- border-radius: 16px;
+ border-radius: 20px;
+ background: #FFFFFF;
box-shadow: 0px 4px 16px 0px rgba(84, 92, 143, 0.1);
- background: rgba(255, 255, 255, 1);
overflow: hidden;
- padding: 4px 4px;
+ padding: 20px 16px;
z-index: 300;
&.has-tip {
@@ -106,8 +108,8 @@
}
.map-entry-icon {
- width: 64px;
- height: 64px;
+ width: 48px;
+ height: 48px;
}
}
@@ -116,13 +118,13 @@
align-items: flex-end;
justify-content: center;
font-size: 24px;
- color: rgba(0, 0, 0, 0.9);
+ color: #FFFFFF;
}
.car-time-num {
font-size: 36px;
line-height: 40px;
- color: #6d7fd9;
+ color: #FFFFFF;
}
.car-time-tip {
@@ -135,21 +137,25 @@
.calling-car-head {
position: relative;
z-index: 9;
- font-size: 32px;
+ margin-top: 32px;
+ font-size: 24px;
font-weight: 500;
- color: rgba(0, 0, 0, 0.9);
-
+ color: #FFFFFF;
+ width: 100%;
+ height: 74px;
+ display: flex;
+ align-items: center;
+ padding: 0 32px;
+ border-radius: 24px;
+ overflow: hidden;
+ background-image: url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/start-status-20260714.png');
+ background-repeat: no-repeat;
+ background-size: 100% 100%;
.calling-car-head-title {
+ width: 100%;
display: flex;
align-items: center;
- }
-
- .calling-car-head-title-eta {
- font-size: 40px;
- font-weight: 500;
- color: #6d7fd9;
- margin: 0 4px;
- line-height: 44px;
+ justify-content: space-between;
}
}
@@ -204,17 +210,27 @@
.calling-car {
position: relative;
z-index: 9;
- padding: 40px 24px 80px 24px;
- background: #f2f3f7;
- border-top: 2px solid #fff;
- border-radius: 40px 40px 0 0;
+ // padding: 40px 24px 80px 24px;
+ background: @gary-theme;
+ border-radius: 60px 60px 0 0;
- &-bg {
+ .calling-car-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
- height: 326px;
+ height: 282px;
+ z-index: 1;
+ }
+
+ // 车型图:参考 calling-car 的 calling-car-vehicle-pic,浮于背景图顶部右侧
+ .calling-car-vehicle-pic {
+ position: absolute;
+ top: 24px;
+ right: 24px;
+ width: 320px;
+ height: 224px;
+ z-index: 10;
}
&-content {
@@ -275,15 +291,14 @@
}
}
}
-
- .car-card {
+ .car-card-content {
position: relative;
- z-index: 1;
- margin-top: 32px;
- border-radius: 24px;
- background: #ffffff;
- padding: 32px 16px 32px 36px;
-
+ z-index: 9;
+ border-radius: 60px 60px 0px 0px;
+ background: @gary-theme;;
+ padding: 48px;
+ }
+ .car-card {
.car-card-line {
margin-top: 11px;
margin-bottom: 29px;
@@ -298,101 +313,115 @@
align-items: center;
justify-content: space-between;
- &-num {
- font-size: 48px;
- line-height: 64px;
- letter-spacing: normal;
- font-weight: 500;
- color: rgba(0, 0, 0, 0.9);
- }
-
- &-color {
- margin-top: 4px;
+ .car-card-detail-plate {
display: flex;
align-items: center;
- font-size: 24px;
- letter-spacing: normal;
- color: rgba(0, 0, 0, 0.6);
-
- &-line {
- width: 1.4px;
- height: 20px;
- background: #d8d8d8;
- margin: 0 14px;
- }
}
- &-img {
+ &-num {
+ font-size: 44px;
+ font-weight: 500;
+ line-height: 56px;
+ letter-spacing: normal;
+ color: @black-text-main;
+ }
+
+ .car-card-detail-dot {
flex-shrink: 0;
- width: 300px;
- height: 140px;
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: @black-text-main;
+ margin: 0 8px;
+ }
+
+ .car-card-detail-color-text {
+ font-size: 40px;
+ font-weight: 500;
+ line-height: 56px;
+ letter-spacing: normal;
+ color: @black-text-main;
+ }
+
+ .car-card-detail-model {
+ font-size: 24px;
+ line-height: 44px;
+ letter-spacing: normal;
+ color: rgba(0, 0, 0, 0.6);
+ }
+ }
+ }
+
+ // 站点信息卡:参考 cancel-site-box 的布局与背景卡片样式,独立于车辆信息卡
+ .share-site-box {
+ margin-top: 32px;
+ display: flex;
+ gap: 16px;
+ border-radius: 40px;
+ background: rgba(204, 208, 215, 0.25);
+ padding: 48px 32px 42px 32px;
+
+ .share-site-box-icon {
+ padding: 16px 0;
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ .share-site-box-icon-start {
+ width: 16px;
+ height: 16px;
+ background: @gold-light;
+ border-radius: 50%;
+ }
+
+ .share-site-box-icon-end {
+ width: 16px;
+ height: 16px;
+ background: @black-text-main;
+ border-radius: 50%;
+ }
+
+ image {
+ flex: 1;
+ width: 2px;
+ margin: 7px 0;
+ height: fit-content;
}
}
- .car-card-temp {
- &-list {
- width: 100%;
- padding-right: 22px;
- display: flex;
- flex-direction: column;
- position: relative;
- }
+ .share-site-box-content {
+ flex: 1;
- &-img {
- position: absolute;
- top: 28px;
- left: 5px;
- width: 2px;
- height: 47px;
- transform: translateX(50%);
- }
-
- &-row {
+ .share-site-row {
display: flex;
align-items: center;
justify-content: space-between;
+ }
- &-left {
- display: flex;
- align-items: center;
+ .share-site-name {
+ font-size: 32px;
+ line-height: 48px;
+ color: @black-text-main;
- .dot {
- width: 12px;
- height: 12px;
- border-radius: 50%;
- margin-right: 12px;
- flex-shrink: 0;
-
- &.start {
- background: #768aff;
- }
-
- &.end {
- background: #8bb654;
- }
- }
+ &.gray-text {
+ color: @black-text-04;
}
}
- .car-card-temp-title {
- font-size: 24px;
- line-height: 40px;
- color: rgba(0, 0, 0, 0.9);
-
- &.disabled-text {
- color: rgba(0, 0, 0, 0.4);
- }
- }
-
- .car-card-temp-time {
+ .share-site-time {
+ flex-shrink: 0;
min-width: 120px;
+ margin-left: 12px;
+ text-align: right;
font-size: 24px;
line-height: 40px;
- color: rgba(0, 0, 0, 0.9);
+ color: @black-text-04;
+ }
- &.disabled-text {
- color: rgba(0, 0, 0, 0.4);
- }
+ .share-site-box-divider {
+ margin: 34px 32px 40px 0;
+ border-top: 1px solid rgba(204, 208, 215, 0.4);
}
}
}
@@ -422,16 +451,76 @@
.custom-callout-content {
display: flex;
+ position: relative;
align-items: center;
white-space: nowrap;
border-radius: 16px;
- background: rgba(255, 255, 255, 1);
+ background: #000000;
font-size: 24px;
font-weight: 400;
- color: rgba(0, 0, 0, 0.9);
- line-height: 38px;
+ color: #FFFFFF;
+ line-height: normal;
box-shadow: 0px 4px 16px 0px rgba(45, 48, 64, 0.2);
- padding: 8px 20px;
+ padding: 20px;
+}
+
+// 接驾/送驾态车辆气泡:金色圆角药丸,仅展示里程(KM/M),对齐 begin-route .car-metric-callout
+.car-metric-callout {
+ background: #DFB369;
+ color: #FFFFFF;
+ border-radius: 58px;
+ padding: 12px 20px;
+}
+
+.car-metric-callout .car-time {
+ color: #FFFFFF;
+ padding-bottom: 0;
+}
+
+.car-metric-callout .car-time-num {
+ color: #FFFFFF;
+}
+
+// 送驾态终点浮窗:直接展示 end-site 图片,对齐 begin-route .site-status-end-img
+.site-status-end-img {
+ width: 96px;
+ height: 78px;
+ display: block;
+}
+
+// 接驾态上车点限停浮窗:黑底 + 金色车图标 + 金色「限停 X MIN」,对齐 begin-route .site-status-callout
+.custom-callout-content.site-status-callout {
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ background: #000000;
+ border-radius: 32px;
+ padding: 12px 20px;
+}
+
+.site-status-car-icon {
+ width: 32px;
+ height: 32px;
+ display: block;
+}
+
+.site-status-time {
+ font-size: 18px;
+ font-weight: 500;
+ line-height: normal;
+ text-align: center;
+ letter-spacing: 0.04em;
+ color: #FFD181;
+ margin-top: 6px;
+}
+
+// 浮窗底部的钻石指示图标,对齐 begin-route .callout-diamond-icon
+.callout-diamond-icon {
+ width: 28px;
+ height: 28px;
+ margin-top: -14px;
+ flex-shrink: 0;
+ z-index: 0;
}
.route-summary-callout {
@@ -445,8 +534,7 @@
padding-right: 20px;
box-sizing: border-box;
border-radius: 27px;
- background: #ffffff;
- border: 2px solid rgba(109, 127, 217, 0.18);
+ background: #000000;
box-shadow: 0px 6px 18px 0px rgba(45, 48, 64, 0.12);
overflow: visible;
}
@@ -457,7 +545,7 @@
font-size: 24px;
font-weight: 500;
line-height: 38px;
- color: rgba(0, 0, 0, 0.9);
+ color: #FFFFFF;
white-space: nowrap;
text-align: center;
display: flex;
@@ -481,7 +569,7 @@
.car-time-num {
font-size: 36px;
letter-spacing: normal;
- color: #6D7FD9;
+ color: #FFFFFF;
margin-bottom: 4px;
}
@@ -533,8 +621,11 @@
.waiting-callout-bg {
position: relative;
- height: 70px;
- min-width: 155px;
+ padding: 3px 40px 0 20px;
+ height: 80px;
+ min-width: 186px;
+ max-width: 400px;
+ box-sizing: border-box;
text-align: center;
display: flex;
align-items: flex-start;
@@ -562,18 +653,24 @@
padding: 0 20px;
box-sizing: border-box;
white-space: nowrap;
+ min-width: 198px;
+ max-width: 400px;
+ box-sizing: border-box;
}
.waiting-callout-txt {
z-index: 200;
border-radius: 16px;
- color: rgba(0, 0, 0, 0.9);
+ color: #FFFFFF;
font-size: 24px;
font-weight: 400;
- background: rgba(255, 255, 255, 1);
- padding: 8px 20px;
+ background: #000000;
+ padding: 20px;
line-height: 48px;
display: flex;
+ min-width: 198px;
+ max-width: 400px;
+ box-sizing: border-box;
}
.pr-6 {
@@ -582,4 +679,4 @@
.pr-0 {
padding-right: 0;
-}
\ No newline at end of file
+}
diff --git a/src/robotaxi/pages/share-trip/index.tsx b/src/robotaxi/pages/share-trip/index.tsx
index 1e36612..1195699 100644
--- a/src/robotaxi/pages/share-trip/index.tsx
+++ b/src/robotaxi/pages/share-trip/index.tsx
@@ -16,6 +16,7 @@
import { checkOtp } from '../../../utils/tools';
import { safeMakePhoneCall } from '../../../utils/Robotaxi-public-method';
import { withRobotaxiWhitelist } from '../../../utils/whitelist';
+import { withToast } from '../../components/toast/with-toast';
const ROUTE_SUMMARY_MARKER_ID = 999001;
@@ -71,7 +72,7 @@
[TripStatus.VehicleApproaching]: { showPickup: true, showDropoff: false, showCar: true, showRoute: true, showWalking: false, viewport: 'route', translateCar: true },
[TripStatus.VehicleArrivedPickup]: { showPickup: true, showDropoff: false, showCar: true, showRoute: false, showWalking: false, viewport: 'pickup_center', translateCar: true },
[TripStatus.InService]: { showPickup: false, showDropoff: true, showCar: true, showRoute: true, showWalking: false, viewport: 'route', translateCar: true },
- [TripStatus.ArrivedDestination]: { showPickup: false, showDropoff: true, showCar: false, showRoute: true, showWalking: false, viewport: 'order_detail', translateCar: false },
+ [TripStatus.ArrivedDestination]: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: false, viewport: 'order_detail', translateCar: false },
[TripStatus.TripFinished]: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: false, viewport: 'order_detail', translateCar: false },
[TripStatus.PaymentSuccess]: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: false, viewport: 'order_detail', translateCar: false },
[TripStatus.CancelOnlyPickup]: { showPickup: true, showDropoff: true, showCar: false, showRoute: false, showWalking: false, viewport: 'order_detail', translateCar: false },
@@ -95,6 +96,10 @@
const mapLongitude = longitude ?? 116.397128; // 地图兜底经度
const mapLatitude = latitude ?? 39.916527; // 地图兜底纬度
const vehicleMarkerId = !isAndroid && tripStatus === TripStatus.InService ? 30 : 3; // 车辆 marker ID
+ // 地图渲染归一化:到达目的地及之后(完单/已支付)且非取消态,地图层统一按 TripFinished 渲染,对齐 begin-route
+ const mapTripStatus = (tripStatus >= TripStatus.ArrivedDestination && tripStatus !== TripStatus.CancelOnlyPickup)
+ ? TripStatus.TripFinished
+ : tripStatus;
const {
customCalloutIconSrc,
@@ -103,7 +108,7 @@
tripStatus,
assetUrls: {
customCalloutIconUrl: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/maker-tooltip-right.png',
- waitingRadarUrl: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/waiting-radar.webp',
+ waitingRadarUrl: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/waiting-radar-20260728.webp',
}
});
@@ -124,7 +129,7 @@
const arrivedMarkerReady = tripStatus !== TripStatus.ArrivedDestination || typeof vehicleDirection === 'number';
const { markers } = useMapMarkers({
- tripStatus,
+ tripStatus: mapTripStatus,
statusRule,
isAndroid,
startSite,
@@ -330,7 +335,7 @@
const { renderMapOverlays } = useViewDelegates({
isAndroid,
- tripStatus,
+ tripStatus: mapTripStatus,
mapScale,
markers: markersWithRouteSummary,
shouldShowWaitingRadar: false,
@@ -347,6 +352,7 @@
routeDuration: displayRouteDuration,
customCalloutIconSrc,
arrivedTooltipReady,
+ startSite,
}); // 地图浮层视图委托
const goHome = () => {
@@ -390,4 +396,4 @@
);
}
-export default withRobotaxiWhitelist(ShareTrip);
+export default withRobotaxiWhitelist(withToast(ShareTrip));
diff --git a/src/robotaxi/pages/site-search/components/favorite-popup/index.tsx b/src/robotaxi/pages/site-search/components/favorite-popup/index.tsx
index f53ef83..dbf8e44 100644
--- a/src/robotaxi/pages/site-search/components/favorite-popup/index.tsx
+++ b/src/robotaxi/pages/site-search/components/favorite-popup/index.tsx
@@ -1,4 +1,5 @@
import Taro from '@tarojs/taro';
+import { showToast } from '../../../../components/toast';
import { View, Text, Image, ScrollView } from '@tarojs/components';
import { useEffect, useState } from 'react';
import './index.less';
@@ -80,10 +81,10 @@
await collectPoint(payload);
setList(prev => prev.filter(f => (f.id || f.pointId || f.stationId) !== (item.id || item.pointId || item.stationId)));
- Taro.showToast({ title: '已取消收藏', icon: 'none' });
+ showToast({ title: '已取消收藏', icon: 'none' });
} catch (e) {
console.log('取消收藏失败', e);
- Taro.showToast({ title: '取消收藏失败', icon: 'none' });
+ showToast({ title: '取消收藏失败', icon: 'none' });
}
};
@@ -96,7 +97,7 @@
if (itemType === 2) {
const children = Array.isArray(item?.children) ? item.children : [];
if (children.length === 0) {
- Taro.showToast({
+ showToast({
title: '此场站无有效站点',
icon: 'none',
});
diff --git a/src/robotaxi/pages/site-search/components/site-list-item.tsx b/src/robotaxi/pages/site-search/components/site-list-item.tsx
index e1e19cc..7800293 100644
--- a/src/robotaxi/pages/site-search/components/site-list-item.tsx
+++ b/src/robotaxi/pages/site-search/components/site-list-item.tsx
@@ -1,4 +1,5 @@
import Taro from '@tarojs/taro';
+import { showToast } from '../../../components/toast';
import { View, Image } from '@tarojs/components';
import { useState, useEffect } from 'react';
import { collectPoint, getCollectPoints } from '../../../api/site-search';
@@ -83,7 +84,7 @@
}
const firstSite = stationChildren[0];
if (!firstSite) {
- Taro.showToast({
+ showToast({
title: '此场站无有效站点',
icon: 'none'
});
@@ -107,7 +108,7 @@
});
if (Number(res?.total) >= MAX_COLLECT_COUNT) {
- Taro.showToast({
+ showToast({
title: `最多收藏${MAX_COLLECT_COUNT}个站点`,
icon: 'none'
});
@@ -153,7 +154,7 @@
await collectPoint(payload);
setIsCollected(!isCollected);
- Taro.showToast({
+ showToast({
title: isCollected ? '已取消收藏' : '收藏成功',
icon: 'none'
});
diff --git a/src/robotaxi/pages/site-search/index.less b/src/robotaxi/pages/site-search/index.less
index 53fbff3..55a2b05 100644
--- a/src/robotaxi/pages/site-search/index.less
+++ b/src/robotaxi/pages/site-search/index.less
@@ -1,27 +1,27 @@
+@import "../../style/ui-variables.less";
+
.search-page {
- background: #F2F3F7;
+ background: @gary-theme;
min-height: 100vh;
padding: 16px 32px;
display: flex;
flex-direction: column;
+ padding-bottom: 40px;
.search-card {
display: flex;
align-items: center;
justify-content: space-between;
- border-radius: 24px;
- background: #FFFFFF;
- box-sizing: border-box;
- border: 2px solid #F1F1F1;
- box-shadow: 0px 16px 60px 0px rgba(0, 0, 0, 0.05);
- padding: 22px 36px 24px 32px;
+ border-radius: 40px;
+ background: #fff;
+ padding: 32px;
.search-icon {
/* 自动布局子元素 */
- width: 10px;
- height: 10px;
+ width: 16px;
+ height: 16px;
z-index: 0;
- background: #768AFF;
+ background: @gold-light;
border-radius: 50%;
margin-right: 16px;
}
@@ -45,16 +45,16 @@
}
.end-icon {
- background: #8BB654;
- }
+ background: @gold-light ;
+ }
.search-input {
flex: 1;
height: 40px;
- font-size: 28px;
+ font-size: 32px;
font-weight: normal;
- line-height: 40px;
- color: rgba(0, 0, 0, 0.9);
+ line-height: normal;
+ color: @black-text-main;
text-align: left;
padding: 0;
margin: 0;
@@ -69,9 +69,9 @@
.fast-box {
display: flex;
align-items: center;
- margin-top: 24px;
- border-radius: 20px;
- background: #FFFFFF;
+ margin-top: 16px;
+ border-radius: 52px;
+ background: rgba(204, 208, 215, 0.25);
position: relative;
.fast-split {
@@ -109,22 +109,24 @@
font-size: 24px;
line-height: 44px;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
+ color: @black-text-main;
}
}
}
.history-item {
display: flex;
- align-items: center;
+ align-items: flex-start;
+ justify-content: flex-start;
margin-bottom: 32px;
- padding: 0 24px;
+ padding: 0px 16px 0px 32px;
.history-icon {
flex-shrink: 0;
width: 32px;
height: 32px;
margin-right: 16px;
+ margin-top: 4px;
}
.history-info {
@@ -137,8 +139,8 @@
font-weight: 500;
line-height: normal;
letter-spacing: normal;
- color: #3D3D3D;
- margin-bottom: 8px;
+ color: @black-text-main;
+ margin-bottom: 4px;
}
.history-address-box {
@@ -152,7 +154,7 @@
font-weight: normal;
line-height: normal;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.4);
+ color: @black-text-04;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -169,9 +171,9 @@
justify-content: center;
.collect-icon {
- width: 24px;
- height: 22.91px;
- margin-bottom: 10.54px;
+ width: 40px;
+ height: 40px;
+ margin-bottom: 4px;
}
.history-distance {
@@ -180,7 +182,7 @@
font-weight: normal;
line-height: normal;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.4);
+ color: @black-text-04;
}
}
}
@@ -201,7 +203,8 @@
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 14px 16px;
- margin-bottom: 24px;
+ margin-bottom: 16px;
+ margin-top: 16px;
view {
border-radius: 12px;
@@ -212,7 +215,7 @@
line-height: 48px;
text-align: center;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
+ color: @black-text-main;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -249,7 +252,7 @@
line-height: normal;
letter-spacing: normal;
/* 文字色/黑色/辅助文字 40% */
- color: rgba(0, 0, 0, 0.4);
+ color: @black-text-04;
}
.more-icon {
@@ -273,43 +276,21 @@
}
.history-card {
- border-radius: 16px;
+ border-radius: 40px;
background: #FFFFFF;
- margin-top: 24px;
+ margin-top: 16px;
+ overflow: hidden;
.card-header {
border-radius: 16px 16px 0 0;
- background: rgba(0, 0, 0, 0.03);
- padding: 16px 24px;
+ background: #fff;
+ padding: 24px 32px 16px 32px;
font-family: PingFang SC;
font-size: 24px;
font-weight: 500;
line-height: normal;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
- }
-
- .card-content {
- padding: 16px 0 32px 0;
- }
- }
-
- .recommend-card {
- border-radius: 16px;
- background: #FFFFFF;
- border: 2px solid #FFFFFF;
- margin-top: 24px;
-
- .card-header {
- background: linear-gradient(94deg, rgba(132, 117, 255, 0.15) 0%, rgba(125, 214, 255, 0.15) 104%);
- padding: 16px 24px;
- border-radius: 16px 16px 0 0;
- font-family: PingFang SC;
- font-size: 24px;
- font-weight: 500;
- line-height: normal;
- letter-spacing: normal;
- color: rgba(0, 0, 0, 0.9);
+ color: @black-text-main;
}
.card-content {
@@ -317,22 +298,65 @@
}
}
- .nonOperational-card {
- border-radius: 16px;
- background: #FFFFFF;
- margin-top: 24px;
- margin-bottom: 60px;
+ .recommend-card {
+ position: relative;
+ border-radius: 40px;
+ background-color: #fff;
+ margin-top: 16px;
+ overflow: hidden;
.card-header {
- background: rgba(0, 0, 0, 0.03);
- padding: 16px 24px;
+ padding: 24px 32px 16px 32px;
+ font-family: PingFang SC;
+ font-size: 24px;
+ font-weight: 500;
+ line-height: normal;
+ letter-spacing: normal;
+ color: @gold-light;
+ }
+
+ .card-content {
+ padding: 24px 0;
+ }
+ .recommend-content-box {
+ position: relative;
+ z-index: 2;
+ border-radius: 40px;
+ overflow: hidden;
+ }
+ .recommend-list-content {
+ border-radius: 40px;
+ background: #FFFFFF;
+ overflow: hidden;
+ transition: max-height 0.3s ease;
+ }
+ .recommend-card-bg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 686px;
+ height: 260px;
+ z-index: 1;
+ overflow: hidden;
+ }
+ }
+
+ .nonOperational-card {
+ border-radius: 40px;
+ background: #FFFFFF;
+ margin-top: 16px;
+ margin-bottom: 60px;
+ overflow: hidden;
+
+ .card-header {
+ padding: 24px 32px 16px 32px;
border-radius: 16px 16px 0 0;
font-family: PingFang SC;
font-size: 24px;
font-weight: 500;
line-height: normal;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.4);
+ color: @black-text-04;
}
.card-content {
@@ -353,7 +377,7 @@
}
.highlight-match {
- color: #5A960A !important;
+ color:@gold-text !important;
}
}
@@ -366,10 +390,10 @@
margin: 176px 135px 0 135px;
font-size: 32px;
letter-spacing: normal;
- color: rgba(0, 0, 0, 0.6);
+ color: @black-text-04;
.no-data-image {
width: 480px;
height: 400px;
}
-}
\ No newline at end of file
+}
diff --git a/src/robotaxi/pages/site-search/index.tsx b/src/robotaxi/pages/site-search/index.tsx
index b5c920f..409b124 100644
--- a/src/robotaxi/pages/site-search/index.tsx
+++ b/src/robotaxi/pages/site-search/index.tsx
@@ -64,6 +64,8 @@
} from '../../constants/confirm-ride-report';
import { hasActiveTripOrderInStorage } from '../../utils/order';
import { withRobotaxiWhitelist } from '../../../utils/whitelist';
+import { showToast } from '../../components/toast';
+import { withToast } from '../../components/toast/with-toast';
import { TripStatus } from '../../constants/trip-status';
function SiteSearchPage() {
@@ -134,7 +136,11 @@
) => {
// 检查是否有行程订单
const modifySession = Taro.getStorageSync(MODIFY_DESTINATION_SESSION_KEY);
- if (!modifySession?.active && hasActiveTripOrderInStorage()) {
+ const hasActiveTrip = hasActiveTripOrderInStorage();
+ // 增加 hasActiveTrip 联合判断,防止上一单异常终止导致 modifySession 缓存残留,污染新下单流程
+ const isModifyDestinationActive = modifySession?.active && hasActiveTrip;
+
+ if (!isModifyDestinationActive && hasActiveTrip) {
Taro.reLaunch({
url: '/robotaxi/pages/begin-route/index?skipPageFlag=1',
});
@@ -144,7 +150,7 @@
const resolvedDistanceConfig = await ensureDistanceConfigReady();
const startSiteCache = Taro.getStorageSync('startSite');
if (startSiteCache?.electronicAreaId && item?.electronicAreaId && startSiteCache.electronicAreaId !== item.electronicAreaId) {
- Taro.showToast({
+ showToast({
title: '起终点需在同一运营区域',
icon: 'none',
})
@@ -168,7 +174,7 @@
// 1. isSameSite:坐标、ID或名称一致视为同一站点。
// 2. isTooClose:物理直线距离小于后台配置的 minDistance(如有)。
// 命中任一条件,即阻断并提示“建议步行前往”。
- if (modifySession?.active) {
+ if (isModifyDestinationActive) {
const isPickupTripStatus = [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(modifySession.tripStatus);
if (isPickupTripStatus) {
const isSameSite = isSameModifyDestinationSite(startSiteCache, item);
@@ -177,7 +183,7 @@
endSite: item,
minDistance: resolvedDistanceConfig?.minDistance
});
-
+
if (isSameSite || isTooClose) {
setTipType(ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE);
setShowTipPopup(true);
@@ -186,10 +192,10 @@
}
}
// 检查是否与上一个选择的终点相同
- if (modifySession?.active && isSameModifyDestinationSite(modifySession?.previousEndSite, item)) {
+ if (isModifyDestinationActive && isSameModifyDestinationSite(modifySession?.previousEndSite, item)) {
Taro.setStorageSync(MODIFY_DESTINATION_PENDING_SAME_SITE_TOAST_KEY, 1);
}
- if (modifySession?.active) {
+ if (isModifyDestinationActive) {
Taro.setStorageSync(MODIFY_DESTINATION_CANDIDATE_KEY, item);
Taro.setStorageSync(MODIFY_DESTINATION_SKIP_AC_QUERY_ON_RETURN_KEY, 1); // 修改目的地返回行程页时跳过一次空调/温度状态查询
} else {
@@ -197,7 +203,7 @@
}
writeConfirmRideSiteSourceType('end', sourceType);
completeConfirmRideSelectionSession(startSiteCache, item);
- const returnToBeginRouteUrl = modifySession?.active
+ const returnToBeginRouteUrl = isModifyDestinationActive
? '/robotaxi/pages/begin-route/index?skipPageFlag=1&fromModifyDestination=1'
: (Taro.getStorageSync('orderId') ? '/robotaxi/pages/begin-route/index?skipPageFlag=1' : '/robotaxi/pages/begin-route/index');
Taro.reLaunch({
@@ -377,15 +383,15 @@
sourceType = UNKNOWN_CONFIRM_RIDE_SOURCE_TYPE
) => {
if (item?.operationStatus === 2) {
- Taro.showToast({
- title: '当前点位暂未开放,请重新选点',
+ showToast({
+ title: '当前站点暂未开放',
icon: 'none',
})
return
}
if (!isNonOperational) {
- Taro.showToast({
- title: '当前点位暂未开放,请重新选点',
+ showToast({
+ title: '当前站点暂未开放',
icon: 'none',
})
return
@@ -394,7 +400,7 @@
const resolvedDistanceConfig = await ensureDistanceConfigReady();
const endSiteCache = Taro.getStorageSync('endSite');
if (endSiteCache?.electronicAreaId && item?.electronicAreaId && endSiteCache.electronicAreaId !== item.electronicAreaId) {
- Taro.showToast({
+ showToast({
title: '起终点需在同一运营区域',
icon: 'none',
})
@@ -506,9 +512,9 @@
key={item.id || index}
itemKey={item.id || index}
item={item}
- iconSrc='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/address.png'
- starSrc='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/active-star.png'
- noStarSrc='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/no-star.png'
+ iconSrc='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/address-20260630.png'
+ starSrc='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/active-star-20260726.png'
+ noStarSrc='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/no-star-20260726.png'
showDistance={showDistance}
onSelect={(selectedItem: any) => onSelect(selectedItem, index)}
renderHighlightedText={renderHighlightedText}
@@ -594,6 +600,44 @@
<Text className='fast-item-text'>地图选点</Text>
</View>
</View>
+ {/* 推荐站点 */}
+ {
+ recommendList.length > 0 &&
+ <View className='recommend-card'>
+ <Image className='recommend-card-bg' src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/seadrch-site-bg-20260714.png' />
+ <View className='recommend-content-box'>
+ <View className='card-header'>{searchValue ? '搜索站点' : '推荐站点'}</View>
+ <View className='card-content recommend-list-content' id='recommend-card-content' key={`recommend-${showAllRecommend ? 'expanded' : 'collapsed'}`}>
+ {
+ renderSiteItems(visibleRecommend, (item, index = 0) => {
+ if (pageType === 'start' && searchValue.trim()) {
+ reportSearchResultEvent(item, index, recommendList.length);
+ }
+ if (pageType === 'end' && trimmedSearchValue) {
+ reportSearchResultEvent(item, index, recommendList.length);
+ }
+ if (pageType === 'end' && !trimmedSearchValue) {
+ reportRecommendedDropoffStationEvent(item, index, recommendList.length);
+ }
+ onChangeSelectSite(
+ item,
+ true,
+ trimmedSearchValue
+ ? SEARCH_CONFIRM_RIDE_SOURCE_TYPE
+ : RECOMMENDED_CONFIRM_RIDE_SOURCE_TYPE
+ );
+ }, true, true)
+ }
+ </View>
+ {recommendList.length > RECOMMEND_SHOW_COUNT && (
+ <View className='show-more' hoverClass='none' onClick={toggleRecommendShow}>
+ <Text className='more-text'>{showAllRecommend ? '收起' : '展示更多'}</Text>
+ <Image className={`more-icon ${showAllRecommend ? 'rotate-up' : ''}`} src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/arrow.png"></Image>
+ </View>
+ )}
+ </View>
+ </View>
+ }
{/* 历史记录 */}
{
(sortedHistorySite.length > 0 && !searchValue) &&
@@ -620,41 +664,6 @@
)}
</View>
}
- {/* 推荐站点 */}
- {
- recommendList.length > 0 &&
- <View className='recommend-card'>
- <View className='card-header'>{searchValue ? '搜索站点' : '推荐站点'}</View>
- <View className='card-content list-content' id='recommend-card-content' key={`recommend-${showAllRecommend ? 'expanded' : 'collapsed'}`}>
- {
- renderSiteItems(visibleRecommend, (item, index = 0) => {
- if (pageType === 'start' && searchValue.trim()) {
- reportSearchResultEvent(item, index, recommendList.length);
- }
- if (pageType === 'end' && trimmedSearchValue) {
- reportSearchResultEvent(item, index, recommendList.length);
- }
- if (pageType === 'end' && !trimmedSearchValue) {
- reportRecommendedDropoffStationEvent(item, index, recommendList.length);
- }
- onChangeSelectSite(
- item,
- true,
- trimmedSearchValue
- ? SEARCH_CONFIRM_RIDE_SOURCE_TYPE
- : RECOMMENDED_CONFIRM_RIDE_SOURCE_TYPE
- );
- }, true, true)
- }
- </View>
- {recommendList.length > RECOMMEND_SHOW_COUNT && (
- <View className='show-more' hoverClass='none' onClick={toggleRecommendShow}>
- <Text className='more-text'>{showAllRecommend ? '收起' : '展示更多'}</Text>
- <Image className={`more-icon ${showAllRecommend ? 'rotate-up' : ''}`} src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/arrow.png"></Image>
- </View>
- )}
- </View>
- }
{/* 非营运区域 */}
{
nonOperationalSite.length > 0 &&
@@ -669,16 +678,19 @@
}
{
isInitLoaded && canShowSearchFallback && fallbackNearbyList.length > 0 && (!siteList.length && (!searchValue ? !sortedNearbyList.length : true) && !nonOperationalSite.length && (!searchValue ? !sortedHistorySite.length : true)) &&
- <View className='history-card'>
- <View className='card-header'>暂无搜索结果,为您推荐附近站点</View>
- <View className='card-content list-content'>
- {
- renderSiteItems(
- fallbackNearbyList,
- (item) => onChangeSelectSite(item, true, RECOMMENDED_CONFIRM_RIDE_SOURCE_TYPE),
- true
- )
- }
+ <View className='recommend-card'>
+ <Image className='recommend-card-bg' src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/seadrch-site-bg-20260714.png' />
+ <View className='recommend-content-box'>
+ <View className='card-header'>暂无搜索结果,为您推荐附近站点</View>
+ <View className='card-content recommend-list-content'>
+ {
+ renderSiteItems(
+ fallbackNearbyList,
+ (item) => onChangeSelectSite(item, true, RECOMMENDED_CONFIRM_RIDE_SOURCE_TYPE),
+ true
+ )
+ }
+ </View>
</View>
</View>
}
@@ -698,4 +710,4 @@
)
}
-export default withRobotaxiWhitelist(SiteSearchPage);
+export default withRobotaxiWhitelist(withToast(SiteSearchPage));
diff --git a/src/robotaxi/style/ui-variables.less b/src/robotaxi/style/ui-variables.less
new file mode 100644
index 0000000..4cac50d
--- /dev/null
+++ b/src/robotaxi/style/ui-variables.less
@@ -0,0 +1,26 @@
+// 字体
+@font-face {
+ font-family: "XPSANS-Bold";
+ src: url("https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/font/XPSANS-Bold.otf")
+ format("opentype");
+ font-weight: bold;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "XPSANS-Regular";
+ src: url("https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/font/XPSANS-Regular.otf")
+ format("opentype");
+ font-weight: normal;
+ font-style: normal;
+}
+
+@font-xpsans-bold: "XPSANS-Bold"; // 加粗字体
+@font-xpsans-regular: "XPSANS-Regular"; // 常规字体
+
+@gold-text: #A68042; // 文字金
+@gold-light: #DFB369; // 主题金
+@black-text-main: #141E27; // 重要文字
+@black-text-04: rgba(20, 30, 39, 0.4); // 次要文字
+@gary-theme: #F1F5FA; // 浅色面板
+@gray-2-theme: #E8ECF1; //深灰色面板
diff --git a/src/robotaxi/utils/api-logger.ts b/src/robotaxi/utils/api-logger.ts
new file mode 100644
index 0000000..e4c09f0
--- /dev/null
+++ b/src/robotaxi/utils/api-logger.ts
@@ -0,0 +1,26 @@
+/**
+ * 座舱控制类接口(座椅通风加热、空调控温、香氛等)请求/响应日志工具。
+ * 统一打印请求入参、响应结果与异常,方便调试和线上问题定位。
+ */
+
+/** 日志统一前缀,便于在控制台按关键字过滤。 */
+const LOG_PREFIX = '[座舱控制接口]';
+
+/**
+ * 包裹接口请求 Promise,打印请求入参、响应与异常后透传结果(不改变原有 resolve/reject 行为)。
+ * @param tag 接口中文名,如 '查询座椅通风加热状态'
+ * @param payload 请求入参(用于日志展示)
+ * @param promise request(...) 返回的 Promise
+ */
+export function withApiLog<T>(tag: string, payload: any, promise: Promise<T>): Promise<T> {
+ console.log(`${LOG_PREFIX}【${tag}】请求参数:`, payload);
+ return promise
+ .then((res) => {
+ console.log(`${LOG_PREFIX}【${tag}】响应结果:`, res);
+ return res;
+ })
+ .catch((err) => {
+ console.error(`${LOG_PREFIX}【${tag}】请求异常:`, err);
+ return Promise.reject(err);
+ });
+}
diff --git a/src/utils/Robotaxi-public-method.ts b/src/utils/Robotaxi-public-method.ts
index d38997e..bda96d9 100644
--- a/src/utils/Robotaxi-public-method.ts
+++ b/src/utils/Robotaxi-public-method.ts
@@ -12,6 +12,15 @@
GEOFENCE_TYPE_STATION
} from '../robotaxi/constants/geofence';
import { APPID } from '../constants/app'
+import {
+ MODIFY_DESTINATION_SESSION_KEY,
+ MODIFY_DESTINATION_CANDIDATE_KEY,
+ MODIFY_DESTINATION_OVERRIDE_KEY,
+ MODIFY_DESTINATION_ROUTE_POINTS_KEY,
+ MODIFY_DESTINATION_SKIP_AC_QUERY_ON_RETURN_KEY,
+ MODIFY_DESTINATION_SOCKET_STATUS_KEY,
+ MODIFY_DESTINATION_PENDING_SAME_SITE_TOAST_KEY
+} from '../robotaxi/constants/modify-destination';
/**
* 安全的 JSON 解析函数
@@ -520,9 +529,9 @@
...site,
name: site.name,
// id: index + 1,
- iconPath: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/site-poi.png',
- width: 15,
- height: 15,
+ iconPath: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/golden-site-20260708.png',
+ width: 18,
+ height: 18,
anchor: { x: 0.5, y: 0.5 }
}));
};
@@ -586,9 +595,9 @@
latitude: p.lat,
longitude: p.lng
})),
- strokeWidth: 2,
- strokeColor: type === GEOFENCE_TYPE_STATION ? GEOFENCE_STATION_STROKE_COLOR : '#A1B1FF',
- fillColor: type === GEOFENCE_TYPE_STATION ? GEOFENCE_STATION_FILL_COLOR : '#6d7fd933',
+ strokeWidth: type === GEOFENCE_TYPE_STATION ? 2 : 1.5,
+ strokeColor: type === GEOFENCE_TYPE_STATION ? GEOFENCE_STATION_STROKE_COLOR : '#DFB369',
+ fillColor: type === GEOFENCE_TYPE_STATION ? GEOFENCE_STATION_FILL_COLOR : '#DFB36929',
dashArray: type === GEOFENCE_TYPE_STATION ? GEOFENCE_STATION_DASH_ARRAY : [0, 0],
zIndex: 1
}
@@ -1024,6 +1033,14 @@
export const clearOrderTripStorage = () => {
Taro.removeStorageSync('orderId');
Taro.removeStorageSync('tripStatus');
+ // 一并清空修改目的地的全部会话缓存,防止脏数据残留污染后续的新订单
+ Taro.removeStorageSync(MODIFY_DESTINATION_SESSION_KEY);
+ Taro.removeStorageSync(MODIFY_DESTINATION_CANDIDATE_KEY);
+ Taro.removeStorageSync(MODIFY_DESTINATION_OVERRIDE_KEY);
+ Taro.removeStorageSync(MODIFY_DESTINATION_ROUTE_POINTS_KEY);
+ Taro.removeStorageSync(MODIFY_DESTINATION_SKIP_AC_QUERY_ON_RETURN_KEY);
+ Taro.removeStorageSync(MODIFY_DESTINATION_SOCKET_STATUS_KEY);
+ Taro.removeStorageSync(MODIFY_DESTINATION_PENDING_SAME_SITE_TOAST_KEY);
};
/**
diff --git a/src/utils/weather.ts b/src/utils/weather.ts
index 22a445d..ea4d97a 100644
--- a/src/utils/weather.ts
+++ b/src/utils/weather.ts
@@ -2,58 +2,56 @@
export const DEFAULT_TEMPERATURE_UNIT = '°C'; // 默认温度单位
/**
- * 根据高德天气返回的天气描述,匹配系统定义的天气图标。
- * 目前暂无真实切图,全部占位使用 weatherIcon,后续有了切图后替换返回的变量即可。
- * @param weatherDesc 高德天气返回的天气文字
+ * 原始天气类型(接口返回)→ 前端展示文案 的分类表。
+ * 依据产品需求文档「原始天气类型(接口返回) / 前端展示文案」对照表逐行整理,
+ * 每一项 text 为前端展示文案,types 为归入该文案的原始天气类型集合。
+ */
+const WEATHER_DESC_CATEGORIES: Array<{ text: string; types: string[] }> = [
+ { text: '晴', types: ['晴'] },
+ { text: '多云', types: ['少云', '多云', '晴间多云'] },
+ { text: '阴', types: ['阴'] },
+ { text: '阵雨', types: ['阵雨', '强阵雨'] },
+ { text: '雷阵雨', types: ['雷阵雨', '强雷阵雨'] },
+ { text: '雷雨冰雹', types: ['雷阵雨并伴有冰雹'] },
+ { text: '小雨', types: ['毛毛雨/细雨', '小雨'] },
+ { text: '中雨', types: ['中雨'] },
+ { text: '大雨', types: ['大雨'] },
+ { text: '暴雨', types: ['暴雨', '大暴雨', '特大暴雨', '极端降雨', '大雨-暴雨'] },
+ { text: '小到中雨', types: ['小雨-中雨'] },
+ { text: '中到大雨', types: ['中雨-大雨'] },
+ { text: '小雪', types: ['雪', '小雪'] },
+ { text: '中雪', types: ['中雪'] },
+ { text: '大雪', types: ['大雪'] },
+ { text: '暴雪', types: ['暴雪'] },
+ { text: '小到中雪', types: ['小雪-中雪'] },
+ { text: '中到大雪', types: ['中雪-大雪'] },
+ { text: '雨夹雪', types: ['雨雪天气', '雨夹雪', '阵雨夹雪'] },
+ { text: '冻雨', types: ['冻雨'] },
+ { text: '大雾', types: ['雾', '轻雾', '大雾'] },
+ { text: '浓雾', types: ['浓雾', '强浓雾', '特强浓雾'] },
+ { text: '霾', types: ['霾'] },
+ { text: '中度霾', types: ['中度霾'] },
+ { text: '重度霾', types: ['重度霾', '严重霾'] },
+ { text: '微风', types: ['平静', '微风', '和风', '清风', '有风'] },
+ { text: '大风', types: ['强风/劲风', '疾风', '大风', '烈风'] },
+ { text: '飓风', types: ['风暴', '狂爆风', '热带风暴', '飓风'] },
+ { text: '扬沙', types: ['浮尘', '扬沙'] },
+ { text: '沙尘暴', types: ['沙尘暴', '强沙尘暴'] },
+ { text: '龙卷风', types: ['龙卷风'] },
+ { text: '酷热', types: ['热'] },
+ { text: '严寒', types: ['冷'] },
+ { text: '', types: ['未知'] },
+];
+
+/**
+ * 根据高德天气返回的原始天气类型,转换为前端统一展示文案。
+ * @param weatherDesc 高德天气返回的原始天气类型(接口返回值)
+ * @returns 前端展示文案;未命中分类表时原样返回入参,空值返回空字符串
*/
export const getWeatherIconByDesc = (weatherDesc: string) => {
- if (!weatherDesc) return 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/weather.png';
-
- // 优先级建议:雷阵雨/冰雹 > 大雨/暴雨 > 小雨/中雨 > 雪 > 雨夹雪 > 沙尘 > 雾霾 > 大风 > 多云/阴 > 晴
-
- // 雷阵雨/冰雹
- if (['雷阵雨', '强雷阵雨', '雷阵雨并伴有冰雹'].includes(weatherDesc)) {
- return 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/thundershower.png';
- }
- // 大雨/暴雨
- if (['大雨', '暴雨', '大暴雨', '特大暴雨', '强阵雨', '极端降雨', '中雨-大雨', '大雨-暴雨', '暴雨-大暴雨', '大暴雨-特大暴雨'].includes(weatherDesc)) {
- return 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/rainstorm.png';
- }
- // 小雨/中雨
- if (['阵雨', '小雨', '中雨', '毛毛雨/细雨', '雨', '小雨-中雨', '阵雨天气'].includes(weatherDesc)) {
- return 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/rain.png';
- }
- // 雪
- if (['雪', '阵雪', '小雪', '中雪', '大雪', '暴雪', '小雪-中雪', '中雪-大雪', '大雪-暴雪'].includes(weatherDesc)) {
- return 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/snow.png';
- }
- // 雨夹雪
- if (['雨夹雪', '阵雨夹雪', '雨雪天气', '冻雨'].includes(weatherDesc)) {
- return 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/sleet.png';
- }
- // 沙尘/浮尘
- if (['浮尘', '扬沙', '沙尘暴', '强沙尘暴'].includes(weatherDesc)) {
- return 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/sandDust.png';
- }
- // 雾/霾
- if (['雾', '浓雾', '强浓雾', '轻雾', '大雾', '特强浓雾', '霾', '中度霾', '重度霾', '严重霾'].includes(weatherDesc)) {
- return 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/fog.png';
- }
- // 大风/台风
- if (['有风', '和风', '微风', '清风', '强风/劲风', '疾风', '大风', '烈风', '风暴', '狂暴风', '飓风', '热带风暴', '龙卷风'].includes(weatherDesc)) {
- return 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/wind.png';
- }
- // 多云/阴
- if (['晴间多云', '多云', '阴'].includes(weatherDesc)) {
- return 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/cloudy.png';
- }
- // 晴
- if (['晴', '少云', '平静'].includes(weatherDesc)) {
- return 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/sunny.png';
- }
-
- // 其他/未知:['热', '冷', '未知']
- return 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/weather.png';
+ if (!weatherDesc) return '';
+ const category = WEATHER_DESC_CATEGORIES.find((item) => item.types.includes(weatherDesc));
+ return category ? category.text : weatherDesc;
};
/**
--
Gitblit v1.7.1