This commit is contained in:
tx 2024-12-25 16:04:59 +08:00
parent 1f188eeaf3
commit de63606dd3
8 changed files with 743 additions and 216 deletions

View File

@ -0,0 +1,452 @@
import React, { useState, useEffect, useRef } from 'react';
import { View, Image, Text, StyleSheet, TouchableOpacity, Alert, Animated } from 'react-native';
import { Spinner } from '@ui-kitten/components';
import { apiService } from '../utils/api';
import BluetoothManager, { CommandType, ConnectionState } from '../utils/BluetoothManager';
import { rpx } from '../utils/rpx';
import Slider from './slider';
interface DeviceControlProps {
defaultDevice: any;
onDeviceUpdate?: () => void;
}
const DeviceControl: React.FC<DeviceControlProps> = ({ defaultDevice, onDeviceUpdate }) => {
const [isConnecting, setIsConnecting] = useState(false);
const [showBluetoothStatus, setShowBluetoothStatus] = useState(false);
const [connectionState, setConnectionState] = useState<ConnectionState>(ConnectionState.DISCONNECTED);
const retryAttemptRef = useRef(false);
const fadeAnim = useRef(new Animated.Value(0)).current;
const slideAnim = useRef(new Animated.Value(-rpx(272))).current;
const showWithAnimation = () => {
setShowBluetoothStatus(true);
Animated.parallel([
Animated.timing(fadeAnim, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}),
Animated.spring(slideAnim, {
toValue: 0,
useNativeDriver: true,
friction: 8,
})
]).start();
};
const hideWithAnimation = () => {
Animated.parallel([
Animated.timing(fadeAnim, {
toValue: 0,
duration: 300,
useNativeDriver: true,
}),
Animated.timing(slideAnim, {
toValue: -rpx(272),
duration: 300,
useNativeDriver: true,
})
]).start(() => {
setShowBluetoothStatus(false);
});
};
const handleBluetoothConnection = async (isRetry = false): Promise<boolean> => {
if (isConnecting) return false;
try {
setIsConnecting(true);
showWithAnimation();
const targetMac = 'FD51BB7A4EE0'; // 暂时使用固定MAC
console.log(`${isRetry ? '重试' : '开始'}连接设备, 目标MAC:`, targetMac);
const initialized = await BluetoothManager.init();
if (!initialized) {
throw new Error('蓝牙初始化失败');
}
let targetDevice = null;
let scannedDevices = new Set();
await BluetoothManager.startScan((device) => {
const deviceName = device.name || device.localName || '';
const macMatch = deviceName.match(/BBLE:([A-Fa-f0-9]{12})/);
const deviceMac = macMatch ? macMatch[1] : '';
if (!scannedDevices.has(deviceMac) && deviceMac) {
scannedDevices.add(deviceMac);
console.log('扫描到设备:', {
name: deviceName,
extractedMac: deviceMac,
rssi: device.rssi
});
}
if (deviceMac === targetMac) {
console.log('找到目标设备:', deviceMac);
targetDevice = device;
BluetoothManager.stopScan();
}
});
await new Promise(resolve => setTimeout(resolve, 5000));
BluetoothManager.stopScan();
if (!targetDevice) {
throw new Error('未找到指定设备,请确保设备在范围内且已开启');
}
const connected = await BluetoothManager.connectToDevice(targetDevice);
if (!connected) {
throw new Error('连接设备失败');
}
console.log('成功连接到设备');
retryAttemptRef.current = false;
return true;
} catch (error) {
console.error(`${isRetry ? '重试' : ''}蓝牙连接失败:`, error);
if (!isRetry && !retryAttemptRef.current) {
console.log('将在3秒后尝试重新连接...');
retryAttemptRef.current = true;
setTimeout(async () => {
await handleBluetoothConnection(true);
}, 3000);
} else {
setTimeout(hideWithAnimation, 3000);
}
return false;
} finally {
setIsConnecting(false);
}
};
const handleRing = async () => {
console.log('响铃操作 - 设备信息:', {
完整设备信息: defaultDevice,
设备SN: defaultDevice?.sn,
当前时间: new Date().toISOString()
});
if (!defaultDevice?.sn) {
Alert.alert('提示', '请先选择设备');
return;
}
try {
const response = await apiService.ring(defaultDevice.sn);
console.log('响铃API响应:', response);
if (response.code != 200) {
retryAttemptRef.current = false;
const connected = await handleBluetoothConnection(false);
if (connected) {
await BluetoothManager.playResponse();
}
}
} catch (error) {
console.error('响铃失败:', error);
retryAttemptRef.current = false;
const connected = await handleBluetoothConnection(false);
if (connected) {
await BluetoothManager.playResponse();
}
}
};
const handleLockStatus = async (status: boolean) => {
if (!defaultDevice || !defaultDevice.sn) {
console.warn('设备数据无效:', {
设备对象: defaultDevice,
操作类型: status ? '开锁' : '关锁'
});
Alert.alert('提示', '设备数据无效,请确保设备已正确选择');
return;
}
const response = await (status ?
apiService.unlocking(defaultDevice.sn) :
apiService.lock(defaultDevice.sn));
console.log('开关锁API响应:', {
响应数据: response,
操作类型: status ? '开锁' : '关锁',
时间: new Date().toISOString()
});
if (response.code != 200) {
retryAttemptRef.current = false;
const connected = await handleBluetoothConnection(false);
if (connected) {
try {
await (status ?
BluetoothManager.openDevice() :
BluetoothManager.closeDevice());
if (onDeviceUpdate) {
onDeviceUpdate(); // 蓝牙操作成功后调用更新
}
} catch (bluetoothError) {
console.error('蓝牙命令执行失败:', bluetoothError);
Alert.alert('错误', '蓝牙命令执行失败,请重试');
}
} else {
Alert.alert('提示', '蓝牙连接失败,请确保设备在范围内且已开启');
}
} else {
// API 调用成功,直接更新状态
if (onDeviceUpdate) {
onDeviceUpdate();
}
}
};
const getPowerColor = (power: number): string => {
if (power >= 60) {
return 'rgba(89, 202, 112, 0.5)';
} else if (power >= 20) {
return 'rgba(255, 149, 0, 0.5)';
} else {
return 'rgba(255, 69, 58, 0.5)';
}
};
useEffect(() => {
const removeListener = BluetoothManager.addConnectionStateListener((state) => {
console.log('蓝牙连接状态变化:', state);
setConnectionState(state);
if (state === ConnectionState.DISCONNECTED) {
hideWithAnimation();
}
});
return () => {
removeListener();
BluetoothManager.disconnect();
hideWithAnimation();
};
}, []);
return (
<>
<View style={styles.infoBox}>
<View style={styles.eleBox}>
<View style={[
styles.eleType,
{
height: `${defaultDevice?.remainingPower || 0}%`,
backgroundColor: getPowerColor(defaultDevice?.remainingPower || 0)
}
]}>
<Text style={styles.eleTypeTxt}>{defaultDevice?.remainingPower || 0}%</Text>
</View>
</View>
<View style={styles.carBox}>
<Image
source={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/uVnIDwcwQP7oo12PeYVJ' }}
style={{ width: rpx(440), height: rpx(340) }}
/>
<View style={styles.txtbox}>
<View style={styles.yuan}></View>
<Text style={styles.txt}> {defaultDevice?.lockStatus == 1 ? '开锁' : '关锁'}</Text>
</View>
</View>
{showBluetoothStatus && (
<Animated.View
style={[
styles.switch,
{
opacity: fadeAnim,
transform: [{ translateX: slideAnim }]
}
]}
>
{isConnecting ? (
<>
<Spinner size='small' status='warning' />
<Text style={[styles.statusText, styles.connecting]}></Text>
<Image
source={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/ukhgaoFSHmlJWkgyC4U4' }}
style={styles.bluetooth}
/>
</>
) : (
<>
<Text style={[styles.statusText, styles.connected]}></Text>
<Image
source={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/udli5LEVfOT62XShk99A' }}
style={styles.bluetooth}
/>
</>
)}
</Animated.View>
)}
</View>
<View style={styles.car_stause_box}>
<TouchableOpacity
style={styles.car_stause_li}
onPress={handleRing}
disabled={isConnecting}
>
<Image
source={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/uro1vIU1WydjNWgi7PUg' }}
style={{ width: rpx(90), height: rpx(90), marginLeft: rpx(18) }}
/>
<Text style={styles.stauseText}></Text>
</TouchableOpacity>
{defaultDevice && (
<Slider
key={defaultDevice.sn}
lockStatus={Number(defaultDevice.lockStatus)}
onStatusChange={handleLockStatus}
disabled={isConnecting}
defaultDevice={defaultDevice}
/>
)}
<View style={styles.car_stause_li}>
<Image
source={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/uVpJNxwWXlyXt4IdHQoe' }}
style={{ width: rpx(90), height: rpx(90), marginLeft: rpx(18) }}
/>
<Text style={styles.stauseText}></Text>
</View>
</View>
</>
);
};
const styles = StyleSheet.create({
infoBox: {
marginTop: rpx(66),
flexDirection: 'row',
justifyContent: 'space-between'
},
carBox: {
width: rpx(440),
position: 'absolute',
top: rpx(20),
left: rpx(124),
zIndex: 0,
},
switch: {
width: rpx(272),
height: rpx(60),
backgroundColor: '#FFFFFF',
borderRadius: rpx(30),
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: rpx(20),
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 4,
},
statusText: {
fontSize: rpx(32),
fontWeight: '400',
marginLeft: rpx(10),
},
connecting: {
color: '#FF8282',
},
connected: {
color: '#4297F3',
marginLeft: rpx(40),
},
bluetooth: {
width: rpx(60),
height: rpx(60),
marginLeft: 'auto',
},
Bind_type: {
flexDirection: 'row',
width: rpx(272),
height: rpx(60),
backgroundColor: '#fff',
borderRadius: rpx(30),
alignItems: 'center',
},
txtbox: {
width: rpx(440),
justifyContent: 'center',
flexDirection: 'row',
alignItems: 'center',
},
txt: {
marginLeft: rpx(14),
fontSize: rpx(28),
fontWeight: '400',
color: '#3D3D3D',
},
yuan: {
width: rpx(22),
height: rpx(22),
borderRadius: rpx(11),
backgroundColor: 'rgba(255, 130, 130, 1)',
},
eleBox: {
paddingTop: rpx(14),
paddingRight: rpx(6),
paddingBottom: rpx(6),
paddingLeft: rpx(6),
width: rpx(86),
height: rpx(166),
borderRadius: rpx(16),
backgroundColor: '#FFFFFF',
justifyContent: 'flex-end',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 4,
borderWidth: 1,
borderColor: '#E0E0E0',
},
eleType: {
width: '100%',
minHeight: rpx(30),
borderRadius: rpx(16),
position: 'relative',
},
eleTypeTxt: {
position: 'absolute',
top: rpx(-40),
width: '100%',
textAlign: 'center',
fontSize: rpx(32),
color: '#3D3D3D',
textShadowColor: 'rgba(255, 255, 255, 0.5)',
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 2
},
car_stause_box: {
marginTop: rpx(308),
flexDirection: 'row',
justifyContent: 'space-around'
},
car_stause_li: {
width: rpx(136),
justifyContent: 'center'
},
stauseText: {
fontSize: rpx(32),
color: '#3D3D3D',
textAlign: 'center',
marginTop: rpx(24)
},
});
export default DeviceControl;

View File

@ -6,21 +6,21 @@ interface SliderProps {
onStatusChange?: (status: boolean) => void; onStatusChange?: (status: boolean) => void;
} }
// 添加 rpx 计算函数
const { width: SCREEN_WIDTH } = Dimensions.get('window'); const { width: SCREEN_WIDTH } = Dimensions.get('window');
const rpx = (px: number) => { const rpx = (px: number) => {
return (SCREEN_WIDTH / 750) * px; return (SCREEN_WIDTH / 750) * px;
}; };
const Slider: React.FC<SliderProps> = ({ lockStatus = 1, onStatusChange }) => { const Slider: React.FC<SliderProps> = ({ lockStatus = 1, onStatusChange }) => {
const translateX = useRef(new Animated.Value(lockStatus === 0 ? rpx(180) : 0)).current; // 修改初始值逻辑lockStatus = 0关锁时滑块在左侧lockStatus = 1开锁时滑块在右侧
const translateX = useRef(new Animated.Value(lockStatus === 0 ? 0 : rpx(180))).current;
const maxWidth = rpx(180); const maxWidth = rpx(180);
const buttonWidth = rpx(86); const buttonWidth = rpx(86);
const [isRight, setIsRight] = useState(lockStatus === 0); // 修改状态判断lockStatus = 1 表示开锁isRight = true
const [isRight, setIsRight] = useState(lockStatus === 1);
const hasVibratedRef = useRef(false); const hasVibratedRef = useRef(false);
const offsetRef = useRef(0); const offsetRef = useRef(0);
// 添加监听器
useEffect(() => { useEffect(() => {
const id = translateX.addListener(({ value }) => { const id = translateX.addListener(({ value }) => {
if (value >= maxWidth * 0.95) { if (value >= maxWidth * 0.95) {
@ -54,13 +54,7 @@ const Slider: React.FC<SliderProps> = ({ lockStatus = 1, onStatusChange }) => {
offsetRef.current = translateX._value; offsetRef.current = translateX._value;
}, },
onPanResponderMove: (_, gestureState) => { onPanResponderMove: (_, gestureState) => {
let newValue; let newValue = offsetRef.current + gestureState.dx;
if (isRight) {
newValue = offsetRef.current + gestureState.dx;
} else {
newValue = offsetRef.current + gestureState.dx;
}
newValue = Math.max(0, Math.min(maxWidth, newValue)); newValue = Math.max(0, Math.min(maxWidth, newValue));
translateX.setValue(newValue); translateX.setValue(newValue);
@ -100,7 +94,13 @@ const Slider: React.FC<SliderProps> = ({ lockStatus = 1, onStatusChange }) => {
useNativeDriver: false, useNativeDriver: false,
}).start(() => { }).start(() => {
hasVibratedRef.current = false; hasVibratedRef.current = false;
onStatusChange?.(finalIsRight); console.log('Slider 状态改变:', {
finalIsRight,
当前状态: finalIsRight ? '开锁' : '关锁'
});
if (onStatusChange) {
onStatusChange(finalIsRight);
}
}); });
}, },
}) })
@ -109,10 +109,8 @@ const Slider: React.FC<SliderProps> = ({ lockStatus = 1, onStatusChange }) => {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<View style={styles.car_Opne_box}> <View style={styles.car_Opne_box}>
{/* 默认背景 */}
<View style={styles.defaultBackground} /> <View style={styles.defaultBackground} />
{/* 蓝色滑动背景 */}
<Animated.View <Animated.View
style={[ style={[
styles.background, styles.background,
@ -122,7 +120,6 @@ const Slider: React.FC<SliderProps> = ({ lockStatus = 1, onStatusChange }) => {
]} ]}
/> />
{/* 滑块 */}
<Animated.View <Animated.View
{...panResponder.panHandlers} {...panResponder.panHandlers}
style={[ style={[
@ -139,7 +136,6 @@ const Slider: React.FC<SliderProps> = ({ lockStatus = 1, onStatusChange }) => {
/> />
</Animated.View> </Animated.View>
{/* 箭头图标 */}
<Animated.Image <Animated.Image
source={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/uEJob4XbADaL9ohOTVTL' }} source={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/uEJob4XbADaL9ohOTVTL' }}
style={[ style={[

View File

@ -20,7 +20,6 @@ const Tab = createBottomTabNavigator<MainTabParamList>();
// 定义需要显示底部导航栏的路由名称 // 定义需要显示底部导航栏的路由名称
const showTabBarRoutes = ['爱车', '个人中心']; const showTabBarRoutes = ['爱车', '个人中心'];
const MainNavigator = () => { const MainNavigator = () => {
return ( return (
<Tab.Navigator <Tab.Navigator
@ -29,10 +28,18 @@ const MainNavigator = () => {
tabBarHideOnKeyboard: true, tabBarHideOnKeyboard: true,
}} }}
tabBar={props => { tabBar={props => {
const routeName = getFocusedRouteNameFromRoute(props.state.routes[props.state.index]) ?? '爱车'; // 获取当前路由名称和路由状态
if (!showTabBarRoutes.includes(routeName)) { const currentRoute = props.state.routes[props.state.index];
const routeName = getFocusedRouteNameFromRoute(currentRoute);
// 只在主页面(爱车)和个人中心显示底部导航栏
const showTabBarRoutes = ['Home', undefined]; // undefined 表示在根路由时显示
// 如果不是主页面或个人中心,则隐藏底部导航栏
if (routeName && !showTabBarRoutes.includes(routeName)) {
return null; return null;
} }
return ( return (
<BottomNavigation <BottomNavigation
selectedIndex={props.state.index} selectedIndex={props.state.index}
@ -69,10 +76,12 @@ const MainNavigator = () => {
<Tab.Screen <Tab.Screen
name="爱车" name="爱车"
component={HomeStack} component={HomeStack}
options={{ tabBarLabel: '爱车' }}
/> />
<Tab.Screen <Tab.Screen
name="个人中心" name="个人中心"
component={ProfileScreens} component={ProfileScreens}
options={{ tabBarLabel: '个人中心' }}
/> />
</Tab.Navigator> </Tab.Navigator>
); );

View File

@ -103,4 +103,7 @@ export const apiService = {
getDeviceList: () => api.get('/appVerify/getDeviceListByMerchantToken'), getDeviceList: () => api.get('/appVerify/getDeviceListByMerchantToken'),
toggleDefault: (sn: string) => api.put(`/appVerify/toggleDefault?sn=${sn}`), toggleDefault: (sn: string) => api.put(`/appVerify/toggleDefault?sn=${sn}`),
ring: (sn: string) => api.post(`/app/device/ring?sn=${sn}`),
unlocking: (sn: string) => api.post(`/appVerify/admin/unlocking?sn=${sn}`),
lock: (sn: string) => api.post(`/appVerify/admin/lock?sn=${sn}`),
}; };

View File

@ -9,54 +9,81 @@ import { StackNavigationProp } from '@react-navigation/stack';
type RootStackParamList = { type RootStackParamList = {
Home: undefined; Home: undefined;
DeviceList: undefined; DeviceList: undefined;
DeviceMap: undefined; DeviceMap: {
sn: string;
};
}; };
interface DeviceControlProps {
defaultDevice: any;
}
type NavigationProp = StackNavigationProp<RootStackParamList>; type NavigationProp = StackNavigationProp<RootStackParamList>;
const MiniMap = () => { const MiniMap: React.FC<DeviceControlProps> = ({ defaultDevice }) => {
const navigation = useNavigation<NavigationProp>(); const navigation = useNavigation<NavigationProp>();
// useEffect(() => { // 确保经纬度是数字类型,使用设备的实际定位
// AMapSdk.init( const latitude = defaultDevice?.latitude ? parseFloat(defaultDevice.latitude) : 26.95500669;
// Platform.select({ const longitude = defaultDevice?.longitude ? parseFloat(defaultDevice.longitude) : 120.32736769;
// android: "812efd3a950ba3675f928630302c6463", // const address = defaultDevice?.location || '位置获取中...';
// }) // console.log(defaultDevice.latitude, defaultDevice.longitude, 'defaultDevice');
// ); // console.log(latitude, longitude, 'latitude, longitude');
// }, []); // 格式化更新时间
const formatUpdateTime = (timeStr: string): string => {
if (!timeStr) return '未知';
const handleMapPress = () => { const timestamp = new Date(timeStr).getTime();
navigation.navigate('DeviceMap'); const now = Date.now();
const diff = now - timestamp;
if (diff < 60000) { // 小于1分钟
return '刚刚';
} else if (diff < 3600000) { // 小于1小时
return `${Math.floor(diff / 60000)}分钟前`;
} else if (diff < 86400000) { // 小于24小时
return `${Math.floor(diff / 3600000)}小时前`;
} else {
return `${Math.floor(diff / 86400000)}天前`;
}
}; };
const latitude = 26.95500669; const updateTime = defaultDevice?.lastLocationTime ? formatUpdateTime(defaultDevice.lastLocationTime) : '未知';
const longitude = 120.32736769;
const imageUrl = "https://lxnapi.ccttiot.com/bike/img/static/uRx1B8B8acbquF2TO7Ry"; const handleMapPress = () => {
navigation.navigate('DeviceMap', {
sn: defaultDevice?.sn || ''
});
};
return ( return (
<View style={styles.container}> <View style={styles.container}>
{ defaultDevice&&(
<MapView <MapView
style={styles.map} style={styles.map}
mapType={MapType.Standard} mapType={MapType.Standard}
zoomControlsEnabled={false} zoomControlsEnabled={false}
scrollEnabled={true} // scrollEnabled={false}
zoomEnabled={true} // zoomEnabled={true}
initialCameraPosition={{ initialCameraPosition={{
target: { target: {
latitude, latitude: latitude,
longitude, longitude: longitude,
}, },
zoom: 15, zoom: 15,
}}
>
<Marker
position={{
latitude: latitude,
longitude: longitude,
}} }}
> icon={{ uri: "https://lxnapi.ccttiot.com/bike/img/static/uRx1B8B8acbquF2TO7Ry" }}
<Marker />
position={{ </MapView>
latitude, )}
longitude,
}}
icon={{ uri: imageUrl }}
/>
</MapView>
<LinearGradient <LinearGradient
colors={[ colors={[
@ -78,15 +105,15 @@ const MiniMap = () => {
<View style={styles.cont_left}> <View style={styles.cont_left}>
<View style={styles.cont_left_top}> <View style={styles.cont_left_top}>
<Text style={styles.cont_left_top_txt}> <Text style={styles.cont_left_top_txt}>
</Text> </Text>
<Text style={styles.updata}> <Text style={styles.updata}>
3 {updateTime}
</Text> </Text>
</View> </View>
<Text style={styles.cont_left_bot}> {/* <Text style={styles.cont_left_bot} numberOfLines={2} ellipsizeMode="tail">
200 {address}
</Text> </Text> */}
</View> </View>
<View style={styles.cont_right}> <View style={styles.cont_right}>
<Image <Image

View File

@ -18,6 +18,7 @@ import { rpx } from '../../utils/rpx';
import Slider from '../../components/slider'; import Slider from '../../components/slider';
import MiniMap from './MiniMap'; import MiniMap from './MiniMap';
import { apiService } from '../../utils/api'; import { apiService } from '../../utils/api';
import DeviceControl from '../../components/DeviceControl';
// 定义导航参数类型 // 定义导航参数类型
type RootStackParamList = { type RootStackParamList = {
Home: undefined; Home: undefined;
@ -126,6 +127,20 @@ const NormaIndex: React.FC = () => {
return 'rgba(255, 69, 58, 0.5)'; // 红色 return 'rgba(255, 69, 58, 0.5)'; // 红色
} }
}; };
const handleDeviceUpdate = async () => {
console.log('handleDeviceUpdate');
const response = await apiService.getDeviceList();
if (response?.code == 200 && response.data) {
const defaultDev = response.data.find((device: DeviceType) => device.isDefault == 1);
if (defaultDev) {
// console.log(defaultDev, 'defaultDev');
setDefaultDevice(defaultDev);
}
}
};
const fetchDeviceList = async () => { const fetchDeviceList = async () => {
try { try {
@ -134,7 +149,7 @@ const NormaIndex: React.FC = () => {
if (response?.code === 200 && response.data) { if (response?.code === 200 && response.data) {
const defaultDev = response.data.find((device: DeviceType) => device.isDefault == 1); const defaultDev = response.data.find((device: DeviceType) => device.isDefault == 1);
if (defaultDev) { if (defaultDev) {
console.log(defaultDev, 'defaultDev'); // console.log(defaultDev, 'defaultDev');
setDefaultDevice(defaultDev); setDefaultDevice(defaultDev);
} }
@ -202,8 +217,8 @@ const NormaIndex: React.FC = () => {
</View> </View>
</View> </View>
</View> </View>
<DeviceControl defaultDevice={defaultDevice} onDeviceUpdate={handleDeviceUpdate}/>
<View style={styles.infoBox}> {/* <View style={styles.infoBox}>
<View style={styles.eleBox}> <View style={styles.eleBox}>
<View style={[ <View style={[
styles.eleType, styles.eleType,
@ -262,12 +277,12 @@ const NormaIndex: React.FC = () => {
/> />
<Text style={styles.stauseText}></Text> <Text style={styles.stauseText}></Text>
</View> </View>
</View> </View> */}
<TouchableWithoutFeedback > <TouchableWithoutFeedback >
<View style={styles.mapWrapper}> <View style={styles.mapWrapper}>
<MiniMap /> <MiniMap defaultDevice={defaultDevice}/>
</View> </View>
</TouchableWithoutFeedback> </TouchableWithoutFeedback>

View File

@ -5,15 +5,16 @@ import {
Image, Image,
View, View,
TouchableOpacity, TouchableOpacity,
ImageBackground ImageBackground,
Alert
} from 'react-native'; } from 'react-native';
import { Text } from '@ui-kitten/components'; import { Text, Button } from '@ui-kitten/components';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import axios from 'axios';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import { rpx } from '../utils/rpx'; import { rpx } from '../utils/rpx';
import { apiService } from '../utils/api'; import { apiService } from '../utils/api';
import { auth } from '../utils/auth';
const ProfileScreen = () => { const ProfileScreen = () => {
const navigation = useNavigation(); const navigation = useNavigation();
const { setIsLoggedIn } = useAuth(); const { setIsLoggedIn } = useAuth();
@ -39,6 +40,17 @@ const ProfileScreen = () => {
} }
}; };
const handleLogout = async () => {
try {
await auth.removeToken(); // token
// setIsLoggedIn(false); //
// AppNavigator isLoggedIn
getUserInfo();
} catch (error) {
console.error('退出登录失败:', error);
}
};
return ( return (
<ImageBackground <ImageBackground
source={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/uYRs7Cv2Pbp95w3KjGO3' }} source={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/uYRs7Cv2Pbp95w3KjGO3' }}
@ -72,7 +84,7 @@ const ProfileScreen = () => {
/> />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{/* <Text style={styles.tit}>我的设备</Text> */}
{/* 管理与服务区域 */} {/* 管理与服务区域 */}
<Text style={styles.tit}>管理与服务</Text> <Text style={styles.tit}>管理与服务</Text>
<View style={styles.content}> <View style={styles.content}>
@ -136,10 +148,14 @@ const ProfileScreen = () => {
)} )}
</View> </View>
{/* TabBar */} {/* 退出登录按钮 */}
{/* <View style={styles.tabBarContainer}> <Button
<TabBar indexs={1} /> style={styles.logoutButton}
</View> */} status='danger'
onPress={handleLogout}
>
退出登录
</Button>
</View> </View>
</ScrollView> </ScrollView>
</ImageBackground> </ImageBackground>
@ -229,8 +245,10 @@ const styles = StyleSheet.create({
fontSize: rpx(32), fontSize: rpx(32),
color: '#3D3D3D', color: '#3D3D3D',
}, },
tabBarContainer: { logoutButton: {
marginLeft: rpx(-32), marginTop: rpx(40),
marginBottom: rpx(40),
borderRadius: rpx(30),
} }
}); });

View File

@ -1,95 +1,102 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Image, StatusBar, Linking, Platform, PermissionsAndroid } from 'react-native'; import { View, Text, StyleSheet, TouchableOpacity, Image, StatusBar, Linking, Platform, PermissionsAndroid } from 'react-native';
import { MapView, Marker, MapType } from 'react-native-amap3d'; import { MapView, Marker, MapType } from 'react-native-amap3d';
import Geolocation from '@react-native-community/geolocation'; import Geolocation from '@react-native-community/geolocation';
import { rpx } from '../../utils/rpx'; import { rpx } from '../../utils/rpx';
import { useNavigation } from '@react-navigation/native'; import { useNavigation, useRoute } from '@react-navigation/native';
import { transformFromWGSToGCJ } from '../../utils/coordtransform'; import { transformFromWGSToGCJ } from '../../utils/coordtransform';
import { apiService } from '../../utils/api';
interface DeviceLocation {
latitude: number;
longitude: number;
address?: string;
updateTime?: string;
}
interface DeviceInfo {
latitude: string;
longitude: string;
lastLocationTime: string;
location?: string;
}
const DeviceMap = () => { const DeviceMap = () => {
const navigation = useNavigation(); const navigation = useNavigation();
const [location, setLocation] = useState({ const route = useRoute();
latitude: 26.95500669, const { sn } = route.params as { sn: string };
longitude: 120.32736769, const mapRef = useRef<MapView | null>(null);
});
const [isLoading, setIsLoading] = useState(false);
// 请求 Android 定位权限 const [userLocation, setUserLocation] = useState<{latitude: number; longitude: number} | null>(null);
const requestAndroidPermission = async () => { const [deviceLocation, setDeviceLocation] = useState<DeviceLocation | null>(null);
const [isLoading, setIsLoading] = useState(true);
// 获取设备位置信息
const getDeviceLocation = async () => {
try { try {
const granted = await PermissionsAndroid.request( if (!sn) {
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION, console.warn('设备SN不存在');
{ return;
title: "位置信息权限", }
message: "需要获取您的位置信息",
buttonNeutral: "稍后询问",
buttonNegative: "取消",
buttonPositive: "确定"
}
);
return granted === PermissionsAndroid.RESULTS.GRANTED;
} catch (err) {
console.warn(err);
return false;
}
};
const getCurrentLocation = async () => { const response = await apiService.getDeviceInfo(sn);
setIsLoading(true); if (response.code === 200 && response.data) {
try { const { latitude, longitude, lastLocationTime, location } = response.data;
// ... 权限检查和配置代码保持不变 setDeviceLocation({
latitude: Number(latitude),
// 同时发起高精度和低精度定位请求 longitude: Number(longitude),
const highAccuracyPromise = new Promise((resolve, reject) => { address: location || '',
Geolocation.getCurrentPosition( updateTime: lastLocationTime || ''
resolve,
reject,
{
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 1000
}
);
});
const lowAccuracyPromise = new Promise((resolve, reject) => {
Geolocation.getCurrentPosition(
resolve,
reject,
{
enableHighAccuracy: false,
timeout: 10000,
maximumAge: 5000
}
);
});
Promise.race([highAccuracyPromise, lowAccuracyPromise])
.then((position: any) => {
console.log('原始定位结果:', position);
// 转换坐标系
const gcjLocation = transformFromWGSToGCJ(
position.coords.latitude,
position.coords.longitude
);
console.log('转换后的坐标:', gcjLocation);
setLocation(gcjLocation);
setIsLoading(false);
})
.catch((error) => {
console.error('定位失败:', error);
setIsLoading(false);
}); });
setIsLoading(false);
} else {
console.warn('获取设备信息失败:', response);
}
} catch (error) { } catch (error) {
console.error('获取位置信息失败:', error); console.error('获取设备位置信息失败:', error);
setIsLoading(false); }
};
const getCurrentLocation = () => {
// const watchId = Geolocation.watchPosition(
// (position) => {
// const gcjLocation = transformFromWGSToGCJ(
// position.coords.latitude,
// position.coords.longitude
// );
// setUserLocation(gcjLocation);
// },
// (error) => {
// console.error('位置监听错误:', error);
// },
// {
// enableHighAccuracy: true,
// timeout: 5000,
// maximumAge: 1000,
// distanceFilter: 10
// }
// );
if (!userLocation) {
console.warn('用户位置未获取');
return;
}
try {
// 使用 setStatus 方法移动地图
if (mapRef.current) {
mapRef.current.moveCamera({
target: userLocation,
zoom: 15,
}, 1000);
}
} catch (error) {
console.error('移动地图失败:', error);
} }
}; };
// 位置监听也需要转换坐标系
useEffect(() => { useEffect(() => {
getCurrentLocation(); getCurrentLocation();
getDeviceLocation();
const watchId = Geolocation.watchPosition( const watchId = Geolocation.watchPosition(
(position) => { (position) => {
@ -97,7 +104,9 @@ const DeviceMap = () => {
position.coords.latitude, position.coords.latitude,
position.coords.longitude position.coords.longitude
); );
setLocation(gcjLocation); console.log(gcjLocation,'gcjLocation');
setUserLocation(gcjLocation);
}, },
(error) => { (error) => {
console.error('位置监听错误:', error); console.error('位置监听错误:', error);
@ -113,37 +122,50 @@ const DeviceMap = () => {
return () => { return () => {
Geolocation.clearWatch(watchId); Geolocation.clearWatch(watchId);
}; };
}, []); }, [sn]);
// 跳转到高德地图 // 跳转到高德地图
const openAMap = async () => { const openAMap = async () => {
if (!deviceLocation) return;
const url = Platform.select({ const url = Platform.select({
android: `androidamap://navi?sourceApplication=appname&lat=${location.latitude}&lon=${location.longitude}&dev=0&style=2`, android: `androidamap://navi?sourceApplication=appname&lat=${deviceLocation.latitude}&lon=${deviceLocation.longitude}&dev=0&style=2`,
ios: `iosamap://navi?sourceApplication=appname&lat=${location.latitude}&lon=${location.longitude}&dev=0&style=2`, ios: `iosamap://navi?sourceApplication=appname&lat=${deviceLocation.latitude}&lon=${deviceLocation.longitude}&dev=0&style=2`,
}); });
const imageUrl = "https://lxnapi.ccttiot.com/bike/img/static/uRx1B8B8acbquF2TO7Ry"; const fallbackUrl = `https://uri.amap.com/navigation?to=${deviceLocation.longitude},${deviceLocation.latitude},目的地&mode=car&coordinate=gaode`;
const fallbackUrl = `https://uri.amap.com/navigation?to=${location.longitude},${location.latitude},目的地&mode=car&coordinate=gaode`;
try { try {
// 检查是否安装了高德地图
const supported = await Linking.canOpenURL(url); const supported = await Linking.canOpenURL(url);
if (supported) { if (supported) {
await Linking.openURL(url); await Linking.openURL(url);
} else { } else {
// 如果没有安装高德地图,则打开网页版
await Linking.openURL(fallbackUrl); await Linking.openURL(fallbackUrl);
} }
} catch (error) { } catch (error) {
console.error('无法打开高德地图:', error); console.error('无法打开高德地图:', error);
// 打开网页版作为后备方案
await Linking.openURL(fallbackUrl); await Linking.openURL(fallbackUrl);
} }
}; };
// 格式化时间
const formatTime = (timeString: string) => {
if (!timeString) return '12:00';
const date = new Date(timeString);
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
};
if (isLoading || !deviceLocation) {
return (
<View style={styles.container}>
<Text>...</Text>
</View>
);
}
return ( return (
<View style={styles.container}> <View style={styles.container}>
<MapView <MapView
ref={mapRef}
style={styles.map} style={styles.map}
mapType={MapType.Standard} mapType={MapType.Standard}
zoomControlsEnabled={false} zoomControlsEnabled={false}
@ -151,36 +173,64 @@ const DeviceMap = () => {
zoomEnabled={true} zoomEnabled={true}
initialCameraPosition={{ initialCameraPosition={{
target: { target: {
latitude: location.latitude, latitude: deviceLocation.latitude,
longitude: location.longitude, longitude: deviceLocation.longitude,
}, },
zoom: 15, zoom: 15,
}} }}
> >
{userLocation && (
<Marker
position={{
latitude: userLocation.latitude,
longitude: userLocation.longitude,
}}
// icon={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/uY9tYXXZztuE1VTLDl5y' }}
/>
)}
<Marker <Marker
position={{ position={{
latitude: location.latitude, latitude: deviceLocation.latitude,
longitude: location.longitude, longitude: deviceLocation.longitude,
}} }}
icon={{ uri: 'imageUrl' }} icon={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/uRx1B8B8acbquF2TO7Ry' }}
/> />
</MapView> </MapView>
<Image source={{ uri: 'https://api.ccttiot.com/smartmeter/img/static/uOCaLkinKXhZLxCkTFAQ' }} style={styles.locationIcon} />
<TouchableOpacity
style={styles.locationIcon}
onPress={getCurrentLocation}
>
<Image
source={{ uri: 'https://api.ccttiot.com/smartmeter/img/static/uOCaLkinKXhZLxCkTFAQ' }}
style={{ width: rpx(90), height: rpx(90) }}
/>
</TouchableOpacity>
<View style={styles.bottomCard}> <View style={styles.bottomCard}>
<View style={styles.addressInfo}> <View style={styles.addressInfo}>
<Text style={styles.addressText}> <Text style={styles.addressText}>
200 {deviceLocation.address || '获取地址中...'}
</Text> </Text>
<TouchableOpacity onPress={()=>{
const response = apiService.ring(sn);
console.log(response,'response');
}}>
<Image <Image
source={{ uri: 'https://api.ccttiot.com/smartmeter/img/static/ucBlLZW1SpAaKxSQYkr6' }} source={{ uri: 'https://api.ccttiot.com/smartmeter/img/static/ucBlLZW1SpAaKxSQYkr6' }}
style={styles.voiceIcon} style={styles.voiceIcon}
/> />
</TouchableOpacity>
</View> </View>
<View style={styles.timeBlock}> <View style={styles.timeBlock}>
<Image source={{ uri: 'https://api.ccttiot.com/smartmeter/img/static/uMnpK2e8az06pzJrKms5' }} style={styles.timeClock} /> <Image
source={{ uri: 'https://api.ccttiot.com/smartmeter/img/static/uMnpK2e8az06pzJrKms5' }}
style={styles.timeClock}
/>
<Text style={styles.timeText1}> <Text style={styles.timeText1}>
12:00 {deviceLocation.updateTime}
</Text> </Text>
</View> </View>
<TouchableOpacity <TouchableOpacity
@ -195,7 +245,6 @@ const DeviceMap = () => {
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: '#FFFFFF', backgroundColor: '#FFFFFF',
@ -213,18 +262,17 @@ const styles = StyleSheet.create({
}, },
timeBlock: { timeBlock: {
display: 'flex', display: 'flex',
flexDirection: 'row', // 确保内容水平排列 flexDirection: 'row',
alignSelf: 'flex-start', alignSelf: 'flex-start',
justifyContent:'center', justifyContent:'center',
alignItems:'center', alignItems:'center',
padding: rpx(8) , padding: rpx(8),
paddingHorizontal:rpx(18), paddingHorizontal:rpx(18),
backgroundColor: '#EFEFEF', backgroundColor: '#EFEFEF',
borderRadius: rpx(29), borderRadius: rpx(29),
flexWrap: 'nowrap', // 防止换行 flexWrap: 'nowrap',
marginBottom:rpx(40), marginBottom:rpx(40),
}, },
timeClock:{ timeClock:{
marginRight:rpx(14), marginRight:rpx(14),
width:rpx(26), width:rpx(26),
@ -233,47 +281,6 @@ const styles = StyleSheet.create({
timeText1:{ timeText1:{
fontSize:rpx(24), fontSize:rpx(24),
color:'#808080', color:'#808080',
},
header: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: rpx(88),
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: rpx(32),
backgroundColor: 'transparent',
},
backButton: {
width: rpx(44),
height: rpx(44),
justifyContent: 'center',
alignItems: 'center',
},
backIcon: {
width: rpx(32),
height: rpx(32),
},
timeContainer: {
flex: 1,
alignItems: 'center',
},
timeText: {
fontSize: rpx(28),
color: '#333333',
},
menuButton: {
width: rpx(44),
height: rpx(44),
justifyContent: 'center',
alignItems: 'center',
},
menuIcon: {
width: rpx(32),
height: rpx(32),
}, },
bottomCard: { bottomCard: {
position: 'absolute', position: 'absolute',