chore: 初始化仓库基线(AGENTS.md、git 规范、敏感文件排除)
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '蚕房详情',
|
||||
enablePullDownRefresh: true,
|
||||
});
|
||||
@@ -0,0 +1,244 @@
|
||||
@use '@/styles/variables.scss' as *;
|
||||
|
||||
.detailPage {
|
||||
min-height: 100vh;
|
||||
background: $color-bg-page;
|
||||
padding-bottom: $spacing-xl;
|
||||
}
|
||||
|
||||
.infoCard {
|
||||
background: linear-gradient(135deg, $color-primary 0%, $color-primary-dark 100%);
|
||||
padding: $spacing-lg;
|
||||
color: $color-text-white;
|
||||
}
|
||||
|
||||
.infoHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: $spacing-md;
|
||||
}
|
||||
|
||||
.infoName {
|
||||
font-size: $font-size-xl;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $color-text-white;
|
||||
}
|
||||
|
||||
.infoStatus {
|
||||
font-size: $font-size-xs;
|
||||
padding: 4rpx $spacing-sm;
|
||||
border-radius: $radius-xs;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
color: $color-text-white;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.infoMeta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $spacing-md;
|
||||
}
|
||||
|
||||
.metaItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
font-size: $font-size-sm;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.section {
|
||||
margin: $spacing-md $spacing-lg 0;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: $font-size-lg;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $color-text-primary;
|
||||
margin-bottom: $spacing-md;
|
||||
}
|
||||
|
||||
.telemetryGrid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $spacing-md;
|
||||
}
|
||||
|
||||
.telemetryCard {
|
||||
width: calc(50% - 12rpx);
|
||||
background: $color-bg-card;
|
||||
border-radius: $radius-md;
|
||||
padding: $spacing-md $spacing-lg;
|
||||
box-shadow: $shadow-card;
|
||||
}
|
||||
|
||||
.telemetryTop {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
|
||||
.telemetryLabel {
|
||||
font-size: $font-size-sm;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.telemetryIcon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.telemetryValue {
|
||||
font-size: $font-size-xxl;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $color-text-primary;
|
||||
}
|
||||
|
||||
.telemetryUnit {
|
||||
font-size: $font-size-sm;
|
||||
color: $color-text-tertiary;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
|
||||
.telemetryTime {
|
||||
font-size: $font-size-xs;
|
||||
color: $color-text-tertiary;
|
||||
margin-top: $spacing-xs;
|
||||
}
|
||||
|
||||
.deviceList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-sm;
|
||||
}
|
||||
|
||||
.deviceItem {
|
||||
background: $color-bg-card;
|
||||
border-radius: $radius-md;
|
||||
padding: $spacing-md $spacing-lg;
|
||||
box-shadow: $shadow-card;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.deviceLeft {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.deviceIcon {
|
||||
font-size: 36rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.deviceInfo {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.deviceName {
|
||||
font-size: $font-size-md;
|
||||
color: $color-text-primary;
|
||||
@include text-ellipsis;
|
||||
}
|
||||
|
||||
.deviceMeta {
|
||||
font-size: $font-size-xs;
|
||||
color: $color-text-tertiary;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.deviceStatus {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
font-size: $font-size-xs;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.statusDot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: $radius-round;
|
||||
}
|
||||
|
||||
.loadingWrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $spacing-xl 0;
|
||||
}
|
||||
|
||||
.loadingText {
|
||||
font-size: $font-size-sm;
|
||||
color: $color-text-tertiary;
|
||||
}
|
||||
|
||||
.emptyWrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $spacing-xl 0;
|
||||
}
|
||||
|
||||
.emptyIcon {
|
||||
font-size: 64rpx;
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
|
||||
.emptyText {
|
||||
font-size: $font-size-sm;
|
||||
color: $color-text-tertiary;
|
||||
}
|
||||
|
||||
.trendCard {
|
||||
background: $color-bg-card;
|
||||
border-radius: $radius-md;
|
||||
padding: $spacing-md $spacing-lg;
|
||||
box-shadow: $shadow-card;
|
||||
}
|
||||
|
||||
.trendLoading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200rpx;
|
||||
}
|
||||
|
||||
.trendLabel {
|
||||
font-size: $font-size-sm;
|
||||
color: $color-text-tertiary;
|
||||
margin-top: $spacing-sm;
|
||||
margin-bottom: $spacing-xs;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.legendRow {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: $spacing-xl;
|
||||
margin-top: $spacing-md;
|
||||
}
|
||||
|
||||
.legendItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-xs;
|
||||
}
|
||||
|
||||
.legendDot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: $radius-round;
|
||||
}
|
||||
|
||||
.legendText {
|
||||
font-size: $font-size-sm;
|
||||
color: $color-text-tertiary;
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { View, Text, ScrollView } from '@tarojs/components';
|
||||
import Taro, { useRouter, usePullDownRefresh } from '@tarojs/taro';
|
||||
import styles from './index.module.scss';
|
||||
import { getRoomById } from '@/api/rooms';
|
||||
import { getDevices } from '@/api/devices';
|
||||
import { getLatestTelemetry, fetchTrend } from '@/api/telemetry';
|
||||
import MiniChart from '@/components/MiniChart';
|
||||
import {
|
||||
roomStatusLabel,
|
||||
onlineStatusLabel,
|
||||
deviceKindLabel,
|
||||
metricLabel,
|
||||
metricUnit,
|
||||
formatNumber,
|
||||
formatRelativeTime,
|
||||
} from '@/utils/format';
|
||||
import type { Room, Device, TelemetryRecord, TrendPoint } from '@/types';
|
||||
|
||||
const RoomDetailPage: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const roomId = router.params.id || '';
|
||||
const [room, setRoom] = useState<Room | null>(null);
|
||||
const [devices, setDevices] = useState<Device[]>([]);
|
||||
const [telemetryMap, setTelemetryMap] = useState<Record<string, TelemetryRecord[]>>({});
|
||||
const [trend, setTrend] = useState<TrendPoint[]>([]);
|
||||
const [trendLoading, setTrendLoading] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
if (!roomId) {
|
||||
setError('缺少蚕房ID');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setError('');
|
||||
const [roomRes, devicesRes] = await Promise.all([
|
||||
getRoomById(roomId).catch((e) => {
|
||||
console.error('[RoomDetail] 获取蚕房失败:', e);
|
||||
return null;
|
||||
}),
|
||||
getDevices().catch(() => [] as Device[]),
|
||||
]);
|
||||
|
||||
if (roomRes) {
|
||||
setRoom(roomRes);
|
||||
}
|
||||
|
||||
const roomDevices = devicesRes.filter((d) => d.roomId === roomId);
|
||||
setDevices(roomDevices);
|
||||
|
||||
// 获取每个设备的最新遥测
|
||||
const telemetryEntries = await Promise.all(
|
||||
roomDevices.map(async (device) => {
|
||||
try {
|
||||
const telemetry = await getLatestTelemetry(device.deviceKey).catch(
|
||||
() => [] as TelemetryRecord[]
|
||||
);
|
||||
return [device.deviceKey, telemetry] as [string, TelemetryRecord[]];
|
||||
} catch {
|
||||
return [device.deviceKey, []] as [string, TelemetryRecord[]];
|
||||
}
|
||||
})
|
||||
);
|
||||
const map: Record<string, TelemetryRecord[]> = {};
|
||||
telemetryEntries.forEach(([key, val]) => {
|
||||
map[key] = val;
|
||||
});
|
||||
setTelemetryMap(map);
|
||||
|
||||
// 加载24小时趋势数据
|
||||
setTrendLoading(true);
|
||||
const trendData = await fetchTrend(24).catch(() => [] as TrendPoint[]);
|
||||
setTrend(trendData);
|
||||
} catch (err) {
|
||||
console.error('[RoomDetail] 数据加载失败:', err);
|
||||
setError(err instanceof Error ? err.message : '数据加载失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setTrendLoading(false);
|
||||
}
|
||||
}, [roomId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
intervalRef.current = setInterval(() => {
|
||||
fetchData();
|
||||
}, 30000);
|
||||
return () => {
|
||||
if (intervalRef.current) clearInterval(intervalRef.current);
|
||||
};
|
||||
}, [fetchData]);
|
||||
|
||||
usePullDownRefresh(() => {
|
||||
fetchData().then(() => {
|
||||
Taro.stopPullDownRefresh();
|
||||
});
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View className={styles.detailPage}>
|
||||
<View className={styles.loadingWrap}>
|
||||
<Text className={styles.loadingText}>加载中...</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (error && !room) {
|
||||
return (
|
||||
<View className={styles.detailPage}>
|
||||
<View className={styles.emptyWrap}>
|
||||
<Text className={styles.emptyIcon}>⚠️</Text>
|
||||
<Text className={styles.emptyText}>{error}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (!room) {
|
||||
return (
|
||||
<View className={styles.detailPage}>
|
||||
<View className={styles.emptyWrap}>
|
||||
<Text className={styles.emptyIcon}>🏠</Text>
|
||||
<Text className={styles.emptyText}>未找到蚕房信息</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View className={styles.detailPage}>
|
||||
{/* 蚕房信息卡片 */}
|
||||
<View className={styles.infoCard}>
|
||||
<View className={styles.infoHeader}>
|
||||
<Text className={styles.infoName}>{room.name}</Text>
|
||||
<View className={styles.infoStatus}>{roomStatusLabel(room.status)}</View>
|
||||
</View>
|
||||
<View className={styles.infoMeta}>
|
||||
{room.code && (
|
||||
<View className={styles.metaItem}>
|
||||
<Text>编码: {room.code}</Text>
|
||||
</View>
|
||||
)}
|
||||
{room.location && (
|
||||
<View className={styles.metaItem}>
|
||||
<Text>📍 {room.location}</Text>
|
||||
</View>
|
||||
)}
|
||||
<View className={styles.metaItem}>
|
||||
<Text>📡 设备: {devices.length}</Text>
|
||||
</View>
|
||||
</View>
|
||||
{room.description && (
|
||||
<Text style={{ fontSize: '24rpx', color: 'rgba(255,255,255,0.8)', marginTop: '16rpx', lineHeight: '1.6' }}>
|
||||
{room.description}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 最新遥测数据 */}
|
||||
{devices.length > 0 && (
|
||||
<View className={styles.section}>
|
||||
<Text className={styles.sectionTitle}>实时环境数据</Text>
|
||||
<ScrollView scrollY>
|
||||
{devices.map((device) => {
|
||||
const telemetry = telemetryMap[device.deviceKey] || [];
|
||||
if (telemetry.length === 0) return null;
|
||||
return (
|
||||
<View key={device.deviceKey} style={{ marginBottom: '16rpx' }}>
|
||||
<Text style={{ fontSize: '24rpx', color: '#86909c', marginBottom: '8rpx', display: 'block' }}>
|
||||
{device.name}
|
||||
</Text>
|
||||
<View className={styles.telemetryGrid}>
|
||||
{telemetry.map((item) => (
|
||||
<View key={`${item.deviceKey}-${item.metric}`} className={styles.telemetryCard}>
|
||||
<View className={styles.telemetryTop}>
|
||||
<Text className={styles.telemetryLabel}>{metricLabel(item.metric)}</Text>
|
||||
<Text className={styles.telemetryIcon}>
|
||||
{item.metric === 'temperature' ? '🌡' : item.metric === 'humidity' ? '💧' : '📈'}
|
||||
</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text className={styles.telemetryValue}>{formatNumber(item.value)}</Text>
|
||||
<Text className={styles.telemetryUnit}>{metricUnit(item.metric)}</Text>
|
||||
</View>
|
||||
<Text className={styles.telemetryTime}>{formatRelativeTime(item.timestamp)}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 24小时趋势图 */}
|
||||
<View className={styles.section}>
|
||||
<Text className={styles.sectionTitle}>24小时趋势</Text>
|
||||
<View className={styles.trendCard}>
|
||||
{trendLoading && trend.length === 0 ? (
|
||||
<View className={styles.trendLoading}>
|
||||
<Text className={styles.loadingText}>趋势数据加载中...</Text>
|
||||
</View>
|
||||
) : trend.length > 0 ? (
|
||||
<>
|
||||
<Text className={styles.trendLabel}>温度 (°C)</Text>
|
||||
<MiniChart data={trend} metric="temp" color="#f53f3f" height={200} />
|
||||
<Text className={styles.trendLabel}>湿度 (%)</Text>
|
||||
<MiniChart data={trend} metric="humidity" color="#10b981" height={200} />
|
||||
<View className={styles.legendRow}>
|
||||
<View className={styles.legendItem}>
|
||||
<View className={styles.legendDot} style={{ background: '#f53f3f' }} />
|
||||
<Text className={styles.legendText}>温度</Text>
|
||||
</View>
|
||||
<View className={styles.legendItem}>
|
||||
<View className={styles.legendDot} style={{ background: '#10b981' }} />
|
||||
<Text className={styles.legendText}>湿度</Text>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<View className={styles.trendLoading}>
|
||||
<Text className={styles.loadingText}>暂无趋势数据</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 设备列表 */}
|
||||
<View className={styles.section}>
|
||||
<Text className={styles.sectionTitle}>蚕房设备</Text>
|
||||
{devices.length > 0 ? (
|
||||
<View className={styles.deviceList}>
|
||||
{devices.map((device) => (
|
||||
<View key={device.id} className={styles.deviceItem}>
|
||||
<View className={styles.deviceLeft}>
|
||||
<Text className={styles.deviceIcon}>
|
||||
{device.kind === 'camera' ? '📹' : device.kind === 'sensor' ? '🌡' : '🔌'}
|
||||
</Text>
|
||||
<View className={styles.deviceInfo}>
|
||||
<Text className={styles.deviceName}>{device.name}</Text>
|
||||
<Text className={styles.deviceMeta}>
|
||||
{deviceKindLabel(device.kind)} · {device.deviceKey}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles.deviceStatus}>
|
||||
<View
|
||||
className={styles.statusDot}
|
||||
style={{
|
||||
background: device.onlineStatus === 'online' ? '#00b42a' : '#c9cdd4',
|
||||
}}
|
||||
/>
|
||||
<Text style={{ color: device.onlineStatus === 'online' ? '#00b42a' : '#86909c' }}>
|
||||
{onlineStatusLabel(device.onlineStatus)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
<View className={styles.emptyWrap}>
|
||||
<Text className={styles.emptyIcon}>📭</Text>
|
||||
<Text className={styles.emptyText}>该蚕房暂无设备</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoomDetailPage;
|
||||
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '蚕房',
|
||||
enablePullDownRefresh: true,
|
||||
});
|
||||
@@ -0,0 +1,175 @@
|
||||
@use '@/styles/variables.scss' as *;
|
||||
|
||||
.roomsPage {
|
||||
min-height: 100vh;
|
||||
background: $color-bg-page;
|
||||
padding: $spacing-md $spacing-lg;
|
||||
padding-bottom: $spacing-xl;
|
||||
}
|
||||
|
||||
.roomList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-md;
|
||||
}
|
||||
|
||||
.roomCard {
|
||||
background: $color-bg-card;
|
||||
border-radius: $radius-md;
|
||||
padding: $spacing-lg;
|
||||
box-shadow: $shadow-card;
|
||||
transition: all $transition-base;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
opacity: 0.95;
|
||||
}
|
||||
}
|
||||
|
||||
.roomCardHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: $spacing-md;
|
||||
}
|
||||
|
||||
.roomNameRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.roomIcon {
|
||||
font-size: 36rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.roomName {
|
||||
font-size: $font-size-lg;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $color-text-primary;
|
||||
@include text-ellipsis;
|
||||
}
|
||||
|
||||
.roomStatus {
|
||||
font-size: $font-size-xs;
|
||||
padding: 4rpx $spacing-sm;
|
||||
border-radius: $radius-xs;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
margin-left: $spacing-sm;
|
||||
}
|
||||
|
||||
.statusActive {
|
||||
background: rgba(0, 180, 42, 0.1);
|
||||
color: $color-success;
|
||||
}
|
||||
|
||||
.statusAlarm {
|
||||
background: rgba(245, 63, 63, 0.1);
|
||||
color: $color-error;
|
||||
}
|
||||
|
||||
.statusMaintenance {
|
||||
background: rgba(255, 125, 0, 0.1);
|
||||
color: $color-warning;
|
||||
}
|
||||
|
||||
.statusInactive {
|
||||
background: rgba(134, 144, 156, 0.1);
|
||||
color: $color-text-tertiary;
|
||||
}
|
||||
|
||||
.roomInfo {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $spacing-md;
|
||||
margin-bottom: $spacing-md;
|
||||
}
|
||||
|
||||
.infoItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.infoIcon {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.infoText {
|
||||
font-size: $font-size-sm;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.roomDesc {
|
||||
font-size: $font-size-sm;
|
||||
color: $color-text-tertiary;
|
||||
line-height: $line-height-loose;
|
||||
@include text-ellipsis-multi(2);
|
||||
}
|
||||
|
||||
.roomCode {
|
||||
font-size: $font-size-xs;
|
||||
color: $color-text-tertiary;
|
||||
margin-top: $spacing-sm;
|
||||
}
|
||||
|
||||
.loadingWrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $spacing-xl 0;
|
||||
}
|
||||
|
||||
.loadingText {
|
||||
font-size: $font-size-sm;
|
||||
color: $color-text-tertiary;
|
||||
}
|
||||
|
||||
.errorWrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $spacing-xl 0;
|
||||
}
|
||||
|
||||
.errorIcon {
|
||||
font-size: 56rpx;
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
|
||||
.errorText {
|
||||
font-size: $font-size-sm;
|
||||
color: $color-text-tertiary;
|
||||
}
|
||||
|
||||
.retryBtn {
|
||||
margin-top: $spacing-md;
|
||||
padding: $spacing-xs $spacing-lg;
|
||||
border-radius: $radius-button;
|
||||
background: $color-primary;
|
||||
color: $color-text-white;
|
||||
font-size: $font-size-sm;
|
||||
}
|
||||
|
||||
.emptyWrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.emptyIcon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: $spacing-md;
|
||||
}
|
||||
|
||||
.emptyText {
|
||||
font-size: $font-size-md;
|
||||
color: $color-text-tertiary;
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro';
|
||||
import styles from './index.module.scss';
|
||||
import { getRooms } from '@/api/rooms';
|
||||
import { getDevices } from '@/api/devices';
|
||||
import { getAlarms } from '@/api/alarms';
|
||||
import { roomStatusLabel } from '@/utils/format';
|
||||
import type { Room, Device, Alarm } from '@/types';
|
||||
|
||||
const RoomsPage: React.FC = () => {
|
||||
const [rooms, setRooms] = useState<Room[]>([]);
|
||||
const [devices, setDevices] = useState<Device[]>([]);
|
||||
const [alarms, setAlarms] = useState<Alarm[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setError('');
|
||||
const [roomsRes, devicesRes, alarmsRes] = await Promise.all([
|
||||
getRooms().catch(() => [] as Room[]),
|
||||
getDevices().catch(() => [] as Device[]),
|
||||
getAlarms({ openOnly: true }).catch(() => [] as Alarm[]),
|
||||
]);
|
||||
setRooms(roomsRes);
|
||||
setDevices(devicesRes);
|
||||
setAlarms(alarmsRes);
|
||||
} catch (err) {
|
||||
console.error('[Rooms] 数据加载失败:', err);
|
||||
setError(err instanceof Error ? err.message : '数据加载失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
useDidShow(() => {
|
||||
if (rooms.length > 0) fetchData();
|
||||
});
|
||||
|
||||
usePullDownRefresh(() => {
|
||||
fetchData().then(() => Taro.stopPullDownRefresh());
|
||||
});
|
||||
|
||||
const handleRoomTap = (roomId: string) => {
|
||||
Taro.navigateTo({ url: `/pages/rooms/detail/index?id=${roomId}` });
|
||||
};
|
||||
|
||||
const getStatusClass = (status?: string) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return styles.statusActive;
|
||||
case 'alarm':
|
||||
return styles.statusAlarm;
|
||||
case 'maintenance':
|
||||
return styles.statusMaintenance;
|
||||
default:
|
||||
return styles.statusInactive;
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View className={styles.roomsPage}>
|
||||
<View className={styles.loadingWrap}>
|
||||
<Text className={styles.loadingText}>加载中...</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<View className={styles.roomsPage}>
|
||||
<View className={styles.errorWrap}>
|
||||
<Text className={styles.errorIcon}>⚠️</Text>
|
||||
<Text className={styles.errorText}>{error}</Text>
|
||||
<View className={styles.retryBtn} onClick={fetchData}>
|
||||
<Text style={{ color: '#fff' }}>重试</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (rooms.length === 0) {
|
||||
return (
|
||||
<View className={styles.roomsPage}>
|
||||
<View className={styles.emptyWrap}>
|
||||
<Text className={styles.emptyIcon}>🏠</Text>
|
||||
<Text className={styles.emptyText}>暂无蚕房数据</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View className={styles.roomsPage}>
|
||||
<View className={styles.roomList}>
|
||||
{rooms.map((room) => {
|
||||
const roomDevices = devices.filter((d) => d.roomId === room.id);
|
||||
const onlineCount = roomDevices.filter((d) => d.onlineStatus === 'online').length;
|
||||
const roomAlarms = alarms.filter((a) => a.deviceKey && roomDevices.some((d) => d.deviceKey === a.deviceKey));
|
||||
|
||||
return (
|
||||
<View key={room.id} className={styles.roomCard} onClick={() => handleRoomTap(room.id)}>
|
||||
<View className={styles.roomCardHeader}>
|
||||
<View className={styles.roomNameRow}>
|
||||
<Text className={styles.roomIcon}>🏠</Text>
|
||||
<Text className={styles.roomName}>{room.name}</Text>
|
||||
</View>
|
||||
<View className={`${styles.roomStatus} ${getStatusClass(room.status)}`}>
|
||||
{roomStatusLabel(room.status)}
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles.roomInfo}>
|
||||
<View className={styles.infoItem}>
|
||||
<Text className={styles.infoIcon}>📡</Text>
|
||||
<Text className={styles.infoText}>设备 {onlineCount}/{roomDevices.length}</Text>
|
||||
</View>
|
||||
{roomAlarms.length > 0 && (
|
||||
<View className={styles.infoItem}>
|
||||
<Text className={styles.infoIcon}>🔔</Text>
|
||||
<Text className={styles.infoText} style={{ color: '#f53f3f' }}>告警 {roomAlarms.length}</Text>
|
||||
</View>
|
||||
)}
|
||||
{room.location && (
|
||||
<View className={styles.infoItem}>
|
||||
<Text className={styles.infoIcon}>📍</Text>
|
||||
<Text className={styles.infoText}>{room.location}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{room.description && (
|
||||
<Text className={styles.roomDesc}>{room.description}</Text>
|
||||
)}
|
||||
{room.code && (
|
||||
<Text className={styles.roomCode}>编码: {room.code}</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoomsPage;
|
||||
Reference in New Issue
Block a user