tabView.tsx 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. //the size of the TabView will follow its parent-container's size.
  2. import * as React from 'react';
  3. import * as Location from 'expo-location';
  4. import {
  5. View,
  6. Text,
  7. useWindowDimensions,
  8. Dimensions,
  9. StyleSheet,
  10. Image,
  11. ImageSourcePropType,
  12. ActivityIndicator,
  13. Pressable
  14. } from 'react-native';
  15. import { TabView, SceneMap, TabBar } from 'react-native-tab-view';
  16. import { FlashList } from '@shopify/flash-list';
  17. import { useEffect, useState } from 'react';
  18. import { calculateDistance } from './distanceCalculator';
  19. import { router } from 'expo-router';
  20. export interface TabItem {
  21. imgURL: ImageSourcePropType;
  22. date: string;
  23. time: string;
  24. chargeStationName: string;
  25. chargeStationAddress: string;
  26. stationLat: string | number;
  27. stationLng: string | number;
  28. distance: string;
  29. format_order_id: string;
  30. }
  31. interface TabViewComponentProps {
  32. titles: string[];
  33. tabItems: TabItem[];
  34. completedReservationTabItems: TabItem[];
  35. isLoading?: boolean;
  36. }
  37. const TabViewComponent: React.FC<TabViewComponentProps> = ({
  38. titles,
  39. isLoading,
  40. tabItems,
  41. completedReservationTabItems
  42. }) => {
  43. const layout = useWindowDimensions();
  44. const [currentLocation, setCurrentLocation] = useState<Location.LocationObject | null>(null);
  45. useEffect(() => {
  46. const getCurrentLocation = async () => {
  47. let { status } = await Location.requestForegroundPermissionsAsync();
  48. if (status !== 'granted') {
  49. console.error('Permission to access location was denied');
  50. return;
  51. }
  52. let location = await Location.getLastKnownPositionAsync({});
  53. setCurrentLocation(location);
  54. };
  55. getCurrentLocation();
  56. }, []);
  57. // 修复 FirstRoute 组件
  58. const FirstRoute = ({ tabItems, isLoading, currentLocation }: {
  59. tabItems: TabItem[];
  60. isLoading?: boolean;
  61. currentLocation: Location.LocationObject | null
  62. }) => (
  63. <View style={{ flex: 1, backgroundColor: 'white' }}>
  64. {isLoading ? (
  65. <View className="items-center justify-center flex-1">
  66. <ActivityIndicator color="#34657b" />
  67. </View>
  68. ) : (
  69. <FlashList
  70. nestedScrollEnabled={true}
  71. data={tabItems.filter((item) => item?.actual_total_power && item?.actual_total_power !== 0)}
  72. renderItem={({ item }) => <TabItem item={item} currentLocation={currentLocation} />}
  73. estimatedItemSize={10}
  74. keyExtractor={(item, index) => index.toString()}
  75. />
  76. )}
  77. </View>
  78. );
  79. // 修复 SecondRoute 组件
  80. const SecondRoute = ({ completedReservationTabItems, currentLocation }: {
  81. completedReservationTabItems: TabItem[];
  82. currentLocation: Location.LocationObject | null
  83. }) => (
  84. <View style={{ flex: 1, backgroundColor: 'white' }}>
  85. <FlashList
  86. nestedScrollEnabled={true}
  87. data={completedReservationTabItems.filter(
  88. (item) => item.actual_total_power && item.actual_total_power > 1
  89. )}
  90. renderItem={({ item }) => <TabItem item={item} currentLocation={currentLocation} />}
  91. estimatedItemSize={20}
  92. keyExtractor={(item, index) => index.toString()}
  93. />
  94. </View>
  95. );
  96. // 更新 renderScene
  97. const renderScene = SceneMap({
  98. firstRoute: () => <FirstRoute tabItems={tabItems} isLoading={isLoading} currentLocation={currentLocation} />,
  99. secondRoute: () => <SecondRoute completedReservationTabItems={completedReservationTabItems} currentLocation={currentLocation} />
  100. });
  101. const routes = [
  102. { key: 'firstRoute', title: titles[0] },
  103. { key: 'secondRoute', title: titles[1] }
  104. ];
  105. const [index, setIndex] = React.useState(0);
  106. const renderTabBar = (props: any) => (
  107. <TabBar
  108. {...props}
  109. activeColor ='#025c72'
  110. inactiveColor='#888888'
  111. indicatorStyle={{
  112. backgroundColor: '#025c72',
  113. }}
  114. style={{
  115. backgroundColor: 'white',
  116. borderColor: '#DBE4E8',
  117. elevation: 0,
  118. marginHorizontal: 15,
  119. borderBottomWidth: 0.5
  120. }}
  121. labelStyle={{
  122. fontSize: 20, // 文字大小
  123. fontWeight: 700, // 字重
  124. }}
  125. />
  126. );
  127. return (
  128. <TabView
  129. navigationState={{ index, routes }}
  130. renderTabBar={renderTabBar}
  131. renderScene={renderScene}
  132. onIndexChange={setIndex}
  133. initialLayout={{ width: layout.width }}
  134. commonOptions={{
  135. labelStyle: {
  136. fontSize: 20, // 文字大小
  137. fontWeight: 700, // 字重
  138. }
  139. }}
  140. lazy
  141. />
  142. );
  143. };
  144. const TabItem = ({ item, currentLocation }: { item: TabItem; currentLocation: Location.LocationObject | null }) => {
  145. const [distance, setDistance] = useState<number | null>(null);
  146. useEffect(() => {
  147. const getDistance = async () => {
  148. if (currentLocation) {
  149. const result = await calculateDistance(
  150. Number(item.stationLat),
  151. Number(item.stationLng),
  152. currentLocation
  153. );
  154. setDistance(result);
  155. }
  156. };
  157. getDistance();
  158. }, [currentLocation, item.stationLat, item.stationLng]);
  159. const formatDistance = (distanceInMeters: number): string => {
  160. if (distanceInMeters < 1000) {
  161. return `${Math.round(distanceInMeters)}米`;
  162. } else {
  163. const distanceInKm = distanceInMeters / 1000;
  164. return `${distanceInKm.toFixed(1)}公里`;
  165. }
  166. };
  167. return (
  168. <Pressable
  169. onPress={() => {
  170. console.log(item.format_order_id);
  171. }}
  172. >
  173. <View style={styles.container}>
  174. <Image style={styles.image} source={item.imgURL} />
  175. <View className="flex flex-col gap-2 mr-2">
  176. <Text
  177. style={{
  178. fontWeight: '700',
  179. color: '#02677D',
  180. fontSize: 16
  181. }}
  182. >
  183. {`${item.date}日 - ${item.time}至${item.actual_end_time}`}
  184. </Text>
  185. <View className="flex flex-row justify-between space-x-2">
  186. <Text
  187. style={{
  188. fontWeight: '400',
  189. fontSize: 14,
  190. color: '#222222'
  191. }}
  192. >
  193. 已充電度數:{' '}
  194. {item.actual_total_power
  195. ? item.actual_total_power % 1 === 0
  196. ? item.actual_total_power
  197. : item.actual_total_power.toFixed(1)
  198. : ''}
  199. </Text>
  200. <Text
  201. style={{
  202. fontWeight: '400',
  203. fontSize: 14,
  204. color: '#222222'
  205. }}
  206. >
  207. 應付金額:{' '}
  208. {item.actual_fee !== undefined && item.actual_fee !== null
  209. ? item.actual_fee <= 0
  210. ? '$0'
  211. : item.actual_fee % 1 === 0
  212. ? `$${item.actual_fee}`
  213. : `$${item.actual_fee.toFixed(1)}`
  214. : ''}
  215. </Text>
  216. </View>
  217. <View className="flex flex-row space-x-2">
  218. <Text
  219. style={{
  220. fontWeight: '400',
  221. fontSize: 14,
  222. color: '#222222'
  223. }}
  224. >
  225. 每度電金額: ${item.current_price}
  226. </Text>
  227. </View>
  228. <Text
  229. style={{
  230. fontWeight: '400',
  231. fontSize: 14,
  232. color: '#888888'
  233. }}
  234. >
  235. {item.chargeStationName}
  236. </Text>
  237. </View>
  238. </View>
  239. </Pressable>
  240. );
  241. };
  242. export default TabViewComponent;
  243. const styles = StyleSheet.create({
  244. container: {
  245. flexDirection: 'row',
  246. width: '100%',
  247. flex: 1,
  248. alignItems: 'center'
  249. },
  250. image: {
  251. width: 100,
  252. height: 100,
  253. margin: 15,
  254. borderRadius: 10
  255. },
  256. textContainer: {
  257. flex: 1,
  258. flexDirection: 'column',
  259. gap: 8,
  260. marginTop: 20,
  261. marginRight: 8 // Add right margin to prevent text from touching the edge
  262. }
  263. });