|
|
@@ -0,0 +1,209 @@
|
|
|
+//the size of the TabView will follow its parent-container's size.
|
|
|
+
|
|
|
+import * as React from 'react';
|
|
|
+import * as Location from 'expo-location';
|
|
|
+import {
|
|
|
+ View,
|
|
|
+ Text,
|
|
|
+ useWindowDimensions,
|
|
|
+ Dimensions,
|
|
|
+ StyleSheet,
|
|
|
+ Image,
|
|
|
+ ImageSourcePropType,
|
|
|
+ ActivityIndicator,
|
|
|
+ Pressable
|
|
|
+} from 'react-native';
|
|
|
+import { TabView, SceneMap, TabBar } from 'react-native-tab-view';
|
|
|
+import { FlashList } from '@shopify/flash-list';
|
|
|
+import { useEffect, useState } from 'react';
|
|
|
+import { calculateDistance } from './distanceCalculator';
|
|
|
+import { router } from 'expo-router';
|
|
|
+
|
|
|
+export interface TabItem {
|
|
|
+ imgURL: ImageSourcePropType;
|
|
|
+ date: string;
|
|
|
+ time: string;
|
|
|
+ chargeStationName: string;
|
|
|
+ chargeStationAddress: string;
|
|
|
+ stationLat: string | number;
|
|
|
+ stationLng: string | number;
|
|
|
+ distance: string;
|
|
|
+ format_order_id: string;
|
|
|
+ actual_total_power?: number;
|
|
|
+ actual_end_time?: string;
|
|
|
+ actual_fee?: number;
|
|
|
+ current_price?: number;
|
|
|
+}
|
|
|
+
|
|
|
+interface TabViewComponentProps {
|
|
|
+ titles: string[];
|
|
|
+ tabItems: TabItem[];
|
|
|
+ isLoading?: boolean;
|
|
|
+}
|
|
|
+
|
|
|
+const TabViewComponent: React.FC<TabViewComponentProps> = ({
|
|
|
+ titles,
|
|
|
+ isLoading,
|
|
|
+ tabItems,
|
|
|
+}) => {
|
|
|
+ const layout = useWindowDimensions();
|
|
|
+ const [currentLocation, setCurrentLocation] = useState<Location.LocationObject | null>(null);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ const getCurrentLocation = async () => {
|
|
|
+ let { status } = await Location.requestForegroundPermissionsAsync();
|
|
|
+ if (status !== 'granted') {
|
|
|
+ console.error('Permission to access location was denied');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ let location = await Location.getLastKnownPositionAsync({});
|
|
|
+ setCurrentLocation(location);
|
|
|
+ };
|
|
|
+
|
|
|
+ getCurrentLocation();
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ // 修复 FirstRoute 组件
|
|
|
+ const FirstRoute = ({ tabItems, isLoading, currentLocation }: {
|
|
|
+ tabItems: TabItem[];
|
|
|
+ isLoading?: boolean;
|
|
|
+ currentLocation: Location.LocationObject | null
|
|
|
+ }) => (
|
|
|
+ <View style={{ flex: 1, backgroundColor: 'white' }}>
|
|
|
+ {isLoading ? (
|
|
|
+ <View className="items-center justify-center flex-1">
|
|
|
+ <ActivityIndicator color="#34657b" />
|
|
|
+ </View>
|
|
|
+ ) : (
|
|
|
+ <FlashList
|
|
|
+ nestedScrollEnabled={true}
|
|
|
+ data={tabItems.filter((item) => item?.actual_total_power && item?.actual_total_power !== 0)}
|
|
|
+ renderItem={({ item }) => <TabItem item={item} currentLocation={currentLocation} />}
|
|
|
+ keyExtractor={(item, index) => index.toString()}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ </View>
|
|
|
+ );
|
|
|
+
|
|
|
+ return (
|
|
|
+ <FirstRoute tabItems={tabItems} isLoading={isLoading} currentLocation={currentLocation} />
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const TabItem = ({ item, currentLocation }: { item: TabItem; currentLocation: Location.LocationObject | null }) => {
|
|
|
+ const [distance, setDistance] = useState<number | null>(null);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ const getDistance = async () => {
|
|
|
+ if (currentLocation) {
|
|
|
+ const result = await calculateDistance(
|
|
|
+ Number(item.stationLat),
|
|
|
+ Number(item.stationLng),
|
|
|
+ currentLocation
|
|
|
+ );
|
|
|
+ setDistance(result);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ getDistance();
|
|
|
+ }, [currentLocation, item.stationLat, item.stationLng]);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Pressable
|
|
|
+ onPress={() => {
|
|
|
+ console.log(item.format_order_id);
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <View style={styles.container}>
|
|
|
+ <Image style={styles.image} source={item.imgURL} />
|
|
|
+ <View className="flex flex-col gap-2 mr-2">
|
|
|
+ <Text
|
|
|
+ style={{
|
|
|
+ fontWeight: '700',
|
|
|
+ color: '#02677D',
|
|
|
+ fontSize: 16
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {`${item.date}日 - ${item.time}至${item.actual_end_time}`}
|
|
|
+ </Text>
|
|
|
+ <View className="flex flex-row justify-between space-x-2">
|
|
|
+ <Text
|
|
|
+ style={{
|
|
|
+ fontWeight: '400',
|
|
|
+ fontSize: 14,
|
|
|
+ color: '#222222'
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ 已充電度數:{' '}
|
|
|
+ {item.actual_total_power
|
|
|
+ ? item.actual_total_power % 1 === 0
|
|
|
+ ? item.actual_total_power
|
|
|
+ : item.actual_total_power.toFixed(1)
|
|
|
+ : ''}
|
|
|
+ </Text>
|
|
|
+ <Text
|
|
|
+ style={{
|
|
|
+ fontWeight: '400',
|
|
|
+ fontSize: 14,
|
|
|
+ color: '#222222'
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ 應付金額:{' '}
|
|
|
+ {item.actual_fee !== undefined && item.actual_fee !== null
|
|
|
+ ? item.actual_fee <= 0
|
|
|
+ ? '$0'
|
|
|
+ : item.actual_fee % 1 === 0
|
|
|
+ ? `$${item.actual_fee}`
|
|
|
+ : `$${item.actual_fee.toFixed(1)}`
|
|
|
+ : ''}
|
|
|
+ </Text>
|
|
|
+ </View>
|
|
|
+ <View className="flex flex-row space-x-2">
|
|
|
+ <Text
|
|
|
+ style={{
|
|
|
+ fontWeight: '400',
|
|
|
+ fontSize: 14,
|
|
|
+ color: '#222222'
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ 每度電金額: ${item.current_price}
|
|
|
+ </Text>
|
|
|
+ </View>
|
|
|
+ <Text
|
|
|
+ style={{
|
|
|
+ fontWeight: '400',
|
|
|
+ fontSize: 14,
|
|
|
+ color: '#888888'
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {item.chargeStationName}
|
|
|
+ </Text>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ </Pressable>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default TabViewComponent;
|
|
|
+
|
|
|
+const styles = StyleSheet.create({
|
|
|
+ container: {
|
|
|
+ flexDirection: 'row',
|
|
|
+ width: '100%',
|
|
|
+ flex: 1,
|
|
|
+ alignItems: 'center'
|
|
|
+ },
|
|
|
+ image: {
|
|
|
+ width: 100,
|
|
|
+ height: 100,
|
|
|
+ margin: 15,
|
|
|
+ borderRadius: 10
|
|
|
+ },
|
|
|
+ textContainer: {
|
|
|
+ flex: 1,
|
|
|
+ flexDirection: 'column',
|
|
|
+ gap: 8,
|
|
|
+ marginTop: 20,
|
|
|
+ marginRight: 8 // Add right margin to prevent text from touching the edge
|
|
|
+ }
|
|
|
+});
|