tabView.tsx 9.7 KB

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