tabView.tsx 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  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. keyExtractor={(item, index) => index.toString()}
  74. />
  75. )}
  76. </View>
  77. );
  78. // 修复 SecondRoute 组件
  79. const SecondRoute = ({ completedReservationTabItems, currentLocation }: {
  80. completedReservationTabItems: TabItem[];
  81. currentLocation: Location.LocationObject | null
  82. }) => (
  83. <View style={{ flex: 1, backgroundColor: 'white' }}>
  84. <FlashList
  85. nestedScrollEnabled={true}
  86. data={completedReservationTabItems.filter(
  87. (item) => item.actual_total_power && item.actual_total_power > 1
  88. )}
  89. renderItem={({ item }) => <TabItem item={item} currentLocation={currentLocation} />}
  90. keyExtractor={(item, index) => index.toString()}
  91. />
  92. </View>
  93. );
  94. // 更新 renderScene
  95. const renderScene = SceneMap({
  96. firstRoute: () => <FirstRoute tabItems={tabItems} isLoading={isLoading} currentLocation={currentLocation} />,
  97. secondRoute: () => <SecondRoute completedReservationTabItems={completedReservationTabItems} currentLocation={currentLocation} />
  98. });
  99. const routes = [
  100. { key: 'firstRoute', title: titles[0] },
  101. { key: 'secondRoute', title: titles[1] }
  102. ];
  103. const [index, setIndex] = React.useState(0);
  104. const renderTabBar = (props: any) => (
  105. <TabBar
  106. {...props}
  107. activeColor ='#025c72'
  108. inactiveColor='#888888'
  109. indicatorStyle={{
  110. backgroundColor: '#025c72',
  111. }}
  112. style={{
  113. backgroundColor: 'white',
  114. borderColor: '#DBE4E8',
  115. elevation: 0,
  116. marginHorizontal: 15,
  117. borderBottomWidth: 0.5
  118. }}
  119. labelStyle={{
  120. fontSize: 20, // 文字大小
  121. fontWeight: 700, // 字重
  122. }}
  123. />
  124. );
  125. return (
  126. <TabView
  127. navigationState={{ index, routes }}
  128. renderTabBar={renderTabBar}
  129. renderScene={renderScene}
  130. onIndexChange={setIndex}
  131. initialLayout={{ width: layout.width }}
  132. commonOptions={{
  133. labelStyle: {
  134. fontSize: 20, // 文字大小
  135. fontWeight: 700, // 字重
  136. }
  137. }}
  138. lazy
  139. />
  140. );
  141. };
  142. const TabItem = ({ item, currentLocation }: { item: TabItem; currentLocation: Location.LocationObject | null }) => {
  143. const [distance, setDistance] = useState<number | null>(null);
  144. useEffect(() => {
  145. const getDistance = async () => {
  146. if (currentLocation) {
  147. const result = await calculateDistance(
  148. Number(item.stationLat),
  149. Number(item.stationLng),
  150. currentLocation
  151. );
  152. setDistance(result);
  153. }
  154. };
  155. getDistance();
  156. }, [currentLocation, item.stationLat, item.stationLng]);
  157. const formatDistance = (distanceInMeters: number): string => {
  158. if (distanceInMeters < 1000) {
  159. return `${Math.round(distanceInMeters)}米`;
  160. } else {
  161. const distanceInKm = distanceInMeters / 1000;
  162. return `${distanceInKm.toFixed(1)}公里`;
  163. }
  164. };
  165. return (
  166. <Pressable
  167. onPress={() => {
  168. console.log(item.format_order_id);
  169. }}
  170. >
  171. <View style={styles.container}>
  172. <Image style={styles.image} source={item.imgURL} />
  173. <View className="flex flex-col gap-2 mr-2">
  174. <Text
  175. style={{
  176. fontWeight: '700',
  177. color: '#02677D',
  178. fontSize: 16
  179. }}
  180. >
  181. {`${item.date}日 - ${item.time}至${item.actual_end_time}`}
  182. </Text>
  183. <View className="flex flex-row justify-between space-x-2">
  184. <Text
  185. style={{
  186. fontWeight: '400',
  187. fontSize: 14,
  188. color: '#222222'
  189. }}
  190. >
  191. 已充電度數:{' '}
  192. {item.actual_total_power
  193. ? item.actual_total_power % 1 === 0
  194. ? item.actual_total_power
  195. : item.actual_total_power.toFixed(1)
  196. : ''}
  197. </Text>
  198. <Text
  199. style={{
  200. fontWeight: '400',
  201. fontSize: 14,
  202. color: '#222222'
  203. }}
  204. >
  205. 應付金額:{' '}
  206. {item.actual_fee !== undefined && item.actual_fee !== null
  207. ? item.actual_fee <= 0
  208. ? '$0'
  209. : item.actual_fee % 1 === 0
  210. ? `$${item.actual_fee}`
  211. : `$${item.actual_fee.toFixed(1)}`
  212. : ''}
  213. </Text>
  214. </View>
  215. <View className="flex flex-row space-x-2">
  216. <Text
  217. style={{
  218. fontWeight: '400',
  219. fontSize: 14,
  220. color: '#222222'
  221. }}
  222. >
  223. 每度電金額: ${item.current_price}
  224. </Text>
  225. </View>
  226. <Text
  227. style={{
  228. fontWeight: '400',
  229. fontSize: 14,
  230. color: '#888888'
  231. }}
  232. >
  233. {item.chargeStationName}
  234. </Text>
  235. </View>
  236. </View>
  237. </Pressable>
  238. );
  239. };
  240. export default TabViewComponent;
  241. const styles = StyleSheet.create({
  242. container: {
  243. flexDirection: 'row',
  244. width: '100%',
  245. flex: 1,
  246. alignItems: 'center'
  247. },
  248. image: {
  249. width: 100,
  250. height: 100,
  251. margin: 15,
  252. borderRadius: 10
  253. },
  254. textContainer: {
  255. flex: 1,
  256. flexDirection: 'column',
  257. gap: 8,
  258. marginTop: 20,
  259. marginRight: 8 // Add right margin to prevent text from touching the edge
  260. }
  261. });