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

View File

@ -20,7 +20,6 @@ const Tab = createBottomTabNavigator<MainTabParamList>();
// 定义需要显示底部导航栏的路由名称
const showTabBarRoutes = ['爱车', '个人中心'];
const MainNavigator = () => {
return (
<Tab.Navigator
@ -29,10 +28,18 @@ const MainNavigator = () => {
tabBarHideOnKeyboard: true,
}}
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 (
<BottomNavigation
selectedIndex={props.state.index}
@ -69,10 +76,12 @@ const MainNavigator = () => {
<Tab.Screen
name="爱车"
component={HomeStack}
options={{ tabBarLabel: '爱车' }}
/>
<Tab.Screen
name="个人中心"
component={ProfileScreens}
options={{ tabBarLabel: '个人中心' }}
/>
</Tab.Navigator>
);

View File

@ -103,4 +103,7 @@ export const apiService = {
getDeviceList: () => api.get('/appVerify/getDeviceListByMerchantToken'),
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 = {
Home: undefined;
DeviceList: undefined;
DeviceMap: undefined;
DeviceMap: {
sn: string;
};
};
interface DeviceControlProps {
defaultDevice: any;
}
type NavigationProp = StackNavigationProp<RootStackParamList>;
const MiniMap = () => {
const MiniMap: React.FC<DeviceControlProps> = ({ defaultDevice }) => {
const navigation = useNavigation<NavigationProp>();
// useEffect(() => {
// AMapSdk.init(
// Platform.select({
// android: "812efd3a950ba3675f928630302c6463",
// })
// );
// }, []);
const handleMapPress = () => {
navigation.navigate('DeviceMap');
// 确保经纬度是数字类型,使用设备的实际定位
const latitude = defaultDevice?.latitude ? parseFloat(defaultDevice.latitude) : 26.95500669;
const longitude = defaultDevice?.longitude ? parseFloat(defaultDevice.longitude) : 120.32736769;
// 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 timestamp = new Date(timeStr).getTime();
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 longitude = 120.32736769;
const imageUrl = "https://lxnapi.ccttiot.com/bike/img/static/uRx1B8B8acbquF2TO7Ry";
const updateTime = defaultDevice?.lastLocationTime ? formatUpdateTime(defaultDevice.lastLocationTime) : '未知';
const handleMapPress = () => {
navigation.navigate('DeviceMap', {
sn: defaultDevice?.sn || ''
});
};
return (
<View style={styles.container}>
{ defaultDevice&&(
<MapView
style={styles.map}
mapType={MapType.Standard}
zoomControlsEnabled={false}
scrollEnabled={true}
zoomEnabled={true}
initialCameraPosition={{
target: {
latitude,
longitude,
},
zoom: 15,
style={styles.map}
mapType={MapType.Standard}
zoomControlsEnabled={false}
// scrollEnabled={false}
// zoomEnabled={true}
initialCameraPosition={{
target: {
latitude: latitude,
longitude: longitude,
},
zoom: 15,
}}
>
<Marker
position={{
latitude: latitude,
longitude: longitude,
}}
>
<Marker
position={{
latitude,
longitude,
}}
icon={{ uri: imageUrl }}
/>
</MapView>
icon={{ uri: "https://lxnapi.ccttiot.com/bike/img/static/uRx1B8B8acbquF2TO7Ry" }}
/>
</MapView>
)}
<LinearGradient
colors={[
@ -78,15 +105,15 @@ const MiniMap = () => {
<View style={styles.cont_left}>
<View style={styles.cont_left_top}>
<Text style={styles.cont_left_top_txt}>
</Text>
<Text style={styles.updata}>
3
{updateTime}
</Text>
</View>
<Text style={styles.cont_left_bot}>
200
</Text>
{/* <Text style={styles.cont_left_bot} numberOfLines={2} ellipsizeMode="tail">
{address}
</Text> */}
</View>
<View style={styles.cont_right}>
<Image

View File

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

View File

@ -5,15 +5,16 @@ import {
Image,
View,
TouchableOpacity,
ImageBackground
ImageBackground,
Alert
} from 'react-native';
import { Text } from '@ui-kitten/components';
import { Text, Button } from '@ui-kitten/components';
import { useNavigation } from '@react-navigation/native';
import axios from 'axios';
import { useAuth } from '../context/AuthContext';
import { rpx } from '../utils/rpx';
import { apiService } from '../utils/api';
import { auth } from '../utils/auth';
const ProfileScreen = () => {
const navigation = useNavigation();
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 (
<ImageBackground
source={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/uYRs7Cv2Pbp95w3KjGO3' }}
@ -72,7 +84,7 @@ const ProfileScreen = () => {
/>
</TouchableOpacity>
</View>
{/* <Text style={styles.tit}>我的设备</Text> */}
{/* 管理与服务区域 */}
<Text style={styles.tit}>管理与服务</Text>
<View style={styles.content}>
@ -135,11 +147,15 @@ const ProfileScreen = () => {
</TouchableOpacity>
)}
</View>
{/* TabBar */}
{/* <View style={styles.tabBarContainer}>
<TabBar indexs={1} />
</View> */}
{/* 退出登录按钮 */}
<Button
style={styles.logoutButton}
status='danger'
onPress={handleLogout}
>
退出登录
</Button>
</View>
</ScrollView>
</ImageBackground>
@ -229,8 +245,10 @@ const styles = StyleSheet.create({
fontSize: rpx(32),
color: '#3D3D3D',
},
tabBarContainer: {
marginLeft: rpx(-32),
logoutButton: {
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 { MapView, Marker, MapType } from 'react-native-amap3d';
import Geolocation from '@react-native-community/geolocation';
import { rpx } from '../../utils/rpx';
import { useNavigation } from '@react-navigation/native';
import { useNavigation, useRoute } from '@react-navigation/native';
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 navigation = useNavigation();
const [location, setLocation] = useState({
latitude: 26.95500669,
longitude: 120.32736769,
});
const [isLoading, setIsLoading] = useState(false);
const route = useRoute();
const { sn } = route.params as { sn: string };
const mapRef = useRef<MapView | null>(null);
const [userLocation, setUserLocation] = useState<{latitude: number; longitude: number} | null>(null);
const [deviceLocation, setDeviceLocation] = useState<DeviceLocation | null>(null);
const [isLoading, setIsLoading] = useState(true);
// 请求 Android 定位权限
const requestAndroidPermission = async () => {
// 获取设备位置信息
const getDeviceLocation = async () => {
try {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
{
title: "位置信息权限",
message: "需要获取您的位置信息",
buttonNeutral: "稍后询问",
buttonNegative: "取消",
buttonPositive: "确定"
}
);
return granted === PermissionsAndroid.RESULTS.GRANTED;
} catch (err) {
console.warn(err);
return false;
}
};
if (!sn) {
console.warn('设备SN不存在');
return;
}
const getCurrentLocation = async () => {
setIsLoading(true);
try {
// ... 权限检查和配置代码保持不变
// 同时发起高精度和低精度定位请求
const highAccuracyPromise = new Promise((resolve, reject) => {
Geolocation.getCurrentPosition(
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);
const response = await apiService.getDeviceInfo(sn);
if (response.code === 200 && response.data) {
const { latitude, longitude, lastLocationTime, location } = response.data;
setDeviceLocation({
latitude: Number(latitude),
longitude: Number(longitude),
address: location || '',
updateTime: lastLocationTime || ''
});
setIsLoading(false);
} else {
console.warn('获取设备信息失败:', response);
}
} catch (error) {
console.error('获取位置信息失败:', error);
setIsLoading(false);
console.error('获取设备位置信息失败:', error);
}
};
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(() => {
getCurrentLocation();
getDeviceLocation();
const watchId = Geolocation.watchPosition(
(position) => {
@ -97,7 +104,9 @@ const DeviceMap = () => {
position.coords.latitude,
position.coords.longitude
);
setLocation(gcjLocation);
console.log(gcjLocation,'gcjLocation');
setUserLocation(gcjLocation);
},
(error) => {
console.error('位置监听错误:', error);
@ -113,37 +122,50 @@ const DeviceMap = () => {
return () => {
Geolocation.clearWatch(watchId);
};
}, []);
}, [sn]);
// 跳转到高德地图
const openAMap = async () => {
if (!deviceLocation) return;
const url = Platform.select({
android: `androidamap://navi?sourceApplication=appname&lat=${location.latitude}&lon=${location.longitude}&dev=0&style=2`,
ios: `iosamap://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=${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=${location.longitude},${location.latitude},目的地&mode=car&coordinate=gaode`;
const fallbackUrl = `https://uri.amap.com/navigation?to=${deviceLocation.longitude},${deviceLocation.latitude},目的地&mode=car&coordinate=gaode`;
try {
// 检查是否安装了高德地图
const supported = await Linking.canOpenURL(url);
if (supported) {
await Linking.openURL(url);
} else {
// 如果没有安装高德地图,则打开网页版
await Linking.openURL(fallbackUrl);
}
} catch (error) {
console.error('无法打开高德地图:', error);
// 打开网页版作为后备方案
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 (
<View style={styles.container}>
<MapView
ref={mapRef}
style={styles.map}
mapType={MapType.Standard}
zoomControlsEnabled={false}
@ -151,36 +173,64 @@ const DeviceMap = () => {
zoomEnabled={true}
initialCameraPosition={{
target: {
latitude: location.latitude,
longitude: location.longitude,
latitude: deviceLocation.latitude,
longitude: deviceLocation.longitude,
},
zoom: 15,
}}
>
{userLocation && (
<Marker
position={{
latitude: userLocation.latitude,
longitude: userLocation.longitude,
}}
// icon={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/uY9tYXXZztuE1VTLDl5y' }}
/>
)}
<Marker
position={{
latitude: location.latitude,
longitude: location.longitude,
latitude: deviceLocation.latitude,
longitude: deviceLocation.longitude,
}}
icon={{ uri: 'imageUrl' }}
icon={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/uRx1B8B8acbquF2TO7Ry' }}
/>
</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.addressInfo}>
<Text style={styles.addressText}>
200
{deviceLocation.address || '获取地址中...'}
</Text>
<TouchableOpacity onPress={()=>{
const response = apiService.ring(sn);
console.log(response,'response');
}}>
<Image
source={{ uri: 'https://api.ccttiot.com/smartmeter/img/static/ucBlLZW1SpAaKxSQYkr6' }}
style={styles.voiceIcon}
/>
</TouchableOpacity>
</View>
<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}>
12:00
{deviceLocation.updateTime}
</Text>
</View>
<TouchableOpacity
@ -195,7 +245,6 @@ const DeviceMap = () => {
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FFFFFF',
@ -213,18 +262,17 @@ const styles = StyleSheet.create({
},
timeBlock: {
display: 'flex',
flexDirection: 'row', // 确保内容水平排列
flexDirection: 'row',
alignSelf: 'flex-start',
justifyContent:'center',
alignItems:'center',
padding: rpx(8) ,
padding: rpx(8),
paddingHorizontal:rpx(18),
backgroundColor: '#EFEFEF',
borderRadius: rpx(29),
flexWrap: 'nowrap', // 防止换行
flexWrap: 'nowrap',
marginBottom:rpx(40),
},
timeClock:{
marginRight:rpx(14),
width:rpx(26),
@ -233,47 +281,6 @@ const styles = StyleSheet.create({
timeText1:{
fontSize:rpx(24),
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: {
position: 'absolute',