2
0

33 Commits 434e42ebae ... ae0ab3f2ac

Autor SHA1 Mensagem Data
  simsir ae0ab3f2ac feat:显示所有电站 há 2 anos atrás
  needcode 5e039e38e6 fix:不存在电桩提示 há 2 anos atrás
  needcode 1386c55758 fix:充电时间 há 2 anos atrás
  needcode 64ee76c13d fix:预约时间 há 2 anos atrás
  needcode 99fa2d8752 feat:活动页 há 2 anos atrás
  needcode d0776ba1e2 fix:滚动问题 há 2 anos atrás
  needcode 3a806c887c fix:优惠选择 há 2 anos atrás
  needcode 6d636c7a9d feat:优化 há 2 anos atrás
  needcode fd6b26f3bb fix:图标颜色 há 2 anos atrás
  needcode cb0dd5b6da fix:样式问题 há 2 anos atrás
  needcode 9581768c9e feat:充电状态 há 2 anos atrás
  needcode 47be629f9e fix:预约状态问题 há 2 anos atrás
  needcode e21c34d1db feat:充值优惠选择 há 2 anos atrás
  needcode 31fb8c646e fix:省电模式 há 2 anos atrás
  needcode e3b8521e57 fix:滚动 há 2 anos atrás
  needcode 28ac1da2c8 fix:颜色 há 2 anos atrás
  needcode 8a65b215f2 fix:邮箱 há 2 anos atrás
  needcode 96508fe189 feat:优化体验 há 2 anos atrás
  needcode b3da413f35 fix:发票显示 há 2 anos atrás
  needcode 2ef8579d87 fix:问题 há 2 anos atrás
  needcode 9db7b806ac fix:问题 há 2 anos atrás
  needcode c2c64f1574 fix:修复问题 há 2 anos atrás
  needcode 3a526814d4 fix:bug há 2 anos atrás
  needcode f26f3217ce fix:修复问题 há 2 anos atrás
  needcode 7bfdb9fe4b feat:剩余问题修复 há 2 anos atrás
  needcode 19fdb83b9b feat:地图功能 há 2 anos atrás
  needcode 1e922bb820 feat:预约充电 há 2 anos atrás
  needcode 8e19af496e feat:发票 há 2 anos atrás
  needcode 02c406d3c6 feat:首页优化 & 退款接口 há 2 anos atrás
  needcode bc55e8ec60 feat:预约充电 há 2 anos atrás
  needcode bd2787d349 feat:相机页面 há 2 anos atrás
  needcode d3d1e24c7e feat:新增更多页面 há 2 anos atrás
  needcode e80fb21046 feat:常见问题 há 2 anos atrás
75 ficheiros alterados com 4338 adições e 1157 exclusões
  1. 1 1
      README.md
  2. 1 0
      package.json
  3. 86 1
      src/App.vue
  4. 204 59
      src/api/charge.ts
  5. 33 5
      src/api/index.ts
  6. 41 3
      src/api/user.ts
  7. 28 9
      src/components/charge-station/charge-station.vue
  8. 15 8
      src/components/shadow-card/shadow-card.vue
  9. 5 2
      src/components/style-bottom-view/style-bottom-view.vue
  10. 38 0
      src/components/style-checkbox/style-checkbox.vue
  11. 69 0
      src/components/style-dialog/style-dialog.vue
  12. 53 0
      src/components/style-result/style-result.vue
  13. 5 41
      src/custom-tab-bar/index.js
  14. 0 17
      src/custom-tab-bar/index.wxml
  15. 2 2
      src/custom-tab-bar/index.wxss
  16. 834 0
      src/pages-charge/appointment/appointment.vue
  17. 248 0
      src/pages-charge/camera/camera.vue
  18. 22 3
      src/pages-charge/codeing/codeing.vue
  19. 175 0
      src/pages-charge/discount/discount.vue
  20. 38 20
      src/pages-charge/machines/charge-machine/charge-machine.vue
  21. 154 184
      src/pages-charge/machines/machines.vue
  22. 180 0
      src/pages-charge/machines/price-desc/price-desc.vue
  23. 39 9
      src/pages-charge/order/order.vue
  24. 30 17
      src/pages-charge/ordering/ordering.vue
  25. 235 30
      src/pages-charge/orders/orders.vue
  26. BIN
      src/pages-charge/static/camera-back.png
  27. BIN
      src/pages-charge/static/charge-status-icon.png
  28. BIN
      src/pages-charge/static/discount-bg.png
  29. BIN
      src/pages-charge/static/discount-coupon.png
  30. BIN
      src/pages-charge/static/icon-alarm.png
  31. BIN
      src/pages-charge/static/scan-code-album.png
  32. BIN
      src/pages-charge/static/scan-code-input.png
  33. BIN
      src/pages-charge/static/scan-code-light.png
  34. 70 0
      src/pages-common/activity/activity.vue
  35. 148 0
      src/pages-common/contact/contact.vue
  36. 177 0
      src/pages-common/invoice/invoice.vue
  37. 43 30
      src/pages-user/profile/profile.vue
  38. BIN
      src/pages-user/static/icon-submit-success.png
  39. BIN
      src/pages-user/static/wallet-banner.png
  40. 0 142
      src/pages-user/wallet-detail/wallet-detail.vue
  41. 194 0
      src/pages-user/wallet-recharge/wallet-recharge.vue
  42. 180 0
      src/pages-user/wallet-refund/wallet-refund.vue
  43. 217 161
      src/pages-user/wallet/wallet.vue
  44. 68 11
      src/pages.json
  45. 16 1
      src/pages/index/index.vue
  46. 1 0
      src/pages/list/list.vue
  47. 477 224
      src/pages/map/map.vue
  48. 57 122
      src/pages/user/user.vue
  49. BIN
      src/static/images/contact-servicer.png
  50. BIN
      src/static/images/custom-tab-bar/2-2.png
  51. BIN
      src/static/images/custom-tab-bar/2-3.png
  52. BIN
      src/static/images/custom-tab-bar/2-4.png
  53. BIN
      src/static/images/custom-tab-bar/bg.png
  54. BIN
      src/static/images/map-bg.jpg
  55. BIN
      src/static/images/map-charging.png
  56. BIN
      src/static/images/map-filter.png
  57. BIN
      src/static/images/map-input.png
  58. BIN
      src/static/images/map-location.png
  59. BIN
      src/static/images/map-logo.png
  60. BIN
      src/static/images/map-re-location.png
  61. BIN
      src/static/images/map-search.png
  62. BIN
      src/static/images/user/1.png
  63. BIN
      src/static/images/user/2.png
  64. BIN
      src/static/images/user/3.png
  65. BIN
      src/static/images/user/4.png
  66. BIN
      src/static/images/user/6.png
  67. 4 0
      src/styles/flex.scss
  68. 11 0
      src/styles/layout.scss
  69. 1 1
      src/utils/code.ts
  70. 3 1
      src/utils/constant.ts
  71. 9 9
      src/utils/date.ts
  72. 17 0
      src/utils/device.ts
  73. 3 5
      src/utils/infinite-scroll.ts
  74. 43 0
      src/utils/navigate.ts
  75. 63 39
      src/utils/uploader.ts

+ 1 - 1
README.md

@@ -1 +1 @@
-## 
+## Q

+ 1 - 0
package.json

@@ -20,6 +20,7 @@
     "dev:quickapp-webview": "uni -p quickapp-webview",
     "dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
     "dev:quickapp-webview-union": "uni -p quickapp-webview-union",
+    "build": "uni build -p mp-weixin",
     "build:app": "uni build -p app",
     "build:app-android": "uni build -p app-android",
     "build:app-ios": "uni build -p app-ios",

+ 86 - 1
src/App.vue

@@ -8,6 +8,11 @@ export default <any>{
     normalCode: "",
   },
   onLaunch() {
+    uni.getSystemInfo({
+      success: (res) => {
+        this.globalData.device = res;
+      },
+    });
     return fetchToken().then((cache) => {
       if (cache) {
         this.globalData.token = cache;
@@ -30,10 +35,13 @@ view {
   box-sizing: border-box;
   --color-primary: #347dff;
   --color-warning: #ff9900;
-  --color-gray: #666666;
   --color-sec: #f7f7f7;
 }
 
+.box-content {
+  box-sizing: content-box;
+}
+
 image {
   width: 20rpx;
   height: 20rpx;
@@ -51,6 +59,18 @@ image {
   color: #000;
 }
 
+.color-000-5 {
+  color: rgba(0, 0, 0, 0.5);
+}
+
+.color-000-6 {
+  color: rgba(0, 0, 0, 0.6);
+}
+
+.color-000-08 {
+  color: rgba(0, 0, 0, 0.08);
+}
+
 .color-333 {
   color: #333;
 }
@@ -71,7 +91,72 @@ image {
   color: var(--color-warning);
 }
 
+.bg-fff {
+  background-color: #fff;
+}
+
+.bg-666 {
+  background-color: #666;
+}
+
+.bg-000-08 {
+  background-color: rgba(0, 0, 0, 0.08);
+}
+
+.bg-primary {
+  background-color: var(--color-primary);
+}
+
+.full-percent {
+  height: 100%;
+  width: 100%;
+}
+
+.absolute-top-left {
+  position: absolute;
+  left: 0px;
+  top: 0px;
+}
+
+.absolute-full {
+  position: absolute;
+  left: 0px;
+  top: 0px;
+  width: 100%;
+  height: 100%;
+}
+
+.absolute-center {
+  position: absolute;
+  left: 50%;
+  top: 50%;
+  transform: translate(-50%, -50%);
+}
+
 .transition {
   transition: all 0.3s;
 }
+
+.hover-scale {
+  transition: all 0.3s;
+  transform: scale(0.98);
+}
+
+.animation-loading {
+  animation: animation-turn 2s linear infinite;
+}
+
+@keyframes animation-turn {
+  0% {
+    transform: rotate(0deg);
+  }
+
+  50% {
+    transform: rotate(180deg);
+  }
+
+  100% {
+    transform: rotate(360deg);
+  }
+}
 </style>

+ 204 - 59
src/api/charge.ts

@@ -1,29 +1,51 @@
 import Http from "../utils/http";
-import { host } from "../utils/constant";
+import { host, isDebug } from "../utils/constant";
 const cHttp = new Http(host);
 
-export function startCharge(sn: string) {
+export function startCharge(sn: string, query?: string) {
   return cHttp.get<{
     ConnectorID: string;
     FailReason: number;
     StartChargeSeq: string;
     StartChargeSeqStat: number;
     SuccStat: number;
-  }>(`/charge/startCharge/${sn}`, {
+  }>(`/charge/startCharge/${sn}?${query}`, {
     statusCodeHandle: false,
   });
 }
 
+export async function cancelAppointmentCharge() {
+  return cHttp.get("/charge/cancelBooking");
+}
+
+export async function startAppointmentCharge(connectorId: string) {
+  return cHttp.get(`/charge/immediatelyCharge/${connectorId}`);
+}
+
+export async function searchQRCode(imgUrl: string) {
+  return cHttp.get(`/charge/qrCode?imgUrl=${imgUrl}`);
+}
+
+export async function changeAppointmentTime(
+  startChargeSeq: string,
+  startTime: string
+) {
+  return cHttp.get(
+    `/charge/modifyBookingTime?startChargeSeq=${startChargeSeq}&startTime=${startTime}`
+  );
+}
+
 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 minPriceTime = "00:00";
     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) => {
+    if (res && res.policyInfos && res.policyInfos.length) {
+      res.policyInfos.forEach((item: any, index: number) => {
         const hour = item.startTime.substring(0, 2);
         const min = item.startTime.substring(3, 5);
         if (index === 0) {
@@ -32,32 +54,39 @@ export function fetchStationPriceDesc(ConnectorID: string, StationID?: string) {
         let tempPrice = Number(
           Number(item.elecPrice + item.servicePrice).toFixed(2)
         );
+        item.totalPrice = tempPrice;
         if (tempPrice > maxPrice) {
           maxPrice = tempPrice;
         }
-        if (tempPrice < minPrice) {
-          minPrice = tempPrice;
-        }
-        if (index >= res.policyInfoss.length - 1) {
+        if (index >= res.policyInfos.length - 1) {
           // 最后一个
           item.startTimeFormat = `${hour}:${min}~24:00`;
           if (Number(hour) <= nowHour) {
             currentPrice = tempPrice;
             currentTime = `${hour}:${min}~24:00`;
           }
+          if (tempPrice < minPrice) {
+            minPrice = tempPrice;
+            minPriceTime = `${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);
+          const nhour = res.policyInfos[index + 1].startTime.substring(0, 2);
+          const nmin = res.policyInfos[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}`;
           }
+          if (tempPrice < minPrice) {
+            minPrice = tempPrice;
+            minPriceTime = `${hour}:${min}~${nhour}:${nmin}`;
+          }
         }
       });
     }
     res.maxPrice = maxPrice;
     res.minPrice = minPrice;
+    res.minPriceTime = minPriceTime;
     res.currentPrice = currentPrice;
     res.currentTime = currentTime;
     if (StationID) {
@@ -73,10 +102,56 @@ export function cancelCharge(sn: string) {
   });
 }
 
-export function fetchChargeStatus() {
-  return cHttp.get("/charge/chargeStatus", {
-    statusCodeHandle: false,
-  });
+export function fetchChargeStatus(
+  checkAppointment?: boolean,
+  checkCharge?: boolean
+) {
+  return cHttp
+    .get("/charge/chargeStatus", {
+      statusCodeHandle: false,
+    })
+    .then((res) => {
+      if (res) {
+        res.isAppointment = [0].includes(res.chargeStatus);
+        res.isStarted = [1, 2, 3].includes(res.chargeStatus);
+      }
+      // 充电状态:0:预约中 1:启动中 2:充电中 3:停止中 4:已结束 5:未知
+      if (checkAppointment && res && res.isAppointment) {
+        uni.hideLoading();
+        uni.showModal({
+          title: "温馨提示",
+          content: "当前已有预约中的订单,请取消再扫码充电",
+          showCancel: false,
+          confirmText: "查看预约",
+          confirmColor: "#347DFF",
+          success(modal) {
+            if (modal.confirm) {
+              uni.redirectTo({
+                url: `/pages-charge/appointment/appointment?sn=${res.connectorId}`,
+              });
+            }
+          },
+        });
+      }
+      if (checkCharge && res && res.isStarted) {
+        uni.hideLoading();
+        uni.showModal({
+          title: "温馨提示",
+          content: "当前已有进行中的订单,请结束订单再扫码充电",
+          showCancel: false,
+          confirmText: "查看详情",
+          confirmColor: "#347DFF",
+          success(modal) {
+            if (modal.confirm) {
+              uni.redirectTo({
+                url: `/pages-charge/ordering/ordering?sn=${res.connectorId}&start=1`,
+              });
+            }
+          },
+        });
+      }
+      return res;
+    });
 }
 
 export function fetchStations(
@@ -94,12 +169,40 @@ export function fetchStations(
   return fetchAllStations()
     .then((res) => {
       // console.log(latitude, longitude);
-      let list = JSON.parse(JSON.stringify(res));
-      const data: any[] = [];
+      // let list = JSON.parse(JSON.stringify(res));
+      const data: any[] = JSON.parse(JSON.stringify(res)); // []
       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 && !isDebug) {
+      //     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 (latitude && longitude) {
-        list.forEach((item: any) => {
+        data.forEach((item: any) => {
           item.stationLatDistance = _getDistance(
             latitude,
             longitude,
@@ -107,22 +210,10 @@ export function fetchStations(
             item.location.stationLng
           );
         });
-        list.sort((item1: any, item2: any) => {
+        data.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(
@@ -136,33 +227,34 @@ export function fetchStations(
       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;
-        });
-      }
+      // 暂时废弃状态筛选
+      // 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;
     });
 }
@@ -190,11 +282,62 @@ export function fetchStationByIds(ids: number[]) {
   });
 }
 
+export function fetchStationByConnectorIdOrShortId(id: string) {
+  let equipment = -1;
+  return fetchAllStations()
+    .then((res) => {
+      let station: any = undefined;
+      res.forEach((item) => {
+        if (!station && item.equipmentInfos && item.equipmentInfos) {
+          item.equipmentInfos.forEach(
+            (equipmentInfo: any, equipmentIndex: number) => {
+              if (id.length <= 16) {
+                // 此处传入的是 shortId
+                if (equipmentInfo.shortId === id) {
+                  station = item;
+                  equipment = equipmentIndex;
+                }
+              } else {
+                if (
+                  equipmentInfo.connectorInfos &&
+                  equipmentInfo.connectorInfos
+                ) {
+                  // connectorId
+                  equipmentInfo.connectorInfos.forEach((connectorInfo: any) => {
+                    if (connectorInfo.connectorId === id) {
+                      station = item;
+                      equipment = equipmentIndex;
+                    }
+                  });
+                }
+              }
+            }
+          );
+        }
+      });
+      if (station) {
+        return _fetchStationStatus([station]);
+      } else {
+        return Promise.reject({});
+      }
+    })
+    .then((res) => {
+      if (res && res.length) {
+        return {
+          station: res[0],
+          equipment: res[0].equipmentInfos[equipment],
+        };
+      } else {
+        return Promise.reject({});
+      }
+    });
+}
+
 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)
+      (item) => reg.test(item.stationName) || reg.test(item.address)
     );
     return _fetchStationStatus(list);
   });
@@ -315,6 +458,8 @@ function _fetchStationStatus(list: any[]) {
                   (costatus: any) => costatus.connectorId === coInfo.connectorId
                 );
               if (connectorStatusInfo) {
+                // debug
+                // connectorStatusInfo.status = 255
                 coInfo.connectorStatusInfo = connectorStatusInfo;
               }
             }

+ 33 - 5
src/api/index.ts

@@ -1,8 +1,36 @@
-import Http from '../utils/http'
-import { host } from '../utils/constant'
+import Http from "../utils/http";
+import { host } from "../utils/constant";
 
-const indexHttp = new Http(host)
+const indexHttp = new Http(host);
 
 export function fetchContact() {
-  return indexHttp.get('/common/contact')
-}
+  return indexHttp.get("/common/contact");
+}
+
+export function fetchCommonQuestions() {
+  return indexHttp.get("/qa");
+}
+
+export function applyInvoice(startChargeSeqs: string[]) {
+  return indexHttp.post("/invoice/applyInvoice", {
+    data: {
+      startChargeSeqs,
+    },
+  });
+}
+
+export function fetchHomeBanner() {
+  return indexHttp.get("/banner");
+}
+
+export function fetchInvoiceList() {
+  return indexHttp.get("/invoice/list?status=1");
+}
+
+export function cancelApplyInvoice() {
+  return indexHttp.get("/invoice/cancelApplyInvoice");
+}
+
+export function fetchInvoicePDF(invoiceId: string) {
+  return indexHttp.get(`/invoice/downloadInvoice/${invoiceId}`);
+}

+ 41 - 3
src/api/user.ts

@@ -1,10 +1,20 @@
 import Http from "../utils/http";
-import { host } from "../utils/constant";
+import { host, isDebug } from "../utils/constant";
 
 const userHttp = new Http(host);
 
 export function fetchProfile() {
   return userHttp.get("/user/me").then((res) => {
+    res.balance = (Number(res.balance) / 100).toFixed(2);
+    res.discountAmount = (Number(res.discountAmount) / 100).toFixed(2);
+    res.refundableAmount = (Number(res.refundableAmount) / 100).toFixed(2);
+    // if (isDebug) {
+    //   res.userRechargeRightsList = [
+    //     {
+    //       discount: 90,
+    //     },
+    //   ];
+    // }
     getApp<any>().globalData.user = res;
     return res;
   });
@@ -65,6 +75,9 @@ export function updateProfile(data: any) {
 declare const wx: any;
 
 export function insertMoney(amount: number) {
+  if (isDebug) {
+    return Promise.resolve();
+  }
   return userHttp
     .post("/payment/wxPay", {
       data: {
@@ -100,10 +113,35 @@ 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 fetchWallet(
+  type: number,
+  page: number,
+  pageSize: number,
+  query?: string
+) {
+  return userHttp.get(
+    `/account/walletDetail?page=${page}&page_size=${pageSize}&type=${type}${
+      query ? "&" + query : ""
+    }`
+  );
 }
 
 export function fetchOrder(orderid: string) {
   return userHttp.get(`/charge/orderDetail/${orderid}`);
 }
+
+export function fetchOrders(page: number, pageSize: number, query?: string) {
+  return userHttp.get(
+    `/charge/listUserChargeOrders?pageNum=${page}&pageSize=${pageSize}${
+      query ? "&" + query : ""
+    }`
+  );
+}
+
+export function applyRefund(reason: string) {
+  return userHttp.post(`/payment/wxApplyRefund`, {
+    data: {
+      reason,
+    },
+  });
+}

+ 28 - 9
src/components/charge-station/charge-station.vue

@@ -5,11 +5,13 @@
       border: border ? '1rpx solid rgba(0, 0, 0, 0.1)' : 'none',
       'border-radius': fromMap ? '20rpx 20rpx 0 0' : '20rpx',
     }"
-    @click="detail"
+    @touchstart="touchstart"
+    @touchend="touchend"
+    data-method="detail"
   >
     <view class="flex-between">
       <view>
-        <view class="fs-32 fw-blod">{{ title }}</view>
+        <view class="fs-32 fw-bold">{{ title }}</view>
         <view v-if="tag">
           <view class="flex-center tag">{{ tagMap[tag] }}</view>
         </view>
@@ -17,7 +19,12 @@
           >{{ address }} | {{ distance }}km</view
         >
       </view>
-      <view class="nav flex-center flex-shrink" @click.stop="nav">
+      <view
+        class="nav flex-center flex-shrink"
+        @touchstart="touchstart"
+        @touchend.stop="touchend"
+        data-method="nav"
+      >
         <image src="/static/images/icon-nav.png" mode="widthFix" />
         <text>导航</text>
       </view>
@@ -28,17 +35,17 @@
           <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>
+          <text class="fs-22 ml-6 color-666">元/度</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"
+          <view class="fs-22 ml-8 color-666" 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"
+          <view class="fs-22 ml-8 color-666" v-if="slow"
             >空闲:{{ freeLength(slow) }}|共:{{ slow.length }}</view
           >
         </view>
@@ -51,7 +58,7 @@
             v-if="slow && slow.length"
           >
             <charge-icon type="slow"></charge-icon>
-            <view class="ml-6 fs-22" style="color: var(--color-gray)"
+            <view class="ml-6 fs-22 color-666"
               >{{ freeLength(slow) }} / {{ slow.length }}</view
             >
           </view>
@@ -60,7 +67,7 @@
             v-if="fast && fast.length"
           >
             <charge-icon type="fast"></charge-icon>
-            <view class="ml-6 fs-22" style="color: var(--color-gray)"
+            <view class="ml-6 fs-22 color-666"
               >{{ freeLength(fast) }} / {{ fast.length }}</view
             >
           </view>
@@ -68,7 +75,9 @@
       </view>
       <view
         class="btn flex-shrink flex-column flex-center"
-        @click.stop="collect"
+        @touchstart="touchstart"
+        @touchend.stop="touchend"
+        data-method="collect"
       >
         <uni-icons
           :type="collected ? 'star-filled' : 'star'"
@@ -120,6 +129,7 @@ export default {
   },
   data() {
     return {
+      touchtime: 0,
       collected: false,
       tagMap: [
         "",
@@ -183,6 +193,15 @@ export default {
       });
       return length;
     },
+    touchstart() {
+      this.touchtime = new Date().getTime();
+    },
+    touchend(e: any) {
+      const current = new Date().getTime();
+      if (current - this.touchtime < 100) {
+        this[e.currentTarget.dataset.method]();
+      }
+    },
   },
 };
 </script>

+ 15 - 8
src/components/shadow-card/shadow-card.vue

@@ -1,8 +1,21 @@
 <template>
   <view class="shadow-card">
-    <view v-for="(item, index) in list" :key="index">
+    <slot></slot>
+    <view
+      v-for="(item, index) in list"
+      :key="index"
+      :style="{
+        marginTop: index === 0 ? '0rpx' : '40rpx',
+      }"
+    >
       <view class="label">{{ item.label }}</view>
-      <view class="value">{{ item.value }}</view>
+      <view
+        class="value"
+        :style="{
+          color: item.color ? item.color : '#000',
+        }"
+        >{{ item.value }}</view
+      >
     </view>
   </view>
 </template>
@@ -27,13 +40,8 @@ export default {
   padding: 40rpx;
 
   & > view {
-    margin-top: 40rpx;
     display: flex;
 
-    &:first-child {
-      margin-top: 0rpx;
-    }
-
     .label {
       width: 156rpx;
       flex-shrink: 0;
@@ -43,7 +51,6 @@ export default {
 
     .value {
       flex-grow: 1;
-      color: #000;
       font-size: 26rpx;
     }
   }

+ 5 - 2
src/components/style-bottom-view/style-bottom-view.vue

@@ -6,7 +6,10 @@
       'fixed-bottom-view-shadow ': shadow,
       'fixed-bottom-view-hidden': hidden,
     }"
-    style="z-index: {{zIndex}};background:{{background}};"
+    :style="{
+      zIndex,
+      background,
+    }"
   >
     <slot></slot>
   </view>
@@ -23,7 +26,7 @@ export default {
     },
     zIndex: {
       type: Number,
-      default: 999999,
+      default: 999,
     },
   },
 };

+ 38 - 0
src/components/style-checkbox/style-checkbox.vue

@@ -0,0 +1,38 @@
+<template>
+  <view
+    class="lh-24 br-round flex-center"
+    :style="{
+      width: `${size}rpx`,
+      height: `${size}rpx`,
+      border: checked
+        ? '1rpx solid var(--color-primary)'
+        : '1rpx solid rgba(0,0,0,0.15)',
+      backgroundColor: checked ? 'var(--color-primary)' : 'rgba(0,0,0,0.1)',
+    }"
+  >
+    <uni-icons
+      v-if="checked"
+      type="checkmarkempty"
+      :size="iconsize"
+      color="#ffffff"
+    ></uni-icons>
+  </view>
+</template>
+
+<script lang="ts">
+export default {
+  props: {
+    checked: Boolean,
+    size: {
+      type: Number,
+      default: 32,
+    },
+    iconsize: {
+      type: Number,
+      default: 10,
+    },
+  },
+};
+</script>
+
+<style lang="scss"></style>

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

@@ -0,0 +1,69 @@
+<template>
+  <view class="style-dialog">
+    <view :style="{ background }">
+      <view class="style-dialog_head flex-center">
+        <view class="fw-500 color-000 fs-32">{{ title }}</view>
+        <view class="close" @click="close">
+          <uni-icons type="closeempty" size="24" color="#2D284B"></uni-icons>
+        </view>
+      </view>
+      <view class="style-dialog_body">
+        <slot></slot>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script lang="ts">
+export default {
+  props: {
+    title: String,
+    background: {
+      type: String,
+      default: "#fff",
+    },
+  },
+  methods: {
+    close() {
+      this.$emit("close");
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+.style-dialog {
+  position: fixed;
+  width: 100%;
+  height: 100%;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  top: 0;
+  z-index: 999;
+  background-color: rgba(0, 0, 0, 0.6);
+  display: flex;
+  align-items: flex-end;
+  & > view {
+    width: 100%;
+    overflow: hidden;
+    border-radius: 20rpx 20rpx 0px 0px;
+    box-sizing: content-box;
+  }
+  &_head {
+    height: 92rpx;
+    position: relative;
+    background-color: #fff;
+    .close {
+      position: absolute;
+      right: 30rpx;
+      top: 50%;
+      transform: translateY(-50%);
+    }
+  }
+  &_body {
+    max-height: 1000rpx;
+    overflow-y: auto;
+  }
+}
+</style>

+ 53 - 0
src/components/style-result/style-result.vue

@@ -0,0 +1,53 @@
+<template>
+  <view class="style-result">
+    <image class="style-result_icon" :src="icon" v-if="icon" mode="widthFix"></image>
+    <view class="style-result_title">{{ title }}</view>
+    <view class="style-result_tip" v-if="tip">{{ tip }}</view>
+    <slot></slot>
+  </view>
+</template>
+
+<script lang="ts">
+export default {
+  props: {
+    icon: String,
+    title: String,
+    tip: String,
+    btn: String,
+  },
+};
+</script>
+
+<style lang="scss">
+.style-result {
+  position: fixed;
+  width: 100vw;
+  height: 100vh;
+  left: 0;
+  top: 0;
+  z-index: 99999;
+  background-color: #fff;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+  &_icon {
+    width: 324rpx;
+  }
+  &_title {
+    color: #000;
+    text-align: center;
+    font-size: 30rpx;
+    font-weight: 500;
+    line-height: normal;
+  }
+  &_tip {
+    width: 370rpx;
+    margin-top: 16rpx;
+    color: rgba(0, 0, 0, 0.6);
+    text-align: center;
+    font-size: 26rpx;
+    font-weight: 400;
+  }
+}
+</style>

+ 5 - 41
src/custom-tab-bar/index.js

@@ -1,11 +1,8 @@
 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: [
       {
@@ -13,19 +10,14 @@ Component({
         text: "充电地图",
       },
       {
-        path: "",
+        path: "/pages-charge/camera/camera",
         text: "扫码充电",
       },
-      {
-        path: "/pages/list/list",
-        text: "电站列表",
-      },
       {
         path: "/pages/user/user",
         text: "个人中心",
       },
     ],
-    charging: "",
   },
   lifetimes: {
     ready() {
@@ -51,24 +43,14 @@ Component({
   },
   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();
+      if (/camera/.test(url)) {
+        wx.navigateTo({
+          url,
+        });
         return;
       }
-      this.setData({
-        dialog: false,
-      });
       wx.vibrateShort &&
         wx.vibrateShort({
           type: "light",
@@ -77,11 +59,6 @@ Component({
         url,
       });
     },
-    switchDialog() {
-      this.setData({
-        dialog: !this.data.dialog,
-      });
-    },
     hiddenTab() {
       this.setData({
         hidden: true,
@@ -95,18 +72,5 @@ Component({
     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`,
-      });
-    },
   },
 });

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

@@ -16,20 +16,3 @@
   </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>

+ 2 - 2
src/custom-tab-bar/index.wxss

@@ -115,8 +115,8 @@ button::after {
 }
 
 .tab-bar-charge_menu--icon {
-  width: 104rpx;
-  height: 104rpx;
+  width: 128rpx;
+  height: 128rpx;
 }
 
 .tab-bar-charge_menu--text {

+ 834 - 0
src/pages-charge/appointment/appointment.vue

@@ -0,0 +1,834 @@
+<template>
+  <!--  #ifdef MP-WEIXIN -->
+  <page-meta page-style="overflow: {{X || R ? 'hidden' : 'visible'}}" />
+  <!--  #endif -->
+  <view :class="['page']">
+    <view v-if="data && priceInfo">
+      <view class="block">
+        <view class="station">
+          <ChargeMachine
+            :title="'NO.' + data.equipment.shortId"
+            :list="data.equipment.connectorInfos"
+            :time="priceInfo.useTime"
+            :parkingNo="data.equipment.parkingNo"
+          ></ChargeMachine>
+        </view>
+        <view class="pt-20 pb-20 pl-30 pr-30 flex-align-center">
+          <image
+            src="/pages-charge/static/machines-banner-address.png"
+            mode="widthFix"
+            class="flex-shrink mr-12 width-40"
+          />
+          <view class="fs-26 color-666">{{ data.station.address }}</view>
+        </view>
+      </view>
+      <view class="mt-40 color-999 fs-32 fw-500">费用说明</view>
+      <view
+        class="mt-20 block height-96 flex-align-center pl-30 pr-30"
+        @click="openPriceDesc"
+      >
+        <view class="fs-26 color-666">充电费用</view>
+        <view class="ml-64 fs-26 color-333"
+          >{{ priceInfo.minPrice }}~{{ priceInfo.maxPrice }}元/度</view
+        >
+        <view class="ml-auto">
+          <uni-icons type="right" size="16" color="rgba(0,0,0,0.4)"></uni-icons>
+        </view>
+      </view>
+      <template v-if="appointmentData">
+        <view class="mt-40 color-999 fs-32 fw-500">已预约充电</view>
+        <view class="mt-20 block flex-align-center" style="height: 180rpx">
+          <image
+            class="width-56 ml-30"
+            src="/pages-charge/static/icon-alarm.png"
+            mode="widthFix"
+          ></image>
+          <view class="ml-24">
+            <view class="flex" v-if="appointmentCountDown">
+              <view class="fs-28 color-000">将在</view>
+              <view class="fs-44 lh-36 color-primary ml-8 mr-8">{{
+                appointmentCountDown
+              }}</view>
+              <view class="fs-28 color-000">后开始充电</view>
+            </view>
+            <view class="fs-24 color-999 mt-8"
+              >请在{{ appointmentData.startTime }}前到达充电桩并开始充电</view
+            >
+          </view>
+        </view>
+        <view class="mt-40 flex-center">
+          <view
+            class="flex-center height-68 br-68 fs-26 color-666"
+            style="width: 184rpx; border: 1px solid rgba(0, 0, 0, 0.3)"
+            @click="cancelAppointment"
+            >取消预约</view
+          >
+        </view>
+      </template>
+      <template v-else>
+        <view class="mt-40 color-999 fs-32 fw-500">选择充电方式</view>
+        <view class="mt-20 block pl-30 pr-30">
+          <view
+            v-for="(type, index) in chargeTypes"
+            :key="index"
+            :class="['pt-40', 'pb-40', 'flex-align-center']"
+            :style="{
+              borderTop: index === 0 ? '' : '1rpx solid rgba(0, 0, 0, 0.10)',
+            }"
+            @click="changeType(index)"
+          >
+            <view>
+              <view class="fs-28 lh-28 color-000 fw-500">{{ type.title }}</view>
+              <view
+                class="fs-24 lh-24 mt-16"
+                style="color: #f8386a"
+                v-if="chargeType === index"
+                >{{ type.checkTip }}</view
+              >
+              <view class="fs-24 color-999 lh-24 mt-16" v-else>{{
+                type.tip
+              }}</view>
+            </view>
+            <view class="ml-auto">
+              <style-checkbox :checked="chargeType === index"></style-checkbox>
+            </view>
+          </view>
+        </view>
+        <view class="mt-20 block pl-30 pr-30" v-if="chargeType === 0">
+          <view
+            class="pt-40 pb-40 flex-between"
+            style="border-bottom: 1rpx solid rgba(0, 0, 0, 0.1)"
+            @click="selectTime"
+          >
+            <view class="fs-28 color-000 fw-500">充电时间</view>
+            <view class="flex-align-center lh-28">
+              <block v-if="chargeTime.isPowerSaving">
+                <view class="fs-28 color-333 fw-500 mr-16">00:00 - 08:00</view>
+              </block>
+              <block v-else>
+                <view
+                  v-if="chargeTime.format"
+                  class="fs-28 color-333 fw-500 mr-16"
+                  >{{ chargeTime.format }}</view
+                >
+                <view v-else class="fs-26 mr-16" style="color: #cacaca"
+                  >请选择</view
+                >
+                <uni-icons type="right" size="16" color="#CACACA"></uni-icons>
+              </block>
+            </view>
+          </view>
+          <view class="pt-40 pb-40 flex-between">
+            <view class="fs-28 color-000 fw-500">预计费用</view>
+            <view class="flex-align-center lh-28">
+              <block v-if="chargeTime.isPowerSaving">
+                <view class="fs-28 color-333 fw-500"
+                  >低至{{ priceInfo.minPrice }}元/kwh</view
+                >
+              </block>
+              <block v-else>
+                <view
+                  v-if="chargeTime.formatPrice"
+                  class="fs-28 color-333 fw-500"
+                  >{{ chargeTime.formatPrice }}</view
+                >
+                <view v-else class="fs-26 mr-16" style="color: #cacaca"
+                  >选择时间后显示</view
+                >
+              </block>
+            </view>
+          </view>
+        </view>
+        <view
+          class="mt-20 block flex-align-center pl-30 pr-30 pt-30 pb-30"
+          @click="
+            to(
+              `/pages-charge/discount/discount?index=${discountIndex}${
+                activity && activity.id ? `&id=${activity.id}` : ''
+              }`
+            )
+          "
+        >
+          <view class="fs-28 lh-28 color-000 fw-500">优惠</view>
+          <block v-if="discountList.length > 0">
+            <view class="fs-28 lh-28 ml-auto" style="color: #f43636"
+              >{{ discountList[discountIndex].discountFormat }}折</view
+            >
+            <uni-icons type="right" size="16" color="#CACACA"></uni-icons>
+          </block>
+          <block v-else-if="activity">
+            <view class="fs-28 lh-28 ml-auto" style="color: #f43636"
+              >{{ activity.minDiscount }}折</view
+            >
+            <view class="fs-28 lh-28 color-666 ml-8">权益待领取</view>
+            <uni-icons type="right" size="16" color="#CACACA"></uni-icons>
+          </block>
+          <view v-else class="fs-28 lh-28 color-666 ml-auto">无</view>
+        </view>
+        <view
+          class="mt-20 block flex-align-center pl-30 pr-30 pt-30 pb-30"
+          v-if="chargeType === 0"
+        >
+          <view class="flex-column">
+            <view class="fs-28 lh-28 color-000 fw-500">省钱模式</view>
+            <view class="fs-24 lh-30 color-999 mt-8"
+              >处于00:00-08:00时,电费低至{{ priceInfo.minPrice }}元/kwh</view
+            >
+          </view>
+          <view class="ml-auto">
+            <switch
+              :checked="chargeTime.isPowerSaving"
+              color="#347DFF"
+              @change="changePowerSaving"
+            />
+          </view>
+        </view>
+      </template>
+      <view class="pt-60 pb-80"></view>
+      <style-bottom-view>
+        <view class="pt-20 pl-40 pr-40 pb-20 bg-fff flex">
+          <template v-if="appointmentData">
+            <view class="mr-10" style="width: 50%">
+              <style-button size="small" @click="selectTime"
+                >修改时间</style-button
+              ></view
+            >
+            <view class="ml-10" style="width: 50%">
+              <style-button type="primary" size="small" @click="submitNow"
+                >立即开始</style-button
+              >
+            </view>
+          </template>
+          <style-button v-else type="primary" size="small" @click="submit">{{
+            chargeType === 0 ? "提交预约" : "马上充电"
+          }}</style-button>
+        </view>
+      </style-bottom-view>
+    </view>
+    <style-dialog
+      v-if="chargeTimeDialog"
+      @close="closeTime"
+      :title="
+        '请选择' + (chargeTime.isPowerSaving ? '省电模式' : '') + '充电时间'
+      "
+    >
+      <view class="flex" style="border-top: 1rpx solid rgba(0, 0, 0, 0.1)">
+        <view
+          class="flex-shrink"
+          style="height: 840rpx; width: 256rpx; background-color: #f0f0f0"
+        >
+          <view
+            class="flex-align-center pl-32 height-90"
+            v-for="(day, index) in timesInfo.day"
+            :key="index"
+            :style="{
+              backgroundColor:
+                day.format === chargeTime.day ? '#fff' : 'transparent',
+            }"
+            @click="changeDay(index)"
+          >
+            <view
+              :class="[
+                'fs-32',
+                'fw-500',
+                day.format === chargeTime.day ? 'color-primary' : 'color-000',
+              ]"
+              >{{ day.format }}</view
+            >
+            <view
+              class="fs-24 color-666 ml-12 br-8 width-60 height-40 flex-center"
+              style="background-color: rgba(0, 0, 0, 0.07)"
+              v-if="day.year && day.format !== chargeTime.day"
+              >{{ day.year }}年</view
+            >
+          </view>
+        </view>
+        <scroll-view
+          scroll-y
+          class="flex-grow"
+          style="height: 840rpx"
+          :scroll-into-view="chargeTime.scrollId"
+        >
+          <view
+            :id="`hour-${hour.format}`"
+            class="flex-align-center pl-40 height-90 fs-32"
+            v-for="(hour, index) in timesInfo.hour"
+            :key="index"
+            :style="{
+              color: hour.disabled
+                ? '#999'
+                : hour.format === chargeTime.hour
+                ? 'var(--color-primary)'
+                : '#000',
+            }"
+            @click="changeHour(index)"
+          >
+            {{ hour.format }}
+          </view>
+          <view class="hour-placeholder"></view>
+        </scroll-view>
+      </view>
+    </style-dialog>
+    <PriceDesc
+      v-if="priceDescVisible"
+      :desc="priceInfo.policyInfos || []"
+      @close="closePriceDesc"
+    ></PriceDesc>
+  </view>
+</template>
+
+<script setup lang="ts">
+import {
+  fetchChargeStatus,
+  fetchStationByConnectorIdOrShortId,
+  fetchStationPriceDesc,
+  startCharge,
+  startAppointmentCharge,
+  cancelAppointmentCharge,
+  changeAppointmentTime,
+} from "@/api/charge";
+import { fetchProfile } from "@/api/user";
+import { onLoad, onShow } from "@dcloudio/uni-app";
+import { ref } from "vue";
+import ChargeMachine from "../machines/charge-machine/charge-machine.vue";
+import PriceDesc from "../machines/price-desc/price-desc.vue";
+import { format } from "@/utils/date";
+import { to,redirect } from "../../utils/navigate";
+
+const DAY = 24 * 60 * 60 * 1000;
+const options = ref<any>();
+const data = ref<any>();
+const priceInfo = ref();
+const priceDescVisible = ref(false);
+const timesInfo = ref<any>({
+  day: [],
+  time: [],
+});
+
+const chargeType = ref(1);
+const chargeTypes = ref([
+  {
+    title: "预约充电",
+    tip: "预约特定时间点开始充电",
+    checkTip: "部分车辆可能进入休眠状态无法唤醒,将导致充电失败",
+  },
+  {
+    title: "立即充电",
+  },
+]);
+const chargeTimeDialog = ref(false);
+const chargeTime = ref({
+  time: 0, // 时间戳
+  day: "",
+  hour: "",
+  format: "",
+  formatPrice: "",
+  scrollId: "",
+  isPowerSaving: false, // 省电模式
+});
+const appointmentData = ref();
+const appointmentCountDown = ref();
+
+const discountIndex = ref(-1);
+const discountList = ref<any[]>([]);
+const activity = ref();
+
+const changeType = (index: number) => {
+  chargeType.value = index;
+};
+
+const isPassTime = (hour: string, day: number) => {
+  const now = new Date();
+  const time = new Date(format("y/M/d", day) + " " + hour + ":00");
+  return now > time;
+};
+
+const getHourPrice = (hour: string) => {
+  let price = "";
+  priceInfo.value.policyInfos.forEach((item: any) => {
+    if (Number(hour) >= Number(item.startTime.substring(0, 2))) {
+      price = item.totalPrice;
+    }
+  });
+  return price;
+};
+
+const rebuildHours = (day: Date) => {
+  timesInfo.value.hour = [];
+  const nowHour = format("h");
+  const isTaday = format("d") === format("d", day.getTime());
+  // 当前时间的24小时内
+  let startHour = isTaday ? Number(nowHour) : 0;
+  let endHour = isTaday ? 24 : Number(nowHour) + 1;
+  if (chargeTime.value.isPowerSaving) {
+    startHour = 0;
+    endHour = 8;
+  }
+  for (let index = startHour; index < endHour; index++) {
+    timesInfo.value.hour = [...timesInfo.value.hour, ...buildHours(index, day)];
+  }
+};
+
+const buildHours = (hour: number, day: Date) => {
+  let res: any[] = [];
+  let hourTemp = hour >= 10 ? `${hour}` : `0${hour}`;
+  let priceTemp = getHourPrice(hourTemp);
+  for (let index = 0; index < 12; index++) {
+    res.push({
+      format: `${hourTemp}:${index * 5 >= 10 ? index * 5 : `0${index * 5}`}`,
+      formatPrice: priceTemp,
+      disabled: isPassTime(
+        `${hourTemp}:${index * 5 >= 10 ? index * 5 : `0${index * 5}`}`,
+        chargeTime.value.time ? chargeTime.value.time : day.getTime()
+      ),
+    });
+  }
+  return res;
+};
+
+const selectTime = () => {
+  if (chargeTime.value.isPowerSaving) {
+    uni.showToast({
+      title: "省电模式将在00:00-08:00进行充电",
+      icon: "none",
+    });
+    return;
+  }
+  if (!appointmentData.value && chargeType.value === 1) {
+    uni.showToast({
+      title: "请选择预约模式",
+      icon: "none",
+    });
+    return;
+  }
+  const now = appointmentData.value
+    ? new Date(appointmentData.value.startTime.replace(/-/g, "/"))
+    : new Date();
+  const year = format("y", now.getTime());
+  const nextyear = format("y", now.getTime() + DAY);
+
+  // 天
+  timesInfo.value.day = [
+    {
+      time: now.getTime(),
+      format: format("M月d日", now.getTime()),
+      year: "",
+    },
+    {
+      time: now.getTime() + DAY,
+      format: format("M月d日", now.getTime() + DAY),
+      year: year === nextyear ? "" : nextyear.slice(2),
+    },
+  ];
+  rebuildHours(now);
+
+  // 预约时间
+  if (!appointmentData.value) {
+    if (!chargeTime.value.day) {
+      chargeTime.value.time = timesInfo.value.day[0].time;
+      chargeTime.value.day = timesInfo.value.day[0].format;
+    }
+    // 滚动到特定位置
+    if (chargeTime.value.hour) {
+      chargeTime.value.scrollId = `hour-${chargeTime.value.hour}`;
+    } else {
+      let hourNow = now.getHours();
+      chargeTime.value.scrollId = `hour-${hourNow}:00`;
+    }
+  }
+  // 修改时间
+  if (appointmentData.value) {
+    chargeTime.value.time = timesInfo.value.day[0].time;
+    chargeTime.value.day = timesInfo.value.day[0].format;
+    const findIndex = timesInfo.value.hour.findIndex(
+      (item: any) => item.format === format("h:m:s", now.getTime())
+    );
+    if (findIndex >= 0) {
+      chargeTime.value.hour = timesInfo.value.hour[findIndex].format;
+      chargeTime.value.formatPrice =
+        timesInfo.value.hour[findIndex].formatPrice;
+    }
+  }
+  chargeTimeDialog.value = true;
+};
+
+const changeDay = (index: number) => {
+  chargeTime.value.time = timesInfo.value.day[index].time;
+  chargeTime.value.day = timesInfo.value.day[index].format;
+  if (chargeTime.value.hour) {
+    chargeTime.value.hour = "";
+    chargeTime.value.formatPrice = "";
+  }
+  const now = new Date(chargeTime.value.time);
+  rebuildHours(now);
+  setTimeout(() => {
+    chargeTime.value.scrollId = `hour-${timesInfo.value.hour[0].format}`;
+  }, 0);
+};
+
+const changeHour = (index: number) => {
+  if (timesInfo.value.hour[index].disabled) {
+    uni.showToast({
+      title: "无法选择过去的时间",
+      icon: "none",
+    });
+    return;
+  }
+  if (appointmentData.value) {
+    // 修改时间
+    uni.showModal({
+      title: "温馨提示",
+      content: `确认修改为${timesInfo.value.hour[index].format}吗?`,
+      confirmText: "确定",
+      confirmColor: "#347DFF",
+      success(modal) {
+        if (modal.confirm) {
+          uni.showLoading({
+            title: "提交中",
+            mask: true,
+          });
+          changeAppointmentTime(
+            appointmentData.value.startChargeSeq,
+            `${format("y-M-d", chargeTime.value.time)} ${
+              timesInfo.value.hour[index].format
+            }:00`
+          )
+            .then(() => {
+              return fetchChargeStatus();
+            })
+            .then((res) => {
+              uni.hideLoading();
+              if (res && res.isAppointment) {
+                uni.showToast({
+                  icon: "success",
+                  title: "修改成功",
+                });
+                closeTime();
+                appointmentData.value = res;
+                startAppointmentCountDown();
+              }
+            });
+        }
+      },
+    });
+    return;
+  }
+  chargeTime.value.hour = timesInfo.value.hour[index].format;
+  chargeTime.value.format = `${format("y年M月d日", chargeTime.value.time)} ${
+    chargeTime.value.hour
+  }`;
+  chargeTime.value.formatPrice = timesInfo.value.hour[index].formatPrice;
+  closeTime();
+};
+
+const changePowerSaving = (e: any) => {
+  chargeTime.value.isPowerSaving = e.detail.value;
+};
+
+const closeTime = () => {
+  chargeTimeDialog.value = false;
+  if (appointmentData.value) {
+    return;
+  }
+  if (!chargeTime.value.hour) {
+    chargeTime.value = {
+      time: 0, // 时间戳
+      day: "",
+      hour: "",
+      isPowerSaving: chargeTime.value.isPowerSaving, // 省电模式
+      format: "",
+      formatPrice: "",
+      scrollId: "",
+    };
+  }
+};
+
+const openPriceDesc = () => {
+  priceDescVisible.value = true;
+};
+
+const closePriceDesc = () => {
+  priceDescVisible.value = false;
+};
+
+const cancelAppointment = () => {
+  uni.showModal({
+    title: "取消预约",
+    content: "确定取消预约吗?",
+    cancelText: "取消预约",
+    cancelColor: "#999999",
+    confirmText: "点错了",
+    confirmColor: "#347DFF",
+    success: (res) => {
+      if (res.cancel) {
+        uni.showLoading({
+          title: "提交中",
+          mask: true,
+        });
+        cancelAppointmentCharge()
+          .then(() => {
+            uni.hideLoading();
+            uni.showToast({
+              icon: "success",
+              title: "已取消",
+            });
+            appointmentData.value = undefined;
+            startAppointmentCountDown();
+          })
+          .catch((err) => {
+            uni.hideLoading();
+          });
+      }
+    },
+  });
+};
+
+let appointmentCountDownTimer = 0;
+
+const startAppointmentCountDown = () => {
+  appointmentCountDownTimer && clearTimeout(appointmentCountDownTimer);
+  if (!appointmentData.value) {
+    appointmentCountDown.value = "";
+    return;
+  }
+  appointmentCountDown.value = formatAppointmentCountDown(
+    appointmentData.value.startTime
+  );
+  if (appointmentCountDown.value) {
+    appointmentCountDownTimer = setTimeout(() => {
+      startAppointmentCountDown();
+    }, 1000);
+  } else {
+    uni.showModal({
+      content: "当前预约时间已到,将开始进行充电",
+      showCancel: false,
+      success() {
+        uni.switchTab({
+          url: "/pages/map/map",
+        });
+      },
+    });
+  }
+};
+
+const formatAppointmentCountDown = (time: string) => {
+  const now = new Date();
+  const date = new Date(time.replace(/-/g, "/"));
+  let secondTime = parseInt(`${(date.getTime() - now.getTime()) / 1000}`);
+  if (secondTime <= 0) {
+    return "";
+  }
+  let hourTime = 0;
+  let minuteTime = 0; // 分
+  if (secondTime >= 60) {
+    minuteTime = parseInt(`${secondTime / 60}`);
+    secondTime = parseInt(`${secondTime % 60}`);
+    if (minuteTime >= 60) {
+      hourTime = parseInt(`${minuteTime / 60}`);
+      minuteTime = parseInt(`${minuteTime % 60}`);
+    }
+  }
+  return `${hourTime < 10 ? "0" + hourTime : hourTime}:${
+    minuteTime < 10 ? "0" + minuteTime : minuteTime
+  }:${secondTime < 10 ? "0" + secondTime : secondTime}`;
+};
+
+const submit = () => {
+  if (
+    chargeType.value === 0 &&
+    !chargeTime.value.isPowerSaving &&
+    !chargeTime.value.hour
+  ) {
+    uni.showToast({
+      title: "请选择充电时间",
+      icon: "none",
+    });
+    return;
+  }
+  const now = new Date();
+  let startTime = chargeTime.value.time;
+  if (chargeTime.value.isPowerSaving) {
+    startTime = now.getTime();
+    // 省电模式
+    if (
+      now.getHours() >= 8 &&
+      now.getDate() === Number(format("d", chargeTime.value.time))
+    ) {
+      startTime += DAY;
+    }
+  }
+  let query = "";
+  if (chargeType.value === 0) {
+    query += "isBooking=true";
+    query += `&startTime=${format("y-M-d", startTime)} ${
+      chargeTime.value.isPowerSaving
+        ? "00:00:00"
+        : `${chargeTime.value.hour}:00`
+    }`;
+    if (chargeTime.value.isPowerSaving) {
+      query += `&endTime=${format("y-M-d", startTime)} 08:00:00`;
+    }
+  }
+  if (chargeType.value === 1) {
+    query += "isBooking=false";
+  }
+  if (discountIndex.value >= 0) {
+    query += `&rechargeRightsId=${discountList.value[discountIndex.value].id}`;
+  }
+  console.log(query)
+  uni.showLoading({
+    title: "提交中",
+    mask: true,
+  });
+  startCharge(options.value.sn, query)
+    .then(() => {
+      if (chargeType.value === 0) {
+        fetchChargeStatus()
+          .then((res) => {
+            if (res && res.isAppointment) {
+              uni.hideLoading();
+              uni.showToast({
+                icon: "success",
+                title: "预约成功",
+              });
+              appointmentData.value = res;
+              startAppointmentCountDown();
+            }
+          })
+          .catch(() => {
+            uni.navigateBack();
+          });
+      }
+      if (chargeType.value === 1) {
+        redirect(`/pages-charge/ordering/ordering?sn=${options.value.sn}&start=1`)
+      }
+    })
+    .catch((err) => {
+      uni.hideLoading();
+      uni.showModal({
+        content: `${err.errMsg}`,
+        showCancel: false,
+      });
+    });
+};
+
+const submitNow = () => {
+  uni.showLoading({
+    title: "提交中",
+    mask: true,
+  });
+  startAppointmentCharge(options.value.sn)
+    .then(() => {
+      _checkStartCharge();
+    })
+    .catch((err) => {
+      uni.hideLoading();
+      uni.showModal({
+        content: `${err.errMsg}`,
+        showCancel: false,
+      });
+    });
+};
+
+const _checkStartCharge = () => {
+  fetchChargeStatus().then((res) => {
+    if (res && [2].includes(res.chargeStatus)) {
+      uni.hideLoading();
+      redirect(`/pages-charge/ordering/ordering?sn=${options.value.sn}&start=1`)
+    } else {
+      _checkStartCharge();
+    }
+  });
+};
+
+onLoad((_options: any) => {
+  // sn=SN100523042860091 测试环境
+  console.log("options", _options);
+  let sn = _options.sn;
+  uni.showLoading({
+    title: "加载中",
+  });
+  fetchStationByConnectorIdOrShortId(sn)
+    .then((res) => {
+      console.log(res);
+      if (
+        res.equipment &&
+        res.equipment.connectorInfos &&
+        res.equipment.connectorInfos.length
+      ) {
+        sn = res.equipment.connectorInfos[0].connectorId;
+        _options.sn = res.equipment.connectorInfos[0].connectorId;
+      }
+      options.value = _options;
+      data.value = res;
+      return fetchStationPriceDesc(sn);
+    })
+    .then((res) => {
+      // console.log(res);
+      uni.hideLoading();
+      priceInfo.value = res;
+      return fetchChargeStatus(false, true);
+    })
+    .then((res) => {
+      if (res && res.isAppointment) {
+        appointmentData.value = res;
+        startAppointmentCountDown();
+      }
+    })
+    .catch((err) => {
+      console.log(err);
+      uni.hideLoading();
+    });
+  fetchProfile().then((res) => {
+    if (res.userRechargeRightsList.length) {
+      res.userRechargeRightsList.forEach((item: any) => {
+        item.discountFormat = Number((Number(item.discount) / 10).toFixed(2));
+      });
+      discountList.value = res.userRechargeRightsList;
+      discountIndex.value = 0;
+    }
+
+    if (res.activityList && res.activityList.length) {
+      res.activityList[0].rechargeRightsList.sort((a: any, b: any) => {
+        return a.discount - b.discount;
+      });
+      activity.value = res.activityList[0];
+      activity.value.minDiscount = Number(
+        (
+          Number(res.activityList[0].rechargeRightsList[0].discount) / 10
+        ).toFixed(2)
+      );
+    }
+  });
+});
+
+onShow(() => {
+  if (getApp<any>().globalData.lastData.discount) {
+    discountIndex.value = getApp<any>().globalData.lastData.discount.index;
+    getApp<any>().globalData.lastData.discount = undefined;
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+.page {
+  min-height: 100vh;
+  width: 100%;
+  padding: 20rpx;
+  background-color: #f6f7fa;
+}
+.block {
+  border-radius: 20rpx;
+  background: #fff;
+}
+.station {
+  height: 148rpx;
+  border-bottom: 1rpx solid rgba(0, 0, 0, 0.1);
+}
+
+.hour-placeholder {
+  box-sizing: content-box;
+  padding-bottom: constant(safe-area-inset-bottom);
+  padding-bottom: env(safe-area-inset-bottom);
+}
+</style>

+ 248 - 0
src/pages-charge/camera/camera.vue

@@ -0,0 +1,248 @@
+<template>
+  <view class="page flex-column">
+    <cover-image
+      class="page_back"
+      :style="backStyle"
+      src="/pages-charge/static/camera-back.png"
+      @click="back"
+    ></cover-image>
+    <view class="page_camera flex-grow">
+      <camera
+        v-if="!isDevTool"
+        device-position="back"
+        mode="scanCode"
+        :flash="cameraFlash"
+        @scancode="handleScancode"
+        @ready="handleReady"
+        @error="handleError"
+        :style="{
+          width: '100%',
+          height: `${cameraHeight}px`,
+        }"
+      ></camera>
+      <view
+        v-else
+        class="mock-camera"
+        :style="{
+          width: '100%',
+          height: `${cameraHeight}px`,
+        }"
+      ></view>
+    </view>
+    <view class="page_action flex-shrink" id="page_action">
+      <cover-view
+        @click="toggleFlash"
+        style="
+          text-align: center;
+          position: absolute;
+          width: 100%;
+          left: 0;
+          top: -96px;
+        "
+      >
+        <cover-image
+          style="width: 28px; height: 28px; margin: 0 auto"
+          src="/pages-charge/static/scan-code-light.png"
+        ></cover-image>
+        <cover-view
+          style="
+            color: rgba(255, 255, 255, 0.7);
+            font-size: 13px;
+            margin-top: 10px;
+          "
+          >{{ cameraFlash === "off" ? "轻触照亮" : "轻触关闭" }}</cover-view
+        >
+      </cover-view>
+      <view class="flex">
+        <view
+          class="flex-center flex-column"
+          @click="redirect('/pages-charge/codeing/codeing')"
+        >
+          <image
+            class="width-96"
+            mode="widthFix"
+            src="/pages-charge/static/scan-code-input.png"
+          ></image>
+          <view class="fs-24 color-fff mt-20">输入充电桩编码</view>
+        </view>
+        <view class="flex-center flex-column" @click="chooseImage">
+          <image
+            class="width-96"
+            mode="widthFix"
+            src="/pages-charge/static/scan-code-album.png"
+          ></image>
+          <view class="fs-24 color-fff mt-20">打开相册</view>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref } from "vue";
+import { isDevTool } from "../../utils/device";
+import { deCode } from "../../utils/code";
+import { back, redirect } from "../../utils/navigate";
+import { onLoad, onReady } from "@dcloudio/uni-app";
+import { fetchChargeStatus, searchQRCode } from "../../api/charge";
+import { upload } from "@/utils/uploader";
+
+const cameraHeight = ref(0);
+const cameraFlash = ref("off");
+const backStyle = ref("");
+
+onLoad(() => {
+  const menuButtonRect = uni.getMenuButtonBoundingClientRect();
+  backStyle.value = `left:12px;top:${menuButtonRect.top + 6}px;`;
+  uni.showLoading({
+    title: "加载中",
+  });
+  fetchChargeStatus(true, true)
+    .then(() => {
+      uni.hideLoading();
+    })
+    .catch(() => {
+      uni.hideLoading();
+    });
+});
+
+onReady(() => {
+  // #ifdef MP-WEIXIN
+  const query = wx.createSelectorQuery();
+  query.select("#page_action").boundingClientRect();
+  query.exec(function (res: any) {
+    if (res && res.length) {
+      cameraHeight.value =
+        getApp<any>().globalData.device.windowHeight - res[0].height;
+      uni.hideLoading();
+    }
+  });
+  // #endif
+});
+
+const handleReady = () => {
+  uni.hideLoading();
+};
+
+const handleScancode = (e: any) => {
+  uni.vibrateShort({});
+  if (e.detail && e.detail.result) {
+    deCode(e.detail.result);
+  }
+};
+
+const handleError = (e: any) => {
+  uni.hideLoading();
+  if (e.detail && e.detail.errMsg && /auth/.test(e.detail.errMsg)) {
+    uni.showModal({
+      content: "请打开摄像头权限开关",
+      success: () => {
+        uni.openSetting();
+      },
+    });
+    return;
+  }
+  uni.showModal({
+    content: e.detail.errMsg,
+  });
+};
+
+const toggleFlash = () => {
+  if (cameraFlash.value === "off") {
+    cameraFlash.value = "on";
+    return;
+  }
+  cameraFlash.value = "off";
+};
+
+const chooseImage = () => {
+  uni.chooseImage({
+    count: 1,
+    sizeType: ["compressed"],
+    sourceType: ["album"],
+    success: (res) => {
+      uni.showLoading({
+        title: "正在识别",
+      });
+      upload(res.tempFilePaths[0], {
+        onSuccess: (res) => {
+          searchQRCode(res.url)
+            .then((res) => {
+              if (
+                res.errmsg === "ok" &&
+                res.errcode === 0 &&
+                res.code_results &&
+                res.code_results.length
+              ) {
+                const fi = res.code_results.findIndex(
+                  (item: any) => item.type_name === "QR_CODE"
+                );
+                if (fi >= 0) {
+                  deCode(res.code_results[fi].data);
+                  return;
+                }
+              }
+              console.log(res);
+              return Promise.reject({
+                errMsg: "微信识别出错,请重试",
+              });
+            })
+            .catch((err) => {
+              uni.hideLoading();
+              uni.showModal({
+                content: err.errMsg || "出现错误,请重试",
+              });
+            });
+        },
+        onFail: (err) => {
+          uni.hideLoading();
+          uni.showModal({
+            content: err.errMsg || "出现错误,请重试",
+          });
+        },
+      });
+    },
+    fail: (err) => {
+      console.log(err);
+      if (/cancel/.test(err.errMsg)) {
+        return;
+      }
+      uni.showModal({
+        content: err.errMsg || "出现错误,请重试",
+      });
+    },
+  });
+};
+</script>
+
+<style lang="scss">
+.page {
+  position: relative;
+  height: 100vh;
+  background-color: #000;
+  &_back {
+    position: absolute;
+    width: 24px;
+    height: auto;
+  }
+  &_camera {
+    height: 100%;
+    .mock-camera {
+      background-color: #999;
+    }
+  }
+  &_action {
+    position: relative;
+    box-sizing: content-box;
+    padding-bottom: constant(safe-area-inset-bottom);
+    padding-bottom: env(safe-area-inset-bottom);
+    & > view {
+      height: 222rpx;
+      & > view {
+        width: 50%;
+        height: 222rpx;
+      }
+    }
+  }
+}
+</style>

+ 22 - 3
src/pages-charge/codeing/codeing.vue

@@ -2,7 +2,7 @@
   <view class="pt-10 pr-60 pl-60">
     <view class="lh-0">
       <!-- #ifdef MP-WEIXIN -->
-		  <!-- #endif -->
+      <!-- #endif -->
       <image
         src="/pages-charge/static/charge-input-head.png"
         mode="widthFix"
@@ -29,8 +29,10 @@
 </template>
 
 <script setup lang="ts">
+import { fetchStationByConnectorIdOrShortId } from "@/api/charge";
 import { ref } from "vue";
 import { scanCode } from "../../utils/code";
+import { to } from "../../utils/navigate";
 
 const value = ref("");
 const focus = ref(true);
@@ -43,9 +45,26 @@ const submit = () => {
     });
     return;
   }
-  uni.navigateTo({
-    url: `/pages-charge/ordering/ordering?sn=${value.value}`,
+  uni.showLoading({
+    title: "加载中",
   });
+  fetchStationByConnectorIdOrShortId(value.value)
+    .then(() => {
+      uni.hideLoading();
+      to(`/pages-charge/appointment/appointment?sn=${value.value}`);
+    })
+    .catch((err) => {
+      value.value = ''
+      console.log(err);
+      uni.hideLoading();
+      uni.showModal({
+        title: "温馨提示",
+        content: `充电桩编号不存在,请检查后重新输入`,
+        confirmText: "确定",
+        confirmColor: "#347DFF",
+        showCancel: false,
+      });
+    });
 };
 const input = (e: any) => {
   value.value = e.value;

+ 175 - 0
src/pages-charge/discount/discount.vue

@@ -0,0 +1,175 @@
+<template>
+  <view class="page pt-20 pr-40 pl-40">
+    <view class="fs-28 color-666 mt-20" v-if="mode === 0">充电优惠权益卡</view>
+    <view
+      class="item mt-20"
+      v-for="(item, index) in list"
+      v-if="mode >= 0"
+      :key="index"
+      @click="checkDiscount(index)"
+    >
+      <image
+        class="full-percent"
+        src="/pages-charge/static/discount-bg.png"
+      ></image>
+      <view class="absolute-full flex pt-8 pb-8">
+        <view class="left flex-column flex-justify-center pl-48">
+          <view class="flex-align-end">
+            <image
+              class="width-92"
+              src="/pages-charge/static/discount-coupon.png"
+              mode="widthFix"
+            ></image>
+            <view class="fs-48 lh-58 color-000 fw-500 ml-16"
+              >{{ item.discountFormat }}折</view
+            >
+            <view class="fs-26 color-999 ml-16" v-if="mode === 0"
+              >充电服务费</view
+            >
+          </view>
+          <view class="flex-align-center" v-if="mode === 0">
+            <view class="fs-26 color-999 mt-24"
+              >权益余额{{ item.rightsBalanceFormat }}元|</view
+            >
+            <view class="fs-26 color-999 mt-24"
+              >{{ item.endTimeFormat }}到期</view
+            >
+          </view>
+          <block v-if="mode === 1">
+            <view class="fs-26 color-999 mt-24">{{ item.rightsDesc }}</view>
+            <view
+              class="fs-26 color-999 mt-8"
+              @click.stop="
+                to(`/pages-common/activity/activity?id=${activity.id}`)
+              "
+            >
+              <text>有效期{{ item.validity }}天,</text>
+              <text class="color-primary">详细规则</text>
+              <text class="fs-24 ml-4 color-primary">>></text>
+            </view>
+          </block>
+        </view>
+        <view v-if="mode === 0" class="right width-80 flex-center">
+          <style-checkbox
+            :size="48"
+            :checked="checkIndex === index"
+            :iconsize="17"
+          ></style-checkbox>
+        </view>
+        <view
+          v-if="mode === 1"
+          class="right flex-center"
+          :style="{
+            width: '210rpx',
+            borderLeft: '1px dashed rgba(0, 0, 0, 0.10)',
+          }"
+        >
+          <view class="btn flex-center fs-28" @click.stop="toCharge(index)"
+            >前往充值</view
+          >
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { onLoad, onShow } from "@dcloudio/uni-app";
+import { ref } from "vue";
+import { to } from "../../utils/navigate";
+import { fetchProfile } from "@/api/user";
+
+const mode = ref(-1);
+const list = ref<any[]>([]);
+const activity = ref();
+const checkIndex = ref(-1);
+const needRefresh = ref(false);
+
+const toCharge = (index: number) => {
+  needRefresh.value = true;
+  to(
+    "/pages-user/wallet-recharge/wallet-recharge?back=1&value=" +
+      list.value[index].amountMin
+  );
+};
+
+const setUserRecharge = (_list: any[]) => {
+  _list.forEach((item: any) => {
+    item.endTimeFormat = item.endTime.split(" ")[0];
+    item.rightsBalanceFormat = Number(
+      (Number(item.rightsBalance) / 100).toFixed(2)
+    );
+  });
+  list.value = _list;
+  mode.value = 0;
+};
+
+const checkDiscount = (index: number) => {
+  checkIndex.value = index;
+  getApp<any>().globalData.lastData.discount = {
+    index,
+  };
+};
+
+onLoad((options: any) => {
+  if (getApp<any>().globalData.user) {
+    if (getApp<any>().globalData.user.userRechargeRightsList.length) {
+      setUserRecharge(getApp<any>().globalData.user.userRechargeRightsList);
+      checkIndex.value = options.index ? Number(options.index) : 0;
+    } else {
+      const _activity = getApp<any>().globalData.user.activityList.find(
+        (item: any) => item.id === options.id
+      );
+      _activity.rechargeRightsList.forEach((item: any) => {
+        item.discountFormat = Number((Number(item.discount) / 10).toFixed(2));
+      });
+      list.value = _activity.rechargeRightsList;
+      activity.value = _activity;
+      mode.value = 1;
+    }
+  }
+});
+onShow(() => {
+  if (needRefresh.value) {
+    needRefresh.value = false;
+    uni.showLoading({
+      title: "加载中",
+    });
+    fetchProfile().then((res) => {
+      uni.hideLoading();
+      if (res.userRechargeRightsList.length) {
+        setUserRecharge(res.userRechargeRightsList);
+        checkDiscount(0)
+      }
+    });
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+.page {
+  min-height: 100vh;
+  width: 100%;
+  background-color: #f6f7fa;
+  .item {
+    position: relative;
+    height: 240rpx;
+    border-radius: 20rpx;
+    overflow: hidden;
+    background: #fff;
+    .left {
+      flex-grow: 1;
+    }
+    .right {
+      flex-shrink: 0;
+      .btn {
+        border-radius: 8rpx;
+        border: 1px solid var(--color-primary);
+        color: var(--color-primary);
+        width: 144rpx;
+        height: 56rpx;
+      }
+    }
+  }
+}
+</style>

+ 38 - 20
src/pages-charge/machines/charge-machine/charge-machine.vue

@@ -8,10 +8,15 @@
     <view
       :class="[
         'charge-point_icon',
+        'flex-center',
         `charge-point_icon-${item.connectorStatusInfo.status}`,
       ]"
     >
-      <!-- TODO 图标 -->
+      <image
+        class="width-28"
+        src="/pages-charge/static/charge-status-icon.png"
+        mode="widthFix"
+      ></image>
     </view>
     <view class="charge-point_title ml-16">
       <view class="flex-align-center">
@@ -22,12 +27,8 @@
             'fs-24',
             'flex-center',
             'ml-8',
-            item.connectorStatusInfo.status === 255 ||
-            item.connectorStatusInfo.status === 0
-              ? 'tag-disabled'
-              : 'tag-warning',
+            `tag-${item.connectorStatusInfo.status}`,
           ]"
-          v-if="item.connectorStatusInfo.status !== 1"
           >{{ (statusMap as any)[item.connectorStatusInfo.status] }}</view
         >
       </view>
@@ -37,10 +38,10 @@
           }}{{ item.power }}kw</view
         >
         <view class="fs-24 color-666 mr-6 ml-6">|</view>
-        <view class="fs-24 color-666">车位号TODO</view>
+        <view class="fs-24 color-666">车位号{{ parkingNo }}</view>
       </view>
     </view>
-    <view class="ml-auto">
+    <view class="ml-auto" v-if="price">
       <view
         class="lh-32 color-warning"
         style="text-align: right; margin-top: -8rpx"
@@ -51,6 +52,9 @@
       </view>
       <view class="fs-24 lh-24 color-999 mt-10">预计费用</view>
     </view>
+    <view class="ml-auto height-60 flex-align-end" v-if="time">
+      <view class="fs-24 lh-24 color-999">{{ time }}</view>
+    </view>
   </view>
 </template>
 
@@ -58,9 +62,10 @@
 export default {
   props: {
     title: String,
-    price: String,
+    price: [String, Number],
     time: String,
     list: Array<any>,
+    parkingNo: String,
   },
   data() {
     return {
@@ -68,7 +73,7 @@ export default {
         255: "故障",
         0: "离网",
         1: "空闲",
-        2: "未充电",
+        2: "占用中",
         3: "充电中",
         4: "已预约",
       },
@@ -81,14 +86,15 @@ export default {
         list &&
         list[index].connectorStatusInfo &&
         list[index].connectorStatusInfo.status &&
-        list[index].connectorStatusInfo.status === 1
+        (list[index].connectorStatusInfo.status === 1 ||
+          list[index].connectorStatusInfo.status === 2)
       ) {
         uni.showActionSheet({
           itemList: ["去充电"],
           success: (res) => {
             if (res.tapIndex === 0) {
               uni.navigateTo({
-                url: `/pages-charge/ordering/ordering?sn=${list[index].connectorId}`,
+                url: `/pages-charge/appointment/appointment?sn=${list[index].connectorId}`,
               });
             }
           },
@@ -114,17 +120,19 @@ export default {
     height: 56rpx;
     width: 56rpx;
     border-radius: 50%;
-    &-0,
     &-255 {
+      background: #f43636;
+    }
+    &-0 {
       background-color: #b1b1b1;
     }
     &-1 {
-      background-color: #00dab3;
+      background: #21dc82;
     }
     &-2,
     &-3,
     &-4 {
-      background-color: var(--color-warning);
+      background: rgba(255, 153, 0, 1);
     }
   }
   .tag {
@@ -133,13 +141,23 @@ export default {
     box-sizing: border-box;
     border-radius: 8rpx;
   }
-  .tag-warning {
-    color: var(--color-warning);
-    background: rgba(255, 153, 0, 0.1);
+  .tag-255 {
+    color: #f43636;
+    background: rgba(244, 54, 54, 0.1);
   }
-  .tag-disabled {
+  .tag-0 {
     color: #b1b1b1;
-    background: rgba(177, 177, 177, 0.1);
+    background-color: rgba(177, 177, 177, 0.1);
+  }
+  .tag-1 {
+    color: #21dc82;
+    background: rgba(33, 220, 130, 0.1);
+  }
+  .tag-2,
+  .tag-3,
+  .tag-4 {
+    color: rgba(255, 153, 0, 1);
+    background: rgba(255, 153, 0, 0.1);
   }
 }
 </style>

+ 154 - 184
src/pages-charge/machines/machines.vue

@@ -1,146 +1,152 @@
 <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">
+  <!--  #ifdef MP-WEIXIN -->
+  <page-meta page-style="overflow: {{y ? 'hidden' : 'visible'}}" />
+  <!--  #endif -->
+  <view :class="['pb-40']" v-if="station">
+    <view class="banner">
+      <swiper
+        class="full-percent"
+        circular
+        :indicator-dots="true"
+        :autoplay="true"
+        :interval="3000"
+      >
+        <swiper-item
+          class="full-percent"
+          v-for="(item, index) in station.bannerImages"
+          :key="index"
+        >
+          <view
+            class="full-percent banner-image"
+            :style="{
+              backgroundImage: `url(${item})`,
+            }"
+            @click="previewImage(item)"
+          ></view>
+        </swiper-item>
+      </swiper>
+    </view>
+    <view class="pl-20 pr-20">
+      <view class="station 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-nav.png"
+            src="/pages-charge/static/machines-banner-address.png"
             mode="widthFix"
+            style="width: 16px; display: block; flex-shrink: 0"
           />
-          <view class="fs-26" style="color: #347dff">导航</view>
+          <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>
-      <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
+          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="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 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="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 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>
-    </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
+      <template
+        v-if="station"
+        v-for="(item, index) in stationEquipmentInfos"
+        :key="index"
       >
-      <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>
+        <ChargeMachine
+          :title="'NO.' + item.shortId"
+          :price="station.totalFee"
+          :list="item.connectorInfos"
+          :parkingNo="item.parkingNo"
+        ></ChargeMachine>
+      </template>
     </view>
-    <template
-      v-if="station"
-      v-for="(item, index) in stationEquipmentInfos"
-      :key="index"
+    <PriceDesc
+      v-if="dialogVisible && dialogType === 'desc'"
+      :desc="desc"
+      @close="closeDialog"
+    ></PriceDesc>
+    <view
+      class="dialog flex-align-end"
+      v-if="dialogVisible && dialogType === 'status'"
     >
-      <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="status-dialog">
+        <view class="status-dialog_head flex-center">
+          <view class="fw-500 color-000" style="font-size: 16px"
+            >选择充电桩状态</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="status-dialog_body">
+          <view class="status">
             <view
               :class="['fs-32', 'flex-align-center']"
               :style="{ color: status === index ? '#347DFF' : '#2d284b' }"
@@ -162,6 +168,7 @@ import { ref } from "vue";
 import { fetchStation, fetchStationPriceDesc } from "../../api/charge";
 import { onLoad } from "@dcloudio/uni-app";
 import ChargeMachine from "./charge-machine/charge-machine.vue";
+import PriceDesc from "./price-desc/price-desc.vue";
 const dialogVisible = ref(false);
 const dialogType = ref("");
 const desc = ref<any[]>([]);
@@ -235,6 +242,7 @@ onLoad((options: any) => {
           }
         });
       }
+      res.bannerImages = res.pictures.split(",");
       station.value = res;
       stationEquipmentInfos.value = res.equipmentInfos.map((item: any) => {
         return {
@@ -246,7 +254,7 @@ onLoad((options: any) => {
       } else {
         // eslint-disable-next-line promise/no-return-wrap
         return Promise.resolve({
-          policyInfoss: [],
+          policyInfos: [],
         });
       }
     })
@@ -255,7 +263,7 @@ onLoad((options: any) => {
       currentTime.value = res.currentTime;
       canUseTime.value = res.useTime;
       canUsePrice.value = `${res.minPrice}~${res.maxPrice}`;
-      desc.value = res.policyInfoss || [];
+      desc.value = res.policyInfos || [];
     })
     .catch((err) => {
       // eslint-disable-next-line no-console
@@ -281,9 +289,6 @@ const openDesc = function () {
   dialogType.value = "desc";
 };
 const openStatus = function () {
-  if (!stationEquipmentInfos.value || stationEquipmentInfos.value.length <= 0) {
-    return;
-  }
   dialogVisible.value = true;
   dialogType.value = "status";
 };
@@ -328,6 +333,12 @@ const changeStatus = function (index: number) {
   stationEquipmentInfos.value = newStationEquipmentInfos;
   closeDialog();
 };
+const previewImage = (url: string) => {
+  uni.previewImage({
+    urls: station.value.bannerImages,
+    current: url,
+  });
+};
 </script>
 
 <style lang="scss">
@@ -336,14 +347,18 @@ page {
   background-color: #f5f5f5;
 }
 
-.container-hidden {
-  width: 100%;
-  height: 100vh;
-  overflow: hidden;
+.banner {
+  height: 440rpx;
+  background-color: transparent;
+  .banner-image {
+    background-position: center;
+    background-size: cover;
+    background-repeat: no-repeat;
+  }
 }
 
-.banner {
-  margin-top: 380rpx;
+.station {
+  margin-top: -60rpx;
   border-radius: 20rpx;
   overflow: hidden;
   position: relative;
@@ -383,11 +398,11 @@ page {
   }
 }
 
-.desc-dialog {
+.status-dialog {
   background-color: transparent;
   width: 100%;
   overflow: hidden;
-  border-radius: 20rpx;
+  border-radius: 20rpx 20rpx 0px 0px;
 
   &_head {
     padding: 24rpx 0px;
@@ -408,51 +423,6 @@ page {
     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;

+ 180 - 0
src/pages-charge/machines/price-desc/price-desc.vue

@@ -0,0 +1,180 @@
+<template>
+  <view class="dialog flex-align-end">
+    <view class="desc-dialog">
+      <view class="desc-dialog_head flex-center">
+        <view class="fw-500 color-000" style="font-size: 16px"
+          >充电费用说明</view
+        >
+        <view class="close" @click="close">
+          <uni-icons type="closeempty" size="24" color="#2D284B"></uni-icons>
+        </view>
+      </view>
+      <view class="desc-dialog_body">
+        <view class="table">
+          <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">
+              <text style="width: 158rpx; text-align: center">{{
+                item.startTimeFormat
+              }}</text>
+              <text
+                class="ml-16 width-38 height-38 br-round fs-24 flex-center"
+                v-if="item.iconIndex >= 0"
+                :style="{
+                  color: iconColor[item.iconIndex],
+                  backgroundColor: iconBgColor[item.iconIndex],
+                }"
+                >{{ item.pricePeriod }}</text
+              ></view
+            >
+            <view class="td flex-center">{{ item.elecPrice }}</view>
+            <view class="td flex-center">{{ item.servicePrice }}</view>
+          </view>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script lang="ts">
+import { defineComponent } from "vue";
+export default defineComponent({
+  props: {
+    desc: Array<any>,
+  },
+  data() {
+    return {
+      _desc: [],
+      iconText: ["谷", "平", "峰", "尖"],
+      iconBgColor: [
+        "rgba(0, 218, 179, 0.20)",
+        "rgba(52, 125, 255, 0.20)",
+        "rgba(255, 153, 0, 0.20)",
+        "rgba(255, 46, 0, 0.20)",
+      ],
+      iconColor: ["#00DAB3", "#347DFF", "#FF9900", "#FF2E00"],
+    };
+  },
+  mounted() {
+    console.log(this.desc);
+    if (this.desc && this.desc.length) {
+      this._desc = this.desc.map((item) => {
+        return {
+          ...item,
+          iconIndex: this.iconText.findIndex(
+            (text) => item.pricePeriod === text
+          ),
+        };
+      });
+    }
+  },
+  methods: {
+    close() {
+      this.$emit("close");
+    },
+  },
+});
+</script>
+
+<style lang="scss">
+@import "../../../styles/font.scss";
+@import "../../../styles/flex.scss";
+@import "../../../styles/layout.scss";
+@import "../../../styles/dialog.scss";
+.desc-dialog {
+  background-color: transparent;
+  width: 100%;
+  overflow: hidden;
+  border-radius: 20rpx 20rpx 0px 0px;
+
+  &_head {
+    padding: 24rpx 0px;
+    position: relative;
+    background-color: #fff;
+    .close {
+      position: absolute;
+      right: 30rpx;
+      top: 50%;
+      transform: translateY(-50%);
+    }
+  }
+
+  &_body {
+    box-sizing: border-box;
+    padding: 0rpx 30rpx 80rpx 30rpx;
+    background-color: #fff;
+
+    .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;
+      }
+    }
+
+    .tr:nth-child(2n) {
+      background-color: #fbfcff;
+    }
+
+    .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;
+    }
+  }
+}
+</style>

+ 39 - 9
src/pages-charge/order/order.vue

@@ -1,6 +1,16 @@
 <template>
   <view class="body" v-if="list">
-    <shadow-card :list="list"></shadow-card>
+    <shadow-card :list="list">
+      <view class="flex-column flex-center pb-48">
+        <view class="fw-500 color-000">
+          <text class="fs-40 mr-8">¥</text>
+          <text class="fs-60 lh-60">{{ price }}</text>
+        </view>
+        <view class="fs-26 mt-8" style="color: rgba(0, 0, 0, 0.4)"
+          >实付金额</view
+        >
+      </view>
+    </shadow-card>
   </view>
 </template>
 
@@ -10,14 +20,22 @@ import { onLoad } from "@dcloudio/uni-app";
 import { ref } from "vue";
 
 const list = ref<any[]>();
+const price = ref();
 onLoad((options: any) => {
   fetchOrder(options.id)
     .then((res) => {
-      const total_money = (res.totalMoney / 100).toFixed(2);
+      const totalMoney = (res.totalMoney / 100).toFixed(2);
+      const discountAmount = (res.discountAmount / 100).toFixed(2);
+      const discountAmountText = res.rightsDesc;
       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 end = res.endTime
+        ? new Date(res.endTime.replace(/-/g, "/"))
+        : new Date();
+      let diff = parseInt(`${(end.getTime() - start.getTime()) / 1000}`);
+      if (diff <= 0) {
+        diff = 60;
+      }
       const min = parseInt(`${diff / 60}`);
       const time =
         min >= 60
@@ -25,28 +43,40 @@ onLoad((options: any) => {
               `${min - parseInt(`${min / 60}`) * 60}`
             )}分钟`
           : `${parseInt(`${diff / 60}`)}分钟`;
-      list.value = [
+      const _list = [
         {
           label: "累计充电量",
           value: `${res.totalPower}度`,
         },
         {
-          label: "累计费用",
-          value: `${total_money.replace(reg, ",")}元`,
+          label: "订单金额",
+          value: `${totalMoney.replace(reg, ",")}元`,
         },
         {
           label: "开始时间",
           value: res.startTime,
+          color: "",
         },
         {
           label: "结束时间",
-          value: res.endTime,
+          value: res.chargeStatus === 2 ? "充电中" : res.endTime,
         },
         {
-          label: "累计用时",
+          label: "充电用时",
           value: time,
         },
       ];
+      if (Number(discountAmount) > 0) {
+        _list.splice(2, 0, {
+          label: "优惠金额",
+          value: `${
+            Number(discountAmount) <= 0 ? "" : "-"
+          }${discountAmount}元${discountAmountText}`,
+          color: "#F43636",
+        });
+      }
+      list.value = _list;
+      price.value = (res.payAmount / 100).toFixed(2);
     })
     .catch(() => {
       uni.showModal({

+ 30 - 17
src/pages-charge/ordering/ordering.vue

@@ -62,9 +62,13 @@
         type="warning"
         size="small"
         @click="cancel"
-        >取消充电</style-button
+        >停止充电</style-button
       >
-      <style-button v-if="step === 3" type="primary" size="small" @click="back"
+      <style-button
+        v-if="step === 3"
+        type="primary"
+        size="small"
+        @click="reLaunch"
         >返回首页</style-button
       >
     </view>
@@ -76,6 +80,7 @@ import { onHide, onLoad, onShow } from "@dcloudio/uni-app";
 import { cancelCharge, fetchChargeStatus, startCharge } from "../../api/charge";
 import { ref } from "vue";
 import { format } from "../../utils/date";
+import { to, reLaunch } from "@/utils/navigate";
 
 let timer: any;
 let statusTimer: any;
@@ -118,7 +123,7 @@ const startStatusTimer = () => {
         if (status.value.cancel) {
           return;
         }
-        if ([1, 2].includes(res.chargeStatus)) {
+        if (res.isStarted) {
           setChargeData(res);
         }
         startStatusTimer();
@@ -149,9 +154,7 @@ const start = () => {
       }, 300);
     });
 };
-const back = () => {
-  uni.navigateBack();
-};
+
 const cancel = () => {
   uni.showModal({
     title: "停止充电",
@@ -178,7 +181,7 @@ const cancel = () => {
                 // console.log(res)
                 uni.hideLoading();
                 uni.showToast({
-                  title: "已取消充电",
+                  title: "已停止充电",
                   icon: "none",
                 });
                 setTimeout(() => {
@@ -188,7 +191,7 @@ const cancel = () => {
               .catch(() => {
                 uni.hideLoading();
                 uni.showToast({
-                  title: "已取消充电",
+                  title: "已停止充电",
                   icon: "none",
                 });
                 setTimeout(() => {
@@ -207,9 +210,11 @@ const finish = () => {
   });
   fetchChargeStatus()
     .then((res) => {
-      if ([1, 2].includes(res.chargeStatus)) {
+      if (res.isStarted) {
         // 当前充电中
-        const start = new Date(res.startTime.replace(/-/g, "/"));
+        const start = res.startTime
+          ? new Date(res.startTime.replace(/-/g, "/"))
+          : new Date();
         const end = new Date();
         const diff = parseInt(`${(end.getTime() - start.getTime()) / 1000}`);
         const min = parseInt(`${diff / 60}`);
@@ -232,7 +237,7 @@ const finish = () => {
           },
           {
             label: "开始时间",
-            value: res.startTime,
+            value: res.startTime || format("y-M-d h:m:s"),
           },
           {
             label: "结束时间",
@@ -263,13 +268,15 @@ const finish = () => {
               });
               return;
             }
-            step.value = 3;
+            setTimeout(() => {
+              step.value = 3;
+            }, 3000);
           })
           .catch((err) => {
             console.log(err);
             uni.hideLoading();
             uni.showToast({
-              title: "已取消充电",
+              title: "已停止充电",
               icon: "none",
             });
             setTimeout(() => {
@@ -294,7 +301,9 @@ const finish = () => {
     });
 };
 const setChargeData = (res: any) => {
-  const start = new Date(res.startTime.replace(/-/g, "/"));
+  const start = res.startTime
+          ? new Date(res.startTime.replace(/-/g, "/"))
+          : new Date();
   const end = new Date();
   const diff = parseInt(`${(end.getTime() - start.getTime()) / 1000}`);
   const min = parseInt(`${diff / 60}`);
@@ -316,7 +325,7 @@ const setChargeData = (res: any) => {
     },
     {
       label: "开始时间",
-      value: res.startTime,
+      value: res.startTime || format("y-M-d h:m:s"),
     },
     {
       label: "累计用时",
@@ -338,7 +347,7 @@ const fetchStatus = (data?: any) => {
   const promise = data ? Promise.resolve(data) : fetchChargeStatus();
   promise
     .then((res) => {
-      if ([1, 2].includes(res.chargeStatus)) {
+      if (res.isStarted) {
         // 当前充电中
         status.value.start = true;
         step.value = 2;
@@ -356,7 +365,11 @@ const fetchStatus = (data?: any) => {
 const onImgLoad = () => {
   fetchChargeStatus()
     .then((res) => {
-      if ([1, 2].includes(res.chargeStatus)) {
+      if (res && res.isAppointment) {
+        to(`/pages-charge/appointment/appointment?sn=${options.value.sn}`);
+        return;
+      }
+      if (res && res.isStarted) {
         if (options.value && options.value.sn === res.connectorId) {
           fetchStatus(res);
           return;

+ 235 - 30
src/pages-charge/orders/orders.vue

@@ -3,28 +3,49 @@
     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>
+    <block v-for="(item, index) in infiniteScroller.list" :key="index">
+      <view
+        class="item flex-align-center"
+        @click="detail(index)"
+        v-if="!item.hidden"
+      >
+        <view class="mr-28" v-if="isInvoice">
+          <style-checkbox :checked="item.checked"></style-checkbox>
+        </view>
+        <view>
+          <view class="flex-align-center">
+            <view
+              class="fs-30 fw-500"
+              :style="{ opacity: item.invoiceStatus === 1 ? 0.5 : 1 }"
+              >充电费用</view
+            >
+            <view
+              v-if="
+                isInvoice &&
+                (item.invoiceStatus === 1 ||
+                  item.invoiceStatus === 2 ||
+                  item.invoiceStatus === 3)
+              "
+              class="flex-center ml-24 fs-24 color-000-6 width-96 height-46 br-8"
+              style="background: #e5e5e5"
+              >{{ isInvoiceTitles[item.invoiceStatus] }}</view
+            >
+          </view>
+          <view class="fs-24 mt-10" style="color: rgba(0, 0, 0, 0.4)">{{
+            item.endTime
+          }}</view>
+        </view>
+        <view class="ml-auto" style="text-align: right">
+          <view class="fs-36 fw-500">
+            <text>{{ item.payAmount }}</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 class="ml-20">
-        <uni-icons type="right" size="12" color="rgba(0,0,0,0.4)"></uni-icons>
-      </view>
-    </view>
+    </block>
   </view>
 
   <view
@@ -33,34 +54,205 @@
     v-if="infiniteScroller.list && infiniteScroller.list.length <= 0"
     >暂无数据</view
   >
+  <view class="invoice-placeholder" v-if="isInvoice"></view>
+  <view class="foot-placeholder"></view>
+
+  <style-bottom-view>
+    <view class="foot flex-align-center" v-if="!isInvoice">
+      <view
+        class="flex-center ml-auto mr-40 height-72 br-72"
+        style="width: 176rpx; border: 1rpx solid rgba(0, 0, 0, 0.2)"
+        @click="openInvoice"
+      >
+        <view class="fs-28 color-333 mr-12">开发票</view>
+        <uni-icons type="right" size="14" color="#333"></uni-icons>
+      </view>
+    </view>
+    <view class="foot flex-align-center relative" v-else>
+      <view
+        class="height-56 flex-align-center absolute-top-left pl-30"
+        style="
+          background-color: #f6f7fa;
+          width: 100%;
+          transform: translateY(-100%);
+        "
+      >
+        <view class="fs-28 color-primary mr-8">{{ checkedCount }}</view>
+        <view class="fs-28 color-000">个订单,共</view>
+        <view class="fs-28 color-primary mr-8 ml-8">{{ checkedPrice }}</view>
+        <view class="fs-28 color-000">元</view>
+      </view>
+      <view class="flex-align-center ml-30" @click="checkPage">
+        <style-checkbox :checked="checkAll"></style-checkbox>
+        <view class="ml-16 fs-28 color666">本页全选</view>
+      </view>
+      <view
+        class="ml-auto mr-40 bg-primary height-80 br-80 fs-32 fw-500 color-fff flex-center"
+        style="width: 216rpx"
+        @click="nextInvoice"
+        >下一步</view
+      >
+    </view>
+  </style-bottom-view>
 </template>
 
 <script setup lang="ts">
-import { onLoad, onPullDownRefresh, onReachBottom } from "@dcloudio/uni-app";
-import { fetchWallet } from "../../api/user";
+import {
+  onLoad,
+  onPullDownRefresh,
+  onReachBottom,
+  onShow,
+} from "@dcloudio/uni-app";
+import { fetchOrders } from "../../api/user";
+import {
+  applyInvoice,
+  cancelApplyInvoice,
+  fetchInvoiceList,
+} from "../../api/index";
 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);
+import { ref } from "vue";
+import { to } from "@/utils/navigate";
+
+const isInvoice = ref(false);
+const isInvoiceing = ref(false);
+const isInvoiceHistory = ref(false);
+const isInvoiceTitles = ref(["未开票", "已开票", "已作废", "开票中"]);
+const checkAll = ref(false);
+const checkedPrice = ref(0);
+const checkedCount = ref(0);
+
+// 0-未开票 1-已开票 2-已作废 3-开票中
+const infiniteScroller = useInfiniteScroll(20, (page) => {
+  return fetchOrders(page, 20).then((res: any) => {
+    if (res && res.list && res.list.length) {
+      res.list.forEach((item: any) => {
+        item.payAmount = (Number(item.payAmount) / 100).toFixed(2);
+        item.checked = false;
+        item.hidden = item.chargeStatus === 4 && !item.endTime;
       });
     }
-    return res;
+    return res.list;
   });
 });
+
+const setCheckData = () => {
+  let count = 0;
+  let price = 0;
+  infiniteScroller.list.forEach((item: any) => {
+    if (item.checked) {
+      count++;
+      price += Number(item.payAmount);
+    }
+  });
+  checkAll.value = infiniteScroller.list
+    .filter((item: any) => item.invoiceStatus === 0 && !item.hidden)
+    .every((item: any) => item.checked);
+  checkedCount.value = count;
+  checkedPrice.value = Number(Number(price).toFixed(2));
+};
 const detail = (index: number) => {
   if (!infiniteScroller.list) {
     return;
   }
-  uni.navigateTo({
-    url: `/pages-charge/order/order?id=${infiniteScroller.list[index].orderNo}`,
+  if (isInvoice.value) {
+    if ([0].includes(infiniteScroller.list[index].invoiceStatus)) {
+      infiniteScroller.list[index].checked =
+        !infiniteScroller.list[index].checked;
+      setCheckData();
+    }
+    if ([1].includes(infiniteScroller.list[index].invoiceStatus)) {
+      to(
+        `/pages-common/invoice/invoice?id=${infiniteScroller.list[index].startChargeSeq}`
+      );
+    }
+    return;
+  }
+  to(
+    `/pages-charge/order/order?id=${infiniteScroller.list[index].startChargeSeq}`
+  );
+};
+const openInvoice = () => {
+  isInvoice.value = true;
+};
+// 获取发票历史
+const openInvoiceHistory = () => {
+  uni.showLoading({
+    title: "请求中",
+  });
+  fetchInvoiceList().then((res) => {
+    uni.hideLoading();
+    if (res.length <= 0) {
+      uni.showToast({
+        title: "无开票历史",
+        icon: "none",
+      });
+      return;
+    }
+    isInvoiceHistory.value = true;
   });
 };
+const nextInvoice = () => {
+  uni.showLoading({
+    title: "请求中",
+  });
+  applyInvoice(
+    infiniteScroller.list
+      .filter((item: any) => item.checked)
+      .map((item: any) => item.startChargeSeq)
+  )
+    .then((res) => {
+      uni.hideLoading();
+      uni.navigateToMiniProgram({
+        appId: res.miniprogramAppid,
+        path: res.miniprogramPath,
+        success(res) {
+          console.log("打开成功");
+          isInvoiceing.value = true;
+        },
+        fail() {
+          console.log("打开失败");
+          cancelApplyInvoice();
+        },
+      });
+    })
+    .catch((err) => {
+      uni.hideLoading();
+      uni.showModal({
+        content: `${err.errMsg}`,
+      });
+    });
+};
+const checkPage = () => {
+  infiniteScroller.list = infiniteScroller.list.map((item: any) => {
+    if (item.invoiceStatus === 0 && !item.hidden) {
+      return {
+        ...item,
+        checked: checkAll.value ? false : true,
+      };
+    }
+    return {
+      ...item,
+      checked: false,
+    };
+  });
+  setCheckData();
+};
+
 onLoad(() => {
   infiniteScroller.refresh();
 });
+onShow(() => {
+  if (isInvoiceing.value) {
+    isInvoiceing.value = false;
+    isInvoice.value = false;
+    infiniteScroller.refresh();
+  }
+});
 onPullDownRefresh(() => {
+  if (isInvoice.value) {
+    uni.stopPullDownRefresh();
+    return;
+  }
   infiniteScroller.refresh();
 });
 onReachBottom(() => {
@@ -72,5 +264,18 @@ onReachBottom(() => {
 .item {
   height: 170rpx;
   border-bottom: 1rpx solid rgba(0, 0, 0, 0.1);
+  &:last-child {
+    border-bottom: none;
+  }
+}
+.invoice-placeholder {
+  height: 60rpx;
+}
+.foot-placeholder {
+  height: 112rpx;
+}
+.foot {
+  height: 112rpx;
+  border-top: 1px solid rgba(0, 0, 0, 0.1);
 }
 </style>

BIN
src/pages-charge/static/camera-back.png


BIN
src/pages-charge/static/charge-status-icon.png


BIN
src/pages-charge/static/discount-bg.png


BIN
src/pages-charge/static/discount-coupon.png


BIN
src/pages-charge/static/icon-alarm.png


BIN
src/pages-charge/static/scan-code-album.png


BIN
src/pages-charge/static/scan-code-input.png


BIN
src/pages-charge/static/scan-code-light.png


+ 70 - 0
src/pages-common/activity/activity.vue

@@ -0,0 +1,70 @@
+<template>
+  <view class="page">
+    <block v-if="activity">
+      <navigation-bar
+        :title="activity.name"
+        :color="'#fff'"
+        :placeholder="false"
+        :background="'#3F3EC2'"
+      ></navigation-bar>
+      <image
+        v-if="activity.activityDescPage"
+        style="width: 100%"
+        mode="widthFix"
+        :src="activity.activityDescPage"
+      ></image>
+    </block>
+    <view class="desc">
+      <view class="flex mb-40" v-for="(item, index) in desc" :key="index">
+        <view
+          class="width-40 height-40 br-round fs-28 color-fff flex-center flex-shrink mr-20"
+          style="
+            shadow: 0px 1px 1px rgba(0, 0, 0, 0.25));
+            background: linear-gradient(180deg, #9767f8 0%, #564eed 100%);
+          "
+          >{{ index + 1 }}</view
+        >
+        <view class="fs-28 lh-40 flex-grow" style="color: rgba(8, 0, 61, 0.8)">{{ item }}</view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { fetchProfile } from "../../api/user";
+import { onLoad } from "@dcloudio/uni-app";
+import { ref } from "vue";
+
+const desc = ref<string[]>([]);
+const activity = ref();
+
+onLoad((options: any) => {
+  uni.showLoading({
+    title: "加载中",
+  });
+  fetchProfile().then((res) => {
+    const a = res.activityList.find((item: any) => item.id === options.id);
+    desc.value = a.activityDesc.split("\n");
+    activity.value = a;
+    uni.hideLoading();
+  });
+});
+</script>
+
+<style lang="scss">
+page {
+  background-color: #716ffc;
+}
+.page {
+  position: relative;
+  .desc {
+    position: absolute;
+    width: 100%;
+    left: 0;
+    top: 650rpx;
+    box-sizing: border-box;
+    padding: 0 68rpx;
+    padding-top: 100rpx;
+  }
+}
+</style>

+ 148 - 0
src/pages-common/contact/contact.vue

@@ -0,0 +1,148 @@
+<template>
+  <view class="page">
+    <view v-if="questions.length <= 0" class="flex-center mt-40 animation-loading"
+      ><uni-icons
+        type="spinner-cycle"
+        size="20"
+        color="rgba(0, 0, 0, 0.5)"
+      ></uni-icons
+    ></view>
+    <view class="sheet">
+      <view class="sheet_bar" v-for="(item, index) in questions" :key="index">
+        <view class="head" @click="toggle(index)">
+          <view class="pt-32 pb-32 flex-align-center">
+            <view class="fs-28 fw-500 color-000">{{ item.question }}</view>
+            <view
+              class="width-40 ml-auto flex-end flex-shrink transition"
+              :style="{
+                transform: item.open ? 'rotate(180deg)' : 'rotate(0deg)',
+              }"
+            >
+              <uni-icons
+                type="bottom"
+                size="18"
+                color="rgba(0,0,0,0.4)"
+              ></uni-icons>
+            </view>
+          </view>
+        </view>
+        <view :class="['body', item.open ? 'body-open' : 'body-hidden']">
+          <view
+            v-for="(answerItem, answerIndex) in item.answer"
+            :key="answerIndex"
+            :class="[
+              'fs-28',
+              'lh-48',
+              'color-666',
+              answerIndex === 0 ? 'mt-0' : 'mt-10',
+            ]"
+            >{{ answerItem }}</view
+          >
+        </view>
+      </view>
+    </view>
+    <view
+      class="flex-center flex-column contact mt-20"
+      hover-class="hover-scale"
+      @click="call"
+      v-if="questions.length"
+    >
+      <image
+        class="width-96"
+        mode="widthFix"
+        src="/static/images/contact-servicer.png"
+      />
+      <view class="mt-16 color-666 fs-28">电话客服</view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { fetchCommonQuestions } from "@/api";
+import { onLoad } from "@dcloudio/uni-app";
+import { ref } from "vue";
+
+const questions = ref<any[]>([]);
+const servicerPhone = ref("");
+
+const call = () => {
+  uni.makePhoneCall({
+    phoneNumber: servicerPhone.value,
+  });
+};
+
+const toggle = (index: number) => {
+  questions.value = questions.value.map((item, i) => {
+    return {
+      ...item,
+      open: item.open ? false : i === index,
+    };
+  });
+};
+
+onLoad((options) => {
+  if (options) {
+    servicerPhone.value = options.service;
+  }
+  fetchCommonQuestions().then((res) => {
+    questions.value = res.map((item: any) => {
+      return {
+        ...item,
+        answer: item.answer.split("\\r\r\n"),
+        open: false,
+      };
+    });
+  });
+});
+</script>
+
+<style lang="scss">
+.page {
+  min-height: 100vh;
+  background: #f6f7fa;
+  box-sizing: border-box;
+  padding: 40rpx 32rpx;
+}
+.contact {
+  height: 216rpx;
+  border-radius: 24rpx;
+  background: #fff;
+}
+.sheet {
+  box-sizing: border-box;
+  border-radius: 24rpx;
+  background: #fff;
+  overflow: hidden;
+  &_bar {
+    .head {
+      padding: 0rpx 24rpx;
+      box-sizing: border-box;
+      & > view {
+        border-bottom: 1rpx solid rgba(0, 0, 0, 0.1);
+      }
+    }
+    .body {
+      box-sizing: border-box;
+      background-color: rgba(52, 125, 255, 0.06);
+      transition: all 0.3s;
+      padding: 0rpx 24rpx;
+    }
+    .body-hidden {
+      height: 0px;
+      overflow: hidden;
+    }
+    .body-open {
+      height: auto;
+      padding-top: 24rpx;
+      padding-bottom: 24rpx;
+    }
+    &:last-child {
+      .head {
+        & > view {
+          border-bottom: none;
+        }
+      }
+    }
+  }
+}
+</style>

+ 177 - 0
src/pages-common/invoice/invoice.vue

@@ -0,0 +1,177 @@
+<template>
+  <view class="page">
+    <view class="block">
+      <view
+        class="flex-align-center"
+        :style="{
+          marginTop: index === 0 ? 0 : '40rpx',
+        }"
+        v-for="(item, index) in list"
+        :key="index"
+      >
+        <view class="fs-32 lh-48 color-999" style="width: 192rpx">{{
+          item.label
+        }}</view>
+        <view class="fs-28 lh-48 color-000" v-if="index === 7">
+          <text style="color: rgba(255, 153, 0, 1)">{{ item.value }}</text>
+          <text style="margin-left: 2px">元</text>
+        </view>
+        <view class="fs-28 lh-48 color-000" v-else>{{ item.value }}</view>
+      </view>
+    </view>
+    <view class="block pt-40 pb-40 pl-30 pr-30 flex mt-30" v-if="email">
+      <view class="fs-32 lh-48 color-999" style="width: 192rpx">电子邮箱</view>
+      <view class="fs-28 lh-48 color-000">{{ email }}</view>
+    </view>
+    <view class="block pt-40 pb-40 pl-30 pr-30 flex mt-30">
+      <view class="fs-32 lh-48 color-999" style="width: 192rpx">发票</view>
+      <view class="ml-auto flex-align-center" @click="openInvoice">
+        <view class="fs-28 color-primary mr-4">立即查看</view>
+        <uni-icons
+          type="right"
+          size="14"
+          color="var(--color-primary)"
+        ></uni-icons>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { fetchInvoiceList, fetchInvoicePDF } from "@/api";
+import { onLoad } from "@dcloudio/uni-app";
+import { ref } from "vue";
+
+const openInvoiceId = ref();
+const email = ref();
+const list = ref([
+  {
+    label: "公司名称",
+    value: "",
+  },
+  {
+    label: "公司税号",
+    value: "",
+  },
+  {
+    label: "注册地址",
+    value: "",
+  },
+  {
+    label: "注册电话",
+    value: "",
+  },
+  {
+    label: "开户银行",
+    value: "",
+  },
+  {
+    label: "银行账号",
+    value: "",
+  },
+  {
+    label: "备注",
+    value: "",
+  },
+  {
+    label: "发票金额",
+    value: "",
+  },
+  {
+    label: "申请时间",
+    value: "",
+  },
+]);
+
+onLoad((options: any) => {
+  uni.showLoading({
+    title: "加载中",
+  });
+  fetchInvoiceList()
+    .then((res) => {
+      // console.log(res);
+      uni.hideLoading();
+      const fd = res.findIndex((item: any) => {
+        if (item.orderDetails && item.orderDetails.length) {
+          return item.orderDetails.some(
+            (order: any) => order.startChargeSeq === options.id
+          );
+        }
+        return false;
+      });
+      if (fd >= 0) {
+        list.value[0].value = res[fd].invoiceTitle;
+        list.value[1].value = res[fd].taxId;
+        list.value[2].value = res[fd].address || "-";
+        list.value[3].value = res[fd].phone || "-";
+        list.value[4].value = res[fd].bankName || "-";
+        list.value[5].value = res[fd].bankAccount || "-";
+        list.value[6].value = res[fd].remark || "-";
+        list.value[7].value = `${(Number(res[fd].invoiceAmount) / 100).toFixed(
+          2
+        )}`;
+        list.value[8].value = res[fd].createTime;
+        openInvoiceId.value = res[fd].id;
+        email.value = res[fd].email || "";
+        return;
+      }
+      return Promise.reject({
+        errMsg: "找不到发票数据",
+      });
+    })
+    .catch((err) => {
+      uni.hideLoading();
+      uni.showModal({
+        content: `${err.errMsg},请重试`,
+      });
+    });
+});
+
+const openInvoice = () => {
+  uni.showLoading({
+    title: "加载中",
+  });
+  fetchInvoicePDF(openInvoiceId.value).then((res) => {
+    uni.downloadFile({
+      url: res.downloadUrl,
+      success: function (res) {
+        const filePath = res.tempFilePath;
+        uni.openDocument({
+          filePath,
+          fileType: "pdf",
+          showMenu: true,
+          success: function (res) {
+            uni.hideLoading();
+            console.log("打开文档成功");
+          },
+          fail(err) {
+            uni.hideLoading();
+            uni.showModal({
+              content: `${err.errMsg},请重试`,
+            });
+          },
+        });
+      },
+      fail(err) {
+        uni.hideLoading();
+        uni.showModal({
+          content: `${err.errMsg},请重试`,
+        });
+      },
+    });
+  });
+};
+</script>
+
+<style lang="scss">
+.page {
+  min-height: 100vh;
+  background-color: #f6f7fa;
+  padding: 20rpx;
+}
+.block {
+  border-radius: 20rpx;
+  background: #fff;
+  padding: 30rpx;
+}
+</style>

+ 43 - 30
src/pages-user/profile/profile.vue

@@ -49,35 +49,36 @@ import { onLoad, onShow } from "@dcloudio/uni-app";
 import { ref } from "vue";
 const avatar = ref<string>();
 const menu = ref<any[]>([]);
+const MENU_TEMPLATE = [
+  {
+    title: "昵称",
+    key: "nickname",
+    value: "",
+  },
+  {
+    title: "电话",
+    key: "",
+    disabled: true,
+  },
+  {
+    title: "车牌号",
+    key: "defaultPlateNo",
+    value: "",
+  },
+  {
+    title: "VIN码",
+    key: "vin",
+    value: "",
+  },
+  // {
+  //   title: "充电卡",
+  //   key: "",
+  //   value: "",
+  // },
+];
 
 const refresh = () => {
-  const _menu = [
-    {
-      title: "昵称",
-      key: "nickname",
-      value: "",
-    },
-    {
-      title: "电话",
-      key: "",
-      disabled: true,
-    },
-    {
-      title: "车牌号",
-      key: "defaultPlateNo",
-      value: "",
-    },
-    {
-      title: "VIN码",
-      key: "vin",
-      value: "",
-    },
-    // {
-    //   title: "充电卡",
-    //   key: "",
-    //   value: "",
-    // },
-  ];
+  const _menu = [...MENU_TEMPLATE];
   fetchProfile().then(() => {
     const user = getApp<any>().globalData.user;
     if (user) {
@@ -129,8 +130,8 @@ const chooseAvatar = (e: any) => {
           .then(() => {
             uni.hideLoading();
             uni.showToast({
-              title: '已更新',
-              icon: 'success'
+              title: "已更新",
+              icon: "success",
             });
             avatar.value = res.url;
           })
@@ -228,7 +229,19 @@ const errorHandle = (e: any) => {
 };
 
 onLoad(() => {
-  refresh();
+  if (getApp<any>().globalData.user) {
+    const user = getApp<any>().globalData.user;
+    const _menu = [...MENU_TEMPLATE];
+    _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;
+  }
 });
 onShow(() => {
   if (getApp<any>().globalData.lastData.profile) {

BIN
src/pages-user/static/icon-submit-success.png


BIN
src/pages-user/static/wallet-banner.png


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

@@ -1,142 +0,0 @@
-<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>

+ 194 - 0
src/pages-user/wallet-recharge/wallet-recharge.vue

@@ -0,0 +1,194 @@
+<template>
+  <view class="pl-30 pr-30">
+    <view class="pay">
+      <view class="title pt-30 pb-30">充值金额</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)"
+        >
+          <view
+            class="tag fs-24 color-fff fw-500"
+            v-if="payOptionsDiscount[index]"
+            >{{ payOptionsDiscount[index] }}</view
+          >
+          {{ item }}
+        </view>
+      </view>
+      <view v-if="payOptionsDiscountDay[payOption]" class="flex-align-center">
+        <view class="fs-30 color-666"
+          >折扣优惠有效期{{ payOptionsDiscountDay[payOption] }}天</view
+        >
+        <view
+          class="ml-12 color-primary fs-30"
+          @click="to(`/pages-common/activity/activity?id=${activityId}`)"
+        >
+          <text>详细规则</text>
+          <text class="fs-24 ml-6">>></text>
+        </view>
+      </view>
+      <view class="title pt-60 pb-30">自定义金额</view>
+      <style-input
+        :value="payValue > 0 ? payValue : ''"
+        title="金额"
+        type="digit"
+        @input="input"
+      />
+    </view>
+  </view>
+
+  <style-bottom-view>
+    <view class="pl-60 pr-60 pb-20">
+      <style-button size="small" 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";
+import { to, back } from "@/utils/navigate";
+const balance = ref(0);
+const payOption = ref(3);
+const payOptions = ref([30, 50, 100, 200, 500, 1000]);
+const payOptionsDiscount = ref(["", "", "", "", "", ""]);
+const payOptionsDiscountDay = ref([0, 0, 0, 0, 0, 0]);
+const payValue = ref(0);
+const activityId = ref();
+const needBack = ref(false);
+const input = (e: any) => {
+  payValue.value = e.value;
+};
+const changeOption = (index: number) => {
+  payValue.value = 0;
+  payOption.value = index;
+};
+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;
+  }
+  uni.showLoading({
+    title: "加载中",
+  });
+  insertMoney(params)
+    .then(() => {
+      return fetchProfile();
+    })
+    .then((res) => {
+      payValue.value = 0;
+      balance.value = res.balance;
+      uni.hideLoading();
+      uni.showToast({
+        title: "已支付",
+        icon: "success",
+      });
+      setTimeout(() => {
+        if (needBack.value) {
+          back();
+        } else {
+          to("/pages-user/wallet/wallet");
+        }
+      }, 2000);
+    })
+    .catch((err) => {
+      if (/cancel/.test(err.errMsg)) {
+        return;
+      }
+      uni.showModal({
+        content: `${err.errMsg},请重试`,
+      });
+    });
+};
+onLoad((options: any) => {
+  if (options.value) {
+    payOption.value = payOptions.value.findIndex(
+      (item) => item === Number((Number(options.value) / 100).toFixed(2))
+    );
+    needBack.value = !!options.back;
+  }
+  fetchProfile().then((res) => {
+    // console.log(res);
+    balance.value = res.balance;
+    if (res && res.activityList && res.activityList.length) {
+      res.activityList[0].rechargeRightsList.forEach((item: any) => {
+        const val = Number((Number(item.amountMin) / 100).toFixed(2));
+        const fi = payOptions.value.findIndex((o) => o === val);
+        if (fi >= 0) {
+          payOptionsDiscount.value[fi] = item.rightsDesc;
+          payOptionsDiscountDay.value[fi] =
+            item.validity > 0 ? item.validity : 1;
+        }
+      });
+      activityId.value = res.activityList[0].id;
+    }
+  });
+});
+</script>
+
+<style lang="scss">
+.pay {
+  .title {
+    font-weight: 500;
+    font-size: 32rpx;
+    color: #000;
+  }
+
+  .option {
+    position: relative;
+    width: 214rpx;
+    height: 140rpx;
+    background: var(--color-sec);
+    border-radius: 10rpx;
+    margin-left: 20rpx;
+    margin-bottom: 20rpx;
+    font-size: 36rpx;
+    color: #000;
+    font-weight: 500;
+    overflow: hidden;
+    &:nth-child(3n + 1) {
+      margin-left: 0;
+    }
+    .tag {
+      position: absolute;
+      top: 0px;
+      right: 0px;
+      border-radius: 30rpx 0px 0px 30rpx;
+      box-sizing: content-box;
+      padding: 2rpx 10rpx;
+      background: linear-gradient(90deg, #f366ff 0%, #5e98ff 100%);
+      white-space: nowrap;
+    }
+  }
+  .option-active {
+    background-color: var(--color-primary);
+    color: #fff;
+  }
+}
+</style>

+ 180 - 0
src/pages-user/wallet-refund/wallet-refund.vue

@@ -0,0 +1,180 @@
+<template>
+  <view class="page">
+    <navigation-bar title="退款"></navigation-bar>
+    <view class="block">
+      <view class="fs-28 color-000 fw-500">退款金额</view>
+      <view
+        class="pt-24 pb-24 box-content flex-inline"
+        style="border-bottom: 1rpx solid rgba(0, 0, 0, 0.1)"
+      >
+        <text style="font-size: 32px; line-height: 32px">¥</text>
+        <input
+          style="height: 46px; font-size: 46px; line-height: 46px"
+          class="fw-600 color-000 ml-18"
+          :value="value"
+          :disabled="true"
+        />
+      </view>
+      <view class="height-94 flex-align-center">
+        <view class="fs-28 color-999"
+          >可退金额 {{ user ? user.refundableAmount : 0 }} 元,优惠金额
+          {{ user ? user.discountAmount : 0 }} 元</view
+        >
+        <!-- <view class="fs-28 color-primary" @click="allRefund">全部提现</view> -->
+      </view>
+    </view>
+    <view class="block mt-16">
+      <view class="fs-28 color-000 fw-500 pb-8">退款原因</view>
+      <view class="pb-40 flex-wrap">
+        <view
+          :class="[
+            'reason',
+            'mt-16',
+            'mr-20',
+            'fs-26',
+            reason === i ? 'reason-active' : '',
+          ]"
+          v-for="(r, i) in reasons"
+          :key="i"
+          @click="changeReason(i)"
+          >{{ r }}</view
+        >
+      </view>
+    </view>
+    <view class="block mt-16 relative">
+      <view class="fs-28 color-000 fw-500 pb-16">退款说明</view>
+      <view class="pb-40 reason-text">
+        <textarea
+          class="fs-28"
+          style="height: 100%"
+          placeholder="请您详细填写申请说明"
+          maxlength="200"
+          @input="inputReasonText"
+        ></textarea>
+      </view>
+      <view class="reason-text-length lh-28 fs-28 color-999"
+        >{{ reasonText.length }}/200</view
+      >
+    </view>
+    <style-bottom-view>
+      <view class="pl-40 pr-40 pb-30 pt-30" style="background-color: #fff">
+        <style-button type="primary" @click="submit">提交申请</style-button>
+      </view>
+    </style-bottom-view>
+    <style-result
+      v-if="success"
+      icon="/pages-user/static/icon-submit-success.png"
+      title="提交成功"
+      tip="预计需要5个工作日内审核完成请及时查收"
+    >
+      <view class="pt-64" style="width: 280rpx">
+        <style-button height="80" type="primary" @click="close"
+          >完成</style-button
+        >
+      </view>
+    </style-result>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { applyRefund } from "@/api/user";
+import { onLoad } from "@dcloudio/uni-app";
+import { ref } from "vue";
+
+const user = ref();
+const value = ref(0);
+const reasonText = ref("");
+const reason = ref(-1);
+const reasons = ref([
+  "不常住该小区",
+  "充电故障",
+  "下次使用再充值",
+  "充电费用贵",
+  "很难充上电",
+  "其他原因",
+]);
+const success = ref(false);
+onLoad(() => {
+  if (getApp<any>().globalData.user) {
+    user.value = getApp<any>().globalData.user;
+    allRefund();
+  }
+});
+
+const allRefund = () => {
+  if (user.value) {
+    value.value = Number(user.value.balance);
+  }
+};
+const changeReason = (i: number) => {
+  reason.value = i;
+};
+const inputReasonText = (e: any) => {
+  reasonText.value = e.detail.value;
+};
+const submit = () => {
+  uni.showLoading({
+    title: "提交中",
+  });
+  let r = "";
+  if (reason.value >= 0) {
+    r += `${reasons.value[reason.value]}`;
+    if (reasonText.value) {
+      r += "|";
+    }
+  }
+  if (reasonText.value) {
+    r += reasonText.value;
+  }
+  applyRefund(r)
+    .then(() => {
+      uni.hideLoading();
+      success.value = true;
+    })
+    .catch((err) => {
+      console.log(err);
+      uni.hideLoading();
+      uni.showModal({
+        content: err.errMsg,
+        showCancel: false
+      })
+    });
+};
+const close = () => {
+  success.value = false;
+  uni.navigateBack();
+};
+</script>
+
+<style lang="scss">
+.page {
+  height: 100vh;
+  background-color: #f6f7fa;
+  padding: 20rpx;
+  .block {
+    border-radius: 24rpx;
+    background: #fff;
+    padding: 40rpx 40rpx 0px 40rpx;
+  }
+  .reason {
+    color: #666;
+    background-color: #f7f7f7;
+    border-radius: 4rpx;
+    padding: 12rpx 16rpx;
+    border: 1px solid #f7f7f7;
+  }
+  .reason-active {
+    color: var(--color-primary);
+    border: 1px solid var(--color-primary);
+    background: rgba(52, 125, 255, 0.1);
+  }
+  .reason-text {
+    height: 224rpx;
+  }
+  .reason-text-length {
+    position: absolute;
+    top: 40rpx;
+    right: 40rpx;
+  }
+}
+</style>

+ 217 - 161
src/pages-user/wallet/wallet.vue

@@ -1,191 +1,247 @@
 <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 class="page flex-column">
+    <navigation-bar
+      autoFixedStyle="background-color:#fff;"
+      title="我的钱包"
+      @ready="ready"
+    ></navigation-bar>
+    <view class="head flex-shrink">
+      <view>
+        <image class="bg" src="/pages-user/static/wallet-banner.png"></image>
+        <view class="fg pt-48 pl-48 pr-48">
+          <view class="flex-align-center">
+            <uni-icons
+              type="wallet-filled"
+              size="24"
+              color="#ffffff"
+            ></uni-icons>
+            <view class="fs-36 color-fff fw-500 ml-16">我的钱包</view>
+            <view
+              class="btn flex-center fs-28 ml-auto"
+              @click="to(`/pages-user/wallet-refund/wallet-refund`)"
+              >退款</view
+            >
+          </view>
+          <view class="price mt-50" v-if="user">
+            <text class="color-fff fw-600 fs-48" style="vertical-align: top"
+              >¥</text
+            >
+            <text
+              class="color-fff fw-600 ml-12"
+              style="font-size: 46px; line-height: 46px"
+              >{{ user.balance }}</text
+            >
+          </view>
         </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="body flex-grow flex-column">
+      <view class="tabs flex-shrink flex-align-center">
         <view
-          :class="[
-            'option',
-            'flex-center',
-            `option-${index === payOption && !payValue ? 'active' : ''}`,
-          ]"
-          v-for="(item, index) in payOptions"
+          v-for="(item, index) in tabs"
           :key="index"
-          @click="changeOption(index)"
+          class="flex-align-center fs-30 mr-60"
+          :style="{
+            color:
+              tab === item.value ? 'rgba(0, 0, 0, 1);' : 'rgba(0, 0, 0, 0.6);',
+          }"
+          @click="changeTab(index)"
+          >{{ item.label
+          }}<view
+            :style="{ opacity: tab === item.value ? '1' : '0' }"
+            class="active"
+          ></view
+        ></view>
+      </view>
+      <view class="list pl-30 pr-30">
+        <view
+          class="item flex-align-center"
+          v-for="(item, index) in infiniteScroller.list"
+          :key="index"
+          @click="detail(index)"
         >
-          {{ item }}
+          <view>
+            <view class="fs-30 fw-500" key="title" duration="300">{{
+              typeMap[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="title">自定义金额</view>
-      <style-input
-        :value="payValue > 0 ? payValue : ''"
-        title="金额"
-        type="digit"
-        @input="input"
-      />
+      <view style="height: 170rpx"></view>
     </view>
+    <style-bottom-view background="#ffffff">
+      <view class="pl-40 pr-40 pb-30 pt-30">
+        <style-button
+          type="primary"
+          @click="to(`/pages-user/wallet-recharge/wallet-recharge`)"
+          >充值</style-button
+        >
+      </view>
+    </style-bottom-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 { useInfiniteScroll } from "../../utils/infinite-scroll";
+import { onReachBottom, onPullDownRefresh, onShow } from "@dcloudio/uni-app";
+import { fetchWallet } from "../../api/user";
+import { to } from "../../utils/navigate";
 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},请重试`,
+import { rpxToPx } from "@/utils/device";
+
+const user = ref();
+
+const tab = ref(0);
+const tabs = ref([
+  {
+    label: "全部",
+    value: 0,
+  },
+  {
+    label: "充值",
+    value: 1,
+  },
+  {
+    label: "消费",
+    value: 3,
+  },
+  {
+    label: "退款",
+    value: 2,
+  },
+]);
+
+const typeMap = ref(["充值", "提现", "消费"]);
+const infiniteScroller = useInfiniteScroll(10, (page) => {
+  return fetchWallet(tab.value, page, 10).then((res: any) => {
+    if (res && res.length) {
+      res.forEach((item: any) => {
+        item.amount = (Number(item.amount) / 100).toFixed(2);
       });
-    });
-};
-onLoad(() => {
-  const user = getApp<any>().globalData.user;
-  if (user) {
-    balance.value = user.balance;
-  }
+    }
+    return res;
+  });
 });
-</script>
+const scrollViewHeight = ref(0);
 
-<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;
+onShow(() => {
+  infiniteScroller.refresh();
+  if (getApp<any>().globalData.user) {
+    user.value = getApp<any>().globalData.user;
   }
+})
 
-  .label {
-    font-size: 26rpx;
-    color: rgba(255, 255, 255, 0.8);
-  }
+onReachBottom(() => {
+  infiniteScroller.next();
+});
 
-  .value {
-    color: #fff;
-  }
+onPullDownRefresh(() => {
+  infiniteScroller.refresh();
+});
 
-  .tag {
-    position: absolute;
-    width: 170rpx;
-    height: 58rpx;
-    right: 0;
-    bottom: 0;
-    background: var(--color-primary);
-    border-radius: 29rpx 0 29rpx 0;
-  }
-}
+const ready = (e: any) => {
+  scrollViewHeight.value =
+    getApp<any>().globalData.device.windowHeight -
+    (e.detail.navigationBarHeight + rpxToPx(380));
+};
 
-.pay {
-  padding-top: 20rpx;
-  .title {
-    font-weight: 500;
-    font-size: 32rpx;
-    color: #000;
-    padding-bottom: 30rpx;
-    padding-top: 40rpx;
+const changeTab = (index: number) => {
+  tab.value = tabs.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}`,
+    });
   }
+};
+</script>
 
-  .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;
+<style lang="scss">
+.page {
+  height: 100vh;
+  width: 100vw;
+  background: linear-gradient(
+    180deg,
+    #e0ebff 0%,
+    rgba(255, 255, 255, 0) 60.13%
+  );
+  .head {
+    padding: 30rpx 40rpx;
+    & > view {
+      position: relative;
+      height: 320rpx;
+      border-radius: 40rpx;
+      overflow: hidden;
+      .bg {
+        height: 100%;
+        width: 100%;
+      }
+      .fg {
+        position: absolute;
+        left: 0px;
+        top: 0px;
+        height: 100%;
+        width: 100%;
+        .btn {
+          width: 120rpx;
+          height: 64rpx;
+          border-radius: 40rpx;
+          background: rgba(255, 255, 255, 0.3);
+          color: #076370;
+        }
+      }
     }
   }
-  .option-active {
-    background-color: var(--color-primary);
-    color: #fff;
+  .body {
+    .tabs {
+      padding: 0 40rpx;
+      & > view {
+        position: relative;
+        height: 72rpx;
+        .active {
+          position: absolute;
+          left: 50%;
+          bottom: 0px;
+          transform: translateX(-50%);
+          width: 40rpx;
+          height: 4rpx;
+          border-radius: 4rpx;
+          background-color: var(--color-primary);
+        }
+      }
+    }
+    .list {
+      .item {
+        height: 170rpx;
+        border-bottom: 1rpx solid rgba(0, 0, 0, 0.1);
+        &:last-child {
+          border-bottom: none;
+        }
+      }
+    }
   }
 }
 </style>

+ 68 - 11
src/pages.json

@@ -1,9 +1,6 @@
 {
   "entryPagePath": "pages/map/map",
   "pages": [
-    {
-      "path": "pages/index/index"
-    },
     {
       "path": "pages/map/map",
       "style": {
@@ -15,6 +12,9 @@
     },
     {
       "path": "pages/user/user"
+    },
+    {
+      "path": "pages/index/index"
     }
   ],
   "subPackages": [
@@ -26,7 +26,7 @@
           "style": {
             "navigationBarTitleText": "",
             "navigationStyle": "default",
-            "navigationBarBackgroundColor": "#F5F5F5"
+            "navigationBarBackgroundColor": "#ffffff"
           }
         },
         {
@@ -61,6 +61,29 @@
         },
         {
           "path": "search/search"
+        },
+        {
+          "path": "appointment/appointment",
+          "style": {
+            "navigationBarTitleText": "预约充电",
+            "navigationStyle": "default",
+            "navigationBarBackgroundColor": "#F6F7FA"
+          }
+        },
+        {
+          "path": "discount/discount",
+          "style": {
+            "navigationBarTitleText": "选择充电优惠",
+            "navigationStyle": "default",
+            "navigationBarBackgroundColor": "#F6F7FA"
+          }
+        },
+        {
+          "path": "camera/camera",
+          "style": {
+            "navigationBarTextStyle": "white",
+            "disableScroll": true
+          }
         }
       ]
     },
@@ -92,21 +115,58 @@
         {
           "path": "wallet/wallet",
           "style": {
-            "navigationStyle": "default",
-            "navigationBarTitleText": "我的钱包"
+            "enablePullDownRefresh": true
           }
         },
         {
-          "path": "wallet-detail/wallet-detail",
+          "path": "wallet-refund/wallet-refund",
+          "style": {
+            "disableScroll": true
+          }
+        },
+        {
+          "path": "wallet-recharge/wallet-recharge",
           "style": {
             "navigationStyle": "default",
-            "navigationBarTitleText": "钱包明细",
+            "navigationBarTitleText": "我的钱包",
             "enablePullDownRefresh": true
           }
         }
       ]
+    },
+    {
+      "root": "pages-common",
+      "pages": [
+        {
+          "path": "contact/contact",
+          "style": {
+            "navigationStyle": "default",
+            "navigationBarTitleText": "联系我们",
+            "navigationBarBackgroundColor": "#ffffff"
+          }
+        },
+        {
+          "path": "invoice/invoice",
+          "style": {
+            "navigationStyle": "default",
+            "navigationBarTitleText": "发票详情",
+            "navigationBarBackgroundColor": "#F6F7FA"
+          }
+        },
+        {
+          "path": "activity/activity",
+          "style": {
+          }
+        }
+      ]
     }
   ],
+  "preloadRule": {
+    "pages/map/map": {
+      "network": "all",
+      "packages": ["pages-charge"]
+    }
+  },
   "globalStyle": {
     "navigationBarTextStyle": "black",
     "navigationBarTitleText": "uni-app",
@@ -126,9 +186,6 @@
       {
         "pagePath": "pages/map/map"
       },
-      {
-        "pagePath": "pages/list/list"
-      },
       {
         "pagePath": "pages/user/user"
       }

+ 16 - 1
src/pages/index/index.vue

@@ -1,10 +1,25 @@
 <template>
-  <view></view>
+  <view>
+    <map
+      :style="{
+        width: '100%',
+        height: '100vh',
+      }"
+      :latitude="23.098994"
+      :longitude="113.32252"
+      min-scale="1"
+      :scale="12"
+    ></map>
+  </view>
 </template>
 
 <script setup lang="ts">
+import { isDebug } from "@/utils/constant";
 import { onLoad } from "@dcloudio/uni-app";
 onLoad(() => {
+  if (isDebug) {
+    return;
+  }
   uni.switchTab({
     url: "/pages/map/map",
   });

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

@@ -36,6 +36,7 @@
 </template>
 
 <script setup lang="ts">
+// 废弃页面
 import { fetchStations } from "../../api/charge";
 import { fetchCollectList } from "../../api/user";
 import { useInfiniteScroll } from "../../utils/infinite-scroll";

+ 477 - 224
src/pages/map/map.vue

@@ -1,8 +1,125 @@
 <template>
   <view class="container">
+    <navigation-bar @ready="handleNavReady"></navigation-bar>
+    <view class="dialog" v-if="styleData.dialog" :style="styleData.dialog">
+      <view class="height-88 flex-align-center bg-fff">
+        <view class="dialog_logo ml-24"></view>
+        <view class="dialog_search ml-24" @click.stop="toSearch"></view>
+      </view>
+      <!-- <view class="height-72 flex" style="background-color: #f9f9f9">
+        <view class="width-half flex-center" @click.stop="checkFilterDistance">
+          <view class="fs-26 color-000-6 mr-8">距离</view>
+          <view
+            class="width-20 height-20 br-round bg-000-08 lh-10 text-center transition"
+            :style="{
+              transform: filterDialog.distanceSelector ? 'rotate(180deg)' : '',
+            }"
+          >
+            <uni-icons
+              type="bottom"
+              size="8"
+              color="rgba(0,0,0,0.4)"
+            ></uni-icons>
+          </view>
+        </view>
+        <view class="width-half flex-center">
+          <view
+            class="width-32 height-32 br-round lh-20 text-center"
+            :style="{
+              border: filterDialog.discounts
+                ? '1px solid var(--color-primary)'
+                : '1px solid rgba(0, 0, 0, 0.4)',
+              backgroundColor: filterDialog.discounts
+                ? 'var(--color-primary)'
+                : '#fff',
+            }"
+          >
+            <uni-icons
+              type="checkmarkempty"
+              size="10"
+              color="#ffffff"
+            ></uni-icons>
+          </view>
+          <view
+            :class="[
+              'fs-26',
+              'ml-8',
+              filterDialog.discounts ? 'color-primary' : 'color-000-6',
+            ]"
+            >支持省钱充电</view
+          >
+        </view>
+      </view> -->
+      <view
+        class="dialog_selector"
+        :style="{
+          height: filterDialog.distanceSelector ? '190rpx' : '0rpx',
+          borderTop: `1rpx solid rgba(0, 0, 0, ${
+            filterDialog.distanceSelector ? '0.1' : '0'
+          })`,
+        }"
+      >
+        <view class="flex-center 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.distanceRange"
+            :key="index"
+            @click.stop="changeFilterDistance(index)"
+            style="width: 117rpx"
+            >{{ item.value }}km</view
+          >
+        </view>
+      </view>
+      <view class="dialog_event" v-if="ready && mapBanner.length > 0">
+        <swiper
+          class="swiper"
+          circular
+          :indicator-dots="true"
+          :autoplay="mapBanner.length > 1"
+          :interval="3000"
+        >
+          <swiper-item
+            class="swiper-item full-percent"
+            v-for="(item, index) in mapBanner"
+            :key="index"
+            @click="to(item.linkUrl)"
+          >
+            <view
+              class="full-percent"
+              :style="{
+                backgroundImage: `url(${item.bannerUrl})`,
+              }"
+            ></view>
+          </swiper-item>
+        </swiper>
+      </view>
+    </view>
+    <view
+      v-if="styleData.dialogPlaceHolderHeight"
+      :style="{ height: `${styleData.dialogPlaceHolderHeight}px` }"
+    ></view>
     <map
+      v-if="ready"
       id="map"
-      style="width: 100%; height: 100%; z-index: 1"
+      :style="{
+        width: '100%',
+        height: styleData.dialogHeight
+          ? `calc(100vh - ${styleData.dialogHeight}px - ${
+              styleData.cardHeight - 24
+            }px)`
+          : '50vh',
+        zIndex: 1,
+        pointerEvents: loading || !mapMode ? 'none' : 'auto',
+      }"
       :latitude="mapProps.latitude"
       :longitude="mapProps.longitude"
       :markers="markers"
@@ -14,118 +131,128 @@
       @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>
+    <block v-else>
       <view
-        class="flex-center mt-40"
-        @click="toggleDialogVisible"
-        hover-class="hover"
+        v-if="styleData.dialogHeight"
+        class="mock-map flex-center"
+        :style="{
+          height: `calc(100vh - ${styleData.dialogHeight}px - ${
+            styleData.cardHeight - 24
+          }px)`,
+        }"
       >
-        <image src="/static/images/map-filter.png" mode="widthFix" />
+        <image
+          src="/static/images/map-bg.jpg"
+          mode="widthFix"
+          style="width: 100%"
+        ></image>
+        <image
+          src="/static/images/map-current.png"
+          mode="widthFix"
+          class="absolute-center"
+          style="width: 34px"
+        ></image>
       </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>
+    </block>
     <view
-      class="dialog"
-      v-if="filterDialog.visible"
-      @click="toggleDialogVisible"
+      class="card"
+      :style="{
+        height: mapMode
+          ? `${styleData.cardHeight}px`
+          : `calc(100vh - ${styleData.navHeight}px)`,
+        bottom: '0px',
+        borderRadius: mapMode ? '16rpx 16rpx 0px 0px' : '0px',
+        overflowY: mapMode ? 'visible' : 'auto',
+      }"
+      @touchstart.stop="touchCardStart"
+      @touchmove.stop="touchCardMove"
     >
-      <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
+        v-if="mapMode"
+        class="card_location height-64 width-64 bg-fff flex-center"
+        @touchstart.stop="resetLocation"
+      >
+        <image
+          src="/static/images/map-re-location.png"
+          mode="widthFix"
+          class="width-48"
+        ></image>
+      </view>
+      <block v-if="station.length">
+        <!-- 列表 -->
+        <block v-if="!mapMode">
+          <view class="station" v-for="(item, index) in station" :key="index">
+            <view v-if="index !== 0" class="card_line"></view>
+            <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>
-          <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
-            >
+        </block>
+        <block v-else>
+          <!-- 单个 -->
+          <view class="station">
+            <charge-station
+              :title="station[markersIndex].stationName"
+              :address="station[markersIndex].address"
+              :price="station[markersIndex].totalFee"
+              :fast="station[markersIndex].fastEquipmentInfos"
+              :slow="station[markersIndex].slowEquipmentInfos"
+              :sId="station[markersIndex].StationID"
+              :distance="station[markersIndex].stationLatDistance"
+              :latitude="station[markersIndex].location.stationLat"
+              :longitude="station[markersIndex].location.stationLng"
+              :fromMap="true"
+            ></charge-station>
           </view>
+        </block>
+      </block>
+      <block v-else>
+        <view class="card_empty flex-column flex-align-center pt-20">
+          <view v-if="loading" class="mt-60 animation-loading"
+            ><uni-icons
+              type="spinner-cycle"
+              size="20"
+              color="rgba(0, 0, 0, 0.5)"
+            ></uni-icons
+          ></view>
+          <image
+            class="image"
+            v-else
+            src="/static/images/map-empty.png"
+            mode="widthFix"
+          />
+          <view class="fs-22 mt-14 color-000-5">{{
+            loading
+              ? "加载中"
+              : token
+              ? "暂无充电站信息"
+              : "登录后查看更多电站信息"
+          }}</view>
         </view>
-      </view>
+      </block>
+    </view>
+    <view
+      class="charging flex-align-center"
+      v-if="charging"
+      @click="toCharging"
+    >
+      <image
+        class="width-64 ml-12"
+        src="/static/images/map-charging.png"
+        mode="widthFix"
+      ></image>
+      <view class="fs-26 color-fff ml-12">{{
+        charging.chargeStatus === 0 ? "已预约" : "充电中"
+      }}</view>
     </view>
   </view>
   <view class="login-mask" v-if="!token">
@@ -150,28 +277,33 @@ const pointSize = {
   androidX: -14,
   androidCurrentX: -20,
 };
+import { fetchHomeBanner } from "@/api";
 import { deCode } from "../../utils/code";
+import { rpxToPx } from "../../utils/device";
 import { fetchToken, login, onLogin } from "@/api/auth";
-import { fetchStations } from "@/api/charge";
+import { fetchStations, fetchChargeStatus } from "@/api/charge";
 import { fetchCollectList } from "@/api/user";
 import { fetchLocation } from "@/utils/location";
-import { onLoad } from "@dcloudio/uni-app";
+import { to } from "@/utils/navigate";
+import { onLoad, onShow } 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 loading = ref(false);
+const styleData = ref({
+  dialog: "",
+  dialogHeight: 0,
+  dialogPlaceHolderHeight: 0,
+  cardHeight: 0,
+  navHeight: 0
 });
+
 const filterDialog = ref({
-  visible: false,
-  style: {},
-  range: [
+  discounts: false,
+  distanceSelector: false,
+  distanceRange: [
     {
       value: 1,
       scale: defaultScale + 3,
@@ -229,18 +361,26 @@ const filterDialog = ref({
     },
   ],
 });
-const menuStyle = ref({
-  menu1: {},
-  menu2: {},
-});
+
 const stationPage = ref({
   page: 1,
   pageSize: 6,
   hasNext: false,
 });
 const station = ref<any[]>([]);
-const markersIndex = ref(0);
+const charging = ref<any>();
+
+const mapMode = ref(true);
+const mapProps = ref({
+  latitude: 23.098994,
+  longitude: 113.32252,
+  selflatitude: 23.098994,
+  selflongitude: 113.32252,
+  scale: defaultScale,
+});
+const markersIndex = ref(-1);
 const markers = ref<any[]>([]);
+const mapBanner = ref<any[]>([]);
 
 let isIgnoreChangeLocation = false;
 
@@ -323,9 +463,9 @@ const refreshStation = (location: any) => {
         height: 34,
       });
     }
-    isIgnoreChangeLocation = markers.value.length !== _markers.length
+    isIgnoreChangeLocation = markers.value.length !== _markers.length;
     stationPage.value.hasNext = res.length >= stationPage.value.pageSize;
-    empty.value = stationPage.value.page === 1 ? res.length <= 0 : false;
+    // empty.value = stationPage.value.page === 1 ? res.length <= 0 : false;
     station.value =
       stationPage.value.page === 1 ? res : [...station.value, ...res];
     markersIndex.value = _markersIndex;
@@ -335,16 +475,19 @@ const refreshStation = (location: any) => {
 };
 
 const refresh = () => {
+  if (loading.value) {
+    return;
+  }
   console.log("刷新电站");
   uni.showLoading({
     title: "加载中",
   });
+  loading.value = true;
   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;
@@ -356,17 +499,41 @@ const refresh = () => {
     })
     .then(() => {
       uni.hideLoading();
+      loading.value = false;
       ready.value = true;
     })
     .catch((err) => {
       console.log(err);
       uni.hideLoading();
+      loading.value = false;
       uni.showModal({
         content: `${err.errMsg},请重试`,
       });
     });
 };
 
+const handleNavReady = (e: any) => {
+  console.log(e)
+  styleData.value.dialog = `padding-top:${e.detail.statusBarHeight - 6}px;`;
+  const searchHeight = rpxToPx(88);
+  const filterHeight = 0; // rpxToPx(72);
+  styleData.value.dialogHeight =
+    e.detail.statusBarHeight - 6 + searchHeight + filterHeight;
+  styleData.value.dialogPlaceHolderHeight =
+    styleData.value.dialogHeight - e.detail.navigationBarHeight;
+  styleData.value.cardHeight = rpxToPx(402) + e.detail.statusBarHeight;
+  styleData.value.navHeight = e.detail.navigationBarHeight;
+};
+
+const toSearch = () => {
+  if (!ready.value) {
+    return;
+  }
+  uni.navigateTo({
+    url: "/pages-charge/search/search",
+  });
+};
+
 onLoad((query: any) => {
   // 只为了打包进tab-bar使用
   console.log(fetchToken, login, onLogin);
@@ -375,17 +542,9 @@ onLoad((query: any) => {
     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) {
@@ -415,6 +574,8 @@ onLoad((query: any) => {
         token.value = _token;
         fetchCollectList().then(() => {
           refresh();
+          fetchCharging();
+          fetchBanner();
         });
       });
       return;
@@ -426,37 +587,65 @@ onLoad((query: any) => {
         deCode(code);
       }
       refresh();
+      fetchCharging();
+      fetchBanner();
     });
   }, 300);
 });
 
-const toggleDialogVisible = () => {
-  filterDialog.value.visible = !filterDialog.value.visible;
-};
-const changeFilterDistance = (index: number) => {
-  filterDialog.value.options.distance = filterDialog.value.range[index].value;
+onShow(() => {
+  if (token.value) {
+    fetchBanner();
+    charging.value = undefined;
+    setTimeout(() => {
+      fetchCharging();
+    }, 2000);
+  }
+});
+
+const fetchCharging = () => {
+  fetchChargeStatus().then((res) => {
+    if (res && [0, 1, 2, 3].includes(res.chargeStatus)) {
+      charging.value = res;
+    }
+  });
 };
-const changeFilterStatus = (index: number) => {
-  filterDialog.value.options.status = index;
+
+const fetchBanner = () => {
+  fetchHomeBanner().then((res) => {
+    mapBanner.value = res.filter((item: any) => item.status === 1);
+  });
 };
-const resetFilter = () => {
-  filterDialog.value.options.distance = defaulDistance;
-  filterDialog.value.options.status = 0;
-  refresh();
+
+// const checkDiscounts = () => {
+//   filterDialog.value.discounts = !filterDialog.value.discounts;
+// };
+
+const checkFilterDistance = () => {
+  if (isIgnoreChangeLocation) {
+    return;
+  }
+  if (!mapMode.value) {
+    mapMode.value = true;
+  }
+  filterDialog.value.distanceSelector = !filterDialog.value.distanceSelector;
 };
-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;
+
+const changeFilterDistance = (index: number) => {
+  filterDialog.value.options.distance =
+    filterDialog.value.distanceRange[index].value;
+  if (mapProps.value.scale === filterDialog.value.distanceRange[index].scale) {
     refresh();
     return;
   }
-  filterDialog.value.visible = false;
-  mapProps.value.scale = filterDialog.value.range[findIndex].scale;
+  mapProps.value.scale = filterDialog.value.distanceRange[index].scale;
+  checkFilterDistance();
 };
+
 const resetLocation = () => {
+  if (loading.value) {
+    return;
+  }
   // eslint-disable-next-line promise/catch-or-return
   fetchLocation().then((res: any) => {
     const mapCtx = uni.createMapContext("map");
@@ -468,6 +657,7 @@ const resetLocation = () => {
     mapProps.value.scale = defaultScale;
   });
 };
+
 const mapUpdated = (e: any) => {
   // console.log('map updated', isIgnoreChangeLocation)
   setTimeout(() => {
@@ -479,9 +669,9 @@ const mapChange = (e: any) => {
     return;
   }
   if (e.type === "end" && markers.value.length) {
-    console.log("map change end", {
-      ...e.detail.centerLocation
-    });
+    // console.log("map change end", {
+    //   ...e.detail.centerLocation,
+    // });
     const current = e.target.centerLocation;
     const { latitude, longitude } = current;
     stationPage.value.page = 1;
@@ -523,9 +713,6 @@ const _changeMarker = (current: number) => {
     });
   }
 };
-const changeMarker = (e: any) => {
-  _changeMarker(e.detail.current);
-};
 const tapMarker = (e: any) => {
   if (e.detail.markerId === -1) {
     return;
@@ -537,19 +724,45 @@ const tapMarker = (e: any) => {
     _changeMarker(findIndex);
   }
 };
-const search = () => {
-  uni.navigateTo({
-    url: "/pages-charge/search/search",
-  });
-};
 const loginMask = (e: any) => {
   login(e);
 };
-const emptyTap = () => {};
+
+let startpageY = 0;
+const touchCardStart = (e: any) => {
+  if (loading.value || station.value.length <= 0) {
+    return;
+  }
+  if (e.touches && e.touches.length) {
+    startpageY = e.touches[0].pageY;
+  }
+};
+
+const touchCardMove = (e: any) => {
+  if (!startpageY) {
+    return;
+  }
+  const threshold = 150;
+  if (e.touches && e.touches.length) {
+    if (mapMode.value && startpageY - e.touches[0].pageY > threshold) {
+      mapMode.value = false;
+    }
+    if (!mapMode.value && e.touches[0].pageY - startpageY > threshold) {
+      mapMode.value = true;
+    }
+  }
+};
+
+const toCharging = () => {
+  to(
+    charging.value.chargeStatus === 0
+      ? `/pages-charge/appointment/appointment?sn=${charging.value.connectorId}`
+      : `/pages-charge/ordering/ordering?sn=${charging.value.connectorId}&start=1`
+  );
+};
 </script>
 
 <style lang="scss">
-@import "../../styles/dialog.scss";
 page {
   background-color: #ffffff;
 }
@@ -561,6 +774,13 @@ page {
   background-color: #ffffff;
 }
 
+.mock-map {
+  position: relative;
+  width: 100vw;
+  background-color: #ffffff;
+  overflow: hidden;
+}
+
 .login-mask {
   position: fixed;
   left: 0;
@@ -576,31 +796,72 @@ page {
   }
 }
 
-.icon-menu {
-  position: absolute;
-  z-index: 9;
+.dialog {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  z-index: 1000;
 
-  image {
-    width: 60rpx;
+  &_logo {
+    width: 156rpx;
+    height: 100%;
+    background-position: center;
+    background-size: 100% auto;
+    background-repeat: no-repeat;
+    background-image: url("/static/images/map-logo.png");
   }
 
-  & > view {
-    height: 96rpx;
-    width: 96rpx;
-    border-radius: 50%;
-    background-color: #fff;
-    box-shadow: 0px 8rpx 20rpx rgba(0, 0, 0, 0.2);
+  &_search {
+    width: 352rpx;
+    height: 100%;
+    background-position: center;
+    background-size: 100% auto;
+    background-repeat: no-repeat;
+    background-image: url("/static/images/map-input.png");
   }
 
-  .hover {
-    box-shadow: none;
+  &_event {
+    margin-top: 20rpx;
+    padding: 0rpx 32rpx;
+    .swiper {
+      height: 160rpx;
+      width: 100%;
+      border-radius: 16rpx;
+      overflow: hidden;
+      .swiper-item {
+        border-radius: 16rpx;
+        overflow: hidden;
+      }
+      view {
+        background-position: center;
+        background-repeat: no-repeat;
+        background-size: 100% 100%;
+      }
+    }
   }
-}
 
-.filter-dialog {
-  background-color: #fff;
+  &_selector {
+    background-color: #f9f9f9;
+    transition: all 0.3s;
+    overflow: hidden;
+    .type {
+      width: 160rpx;
+      height: 60rpx;
+      background: var(--color-sec);
+      border-radius: 4rpx;
+      color: #666;
+      font-size: 26rpx;
+      border: 1px solid var(--color-sec);
+    }
+
+    .type-active {
+      border: 1px solid var(--color-primary);
+      color: var(--color-primary);
+    }
+  }
 
-  .slider {
+  /* .slider {
     position: relative;
     height: 12rpx;
     width: 100%;
@@ -637,61 +898,53 @@ page {
       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;
+  position: fixed;
   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);
+  z-index: 1000;
+  transition: all 0.3s;
+  box-shadow: 0px 8rpx 20rpx rgba(0, 0, 0, 0.2);
+  background-color: #fff;
 
-  swiper,
-  swiper-item {
-    width: 100%;
-    height: 300rpx;
+  &_location {
+    position: absolute;
+    border-radius: 16rpx;
+    box-shadow: 0px 8rpx 20rpx 0px rgba(0, 0, 0, 0.2);
+    right: 20rpx;
+    top: -104rpx;
+    z-index: 99;
   }
 
-  .station {
-    width: 100%;
-    height: 100%;
-    padding: 0rpx 20rpx;
+  &_line {
+    height: 1rpx;
+    background-color: rgba(0, 0, 0, 0.1);
+    margin-left: 30rpx;
+    margin-right: 30rpx;
   }
 
-  .station-empty {
+  &_empty {
     width: 100%;
     height: 100%;
-    background-color: #fff;
-    border-radius: 20rpx 20rpx 0 0;
-    image {
+    .image {
       width: 160rpx;
     }
   }
 }
+
+.charging {
+  position: fixed;
+  z-index: 1000;
+  right: 0;
+  bottom: 164rpx;
+  width: 186rpx;
+  height: 88rpx;
+  border-radius: 88rpx 0px 0px 88rpx;
+  background: linear-gradient(106deg, #34b6ff 15.97%, #347dff 72.38%);
+  margin-bottom: constant(safe-area-inset-bottom);
+  margin-bottom: env(safe-area-inset-bottom);
+}
 </style>

+ 57 - 122
src/pages/user/user.vue

@@ -16,79 +16,52 @@
         <view class="main flex-shrink">
           <view
             class="avatar"
-            @click="toPage(3)"
+            @click="toPage(2)"
             :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">{{
+          <view class="phone fs-40 fw-500">{{ user.mobilePhoneFormat }}</view>
+          <view class="money flex-align-center" @click="toPage(-1)">
+            <uni-icons
+              type="wallet-filled"
+              size="24"
+              color="var(--color-primary)"
+            ></uni-icons>
+            <view class="color-primary ml-12" style="font-size: 16px"
+              >我的钱包</view
+            >
+            <view class="fs-40 fw-500 ml-auto mt-12 color-primary">¥</view>
+            <view class="fs-60 fw-500 ml-12 mr-48 color-primary">{{
               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>
+        <block v-for="(item, index) in menu" :key="index">
+          <view
+            class="menu flex-align-center"
+            v-if="
+              index !== menu.length - 1 || user.mobilePhone === '15986907592'
+            "
+            @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>
+        </block>
       </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">
@@ -100,11 +73,8 @@ const containerStyle = ref({});
 const user = ref<any>({
   avatar: "",
 });
+const service = ref("");
 const menu = ref([
-  {
-    title: "我的钱包",
-    path: "/pages-user/wallet/wallet",
-  },
   {
     title: "充电订单",
     path: "/pages-charge/orders/orders",
@@ -119,31 +89,36 @@ const menu = ref([
   },
   {
     title: "联系我们",
-    path: "",
-    code: "",
-    mobile: "",
+    path: "/pages-common/contact/contact",
+  },
+  {
+    title: "常见问题",
+    path: "/pages-common/contact/contact",
+  },
+  {
+    title: "电站列表",
+    path: "/pages/list/list",
   },
 ]);
-const contactDialogVisible = ref(false);
 const toPage = (index: number) => {
-  const item = menu.value[index];
-  if (item.mobile) {
-    contactDialogVisible.value = true;
+  if (index < 0) {
+    uni.navigateTo({
+      url: "/pages-user/wallet/wallet",
+    });
     return;
   }
+  if (index === 3) {
+    uni.makePhoneCall({
+      phoneNumber: service.value,
+    });
+    return;
+  }
+  const item = menu.value[index];
   uni.navigateTo({
-    url: item.path,
-  });
-};
-const close = () => {
-  contactDialogVisible.value = false;
-};
-const emptyTap = () => {};
-const call = () => {
-  uni.makePhoneCall({
-    phoneNumber: menu.value[4].mobile as string,
+    url: item.path + (index === 4 ? `?service=${service.value}` : ""),
   });
 };
+
 onLoad(() => {
   const bound = uni.getMenuButtonBoundingClientRect();
   containerStyle.value = {
@@ -151,16 +126,14 @@ onLoad(() => {
   };
   fetchContact().then((res) => {
     if (res && res.mobile) {
-      menu.value[4].mobile = res.mobile;
-    }
-    if (res && res.codeUrl) {
-      menu.value[4].code = res.codeUrl;
+      service.value = res.mobile;
     }
   });
 });
 onShow(() => {
   fetchProfile().then((res) => {
-    res.balance = (Number(res.balance) / 100).toFixed(2);
+    res.mobilePhoneFormat =
+      res.mobilePhone.slice(0, 3) + "****" + res.mobilePhone.slice(7);
     res.avatar = res.avatar
       ? res.avatar
       : "https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0";
@@ -252,42 +225,4 @@ onShow(() => {
     }
   }
 }
-
-.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
src/static/images/contact-servicer.png


BIN
src/static/images/custom-tab-bar/2-2.png


BIN
src/static/images/custom-tab-bar/2-3.png


BIN
src/static/images/custom-tab-bar/2-4.png


BIN
src/static/images/custom-tab-bar/bg.png


BIN
src/static/images/map-bg.jpg


BIN
src/static/images/map-charging.png


BIN
src/static/images/map-filter.png


BIN
src/static/images/map-input.png


BIN
src/static/images/map-location.png


BIN
src/static/images/map-logo.png


BIN
src/static/images/map-re-location.png


BIN
src/static/images/map-search.png


BIN
src/static/images/user/1.png


BIN
src/static/images/user/2.png


BIN
src/static/images/user/3.png


BIN
src/static/images/user/4.png


BIN
src/static/images/user/6.png


+ 4 - 0
src/styles/flex.scss

@@ -98,4 +98,8 @@
 
 .flex-shrink {
   flex-shrink: 0;
+}
+
+.flex-inline {
+  display: inline-flex;
 }

+ 11 - 0
src/styles/layout.scss

@@ -24,6 +24,13 @@ $position: top, right, bottom, left;
   .width-#{$i} {
     width: #{$i}rpx;
   }
+  .br-#{$i} {
+    border-radius: #{$i}rpx;
+  }
+}
+
+.br-round {
+  border-radius: 50%;
 }
 
 .mb-0 {
@@ -40,4 +47,8 @@ $position: top, right, bottom, left;
 
 .width-168 {
   width: 168rpx;
+}
+
+.width-half {
+  width: 50%;
 }

+ 1 - 1
src/utils/code.ts

@@ -22,7 +22,7 @@ export function deCode(url: string) {
     const split = url.split("/");
     const sn = split[split.length - 1].replace("#", "");
     uni.navigateTo({
-      url: `/pages-charge/ordering/ordering?sn=${sn}`,
+      url: `/pages-charge/appointment/appointment?sn=${sn}`,
     });
   } else {
     uni.showModal({

+ 3 - 1
src/utils/constant.ts

@@ -12,5 +12,7 @@ console.log("env", env);
 isDevelopment = env === "develop" || env === "trial";
 // #endif
 
-export const domain = !isDevelopment ? "dev.kuaiyuman.cn" : "www.kuaiyuman.cn";
+export const domain = isDevelopment ? "dev.kuaiyuman.cn" : "www.kuaiyuman.cn";
 export const host = `https://${domain}/api`;
+
+export const isDebug = isDevelopment

+ 9 - 9
src/utils/date.ts

@@ -1,20 +1,20 @@
 export function format(format: string, time?: number) {
-  const now = time ? new Date(time) : new Date()
+  const now = time ? new Date(time) : new Date();
   const map: Record<string, string | number> = {
     y: now.getFullYear(),
-    M: now.getMonth() + 1,
-    d: now.getDate(),
+    M: now.getMonth() + 1 > 9 ? now.getMonth() + 1 : `0${now.getMonth() + 1}`,
+    d: now.getDate() > 9 ? now.getDate() : `0${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 = []
+    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)])
+      res.push(map[format.charAt(i)]);
     } else {
-      res.push(format.charAt(i))
+      res.push(format.charAt(i));
     }
   }
-  return res.join('')
+  return res.join("");
 }

+ 17 - 0
src/utils/device.ts

@@ -0,0 +1,17 @@
+export function rpxToPx(rpx: number): number {
+  if (getApp<any>().globalData.device) {
+    return (rpx * getApp<any>().globalData.device.windowWidth) / 750;
+  }
+  return rpx;
+}
+
+export function pxToRpx(px: number): number {
+  if (getApp<any>().globalData.device) {
+    return (750 * px) / getApp<any>().globalData.device.windowWidth;
+  }
+  return px;
+}
+
+export const isDevTool =
+  getApp<any>().globalData.device &&
+  getApp<any>().globalData.device.platform === "devtools";

+ 3 - 5
src/utils/infinite-scroll.ts

@@ -16,9 +16,6 @@ export function useInfiniteScroll<T = any>(
     uni.showLoading({
       title: "加载中",
     });
-    if (list.value) {
-      list.value = [];
-    }
     loading.value = true;
     return loadData(1)
       .then((res) => {
@@ -31,6 +28,7 @@ export function useInfiniteScroll<T = any>(
       })
       .catch(() => {
         uni.hideLoading();
+        uni.stopPullDownRefresh();
         loading.value = false;
       });
   };
@@ -48,18 +46,18 @@ export function useInfiniteScroll<T = any>(
         .then((res) => {
           uni.hideLoading();
           loading.value = false;
+          hasNext.value = res.length >= pageSize;
           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();
+          hasNext.value = false;
           loading.value = false;
         });
     }

+ 43 - 0
src/utils/navigate.ts

@@ -0,0 +1,43 @@
+export function to(path: string) {
+  if (nextPageIsPrevPage(path)) {
+    uni.navigateBack();
+    return;
+  }
+  uni.navigateTo({
+    url: path,
+  });
+}
+
+export function back() {
+  uni.navigateBack();
+}
+
+export function redirect(url: string) {
+  uni.redirectTo({
+    url,
+  });
+}
+
+export function reLaunch() {
+  uni.reLaunch({
+    url: "/pages/map/map",
+  });
+}
+
+export function nextPageIsPrevPage(path: string): boolean {
+  const isFull = path[0] === "/";
+  const pages = getCurrentPages();
+  const prevPage = pages.length >= 2 ? pages[pages.length - 2] : undefined;
+  const prevPageQuery = prevPage
+    ? Object.keys(prevPage.options)
+        .map((key) => `${key}=${prevPage.options[key]}`)
+        .join("&")
+    : "";
+  if (prevPage) {
+    return (
+      `${prevPage.route}${prevPageQuery ? "?" + prevPageQuery : ""}` ===
+      (isFull ? path.substring(1) : path)
+    );
+  }
+  return false;
+}

+ 63 - 39
src/utils/uploader.ts

@@ -1,4 +1,6 @@
 import { host } from "../utils/constant";
+import Http from "../utils/http";
+const uploaderHttp = new Http(host);
 
 type UploadCallback = {
   onSuccess?(result: { url: string }): void;
@@ -49,51 +51,53 @@ export function uploadByQueue(filePath: string, callback: UploadCallback) {
 
 // 单独上传一个
 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") {
+  uploaderHttp
+    .get("/file/getSts")
+    .then((oss) => {
+      const key =
+        "uniapp-scan-code-" +
+        guid() +
+        filePath.slice(filePath.lastIndexOf("."));
+      const uploadTask = wxUploadFile(
+        oss.host,
+        filePath,
+        {
+          key,
+          policy: oss.policy,
+          OSSAccessKeyId: oss.accesskeyId,
+          expire: oss.expire,
+          signature: oss.signature,
+          success_action_status: "200",
+        },
+        {
+          success: (res: any) => {
+            if (res.statusCode == 200 && /ok/.test(res.errMsg)) {
+              // https://kym-static.oss-cn-shenzhen.aliyuncs.com/uniapp-scan-code-41d308c3-2252-1b46-744a-104808f1c0c3.png
               callback.onSuccess &&
                 callback.onSuccess({
-                  url: data.data.url,
+                  url: `${oss.host}/${key}`,
                 });
             } else {
               callback.onFail &&
-              callback.onFail({
-                errMsg: data.message,
-              });
+                callback.onFail({
+                  errMsg: `update error(${JSON.stringify(res)})`,
+                });
             }
-          } 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);
+          },
         }
-      },
-      fail: callback.onFail,
-      complete: () => {
-        // 取消监听
-        callback.onProgressUpdate &&
-          uploadTask.offProgressUpdate(callback.onProgressUpdate);
-      },
-    }
-  );
-  callback.onStart && callback.onStart(uploadTask);
-  if (callback.onProgressUpdate) {
-    uploadTask.onProgressUpdate(callback.onProgressUpdate);
-  }
+      );
+      callback.onStart && callback.onStart(uploadTask);
+      if (callback.onProgressUpdate) {
+        uploadTask.onProgressUpdate(callback.onProgressUpdate);
+      }
+    })
+    .catch(callback.onFail);
 }
 
 function wxUploadFile(
@@ -106,7 +110,7 @@ function wxUploadFile(
     url,
     filePath,
     header: {
-      satoken: getApp<any>().globalData.token || "",
+      // satoken: getApp<any>().globalData.token || "",
     },
     name: "file",
     formData,
@@ -125,3 +129,23 @@ function wxUploadFile(
     },
   });
 }
+
+function guid() {
+  function S4() {
+    return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
+  }
+  return (
+    S4() +
+    S4() +
+    "-" +
+    S4() +
+    "-" +
+    S4() +
+    "-" +
+    S4() +
+    "-" +
+    S4() +
+    S4() +
+    S4()
+  );
+}