240 lines
7.8 KiB
TypeScript
240 lines
7.8 KiB
TypeScript
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
import { View, Text, ScrollView, Switch, Input } from '@tarojs/components';
|
|
import Taro, { useRouter } from '@tarojs/taro';
|
|
import styles from './index.module.scss';
|
|
import { sendControl } from '@/api/devices';
|
|
import { getTelemetry, normalizeRealtime } from '@/api/telemetry';
|
|
import { formatNumber } from '@/utils/format';
|
|
import type { RealtimeMetric } from '@/types';
|
|
|
|
const MODES = [
|
|
{ value: 'cool', label: '制冷' },
|
|
{ value: 'heat', label: '制热' },
|
|
{ value: 'fan', label: '通风' },
|
|
{ value: 'auto', label: '自动' },
|
|
];
|
|
|
|
const SPEEDS = [
|
|
{ value: '0', label: '自动' },
|
|
{ value: '1', label: '低' },
|
|
{ value: '2', label: '中' },
|
|
{ value: '3', label: '高' },
|
|
];
|
|
|
|
const ControlPage: React.FC = () => {
|
|
const router = useRouter();
|
|
const deviceKey = router.params.deviceKey || '';
|
|
const deviceName = router.params.deviceName || '';
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
const [metrics, setMetrics] = useState<RealtimeMetric[]>([]);
|
|
const [isOn, setIsOn] = useState(false);
|
|
const [sending, setSending] = useState(false);
|
|
const [mode, setMode] = useState('cool');
|
|
const [speed, setSpeed] = useState('1');
|
|
const [tempValue, setTempValue] = useState('26');
|
|
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
|
|
const loadData = useCallback(async () => {
|
|
try {
|
|
const telemetryData = await getTelemetry({ deviceKey, limit: 50 }).catch(
|
|
() => [] as never[]
|
|
);
|
|
setMetrics(normalizeRealtime(telemetryData));
|
|
} catch {
|
|
// ignore
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [deviceKey]);
|
|
|
|
useEffect(() => {
|
|
loadData();
|
|
timerRef.current = setInterval(loadData, 30000);
|
|
return () => {
|
|
if (timerRef.current) {
|
|
clearInterval(timerRef.current);
|
|
timerRef.current = null;
|
|
}
|
|
};
|
|
}, [loadData]);
|
|
|
|
const handleSend = async (action: string, value?: string | number) => {
|
|
setSending(true);
|
|
Taro.showLoading({ title: '发送指令...' });
|
|
try {
|
|
await sendControl({ deviceKey, action, value });
|
|
if (action === 'power') {
|
|
setIsOn(value === 'on');
|
|
}
|
|
Taro.showToast({ title: `命令已发送: ${action}`, icon: 'success' });
|
|
} catch (err) {
|
|
Taro.showToast({
|
|
title: err instanceof Error ? `发送失败: ${err.message}` : '发送失败',
|
|
icon: 'none',
|
|
});
|
|
} finally {
|
|
setSending(false);
|
|
Taro.hideLoading();
|
|
}
|
|
};
|
|
|
|
const handleToggle = (event: { detail: { value: boolean } }) => {
|
|
handleSend('power', event.detail.value ? 'on' : 'off');
|
|
};
|
|
|
|
const getMetricColor = (status: string) => {
|
|
if (status === 'danger') return '#f53f3f';
|
|
if (status === 'warn') return '#ff7d00';
|
|
return '#10b981';
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<View className={styles.controlPage}>
|
|
<View className={styles.loadingWrap}>
|
|
<Text className={styles.loadingText}>加载中...</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View className={styles.controlPage}>
|
|
<ScrollView scrollY style={{ height: '100%' }}>
|
|
{/* 头部卡片 */}
|
|
<View className={styles.headerCard}>
|
|
<View className={styles.headerRow}>
|
|
<View className={styles.headerInfo}>
|
|
<Text className={styles.deviceName}>{deviceName}</Text>
|
|
<Text className={styles.deviceKey}>Key: {deviceKey}</Text>
|
|
</View>
|
|
<View className={styles.powerRow}>
|
|
<Text className={styles.powerLabel}>{isOn ? '开启' : '关闭'}</Text>
|
|
<Switch
|
|
checked={isOn}
|
|
onChange={handleToggle}
|
|
disabled={sending}
|
|
color="#10b981"
|
|
/>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{/* 控制面板 */}
|
|
<View className={styles.card}>
|
|
<Text className={styles.sectionTitle}>控制面板</Text>
|
|
|
|
<Text className={styles.controlLabel}>运行模式</Text>
|
|
<View className={styles.segmentGroup}>
|
|
{MODES.map((m) => (
|
|
<View
|
|
key={m.value}
|
|
className={`${styles.segmentBtn} ${mode === m.value ? styles.segmentBtnActive : ''}`}
|
|
onClick={() => !sending && setMode(m.value)}
|
|
>
|
|
<Text>{m.label}</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
|
|
<View className={styles.divider} />
|
|
|
|
<Text className={styles.controlLabel}>风速</Text>
|
|
<View className={styles.segmentGroup}>
|
|
{SPEEDS.map((s) => (
|
|
<View
|
|
key={s.value}
|
|
className={`${styles.segmentBtn} ${speed === s.value ? styles.segmentBtnActive : ''}`}
|
|
onClick={() => !sending && setSpeed(s.value)}
|
|
>
|
|
<Text>{s.label}</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
|
|
<View className={styles.divider} />
|
|
|
|
<Text className={styles.controlLabel}>目标温度</Text>
|
|
<View className={styles.tempRow}>
|
|
<Input
|
|
className={styles.tempInput}
|
|
type="digit"
|
|
value={tempValue}
|
|
onInput={(e) => setTempValue(e.detail.value)}
|
|
placeholder="26"
|
|
/>
|
|
<View
|
|
className={`${styles.sendBtn} ${sending ? styles.sendBtnDisabled : ''}`}
|
|
onClick={() => !sending && handleSend('set_temp', parseInt(tempValue, 10) || 26)}
|
|
>
|
|
<Text>设置</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View className={styles.divider} />
|
|
|
|
<View className={styles.actionRow}>
|
|
<View
|
|
className={`${styles.actionBtn} ${sending ? styles.actionBtnDisabled : ''}`}
|
|
onClick={() => !sending && handleSend('fan_on')}
|
|
>
|
|
<Text>开启风扇</Text>
|
|
</View>
|
|
<View
|
|
className={`${styles.actionBtn} ${sending ? styles.actionBtnDisabled : ''}`}
|
|
onClick={() => !sending && handleSend('fan_off')}
|
|
>
|
|
<Text>关闭风扇</Text>
|
|
</View>
|
|
</View>
|
|
<View className={styles.actionRow}>
|
|
<View
|
|
className={`${styles.actionBtn} ${sending ? styles.actionBtnDisabled : ''}`}
|
|
onClick={() => !sending && handleSend('dehumidifier_on')}
|
|
>
|
|
<Text>开启除湿</Text>
|
|
</View>
|
|
<View
|
|
className={`${styles.actionBtn} ${sending ? styles.actionBtnDisabled : ''}`}
|
|
onClick={() => !sending && handleSend('dehumidifier_off')}
|
|
>
|
|
<Text>关闭除湿</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{/* 实时数据 */}
|
|
<Text className={styles.sectionTitle}>实时数据</Text>
|
|
{metrics.length === 0 ? (
|
|
<View className={styles.emptyWrap}>
|
|
<Text className={styles.emptyIcon}>📊</Text>
|
|
<Text className={styles.emptyText}>暂无遥测数据</Text>
|
|
</View>
|
|
) : (
|
|
<View className={styles.metricsGrid}>
|
|
{metrics.map((m) => {
|
|
const color = getMetricColor(m.status);
|
|
return (
|
|
<View
|
|
key={m.key}
|
|
className={styles.metricCard}
|
|
style={{ borderLeftColor: color }}
|
|
>
|
|
<Text className={styles.metricName}>{m.name}</Text>
|
|
<Text className={styles.metricValue} style={{ color }}>
|
|
{formatNumber(m.value, 1)}
|
|
<Text className={styles.metricUnit}> {m.unit}</Text>
|
|
</Text>
|
|
</View>
|
|
);
|
|
})}
|
|
</View>
|
|
)}
|
|
</ScrollView>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
export default ControlPage;
|