Kaynağa Gözat

add maps file

wudebin 6 ay önce
ebeveyn
işleme
90e649a04f

+ 402 - 0
Strides-SPAPP/app/pages/home/maps/BottomSiteCard.js

@@ -0,0 +1,402 @@
+/**
+ * 地图底部充电站信息组件
+ * @邠心vbe on 2022/12/23
+ */
+import React, { useEffect, useState } from 'react';
+import { Pressable, StyleSheet, View, Text } from 'react-native';
+import { ElevationObject } from '../../../components/Button';
+import utils from '../../../utils/utils';
+import { ChargeStyle } from '../../charge/Charging';
+import { PageList } from '../../Router';
+import ConnectType, { ConnectTypeV2 } from '../../search/ConnectType';
+import app from '../../../../app.json';
+import TextView from '../../../components/TextView';
+import BottomModal from '../../../components/BottomModal';
+import SiteLabelView from '../../../components/SiteLabelView';
+import VbeSkeleton from '../../../components/VbeSkeleton';
+
+export const BottomSiteCard = ({
+  visible=false,
+  stationInfo = {},
+  onFavorite,
+  onClose
+}) => {
+  const [loading, showDialog] = useState(true);
+
+  useEffect(() => {
+    if (stationInfo.id) {
+      showDialog(false)
+    }
+  }, [stationInfo]);
+
+  useEffect(() => {
+    showDialog(true);
+  }, [visible]);
+
+  const getAvailable = (type) => {
+    const all = stationInfo.allConnector;
+    if (all) {
+      if (type == 'box') {
+        return all.boxAvailable + '/' + all.boxAll;
+      } else {
+        return all.available + '/' + all.all;
+      }
+    } else {
+      return '0/0';
+    }
+  }
+
+  const getOperatingHours = () => {
+    if (stationInfo.endlessService) {
+      return "24/7";
+    } else if (stationInfo.operatingHours) {
+      return stationInfo.operatingHours;
+    } else {
+      return $t('charging.toBeUpdated');
+    }
+  }
+
+  const getParkingFee = () => {
+    if (stationInfo.parkingFeeFree) {
+      return $t('charging.free');
+    } else if (stationInfo.parkingFee) {
+      return stationInfo.parkingFee;
+    } else {
+      return $t('charging.toBeUpdated');
+    }
+  }
+
+  const toChargePage = () => {
+    onClose();
+    if (stationInfo.upcoming) {
+      toastShort($t("home.upcoming"))
+    } else {
+      utils.toChargeDetailPage(stationInfo.id, 'view', PageList.home);
+      //startPage(PageList.chargeDetailPage, {stationInfo: stationInfo, action: 'view', from: PageList.home});
+    }
+  }
+
+  return (
+    <BottomModal
+      visible={visible}
+      onHide={() => {
+        onClose()
+      }}
+      backdropOpacity={0}
+      contentStyle={styles.bottomModal}>
+      { loading
+      ? <View style={styles.stationBarView}>
+          <View style={ui.flex}>
+            <VbeSkeleton
+              style={ui.flex1}
+              layout={[
+                {width: '80%', height: 20},
+                {width: '100%', height: 15, marginTop: 16},
+                {width: '60%', height: 15, marginTop: 4}
+              ]}
+              animationDirection={"horizontalRight"}
+            />
+          </View>
+          <EndView/>
+          <View style={ui.flex}>
+            <VbeSkeleton
+              style={[ui.flexc, ui.flex1]}
+              viewStyle={ui.flex1}
+              layout={[
+                {width: '16%', height: 15, marginRight: 8, borderRadius: 30},
+                {width: '16%', height: 15, marginRight: 8, borderRadius: 30},
+                {width: '16%', height: 15, marginRight: 8, borderRadius: 30},
+                {width: '16%', height: 15, borderRadius: 30}
+              ]}
+              animationDirection={"horizontalRight"}
+            />
+          </View>
+          <EndView/>
+          <View style={ui.flex}>
+            <VbeSkeleton
+              style={[ui.flexc, ui.flex1]}
+              viewStyle={ui.flex1}
+              layout={[
+                {width: '20%', height: 25, marginRight: 12, borderRadius: 30},
+                {width: '20%', height: 25, marginRight: 12, borderRadius: 30},
+                {width: '20%', height: 25, marginRight: 12, borderRadius: 30}
+              ]}
+              animationDirection={"horizontalRight"}
+            />
+          </View>
+        </View>
+      : <View style={styles.stationBarView}>
+          <View style={ui.flexc}>
+            <TextView
+              ellipsizeMode='tail'
+              numberOfLines={1}
+              style={styles.stationTitle}>{stationInfo.name}</TextView>
+            <Pressable
+              style={styles.closeIcon}
+              android_ripple={rippleLess}
+              onPress={onClose}>
+              <MaterialCommunityIcons
+                name="close"
+                size={22}
+                color={textSecondary}/>
+            </Pressable>
+          </View>
+          <TextView
+            style={styles.stationAddress}
+            ellipsizeMode='tail'
+            numberOfLines={3}>{stationInfo.address}</TextView>
+          <View style={ui.flexc}>
+            <TextView style={styles.siteTypes}>{stationInfo.siteType}</TextView>
+            { (stationInfo.allConnector && stationInfo.allConnector.available > 0) &&
+              <TextView style={styles.stationAvailable}>
+                <MaterialCommunityIcons
+                  name="circle"
+                  size={10}
+                  color={colorAccent}/>
+                <Text>  </Text>
+                {$t("charging.statusAvailable")}
+              </TextView>
+            }
+            <ConnectTypeV2 {...stationInfo?.acConnector}/>
+            <ConnectTypeV2 {...stationInfo?.dcConnector}/>
+          </View>
+          <View style={styles.infoDetailsView}>
+            <View style={ui.flex1}>
+              <TextView style={styles.infoTitle}>{$t("charging.operatingHours")}</TextView>
+              <View style={styles.infoView}>
+                <TextView style={styles.infoText}>{getOperatingHours()}</TextView>
+              </View>
+            </View>
+            <View style={{width: 4}}></View>
+            <View style={ui.flex1}>
+              <TextView style={styles.infoTitle}>{$t("charging.parkingFees")}</TextView>
+              <View style={styles.infoView}>
+                <TextView style={styles.infoText}>{getParkingFee()}</TextView>
+              </View>
+            </View>
+            <View style={{width: 4}}></View>
+            <View style={ui.flex1}>
+              <TextView style={styles.infoTitle}>{$t("charging.additionalInfo")}</TextView>
+              <View style={styles.infoView}>
+                <TextView style={styles.infoText}>{stationInfo?.additionalNotes}</TextView>
+              </View>
+            </View>
+          </View>
+          <EndView half/>
+          <View style={ui.flexc}>
+            { stationInfo.upcoming
+            ? <View style={[ui.center, $margin(0, 8)]}>
+                <MaterialIcons
+                  name="upcoming"
+                  size={42}
+                  color={colorAccent}
+                  style={styles.upcomingIcon} />
+                <TextView style={styles.upcomingText}>{$t("home.upcoming")}</TextView>
+              </View>
+            : <> 
+              <Pressable
+                style={styles.directIconView}
+                onPress={() => {
+                  utils.directMaps(stationInfo.latitude, stationInfo.longitude, stationInfo.address);
+                }}>
+                <MaterialCommunityIcons
+                  name="directions"
+                  size={20}
+                  color={textPrimary}/>
+                <TextView style={styles.directText}>Directions</TextView>
+              </Pressable>
+              { app.modules.bookmarks &&
+                <Pressable
+                  style={[styles.directIconView, stationInfo.favorite ? styles.bookmarked : {}]}
+                  onPress={onFavorite}>
+                  <MaterialIcons
+                    name="star"
+                    size={20}
+                    color={stationInfo.favorite ? textLight : textPrimary}/>
+                  <TextView style={[styles.directText, stationInfo.favorite ? styles.bookmarked : {}]}>
+                    {stationInfo.favorite ? "Saved" : "Save"}
+                  </TextView>
+                </Pressable>
+              }
+              {/* <Pressable
+                style={styles.directIconView}
+                onPress={() => {
+                  startPage(PageList.scanqr, {actionDetail: true});
+                }}>
+                <MaterialCommunityIcons
+                  name="qrcode-scan"
+                  size={12}
+                  color={textPrimary}
+                  style={$padding(3)}/>
+                <TextView style={styles.directText}>Scan</TextView>
+              </Pressable> */}
+              <Pressable
+                style={styles.directIconView}
+                onPress={() => toChargePage()}>
+                <MaterialCommunityIcons
+                  name="information-variant"
+                  size={20}
+                  color={textPrimary}/>
+                <TextView style={styles.directText}>More Info</TextView>
+              </Pressable>
+            </> }
+          </View>
+          {/* stationInfo?.labels?.length > 0 &&
+            <View style={styles.siteLabelsView}>
+              <Image
+                style={styles.labelIcon}
+                source={require('../../../images/maps/ic_marker_additional.png')}/>
+              { stationInfo.labels.map((item, index) => 
+                <SiteLabelView key={index} {...item}/>
+              )}
+            </View>
+          */}
+        </View>
+      }
+    </BottomModal>
+  )
+}
+
+const styles = StyleSheet.create({
+  bottomModal: {
+    backgroundColor: pageBackground
+  },
+  stationBarView: {
+    padding: 16
+  },
+  stationBarPresed: {
+    backgroundColor: "#F6F6F6",
+    ...ElevationObject(5)
+  },
+  stationInfo: {
+    flex: 1,
+    height: 45,
+    paddingLeft: 4,
+    paddingRight: 8,
+    justifyContent: 'space-around'
+  },
+  stationTitle: {
+    flex: 1,
+    color: textPrimary,
+    fontSize: 24,
+    fontWeight: 'bold',
+    paddingBottom: 4
+  },
+  stationAddress: {
+    color: textSecondary,
+    fontSize: 14,
+    paddingTop: 6,
+    paddingBottom: 8
+  },
+  siteTypes: {
+    color: textLight,
+    height: 20,
+    fontSize: 12,
+    marginRight: 6,
+    borderRadius: 30,
+    ...$padding(0, 10),
+    backgroundColor: textPrimary
+  },
+  stationAvailable: {
+    color: textPrimary,
+    height: 20,
+    fontSize: 12,
+    marginRight: 6,
+    borderRadius: 30,
+    borderWidth: 1,
+    borderColor: colorAccent,
+    ...$padding(0, 8, 0, 4)
+  },
+  directView: {
+    zIndex: 1,
+    height: 45,
+    marginLeft: 8,
+    marginRight: 4,
+    alignItems: 'center',
+    justifyContent: 'space-between'
+  },
+  distanceText: {
+    color: textPrimary,
+    fontSize: 12,
+  },
+  directIconView: {
+    zIndex: 1,
+    height: 32,
+    marginRight: 10,
+    borderWidth: 1,
+    borderRadius: 30,
+    borderColor: textPrimary,
+    alignItems: 'center',
+    flexDirection: 'row',
+    justifyContent: 'center',
+    ...$padding(2, 10, 2, 6)
+  },
+  bookmarked: {
+    color: textLight,
+    borderColor: colorAccent,
+    backgroundColor: colorAccent
+  },
+  directText: {
+    color: textPrimary,
+    fontSize: 14,
+    paddingLeft: 4
+  },
+  connectView: {
+    flex: 1,
+    paddingTop: 12,
+    paddingBottom: 12,
+    alignItems: 'center',
+    flexDirection: 'row'
+  },
+  divideLine: {
+    height: 1,
+    backgroundColor: '#AEAEAE'
+  },
+  infoDetailsView: {
+    paddingTop: 8,
+    flexDirection: 'row'
+  },
+  infoTitle: {
+    color: textPrimary,
+    fontSize: 14,
+    //fontWeight: 'bold',
+    ...$padding(8, 0, 8)
+  },
+  infoView: {
+    paddingBottom: 8
+  },
+  infoText: {
+    color: textCancel,
+    fontSize: 12
+  },
+  closeIcon: {
+    width: 30,
+    height: 30,
+    marginTop: -16,
+    marginRight: -8,
+    alignItems: 'center',
+    justifyContent: 'center'
+  },
+  siteLabelsView: {
+    marginBottom: 5,
+    flexWrap: 'wrap',
+    alignItems: 'center',
+    flexDirection: 'row'
+  },
+  labelIcon: {
+    width: 16,
+    height: 16,
+    marginRight: 6
+  },
+  upcomingIcon: {
+    marginLeft: 8,
+    opacity: .3
+  },
+  upcomingText: {
+    color: colorAccent,
+    fontSize: 10,
+    opacity: .3,
+    marginLeft: 8,
+    marginTop: -3
+  }
+})

+ 412 - 0
Strides-SPAPP/app/pages/home/maps/BottomSiteInfo.js

@@ -0,0 +1,412 @@
+/**
+ * 地图底部充电站信息组件
+ * @邠心vbe on 2022/12/23
+ */
+import React, { useEffect, useState } from 'react';
+import { Pressable, StyleSheet, View, Text, Image } from 'react-native';
+import { ElevationObject } from '../../../components/Button';
+import utils from '../../../utils/utils';
+import { ChargeStyle } from '../../charge/Charging';
+import { PageList } from '../../Router';
+import ConnectType from '../../search/ConnectType';
+import app from '../../../../app.json';
+import TextView from '../../../components/TextView';
+import SiteLabelView from '../../../components/SiteLabelView';
+import VbeSkeleton from '../../../components/VbeSkeleton';
+
+const BottomSiteInfo = ({
+  visible=false,
+  stationInfo = {},
+  onFavorite,
+  onClose,
+  style=styles.stationBarView
+}) => {
+  const [loading, showLoading] = useState(true);
+
+  useEffect(() => {
+    if (stationInfo.id) {
+      showLoading(false)
+    } else {
+      showLoading(true)
+    }
+  }, [stationInfo]);
+
+  useEffect(() => {
+    showLoading(true);
+  }, [visible]);
+
+  const getAvailable = (type) => {
+    const all = stationInfo.allConnector;
+    if (all) {
+      if (type == 'box') {
+        return all.boxAvailable + '/' + all.boxAll;
+      } else {
+        return all.available + '/' + all.all;
+      }
+    } else {
+      return '0/0';
+    }
+  }
+
+  const getOperatingHours = () => {
+    if (stationInfo.endlessService) {
+      return "24/7";
+    } else if (stationInfo.operatingHours) {
+      return stationInfo.operatingHours;
+    } else {
+      return $t('charging.toBeUpdated');
+    }
+  }
+
+  const getParkingFee = () => {
+    if (stationInfo.parkingFeeFree) {
+      return $t('charging.free');
+    } else if (stationInfo.parkingFee) {
+      return stationInfo.parkingFee;
+    } else {
+      return $t('charging.toBeUpdated');
+    }
+  }
+
+  const toChargePage = () => {
+    if (stationInfo.upcoming) {
+      toastShort($t("home.upcoming"))
+    } else {
+      utils.toChargeDetailPage(stationInfo.id, 'view', PageList.home);
+      //startPage(PageList.chargeDetailPage, {stationInfo: stationInfo, action: 'view', from: PageList.home});
+    }
+  }
+
+  if (visible) {
+    if (loading) {
+      return (
+        <View style={style}>
+          <View style={ui.flex}>
+            <VbeSkeleton
+              style={ui.flex1}
+              layout={[
+                {width: '60%', height: 20, marginTop: 4}
+              ]}
+              animationDirection={"horizontalRight"}
+            />
+            <Pressable
+              style={styles.closeIcon}
+              android_ripple={rippleLess}
+              onPress={onClose}>
+              <MaterialCommunityIcons
+                name="close"
+                size={22}
+                color={textCancel}/>
+            </Pressable>
+          </View>
+          <EndView half/>
+          <View style={ui.flexc}>
+            <VbeSkeleton
+              style={ui.flex1}
+              layout={[
+                {width: '100%', height: 15, marginTop: 4},
+                {width: '60%', height: 15, marginTop: 4}
+              ]}
+              animationDirection={"horizontalRight"}
+            />
+            <VbeSkeleton
+              style={ui.flexc}
+              layout={[
+                {width: 42, height: 42, marginLeft: 16, borderRadius: 48},
+                {width: 42, height: 42, marginLeft: 8, borderRadius: 48}
+              ]}
+              animationDirection={"horizontalRight"}
+            />
+          </View>
+          <EndView/>
+          <View style={ui.flex}>
+            <VbeSkeleton
+              style={[ui.flexc, ui.flex1]}
+              viewStyle={ui.flex1}
+              layout={[
+                {width: '16%', height: 20, marginRight: 8, borderRadius: 30},
+                {width: '16%', height: 20, marginRight: 8, borderRadius: 30},
+                {width: '16%', height: 20, marginRight: 8, borderRadius: 30}
+              ]}
+              animationDirection={"horizontalRight"}
+            />
+          </View>
+          <EndView/>
+          <VbeSkeleton
+            style={ui.flex1}
+            layout={[
+              {width: '100%', height: 12},
+              {width: '60%', height: 12, marginTop: 4}
+            ]}
+            animationDirection={"horizontalRight"}
+          />
+          <EndView/>
+        </View>
+      );
+    } else if (stationInfo.id) {
+      return (
+        <Pressable
+          style={({pressed}) => [style, pressed ? styles.stationBarPresed : {}]}
+          onPress={() => toChargePage()}>
+          <View style={ui.flexc}>
+            <TextView
+              ellipsizeMode='tail'
+              numberOfLines={1}
+              style={styles.stationTitle}>{stationInfo.name}</TextView>
+            <Pressable
+              style={styles.closeIcon}
+              android_ripple={rippleLess}
+              onPress={onClose}>
+              <MaterialCommunityIcons
+                name="close"
+                size={22}
+                color={textCancel}/>
+            </Pressable>
+          </View>
+          <View style={{height: 4}}></View>
+          <View style={ui.flexc}>
+            <TextView
+              style={styles.stationAddress}
+              ellipsizeMode='tail'
+              numberOfLines={3}>{stationInfo.address}</TextView>
+            { stationInfo.upcoming
+            ? <View style={[ui.center, $margin(0, 8)]}>
+                <MaterialIcons
+                  name="upcoming"
+                  size={42}
+                  color={colorAccent}
+                  style={styles.upcomingIcon} />
+                <TextView style={styles.upcomingText}>{$t("home.upcoming")}</TextView>
+              </View>
+            : <> 
+              { app.modules.bookmarks &&
+                <Pressable
+                  style={[styles.directIconView, {backgroundColor: stationInfo.favorite ? colorPrimary : colorCancel}]}
+                  android_ripple={rippleLess}
+                  onPress={onFavorite}>
+                  <MaterialIcons
+                    name="star"
+                    size={26}
+                    color={colorLight}/>
+                </Pressable>
+              }
+              <Pressable
+                style={styles.directIconView}
+                android_ripple={rippleLess}
+                onPress={() => {
+                  utils.directMaps(stationInfo.latitude, stationInfo.longitude, stationInfo.address);
+                }}>
+                <MaterialCommunityIcons
+                  name="navigation-variant"
+                  size={25}
+                  color={colorLight}/>
+                {/* <MaterialIcons
+                  name='directions'
+                  size={28}
+                  color={colorAccent}/>
+                <Text style={styles.distanceText}>{stationInfo.distance}</Text> */}
+              </Pressable>
+            </> }
+          </View>
+          <View style={ui.flex}>
+            <View style={styles.connectView}>
+              <ConnectType {...stationInfo?.acConnector}/>
+              <ConnectType {...stationInfo?.dcConnector}/>
+            </View>
+            <View style={styles.stationStatusView}>
+              { (stationInfo.allConnector && stationInfo.allConnector.available > 0) &&
+                <TextView style={[ChargeStyle.infoStatus, ChargeStyle.statusAvailable, styles.stationStatus]}>{$t("charging.statusAvailable")}</TextView>
+              }
+              <TextView style={[ChargeStyle.infoStatus, stationInfo.siteType == 'Public' ? ChargeStyle.statusAvailable : ChargeStyle.statusWarning, styles.siteTypes]}>{stationInfo.siteType}</TextView>
+            </View>
+          </View>
+          { stationInfo?.labels?.length > 0 &&
+            <View style={styles.siteLabelsView}>
+              <Image
+                style={styles.labelIcon}
+                source={require('../../../images/maps/ic_marker_additional.png')}/>
+              { stationInfo.labels.map((item, index) => 
+                <SiteLabelView key={index} {...item}/>
+              )}
+            </View>
+          }
+          <View style={styles.divideLine}></View>
+          <View style={styles.infoDetailsView}>
+            <View style={ui.flex1}>
+              <TextView style={styles.infoTitle}>{$t("charging.operatingHours")}</TextView>
+              <View style={styles.infoView}>
+                <TextView style={styles.infoText}>{getOperatingHours()}</TextView>
+              </View>
+            </View>
+            <View style={{width: 4}}></View>
+            <View style={ui.flex1}>
+              <TextView style={styles.infoTitle}>{$t("charging.parkingFees")}</TextView>
+              <View style={styles.infoView}>
+                <TextView style={styles.infoText} numberOfLines={3}>{getParkingFee()}</TextView>
+              </View>
+            </View>
+            <View style={{width: 4}}></View>
+            <View style={ui.flex1}>
+              <TextView style={styles.infoTitle}>{$t("charging.additionalInfo")}</TextView>
+              <View style={styles.infoView}>
+                <TextView style={styles.infoText} numberOfLines={3}>{stationInfo?.additionalNotes}</TextView>
+              </View>
+            </View>
+          </View>
+        </Pressable>
+      );
+    }
+  } else {
+    return <></>;
+  }
+}
+
+export default BottomSiteInfo;
+
+const styles = StyleSheet.create({
+  stationBarView: {
+    left: 16,
+    right: 16,
+    bottom: 48,
+    zIndex: 10,
+    borderRadius: 6,
+    ...$padding(8, 16),
+    position: 'absolute',
+    backgroundColor: colorLight,
+    ...ElevationObject(3)
+  },
+  stationBarPresed: {
+    backgroundColor: "#F6F6F6",
+    ...ElevationObject(5)
+  },
+  stationInfo: {
+    flex: 1,
+    height: 45,
+    paddingLeft: 4,
+    paddingRight: 8,
+    justifyContent: 'space-around'
+  },
+  stationTitle: {
+    flex: 1,
+    color: textPrimary,
+    fontSize: 17,
+    fontWeight: 'bold'
+  },
+  stationAddress: {
+    flex: 1,
+    color: textCancel,
+    fontSize: 12
+  },
+  stationAvailable: {
+    height: 45,
+    paddingLeft: 8,
+    paddingRight: 8,
+    alignItems: 'center',
+    justifyContent: 'space-between'
+  },
+  stationStatusView: {
+    marginRight: 4,
+    alignItems: 'center',
+    flexDirection: 'row-reverse'
+  },
+  stationStatus: {
+    fontSize: 10,
+    ...$padding(2, 6)
+  },
+  siteTypes: {
+    fontSize: 10,
+    marginTop: 0,
+    marginRight: 4,
+    ...$padding(2, 6)
+  },
+  availableIcon: {
+    width: 23,
+    height: 23,
+  },
+  availableText: {
+    color: textPrimary,
+    fontSize: 13,
+    textAlign: 'center'
+  },
+  directView: {
+    zIndex: 1,
+    height: 45,
+    marginLeft: 8,
+    marginRight: 4,
+    alignItems: 'center',
+    justifyContent: 'space-between'
+  },
+  distanceText: {
+    color: textPrimary,
+    fontSize: 12,
+  },
+  directIconView: {
+    zIndex: 1,
+    width: 42,
+    height: 42,
+    marginLeft: 8,
+    marginRight: 4,
+    borderRadius: 45,
+    alignItems: 'center',
+    justifyContent: 'center',
+    backgroundColor: colorAccent,
+    ...ElevationObject(2)
+  },
+  connectView: {
+    flex: 1,
+    paddingTop: 12,
+    paddingBottom: 12,
+    alignItems: 'center',
+    flexDirection: 'row'
+  },
+  divideLine: {
+    height: 1,
+    backgroundColor: '#AEAEAE'
+  },
+  infoDetailsView: {
+    flexDirection: 'row'
+  },
+  infoTitle: {
+    color: '#000',
+    fontSize: 12,
+    fontWeight: 'bold',
+    ...$padding(8, 0, 8)
+  },
+  infoView: {
+    paddingBottom: 8
+  },
+  infoText: {
+    color: '#444',
+    fontSize: 10
+  },
+  closeIcon: {
+    width: 30,
+    height: 30,
+    marginTop: -2,
+    marginRight: -8,
+    alignItems: 'center',
+    justifyContent: 'center'
+  },
+  siteLabelsView: {
+    marginBottom: 5,
+    flexWrap: 'wrap',
+    alignItems: 'center',
+    flexDirection: 'row'
+  },
+  labelIcon: {
+    width: 16,
+    height: 16,
+    marginRight: 6
+  },
+  upcomingIcon: {
+    marginLeft: 8,
+    opacity: .3
+  },
+  upcomingText: {
+    color: colorAccent,
+    fontSize: 10,
+    opacity: .3,
+    marginLeft: 8,
+    marginTop: -3
+  }
+})

+ 129 - 0
Strides-SPAPP/app/pages/home/maps/Cluster.js

@@ -0,0 +1,129 @@
+/**
+ * 地图聚合组件
+ * @邠心vbe on 2021/08/13
+ */
+import React from 'react';
+import { Image, ImageBackground, StyleSheet, Text } from 'react-native';
+import { Marker } from 'react-native-maps';
+
+
+export const MyMarker = ({id, coordinate, onPress}) => {
+  if (coordinate.upcoming) {
+    return (
+      <Marker
+        key={id}
+        zIndex={1}
+        coordinate={coordinate}
+        onPress={onPress}>
+        <Image
+          style={styles.marker}
+          source={require('../../../images/maps/ic_marker_upcoming.png')}/>
+      </Marker>
+    )
+  } else {
+    return (
+      <Marker
+        key={id}
+        zIndex={1}
+        coordinate={coordinate}
+        onPress={onPress}
+        style={ui.center}>
+        { coordinate.hasLabel &&
+          <Image
+            style={styles.markerTop}
+            source={require('../../../images/maps/ic_marker_additional.png')}/>
+        }
+        { coordinate.favorite
+          ? coordinate.available
+            ? <Image
+                style={styles.marker}
+                source={require('../../../images/maps/ic_marker_star.png')}/>
+            : <Image
+                style={styles.marker}
+                source={require('../../../images/maps/ic_marker_unstar.png')}/>
+          : coordinate.available
+            ? <Image
+                style={styles.marker}
+                source={require('../../../images/maps/ic_marker.png')}/>
+            : <Image
+                style={styles.marker}
+                source={require('../../../images/maps/ic_marker_un.png')}/>
+        }
+      </Marker>
+    )
+  }
+}
+
+export const MyCluster = (props) => {
+  const {id, location, properties, onPress, available=false, onOpen} = props;
+  const pointCount = props.pointCount ? props.pointCount : properties.point_count
+  //console.log('renderCluster', props);
+
+  return (
+    <Marker
+      key={id}
+      style={{ zIndex: pointCount + 1 }}
+      coordinate={location}
+      zIndex={pointCount + 1}
+      onPress={() => onOpen ? onOpen(location) : onPress()}>
+      <ClusterView pointCount={pointCount} available={available}/>
+    </Marker>
+  );
+}
+
+export const ClusterView = ({pointCount, available=false}) => {
+  const getStyle = (count) => {
+    if (count >= 100) {
+      return [ui.flexcc, styles.large];
+    } else if (count >= 10) {
+      return [ui.flexcc, styles.middle];
+    } else {
+      return [ui.flexcc, styles.small];
+    }
+  }
+  return (
+    <ImageBackground
+      style={getStyle(pointCount)}
+      source={available
+            ? require('../../../images/maps/ic_cluster.png')
+            : require('../../../images/maps/ic_cluster_un.png')}>
+      <Text style={available ? styles.textAvailable : styles.textUnavailable}>{pointCount}</Text>
+    </ImageBackground>
+  );
+}
+
+const styles = StyleSheet.create({
+  marker: {
+    width: 48,
+    height: 48
+  },
+  markerTop: {
+    width: 24,
+    height: 24,
+    marginBottom: -2
+  },
+  small: {
+    width: 48,
+    height: 48
+  },
+  middle: {
+    width: 50,
+    height: 50
+  },
+  large: {
+    width: 52,
+    height: 52
+  },
+  textAvailable: {
+    color: textLight,
+    fontSize: 18,
+    fontWeight: 'bold',
+    paddingBottom: 10
+  },
+  textUnavailable: {
+    color: '#999',
+    fontSize: 18,
+    fontWeight: 'bold',
+    paddingBottom: 10
+  }
+})

+ 341 - 0
Strides-SPAPP/app/pages/home/maps/Filter.js

@@ -0,0 +1,341 @@
+/**
+ * 过滤器
+ * @邠心vbe on 2021/03/24
+ */
+import React from 'react'
+import { Image, Pressable, StyleSheet, Text, TouchableOpacity, TouchableOpacityBase, View } from 'react-native'
+import BadgeSelectItem from '../../../components/BadgeSelectItem';
+import Button, { ElevationObject } from '../../../components/Button';
+import TextView from '../../../components/TextView';
+import { TypeImageList } from '../../charge/Charging';
+
+class Filter extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      count: 0,
+      connectorType: TypeImageList,
+      parkingFee: [{
+        name: $t('home.all'),
+        value: 'ALL'
+      },{
+        name: $t('home.free'),
+        value: 'FREE'
+      },{
+        name: $t('home.chargeable'),
+        value: 'CHARGEABLE'
+      }],
+      activationType: [/*'SCAN QR', 'RFID Card needed', 'Reservation Required'*/],
+      providerType: [/*'All', 'SP', 'GreenLots', 'Juice+Stations'*/],
+      filterIndex: {
+        providerIndex: -1,
+        connectorIndex: -1,
+        activationIndex: -1,
+        parkingIndex: 0
+      }
+    };
+  }
+
+  componentDidMount() {
+    if (this.props.index.connectorIndex || this.props.index.parkingIndex) {
+      this.setState({
+        filterIndex: this.props.index
+      });
+    }
+    this.setState({
+      count: this.props.count
+    })
+  }
+
+  selectConnector(index) {
+    const indexs = this.state.filterIndex;
+    indexs.connectorIndex = index;
+    this.setState({
+      count: 0,
+      filterIndex: indexs
+    });
+  }
+
+  selectActivation(index) {
+    const indexs = this.state.filterIndex;
+    indexs.activationIndex = index;
+    this.setState({
+      count: 0,
+      filterIndex: indexs
+    });
+  }
+
+  selectProvidor(index) {
+    const indexs = this.state.filterIndex;
+    indexs.providerIndex = index;
+    this.setState({
+      count: 0,
+      filterIndex: indexs
+    });
+  }
+
+  selectParkingFee(index) {
+    const indexs = this.state.filterIndex;
+    indexs.parkingIndex = index;
+    this.setState({
+      count: 0,
+      filterIndex: indexs
+    });
+  }
+
+  done() {
+    if (this.props.onDone) {
+      const index = this.state.filterIndex;
+      const filter = {
+        connectorType: '',
+        activationType: '',
+        providerType: '',
+        parkingFee: 'ALL'
+      }
+      try {
+        if (index.connectorIndex >= 0)
+          filter.connectorType = this.state.connectorType[index.connectorIndex].key;
+        if (index.activationIndex >= 0)
+          filter.activationType = this.state.activationType[index.activationIndex];
+        if (index.providerIndex >= 0)
+          filter.providerType = this.state.providerType[index.providerIndex];
+        filter.parkingFee = this.state.parkingFee[index.parkingIndex].value;
+      } catch {
+
+      }
+      this.props.onDone(filter, index);
+    }
+  }
+
+  render() {
+    return (
+      <View style={styles.filterView}>
+        <View style={ui.flexc}>
+          <TextView style={styles.titleText}>{$t('home.filters')}</TextView>
+          {/* <Pressable
+            android_ripple = {rippleLess}
+            onPress={() => {
+              if (this.props.onHide) {
+                this.props.onHide();
+              }
+            }}>
+            <AntDesign name='close' size={24} color='#999'/>
+          </Pressable> */}
+        </View>
+        {/* Connector Type */}
+        <TextView style={styles.labelText}>{$t('home.chooseConnecterType')}</TextView>
+        <View style={styles.connectorView}>
+          { this.state.connectorType.map((item, index) => {
+              return (
+                <BadgeSelectItem
+                  key={index}
+                  style={styles.ctypeView}
+                  borderColor={textCancel}
+                  checked={index==this.state.filterIndex.connectorIndex}
+                  onPress={() => this.selectConnector(index)}>
+                  <Image 
+                    style={styles.typeIcon}
+                    source={item.icon}/>
+                  <TextView style={styles.typeName}>{item.nameScope ? $t(item.nameScope) : item.name}</TextView>
+                </BadgeSelectItem>
+              )
+            })
+          }
+        </View>
+        { /* Choose Parking Fee */
+          this.state.parkingFee.length > 0 &&
+          <>
+            <TextView style={styles.labelText}>{$t('home.chooseParkingFee')}</TextView>
+            <View style={styles.activationView}>
+              { this.state.parkingFee.map((item, index) => {
+                return (
+                  <TouchableOpacity
+                    key={index}
+                    activeOpacity={0.7}
+                    style={[
+                      styles.ptypeView,
+                      index < this.state.parkingFee.length - 1 && { marginRight: 16 },
+                      index==this.state.filterIndex.parkingIndex && styles.actived
+                    ]}
+                    onPressOut={() => {
+                      this.selectParkingFee(index);
+                    }}>
+                      <TextView style={styles.typeText}>{item.name}</TextView>
+                    </TouchableOpacity>
+                )
+              })}
+            </View>
+          </>
+        }
+        { /* Activation Type */
+          this.state.activationType.length > 0 &&
+          <>
+            <TextView style={styles.labelText}>{$t('home.chooseActivationType')}</TextView>
+            <View style={styles.activationView}>
+              { this.state.activationType.map((item, index) => {
+                return (
+                  <TouchableOpacity
+                    key={index}
+                    activeOpacity={0.7}
+                    style={[
+                      styles.ptypeView,
+                      index < this.state.activationType.length - 1 && { marginRight: 16 },
+                      index==this.state.filterIndex.activationIndex && styles.actived
+                    ]}
+                    onPress={() => {
+                      this.selectActivation(index);
+                    }}>
+                    <TextView style={styles.typeText}>{item}</TextView>
+                  </TouchableOpacity>
+                )
+              })}
+            </View>
+          </>
+        }
+        { /* Provider Type */
+          this.state.providerType.length > 0 &&
+          <>
+            <TextView style={styles.labelText}>{$t('home.chooseProvider')}</TextView>
+            <View style={styles.activationView}>
+            { this.state.providerType.map((item, index) => {
+                return (
+                  <TouchableOpacity
+                    key={index}
+                    activeOpacity={0.7}
+                    style={[
+                      styles.ptypeView,
+                      index < this.state.providerType.length - 1 && { marginRight: 16 },
+                      index==this.state.filterIndex.providerIndex && styles.actived
+                    ]}
+                    onPress={() => {
+                      this.selectProvidor(index);
+                    }}>
+                    <TextView style={styles.typeText}>{item}</TextView>
+                  </TouchableOpacity>
+                )
+              })}
+            </View>
+          </>
+        }
+        <View style={styles.bottomView}>
+          { this.state.count > 0 &&
+            <TextView style={{
+              color: '#000',
+              fontSize: 15,
+              fontWeight: 'bold',
+              marginRight: 16
+            }}>{this.state.count} {$t('home.filterResults')}</TextView>
+          }
+          <Button
+            viewStyle={styles.done}
+            textStyle={styles.doneText}
+            text={$t('home.done')}
+            borderRadius={3}
+            onClick={() => this.done()}/>
+        </View>
+      </View>
+    )
+  }
+}
+
+export default Filter
+
+const styles = StyleSheet.create({
+  filterView: {
+    padding: 16,
+    //backgroundColor: 'white'
+  },
+  titleText: {
+    flex: 1,
+    color: '#222',
+    fontSize: 17,
+    paddingLeft: 24,
+    textAlign: 'center',
+  },
+  labelText: {
+    color: textPrimary,
+    fontSize: 16,
+    fontWeight: 'bold',
+    paddingTop: 16,
+    paddingBottom: 8,
+    textShadowOffset: {
+      width: 0,
+      height: 1
+    },
+    textShadowRadius: 4,
+    textShadowColor: "rgba(0, 0, 0, 0.25)",
+  },
+  connectorView: {
+    paddingTop: 8,
+    alignItems: 'center',
+    flexDirection: 'row'
+  },
+  ctypeView: {
+    minWidth: 90,
+    padding: 8,
+    borderRadius: 6,
+    marginRight: 16,
+    marginBottom: 8,
+    borderWidth: 1.5,
+    alignItems: 'center',
+    /*...ElevationObject(5),
+    backgroundColor: colorLight*/
+  },
+  ptypeView: {
+    minWidth: 54,
+    fontSize: 14,
+    paddingTop: 8,
+    paddingLeft: 12,
+    paddingRight: 12,
+    paddingBottom: 8,
+    borderRadius: 6,
+    marginTop: 12,
+    borderWidth: 1.5,
+    borderColor: textCancel,
+    textAlign: 'center',
+    /*...ElevationObject(5),
+    backgroundColor: colorLight*/
+  },
+  actived: {
+    borderColor: colorAccent
+  },
+  typeText: {
+    color: textPrimary,
+    fontSize: 14,
+    textAlign: 'center'
+  },
+  typeIcon: {
+    width: 38,
+    height: 38
+  },
+  typeName: {
+    color: '#000',
+    fontSize: 15,
+    textAlign: 'center',
+    paddingTop: 8
+  },
+  activationView: {
+    flexWrap: 'wrap',
+    alignItems: 'center',
+    flexDirection: 'row'
+  },
+  bottomView: {
+    padding: 16,
+    alignItems: 'center',
+    flexDirection: 'row',
+    justifyContent: 'flex-end'
+  },
+  done: {
+    height: 40,
+    paddingLeft: 14,
+    paddingRight: 14,
+    alignItems: 'center',
+    justifyContent: 'center'
+  },
+  doneText: {
+    color: textButton,
+    fontSize: 15,
+    fontWeight: 'bold',
+    textAlign: 'center'
+  }
+})

+ 190 - 0
Strides-SPAPP/app/pages/home/maps/FilterTop.js

@@ -0,0 +1,190 @@
+/**
+ * 顶部过滤器
+ * @邠心vbe on 2024/05/09
+ */
+import React from 'react'
+import { View, Pressable, StyleSheet, ScrollView, Text, Image } from 'react-native'
+import TextView from '../../../components/TextView';
+
+export default class FilterTop extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      count: 0,
+      connectorType: [{
+        name: 'charging.onlyAC',
+        key: 'AC',
+        icon: 'ev-plug-type2'
+      }, {
+        name:'charging.onlyDC',
+        key: 'DC',
+        icon: 'ev-plug-ccs2'
+      }],
+      parkingFee: [{
+        name: 'home.free',
+        value: 'FREE'
+      },{
+        name: 'home.chargeable',
+        value: 'CHARGEABLE'
+      }],
+      filterIndex: {
+        bookmark: false,
+        currentLocation: true,
+        connectorIndex: -1,
+        parkingIndex: -1
+      }
+    };
+  }
+
+  currentLocation() {
+    if (this.props.onLocation) {
+      this.props.onLocation();
+    }
+  }
+
+  changeBookmark() {
+    const indexs = this.state.filterIndex;
+    indexs.bookmark = !indexs.bookmark;
+    this.setState({
+      filterIndex: indexs
+    }, () => {
+      this.onFilter()
+    });
+  }
+
+  changeConnector(index) {
+    const indexs = this.state.filterIndex;
+    if (indexs.connectorIndex != index) {
+      indexs.connectorIndex = index;
+    } else {
+      indexs.connectorIndex = -1;
+    }
+    this.setState({
+      filterIndex: indexs
+    }, () => {
+      this.onFilter()
+    });
+  }
+
+  changeParkingFee(index) {
+    const indexs = this.state.filterIndex;
+    if (indexs.parkingIndex != index) {
+      indexs.parkingIndex = index;
+    } else {
+      indexs.parkingIndex = -1;
+    }
+    this.setState({
+      filterIndex: indexs
+    }, () => {
+      this.onFilter()
+    });
+  }
+
+  onFilter() {
+    if (this.props.onFilter) {
+      const index = this.state.filterIndex;
+      const filter = {
+        connectorType: '',
+        parkingFee: 'ALL',
+        bookmark: index.bookmark
+      }
+      if (index.connectorIndex >= 0) {
+        filter.connectorType = this.state.connectorType[index.connectorIndex].key;
+      }
+      if (index.parkingIndex >= 0) {
+        filter.parkingFee = this.state.parkingFee[index.parkingIndex].value;
+      }
+      this.props.onFilter(filter);
+    }
+  }
+
+  render() {
+    return (
+      <ScrollView
+        style={styles.filterTopView}
+        horizontal={true}
+        showsHorizontalScrollIndicator={false}
+        contentContainerStyle={$padding(16)}>
+        <Pressable
+          style={[styles.filterItem, styles.selected]}
+          onPress={() => this.currentLocation()}>
+          <MaterialCommunityIcons
+            name="map-marker-outline"
+            size={15}
+            color={textLight}
+          />
+          <TextView style={[styles.filterText, styles.active]}>Current Location</TextView>
+        </Pressable>
+        <Pressable
+          style={[styles.filterItem, this.state.filterIndex.bookmark ? styles.selected : {}]}
+          onPress={() => this.changeBookmark()}>
+          <MaterialCommunityIcons
+            name="star"
+            size={15}
+            color={this.state.filterIndex.bookmark ? textLight : textPrimary}
+          />
+          <TextView style={[styles.filterText, this.state.filterIndex.bookmark ? styles.active : {}]}>{$t("route.bookmarks")}</TextView>
+        </Pressable>
+        { this.state.connectorType.map((item, index) => (
+          <Pressable
+            style={[styles.filterItem, this.state.filterIndex.connectorIndex == index ? styles.selected : {}]}
+            key={item.key}
+            onPress={() => this.changeConnector(index)}>
+            <MaterialCommunityIcons
+              name={item.icon}
+              size={15}
+              color={this.state.filterIndex.connectorIndex == index ? textLight : textPrimary}
+            />
+            <TextView style={[styles.filterText, this.state.filterIndex.connectorIndex == index ? styles.active : {}]}>{$t(item.name)}</TextView>
+          </Pressable>
+        ))}
+        { this.state.parkingFee.map((item, index) => (
+          <Pressable
+            style={[styles.filterItem, this.state.filterIndex.parkingIndex == index ? styles.selected : {}]}
+            key={item.value}
+            onPress={() => this.changeParkingFee(index)}>
+            <MaterialCommunityIcons
+              name={"parking"}
+              size={15}
+              color={this.state.filterIndex.parkingIndex == index ? textLight : textPrimary}
+            />
+            <TextView style={[styles.filterText, this.state.filterIndex.parkingIndex == index ? styles.active : {}]}>{$t(item.name)}</TextView>
+          </Pressable>
+        ))}
+      </ScrollView>
+    )
+  }
+}
+
+const styles = StyleSheet.create({
+  filterTopView: {
+    top: 45 + statusHeight,
+    position: 'absolute'
+  },
+  typeIcon: {
+    width: 14,
+    height: 14
+  },
+  filterItem: {
+    marginRight: 8,
+    borderWidth: 1,
+    borderColor: '#DADADA',
+    borderRadius: 30,
+    ...$padding(6, 12),
+    alignItems: 'center',
+    flexDirection: 'row',
+    backgroundColor: colorLight,
+  },
+  filterText: {
+    fontSize: 10,
+    paddingLeft: 4,
+    color: textPrimary
+  },
+  selected: {
+    borderColor: colorPrimary,
+    backgroundColor: colorPrimary
+  },
+  active: {
+    color: textLight
+  }
+})

+ 192 - 0
Strides-SPAPP/app/pages/home/maps/LocationPermission.js

@@ -0,0 +1,192 @@
+/**
+ * 获取位置权限处理
+ * @邠心vbe on 2023/02/15
+ */
+import React from 'react';
+import { StyleSheet, View } from 'react-native';
+import { check, openSettings, PERMISSIONS, request, RESULTS } from 'react-native-permissions';
+import Button from '../../../components/Button';
+import TextView from '../../../components/TextView';
+import LocationEnabler from 'react-native-location-enabler';
+
+//global.hasPermission = false;
+
+class LocationListener {
+  constructor() {
+    this.listener = undefined;
+    this.config = {
+      priority: LocationEnabler.PRIORITIES.HIGH_ACCURACY, // default BALANCED_POWER_ACCURACY
+      alwaysShow: false, // default false
+      needBle: false // default false
+    };
+    this.status = undefined;
+  }
+  
+  addListener() {
+    this.listener = LocationEnabler?.addListener(({ locationEnabled }) => {
+      if (!locationEnabled) {
+        console.log("status: " + this.status  + "," + locationEnabled);
+        if (this.status != locationEnabled) {
+          this.status = locationEnabled;
+          LocationEnabler.requestResolutionSettings(this.config);
+        }
+      }
+    })
+  }
+
+  removeListener() {
+    if (this.listener) {
+      this.listener.remove();
+    }
+  }
+
+  check() {
+    this.status = undefined;
+    LocationEnabler.checkSettings(this.config)
+  }
+}
+
+
+/**
+ * 检查是否有定位权限
+ * @param {function} back callback(hasPermission, canRequestPermission)
+ */
+const checkPermission = (back) => {
+  check(
+      isIOS 
+    ? PERMISSIONS.IOS.LOCATION_WHEN_IN_USE
+    : PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION)
+  .then(res => {
+    console.log('[LocationPermission] checkPermission', res);
+    switch (res) {
+      case RESULTS.UNAVAILABLE:
+        console.log('此功能不可用(在此设备上/在此上下文中)');
+        if (isIOS) {
+          back(true, true);
+        } else {
+          back(false, true);
+        }
+        break;
+      case RESULTS.DENIED:
+        console.log('权限未被请求/被拒绝,但可以请求');
+        back(false, true);
+        break;
+      case RESULTS.LIMITED:
+        console.log('权限是有限的:有些操作是可能的');
+        back(true, true);
+        break;
+      case RESULTS.GRANTED:
+        console.log('许可被授予');
+        back(true, true);
+        break;
+      case RESULTS.BLOCKED:
+        console.log('权限被拒绝,不再可请求');
+        back(false, false);
+        /*Dialog.showDialog({
+          title: 'Error',
+          message: 'Can not get charge station, Please grant location permissions.',
+          ok: 'SETTING',
+          callback: btn => {
+            if (btn == Dialog.BUTTON_OK) {
+              console.log('ok');
+              openSettings().catch(() => console.warn('cannot open settings'));
+            }
+          }
+        });*/
+        break;
+    }
+  }).catch(erros => {
+    console.log('[LocationPermission] checkPermission-catch', erros);
+    back(false, false);
+  });
+}
+
+/**
+ * 请求定位权限
+ * @param {function} back callback(hasPermission)
+ */
+const getPermission = (back) => {
+  request(
+      isIOS 
+    ? PERMISSIONS.IOS.LOCATION_WHEN_IN_USE
+    : PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION)
+  .then(res => {
+    console.log('[LocationPermission] requestPermission', res);
+    back(res == RESULTS.GRANTED);
+  }).catch(erros => {
+    console.info('[LocationPermission] requestPermission-catch', erros)
+    back(false);
+  });
+}
+
+const LocationPermission = ({visible=false, bottomDivide=false, onView}) => (
+  visible
+  ? <View style={[styles.permissionView, (bottomDivide ? styles.bottomTabDivide : {})]}>
+      <TextView
+        style={styles.permissionText}
+        numberOfLines={1}>{$t("home.locationPermissionTips")}</TextView>
+      <Button
+        style={styles.viewButton}
+        viewStyle={styles.viewButtonStyle}
+        onClick={() => {
+          openSettings().then(res => {
+            if (onView) onView();
+          }).catch(() => console.warn('cannot open settings'))
+        }}>
+        <TextView style={styles.buttonText}>{$t("nav.view")}</TextView>
+        <FontAwesome
+          size={16}
+          color={colorPrimary}
+          name='angle-right'/>
+      </Button>
+    </View>
+  : <></>
+);
+
+const styles = StyleSheet.create({
+  permissionView: {
+    //top: 80,
+    left: 16,
+    right: 16,
+    bottom: 32,
+    opacity: .8,
+    zIndex: 5,
+    overflow: 'hidden',
+    borderRadius: 30,
+    position: 'absolute',
+    alignItems: 'center',
+    flexDirection: 'row',
+    backgroundColor: '#FEB751'
+  },
+  bottomTabDivide: {
+    bottom: 76
+  },
+  permissionText: {
+    flex: 1,
+    color: textDark,
+    fontSize: 11,
+    paddingLeft: 16,
+    paddingRight: 2
+  },
+  viewButton: {
+    borderRadius: 0,
+    backgroundColor: 'transparent'
+  },
+  viewButtonStyle: {
+    alignItems: 'center',
+    flexDirection: 'row',
+    ...$padding(8, 12, 8, 8),
+  },
+  buttonText: {
+    color: colorPrimary,
+    fontSize: 11,
+    paddingRight: 4
+  }
+})
+
+export default {
+  VIEW: LocationPermission,
+  checkPermission: checkPermission,
+  requestPermission: getPermission,
+  LocationListener: LocationListener
+}

+ 196 - 0
Strides-SPAPP/app/pages/home/maps/MapTool.js

@@ -0,0 +1,196 @@
+/**
+ * 首页地图工具组件
+ * @邠心vbe on 2021/08/13
+ */
+import React, { Component } from 'react';
+import { View, StyleSheet, TouchableOpacity } from 'react-native';
+import Button, { ElevationObject } from '../../../components/Button';
+import BottomModal from '../../../components/BottomModal';
+import Filter from './Filter';
+import { PageList } from '../../Router';
+import utils from '../../../utils/utils';
+import Dialog from '../../../components/Dialog';
+import apiStation from '../../../api/apiStation';
+
+export default class MapTool extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      showFilter: false,
+      filterIndex: {},
+      filterTotal: 0,
+    };
+  }
+
+  componentDidUpdate() {
+    if (this.props.count != this.state.filterTotal) {
+      this.setState({
+        filterTotal: this.props.count
+      })
+    }
+  }
+
+  findFilter(data, index) {
+    this.setState({
+      filterIndex: index
+    });
+    this.hideFilter();
+    if (this.props.onFilter) {
+      this.props.onFilter(data);
+    }
+  }
+
+  showFilter() {
+    this.setState({
+      showFilter: true
+    });
+  }
+
+  hideFilter() {
+    this.setState({
+      showFilter: false
+    });
+  }
+
+  getNearestStation() {
+    navigator.geolocation.getCurrentPosition(location => {
+      let params = {
+        lat: location.coords.latitude,
+        lng: location.coords.longitude
+      }
+      Dialog.showProgressDialog();
+      apiStation.getNearestSite(params).then(res => {
+        Dialog.dismissLoading();
+        if (res.data.sitePk) {
+          var station = {
+            id: res.data.sitePk,
+            name: res.data.siteName,
+            address: res.data.address,
+            latitude: res.data.locationLatitude,
+            longitude: res.data.locationLongitude,
+          }
+          setTimeout(() => {
+            utils.directMaps(station.latitude, station.longitude, station.address);
+          }, 500);
+        } else {
+          toastShort("您附近没有充电桩");
+        }
+      }).catch(err => {
+        toastShort(err);
+        Dialog.dismissLoading();
+      });
+    });
+  }
+
+  render() {
+    return (
+      <View>
+        <View style={styles.mapToolView}>
+          { this.props.mapReady &&
+            <TouchableOpacity
+              style={styles.mapIcon}
+              activeOpacity={0.7}
+              onPress={() => this.props.onLocation()}>
+              <MaterialIcons
+                name='my-location'
+                size={26}
+                color={colorAccent}/>
+            </TouchableOpacity>
+          }
+          { this.props.mapReady &&
+            <TouchableOpacity
+              style={styles.mapIcon}
+              activeOpacity={0.7}
+              onPress={() => {
+                this.showFilter()
+              }}>
+              <FontAwesome
+                name='filter'
+                size={26}
+                color={colorAccent}/>
+            </TouchableOpacity>
+          }
+          <TouchableOpacity
+            style={styles.mapIcon}
+            activeOpacity={0.7}
+            onPress={() => {
+              startPage(PageList.scanqr, {actionDetail: true});
+            }}>
+            <FontAwesome
+              name='qrcode'
+              size={25}
+              color={colorAccent}/>
+          </TouchableOpacity>
+        </View>
+
+        {/* <View style={styles.bottomView}>
+          <Button
+            style={styles.bottomButton}
+            textSize={17}
+            text='Go to Nearest Charging Station'
+            onClick={() => {
+              this.getNearestStation();
+            }
+          }/>
+        </View> */}
+        <BottomModal
+          visible={this.state.showFilter}
+          onHide={() => {
+            this.hideFilter()
+          }}>
+          <Filter
+            index={this.state.filterIndex}
+            count={this.state.filterTotal}
+            onDone={(data, index) => this.findFilter(data, index)}
+            onHide={() => this.hideFilter()}/>
+        </BottomModal>
+      </View>
+    );
+  }
+}
+
+const styles = StyleSheet.create({
+  mapIcon: {
+    width: 50,
+    height: 50,
+    marginTop: 10,
+    borderRadius: 56,
+    alignItems: 'center',
+    backgroundColor: '#333',
+    justifyContent: 'center',
+    ...ElevationObject(1),
+    shadowColor: '#cccccc'
+  },
+  mapToolView: {
+    right: 24,
+    zIndex: 10,
+    bottom: 112,
+    width: 56,
+    position: 'absolute',
+    alignItems: 'center'
+  },
+  bottomView: {
+    left: 0,
+    right: 0,
+    bottom: 32,
+    zIndex: 210,
+    alignItems: 'center',
+    position: 'absolute',
+  },
+  bottomButton: {
+    elevation: 1.5,
+    paddingLeft: 32,
+    paddingRight: 32,
+    borderRadius: 54,
+    overflow: 'hidden',
+    justifyContent: 'center'
+  },
+  nearStation: {
+    flex: 1,
+    paddingLeft: 32,
+    paddingRight: 32,
+    ...ElevationObject(2),
+    justifyContent: 'center',
+    backgroundColor: colorAccent
+  },
+})

+ 42 - 0
Strides-SPAPP/app/pages/home/maps/Maps.js

@@ -0,0 +1,42 @@
+
+import React, { useEffect, useRef } from 'react';
+import MapView, { PROVIDER_GOOGLE } from 'react-native-maps';
+//import Maps1 from './Maps1'
+//import Maps2 from './Maps2'
+//import Maps3 from './Maps3'
+import { MyMarker } from './Cluster';
+
+const MapOnly= ({region, stopList, onMapReady, onMarkerPress}) => {
+  const mapRef = useRef();
+  useEffect(() => {
+    console.log('mapRef.current', mapRef.current);
+    //mapRef.current.animateToRegion(region, 500);
+    mapRef.current.animateCamera({ center: region, zoom: 17 }, { duration: 500 }); //移动地图到当前位置并放大
+  }, [region])
+  return (
+    <MapView
+      ref={mapRef}
+      style={ui.flex1}
+      initialRegion={region}
+      onMapReady={onMapReady}
+      provider={PROVIDER_GOOGLE}
+      showsUserLocation={true}>
+      { stopList.map((marker, index) => {
+        return (
+          <MyMarker
+            key={index}
+            coordinate={marker}
+            onPress={() => onMarkerPress(marker.id)}
+          />
+        );
+      })}
+    </MapView>
+  )
+}
+
+export default {
+  //Maps1: Maps2,
+  Maps2: <></>,
+  Maps3: <></>,
+  MapOnly: MapOnly
+}

+ 75 - 0
Strides-SPAPP/app/pages/home/maps/Maps1.js

@@ -0,0 +1,75 @@
+/**
+ * 首页地图组件-使用ClusterMap
+ * @邠心vbe on 2021/03/18
+ */
+import React, { useEffect, useRef } from 'react';
+import { PROVIDER_GOOGLE } from 'react-native-maps';
+import MapView from "react-native-map-clustering";
+import { ClusterView, MyCluster, MyMarker } from './Cluster';
+
+export default Maps = ({ region, onMapReady, stopList, onMarkerPress }) => {
+  const mapRef = useRef();
+  const superClusterRef = useRef();
+
+  useEffect(() => {
+    console.log("mapRef", mapRef.current);
+    //mapRef.current.animateCamera({ center: region, zoom: 17 }, 500); //移动地图到当前位置并放大
+  }, [region])
+
+  /*componentDidMount() {
+    this.mapRef = this.ref.current.mapRef ? this.ref.current.mapRef : this.ref.current;
+  }*/
+
+  const renderCluster = (props) => {
+    //InteractionManager.runAfterInteractions();
+    return <MyCluster {...props} key={props.id} onOpen1={latlng => {openClusterMarker(latlng)}}/>
+  }
+
+  const openClusterMarker = async (latlng) => {
+    var camera = await mapRef.current.getCamera();
+    camera.center = latlng
+    camera.zoom = camera.zoom + 2
+    //console.log('camera', camera);
+    mapRef.current.animateCamera(camera, 300);
+  }
+
+  getMyCluster = (props) => {
+    return <ClusterView id={props.clusterId} pointCount={props.pointCount}/>
+  }
+  
+  const superClusterOptions = {
+    minZoom: 1,
+    maxZoom: 15,
+    radius: 25,
+    extent: 512,
+    nodeSize: 32
+  }
+
+  return (
+    <MapView
+      ref={mapRef}
+      superClusterRef={superClusterRef}
+      style={ui.flex1}
+      minZoom={10}
+      maxZoom={15}
+      radius={45}
+      extent={512}
+      nodeSize={64}
+      provider={PROVIDER_GOOGLE}
+      onMapReady={onMapReady}
+      showsUserLocation={true}
+      initialRegion={region}
+      renderCluster={(info) => renderCluster(info)}>
+      { stopList.map((marker, index) => {
+        return (
+          <MyMarker
+            key={index}
+            {...marker}
+            coordinate={marker.location}
+            onPress={() => onMarkerPress(marker.id)}
+          />
+        );
+      })}
+    </MapView>
+  );
+}

+ 93 - 0
Strides-SPAPP/app/pages/home/maps/Maps2.js

@@ -0,0 +1,93 @@
+/**
+ * 首页地图组件-使用ClusteringMapView
+ * @邠心vbe on 2021/03/18
+ */
+import React, { useEffect, useRef } from 'react';
+import { PROVIDER_GOOGLE } from 'react-native-maps';
+import MapView from "react-native-map-clustering";
+import { MyCluster, MyMarker } from './Cluster';
+
+const Maps2 = ({ region, onMapReady, stopList, onMarkerPress }) => {
+  const mapRef = useRef();
+  const superClusterRef = useRef();
+
+  useEffect(() => {
+    console.log('mapRef.current', mapRef.current);
+    //mapRef.current.animateToRegion(region, 500);
+    mapRef.current.animateCamera({ center: region, zoom: 17 }, { duration: 500 }); //移动地图到当前位置并放大
+  }, [region])
+
+  /*componentDidMount() {
+    this.mapRef = this.ref.current.mapRef ? this.ref.current.mapRef : this.ref.current;
+  }*/
+
+  const renderCluster = (props) => {
+    //console.log("renderCluster", props);
+    let hasAvailableConnector = false;
+    if (props.geometry.hasAvailableConnector === undefined) {
+      if (superClusterRef) {
+        const points = superClusterRef.current.getLeaves(props.id, Infinity, 0);
+        if (points) {
+          for (let index = 0; index < points.length; index++) {
+            const point = points[index];
+            if (point.properties.coordinate?.available) {
+              hasAvailableConnector = true;
+              break;
+            }
+          }
+        }
+      }
+    } else {
+      hasAvailableConnector = props.geometry.hasAvailableConnector
+    }
+    props.geometry.hasAvailableConnector = hasAvailableConnector;
+    const latlng = {latitude: props.geometry.coordinates[1], longitude: props.geometry.coordinates[0]}
+    //InteractionManager.runAfterInteractions();
+    return (
+      <MyCluster
+        {...props}
+        key={props.id}
+        location={latlng}
+        available={hasAvailableConnector}
+        onOpen2={latlng => openClusterMarker(latlng)}/>
+    );
+  }
+
+  const openClusterMarker = async (latlng) => {
+    var camera = await mapRef.current.getCamera();
+    camera.center = latlng
+    camera.zoom = camera.zoom + 2
+    //console.log('camera', camera);
+    mapRef.current.animateCamera(camera, 300);
+  }
+
+  return (
+    <MapView
+      ref={mapRef}
+      superClusterRef={superClusterRef}
+      style={ui.flex1}
+      minZoom={10}
+      maxZoom={15}
+      radius={45}
+      extent={512}
+      nodeSize={64}
+      provider={PROVIDER_GOOGLE}
+      onMapReady={onMapReady}
+      showsUserLocation={true}
+      initialRegion={region}
+      renderCluster={(info) => renderCluster(info)}>
+      { stopList.map((marker, index) => {
+        return (
+          <MyMarker
+            key={index}
+            {...marker}
+            coordinate={marker}
+            onPress={() => onMarkerPress(marker.id)}
+          />
+        );
+      })}
+    </MapView>
+  );
+}
+
+export default Maps2;

+ 112 - 0
Strides-SPAPP/app/pages/home/maps/Maps3.js

@@ -0,0 +1,112 @@
+/**
+ * 首页地图组件-使用ClusteringMapView和VbeClusterMap
+ * @邠心vbe on 2021/03/18
+ */
+import React, { useEffect, useRef } from 'react';
+import { PROVIDER_DEFAULT, PROVIDER_GOOGLE } from 'react-native-maps';
+import MapView from "react-native-map-clustering";
+import { MyCluster, MyMarker } from './Cluster';
+import VbeClusterMap from 'vbe-cluster-map';
+import app from '../../../../app.json';
+
+export default Maps3 = ({ region, onMapReady, stopList, useApplesMap, showUserLocation=true, onMarkerPress }) => {
+  const mapRef = useRef();
+  const superClusterRef = useRef();
+
+  useEffect(() => {
+  if (isIOS) {
+    //mapRef.current.animateToRegion(region, 500);
+    mapRef.current.animateCamera({ center: region, zoom: 17 }, { duration: 500 });
+    //mapRef.current.animateCamera({ center: region, zoom: 17 }, 500); //移动地图到当前位置并放大
+  } else {
+    if (mapRef.current.moveCamera) {
+      mapRef.current.moveCamera(region, 17);
+      //mapRef.current.showUserLocation();
+    }
+  }
+  }, [region])
+
+  /*componentDidMount() {
+    this.mapRef = this.ref.current.mapRef ? this.ref.current.mapRef : this.ref.current;
+  }*/
+
+  const renderCluster = (props) => {
+    //console.log("renderCluster", props);
+    let hasAvailableConnector = false;
+    if (props.geometry.hasAvailableConnector === undefined) {
+      if (superClusterRef) {
+        const points = superClusterRef.current.getLeaves(props.id, Infinity, 0);
+        if (points) {
+          for (let index = 0; index < points.length; index++) {
+            const point = points[index];
+            if (point.properties.coordinate?.available) {
+              hasAvailableConnector = true;
+              break;
+            }
+          }
+        }
+      }
+    } else {
+      hasAvailableConnector = props.geometry.hasAvailableConnector
+    }
+    props.geometry.hasAvailableConnector = hasAvailableConnector;
+    const latlng = {latitude: props.geometry.coordinates[1], longitude: props.geometry.coordinates[0]}
+    //InteractionManager.runAfterInteractions();
+    return (
+      <MyCluster
+        {...props}
+        key={props.id}
+        location={latlng}
+        available={hasAvailableConnector}
+        onOpen1={latlng => openClusterMarker(latlng)}/>
+    );
+  }
+
+  const openClusterMarker = async (latlng) => {
+    var camera = await mapRef.current.getCamera();
+    camera.center = latlng
+    camera.zoom = camera.zoom + 2
+    //console.log('camera', camera);
+    mapRef.current.animateCamera(camera, 300);
+  }
+
+  return (
+    isIOS
+    ? <MapView
+        ref={mapRef}
+        superClusterRef={superClusterRef}
+        style={ui.flex1}
+        minZoom={5}
+        maxZoom={16}
+        radius={45}
+        extent={app.modules.nationally ? 100 : 512}
+        nodeSize={64}
+        provider={useApplesMap ? PROVIDER_DEFAULT : PROVIDER_GOOGLE}
+        onMapReady={onMapReady}
+        showsUserLocation={showUserLocation}
+        initialRegion={region}
+        renderCluster={(info) => renderCluster(info)}>
+      { stopList.map((marker, index) => {
+        return (
+          <MyMarker
+            key={index}
+            {...marker}
+            coordinate={marker}
+            onPress={() => onMarkerPress(marker.id)}
+          />
+        );
+      })}
+    </MapView>
+  : <VbeClusterMap
+      ref={mapRef}
+      style={ui.flex1}
+      region={region}
+      data={stopList}
+      animation={true}
+      onMapReady={onMapReady}
+      onMarkerPress={e => onMarkerPress(e.id)}
+      showUserLocation={showUserLocation}
+      moveOnMarkerPress={true}
+    />
+  );
+}

+ 207 - 0
Strides-SPAPP/app/pages/home/maps/PinMessage.js

@@ -0,0 +1,207 @@
+import React, { useEffect, useState } from 'react';
+import { ScrollView, StyleSheet, Text, View } from 'react-native';
+//import { Marquee } from '@animatereactnative/marquee';
+import { ElevationObject } from '../../../components/Button';
+import TextView from '../../../components/TextView';
+import MyModal from '../../../components/MyModal';
+import Swiper from 'react-native-swiper';
+import Dialog from '../../../components/Dialog';
+
+const PinMessage = ({
+  messageList=[]
+}) => {
+  const [visible, showDialog] = useState(false);
+  useEffect(() => {
+    if (messageList.length > 0) {
+      showDialog(true)
+    }
+  }, [messageList])
+  if (isIOS) {
+    if (visible)
+      return (
+        <View
+          style={styles.noticeLayer}>
+          <View style={styles.noticeDialog2}>
+            <Swiper
+              //style={{height: $vh(50)}}
+              loop={true}
+              autoplay={false}
+              //dotColor={"#ccc"}
+              activeDotColor={colorAccent}
+              removeClippedSubviews={false}
+              decelerationRate={"fast"}
+              automaticallyAdjustContentInsets={true}>
+              { messageList.map((item, index) =>
+                <View key={index} style={styles.noticeDialogChild}>
+                  <NoticeView
+                    key={index}
+                    item={item}
+                    onClose={() => showDialog(false)}/>
+                </View>
+              )}
+            </Swiper>
+          </View>
+        </View>
+      );
+    else
+      return <></>
+  } else {
+    return (
+      <MyModal
+        style={styles.noticeDialog}
+        visible={visible}>
+        <Swiper
+          style={{height: $vh(50)}}
+          loop={false}
+          autoplay={false}
+          //dotColor={"#ccc"}
+          activeDotColor={colorAccent}
+          removeClippedSubviews={false}
+          decelerationRate={"fast"}
+          automaticallyAdjustContentInsets={true}>
+          { messageList.map((item, index) =>
+            <NoticeView
+              key={index}
+              item={item}
+              onClose={() => showDialog(false)}/>
+          )}
+        </Swiper>
+      </MyModal>
+    );
+  }
+}
+
+const Message = ({item}) => {
+  const [expand, setExpand] = useState(false)
+  const max = ($width-32)/8;
+  return (
+    <View style={styles.messageCard}>
+      <View style={ui.flexc}>
+        { item.pinTitle?.length>max
+        ? <Marquee style={styles.marquee} spacing={$vw(80)} speed={0.8}>
+            <Text style={styles.textTitle} numberOfLines={1}>{item.pinTitle}</Text>
+          </Marquee>
+        : <Text style={styles.fixedTitle} numberOfLines={expand ? 5 : 1}>{item.pinTitle}</Text>
+        }
+        <FontAwesome6
+          style={$padding(12, 16, 12, 0)}
+          name={expand == true ? "chevron-up" : "chevron-down"}
+          size={16}
+          color={colorCancel}
+          onPress={() => setExpand(!expand)}/>
+      </View>
+      { expand &&
+        <ScrollView>
+          <TextView style={styles.textMessage}>{item.pinContent}</TextView>
+        </ScrollView>
+      }
+    </View>
+  )
+}
+
+const NoticeView = ({item, onClose}) => {
+  return (
+    <View style={styles.notiveView}>
+      <TextView style={styles.noticeTitle}>{item.pinTitle}</TextView>
+      <View style={styles.closeIcon}>
+        <MaterialIcons
+          onPress={onClose}
+          name="close"
+          color="#ccc"
+          size={24}/>
+      </View>
+      <ScrollView
+        style={ui.flex1}>
+        <TextView style={styles.textMessage}>{item.pinContent}</TextView>
+      </ScrollView>
+    </View>
+  )
+}
+
+export default PinMessage;
+
+const styles = StyleSheet.create({
+  pinMessageView: {
+    top: 180,
+    left: 16,
+    right: 16,
+    zIndex: 10,
+    maxHeight: $vh(60),
+    overflow: "hidden",
+    position: 'absolute',
+  },
+  messageCard: {
+    marginBottom: 16,
+    maxHeight: $vh(50),
+    overflow: "hidden",
+    borderRadius: 6,
+    backgroundColor: colorLight,
+    ...ElevationObject(3)
+  },
+  marquee: {
+    flex: 1,
+    overflow: "hidden",
+    ...$padding(12, 16)
+  },
+  textTitle: {
+    color: textPrimary,
+    fontSize: 16,
+    fontWeight: "bold"
+  },
+  fixedTitle: {
+    flex: 1,
+    color: textPrimary,
+    fontSize: 16,
+    fontWeight: "bold",
+    ...$padding(12, 16)
+  },
+  textMessage: {
+    color: textSecondary,
+    paddingLeft: 16,
+    paddingRight: 16,
+    paddingBottom: 16,
+    fontSize: 14
+  },
+  noticeLayer: {
+    top: 0,
+    left: 0,
+    right: 0,
+    bottom: 0,
+    zIndex: 20,
+    position: "absolute",
+    alignItems: 'center',
+    justifyContent: 'center',
+    backgroundColor: 'rgba(0,0,0,.5)'
+  },
+  noticeDialog: {
+    width: Dialog.dialogWidth,
+    height: $vw(100) + 48
+  },
+  noticeDialog2: {
+    height: $vh(50)
+  },
+  noticeDialogChild: {
+    width: Dialog.dialogWidth,
+    marginLeft: "auto",
+    marginRight: "auto"
+  },
+  closeIcon: {
+    top: 8,
+    right: 8,
+    position: "absolute"
+  },
+  notiveView: {
+    height: $vh(50) - 48,
+    marginLeft: 4,
+    marginRight: 4,
+    borderRadius: 12,
+    backgroundColor: colorLight
+  },
+  noticeTitle: {
+    color: textPrimary,
+    padding: 16,
+    fontSize: 16,
+    fontWeight: "bold",
+    marginRight: 16
+  }
+})

+ 205 - 0
Strides-SPAPP/app/pages/home/maps/SearchTool.js

@@ -0,0 +1,205 @@
+/**
+ * 首页地图工具和搜索框组件
+ * @邠心vbe on 2022/12/20
+ */
+import React, { Component } from 'react';
+import { View, StyleSheet, Text, Pressable } from 'react-native';
+import BottomModal from '../../../components/BottomModal';
+import Filter from './Filter';
+import { PageList } from '../../Router';
+import { BackButton } from '../../../components/Toolbar';
+ 
+export default class SearchTool extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      showFilter: false,
+      filterIndex: {},
+      filterTotal: 0,
+    };
+  }
+
+  componentDidUpdate() {
+    if (this.props.count != this.state.filterTotal) {
+      this.setState({
+        filterTotal: this.props.count
+      })
+    }
+  }
+
+  findFilter(data, index) {
+    this.setState({
+      filterIndex: index
+    });
+    this.hideFilter();
+    if (this.props.onFilter) {
+      this.props.onFilter(data);
+    }
+  }
+
+  showFilter() {
+    this.setState({
+      showFilter: true
+    });
+  }
+
+  hideFilter() {
+    this.setState({
+      showFilter: false
+    });
+  }
+
+  /*getNearestStation() {
+    navigator.geolocation.getCurrentPosition(location => {
+      let params = {
+        lat: location.coords.latitude,
+        lng: location.coords.longitude
+      }
+      Dialog.showProgressDialog();
+      apiStation.getNearestSite(params).then(res => {
+        Dialog.dismissLoading();
+        if (res.data.sitePk) {
+          var station = {
+            id: res.data.sitePk,
+            name: res.data.siteName,
+            address: res.data.address,
+            latitude: res.data.locationLatitude,
+            longitude: res.data.locationLongitude,
+          }
+          setTimeout(() => {
+            utils.directMaps(station.latitude, station.longitude, station.address);
+          }, 500);
+        } else {
+          toastShort("您附近没有充电桩");
+        }
+      }).catch(err => {
+        toastShort(err);
+        Dialog.dismissLoading();
+      });
+    });
+  }*/
+
+  render() {
+    return (
+      <View>
+        <View style={styles.searchView}>
+          <Pressable
+            style={styles.searchInput}
+            onPress={() => {
+              startPage(PageList.search);
+            }}>
+            <Feather
+              name={'search'}
+              size={24}
+              color={'#444'}/>
+            <Text style={styles.searchText}>
+              {$t('home.search')}
+            </Text>
+          </Pressable>
+          { this.props.mapReady &&
+            <BackButton
+              style={styles.mapIcon}
+              onPress={() => this.showFilter()}>
+              <FontAwesome
+                name='filter'
+                size={26}
+                color={colorPrimary}/>
+            </BackButton>
+          }
+          { this.props.mapReady &&
+            <BackButton
+              style={styles.mapIcon}
+              onPress={() => this.props.onLocation()}>
+              <MaterialIcons
+                name='my-location'
+                size={26}
+                color={colorPrimary}/>
+            </BackButton>
+          }
+          {/* <View style={styles.mapToolView}>
+            <TouchableOpacity
+              style={styles.mapIcon}
+              activeOpacity={0.7}
+              onPress={() => {
+                startPage(PageList.scanqr, {actionDetail: true});
+              }}>
+              <FontAwesome
+                name='qrcode'
+                size={25}
+                color={colorPrimary}/>
+            </TouchableOpacity>
+          </View> */}
+        </View>
+        <BottomModal
+          visible={this.state.showFilter}
+          onHide={() => {
+            this.hideFilter()
+          }}>
+          <Filter
+            index={this.state.filterIndex}
+            count={this.state.filterTotal}
+            onDone={(data, index) => this.findFilter(data, index)}
+            onHide={() => this.hideFilter()}/>
+        </BottomModal>
+      </View>
+    );
+  }
+}
+
+const styles = StyleSheet.create({
+  searchView: {
+    alignItems: 'center',
+    flexDirection: 'row',
+    ...$padding(8, 16, 16),
+    backgroundColor: colorThemes
+  },
+  searchInput: {
+    flex: 1,
+    alignItems: 'center',
+    borderWidth: 1,
+    borderStyle: 'solid',
+    borderRadius: 60,
+    borderColor: '#CCD0E7',
+    flexDirection: 'row',
+    paddingLeft: 16,
+    paddingRight: 16,
+    backgroundColor: 'rgba(255, 255, 255, 0.5)'
+  },
+  searchText: {
+    flex: 1,
+    color: '#444',
+    padding: 8,
+    fontSize: 15
+  },
+  mapIcon: {
+    width: 32,
+    height: 32,
+    marginLeft: 16,
+    alignItems: 'center',
+    justifyContent: 'center',
+  },
+  mapToolView: {
+    right: 24,
+    zIndex: 10,
+    bottom: 112,
+    width: 56,
+    position: 'absolute',
+    alignItems: 'center'
+  },
+  bottomView: {
+    left: 0,
+    right: 0,
+    bottom: 32,
+    zIndex: 210,
+    alignItems: 'center',
+    position: 'absolute',
+  },
+  bottomButton: {
+    elevation: 1.5,
+    paddingLeft: 32,
+    paddingRight: 32,
+    borderRadius: 54,
+    overflow: 'hidden',
+    justifyContent: 'center'
+  }
+})

+ 122 - 0
Strides-SPAPP/app/pages/home/maps/Test.js

@@ -0,0 +1,122 @@
+import React, { Component } from 'react';
+import { View, Text } from 'react-native';
+import apiStation from '../../../api/apiStation';
+import Dialog from '../../../components/Dialog';
+import utils from '../../../utils/utils';
+import Button from '../../../components/Button';
+import { PageList } from '../../Router';
+import MapView from 'react-native-maps';
+
+export default class Test extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      region: {
+        latitude: 1.3532623163977149,
+        longitude: 103.87092316860532,
+        latitudeDelta: 0.0922,
+        longitudeDelta: 0.0421,
+        zoom: 10
+      },
+      stopList: [{
+        id: 1,
+        latitude: 1.3532623163977149,
+        longitude: 103.87092316860532,
+        name: "Text",
+        available: false
+      }],
+    };
+  }
+
+  viewChargeStation(e) {
+    console.log("onMarkerPress", e);
+  }
+
+  getGeoLocation() {
+    Dialog.showProgressDialog();
+    apiStation.getAllStation({}).then(res => {
+      Dialog.dismissLoading();
+      if (res.data.sites) {
+        const list = [];
+        res.data.sites.forEach(item => {
+          let available = false
+          if (item.allConnector && item.allConnector.available) {
+            available = true
+          }
+          list.push({
+            id: item.sitePk,
+            name: item.siteName,
+            //address: item.siteAddress,
+            available: available,
+            latitude: item.locationLatitude,
+            longitude: item.locationLongitude,
+            favorite: item?.favorite ? true : false,
+            /*acConnector: item.acConnector,
+            allConnector: item.allConnector,
+            dcConnector: item.dcConnector,*/
+            //distance: utils.getDistance(item.distance)
+          });
+        });
+        this.setState({
+          stopList: list
+        });
+      }
+    }).catch(err => {
+      Dialog.dismissLoading();
+      this.setState({
+        stopList: []
+      });
+    })
+  }
+
+  render() {
+    return (
+      <View style={ui.flex1}>
+        <View style={[ui.flexc, $padding(8, 16)]}>
+          <Text style={ui.flex1}>Vbe Map Test</Text>
+          <Button
+            style={ui.flex2}
+            text="Move"
+            onClick={() => {
+              if (this.state.mapReady) {
+                this.setState({
+                  region: {
+                    latitude: 1.3532623163977140,
+                    longitude: 103.87092316860532,
+                    latitudeDelta: 0.0922,
+                    longitudeDelta: 0.0421,
+                    zoom: 17
+                  }
+                });
+              } else {
+                this.setState({
+                  mapReady: true
+                }, () => {
+                  this.getGeoLocation();
+                });
+              }
+            }}/>
+        </View>
+        
+        <MapView
+          style={ui.flex1}
+          data={this.state.stopList}
+          region={this.state.region}
+          animation={true}
+          onMapReady={() => {
+            this.setState({
+              mapReady: true
+            }, () => {
+              this.getGeoLocation();
+            });
+          }}
+          onMarkerPress={e => this.viewChargeStation(e)}
+        />
+        <Button
+          text="Test Page"
+          onClick={() => startPage(PageList.paymentMethod)}
+        />
+      </View>
+    );
+  }
+}

+ 151 - 0
Strides-SPAPP/app/pages/home/maps/TopInfo.js

@@ -0,0 +1,151 @@
+/**
+ * 首页顶部充电站信息组件
+ * @邠心vbe on 2021/08/13
+ */
+import React from 'react';
+import { Pressable, StyleSheet, View, Text, Image } from 'react-native';
+import { ElevationObject } from '../../../components/Button';
+import utils from '../../../utils/utils';
+import { ChargeStyle } from '../../charge/Charging';
+import { PageList } from '../../Router';
+
+
+export default TopInfo = ({stationInfo = {}}) => {
+  const getAvailable = (type) => {
+    const all = stationInfo.allConnector;
+    if (all) {
+      if (type == 'box') {
+        return all.boxAvailable + '/' + all.boxAll;
+      } else {
+        return all.available + '/' + all.all;
+      }
+    } else {
+      return '0/0';
+    }
+  }
+
+  if (stationInfo.id) {
+    return (
+      <View style={styles.stationBarView}>
+        <Pressable
+          style={styles.stationInfo}
+          onPress={() => startPage(PageList.chargeDetail, {stationInfo: stationInfo, action: 'view'})}>
+          <Text
+            ellipsizeMode='tail'
+            numberOfLines={1}
+            style={styles.stationTitle}>{stationInfo.name}</Text>
+          <Text
+            style={styles.stationAddress}
+            ellipsizeMode='tail'
+            numberOfLines={2}>{stationInfo.address}</Text>
+        </Pressable>
+        <View style={styles.stationStatusView}>
+          { stationInfo.allConnector && stationInfo.allConnector.available > 0 &&
+            <Text style={[ChargeStyle.infoStatus, ChargeStyle.statusAvailable, styles.stationStatus]}>Available</Text>
+          }
+          <Text style={[ChargeStyle.infoStatus, stationInfo.siteType == 'Public' ? ChargeStyle.statusAvailable : ChargeStyle.statusWarning, styles.siteTypes]}>{stationInfo.siteType}</Text>
+        </View>
+        <View style={styles.stationAvailable}>
+          <Image
+            style={styles.availableIcon}
+            source={require('../../../images/charge/icon-type-stops.png')}/>
+          <Text style={styles.availableText}>{getAvailable('box')}</Text>
+        </View>
+        <View style={styles.stationAvailable}>
+          <Image
+            style={styles.availableIcon}
+            source={require('../../../images/charge/icon-type-interfaces.png')}/>
+          <Text style={styles.availableText}>{getAvailable('inc')}</Text>
+        </View>
+        <Pressable
+          style={styles.directView}
+          onPress={() => {
+            utils.directMaps(stationInfo.latitude, stationInfo.longitude, stationInfo.address);
+          }}>
+          <MaterialIcons
+            name='directions'
+            size={27}
+            color={colorAccent}/>
+          <Text style={styles.distanceText}>{stationInfo.distance}</Text>
+        </Pressable>
+      </View>
+    );
+  } else {
+    return <></>;
+  }
+}
+
+const styles = StyleSheet.create({
+  stationBarView: {
+    top: 16,
+    left: 16,
+    right: 16,
+    height: 69,
+    zIndex: 10,
+    borderRadius: 6,
+    ...$padding(12, 9),
+    position: 'absolute',
+    alignItems: 'center',
+    flexDirection: 'row',
+    backgroundColor: colorLight,
+    ...ElevationObject(1.5)
+  },
+  stationInfo: {
+    flex: 1,
+    height: 45,
+    paddingLeft: 4,
+    paddingRight: 8,
+    justifyContent: 'space-around'
+  },
+  stationTitle: {
+    color: '#000',
+    fontSize: 16,
+    paddingBottom: 2
+  },
+  stationAddress: {
+    color: '#999',
+    fontSize: 12,
+  },
+  stationAvailable: {
+    height: 45,
+    paddingLeft: 8,
+    paddingRight: 8,
+    alignItems: 'center',
+    justifyContent: 'space-between'
+  },
+  stationStatusView: {
+    marginRight: 4,
+    alignItems: 'center',
+    justifyContent: 'center'
+  },
+  stationStatus: {
+    fontSize: 10,
+    ...$padding(2, 6)
+  },
+  siteTypes: {
+    fontSize: 10,
+    marginTop: 4,
+    ...$padding(2, 6)
+  },
+  availableIcon: {
+    width: 23,
+    height: 23,
+  },
+  availableText: {
+    color: textPrimary,
+    fontSize: 13,
+    textAlign: 'center'
+  },
+  directView: {
+    zIndex: 1,
+    height: 45,
+    marginLeft: 8,
+    marginRight: 4,
+    alignItems: 'center',
+    justifyContent: 'space-between'
+  },
+  distanceText: {
+    color: textPrimary,
+    fontSize: 12,
+  }
+})