resultDetailPageComponent.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. import {
  2. View,
  3. Text,
  4. ScrollView,
  5. Image,
  6. useWindowDimensions,
  7. StyleSheet,
  8. Pressable,
  9. Platform,
  10. Linking,
  11. ActivityIndicator,
  12. Alert
  13. } from 'react-native';
  14. import React, { useCallback, useEffect, useMemo, useState } from 'react';
  15. import { SceneMap, TabBar, TabView } from 'react-native-tab-view';
  16. import NormalButton from '../global/normal_button';
  17. import { router, useFocusEffect, useLocalSearchParams } from 'expo-router';
  18. import { CheckMarkLogoSvg, DirectionLogoSvg, PreviousPageSvg } from '../global/SVG';
  19. import { SafeAreaView } from 'react-native-safe-area-context';
  20. import { chargeStationService } from '../../service/chargeStationService';
  21. import * as Location from 'expo-location';
  22. import { ChargingDetails, Remark, PriceWeek, Special } from '../../service/type/chargeStationType';
  23. import { useTranslation } from '../../util/hooks/useTranslation';
  24. interface ChargingStationTabViewProps {
  25. titles: string[];
  26. pricemodel_id: string;
  27. }
  28. interface ChargingPeriod{
  29. event_name: string;
  30. price: number;
  31. from: string;
  32. to: string;
  33. }
  34. const ChargingStationTabView: React.FC<ChargingStationTabViewProps> = ({ titles, pricemodel_id }) => {
  35. const layout = useWindowDimensions();
  36. const [list, setList] = useState<ChargingPeriod []>([])
  37. const [strWeek, setStrWeek] = useState<string>('')
  38. const { t } = useTranslation();
  39. // 添加AM/PM标识但保持24小时制
  40. const addPeriodToTime = (timeString: string): string => {
  41. // 假设输入格式为 HH.mm 或 HH:mm
  42. const [hours] = timeString.split(/[.:]/).map(Number);
  43. if (isNaN(hours)) return timeString;
  44. const period = hours >= 12 ? 'PM' : 'AM';
  45. return `${timeString}${period}`;
  46. };
  47. const fetchElectricityPrice = () => {
  48. chargeStationService.fetchElectricityPrice(pricemodel_id || 'a').then(res => {
  49. const date = new Date();
  50. const str = (date.toLocaleString('en-US', { weekday: 'short' })).toLowerCase();
  51. setStrWeek(date.toLocaleString('zh', { weekday: 'long' }))
  52. const newList = [] as ChargingPeriod[]
  53. res?.forEach((item) => {
  54. const obj = item[str as keyof PriceWeek]
  55. newList.push({event_name: item.event_name, price: obj.price, from: addPeriodToTime(obj.from),to: addPeriodToTime(obj.to)})
  56. setList(newList)
  57. })
  58. })
  59. };
  60. useEffect(() => {
  61. // 初始加载
  62. fetchElectricityPrice();
  63. // 计算到下一个整点的时间
  64. const now = new Date();
  65. const nextHour = new Date(now);
  66. nextHour.setHours(nextHour.getHours() + 1, 0, 0, 0); // 设置为下一个整点
  67. const timeToNextHour = nextHour.getTime() - now.getTime();
  68. // 设置第一次定时器,在下一个整点触发
  69. const firstTimer = setTimeout(() => {
  70. fetchElectricityPrice();
  71. // 之后每小时执行一次
  72. const hourlyInterval = setInterval(fetchElectricityPrice, 60 * 60 * 1000);
  73. // 保存interval ID以便清理
  74. intervalRef.current = hourlyInterval;
  75. }, timeToNextHour);
  76. // 保存timeout ID以便清理
  77. timeoutRef.current = firstTimer;
  78. // 清理函数
  79. return () => {
  80. clearTimeout(firstTimer);
  81. if (intervalRef.current) {
  82. clearInterval(intervalRef.current);
  83. }
  84. };
  85. }, [pricemodel_id]);
  86. // 使用useRef保存定时器ID
  87. const timeoutRef = React.useRef<NodeJS.Timeout | null>(null);
  88. const intervalRef = React.useRef<NodeJS.Timeout | null>(null);
  89. //tab 1
  90. const FirstRoute = () => (
  91. <ScrollView style={{ flex: 1, marginHorizontal: '5%' }}>
  92. <View>
  93. <View className='w-full flex-row justify-between mt-4'>
  94. <Text style={styles.leftLable}>{t('charging.result_detail_page.time_period')}</Text>
  95. <Text style={styles.rightLable}>{t('charging.result_detail_page.price_per_kwh')}</Text>
  96. </View>
  97. {
  98. list.map((item, index) => (
  99. <View key={index} className='w-full flex-row justify-between mt-3'>
  100. <Text style={styles.leftLable}>{item.from} - {item.to}</Text>
  101. <Text style={styles.rightLable}>${item.price}</Text>
  102. </View>
  103. ))
  104. }
  105. </View>
  106. </ScrollView>
  107. );
  108. //tab 2
  109. const SecondRoute = () => (
  110. <ScrollView style={{ flex: 1, marginHorizontal: '5%' }}>
  111. <Text className="text-lg " style={styles.text}></Text>
  112. </ScrollView>
  113. );
  114. const renderScene = SceneMap({
  115. firstRoute: FirstRoute,
  116. secondRoute: SecondRoute
  117. });
  118. const [routes] = React.useState([
  119. { key: 'firstRoute', title: titles[0] },
  120. { key: 'secondRoute', title: titles[1] }
  121. ]);
  122. const [index, setIndex] = React.useState(0);
  123. const renderTabBar = (props: any) => (
  124. <TabBar
  125. {...props}
  126. indicatorStyle={{
  127. backgroundColor: '#000000',
  128. height: 1
  129. }}
  130. style={{
  131. backgroundColor: 'white',
  132. elevation: 0,
  133. marginHorizontal: 15,
  134. borderBottomWidth: 0.5
  135. }}
  136. />
  137. );
  138. return (
  139. <TabView
  140. navigationState={{ index, routes }}
  141. renderScene={renderScene}
  142. onIndexChange={setIndex}
  143. initialLayout={{ width: layout.width }}
  144. renderTabBar={renderTabBar}
  145. commonOptions={{
  146. label: ({ route, focused }) => (
  147. <Text
  148. style={{
  149. color: focused ? '#000000' : '#CCCCCC',
  150. fontWeight: focused ? '300' : 'thin',
  151. fontSize: 17
  152. }}
  153. >
  154. {route.title}
  155. </Text>
  156. )
  157. }}
  158. />
  159. );
  160. };
  161. const ResultDetailPageComponent = () => {
  162. const params = useLocalSearchParams();
  163. const chargeStationID = params.chargeStationID as string;
  164. const chargeStationName = params.chargeStationName as string;
  165. const chargeStationAddress = params.chargeStationAddress as string;
  166. const pricemodel_id = params.pricemodel_id as string;
  167. const imageSourceProps = params.imageSource;
  168. const stationLng = params.stationLng as string;
  169. const stationLat = params.stationLat as string;
  170. const [isLoading, setIsLoading] = useState(true);
  171. const [imageSource, setImageSource] = useState();
  172. const [currentLocation, setCurrentLocation] = useState<Location.LocationObject | null>(null);
  173. const [price, setPrice] = useState('');
  174. const [newAvailableConnectors, setNewAvailableConnectors] = useState<any>([]);
  175. const { t , getCurrentLanguageConfig} = useTranslation();
  176. const isEn = getCurrentLanguageConfig()?.code === 'en';
  177. useEffect(() => {
  178. const imgObj = imageSourceProps? {uri: imageSourceProps} : require('../../assets/dummyStationPicture.png')
  179. setImageSource(imgObj);
  180. }, [imageSourceProps])
  181. useEffect(() => {
  182. const fetchPrice = async () => {
  183. try {
  184. const price = await chargeStationService.fetchChargeStationPrice(chargeStationID);
  185. setPrice(price);
  186. } catch (error) {
  187. console.error('Error fetching price:', error);
  188. }
  189. };
  190. const getCurrentLocation = async () => {
  191. let { status } = await Location.requestForegroundPermissionsAsync();
  192. if (status !== 'granted') {
  193. console.error('Permission to access location was denied');
  194. return;
  195. }
  196. let location = await Location.getLastKnownPositionAsync({});
  197. setCurrentLocation(location);
  198. };
  199. getCurrentLocation();
  200. fetchPrice();
  201. }, []);
  202. useFocusEffect(
  203. useCallback(() => {
  204. setIsLoading(true);
  205. let isMounted = true; // Simple cleanup flag
  206. const fetchAllConnectors = async () => {
  207. try {
  208. const newAvailableConnectors = await chargeStationService.NewfetchAvailableConnectors(isEn);
  209. // Only update state if component is still mounted
  210. if (isMounted) {
  211. setNewAvailableConnectors(newAvailableConnectors);
  212. }
  213. } catch (error) {
  214. console.error('Fetch error:', error);
  215. }
  216. };
  217. fetchAllConnectors();
  218. setIsLoading(false);
  219. // Simple cleanup - prevents state updates if component unmounts
  220. return () => {
  221. isMounted = false;
  222. };
  223. }, [])
  224. );
  225. const handleNavigationPress = (StationLat: string, StationLng: string) => {
  226. console.log('starting navigation press in resultDetail', StationLat, StationLng);
  227. if (StationLat && StationLng) {
  228. const label = encodeURIComponent(chargeStationName);
  229. const googleMapsUrl = `https://www.google.com/maps/search/?api=1&query=${StationLat},${StationLng}`;
  230. // Fallback URL for web browser
  231. const webUrl = `https://www.google.com/maps/dir/?api=1&destination=${StationLat},${StationLng}`;
  232. Linking.canOpenURL(googleMapsUrl)
  233. .then((supported) => {
  234. if (supported) {
  235. Linking.openURL(googleMapsUrl);
  236. } else {
  237. Linking.openURL(webUrl).catch((err) => {
  238. console.error('An error occurred', err);
  239. Alert.alert(
  240. t('common.error'),
  241. t('charging.result_detail_page.unable_open_maps'),
  242. [{ text: t('common.ok') }],
  243. { cancelable: false }
  244. );
  245. });
  246. }
  247. })
  248. .catch((err) => console.error('An error occurred', err));
  249. }
  250. };
  251. return (
  252. <SafeAreaView edges={['top', 'left', 'right']} className="flex-1 bg-white">
  253. <ScrollView className="flex-1 ">
  254. <View className="relative">
  255. <Image
  256. source={ imageSource }
  257. resizeMode="cover"
  258. style={{ flex: 1, width: '100%', height: 300 }}
  259. />
  260. <View className="absolute top-8 left-7 ">
  261. <Pressable
  262. onPress={() => {
  263. if (router.canGoBack()) {
  264. router.back();
  265. } else {
  266. router.replace('./');
  267. }
  268. }}
  269. >
  270. <PreviousPageSvg />
  271. </Pressable>
  272. </View>
  273. </View>
  274. <View className="flex-column mx-[5%] mt-[5%]">
  275. <View>
  276. <Text className="text-3xl ">{chargeStationName}</Text>
  277. </View>
  278. <View className="flex-row justify-between items-center">
  279. <Text className="text-base w-[82%]" style={{ color: '#888888' }}>
  280. {chargeStationAddress}
  281. </Text>
  282. <NormalButton
  283. title={
  284. <View className="flex-row items-center justify-center text-center space-x-1">
  285. <DirectionLogoSvg />
  286. <Text className="text-base">{t('charging.result_detail_page.route')}</Text>
  287. </View>
  288. }
  289. onPress={() => handleNavigationPress(stationLat, stationLng)}
  290. extendedStyle={{
  291. backgroundColor: '#E3F2F8',
  292. borderRadius: 61,
  293. paddingHorizontal: 14,
  294. paddingVertical: 6
  295. }}
  296. />
  297. </View>
  298. <View className="flex-row space-x-2 items-center pb-4 ">
  299. <CheckMarkLogoSvg />
  300. <Text>Walk-In</Text>
  301. {/* <Text>{distance} </Text> */}
  302. </View>
  303. <View
  304. className="flex-1 flex-row min-h-[20px] border-slate-300 my-6 rounded-2xl"
  305. style={{ borderWidth: 1 }}
  306. >
  307. <View className="flex-1 m-4">
  308. <View className="flex-1 flex-row ">
  309. <View className=" flex-1 flex-column ustify-between">
  310. <Text className="text-xl " style={styles.text}>
  311. {t('charging.result_detail_page.charging_fee')}
  312. </Text>
  313. <View className="flex-row items-center space-x-2">
  314. <Text className="text-3xl text-[#02677D]">${price}</Text>
  315. <Text style={styles.text}>{t('charging.result_detail_page.per_kwh')}</Text>
  316. </View>
  317. </View>
  318. <View className="items-center justify-center">
  319. <View className="w-[1px] h-[60%] bg-[#CCCCCC]" />
  320. </View>
  321. <View className=" flex-1 pl-4 flex-column justify-between">
  322. <Text className="text-xl " style={styles.text}>
  323. {t('charging.result_detail_page.available_connectors')}
  324. </Text>
  325. <View className="flex-row items-center space-x-2">
  326. {isLoading ? (
  327. <Text>{t('charging.result_detail_page.loading')}</Text>
  328. ) : (
  329. // <ActivityIndicator />
  330. <Text className="text-3xl text-[#02677D]">
  331. {
  332. newAvailableConnectors.find(
  333. (station: any) => station.stationID === chargeStationID
  334. )?.availableConnectors
  335. }
  336. </Text>
  337. )}
  338. </View>
  339. </View>
  340. </View>
  341. </View>
  342. </View>
  343. </View>
  344. <View className="min-h-[300px]">
  345. <Text className="text-xl pb-2 mx-[5%]" style={styles.text}>
  346. {t('charging.result_detail_page.station_info')}
  347. </Text>
  348. <ChargingStationTabView titles={[t('charging.result_detail_page.pricing_details'), t('charging.result_detail_page.others')]} pricemodel_id={pricemodel_id} />
  349. </View>
  350. </ScrollView>
  351. </SafeAreaView>
  352. );
  353. };
  354. export default ResultDetailPageComponent;
  355. const styles = StyleSheet.create({
  356. text: {
  357. fontWeight: 300,
  358. color: '#000000'
  359. },
  360. leftLable: {
  361. width: '65%',
  362. fontSize: 17,
  363. color:'#000000',
  364. textAlign: 'center'
  365. },
  366. rightLable: {
  367. fontSize: 17,
  368. width: '30%',
  369. textAlign: 'center',
  370. borderLeftWidth: 1,
  371. paddingLeft: 0,
  372. },
  373. });