chore: 初始化仓库基线(AGENTS.md、git 规范、敏感文件排除)

This commit is contained in:
weijuesen
2026-08-10 22:30:53 +08:00
commit 84abf4454c
358 changed files with 75993 additions and 0 deletions
@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '设备控制',
});
@@ -0,0 +1,250 @@
@use '@/styles/variables.scss' as *;
.controlPage {
min-height: 100vh;
background: $color-bg-page;
padding: $spacing-md $spacing-lg;
padding-bottom: $spacing-xl;
}
// ===== 头部卡片 =====
.headerCard {
background: $color-bg-card;
border-radius: $radius-lg;
padding: $spacing-lg;
margin-bottom: $spacing-md;
box-shadow: $shadow-card;
}
.headerRow {
display: flex;
justify-content: space-between;
align-items: center;
}
.headerInfo {
flex: 1;
min-width: 0;
}
.deviceName {
font-size: $font-size-lg;
font-weight: $font-weight-semibold;
color: $color-text-primary;
@include text-ellipsis;
}
.deviceKey {
font-size: $font-size-xs;
color: $color-text-tertiary;
margin-top: 4rpx;
}
.powerRow {
display: flex;
align-items: center;
gap: $spacing-xs;
flex-shrink: 0;
}
.powerLabel {
font-size: $font-size-sm;
color: $color-text-tertiary;
}
// ===== 通用卡片 =====
.card {
background: $color-bg-card;
border-radius: $radius-lg;
padding: $spacing-lg;
margin-bottom: $spacing-md;
box-shadow: $shadow-card;
}
.sectionTitle {
font-size: $font-size-md;
font-weight: $font-weight-semibold;
color: $color-text-primary;
margin-bottom: $spacing-md;
}
// ===== 控制面板 =====
.controlLabel {
font-size: $font-size-sm;
color: $color-text-tertiary;
margin-bottom: $spacing-sm;
margin-top: $spacing-sm;
}
.segmentGroup {
display: flex;
gap: $spacing-xs;
margin-bottom: $spacing-sm;
}
.segmentBtn {
flex: 1;
height: $button-height-sm;
border-radius: $radius-sm;
background: $color-bg-hover;
border: 2rpx solid $color-border;
display: flex;
align-items: center;
justify-content: center;
font-size: $font-size-sm;
color: $color-text-secondary;
transition: all $transition-base;
white-space: nowrap;
&:active {
opacity: 0.85;
transform: scale(0.98);
}
}
.segmentBtnActive {
background: linear-gradient(135deg, $color-primary 0%, $color-primary-light 100%);
border-color: $color-primary;
color: $color-text-white;
}
.divider {
height: 2rpx;
background: $color-divider;
margin: $spacing-md 0;
}
.tempRow {
display: flex;
align-items: center;
gap: $spacing-sm;
}
.tempInput {
flex: 1;
height: $button-height-sm;
background: $color-bg-hover;
border: 2rpx solid $color-border;
border-radius: $radius-sm;
padding: 0 $spacing-md;
font-size: $font-size-md;
color: $color-text-primary;
box-sizing: border-box;
}
.sendBtn {
padding: 0 $spacing-lg;
height: $button-height-sm;
border-radius: $radius-button;
background: linear-gradient(135deg, $color-primary 0%, $color-primary-light 100%);
color: $color-text-white;
font-size: $font-size-sm;
display: flex;
align-items: center;
justify-content: center;
white-space: nowrap;
&:active {
opacity: 0.85;
transform: scale(0.98);
}
}
.sendBtnDisabled {
opacity: 0.5;
}
.actionRow {
display: flex;
gap: $spacing-sm;
margin-bottom: $spacing-sm;
}
.actionBtn {
flex: 1;
height: $button-height-sm;
border-radius: $radius-button;
background: $color-bg-hover;
color: $color-text-secondary;
border: 2rpx solid $color-border;
font-size: $font-size-sm;
display: flex;
align-items: center;
justify-content: center;
white-space: nowrap;
&:active {
opacity: 0.85;
transform: scale(0.98);
}
}
.actionBtnDisabled {
opacity: 0.5;
}
// ===== 实时数据 =====
.metricsGrid {
display: flex;
flex-wrap: wrap;
gap: $spacing-sm;
}
.metricCard {
flex: 0 0 calc(50% - #{$spacing-sm} / 2);
background: $color-bg-card;
border-radius: $radius-md;
padding: $spacing-md;
border-left: 6rpx solid $color-primary;
box-shadow: $shadow-card;
box-sizing: border-box;
}
.metricName {
font-size: $font-size-xs;
color: $color-text-tertiary;
margin-bottom: $spacing-xs;
}
.metricValue {
font-size: $font-size-xl;
font-weight: $font-weight-bold;
color: $color-text-primary;
}
.metricUnit {
font-size: $font-size-sm;
font-weight: $font-weight-normal;
color: $color-text-tertiary;
}
// ===== 空状态/加载 =====
.emptyWrap {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 0;
}
.emptyIcon {
font-size: 80rpx;
margin-bottom: $spacing-md;
}
.emptyText {
font-size: $font-size-md;
color: $color-text-tertiary;
}
.loadingWrap {
display: flex;
align-items: center;
justify-content: center;
padding: $spacing-xl 0;
}
.loadingText {
font-size: $font-size-sm;
color: $color-text-tertiary;
}
+239
View File
@@ -0,0 +1,239 @@
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 = (newVal: boolean) => {
handleSend('power', newVal ? '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;