瀏覽代碼

feat:用户相关页面

needcode 2 年之前
父節點
當前提交
a3ed72a245

+ 5 - 5
src/api/auth.ts

@@ -114,10 +114,10 @@ export function refresh(): Promise<string> {
           errMsg: "请登录",
         });
         uni.reLaunch({
-          url: "/pages/map/map",
+          url: "/pages/map/index",
         });
       } else {
-        getApp().globalData.token = "";
+        getApp<any>().globalData.token = "";
         _tokenStorage.clear("token");
         uni.request({
           url: `${host}/user/refresh`,
@@ -139,7 +139,7 @@ export function refresh(): Promise<string> {
             } else {
               if (data.code === 21005) {
                 uni.reLaunch({
-                  url: "/pages/map/map",
+                  url: "/pages/map/index",
                 });
               }
               reject({
@@ -165,11 +165,11 @@ export function fetchToken() {
 }
 
 export function setToken(token: string) {
-  getApp().globalData.token = token;
+  getApp<any>().globalData.token = token;
   return _tokenStorage.set("token", token);
 }
 
 export function clearToken() {
-  getApp().globalData.token = "";
+  getApp<any>().globalData.token = "";
   return _tokenStorage.clear("token");
 }

+ 24 - 18
src/api/user.ts

@@ -5,14 +5,14 @@ const userHttp = new Http(host);
 
 export function fetchProfile() {
   return userHttp.get("/user/me").then((res) => {
-    getApp().globalData.user = res;
+    getApp<any>().globalData.user = res;
     return res;
   });
 }
 
 export function fetchCollectList() {
-  if (getApp().globalData.collectIds) {
-    return Promise.resolve(getApp().globalData.collectIds);
+  if (getApp<any>().globalData.collectIds) {
+    return Promise.resolve(getApp<any>().globalData.collectIds);
   }
   return userHttp
     .get<{
@@ -22,20 +22,20 @@ export function fetchCollectList() {
       }[];
     }>("/user/collectList?page=1&page_size=999")
     .then((res) => {
-      getApp().globalData.collectIds = res.data
+      getApp<any>().globalData.collectIds = res.data
         ? res.data
             .filter((item) => Number(item.status) === 1)
             .map((item) => {
               return Number(item.station_id);
             })
         : [];
-      return getApp().globalData.collectIds;
+      return getApp<any>().globalData.collectIds;
     });
 }
 
 export function addCollectList(sid: number) {
-  let ids = getApp().globalData.collectIds
-    ? (getApp().globalData.collectIds as number[])
+  let ids = getApp<any>().globalData.collectIds
+    ? (getApp<any>().globalData.collectIds as number[])
     : [];
   const status = ids.includes(sid) ? 2 : 1;
   return userHttp
@@ -51,7 +51,7 @@ export function addCollectList(sid: number) {
       } else {
         ids = ids.filter((id) => id !== sid);
       }
-      getApp().globalData.collectIds = ids;
+      getApp<any>().globalData.collectIds = ids;
       return status === 1;
     });
 }
@@ -68,6 +68,8 @@ export function updateProfile(data: {
   });
 }
 
+declare const wx: any;
+
 export function insertMoney(amount: number) {
   return userHttp
     .post<{
@@ -80,25 +82,29 @@ export function insertMoney(amount: number) {
     }>("/payment/pay", {
       data: {
         amount: parseInt(`${amount * 100}`),
-        openid: getApp().globalData.user?.openid,
+        openid: getApp<any>().globalData.user.openid,
       },
     })
     .then((res: any) => {
       return new Promise((resolve, reject) => {
-        uni.requestPayment({
-          provider: "wxpay",
-          orderInfo: {
-            timeStamp: res.timestamp,
-            nonceStr: res.nonceStr,
-            package: res.package,
-            signType: "MD5",
-            paySign: res.paySign,
-          },
+        // #ifdef MP-WEIXIN
+        wx.requestPayment({
+          timeStamp: res.timestamp,
+          nonceStr: res.nonceStr,
+          package: res.package,
+          signType: "MD5",
+          paySign: res.paySign,
           success(res: any) {
             resolve(res);
           },
           fail: reject,
         });
+        // #endif
+        // #ifndef MP-WEIXIN
+        reject({
+          errMsg: "目前仅支持微信支付",
+        });
+        // #endif
       });
     });
 }

+ 57 - 0
src/components/style-bottom-view/style-bottom-view.vue

@@ -0,0 +1,57 @@
+<template>
+  <view class="fixed-bottom-view-placeholder"></view>
+  <view
+    class="fixed-bottom-view"
+    :class="{
+      'fixed-bottom-view-shadow ': shadow,
+      'fixed-bottom-view-hidden': hidden,
+    }"
+    style="z-index: {{zIndex}};background:{{background}};"
+  >
+    <slot></slot>
+  </view>
+</template>
+
+<script lang="ts">
+export default {
+  props: {
+    shadow: Boolean,
+    hidden: Boolean,
+    background: {
+      type: String,
+      default: "#ffffff",
+    },
+    zIndex: {
+      type: Number,
+      default: 999999,
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+.fixed-bottom-view {
+  position: fixed;
+  width: 100%;
+  left: 0;
+  bottom: 0;
+  transition: all 0.3s linear;
+  box-sizing: content-box;
+  padding-bottom: constant(safe-area-inset-bottom);
+  padding-bottom: env(safe-area-inset-bottom);
+}
+
+.fixed-bottom-view-placeholder {
+  box-sizing: content-box;
+  padding-bottom: constant(safe-area-inset-bottom);
+  padding-bottom: env(safe-area-inset-bottom);
+}
+
+.fixed-bottom-view-shadow {
+  box-shadow: 0px 0px 40rpx 0px rgba(157, 147, 142, 0.36);
+}
+
+.fixed-bottom-view-hidden {
+  transform: translateY(100%);
+}
+</style>

+ 49 - 0
src/pages-user/collect/collect.vue

@@ -0,0 +1,49 @@
+<template>
+  <view class="pl-20 pr-20" v-if="list && list.length">
+    <view class="mt-20" v-for="(item, index) in list" :key="index">
+      <charge-station
+        :title="item.StationName"
+        :tag="item.Construction"
+        :price="item.TotalFee"
+        :fast="item.fastEquipmentInfos"
+        :slow="item.slowEquipmentInfos"
+        :sId="item.StationID"
+        :address="item.Address"
+        :latitude="item.StationLat"
+        :longitude="item.StationLng"
+      ></charge-station>
+    </view>
+  </view>
+
+  <view
+    class="pt-40 flex-center fs-30"
+    style="color: rgba(0, 0, 0, 0.6)"
+    v-if="list && list.length <= 0"
+    >暂无数据</view
+  >
+
+  <view class="pt-40 flex-center fs-28" style="opacity: 0.6" v-if="!list"
+    >加载中</view
+  >
+</template>
+
+<script setup lang="ts">
+import { fetchStationByIds } from "../../api/charge";
+import { fetchCollectList } from "../../api/user";
+import { onLoad } from "@dcloudio/uni-app";
+import { ref } from "vue";
+const list = ref<any[]>();
+onLoad(() => {
+  fetchCollectList().then((collectIds) => {
+    fetchStationByIds(collectIds || []).then((res: any) => {
+      list.value = res;
+    });
+  });
+});
+</script>
+
+<style lang="scss">
+page {
+  background-color: #f5f5f5;
+}
+</style>

+ 82 - 0
src/pages-user/profile-edit/profile-edit.vue

@@ -0,0 +1,82 @@
+<template>
+  <view class="pt-30 pr-30 pl-30 flex-column flex-between">
+    <style-input
+      :title="form.title"
+      :value="form.value"
+      :focus="true"
+      :type="form.title === '昵称' ? 'nickname' : 'text'"
+      @input="input"
+      @focus="inputFocus"
+    />
+  </view>
+
+  <style-bottom-view>
+    <view class="pl-30 pr-30 pb-30" :style="bottomViewStyle">
+      <style-button type="primary" @click="save">保存</style-button>
+    </view>
+  </style-bottom-view>
+</template>
+
+<script setup lang="ts">
+import { onLoad } from "@dcloudio/uni-app";
+import { ref } from "vue";
+const form = ref({
+  key: "",
+  title: "",
+  value: "",
+  originValue: "",
+});
+const bottomViewStyle = ref({});
+const input = (e: any) => {
+  form.value.value = e.value;
+};
+const inputFocus = (e: any) => {
+  let pb = e.height + 20;
+  if (form.value.title === "昵称") {
+    pb += 40;
+  }
+  bottomViewStyle.value =
+    e.focus && e.height > 0
+      ? {
+          "padding-bottom": `${pb}px`,
+        }
+      : {};
+};
+const save = () => {
+  const { value, originValue } = form.value;
+  if (value === originValue) {
+    uni.showModal({
+      title: "提示",
+      content: "未保存修改,返回后本次编辑无效",
+      confirmColor: "#347DFF",
+      confirmText: "知道了",
+      showCancel: false,
+      success() {
+        uni.navigateBack();
+      },
+    });
+    return;
+  }
+  getApp<any>().globalData.lastData.profile = {
+    key: form.value.key,
+    value,
+  };
+  uni.navigateBack();
+};
+onLoad((options: any) => {
+  // console.log(options)
+  let value = "";
+  if (options.value) {
+    value = decodeURIComponent(options.value);
+  }
+  uni.setNavigationBarTitle({
+    title: options.title || "",
+  });
+  form.value.key = options.key;
+  form.value.title = options.title || "";
+  form.value.value = value;
+  form.value.originValue = value;
+});
+</script>
+
+<style lang="scss"></style>

+ 266 - 0
src/pages-user/profile/profile.vue

@@ -0,0 +1,266 @@
+<template>
+  <view class="pt-60 pb-20 flex-center">
+    <button
+      class="avatar"
+      open-type="chooseAvatar"
+      @chooseavatar="chooseAvatar"
+    >
+      <image class="avatar_image" :src="avatar"></image>
+      <view class="avatar_text flex-center">编辑</view>
+    </button>
+  </view>
+
+  <view class="pl-50 pr-50">
+    <view
+      class="menu flex-align-center flex-between"
+      v-for="(item, index) in menu"
+      :key="index"
+      @click="edit(index)"
+    >
+      <view class="fs-30">{{ item.title }}</view>
+      <view class="flex">
+        <view
+          :class="['fs-30', 'fw-500', `mr-${item.disabled ? '0' : '20'}`]"
+          style="color: rgba(0, 0, 0, 0.8)"
+          >{{ item.value }}</view
+        >
+        <uni-icons
+          type="right"
+          size="12"
+          color="rgba(0,0,0,0.4)"
+          v-if="!item.disabled"
+        ></uni-icons>
+      </view>
+    </view>
+  </view>
+
+  <style-bottom-view>
+    <view class="pl-60 pr-60 pb-40">
+      <style-button type="primary" @click="logoutUser">退出登录</style-button>
+    </view>
+  </style-bottom-view>
+</template>
+
+<script setup lang="ts">
+import { clearToken } from "../../api/auth";
+import { fetchProfile, updateProfile, logout } from "../../api/user";
+import { upload } from "../../utils/uploader";
+import { onLoad, onShow } from "@dcloudio/uni-app";
+import { ref } from "vue";
+const avatar = ref<string>();
+const menu = ref<any[]>([]);
+
+const refresh = () => {
+  const _menu = [
+    {
+      title: "昵称",
+      key: "nick_name",
+      value: "",
+    },
+    {
+      title: "电话",
+      key: "phone",
+      disabled: true,
+    },
+    {
+      title: "车牌号",
+      key: "license_plate",
+      value: "",
+    },
+    {
+      title: "VIN码",
+      key: "vin",
+      value: "",
+    },
+    {
+      title: "充电卡",
+      key: "card_no",
+      value: "",
+    },
+  ];
+  fetchProfile().then(() => {
+    const user = getApp<any>().globalData.user;
+    if (user) {
+      _menu[0].value = user.nickname;
+      _menu[1].value = user.phone;
+      _menu[2].value = user.license_plate;
+      _menu[3].value = user.vin;
+      _menu[4].value = user.card_no;
+      avatar.value =
+        user.avatar ||
+        "https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0";
+      menu.value = _menu;
+    }
+  });
+};
+
+const save = (form: Record<string, any>) => {
+  uni.showLoading({
+    title: "保存中",
+  });
+  return updateProfile(form)
+    .then((res) => {
+      uni.hideLoading();
+      uni.showToast({
+        icon: "success",
+        title: "保存成功",
+      });
+      refresh();
+      return res;
+    })
+    .catch((err) => {
+      uni.hideLoading();
+      uni.showModal({
+        content: `${err.errMsg},请重试`,
+      });
+    });
+};
+
+const chooseAvatar = (e: any) => {
+  if (e.detail.avatarUrl) {
+    uni.showLoading({
+      title: "上传中",
+    });
+    upload(e.detail.avatarUrl, {
+      onSuccess: (res) => {
+        updateProfile({
+          avatar: res.url,
+        })
+          .then(() => {
+            uni.hideLoading();
+            avatar.value = res.url;
+          })
+          .catch((err) => {
+            uni.hideLoading();
+            uni.showModal({
+              content: `${err.errMsg},请重试`,
+            });
+          });
+      },
+      onFail: (err) => {
+        uni.hideLoading();
+        uni.showModal({
+          content: `${err.errMsg},请重试`,
+        });
+      },
+    });
+  } else {
+    uni.showModal({
+      content: `${e.detail.errMsg},请重试`,
+    });
+  }
+};
+const edit = (index: number) => {
+  const menuItem = menu.value[index];
+  if (menuItem.disabled) {
+    return;
+  }
+  if (/车牌/.test(menuItem.title)) {
+    uni.chooseLicensePlate({
+      success: (res) => {
+        save({
+          license_plate: res.plateNumber,
+        });
+      },
+      fail: (err) => {
+        console.log(err);
+      },
+    });
+    return;
+  }
+  uni.navigateTo({
+    url: `/pages-user/profile-edit/profile-edit?key=${menuItem.key}&title=${
+      menuItem.title
+    }${menuItem.value ? `&value=${encodeURIComponent(menuItem.value)}` : ""}`,
+  });
+};
+
+const logoutUser = () => {
+  uni.showModal({
+    title: "温馨提示",
+    content: "确定退出登录吗?",
+    confirmColor: "#347DFF",
+    confirmText: "确定退出",
+    cancelText: "手滑了",
+    success: (res) => {
+      if (res.confirm) {
+        uni.showLoading({
+          title: "退出中",
+        });
+        logout()
+          .then(() => {
+            uni.hideLoading();
+            uni.showToast({
+              icon: "success",
+              title: "已退出",
+            });
+            clearToken();
+            setTimeout(() => {
+              uni.reLaunch({
+                url: "/pages/map/index",
+              });
+            }, 1500);
+          })
+          .catch((err) => {
+            uni.hideLoading();
+            uni.showModal({
+              content: `${err.errMsg},请重试`,
+            });
+          });
+      }
+    },
+  });
+};
+
+onLoad(() => {
+  refresh();
+});
+onShow(() => {
+  if (getApp<any>().globalData.lastData.profile) {
+    const { key, value } = getApp<any>().globalData.lastData.profile;
+    save({
+      [key]: value,
+    }).then(() => {
+      getApp<any>().globalData.lastData.profile = undefined;
+    });
+  }
+});
+</script>
+
+<style lang="scss">
+.avatar {
+  position: relative;
+  height: 116rpx !important;
+  width: 116rpx !important;
+  border-radius: 50%;
+  border: 2rpx solid rgba(0, 0, 0, 0.15);
+  overflow: hidden;
+  &_image {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    left: 0;
+    top: 0;
+    border-radius: 50%;
+  }
+  &_text {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    width: 100%;
+    height: 40rpx;
+    background: rgba(0, 0, 0, 0.5);
+    color: #fff;
+    font-size: 24rpx;
+  }
+}
+
+.menu {
+  background-color: #fff;
+  height: 120rpx;
+  border-bottom: 1rpx solid rgba(0, 0, 0, 0.1);
+  &:last-child {
+    border-bottom: none;
+  }
+}
+</style>

+ 142 - 0
src/pages-user/wallet-detail/wallet-detail.vue

@@ -0,0 +1,142 @@
+<template>
+  <view class="tabs flex-align-center">
+    <view
+      v-for="(item, index) in types"
+      :key="index"
+      class="fs-30 mr-60"
+      :style="{
+        color:
+          type === item.value ? 'rgba(0, 0, 0, 1);' : 'rgba(0, 0, 0, 0.6);',
+      }"
+      @click="changeType(index)"
+      >{{ item.label
+      }}<view
+        :style="{ opacity: type === item.value ? '1' : '0' }"
+        class="active"
+      ></view
+    ></view>
+  </view>
+
+  <view
+    class="pl-30 pr-30"
+    v-if="infiniteScroller.list && infiniteScroller.list.length"
+  >
+    <view
+      class="item flex-align-center"
+      v-for="(item, index) in infiniteScroller.list"
+      :key="index"
+      @click="detail(index)"
+    >
+      <view>
+        <view class="fs-30 fw-500" key="title" duration="300">{{
+          typesMap[item.type - 1]
+        }}</view>
+        <view class="fs-24" style="color: rgba(0, 0, 0, 0.4)">余额</view>
+      </view>
+      <view class="ml-auto" style="text-align: right">
+        <view class="fs-30 fw-500">
+          <text>{{ item.type > 1 ? "- " : "" }}{{ item.amount }}</text>
+          <text class="fs-24 ml-6">元</text>
+        </view>
+        <view class="fs-24" style="color: rgba(0, 0, 0, 0.4)">{{
+          item.transaction_time
+        }}</view>
+      </view>
+      <view class="ml-32" v-if="item.type === 3">
+        <uni-icons type="right" size="12" color="rgba(0,0,0,0.4)"></uni-icons>
+      </view>
+    </view>
+  </view>
+
+  <view
+    class="pt-40 flex-center fs-30"
+    style="color: rgba(0, 0, 0, 0.6)"
+    v-if="infiniteScroller.list && infiniteScroller.list.length <= 0"
+    >暂无数据</view
+  >
+</template>
+
+<script setup lang="ts">
+import { useInfiniteScroll } from "../../utils/infinite-scroll";
+import { onLoad, onPullDownRefresh, onReachBottom } from "@dcloudio/uni-app";
+import { ref } from "vue";
+import { fetchWallet } from "../../api/user";
+const type = ref(0);
+const types = ref([
+  {
+    label: "全部",
+    value: 0,
+  },
+  {
+    label: "充值",
+    value: 1,
+  },
+  {
+    label: "消费",
+    value: 3,
+  },
+]);
+const typesMap = ref(["充值", "提现", "消费"]);
+const infiniteScroller = useInfiniteScroll(10, (page) => {
+  return fetchWallet(type.value, page, 10).then((res: any) => {
+    if (res && res.length) {
+      res.forEach((item: any) => {
+        item.amount = (Number(item.amount) / 100).toFixed(2);
+      });
+    }
+    return res;
+  });
+});
+const changeType = (index: number) => {
+  type.value = types.value[index].value;
+  infiniteScroller.refresh();
+};
+const detail = (index: number) => {
+  if (!infiniteScroller.list) {
+    return;
+  }
+  if (infiniteScroller.list[index].type === 3) {
+    uni.navigateTo({
+      url: `/pages-charge/order/order?id=${infiniteScroller.list[index].transaction_id}`,
+    });
+  }
+};
+onLoad(() => {
+  infiniteScroller.refresh();
+});
+onReachBottom(() => {
+  infiniteScroller.next();
+});
+onPullDownRefresh(() => {
+  infiniteScroller.refresh();
+});
+</script>
+
+<style lang="scss">
+.tabs {
+  height: 92rpx;
+  padding: 0 40rpx;
+  & > view {
+    position: relative;
+    height: 62rpx;
+    .active {
+      position: absolute;
+      left: 50%;
+      bottom: 0px;
+      transform: translateX(-50%);
+      width: 40rpx;
+      height: 4rpx;
+      border-radius: 4rpx;
+      background-color: var(--color-primary);
+    }
+  }
+}
+
+.item {
+  height: 170rpx;
+  border-bottom: 1rpx solid rgba(0, 0, 0, 0.1);
+  &:last-child {
+    border-bottom: none;
+  }
+}
+</style>

+ 191 - 0
src/pages-user/wallet/wallet.vue

@@ -0,0 +1,191 @@
+<template>
+  <view class="pl-30 pr-30">
+    <view class="wallet">
+      <image
+        src="/static/images/wallet-logo.png"
+        mode="widthFix"
+        class="image"
+      />
+      <view class="tag flex-center" @click="detail">
+        <view class="fs-26" style="color: #fff; line-height: 58rpx"
+          >钱包明细</view
+        >
+        <view style="margin-top: -8rpx; margin-left: 6rpx">
+          <uni-icons type="right" size="10" color="#FFFFFF"></uni-icons>
+        </view>
+      </view>
+      <view class="label">钱包余额</view>
+      <view class="value mt-16">
+        <text class="fs-40 lh-48 fw-500 mr-12">¥</text>
+        <text class="fw-500" style="font-size: 60rpx; line-height: 72rpx">{{
+          balance
+        }}</text>
+      </view>
+    </view>
+    <view class="pay">
+      <view class="title">充值金额</view>
+      <view class="flex-wrap">
+        <view
+          :class="[
+            'option',
+            'flex-center',
+            `option-${index === payOption && !payValue ? 'active' : ''}`,
+          ]"
+          v-for="(item, index) in payOptions"
+          :key="index"
+          @click="changeOption(index)"
+        >
+          {{ item }}
+        </view>
+      </view>
+      <view class="title">自定义金额</view>
+      <style-input
+        :value="payValue > 0 ? payValue : ''"
+        title="金额"
+        type="digit"
+        @input="input"
+      />
+    </view>
+  </view>
+
+  <style-bottom-view>
+    <view class="pl-30 pr-30 pb-20">
+      <style-button type="primary" @click="confirm">充值</style-button>
+    </view>
+  </style-bottom-view>
+</template>
+
+<script setup lang="ts">
+import { ref } from "vue";
+import { fetchProfile, insertMoney } from "../../api/user";
+import { onLoad } from "@dcloudio/uni-app";
+const balance = ref(0);
+const payOption = ref(1);
+const payOptions = ref([50, 100, 200, 500, 1000]);
+const payValue = ref(0);
+const input = (e: any) => {
+  payValue.value = e.value;
+};
+const changeOption = (index: number) => {
+  payValue.value = 0;
+  payOption.value = index;
+};
+const detail = () => {
+  uni.navigateTo({
+    url: "/pages-user/wallet-detail/wallet-detail",
+  });
+};
+const confirm = () => {
+  if (payValue.value && !/^[0-9]*(\.\d{1,2})?$/.test(`${payValue.value}`)) {
+    uni.showModal({
+      title: "温馨提示",
+      content: "请输入正确的金额",
+      showCancel: false,
+      confirmColor: "#347DFF",
+    });
+    return;
+  }
+  const params = payValue.value
+    ? Number(payValue.value)
+    : payOptions.value[payOption.value];
+  if (params > 10000) {
+    uni.showModal({
+      title: "温馨提示",
+      content: "每次最大充值金额10000,请修改金额",
+      showCancel: false,
+      confirmColor: "#347DFF",
+    });
+    return;
+  }
+  insertMoney(params)
+    .then(() => {
+      uni.showToast({
+        title: "已支付",
+        icon: "success",
+      });
+      fetchProfile().then((res) => {
+        payValue.value = 0;
+        balance.value = Number((Number(res.balance) / 100).toFixed(2))
+      });
+    })
+    .catch((err) => {
+      if (/cancel/.test(err.errMsg)) {
+        return;
+      }
+      uni.showModal({
+        content: `${err.errMsg},请重试`,
+      });
+    });
+};
+onLoad(() => {
+  const user = getApp<any>().globalData.user;
+  if (user) {
+    balance.value = user.balance;
+  }
+});
+</script>
+
+<style lang="scss">
+.wallet {
+  height: 220rpx;
+  background: linear-gradient(180deg, #347dff 0%, #4faaff 100%);
+  box-shadow: 0px 12rpx 22rpx rgba(13, 21, 62, 0.1);
+  border-radius: 40rpx;
+  position: relative;
+  margin-top: 100rpx;
+  padding: 44rpx 40rpx;
+
+  .image {
+    position: absolute;
+    width: 148rpx;
+    right: 40rpx;
+    top: -60rpx;
+  }
+
+  .label {
+    font-size: 26rpx;
+    color: rgba(255, 255, 255, 0.8);
+  }
+
+  .value {
+    color: #fff;
+  }
+
+  .tag {
+    position: absolute;
+    width: 170rpx;
+    height: 58rpx;
+    right: 0;
+    bottom: 0;
+    background: var(--color-primary);
+    border-radius: 29rpx 0 29rpx 0;
+  }
+}
+
+.pay {
+  padding-top: 20rpx;
+  .title {
+    font-weight: 500;
+    font-size: 32rpx;
+    color: #000;
+    padding-bottom: 30rpx;
+    padding-top: 40rpx;
+  }
+
+  .option {
+    width: 214rpx;
+    height: 82rpx;
+    background: var(--color-sec);
+    border-radius: 10rpx;
+    margin-left: 20rpx;
+    margin-bottom: 20rpx;
+    &:nth-child(3n + 1) {
+      margin-left: 0;
+    }
+  }
+  .option-active {
+    background-color: var(--color-primary);
+    color: #fff;
+  }
+}
+</style>

+ 42 - 0
src/pages.json

@@ -45,6 +45,48 @@
           "path": "search/search"
         }
       ]
+    },
+    {
+      "root": "pages-user",
+      "pages": [
+        {
+          "path": "collect/collect",
+          "style": {
+            "navigationStyle": "default",
+            "navigationBarTitleText": "我的收藏",
+            "navigationBarBackgroundColor": "#F5F5F5"
+          }
+        },
+        {
+          "path": "profile/profile",
+          "style": {
+            "navigationStyle": "default",
+            "navigationBarTitleText": "个人信息"
+          }
+        },
+        {
+          "path": "profile-edit/profile-edit",
+          "style": {
+            "navigationStyle": "default",
+            "navigationBarTitleText": ""
+          }
+        },
+        {
+          "path": "wallet/wallet",
+          "style": {
+            "navigationStyle": "default",
+            "navigationBarTitleText": "我的钱包"
+          }
+        },
+        {
+          "path": "wallet-detail/wallet-detail",
+          "style": {
+            "navigationStyle": "default",
+            "navigationBarTitleText": "钱包明细",
+            "enablePullDownRefresh": true
+          }
+        }
+      ]
     }
   ],
   "globalStyle": {

+ 8 - 4
src/pages/user/index.vue

@@ -27,8 +27,12 @@
             <view class="fw-500 ml-12" style="font-size: 60rpx">{{
               user.balance
             }}</view>
-            <view class="ml-auto" style="width: 140rpx" @click="toPage(0)">
-              <style-button size="small" type="primary" height="70"
+            <view class="ml-auto" style="width: 140rpx">
+              <style-button
+                @click="toPage(0)"
+                size="small"
+                type="primary"
+                height="70"
                 >充值</style-button
               >
             </view>
@@ -133,11 +137,11 @@ const toPage = (index: number) => {
 };
 const close = () => {
   contactDialogVisible.value = false;
-}
+};
 const emptyTap = () => {};
 const call = () => {
   uni.makePhoneCall({
-    phoneNumber: menu.value[4].mobile as string
+    phoneNumber: menu.value[4].mobile as string,
   });
 };
 onLoad(() => {

+ 3 - 3
src/utils/http.ts

@@ -39,7 +39,7 @@ class Http {
       const urlHasParams = url.indexOf("?") >= 0;
       const header = {
         // 'content-type': 'application/json',
-        Authorization: getApp().globalData.token || "",
+        Authorization: getApp<any>().globalData.token || "",
         ...(this.header || {}),
         ...(options.header || {}),
       };
@@ -67,7 +67,7 @@ class Http {
               data,
               header: {
                 ...header,
-                Authorization: getApp().globalData.token || "",
+                Authorization: getApp<any>().globalData.token || "",
               },
             });
           }
@@ -75,7 +75,7 @@ class Http {
             clearToken();
             setTimeout(() => {
               uni.reLaunch({
-                url: "/pages/map/map",
+                url: "/pages/map/index",
               });
             }, 1000);
             throw {

+ 117 - 0
src/utils/uploader.ts

@@ -0,0 +1,117 @@
+import { host } from "../utils/constant";
+
+type UploadCallback = {
+  onSuccess?(result: { url: string }): void;
+  onFail?(error: any): void;
+  /** [onStart]
+   *
+   * 开始上传回调函数,返回微信UploadTask
+   *
+   */
+  onStart?(task: any): void;
+  onProgressUpdate?(progress: any): void;
+};
+
+type UploadItem = {
+  filePath: string;
+  callback: UploadCallback;
+};
+
+// 上传并行控制
+const UPLOAD_PARALLEL_COUNT = 2;
+const UPLOAD_QUEUE: Array<UploadItem> = [];
+
+// 单独上传一个并限制在上传并行数量中
+export function uploadByQueue(filePath: string, callback: UploadCallback) {
+  if (UPLOAD_QUEUE.length >= UPLOAD_PARALLEL_COUNT) {
+    UPLOAD_QUEUE.push({
+      filePath,
+      callback,
+    });
+    return;
+  }
+  upload(filePath, {
+    onFail: callback.onFail,
+    onStart: callback.onStart,
+    onProgressUpdate: callback.onProgressUpdate,
+    onSuccess: (res) => {
+      // 还有待上传的
+      if (UPLOAD_QUEUE.length) {
+        const item = UPLOAD_QUEUE.shift();
+        if (item) {
+          uploadByQueue(item.filePath, item.callback);
+        }
+      }
+      callback.onSuccess && callback.onSuccess(res);
+    },
+  });
+}
+
+// 单独上传一个
+export function upload(filePath: string, callback: UploadCallback): void {
+  const uploadTask = wxUploadFile(
+    `${host}/common/upload`,
+    filePath,
+    {},
+    {
+      success: (res: any) => {
+        if (res.statusCode == 200) {
+          try {
+            const data = JSON.parse(res.data);
+            callback.onSuccess &&
+              callback.onSuccess({
+                url: data.data.url,
+              });
+          } catch (error) {
+            callback.onFail &&
+              callback.onFail({
+                errMsg: `update error JSON.parse`,
+              });
+          }
+        } else {
+          callback.onFail &&
+            callback.onFail({
+              errMsg: `update error(${JSON.stringify(res)})`,
+            });
+        }
+      },
+      fail: callback.onFail,
+      complete: () => {
+        // 取消监听
+        callback.onProgressUpdate &&
+          uploadTask.offProgressUpdate(callback.onProgressUpdate);
+      },
+    }
+  );
+  callback.onStart && callback.onStart(uploadTask);
+  if (callback.onProgressUpdate) {
+    uploadTask.onProgressUpdate(callback.onProgressUpdate);
+  }
+}
+
+function wxUploadFile(
+  url: string,
+  filePath: string,
+  formData: any,
+  callback: any
+) {
+  return uni.uploadFile({
+    url,
+    filePath,
+    name: "file",
+    formData,
+    success: (res) => {
+      callback.success && callback.success(res);
+    },
+    fail: (err) => {
+      if (err && err.errMsg.indexOf("abort") >= 0) {
+        // 主动中止s
+        return;
+      }
+      callback.fail && callback.fail(err);
+    },
+    complete: () => {
+      callback.complete && callback.complete();
+    },
+  });
+}

+ 1 - 1
src/wxcomponents/navigation-bar/index.js

@@ -17,7 +17,7 @@ Component({
     },
     homePath: {
       type: String,
-      value: '/pages/map/map'
+      value: '/pages/map/index'
     },
     placeholder: {
       type: Boolean,

+ 2 - 2
types/index.d.ts

@@ -1,3 +1,3 @@
-declare const getApp: any
+// declare const getApp: any
 
-declare const uni: any
+// declare const uni: any