21 Revize c383218293 ... 434e42ebae

Autor SHA1 Zpráva Datum
  needcode 434e42ebae feat:电站详情调整 před 2 roky
  needcode dafd6a1b9f fix:地图拖动问题 před 2 roky
  needcode 6dc9842b6d fix:文案 před 2 roky
  needcode 065f3ed839 fix:支付 před 2 roky
  needcode ea924942ba fix:支付 před 2 roky
  needcode 4d25cc1aa1 fix:充电查询 před 2 roky
  needcode 3865c2a6a0 fix:扫码 před 2 roky
  needcode 9ccba07f85 fix:修复字段 před 2 roky
  needcode a48f4dfecd fix:打包问题 před 2 roky
  needcode 2b2e65124b fix:分包问题 před 2 roky
  needcode 566b1b4e80 feat:订单接口 před 2 roky
  needcode abab1a0881 feat:充电接口 před 2 roky
  needcode bbf5cc6cf1 feat:调整接口 před 2 roky
  needcode 8d44547e63 feat:订单 před 2 roky
  needcode a3ed72a245 feat:用户相关页面 před 2 roky
  needcode 7bda220bee feat:我的页面 před 2 roky
  needcode 12f6e3badc feat:主页 před 2 roky
  needcode 6132c9c022 feat:充电页面 před 2 roky
  needcode bfb9b35c93 feat:充电站列表 před 2 roky
  needcode 6c8d77c853 chore:配置迁移 před 2 roky
  needcode 89069c66ce feat:导航栏 před 2 roky
98 změnil soubory, kde provedl 8019 přidání a 131 odebrání
  1. 1 0
      README.md
  2. 2 1
      package.json
  3. 76 12
      src/App.vue
  4. 11 0
      src/App.wxs
  5. 177 0
      src/api/auth.ts
  6. 354 0
      src/api/charge.ts
  7. 8 0
      src/api/index.ts
  8. 109 0
      src/api/user.ts
  9. 44 0
      src/components/charge-icon/charge-icon.vue
  10. 247 0
      src/components/charge-station/charge-station.vue
  11. 7 0
      src/components/demo/index.vue
  12. 51 0
      src/components/shadow-card/shadow-card.vue
  13. 57 0
      src/components/style-bottom-view/style-bottom-view.vue
  14. 100 0
      src/components/style-button/style-button.vue
  15. 69 0
      src/components/style-input/style-input.vue
  16. 1169 0
      src/components/uni-icons/icons.js
  17. 96 0
      src/components/uni-icons/uni-icons.vue
  18. 663 0
      src/components/uni-icons/uniicons.css
  19. binární
      src/components/uni-icons/uniicons.ttf
  20. 112 0
      src/custom-tab-bar/index.js
  21. 3 0
      src/custom-tab-bar/index.json
  22. 35 0
      src/custom-tab-bar/index.wxml
  23. 126 0
      src/custom-tab-bar/index.wxss
  24. 106 67
      src/manifest.json
  25. 53 0
      src/pages-charge/codeing/codeing.vue
  26. 145 0
      src/pages-charge/machines/charge-machine/charge-machine.vue
  27. 467 0
      src/pages-charge/machines/machines.vue
  28. 66 0
      src/pages-charge/order/order.vue
  29. 472 0
      src/pages-charge/ordering/ordering.vue
  30. 76 0
      src/pages-charge/orders/orders.vue
  31. 174 0
      src/pages-charge/search/search.vue
  32. binární
      src/pages-charge/static/charge-input-head.png
  33. binární
      src/pages-charge/static/charge-ordering-bg.png
  34. binární
      src/pages-charge/static/charge-ordering-border.png
  35. binární
      src/pages-charge/static/charge-ordering-finish.png
  36. binární
      src/pages-charge/static/charge-ordering-icon.png
  37. binární
      src/pages-charge/static/machines-banner-address.png
  38. binární
      src/pages-charge/static/machines-banner-nav.png
  39. 49 0
      src/pages-user/collect/collect.vue
  40. 82 0
      src/pages-user/profile-edit/profile-edit.vue
  41. 281 0
      src/pages-user/profile/profile.vue
  42. 142 0
      src/pages-user/wallet-detail/wallet-detail.vue
  43. 191 0
      src/pages-user/wallet/wallet.vue
  44. 136 15
      src/pages.json
  45. 8 35
      src/pages/index/index.vue
  46. 66 0
      src/pages/list/list.vue
  47. 697 0
      src/pages/map/map.vue
  48. 293 0
      src/pages/user/user.vue
  49. binární
      src/static/images/back.png
  50. binární
      src/static/images/custom-tab-bar/1-1.png
  51. binární
      src/static/images/custom-tab-bar/1.png
  52. binární
      src/static/images/custom-tab-bar/2-2.png
  53. binární
      src/static/images/custom-tab-bar/2-3.png
  54. binární
      src/static/images/custom-tab-bar/2-4.png
  55. binární
      src/static/images/custom-tab-bar/2.png
  56. binární
      src/static/images/custom-tab-bar/3-1.png
  57. binární
      src/static/images/custom-tab-bar/3.png
  58. binární
      src/static/images/custom-tab-bar/4-1.png
  59. binární
      src/static/images/custom-tab-bar/4.png
  60. binární
      src/static/images/custom-tab-bar/bg.png
  61. binární
      src/static/images/icon-nav.png
  62. binární
      src/static/images/map-current.png
  63. binární
      src/static/images/map-empty.png
  64. binární
      src/static/images/map-filter.png
  65. binární
      src/static/images/map-location.png
  66. binární
      src/static/images/map-point-current.png
  67. binární
      src/static/images/map-point.png
  68. binární
      src/static/images/map-search.png
  69. binární
      src/static/images/search-close.png
  70. binární
      src/static/images/search-empty.png
  71. binární
      src/static/images/user-bg.png
  72. binární
      src/static/images/user/1.png
  73. binární
      src/static/images/user/2.png
  74. binární
      src/static/images/user/3.png
  75. binární
      src/static/images/user/4.png
  76. binární
      src/static/images/user/5.png
  77. binární
      src/static/images/user/6.png
  78. binární
      src/static/images/user/round.png
  79. binární
      src/static/images/wallet-logo.png
  80. binární
      src/static/logo.png
  81. 11 0
      src/styles/dialog.scss
  82. 101 0
      src/styles/flex.scss
  83. 36 0
      src/styles/font.scss
  84. 43 0
      src/styles/layout.scss
  85. 33 0
      src/utils/code.ts
  86. 16 0
      src/utils/constant.ts
  87. 20 0
      src/utils/date.ts
  88. 159 0
      src/utils/http.ts
  89. 76 0
      src/utils/infinite-scroll.ts
  90. 38 0
      src/utils/location.ts
  91. 112 0
      src/utils/storage.ts
  92. 127 0
      src/utils/uploader.ts
  93. 98 0
      src/wxcomponents/navigation-bar/index.js
  94. 4 0
      src/wxcomponents/navigation-bar/index.json
  95. 18 0
      src/wxcomponents/navigation-bar/index.wxml
  96. 101 0
      src/wxcomponents/navigation-bar/index.wxss
  97. 2 1
      tsconfig.json
  98. 3 0
      types/index.d.ts

+ 1 - 0
README.md

@@ -0,0 +1 @@
+## 无

+ 2 - 1
package.json

@@ -53,6 +53,7 @@
     "@dcloudio/uni-mp-toutiao": "3.0.0-3080720230703001",
     "@dcloudio/uni-mp-weixin": "3.0.0-3080720230703001",
     "@dcloudio/uni-quickapp-webview": "3.0.0-3080720230703001",
+    "sass": "^1.65.1",
     "vue": "^3.2.45",
     "vue-i18n": "^9.1.9"
   },
@@ -63,7 +64,7 @@
     "@dcloudio/uni-stacktracey": "3.0.0-3080720230703001",
     "@dcloudio/vite-plugin-uni": "3.0.0-3080720230703001",
     "@vue/tsconfig": "^0.1.3",
-    "typescript": "^4.9.4",
+    "typescript": "^5.1.6",
     "vite": "4.0.4",
     "vue-tsc": "^1.0.24"
   }

+ 76 - 12
src/App.vue

@@ -1,13 +1,77 @@
-<script setup lang="ts">
-import { onLaunch, onShow, onHide } from "@dcloudio/uni-app";
-onLaunch(() => {
-  console.log("App Launch");
-});
-onShow(() => {
-  console.log("App Show");
-});
-onHide(() => {
-  console.log("App Hide");
-});
+<script lang="ts">
+import { fetchToken } from "./api/auth";
+export default <any>{
+  globalData: {
+    token: "",
+    lastData: {},
+    stations: [],
+    normalCode: "",
+  },
+  onLaunch() {
+    return fetchToken().then((cache) => {
+      if (cache) {
+        this.globalData.token = cache;
+      }
+    });
+  },
+  onPageNotFound() {
+    uni.switchTab({
+      url: "/pages/map/map",
+    });
+  },
+};
 </script>
-<style></style>
+<style lang="scss">
+@import "./styles/flex.scss";
+@import "./styles/font.scss";
+@import "./styles/layout.scss";
+
+view {
+  box-sizing: border-box;
+  --color-primary: #347dff;
+  --color-warning: #ff9900;
+  --color-gray: #666666;
+  --color-sec: #f7f7f7;
+}
+
+image {
+  width: 20rpx;
+  height: 20rpx;
+}
+
+.relative {
+  position: relative;
+}
+
+.color-fff {
+  color: #fff;
+}
+
+.color-000 {
+  color: #000;
+}
+
+.color-333 {
+  color: #333;
+}
+
+.color-666 {
+  color: #666;
+}
+
+.color-999 {
+  color: #999;
+}
+
+.color-primary {
+  color: var(--color-primary);
+}
+
+.color-warning {
+  color: var(--color-warning);
+}
+
+.transition {
+  transition: all 0.3s;
+}
+</style>

+ 11 - 0
src/App.wxs

@@ -0,0 +1,11 @@
+{
+  /* <script module="utils" lang="wxs" src="./utils.wxs"></script> */
+}
+
+function formatPrice(price) {
+  return (price / 100).toFixed(2);
+}
+
+module.exports = {
+  formatPrice: formatPrice
+};

+ 177 - 0
src/api/auth.ts

@@ -0,0 +1,177 @@
+import { host } from "../utils/constant";
+import Storage from "../utils/storage";
+
+const _tokenStorage = new Storage("AUTH");
+
+const _tokenQueue: {
+  pending: boolean;
+  list: {
+    resolve: (value: string) => void;
+    reject: (err: any) => void;
+  }[];
+} = {
+  pending: false,
+  list: [],
+};
+
+let _onTokenListener: {
+  cb: (token: string) => void;
+}[] = [];
+
+const _resolveTokenQueue = function (res: any) {
+  _tokenQueue.pending = false;
+  uni.hideLoading();
+  if (res.errMsg) {
+    console.log(res);
+    uni.showModal({
+      title: "登录失败",
+      content: `${res.errMsg}`,
+    });
+    _tokenQueue.list.forEach((item) => {
+      item.reject(res);
+    });
+  } else {
+    uni.showToast({
+      icon: "success",
+      title: "登录成功",
+    });
+    setToken(res as string);
+    _onTokenListener.forEach((item) => {
+      item.cb(res as string);
+    });
+    _tokenQueue.list.forEach((item) => {
+      item.resolve(res as string);
+    });
+  }
+  _onTokenListener = [];
+  _tokenQueue.list = [];
+};
+
+export function login(e: any): Promise<string> {
+  return new Promise((resolve, reject) => {
+    if (/deny|cancel/.test(e.detail.errMsg)) {
+      reject();
+      return;
+    }
+    if (!e.detail.code) {
+      uni.showModal({
+        title: `${e.detail.errMsg},请重试`,
+      });
+      reject();
+      return;
+    }
+    _tokenQueue.list.push({
+      resolve,
+      reject,
+    });
+    if (_tokenQueue.pending) {
+      return;
+    }
+    _tokenQueue.pending = true;
+    uni.showLoading({
+      title: "登录中",
+      mask: true,
+    });
+    uni.login({
+      success: (res) => {
+        uni.request<any>({
+          url: `${host}/user/wxLogin`,
+          method: "POST",
+          dataType: "json",
+          data: {
+            phoneCode: e.detail.code,
+            code: res.code,
+            avatar: "",
+            nickname: "",
+          },
+          success: (res: any) => {
+            const { statusCode, data } = res;
+            if (
+              statusCode === 200 &&
+              data &&
+              data.message === "ok" &&
+              data.code === 200
+            ) {
+              _resolveTokenQueue(data.data.satoken);
+            } else {
+              _resolveTokenQueue({
+                errMsg: data.msg ? data.msg : `${JSON.stringify(res)}`,
+              });
+            }
+          },
+          fail: _resolveTokenQueue,
+        });
+      },
+      fail: _resolveTokenQueue,
+    });
+  });
+}
+
+export function refresh(): Promise<string> {
+  return new Promise((resolve, reject) => {
+    _tokenStorage.get("token").then((token) => {
+      if (!token) {
+        clearToken();
+        reject({
+          errMsg: "请登录",
+        });
+        uni.reLaunch({
+          url: "/pages/map/map",
+        });
+      } else {
+        getApp<any>().globalData.token = "";
+        _tokenStorage.clear("token");
+        uni.request({
+          url: `${host}/user/refresh`,
+          method: "GET",
+          dataType: "json",
+          header: {
+            Authorization: token,
+          },
+          success: (res: any) => {
+            console.log("refresh返回", res);
+            const { statusCode, data } = res;
+            if (
+              statusCode === 200 &&
+              data &&
+              data.msg === "OK" &&
+              data.code === 200
+            ) {
+              resolve(data.data.access_token);
+            } else {
+              if (data.code === 21005) {
+                uni.reLaunch({
+                  url: "/pages/map/map",
+                });
+              }
+              reject({
+                errMsg: `${JSON.stringify(res)}`,
+              });
+            }
+          },
+          fail: reject,
+        });
+      }
+    });
+  });
+}
+
+export function onLogin(cb: (token: string) => void) {
+  _onTokenListener.push({
+    cb,
+  });
+}
+
+export function fetchToken() {
+  return _tokenStorage.get("token");
+}
+
+export function setToken(token: string) {
+  getApp<any>().globalData.token = token;
+  return _tokenStorage.set("token", token);
+}
+
+export function clearToken() {
+  getApp<any>().globalData.token = "";
+  return _tokenStorage.clear("token");
+}

+ 354 - 0
src/api/charge.ts

@@ -0,0 +1,354 @@
+import Http from "../utils/http";
+import { host } from "../utils/constant";
+const cHttp = new Http(host);
+
+export function startCharge(sn: string) {
+  return cHttp.get<{
+    ConnectorID: string;
+    FailReason: number;
+    StartChargeSeq: string;
+    StartChargeSeqStat: number;
+    SuccStat: number;
+  }>(`/charge/startCharge/${sn}`, {
+    statusCodeHandle: false,
+  });
+}
+
+export function fetchStationPriceDesc(ConnectorID: string, StationID?: string) {
+  return cHttp.get(`/charge/businessPolicy/${ConnectorID}`).then((res) => {
+    const nowHour = new Date().getHours();
+    let maxPrice = 0;
+    let minPrice = 9;
+    let currentPrice = 0;
+    let currentTime = "00:00~24:00";
+    res.useTime = "";
+    if (res && res.policyInfoss && res.policyInfoss.length) {
+      res.policyInfoss.forEach((item: any, index: number) => {
+        const hour = item.startTime.substring(0, 2);
+        const min = item.startTime.substring(3, 5);
+        if (index === 0) {
+          res.useTime = `${hour}:${min}~24:00`;
+        }
+        let tempPrice = Number(
+          Number(item.elecPrice + item.servicePrice).toFixed(2)
+        );
+        if (tempPrice > maxPrice) {
+          maxPrice = tempPrice;
+        }
+        if (tempPrice < minPrice) {
+          minPrice = tempPrice;
+        }
+        if (index >= res.policyInfoss.length - 1) {
+          // 最后一个
+          item.startTimeFormat = `${hour}:${min}~24:00`;
+          if (Number(hour) <= nowHour) {
+            currentPrice = tempPrice;
+            currentTime = `${hour}:${min}~24:00`;
+          }
+        } else {
+          const nhour = res.policyInfoss[index + 1].startTime.substring(0, 2);
+          const nmin = res.policyInfoss[index + 1].startTime.substring(3, 5);
+          item.startTimeFormat = `${hour}:${min}~${nhour}:${nmin}`;
+          if (nowHour >= Number(hour) && nowHour < Number(nhour)) {
+            currentPrice = tempPrice;
+            currentTime = `${hour}:${min}~${nhour}:${nmin}`;
+          }
+        }
+      });
+    }
+    res.maxPrice = maxPrice;
+    res.minPrice = minPrice;
+    res.currentPrice = currentPrice;
+    res.currentTime = currentTime;
+    if (StationID) {
+      res.StationID = StationID;
+    }
+    return res;
+  });
+}
+
+export function cancelCharge(sn: string) {
+  return cHttp.get(`/charge/stopCharge/${sn}`, {
+    statusCodeHandle: false,
+  });
+}
+
+export function fetchChargeStatus() {
+  return cHttp.get("/charge/chargeStatus", {
+    statusCodeHandle: false,
+  });
+}
+
+export function fetchStations(
+  page: number,
+  pageSize: number,
+  latitude?: number,
+  longitude?: number,
+  baseLatitude?: number,
+  baseLongitude?: number,
+  options?: {
+    distance?: number;
+    status?: number;
+  }
+): Promise<any[]> {
+  return fetchAllStations()
+    .then((res) => {
+      // console.log(latitude, longitude);
+      let list = JSON.parse(JSON.stringify(res));
+      const data: any[] = [];
+      const start = (page - 1) * pageSize;
+      const end = start + pageSize;
+      if (latitude && longitude) {
+        list.forEach((item: any) => {
+          item.stationLatDistance = _getDistance(
+            latitude,
+            longitude,
+            item.location.stationLat,
+            item.location.stationLng
+          );
+        });
+        list.sort((item1: any, item2: any) => {
+          return item1.stationLatDistance - item2.stationLatDistance;
+        });
+      }
+      if (options) {
+        if (options.distance) {
+          list = list.filter(
+            (item: any) => item.stationLatDistance <= (options.distance || 20)
+          );
+        }
+      }
+      list.forEach((item: any, index: number) => {
+        if (index >= start && index < end) {
+          data.push(item);
+        }
+      });
+      if (baseLatitude && baseLongitude) {
+        data.forEach((item: any) => {
+          item.stationLatDistance = _getDistance(
+            baseLatitude,
+            baseLongitude,
+            item.location.stationLat,
+            item.location.stationLng
+          );
+        });
+      }
+      return _fetchStationStatus(data);
+    })
+    .then((list) => {
+      if (options && options.status) {
+        let res = false;
+        list = list.filter((item: any) => {
+          res = false;
+          item.equipmentInfos.forEach((eqInfo: any) => {
+            eqInfo.connectorInfos.forEach((coInfo: any) => {
+              if (
+                !res &&
+                options.status === 1 &&
+                coInfo.connectorStatusInfo &&
+                coInfo.connectorStatusInfo.status === 1
+              ) {
+                res = true;
+              }
+              if (
+                !res &&
+                options.status === 2 &&
+                coInfo.connectorStatusInfo &&
+                coInfo.connectorStatusInfo.status !== 1
+              ) {
+                res = true;
+              }
+            });
+          });
+          return res;
+        });
+      }
+      return list;
+    });
+}
+
+export function fetchStation(id: number) {
+  return fetchAllStations()
+    .then((res) => {
+      const findIndex = res.findIndex((item) => Number(item.StationID) === id);
+      if (findIndex < 0) {
+        throw {
+          errMsg: "not found",
+        };
+      }
+      return _fetchStationStatus([res[findIndex]]);
+    })
+    .then((list) => {
+      return list[0];
+    });
+}
+
+export function fetchStationByIds(ids: number[]) {
+  return fetchAllStations().then((res) => {
+    const list = res.filter((item) => ids.includes(Number(item.StationID)));
+    return _fetchStationStatus(list);
+  });
+}
+
+export function searchStation(keyword: string) {
+  return fetchAllStations().then((res) => {
+    const reg = new RegExp(keyword, "ig");
+    const list = res.filter(
+      (item) => reg.test(item.StationName) || reg.test(item.Address)
+    );
+    return _fetchStationStatus(list);
+  });
+}
+
+export function fetchAllStations(): Promise<any[]> {
+  if (getApp<any>().globalData.stations.length > 0) {
+    return Promise.resolve(getApp<any>().globalData.stations);
+  }
+  const page = 1;
+  const page_size = 99;
+  return new Promise((resolve, reject) => {
+    _fetchAllStations(page, page_size, [])
+      .then((list) => {
+        // if (!isProduction) {
+        //   list.push({
+        //     ...list[0],
+        //     StationName: '这是模拟数据,不要使用充电',
+        //     Address: '这是模拟数据,测试一下附近的点',
+        //     StationLat: 22.540545,
+        //     StationLng: 113.942695,
+        //     StationID: '0000'
+        //   })
+        // }
+        console.log("所有电站数据", list);
+        getApp<any>().globalData.stations = list;
+        resolve(list);
+      })
+      .catch(reject);
+  });
+}
+
+function _fetchAllStations(
+  page: number,
+  pageSize: number,
+  list: any[]
+): Promise<any[]> {
+  return _fetchStations(page, pageSize).then((res) => {
+    list = list.concat(res);
+    if (res.length >= pageSize) {
+      return _fetchAllStations(page + 1, pageSize, list);
+    } else {
+      // eslint-disable-next-line promise/no-return-wrap
+      return Promise.resolve(list);
+    }
+  });
+}
+
+function _fetchStations(page: number, pageSize: number) {
+  return cHttp
+    .get(`/charge/listStation?pageNum=${page}&pageSize=${pageSize}`)
+    .then((res) => {
+      const data = res || [];
+      data.forEach((item: any) => {
+        item.StationID = item.stationId;
+        item.fastEquipmentInfos = [];
+        item.slowEquipmentInfos = [];
+        item.totalFee = Number(
+          Number(
+            Number(item.electricityFee) +
+              Number(item.parkFee) +
+              Number(item.serviceFee)
+          ).toFixed(2)
+        );
+      });
+      return data.filter((item: any) => {
+        return !["1657"].includes(item.StationID);
+      });
+    });
+}
+
+function _getDistance(lat1: number, lng1: number, lat2: number, lng2: number) {
+  var radLat1 = (lat1 * Math.PI) / 180.0;
+  var radLat2 = (lat2 * Math.PI) / 180.0;
+  var a = radLat1 - radLat2;
+  var b = (lng1 * Math.PI) / 180.0 - (lng2 * Math.PI) / 180.0;
+  var s =
+    2 *
+    Math.asin(
+      Math.sqrt(
+        Math.pow(Math.sin(a / 2), 2) +
+          Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2)
+      )
+    );
+  s = s * 6378.137; // EARTH_RADIUS;
+  s = Math.round(s * 10000) / 10000;
+  return s;
+}
+
+function _fetchStationStatus(list: any[]) {
+  if (list.length <= 0) {
+    return Promise.resolve([]);
+  }
+  let _list: any[] = [];
+  return cHttp
+    .get(
+      `/charge/stationStatus?stationIds=${list
+        .filter((item) => item.StationID !== "0000")
+        .map((item) => item.StationID)
+        .join(",")}`
+    )
+    .then((res) => {
+      const StationStatusInfos = res || [];
+      const ConnectorIDs: string[] = [];
+      const StationIDs: string[] = [];
+      list.forEach((item: any) => {
+        item.fastEquipmentInfos = [];
+        item.slowEquipmentInfos = [];
+        const StationStatusInfo = StationStatusInfos.find(
+          (status: any) => Number(status.stationId) === Number(item.StationID)
+        );
+        let ConnectorID = "";
+        item.equipmentInfos.forEach((eqInfo: any) => {
+          eqInfo.connectorInfos.forEach((coInfo: any) => {
+            if (StationStatusInfo) {
+              const connectorStatusInfo =
+                StationStatusInfo.connectorStatusInfos.find(
+                  (costatus: any) => costatus.connectorId === coInfo.connectorId
+                );
+              if (connectorStatusInfo) {
+                coInfo.connectorStatusInfo = connectorStatusInfo;
+              }
+            }
+            if (!ConnectorID) {
+              ConnectorID = coInfo.connectorId;
+            }
+          });
+          if ([1].includes(Number(eqInfo.equipmentType))) {
+            item.fastEquipmentInfos.push(eqInfo);
+          } else {
+            item.slowEquipmentInfos.push(eqInfo);
+          }
+        });
+        if (ConnectorID) {
+          ConnectorIDs.push(ConnectorID);
+          StationIDs.push(item.StationID);
+        }
+      });
+      _list = list;
+      return Promise.all(
+        ConnectorIDs.map((cid, cindex) => {
+          return fetchStationPriceDesc(cid, StationIDs[cindex]);
+        })
+      );
+    })
+    .then((res) => {
+      if (res && res.length) {
+        _list.forEach((item) => {
+          const i = res.findIndex((r) => r.StationID === item.StationID);
+          if (i >= 0) {
+            item.totalFee = res[i].currentPrice;
+          }
+        });
+      }
+      return _list;
+    });
+}

+ 8 - 0
src/api/index.ts

@@ -0,0 +1,8 @@
+import Http from '../utils/http'
+import { host } from '../utils/constant'
+
+const indexHttp = new Http(host)
+
+export function fetchContact() {
+  return indexHttp.get('/common/contact')
+}

+ 109 - 0
src/api/user.ts

@@ -0,0 +1,109 @@
+import Http from "../utils/http";
+import { host } from "../utils/constant";
+
+const userHttp = new Http(host);
+
+export function fetchProfile() {
+  return userHttp.get("/user/me").then((res) => {
+    getApp<any>().globalData.user = res;
+    return res;
+  });
+}
+
+export function fetchCollectList() {
+  if (getApp<any>().globalData.collectIds) {
+    return Promise.resolve(getApp<any>().globalData.collectIds);
+  }
+  return userHttp
+    .get<
+      {
+        status: number;
+        stationId: number;
+      }[]
+    >("/user/collectList?page=1&page_size=999")
+    .then((res) => {
+      getApp<any>().globalData.collectIds = res
+        ? res
+            .filter((item) => Number(item.status) === 1)
+            .map((item) => {
+              return Number(item.stationId);
+            })
+        : [];
+      return getApp<any>().globalData.collectIds;
+    });
+}
+
+export function addCollectList(sid: number) {
+  let ids = getApp<any>().globalData.collectIds
+    ? (getApp<any>().globalData.collectIds as number[])
+    : [];
+  const status = ids.includes(sid) ? 0 : 1;
+  return userHttp
+    .post("/user/collect", {
+      data: {
+        stationId: sid,
+        status,
+      },
+    })
+    .then(() => {
+      if (status === 1) {
+        ids.push(sid);
+      } else {
+        ids = ids.filter((id) => id !== sid);
+      }
+      getApp<any>().globalData.collectIds = ids;
+      return status === 1;
+    });
+}
+
+export function updateProfile(data: any) {
+  return userHttp.put("/user", {
+    data,
+  });
+}
+
+declare const wx: any;
+
+export function insertMoney(amount: number) {
+  return userHttp
+    .post("/payment/wxPay", {
+      data: {
+        amount: parseInt(`${amount * 100}`),
+        openid: getApp<any>().globalData.user.openid,
+      },
+    })
+    .then((res: any) => {
+      return new Promise((resolve, reject) => {
+        // #ifdef MP-WEIXIN
+        wx.requestPayment({
+          timeStamp: `${res.timeStamp}`,
+          nonceStr: res.nonceStr,
+          package: res.packageVal,
+          signType: res.signType,
+          paySign: res.paySign,
+          success(res: any) {
+            resolve(res);
+          },
+          fail: reject,
+        });
+        // #endif
+        // #ifndef MP-WEIXIN
+        reject({
+          errMsg: "目前仅支持微信支付",
+        });
+        // #endif
+      });
+    });
+}
+
+export function logout() {
+  return userHttp.get("/user/logout");
+}
+
+export function fetchWallet(type: number, page: number, pageSize: number) {
+  return userHttp.get(`/account/walletDetail?page=${page}&page_size=${pageSize}&type=${type}`);
+}
+
+export function fetchOrder(orderid: string) {
+  return userHttp.get(`/charge/orderDetail/${orderid}`);
+}

+ 44 - 0
src/components/charge-icon/charge-icon.vue

@@ -0,0 +1,44 @@
+<template>
+  <view class="charge-icon" :class="[`charge-icon-${type}`]">
+    {{ type ? label[type] : "" }}
+  </view>
+</template>
+
+<script lang="ts">
+export default {
+  props: {
+    type: {
+      type: String,
+    },
+  },
+  data() {
+    return {
+      label: {
+        fast: "快",
+        slow: "慢",
+      },
+    };
+  },
+};
+</script>
+
+<style lang="scss">
+.charge-icon {
+  height: 32rpx;
+  width: 32rpx;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border-radius: 50%;
+  font-size: 20rpx;
+  color: #fff;
+}
+
+.charge-icon-fast {
+  background: linear-gradient(154.59deg, #ffe767 11.86%, #f2751a 87.4%);
+}
+
+.charge-icon-slow {
+  background: linear-gradient(154.59deg, #5ff19a 11.86%, #1aa4f2 87.4%);
+}
+</style>

+ 247 - 0
src/components/charge-station/charge-station.vue

@@ -0,0 +1,247 @@
+<template>
+  <view
+    class="charge-station"
+    :style="{
+      border: border ? '1rpx solid rgba(0, 0, 0, 0.1)' : 'none',
+      'border-radius': fromMap ? '20rpx 20rpx 0 0' : '20rpx',
+    }"
+    @click="detail"
+  >
+    <view class="flex-between">
+      <view>
+        <view class="fs-32 fw-blod">{{ title }}</view>
+        <view v-if="tag">
+          <view class="flex-center tag">{{ tagMap[tag] }}</view>
+        </view>
+        <view v-else class="fs-22" style="color: rgba(0, 0, 0, 0.5)"
+          >{{ address }} | {{ distance }}km</view
+        >
+      </view>
+      <view class="nav flex-center flex-shrink" @click.stop="nav">
+        <image src="/static/images/icon-nav.png" mode="widthFix" />
+        <text>导航</text>
+      </view>
+    </view>
+    <view class="mt-20 flex">
+      <view class="status flex-column flex-grow">
+        <view class="flex-align-center" v-if="price">
+          <text class="fs-32 fw-600" style="color: var(--color-warning)">{{
+            price
+          }}</text>
+          <text class="fs-22 ml-6" style="color: var(--color-gray)">元/度</text>
+        </view>
+        <view class="flex-align-center lh-0 mt-auto" v-if="!fromMap">
+          <charge-icon type="fast"></charge-icon>
+          <view class="fs-22 ml-8" style="color: var(--color-gray)" v-if="fast"
+            >空闲:{{ freeLength(fast) }}|共:{{ fast.length }}</view
+          >
+          <view class="ml-60">
+            <charge-icon type="slow"></charge-icon>
+          </view>
+          <view class="fs-22 ml-8" style="color: var(--color-gray)" v-if="slow"
+            >空闲:{{ freeLength(slow) }}|共:{{ slow.length }}</view
+          >
+        </view>
+        <block v-else>
+          <view class="lh-0 mt-18 fs-22" style="color: #999"
+            >以场地方公告为准!</view
+          >
+          <view
+            class="top-right-bound flex-align-center lh-32"
+            v-if="slow && slow.length"
+          >
+            <charge-icon type="slow"></charge-icon>
+            <view class="ml-6 fs-22" style="color: var(--color-gray)"
+              >{{ freeLength(slow) }} / {{ slow.length }}</view
+            >
+          </view>
+          <view
+            class="top-right-bound flex-align-center lh-32"
+            v-if="fast && fast.length"
+          >
+            <charge-icon type="fast"></charge-icon>
+            <view class="ml-6 fs-22" style="color: var(--color-gray)"
+              >{{ freeLength(fast) }} / {{ fast.length }}</view
+            >
+          </view>
+        </block>
+      </view>
+      <view
+        class="btn flex-shrink flex-column flex-center"
+        @click.stop="collect"
+      >
+        <uni-icons
+          :type="collected ? 'star-filled' : 'star'"
+          size="20"
+          :color="collected ? 'var(--color-warning)' : '#999999'"
+        ></uni-icons>
+        <view
+          class="fs-22 mt-6"
+          :style="{
+            color: collected ? 'var(--color-warning)' : '#999999',
+          }"
+          >收藏</view
+        >
+      </view>
+    </view>
+  </view>
+</template>
+
+<script lang="ts">
+import { fetchCollectList, addCollectList } from "../../api/user";
+export default {
+  props: {
+    sId: {
+      type: String,
+    },
+    title: String,
+    address: String,
+    tag: Number,
+    price: String,
+    fast: Array,
+    slow: Array,
+    latitude: Number,
+    longitude: Number,
+    distance: String,
+    border: Boolean,
+    fromMap: Boolean,
+  },
+  watch: {
+    sId: {
+      handler: function (newV) {
+        if (newV) {
+          fetchCollectList().then((res) => {
+            this.collected = res ? res.includes(Number(newV)) : false;
+          });
+        }
+      },
+      immediate: true,
+    },
+  },
+  data() {
+    return {
+      collected: false,
+      tagMap: [
+        "",
+        "居民区",
+        "公共机构",
+        "企事业单位",
+        "写字楼",
+        "工业园区",
+        "交通枢纽",
+        "大型文体设施",
+        "城市绿地",
+        "大型建筑配建停车场",
+        "路边停车位",
+        "城际高速服务区",
+      ],
+    };
+  },
+  methods: {
+    detail() {
+      const { address, latitude, longitude } = this;
+      getApp<any>().globalData.lastData.station = {
+        address,
+        latitude,
+        longitude,
+      };
+      uni.navigateTo({
+        url: `/pages-charge/machines/machines?title=${this.title}&id=${this.sId}`,
+      });
+    },
+    collect() {
+      addCollectList(Number(this.sId)).then((collected) => {
+        collected &&
+          uni.vibrateShort &&
+          uni.vibrateShort({
+            type: "light",
+          });
+        this.collected = collected;
+      });
+    },
+    nav() {
+      const { latitude, longitude, title, address } = this;
+      uni.openLocation({
+        latitude,
+        longitude,
+        scale: 18,
+        name: title,
+        address,
+      });
+    },
+    freeLength(infos: any[]) {
+      var length = 0;
+      infos.forEach(function (item) {
+        item.connectorInfos.forEach(function (conItem: any) {
+          if (
+            conItem.connectorStatusInfo &&
+            conItem.connectorStatusInfo.status === 1
+          ) {
+            length += 1;
+          }
+        });
+      });
+      return length;
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+.charge-station {
+  background: #ffffff;
+  width: 100%;
+  padding: 30rpx;
+
+  .tag {
+    display: inline-block;
+    box-sizing: content-box;
+    padding: 0px 6rpx;
+    height: 34rpx;
+    background: var(--color-sec);
+    border-radius: 4rpx;
+    color: #999999;
+    font-size: 22rpx;
+  }
+
+  .nav {
+    width: 120rpx;
+    height: 60rpx;
+    border-radius: 30rpx;
+    background-color: var(--color-primary);
+    color: #fff;
+    font-size: 26rpx;
+
+    image {
+      width: 24rpx;
+      margin-right: 8rpx;
+    }
+  }
+
+  .status {
+    background: var(--color-sec);
+    border-radius: 10rpx;
+    padding: 16rpx 20rpx;
+    position: relative;
+  }
+
+  .top-right-bound {
+    position: absolute;
+    top: 18rpx;
+    right: 26rpx;
+    padding: 4rpx 10rpx 4rpx 4rpx;
+    background: #ffffff;
+    border: 1rpx solid #44deba;
+    border-radius: 20rpx;
+  }
+
+  .btn {
+    height: 118rpx;
+    width: 64rpx;
+    margin-left: 10rpx;
+    background: var(--color-sec);
+    border-radius: 10rpx;
+    transition: all 0.3s;
+  }
+}
+</style>

+ 7 - 0
src/components/demo/index.vue

@@ -0,0 +1,7 @@
+<template>
+  <view class="list"> </view>
+</template>
+
+<script setup lang="ts"></script>
+
+<style></style>

+ 51 - 0
src/components/shadow-card/shadow-card.vue

@@ -0,0 +1,51 @@
+<template>
+  <view class="shadow-card">
+    <view v-for="(item, index) in list" :key="index">
+      <view class="label">{{ item.label }}</view>
+      <view class="value">{{ item.value }}</view>
+    </view>
+  </view>
+</template>
+
+<script lang="ts">
+export default {
+  props: {
+    list: {
+      type: Array<any>,
+      defalut: [],
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+.shadow-card {
+  background: #ffffff;
+  border: 1rpx solid rgba(0, 0, 0, 0.1);
+  box-shadow: 0px 4rpx 20rpx rgba(0, 0, 0, 0.1);
+  border-radius: 20rpx;
+  padding: 40rpx;
+
+  & > view {
+    margin-top: 40rpx;
+    display: flex;
+
+    &:first-child {
+      margin-top: 0rpx;
+    }
+
+    .label {
+      width: 156rpx;
+      flex-shrink: 0;
+      color: rgba(0, 0, 0, 0.4);
+      font-size: 26rpx;
+    }
+
+    .value {
+      flex-grow: 1;
+      color: #000;
+      font-size: 26rpx;
+    }
+  }
+}
+</style>

+ 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>

+ 100 - 0
src/components/style-button/style-button.vue

@@ -0,0 +1,100 @@
+<template>
+  <button
+    class="button ext-class"
+    hover-class="none"
+    :openType="openType"
+    :style="{
+      display: `var(--display-${size})`,
+      width: `var(--width-${size})`,
+      height: `${height ? height + 'rpx' : 'var(--height-' + size + ')'}`,
+      'background-color': `var(--bg-${type}${border ? '-border' : ''})`,
+      color: `var(--color-${type}${border ? '-border' : ''})`,
+      'border-color': `var(--border-${type}${border ? '-border' : ''})`,
+      'font-size': `var(--fs-${size})`,
+      'border-radius': `var(--height-${size})`,
+      'font-weight': 500,
+    }"
+    @click.stop="$emit('click')"
+  >
+    <slot></slot>
+  </button>
+</template>
+
+<script lang="ts">
+export default {
+  emits: ["click"],
+  options: {
+    virtualHost: true,
+  },
+  externalClasses: ["ext-class"],
+  props: {
+    type: {
+      type: String,
+      default: "default",
+    },
+    size: {
+      type: String,
+      default: "medium", // 'medium' 'small'
+    },
+    border: Boolean,
+    openType: String,
+    height: String,
+  },
+};
+</script>
+
+<style lang="scss">
+button::after {
+  content: "";
+  border: none;
+}
+
+.button {
+  --bg-default: #ffffff;
+  --bg-primary: #347dff;
+  --bg-warning: #ff9900;
+  --bg-error: red;
+  --color-default: #666666;
+  --color-primary: #ffffff;
+  --color-warning: #ffffff;
+  --color-error: #ffffff;
+  --border-default: #999999;
+  --border-primary: #347dff;
+  --border-warning: #ff9900;
+  --border-error: red;
+
+  --bg-default-border: #ffffff;
+  --bg-primary-border: #ffffff;
+  --bg-warning-border: #ffffff;
+  --bg-error-border: #ffffff;
+  --color-default-border: #666666;
+  --color-primary-border: #347dff;
+  --color-warning-border: #ff9900;
+  --color-error-border: red;
+  --border-default-border: #666666;
+  --border-primary-border: #347dff;
+  --border-warning-border: #ff9900;
+  --border-error-border: red;
+
+  --display-medium: flex;
+  --display-small: inline-flex;
+  --height-medium: 96rpx;
+  --height-small: 82rpx;
+  --width-medium: 100%;
+  --width-small: 100%;
+  --fs-medium: 32rpx;
+  --fs-small: 30rpx;
+
+  position: relative;
+  justify-content: center;
+  align-items: center;
+  line-height: normal;
+  padding: 0;
+  margin: 0;
+  margin-right: 0;
+  margin-left: 0;
+  box-sizing: border-box;
+  border-width: 1px;
+  border-style: solid;
+}
+</style>

+ 69 - 0
src/components/style-input/style-input.vue

@@ -0,0 +1,69 @@
+<template>
+  <view class="input">
+    <input
+      class="input-ele"
+      :value="value"
+      :focus="focus"
+      :type="type"
+      placeholder-class="placeholder"
+      :placeholder="'请输入' + title"
+      bindinput="input"
+      bindfocus="inputFocus"
+      bindblur="inputBlur"
+      @input="input"
+      @focus="inputFocus"
+      @blur="inputBlur"
+    />
+  </view>
+</template>
+
+<script lang="ts">
+export default {
+  emits: ["input", "focus"],
+  options: {
+    virtualHost: true,
+  },
+  props: {
+    type: String,
+    title: String,
+    value: String,
+    focus: Boolean,
+  },
+  methods: {
+    input(e: any) {
+      this.$emit("input", e.detail);
+    },
+    inputBlur() {
+      this.$emit("focus", {
+        focus: false,
+      });
+    },
+    inputFocus(e: any) {
+      this.$emit("focus", {
+        focus: true,
+        height: e.detail.height,
+      });
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+.input {
+  height: 84rpx;
+  background: #f7f7f7;
+  border-radius: 10rpx;
+  font-size: 30rpx;
+  display: flex;
+  align-items: center;
+  padding: 0 40rpx;
+}
+
+.placeholder {
+  color: rgba(0, 0, 0, 0.3);
+}
+
+.input-ele {
+  font-size: 30rpx;
+}
+</style>

+ 1169 - 0
src/components/uni-icons/icons.js

@@ -0,0 +1,1169 @@
+export default {
+  "id": "2852637",
+  "name": "uniui图标库",
+  "font_family": "uniicons",
+  "css_prefix_text": "uniui-",
+  "description": "",
+  "glyphs": [
+    {
+      "icon_id": "25027049",
+      "name": "yanse",
+      "font_class": "color",
+      "unicode": "e6cf",
+      "unicode_decimal": 59087
+    },
+    {
+      "icon_id": "25027048",
+      "name": "wallet",
+      "font_class": "wallet",
+      "unicode": "e6b1",
+      "unicode_decimal": 59057
+    },
+    {
+      "icon_id": "25015720",
+      "name": "settings-filled",
+      "font_class": "settings-filled",
+      "unicode": "e6ce",
+      "unicode_decimal": 59086
+    },
+    {
+      "icon_id": "25015434",
+      "name": "shimingrenzheng-filled",
+      "font_class": "auth-filled",
+      "unicode": "e6cc",
+      "unicode_decimal": 59084
+    },
+    {
+      "icon_id": "24934246",
+      "name": "shop-filled",
+      "font_class": "shop-filled",
+      "unicode": "e6cd",
+      "unicode_decimal": 59085
+    },
+    {
+      "icon_id": "24934159",
+      "name": "staff-filled-01",
+      "font_class": "staff-filled",
+      "unicode": "e6cb",
+      "unicode_decimal": 59083
+    },
+    {
+      "icon_id": "24932461",
+      "name": "VIP-filled",
+      "font_class": "vip-filled",
+      "unicode": "e6c6",
+      "unicode_decimal": 59078
+    },
+    {
+      "icon_id": "24932462",
+      "name": "plus_circle_fill",
+      "font_class": "plus-filled",
+      "unicode": "e6c7",
+      "unicode_decimal": 59079
+    },
+    {
+      "icon_id": "24932463",
+      "name": "folder_add-filled",
+      "font_class": "folder-add-filled",
+      "unicode": "e6c8",
+      "unicode_decimal": 59080
+    },
+    {
+      "icon_id": "24932464",
+      "name": "yanse-filled",
+      "font_class": "color-filled",
+      "unicode": "e6c9",
+      "unicode_decimal": 59081
+    },
+    {
+      "icon_id": "24932465",
+      "name": "tune-filled",
+      "font_class": "tune-filled",
+      "unicode": "e6ca",
+      "unicode_decimal": 59082
+    },
+    {
+      "icon_id": "24932455",
+      "name": "a-rilidaka-filled",
+      "font_class": "calendar-filled",
+      "unicode": "e6c0",
+      "unicode_decimal": 59072
+    },
+    {
+      "icon_id": "24932456",
+      "name": "notification-filled",
+      "font_class": "notification-filled",
+      "unicode": "e6c1",
+      "unicode_decimal": 59073
+    },
+    {
+      "icon_id": "24932457",
+      "name": "wallet-filled",
+      "font_class": "wallet-filled",
+      "unicode": "e6c2",
+      "unicode_decimal": 59074
+    },
+    {
+      "icon_id": "24932458",
+      "name": "paihangbang-filled",
+      "font_class": "medal-filled",
+      "unicode": "e6c3",
+      "unicode_decimal": 59075
+    },
+    {
+      "icon_id": "24932459",
+      "name": "gift-filled",
+      "font_class": "gift-filled",
+      "unicode": "e6c4",
+      "unicode_decimal": 59076
+    },
+    {
+      "icon_id": "24932460",
+      "name": "fire-filled",
+      "font_class": "fire-filled",
+      "unicode": "e6c5",
+      "unicode_decimal": 59077
+    },
+    {
+      "icon_id": "24928001",
+      "name": "refreshempty",
+      "font_class": "refreshempty",
+      "unicode": "e6bf",
+      "unicode_decimal": 59071
+    },
+    {
+      "icon_id": "24926853",
+      "name": "location-ellipse",
+      "font_class": "location-filled",
+      "unicode": "e6af",
+      "unicode_decimal": 59055
+    },
+    {
+      "icon_id": "24926735",
+      "name": "person-filled",
+      "font_class": "person-filled",
+      "unicode": "e69d",
+      "unicode_decimal": 59037
+    },
+    {
+      "icon_id": "24926703",
+      "name": "personadd-filled",
+      "font_class": "personadd-filled",
+      "unicode": "e698",
+      "unicode_decimal": 59032
+    },
+    {
+      "icon_id": "24923351",
+      "name": "back",
+      "font_class": "back",
+      "unicode": "e6b9",
+      "unicode_decimal": 59065
+    },
+    {
+      "icon_id": "24923352",
+      "name": "forward",
+      "font_class": "forward",
+      "unicode": "e6ba",
+      "unicode_decimal": 59066
+    },
+    {
+      "icon_id": "24923353",
+      "name": "arrowthinright",
+      "font_class": "arrow-right",
+      "unicode": "e6bb",
+      "unicode_decimal": 59067
+    },
+		{
+		  "icon_id": "24923353",
+		  "name": "arrowthinright",
+		  "font_class": "arrowthinright",
+		  "unicode": "e6bb",
+		  "unicode_decimal": 59067
+		},
+    {
+      "icon_id": "24923354",
+      "name": "arrowthinleft",
+      "font_class": "arrow-left",
+      "unicode": "e6bc",
+      "unicode_decimal": 59068
+    },
+		{
+		  "icon_id": "24923354",
+		  "name": "arrowthinleft",
+		  "font_class": "arrowthinleft",
+		  "unicode": "e6bc",
+		  "unicode_decimal": 59068
+		},
+    {
+      "icon_id": "24923355",
+      "name": "arrowthinup",
+      "font_class": "arrow-up",
+      "unicode": "e6bd",
+      "unicode_decimal": 59069
+    },
+		{
+		  "icon_id": "24923355",
+		  "name": "arrowthinup",
+		  "font_class": "arrowthinup",
+		  "unicode": "e6bd",
+		  "unicode_decimal": 59069
+		},
+    {
+      "icon_id": "24923356",
+      "name": "arrowthindown",
+      "font_class": "arrow-down",
+      "unicode": "e6be",
+      "unicode_decimal": 59070
+    },{
+      "icon_id": "24923356",
+      "name": "arrowthindown",
+      "font_class": "arrowthindown",
+      "unicode": "e6be",
+      "unicode_decimal": 59070
+    },
+    {
+      "icon_id": "24923349",
+      "name": "arrowdown",
+      "font_class": "bottom",
+      "unicode": "e6b8",
+      "unicode_decimal": 59064
+    },{
+      "icon_id": "24923349",
+      "name": "arrowdown",
+      "font_class": "arrowdown",
+      "unicode": "e6b8",
+      "unicode_decimal": 59064
+    },
+    {
+      "icon_id": "24923346",
+      "name": "arrowright",
+      "font_class": "right",
+      "unicode": "e6b5",
+      "unicode_decimal": 59061
+    },
+		{
+		  "icon_id": "24923346",
+		  "name": "arrowright",
+		  "font_class": "arrowright",
+		  "unicode": "e6b5",
+		  "unicode_decimal": 59061
+		},
+    {
+      "icon_id": "24923347",
+      "name": "arrowup",
+      "font_class": "top",
+      "unicode": "e6b6",
+      "unicode_decimal": 59062
+    },
+		{
+		  "icon_id": "24923347",
+		  "name": "arrowup",
+		  "font_class": "arrowup",
+		  "unicode": "e6b6",
+		  "unicode_decimal": 59062
+		},
+    {
+      "icon_id": "24923348",
+      "name": "arrowleft",
+      "font_class": "left",
+      "unicode": "e6b7",
+      "unicode_decimal": 59063
+    },
+		{
+		  "icon_id": "24923348",
+		  "name": "arrowleft",
+		  "font_class": "arrowleft",
+		  "unicode": "e6b7",
+		  "unicode_decimal": 59063
+		},
+    {
+      "icon_id": "24923334",
+      "name": "eye",
+      "font_class": "eye",
+      "unicode": "e651",
+      "unicode_decimal": 58961
+    },
+    {
+      "icon_id": "24923335",
+      "name": "eye-filled",
+      "font_class": "eye-filled",
+      "unicode": "e66a",
+      "unicode_decimal": 58986
+    },
+    {
+      "icon_id": "24923336",
+      "name": "eye-slash",
+      "font_class": "eye-slash",
+      "unicode": "e6b3",
+      "unicode_decimal": 59059
+    },
+    {
+      "icon_id": "24923337",
+      "name": "eye-slash-filled",
+      "font_class": "eye-slash-filled",
+      "unicode": "e6b4",
+      "unicode_decimal": 59060
+    },
+    {
+      "icon_id": "24923305",
+      "name": "info-filled",
+      "font_class": "info-filled",
+      "unicode": "e649",
+      "unicode_decimal": 58953
+    },
+    {
+      "icon_id": "24923299",
+      "name": "reload-01",
+      "font_class": "reload",
+      "unicode": "e6b2",
+      "unicode_decimal": 59058
+    },
+    {
+      "icon_id": "24923195",
+      "name": "mic_slash_fill",
+      "font_class": "micoff-filled",
+      "unicode": "e6b0",
+      "unicode_decimal": 59056
+    },
+    {
+      "icon_id": "24923165",
+      "name": "map-pin-ellipse",
+      "font_class": "map-pin-ellipse",
+      "unicode": "e6ac",
+      "unicode_decimal": 59052
+    },
+    {
+      "icon_id": "24923166",
+      "name": "map-pin",
+      "font_class": "map-pin",
+      "unicode": "e6ad",
+      "unicode_decimal": 59053
+    },
+    {
+      "icon_id": "24923167",
+      "name": "location",
+      "font_class": "location",
+      "unicode": "e6ae",
+      "unicode_decimal": 59054
+    },
+    {
+      "icon_id": "24923064",
+      "name": "starhalf",
+      "font_class": "starhalf",
+      "unicode": "e683",
+      "unicode_decimal": 59011
+    },
+    {
+      "icon_id": "24923065",
+      "name": "star",
+      "font_class": "star",
+      "unicode": "e688",
+      "unicode_decimal": 59016
+    },
+    {
+      "icon_id": "24923066",
+      "name": "star-filled",
+      "font_class": "star-filled",
+      "unicode": "e68f",
+      "unicode_decimal": 59023
+    },
+    {
+      "icon_id": "24899646",
+      "name": "a-rilidaka",
+      "font_class": "calendar",
+      "unicode": "e6a0",
+      "unicode_decimal": 59040
+    },
+    {
+      "icon_id": "24899647",
+      "name": "fire",
+      "font_class": "fire",
+      "unicode": "e6a1",
+      "unicode_decimal": 59041
+    },
+    {
+      "icon_id": "24899648",
+      "name": "paihangbang",
+      "font_class": "medal",
+      "unicode": "e6a2",
+      "unicode_decimal": 59042
+    },
+    {
+      "icon_id": "24899649",
+      "name": "font",
+      "font_class": "font",
+      "unicode": "e6a3",
+      "unicode_decimal": 59043
+    },
+    {
+      "icon_id": "24899650",
+      "name": "gift",
+      "font_class": "gift",
+      "unicode": "e6a4",
+      "unicode_decimal": 59044
+    },
+    {
+      "icon_id": "24899651",
+      "name": "link",
+      "font_class": "link",
+      "unicode": "e6a5",
+      "unicode_decimal": 59045
+    },
+    {
+      "icon_id": "24899652",
+      "name": "notification",
+      "font_class": "notification",
+      "unicode": "e6a6",
+      "unicode_decimal": 59046
+    },
+    {
+      "icon_id": "24899653",
+      "name": "staff",
+      "font_class": "staff",
+      "unicode": "e6a7",
+      "unicode_decimal": 59047
+    },
+    {
+      "icon_id": "24899654",
+      "name": "VIP",
+      "font_class": "vip",
+      "unicode": "e6a8",
+      "unicode_decimal": 59048
+    },
+    {
+      "icon_id": "24899655",
+      "name": "folder_add",
+      "font_class": "folder-add",
+      "unicode": "e6a9",
+      "unicode_decimal": 59049
+    },
+    {
+      "icon_id": "24899656",
+      "name": "tune",
+      "font_class": "tune",
+      "unicode": "e6aa",
+      "unicode_decimal": 59050
+    },
+    {
+      "icon_id": "24899657",
+      "name": "shimingrenzheng",
+      "font_class": "auth",
+      "unicode": "e6ab",
+      "unicode_decimal": 59051
+    },
+    {
+      "icon_id": "24899565",
+      "name": "person",
+      "font_class": "person",
+      "unicode": "e699",
+      "unicode_decimal": 59033
+    },
+    {
+      "icon_id": "24899566",
+      "name": "email-filled",
+      "font_class": "email-filled",
+      "unicode": "e69a",
+      "unicode_decimal": 59034
+    },
+    {
+      "icon_id": "24899567",
+      "name": "phone-filled",
+      "font_class": "phone-filled",
+      "unicode": "e69b",
+      "unicode_decimal": 59035
+    },
+    {
+      "icon_id": "24899568",
+      "name": "phone",
+      "font_class": "phone",
+      "unicode": "e69c",
+      "unicode_decimal": 59036
+    },
+    {
+      "icon_id": "24899570",
+      "name": "email",
+      "font_class": "email",
+      "unicode": "e69e",
+      "unicode_decimal": 59038
+    },
+    {
+      "icon_id": "24899571",
+      "name": "personadd",
+      "font_class": "personadd",
+      "unicode": "e69f",
+      "unicode_decimal": 59039
+    },
+    {
+      "icon_id": "24899558",
+      "name": "chatboxes-filled",
+      "font_class": "chatboxes-filled",
+      "unicode": "e692",
+      "unicode_decimal": 59026
+    },
+    {
+      "icon_id": "24899559",
+      "name": "contact",
+      "font_class": "contact",
+      "unicode": "e693",
+      "unicode_decimal": 59027
+    },
+    {
+      "icon_id": "24899560",
+      "name": "chatbubble-filled",
+      "font_class": "chatbubble-filled",
+      "unicode": "e694",
+      "unicode_decimal": 59028
+    },
+    {
+      "icon_id": "24899561",
+      "name": "contact-filled",
+      "font_class": "contact-filled",
+      "unicode": "e695",
+      "unicode_decimal": 59029
+    },
+    {
+      "icon_id": "24899562",
+      "name": "chatboxes",
+      "font_class": "chatboxes",
+      "unicode": "e696",
+      "unicode_decimal": 59030
+    },
+    {
+      "icon_id": "24899563",
+      "name": "chatbubble",
+      "font_class": "chatbubble",
+      "unicode": "e697",
+      "unicode_decimal": 59031
+    },
+    {
+      "icon_id": "24881290",
+      "name": "upload-filled",
+      "font_class": "upload-filled",
+      "unicode": "e68e",
+      "unicode_decimal": 59022
+    },
+    {
+      "icon_id": "24881292",
+      "name": "upload",
+      "font_class": "upload",
+      "unicode": "e690",
+      "unicode_decimal": 59024
+    },
+    {
+      "icon_id": "24881293",
+      "name": "weixin",
+      "font_class": "weixin",
+      "unicode": "e691",
+      "unicode_decimal": 59025
+    },
+    {
+      "icon_id": "24881274",
+      "name": "compose",
+      "font_class": "compose",
+      "unicode": "e67f",
+      "unicode_decimal": 59007
+    },
+    {
+      "icon_id": "24881275",
+      "name": "qq",
+      "font_class": "qq",
+      "unicode": "e680",
+      "unicode_decimal": 59008
+    },
+    {
+      "icon_id": "24881276",
+      "name": "download-filled",
+      "font_class": "download-filled",
+      "unicode": "e681",
+      "unicode_decimal": 59009
+    },
+    {
+      "icon_id": "24881277",
+      "name": "pengyouquan",
+      "font_class": "pyq",
+      "unicode": "e682",
+      "unicode_decimal": 59010
+    },
+    {
+      "icon_id": "24881279",
+      "name": "sound",
+      "font_class": "sound",
+      "unicode": "e684",
+      "unicode_decimal": 59012
+    },
+    {
+      "icon_id": "24881280",
+      "name": "trash-filled",
+      "font_class": "trash-filled",
+      "unicode": "e685",
+      "unicode_decimal": 59013
+    },
+    {
+      "icon_id": "24881281",
+      "name": "sound-filled",
+      "font_class": "sound-filled",
+      "unicode": "e686",
+      "unicode_decimal": 59014
+    },
+    {
+      "icon_id": "24881282",
+      "name": "trash",
+      "font_class": "trash",
+      "unicode": "e687",
+      "unicode_decimal": 59015
+    },
+    {
+      "icon_id": "24881284",
+      "name": "videocam-filled",
+      "font_class": "videocam-filled",
+      "unicode": "e689",
+      "unicode_decimal": 59017
+    },
+    {
+      "icon_id": "24881285",
+      "name": "spinner-cycle",
+      "font_class": "spinner-cycle",
+      "unicode": "e68a",
+      "unicode_decimal": 59018
+    },
+    {
+      "icon_id": "24881286",
+      "name": "weibo",
+      "font_class": "weibo",
+      "unicode": "e68b",
+      "unicode_decimal": 59019
+    },
+    {
+      "icon_id": "24881288",
+      "name": "videocam",
+      "font_class": "videocam",
+      "unicode": "e68c",
+      "unicode_decimal": 59020
+    },
+    {
+      "icon_id": "24881289",
+      "name": "download",
+      "font_class": "download",
+      "unicode": "e68d",
+      "unicode_decimal": 59021
+    },
+    {
+      "icon_id": "24879601",
+      "name": "help",
+      "font_class": "help",
+      "unicode": "e679",
+      "unicode_decimal": 59001
+    },
+    {
+      "icon_id": "24879602",
+      "name": "navigate-filled",
+      "font_class": "navigate-filled",
+      "unicode": "e67a",
+      "unicode_decimal": 59002
+    },
+    {
+      "icon_id": "24879603",
+      "name": "plusempty",
+      "font_class": "plusempty",
+      "unicode": "e67b",
+      "unicode_decimal": 59003
+    },
+    {
+      "icon_id": "24879604",
+      "name": "smallcircle",
+      "font_class": "smallcircle",
+      "unicode": "e67c",
+      "unicode_decimal": 59004
+    },
+    {
+      "icon_id": "24879605",
+      "name": "minus-filled",
+      "font_class": "minus-filled",
+      "unicode": "e67d",
+      "unicode_decimal": 59005
+    },
+    {
+      "icon_id": "24879606",
+      "name": "micoff",
+      "font_class": "micoff",
+      "unicode": "e67e",
+      "unicode_decimal": 59006
+    },
+    {
+      "icon_id": "24879588",
+      "name": "closeempty",
+      "font_class": "closeempty",
+      "unicode": "e66c",
+      "unicode_decimal": 58988
+    },
+    {
+      "icon_id": "24879589",
+      "name": "clear",
+      "font_class": "clear",
+      "unicode": "e66d",
+      "unicode_decimal": 58989
+    },
+    {
+      "icon_id": "24879590",
+      "name": "navigate",
+      "font_class": "navigate",
+      "unicode": "e66e",
+      "unicode_decimal": 58990
+    },
+    {
+      "icon_id": "24879591",
+      "name": "minus",
+      "font_class": "minus",
+      "unicode": "e66f",
+      "unicode_decimal": 58991
+    },
+    {
+      "icon_id": "24879592",
+      "name": "image",
+      "font_class": "image",
+      "unicode": "e670",
+      "unicode_decimal": 58992
+    },
+    {
+      "icon_id": "24879593",
+      "name": "mic",
+      "font_class": "mic",
+      "unicode": "e671",
+      "unicode_decimal": 58993
+    },
+    {
+      "icon_id": "24879594",
+      "name": "paperplane",
+      "font_class": "paperplane",
+      "unicode": "e672",
+      "unicode_decimal": 58994
+    },
+    {
+      "icon_id": "24879595",
+      "name": "close",
+      "font_class": "close",
+      "unicode": "e673",
+      "unicode_decimal": 58995
+    },
+    {
+      "icon_id": "24879596",
+      "name": "help-filled",
+      "font_class": "help-filled",
+      "unicode": "e674",
+      "unicode_decimal": 58996
+    },
+    {
+      "icon_id": "24879597",
+      "name": "plus-filled",
+      "font_class": "paperplane-filled",
+      "unicode": "e675",
+      "unicode_decimal": 58997
+    },
+    {
+      "icon_id": "24879598",
+      "name": "plus",
+      "font_class": "plus",
+      "unicode": "e676",
+      "unicode_decimal": 58998
+    },
+    {
+      "icon_id": "24879599",
+      "name": "mic-filled",
+      "font_class": "mic-filled",
+      "unicode": "e677",
+      "unicode_decimal": 58999
+    },
+    {
+      "icon_id": "24879600",
+      "name": "image-filled",
+      "font_class": "image-filled",
+      "unicode": "e678",
+      "unicode_decimal": 59000
+    },
+    {
+      "icon_id": "24855900",
+      "name": "locked-filled",
+      "font_class": "locked-filled",
+      "unicode": "e668",
+      "unicode_decimal": 58984
+    },
+    {
+      "icon_id": "24855901",
+      "name": "info",
+      "font_class": "info",
+      "unicode": "e669",
+      "unicode_decimal": 58985
+    },
+    {
+      "icon_id": "24855903",
+      "name": "locked",
+      "font_class": "locked",
+      "unicode": "e66b",
+      "unicode_decimal": 58987
+    },
+    {
+      "icon_id": "24855884",
+      "name": "camera-filled",
+      "font_class": "camera-filled",
+      "unicode": "e658",
+      "unicode_decimal": 58968
+    },
+    {
+      "icon_id": "24855885",
+      "name": "chat-filled",
+      "font_class": "chat-filled",
+      "unicode": "e659",
+      "unicode_decimal": 58969
+    },
+    {
+      "icon_id": "24855886",
+      "name": "camera",
+      "font_class": "camera",
+      "unicode": "e65a",
+      "unicode_decimal": 58970
+    },
+    {
+      "icon_id": "24855887",
+      "name": "circle",
+      "font_class": "circle",
+      "unicode": "e65b",
+      "unicode_decimal": 58971
+    },
+    {
+      "icon_id": "24855888",
+      "name": "checkmarkempty",
+      "font_class": "checkmarkempty",
+      "unicode": "e65c",
+      "unicode_decimal": 58972
+    },
+    {
+      "icon_id": "24855889",
+      "name": "chat",
+      "font_class": "chat",
+      "unicode": "e65d",
+      "unicode_decimal": 58973
+    },
+    {
+      "icon_id": "24855890",
+      "name": "circle-filled",
+      "font_class": "circle-filled",
+      "unicode": "e65e",
+      "unicode_decimal": 58974
+    },
+    {
+      "icon_id": "24855891",
+      "name": "flag",
+      "font_class": "flag",
+      "unicode": "e65f",
+      "unicode_decimal": 58975
+    },
+    {
+      "icon_id": "24855892",
+      "name": "flag-filled",
+      "font_class": "flag-filled",
+      "unicode": "e660",
+      "unicode_decimal": 58976
+    },
+    {
+      "icon_id": "24855893",
+      "name": "gear-filled",
+      "font_class": "gear-filled",
+      "unicode": "e661",
+      "unicode_decimal": 58977
+    },
+    {
+      "icon_id": "24855894",
+      "name": "home",
+      "font_class": "home",
+      "unicode": "e662",
+      "unicode_decimal": 58978
+    },
+    {
+      "icon_id": "24855895",
+      "name": "home-filled",
+      "font_class": "home-filled",
+      "unicode": "e663",
+      "unicode_decimal": 58979
+    },
+    {
+      "icon_id": "24855896",
+      "name": "gear",
+      "font_class": "gear",
+      "unicode": "e664",
+      "unicode_decimal": 58980
+    },
+    {
+      "icon_id": "24855897",
+      "name": "smallcircle-filled",
+      "font_class": "smallcircle-filled",
+      "unicode": "e665",
+      "unicode_decimal": 58981
+    },
+    {
+      "icon_id": "24855898",
+      "name": "map-filled",
+      "font_class": "map-filled",
+      "unicode": "e666",
+      "unicode_decimal": 58982
+    },
+    {
+      "icon_id": "24855899",
+      "name": "map",
+      "font_class": "map",
+      "unicode": "e667",
+      "unicode_decimal": 58983
+    },
+    {
+      "icon_id": "24855825",
+      "name": "refresh-filled",
+      "font_class": "refresh-filled",
+      "unicode": "e656",
+      "unicode_decimal": 58966
+    },
+    {
+      "icon_id": "24855826",
+      "name": "refresh",
+      "font_class": "refresh",
+      "unicode": "e657",
+      "unicode_decimal": 58967
+    },
+    {
+      "icon_id": "24855808",
+      "name": "cloud-upload",
+      "font_class": "cloud-upload",
+      "unicode": "e645",
+      "unicode_decimal": 58949
+    },
+    {
+      "icon_id": "24855809",
+      "name": "cloud-download-filled",
+      "font_class": "cloud-download-filled",
+      "unicode": "e646",
+      "unicode_decimal": 58950
+    },
+    {
+      "icon_id": "24855810",
+      "name": "cloud-download",
+      "font_class": "cloud-download",
+      "unicode": "e647",
+      "unicode_decimal": 58951
+    },
+    {
+      "icon_id": "24855811",
+      "name": "cloud-upload-filled",
+      "font_class": "cloud-upload-filled",
+      "unicode": "e648",
+      "unicode_decimal": 58952
+    },
+    {
+      "icon_id": "24855813",
+      "name": "redo",
+      "font_class": "redo",
+      "unicode": "e64a",
+      "unicode_decimal": 58954
+    },
+    {
+      "icon_id": "24855814",
+      "name": "images-filled",
+      "font_class": "images-filled",
+      "unicode": "e64b",
+      "unicode_decimal": 58955
+    },
+    {
+      "icon_id": "24855815",
+      "name": "undo-filled",
+      "font_class": "undo-filled",
+      "unicode": "e64c",
+      "unicode_decimal": 58956
+    },
+    {
+      "icon_id": "24855816",
+      "name": "more",
+      "font_class": "more",
+      "unicode": "e64d",
+      "unicode_decimal": 58957
+    },
+    {
+      "icon_id": "24855817",
+      "name": "more-filled",
+      "font_class": "more-filled",
+      "unicode": "e64e",
+      "unicode_decimal": 58958
+    },
+    {
+      "icon_id": "24855818",
+      "name": "undo",
+      "font_class": "undo",
+      "unicode": "e64f",
+      "unicode_decimal": 58959
+    },
+    {
+      "icon_id": "24855819",
+      "name": "images",
+      "font_class": "images",
+      "unicode": "e650",
+      "unicode_decimal": 58960
+    },
+    {
+      "icon_id": "24855821",
+      "name": "paperclip",
+      "font_class": "paperclip",
+      "unicode": "e652",
+      "unicode_decimal": 58962
+    },
+    {
+      "icon_id": "24855822",
+      "name": "settings",
+      "font_class": "settings",
+      "unicode": "e653",
+      "unicode_decimal": 58963
+    },
+    {
+      "icon_id": "24855823",
+      "name": "search",
+      "font_class": "search",
+      "unicode": "e654",
+      "unicode_decimal": 58964
+    },
+    {
+      "icon_id": "24855824",
+      "name": "redo-filled",
+      "font_class": "redo-filled",
+      "unicode": "e655",
+      "unicode_decimal": 58965
+    },
+    {
+      "icon_id": "24841702",
+      "name": "list",
+      "font_class": "list",
+      "unicode": "e644",
+      "unicode_decimal": 58948
+    },
+    {
+      "icon_id": "24841489",
+      "name": "mail-open-filled",
+      "font_class": "mail-open-filled",
+      "unicode": "e63a",
+      "unicode_decimal": 58938
+    },
+    {
+      "icon_id": "24841491",
+      "name": "hand-thumbsdown-filled",
+      "font_class": "hand-down-filled",
+      "unicode": "e63c",
+      "unicode_decimal": 58940
+    },
+    {
+      "icon_id": "24841492",
+      "name": "hand-thumbsdown",
+      "font_class": "hand-down",
+      "unicode": "e63d",
+      "unicode_decimal": 58941
+    },
+    {
+      "icon_id": "24841493",
+      "name": "hand-thumbsup-filled",
+      "font_class": "hand-up-filled",
+      "unicode": "e63e",
+      "unicode_decimal": 58942
+    },
+    {
+      "icon_id": "24841494",
+      "name": "hand-thumbsup",
+      "font_class": "hand-up",
+      "unicode": "e63f",
+      "unicode_decimal": 58943
+    },
+    {
+      "icon_id": "24841496",
+      "name": "heart-filled",
+      "font_class": "heart-filled",
+      "unicode": "e641",
+      "unicode_decimal": 58945
+    },
+    {
+      "icon_id": "24841498",
+      "name": "mail-open",
+      "font_class": "mail-open",
+      "unicode": "e643",
+      "unicode_decimal": 58947
+    },
+    {
+      "icon_id": "24841488",
+      "name": "heart",
+      "font_class": "heart",
+      "unicode": "e639",
+      "unicode_decimal": 58937
+    },
+    {
+      "icon_id": "24839963",
+      "name": "loop",
+      "font_class": "loop",
+      "unicode": "e633",
+      "unicode_decimal": 58931
+    },
+    {
+      "icon_id": "24839866",
+      "name": "pulldown",
+      "font_class": "pulldown",
+      "unicode": "e632",
+      "unicode_decimal": 58930
+    },
+    {
+      "icon_id": "24813798",
+      "name": "scan",
+      "font_class": "scan",
+      "unicode": "e62a",
+      "unicode_decimal": 58922
+    },
+    {
+      "icon_id": "24813786",
+      "name": "bars",
+      "font_class": "bars",
+      "unicode": "e627",
+      "unicode_decimal": 58919
+    },
+    {
+      "icon_id": "24813788",
+      "name": "cart-filled",
+      "font_class": "cart-filled",
+      "unicode": "e629",
+      "unicode_decimal": 58921
+    },
+    {
+      "icon_id": "24813790",
+      "name": "checkbox",
+      "font_class": "checkbox",
+      "unicode": "e62b",
+      "unicode_decimal": 58923
+    },
+    {
+      "icon_id": "24813791",
+      "name": "checkbox-filled",
+      "font_class": "checkbox-filled",
+      "unicode": "e62c",
+      "unicode_decimal": 58924
+    },
+    {
+      "icon_id": "24813794",
+      "name": "shop",
+      "font_class": "shop",
+      "unicode": "e62f",
+      "unicode_decimal": 58927
+    },
+    {
+      "icon_id": "24813795",
+      "name": "headphones",
+      "font_class": "headphones",
+      "unicode": "e630",
+      "unicode_decimal": 58928
+    },
+    {
+      "icon_id": "24813796",
+      "name": "cart",
+      "font_class": "cart",
+      "unicode": "e631",
+      "unicode_decimal": 58929
+    }
+  ]
+}

+ 96 - 0
src/components/uni-icons/uni-icons.vue

@@ -0,0 +1,96 @@
+<template>
+	<!-- #ifdef APP-NVUE -->
+	<text :style="{ color: color, 'font-size': iconSize }" class="uni-icons" @click="_onClick">{{unicode}}</text>
+	<!-- #endif -->
+	<!-- #ifndef APP-NVUE -->
+	<text :style="{ color: color, 'font-size': iconSize }" class="uni-icons" :class="['uniui-'+type,customPrefix,customPrefix?type:'']" @click="_onClick"></text>
+	<!-- #endif -->
+</template>
+
+<script>
+	import icons from './icons.js';
+	const getVal = (val) => {
+		const reg = /^[0-9]*$/g
+		return (typeof val === 'number' || reg.test(val) )? val + 'px' : val;
+	} 
+	// #ifdef APP-NVUE
+	var domModule = weex.requireModule('dom');
+	import iconUrl from './uniicons.ttf'
+	domModule.addRule('fontFace', {
+		'fontFamily': "uniicons",
+		'src': "url('"+iconUrl+"')"
+	});
+	// #endif
+
+	/**
+	 * Icons 图标
+	 * @description 用于展示 icons 图标
+	 * @tutorial https://ext.dcloud.net.cn/plugin?id=28
+	 * @property {Number} size 图标大小
+	 * @property {String} type 图标图案,参考示例
+	 * @property {String} color 图标颜色
+	 * @property {String} customPrefix 自定义图标
+	 * @event {Function} click 点击 Icon 触发事件
+	 */
+	export default {
+		name: 'UniIcons',
+		emits:['click'],
+		props: {
+			type: {
+				type: String,
+				default: ''
+			},
+			color: {
+				type: String,
+				default: '#333333'
+			},
+			size: {
+				type: [Number, String],
+				default: 16
+			},
+			customPrefix:{
+				type: String,
+				default: ''
+			}
+		},
+		data() {
+			return {
+				icons: icons.glyphs
+			}
+		},
+		computed:{
+			unicode(){
+				let code = this.icons.find(v=>v.font_class === this.type)
+				if(code){
+					return unescape(`%u${code.unicode}`)
+				}
+				return ''
+			},
+			iconSize(){
+				return getVal(this.size)
+			}
+		},
+		methods: {
+			_onClick() {
+				this.$emit('click')
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	/* #ifndef APP-NVUE */
+	@import './uniicons.css';
+	@font-face {
+		font-family: uniicons;
+		src: url('./uniicons.ttf') format('truetype');
+	}
+
+	/* #endif */
+	.uni-icons {
+		font-family: uniicons;
+		text-decoration: none;
+		text-align: center;
+	}
+
+</style>

+ 663 - 0
src/components/uni-icons/uniicons.css

@@ -0,0 +1,663 @@
+.uniui-color:before {
+  content: "\e6cf";
+}
+
+.uniui-wallet:before {
+  content: "\e6b1";
+}
+
+.uniui-settings-filled:before {
+  content: "\e6ce";
+}
+
+.uniui-auth-filled:before {
+  content: "\e6cc";
+}
+
+.uniui-shop-filled:before {
+  content: "\e6cd";
+}
+
+.uniui-staff-filled:before {
+  content: "\e6cb";
+}
+
+.uniui-vip-filled:before {
+  content: "\e6c6";
+}
+
+.uniui-plus-filled:before {
+  content: "\e6c7";
+}
+
+.uniui-folder-add-filled:before {
+  content: "\e6c8";
+}
+
+.uniui-color-filled:before {
+  content: "\e6c9";
+}
+
+.uniui-tune-filled:before {
+  content: "\e6ca";
+}
+
+.uniui-calendar-filled:before {
+  content: "\e6c0";
+}
+
+.uniui-notification-filled:before {
+  content: "\e6c1";
+}
+
+.uniui-wallet-filled:before {
+  content: "\e6c2";
+}
+
+.uniui-medal-filled:before {
+  content: "\e6c3";
+}
+
+.uniui-gift-filled:before {
+  content: "\e6c4";
+}
+
+.uniui-fire-filled:before {
+  content: "\e6c5";
+}
+
+.uniui-refreshempty:before {
+  content: "\e6bf";
+}
+
+.uniui-location-filled:before {
+  content: "\e6af";
+}
+
+.uniui-person-filled:before {
+  content: "\e69d";
+}
+
+.uniui-personadd-filled:before {
+  content: "\e698";
+}
+
+.uniui-back:before {
+  content: "\e6b9";
+}
+
+.uniui-forward:before {
+  content: "\e6ba";
+}
+
+.uniui-arrow-right:before {
+  content: "\e6bb";
+}
+
+.uniui-arrowthinright:before {
+  content: "\e6bb";
+}
+
+.uniui-arrow-left:before {
+  content: "\e6bc";
+}
+
+.uniui-arrowthinleft:before {
+  content: "\e6bc";
+}
+
+.uniui-arrow-up:before {
+  content: "\e6bd";
+}
+
+.uniui-arrowthinup:before {
+  content: "\e6bd";
+}
+
+.uniui-arrow-down:before {
+  content: "\e6be";
+}
+
+.uniui-arrowthindown:before {
+  content: "\e6be";
+}
+
+.uniui-bottom:before {
+  content: "\e6b8";
+}
+
+.uniui-arrowdown:before {
+  content: "\e6b8";
+}
+
+.uniui-right:before {
+  content: "\e6b5";
+}
+
+.uniui-arrowright:before {
+  content: "\e6b5";
+}
+
+.uniui-top:before {
+  content: "\e6b6";
+}
+
+.uniui-arrowup:before {
+  content: "\e6b6";
+}
+
+.uniui-left:before {
+  content: "\e6b7";
+}
+
+.uniui-arrowleft:before {
+  content: "\e6b7";
+}
+
+.uniui-eye:before {
+  content: "\e651";
+}
+
+.uniui-eye-filled:before {
+  content: "\e66a";
+}
+
+.uniui-eye-slash:before {
+  content: "\e6b3";
+}
+
+.uniui-eye-slash-filled:before {
+  content: "\e6b4";
+}
+
+.uniui-info-filled:before {
+  content: "\e649";
+}
+
+.uniui-reload:before {
+  content: "\e6b2";
+}
+
+.uniui-micoff-filled:before {
+  content: "\e6b0";
+}
+
+.uniui-map-pin-ellipse:before {
+  content: "\e6ac";
+}
+
+.uniui-map-pin:before {
+  content: "\e6ad";
+}
+
+.uniui-location:before {
+  content: "\e6ae";
+}
+
+.uniui-starhalf:before {
+  content: "\e683";
+}
+
+.uniui-star:before {
+  content: "\e688";
+}
+
+.uniui-star-filled:before {
+  content: "\e68f";
+}
+
+.uniui-calendar:before {
+  content: "\e6a0";
+}
+
+.uniui-fire:before {
+  content: "\e6a1";
+}
+
+.uniui-medal:before {
+  content: "\e6a2";
+}
+
+.uniui-font:before {
+  content: "\e6a3";
+}
+
+.uniui-gift:before {
+  content: "\e6a4";
+}
+
+.uniui-link:before {
+  content: "\e6a5";
+}
+
+.uniui-notification:before {
+  content: "\e6a6";
+}
+
+.uniui-staff:before {
+  content: "\e6a7";
+}
+
+.uniui-vip:before {
+  content: "\e6a8";
+}
+
+.uniui-folder-add:before {
+  content: "\e6a9";
+}
+
+.uniui-tune:before {
+  content: "\e6aa";
+}
+
+.uniui-auth:before {
+  content: "\e6ab";
+}
+
+.uniui-person:before {
+  content: "\e699";
+}
+
+.uniui-email-filled:before {
+  content: "\e69a";
+}
+
+.uniui-phone-filled:before {
+  content: "\e69b";
+}
+
+.uniui-phone:before {
+  content: "\e69c";
+}
+
+.uniui-email:before {
+  content: "\e69e";
+}
+
+.uniui-personadd:before {
+  content: "\e69f";
+}
+
+.uniui-chatboxes-filled:before {
+  content: "\e692";
+}
+
+.uniui-contact:before {
+  content: "\e693";
+}
+
+.uniui-chatbubble-filled:before {
+  content: "\e694";
+}
+
+.uniui-contact-filled:before {
+  content: "\e695";
+}
+
+.uniui-chatboxes:before {
+  content: "\e696";
+}
+
+.uniui-chatbubble:before {
+  content: "\e697";
+}
+
+.uniui-upload-filled:before {
+  content: "\e68e";
+}
+
+.uniui-upload:before {
+  content: "\e690";
+}
+
+.uniui-weixin:before {
+  content: "\e691";
+}
+
+.uniui-compose:before {
+  content: "\e67f";
+}
+
+.uniui-qq:before {
+  content: "\e680";
+}
+
+.uniui-download-filled:before {
+  content: "\e681";
+}
+
+.uniui-pyq:before {
+  content: "\e682";
+}
+
+.uniui-sound:before {
+  content: "\e684";
+}
+
+.uniui-trash-filled:before {
+  content: "\e685";
+}
+
+.uniui-sound-filled:before {
+  content: "\e686";
+}
+
+.uniui-trash:before {
+  content: "\e687";
+}
+
+.uniui-videocam-filled:before {
+  content: "\e689";
+}
+
+.uniui-spinner-cycle:before {
+  content: "\e68a";
+}
+
+.uniui-weibo:before {
+  content: "\e68b";
+}
+
+.uniui-videocam:before {
+  content: "\e68c";
+}
+
+.uniui-download:before {
+  content: "\e68d";
+}
+
+.uniui-help:before {
+  content: "\e679";
+}
+
+.uniui-navigate-filled:before {
+  content: "\e67a";
+}
+
+.uniui-plusempty:before {
+  content: "\e67b";
+}
+
+.uniui-smallcircle:before {
+  content: "\e67c";
+}
+
+.uniui-minus-filled:before {
+  content: "\e67d";
+}
+
+.uniui-micoff:before {
+  content: "\e67e";
+}
+
+.uniui-closeempty:before {
+  content: "\e66c";
+}
+
+.uniui-clear:before {
+  content: "\e66d";
+}
+
+.uniui-navigate:before {
+  content: "\e66e";
+}
+
+.uniui-minus:before {
+  content: "\e66f";
+}
+
+.uniui-image:before {
+  content: "\e670";
+}
+
+.uniui-mic:before {
+  content: "\e671";
+}
+
+.uniui-paperplane:before {
+  content: "\e672";
+}
+
+.uniui-close:before {
+  content: "\e673";
+}
+
+.uniui-help-filled:before {
+  content: "\e674";
+}
+
+.uniui-paperplane-filled:before {
+  content: "\e675";
+}
+
+.uniui-plus:before {
+  content: "\e676";
+}
+
+.uniui-mic-filled:before {
+  content: "\e677";
+}
+
+.uniui-image-filled:before {
+  content: "\e678";
+}
+
+.uniui-locked-filled:before {
+  content: "\e668";
+}
+
+.uniui-info:before {
+  content: "\e669";
+}
+
+.uniui-locked:before {
+  content: "\e66b";
+}
+
+.uniui-camera-filled:before {
+  content: "\e658";
+}
+
+.uniui-chat-filled:before {
+  content: "\e659";
+}
+
+.uniui-camera:before {
+  content: "\e65a";
+}
+
+.uniui-circle:before {
+  content: "\e65b";
+}
+
+.uniui-checkmarkempty:before {
+  content: "\e65c";
+}
+
+.uniui-chat:before {
+  content: "\e65d";
+}
+
+.uniui-circle-filled:before {
+  content: "\e65e";
+}
+
+.uniui-flag:before {
+  content: "\e65f";
+}
+
+.uniui-flag-filled:before {
+  content: "\e660";
+}
+
+.uniui-gear-filled:before {
+  content: "\e661";
+}
+
+.uniui-home:before {
+  content: "\e662";
+}
+
+.uniui-home-filled:before {
+  content: "\e663";
+}
+
+.uniui-gear:before {
+  content: "\e664";
+}
+
+.uniui-smallcircle-filled:before {
+  content: "\e665";
+}
+
+.uniui-map-filled:before {
+  content: "\e666";
+}
+
+.uniui-map:before {
+  content: "\e667";
+}
+
+.uniui-refresh-filled:before {
+  content: "\e656";
+}
+
+.uniui-refresh:before {
+  content: "\e657";
+}
+
+.uniui-cloud-upload:before {
+  content: "\e645";
+}
+
+.uniui-cloud-download-filled:before {
+  content: "\e646";
+}
+
+.uniui-cloud-download:before {
+  content: "\e647";
+}
+
+.uniui-cloud-upload-filled:before {
+  content: "\e648";
+}
+
+.uniui-redo:before {
+  content: "\e64a";
+}
+
+.uniui-images-filled:before {
+  content: "\e64b";
+}
+
+.uniui-undo-filled:before {
+  content: "\e64c";
+}
+
+.uniui-more:before {
+  content: "\e64d";
+}
+
+.uniui-more-filled:before {
+  content: "\e64e";
+}
+
+.uniui-undo:before {
+  content: "\e64f";
+}
+
+.uniui-images:before {
+  content: "\e650";
+}
+
+.uniui-paperclip:before {
+  content: "\e652";
+}
+
+.uniui-settings:before {
+  content: "\e653";
+}
+
+.uniui-search:before {
+  content: "\e654";
+}
+
+.uniui-redo-filled:before {
+  content: "\e655";
+}
+
+.uniui-list:before {
+  content: "\e644";
+}
+
+.uniui-mail-open-filled:before {
+  content: "\e63a";
+}
+
+.uniui-hand-down-filled:before {
+  content: "\e63c";
+}
+
+.uniui-hand-down:before {
+  content: "\e63d";
+}
+
+.uniui-hand-up-filled:before {
+  content: "\e63e";
+}
+
+.uniui-hand-up:before {
+  content: "\e63f";
+}
+
+.uniui-heart-filled:before {
+  content: "\e641";
+}
+
+.uniui-mail-open:before {
+  content: "\e643";
+}
+
+.uniui-heart:before {
+  content: "\e639";
+}
+
+.uniui-loop:before {
+  content: "\e633";
+}
+
+.uniui-pulldown:before {
+  content: "\e632";
+}
+
+.uniui-scan:before {
+  content: "\e62a";
+}
+
+.uniui-bars:before {
+  content: "\e627";
+}
+
+.uniui-cart-filled:before {
+  content: "\e629";
+}
+
+.uniui-checkbox:before {
+  content: "\e62b";
+}
+
+.uniui-checkbox-filled:before {
+  content: "\e62c";
+}
+
+.uniui-shop:before {
+  content: "\e62f";
+}
+
+.uniui-headphones:before {
+  content: "\e630";
+}
+
+.uniui-cart:before {
+  content: "\e631";
+}

binární
src/components/uni-icons/uniicons.ttf


+ 112 - 0
src/custom-tab-bar/index.js

@@ -0,0 +1,112 @@
+import { fetchToken, login, onLogin } from "../api/auth";
+import { fetchChargeStatus } from "../api/charge";
+import { scanCode } from "../utils/code";
+
+Component({
+  data: {
+    hidden: false,
+    dialog: false,
+    tabIndex: -1,
+    tabs: [
+      {
+        path: "/pages/map/map",
+        text: "充电地图",
+      },
+      {
+        path: "",
+        text: "扫码充电",
+      },
+      {
+        path: "/pages/list/list",
+        text: "电站列表",
+      },
+      {
+        path: "/pages/user/user",
+        text: "个人中心",
+      },
+    ],
+    charging: "",
+  },
+  lifetimes: {
+    ready() {
+      fetchToken().then((token) => {
+        const pages = getCurrentPages();
+        const index = this.data.tabs.findIndex(
+          (item) => item.path === `/${pages[pages.length - 1].route}`
+        );
+        this.setData({
+          hidden: false,
+          tabIndex: index,
+          token,
+        });
+        if (!token) {
+          onLogin((token) => {
+            this.setData({
+              token,
+            });
+          });
+        }
+      });
+    },
+  },
+  methods: {
+    switchTab(e) {
+      fetchChargeStatus().then(res => {
+        this.setData({
+          charging: [1, 2].includes(res.chargeStatus) ? res.connectorId : ''
+        })
+      }).catch(() => {
+        this.setData({
+          charging: ''
+        })
+      })
+      const data = e.currentTarget.dataset;
+      const url = data.path;
+      if (!url) {
+        this.switchDialog();
+        return;
+      }
+      this.setData({
+        dialog: false,
+      });
+      wx.vibrateShort &&
+        wx.vibrateShort({
+          type: "light",
+        });
+      wx.switchTab({
+        url,
+      });
+    },
+    switchDialog() {
+      this.setData({
+        dialog: !this.data.dialog,
+      });
+    },
+    hiddenTab() {
+      this.setData({
+        hidden: true,
+      });
+    },
+    showTab() {
+      this.setData({
+        hidden: false,
+      });
+    },
+    login(e) {
+      login(e);
+    },
+    scanCode() {
+      scanCode()
+    },
+    inputCode() {
+      wx.navigateTo({
+        url: "/pages-charge/codeing/codeing",
+      });
+    },
+    toCharging() {
+      wx.navigateTo({
+        url: `/pages-charge/ordering/ordering?sn=${this.data.charging}&start=1`,
+      });
+    },
+  },
+});

+ 3 - 0
src/custom-tab-bar/index.json

@@ -0,0 +1,3 @@
+{
+  "component": true
+}

+ 35 - 0
src/custom-tab-bar/index.wxml

@@ -0,0 +1,35 @@
+<view class="tab-bar {{hidden ? 'tab-bar-hidden' : ''}}">
+  <image class="tab-bar-bg" src="/static/images/custom-tab-bar/bg.png" mode="widthFix" />
+  <view class="tab-bar-main">
+    <view wx:for="{{tabs}}" wx:key="index" class="tab-bar-item" data-path="{{item.path}}" data-index="{{index}}" bindtap="switchTab">
+      <block wx:if="{{index === 1}}">
+        <image class="image image-big" src="/static/images/custom-tab-bar/2.png"></image>
+        <image class="image" style="opacity: 0;" src="/static/images/custom-tab-bar/2.png"></image>
+        <view class="text {{tabIndex === index ? 'selected-color' : ''}}">{{item.text}}</view>
+      </block>
+      <block wx:else>
+        <image wx:if="{{tabIndex === index}}" class="image" src="/static/images/custom-tab-bar/{{index+1}}-1.png"></image>
+        <image wx:else class="image" src="/static/images/custom-tab-bar/{{index+1}}.png" mode="widthFix" />
+        <view class="text {{tabIndex === index ? 'selected-color' : ''}}">{{item.text}}</view>
+      </block>
+    </view>
+  </view>
+  <button class="tab-bar-login" wx:if="{{!token}}" style="width: 100vw;height: var(--tab-bar-height);border: none;" open-type="getPhoneNumber" bindgetphonenumber="login"></button>
+</view>
+
+<view class="tab-bar-charge" style="height: {{dialog ? '100vh':'0px'}};" bindtap="switchDialog">
+  <view style="padding-left: {{charging ? '92' : '112'}}rpx;">
+    <view class="tab-bar-charge_menu" catchtouchend="scanCode" style="margin-right: {{charging ? '100' : '140'}}rpx;">
+      <image class="tab-bar-charge_menu--icon" src="/static/images/custom-tab-bar/2-2.png" mode="widthFix" />
+      <view class="tab-bar-charge_menu--text">扫二维码</view>
+    </view>
+    <view class="tab-bar-charge_menu" catchtouchend="inputCode" style="margin-right: {{charging ? '100' : '140'}}rpx;">
+      <image class="tab-bar-charge_menu--icon" src="/static/images/custom-tab-bar/2-3.png" mode="widthFix" />
+      <view class="tab-bar-charge_menu--text">输入编码</view>
+    </view>
+    <view class="tab-bar-charge_menu" catchtouchend="toCharging" wx:if="{{charging}}">
+      <image class="tab-bar-charge_menu--icon" src="/static/images/custom-tab-bar/2-4.png" mode="widthFix" />
+      <view class="tab-bar-charge_menu--text">充电中</view>
+    </view>
+  </view>
+</view>

+ 126 - 0
src/custom-tab-bar/index.wxss

@@ -0,0 +1,126 @@
+:host {
+  --tab-bar-height: 104rpx;
+}
+
+button::after {
+  content: '';
+  border: none;
+}
+
+.tab-bar {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  z-index: 99;
+  height: var(--tab-bar-height);
+  width: 100vw;
+  padding-bottom: constant(safe-area-inset-bottom);
+  padding-bottom: env(safe-area-inset-bottom);
+  box-sizing: content-box;
+  background-color: #fff;
+}
+
+.tab-bar-bg {
+  position: absolute;
+  left: 0;
+  bottom: constant(safe-area-inset-bottom);
+  bottom: env(safe-area-inset-bottom);
+  width: 100%;
+}
+
+.tab-bar-login {
+  position: absolute;
+  left: 0;
+  top: 0;
+  opacity: 0;
+}
+
+.tab-bar-main {
+  height: var(--tab-bar-height);
+  width: 100vw;
+  display: flex;
+  align-items: center;
+}
+
+.tab-bar-hidden {
+  opacity: 0;
+  pointer-events: none;
+}
+
+.tab-bar-item {
+  flex: 1;
+  text-align: center;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+  position: relative;
+}
+
+.tab-bar-item .image {
+  width: 48rpx;
+  height: 48rpx;
+  margin-bottom: 8rpx;
+}
+
+.tab-bar-item .image-big {
+  position: absolute;
+  left: 50%;
+  bottom: 24rpx;
+  transform: translateX(-50%);
+  width: 110rpx;
+  height: 110rpx;
+}
+
+.tab-bar-item .text {
+  font-size: 20rpx;
+  font-weight: 400;
+  color: #646E7E;
+}
+
+.tab-bar-item .selected-color {
+  color: #347DFF;
+  font-weight: 500;
+}
+
+.tab-bar-charge {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  height: 100vh;
+  width: 100vw;
+  z-index: 98;
+  background: linear-gradient(180deg, rgba(0, 0, 0, 0.3) 0%, #000000 100%);
+  transition: all 0.3;
+  overflow: hidden;
+  display: flex;
+  align-items: flex-end;
+  box-sizing: content-box;
+  padding-bottom: constant(safe-area-inset-bottom);
+  padding-bottom: env(safe-area-inset-bottom);
+}
+
+.tab-bar-charge>view {
+  display: flex;
+  align-items: center;
+  padding-bottom: 206rpx;
+  box-sizing: border-box;
+}
+
+.tab-bar-charge_menu {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+}
+
+.tab-bar-charge_menu--icon {
+  width: 104rpx;
+  height: 104rpx;
+}
+
+.tab-bar-charge_menu--text {
+  font-size: 28rpx;
+  color: #fff;
+  margin-top: 16rpx;
+}

+ 106 - 67
src/manifest.json

@@ -1,72 +1,111 @@
 {
-    "name" : "",
-    "appid" : "",
-    "description" : "",
-    "versionName" : "1.0.0",
-    "versionCode" : "100",
-    "transformPx" : false,
-    /* 5+App特有相关 */
-    "app-plus" : {
-        "usingComponents" : true,
-        "nvueStyleCompiler" : "uni-app",
-        "compilerVersion" : 3,
-        "splashscreen" : {
-            "alwaysShowBeforeRender" : true,
-            "waiting" : true,
-            "autoclose" : true,
-            "delay" : 0
-        },
-        /* 模块配置 */
-        "modules" : {},
-        /* 应用发布信息 */
-        "distribute" : {
-            /* android打包配置 */
-            "android" : {
-                "permissions" : [
-                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
-                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
-                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
-                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
-                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
-                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
-                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
-                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
-                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
-                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
-                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
-                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
-                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
-                    "<uses-feature android:name=\"android.hardware.camera\"/>",
-                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
-                ]
-            },
-            /* ios打包配置 */
-            "ios" : {},
-            /* SDK配置 */
-            "sdkConfigs" : {}
-        }
+  "name": "",
+  "appid": "",
+  "description": "",
+  "versionName": "1.0.0",
+  "versionCode": "100",
+  "transformPx": false,
+  /* 5+App特有相关 */
+  "app-plus": {
+    "usingComponents": true,
+    "nvueStyleCompiler": "uni-app",
+    "compilerVersion": 3,
+    "splashscreen": {
+      "alwaysShowBeforeRender": true,
+      "waiting": true,
+      "autoclose": true,
+      "delay": 0
     },
-    /* 快应用特有相关 */
-    "quickapp" : {},
-    /* 小程序特有相关 */
-    "mp-weixin" : {
-        "appid" : "",
-        "setting" : {
-            "urlCheck" : false
-        },
-        "usingComponents" : true
+    /* 模块配置 */
+    "modules": {},
+    /* 应用发布信息 */
+    "distribute": {
+      /* android打包配置 */
+      "android": {
+        "permissions": [
+          "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+          "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+          "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+          "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+          "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+          "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+          "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+          "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+          "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+          "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+          "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+          "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+          "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+          "<uses-feature android:name=\"android.hardware.camera\"/>",
+          "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+        ]
+      },
+      /* ios打包配置 */
+      "ios": {},
+      /* SDK配置 */
+      "sdkConfigs": {}
+    }
+  },
+  "quickapp": {},
+  "mp-weixin": {
+    "optimization": { "subPackages": true },
+    "appid": "wx369fcff95d387bde",
+    "setting": {
+      "urlCheck": false,
+      "es6": true,
+      "enhance": true,
+      "postcss": true,
+      "preloadBackgroundData": false,
+      "minified": true,
+      "newFeature": false,
+      "coverView": true,
+      "nodeModules": false,
+      "autoAudits": false,
+      "showShadowRootInWxmlPanel": true,
+      "scopeDataCheck": false,
+      "uglifyFileName": true,
+      "checkInvalidKey": true,
+      "checkSiteMap": true,
+      "uploadWithSourceMap": true,
+      "compileHotReLoad": false,
+      "lazyloadPlaceholderEnable": false,
+      "useMultiFrameRuntime": true,
+      "useIsolateContext": false,
+      "userConfirmedBundleSwitch": false,
+      "minifyWXSS": true,
+      "disableUseStrict": false,
+      "minifyWXML": true,
+      "showES6CompileOption": false,
+      "ignoreUploadUnusedFiles": true,
+      "useStaticServer": true,
+      "condition": false
     },
-    "mp-alipay" : {
-        "usingComponents" : true
+    "useExtendedLib": {
+      "weui": true
     },
-    "mp-baidu" : {
-        "usingComponents" : true
-    },
-    "mp-toutiao" : {
-        "usingComponents" : true
-    },
-    "uniStatistics": {  
-        "enable": false
-    },
-    "vueVersion" : "3"
+    "style": "v2",
+    "darkmode": false,
+    "resizable": false,
+    "lazyCodeLoading": "requiredComponents",
+    "usingComponents": true,
+    "requiredPrivateInfos": ["getLocation"],
+    "permission": {
+      "scope.userLocation": {
+        "desc": "你的位置信息将用于获取附近充电桩位置"
+      }
+    }
+  },
+  "mp-alipay": {
+    "usingComponents": true
+  },
+  "mp-baidu": {
+    "usingComponents": true
+  },
+  "mp-toutiao": {
+    "usingComponents": true
+  },
+  "uniStatistics": {
+    "enable": false
+  },
+  "vueVersion": "3"
 }

+ 53 - 0
src/pages-charge/codeing/codeing.vue

@@ -0,0 +1,53 @@
+<template>
+  <view class="pt-10 pr-60 pl-60">
+    <view class="lh-0">
+      <!-- #ifdef MP-WEIXIN -->
+		  <!-- #endif -->
+      <image
+        src="/pages-charge/static/charge-input-head.png"
+        mode="widthFix"
+        style="width: 100%"
+      ></image>
+    </view>
+    <view class="mt-20">
+      <style-input
+        title="充电编码"
+        :value="value"
+        :focus="focus"
+        type="text"
+        @input="input"
+      />
+    </view>
+    <view style="margin-top: 100rpx">
+      <style-button type="primary" @click="submit">确认</style-button>
+      <view class="mt-20"></view>
+      <style-button type="primary" :border="true" @click="scanCode"
+        >扫码充电</style-button
+      >
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref } from "vue";
+import { scanCode } from "../../utils/code";
+
+const value = ref("");
+const focus = ref(true);
+
+const submit = () => {
+  if (!value.value) {
+    uni.showToast({
+      title: "请输入充电编码",
+      icon: "none",
+    });
+    return;
+  }
+  uni.navigateTo({
+    url: `/pages-charge/ordering/ordering?sn=${value.value}`,
+  });
+};
+const input = (e: any) => {
+  value.value = e.value;
+};
+</script>

+ 145 - 0
src/pages-charge/machines/charge-machine/charge-machine.vue

@@ -0,0 +1,145 @@
+<template>
+  <view
+    class="charge-point flex-align-center mt-20"
+    v-for="(item, index) in list"
+    :key="index"
+    @click="toOrdering(index)"
+  >
+    <view
+      :class="[
+        'charge-point_icon',
+        `charge-point_icon-${item.connectorStatusInfo.status}`,
+      ]"
+    >
+      <!-- TODO 图标 -->
+    </view>
+    <view class="charge-point_title ml-16">
+      <view class="flex-align-center">
+        <view class="fs-32 fw-600 color-000 lh-32">{{ title }}</view>
+        <view
+          :class="[
+            'tag',
+            'fs-24',
+            'flex-center',
+            'ml-8',
+            item.connectorStatusInfo.status === 255 ||
+            item.connectorStatusInfo.status === 0
+              ? 'tag-disabled'
+              : 'tag-warning',
+          ]"
+          v-if="item.connectorStatusInfo.status !== 1"
+          >{{ (statusMap as any)[item.connectorStatusInfo.status] }}</view
+        >
+      </view>
+      <view class="mt-10 flex-align-center">
+        <view class="fs-24 color-666"
+          >{{ item.connectorType === 1 ? "直流" : "交流"
+          }}{{ item.power }}kw</view
+        >
+        <view class="fs-24 color-666 mr-6 ml-6">|</view>
+        <view class="fs-24 color-666">车位号TODO</view>
+      </view>
+    </view>
+    <view class="ml-auto">
+      <view
+        class="lh-32 color-warning"
+        style="text-align: right; margin-top: -8rpx"
+      >
+        <text class="fs-22 mr-4">¥</text>
+        <text class="fs-32">{{ price }}</text>
+        <text class="fs-22 ml-4" style="vertical-align: text-top">元</text>
+      </view>
+      <view class="fs-24 lh-24 color-999 mt-10">预计费用</view>
+    </view>
+  </view>
+</template>
+
+<script lang="ts">
+export default {
+  props: {
+    title: String,
+    price: String,
+    time: String,
+    list: Array<any>,
+  },
+  data() {
+    return {
+      statusMap: {
+        255: "故障",
+        0: "离网",
+        1: "空闲",
+        2: "未充电",
+        3: "充电中",
+        4: "已预约",
+      },
+    };
+  },
+  methods: {
+    toOrdering(index: number) {
+      const { list } = this;
+      if (
+        list &&
+        list[index].connectorStatusInfo &&
+        list[index].connectorStatusInfo.status &&
+        list[index].connectorStatusInfo.status === 1
+      ) {
+        uni.showActionSheet({
+          itemList: ["去充电"],
+          success: (res) => {
+            if (res.tapIndex === 0) {
+              uni.navigateTo({
+                url: `/pages-charge/ordering/ordering?sn=${list[index].connectorId}`,
+              });
+            }
+          },
+        });
+      }
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+@import "../../../styles/font.scss";
+@import "../../../styles/flex.scss";
+@import "../../../styles/layout.scss";
+
+.charge-point {
+  background-color: #fff;
+  border-radius: 20rpx;
+  transition: all 0.3s;
+  overflow: hidden;
+  padding: 30rpx;
+  &_icon {
+    height: 56rpx;
+    width: 56rpx;
+    border-radius: 50%;
+    &-0,
+    &-255 {
+      background-color: #b1b1b1;
+    }
+    &-1 {
+      background-color: #00dab3;
+    }
+    &-2,
+    &-3,
+    &-4 {
+      background-color: var(--color-warning);
+    }
+  }
+  .tag {
+    height: 38rpx;
+    padding: 0rpx 8rpx;
+    box-sizing: border-box;
+    border-radius: 8rpx;
+  }
+  .tag-warning {
+    color: var(--color-warning);
+    background: rgba(255, 153, 0, 0.1);
+  }
+  .tag-disabled {
+    color: #b1b1b1;
+    background: rgba(177, 177, 177, 0.1);
+  }
+}
+</style>

+ 467 - 0
src/pages-charge/machines/machines.vue

@@ -0,0 +1,467 @@
+<template>
+  <!-- TODO 图片 -->
+  <view
+    :class="[
+      'pl-20',
+      'pr-20',
+      'pb-40',
+      `container-${dialogVisible ? 'hidden' : ''}`,
+    ]"
+  >
+    <view class="banner flex-column" v-if="canUseCount >= 0">
+      <view class="fs-40 fw-600 color-000">{{ title }}</view>
+      <view class="flex-align-center mt-10 height-48 relative flex-shrink">
+        <image
+          src="/pages-charge/static/machines-banner-address.png"
+          mode="widthFix"
+          style="width: 16px; display: block; flex-shrink: 0"
+        />
+        <view class="ml-12 fs-26 color-666">{{ location.address }}</view>
+        <view class="flex-center ml-auto nav" @click="openAddress">
+          <image
+            src="/pages-charge/static/machines-banner-nav.png"
+            mode="widthFix"
+          />
+          <view class="fs-26" style="color: #347dff">导航</view>
+        </view>
+      </view>
+      <view class="foot mt-30 flex-align-center flex-shrink" @click="openDesc">
+        <view class="fs-28 color-666">可用充电桩:</view>
+        <view class="fs-36 fw-500 color-primary">{{ canUseCount }}</view>
+        <view class="fs-28 color-primary ml-6">个</view>
+        <view class="fs-28 color-666 ml-auto">{{ canUseTime }}</view>
+      </view>
+    </view>
+    <view class="pt-40 pb-20 fs-32 fw-500 color-999">详细说明</view>
+    <view class="desc" v-if="station">
+      <view class="flex-align-center" @click="openDesc">
+        <view class="width-168 fs-26 color-666">充电费用</view>
+        <view class="fs-26 color-000">{{ canUsePrice }}元/度</view>
+        <view class="ml-auto lh-0">
+          <uni-icons type="right" size="18" color="rgba(0,0,0,0.4)"></uni-icons>
+        </view>
+      </view>
+      <view class="flex-align-center">
+        <view class="width-168 fs-26 color-666">停车费用</view>
+        <view class="fs-26 color-000">{{
+          Number(station.parkFee) > 0 ? station.parkFee : "免费"
+        }}</view>
+      </view>
+      <view
+        class="flex-align-center"
+        v-if="
+          station &&
+          station.equipmentInfos &&
+          station.equipmentInfos.length &&
+          station.equipmentInfos[0].connectorInfos
+        "
+      >
+        <view class="width-168 fs-26 color-666">充电桩类型</view>
+        <view class="fs-26 color-000"
+          >{{
+            station.equipmentInfos[0].connectorInfos[0].connectorType === 1
+              ? "直流"
+              : "交流"
+          }}{{ station.equipmentInfos[0].connectorInfos[0].power }}kw</view
+        >
+      </view>
+    </view>
+    <view class="pt-40 flex-align-center">
+      <view class="fs-32 fw-500 color-999">充电桩</view>
+      <view class="fs-26 color-999" v-if="station"
+        >(共{{ totalCount }}个)</view
+      >
+      <view class="ml-auto flex-align-center" @click="openStatus">
+        <view class="fs-28 color-333 mr-16">{{
+          statusList[status].title
+        }}</view>
+        <view
+          style="
+            width: 0;
+            height: 0;
+            border: 8rpx solid;
+            border-color: #333 transparent transparent transparent;
+            margin-top: 8rpx;
+          "
+        ></view>
+      </view>
+    </view>
+    <template
+      v-if="station"
+      v-for="(item, index) in stationEquipmentInfos"
+      :key="index"
+    >
+      <ChargeMachine
+        :title="'NO.' + item.shortId"
+        :price="station.totalFee"
+        :time="currentTime"
+        :list="item.connectorInfos"
+      ></ChargeMachine>
+    </template>
+    <view class="dialog flex-align-end" v-if="dialogVisible">
+      <view class="desc-dialog">
+        <view class="desc-dialog_head flex-center">
+          <view class="fw-500 color-000" style="font-size: 16px">{{
+            dialogType === "desc" ? "充电费用说明" : "选择充电桩状态"
+          }}</view>
+          <view class="close" @click="closeDialog">
+            <uni-icons type="closeempty" size="24" color="#2D284B"></uni-icons>
+          </view>
+        </view>
+        <view class="desc-dialog_body">
+          <view class="table" v-if="dialogType === 'desc'">
+            <view class="tr flex">
+              <view class="th flex-center fs-28">时间</view>
+              <view class="th flex-center flex-column">
+                <view>电费</view>
+                <view class="fs-24 lh-24" style="color: rgba(0, 0, 0, 0.5)"
+                  >(元/kwh)</view
+                >
+              </view>
+              <view class="th flex-center flex-column">
+                <view>服务费</view>
+                <view class="fs-24 lh-24" style="color: rgba(0, 0, 0, 0.5)"
+                  >(元/kwh)</view
+                >
+              </view>
+            </view>
+            <view
+              class="tr flex"
+              v-for="(item, index) in desc"
+              :key="index"
+              :style="{
+                'border-top': `1rpx solid ${
+                  index === 0 ? 'transparent' : '#E5E5E5'
+                }`,
+              }"
+            >
+              <view class="td flex-center">{{ item.startTimeFormat }}</view>
+              <view class="td flex-center">{{ item.elecPrice }}</view>
+              <view class="td flex-center">{{ item.servicePrice }}</view>
+            </view>
+          </view>
+          <view class="status" v-if="dialogType === 'status'">
+            <view
+              :class="['fs-32', 'flex-align-center']"
+              :style="{ color: status === index ? '#347DFF' : '#2d284b' }"
+              v-for="(statusItem, index) in statusList"
+              :key="index"
+              @click="changeStatus(index)"
+            >
+              {{ statusItem.title }}
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref } from "vue";
+import { fetchStation, fetchStationPriceDesc } from "../../api/charge";
+import { onLoad } from "@dcloudio/uni-app";
+import ChargeMachine from "./charge-machine/charge-machine.vue";
+const dialogVisible = ref(false);
+const dialogType = ref("");
+const desc = ref<any[]>([]);
+const status = ref(0);
+const statusList = ref([
+  {
+    title: "全部",
+  },
+  {
+    title: "空闲",
+  },
+  {
+    title: "充电中",
+  },
+]);
+
+const totalCount = ref(0);
+const canUseCount = ref(0);
+const canUseTime = ref("");
+const canUsePrice = ref("");
+const title = ref("");
+const location = ref({
+  address: "",
+  latitude: "",
+  longitude: "",
+});
+
+const station = ref();
+const stationEquipmentInfos = ref();
+const currentTime = ref();
+
+onLoad((options: any) => {
+  const _title = options.title || "";
+  uni.setNavigationBarTitle({
+    title: _title,
+  });
+  title.value = _title;
+  if (getApp<any>().globalData.lastData.station) {
+    const { address, latitude, longitude } =
+      getApp<any>().globalData.lastData.station;
+    location.value = {
+      address,
+      latitude,
+      longitude,
+    };
+    getApp<any>().globalData.lastData.station = undefined;
+  }
+  uni.showLoading({
+    title: "加载中",
+    mask: true,
+  });
+  fetchStation(Number(options.id))
+    .then((res) => {
+      console.log(res);
+      let ConnectorID = "";
+      if (res.equipmentInfos && res.equipmentInfos.length) {
+        res.equipmentInfos.forEach((item: any) => {
+          if (item.connectorInfos && item.connectorInfos.length) {
+            item.connectorInfos.forEach((con: any) => {
+              totalCount.value++;
+              if (!ConnectorID) {
+                ConnectorID = con.connectorId;
+              }
+              if (
+                con.connectorStatusInfo &&
+                con.connectorStatusInfo.status === 1
+              ) {
+                canUseCount.value++;
+              }
+            });
+          }
+        });
+      }
+      station.value = res;
+      stationEquipmentInfos.value = res.equipmentInfos.map((item: any) => {
+        return {
+          ...item,
+        };
+      });
+      if (ConnectorID) {
+        return fetchStationPriceDesc(ConnectorID);
+      } else {
+        // eslint-disable-next-line promise/no-return-wrap
+        return Promise.resolve({
+          policyInfoss: [],
+        });
+      }
+    })
+    .then((res) => {
+      uni.hideLoading();
+      currentTime.value = res.currentTime;
+      canUseTime.value = res.useTime;
+      canUsePrice.value = `${res.minPrice}~${res.maxPrice}`;
+      desc.value = res.policyInfoss || [];
+    })
+    .catch((err) => {
+      // eslint-disable-next-line no-console
+      console.log(err);
+      uni.hideLoading();
+      uni.showToast({
+        title: "加载失败,请重试",
+        icon: "none",
+      });
+    });
+});
+const openAddress = function () {
+  uni.openLocation({
+    latitude: Number(location.value.latitude),
+    longitude: Number(location.value.longitude),
+    scale: 18,
+    name: title.value,
+    address: location.value.address,
+  });
+};
+const openDesc = function () {
+  dialogVisible.value = true;
+  dialogType.value = "desc";
+};
+const openStatus = function () {
+  if (!stationEquipmentInfos.value || stationEquipmentInfos.value.length <= 0) {
+    return;
+  }
+  dialogVisible.value = true;
+  dialogType.value = "status";
+};
+const closeDialog = function () {
+  dialogVisible.value = false;
+};
+const changeStatus = function (index: number) {
+  if (status.value === index) {
+    closeDialog();
+    return;
+  }
+  status.value = index;
+  if (index === 0) {
+    stationEquipmentInfos.value = station.value.equipmentInfos.map(
+      (item: any) => {
+        return {
+          ...item,
+        };
+      }
+    );
+    closeDialog();
+    return;
+  }
+  const STATUS_MAP = [-1, 1, 3];
+  let newStationEquipmentInfos: any[] = [];
+  station.value.equipmentInfos.forEach((item: any) => {
+    let check = false;
+    if (item.connectorInfos && item.connectorInfos.length) {
+      item.connectorInfos.forEach((con: any) => {
+        if (
+          con.connectorStatusInfo &&
+          Number(con.connectorStatusInfo.status) === STATUS_MAP[status.value]
+        ) {
+          check = true;
+        }
+      });
+    }
+    if (check) {
+      newStationEquipmentInfos.push(item);
+    }
+  });
+  stationEquipmentInfos.value = newStationEquipmentInfos;
+  closeDialog();
+};
+</script>
+
+<style lang="scss">
+@import "../../styles/dialog.scss";
+page {
+  background-color: #f5f5f5;
+}
+
+.container-hidden {
+  width: 100%;
+  height: 100vh;
+  overflow: hidden;
+}
+
+.banner {
+  margin-top: 380rpx;
+  border-radius: 20rpx;
+  overflow: hidden;
+  position: relative;
+  background-color: #fff;
+  min-height: 264rpx;
+  padding: 0 40rpx;
+  padding-top: 30rpx;
+
+  .nav {
+    flex-shrink: 0;
+    width: 120rpx;
+    height: 48rpx;
+    border-radius: 40rpx;
+    background: rgba(52, 125, 255, 0.1);
+    image {
+      width: 36rpx;
+    }
+  }
+
+  .foot {
+    border-top: 1px dashed rgba(0, 0, 0, 0.1);
+    height: 96rpx;
+  }
+}
+
+.desc {
+  box-sizing: border-box;
+  padding: 0rpx 30rpx;
+  border-radius: 20rpx;
+  background-color: #fff;
+  & > view {
+    height: 90rpx;
+    border-top: 1px solid rgba(0, 0, 0, 0.1);
+    &:first-child {
+      border-top: none;
+    }
+  }
+}
+
+.desc-dialog {
+  background-color: transparent;
+  width: 100%;
+  overflow: hidden;
+  border-radius: 20rpx;
+
+  &_head {
+    padding: 24rpx 0px;
+    position: relative;
+    background-color: #fff;
+    .close {
+      position: absolute;
+      right: 30rpx;
+      top: 50%;
+      transform: translateY(-50%);
+    }
+  }
+
+  &_body {
+    max-height: 860rpx;
+    box-sizing: border-box;
+    padding: 0rpx 30rpx 80rpx 30rpx;
+    background-color: #fff;
+    overflow-y: auto;
+
+    .table {
+      border: 1rpx solid #e5e5e5;
+      border-radius: 10rpx;
+      overflow: hidden;
+    }
+
+    .tr {
+      width: 100%;
+
+      & > view:nth-child(1) {
+        width: 254rpx;
+        flex-shrink: 0;
+        border-right: 1rpx solid #e5e5e5;
+      }
+
+      & > view:nth-child(2) {
+        width: 200rpx;
+        flex-shrink: 0;
+        border-right: 1rpx solid #e5e5e5;
+      }
+
+      & > view:nth-child(3) {
+        flex-grow: 1;
+      }
+    }
+
+    .th {
+      border-bottom: 1rpx solid #e5e5e5;
+      background-color: #f1f6ff;
+      height: 92rpx;
+      color: rgba(0, 0, 0, 0.7);
+      font-size: 28rpx;
+
+      view {
+        color: rgba(0, 0, 0, 0.7);
+        font-size: 28rpx;
+      }
+    }
+
+    .td {
+      color: #000;
+      font-size: 26rpx;
+      height: 76rpx;
+    }
+
+    .status {
+      & > view {
+        height: 92rpx;
+        border-top: 1px solid rgba(0, 0, 0, 0.1);
+        &:first-child {
+          border-top: none;
+        }
+      }
+    }
+  }
+}
+</style>

+ 66 - 0
src/pages-charge/order/order.vue

@@ -0,0 +1,66 @@
+<template>
+  <view class="body" v-if="list">
+    <shadow-card :list="list"></shadow-card>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { fetchOrder } from "../../api/user";
+import { onLoad } from "@dcloudio/uni-app";
+import { ref } from "vue";
+
+const list = ref<any[]>();
+onLoad((options: any) => {
+  fetchOrder(options.id)
+    .then((res) => {
+      const total_money = (res.totalMoney / 100).toFixed(2);
+      const reg = new RegExp("\B(?=(\d{3})+(?!\d))", "g");
+      const start = new Date(res.startTime.replace(/-/g, "/"));
+      const end = new Date(res.endTime.replace(/-/g, "/"));
+      const diff = parseInt(`${(end.getTime() - start.getTime()) / 1000}`);
+      const min = parseInt(`${diff / 60}`);
+      const time =
+        min >= 60
+          ? `${parseInt(`${min / 60}`)}小时${parseInt(
+              `${min - parseInt(`${min / 60}`) * 60}`
+            )}分钟`
+          : `${parseInt(`${diff / 60}`)}分钟`;
+      list.value = [
+        {
+          label: "累计充电量",
+          value: `${res.totalPower}度`,
+        },
+        {
+          label: "累计费用",
+          value: `${total_money.replace(reg, ",")}元`,
+        },
+        {
+          label: "开始时间",
+          value: res.startTime,
+        },
+        {
+          label: "结束时间",
+          value: res.endTime,
+        },
+        {
+          label: "累计用时",
+          value: time,
+        },
+      ];
+    })
+    .catch(() => {
+      uni.showModal({
+        content: "出现错误,请退出重进",
+        success() {
+          uni.navigateBack();
+        },
+      });
+    });
+});
+</script>
+
+<style lang="scss">
+.body {
+  padding: 30rpx 60rpx;
+}
+</style>

+ 472 - 0
src/pages-charge/ordering/ordering.vue

@@ -0,0 +1,472 @@
+<template>
+  <view class="container">
+    <image
+      class="bg"
+      src="/pages-charge/static/charge-ordering-bg.png"
+      mode="widthFix"
+    />
+    <view class="body">
+      <view class="iphonex-placeholder"></view>
+      <view
+        class="status flex-center transition"
+        :style="{
+          opacity: step >= 1 ? '1' : '0',
+        }"
+      >
+        <image
+          :class="['border', `${step <= 2 ? 'border-animation' : ''}`]"
+          src="/pages-charge/static/charge-ordering-border.png"
+          @load="onImgLoad"
+        />
+        <view class="timer flex-column flex-center">
+          <block v-if="step === 1">
+            <view>
+              <text class="fw-600" style="font-size: 100rpx">{{
+                status.time
+              }}</text>
+              <text class="fs-40 fw-500 ml-10">s</text>
+            </view>
+            <view class="fs-26">充电启动中</view>
+          </block>
+          <block v-if="step === 2">
+            <image
+              class="icon"
+              src="/pages-charge/static/charge-ordering-icon.png"
+            />
+            <view class="fs-26 mt-10">充电中...</view>
+          </block>
+          <block v-if="step === 3">
+            <image
+              class="icon"
+              src="/pages-charge/static/charge-ordering-finish.png"
+            />
+            <view class="fs-26 mt-10">充电结束</view>
+          </block>
+        </view>
+      </view>
+    </view>
+  </view>
+
+  <style-bottom-view>
+    <view
+      class="transition pl-60 pr-60 pb-40"
+      :style="{
+        opacity: step >= 1 ? '1' : '0',
+      }"
+    >
+      <view class="mb-40" v-if="step === 2 || step === 3">
+        <shadow-card :list="chargeInfo"></shadow-card>
+      </view>
+      <style-button
+        v-if="step === 1 || step === 2"
+        type="warning"
+        size="small"
+        @click="cancel"
+        >取消充电</style-button
+      >
+      <style-button v-if="step === 3" type="primary" size="small" @click="back"
+        >返回首页</style-button
+      >
+    </view>
+  </style-bottom-view>
+</template>
+
+<script setup lang="ts">
+import { onHide, onLoad, onShow } from "@dcloudio/uni-app";
+import { cancelCharge, fetchChargeStatus, startCharge } from "../../api/charge";
+import { ref } from "vue";
+import { format } from "../../utils/date";
+
+let timer: any;
+let statusTimer: any;
+const step = ref(0);
+const options = ref<any>();
+const status = ref({
+  time: 30,
+  start: false,
+  cancel: false,
+  error: false,
+});
+const chargeInfo = ref<any[]>([]);
+const startStartTimer = () => {
+  if (status.value.start || status.value.cancel || status.value.error) {
+    timer && clearTimeout(timer);
+    return;
+  }
+  timer = setTimeout(() => {
+    if (status.value.time <= 1 && !status.value.start) {
+      uni.showModal({
+        title: "充电错误",
+        content: "出现问题,请重试",
+        showCancel: false,
+        confirmText: "知道了",
+        confirmColor: "#347DFF",
+        success() {
+          uni.navigateBack();
+        },
+      });
+      return;
+    }
+    status.value.time = status.value.time - 1;
+    startStartTimer();
+  }, 1000);
+};
+const startStatusTimer = () => {
+  statusTimer = setTimeout(() => {
+    fetchChargeStatus()
+      .then((res) => {
+        if (status.value.cancel) {
+          return;
+        }
+        if ([1, 2].includes(res.chargeStatus)) {
+          setChargeData(res);
+        }
+        startStatusTimer();
+      })
+      .catch(() => {
+        startStatusTimer();
+      });
+  }, 30000);
+};
+const start = () => {
+  step.value = 1;
+  startStartTimer();
+  startCharge(options.value && options.value.sn ? options.value.sn : "")
+    .then(() => {
+      fetchStatus();
+    })
+    .catch((err) => {
+      setTimeout(() => {
+        status.value.error = true;
+        timer && clearTimeout(timer);
+        uni.showModal({
+          content: `${err.errMsg}`,
+          showCancel: false,
+          success: () => {
+            uni.navigateBack();
+          },
+        });
+      }, 300);
+    });
+};
+const back = () => {
+  uni.navigateBack();
+};
+const cancel = () => {
+  uni.showModal({
+    title: "停止充电",
+    content: "是否停止充电",
+    cancelText: "停止",
+    cancelColor: "#999999",
+    confirmText: "点错了",
+    confirmColor: "#347DFF",
+    success: (res) => {
+      if (res.cancel) {
+        clearTimeout(timer);
+        status.value.cancel = true;
+        if ([1, 2].includes(step.value)) {
+          if (status.value.start) {
+            finish();
+          } else {
+            uni.showLoading({
+              title: "正在取消",
+            });
+            cancelCharge(
+              options.value && options.value.sn ? options.value.sn : ""
+            )
+              .then(() => {
+                // console.log(res)
+                uni.hideLoading();
+                uni.showToast({
+                  title: "已取消充电",
+                  icon: "none",
+                });
+                setTimeout(() => {
+                  uni.navigateBack();
+                }, 2000);
+              })
+              .catch(() => {
+                uni.hideLoading();
+                uni.showToast({
+                  title: "已取消充电",
+                  icon: "none",
+                });
+                setTimeout(() => {
+                  uni.navigateBack();
+                }, 2000);
+              });
+          }
+        }
+      }
+    },
+  });
+};
+const finish = () => {
+  uni.showLoading({
+    title: "正在结束中",
+  });
+  fetchChargeStatus()
+    .then((res) => {
+      if ([1, 2].includes(res.chargeStatus)) {
+        // 当前充电中
+        const start = new Date(res.startTime.replace(/-/g, "/"));
+        const end = new Date();
+        const diff = parseInt(`${(end.getTime() - start.getTime()) / 1000}`);
+        const min = parseInt(`${diff / 60}`);
+        const time =
+          min >= 60
+            ? `${parseInt(`${min / 60}`)}小时${parseInt(
+                `${min - parseInt(`${min / 60}`) * 60}`
+              )}分钟`
+            : `${parseInt(`${diff / 60}`)}分钟`;
+        const endFormat = format("y-M-d h:m:s");
+        const totalMoney = (res.totalMoney / 100).toFixed(2);
+        const _chargeInfo = [
+          {
+            label: "累计充电量",
+            value: `${res.totalPower}度`,
+          },
+          {
+            label: "累计费用",
+            value: `${totalMoney}元`,
+          },
+          {
+            label: "开始时间",
+            value: res.startTime,
+          },
+          {
+            label: "结束时间",
+            value: endFormat,
+          },
+          {
+            label: "累计用时",
+            value: time,
+          },
+        ];
+        if (res.soc) {
+          _chargeInfo.unshift({
+            label: "剩余电量",
+            value: `${res.soc}%`,
+          });
+        }
+        chargeInfo.value = _chargeInfo;
+        cancelCharge(options.value && options.value.sn ? options.value.sn : "")
+          .then(() => {
+            uni.hideLoading();
+            if (res.failReason) {
+              uni.showModal({
+                content: res.failReason,
+                showCancel: false,
+                success() {
+                  uni.navigateBack();
+                },
+              });
+              return;
+            }
+            step.value = 3;
+          })
+          .catch((err) => {
+            console.log(err);
+            uni.hideLoading();
+            uni.showToast({
+              title: "已取消充电",
+              icon: "none",
+            });
+            setTimeout(() => {
+              uni.navigateBack();
+            }, 2000);
+          });
+      }
+    })
+    .catch((err) => {
+      console.log(err);
+      uni.hideLoading();
+      uni.showModal({
+        title: "温馨提示",
+        content: "出现错误,请重试",
+        showCancel: false,
+        confirmText: "重试",
+        confirmColor: "#347DFF",
+        success: () => {
+          finish();
+        },
+      });
+    });
+};
+const setChargeData = (res: any) => {
+  const start = new Date(res.startTime.replace(/-/g, "/"));
+  const end = new Date();
+  const diff = parseInt(`${(end.getTime() - start.getTime()) / 1000}`);
+  const min = parseInt(`${diff / 60}`);
+  const time =
+    min >= 60
+      ? `${parseInt(`${min / 60}`)}小时${parseInt(
+          `${min - parseInt(`${min / 60}`) * 60}`
+        )}分钟`
+      : `${parseInt(`${diff / 60}`)}分钟`;
+  const totalMoney = (res.totalMoney / 100).toFixed(2);
+  const _chargeInfo = [
+    {
+      label: "累计充电量",
+      value: `${res.totalPower}度`,
+    },
+    {
+      label: "累计费用",
+      value: `${totalMoney}元`,
+    },
+    {
+      label: "开始时间",
+      value: res.startTime,
+    },
+    {
+      label: "累计用时",
+      value: time,
+    },
+  ];
+  if (res.soc) {
+    _chargeInfo.unshift({
+      label: "剩余电量",
+      value: `${res.soc}%`,
+    });
+  }
+  chargeInfo.value = _chargeInfo;
+};
+const fetchStatus = (data?: any) => {
+  if (status.value.cancel) {
+    return;
+  }
+  const promise = data ? Promise.resolve(data) : fetchChargeStatus();
+  promise
+    .then((res) => {
+      if ([1, 2].includes(res.chargeStatus)) {
+        // 当前充电中
+        status.value.start = true;
+        step.value = 2;
+        setChargeData(res);
+        startStatusTimer();
+      }
+    })
+    .catch((err) => {
+      console.log(err);
+      setTimeout(() => {
+        fetchStatus(data);
+      }, 1000);
+    });
+};
+const onImgLoad = () => {
+  fetchChargeStatus()
+    .then((res) => {
+      if ([1, 2].includes(res.chargeStatus)) {
+        if (options.value && options.value.sn === res.connectorId) {
+          fetchStatus(res);
+          return;
+        }
+        // 当前充电中
+        uni.showModal({
+          title: "温馨提示",
+          content: "当前有正在充电中的订单",
+          showCancel: false,
+          confirmText: "查看订单",
+          confirmColor: "#347DFF",
+          success: () => {
+            fetchStatus(res);
+          },
+        });
+      } else {
+        start();
+      }
+    })
+    .catch((err) => {
+      console.log(err);
+      start();
+    });
+};
+
+onLoad((_options: any) => {
+  options.value = _options;
+});
+onHide(() => {
+  timer && clearTimeout(timer);
+  statusTimer && clearTimeout(statusTimer);
+});
+onShow(() => {
+  startStartTimer();
+});
+</script>
+
+<style lang="scss">
+.container {
+  height: 100vh;
+  width: 100%;
+  background-color: #fff;
+  position: relative;
+  overflow: hidden;
+
+  .bg {
+    width: 100%;
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%);
+    transform-origin: center;
+  }
+
+  .body {
+    position: absolute;
+    left: 0;
+    top: 0;
+    height: 100%;
+    width: 100%;
+
+    .iphonex-placeholder {
+      box-sizing: content-box;
+      padding-bottom: constant(safe-area-inset-bottom);
+      padding-bottom: env(safe-area-inset-bottom);
+    }
+
+    .status {
+      width: 420rpx;
+      height: 420rpx;
+      border-radius: 50%;
+      position: relative;
+      margin: 0 auto;
+      margin-top: 130rpx;
+
+      .border {
+        width: 100%;
+        height: 100%;
+      }
+
+      .border-animation {
+        animation: rot 3s linear infinite;
+      }
+
+      .timer {
+        position: absolute;
+        width: 340rpx;
+        height: 340rpx;
+        left: 50%;
+        top: 50%;
+        transform: translate(-50%, -50%);
+        background: linear-gradient(180deg, #ffffff 0%, #ecf3ff 100%);
+        box-shadow: 0px 18rpx 30rpx rgba(0, 0, 0, 0.1);
+        border-radius: 50%;
+        .icon {
+          width: 152rpx;
+          height: 152rpx;
+        }
+      }
+    }
+  }
+}
+
+@keyframes rot {
+  0% {
+    transform: rotate(0deg);
+  }
+
+  100% {
+    transform: rotate(360deg);
+  }
+}
+</style>

+ 76 - 0
src/pages-charge/orders/orders.vue

@@ -0,0 +1,76 @@
+<template>
+  <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">充电费用</view>
+        <view class="fs-24 mt-10" style="color: rgba(0, 0, 0, 0.4)">{{
+          item.transactionTime
+        }}</view>
+      </view>
+      <view class="ml-auto" style="text-align: right">
+        <view class="fs-36 fw-500">
+          <text>{{ item.amount }}</text>
+          <text class="fs-24 ml-6">元</text>
+        </view>
+      </view>
+      <view class="ml-20">
+        <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 { onLoad, onPullDownRefresh, onReachBottom } from "@dcloudio/uni-app";
+import { fetchWallet } from "../../api/user";
+import { useInfiniteScroll } from "../../utils/infinite-scroll";
+const infiniteScroller = useInfiniteScroll(10, (page) => {
+  return fetchWallet(3, page, 10).then((res: any) => {
+    if (res && res.length) {
+      res.forEach((item: any) => {
+        item.amount = (Number(item.amount) / 100).toFixed(2);
+      });
+    }
+    return res;
+  });
+});
+const detail = (index: number) => {
+  if (!infiniteScroller.list) {
+    return;
+  }
+  uni.navigateTo({
+    url: `/pages-charge/order/order?id=${infiniteScroller.list[index].orderNo}`,
+  });
+};
+onLoad(() => {
+  infiniteScroller.refresh();
+});
+onPullDownRefresh(() => {
+  infiniteScroller.refresh();
+});
+onReachBottom(() => {
+  infiniteScroller.next();
+});
+</script>
+
+<style lang="scss">
+.item {
+  height: 170rpx;
+  border-bottom: 1rpx solid rgba(0, 0, 0, 0.1);
+}
+</style>

+ 174 - 0
src/pages-charge/search/search.vue

@@ -0,0 +1,174 @@
+<template>
+  <view style="opacity: 0; pointer-events: none">
+    <navigation-bar></navigation-bar>
+  </view>
+
+  <image
+    src="/static/images/back.png"
+    mode="widthFix"
+    class="back"
+    @click="back"
+    :style="backStyle"
+  ></image>
+
+  <view
+    class="search flex-align-center"
+    v-if="searchStyle"
+    :style="searchStyle"
+  >
+    <view class="search_icon flex-center flex-shrink">
+      <icon type="search" size="18" color="rgba(0,0,0,0.3)"></icon>
+    </view>
+    <input
+      class="search_input flex-grow"
+      :value="keyword"
+      type="text"
+      :focus="true"
+      placeholder="搜索附近充电站"
+      placeholder-class="input-placeholder"
+      @input="input"
+      @confirm="search"
+      confirm-type="search"
+    />
+    <view
+      class="search_close flex-align-center flex-shrink"
+      @click.stop="clear"
+      v-if="keyword"
+    >
+      <image class="icon" src="/static/images/search-close.png"></image>
+    </view>
+  </view>
+
+  <block v-if="list">
+    <view class="pl-20 pr-20" v-if="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.location.stationLat"
+          :longitude="item.location.stationLng"
+          :border="true"
+        ></charge-station>
+      </view>
+    </view>
+    <view class="empty flex-center flex-column" v-else>
+      <image
+        class="image"
+        src="/static/images/search-empty.png"
+        mode="widthFix"
+      ></image>
+      <view class="tip">无搜索结果...</view>
+    </view>
+  </block>
+</template>
+
+<script setup lang="ts">
+import { searchStation } from "@/api/charge";
+import { onLoad } from "@dcloudio/uni-app";
+import { ref } from "vue";
+const searchStyle = ref<any>();
+const backStyle = ref<any>();
+const keyword = ref("");
+const list = ref<any[]>();
+
+onLoad(() => {
+  const menu = uni.getMenuButtonBoundingClientRect();
+  searchStyle.value = {
+    top: `${menu.top}px`,
+    height: `${menu.height}px`,
+  };
+  backStyle.value = {
+    top: `${menu.top + 8}px`,
+  };
+});
+
+const back = function () {
+  uni.navigateBack();
+};
+const clear = function () {
+  keyword.value = "";
+};
+const input = function (e: any) {
+  if (e.detail && e.detail.value) {
+    keyword.value = e.detail.value;
+  }
+};
+const search = function () {
+  if (!keyword.value) {
+    uni.showModal({
+      title: "温馨提示",
+      content: "请输入搜素内容",
+      showCancel: false,
+      confirmColor: "#347DFF",
+    });
+    return;
+  }
+  uni.showLoading({
+    title: "搜索中",
+  });
+  searchStation(keyword.value)
+    .then((res) => {
+      uni.hideLoading();
+      list.value = res;
+    })
+    .catch(() => {
+      uni.hideLoading();
+      list.value = [];
+    });
+};
+</script>
+
+<style lang="scss">
+.back {
+  position: absolute;
+  width: 18rpx;
+  left: 34rpx;
+}
+
+.search {
+  position: absolute;
+  left: 80rpx;
+  width: 450rpx;
+  border-radius: 32rpx;
+  background-color: var(--color-sec);
+
+  &_icon {
+    width: 72rpx;
+  }
+
+  &_input {
+    width: 100%;
+    background-color: rgba(0, 0, 0, 0);
+    font-size: 28rpx;
+  }
+
+  .input-placeholder {
+    color: rgba(0, 0, 0, 0.3);
+  }
+
+  &_close {
+    width: 50rpx;
+    .icon {
+      height: 30rpx;
+      width: 30rpx;
+    }
+  }
+}
+
+.empty {
+  margin-top: 240rpx;
+  .image {
+    width: 368rpx;
+  }
+  .tip {
+    font-size: 24rpx;
+    color: rgba(0, 0, 0, 0.4);
+    margin-top: 20rpx;
+  }
+}
+</style>

binární
src/pages-charge/static/charge-input-head.png


binární
src/pages-charge/static/charge-ordering-bg.png


binární
src/pages-charge/static/charge-ordering-border.png


binární
src/pages-charge/static/charge-ordering-finish.png


binární
src/pages-charge/static/charge-ordering-icon.png


binární
src/pages-charge/static/machines-banner-address.png


binární
src/pages-charge/static/machines-banner-nav.png


+ 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.location.stationLat"
+        :longitude="item.location.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>

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

@@ -0,0 +1,281 @@
+<template>
+  <view class="pt-60 pb-20 flex-center">
+    <button
+      class="avatar"
+      open-type="chooseAvatar"
+      @chooseavatar="chooseAvatar"
+    >
+      <image class="avatar_image" :src="avatar" @error="errorHandle"></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: "nickname",
+      value: "",
+    },
+    {
+      title: "电话",
+      key: "",
+      disabled: true,
+    },
+    {
+      title: "车牌号",
+      key: "defaultPlateNo",
+      value: "",
+    },
+    {
+      title: "VIN码",
+      key: "vin",
+      value: "",
+    },
+    // {
+    //   title: "充电卡",
+    //   key: "",
+    //   value: "",
+    // },
+  ];
+  fetchProfile().then(() => {
+    const user = getApp<any>().globalData.user;
+    if (user) {
+      _menu[0].value = user.nickname;
+      _menu[1].value = user.mobilePhone;
+      _menu[2].value = user.defaultPlateNo;
+      _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();
+            uni.showToast({
+              title: '已更新',
+              icon: 'success'
+            });
+            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 (!menuItem.key) {
+    uni.showToast({
+      icon: "none",
+      title: "暂不支持修改",
+    });
+    return;
+  }
+  if (/车牌/.test(menuItem.title)) {
+    uni.chooseLicensePlate({
+      success: (res) => {
+        save({
+          defaultPlateNo: 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/map",
+              });
+            }, 1500);
+          })
+          .catch((err) => {
+            uni.hideLoading();
+            uni.showModal({
+              content: `${err.errMsg},请重试`,
+            });
+          });
+      }
+    },
+  });
+};
+
+const errorHandle = (e: any) => {
+  console.log(e);
+};
+
+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.transactionTime
+        }}</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].orderNo}`,
+    });
+  }
+};
+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 || params <= 0) {
+    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>

+ 136 - 15
src/pages.json

@@ -1,16 +1,137 @@
 {
-	"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
-		{
-			"path": "pages/index/index",
-			"style": {
-				"navigationBarTitleText": "uni-app"
-			}
-		}
-	],
-	"globalStyle": {
-		"navigationBarTextStyle": "black",
-		"navigationBarTitleText": "uni-app",
-		"navigationBarBackgroundColor": "#F8F8F8",
-		"backgroundColor": "#F8F8F8"
-	}
-}
+  "entryPagePath": "pages/map/map",
+  "pages": [
+    {
+      "path": "pages/index/index"
+    },
+    {
+      "path": "pages/map/map",
+      "style": {
+        "disableScroll": true
+      }
+    },
+    {
+      "path": "pages/list/list"
+    },
+    {
+      "path": "pages/user/user"
+    }
+  ],
+  "subPackages": [
+    {
+      "root": "pages-charge",
+      "pages": [
+        {
+          "path": "machines/machines",
+          "style": {
+            "navigationBarTitleText": "",
+            "navigationStyle": "default",
+            "navigationBarBackgroundColor": "#F5F5F5"
+          }
+        },
+        {
+          "path": "codeing/codeing",
+          "style": {
+            "navigationBarTitleText": "编码充电",
+            "navigationStyle": "default"
+          }
+        },
+        {
+          "path": "order/order",
+          "style": {
+            "navigationStyle": "default",
+            "navigationBarTitleText": "充电费用"
+          }
+        },
+        {
+          "path": "ordering/ordering",
+          "style": {
+            "navigationBarTitleText": "充电",
+            "navigationStyle": "default",
+            "disableScroll": true
+          }
+        },
+        {
+          "path": "orders/orders",
+          "style": {
+            "navigationStyle": "default",
+            "navigationBarTitleText": "充电订单",
+            "enablePullDownRefresh": true
+          }
+        },
+        {
+          "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": {
+    "navigationBarTextStyle": "black",
+    "navigationBarTitleText": "uni-app",
+    "navigationBarBackgroundColor": "#ffffff",
+    "backgroundColor": "#ffffff",
+    "navigationStyle": "custom",
+    "usingComponents": {
+      "navigation-bar": "./wxcomponents/navigation-bar/index"
+    }
+  },
+  "easycom": {
+    "autoscan": true
+  },
+  "tabBar": {
+    "custom": true,
+    "list": [
+      {
+        "pagePath": "pages/map/map"
+      },
+      {
+        "pagePath": "pages/list/list"
+      },
+      {
+        "pagePath": "pages/user/user"
+      }
+    ]
+  }
+}

+ 8 - 35
src/pages/index/index.vue

@@ -1,41 +1,14 @@
 <template>
-  <view class="content">
-    <image class="logo" src="/static/logo.png" />
-    <view class="text-area">
-      <text class="title">{{ title }}</text>
-    </view>
-  </view>
+  <view></view>
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue'
-const title = ref('Hello')
+import { onLoad } from "@dcloudio/uni-app";
+onLoad(() => {
+  uni.switchTab({
+    url: "/pages/map/map",
+  });
+});
 </script>
 
-<style>
-.content {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-}
-
-.logo {
-  height: 200rpx;
-  width: 200rpx;
-  margin-top: 200rpx;
-  margin-left: auto;
-  margin-right: auto;
-  margin-bottom: 50rpx;
-}
-
-.text-area {
-  display: flex;
-  justify-content: center;
-}
-
-.title {
-  font-size: 36rpx;
-  color: #8f8f94;
-}
-</style>
+<style></style>

+ 66 - 0
src/pages/list/list.vue

@@ -0,0 +1,66 @@
+<template>
+  <view class="list">
+    <navigation-bar title="充电站列表"></navigation-bar>
+    <view class="pl-20 pr-20" v-if="infiniteScroller.list">
+      <view
+        class="mt-20"
+        v-for="(item, index) in infiniteScroller.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.location.stationLat"
+          :longitude="item.location.stationLng"
+        ></charge-station>
+      </view>
+    </view>
+    <view
+      class="flex-center flex-column pt-79"
+      v-if="infiniteScroller.list && infiniteScroller.list.length <= 0"
+    >
+      <image class="empty" src="/static/images/map-empty.png" mode="widthFix" />
+      <view class="fs-22 mt-14" style="color: rgba(0, 0, 0, 0.5)"
+        >暂无充电站信息</view
+      >
+    </view>
+    <view class="flex-center flex-column pt-30" v-if="!infiniteScroller.list">
+      <view class="fs-28 mt-14" style="color: rgba(0, 0, 0, 0.5)">加载中</view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { fetchStations } from "../../api/charge";
+import { fetchCollectList } from "../../api/user";
+import { useInfiniteScroll } from "../../utils/infinite-scroll";
+import { onLoad, onReachBottom } from "@dcloudio/uni-app";
+const infiniteScroller = useInfiniteScroll(6, (page) => {
+  return fetchStations(page, 6);
+});
+onLoad(() => {
+  fetchCollectList().then(() => {
+    infiniteScroller.refresh();
+  });
+});
+onReachBottom(() => {
+  infiniteScroller.next();
+});
+</script>
+
+<style>
+.list {
+  width: 100w;
+  min-height: 100vh;
+  background-color: #f5f5f5;
+}
+
+.empty {
+  width: 80px;
+}
+</style>

+ 697 - 0
src/pages/map/map.vue

@@ -0,0 +1,697 @@
+<template>
+  <view class="container">
+    <map
+      id="map"
+      style="width: 100%; height: 100%; z-index: 1"
+      :latitude="mapProps.latitude"
+      :longitude="mapProps.longitude"
+      :markers="markers"
+      min-scale="1"
+      :scale="mapProps.scale"
+      @regionchange="mapChange"
+      @updated="mapUpdated"
+      @labeltap="tapMarker"
+      @markertap="tapMarker"
+      :show-scale="true"
+    ></map>
+    <view class="card" v-if="ready">
+      <swiper
+        class="mt-68"
+        :autoplay="false"
+        @change="changeMarker"
+        :current="markersIndex"
+      >
+        <block v-if="!empty">
+          <swiper-item v-for="(item, index) in station" :key="index">
+            <view class="station">
+              <charge-station
+                :title="item.stationName"
+                :address="item.address"
+                :price="item.totalFee"
+                :fast="item.fastEquipmentInfos"
+                :slow="item.slowEquipmentInfos"
+                :sId="item.StationID"
+                :distance="item.stationLatDistance"
+                :latitude="item.location.stationLat"
+                :longitude="item.location.stationLng"
+                :fromMap="true"
+              ></charge-station>
+            </view>
+          </swiper-item>
+        </block>
+        <block v-else>
+          <swiper-item>
+            <view class="station">
+              <view class="station-empty flex-column flex-align-center pt-20">
+                <image src="/static/images/map-empty.png" mode="widthFix" />
+                <view class="fs-22 mt-14" style="color: rgba(0, 0, 0, 0.5)"
+                  >暂无充电站信息</view
+                >
+              </view>
+            </view>
+          </swiper-item>
+        </block>
+      </swiper>
+    </view>
+    <view class="icon-menu" v-if="menuStyle.menu1" :style="menuStyle.menu1">
+      <view class="flex-center mt-40" @click="search" hover-class="hover">
+        <image src="/static/images/map-search.png" mode="widthFix" />
+      </view>
+      <view
+        class="flex-center mt-40"
+        @click="toggleDialogVisible"
+        hover-class="hover"
+      >
+        <image src="/static/images/map-filter.png" mode="widthFix" />
+      </view>
+    </view>
+    <view class="icon-menu" v-if="menuStyle.menu2" :style="menuStyle.menu2">
+      <view class="flex-center" hover-class="hover" @click="resetLocation">
+        <image src="/static/images/map-location.png" mode="widthFix" />
+      </view>
+    </view>
+    <view
+      class="dialog"
+      v-if="filterDialog.visible"
+      @click="toggleDialogVisible"
+    >
+      <view class="filter-dialog" @click.stop="emptyTap">
+        <view :style="filterDialog.style"></view>
+        <view class="pl-40 pr-40">
+          <view class="pt-20 pb-20">
+            <text class="fs-30 fw-500">距离</text>
+          </view>
+          <view class="flex-wrap pb-14">
+            <view
+              :class="[
+                'type',
+                'flex-shrink',
+                'flex-center',
+                'mt-20',
+                (index + 1) % 5 === 0 ? 'mr-0' : 'mr-20',
+                `type-${
+                  item.value === filterDialog.options.distance ? 'active' : ''
+                }`,
+              ]"
+              v-for="(item, index) in filterDialog.range"
+              :key="index"
+              @click.stop="changeFilterDistance(index)"
+              style="width: 117rpx"
+              >{{ item.value }}km</view
+            >
+          </view>
+          <view class="fs-30 fw-500 pt-38 pb-20">充电状态</view>
+          <view class="flex pb-30">
+            <view
+              :class="[
+                'type',
+                'flex-center',
+                'mr-20',
+                `type-${index === filterDialog.options.status ? 'active' : ''}`,
+              ]"
+              v-for="(item, index) in filterDialog.status"
+              :key="index"
+              @click.stop="changeFilterStatus(index)"
+              >{{ item.title }}</view
+            >
+          </view>
+          <view class="foot flex-align-center">
+            <style-button size="small" @click.stop="resetFilter"
+              >重置</style-button
+            >
+            <view style="width: 30rpx"></view>
+            <style-button size="small" type="primary" @click.stop="submitFilter"
+              >确定</style-button
+            >
+          </view>
+        </view>
+      </view>
+    </view>
+  </view>
+  <view class="login-mask" v-if="!token">
+    <button open-type="getPhoneNumber" @getphonenumber="loginMask" class="full">
+      登录按钮
+    </button>
+  </view>
+</template>
+
+<script setup lang="ts">
+const defaulDistance = 3;
+const defaultScale = 12;
+const pointSize = {
+  width: 34,
+  height: 58,
+  fontSize: 10,
+  iconPath: "/static/images/map-point.png",
+  currentWidth: 52,
+  currentHeight: 86,
+  currentFontSize: 11,
+  currentIconPath: "/static/images/map-point-current.png",
+  androidX: -14,
+  androidCurrentX: -20,
+};
+import { deCode } from "../../utils/code";
+import { fetchToken, login, onLogin } from "@/api/auth";
+import { fetchStations } from "@/api/charge";
+import { fetchCollectList } from "@/api/user";
+import { fetchLocation } from "@/utils/location";
+import { onLoad } from "@dcloudio/uni-app";
+import { ref } from "vue";
+const isIOS = ref(false);
+const token = ref<string>();
+const ready = ref(false);
+const empty = ref(true);
+const mapProps = ref({
+  latitude: 23.098994,
+  longitude: 113.32252,
+  selflatitude: 23.098994,
+  selflongitude: 113.32252,
+  scale: defaultScale,
+});
+const filterDialog = ref({
+  visible: false,
+  style: {},
+  range: [
+    {
+      value: 1,
+      scale: defaultScale + 3,
+    },
+    {
+      value: 2,
+      scale: defaultScale + 1,
+    },
+    {
+      value: 3,
+      scale: defaultScale,
+    },
+    {
+      value: 5,
+      scale: defaultScale - 0.2,
+    },
+    {
+      value: 10,
+      scale: defaultScale - 0.5,
+    },
+    {
+      value: 20,
+      scale: defaultScale - 1.5,
+    },
+    {
+      value: 30,
+      scale: defaultScale - 2,
+    },
+    {
+      value: 50,
+      scale: defaultScale - 2.5,
+    },
+    {
+      value: 100,
+      scale: defaultScale - 3.5,
+    },
+    {
+      value: 200,
+      scale: defaultScale - 4.5,
+    },
+  ],
+  options: {
+    distance: defaulDistance,
+    status: 0,
+  },
+  status: [
+    {
+      title: "全部",
+    },
+    {
+      title: "空闲",
+    },
+    {
+      title: "忙碌",
+    },
+  ],
+});
+const menuStyle = ref({
+  menu1: {},
+  menu2: {},
+});
+const stationPage = ref({
+  page: 1,
+  pageSize: 6,
+  hasNext: false,
+});
+const station = ref<any[]>([]);
+const markersIndex = ref(0);
+const markers = ref<any[]>([]);
+
+let isIgnoreChangeLocation = false;
+
+const refreshStation = (location: any) => {
+  let length = 0;
+  let available = 0;
+  const { latitude, longitude } = location;
+  if (!token.value) {
+    return;
+  }
+  return fetchStations(
+    stationPage.value.page,
+    stationPage.value.pageSize,
+    latitude,
+    longitude,
+    mapProps.value.selflatitude,
+    mapProps.value.selflongitude,
+    {
+      distance: filterDialog.value.options.distance,
+      status: filterDialog.value.options.status,
+    }
+  ).then((res) => {
+    const _markersIndex = stationPage.value.page === 1 ? 0 : markersIndex.value;
+    const _markers: any[] = res.map((item, index) => {
+      length = 0;
+      available = 0;
+      item.equipmentInfos &&
+        item.equipmentInfos.forEach((eq: any) => {
+          eq.connectorInfos &&
+            eq.connectorInfos.forEach((co: any) => {
+              length += 1;
+              if (
+                co.connectorStatusInfo &&
+                co.connectorStatusInfo.status === 1
+              ) {
+                available += 1;
+              }
+            });
+        });
+      return {
+        id: Number(item.StationID),
+        latitude: item.location.stationLat,
+        longitude: item.location.stationLng,
+        iconPath:
+          index === _markersIndex
+            ? pointSize.currentIconPath
+            : pointSize.iconPath,
+        width:
+          index === _markersIndex ? pointSize.currentWidth : pointSize.width,
+        height:
+          index === _markersIndex ? pointSize.currentHeight : pointSize.height,
+        label: {
+          content: `${available}/${length}`,
+          color: "#ffffff",
+          fontSize:
+            index === _markersIndex
+              ? pointSize.currentFontSize
+              : pointSize.fontSize,
+          textAlign: isIOS.value ? "center" : "left",
+          anchorX: isIOS.value
+            ? 0
+            : index === _markersIndex
+            ? pointSize.androidCurrentX
+            : pointSize.androidX,
+          anchorY: -(
+            (index === _markersIndex
+              ? pointSize.currentHeight
+              : pointSize.height) - 3
+          ),
+        },
+      };
+    });
+    if (stationPage.value.page === 1) {
+      _markers.push({
+        id: -1,
+        latitude: mapProps.value.selflatitude,
+        longitude: mapProps.value.selflongitude,
+        iconPath: "/static/images/map-current.png",
+        width: 34,
+        height: 34,
+      });
+    }
+    isIgnoreChangeLocation = markers.value.length !== _markers.length
+    stationPage.value.hasNext = res.length >= stationPage.value.pageSize;
+    empty.value = stationPage.value.page === 1 ? res.length <= 0 : false;
+    station.value =
+      stationPage.value.page === 1 ? res : [...station.value, ...res];
+    markersIndex.value = _markersIndex;
+    markers.value = _markers;
+    return res;
+  });
+};
+
+const refresh = () => {
+  console.log("刷新电站");
+  uni.showLoading({
+    title: "加载中",
+  });
+  stationPage.value.page = 1;
+  stationPage.value.hasNext = false;
+  station.value = [];
+  markers.value = [];
+  markersIndex.value = 0;
+  filterDialog.value.visible = false;
+  fetchLocation()
+    .then((res: any) => {
+      mapProps.value.latitude = res.latitude;
+      mapProps.value.longitude = res.longitude;
+      mapProps.value.selflatitude = res.latitude;
+      mapProps.value.selflongitude = res.longitude;
+      isIgnoreChangeLocation = true;
+      return refreshStation(res);
+    })
+    .then(() => {
+      uni.hideLoading();
+      ready.value = true;
+    })
+    .catch((err) => {
+      console.log(err);
+      uni.hideLoading();
+      uni.showModal({
+        content: `${err.errMsg},请重试`,
+      });
+    });
+};
+
+onLoad((query: any) => {
+  // 只为了打包进tab-bar使用
+  console.log(fetchToken, login, onLogin);
+  // 扫普通码
+  if (query.q) {
+    console.log("扫普通码", decodeURIComponent(query.q));
+    getApp<any>().globalData.normalCode = decodeURIComponent(query.q); // 获取到二维码原始链接内容
+  }
+  const menu = uni.getMenuButtonBoundingClientRect();
+  const window = uni.getWindowInfo();
+  const device = uni.getSystemInfoSync();
+  isIOS.value = device.osName === "ios";
+  menuStyle.value.menu1 = `top:${menu.top + menu.height}px;right:${
+    window.windowWidth - menu.right
+  }px;`;
+  menuStyle.value.menu2 = `bottom:420rpx;right:${
+    window.windowWidth - menu.right
+  }px;margin-bottom: constant(safe-area-inset-bottom);margin-bottom: env(safe-area-inset-bottom);`;
+  filterDialog.value.style = `height:${menu.bottom + 6}px;`;
+  setTimeout(() => {
+    token.value = getApp<any>().globalData.token || "";
+    if (!token.value) {
+      isIgnoreChangeLocation = true;
+      fetchLocation().then((res: any) => {
+        mapProps.value.latitude = res.latitude;
+        mapProps.value.longitude = res.longitude;
+        mapProps.value.selflatitude = res.latitude;
+        mapProps.value.selflongitude = res.longitude;
+        markers.value = [
+          {
+            id: -1,
+            latitude: res.latitude,
+            longitude: res.longitude,
+            iconPath: "/static/images/map-current.png",
+            width: 34,
+            height: 34,
+          },
+        ];
+      });
+      onLogin((_token) => {
+        if (getApp<any>().globalData.normalCode) {
+          const code: string = getApp<any>().globalData.normalCode;
+          getApp<any>().globalData.normalCode = "";
+          deCode(code);
+        }
+        token.value = _token;
+        fetchCollectList().then(() => {
+          refresh();
+        });
+      });
+      return;
+    }
+    fetchCollectList().then(() => {
+      if (getApp<any>().globalData.normalCode) {
+        const code: string = getApp<any>().globalData.normalCode;
+        getApp<any>().globalData.normalCode = "";
+        deCode(code);
+      }
+      refresh();
+    });
+  }, 300);
+});
+
+const toggleDialogVisible = () => {
+  filterDialog.value.visible = !filterDialog.value.visible;
+};
+const changeFilterDistance = (index: number) => {
+  filterDialog.value.options.distance = filterDialog.value.range[index].value;
+};
+const changeFilterStatus = (index: number) => {
+  filterDialog.value.options.status = index;
+};
+const resetFilter = () => {
+  filterDialog.value.options.distance = defaulDistance;
+  filterDialog.value.options.status = 0;
+  refresh();
+};
+const submitFilter = () => {
+  const findIndex = filterDialog.value.range.findIndex(
+    (item) => item.value === filterDialog.value.options.distance
+  );
+  if (mapProps.value.scale === filterDialog.value.range[findIndex].scale) {
+    filterDialog.value.visible = false;
+    refresh();
+    return;
+  }
+  filterDialog.value.visible = false;
+  mapProps.value.scale = filterDialog.value.range[findIndex].scale;
+};
+const resetLocation = () => {
+  // eslint-disable-next-line promise/catch-or-return
+  fetchLocation().then((res: any) => {
+    const mapCtx = uni.createMapContext("map");
+    const { latitude, longitude } = res;
+    mapCtx.moveToLocation({
+      latitude,
+      longitude,
+    });
+    mapProps.value.scale = defaultScale;
+  });
+};
+const mapUpdated = (e: any) => {
+  // console.log('map updated', isIgnoreChangeLocation)
+  setTimeout(() => {
+    isIgnoreChangeLocation = false;
+  }, 500);
+};
+const mapChange = (e: any) => {
+  if (isIgnoreChangeLocation) {
+    return;
+  }
+  if (e.type === "end" && markers.value.length) {
+    console.log("map change end", {
+      ...e.detail.centerLocation
+    });
+    const current = e.target.centerLocation;
+    const { latitude, longitude } = current;
+    stationPage.value.page = 1;
+    refreshStation({
+      latitude,
+      longitude,
+    });
+  }
+};
+const _changeMarker = (current: number) => {
+  const _markers = JSON.parse(JSON.stringify(markers.value));
+  const markersNewIndex = current;
+  _markers[markersIndex.value].iconPath = pointSize.iconPath;
+  _markers[markersIndex.value].width = pointSize.width;
+  _markers[markersIndex.value].height = pointSize.height;
+  _markers[markersIndex.value].label.fontSize = pointSize.fontSize;
+  _markers[markersIndex.value].label.anchorY = -(pointSize.height - 3);
+  if (!isIOS) {
+    _markers[markersIndex.value].label.anchorX = pointSize.androidX;
+  }
+
+  _markers[markersNewIndex].iconPath = pointSize.currentIconPath;
+  _markers[markersNewIndex].width = pointSize.currentWidth;
+  _markers[markersNewIndex].height = pointSize.currentHeight;
+  _markers[markersNewIndex].label.fontSize = pointSize.currentFontSize;
+  _markers[markersNewIndex].label.anchorY = -(pointSize.currentHeight - 3);
+  if (!isIOS) {
+    _markers[markersNewIndex].label.anchorX = pointSize.androidCurrentX;
+  }
+
+  isIgnoreChangeLocation = true;
+  markers.value = _markers;
+  markersIndex.value = markersNewIndex;
+  if (stationPage.value.hasNext && markersNewIndex >= _markers.length - 2) {
+    stationPage.value.page += 1;
+    refreshStation({
+      latitude: mapProps.value.selflatitude,
+      longitude: mapProps.value.selflongitude,
+    });
+  }
+};
+const changeMarker = (e: any) => {
+  _changeMarker(e.detail.current);
+};
+const tapMarker = (e: any) => {
+  if (e.detail.markerId === -1) {
+    return;
+  }
+  const findIndex = station.value.findIndex(
+    (item) => Number(item.StationID) === Number(e.detail.markerId)
+  );
+  if (findIndex >= 0) {
+    _changeMarker(findIndex);
+  }
+};
+const search = () => {
+  uni.navigateTo({
+    url: "/pages-charge/search/search",
+  });
+};
+const loginMask = (e: any) => {
+  login(e);
+};
+const emptyTap = () => {};
+</script>
+
+<style lang="scss">
+@import "../../styles/dialog.scss";
+page {
+  background-color: #ffffff;
+}
+
+.container {
+  position: relative;
+  height: 100vh;
+  width: 100vw;
+  background-color: #ffffff;
+}
+
+.login-mask {
+  position: fixed;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 999999;
+  opacity: 0;
+
+  .full {
+    width: 100%;
+    height: 100%;
+  }
+}
+
+.icon-menu {
+  position: absolute;
+  z-index: 9;
+
+  image {
+    width: 60rpx;
+  }
+
+  & > view {
+    height: 96rpx;
+    width: 96rpx;
+    border-radius: 50%;
+    background-color: #fff;
+    box-shadow: 0px 8rpx 20rpx rgba(0, 0, 0, 0.2);
+  }
+
+  .hover {
+    box-shadow: none;
+  }
+}
+
+.filter-dialog {
+  background-color: #fff;
+
+  .slider {
+    position: relative;
+    height: 12rpx;
+    width: 100%;
+    border-radius: 8rpx;
+    background-color: var(--color-sec);
+    margin-top: 10rpx;
+
+    &_active {
+      position: absolute;
+      left: 0;
+      top: 0;
+      background-color: var(--color-primary);
+      border-radius: 8rpx;
+      height: 12rpx;
+      width: 0%;
+    }
+
+    &_block {
+      position: absolute;
+      width: 40rpx;
+      height: 40rpx;
+      border-radius: 50%;
+      top: -15rpx;
+      left: 0;
+      background: rgba(255, 255, 255, 1);
+      box-shadow: 0px 4rpx 6rpx rgba(52, 125, 255, 0.4);
+    }
+
+    &_wx {
+      position: absolute;
+      width: 100%;
+      left: 0px;
+      top: -5px;
+      margin: 0;
+      opacity: 0;
+    }
+  }
+
+  .type {
+    width: 160rpx;
+    height: 60rpx;
+    background: var(--color-sec);
+    border-radius: 4rpx;
+    color: var(--color-gray);
+    font-size: 26rpx;
+    border: 1px solid var(--color-sec);
+  }
+
+  .type-active {
+    border: 1px solid var(--color-primary);
+    color: var(--color-primary);
+  }
+
+  .foot {
+    height: 120rpx;
+    border-top: 1rpx solid var(--color-sec);
+  }
+}
+
+.card {
+  position: absolute;
+  width: 100%;
+  height: 450rpx;
+  left: 0px;
+  bottom: 0rpx;
+  z-index: 9;
+  background: linear-gradient(180deg, rgba(87, 104, 133, 0) 0%, #576885 100%);
+  padding-left: 10rpx;
+  margin-bottom: constant(safe-area-inset-bottom);
+  margin-bottom: env(safe-area-inset-bottom);
+
+  swiper,
+  swiper-item {
+    width: 100%;
+    height: 300rpx;
+  }
+
+  .station {
+    width: 100%;
+    height: 100%;
+    padding: 0rpx 20rpx;
+  }
+
+  .station-empty {
+    width: 100%;
+    height: 100%;
+    background-color: #fff;
+    border-radius: 20rpx 20rpx 0 0;
+    image {
+      width: 160rpx;
+    }
+  }
+}
+</style>

+ 293 - 0
src/pages/user/user.vue

@@ -0,0 +1,293 @@
+<template>
+  <navigation-bar title="个人中心"></navigation-bar>
+
+  <image src="/static/images/user-bg.png" mode="widthFix" class="bg" />
+
+  <block v-if="user && user.mobilePhone">
+    <view class="container" :style="containerStyle">
+      <view class="header flex-column">
+        <view class="flex-grow flex-center">
+          <image
+            src="/static/images/user/round.png"
+            mode="heightFix"
+            style="height: 100%"
+          />
+        </view>
+        <view class="main flex-shrink">
+          <view
+            class="avatar"
+            @click="toPage(3)"
+            :style="{
+              'background-image': `url(${user.avatar})`,
+            }"
+          ></view>
+          <view class="phone fs-40 fw-500">{{ user.mobilePhone }}</view>
+          <view class="money flex-align-center">
+            <view class="fs-40 fw-500" style="margin-top: 10rpx">¥</view>
+            <view class="fw-500 ml-12" style="font-size: 60rpx">{{
+              user.balance
+            }}</view>
+            <view class="ml-auto" style="width: 140rpx">
+              <style-button
+                @click="toPage(0)"
+                size="small"
+                type="primary"
+                height="70"
+                >充值</style-button
+              >
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <view class="body">
+        <view
+          class="menu flex-align-center"
+          v-for="(item, index) in menu"
+          :key="index"
+          @click="toPage(index)"
+        >
+          <image src="/static/images/user/{{index + 1}}.png"></image>
+          <view>{{ item.title }}</view>
+          <view class="ml-auto">
+            <uni-icons
+              type="right"
+              size="12"
+              color="rgba(0,0,0,0.4)"
+            ></uni-icons>
+          </view>
+        </view>
+      </view>
+    </view>
+  </block>
+
+  <view
+    class="dialog flex-align-end"
+    style="z-index: 999999"
+    v-if="contactDialogVisible"
+    @click="close"
+  >
+    <view class="contact-dialog" @click.stop="emptyTap">
+      <view class="flex-center fs-32 fw-600 pb-40">联系我们</view>
+      <!-- <view class="code" v-if="menu[4].code">
+        <image-proxy
+          style="width: 100%"
+          :src="menu[4].code"
+          mode="widthFix"
+          menu
+        ></image-proxy>
+      </view>
+      <view
+        v-if="menu[4].code"
+        class="fs-22 mt-16"
+        style="color: rgba(0, 0, 0, 0.4)"
+        >长按识别二维码</view
+      > -->
+      <view class="phone flex-align-center flex-between" @click.stop="call">
+        <view class="fs-26 fw-500">联系方式:{{ menu[4].mobile }}</view>
+        <view class="flex-center btn">拨打电话</view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { fetchContact } from "@/api";
+import { fetchProfile } from "@/api/user";
+import { onLoad, onShow } from "@dcloudio/uni-app";
+import { ref } from "vue";
+const containerStyle = ref({});
+const user = ref<any>({
+  avatar: "",
+});
+const menu = ref([
+  {
+    title: "我的钱包",
+    path: "/pages-user/wallet/wallet",
+  },
+  {
+    title: "充电订单",
+    path: "/pages-charge/orders/orders",
+  },
+  {
+    title: "我的收藏",
+    path: "/pages-user/collect/collect",
+  },
+  {
+    title: "个人信息",
+    path: "/pages-user/profile/profile",
+  },
+  {
+    title: "联系我们",
+    path: "",
+    code: "",
+    mobile: "",
+  },
+]);
+const contactDialogVisible = ref(false);
+const toPage = (index: number) => {
+  const item = menu.value[index];
+  if (item.mobile) {
+    contactDialogVisible.value = true;
+    return;
+  }
+  uni.navigateTo({
+    url: item.path,
+  });
+};
+const close = () => {
+  contactDialogVisible.value = false;
+};
+const emptyTap = () => {};
+const call = () => {
+  uni.makePhoneCall({
+    phoneNumber: menu.value[4].mobile as string,
+  });
+};
+onLoad(() => {
+  const bound = uni.getMenuButtonBoundingClientRect();
+  containerStyle.value = {
+    top: `${bound.bottom + 10}px`,
+  };
+  fetchContact().then((res) => {
+    if (res && res.mobile) {
+      menu.value[4].mobile = res.mobile;
+    }
+    if (res && res.codeUrl) {
+      menu.value[4].code = res.codeUrl;
+    }
+  });
+});
+onShow(() => {
+  fetchProfile().then((res) => {
+    res.balance = (Number(res.balance) / 100).toFixed(2);
+    res.avatar = res.avatar
+      ? res.avatar
+      : "https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0";
+    user.value = res;
+  });
+});
+</script>
+
+<style lang="scss">
+@import "../../styles/dialog.scss";
+.bg {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 660rpx;
+}
+
+.container {
+  position: absolute;
+  left: 0;
+  top: 0;
+  height: 100vh;
+  width: 100%;
+  overflow-y: auto;
+}
+
+.header {
+  height: 400rpx;
+  width: 100%;
+
+  .main {
+    height: 334rpx;
+    background: rgba(254, 255, 255, 0.7);
+    border-radius: 40rpx 40rpx 0 0;
+    position: relative;
+    text-align: center;
+
+    .avatar {
+      position: absolute;
+      left: 50%;
+      transform: translateX(-50%);
+      top: -50rpx;
+      width: 110rpx;
+      height: 110rpx;
+      border: 2rpx solid #ffffff;
+      filter: drop-shadow(0px 4rpx 8rpx rgba(0, 24, 60, 0.1));
+      background-size: cover;
+      background-repeat: no-repeat;
+      border-radius: 50%;
+    }
+
+    .phone {
+      padding-top: 78rpx;
+      color: #000;
+    }
+
+    .money {
+      width: 690rpx;
+      height: 118rpx;
+      background: #feffff;
+      border-radius: 120rpx;
+      margin: 0 auto;
+      margin-top: 30rpx;
+      color: #000000;
+      padding: 0 24rpx 0 40rpx;
+    }
+  }
+}
+
+.body {
+  width: 100%;
+  background-color: #fff;
+  padding: 0rpx 30rpx;
+
+  .menu {
+    background-color: #fff;
+    height: 120rpx;
+    border-bottom: 1rpx solid rgba(0, 0, 0, 0.1);
+
+    image {
+      width: 52rpx;
+      height: 52rpx;
+      margin-right: 20rpx;
+    }
+
+    &:last-child {
+      border-bottom: none;
+    }
+  }
+}
+
+.contact-dialog {
+  width: 100%;
+  padding: 40rpx 30rpx 70rpx 30rpx;
+  padding-bottom: 70rpx;
+  background-color: #fff;
+  border-radius: 40rpx 40rpx 0rpx 0rpx;
+  text-align: center;
+
+  .icon {
+    width: 52rpx;
+  }
+
+  .code {
+    width: 320rpx;
+    min-height: 320rpx;
+    margin: 0 auto;
+    margin-bottom: 40rpx;
+  }
+
+  .phone {
+    height: 76rpx;
+    background: var(--color-sec);
+    border-radius: 76rpx;
+    padding: 0 30rpx;
+  }
+
+  .btn {
+    width: 144rpx;
+    height: 56rpx;
+    background: #ffffff;
+    border: 1rpx solid var(--color-primary);
+    border-radius: 56rpx;
+    font-size: 26rpx;
+    line-height: 56rpx;
+    color: var(--color-primary);
+  }
+}
+</style>

binární
src/static/images/back.png


binární
src/static/images/custom-tab-bar/1-1.png


binární
src/static/images/custom-tab-bar/1.png


binární
src/static/images/custom-tab-bar/2-2.png


binární
src/static/images/custom-tab-bar/2-3.png


binární
src/static/images/custom-tab-bar/2-4.png


binární
src/static/images/custom-tab-bar/2.png


binární
src/static/images/custom-tab-bar/3-1.png


binární
src/static/images/custom-tab-bar/3.png


binární
src/static/images/custom-tab-bar/4-1.png


binární
src/static/images/custom-tab-bar/4.png


binární
src/static/images/custom-tab-bar/bg.png


binární
src/static/images/icon-nav.png


binární
src/static/images/map-current.png


binární
src/static/images/map-empty.png


binární
src/static/images/map-filter.png


binární
src/static/images/map-location.png


binární
src/static/images/map-point-current.png


binární
src/static/images/map-point.png


binární
src/static/images/map-search.png


binární
src/static/images/search-close.png


binární
src/static/images/search-empty.png


binární
src/static/images/user-bg.png


binární
src/static/images/user/1.png


binární
src/static/images/user/2.png


binární
src/static/images/user/3.png


binární
src/static/images/user/4.png


binární
src/static/images/user/5.png


binární
src/static/images/user/6.png


binární
src/static/images/user/round.png


binární
src/static/images/wallet-logo.png


binární
src/static/logo.png


+ 11 - 0
src/styles/dialog.scss

@@ -0,0 +1,11 @@
+.dialog {
+  position: fixed;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  width: 100%;
+  height: 100%;
+  background-color: rgba(0, 0, 0, 0.6);
+  z-index: 9999;
+}

+ 101 - 0
src/styles/flex.scss

@@ -0,0 +1,101 @@
+.flex {
+  display: flex;
+}
+
+.inline-flex {
+  display: inline-flex;
+}
+
+.flex-center {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.flex-end {
+  display: flex;
+  justify-content: flex-end;
+  align-items: flex-end;
+}
+
+.flex-wrap {
+  display: flex;
+  flex-wrap: wrap;
+}
+
+.flex-align-center {
+  display: flex;
+  align-items: center;
+}
+
+.flex-align-baseline {
+  display: flex;
+  align-items: baseline;
+}
+
+.flex-justify-center {
+  display: flex;
+  justify-content: center;
+}
+
+.flex-justify-end {
+  display: flex;
+  justify-content: flex-end;
+}
+
+.flex-align-end {
+  display: flex;
+  align-items: flex-end;
+}
+
+.flex-justify-end {
+  display: flex;
+  justify-content: flex-end;
+}
+
+.flex-between {
+  display: flex;
+  justify-content: space-between;
+}
+
+.flex-around {
+  display: flex;
+  justify-content: space-around;
+}
+
+.flex-column {
+  display: flex;
+  flex-direction: column;
+}
+
+.flex-column-between {
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+}
+
+.flex-column-justify-center {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+}
+
+.flex-column-justify-end {
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-end;
+}
+
+.flex-column-align-end {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.flex-grow {
+  flex-grow: 1;
+}
+
+.flex-shrink {
+  flex-shrink: 0;
+}

+ 36 - 0
src/styles/font.scss

@@ -0,0 +1,36 @@
+@for $i from 2 to 100 {
+  .fs-#{$i} {
+    font-size: #{$i}rpx;
+  }
+  .lh-#{$i} {
+    line-height: #{$i}rpx;
+  }
+}
+
+.fs-0 {
+  font-size: 0px;
+}
+
+.fw-500 {
+  font-weight: 500;
+}
+
+.fw-600 {
+  font-weight: 600;
+}
+
+.fw-bold {
+  font-weight: bold;
+}
+
+.lh-0 {
+  line-height: 0px;
+}
+
+.lh-normal {
+  line-height: normal;
+}
+
+.text-center {
+  text-align: center;
+}

+ 43 - 0
src/styles/layout.scss

@@ -0,0 +1,43 @@
+$position: top, right, bottom, left;
+
+@each $item in $position {
+  .m#{str-slice($item, 0, 1)}-auto {
+    margin-#{$item}: auto;
+  }
+  .m#{str-slice($item, 0, 1)}-0 {
+    margin-#{$item}: 0px;
+  }
+}
+
+@for $i from 2 to 100 {
+  @each $item in $position {
+    .m#{str-slice($item, 0, 1)}-#{$i} {
+      margin-#{$item}: #{$i}rpx;
+    }
+    .p#{str-slice($item, 0, 1)}-#{$i} {
+      padding-#{$item}: #{$i}rpx;
+    }
+  }
+  .height-#{$i} {
+    height: #{$i}rpx;
+  }
+  .width-#{$i} {
+    width: #{$i}rpx;
+  }
+}
+
+.mb-0 {
+  margin-bottom: 0px;
+}
+
+.ml-auto {
+  margin-left: auto;
+}
+
+.mt-auto {
+  margin-top: auto;
+}
+
+.width-168 {
+  width: 168rpx;
+}

+ 33 - 0
src/utils/code.ts

@@ -0,0 +1,33 @@
+export function scanCode() {
+  uni.scanCode({
+    scanType: ["qrCode"],
+    success: (res) => {
+      console.log(res);
+      if (res.scanType === "QR_CODE" && res.result) {
+        deCode(res.result);
+      }
+    },
+  });
+}
+
+export function deCode(url: string) {
+  // 线上 https://www.kuaiyuman.cn/#001091
+  if (
+    /h5\.en-plus\.com/.test(url) ||
+    /www.en-plus.cn/.test(url) ||
+    /dev\.en-plus\.com\.cn/.test(url) ||
+    /dev\.kuaiyuman\.cn/.test(url) || 
+    /www\.kuaiyuman\.cn/.test(url)
+  ) {
+    const split = url.split("/");
+    const sn = split[split.length - 1].replace("#", "");
+    uni.navigateTo({
+      url: `/pages-charge/ordering/ordering?sn=${sn}`,
+    });
+  } else {
+    uni.showModal({
+      content: "输扫描正确的二维码",
+      showCancel: false,
+    });
+  }
+}

+ 16 - 0
src/utils/constant.ts

@@ -0,0 +1,16 @@
+declare const process: any;
+declare const __wxConfig: any;
+
+let isDevelopment = process.env.NODE_ENV === "development";
+
+// #ifdef MP-WEIXIN
+const env =
+  typeof __wxConfig !== "undefined"
+    ? __wxConfig.envVersion || "release"
+    : "release";
+console.log("env", env);
+isDevelopment = env === "develop" || env === "trial";
+// #endif
+
+export const domain = !isDevelopment ? "dev.kuaiyuman.cn" : "www.kuaiyuman.cn";
+export const host = `https://${domain}/api`;

+ 20 - 0
src/utils/date.ts

@@ -0,0 +1,20 @@
+export function format(format: string, time?: number) {
+  const now = time ? new Date(time) : new Date()
+  const map: Record<string, string | number> = {
+    y: now.getFullYear(),
+    M: now.getMonth() + 1,
+    d: now.getDate(),
+    h: now.getHours() > 9 ? now.getHours() : `0${now.getHours()}`,
+    m: now.getMinutes() > 9 ? now.getMinutes() : `0${now.getMinutes()}`,
+    s: now.getSeconds() > 9 ? now.getSeconds() : `0${now.getSeconds()}`
+  }
+  const res = []
+  for (let i = 0; i < format.length; i++) {
+    if (map[format.charAt(i)]) {
+      res.push(map[format.charAt(i)])
+    } else {
+      res.push(format.charAt(i))
+    }
+  }
+  return res.join('')
+}

+ 159 - 0
src/utils/http.ts

@@ -0,0 +1,159 @@
+import { clearToken } from "../api/auth";
+
+interface IOptions {
+  data?: any;
+  params?: any;
+  header?: any;
+  statusCodeHandle?: boolean;
+}
+
+interface IResponse<T> {
+  code: number;
+  data: T;
+  message: string;
+}
+
+class Http {
+  private baseUrl: string;
+  private header?: any;
+
+  constructor(baseUrl?: string, header?: any) {
+    this.baseUrl = baseUrl || "";
+    if (header) {
+      this.header = header;
+    }
+  }
+
+  request<R = any>(
+    method: "GET" | "POST" | "PUT",
+    api: string,
+    options?: IOptions
+  ): Promise<R> {
+    return new Promise((resolve, reject) => {
+      if (!options) {
+        options = {
+          statusCodeHandle: true,
+        };
+      }
+      const url = this.baseUrl + api;
+      const urlHasParams = url.indexOf("?") >= 0;
+      const header = {
+        // 'content-type': 'application/json',
+        satoken: getApp<any>().globalData.token || "",
+        ...(this.header || {}),
+        ...(options.header || {}),
+      };
+      const data = options.data || {};
+      const params = options.params || {};
+      const query = Object.keys(params)
+        .map(
+          (key) =>
+            `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`
+        )
+        .join("&");
+
+      WxRequest<IResponse<R>>({
+        url: `${url}${query ? (urlHasParams ? "&" : "?") : ""}${query}`,
+        method,
+        data,
+        header,
+      })
+        .then((res) => {
+          // console.log(res)
+          // 刷新token 已废弃
+          // if (res.code === 888) {
+          //   // eslint-disable-next-line promise/no-nesting
+          //   return WxRequest<IResponse<R>>({
+          //     url: `${url}${query ? (urlHasParams ? "&" : "?") : ""}${query}`,
+          //     method,
+          //     data,
+          //     header: {
+          //       ...header,
+          //       Authorization: getApp<any>().globalData.token || "",
+          //     },
+          //   });
+          // }
+          if ([21005, 21000, 10001].includes(Number(res.code))) {
+            clearToken();
+            setTimeout(() => {
+              uni.reLaunch({
+                url: "/pages/map/map",
+              });
+            }, 1500);
+            throw {
+              errMsg: "请重新登录",
+            };
+          }
+          return res;
+        })
+        .then((res) => {
+          // eslint-disable-next-line no-console
+          console.log("接口返回", res);
+          const { message = '' } = res || {
+            msg: "出现错误",
+          };
+          if (res && message === "ok") {
+            resolve(res.data);
+          } else {
+            throw {
+              errMsg: message,
+            };
+          }
+        })
+        .catch((err) => {
+          if (err && err.errMsg && options && options.statusCodeHandle) {
+            uni.showToast({
+              title: `${err.errMsg}`,
+              icon: "none",
+            });
+          }
+          reject(err);
+        });
+    });
+  }
+
+  get<R = any>(api: string, options?: IOptions) {
+    return this.request<R>("GET", api, options);
+  }
+
+  post<R = any>(api: string, options?: IOptions) {
+    return this.request<R>("POST", api, options);
+  }
+
+  put<R = any>(api: string, options?: IOptions) {
+    return this.request<R>("PUT", api, options);
+  }
+}
+
+export function WxRequest<T = any>(
+  option: UniNamespace.RequestOptions
+): Promise<T> {
+  return new Promise((resolve, reject) => {
+    uni.request({
+      ...option,
+      success(res: any) {
+        // console.log('微信返回', res)
+        const { statusCode, header } = res;
+        // if (header["Authorization"]) {
+        //   setToken(header["Authorization"]);
+        //   resolve({
+        //     ...res.data,
+        //     code: 888,
+        //   });
+        //   return;
+        // }
+        if (statusCode > 200) {
+          reject({
+            errMsg: `${option.url}:${statusCode}`,
+          });
+        }
+        resolve(res.data);
+      },
+      fail: reject,
+    });
+  });
+}
+
+export default Http;
+
+export const http = new Http();

+ 76 - 0
src/utils/infinite-scroll.ts

@@ -0,0 +1,76 @@
+import { reactive, ref } from "vue";
+
+export function useInfiniteScroll<T = any>(
+  pageSize: number,
+  loadData: (page: number) => Promise<T[]>
+) {
+  const loading = ref(false);
+  const page = ref(1);
+  const hasNext = ref(false);
+  const list = ref();
+
+  const refresh = () => {
+    if (loading.value) {
+      return;
+    }
+    uni.showLoading({
+      title: "加载中",
+    });
+    if (list.value) {
+      list.value = [];
+    }
+    loading.value = true;
+    return loadData(1)
+      .then((res) => {
+        uni.hideLoading();
+        loading.value = false;
+        page.value = 1;
+        hasNext.value = res.length >= pageSize;
+        list.value = res;
+        uni.stopPullDownRefresh();
+      })
+      .catch(() => {
+        uni.hideLoading();
+        loading.value = false;
+      });
+  };
+
+  const next = () => {
+    if (hasNext.value) {
+      if (loading.value) {
+        return;
+      }
+      uni.showLoading({
+        title: "加载中",
+      });
+      loading.value = true;
+      return loadData(page.value + 1)
+        .then((res) => {
+          uni.hideLoading();
+          loading.value = false;
+          if (!res || res.length <= 0) {
+            return;
+          }
+          page.value = page.value + 1;
+          hasNext.value = res.length >= pageSize;
+          list.value = res;
+          res.forEach((item) => {
+            list.value.push(item);
+          });
+        })
+        .catch(() => {
+          uni.hideLoading();
+          loading.value = false;
+        });
+    }
+  };
+
+  return reactive({
+    page,
+    loading,
+    hasNext,
+    list,
+    refresh,
+    next,
+  });
+}

+ 38 - 0
src/utils/location.ts

@@ -0,0 +1,38 @@
+let cacheLocation = {
+  latitude: 0,
+  longitude: 0
+}
+
+export function fetchLocation() {
+  return new Promise(resolve => {
+    uni.getLocation({
+      type: 'gcj02',
+      success: res => {
+        const latitude = res.latitude
+        const longitude = res.longitude
+        cacheLocation = {
+          latitude,
+          longitude
+        }
+        resolve(cacheLocation)
+      },
+      fail: err => {
+        // console.log(err)
+        if (/auth/.test(err.errMsg)) {
+          resolve({
+            latitude: 23.098994,
+            longitude: 113.32252
+          })
+          return
+        }
+        if (/电量/.test(err.errMsg)) {
+          resolve(cacheLocation)
+          return
+        }
+        uni.showModal({
+          content: `${err.errMsg},请重试`
+        })
+      }
+    })
+  })
+}

+ 112 - 0
src/utils/storage.ts

@@ -0,0 +1,112 @@
+class Storage {
+  private namespace: string
+
+  constructor(namespace?: string) {
+    this.namespace = `CHONGDIAN_${namespace || 'DEFAULT'}`
+  }
+
+  set<T = any>(key: string, data: T, expire?: number): Promise<void> {
+    const now = new Date().getTime()
+    const _d = {
+      data,
+      expire: expire ? now + expire : 0
+    }
+    return new Promise((resolve, reject) => {
+      uni.setStorage({
+        key: `${this.namespace}_${key}`,
+        data: JSON.stringify(_d),
+        success() {
+          resolve()
+        },
+        fail(err) {
+          reject(err)
+        }
+      })
+    })
+  }
+
+  get<T = any>(key: string, defaultValue?: T): Promise<T | undefined> {
+    return new Promise(resolve => {
+      const _key = `${this.namespace}_${key}`
+      uni.getStorage({
+        key: _key,
+        success(res) {
+          if (res && res.data) {
+            try {
+              const data = JSON.parse(res.data)
+              if (!data.expire) {
+                resolve(data.data)
+                return
+              }
+              const now = new Date().getTime()
+              if (now <= data.expire) {
+                resolve(data.data)
+                return
+              }
+            } catch (error) {
+              // eslint-disable-next-line no-console
+              console.log(error)
+            }
+          }
+          uni.removeStorage({
+            key: _key
+          })
+          resolve(defaultValue ? defaultValue : undefined)
+        },
+        fail(err) {
+          resolve(defaultValue ? defaultValue : undefined)
+          return err
+        }
+      })
+    })
+  }
+
+  clear(key?: string): Promise<void> {
+    if (key) {
+      const _key = `${this.namespace}_${key}`
+      return this.rm(_key)
+    }
+    return new Promise((resolve, reject) => {
+      uni.getStorageInfo({
+        success: res => {
+          if (res.keys && res.keys.length) {
+            const reg = new RegExp(`^${this.namespace.replace(/\^/gi, '\\^')}.+`)
+            const ps: Promise<void>[] = []
+            res.keys.forEach(key => {
+              if (reg.test(key)) {
+                ps.push(this.rm(key))
+              }
+            })
+            Promise.all(ps)
+              .then(() => {
+                resolve()
+                return true
+              })
+              .catch(reject)
+            return
+          }
+          resolve()
+        },
+        fail: err => {
+          reject(err)
+        }
+      })
+    })
+  }
+
+  private rm(key: string): Promise<void> {
+    return new Promise((resolve, reject) => {
+      uni.removeStorage({
+        key,
+        success() {
+          resolve()
+        },
+        fail(err) {
+          reject(err)
+        }
+      })
+    })
+  }
+}
+
+export default Storage

+ 127 - 0
src/utils/uploader.ts

@@ -0,0 +1,127 @@
+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}/file/upload`,
+    filePath,
+    {},
+    {
+      success: (res: any) => {
+        if (res.statusCode == 200 && res.data) {
+          try {
+            const data = JSON.parse(res.data);
+            if (data.message === "ok") {
+              callback.onSuccess &&
+                callback.onSuccess({
+                  url: data.data.url,
+                });
+            } else {
+              callback.onFail &&
+              callback.onFail({
+                errMsg: data.message,
+              });
+            }
+          } 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,
+    header: {
+      satoken: getApp<any>().globalData.token || "",
+    },
+    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();
+    },
+  });
+}

+ 98 - 0
src/wxcomponents/navigation-bar/index.js

@@ -0,0 +1,98 @@
+Component({
+  properties: {
+    title: {
+      type: String
+    },
+    color: {
+      type: String,
+      value: '#000000'
+    },
+    background: {
+      type: String,
+      value: 'rgba(0,0,0,0)'
+    },
+    home: {
+      type: Boolean,
+      value: false
+    },
+    homePath: {
+      type: String,
+      value: '/pages/map/map'
+    },
+    placeholder: {
+      type: Boolean,
+      value: true
+    },
+    autoFixed: {
+      type: Boolean,
+      value: true
+    },
+    autoFixedDistance: {
+      type: Number,
+      value: 4
+    },
+    autoFixedStyle: {
+      type: String
+    },
+  },
+  data: {
+    fixed: false,
+    showBack: false,
+    height: 56,
+    barHeight: 32
+  },
+  lifetimes: {
+    ready() {
+      const NAVIGATION_HEIGHT_CACHE = `NAVIGATION_HEIGHT`
+      const NAVIGATION_BAR_HEIGHT_CACHE = `NAVIGATION_BAR_HEIGHT`
+      const pages = getCurrentPages()
+      let height = wx.getStorageSync(NAVIGATION_HEIGHT_CACHE) || 0
+      let barHeight = wx.getStorageSync(NAVIGATION_BAR_HEIGHT_CACHE) || 0
+      if (!height || !barHeight) {
+        const menuButtonRect = wx.getMenuButtonBoundingClientRect()
+        height = menuButtonRect.bottom
+        barHeight = height - menuButtonRect.top
+        wx.setStorageSync(NAVIGATION_HEIGHT_CACHE, height)
+        wx.setStorageSync(NAVIGATION_BAR_HEIGHT_CACHE, barHeight)
+      }
+      this.setData({
+        showBack: pages.length > 1,
+        height,
+        barHeight
+      })
+      this.triggerEvent('ready', {
+        navigationBarHeight: height + 6,
+        statusBarHeight: height - barHeight
+      })
+    },
+    attached() {
+      if (this.data.autoFixed) {
+        this._createIntersectionObserver()
+      }
+    }
+  },
+  methods: {
+    back() {
+      wx.navigateBack()
+    },
+    backHome() {
+      wx.reLaunch({
+        url: this.data.homePath
+      })
+    },
+    _createIntersectionObserver() {
+      wx.nextTick(() => {
+        const observer = wx.createIntersectionObserver(this, { initialRatio: 1, observeAll: false })
+        observer.relativeToViewport().observe('.navigation-observer', () => {
+          const fixed = !this.data.fixed
+          this.setData({
+            fixed
+          })
+          this.triggerEvent('fixed', {
+            status: fixed
+          })
+        })
+      })
+    }
+  }
+})

+ 4 - 0
src/wxcomponents/navigation-bar/index.json

@@ -0,0 +1,4 @@
+{
+  "component": true,
+  "usingComponents": {}
+}

+ 18 - 0
src/wxcomponents/navigation-bar/index.wxml

@@ -0,0 +1,18 @@
+<view class="navigation-observer" style="height:{{autoFixedDistance}}px;" wx:if="{{autoFixed}}"></view>
+<view class="navigation-placeholder" style="height: {{placeholder ? height + 6 : 0}}px;"></view>
+<view class="navigation navigation-{{autoFixed ? 'fixed':'relative'}}" style="height: {{height + 6}}px;background: {{background}};{{fixed ? autoFixedStyle : ''}}">
+  <view class="navigation-bar" style="height: {{barHeight + 6}}px;line-height: {{barHeight}}px;color:{{color}};">
+    <view wx:if="{{showBack && home}}" class="icon-menu" style="top:{{height - barHeight}}px;height:{{barHeight}}px;border-radius:{{barHeight}}px;">
+      <view class="navigation-bar-font navigation-bar-font-fanhui" catchtap="back"></view>
+      <view class="icon-menu-line"></view>
+      <view class="navigation-bar-font navigation-bar-font-zhuye" catchtap="backHome"></view>
+    </view>
+    <view wx:if="{{!showBack && home}}" class="icon-home" style="top:{{height - barHeight}}px;height:{{barHeight}}px;width:{{barHeight}}px;" catchtap="backHome">
+      <view class="navigation-bar-font navigation-bar-font-zhuye" ></view>
+    </view>
+    <view wx:if="{{showBack && !home}}" class="icon-home" style="top:{{height - barHeight}}px;height:{{barHeight}}px;width:{{barHeight}}px;border:none;background:none;" catchtap="back">
+      <view class="navigation-bar-font navigation-bar-font-fanhui" style="color:{{color}};"></view>
+    </view>
+    <view>{{ title }}</view>
+  </view>
+</view>

+ 101 - 0
src/wxcomponents/navigation-bar/index.wxss

@@ -0,0 +1,101 @@
+@font-face {
+  font-family: "navigation-bar"; /* Project id 2601138 */
+  src: url('//at.alicdn.com/t/font_2601138_flivtnm3xwm.woff2?t=1650434642116') format('woff2'),
+       url('//at.alicdn.com/t/font_2601138_flivtnm3xwm.woff?t=1650434642116') format('woff'),
+       url('//at.alicdn.com/t/font_2601138_flivtnm3xwm.ttf?t=1650434642116') format('truetype');
+}
+
+.navigation-bar-font {
+  font-family: "navigation-bar" !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  color: #000;
+  font-size: 34rpx;
+}
+
+.navigation-bar-font-fanhui:before {
+  content: "\e6c3";
+}
+
+.navigation-bar-font-zhuye:before {
+  content: "\e6c4";
+}
+
+.navigation {
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-end;
+  left: 0;
+  top: 0;
+  z-index: 999;
+}
+
+.navigation-fixed {
+  position: fixed;
+}
+
+.navigation-relative {
+  position: absolute;
+}
+
+.navigation-placeholder, .navigation-observer {
+  width: 100%;
+  opacity: 0;
+  pointer-events: none;
+}
+
+.navigation-observer {
+  height: 4px;
+  position: absolute;
+  left: 0;
+  top: 0;
+  opacity: 0;
+}
+
+.navigation-bar {
+  font-size: 34rpx;
+  font-weight: 500;
+  text-align: center;
+  box-sizing: border-box;
+  position: relative;
+}
+
+.navigation-bar .icon-home {
+  position: fixed;
+  z-index: 999;
+  left: 14rpx;
+  top: 0px;
+  background-color: #fff;
+  color: #000;
+  flex-shrink: 0;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border-radius: 50%;
+  background: rgba(255, 255, 255, 0.7);
+  overflow: hidden;
+  border: 1px solid rgba(151, 151, 151, 0.20);
+}
+
+.navigation-bar .icon-menu {
+  position: fixed;
+  z-index: 999;
+  left: 14rpx;
+  top: 0rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 174rpx;
+  background: rgba(255, 255, 255, 0.7);
+  border: 1px solid rgba(151, 151, 151, 0.20);
+}
+
+.navigation-bar .icon-menu-line {
+  width: 1px;
+  height: 38rpx;
+  background: rgba(0, 0, 0, 0.20);
+  margin: 0 24rpx;
+}

+ 2 - 1
tsconfig.json

@@ -1,6 +1,7 @@
 {
   "extends": "@vue/tsconfig/tsconfig.json",
   "compilerOptions": {
+    "ignoreDeprecations": "5.0",
     "sourceMap": true,
     "baseUrl": ".",
     "paths": {
@@ -9,5 +10,5 @@
     "lib": ["esnext", "dom"],
     "types": ["@dcloudio/types"]
   },
-  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
+  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"]
 }

+ 3 - 0
types/index.d.ts

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