Selaa lähdekoodia

Develop voucher management on app
https://dev.wormwood.com.sg/zentao/task-view-79.html

vbea 2 vuotta sitten
vanhempi
sitoutus
12d5f5cf7b

+ 1 - 0
Strides-APP/app.json

@@ -12,6 +12,7 @@
     "apply_phv": false,
     "membership": true,
     "nationally": false,
+    "vouchers": false,
     "topup_payment_type": false,
     "support": {
       "phone": "+6564775355",

+ 45 - 0
Strides-APP/app/api/apiVoucher.js

@@ -0,0 +1,45 @@
+import { get, post } from "./http";
+
+const prefix = 'devicesApi/dawn/api/v1/';
+
+export default apiVoucher = {
+  /**
+   * 使用兑换码兑换优惠券
+   * @param {redemptionCode} data 
+   * @returns Promise
+   */
+  redeemVoucher(data) {
+    return post(prefix + "redeem-voucher-by-redemption-code", data)
+  },
+  /**
+   * 领券中心兑换优惠券
+   * @param {voucherId} data 
+   * @returns Promise
+   */
+  purchaseVoucher(data) {
+    return post(prefix + "redeem-voucher-by-voucher-id", data)
+  },
+  /**
+   * 分页查询我的优惠券列表
+   * @param {Object*} {lastVoucherId,voucherType} data 
+   * @returns Promise
+   */
+  getMyVouchers(data) {
+    return get(prefix + "user-vouchers", data)
+  },
+  /**
+   * 分页查询优惠券贩卖列表
+   * @param {Object*} {lastVoucherId,voucherType} data 
+   * @returns Promise
+   */
+  getDealVouchers(data) {
+    return get(prefix + "voucher-center", data)
+  },
+  /**
+   * 获取优惠券类型选项
+   * @returns Promise
+   */
+  getVoucherTypeOptions() {
+    return get(prefix + "voucher-type-select")
+  }
+}

+ 4 - 9
Strides-APP/app/components/Dialog.js

@@ -84,7 +84,7 @@ const IOSDialog = (props) => {
   return (
     <View style={iosStyle.modalDialog}>
       { props.title != '' &&
-        <TextView style={iosStyle.modalTitle}>{props.title}</TextView>
+        <TextView style={iosStyle.title}>{props.title}</TextView>
       }
       { props.message != '' &&
         <TextView style={[
@@ -248,12 +248,6 @@ const iosStyle = StyleSheet.create({
     transform: [{scale: 1.3}]
   },
   title: {
-    paddingTop: 18,
-    paddingLeft: 16,
-    paddingRight: 16,
-    paddingBottom: 16
-  },
-  modalTitle: {
     color: '#111',
     fontSize: 18,
     paddingTop: 18,
@@ -400,8 +394,9 @@ export default Dialog = {
     useNativeDriver: true,
     hideModalContentWhileAnimating: true
   },
-  styles: andStyles,
-  IOSProgress: IOSProgress
+  styles: isIOS ? iosStyle : andStyles,
+  IOSProgress: IOSProgress,
+  AndroidButton: AndroidButton
 }
 
 //Toast显示位置

+ 17 - 0
Strides-APP/app/i18n/locales/en.js

@@ -75,6 +75,7 @@ export default {
     termsOfUse: "Terms of Use",
     topUp: "Purchase Credits",
     topUpWithCard: "Top Up with Card",
+    vouchers: "VOUCHERS",
     wallet: "Transactions",
     applyMember: "Apply Membership",
     yourMembers: "Your Membership",
@@ -535,5 +536,21 @@ export default {
     startTime: "Start Time: ",
     endTime: "End Time: ",
     btnReadAll: "Read All"
+  },
+  voucher: {
+    vouchers: "Vouchers",
+    tabPoints: "DEALS",
+    tabVoucher: "MY VOUCHERS",
+    myVouchers: "My Vouchers",
+    inputPromoCode: "Input Promo Code",
+    availablePoints: "Available Points",
+    expiresOn: "Expires on ",
+    btnClaimed: "CLAIMED",
+    btnGetFor: "GET FOR",
+    btnPointsHint: "",
+    btnPoints: " POINTS",
+    noData: "No Data",
+    noMore: "No More",
+    plsinputPromoCode: "Please input promo code"
   }
 }

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

@@ -75,6 +75,7 @@ export default {
     termsOfUse: "使用條款",
     topUp: "餘額充值",
     topUpWithCard: "使用信用卡充值",
+    vouchers: "代金券",
     wallet: "我的餘額",
     applyMember: "申請會員",
     yourMembers: "我的會員",
@@ -535,5 +536,21 @@ export default {
     startTime: "開始時間:",
     endTime: "結束時間:",
     btnReadAll: "全部設為已讀"
+  },
+  voucher: {
+    vouchers: "優惠券",
+    tabPoints: "販售",
+    tabVoucher: "我的優惠券",
+    myVouchers: "我的優惠券",
+    inputPromoCode: "兌換優惠券",
+    availablePoints: "可用積分",
+    expiresOn: "過期時間:",
+    btnClaimed: "領取",
+    btnGetFor: "兌換",
+    btnPointsHint: "花費 ",
+    btnPoints: " 積分",
+    noData: "沒有優惠券數據",
+    noMore: "已經到底了",
+    plsinputPromoCode: "請鍵入兌換代碼"
   }
 }

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

@@ -75,6 +75,7 @@ export default {
     termsOfUse: "使用条款",
     topUp: "余额充值",
     topUpWithCard: "使用信用卡充值",
+    vouchers: "优惠券",
     wallet: "我的余额",
     applyMember: "申请会员",
     yourMembers: "我的会员",
@@ -535,5 +536,21 @@ export default {
     startTime: "开始时间:",
     endTime: "结束时间:",
     btnReadAll: "全部设为已读"
+  },
+  voucher: {
+    vouchers: "代金券",
+    tabPoints: "贩毒",
+    tabVoucher: "我的代金券",
+    myVouchers: "我的代金券",
+    inputPromoCode: "兑换代金券",
+    availablePoints: "可用积分",
+    expiresOn: "过期时间:",
+    btnClaimed: "领取",
+    btnGetFor: "兑换",
+    btnPointsHint: "需要 ",
+    btnPoints: " 积分",
+    noData: "没有代金券数据",
+    noMore: "到底了",
+    plsinputPromoCode: "请输入兑换码"
   }
 }

+ 6 - 0
Strides-APP/app/pages/Router.js

@@ -65,6 +65,7 @@ import RefundPolicy from './payment/RefundPolicy';
 import ViewArticle from './alert/ViewArticle';
 import VehicleListV2 from './vehicles/VehicleListV2';
 import HistoryList from './wallet/HistoryList';
+import VoucherPage from './vouchers/VoucherPage';
 
 export var PageList = {
   'splash': {
@@ -353,6 +354,11 @@ export var PageList = {
     titleScope: 'route.refundPolicy',
     component: RefundPolicy
   },
+  'myVoucher': {
+    title: "Vouchers",
+    titleScope: 'route.vouchers',
+    component: VoucherPage
+  },
   'settings': {
     title: 'Settings',
     titleScope: 'route.settings',

+ 19 - 0
Strides-APP/app/pages/my/ProfileV2.js

@@ -170,6 +170,25 @@ export default class ProfileV2 extends Component {
             color={textCancel}
             name='angle-right'/>
         </Button>
+        { app.modules.vouchers &&
+          <Button
+            style={styles.cardView}
+            viewStyle={styles.profileItem}
+            onClick={() => startPage(PageList.myVoucher)}>
+            <MaterialIcons
+              style={styles.cardIcon}
+              name="discount"
+              size={32}
+              color="#00638C"/>
+            <View style={styles.cardInfo}>
+              <TextView style={styles.cardLabel}>{$t('voucher.myVouchers')}</TextView>
+            </View>
+            <FontAwesome
+              size={28}
+              color={textCancel}
+              name='angle-right'/>
+          </Button>
+        }
         {/* Vehicle List */}
         {/* <View style={styles.titleView}>
           <Text style={styles.title}>{$t('profile.myVehicles')}</Text>

+ 285 - 0
Strides-APP/app/pages/vouchers/ListPoints.js

@@ -0,0 +1,285 @@
+/**
+ * 购买代金券列表
+ * @邠心vbe on 2024/04/09
+ */
+import React, { Component } from 'react';
+import { View, Text, StyleSheet, RefreshControl, FlatList } from 'react-native';
+import ViewRedeem from './ViewRedeem';
+import apiVoucher from '../../api/apiVoucher';
+import { MyRefreshProps } from '../../components/ThemesConfig';
+import TextView from '../../components/TextView';
+import Button from '../../components/Button';
+import Dialog from '../../components/Dialog';
+import VoucherType from './VoucherType';
+
+export default class ListPoints extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      userInfo: {},
+      dataList: [],
+      voucherType: "",
+      hasMore: true,
+      refreshing: false
+    };
+  }
+
+  componentDidMount() {
+    this.props.navigation.addListener('focus', () => {
+      this.refreshUserInfo();
+      this.getDataList();
+    });
+  }
+
+  onRefresh() {
+    this.setState({
+      refreshing: true
+    })
+    this.refreshUserInfo();
+    this.getDataList();
+  }
+
+  refreshUserInfo() {
+    getUserInfo(info => {
+      this.setState({
+        userInfo: info
+      });
+    }, true);
+  }
+
+  getDataList(lastId="") {
+    apiVoucher.getDealVouchers({
+      lastVoucherId: lastId,
+      voucherType: this.state.voucherType
+    }).then(res => {
+      if (res.data) {
+        if (lastId) {
+          if (res.data.length > 0) {
+            const list = this.state.dataList;
+            this.setState({
+              dataList: list.concat(res.data)
+            });
+          } else {
+            this.setState({
+              hasMore: false
+            })
+          }
+        } else {
+          this.setState({
+            dataList: res.data,
+            hasMore: true
+          });
+        }
+      } else {
+        this.setState({
+          dataList: []
+        });
+      }
+    }).catch(err => {
+      toastShort(err)
+    }).finally(() => {
+      this.setState({
+        refreshing: false
+      });
+    });
+  }
+
+  getDataListPage() {
+    if (this.state.dataList.length > 0 && this.state.hasMore) {
+      const last = this.state.dataList[this.state.dataList.length-1]
+      this.getDataList(last.voucherId);
+    }
+  }
+
+  onPurchase(item) {
+    Dialog.showProgressDialog();
+    apiVoucher.purchaseVoucher({
+      voucherId: item.voucherId
+    }).then(res => {
+      Dialog.dismissLoading();
+      if (res.msg) {
+        setTimeout(() => {
+          Dialog.showDialog({
+            title: $t("voucher.vouchers"),
+            message: res.msg,
+            showCancel: false
+          })
+        }, 500);
+      }
+    }).catch(err => {
+      Dialog.dismissLoading();
+      if (err.err) {
+        setTimeout(() => {
+          Dialog.showDialog({
+            title: $t("common.error"),
+            message: err.err,
+            showCancel: false
+          })
+        }, 500);
+      }
+    })
+  }
+
+  changeType(type) {
+    this.setState({
+      voucherType: type
+    });
+    this.getDataList();
+  }
+
+  listItem = ({item, index, separators}) => {
+    return (
+      <View
+        style={styles.itemView}>
+        <View style={styles.itemContent}>
+          <TextView style={styles.voucherTitle}>{item.voucherName}</TextView>
+          <TextView style={styles.voucherDesc}>{item.voucherDesc}</TextView>
+          <TextView style={styles.expireDate}>{$t("voucher.expiresOn") + item.expiresOn}</TextView>
+        </View>
+        { item.purchasePoints > 0
+        ? <Button
+            viewStyle={styles.purchaseButton}
+            borderRadius={6}
+            onClick={() => this.onPurchase(item)}>
+            <TextView
+              style={styles.getForText}>
+              {$t("voucher.btnGetFor")}
+            </TextView>
+            <TextView
+              style={styles.getValueText}>
+              {item.purchasePoints}
+              {$t("voucher.btnPoints")}
+            </TextView>
+          </Button>
+        : <Button
+            style={styles.claimeButton}
+            viewStyle={styles.claimeButtonView}
+            textStyle={styles.claimeButtonText}
+            text={$t("voucher.btnClaimed")}
+            onClick={() => this.onPurchase(item)}/>
+        }
+      </View>
+    )
+  }
+
+  topView = (props) => {
+    return (
+      <View>
+        <ViewRedeem
+          userInfo={this.state.userInfo}/>
+        <VoucherType
+          type={this.state.voucherType}
+          onChange={type => this.changeType(type)}
+        />
+      </View>
+    )
+  }
+
+  bottomView = () => {
+    if (!this.state.hasMore) {
+      return (<Text style={styles.noMore}>{$t('voucher.noMore')}</Text>)
+    } else {
+      return null
+    }
+  }
+
+  render() {
+    return (
+      <FlatList
+        style={styles.container}
+        data={this.state.dataList}
+        renderItem={this.listItem}
+        ListHeaderComponent={this.topView}
+        keyExtractor={item => item.voucherId}
+        onEndReached={() => this.getDataListPage()}
+        onEndReachedThreshold={0.3}
+        ListEmptyComponent={<Text style={styles.noData}>{$t('voucher.noData')}</Text>}
+        refreshControl={
+          <RefreshControl
+            {...MyRefreshProps()}
+            refreshing={this.state.refreshing}
+            onRefresh={() => this.onRefresh()}
+          />
+        }/>
+    );
+  }
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    padding: 16
+  },
+  itemView: {
+    padding: 16,
+    marginTop: 16,
+    borderWidth: 1,
+    borderColor: '#DADADA',
+    borderRadius: 4,
+    alignItems: 'center',
+    flexDirection: 'row',
+    backgroundColor: colorLight
+  },
+  itemContent: {
+    flex: 1,
+    paddingRight: 16
+  },
+  voucherTitle: {
+    color: textPrimary,
+    fontSize: 16,
+    fontWeight: 'bold'
+  },
+  voucherDesc: {
+    color: textPrimary,
+    fontSize: 14,
+    paddingTop: 2,
+    paddingBottom: 4
+  },
+  expireDate: {
+    color: textCancel,
+    fontSize: 12
+  },
+  purchaseButton: {
+    padding: 4,
+    minWidth: 75,
+    alignItems: 'center'
+  },
+  claimeButton: {
+    borderWidth: 1,
+    borderColor: colorAccent,
+    borderRadius: 6,
+    backgroundColor: colorLight
+  },
+  claimeButtonView: {
+    padding: 8,
+    minWidth: 73,
+    alignItems: 'center'
+  },
+  claimeButtonText: {
+    color: colorAccent,
+    fontSize: 12,
+    fontWeight: 'bold'
+  },
+  getForText: {
+    color: textLight,
+    fontSize: 12,
+    fontWeight: 'bold'
+  },
+  getValueText: {
+    color: textLight,
+    fontSize: 10
+  },
+  noData: {
+    color: textPlacehoder,
+    fontSize: 14,
+    padding: 20,
+    marginTop: 16,
+    textAlign: 'center'
+  },
+  noMore: {
+    color: textPlacehoder,
+    fontSize: 14,
+    padding: 16,
+    textAlign: 'center'
+  }
+})

+ 257 - 0
Strides-APP/app/pages/vouchers/ListVoucher.js

@@ -0,0 +1,257 @@
+/**
+ * 我的代金券列表
+ * @邠心vbe on 2024/04/09
+ */
+import React, { Component } from 'react';
+import { View, Text, StyleSheet, RefreshControl, FlatList } from 'react-native';
+import ViewRedeem from './ViewRedeem';
+import apiVoucher from '../../api/apiVoucher';
+import { MyRefreshProps } from '../../components/ThemesConfig';
+import TextView from '../../components/TextView';
+
+export default class ListVoucher extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      userInfo: {},
+      dataList: [],
+      voucherType: "",
+      hasMore: true,
+      refreshing: false
+    };
+  }
+
+  componentDidMount() {
+    this.props.navigation.addListener('focus', () => {
+      this.refreshUserInfo();
+      this.getDataList();
+    });
+  }
+
+  onRefresh() {
+    this.setState({
+      refreshing: true
+    })
+    this.refreshUserInfo();
+    this.getDataList();
+  }
+
+  refreshUserInfo() {
+    getUserInfo(info => {
+      this.setState({
+        userInfo: info
+      });
+    }, true);
+  }
+
+  getDataList(lastId="") {
+    apiVoucher.getMyVouchers({
+      lastVoucherId: lastId,
+      voucherType: this.state.voucherType
+    }).then(res => {
+      if (res.data) {
+        if (lastId) {
+          if (res.data.length > 0) {
+            const list = this.state.dataList;
+            this.setState({
+              dataList: list.concat(res.data)
+            });
+          } else {
+            this.setState({
+              hasMore: false
+            })
+          }
+        } else {
+          this.setState({
+            dataList: res.data,
+            hasMore: true
+          });
+        }
+      } else {
+        this.setState({
+          dataList: []
+        });
+      }
+    }).catch(err => {
+      toastShort(err)
+    }).finally(() => {
+      this.setState({
+        refreshing: false
+      });
+    });
+  }
+
+  getDataListPage() {
+    if (this.state.dataList.length > 0 && this.state.hasMore) {
+      const last = this.state.dataList[this.state.dataList.length-1]
+      this.getDataList(last.voucherId);
+    }
+  }
+
+  getColorByStatus(status) {
+    let color = colorAccent;
+    switch (status) {
+      case "Used":
+      case "Expired":
+        color = "#434343";
+        break;
+      case "Expiring":
+        color = "#ED3F3F"
+        break;
+    }
+    return color;
+  }
+
+  changeType(type) {
+    this.setState({
+      voucherType: type
+    });
+    this.getDataList();
+  }
+
+  listItem = ({item, index, separators}) => {
+    return (
+      <View
+        style={styles.itemView}>
+        <View style={styles.itemContent}>
+          <TextView style={styles.voucherTitle}>{item.voucherName}</TextView>
+          <TextView style={styles.voucherDesc}>{item.voucherDesc}</TextView>
+          <TextView style={styles.expireDate}>{$t("voucher.expiresOn") + item.expiresOn}</TextView>
+        </View>
+        <TextView
+          style={[
+            styles.statusButton, {
+              color: this.getColorByStatus(item.userVoucherStatus),
+              borderColor: this.getColorByStatus(item.userVoucherStatus)
+            }
+          ]}>
+          {item.userVoucherStatus}
+        </TextView>
+      </View>
+    )
+  }
+
+  topView = (props) => {
+    return (
+      <View>
+        <ViewRedeem
+          userInfo={this.state.userInfo}
+          onChange={() => this.onRefresh()}/>
+        <VoucherType
+          type={this.state.voucherType}
+          onChange={type => this.changeType(type)}
+        />
+      </View>
+    );
+  }
+
+  bottomView = () => {
+    if (!this.state.hasMore) {
+      return (<Text style={styles.noMore}>{$t('voucher.noMore')}</Text>)
+    } else {
+      return null
+    }
+  }
+
+  render() {
+    return (
+      <FlatList
+        style={styles.container}
+        data={this.state.dataList}
+        renderItem={this.listItem}
+        ListHeaderComponent={this.topView}
+        keyExtractor={item => item.userVoucherId}
+        onEndReached={() => this.getDataListPage()}
+        onEndReachedThreshold={0.3}
+        ListEmptyComponent={<Text style={styles.noData}>{$t('voucher.noData')}</Text>}
+        refreshControl={
+          <RefreshControl
+            {...MyRefreshProps()}
+            refreshing={this.state.refreshing}
+            onRefresh={() => this.onRefresh()}
+          />
+        }/>
+    );
+  }
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    padding: 16
+  },
+  itemView: {
+    padding: 16,
+    marginTop: 16,
+    borderWidth: 1,
+    borderColor: '#DADADA',
+    borderRadius: 4,
+    alignItems: 'center',
+    flexDirection: 'row',
+    backgroundColor: colorLight
+  },
+  itemContent: {
+    flex: 1,
+    paddingRight: 16
+  },
+  voucherTitle: {
+    color: textPrimary,
+    fontSize: 16,
+    fontWeight: 'bold'
+  },
+  voucherDesc: {
+    color: textPrimary,
+    fontSize: 14,
+    paddingTop: 2,
+    paddingBottom: 4
+  },
+  expireDate: {
+    color: textCancel,
+    fontSize: 12
+  },
+  purchaseButton: {
+    padding: 4,
+    minWidth: 75,
+    alignItems: 'center'
+  },
+  claimeButton: {
+    borderWidth: 1,
+    borderColor: colorAccent,
+    borderRadius: 6,
+    backgroundColor: colorLight
+  },
+  statusButton: {
+    color: colorAccent,
+    padding: 8,
+    minWidth: 73,
+    fontSize: 12,
+    textAlign: 'center',
+    borderWidth: 1,
+    borderColor: colorAccent,
+    borderRadius: 6,
+    textTransform: 'uppercase',
+    backgroundColor: colorLight
+  },
+  getForText: {
+    color: textLight,
+    fontSize: 12,
+    fontWeight: 'bold'
+  },
+  getValueText: {
+    color: textLight,
+    fontSize: 10
+  },
+  noData: {
+    color: textPlacehoder,
+    fontSize: 14,
+    padding: 20,
+    marginTop: 16,
+    textAlign: 'center'
+  },
+  noMore: {
+    color: textPlacehoder,
+    fontSize: 14,
+    padding: 16,
+    textAlign: 'center'
+  }
+})

+ 191 - 0
Strides-APP/app/pages/vouchers/ViewRedeem.js

@@ -0,0 +1,191 @@
+import React, { useState } from 'react';
+import { StyleSheet, Text, TextInput, View } from 'react-native';
+import Button from '../../components/Button';
+import TextView from '../../components/TextView';
+import Modal from "react-native-modal";
+import { ModalProps } from "../../components/BottomModal";
+import Dialog from '../../components/Dialog';
+import apiVoucher from '../../api/apiVoucher';
+
+export default ViewRedeem = ({
+  userInfo={},
+  onChange
+}) => {
+  const [visible, showDialog] = useState(false);
+  const [promoCode, setPromoCode] = useState("");
+  const redeemVoucher = () => {
+    showDialog(false);
+    if (promoCode) {
+      setTimeout(() => {
+        Dialog.showProgressDialog();
+        apiVoucher.redeemVoucher({
+          redemptionCode: promoCode
+        }).then(res => {
+          Dialog.dismissLoading();
+          if (onChange) {
+            onChange();
+          }
+          if (res.msg) {
+            setTimeout(() => {
+              Dialog.showDialog({
+                title: $t("voucher.vouchers"),
+                message: res.msg,
+                showCancel: false
+              })
+            }, 500);
+          }
+        }).catch(err => {
+          Dialog.dismissLoading();
+          if (err.err) {
+            setTimeout(() => {
+              Dialog.showDialog({
+                title: $t("common.error"),
+                message: err.err,
+                showCancel: false
+              })
+            }, 500);
+          }
+        })
+      }, 300);
+    } else {
+      toastLong($t("voucher.plsinputPromoCode"))
+    }
+  }
+  return (
+    <View>
+      <View style={styles.redeemLayout}>
+        <Button
+          style={styles.buttonLeft}
+          viewStyle={styles.buttonView}
+          onClick={() => showDialog(true)}>
+          <MaterialCommunityIcons
+            name="ticket-percent-outline"
+            size={24}
+            color={textPrimary}/>
+          <TextView
+            style={styles.buttonText}>
+            {$t("voucher.inputPromoCode")}
+          </TextView>
+        </Button>
+        <Text style={styles.btnDivide}></Text>
+        <View
+          style={styles.buttonRight}>
+          <TextView
+            style={styles.buttonText}>
+            {$t("voucher.availablePoints")}
+          </TextView>
+          <TextView
+            style={styles.valueText}>
+            {userInfo?.points || '0'}
+          </TextView>
+        </View>
+      </View>
+      <Modal
+        isVisible={visible}
+        onBackButtonPress={() => showDialog(false)}
+        onBackdropPress={() => showDialog(false)}
+        {...ModalProps}>
+        <View style={Dialog.styles.modalDialog}>
+          <TextView style={Dialog.styles.title}>{$t('voucher.inputPromoCode')}</TextView>
+          <View style={Dialog.styles.message}>
+            <TextInput
+              style={styles.inputView}
+              allowFontScaling={false}
+              maxLength={6}
+              placeholderTextColor={textPlacehoder}
+              onChangeText={text => setPromoCode(text)}
+              onSubmitEditing={() => redeemVoucher()}
+            />
+          </View>
+          { isIOS
+          ? <View style={Dialog.styles.modalFooter}>
+              <Button
+                style={Dialog.styles.btnGroup}
+                viewStyle={Dialog.styles.btnView}
+                textStyle={Dialog.styles.btnConfirm}
+                text={$t('nav.confirm')}
+                onClick={() => redeemVoucher()}/>
+              <Button
+                style={[Dialog.styles.btnGroup, Dialog.styles.btnRight]}
+                viewStyle={Dialog.styles.btnView}
+                textStyle={Dialog.styles.btnConfirm}
+                text={$t('nav.cancel')}
+                onClick={() => showDialog(false)}/>
+            </View>
+          : <View style={Dialog.styles.modalFooter}>
+              <Dialog.AndroidButton
+                title={$t('nav.cancel')}
+                onPress={() => showDialog(false)}/>
+              <Dialog.AndroidButton
+                title={$t('nav.confirm')}
+                onPress={() => redeemVoucher()}/>
+            </View>
+          }
+        </View>
+      </Modal>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  redeemLayout: {
+    borderWidth: 1,
+    borderColor: '#DADADA',
+    borderRadius: 4,
+    alignItems: 'center',
+    flexDirection: 'row'
+  },
+  btnDivide: {
+    width: 1,
+    height: 26,
+    backgroundColor: '#DADADA'
+  },
+  buttonLeft: {
+    flex: 1,
+    borderTopLeftRadius: 3,
+    borderTopRightRadius: 0,
+    borderBottomLeftRadius: 3,
+    borderBottomRightRadius: 0,
+    backgroundColor: 'transparent'
+  },
+  buttonRight: {
+    flex: 1,
+    alignItems: 'center',
+    flexDirection: 'row',
+    justifyContent: 'center',
+    backgroundColor: 'transparent'
+  },
+  buttonView: {
+    flex: 1,
+    height: 42,
+    alignItems: 'center',
+    flexDirection: 'row',
+    justifyContent: 'center'
+  },
+  buttonText: {
+    color: textPrimary,
+    fontSize: 13,
+    paddingLeft: 8,
+    paddingRight: 8
+  },
+  valueText: {
+    color: colorAccent,
+    fontSize: 14
+  },
+  inputView: {
+    color: textPrimary,
+    fontSize: 14,
+    height: 42,
+    marginTop: 8,
+    textAlign: 'center',
+    paddingLeft: 12,
+    paddingRight: 12,
+    borderWidth: 1,
+    borderColor: '#DADADA',
+    borderRadius: 4,
+    alignItems: 'center',
+    flexDirection: 'row',
+    textTransform: 'uppercase',
+    backgroundColor: colorLight
+  }
+})

+ 83 - 0
Strides-APP/app/pages/vouchers/VoucherPage.js

@@ -0,0 +1,83 @@
+/**
+ * 代金券页面适配器
+ * @邠心vbe on 2024/04/09
+ */
+import React, { Component } from 'react';
+import { StyleSheet } from 'react-native';
+import ListPoints from './ListPoints';
+import ListVoucher from './ListVoucher';
+import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
+import app from '../../../app.json';
+import { PageList } from '../Router';
+
+export default class VoucherPage extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      pageAdapter: [{
+        title: $t('voucher.tabPoints'),
+        name: "Deals",
+        component: ListPoints
+      },{
+        title: $t('voucher.tabVoucher'),
+        name: "Vouchers",
+        component: ListVoucher
+      }]
+    };
+    this.tabBarStyle = {
+      tabBarStyle: styles.tabStyle,
+      tabBarPressColor: rippleColor,
+      tabBarScrollEnabled: false,
+      tabBarIndicatorStyle: styles.indicator,
+      tabBarActiveTintColor: tabBarTextActive,
+      tabBarInactiveTintColor: tabBarTextInactive
+    }
+    this.isHide = false;
+  }
+
+  backPage() {
+    if (!this.isHide) {
+      startPage(PageList.profile);
+      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
+  }
+})

+ 66 - 0
Strides-APP/app/pages/vouchers/VoucherType.js

@@ -0,0 +1,66 @@
+import React, { useEffect, useState } from 'react';
+import { StyleSheet, View } from 'react-native';
+import Dropdown from '../../components/Dropdown';
+import apiVoucher from '../../api/apiVoucher';
+import TextView from '../../components/TextView';
+
+export default VoucherType = ({
+  type="",
+  onChange
+}) => {
+  const [options, setOption] = useState([])
+  useEffect(() => {
+    apiVoucher.getVoucherTypeOptions().then(res => {
+      if (res.data) {
+        setOption(res.data)
+      }
+    }).catch(err => {
+      setOption([])
+    })
+  },[])
+  return (
+    <View style={styles.typeView}>
+      <View style={ui.flex1}></View>
+      <TextView style={styles.typeText}>{type}</TextView>
+      <Dropdown
+        style={styles.selectView}
+        textStyle={styles.selectText}
+        rippleStyle={{}}
+        title={$t('members.membership')}
+        list={options}
+        value={type}
+        autoSelect={true}
+        //valueKey='groupPk'
+        //nameKey='groupName'
+        onChange={(value, index)=> onChange(value)}/>
+    </View>
+  )
+};
+
+const styles = StyleSheet.create({
+  typeView: {
+    flexDirection: 'row',
+    justifyContent: 'flex-end'
+  },
+  selectView: {
+    flex: 1,
+    marginTop: 8,
+    paddingRight: 8,
+    alignItems: 'center',
+    flexDirection: 'row'
+  },
+  selectText: {
+    color: textPrimary,
+    fontSize: 14,
+    padding: 8,
+    opacity: 0
+  },
+  typeText: {
+    top: 8,
+    bottom: 0,
+    color: textPrimary,
+    right: 35,
+    fontSize: 14,
+    position: 'absolute'
+  }
+})

+ 4 - 0
Strides-APP/app/pages/wallet/HistoryList.js

@@ -63,6 +63,10 @@ export default class HistoryList extends Component {
             hasMore: true
           });
         }
+      } else {
+        this.setState({
+          dataList: []
+        });
       }
     }).catch(err => {
       toastShort(err)