zjk
2026-07-29 429208957f30ab9eedc5533a81aaa284048d09f5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
import Taro from "@tarojs/taro";
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 {
    airConditionerUnlock,
    queryTemperatureByOrderId,
    queryVehicleAirConditionerSwitch,
    setTemperature
} 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';
 
// 入口卡片图标(复用 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;
    orderId?: string;
    pageDidShowVersion?: number;
    /** 后端推送:当前被指派车辆是否仍在执行上一单(连环单) */
    hasPreviousOrder?: boolean;
};
 
export default function TemperatureControl({ tripStatus, orderId, pageDidShowVersion = 0, hasPreviousOrder = false }: Props) {
    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'>();
    const TEMP_MIN = 15.5;
    const TEMP_MAX = 32.5;
    const TEMP_UPDATE_DEBOUNCE_MS = 500;
    const DEFAULT_FIRST_TEMP = 23.5;
    const isTogglingAc = acLoadingKey === 'ac';
 
    const canToggleSwitch = tripStatus === TripStatus.VehicleApproaching && !isTogglingAc;
    const canAdjustTemperature = canToggleSwitch && acStatusLoaded && acSwitchOn && typeof tempStatus === 'number';
    const shouldShowTemperatureUnit = typeof tempStatus === 'number' && tempStatus > TEMP_MIN && tempStatus < TEMP_MAX;
    // 温度展示文案:到达最高温显示 HI,到达最低温显示 LO,其余显示具体温度值
    const tempDisplayText = typeof tempStatus === 'number'
        ? (tempStatus >= TEMP_MAX ? 'HI' : (tempStatus <= TEMP_MIN ? 'LO' : tempStatus.toFixed(1)))
        : '--';
    const isAtMinTemperature = typeof tempStatus === 'number' && tempStatus <= TEMP_MIN;
    const isAtMaxTemperature = typeof tempStatus === 'number' && tempStatus >= TEMP_MAX;
    const decreaseButtonDisabled = !canAdjustTemperature || isAtMinTemperature;
    const increaseButtonDisabled = !canAdjustTemperature || isAtMaxTemperature;
    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(() => {
        const skipOnce = Number(Taro.getStorageSync(MODIFY_DESTINATION_SKIP_AC_QUERY_ON_RETURN_KEY)) === 1;
        skipQueryOnThisCycleRef.current = skipOnce;
        if (skipOnce) {
            Taro.removeStorageSync(MODIFY_DESTINATION_SKIP_AC_QUERY_ON_RETURN_KEY);
        }
    }, [pageDidShowVersion, tripStatus, orderId]);
 
    const parseAcSwitchState = (res: any) => {
        const raw = res?.open ?? res?.isOpen ?? res?.switchStatus ?? res?.airConditionerSwitch ?? res;
        if (typeof raw === 'boolean') {
            return raw;
        }
        if (typeof raw === 'number') {
            return raw === 1;
        }
        if (typeof raw === 'string') {
            const normalized = raw.trim().toLowerCase();
            return normalized === '1' || normalized === 'true' || normalized === 'on';
        }
        return false;
    };
 
    const syncStorageToBackend = async (targetOrderId: string, acOpen: boolean, temperature: number) => {
        try {
            await airConditionerUnlock({ orderId: targetOrderId, open: acOpen });
        } catch {}
        try {
            await setTemperature({ orderId: targetOrderId, targetTemperature: temperature });
        } catch {}
    };
 
    const handleFirstTimeInit = async (targetOrderId: string) => {
        setAcSwitchOn(true);
        setTempStatus(DEFAULT_FIRST_TEMP);
        lastConfirmedTempRef.current = DEFAULT_FIRST_TEMP;
        setAcStatusLoaded(true);
        setAcTogglingTarget(true);
        startLoading('ac');
 
        let acSuccess = false;
        let tempSuccess = false;
 
        try {
            await airConditionerUnlock({ orderId: targetOrderId, open: true });
            acSuccess = true;
        } catch {}
 
        try {
            const res = await setTemperature({ orderId: targetOrderId, targetTemperature: DEFAULT_FIRST_TEMP });
            if (res) {
                tempSuccess = true;
            }
        } catch {}
 
        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 });
    };
 
    const handleCalibration = async (targetOrderId: string, storageAcOpen: boolean, storageTemp: number) => {
        const snapshotSeq = userActionSeqRef.current;
        calibrationSeqRef.current = snapshotSeq;
 
        const CALIBRATION_TIMEOUT_MS = 1500;
 
        type CalibrationResult =
            | { type: 'success'; acOpen: boolean; temperature: number }
            | { type: 'timeout' }
            | { type: 'error' };
 
        const queryPromise: Promise<CalibrationResult> = Promise.all([
            queryVehicleAirConditionerSwitch({ orderId: targetOrderId }),
            queryTemperatureByOrderId({ orderId: targetOrderId })
        ]).then(([acRes, tempRes]): CalibrationResult => {
            const acOpen = parseAcSwitchState(acRes);
            const parsedTemp = Number(tempRes);
            const temperature = Number.isNaN(parsedTemp) ? storageTemp : parsedTemp;
            return { type: 'success', acOpen, temperature };
        }).catch((): CalibrationResult => ({ type: 'error' }));
 
        const timeoutPromise: Promise<CalibrationResult> = new Promise((resolve) =>
            setTimeout(() => resolve({ type: 'timeout' }), CALIBRATION_TIMEOUT_MS)
        );
 
        const result = await Promise.race([queryPromise, timeoutPromise]);
 
        if (userActionSeqRef.current > snapshotSeq) {
            return;
        }
 
        if (result.type === 'timeout' || result.type === 'error') {
            // 超时或出错:展示 storageData,并同步远程
            setAcSwitchOn(storageAcOpen);
            setTempStatus(storageTemp);
            lastConfirmedTempRef.current = storageTemp;
            setAcStatusLoaded(true);
 
            if (storageAcOpen) {
                // 空调开启 → 正常下发开关 + 温度
                syncStorageToBackend(targetOrderId, storageAcOpen, storageTemp);
            } else {
                // 空调关闭 → 只下发关闭,不下发温度(下发温度会自动开空调)
                try { await airConditionerUnlock({ orderId: targetOrderId, open: false }); } catch {}
            }
            return;
        }
 
        // 接口在限定时间内返回:以接口数据为准
        setAcSwitchOn(result.acOpen);
        setTempStatus(result.temperature);
        lastConfirmedTempRef.current = result.temperature;
        setAcStatusLoaded(true);
        saveAcStorage(targetOrderId, { acOpen: result.acOpen, temperature: result.temperature });
    };
 
    useEffect(() => {
        if (tripStatus !== TripStatus.VehicleApproaching) {
            // 离开接驾态:重置去重标记,允许下次进入接驾态时重新查询一次。
            queriedOrderIdRef.current = '';
            return;
        }
        // 连环派单场景:后端明确告知车辆仍在执行上一单(hasPreviousOrder 为 true)时,
        // 不应初始化/下发温度,否则会把车内温度强制改回默认 23.5°C,覆盖上一单乘客的设置。
        // 该字段与 tripStatus 原子下发、首次即正确,无竞态窗口。
        if (hasPreviousOrder) {
            return;
        }
        if (skipQueryOnThisCycleRef.current) {
            return;
        }
        const targetOrderId = getOrderId(orderId);
        if (!targetOrderId) {
            return;
        }
        // 去重:同一订单在本次接驾态周期内只查询一次。tripStatus/pageDidShowVersion/hasPreviousOrder
        // 在页面初始化恢复流程中会分多次异步更新,但 orderId 不变时无需重复查询。
        if (queriedOrderIdRef.current === targetOrderId) {
            return;
        }
        queriedOrderIdRef.current = targetOrderId;
 
        const storageData = getAcStorage(targetOrderId);
 
        if (storageData) {
            handleCalibration(targetOrderId, storageData.acOpen, storageData.temperature);
            return;
        }
 
        handleFirstTimeInit(targetOrderId);
    }, [tripStatus, orderId, pageDidShowVersion, hasPreviousOrder]);
 
    useEffect(() => {
        lastConfirmedTempRef.current = tempStatus;
        return () => {
            if (tempTimerRef.current) {
                clearTimeout(tempTimerRef.current);
            }
        };
    }, []);
 
    const scheduleTemperatureUpdate = (targetTemperature: number) => {
        if (tempTimerRef.current) {
            clearTimeout(tempTimerRef.current);
        }
        tempTimerRef.current = setTimeout(() => {
            const targetOrderId = getOrderId(orderId);
            if (!targetOrderId) {
                return;
            }
            setTemperature({
                orderId: targetOrderId,
                targetTemperature
            }).then((res: any) => {
                if (res) {
                    lastConfirmedTempRef.current = targetTemperature;
                    userActionSeqRef.current++;
                    saveAcStorage(targetOrderId, { acOpen: acSwitchOn, temperature: targetTemperature });
                    // Taro.showToast({ title: '设置成功', icon: 'none' });
                } else {
                    setTempStatus(lastConfirmedTempRef.current ?? null);
                    showToast({ title: AC_TEMP_FAIL_TEXT, icon: 'none' });
                }
            }).catch(() => {
                setTempStatus(lastConfirmedTempRef.current ?? null);
                showToast({ title: AC_TEMP_FAIL_TEXT, icon: 'none' });
            });
        }, TEMP_UPDATE_DEBOUNCE_MS);
    };
 
    const handleTempChange = (delta: number) => {
        if (!canAdjustTemperature) {
            return;
        }
        setTempStatus((prev) => {
            if (typeof prev !== 'number') {
                return prev;
            }
            const next = prev + delta;
            if (delta < 0 && next < TEMP_MIN) {
                showToast({ title: '当前已是空调最低温度', icon: 'none' });
                return prev;
            }
            if (delta > 0 && next > TEMP_MAX) {
                showToast({ title: '当前已是空调最高温度', icon: 'none' });
                return prev;
            }
            scheduleTemperatureUpdate(next);
            return next;
        });
    };
 
    const handleDecreaseTemperature = () => {
        if (!canAdjustTemperature) {
            return;
        }
        if (isAtMinTemperature) {
            showToast({ title: AC_TEMP_MIN_TIP_TEXT, icon: 'none' });
            return;
        }
        handleTempChange(-0.5);
    };
 
    const handleIncreaseTemperature = () => {
        if (!canAdjustTemperature) {
            return;
        }
        if (isAtMaxTemperature) {
            showToast({ title: AC_TEMP_MAX_TIP_TEXT, icon: 'none' });
            return;
        }
        handleTempChange(0.5);
    };
 
    const handleToggleAcSwitch = async () => {
        if (!canToggleSwitch) {
            return;
        }
        const targetOrderId = getOrderId(orderId);
        if (!targetOrderId) {
            return;
        }
        const targetOpenState = !acSwitchOn;
        setAcTogglingTarget(targetOpenState);
        startLoading('ac');
        let toastText = '';
        try {
            // 200就认为是指令下发成功了
            await airConditionerUnlock({
                orderId: targetOrderId,
                open: targetOpenState
            });
            // 开启空调时同时下发温度设置
            if (targetOpenState) {
                const currentTemp = typeof tempStatus === 'number' ? tempStatus : DEFAULT_FIRST_TEMP;
                try {
                    await setTemperature({ orderId: targetOrderId, targetTemperature: currentTemp });
                } catch {
                    console.error('设置空调温度失败');
                }
            }
            setAcSwitchOn(targetOpenState);
            setAcStatusLoaded(true);
            userActionSeqRef.current++;
            const currentTemp = typeof tempStatus === 'number' ? tempStatus : DEFAULT_FIRST_TEMP;
            saveAcStorage(targetOrderId, { acOpen: targetOpenState, temperature: currentTemp });
            toastText = targetOpenState ? '已开启空调' : '已关闭空调';
            if (targetOpenState && typeof tempStatus !== 'number') {
                setTempStatus(DEFAULT_FIRST_TEMP);
                lastConfirmedTempRef.current = DEFAULT_FIRST_TEMP;
            }
        } catch (error) {
            toastText = AC_TOGGLE_FAIL_TEXT;
        } finally {
            // 等 loading 动画真正结束后再弹 toast,避免异常提示已弹出但 loading 仍在转
            stopLoading('ac', () => {
                if (toastText) {
                    showToast({ title: toastText, icon: 'none' });
                }
            });
        }
    };
 
    // 连环单(车还在执行上一单)时不展示空调卡片
    if (tripStatus !== TripStatus.VehicleApproaching || hasPreviousOrder) {
        return null;
    }
 
    return (
        <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>
                    <AcSwitch checked={acSwitchOn} loading={isTogglingAc} onChange={handleToggleAcSwitch} />
                </View>
                <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>
            </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>
    );
}