Forráskód Böngészése

Develop LUMI UI V4
https://dev.wormwood.com.sg/zentao/task-view-206.html

vbea 2 éve
szülő
commit
ef22331e9f

+ 2 - 1
Strides-APP/app/components/Dropdown.js

@@ -4,6 +4,7 @@ import Modal from 'react-native-modal';
 import Button from './Button';
 import Dialog, { getDialogWidth } from './Dialog';
 import TextView from './TextView';
+import app from '../../app.json';
 
 //const DialogMaxWidth = $vw(85) > 500 ? 500 : $vw(85);
 //const DialogIOSWidth = $vw(75) > 450 ? 450 : $vw(75);
@@ -152,7 +153,7 @@ export default Dropdown = ({
             : <TextView style={[placeholderStyle, styles.textView]} numberOfLines={1}>{placeholder}</TextView>
           )
         }
-        { showIcon && (isIOS
+        { showIcon && (isIOS || app.isLumiWhitelabel
         ? <MaterialIcons 
             name={'keyboard-arrow-down'}
             size={24}

+ 30 - 11
Strides-APP/app/components/SiteLabelView.js

@@ -3,26 +3,27 @@
  * @邠心vbe on 2023/11/30
  */
 import React from 'react';
+import { StyleSheet } from 'react-native';
 import TextView from './TextView';
 
 const SiteLabelView = ({
   label,
   color = textLight,
+  version=1,
   background = colorPrimary
 }) => {
   if (label) {
     return (
       <TextView
-        style={{
-          color: color || textLight,
-          height: 22,
-          fontSize: 12,
-          marginRight: 5,
-          marginBottom: 5,
-          borderRadius: 3,
-          ...$padding(0, 6),
-          backgroundColor: background || colorPrimary
-        }}>
+        style={[
+          version == 2
+          ? style.textV2
+          : style.textV1,
+          {
+            color: color || textLight,
+            backgroundColor: background || colorPrimary
+          }
+        ]}>
         {label}
       </TextView>
     )
@@ -31,4 +32,22 @@ const SiteLabelView = ({
   }
 };
 
-export default SiteLabelView;
+export default SiteLabelView;
+
+const style = StyleSheet.create({
+  textV1: {
+    height: 22,
+    fontSize: 12,
+    marginRight: 5,
+    marginBottom: 5,
+    borderRadius: 3,
+    ...$padding(0, 6)
+  },
+  textV2: {
+    height: 20,
+    fontSize: 8,
+    marginRight: 6,
+    borderRadius: 30,
+    ...$padding(0, 10)
+  }
+})

+ 2 - 2
Strides-APP/app/components/TextView.js

@@ -71,9 +71,9 @@ const getRadius = (style, fixedAlign) => {
         text.lineHeight = 18.2;
       }
     }
-    if (text.fontSize && app.isLumiWhitelabel) {
+    /*if (text.fontSize && app.isLumiWhitelabel) {
       text.fontSize -= 2;
-    }
+    }*/
     text.includeFontPadding = false;
   }
   return {

+ 4 - 1
Strides-APP/app/i18n/locales/en.js

@@ -75,8 +75,9 @@ export default {
     termsOfUse: "Terms of Use",
     topUp: "Purchase Credits",
     topUpWithCard: "Top Up with Card",
-    vouchers: "VOUCHERS",
+    vouchers: "Vouchers",
     voucherDetails: "Voucher Details",
+    selectVoucher: "Select Voucher",
     pointsHistory: "Points History",
     wallet: "Transactions",
     applyMember: "Apply Membership",
@@ -293,6 +294,7 @@ export default {
     labelRate: "Rate",
     labelRates: "Rates",
     labelStatus: "Status",
+    labelTags: "Tags",
     labelTimeElapsed: "Time Elapsed",
     labelTotalCharges: "Total Charges",
     labelTotalkWh: "Total kWh Delivered",
@@ -342,6 +344,7 @@ export default {
     tabInfo: "INFO",
     tabReserve: "Reserve",
     tipsDiscount: "You have an applicable membership.\nRate shown is the discounted price.",
+    tipsDiscountTitle: "Applicable Discount",
     tipsDisconnectConnector: "Please disconnect and return connector to charging station",
     tipsRatesTax: "All rates Include 8% GST",
     tipsRatesTax2: "All Rates Include Tax",

+ 3 - 0
Strides-APP/app/i18n/locales/zh-TW.js

@@ -77,6 +77,7 @@ export default {
     topUpWithCard: "使用信用卡充值",
     vouchers: "優惠券",
     voucherDetails: "優惠券詳情",
+    selectVoucher: "選擇優惠券",
     pointsHistory: "積點記錄",
     wallet: "我的餘額",
     applyMember: "申請會員",
@@ -293,6 +294,7 @@ export default {
     labelRate: "費率",
     labelRates: "費率",
     labelStatus: "狀態",
+    labelTags: "標籤",
     labelTimeElapsed: "充電用時",
     labelTotalCharges: "總費用",
     labelTotalkWh: "總電量(kWh)",
@@ -342,6 +344,7 @@ export default {
     tabInfo: "訊息",
     tabReserve: "預訂",
     tipsDiscount: "您有適用折扣的會員資格,\n當前已顯示折扣價。",
+    tipsDiscountTitle: "適用折扣",
     tipsDisconnectConnector: "請斷開連結並將插頭放回充電樁",
     tipsRatesTax: "所有費率均包括消費稅",
     tipsRatesTax2: "所有費率均含稅",

+ 3 - 0
Strides-APP/app/i18n/locales/zh.js

@@ -77,6 +77,7 @@ export default {
     topUpWithCard: "使用信用卡充值",
     vouchers: "代金券",
     voucherDetails: "代金券信息",
+    selectVoucher: "选择代金券",
     pointsHistory: "积分记录",
     wallet: "我的余额",
     applyMember: "申请会员",
@@ -293,6 +294,7 @@ export default {
     labelRate: "费率",
     labelRates: "费率",
     labelStatus: "状态",
+    labelTags: "标签",
     labelTimeElapsed: "充电用时",
     labelTotalCharges: "总费用",
     labelTotalkWh: "总电量(kWh)",
@@ -342,6 +344,7 @@ export default {
     tabInfo: "信息",
     tabReserve: "预订",
     tipsDiscount: "您有适用折扣的会员资格,\n当前已显示折扣价。",
+    tipsDiscountTitle: "适用折扣",
     tipsDisconnectConnector: "请断开连接并将插头放回充电桩",
     tipsRatesTax: "所有费率均包括消费税",
     tipsRatesTax2: "所有费率均含税",

+ 10 - 3
Strides-APP/app/pages/Router.js

@@ -19,6 +19,7 @@ import RegisterPublic from './sign/RegisterPublic';
 import RegisterDriver from './sign/RegisterDriver';
 import Home from './home/Index';
 import Search from './search/SearchV2';
+import SearchV3 from './search/SearchV3';
 import ChargeDetails from './charge/Details';
 import QRScan from './charge/QRScan';
 import Feedback from './my/Feedback';
@@ -70,6 +71,7 @@ import VoucherPage from './vouchers/VoucherPage';
 import VoucherSelect from './vouchers/VoucherSelect';
 import VoucherDetails from './vouchers/VoucherDetails';
 import PointsHistory from './vouchers/PointsHistory';
+import Transaction from './wallet/Transaction';
 
 export var PageList = {
   'splash': {
@@ -85,7 +87,7 @@ export var PageList = {
   'search': {
     title: 'Search',
     titleScope: 'route.search',
-    component: Search
+    component: app.isLumiWhitelabel ? SearchV3 : Search
   },
   'login': {
     component: Login
@@ -180,6 +182,11 @@ export var PageList = {
     titleScope: 'route.history',
     component: HistoryList
   },
+  'transaction': {
+    title: 'Transactions',
+    titleScope: 'route.wallet',
+    component: Transaction
+  },
   'editProfile': {
     title: 'My Profile',
     titleScope: 'route.editProfile',
@@ -374,8 +381,8 @@ export var PageList = {
     component: VoucherDetails
   },
   'selectVoucher': {
-    title: "Vouchers",
-    titleScope: 'route.vouchers',
+    title: "Select Voucher",
+    titleScope: 'route.selectVoucher',
     component: VoucherSelect
   },
   'pointsHistory': {

+ 5 - 4
Strides-APP/app/pages/chargeV2/ChargeAdapter.js

@@ -5,9 +5,10 @@
 import React, { Component } from 'react';
 import {createMaterialTopTabNavigator} from '@react-navigation/material-top-tabs';
 import { BackHandler, Pressable, StyleSheet } from 'react-native';
-import Charge from './TabCharge';
-import Reserve from './TabReserve';
 import TabInfos from './TabInfos';
+import TabCharge from './TabCharge';
+import Reserve from './TabReserve';
+import TabInfosV3 from '../chargeV3/TabInfos';
 import { QRResult } from '../charge/QRScan';
 import apiStation from '../../api/apiStation';
 import PagerUtil from './PagerUtil';
@@ -37,11 +38,11 @@ export default class ChargeAdapter extends Component {
     this.pageAdapter = [{
       title: $t('charging.tabInfo'),
       name: "Info",
-      component: TabInfos
+      component: app.isLumiWhitelabel ? TabInfosV3 : TabInfos
     }, {
       title: $t('charging.tabCharge'),
       name: "Charge",
-      component: Charge
+      component: TabCharge
     }, {
       title: $t('charging.tabReserve'),
       name: "Reserve",

+ 15 - 7
Strides-APP/app/pages/chargeV2/TabCharge.js

@@ -17,6 +17,7 @@ import utils from '../../utils/utils';
 import app from '../../../app.json';
 import { PaymentDefault } from '../payment/PaymentConfig';
 import { MyRefreshProps } from '../../components/ThemesConfig';
+import ChargingStartView from '../chargeV3/ChargingStartView';
 
 export default class TabCharge extends Component {
   constructor(props) {
@@ -634,13 +635,20 @@ export default class TabCharge extends Component {
             onStartCharge={() => this.startCharge()}
             onStopCharge={() => this.onStopCharge()}
             onPaymentMethodChanged={(type) => this.onPaymentMethodChanged(type)}/>
-        : <StepStartView
-            isPrivate={this.state.isPrivate}
-            canIntoCharging={this.state.canIntoCharging}
-            stationInfo={this.state.stationInfo}
-            onEnterStation={() => this.onEnterStation()}
-            onIntoCharging={() => this.toChargingPage()}
-          />
+        : ( app.isLumiWhitelabel
+          ? <ChargingStartView
+              isPrivate={this.state.isPrivate}
+              canIntoCharging={this.state.canIntoCharging}
+              stationInfo={this.state.stationInfo}
+              onEnterStation={() => this.onEnterStation()}
+              onIntoCharging={() => this.toChargingPage()}/>
+          : <StepStartView
+              isPrivate={this.state.isPrivate}
+              canIntoCharging={this.state.canIntoCharging}
+              stationInfo={this.state.stationInfo}
+              onEnterStation={() => this.onEnterStation()}
+              onIntoCharging={() => this.toChargingPage()}/>
+          ) 
         }
         <ErrorDialog
           visible={this.state.showErrorDialog}

+ 224 - 0
Strides-APP/app/pages/chargeV3/ChargingStartView.js

@@ -0,0 +1,224 @@
+/**
+ * V4充电页:扫码认证之后-充电开始之前
+ * @邠心vbe on 2024/05/15
+ */
+import React from 'react';
+import { StyleSheet, View } from 'react-native';
+import Button from '../../components/Button';
+import { ChargeStyle } from '../chargeV2/Charging';
+import { PageList } from '../Router';
+import TextView from '../../components/TextView';
+import utils from '../../utils/utils';
+
+export default ChargingStartView = ({
+  isPrivate,
+  stationInfo={},
+  onEnterStation,
+  canIntoCharging=false,
+  onIntoCharging
+}) => (
+  <View>
+    <View style={{minHeight: $vht(80)}}>
+      <EndView/>
+      <View style={ui.flexcc}>
+        <MaterialIcons
+          name="info-outline"
+          size={20}
+          color="#ED3F3F"/>
+        <TextView style={styles.gstText}>{$t('charging.tipsRatesTax2')}</TextView>
+      </View>
+      <View style={ui.flexc}>
+        <MaterialCommunityIcons
+          name="ev-plug-type2"
+          size={24}
+          color={textPrimary}
+        />
+        <TextView style={styles.title}>{$t('charging.acChargers')}</TextView>
+      </View>
+      { stationInfo.acRates?.length > 0
+        ? stationInfo.acRates.map((item, index) => {
+            return (
+              <View key={index} style={styles.connectorInfoItem}>
+                <View style={styles.infoGroup}>
+                  <TextView style={styles.infoText}>{item.typePower}</TextView>
+                </View>
+                <View style={styles.infoGroup}>
+                  <TextView style={styles.infoText}>{item.rates}</TextView>
+                </View>
+                <View style={styles.infoGroup}>
+                  <MaterialCommunityIcons
+                    name="circle"
+                    size={10}
+                    color={colorAccent}/>
+                  <TextView style={[styles.infoText, {paddingLeft: 4}]}>{item.connectorCount?.available + "/" + item.connectorCount?.all}</TextView>
+                </View>
+              </View>
+            );
+          })
+        : <TextView style={ui.noData}>{$t('charging.noRates')}</TextView>
+      }
+      <View style={ui.flexc}>
+        <MaterialCommunityIcons
+          name="ev-plug-ccs2"
+          size={24}
+          color={textPrimary}
+        />
+        <TextView style={styles.title}>{$t('charging.dcChargers')}</TextView>
+      </View>
+      { stationInfo.dcRates?.length > 0
+        ? stationInfo.dcRates.map((item, index) => {
+            return (
+              <View key={index} style={styles.connectorInfoItem}>
+                <View style={styles.infoGroup}>
+                  <TextView style={styles.infoText}>{item.typePower}</TextView>
+                </View>
+                <View style={styles.infoGroup}>
+                  <TextView style={styles.infoText}>{item.rates}</TextView>
+                </View>
+                <View style={styles.infoGroup}>
+                  <MaterialCommunityIcons
+                    name="circle"
+                    size={10}
+                    color={colorAccent}/>
+                  <TextView style={[styles.infoText, {paddingLeft: 4}]}>{item.connectorCount?.available + "/" + item.connectorCount?.all}</TextView>
+                </View>
+              </View>
+            );
+          })
+        : <TextView style={ui.noData}>{$t('charging.noRates')}</TextView>
+      }
+      { isPrivate &&
+        <View style={styles.privateView}> 
+          <TextView style={styles.privateText}>{$t('charging.ratesPrivateNote')}</TextView>
+        </View>
+      }
+      { utils.isNotEmpty(stationInfo.idleFee) && <>
+        <View style={ui.flexc}>
+          <MaterialCommunityIcons
+            name="timeline-clock"
+            size={24}
+            color={"#ED3F3F"}
+          />
+          <TextView style={styles.title}>Idle Fee</TextView>
+        </View>
+        <TextView style={styles.discountText}>{"Grace Period of " + stationInfo.idleFee.gracePeriod + " Minutes. Rates are " + stationInfo.idleFee.idleFee + " per " + stationInfo.idleFee.everyMinute + ". Idle Fee is capped at " + stationInfo.idleFee.crapFee}</TextView>
+      </>}
+      { !!(stationInfo.hasDiscount) && <>
+        <View style={ui.flexc}>
+          <MaterialCommunityIcons
+            name="brightness-percent"
+            size={24}
+            color={colorAccent}
+          />
+          <TextView style={styles.title}>{$t('charging.tipsDiscountTitle')}</TextView>
+        </View>
+        <TextView style={styles.discountText}>{$t('charging.tipsDiscount')}</TextView>
+      </>}
+    </View>
+    {/* <Payment refreshId={refreshId}/> */}
+    { canIntoCharging
+    ? <Button
+        style={styles.buttonView}
+        borderRadius={4}
+        text={$t('charging.btnIntoCharging')}
+        //disabled={available}
+        onClick={onIntoCharging}/>
+    : <View style={styles.buttonGroup}>
+        <Button
+          style={styles.buttonLeft}
+          text={$t('charging.enterStationId')}
+          //disabled={available}
+          onClick={onEnterStation}/>
+        <Button
+          style={styles.buttonRight}
+          text={$t('charging.scanQR')}
+          //disabled={available}
+          onClick={() => {
+            PagerUtil.onInnerScanQR();
+            startPage(PageList.scanqr, {actionDetail: false, id: stationInfo?.id});
+          }}/>
+      </View>
+    }
+  </View>
+);
+
+const styles = StyleSheet.create({
+  gstText: {
+    color: '#ED3F3F',
+    fontSize: 14,
+    lineHeight: 20,
+    fontWeight: 'bold',
+    textAlign: 'center',
+    paddingLeft: 4
+  },
+  title: {
+    color: '#000',
+    fontSize: 14,
+    fontWeight: 'bold',
+    paddingTop: 16,
+    paddingLeft: 8,
+    paddingBottom: 16
+  },
+  connectorInfoItem: {
+    ...$padding(8, 16),
+    alignItems: 'center',
+    flexDirection: 'row'
+  },
+  infoGroup: {
+    flex: 1,
+    alignItems: 'center',
+    justifyContent: 'center',
+    flexDirection: 'row'
+  },
+  infoText: {
+    color: textPrimary,
+    fontSize: 12
+  },
+  updateTip: {
+    color: '#aaa',
+    fontSize: 10,
+    textAlign: 'center',
+    paddingTop: 32,
+    paddingBottom: 16
+  },
+  privateView: {
+    height: $vht(25),
+    alignItems: 'center',
+    justifyContent: 'center'
+  },
+  privateText: {
+    color: '#FA5759'
+  },
+  buttonGroup: {
+    marginTop: 16,
+    marginBottom: 16,
+    alignItems: 'center',
+    flexDirection: 'row'
+  },
+  buttonView: {
+    marginTop: 16,
+    marginBottom: 32
+  },
+  buttonLeft: {
+    flex: 1,
+    elevation: 1.5,
+    borderRadius: 4,
+    backgroundColor: colorPrimary
+  },
+  buttonRight: {
+    flex: 1,
+    marginLeft: 16,
+    elevation: 1.5,
+    borderRadius: 4
+  },
+  discountView: {
+    borderWidth: 1,
+    borderColor: colorAccent
+  },
+  discountText: {
+    fontSize: 12,
+    textAlign: 'left',
+    color: textPrimary,
+    ...$padding(2, 10)
+  }
+})

+ 189 - 0
Strides-APP/app/pages/chargeV3/TabInfos.js

@@ -0,0 +1,189 @@
+/**
+ * V3版充电站信息页面
+ * @邠心vbe on 2024/05/13
+ */
+import React, { Component } from 'react';
+import { View, StyleSheet, ScrollView, RefreshControl, Text } from 'react-native';
+import TextView from '../../components/TextView';
+import PagerUtil from '../chargeV2/PagerUtil';
+import { MyRefreshProps } from '../../components/ThemesConfig';
+import utils from '../../utils/utils';
+import SiteLabelView from '../../components/SiteLabelView';
+import { ConnectTypeV2 } from '../search/ConnectType';
+import { Pressable } from 'react-native';
+
+export default class TabInfosV3 extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      refreshing: false,
+      stationInfo: {}
+    };
+  }
+
+  componentDidMount() {
+    PagerUtil.addOnRefresh(this);
+    this.onRefresh();
+  }
+
+  onRefresh() {
+    console.log("info刷新", this.props.route.name);
+    this.setState({
+      refreshing: false,
+      stationInfo: PagerUtil.getStationInfo()
+    });
+  }
+
+  onPullRefresh() {
+    this.setState({
+      refreshing: true
+    })
+    PagerUtil.setBackRefreshing();
+  }
+
+  getOperatingHours() {
+    if (this.state.stationInfo?.endlessService) {
+      return "24/7";
+    } else if (this.state.stationInfo?.operatingHours) {
+      return this.state.stationInfo?.operatingHours;
+    } else {
+      return $t('charging.toBeUpdated');
+    }
+  }
+
+  getParkingFee() {
+    if (this.state.stationInfo?.parkingFeeFree) {
+      return $t('charging.free');
+    } else if (this.state.stationInfo?.parkingFee) {
+      return this.state.stationInfo.parkingFee;
+    } else {
+      return $t('charging.toBeUpdated');
+    }
+  }
+  
+  render() {
+    return (
+      <ScrollView
+        style={styles.container}
+        keyboardShouldPersistTaps={isIOS ? 'never' : 'handled'}
+        contentContainerStyle={$padding(0, 16)}
+        refreshControl={
+          <RefreshControl
+            {...MyRefreshProps()}
+            refreshing={this.state.refreshing}
+            onRefresh={() => this.onPullRefresh()}
+          />
+        }>
+        <View style={styles.infoView}>
+          <TextView style={styles.siteName}>{this.state.stationInfo?.name}</TextView>
+          <View style={ui.flexc}>
+            <TextView style={styles.siteTypes}>{this.state.stationInfo.siteType}</TextView>
+            { (this.state.stationInfo.allConnector && this.state.stationInfo.allConnector.available > 0) &&
+              <TextView style={styles.stationAvailable}>
+                <MaterialCommunityIcons
+                  name="circle"
+                  size={10}
+                  color={colorAccent}/>
+                <Text>  </Text>
+                {$t("charging.statusAvailable")}
+              </TextView>
+            }
+            <ConnectTypeV2 {...this.state.stationInfo?.acConnector}/>
+            <ConnectTypeV2 {...this.state.stationInfo?.dcConnector}/>
+          </View>
+        </View>
+        <TextView style={styles.title}>{$t('charging.siteAddress')}</TextView>
+        <View style={[styles.infoView, ui.flexc]}>
+          <TextView style={[styles.infoText, ui.flex1]}>{this.state.stationInfo?.address}</TextView>
+          <Pressable
+            style={ui.center}
+            android_ripple={rippleLess}
+            onPress={() => {
+              utils.directMaps(this.state.stationInfo.latitude, this.state.stationInfo.longitude, this.state.stationInfo.address);
+            }}>
+            <MaterialCommunityIcons
+              name="directions"
+              color={textPrimary}
+              size={24}/>
+          </Pressable>
+        </View>
+        <TextView style={styles.title}>{$t('charging.operatingHours')}</TextView>
+        <View style={styles.infoView}>
+          <TextView style={styles.infoText}>{this.getOperatingHours()}</TextView>
+        </View>
+        <TextView style={styles.title}>{$t('charging.parkingFees')}</TextView>
+        <View style={styles.infoView}>
+          <TextView style={styles.infoText}>{this.getParkingFee()}</TextView>
+        </View>
+        { utils.isNotEmpty(this.state.stationInfo?.additionalNotes) && <>
+          <TextView style={styles.title}>{$t('charging.additionalInfo')}</TextView>
+          <View style={styles.infoView}>
+            <TextView style={styles.infoText}>{this.state.stationInfo?.additionalNotes}</TextView>
+          </View>
+        </>}
+        { utils.isNotEmpty(this.state.stationInfo?.labels) && <>
+          <TextView style={styles.title}>{$t('charging.labelTags')}</TextView>
+          <View style={styles.labelRows}>
+            <TextView style={styles.infoText}>
+              {this.state.stationInfo?.labels.map((label, idx) =>
+                (idx != 0 ? ", " : "") + label.label
+              )}
+            </TextView>
+          </View>
+        </>}
+      </ScrollView>
+    );
+  }
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1
+  },
+  siteName: {
+    color: textPrimary,
+    fontSize: 24,
+    fontWeight: 'bold',
+    paddingBottom: 4
+  },
+  title: {
+    color: '#000',
+    fontSize: 16,
+    fontWeight: 'bold',
+    paddingTop: 8,
+    //borderBottomColor: '#eee',
+    //borderBottomWidth: 1
+  },
+  infoView: {
+    paddingTop: 4,
+    paddingBottom: 8
+  },
+  infoText: {
+    color: '#444',
+    fontSize: 14
+  },
+  siteTypes: {
+    color: textLight,
+    height: 20,
+    fontSize: 10,
+    marginRight: 6,
+    borderRadius: 30,
+    ...$padding(0, 10),
+    backgroundColor: textPrimary
+  },
+  stationAvailable: {
+    color: textPrimary,
+    height: 20,
+    fontSize: 10,
+    marginRight: 6,
+    borderRadius: 30,
+    borderWidth: 1,
+    borderColor: colorAccent,
+    ...$padding(0, 8, 0, 4)
+  },
+  labelRows: {
+    flexWrap: 'wrap',
+    alignItems: 'center',
+    flexDirection: 'row'
+  }
+})

+ 1 - 1
Strides-APP/app/pages/home/DrawerV2.js

@@ -151,7 +151,7 @@ export default DrawerV2 = ({isLogin=false, userInfo, onLogout, sideCountInfo={},
           style={styles.itemButton}
           viewStyle={styles.itemView}
           onClick={() => {
-            startPage(PageList.wallet);
+            startPage(app.isLumiWhitelabel ? PageList.transaction : PageList.wallet);
           }}>
           <MaterialCommunityIcons
             style={styles.icon}

+ 32 - 15
Strides-APP/app/pages/home/Home.js

@@ -14,6 +14,7 @@ import { SettingUtil } from '../Settings';
 import MyStatusBar from '../../components/MyStatusBar';
 import LocationPermission from './maps/LocationPermission';
 import MapUI from './MapUI';
+import MapUILumi from './MapUILumi';
 
 export default class HomePage extends Component {
   constructor(props) {
@@ -52,9 +53,11 @@ export default class HomePage extends Component {
       this.locationListener.removeListener();
       this.locationListener = undefined;
     }
-    this.locationListener = new LocationPermission.LocationListener();
-    if (this.locationListener) {
-      this.locationListener.addListener();
+    if (!isIOS) {
+      this.locationListener = new LocationPermission.LocationListener();
+      if (this.locationListener) {
+        this.locationListener.addListener();
+      }
     }
     navigation.addListener('focus', () => {
       //toastShort('onResume')
@@ -303,6 +306,7 @@ export default class HomePage extends Component {
 
   findFilter(data) {
     this.filter = data;
+    console.log("筛选站点", data);
     this.getStationList();
   }
 
@@ -464,18 +468,31 @@ export default class HomePage extends Component {
   render() {
     return (
       <View style={ui.flex1}>
-        <MapUI
-          state={this.state}
-          navigation={this.props.navigation}
-          onFilter={data => this.findFilter(data)}
-          onMapReady={() => this.onMapReady()}
-          onFavorite={() => this.favoriteSite()}
-          onCloseInfo={() => this.onCloseInfo()}
-          onLocation={() => this.checkPermission2Geo(true)}
-          useApplesMap={this.settingInfo.useApplesMap}
-          showUserLocation={this.state.hasPermission && this.settingInfo.alwaysLocation}
-          viewChargeStation={id => this.viewChargeStation(id)}
-        />
+        { app.isLumiWhitelabel
+        ? <MapUILumi
+            state={this.state}
+            navigation={this.props.navigation}
+            onFilter={data => this.findFilter(data)}
+            onMapReady={() => this.onMapReady()}
+            onFavorite={() => this.favoriteSite()}
+            onCloseInfo={() => this.onCloseInfo()}
+            onLocation={() => this.checkPermission2Geo(true)}
+            useApplesMap={this.settingInfo.useApplesMap}
+            showUserLocation={this.state.hasPermission && this.settingInfo.alwaysLocation}
+            viewChargeStation={id => this.viewChargeStation(id)}/>
+        : <MapUI
+            state={this.state}
+            navigation={this.props.navigation}
+            onFilter={data => this.findFilter(data)}
+            onMapReady={() => this.onMapReady()}
+            onFavorite={() => this.favoriteSite()}
+            onCloseInfo={() => this.onCloseInfo()}
+            onLocation={() => this.checkPermission2Geo(true)}
+            useApplesMap={this.settingInfo.useApplesMap}
+            showUserLocation={this.state.hasPermission && this.settingInfo.alwaysLocation}
+            viewChargeStation={id => this.viewChargeStation(id)}
+          />
+        }
         <LocationPermission.VIEW
           visible={this.state.permissionDenied}
           onView={() => this.hidePermissionPanel()}/>

+ 104 - 0
Strides-APP/app/pages/home/MapUILumi.js

@@ -0,0 +1,104 @@
+/**
+ * 首页地图LUMI定制布局
+ * @邠心vbe on 2024/05/09
+ */
+import React from 'react';
+import { View, Pressable, Image, StyleSheet } from 'react-native';
+import Maps from './maps/Maps';
+import MapTool from './maps/MapTool';
+import SearchTool from './maps/SearchTool';
+import BottomSiteInfo from './maps/BottomSiteInfo';
+import FilterTop from './maps/FilterTop';
+import { BottomSiteCard } from './maps/BottomSiteCard';
+
+export default MapUILumi = ({
+  state,
+  navigation,
+  onFilter,
+  onMapReady,
+  onFavorite,
+  onLocation,
+  onCloseInfo,
+  useApplesMap,
+  showUserLocation,
+  viewChargeStation
+}) => {
+  return (
+    <View style={styles.mapContent}>
+      {/* this.state.hasPermission &&*/}
+      <Maps.Maps3
+        region={state.region}
+        stopList={state.stopList}
+        onMapReady={onMapReady}
+        useApplesMap={useApplesMap}
+        onMarkerPress={viewChargeStation}
+        showUserLocation={showUserLocation}
+      />
+      <SearchTool
+        count={state.stopList?.length}
+        mapReady={state.mapReady}
+        onFilter={onFilter}
+        onLocation={onLocation}
+        navigation={navigation}
+      />
+      <FilterTop
+        onFilter={onFilter}
+        onLocation={onLocation}
+      />
+      {/* <MapTool
+        count={state.stopList?.length}
+        mapReady={state.mapReady}
+        onFilter={onFilter}
+        onLocation={onLocation}
+      /> */}
+      <BottomSiteCard
+        stationInfo={state.stationInfo}
+        onFavorite={onFavorite}
+        onClose={onCloseInfo}
+      />
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  logoView: {
+    top: 0,
+    left: 0,
+    right: 0,
+    bottom: 0,
+    zIndex: 1,
+    alignItems: 'center',
+    position: 'absolute',
+    justifyContent: 'center',
+    paddingTop: isIOS ? statusHeight : 0
+  },
+  searchView: {
+    ...$padding(8, 16, 16),
+    backgroundColor: colorThemes
+  },
+  searchInput: {
+    alignItems: 'center',
+    borderWidth: 1,
+    borderStyle: 'solid',
+    borderRadius: 60,
+    borderColor: colorAccent,
+    flexDirection: 'row',
+    paddingLeft: 16,
+    paddingRight: 16,
+    backgroundColor: 'rgba(255, 255, 255, 0.5)'
+  },
+  searchText: {
+    flex: 1,
+    color: '#444',
+    padding: 8,
+    fontSize: 15
+  },
+  mapContent: {
+    top: 0,
+    left: 0,
+    right: 0,
+    bottom: 0,
+    zIndex: 2,
+    position: 'absolute',
+  }
+})

+ 313 - 0
Strides-APP/app/pages/home/maps/BottomSiteCard.js

@@ -0,0 +1,313 @@
+/**
+ * 地图底部充电站信息组件
+ * @邠心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';
+
+export const BottomSiteCard = ({
+  stationInfo = {},
+  onFavorite,
+  onClose
+}) => {
+  const [visible, showDialog] = useState(false);
+
+  useEffect(() => {
+    if (stationInfo.id) {
+      showDialog(true)
+    } else {
+      showDialog(false)
+    }
+  }, [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';
+    }
+  }
+
+  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});
+    }
+  }
+
+  return (
+    <BottomModal
+      visible={visible}
+      onHide={() => {
+        onClose()
+      }}>
+      <View style={styles.stationBarView}>
+        <TextView
+          ellipsizeMode='tail'
+          numberOfLines={1}
+          style={styles.stationTitle}>{stationInfo.name}</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>
+        <TextView
+          style={styles.stationAddress}
+          ellipsizeMode='tail'
+          numberOfLines={3}>{stationInfo.address}</TextView>
+        <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={18}
+                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={18}
+                  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={17}
+                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({
+  stationBarView: {
+    padding: 16
+  },
+  stationBarPresed: {
+    backgroundColor: "#F6F6F6",
+    ...ElevationObject(5)
+  },
+  stationInfo: {
+    flex: 1,
+    height: 45,
+    paddingLeft: 4,
+    paddingRight: 8,
+    justifyContent: 'space-around'
+  },
+  stationTitle: {
+    color: textPrimary,
+    fontSize: 24,
+    fontWeight: 'bold',
+    paddingBottom: 4
+  },
+  stationAddress: {
+    color: textSecondary,
+    fontSize: 12,
+    paddingTop: 4,
+    paddingBottom: 6
+  },
+  siteTypes: {
+    color: textLight,
+    height: 20,
+    fontSize: 8,
+    marginRight: 6,
+    borderRadius: 30,
+    ...$padding(0, 10),
+    backgroundColor: textPrimary
+  },
+  stationAvailable: {
+    color: textPrimary,
+    height: 20,
+    fontSize: 8,
+    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,
+    marginRight: 10,
+    borderWidth: 1,
+    borderRadius: 30,
+    borderColor: textPrimary,
+    alignItems: 'center',
+    flexDirection: 'row',
+    justifyContent: 'center',
+    ...$padding(2, 8, 2, 4)
+  },
+  bookmarked: {
+    color: textLight,
+    borderColor: colorAccent,
+    backgroundColor: colorAccent
+  },
+  directText: {
+    color: textPrimary,
+    fontSize: 12,
+    paddingLeft: 4
+  },
+  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
+  }
+})

+ 190 - 0
Strides-APP/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: 50,
+    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
+  }
+})

+ 44 - 0
Strides-APP/app/pages/search/ConnectType.js

@@ -15,8 +15,22 @@ export default ConnectType = ({type, available, all, color=textPrimary}) => {
   }
 }
 
+export const ConnectTypeV2 = ({type, available, all, color=textPrimary}) => {
+  if (type) {
+    return (
+      <View style={[styles.connectTypeV2, {borderColor: color}]}>
+        <TextView style={[styles.typeLabelV2, {color: color}]}>{type}</TextView>
+        <TextView style={[styles.typeContentV2]}><Text style={styles.typeBoldV2}>{available}</Text>/{all}</TextView>
+      </View>
+    );
+  } else {
+    return <></>;
+  }
+}
+
 const styles = StyleSheet.create({
   connectType: {
+    overflow: 'hidden',
     borderWidth: 1,
     borderColor: textPrimary,
     borderRadius: 3,
@@ -24,6 +38,16 @@ const styles = StyleSheet.create({
     alignItems: 'center',
     flexDirection: 'row',
   },
+  connectTypeV2: {
+    height: 20,
+    overflow: 'hidden',
+    borderWidth: 1,
+    borderColor: textPrimary,
+    borderRadius: 30,
+    marginRight: 6,
+    alignItems: 'center',
+    flexDirection: 'row'
+  },
   typeLabel: {
     color: textLight,
     fontSize: 12,
@@ -33,15 +57,35 @@ const styles = StyleSheet.create({
     paddingBottom: 2,
     backgroundColor: textPrimary
   },
+  typeLabelV2: {
+    color: textPrimary,
+    fontSize: 8,
+    paddingLeft: 8,
+    paddingRight: 4
+  },
   typeContent: {
     color: textSecondary,
     fontSize: 12,
     paddingLeft: 10,
     paddingRight: 6,
   },
+  typeContentV2: {
+    color: textLight,
+    height: 20,
+    fontSize: 8,
+    paddingLeft: 10,
+    paddingRight: 8,
+    borderRadius: 30,
+    backgroundColor: textPrimary
+  },
   typeBold: {
     color: textPrimary,
     fontSize: 14,
     fontWeight: 'bold'
+  },
+  typeBoldV2: {
+    color: textLight,
+    fontSize: 8,
+    fontWeight: 'bold'
   }
 })

+ 252 - 0
Strides-APP/app/pages/search/ListViewV3.js

@@ -0,0 +1,252 @@
+/**
+ * V3版搜索列表复用组件
+ * @邠心vbe on 2024/05/11
+ */
+import React from 'react';
+import { Pressable, StyleSheet, View, Text } from 'react-native';
+import TextView from '../../components/TextView';
+import utils from '../../utils/utils';
+import ConnectType, { ConnectTypeV2 } from './ConnectType';
+import app from '../../../app.json';
+import Button from '../../components/Button';
+import SiteLabelView from '../../components/SiteLabelView';
+
+export default ListViewV3 = ({item, index, separators, onPress, onFavorite}) => {
+  if (item.id) {
+    return (
+      <Button
+        style={styles.itemView}
+        viewStyle={styles.itemContent}
+        key={index}
+        onClick={onPress}
+        android_ripple={ripple}>
+        <View style={styles.stationInfo} >
+          <TextView style={styles.stationName}>{item.name}</TextView>
+          <View style={ui.flex}>
+            <View style={ui.flex1}>
+              <TextView style={styles.stationAddress}>{item.address}</TextView>
+              <View style={ui.flexc}>
+                { (item.allConnector && item.allConnector.available > 0) &&
+                  <TextView style={styles.stationAvailable}>
+                    <MaterialCommunityIcons
+                      name="circle"
+                      size={10}
+                      color={colorAccent}/>
+                    <Text>  </Text>
+                    {$t("charging.statusAvailable")}
+                  </TextView>
+                }
+                <TextView style={styles.distance}>{item.distance}</TextView>
+                <ConnectTypeV2 {...item?.acConnector}/>
+                <ConnectTypeV2 {...item?.dcConnector}/>
+              </View>
+            </View>
+            { item.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: item.favorite ? colorPrimary : colorCancel}]}
+                  android_ripple={rippleLess}
+                  onPress={onFavorite}>
+                  <MaterialIcons
+                    name="star"
+                    size={22}
+                    color={colorLight}/>
+                </Pressable>
+              }
+              <Pressable
+                style={styles.directIconView}
+                android_ripple={rippleLess}
+                onPress={() => {
+                  utils.directMaps(item.latitude, item.longitude, item.address);
+                }}>
+                <MaterialCommunityIcons
+                  name="navigation-variant"
+                  size={22}
+                  color={colorLight}/>
+              </Pressable>
+            </> }
+          </View>
+          <View style={styles.labelRows}>
+            <TextView style={[styles.siteTypes, item.siteType != "Public" ? styles.private : {}]}>{item.siteType}</TextView>
+            {/* item.allConnector && item.allConnector.available > 0 &&
+              <TextView style={[styles.infoStatus, styles.available]}>{$t('charging.statusAvailable')}</TextView>
+            */}
+            { utils.isNotEmpty(item?.labels) &&
+              item?.labels.map((label, idx) =>
+                <SiteLabelView {...label} key={idx} version={2}/>
+              )
+            }
+          </View>
+        </View>
+      </Button>
+    );
+  } else {
+    return <></>;
+  }
+}
+
+const styles = StyleSheet.create({
+  itemView: {
+    borderRadius: 0,
+    flexDirection: 'row',
+    borderBottomWidth: 1,
+    borderBottomColor: '#eee',
+    backgroundColor: pageBackground
+  },
+  itemContent: {
+    flex: 1,
+    ...$padding(8, 16),
+    flexDirection: 'row'
+  },
+  stationInfo: {
+    flex: 1,
+    paddingLeft: 12
+  },
+  siteIconView: {
+    width: 38,
+    height: 38,
+    alignItems: 'center',
+    justifyContent: 'center',
+    borderWidth: 1,
+    borderColor: colorPrimary,
+    borderRadius: 38
+  },
+  nameView: {
+    paddingTop: 3,
+    alignItems: 'center',
+    flexDirection: 'row'
+  },
+  stationName: {
+    color: textPrimary,
+    fontSize: 16,
+    fontWeight: 'bold'
+  },
+  stationAddress: {
+    color: textSecondary,
+    fontSize: 12,
+    paddingTop: 4,
+    paddingBottom: 4
+  },
+  siteTypes: {
+    color: textLight,
+    height: 20,
+    fontSize: 8,
+    marginRight: 6,
+    borderRadius: 30,
+    ...$padding(0, 10),
+    backgroundColor: colorAccent
+  },
+  stationAvailable: {
+    color: textPrimary,
+    height: 20,
+    fontSize: 8,
+    marginRight: 6,
+    borderRadius: 30,
+    borderWidth: 1,
+    borderColor: colorAccent,
+    ...$padding(0, 8, 0, 4)
+  },
+  labelRows: {
+    paddingTop: 4,
+    flexWrap: 'wrap',
+    alignItems: 'center',
+    flexDirection: 'row'
+  },
+  infoStatus: {
+    height: 22,
+    fontSize: 12,
+    borderRadius: 3,
+    marginRight: 5,
+    marginBottom: 5,
+    borderWidth: 1,
+    ...$padding(0, 8)
+  },
+  selected: {
+    color: textPrimary,
+    borderColor: colorAccent
+  },
+  distance: {
+    fontSize: 8,
+    color: textLight,
+    height: 20,
+    fontSize: 10,
+    marginRight: 6,
+    borderRadius: 30,
+    ...$padding(0, 10),
+    backgroundColor: textPrimary
+  },
+  available: {
+    color: '#90DB0A',
+    borderColor: '#90DB0A'
+  },
+  unavailable: {
+    color: '#999',
+    fontSize: 10.5,
+    paddingTop: 7,
+    paddingLeft: 9,
+    paddingRight: 9,
+    paddingBottom: 7,
+    backgroundColor: '#CCC'
+  },
+  private: {
+    backgroundColor: '#FDB702'
+  },
+  connectView: {
+    paddingTop: 4,
+    paddingBottom: 8,
+    alignItems: 'center',
+    flexDirection: 'row'
+  },
+  connectType: {
+    borderWidth: 1,
+    borderColor: textPrimary,
+    borderRadius: 3,
+    marginRight: 16,
+    alignItems: 'center',
+    flexDirection: 'row',
+  },
+  directView: {
+    zIndex: 1,
+    width: 32,
+    height: 32,
+    marginTop: 4,
+    marginLeft: 8,
+    alignItems: 'center'
+  },
+  distanceText: {
+    color: textPrimary,
+    fontSize: 12,
+    paddingTop: 2
+  },
+  directIconView: {
+    zIndex: 1,
+    width: 32,
+    height: 32,
+    marginTop: 4,
+    marginLeft: 16,
+    borderRadius: 45,
+    alignItems: 'center',
+    justifyContent: 'center',
+    backgroundColor: colorAccent
+  },
+  upcomingIcon: {
+    marginLeft: 8,
+    opacity: .3
+  },
+  upcomingText: {
+    color: colorAccent,
+    fontSize: 10,
+    opacity: .3,
+    marginLeft: 8,
+    marginTop: -3
+  }
+})

+ 1 - 1
Strides-APP/app/pages/search/SearchV2.js

@@ -10,7 +10,7 @@ import utils from '../../utils/utils';
 import { PageList } from '../Router';
 import ListViewV2 from './ListViewV2';
 
-export default class Search extends Component {
+export default class SearchV2 extends Component {
   constructor(props) {
     super(props);
     this.state = {

+ 290 - 0
Strides-APP/app/pages/search/SearchV3.js

@@ -0,0 +1,290 @@
+/**
+ * V3版搜索页
+ * @邠心vbe on 2024/05/11
+ */
+import React, { Component } from 'react';
+import { View, Text, StyleSheet, TextInput, FlatList, Image } from 'react-native';
+import apiStation from '../../api/apiStation';
+import Dialog from '../../components/Dialog';
+import utils from '../../utils/utils';
+import { PageList } from '../Router';
+import ListViewV3 from './ListViewV3';
+
+export default class SearchV3 extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      isSearch: false,
+      searchResult: [{id:0}],
+      searchWorld: "",
+      latlng: {
+        lat: 0.00001,
+        lng: 0.00001
+      }
+    };
+  }
+
+  componentDidMount() {
+    this.getGeoLocation();
+  }
+
+  changeWord(word) {
+    this.setState({
+      searchWorld: word
+    });
+  }
+
+  getGeoLocation() {
+    navigator.geolocation.getCurrentPosition(location => {
+      let latlng = {
+        lat: location.coords.latitude,
+        lng: location.coords.longitude
+      }
+      this.setState({
+        latlng: latlng
+      })
+      this.searchStation(latlng);
+    }, error => {
+      console.info("[Search] getGeoLocation", error);
+      this.searchStation(this.state.latlng);
+    });
+  }
+
+  searchStation(latlng, time=100) {
+    this.setState({
+      isSearch: true
+    });
+    latlng.siteName = this.state.searchWorld
+    apiStation.searchStation(latlng).then(res => {
+      if (res.data.sites) {
+        const list = [];
+        res.data.sites.forEach(item => {
+          list.push({
+            id: item.sitePk,
+            name: item.siteName,
+            address: item.siteAddress,
+            latitude: item.locationLatitude,
+            longitude: item.locationLongitude,
+            acConnector: item.acConnector,
+            allConnector: item.allConnector,
+            dcConnector: item.dcConnector,
+            siteType: item.siteType,
+            favorite: item.favorite,
+            upcoming: item.upcoming,
+            labels: item.siteLabels,
+            distance: utils.getDistance(item.distance),
+            serviceProvider: item.serviceProvider
+          });
+        });
+        setTimeout(() => {
+          this.setState({
+            isSearch: false,
+            searchResult: list
+          });
+        }, time);
+      }
+    }).catch(err => {
+      console.log('searchStation-err', err);
+      this.setState({
+        isSearch: false,
+        searchResult: []
+      });
+    });
+  }
+
+  favoriteSite(index, info) {
+    if (info?.id) {
+      Dialog.showProgressDialog();
+      apiStation.bookmarkSite(info.id).then(res => {
+        if (index >= 0) {
+          const list = [...this.state.searchResult];
+          list[index].favorite = !info.favorite;
+          this.setState({
+            searchResult: list
+          });
+        }
+      }).catch(err => {
+        toastShort(err);
+      }).finally(() => {
+        Dialog.dismissLoading();
+      })
+    }
+  }
+
+  intoStation(info) {
+    if (info.upcoming) {
+      toastShort($t("home.upcoming"))
+    } else {
+      utils.toChargeDetailPage(info.id, 'search', PageList.search);
+      //startPage(PageList.chargeDetailPage, {stationInfo: info, action: 'search', from: PageList.search});
+    }
+  }
+
+  listItem = (props) => {
+    return (
+      <ListViewV3 
+        {...props}
+        onPress={() => this.intoStation(props.item)}
+        onFavorite={() => this.favoriteSite(props.index, props.item)}/>
+    )
+  }
+
+  render() {
+    return (
+      <View style={styles.container}>
+        <View style={styles.searchView}>
+          <Feather
+            name={'search'}
+            size={20}
+            color={textCancel}/>
+          <TextInput
+            style={styles.searchInput}
+            autoFocus={true}
+            maxLength={50}
+            numberOfLines={1}
+            returnKeyType={'search'}
+            clearButtonMode={'while-editing'}
+            placeholder={$t('home.searchHint')}
+            placeholderTextColor={textPlacehoder}
+            value={this.state.searchWorld}
+            onChangeText={text => this.changeWord(text)}
+            onSubmitEditing={() => {
+              //this.getGeoLocation();
+              this.searchStation(this.state.latlng, 300);
+            }}/>
+          { utils.isNotEmpty(this.state.searchWorld) &&
+            <MaterialCommunityIcons
+              name="close-circle"
+              size={20}
+              color={textCancel}
+              onPress={() => this.changeWord("")}/>
+          }
+        </View>
+        { this.state.isSearch
+        ? <View style={styles.searchingView}>
+            <Image
+              style={styles.seachingIcon}
+              source={require('../../images/icon/search-loading.gif')}/>
+          </View>
+        : <FlatList
+            style={styles.listView}
+            data={this.state.searchResult}
+            renderItem={this.listItem}
+            keyExtractor={item => item.id}
+            keyboardShouldPersistTaps="always"
+            ListEmptyComponent={<Text style={styles.noResult}>{$t('home.noSearch')}</Text>}
+          />
+        }
+      </View>
+    );
+  }
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: pageBackground
+  },
+  searchView: {
+    margin: 16,
+    ...$padding(2, 16),
+    borderRadius: 60,
+    borderWidth: 1,
+    borderColor: '#E5E5E5',
+    alignItems: 'center',
+    flexDirection: 'row',
+    backgroundColor: '#F5F5F5'
+  },
+  searchInput: {
+    flex: 1,
+    color: textPrimary,
+    ...$padding(6, 8),
+    fontSize: 15,
+    marginLeft: 4,
+    lineHeight: 20
+  },
+  searchingView: {
+    padding: 16,
+    alignItems: 'center'
+  },
+  seachingIcon: {
+    width: 60,
+    height: 60
+  },
+  noResult: {
+    color: '#999',
+    fontSize: 14,
+    padding: 20,
+    textAlign: 'center',
+  },
+  listView: {
+    flex: 1
+  },
+  itemView: {
+    alignItems: 'center',
+    flexDirection: 'row',
+    borderBottomWidth: 1,
+    borderBottomColor: '#eee'
+  },
+  stationInfo: {
+    flex: 1,
+    padding: 16
+  },
+  nameView: {
+    paddingTop: 3,
+    alignItems: 'center',
+    flexDirection: 'row'
+  },
+  stationName: {
+    color: textPrimary,
+    fontSize: 18,
+    fontWeight: 'bold'
+  },
+  stationAddress: {
+    color: '#666',
+    fontSize: 14,
+    paddingBottom: 8
+  },
+  infoStatus: {
+    fontSize: 10,
+    paddingTop: 3,
+    paddingLeft: 8,
+    paddingRight: 8,
+    paddingBottom: 3,
+    borderRadius: 5,
+    marginLeft: 12,
+  },
+  selected: {
+    color: textPrimary,
+    backgroundColor: colorAccent
+  },
+  available: {
+    color: textLight,
+    backgroundColor: '#90DB0A'
+  },
+  unavailable: {
+    color: '#999',
+    fontSize: 10.5,
+    paddingTop: 7,
+    paddingLeft: 9,
+    paddingRight: 9,
+    paddingBottom: 7,
+    backgroundColor: '#CCC'
+  },
+  connectView: {
+    paddingTop: 4,
+    paddingBottom: 4,
+    alignItems: 'center',
+    flexDirection: 'row'
+  },
+  directView: {
+    zIndex: 1,
+    paddingRight: 16,
+    alignItems: 'center'
+  },
+  distanceText: {
+    color: textPrimary,
+    fontSize: 12,
+    paddingTop: 2
+  }
+});

+ 2 - 2
Strides-APP/app/pages/vouchers/ViewRedeem.js

@@ -171,13 +171,13 @@ const styles = StyleSheet.create({
   },
   buttonText: {
     color: textPrimary,
-    fontSize: 13,
+    fontSize: 12,
     paddingLeft: 8,
     paddingRight: 8
   },
   valueText: {
     color: colorAccent,
-    fontSize: 14
+    fontSize: 12
   },
   inputView: {
     color: textPrimary,

+ 1 - 1
Strides-APP/app/pages/vouchers/VoucherPage.js

@@ -37,7 +37,7 @@ export default class VoucherPage extends Component {
 
   backPage() {
     if (!this.isHide) {
-      startPage(PageList.profile);
+      startPage(PageList.home);
       return true;
     }
   }

+ 15 - 3
Strides-APP/app/pages/wallet/OverviewV2.js

@@ -179,7 +179,7 @@ export default class OverviewV2 extends Component {
         }
         <View style={styles.statisticView}>
           <View style={ui.flexcw}>
-            <TextView style={styles.sectionTitle}>{$t('wallet.forWeekOf')}</TextView>
+            <TextView style={this.props.isTabPage ? styles.sectionTitleV2 : styles.sectionTitle}>{$t('wallet.forWeekOf')}</TextView>
             {/* <Text style={styles.linkText}>1st Jan to 8th Jan </Text> */}
           </View>
           <View style={styles.overviewRow}>
@@ -204,7 +204,7 @@ export default class OverviewV2 extends Component {
         </View>
         <View style={ui.flex1}>
           <View style={styles.statisticView}>
-            <TextView style={styles.sectionTitle}>{$t('wallet.statistics4week')}</TextView>
+            <TextView style={this.props.isTabPage ? styles.sectionTitleV2 : styles.sectionTitle}>{$t('wallet.statistics4week')}</TextView>
             <TextView style={styles.statisticTitle}>{this.state.weekdayData[this.state.weekIndex]?.title}</TextView>
             <View style={styles.barChartView}>
               { this.state.weekdayData.map((item, index) => (
@@ -232,7 +232,7 @@ export default class OverviewV2 extends Component {
             </View>
           </View>
           <View style={styles.statisticView}>
-            <TextView style={styles.sectionTitle}>{$t('wallet.statistics4HalfYear')}</TextView>
+            <TextView style={this.props.isTabPage ? styles.sectionTitleV2 : styles.sectionTitle}>{$t('wallet.statistics4HalfYear')}</TextView>
             <TextView style={styles.statisticTitle}>{this.state.monthData[this.state.monthIndex]?.title}</TextView>
             <View style={styles.barChartView}>
               { this.state.monthData.map((item, index) => (
@@ -340,6 +340,18 @@ const styles = StyleSheet.create({
     paddingLeft: 16,
     fontWeight: 'bold'
   },
+  sectionTitleV2: {
+    flex: 1,
+    color: textPrimary,
+    fontSize: 14,
+    marginLeft: 16,
+    marginRight: 16,
+    paddingBottom: 4,
+    fontWeight: 'bold',
+    textTransform: 'uppercase',
+    borderBottomWidth: 1,
+    borderBottomColor: colorPrimary
+  },
   statisticTitle: {
     color: textPrimary,
     fontSize: 14,

+ 110 - 0
Strides-APP/app/pages/wallet/Transaction.js

@@ -0,0 +1,110 @@
+/**
+ * 历史交易页面
+ * @邠心vbe on 2024/05/16
+ */
+import React, { Component, useState } from 'react';
+import { View, Text, StyleSheet, RefreshControl } from 'react-native';
+import PointsHistory from '../vouchers/PointsHistory';
+import HistoryList from './HistoryList';
+import OverviewV2 from './OverviewV2';
+import app from '../../../app.json';
+import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
+import { ScrollView } from 'react-native';
+import { MyRefreshProps } from '../../components/ThemesConfig';
+
+const TabOverView = () => {
+  const [refresh, setRefresh] = useState(false)
+  return (
+    <ScrollView
+      style={styles.container}
+      contentContainerStyle={$padding(16,0)}
+      refreshControl={
+        <RefreshControl
+          {...MyRefreshProps()}
+          refreshing={refresh}
+          onRefresh={() => setRefresh(true)}
+        />
+      }>
+      <OverviewV2
+        atAglance={false}
+        skeleton={false}
+        refresh={refresh}
+        isTabPage={true}
+        refreshed={() => setRefresh(false)}
+        shown={true}/>
+    </ScrollView>
+  )
+}
+
+export default class Transaction extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      pageAdapter: [{
+        title: $t('wallet.tabOverview'),
+        name: "Overview",
+        component: TabOverView
+      },{
+        title: $t('wallet.tabHistory'),
+        name: "History",
+        component: HistoryList
+      },{
+        title: $t('points.points'),
+        name: "Points",
+        component: PointsHistory
+      }]
+    };
+    this.tabBarStyle = {
+      tabBarStyle: styles.tabStyle,
+      tabBarPressColor: rippleColor,
+      tabBarScrollEnabled: false,
+      tabBarIndicatorStyle: styles.indicator,
+      tabBarActiveTintColor: tabBarTextActive,
+      tabBarInactiveTintColor: tabBarTextInactive
+    }
+  }
+
+  backPage() {
+    startPage(PageList.home);
+    return true;
+  }
+
+  render() {
+    const Tab = createMaterialTopTabNavigator();
+    return (
+      <Tab.Navigator
+        style={styles.container}
+          screenOptions={{
+            lazy: false,
+            lazyPreloadDistance: 1,
+            ...this.tabBarStyle
+          }}
+          backBehavior={() => this.backPage()}>
+        { this.state.pageAdapter.map((item, index) => 
+          <Tab.Screen
+            key={index}
+            name={item.name}
+            component={item.component}
+            options={{
+              title: item.title,
+              tabBarAllowFontScaling: false
+            }}
+          />
+        )}
+      </Tab.Navigator>
+    );
+  }
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: colorLight
+  },
+  tabStyle: {
+    backgroundColor: app.isWhitelabel ? colorLight : colorPrimary
+  },
+  indicator: {
+    backgroundColor: app.isWhitelabel ? colorPrimary : colorLight
+  }
+})

+ 1 - 0
Strides-APP/app/utils/utils.js

@@ -68,6 +68,7 @@ export default {
         favorite: obj.favorite,
         acRates: acRates,
         dcRates: dcRates,
+        idleFee: obj.idleDetail,
         labels: obj.siteLabels,
         upcoming: obj.upcoming,
         rateList: obj.rates,

+ 2 - 1
Strides-APP/index.js

@@ -17,6 +17,7 @@ import {RootSiblingParent} from 'react-native-root-siblings';
 import { SafeAreaView } from 'react-native-safe-area-context';
 import MyStatusBar from './app/components/MyStatusBar';
 import { i18nUtil } from './app/i18n';
+import RouterV2 from './app/pages/RouterV2';
 
 class Index extends Component {
   constructor(props) {
@@ -40,7 +41,7 @@ class Index extends Component {
       <RootSiblingParent>
         <MyStatusBar/>
         { this.state.visible 
-          ? <Router/>
+          ? (app.isLumiWhitelabel ? <RouterV2/> : <Router/>)
           : <></>
         }
         <ModalPortal />