resultDetailPageComponent.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  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 { calculateDistance } from '../global/distanceCalculator';
  23. import { ChargingDetails, Remark, PriceWeek, Special } from '../../service/type/chargeStationType';
  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. // 添加AM/PM标识但保持24小时制
  39. const addPeriodToTime = (timeString: string): string => {
  40. // 假设输入格式为 HH.mm 或 HH:mm
  41. const [hours] = timeString.split(/[.:]/).map(Number);
  42. if (isNaN(hours)) return timeString;
  43. const period = hours >= 12 ? 'PM' : 'AM';
  44. return `${timeString}${period}`;
  45. };
  46. useEffect(() => {
  47. chargeStationService.fetchElectricityPrice(pricemodel_id || 'a').then(res => {
  48. const date = new Date();
  49. const str = (date.toLocaleString('en-US', { weekday: 'short' })).toLowerCase();
  50. setStrWeek(date.toLocaleString('zh', { weekday: 'long' }))
  51. const newList = [] as ChargingPeriod[]
  52. res?.forEach((item) => {
  53. const obj = item[str as keyof PriceWeek]
  54. newList.push({event_name: item.event_name, price: obj.price, from: addPeriodToTime(obj.from),to: addPeriodToTime(obj.to)})
  55. setList(newList)
  56. })
  57. })
  58. }, [pricemodel_id])
  59. //tab 1
  60. const FirstRoute = () => (
  61. <ScrollView style={{ flex: 1, marginHorizontal: '5%' }}>
  62. <View>
  63. <View className='w-full flex-row justify-between mt-4'>
  64. <Text style={styles.leftLable}>時段</Text>
  65. <Text style={styles.rightLable}>價格(/度)</Text>
  66. </View>
  67. {
  68. list.map((item, index) => (
  69. <View key={index} className='w-full flex-row justify-between mt-3'>
  70. <Text style={styles.leftLable}>{item.from} - {item.to}</Text>
  71. <Text style={styles.rightLable}>${item.price}</Text>
  72. </View>
  73. ))
  74. }
  75. </View>
  76. </ScrollView>
  77. );
  78. //tab 2
  79. const SecondRoute = () => (
  80. <ScrollView style={{ flex: 1, marginHorizontal: '5%' }}>
  81. <Text className="text-lg " style={styles.text}></Text>
  82. </ScrollView>
  83. );
  84. const renderScene = SceneMap({
  85. firstRoute: FirstRoute,
  86. secondRoute: SecondRoute
  87. });
  88. const [routes] = React.useState([
  89. { key: 'firstRoute', title: titles[0] },
  90. { key: 'secondRoute', title: titles[1] }
  91. ]);
  92. const [index, setIndex] = React.useState(0);
  93. const renderTabBar = (props: any) => (
  94. <TabBar
  95. {...props}
  96. indicatorStyle={{
  97. backgroundColor: '#000000',
  98. height: 1
  99. }}
  100. style={{
  101. backgroundColor: 'white',
  102. elevation: 0,
  103. marginHorizontal: 15,
  104. borderBottomWidth: 0.5
  105. }}
  106. />
  107. );
  108. return (
  109. <TabView
  110. navigationState={{ index, routes }}
  111. renderScene={renderScene}
  112. onIndexChange={setIndex}
  113. initialLayout={{ width: layout.width }}
  114. renderTabBar={renderTabBar}
  115. commonOptions={{
  116. label: ({ route, focused }) => (
  117. <Text
  118. style={{
  119. color: focused ? '#000000' : '#CCCCCC',
  120. fontWeight: focused ? '300' : 'thin',
  121. fontSize: 17
  122. }}
  123. >
  124. {route.title}
  125. </Text>
  126. )
  127. }}
  128. />
  129. );
  130. };
  131. const ResultDetailPageComponent = () => {
  132. const params = useLocalSearchParams();
  133. const chargeStationID = params.chargeStationID as string;
  134. const chargeStationName = params.chargeStationName as string;
  135. const chargeStationAddress = params.chargeStationAddress as string;
  136. const pricemodel_id = params.pricemodel_id as string;
  137. const imageSourceProps = params.imageSource;
  138. const stationLng = params.stationLng as string;
  139. const stationLat = params.stationLat as string;
  140. const [isLoading, setIsLoading] = useState(true);
  141. const [imageSource, setImageSource] = useState();
  142. const [currentLocation, setCurrentLocation] = useState<Location.LocationObject | null>(null);
  143. const [price, setPrice] = useState('');
  144. const [newAvailableConnectors, setNewAvailableConnectors] = useState<any>([]);
  145. useEffect(() => {
  146. const imgObj = imageSourceProps? {uri: imageSourceProps} : require('../../assets/dummyStationPicture.png')
  147. setImageSource(imgObj);
  148. }, [imageSourceProps])
  149. useEffect(() => {
  150. const fetchPrice = async () => {
  151. try {
  152. const price = await chargeStationService.fetchChargeStationPrice(chargeStationID);
  153. setPrice(price);
  154. } catch (error) {
  155. console.error('Error fetching price:', error);
  156. }
  157. };
  158. const getCurrentLocation = async () => {
  159. let { status } = await Location.requestForegroundPermissionsAsync();
  160. if (status !== 'granted') {
  161. console.error('Permission to access location was denied');
  162. return;
  163. }
  164. let location = await Location.getLastKnownPositionAsync({});
  165. setCurrentLocation(location);
  166. };
  167. getCurrentLocation();
  168. fetchPrice();
  169. }, []);
  170. useFocusEffect(
  171. useCallback(() => {
  172. setIsLoading(true);
  173. let isMounted = true; // Simple cleanup flag
  174. const fetchAllConnectors = async () => {
  175. try {
  176. const newAvailableConnectors = await chargeStationService.NewfetchAvailableConnectors();
  177. // Only update state if component is still mounted
  178. if (isMounted) {
  179. setNewAvailableConnectors(newAvailableConnectors);
  180. }
  181. } catch (error) {
  182. console.error('Fetch error:', error);
  183. }
  184. };
  185. fetchAllConnectors();
  186. setIsLoading(false);
  187. // Simple cleanup - prevents state updates if component unmounts
  188. return () => {
  189. isMounted = false;
  190. };
  191. }, []) // Add any missing dependencies here if needed
  192. );
  193. const handleNavigationPress = (StationLat: string, StationLng: string) => {
  194. console.log('starting navigation press in resultDetail', StationLat, StationLng);
  195. if (StationLat && StationLng) {
  196. const label = encodeURIComponent(chargeStationName);
  197. const googleMapsUrl = `https://www.google.com/maps/search/?api=1&query=${StationLat},${StationLng}`;
  198. // Fallback URL for web browser
  199. const webUrl = `https://www.google.com/maps/dir/?api=1&destination=${StationLat},${StationLng}`;
  200. Linking.canOpenURL(googleMapsUrl)
  201. .then((supported) => {
  202. if (supported) {
  203. Linking.openURL(googleMapsUrl);
  204. } else {
  205. Linking.openURL(webUrl).catch((err) => {
  206. console.error('An error occurred', err);
  207. Alert.alert(
  208. 'Error',
  209. "Unable to open Google Maps. Please make sure it's installed on your device.",
  210. [{ text: 'OK' }],
  211. { cancelable: false }
  212. );
  213. });
  214. }
  215. })
  216. .catch((err) => console.error('An error occurred', err));
  217. }
  218. };
  219. return (
  220. <SafeAreaView edges={['top', 'left', 'right']} className="flex-1 bg-white">
  221. <ScrollView className="flex-1 ">
  222. <View className="relative">
  223. <Image
  224. source={ imageSource }
  225. resizeMode="cover"
  226. style={{ flex: 1, width: '100%', height: 300 }}
  227. />
  228. <View className="absolute top-8 left-7 ">
  229. <Pressable
  230. onPress={() => {
  231. if (router.canGoBack()) {
  232. router.back();
  233. } else {
  234. router.replace('./');
  235. }
  236. }}
  237. >
  238. <PreviousPageSvg />
  239. </Pressable>
  240. </View>
  241. </View>
  242. <View className="flex-column mx-[5%] mt-[5%]">
  243. <View>
  244. <Text className="text-3xl ">{chargeStationName}</Text>
  245. </View>
  246. <View className="flex-row justify-between items-center">
  247. <Text className="text-base" style={{ color: '#888888' }}>
  248. {chargeStationAddress}
  249. </Text>
  250. <NormalButton
  251. title={
  252. <View className="flex-row items-center justify-center text-center space-x-1">
  253. <DirectionLogoSvg />
  254. <Text className="text-base ">路線</Text>
  255. </View>
  256. }
  257. onPress={() => handleNavigationPress(stationLat, stationLng)}
  258. extendedStyle={{
  259. backgroundColor: '#E3F2F8',
  260. borderRadius: 61,
  261. paddingHorizontal: 20,
  262. paddingVertical: 6
  263. }}
  264. />
  265. </View>
  266. <View className="flex-row space-x-2 items-center pb-4 ">
  267. <CheckMarkLogoSvg />
  268. <Text>Walk-In</Text>
  269. {/* <Text>{distance} </Text> */}
  270. </View>
  271. <View
  272. className="flex-1 flex-row min-h-[20px] border-slate-300 my-6 rounded-2xl"
  273. style={{ borderWidth: 1 }}
  274. >
  275. <View className="flex-1 m-4">
  276. <View className="flex-1 flex-row ">
  277. <View className=" flex-1 flex-column ustify-between">
  278. <Text className="text-xl " style={styles.text}>
  279. 收費
  280. </Text>
  281. <View className="flex-row items-center space-x-2">
  282. <Text className="text-3xl text-[#02677D]">${price}</Text>
  283. <Text style={styles.text}>每度電</Text>
  284. </View>
  285. </View>
  286. <View className="items-center justify-center">
  287. <View className="w-[1px] h-[60%] bg-[#CCCCCC]" />
  288. </View>
  289. <View className=" flex-1 pl-4 flex-column justify-between">
  290. <Text className="text-xl " style={styles.text}>
  291. 現時可用充電槍數目
  292. </Text>
  293. <View className="flex-row items-center space-x-2">
  294. {isLoading ? (
  295. <Text>Loading...</Text>
  296. ) : (
  297. // <ActivityIndicator />
  298. <Text className="text-3xl text-[#02677D]">
  299. {
  300. newAvailableConnectors.find(
  301. (station: any) => station.stationID === chargeStationID
  302. )?.availableConnectors
  303. }
  304. </Text>
  305. )}
  306. </View>
  307. </View>
  308. </View>
  309. </View>
  310. </View>
  311. </View>
  312. <View className="min-h-[300px]">
  313. <Text className="text-xl pb-2 mx-[5%]" style={styles.text}>
  314. 充電站資訊
  315. </Text>
  316. <ChargingStationTabView titles={['收費詳情', '其他']} pricemodel_id={pricemodel_id} />
  317. </View>
  318. </ScrollView>
  319. </SafeAreaView>
  320. );
  321. };
  322. export default ResultDetailPageComponent;
  323. const styles = StyleSheet.create({
  324. text: {
  325. fontWeight: 300,
  326. color: '#000000'
  327. },
  328. leftLable: {
  329. width: '70%',
  330. fontSize: 17,
  331. color:'#000000',
  332. textAlign: 'center'
  333. },
  334. rightLable: {
  335. fontSize: 17,
  336. width: '30%',
  337. textAlign: 'center',
  338. borderLeftWidth: 1,
  339. paddingLeft: 0,
  340. },
  341. });