chore: 初始化仓库基线(AGENTS.md、git 规范、敏感文件排除)
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { StyleSheet, View, FlatList, RefreshControl } from 'react-native';
|
||||
import { Text, Card, useTheme, ActivityIndicator, Surface, Searchbar, IconButton } from 'react-native-paper';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { getDevices } from '../api/devices';
|
||||
import { formatRelativeTime } from '../utils/format';
|
||||
import type { Device } from '../types';
|
||||
|
||||
export default function DevicesScreen() {
|
||||
const theme = useTheme();
|
||||
const navigation = useNavigation<any>();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [devices, setDevices] = useState<Device[]>([]);
|
||||
const [filteredDevices, setFilteredDevices] = useState<Device[]>([]);
|
||||
const [search, setSearch] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadData = useCallback(async (isRefresh = false) => {
|
||||
if (isRefresh) setRefreshing(true);
|
||||
try {
|
||||
const data = await getDevices();
|
||||
setDevices(data);
|
||||
setFilteredDevices(data);
|
||||
setError(null);
|
||||
} catch (err: any) {
|
||||
setError(err?.message || '加载失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!search.trim()) {
|
||||
setFilteredDevices(devices);
|
||||
} else {
|
||||
const lower = search.toLowerCase();
|
||||
setFilteredDevices(
|
||||
devices.filter(
|
||||
(d) =>
|
||||
d.name.toLowerCase().includes(lower) ||
|
||||
d.deviceKey.toLowerCase().includes(lower) ||
|
||||
(d.kind || '').toLowerCase().includes(lower),
|
||||
),
|
||||
);
|
||||
}
|
||||
}, [search, devices]);
|
||||
|
||||
const isControllable = (device: Device) => {
|
||||
const kind = (device.kind || device.type || '').toLowerCase();
|
||||
return kind === 'actuator' || kind === 'controller' || kind === 'fan' || kind === 'ac' || kind === 'dehumidifier';
|
||||
};
|
||||
|
||||
const getDeviceIcon = (device: Device) => {
|
||||
const kind = (device.kind || device.type || '').toLowerCase();
|
||||
const name = device.name.toLowerCase();
|
||||
if (name.includes('fan') || kind.includes('fan')) return 'fan';
|
||||
if (name.includes('ac') || name.includes('空调') || kind.includes('ac')) return 'air-conditioner';
|
||||
if (name.includes('humid') || name.includes('除湿') || kind.includes('humid')) return 'water-percent';
|
||||
if (name.includes('light') || name.includes('灯') || kind.includes('light')) return 'lightbulb';
|
||||
if (name.includes('sensor') || kind.includes('sensor')) return 'thermometer';
|
||||
if (name.includes('camera') || kind.includes('camera')) return 'video';
|
||||
return 'router-wireless';
|
||||
};
|
||||
|
||||
const renderItem = ({ item }: { item: Device }) => {
|
||||
const isOnline = item.onlineStatus === 'online' || item.status === 'online';
|
||||
const controllable = isControllable(item);
|
||||
return (
|
||||
<Card
|
||||
style={styles.card}
|
||||
onPress={controllable ? () => navigation.navigate('DeviceControl', { deviceKey: item.deviceKey, deviceName: item.name }) : undefined}
|
||||
>
|
||||
<Card.Content style={styles.cardContent}>
|
||||
<View style={styles.deviceLeft}>
|
||||
<View style={[styles.iconBox, { backgroundColor: isOnline ? '#E8F0E9' : '#F5F0E8' }]}>
|
||||
<IconButton icon={getDeviceIcon(item)} size={24} iconColor={isOnline ? '#7A9E7E' : '#B8B3AA'} />
|
||||
</View>
|
||||
<View style={styles.deviceInfo}>
|
||||
<Text variant="bodyMedium" style={styles.deviceName}>{item.name}</Text>
|
||||
<Text variant="bodySmall" style={styles.deviceKey}>{item.deviceKey}</Text>
|
||||
<View style={styles.tagRow}>
|
||||
<Text variant="bodySmall" style={styles.deviceKind}>{item.kind || item.type || '设备'}</Text>
|
||||
{item.model ? <Text variant="bodySmall" style={styles.deviceModel}> · {item.model}</Text> : null}
|
||||
</View>
|
||||
{item.lastSeen ? (
|
||||
<Text variant="bodySmall" style={styles.deviceTime}>最后在线: {formatRelativeTime(item.lastSeen)}</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.deviceRight}>
|
||||
<Surface style={[styles.statusBadge, { backgroundColor: isOnline ? '#7A9E7E' : '#B8B3AA' }]}>
|
||||
<Text style={styles.statusText}>{isOnline ? '在线' : '离线'}</Text>
|
||||
</Surface>
|
||||
{controllable ? (
|
||||
<IconButton icon="chevron-right" size={24} iconColor="#B8B3AA" />
|
||||
) : null}
|
||||
</View>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<SafeAreaView style={styles.center}>
|
||||
<ActivityIndicator size="large" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container} edges={['bottom']}>
|
||||
<View style={styles.searchContainer}>
|
||||
<Searchbar
|
||||
placeholder="搜索设备..."
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
style={styles.searchbar}
|
||||
/>
|
||||
</View>
|
||||
<FlatList
|
||||
data={filteredDevices}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={styles.list}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={() => loadData(true)} />
|
||||
}
|
||||
ListEmptyComponent={
|
||||
error ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Text style={styles.emptyText}>{error}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Text style={styles.emptyText}>暂无设备</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#FAF8F5',
|
||||
},
|
||||
center: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
searchContainer: {
|
||||
padding: 16,
|
||||
paddingBottom: 8,
|
||||
},
|
||||
searchbar: {
|
||||
borderRadius: 14,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
list: {
|
||||
padding: 16,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 32,
|
||||
},
|
||||
card: {
|
||||
marginBottom: 12,
|
||||
borderRadius: 14,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
cardContent: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 4,
|
||||
},
|
||||
deviceLeft: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
},
|
||||
iconBox: {
|
||||
borderRadius: 12,
|
||||
marginRight: 8,
|
||||
},
|
||||
deviceInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
deviceName: {
|
||||
color: '#2D2A26',
|
||||
fontWeight: '500',
|
||||
},
|
||||
deviceKey: {
|
||||
color: '#B8B3AA',
|
||||
fontSize: 12,
|
||||
marginTop: 2,
|
||||
},
|
||||
tagRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: 2,
|
||||
},
|
||||
deviceKind: {
|
||||
color: '#8C8780',
|
||||
},
|
||||
deviceModel: {
|
||||
color: '#B8B3AA',
|
||||
},
|
||||
deviceTime: {
|
||||
color: '#B8B3AA',
|
||||
marginTop: 2,
|
||||
},
|
||||
deviceRight: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
statusBadge: {
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 14,
|
||||
elevation: 0,
|
||||
},
|
||||
statusText: {
|
||||
color: 'white',
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
},
|
||||
emptyContainer: {
|
||||
alignItems: 'center',
|
||||
paddingTop: 64,
|
||||
},
|
||||
emptyText: {
|
||||
color: '#B8B3AA',
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user