Quellcode durchsuchen

将充电小程序前端工程迁移至 charge-front 目录

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline vor 3 Monaten
Ursprung
Commit
2975464865
100 geänderte Dateien mit 14370 neuen und 0 gelöschten Zeilen
  1. 23 0
      charge-front/.gitignore
  2. 1 0
      charge-front/README.md
  3. 20 0
      charge-front/index.html
  4. 30 0
      charge-front/package.json
  5. 163 0
      charge-front/src/App.vue
  6. 11 0
      charge-front/src/App.wxs
  7. 177 0
      charge-front/src/api/auth.ts
  8. 570 0
      charge-front/src/api/charge.ts
  9. 36 0
      charge-front/src/api/index.ts
  10. 217 0
      charge-front/src/api/user.ts
  11. 44 0
      charge-front/src/components/charge-icon/charge-icon.vue
  12. 318 0
      charge-front/src/components/charge-station/charge-station.vue
  13. 7 0
      charge-front/src/components/demo/index.vue
  14. 244 0
      charge-front/src/components/order-coupon-detail/order-coupon-detail.vue
  15. 87 0
      charge-front/src/components/shadow-card/shadow-card.vue
  16. 372 0
      charge-front/src/components/station-charge-coupon/station-charge-coupon.vue
  17. 317 0
      charge-front/src/components/station-coupon/station-coupon.vue
  18. 60 0
      charge-front/src/components/style-bottom-view/style-bottom-view.vue
  19. 100 0
      charge-front/src/components/style-button/style-button.vue
  20. 45 0
      charge-front/src/components/style-checkbox/style-checkbox.vue
  21. 87 0
      charge-front/src/components/style-dialog/style-dialog.vue
  22. 69 0
      charge-front/src/components/style-input/style-input.vue
  23. 53 0
      charge-front/src/components/style-result/style-result.vue
  24. 1169 0
      charge-front/src/components/uni-icons/icons.js
  25. 96 0
      charge-front/src/components/uni-icons/uni-icons.vue
  26. 663 0
      charge-front/src/components/uni-icons/uniicons.css
  27. BIN
      charge-front/src/components/uni-icons/uniicons.ttf
  28. 76 0
      charge-front/src/custom-tab-bar/index.js
  29. 3 0
      charge-front/src/custom-tab-bar/index.json
  30. 18 0
      charge-front/src/custom-tab-bar/index.wxml
  31. 126 0
      charge-front/src/custom-tab-bar/index.wxss
  32. 8 0
      charge-front/src/env.d.ts
  33. 8 0
      charge-front/src/main.ts
  34. 60 0
      charge-front/src/manifest.json
  35. 861 0
      charge-front/src/pages-charge/appointment/appointment-backup.vue
  36. 981 0
      charge-front/src/pages-charge/appointment/appointment.vue
  37. 268 0
      charge-front/src/pages-charge/camera/camera.vue
  38. 298 0
      charge-front/src/pages-charge/card/card.vue
  39. 76 0
      charge-front/src/pages-charge/codeing/codeing.vue
  40. 221 0
      charge-front/src/pages-charge/coupon-center/coupon-center.vue
  41. 175 0
      charge-front/src/pages-charge/discount/discount.vue
  42. 163 0
      charge-front/src/pages-charge/machines/charge-machine/charge-machine.vue
  43. 486 0
      charge-front/src/pages-charge/machines/machines.vue
  44. 195 0
      charge-front/src/pages-charge/machines/price-desc/price-desc.vue
  45. 560 0
      charge-front/src/pages-charge/order/order.vue
  46. 753 0
      charge-front/src/pages-charge/ordering/ordering.vue
  47. 636 0
      charge-front/src/pages-charge/orders/orders.vue
  48. 194 0
      charge-front/src/pages-charge/search/search.vue
  49. BIN
      charge-front/src/pages-charge/static/camera-back.png
  50. BIN
      charge-front/src/pages-charge/static/charge-input-head.png
  51. BIN
      charge-front/src/pages-charge/static/charge-ordering-bg.png
  52. BIN
      charge-front/src/pages-charge/static/charge-ordering-border.png
  53. BIN
      charge-front/src/pages-charge/static/charge-ordering-finish.png
  54. BIN
      charge-front/src/pages-charge/static/charge-ordering-icon.png
  55. BIN
      charge-front/src/pages-charge/static/charge-status-icon.png
  56. BIN
      charge-front/src/pages-charge/static/discount-bg.png
  57. BIN
      charge-front/src/pages-charge/static/discount-coupon.png
  58. BIN
      charge-front/src/pages-charge/static/discount-rights-card.png
  59. BIN
      charge-front/src/pages-charge/static/icon-alarm.png
  60. BIN
      charge-front/src/pages-charge/static/machines-banner-address.png
  61. BIN
      charge-front/src/pages-charge/static/machines-banner-nav.png
  62. BIN
      charge-front/src/pages-charge/static/scan-code-album.png
  63. BIN
      charge-front/src/pages-charge/static/scan-code-input.png
  64. BIN
      charge-front/src/pages-charge/static/scan-code-light.png
  65. 85 0
      charge-front/src/pages-common/activity/activity.vue
  66. 148 0
      charge-front/src/pages-common/faq/faq.vue
  67. 177 0
      charge-front/src/pages-common/invoice/invoice.vue
  68. 73 0
      charge-front/src/pages-user/collect/collect.vue
  69. 82 0
      charge-front/src/pages-user/profile-edit/profile-edit.vue
  70. 294 0
      charge-front/src/pages-user/profile/profile.vue
  71. BIN
      charge-front/src/pages-user/static/icon-submit-success.png
  72. BIN
      charge-front/src/pages-user/static/wallet-banner.png
  73. 276 0
      charge-front/src/pages-user/wallet-recharge/wallet-recharge.vue
  74. 180 0
      charge-front/src/pages-user/wallet-refund/wallet-refund.vue
  75. 369 0
      charge-front/src/pages-user/wallet/wallet.vue
  76. 209 0
      charge-front/src/pages.json
  77. 29 0
      charge-front/src/pages/index/index.vue
  78. 69 0
      charge-front/src/pages/list/list.vue
  79. 996 0
      charge-front/src/pages/map/map.vue
  80. 232 0
      charge-front/src/pages/user/user.vue
  81. 6 0
      charge-front/src/shime-uni.d.ts
  82. BIN
      charge-front/src/static/images/back.png
  83. BIN
      charge-front/src/static/images/contact-customer.png
  84. BIN
      charge-front/src/static/images/coupon-center.png
  85. BIN
      charge-front/src/static/images/custom-tab-bar/1-1.png
  86. BIN
      charge-front/src/static/images/custom-tab-bar/1.png
  87. BIN
      charge-front/src/static/images/custom-tab-bar/2.png
  88. BIN
      charge-front/src/static/images/custom-tab-bar/3-1.png
  89. BIN
      charge-front/src/static/images/custom-tab-bar/3.png
  90. BIN
      charge-front/src/static/images/custom-tab-bar/4-1.png
  91. BIN
      charge-front/src/static/images/custom-tab-bar/4.png
  92. BIN
      charge-front/src/static/images/custom-tab-bar/bg.png
  93. BIN
      charge-front/src/static/images/discount-bg.png
  94. BIN
      charge-front/src/static/images/discount-coupon.png
  95. BIN
      charge-front/src/static/images/discount-rights-card.png
  96. BIN
      charge-front/src/static/images/icon-nav.png
  97. BIN
      charge-front/src/static/images/map-bg.jpg
  98. BIN
      charge-front/src/static/images/map-charging.png
  99. BIN
      charge-front/src/static/images/map-current.png
  100. BIN
      charge-front/src/static/images/map-empty.png

+ 23 - 0
charge-front/.gitignore

@@ -0,0 +1,23 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+*.local
+
+# Editor directories and files
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+package-lock.json

+ 1 - 0
charge-front/README.md

@@ -0,0 +1 @@
+## Q

+ 20 - 0
charge-front/index.html

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="UTF-8" />
+    <script>
+      var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
+        CSS.supports('top: constant(a)'))
+      document.write(
+        '<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
+        (coverSupport ? ', viewport-fit=cover' : '') + '" />')
+    </script>
+    <title></title>
+    <!--preload-links-->
+    <!--app-context-->
+  </head>
+  <body>
+    <div id="app"><!--app-html--></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 30 - 0
charge-front/package.json

@@ -0,0 +1,30 @@
+{
+  "name": "uni-preset-vue",
+  "version": "0.0.0",
+  "scripts": {
+    "dev": "uni -p mp-weixin",
+    "dev:mp-weixin": "uni -p mp-weixin",
+    "build": "uni build -p mp-weixin",
+    "build:mp-weixin": "uni build -p mp-weixin",
+    "type-check": "vue-tsc --noEmit"
+  },
+  "dependencies": {
+    "@dcloudio/uni-app": "3.0.0-3080720230703001",
+    "@dcloudio/uni-components": "3.0.0-3080720230703001",
+    "@dcloudio/uni-mp-weixin": "3.0.0-3080720230703001",
+    "sass": "^1.65.1",
+    "vue": "^3.2.45",
+    "vue-i18n": "^9.1.9"
+  },
+  "devDependencies": {
+    "@dcloudio/types": "^3.3.2",
+    "@dcloudio/uni-automator": "3.0.0-3080720230703001",
+    "@dcloudio/uni-cli-shared": "3.0.0-3080720230703001",
+    "@dcloudio/uni-stacktracey": "3.0.0-3080720230703001",
+    "@dcloudio/vite-plugin-uni": "3.0.0-3080720230703001",
+    "@vue/tsconfig": "^0.1.3",
+    "typescript": "^5.1.6",
+    "vite": "4.0.4",
+    "vue-tsc": "^1.0.24"
+  }
+}

+ 163 - 0
charge-front/src/App.vue

@@ -0,0 +1,163 @@
+<script lang="ts">
+import { fetchToken } from "./api/auth";
+export default <any>{
+  globalData: {
+    token: "",
+    lastData: {},
+    stations: [],
+    normalCode: "",
+    manualStop:false,//手工停止充电标识
+  },
+  onLaunch() {
+    uni.getSystemInfo({
+      success: (res) => {
+        this.globalData.device = res;
+      },
+    });
+    return fetchToken().then((cache) => {
+      if (cache) {
+        this.globalData.token = cache;
+      }
+    });
+  },
+  onPageNotFound() {
+    uni.switchTab({
+      url: "/pages/map/map",
+    });
+  },
+};
+</script>
+<style lang="scss">
+@import "./styles/flex.scss";
+@import "./styles/font.scss";
+@import "./styles/layout.scss";
+
+view {
+  box-sizing: border-box;
+  --color-primary: #2d9e95;
+  --color-warning: #ff9900;
+  --color-sec: #f7f7f7;
+}
+
+.box-content {
+  box-sizing: content-box;
+}
+
+image {
+  width: 20rpx;
+  height: 20rpx;
+}
+
+.relative {
+  position: relative;
+}
+
+.color-fff {
+  color: #fff;
+}
+
+.color-000 {
+  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;
+}
+
+.color-666 {
+  color: #666;
+}
+
+.color-999 {
+  color: #999;
+}
+
+.color-primary {
+  color: var(--color-primary);
+}
+
+.color-warning {
+  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>

+ 11 - 0
charge-front/src/App.wxs

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

+ 177 - 0
charge-front/src/api/auth.ts

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

+ 570 - 0
charge-front/src/api/charge.ts

@@ -0,0 +1,570 @@
+import Http from "../utils/http";
+import {host, isDebug} from "../utils/constant";
+
+const cHttp = new Http(host);
+
+export function startCharge(connectorId: string, query?: string) {
+    return cHttp.get<{
+        ConnectorID: string;
+        FailReason: number;
+        StartChargeSeq: string;
+        StartChargeSeqStat: number;
+        SuccStat: number;
+    }>(`/charge/startCharge/${connectorId}?${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.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) {
+                    res.useTime = `${hour}:${min}~24:00`;
+                }
+                let tempPrice = Number(
+                    Number(item.elecPrice + item.servicePrice).toFixed(2)
+                );
+                item.totalPrice = tempPrice;
+                if (tempPrice > maxPrice) {
+                    maxPrice = tempPrice;
+                }
+                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.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) {
+            res.StationID = StationID;
+        }
+        return res;
+    });
+}
+
+export function cancelCharge(connectorId: string) {
+    return cHttp.get(`/charge/stopCharge/${connectorId}`, {
+        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: "#2d9e95",
+                    success(modal) {
+                        if (modal.confirm) {
+                            uni.redirectTo({
+                                url: `/pages-charge/appointment/appointment?connectorId=${res.connectorId}`,
+                            });
+                        }
+                    },
+                });
+            }
+            if (checkCharge && res && res.isStarted) {
+                uni.hideLoading();
+                uni.showModal({
+                    title: "温馨提示",
+                    content: "当前已有进行中的订单,请结束订单再扫码充电",
+                    showCancel: false,
+                    confirmText: "查看详情",
+                    confirmColor: "#2d9e95",
+                    success(modal) {
+                        if (modal.confirm) {
+                            uni.redirectTo({
+                                url: `/pages-charge/ordering/ordering?connectorId=${res.connectorId}&start=1`,
+                            });
+                        }
+                    },
+                });
+            }
+            return res;
+        });
+}
+
+export function fetchStations(
+    page: number,
+    pageSize: number,
+    latitude?: number,
+    longitude?: number,
+    baseLatitude?: number,
+    baseLongitude?: number,
+    options?: {
+        distance?: number;
+        status?: number;
+    }
+): Promise<any[]> {
+    return fetchAllStations(latitude, longitude)
+        .then((res) => {
+            // console.log(latitude, longitude);
+            // 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) {
+                data.forEach((item: any) => {
+                    item.stationLatDistance = _getDistance(
+                        latitude,
+                        longitude,
+                        item.location.stationLat,
+                        item.location.stationLng
+                    );
+                });
+                data.sort((item1: any, item2: any) => {
+                    return item1.stationLatDistance - item2.stationLatDistance;
+                });
+            }
+            if (baseLatitude && baseLongitude) {
+                data.forEach((item: any) => {
+                    item.stationLatDistance = _getDistance(
+                        baseLatitude,
+                        baseLongitude,
+                        item.location.stationLat,
+                        item.location.stationLng
+                    );
+                });
+            }
+            return _fetchStationStatus(data);
+        })
+        .then((list) => {
+            // 暂时废弃状态筛选
+            // if (options && options.status) {
+            //   let res = false;
+            //   list = list.filter((item: any) => {
+            //     res = false;
+            //     item.equipmentInfos.forEach((eqInfo: any) => {
+            //       eqInfo.connectorInfos.forEach((coInfo: any) => {
+            //         if (
+            //           !res &&
+            //           options.status === 1 &&
+            //           coInfo.connectorStatusInfo &&
+            //           coInfo.connectorStatusInfo.status === 1
+            //         ) {
+            //           res = true;
+            //         }
+            //         if (
+            //           !res &&
+            //           options.status === 2 &&
+            //           coInfo.connectorStatusInfo &&
+            //           coInfo.connectorStatusInfo.status !== 1
+            //         ) {
+            //           res = true;
+            //         }
+            //       });
+            //     });
+            //     return res;
+            //   });
+            // }
+            return list;
+        });
+}
+
+export function fetchStation(id: number) {
+    return fetchAllStations()
+        .then((res) => {
+            const findIndex = res.findIndex((item) => Number(item.StationID) === id);
+            if (findIndex < 0) {
+                throw {
+                    errMsg: "not found",
+                };
+            }
+            return _fetchStationStatus([res[findIndex]]);
+        })
+        .then((list) => {
+            return list[0];
+        });
+}
+
+export function fetchStationDetail(stationId: string) {
+    return cHttp
+        .get(`/charge/stationInfo/${stationId}`)
+        .then((res) => {
+            console.log("xxxxx", res)
+            return res;
+        });
+}
+
+
+/**
+ * 根据shortId或connectorId或equipmentId 查询站点信息(因前期为查询全部站点后前端过滤,特此进行性能优化)
+ * @param shortId
+ * @param connectorId
+ * @param equipmentId
+ */
+export function fetchStationDetailByShortIdOrConnectorIdOrEquipmentId(shortId?: string, connectorId?: string, equipmentId?: string) {
+    if (shortId) {
+        return fetchStationDetailByShortId(shortId);
+    } else if (connectorId) {
+        return fetchStationDetailByConnectorId(connectorId)
+    } else if (equipmentId) {
+        return fetchStationDetailByByEquipmentId(equipmentId)
+    }
+}
+
+export function fetchStationDetailByShortId(shortId: string) {
+    return cHttp
+        .get(`/charge/stationInfoByShortId/${shortId}`)
+        .then((res) => {
+            return res;
+        });
+}
+
+export function fetchStationDetailByConnectorId(connectorId: string) {
+    return cHttp
+        .get(`/charge/stationInfoByConnectorId/${connectorId}`)
+        .then((res) => {
+            return res;
+        });
+}
+
+/**
+ * equipmentId  可能不唯一,慎重使用
+ * @param equipmentId
+ */
+export function fetchStationDetailByByEquipmentId(equipmentId: string) {
+    return cHttp
+        .get(`/charge/stationInfoByEquipmentId/${equipmentId}`)
+        .then((res) => {
+            return res;
+        });
+}
+
+export function fetchStationByIds(ids: number[]) {
+    return fetchAllStations().then((res) => {
+        const list = res.filter((item) => ids.includes(Number(item.StationID)));
+        return _fetchStationStatus(list);
+    });
+}
+
+export function fetchStationByConnectorIdOrShortId(sn: string) {
+    let equipment = -1;
+    return fetchAllStations()
+        .then((res) => {
+            let station: any = undefined;
+            console.log("fetchStationByConnectorIdOrShortId", res)
+            res.forEach((item) => {
+                if (!station && item.equipmentInfos && item.equipmentInfos) {
+                    item.equipmentInfos.forEach(
+                        (equipmentInfo: any, equipmentIndex: number) => {
+                            if (sn.length <= 16) {
+                                // 此处传入的是 shortId
+                                if (equipmentInfo.shortId === sn) {
+                                    station = item;
+                                    equipment = equipmentIndex;
+                                }
+                            } else {
+                                if (
+                                    equipmentInfo.connectorInfos &&
+                                    equipmentInfo.connectorInfos
+                                ) {
+                                    // connectorId
+                                    equipmentInfo.connectorInfos.forEach((connectorInfo: any) => {
+                                        if (connectorInfo.connectorId === sn) {
+                                            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.trim(), "ig");
+        // console.log(res)
+        const stationIdList = res.filter(
+            (item) => item.stationName.includes(keyword) || item.address.includes(keyword)
+        ).map(k => k.stationId+"");
+        // console.log("serach>>>>>",list)
+        if (stationIdList) {
+            let {latitude, longitude} = getApp<any>().globalData.fetchLocation
+            return _fetchStations(1, 100, latitude, longitude,  stationIdList)
+        } else {
+            return Promise.reject([])
+        }
+
+    });
+}
+
+export function fetchAllStations(latitude?: number, longitude?: number): Promise<any[]> {
+    if (getApp<any>().globalData.stations.length > 0) {
+        return Promise.resolve(getApp<any>().globalData.stations);
+    }
+    const page = 1;
+    const page_size = 99;
+    return new Promise((resolve, reject) => {
+        _fetchAllStations(page, page_size, [], latitude, longitude)
+            .then((list) => {
+                // if (!isProduction) {
+                //   list.push({
+                //     ...list[0],
+                //     StationName: '这是模拟数据,不要使用充电',
+                //     Address: '这是模拟数据,测试一下附近的点',
+                //     StationLat: 22.540545,
+                //     StationLng: 113.942695,
+                //     StationID: '0000'
+                //   })
+                // }
+                console.log("所有电站数据", list);
+                getApp<any>().globalData.stations = list;
+                resolve(list);
+            })
+            .catch(reject);
+    });
+}
+
+function _fetchAllStations(
+    page: number,
+    pageSize: number,
+    list: any[],
+    latitude?: number,
+    longitude?: number
+): Promise<any[]> {
+    return _fetchStations(page, pageSize, latitude, longitude).then((res) => {
+        list = list.concat(res);
+        if (res.length >= pageSize) {
+            return _fetchAllStations(page + 1, pageSize, list, latitude, longitude);
+        } else {
+            // eslint-disable-next-line promise/no-return-wrap
+            return Promise.resolve(list);
+        }
+    });
+}
+
+export function _fetchStations(page: number, pageSize: number,
+                        latitude?: number, longitude?: number, stationIdList?: Array<string>) {
+    return cHttp
+        .get(`/charge/listStation?pageNum=${page}&pageSize=${pageSize}&latitude=${latitude||22.696779}&longitude=${longitude||114.044805}&stationIdList=${stationIdList||[]}`)
+        .then((res) => {
+            // console.log("xxxxx", res)
+            const data = res.list || [];
+            data.forEach((item: any) => {
+                item.StationID = item.stationId;
+                item.fastEquipmentInfos = [];
+                item.slowEquipmentInfos = [];
+                item.totalFee = Number(
+                    Number(
+                        Number(item.electricityFee) +
+                        Number(item.parkFee) +
+                        Number(item.serviceFee)
+                    ).toFixed(2)
+                );
+            });
+            return data.filter((item: any) => {
+                return !["1657"].includes(item.StationID);
+            });
+        });
+}
+
+export function _getDistance(lat1: number, lng1: number, lat2: number, lng2: number) {
+    var radLat1 = (lat1 * Math.PI) / 180.0;
+    var radLat2 = (lat2 * Math.PI) / 180.0;
+    var a = radLat1 - radLat2;
+    var b = (lng1 * Math.PI) / 180.0 - (lng2 * Math.PI) / 180.0;
+    var s =
+        2 *
+        Math.asin(
+            Math.sqrt(
+                Math.pow(Math.sin(a / 2), 2) +
+                Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2)
+            )
+        );
+    s = s * 6378.137; // EARTH_RADIUS;
+    s = Math.round(s * 10000) / 10000;
+    return s;
+}
+
+function _fetchStationStatus(list: any[]) {
+    if (list.length <= 0) {
+        return Promise.resolve([]);
+    }
+    let _list: any[] = [];
+    return cHttp
+        .get(
+            `/charge/stationStatus?stationIds=${list
+                .filter((item) => item.StationID !== "0000")
+                .map((item) => item.StationID)
+                .join(",")}`
+        )
+        .then((res) => {
+            const StationStatusInfos = res || [];
+            const ConnectorIDs: string[] = [];
+            const StationIDs: string[] = [];
+            console.log("_fetchStationStatus", list)
+            list.forEach((item: any) => {
+                item.fastEquipmentInfos = [];
+                item.slowEquipmentInfos = [];
+                const StationStatusInfo = StationStatusInfos.find(
+                    (status: any) => Number(status.stationId) === Number(item.StationID)
+                );
+                let ConnectorID = "";
+                item.equipmentInfos?.forEach((eqInfo: any) => {
+                    eqInfo.connectorInfos.forEach((coInfo: any) => {
+                        if (StationStatusInfo) {
+                            const connectorStatusInfo =
+                                StationStatusInfo.connectorStatusInfos.find(
+                                    (costatus: any) => costatus.connectorId === coInfo.connectorId
+                                );
+                            if (connectorStatusInfo) {
+                                // debug
+                                // connectorStatusInfo.status = 255
+                                coInfo.connectorStatusInfo = connectorStatusInfo;
+                            }
+                        }
+                        if (!ConnectorID) {
+                            ConnectorID = coInfo.connectorId;
+                        }
+                    });
+                    if ([1].includes(Number(eqInfo.equipmentType))) {
+                        item.fastEquipmentInfos.push(eqInfo);
+                    } else {
+                        item.slowEquipmentInfos.push(eqInfo);
+                    }
+                });
+                if (ConnectorID) {
+                    ConnectorIDs.push(ConnectorID);
+                    StationIDs.push(item.StationID);
+                }
+            });
+            _list = list;
+            return Promise.all(
+                ConnectorIDs.map((cid, cindex) => {
+                    return fetchStationPriceDesc(cid, StationIDs[cindex]);
+                })
+            );
+        })
+        .then((res) => {
+            if (res && res.length) {
+                _list.forEach((item) => {
+                    const i = res.findIndex((r) => r.StationID === item.StationID);
+                    if (i >= 0) {
+                        item.totalFee = res[i].currentPrice;
+                    }
+                });
+            }
+            return _list;
+        });
+
+
+}

+ 36 - 0
charge-front/src/api/index.ts

@@ -0,0 +1,36 @@
+import Http from "../utils/http";
+import { host } from "../utils/constant";
+
+const indexHttp = new Http(host);
+
+export function fetchContact() {
+  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}`);
+}

+ 217 - 0
charge-front/src/api/user.ts

@@ -0,0 +1,217 @@
+import Http from "../utils/http";
+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;
+  });
+}
+
+/**
+ * 查询我的权益和优惠券
+ */
+export function fetchRightsAndCoupons() {
+  return userHttp.get("/account/listRightsAndCoupons").then((res) => {
+    return res;
+  });
+}
+
+export function fetchCollectList() {
+  if (getApp<any>().globalData.collectIds) {
+    return Promise.resolve(getApp<any>().globalData.collectIds);
+  }
+  return userHttp
+    .get<
+      {
+        status: number;
+        stationId: number;
+      }[]
+    >("/user/collectList?page=1&page_size=999")
+    .then((res) => {
+      getApp<any>().globalData.collectIds = res
+        ? res
+            .filter((item) => Number(item.status) === 1)
+            .map((item) => {
+              return Number(item.stationId);
+            })
+        : [];
+      return getApp<any>().globalData.collectIds;
+    });
+}
+
+export function addCollectList(sid: number) {
+  let ids = getApp<any>().globalData.collectIds
+    ? (getApp<any>().globalData.collectIds as number[])
+    : [];
+  const status = ids.includes(sid) ? 0 : 1;
+  return userHttp
+    .post("/user/collect", {
+      data: {
+        stationId: sid,
+        status,
+      },
+    })
+    .then(() => {
+      if (status === 1) {
+        ids.push(sid);
+      } else {
+        ids = ids.filter((id) => id !== sid);
+      }
+      getApp<any>().globalData.collectIds = ids;
+      return status === 1;
+    });
+}
+
+export function updateProfile(data: any) {
+  return userHttp.put("/user", {
+    data,
+  });
+}
+
+declare const wx: any;
+
+export function insertMoney(amount: number,rechargeRightsId:string) {
+  if (isDebug) {
+    return Promise.resolve();
+  }
+  return userHttp
+    .post("/payment/wxPay", {
+      data: {
+        amount: parseInt(`${amount * 100}`),
+        openid: getApp<any>().globalData.user.openid,
+        rechargeRightsId
+      },
+    })
+    .then((res: any) => {
+      return new Promise((resolve, reject) => {
+        // #ifdef MP-WEIXIN
+        wx.requestPayment({
+          timeStamp: `${res.timeStamp}`,
+          nonceStr: res.nonceStr,
+          package: res.packageVal,
+          signType: res.signType,
+          paySign: res.paySign,
+          success(res: any) {
+            resolve(res);
+          },
+          fail: reject,
+        });
+        // #endif
+        // #ifndef MP-WEIXIN
+        reject({
+          errMsg: "目前仅支持微信支付",
+        });
+        // #endif
+      });
+    });
+}
+
+export function logout() {
+  return userHttp.get("/user/logout");
+}
+
+export function fetchWallet(
+  type: number,
+  page: number,
+  pageSize: number,
+  query?: string
+) {
+  return userHttp.get(
+    `/account/walletDetail?page=${page}&page_size=${pageSize}&type=${type}${
+      query ? "&" + query : ""
+    }`
+  );
+}
+
+export function fetchOrderDiscount(startChargeSeq: string,discountType:string) {
+  return userHttp.get(`/charge/orderDiscountDetail?startChargeSeq=${startChargeSeq}&discountType=${discountType}`);
+}
+
+
+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,
+    },
+  });
+}
+
+
+export function listRefund(param: any) {
+  return userHttp.post(`/account/listRefund`, {
+    data: {
+      ...param,
+    },
+  });
+}
+
+
+/**
+ * 获取站点可以领取的优惠券
+ * @param stationId
+ */
+export function listAvailableCoupons(stationId: any) {
+  return userHttp.get(`/coupon/listAvailableCoupons?stationId=${stationId}`, {
+  });
+}
+
+
+/**
+ * 获取站点可以领取的权益
+ * @param stationId
+ */
+export function listAvailableRights(stationId: any) {
+  return userHttp.get(`/rechargeRights/listAvailableRechargeRights?stationId=${stationId}`, {
+  });
+}
+
+
+/**
+ * 领取优惠券
+ * @param couponId
+ */
+export function collectCoupon (couponId: number) {
+  return userHttp.get(`/coupon/collectCoupon?couponId=${couponId}`, {
+  });
+}
+
+
+
+
+
+/**
+ * 获取当前用户当前站点有效的权益和优惠券
+ * @param stationId
+ */
+export function listStationAvailableRightsAndCoupons (stationId: string) {
+  return userHttp.get(`/account/listStationAvailableRightsAndCoupons?stationId=${stationId}`, {
+  });
+}
+
+
+

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

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

+ 318 - 0
charge-front/src/components/charge-station/charge-station.vue

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

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

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

+ 244 - 0
charge-front/src/components/order-coupon-detail/order-coupon-detail.vue

@@ -0,0 +1,244 @@
+<template>
+  <style-dialog
+      v-if="visible"
+      @close="handleClose"
+      title="优惠明细">
+
+    <view class="page pt-20 pr-40 pl-40">
+
+      <view v-if="mode===0">
+        <view
+            class="item mt-20 "
+            v-for="(item, index) in rightsList"
+            :key="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-justify-center pl-48">
+
+              <view class="item-content_left">
+                <view class="flex-align-end">
+                  <view class="item-header__left">
+                    <image class="width-84" src="/static/images/discount-rights-card.png" mode="widthFix"></image>
+                    <view class="fs-48 lh-58 color-000 fw-500 ml-16">
+                      {{ (item.discount / 10).toFixed(1) }}折
+                    </view>
+                    <view class="fs-26 color-999 ml-16" v-if="mode === 0"
+                    >抵扣充电服务费
+                    </view>
+                  </view>
+                </view>
+
+
+                <block>
+                  <view class="fs-26 color-999 mt-8"
+                        @click.stop="to(`/pages-common/activity/activity?id=${item.activityId}`)">
+                    <view class="fs-26 color-999">到期时间:{{ item.endTime }}</view>
+                    <view class="flex-align-center" v-if="mode === 0">
+                      <view class="fs-24 color-999">享受{{ (item.discount / 10).toFixed(1) }}折服务费权益,本次充电优惠:{{ ((item.orderDiscountAmount || 0) / 100).toFixed(2) }}元
+                                                <text class="color-primary">详细规则</text>
+                                                <text class="fs-24 ml-4 color-primary">>></text>
+                      </view>
+                    </view>
+                  </view>
+                </block>
+              </view>
+            </view>
+          </view>
+        </view>
+
+      </view>
+
+      <view v-if="mode===1">
+        <view
+            class="item mt-20 "
+            v-for="(item, index) in couponList"
+            :class="item.collected?'inactive':''"
+            :key="index">
+          <image class="full-percent" src="/static/images/discount-bg.png"></image>
+          <view class="absolute-full flex pt-8 pb-8">
+            <view class="left  flex-justify-center pl-48">
+              <view class="item-content_left">
+                <view class="flex-align-end">
+                  <view class="item-header__left">
+
+                    <image
+                        class="width-84"
+                        src="/static/images/discount-coupon.png"
+                        mode="widthFix"
+                    ></image>
+                    <view></view>
+                    <view class="fs-48 lh-58 color-000 fw-500 ml-16" v-if="item.couponType=='FullDiscount'">
+                      {{ ((item.discount || 0) / 100).toFixed(2) }}元
+                    </view>
+
+                    <view class="fs-48 lh-58 color-000 fw-500 ml-16" v-else>
+                      {{ (item.discount / 10).toFixed(1) }}折
+                    </view>
+
+                    <view class="fs-26 color-999 ml-16">
+                      充电服务费:{{ item.couponType == 'FullDiscount' ? '满减券' : '折扣券' }}
+                    </view>
+                  </view>
+
+                </view>
+
+                <block>
+                  <view class="fs-26 color-999 mt-8"
+                        @click.stop="to(`/pages-common/activity/activity?id=${item.activityId}`)">
+                    <view class="fs-26 color-999">到期时间:{{ item.endTime }}</view>
+                    <view class="flex-align-center">
+                      <view class="fs-26 color-999">本次充电优惠:{{ ((item.orderDiscountAmount || 0) / 100).toFixed(2) }}元
+                        <text class="color-primary">详细规则</text>
+                        <text class="fs-24 ml-4 color-primary">>></text>
+                      </view>
+                    </view>
+                  </view>
+                </block>
+              </view>
+            </view>
+          </view>
+        </view>
+      </view>
+
+
+    </view>
+    >
+  </style-dialog>
+
+</template>
+
+<script setup lang="ts">
+import {ref} from "vue";
+import {fetchOrderDiscount} from "@/api/user";
+import {to} from "@/utils/navigate";
+
+const mode = ref(0);
+const rightsList = ref<any[]>([]);
+const couponList = ref<any[]>([]);
+const visible = ref(false);
+
+const handleClose = () => {
+  visible.value = false;
+}
+
+
+const open = (startChargeSeq: string, discountType: string) => {
+  visible.value = true;
+  fetchOrderDiscount(startChargeSeq, discountType).then((res) => {
+    console.log(res)
+    if (res) {
+      if (discountType == "RechargeRights") {
+        rightsList.value = [res]
+        mode.value = 0;
+      } else {
+        couponList.value = [res]
+        mode.value = 1;
+      }
+    }
+  })
+}
+
+
+defineExpose({
+  open
+})
+</script>
+
+<style lang="scss" scoped>
+.page {
+  min-height: 100vh;
+  width: 100%;
+  background-color: #f6f7fa;
+
+  .item {
+    position: relative;
+    min-height: 220rpx;
+    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;
+      }
+    }
+  }
+
+  .tab-list {
+    width: 100%;
+
+    .w50 {
+      width: 50%;
+      text-align: center;
+    }
+
+    .active {
+      color: var(--color-primary);
+    }
+  }
+}
+
+.item-status {
+  font-size: 24rpx;
+  padding: 0rpx 6rpx;
+  color: var(--color-primary);
+  border: 2rpx solid var(--color-primary);
+  border-radius: 6rpx;
+
+}
+
+.item-header__left {
+  display: inline-flex;
+  align-items: flex-end;
+}
+
+.item-header__right {
+  padding: 5rpx;
+  display: flex;
+  //justify-content: flex-end;
+}
+
+.inactive {
+  cursor: not-allowed;
+  pointer-events: none;
+
+  .item-status {
+    font-size: 24rpx;
+    padding: 0rpx 6rpx;
+    color: #ccc !important;
+    border: 2rpx solid #ccc;
+    border-radius: 6rpx;
+
+  }
+
+  .color-primary {
+    color: #ccc !important;
+  }
+}
+
+.item-content_left {
+  flex-grow: 1;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-around;
+
+}
+
+.item-content_right {
+  width: 120rpx;
+  display: flex;
+  align-items: center;
+  align-content: center;
+}
+</style>

+ 87 - 0
charge-front/src/components/shadow-card/shadow-card.vue

@@ -0,0 +1,87 @@
+<template>
+  <view class="shadow-card">
+    <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"
+          :style="{
+          color: item.color ? item.color : '#000',
+        }">
+        {{ item.value }}
+        <text class="copy-btn" v-if="item.copy" @click="copyText(item.value)">复制</text>
+        <text class="copy-btn" v-if="item.discount" @click="handleClickOrderDiscountDesc">明细</text>
+      </view>
+
+    </view>
+  </view>
+</template>
+
+<script lang="ts">
+export default {
+  props: {
+    list: {
+      type: Array<any>,
+      defalut: [],
+    },
+  },
+  methods: {
+    copyText(text: string) {
+      if (!text) return;
+      uni.setClipboardData({
+        data: text,
+        success: function () {
+          uni.showToast({
+            title: '复制成功',
+            icon: 'success',
+          })
+        }
+      })
+    },
+    handleClickOrderDiscountDesc(){
+      this.$emit('on-discount')
+    }
+  }
+};
+</script>
+
+<style lang="scss">
+.shadow-card {
+  background: #ffffff;
+  border: 1rpx solid rgba(0, 0, 0, 0.1);
+  box-shadow: 0px 4rpx 20rpx rgba(0, 0, 0, 0.1);
+  border-radius: 20rpx;
+  padding: 40rpx;
+
+  & > view {
+    display: flex;
+
+    .label {
+      width: 156rpx;
+      flex-shrink: 0;
+      color: rgba(0, 0, 0, 0.4);
+      font-size: 26rpx;
+    }
+
+    .value {
+      flex-grow: 1;
+      font-size: 26rpx;
+      overflow-wrap: anywhere;
+    }
+  }
+}
+
+.copy-btn {
+  margin-left: 10rpx;
+  color: #1aa4f2;
+  /*  padding: 6rpx;
+    background: #1aa4f2;*/
+  font-size: 20rpx;
+}
+</style>

+ 372 - 0
charge-front/src/components/station-charge-coupon/station-charge-coupon.vue

@@ -0,0 +1,372 @@
+<template>
+  <style-dialog
+      v-if="visible"
+      confirm
+      @ok="handleConfirm"
+      @close="handleClose"
+      title="选择优惠">
+
+    <view class="page pt-20 pr-40 pl-40">
+      <view class="tab-list flex-align-end">
+        <view class="fs-28 color-666 mt-20 w50" :class="mode===0?'active':''" @click="changeTab(0)">权益卡</view>
+        <view class="fs-28 color-666 mt-20 w50" :class="mode===1?'active':''" @click="changeTab(1)">优惠券</view>
+      </view>
+      <view v-if="mode===0">
+        <view
+            class="item mt-20 "
+            v-for="(item, index) in rightsList"
+            :key="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-justify-center pl-48">
+
+              <view class="item-content_left">
+                <view class="flex-align-end">
+                  <view class="item-header__left">
+                    <image class="width-84" src="/pages-charge/static/discount-rights-card.png" mode="widthFix"></image>
+                    <view class="fs-48 lh-58 color-000 fw-500 ml-16">
+                      {{ (item.discount / 10).toFixed(1) }}折
+                    </view>
+                    <view class="fs-26 color-999 ml-16" v-if="mode === 0"
+                    >抵扣充电服务费
+                    </view>
+                  </view>
+                </view>
+
+
+                <block>
+                  <view class="fs-26 color-999 mt-8"
+                        @click.stop="to(`/pages-common/activity/activity?id=${item.activityId}`)">
+                    <view class="fs-26 color-999">到期时间:{{ item.endTime }}</view>
+                    <view class="flex-align-center" v-if="mode === 0">
+                      <view class="fs-26 color-999">权益余额:{{ ((item.rightsBalance || 0) / 100).toFixed(2) }}元
+                        <text class="color-primary">详细规则</text>
+                        <text class="fs-24 ml-4 color-primary">>></text>
+                      </view>
+                    </view>
+                  </view>
+                </block>
+              </view>
+              <view class="item-content_right">
+                <style-checkbox
+                    @on-checkChange="handleCheckRights($event,item.rightsId,item.id)"
+                    :size="48"
+                    :checked="item.rightsId === rightsId"
+                    :iconsize="17"
+                ></style-checkbox>
+              </view>
+
+            </view>
+          </view>
+        </view>
+        <view style="width: 100%;text-align: center;">
+          <image v-if="!rightsList||rightsList.length==0" style="width: 240rpx;" mode="widthFix" src="/static/images/search-empty.png"></image>
+        </view>
+
+      </view>
+
+
+      <view v-else-if="mode===1">
+        <view
+            class="item mt-20"
+            v-for="(item, index) in couponList"
+            :key="index">
+
+          <!--            :class="item.usageStatus==1||item.status==0?'inactive':''"-->
+          <view class="absolute-full flex pt-8 pb-8">
+            <view class="left  flex-justify-center pl-48">
+              <view class="item-content_left">
+                <view class="flex-align-end">
+                  <view class="item-header__left">
+
+                    <image
+                        class="width-84"
+                        src="/pages-charge/static/discount-coupon.png"
+                        mode="widthFix"
+                    ></image>
+                    <view></view>
+                    <view class="fs-48 lh-58 color-000 fw-500 ml-16" v-if="item.couponType=='FullDiscount'">
+                      {{ ((item.discount || 0) / 100).toFixed(2) }}元
+                    </view>
+
+                    <view class="fs-48 lh-58 color-000 fw-500 ml-16" v-else>
+                      {{ (item.discount / 10).toFixed(1) }}折
+                    </view>
+
+                    <view class="fs-26 color-999 ml-16">
+                      充电服务费:{{ item.couponType == 'FullDiscount' ? '满减券' : '折扣券' }}
+                    </view>
+                  </view>
+
+                </view>
+
+                <block v-if="mode === 1">
+                  <view class="fs-26 color-999 mt-8"
+                        @click.stop="to(`/pages-common/activity/activity?id=${item.activityId}`)">
+                    <view class="fs-26 color-999">到期时间:{{ item.endTime }}</view>
+                    <view class="flex-align-center">
+                      <view class="fs-26 color-999">服务费门槛:{{ ((item.minServiceMoney || 0) / 100).toFixed(2) }}元
+                        <text class="color-primary">详细规则</text>
+                        <text class="fs-24 ml-4 color-primary">>></text>
+                      </view>
+                    </view>
+                  </view>
+                </block>
+              </view>
+              <view class="item-content_right">
+                <view class="item-content_right">
+                  <!--                  <radio-group @change="handleCheckCoupon($event,item.couponId)">
+                                      <radio  :checked="item.couponId === couponId"/>
+                                    </radio-group>-->
+                  <style-checkbox
+                      @on-checkChange="handleCheckCoupon($event,item.couponId,item.id)"
+                      :size="48"
+                      :checked="item.couponId === couponId"
+                      :iconsize="17"
+                  ></style-checkbox>
+                </view>
+              </view>
+
+
+            </view>
+          </view>
+        </view>
+
+        <view style="width: 100%;text-align: center;">
+          <image v-if="!couponList||couponList.length==0" style="width: 240rpx;" mode="widthFix" src="/static/images/search-empty.png"></image>
+        </view>
+      </view>
+
+    </view>
+
+  </style-dialog>
+
+</template>
+
+<script setup lang="ts">
+import {onPullDownRefresh} from "@dcloudio/uni-app";
+import {ref} from "vue";
+import {listStationAvailableRightsAndCoupons} from "@/api/user";
+import {to} from "@/utils/navigate";
+
+const mode = ref(0);
+const couponList = ref<any[]>([]);
+const rightsList = ref<any[]>([]);
+
+const stationId = ref(null);
+const rightsId = ref(null);
+const userRechargeRightsId = ref(null);
+const couponId = ref(null);
+const userCouponId = ref(null);
+const visible = ref(false);
+
+const emits = defineEmits(['on-change'])
+
+const handleClose = () => {
+  visible.value = false;
+  rightsId.value = null;
+  userRechargeRightsId.value = null;
+  couponId.value = null;
+  userCouponId.value = null;
+  stationId.value = null;
+  mode.value = 0;
+}
+
+const handleConfirm = () => {
+  let emitData = {
+    rightsId: rightsId.value,
+    userRechargeRightsId: userRechargeRightsId.value,
+    couponId: couponId.value,
+    userCouponId: userCouponId.value,
+    stationId: stationId.value,
+    userRights: rightsList.value?.find((k: any) => k.rightsId == rightsId.value ),
+    userCoupon: couponList.value?.find((k: any) => k.couponId == couponId.value),
+  }
+   console.log(emitData)
+  emits('on-change', emitData)
+  handleClose();
+}
+
+onPullDownRefresh(() => {
+  loadData();
+});
+
+const changeTab = (tab: number) => {
+  mode.value = tab;
+}
+
+/**
+ *
+ * @param val
+ * @param rId rechargeRightsId
+ * @param urId userRechargeRightsId
+ */
+const handleCheckRights = (val: boolean, rId: any,urId: any) => {
+  // console.log("handleCheckRights", userRightsId, val)
+  if (rightsId.value == rId) {
+    rightsId.value = null;
+    userRechargeRightsId.value = null;
+  } else {
+    rightsId.value = rId;
+    userRechargeRightsId.value = urId;
+    couponId.value = null;
+  }
+}
+
+/**
+ *
+ * @param e
+ * @param cid couponId
+ * @param ucid userCouponId
+ */
+const handleCheckCoupon = (e: any, cid: any,ucid: any) => {
+  // console.log("handleCheckCoupon", userCouponId, e)
+  if (couponId.value == cid) {
+    couponId.value = null;
+    userCouponId.value = null;
+  } else {
+    couponId.value = cid;
+    userCouponId.value = ucid;
+    rightsId.value = null;
+  }
+}
+
+
+const loadData = () => {
+  uni.showLoading({
+    title: "加载中",
+  });
+  listStationAvailableRightsAndCoupons(stationId.value).then((res) => {
+    let {userRechargeRightsList, userCouponList} = res;
+    couponList.value = (userCouponList || []).sort((a: any, b: any) => new Date(a.endTime).getTime() - new Date(b.endTime).getTime());
+    rightsList.value = (userRechargeRightsList || []).sort((a: any, b: any) => new Date(a.endTime).getTime() - new Date(b.endTime).getTime());
+    if (!rightsId.value && !couponId.value) {
+      if (rightsList.value && rightsList.value.length > 0) {
+        rightsId.value = rightsList.value[0].rightsId;
+        mode.value = 0;
+      } else if (couponList.value && couponList.value.length > 0) {
+        couponId.value = couponList.value[0].couponId;
+        mode.value = 1;
+      }
+    } else if (rightsId.value) {
+      mode.value = 0;
+    } else if (couponId.value) {
+      mode.value = 1;
+    }
+
+    // console.log(res)
+    uni.hideLoading();
+  });
+}
+const open = (sid: any, userRightsId: any, userCouponId: any) => {
+  visible.value = true;
+  stationId.value = sid;
+  rightsId.value = userRightsId;
+  couponId.value = userCouponId;
+  loadData()
+}
+
+
+defineExpose({
+  open
+})
+</script>
+
+<style lang="scss" scoped>
+.page {
+  min-height: 100vh;
+  width: 100%;
+  background-color: #f6f7fa;
+
+  .item {
+    position: relative;
+    min-height: 220rpx;
+    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;
+      }
+    }
+  }
+
+  .tab-list {
+    width: 100%;
+
+    .w50 {
+      width: 50%;
+      text-align: center;
+    }
+
+    .active {
+      color: var(--color-primary);
+    }
+  }
+}
+
+.item-status {
+  font-size: 24rpx;
+  padding: 0rpx 6rpx;
+  color: var(--color-primary);
+  border: 2rpx solid var(--color-primary);
+  border-radius: 6rpx;
+
+}
+
+.item-header__left {
+  display: inline-flex;
+  align-items: flex-end;
+}
+
+.item-header__right {
+  padding: 5rpx;
+  display: flex;
+  //justify-content: flex-end;
+}
+
+.inactive {
+  cursor: not-allowed;
+  pointer-events: none;
+
+  .item-status {
+    font-size: 24rpx;
+    padding: 0rpx 6rpx;
+    color: #ccc !important;
+    border: 2rpx solid #ccc;
+    border-radius: 6rpx;
+
+  }
+
+  .color-primary {
+    color: #ccc !important;
+  }
+}
+
+.item-content_left {
+  flex-grow: 1;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-around;
+
+}
+
+.item-content_right {
+  width: 120rpx;
+  display: flex;
+  align-items: center;
+  align-content: center;
+}
+</style>

+ 317 - 0
charge-front/src/components/station-coupon/station-coupon.vue

@@ -0,0 +1,317 @@
+<template>
+  <style-dialog
+      v-if="visible"
+      @close="handleClose"
+      title="优惠领取">
+
+    <view class="page pt-20 pr-40 pl-40">
+      <view class="tab-list flex-align-end">
+        <view class="fs-28 color-666 mt-20 w50" :class="mode===0?'active':''" @click="changeTab(0)">权益卡</view>
+        <view class="fs-28 color-666 mt-20 w50" :class="mode===1?'active':''" @click="changeTab(1)">优惠券</view>
+      </view>
+
+      <view v-if="mode===0">
+        <view
+            class="item mt-20 "
+            v-for="(item, index) in rightsList"
+            :key="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-justify-center pl-48">
+
+              <view class="item-content_left">
+                <view class="flex-align-end">
+                  <view class="item-header__left">
+                    <image class="width-84" src="/static/images/discount-rights-card.png" mode="widthFix"></image>
+                    <view class="fs-48 lh-58 color-000 fw-500 ml-16">
+                      {{ (item.discount / 10).toFixed(1) }}折
+                    </view>
+                    <view class="fs-26 color-999 ml-16" v-if="mode === 0"
+                    >抵扣充电服务费
+                    </view>
+                  </view>
+                </view>
+
+
+                <block>
+                  <view class="fs-26 color-999 mt-8"
+                        @click.stop="to(`/pages-common/activity/activity?id=${item.activityId}`)">
+                    <view class="fs-26 color-999">到期时间:{{ item.endTime }}</view>
+                    <view class="flex-align-center" v-if="mode === 0">
+                      <view class="fs-24 color-999">充值{{ ((item.amountMin || 0) / 100).toFixed(2) }}元,享受{{ (item.discount / 10).toFixed(1) }}折服务费权益,有效期{{item.validity}}天
+<!--                        <text class="color-primary">详细规则</text>
+                        <text class="fs-24 ml-4 color-primary">>></text>-->
+                      </view>
+                    </view>
+                  </view>
+                </block>
+              </view>
+              <view class="item-content_right">
+                <text class="item-content_right-charge item-status ml-10" @tap.stop="handleCharge(item)">去充值</text>
+              </view>
+
+            </view>
+          </view>
+        </view>
+
+
+        <view style="width: 100%;text-align: center;display: flex;flex-direction: column;align-content: center;align-items: center;">
+          <image v-if="!rightsList||rightsList.length==0"
+                 style="width: 240rpx;" mode="widthFix"
+                 src="/static/images/search-empty.png"></image>
+
+<!--          <text style="font-size: 22rpx;">暂无权益</text>-->
+        </view>
+
+
+<!--        <view style="width: 100%;text-align: center;">
+          <image v-if="!rightsList||rightsList.length==0" style="width: 240rpx;" mode="widthFix" src="/static/images/search-empty.png"></image>
+        </view>-->
+
+      </view>
+
+      <view v-if="mode===1">
+        <view
+            class="item mt-20 "
+            v-for="(item, index) in couponList"
+            :class="item.collected?'inactive':''"
+            :key="index">
+          <image class="full-percent" src="/static/images/discount-bg.png"></image>
+          <view class="absolute-full flex pt-8 pb-8">
+            <view class="left  flex-justify-center pl-48">
+              <view class="item-content_left">
+                <view class="flex-align-end">
+                  <view class="item-header__left">
+
+                    <image
+                        class="width-84"
+                        src="/static/images/discount-coupon.png"
+                        mode="widthFix"
+                    ></image>
+                    <view></view>
+                    <view class="fs-48 lh-58 color-000 fw-500 ml-16" v-if="item.couponType=='FullDiscount'">
+                      {{ ((item.discount || 0) / 100).toFixed(2) }}元
+                    </view>
+
+                    <view class="fs-48 lh-58 color-000 fw-500 ml-16" v-else>
+                      {{ (item.discount / 10).toFixed(1) }}折
+                    </view>
+
+                    <view class="fs-26 color-999 ml-16">
+                      充电服务费:{{ item.couponType == 'FullDiscount' ? '满减券' : '折扣券' }}
+                    </view>
+                  </view>
+
+                </view>
+
+                <block>
+                  <view class="fs-26 color-999 mt-8"
+                        @click.stop="to(`/pages-common/activity/activity?id=${item.activityId}`)">
+                    <view class="fs-26 color-999">到期时间:{{ item.endTime }}</view>
+                    <view class="flex-align-center">
+                      <view class="fs-26 color-999">服务费门槛:{{ ((item.minServiceMoney || 0) / 100).toFixed(2) }}元
+                        <text class="color-primary">详细规则</text>
+                        <text class="fs-24 ml-4 color-primary">>></text>
+                      </view>
+                    </view>
+                  </view>
+                </block>
+              </view>
+              <view class="item-content_right">
+                <view class="item-status ml-10" @tap.stop="handleCollectCoupon(item,index)">{{ item.collected ? '已领取' : '领取' }}</view>
+              </view>
+            </view>
+          </view>
+        </view>
+
+        <view style="width: 100%;text-align: center;display: flex;flex-direction: column;align-content: center;align-items: center;">
+          <image v-if="!couponList||couponList.length==0"
+                 style="width: 240rpx;" mode="widthFix"
+                 src="/static/images/search-empty.png"></image>
+
+<!--          <text style="font-size: 22rpx;">暂无优惠券</text>-->
+        </view>
+      </view>
+
+
+    </view>
+    >
+  </style-dialog>
+
+</template>
+
+<script setup lang="ts">
+import {onPullDownRefresh} from "@dcloudio/uni-app";
+import {ref} from "vue";
+import {collectCoupon, listAvailableCoupons, listAvailableRights} from "@/api/user";
+import {to} from "@/utils/navigate";
+
+const mode = ref(0);
+const rightsList = ref<any[]>([]);
+const couponList = ref<any[]>([]);
+const stationId = ref(null);
+const visible = ref(false);
+
+const handleClose = () => {
+  visible.value = false;
+}
+
+onPullDownRefresh(() => {
+  loadData();
+});
+
+const changeTab = (tab: number) => {
+  mode.value = tab;
+}
+
+const handleCharge = (rights:any) => {
+  to(`/pages-user/wallet-recharge/wallet-recharge?discount=${rights.amountMin}`)
+}
+
+const handleCollectCoupon = (coupon: any, index: number) => {
+  if (coupon.collected) return;
+  collectCoupon(coupon.id).then((res) => {
+    console.log(res)
+    uni.showToast({
+      title: "领券成功",
+      icon: "success"
+    });
+    couponList.value[index].collected = true;
+  }).catch(e => {
+    console.error(e)
+    if (e.errMsg) {
+      uni.showToast({
+        title: e.errMsg,
+        icon: "none"
+      });
+    }
+  });
+}
+
+
+const loadData = () => {
+  uni.showLoading({
+    title: "加载中",
+  });
+  Promise.all([listAvailableCoupons(stationId.value), listAvailableRights(stationId.value)]).then((values) => {
+    console.log(values)
+    uni.hideLoading();
+    couponList.value = values[0].list||[];
+    rightsList.value = values[1].list||[];
+  })
+ /* listAvailableCoupons(stationId.value).then((res) => {
+    console.log(res)
+    uni.hideLoading();
+    couponList.value = res.list;
+  });*/
+}
+const open = (sid: any) => {
+  visible.value = true;
+  stationId.value = sid;
+  loadData()
+}
+
+
+defineExpose({
+  open
+})
+</script>
+
+<style lang="scss" scoped>
+.page {
+  min-height: 100vh;
+  width: 100%;
+  background-color: #f6f7fa;
+
+  .item {
+    position: relative;
+    min-height: 220rpx;
+    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;
+      }
+    }
+  }
+
+  .tab-list {
+    width: 100%;
+
+    .w50 {
+      width: 50%;
+      text-align: center;
+    }
+
+    .active {
+      color: var(--color-primary);
+    }
+  }
+}
+
+.item-status {
+  font-size: 24rpx;
+  padding: 0rpx 6rpx;
+  color: var(--color-primary);
+  border: 2rpx solid var(--color-primary);
+  border-radius: 6rpx;
+
+}
+
+.item-header__left {
+  display: inline-flex;
+  align-items: flex-end;
+}
+
+.item-header__right {
+  padding: 5rpx;
+  display: flex;
+  //justify-content: flex-end;
+}
+
+.inactive {
+  cursor: not-allowed;
+  pointer-events: none;
+
+  .item-status {
+    font-size: 24rpx;
+    padding: 0rpx 6rpx;
+    color: #ccc !important;
+    border: 2rpx solid #ccc;
+    border-radius: 6rpx;
+
+  }
+
+  .color-primary {
+    color: #ccc !important;
+  }
+}
+
+.item-content_left {
+  flex-grow: 1;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-around;
+
+}
+
+.item-content_right {
+  width: 120rpx;
+  display: flex;
+  align-items: center;
+  align-content: center;
+}
+</style>

+ 60 - 0
charge-front/src/components/style-bottom-view/style-bottom-view.vue

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

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

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

+ 45 - 0
charge-front/src/components/style-checkbox/style-checkbox.vue

@@ -0,0 +1,45 @@
+<template>
+  <view
+    class="lh-24 br-round flex-center"
+    @click.stop="handleClick"
+    :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)',
+    }"
+  >
+    <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,
+    },
+  },
+  methods:{
+    handleClick(){
+      console.log(!this.checked)
+      this.$emit('on-checkChange',!this.checked);
+    }
+  }
+};
+</script>
+
+<style lang="scss"></style>

+ 87 - 0
charge-front/src/components/style-dialog/style-dialog.vue

@@ -0,0 +1,87 @@
+<template>
+  <view class="style-dialog" @click.stop>
+    <view :style="{ background }">
+      <view class="style-dialog_head flex-center">
+        <view class="ok" @click.stop="close">
+          <text type="success" v-if="confirm" @click.stop="handleConfirm">确定</text>
+        </view>
+        <view class="fw-500 color-000 fs-32">{{ title }}</view>
+        <view class="close" @click.stop="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",
+    },
+    confirm:{
+      type:Boolean,
+      default:false
+    }
+  },
+  methods: {
+    close() {
+      this.$emit("close");
+    },
+    handleConfirm(){
+      this.$emit('ok');
+    }
+  },
+};
+</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%);
+    }
+
+    .ok {
+      position: absolute;
+      left: 30rpx;
+      top: 50%;
+      transform: translateY(-50%);
+      color: var(--color-primary);
+    }
+  }
+  &_body {
+    max-height: 1000rpx;
+    overflow-y: auto;
+  }
+}
+</style>

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

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

+ 53 - 0
charge-front/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>

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

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

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

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

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

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

BIN
charge-front/src/components/uni-icons/uniicons.ttf


+ 76 - 0
charge-front/src/custom-tab-bar/index.js

@@ -0,0 +1,76 @@
+import { fetchToken, login, onLogin } from "../api/auth";
+
+Component({
+  data: {
+    hidden: false,
+    tabIndex: -1,
+    tabs: [
+      {
+        path: "/pages/map/map",
+        text: "充电地图",
+      },
+      {
+        path: "/pages-charge/camera/camera",
+        text: "扫码充电",
+      },
+      {
+        path: "/pages/user/user",
+        text: "个人中心",
+      },
+    ],
+  },
+  lifetimes: {
+    ready() {
+      fetchToken().then((token) => {
+        const pages = getCurrentPages();
+        const index = this.data.tabs.findIndex(
+          (item) => item.path === `/${pages[pages.length - 1].route}`
+        );
+        this.setData({
+          hidden: false,
+          tabIndex: index,
+          token,
+        });
+        if (!token) {
+          onLogin((token) => {
+            this.setData({
+              token,
+            });
+          });
+        }
+      });
+    },
+  },
+  methods: {
+    switchTab(e) {
+      const data = e.currentTarget.dataset;
+      const url = data.path;
+      if (/camera/.test(url)) {
+        wx.navigateTo({
+          url,
+        });
+        return;
+      }
+      wx.vibrateShort &&
+        wx.vibrateShort({
+          type: "light",
+        });
+      wx.switchTab({
+        url,
+      });
+    },
+    hiddenTab() {
+      this.setData({
+        hidden: true,
+      });
+    },
+    showTab() {
+      this.setData({
+        hidden: false,
+      });
+    },
+    login(e) {
+      login(e);
+    },
+  },
+});

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

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

+ 18 - 0
charge-front/src/custom-tab-bar/index.wxml

@@ -0,0 +1,18 @@
+<view class="tab-bar {{hidden ? 'tab-bar-hidden' : ''}}">
+  <image class="tab-bar-bg" src="/static/images/custom-tab-bar/bg.png" mode="widthFix" />
+  <view class="tab-bar-main">
+    <view wx:for="{{tabs}}" wx:key="index" class="tab-bar-item" data-path="{{item.path}}" data-index="{{index}}" bindtap="switchTab">
+      <block wx:if="{{index === 1}}">
+        <image class="image image-big" src="/static/images/custom-tab-bar/2.png"></image>
+        <image class="image" style="opacity: 0;" src="/static/images/custom-tab-bar/2.png"></image>
+        <view class="text {{tabIndex === index ? 'selected-color' : ''}}">{{item.text}}</view>
+      </block>
+      <block wx:else>
+        <image wx:if="{{tabIndex === index}}" class="image" src="/static/images/custom-tab-bar/{{index+1}}-1.png"></image>
+        <image wx:else class="image" src="/static/images/custom-tab-bar/{{index+1}}.png" mode="widthFix" />
+        <view class="text {{tabIndex === index ? 'selected-color' : ''}}">{{item.text}}</view>
+      </block>
+    </view>
+  </view>
+  <button class="tab-bar-login" wx:if="{{!token}}" style="width: 100vw;height: var(--tab-bar-height);border: none;" open-type="getPhoneNumber" bindgetphonenumber="login"></button>
+</view>

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

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

+ 8 - 0
charge-front/src/env.d.ts

@@ -0,0 +1,8 @@
+/// <reference types="vite/client" />
+
+declare module '*.vue' {
+  import { DefineComponent } from 'vue'
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
+  const component: DefineComponent<{}, {}, any>
+  export default component
+}

+ 8 - 0
charge-front/src/main.ts

@@ -0,0 +1,8 @@
+import { createSSRApp } from "vue";
+import App from "./App.vue";
+export function createApp() {
+  const app = createSSRApp(App);
+  return {
+    app,
+  };
+}

+ 60 - 0
charge-front/src/manifest.json

@@ -0,0 +1,60 @@
+{
+  "name": "",
+  "appid": "",
+  "description": "",
+  "versionName": "1.0.0",
+  "versionCode": "100",
+  "transformPx": false,
+  "mp-weixin": {
+    "optimization": { "subPackages": true },
+    "appid": "wx369fcff95d387bde",
+    "setting": {
+      "urlCheck": false,
+      "es6": true,
+      "enhance": true,
+      "postcss": true,
+      "preloadBackgroundData": false,
+      "minified": true,
+      "newFeature": false,
+      "coverView": true,
+      "nodeModules": false,
+      "autoAudits": false,
+      "showShadowRootInWxmlPanel": true,
+      "scopeDataCheck": false,
+      "uglifyFileName": true,
+      "checkInvalidKey": true,
+      "checkSiteMap": true,
+      "uploadWithSourceMap": true,
+      "compileHotReLoad": false,
+      "lazyloadPlaceholderEnable": false,
+      "useMultiFrameRuntime": true,
+      "useIsolateContext": false,
+      "userConfirmedBundleSwitch": false,
+      "minifyWXSS": true,
+      "disableUseStrict": false,
+      "minifyWXML": true,
+      "showES6CompileOption": false,
+      "ignoreUploadUnusedFiles": true,
+      "useStaticServer": true,
+      "condition": false
+    },
+    "useExtendedLib": {
+      "weui": true
+    },
+    "style": "v2",
+    "darkmode": false,
+    "resizable": false,
+    "lazyCodeLoading": "requiredComponents",
+    "usingComponents": true,
+    "requiredPrivateInfos": ["getLocation"],
+    "permission": {
+      "scope.userLocation": {
+        "desc": "你的位置信息将用于获取附近充电桩位置"
+      }
+    }
+  },
+  "uniStatistics": {
+    "enable": false
+  },
+  "vueVersion": "3"
+}

+ 861 - 0
charge-front/src/pages-charge/appointment/appointment-backup.vue

@@ -0,0 +1,861 @@
+<template>
+  <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 ml-30 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-28 color-000">充电费用</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 ml-30 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/discountV2?rightsId=${userRechargeRightsId}&couponId=${userCouponId}&stationId=${stationId}`
+            )
+          "
+        >
+          <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.toFixed(1) }}折
+            </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="#2d9e95"
+                @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 {cancelAppointmentCharge, changeAppointmentTime, fetchChargeStatus, fetchStationByConnectorIdOrShortId, fetchStationPriceDesc, startAppointmentCharge, startCharge,} 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 {redirect, to} 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 userRechargeRightsId = ref();
+const userCouponId = ref();
+const stationId = 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: "#2d9e95",
+      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 = {
+    time: 0, // 时间戳
+    day: "",
+    hour: "",
+    isPowerSaving: e.detail.value,
+    format: "",
+    formatPrice: "",
+    scrollId: "",
+  };
+};
+
+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: "#2d9e95",
+    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);
+        let {station, equipment} = res;
+        if (station) {
+          stationId.value = station.StationID;
+        }
+        if (equipment && equipment.connectorInfos && equipment.connectorInfos.length) {
+          sn = equipment.connectorInfos[0].connectorId;
+          _options.sn = 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(1)
+      );
+    }
+  });
+
+});
+
+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>

+ 981 - 0
charge-front/src/pages-charge/appointment/appointment.vue

@@ -0,0 +1,981 @@
+<template>
+  <view :class="['page']">
+    <view v-if="station && priceInfo">
+      <view class="block">
+        <view class="station">
+          <ChargeMachine
+              :title="'NO.' + equipment.shortId"
+              :list="[equipment]"
+              :time="priceInfo.useTime"
+              :parkingNo="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">{{ station.address }}</view>
+        </view>
+      </view>
+      <!--      <view class="mt-40 ml-30 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-28 color-000">充电费用</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"
+            >注意:启动充电前请勿拔插充电枪!
+            </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 ml-30 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="handleShowStationChargeCoupon"
+        >
+          <!--          to(-->
+          <!--          `/pages-charge/discount/discountV2?rightsId=${userRechargeRightsId}&couponId=${userCouponId}&stationId=${stationId}`-->
+          <!--          )-->
+          <view class="fs-28 lh-28 color-000 fw-500">充电服务费优惠</view>
+          <block v-if="checkedUserRightsOrCoupon&&checkedUserRightsOrCoupon.rightsId">
+            <view class="fs-28 lh-28 ml-auto" style="color: #f43636"
+            >{{ ((checkedUserRightsOrCoupon?.userRights?.discount || 0) / 10).toFixed(1) }}折
+            </view
+            >
+            <uni-icons type="right" size="16" color="#CACACA"></uni-icons>
+          </block>
+          <block v-else-if="checkedUserRightsOrCoupon&&checkedUserRightsOrCoupon.couponId">
+            <view class="fs-28 lh-28 ml-auto" style="color: #f43636" v-if="checkedUserRightsOrCoupon.userCoupon.couponType=='FullDiscount'">
+              {{ ((checkedUserRightsOrCoupon?.userCoupon?.discount || 0) / 100).toFixed(2) }}元
+            </view>
+            <view class="fs-28 lh-28 ml-auto" style="color: #f43636" v-else>
+              {{ ((checkedUserRightsOrCoupon?.userCoupon?.discount || 0) / 10).toFixed(1) }}折
+            </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="#2d9e95"
+                @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>
+          <template v-else>
+            <style-button v-if="balance>0" type="primary" size="small" @click="submit">{{
+                chargeType === 0 ? "提交预约" : "立即充电"
+              }}
+            </style-button>
+
+            <style-button v-else type="primary" size="small" @click="to('/pages-user/wallet-recharge/wallet-recharge')">
+              余额不足,请充值
+            </style-button>
+          </template>
+
+        </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>
+
+    <StationChargeCoupon ref="station_charge_coupon_ref" @on-change="handleCheckedUserRightsAndCoupon"></StationChargeCoupon>
+  </view>
+</template>
+
+<script setup lang="ts">
+import {
+  cancelAppointmentCharge,
+  changeAppointmentTime,
+  fetchChargeStatus,
+  fetchStationByConnectorIdOrShortId,
+  fetchStationDetail, fetchStationDetailByShortId, fetchStationDetailByShortIdOrConnectorIdOrEquipmentId,
+  fetchStationPriceDesc,
+  startAppointmentCharge,
+  startCharge,
+} from "@/api/charge";
+
+import {fetchProfile, listStationAvailableRightsAndCoupons} 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 StationChargeCoupon from "@/components/station-charge-coupon/station-charge-coupon.vue";
+import {format} from "@/utils/date";
+import {redirect, to} from "../../utils/navigate";
+
+
+const DAY = 24 * 60 * 60 * 1000;
+const options = ref<any>();
+const data = ref<any>();
+const equipment = ref<any>(null);
+const station = ref<any>();
+const stationId = ref<any>();
+const priceInfo = ref();
+const priceDescVisible = ref(false);
+const timesInfo = ref<any>({
+  day: [],
+  time: [],
+});
+
+
+const checkedUserRightsOrCoupon = ref<any>({
+  rightsId: null,
+  userRechargeRightsId: null,
+  couponId: null,
+  userCouponId: null,
+  stationId: null,
+  userRights: null,
+  userCoupon: null
+});
+
+const station_charge_coupon_ref = ref();
+
+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 balance = ref(0);
+
+
+const refreshUserBalance = () => {
+  let user = getApp<any>().globalData.user
+  console.log(user)
+  balance.value = user?.balance || 0;
+}
+
+const handleShowStationChargeCoupon = () => {
+  let {rightsId, couponId} = checkedUserRightsOrCoupon.value;
+  station_charge_coupon_ref.value?.open(stationId.value, rightsId, couponId);
+}
+
+const handleCheckedUserRightsAndCoupon = (data: any) => {
+  // console.log("handleCheckedUserRightsAndCoupon", data);
+  checkedUserRightsOrCoupon.value = data;
+}
+
+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 isToday = format("d") === format("d", day.getTime());
+  // 当前时间的24小时内
+  let startHour = isToday ? Number(nowHour) : 0;
+  let endHour = isToday ? 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: "#2d9e95",
+      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 = {
+    time: 0, // 时间戳
+    day: "",
+    hour: "",
+    isPowerSaving: e.detail.value,
+    format: "",
+    formatPrice: "",
+    scrollId: "",
+  };
+};
+
+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: "#2d9e95",
+    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";
+  }
+  let {userRechargeRightsId, userCouponId} = checkedUserRightsOrCoupon.value;
+  console.log("userRechargeRightsId")
+  if (userRechargeRightsId && userRechargeRightsId >= 0) {
+    query += `&userRechargeRightsId=${userRechargeRightsId}`;
+  }
+  if (userCouponId && userCouponId >= 0) {
+    query += `&userCouponId=${userCouponId}`;
+  }
+  console.log(query)
+  uni.showLoading({
+    title: "提交中",
+    mask: true,
+  });
+  startCharge(equipment.value.connectorId, 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?connectorId=${options.value.connectorId}&start=1`)
+        }
+      })
+      .catch((err) => {
+        uni.hideLoading();
+        uni.showModal({
+          content: `${err.errMsg}`,
+          showCancel: false,
+        });
+      });
+};
+
+const submitNow = () => {
+  uni.showLoading({
+    title: "提交中",
+    mask: true,
+  });
+  startAppointmentCharge(options.value.connectorId)
+      .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?connectorId=${options.value.connectorId}&start=1`)
+    } else {
+      _checkStartCharge();
+    }
+  });
+};
+
+/**
+ * 设置当前用户在该站点的默认充电权益或优惠券
+ */
+const fetchUserStationDefaultRightsAndCoupon = () => {
+  listStationAvailableRightsAndCoupons(stationId.value).then((res) => {
+    let {userRechargeRightsList, userCouponList} = res;
+    let couponList = (userCouponList || []).sort((a: any, b: any) => new Date(a.endTime).getTime() - new Date(b.endTime).getTime());
+    let rightsList = (userRechargeRightsList || []).sort((a: any, b: any) => new Date(a.endTime).getTime() - new Date(b.endTime).getTime());
+    checkedUserRightsOrCoupon.value.stationId = stationId.value;
+    if (rightsList && rightsList.length > 0) {
+      checkedUserRightsOrCoupon.value.userRights = rightsList[0];
+      checkedUserRightsOrCoupon.value.rightsId = rightsList[0].rightsId;
+      checkedUserRightsOrCoupon.value.userRechargeRightsId = rightsList[0].id;
+    } else if (couponList && couponList.length > 0) {
+      checkedUserRightsOrCoupon.value.userCoupon = couponList[0];
+      checkedUserRightsOrCoupon.value.couponId = couponList[0].couponId;
+      checkedUserRightsOrCoupon.value.userCouponId = couponList[0].id;
+    }
+    console.log(res)
+  });
+}
+
+onLoad((_options: any) => {
+  // sn=SN100523042860091 测试环境
+  console.log("options", _options);
+  let {sn, connectorId, equipmentId} = _options
+  let _sn = _options.sn;
+  let _stationId = _options.stationId;
+  if (_stationId) {
+    stationId.value = _stationId;
+  }
+  options.value = {..._options}
+  uni.showLoading({
+    title: "加载中",
+  });
+  fetchStationDetailByShortIdOrConnectorIdOrEquipmentId(sn, connectorId, equipmentId)
+      .then((res) => {
+        console.log("1111111111", res);
+        //充电桩需要再次过滤
+        stationId.value = res.stationId;
+        fetchUserStationDefaultRightsAndCoupon();
+        if (sn) {
+          let cns = res.connectInfoList?.filter(k => k.shortId === sn)
+          if (cns && cns.length > 0) {
+            equipment.value = cns[0]
+          }
+        }
+
+        if (connectorId) {
+          let cns = res.connectInfoList?.filter(k => k.connectorId === connectorId)
+          if (cns && cns.length > 0) {
+            equipment.value = cns[0]
+          }
+        }
+
+        if (equipmentId) {
+          let cns = res.connectInfoList?.filter(k => k.equipmentId === equipmentId)
+          if (cns && cns.length > 0) {
+            equipment.value = cns[0]
+          }
+        }
+
+
+        // if (equipment && equipment.connectorInfos && equipment.connectorInfos.length) {
+        //   sn = equipment.connectorInfos[0].connectorId;
+        //   _options.sn = equipment.connectorInfos[0].connectorId;
+        // }
+        // options.value = _options;
+        // data.value = res;
+        station.value = res;
+        if(!equipment.value){
+          uni.showToast({
+            titile:'none',
+            content:"服务异常,请稍后再试。"
+          })
+          // return Promise.resolve({});
+          throw Error;
+        }
+        return fetchStationPriceDesc(equipment.value.connectorId, res.stationId)
+      })
+      .then((res) => {
+        console.log("xxxx price", res);
+        uni.hideLoading();
+        priceInfo.value = res;
+        return fetchChargeStatus(false, true);
+      })
+      .then((res) => {
+        console.log("charge status", 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(1)
+      );
+    }
+
+    refreshUserBalance();
+  });
+  refreshUserBalance();
+});
+
+onShow(() => {
+  if (getApp<any>().globalData.lastData.discount) {
+    discountIndex.value = getApp<any>().globalData.lastData.discount.index;
+    getApp<any>().globalData.lastData.discount = undefined;
+  }
+  refreshUserBalance();
+});
+</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>

+ 268 - 0
charge-front/src/pages-charge/camera/camera.vue

@@ -0,0 +1,268 @@
+<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 = () => {
+  // #ifdef MP-WEIXIN
+  wx.scanCode({
+    onlyFromCamera:false,
+    scanType:['qrcode','barcode'],
+    success: (res1) => {
+      console.log("scanCode",res1);
+      deCode(res1.result);
+    },
+    fail: (err) => {
+      console.error("scanCode",err);
+      uni.hideLoading();
+      uni.showModal({
+        content: err.errMsg || "出现错误,请重试",
+      });
+    }
+  })
+  // #endif
+}
+
+const chooseImage1 = () => {
+  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>

+ 298 - 0
charge-front/src/pages-charge/card/card.vue

@@ -0,0 +1,298 @@
+<template>
+  <view class="page pt-20 pr-40 pl-40">
+    <view class="tab-list flex-align-end">
+      <view class="fs-28 color-666 mt-20 w50" :class="mode===0?'active':''" @click="changeTab(0)">权益卡</view>
+      <view class="fs-28 color-666 mt-20 w50" :class="mode===1?'active':''" @click="changeTab(1)">优惠券</view>
+    </view>
+    <view v-if="mode===0">
+      <view
+          class="item mt-20 "
+          v-for="(item, index) in list"
+          :class="item.status==0?'inactive':''"
+          :key="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-justify-center pl-48">
+
+            <view class="item-content_left">
+              <view class="flex-align-end">
+                <view class="item-header__left">
+                  <image class="width-84" src="/pages-charge/static/discount-rights-card.png" mode="widthFix"></image>
+                  <view class="fs-48 lh-58 color-000 fw-500 ml-16">
+                    {{ (item.discount / 10).toFixed(1) }}折
+                  </view>
+                  <view class="fs-26 color-999 ml-16" v-if="mode === 0"
+                  >抵扣充电服务费
+                  </view>
+                </view>
+
+                <!--                <view class="item-header__right flex-grow">
+                                  <view class="item-status ml-10">{{ item.status == 0 ? '已失效' : '生效中' }}</view>
+                                </view>-->
+              </view>
+
+
+              <block>
+                <view class="fs-26 color-999 mt-8"
+                      @click.stop="to(`/pages-common/activity/activity?id=${item.activityId}`)">
+                  <view class="fs-26 color-999">到期时间:{{ item.endTime }}</view>
+                  <view class="flex-align-center" v-if="mode === 0">
+                    <view class="fs-26 color-999">权益余额:{{ ((item.rightsBalance || 0) / 100).toFixed(2) }}元
+                      <text class="color-primary">详细规则</text>
+                      <text class="fs-24 ml-4 color-primary">>></text>
+                    </view>
+                  </view>
+                </view>
+              </block>
+            </view>
+            <view class="item-content_right">
+              <view class="item-status ml-10">{{ item.status == 0 ? '已失效' : '生效中' }}</view>
+            </view>
+
+          </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 style="width: 100%;text-align: center;">
+        <image v-if="!list||list.length==0" style="width: 240rpx;" mode="widthFix" src="/static/images/search-empty.png"></image>
+      </view>
+    </view>
+
+
+    <view v-else-if="mode===1">
+      <view
+          class="item mt-20"
+          :class="item.usageStatus==1||item.status==0?'inactive':''"
+          v-for="(item, index) in couponList"
+          :key="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-justify-center pl-48">
+            <view class="item-content_left">
+              <view class="flex-align-end">
+                <view class="item-header__left">
+
+                  <image
+                      class="width-84"
+                      src="/pages-charge/static/discount-coupon.png"
+                      mode="widthFix"
+                  ></image>
+                  <view></view>
+                  <view class="fs-48 lh-58 color-000 fw-500 ml-16" v-if="item.couponType=='FullDiscount'">
+                    {{ ((item.discount || 0) / 100).toFixed(2) }}元
+                  </view>
+
+                  <view class="fs-48 lh-58 color-000 fw-500 ml-16" v-else>
+                    {{ (item.discount / 10).toFixed(1) }}折
+                  </view>
+
+                  <view class="fs-26 color-999 ml-16">
+                    充电服务费:{{ item.couponType == 'FullDiscount' ? '满减券' : '折扣券' }}
+                  </view>
+                </view>
+
+
+                <!--
+                                <view class="item-header__right flex-grow">
+                                  <view class="item-status ml-10" v-if="item.status==0">已失效</view>
+                                  <view class="item-status ml-10" v-else>{{ item.usageStatus == 0 ? '未使用' : '已使用' }}</view>
+                                </view>
+                -->
+
+
+              </view>
+
+              <block v-if="mode === 1">
+                <view class="fs-26 color-999 mt-8"
+                      @click.stop="to(`/pages-common/activity/activity?id=${item.activityId}`)">
+                  <view class="fs-26 color-999">到期时间:{{ item.endTime }}</view>
+                  <view class="flex-align-center">
+                    <view class="fs-26 color-999">服务费门槛:{{ ((item.minServiceMoney || 0) / 100).toFixed(2) }}元
+                      <text class="color-primary">详细规则</text>
+                      <text class="fs-24 ml-4 color-primary">>></text>
+                    </view>
+                  </view>
+                </view>
+              </block>
+            </view>
+            <view class="item-content_right">
+              <view class="item-status ml-10" v-if="item.status==0">已失效</view>
+              <view class="item-status ml-10" v-else>{{ item.usageStatus == 0 ? '未使用' : '已使用' }}</view>
+            </view>
+
+
+          </view>
+        </view>
+      </view>
+
+      <view style="width: 100%;text-align: center;">
+        <image v-if="!couponList||couponList.length==0" style="width: 240rpx;" mode="widthFix" src="/static/images/search-empty.png"></image>
+      </view>
+    </view>
+
+  </view>
+</template>
+
+<script setup lang="ts">
+import {onPullDownRefresh, onShow} from "@dcloudio/uni-app";
+import {ref} from "vue";
+import {to} from "../../utils/navigate";
+import {fetchRightsAndCoupons} from "@/api/user";
+
+const mode = ref(0);
+const list = ref<any[]>([]);
+const couponList = ref<any[]>([]);
+const activity = ref();
+const checkIndex = ref(-1);
+const needRefresh = ref(false);
+
+onPullDownRefresh(() => {
+  loadData();
+});
+
+const changeTab = (tab: number) => {
+  mode.value = tab;
+}
+
+const loadData = () => {
+  uni.showLoading({
+    title: "加载中",
+  });
+  fetchRightsAndCoupons().then((res) => {
+    uni.stopPullDownRefresh();
+    console.log(res)
+    uni.hideLoading();
+    let {userCouponList, userRechargeRightsList} = res;
+    list.value = userRechargeRightsList;
+    couponList.value = userCouponList;
+  });
+}
+
+onShow(() => {
+  loadData();
+
+
+});
+</script>
+
+<style lang="scss" scoped>
+.page {
+  min-height: 100vh;
+  width: 100%;
+  background-color: #f6f7fa;
+
+  .item {
+    position: relative;
+    min-height: 220rpx;
+    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;
+      }
+    }
+  }
+
+  .tab-list {
+    width: 100%;
+
+    .w50 {
+      width: 50%;
+      text-align: center;
+    }
+
+    .active {
+      color: var(--color-primary);
+    }
+  }
+}
+
+.item-status {
+  font-size: 24rpx;
+  padding: 0rpx 2rpx;
+  color: var(--color-primary);
+  border: 2rpx solid var(--color-primary);
+  border-radius: 6rpx;
+
+}
+
+.item-header__left {
+  display: inline-flex;
+  align-items: flex-end;
+}
+
+.item-header__right {
+  padding: 5rpx;
+  display: flex;
+  //justify-content: flex-end;
+}
+
+.inactive {
+  cursor: not-allowed;
+  pointer-events: none;
+
+  .item-status {
+    font-size: 24rpx;
+    padding: 0rpx 2rpx;
+    color: #ccc !important;
+    border: 2rpx solid #ccc;
+    border-radius: 6rpx;
+
+  }
+
+  .color-primary {
+    color: #ccc !important;
+  }
+}
+
+.item-content_left {
+  flex-grow: 1;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-around;
+
+}
+
+.item-content_right {
+  width: 120rpx;
+  display: flex;
+  align-items: center;
+  align-content: center;
+}
+</style>

+ 76 - 0
charge-front/src/pages-charge/codeing/codeing.vue

@@ -0,0 +1,76 @@
+<template>
+  <view class="pt-10 pr-60 pl-60">
+    <view class="lh-0">
+      <!-- #ifdef MP-WEIXIN -->
+      <!-- #endif -->
+      <image
+        src="/pages-charge/static/charge-input-head.png"
+        mode="widthFix"
+        style="width: 100%"
+      ></image>
+    </view>
+    <view class="mt-20">
+      <style-input
+        title="充电编码"
+        :value="value"
+        :focus="focus"
+        type="text"
+        @input="input"
+      />
+    </view>
+    <view style="margin-top: 100rpx">
+      <style-button type="primary" @click="submit">确认</style-button>
+      <view class="mt-20"></view>
+      <style-button type="primary" :border="true" @click="scanCode"
+        >扫码充电</style-button
+      >
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import {
+  fetchStationByConnectorIdOrShortId,
+  fetchStationDetailByConnectorId,
+  fetchStationDetailByShortId
+} from "@/api/charge";
+import { ref } from "vue";
+import { scanCode } from "../../utils/code";
+import { to } from "../../utils/navigate";
+
+const value = ref("");
+const focus = ref(true);
+
+const submit = () => {
+  if (!value.value) {
+    uni.showToast({
+      title: "请输入充电编码",
+      icon: "none",
+    });
+    return;
+  }
+  uni.showLoading({
+    title: "加载中",
+  });
+  fetchStationDetailByShortId(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: "#2d9e95",
+        showCancel: false,
+      });
+    });
+};
+const input = (e: any) => {
+  value.value = e.value;
+};
+</script>

+ 221 - 0
charge-front/src/pages-charge/coupon-center/coupon-center.vue

@@ -0,0 +1,221 @@
+<template>
+  <style-dialog
+      v-if="visible"
+      @close="handleClose"
+      title="">
+
+    <view class="page pt-20 pr-40 pl-40">
+      <view class="tab-list flex-align-end">
+        <view class="fs-28 color-666 mt-20 w50" :class="mode===0?'active':''" @click="changeTab(0)">权益卡</view>
+        <view class="fs-28 color-666 mt-20 w50" :class="mode===1?'active':''" @click="changeTab(1)">优惠券</view>
+      </view>
+      <view>
+        <view
+            class="item mt-20 "
+            v-for="(item, index) in list"
+            :class="item.status==0?'inactive':''"
+            :key="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-justify-center pl-48">
+
+              <view class="item-content_left">
+                <view class="flex-align-end">
+                  <view class="item-header__left">
+                    <image class="width-84" src="/pages-charge/static/discount-rights-card.png" mode="widthFix"></image>
+                    <view class="fs-48 lh-58 color-000 fw-500 ml-16">{{ item.discountAmount }}折
+                    </view>
+                    <view class="fs-26 color-999 ml-16" v-if="mode === 0"
+                    >用于充电服务费抵扣
+                    </view>
+                  </view>
+
+                  <!--                <view class="item-header__right flex-grow">
+                                    <view class="item-status ml-10">{{ item.status == 0 ? '已失效' : '生效中' }}</view>
+                                  </view>-->
+                </view>
+
+
+                <block>
+                  <view class="fs-26 color-999 mt-8"
+                  >
+                    <view class="fs-26 color-999 mt-24">到期时间:{{ item.endTime }}</view>
+                    <!--                  <view class="flex-align-center" v-if="mode === 0">
+                                        <view class="fs-26 color-999 mt-24">权益余额:{{ ((item.rightsBalance || 0) / 100).toFixed(2) }}元
+                                          <text class="color-primary">详细规则</text>
+                                          <text class="fs-24 ml-4 color-primary">>></text>
+                                        </view>
+                                      </view>-->
+                  </view>
+                </block>
+              </view>
+              <view class="item-content_right">
+                <view class="item-status ml-10" @click.stop="handleCollectCoupon(item)">领券</view>
+              </view>
+
+            </view>
+          </view>
+        </view>
+      </view>
+
+
+    </view>
+    >
+  </style-dialog>
+
+</template>
+
+<script setup lang="ts">
+import {onPullDownRefresh} from "@dcloudio/uni-app";
+import {ref} from "vue";
+import {collectCoupon, listAvailableCoupons} from "@/api/user";
+
+const mode = ref(0);
+const list = ref<any[]>([]);
+const couponList = ref<any[]>([]);
+const stationId = ref(-1);
+const visible = ref(false);
+
+const handleClose = () => {
+
+}
+
+onPullDownRefresh(() => {
+  loadData();
+});
+
+const changeTab = (tab: number) => {
+  mode.value = tab;
+}
+
+const handleCollectCoupon = (coupon: any) => {
+  collectCoupon(coupon.id).then((res) => {
+    console.log(res)
+    uni.showToast({
+      title: "领券成功",
+      icon: "success"
+    });
+  });
+}
+
+
+const loadData = () => {
+  uni.showLoading({
+    title: "加载中",
+  });
+  listAvailableCoupons(stationId.value).then((res) => {
+    console.log(res)
+    uni.hideLoading();
+    couponList.value = res.list;
+  });
+}
+const open = (stationId: any) => {
+  visible.value = true;
+  stationId.value = stationId;
+  loadData()
+}
+
+
+defineExpose({
+  open
+})
+</script>
+
+<style lang="scss" scoped>
+.page {
+  min-height: 100vh;
+  width: 100%;
+  background-color: #f6f7fa;
+
+  .item {
+    position: relative;
+    min-height: 220rpx;
+    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;
+      }
+    }
+  }
+
+  .tab-list {
+    width: 100%;
+
+    .w50 {
+      width: 50%;
+      text-align: center;
+    }
+
+    .active {
+      color: var(--color-primary);
+    }
+  }
+}
+
+.item-status {
+  font-size: 24rpx;
+  padding: 0rpx 2rpx;
+  color: var(--color-primary);
+  border: 2rpx solid var(--color-primary);
+  border-radius: 6rpx;
+
+}
+
+.item-header__left {
+  display: inline-flex;
+  align-items: flex-end;
+}
+
+.item-header__right {
+  padding: 5rpx;
+  display: flex;
+  //justify-content: flex-end;
+}
+
+.inactive {
+  cursor: not-allowed;
+  pointer-events: none;
+
+  .item-status {
+    font-size: 24rpx;
+    padding: 0rpx 2rpx;
+    color: #ccc !important;
+    border: 2rpx solid #ccc;
+    border-radius: 6rpx;
+
+  }
+
+  .color-primary {
+    color: #ccc !important;
+  }
+}
+
+.item-content_left {
+  flex-grow: 1;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-around;
+
+}
+
+.item-content_right {
+  width: 120rpx;
+  display: flex;
+  align-items: center;
+  align-content: center;
+}
+</style>

+ 175 - 0
charge-front/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(1));
+      });
+      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>

+ 163 - 0
charge-front/src/pages-charge/machines/charge-machine/charge-machine.vue

@@ -0,0 +1,163 @@
+<template>
+  <view
+    class="charge-point flex-align-center mt-20"
+    v-for="(item, index) in list"
+    :key="index"
+    @click="toOrdering(index)"
+  >
+    <view
+      :class="[
+        'charge-point_icon',
+        'flex-center',
+        `charge-point_icon-${item.status}`,
+      ]"
+    >
+      <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">
+        <view class="fs-32 fw-600 color-000 lh-32">{{ title }}</view>
+        <view
+          :class="[
+            'tag',
+            'fs-24',
+            'flex-center',
+            'ml-8',
+            `tag-${item.status}`,
+          ]"
+          >{{ (statusMap as any)[item.status] }}</view
+        >
+      </view>
+      <view class="mt-10 flex-align-center">
+        <view class="fs-24 color-666"
+          >{{ item.connectorType === 1 ? "直流" : "交流"
+          }}{{ item.power }}kw</view
+        >
+        <view class="fs-24 color-666 mr-6 ml-6">|</view>
+        <view class="fs-24 color-666">车位号{{ parkingNo }}</view>
+      </view>
+    </view>
+    <view class="ml-auto" v-if="price">
+      <view
+        class="lh-32 color-warning  flex-end "
+        style="text-align: right; margin-top: -8rpx"
+      >
+        <text class="fs-22 mr-4">¥</text>
+        <text class="fs-32">{{ price }}</text>
+        <text class="fs-22 ml-4" style="vertical-align: text-top">元</text>
+      </view>
+      <view class="fs-24 lh-24 color-999 mt-10">当前时段</view>
+    </view>
+    <view class="ml-auto height-60 flex-align-end" v-if="time">
+      <view class="fs-24 lh-24 color-999">{{ time }}</view>
+    </view>
+  </view>
+</template>
+
+<script lang="ts">
+export default {
+  props: {
+    title: String,
+    price: [String, Number],
+    time: String,
+    list: Array<any>,
+    parkingNo: String,
+  },
+  data() {
+    return {
+      statusMap: {
+        255: "故障",
+        0: "离网",
+        1: "空闲",
+        2: "占用中",
+        3: "充电中",
+        4: "已预约",
+      },
+    };
+  },
+  methods: {
+    toOrdering(index: number) {
+      const { list } = this;
+      if (
+        list &&
+        list[index] &&
+        list[index].status &&
+        (list[index].status === 1 ||
+          list[index].status === 2)
+      ) {
+        uni.showActionSheet({
+          itemList: ["去充电"],
+          success: (res) => {
+            if (res.tapIndex === 0) {
+              uni.navigateTo({
+                url: `/pages-charge/appointment/appointment?connectorId=${list[index].connectorId}&sn=${list[index].shortId}&stationId=${list[index].stationId}`,
+              });
+            }
+          },
+        });
+      }
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+@import "../../../styles/font.scss";
+@import "../../../styles/flex.scss";
+@import "../../../styles/layout.scss";
+
+.charge-point {
+  background-color: #fff;
+  border-radius: 20rpx;
+  transition: all 0.3s;
+  overflow: hidden;
+  padding: 30rpx;
+  &_icon {
+    height: 56rpx;
+    width: 56rpx;
+    border-radius: 50%;
+    &-255 {
+      background: #f43636;
+    }
+    &-0 {
+      background-color: #b1b1b1;
+    }
+    &-1 {
+      background: #2d9e95;
+    }
+    &-2,
+    &-3,
+    &-4 {
+      background: rgb(154, 168, 168);
+    }
+  }
+  .tag {
+    height: 38rpx;
+    padding: 0rpx 8rpx;
+    box-sizing: border-box;
+    border-radius: 8rpx;
+  }
+  .tag-255 {
+    color: #f43636;
+    background: rgba(244, 54, 54, 0.1);
+  }
+  .tag-0 {
+    color: #abadb0;
+    background-color: rgb(255, 255, 255);
+  }
+  .tag-1 {
+    color: #2d9e95;
+    background: rgb(255, 255, 255);
+  }
+  .tag-2,
+  .tag-3,
+  .tag-4 {
+    color: rgba(171, 173, 176);
+    background: rgb(255, 255, 255);
+  }
+}
+</style>

+ 486 - 0
charge-front/src/pages-charge/machines/machines.vue

@@ -0,0 +1,486 @@
+<template>
+  <!--  #ifdef MP-WEIXIN -->
+  <page-meta page-style="overflow: 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 style="display: inline-flex;align-items: center;">
+          <view class="fs-40 fw-600 color-000">{{ title }}</view>
+          <image class="ml-10"
+                 v-if="location.activityList&&location.activityList.length>0"
+                 src="/static/images/coupon-center.png" mode="widthFix" style="width:32rpx"/>
+          <view class="coupon-box ml-5"
+                v-if="location.activityList&&location.activityList.length>0"
+                @tap.stop="toCouponCenter"
+          >
+            <text @tap.stop="showStationCoupon">活动中</text>
+          </view>
+        </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">{{ station.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: #2d9e95">导航</view>
+          </view>
+        </view>
+        <view
+            class="foot mt-30 flex-align-center flex-shrink"
+            @click="openDesc"
+        >
+          <view class="fs-28 color-666">空闲充电桩:</view>
+          <view class="fs-36 fw-500 color-primary">{{ canUseCount }}</view>
+          <view class="fs-28 color-primary ml-6">个</view>
+          <view class="fs-28 color-666 ml-auto">{{ canUseTime }}</view>
+        </view>
+      </view>
+      <!--      <view class="pt-40 pb-20 fs-32 fw-500 color-999">详细说明</view>-->
+      <view class="desc" v-if="station">
+        <view class="flex-align-center" @click="openDesc">
+          <view class="width-168 fs-26 color-666">充电费用</view>
+          <view class="fs-26 color-000">{{ canUsePrice }}元/度</view>
+          <view class="ml-auto lh-0">
+            <uni-icons
+                type="right"
+                size="18"
+                color="rgba(0,0,0,0.4)"
+            ></uni-icons>
+          </view>
+        </view>
+        <view class="flex-align-center">
+          <view class="width-168 fs-26 color-666">停车费用</view>
+          <view class="fs-26 color-000">{{
+              Number(station.parkFee) > 0
+                  ? station.parkFee
+                  : "以停车场公示信息为准"
+            }}
+          </view>
+        </view>
+        <view
+            class="flex-align-center"
+            v-if="
+            station &&
+            station.connectInfoList &&
+            station.connectInfoList.length
+          "
+        >
+          <view class="width-168 fs-26 color-666">充电桩类型</view>
+          <view class="fs-26 color-000"
+          >{{
+              station.connectInfoList[0].connectorType === 1
+                  ? "直流"
+                  : "交流"
+            }}{{ station.connectInfoList[0].power }}kw
+          </view
+          >
+        </view>
+      </view>
+      <view class="pt-40 flex-align-center">
+        <!--        <view class="fs-32 fw-500 color-999">充电桩</view>-->
+        <view class="fs-26 ml-30 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
+              class="mr-20"
+              style="
+              width: 0;
+              height: 0;
+              border: 8rpx solid;
+              border-color: #333 transparent transparent transparent;
+              margin-top: 8rpx;
+            "
+          ></view>
+        </view>
+      </view>
+
+      <template
+          v-if="station"
+          v-for="(item, index) in stationEquipmentInfos"
+          :key="index"
+      >
+        <ChargeMachine
+            :title="'NO.' + item.shortId"
+            :price="currentPrice"
+            :list="[item]"
+            :parkingNo="item.parkingNo"
+        ></ChargeMachine>
+      </template>
+
+
+    </view>
+    <PriceDesc
+        v-if="dialogVisible && dialogType === 'desc'"
+        :desc="desc"
+        @close="closeDialog"
+    ></PriceDesc>
+    <view
+        class="dialog flex-align-end"
+        v-if="dialogVisible && dialogType === 'status'"
+    >
+      <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="status-dialog_body">
+          <view class="status">
+            <view
+                :class="['fs-32', 'flex-align-center']"
+                :style="{ color: status === index ? '#2d9e95' : '#2d284b' }"
+                v-for="(statusItem, index) in statusList"
+                :key="index"
+                @click="changeStatus(index)"
+            >
+              {{ statusItem.title }}
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+    <station-coupon ref="station_coupon_ref"></station-coupon>
+  </view>
+</template>
+
+<script setup lang="ts">
+import {ref} from "vue";
+import {fetchStation, fetchStationDetail, 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";
+import StationCoupon from "@/components/station-coupon/station-coupon.vue";
+
+const dialogVisible = ref(false);
+const dialogType = ref("");
+const desc = ref<any[]>([]);
+const status = ref(0);
+const statusList = ref([
+  {
+    title: "全部",
+  },
+  {
+    title: "空闲",
+  },
+  {
+    title: "充电中",
+  },
+]);
+
+const totalCount = ref(0);
+const canUseCount = ref(0);
+const currentPrice = ref(0);
+const canUseTime = ref("");
+const canUsePrice = ref("");
+const title = ref("");
+const location = ref({
+  address: "",
+  latitude: "",
+  longitude: "",
+});
+
+const station = ref();
+const stationEquipmentInfos = ref();
+const currentTime = ref();
+
+const station_coupon_ref = ref(null);
+
+onLoad((options: any) => {
+  const _title = options.title || "";
+  uni.setNavigationBarTitle({
+    title: _title,
+  });
+  title.value = _title;
+  if (getApp<any>().globalData.lastData.station) {
+    const {address, latitude, longitude, activityList} =
+        getApp<any>().globalData.lastData.station;
+    location.value = {
+      address,
+      latitude,
+      longitude,
+      activityList
+    };
+    getApp<any>().globalData.lastData.station = undefined;
+  }
+  uni.showLoading({
+    title: "加载中",
+    mask: true,
+  });
+  fetchStationDetail(options.id)
+      .then((res) => {
+        console.log("station detail>>>",res);
+        let ConnectorID = "";
+        if (res.connectInfoList && res.connectInfoList.length) {
+          res.connectInfoList.forEach((con: any) => {
+            totalCount.value++;
+            if (!ConnectorID) {
+              ConnectorID = con.connectorId;
+            }
+            if (
+                con.status &&
+                con.status === 1
+            ) {
+              canUseCount.value++;
+            }
+          });
+        }
+        // res.totalFee = (Number(res.currentPolicyInfo?.servicePrice||0)+Number(res.currentPolicyInfo?.elecPrice||0)).toFixed(2)
+
+        res.bannerImages = res.pictures.split(";");
+        station.value = res;
+        stationEquipmentInfos.value = res.connectInfoList?.map((item: any) => {
+          return {
+            ...item,
+          };
+        });
+        if (ConnectorID) {
+          return fetchStationPriceDesc(ConnectorID);
+        } else {
+          // eslint-disable-next-line promise/no-return-wrap
+          return Promise.resolve({
+            policyInfos: [],
+          });
+        }
+      })
+      .then((res) => {
+        uni.hideLoading();
+        currentPrice.value = res.currentPrice;
+        currentTime.value = res.currentTime;
+        canUseTime.value = res.useTime;
+        canUsePrice.value = `${res.minPrice}~${res.maxPrice}`;
+        desc.value = res.policyInfos || [];
+      })
+      .catch((err) => {
+        // eslint-disable-next-line no-console
+        console.log(err);
+        uni.hideLoading();
+        uni.showToast({
+          title: "加载失败,请重试",
+          icon: "none",
+        });
+      });
+});
+const openAddress = function () {
+  uni.openLocation({
+    latitude: Number(location.value.latitude),
+    longitude: Number(location.value.longitude),
+    scale: 18,
+    name: title.value,
+    address: location.value.address,
+  });
+};
+const openDesc = function () {
+  dialogVisible.value = true;
+  dialogType.value = "desc";
+};
+const openStatus = function () {
+  dialogVisible.value = true;
+  dialogType.value = "status";
+};
+const closeDialog = function () {
+  dialogVisible.value = false;
+};
+const changeStatus = function (index: number) {
+  if (status.value === index) {
+    closeDialog();
+    return;
+  }
+  status.value = index;
+  if (index === 0) {
+    stationEquipmentInfos.value = station.value.connectInfoList?.map(
+        (item: any) => {
+          return {
+            ...item,
+          };
+        }
+    );
+    closeDialog();
+    return;
+  }
+  const STATUS_MAP = [-1, 1, 3];
+  let newStationEquipmentInfos: any[] = [];
+  station.value.connectInfoList?.forEach((connect: any) => {
+    let check = false;
+    if (
+        connect.status &&
+        Number(connect.status) === STATUS_MAP[status.value]
+    ) {
+      check = true;
+    }
+    if (check) {
+      newStationEquipmentInfos.push(connect);
+    }
+  });
+  stationEquipmentInfos.value = newStationEquipmentInfos;
+  closeDialog();
+};
+const previewImage = (url: string) => {
+  uni.previewImage({
+    urls: station.value.bannerImages,
+    current: url,
+  });
+};
+
+const showStationCoupon = () => {
+  // console.log(station.value)
+  station_coupon_ref.value?.open(station.value.StationID);
+}
+</script>
+
+<style lang="scss">
+@import "../../styles/dialog.scss";
+
+page {
+  background-color: #f5f5f5;
+}
+
+.banner {
+  height: 440rpx;
+  background-color: transparent;
+
+  .banner-image {
+    background-position: center;
+    background-size: cover;
+    background-repeat: no-repeat;
+  }
+}
+
+.station {
+  margin-top: -60rpx;
+  border-radius: 20rpx;
+  overflow: hidden;
+  position: relative;
+  background-color: #fff;
+  min-height: 264rpx;
+  padding: 0 40rpx;
+  padding-top: 30rpx;
+
+  .nav {
+    flex-shrink: 0;
+    width: 120rpx;
+    height: 48rpx;
+    border-radius: 40rpx;
+    background: rgba(52, 125, 255, 0.1);
+
+    image {
+      width: 36rpx;
+    }
+  }
+
+  .foot {
+    border-top: 1px dashed rgba(0, 0, 0, 0.1);
+    height: 96rpx;
+  }
+}
+
+.desc {
+  box-sizing: border-box;
+  padding: 0rpx 30rpx;
+  border-radius: 20rpx;
+  background-color: #fff;
+
+  & > view {
+    height: 90rpx;
+    border-top: 1px solid rgba(0, 0, 0, 0.1);
+
+    &:first-child {
+      border-top: none;
+    }
+  }
+}
+
+.status-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 {
+    max-height: 860rpx;
+    box-sizing: border-box;
+    padding: 0rpx 30rpx 80rpx 30rpx;
+    background-color: #fff;
+    overflow-y: auto;
+
+    .status {
+      & > view {
+        height: 92rpx;
+        border-top: 1px solid rgba(0, 0, 0, 0.1);
+
+        &:first-child {
+          border-top: none;
+        }
+      }
+    }
+  }
+}
+
+
+.coupon-box {
+  display: inline-flex;
+  justify-content: space-between;
+  align-items: center;
+  color: #fff;
+  background-color: #2d9e95;
+  font-size: 14rpx;
+  border-radius: 8rpx;
+  padding: 0rpx 6rpx;
+
+  text {
+    font-size: 24rpx;
+  }
+}
+</style>

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

@@ -0,0 +1,195 @@
+<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'
+              }`,
+              'background':`${item.isCurrentTimePeriod?'rgba(52, 125, 255, 0.10)':'transparent'}`
+            }"
+          >
+            <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)",
+        "rgba(0, 218, 179, 0.20)"
+      ],
+      iconColor: ["#00DAB3", "#2d9e95", "#FF9900", "#FF2E00", "#006b57"]
+    };
+  },
+  mounted() {
+    // console.log(this.desc);
+    let currentHour = new Date().getHours()
+    if (this.desc && this.desc.length) {
+      this._desc = this.desc.map((item) => {
+       let  isCurrentTimePeriod = false;
+       if(item.startTimeFormat){
+         let split = item.startTimeFormat.split("~");
+         if(split&&split.length===2){
+           isCurrentTimePeriod =
+           currentHour>=Number(split[0].split(":")[0])&&
+           currentHour<Number(split[1].split(":")[0])
+         }
+       }
+        return {
+          ...item,
+          isCurrentTimePeriod,
+          iconIndex: this.iconText.findIndex(
+            (text) => item.pricePeriod === text
+          ),
+        };
+      });
+
+      // console.log(this._desc)
+    }
+  },
+  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>

+ 560 - 0
charge-front/src/pages-charge/order/order.vue

@@ -0,0 +1,560 @@
+<template>
+  <view class="page-container bg-f5f7fa" v-if="list">
+    <!-- 金额卡片 -->
+    <view class="amount-card bg-theme br-16 mx-32 mt-32 p-36 shadow-md">
+      <view class="flex-column items-center">
+        <view class="fs-26 color-fff mb-12">实付金额</view>
+        <view class="flex-align-center">
+          <text class="fs-44 color-fff mr-10">¥</text>
+          <text class="fs-64 fw-600 color-fff lh-64">{{ price }}</text>
+        </view>
+        <view class="fs-22 color-fff opacity-80 mt-12" v-if="orderDetail">
+          {{ orderDetail.chargeStatus === 2 ? '充电中' : '已完成' }}
+        </view>
+      </view>
+    </view>
+    
+    <!-- 订单信息卡片 -->
+    <view class="info-card bg-fff br-20 mx-32 mt-28 p-36 shadow-sm">
+      <!-- 充电站点 -->
+      <view class="info-item flex-align-center pb-32 border-b border-eee">
+        <view class="info-icon flex-center w-56 h-56 bg-theme-light br-10 mr-20">
+          <uni-icons type="location" size="26" color="#419D95"></uni-icons>
+        </view>
+        <view class="flex-grow">
+          <view class="fs-24 color-999 mb-8">充电站点</view>
+          <view class="fs-28 fw-500 color-333">{{ list[0]?.value }}</view>
+        </view>
+      </view>
+      
+      <!-- 充电详情 -->
+      <view class="charge-detail mt-32">
+        <view class="fs-28 fw-500 color-333 mb-24">充电详情</view>
+        
+        <view class="detail-grid">
+          <!-- 累计充电量 -->
+          <view class="detail-item flex-column items-center p-24 bg-f5f7fa br-12">
+            <view class="fs-24 color-999 mb-8">累计充电量</view>
+            <view class="fs-32 fw-600 color-333">{{ list[1]?.value }}</view>
+          </view>
+          
+          <!-- 充电用时 -->
+          <view class="detail-item flex-column items-center p-24 bg-f5f7fa br-12 ml-16">
+            <view class="fs-24 color-999 mb-8">充电用时</view>
+            <view class="fs-32 fw-600 color-333">{{ list[list.length - 2]?.value }}</view>
+          </view>
+        </view>
+      </view>
+      
+      <!-- 订单信息列表 -->
+      <view class="info-list mt-36">
+        <block v-for="(item, index) in list" :key="index">
+          <view 
+            class="info-row flex-align-center justify-between py-20" 
+            v-if="index > 1 && index < list.length - 2"
+          >
+            <view class="fs-26 color-666">
+              {{ item.label }}
+            </view>
+            <view 
+              class="fs-26" 
+              :class="{ 'color-theme': item.color === '#F43636', 'color-333': !item.color }"
+              :style="{ color: item.color ? '#419D95' : '' }"
+            >
+              {{ item.value }}
+              <uni-icons 
+                v-if="item.copy" 
+                type="copy" 
+                size="24" 
+                color="#999" 
+                class="ml-12"
+                @click="copyText(item.value)"
+              ></uni-icons>
+            </view>
+          </view>
+        </block>
+      </view>
+      
+      <!-- 优惠信息 -->
+      <view class="discount-info mt-32 pt-32 border-t border-eee">
+        <view 
+          class="flex-align-center justify-between cursor-pointer" 
+          @click="handleClickDesc"
+          v-if="orderDetail && orderDetail.discountAmount > 0"
+        >
+          <view class="fs-26 color-666">优惠信息</view>
+          <view class="flex-align-center">
+            <view class="fs-26 color-theme mr-12">查看详情</view>
+            <uni-icons type="right" size="24" color="#999"></uni-icons>
+          </view>
+        </view>
+      </view>
+    </view>
+    
+    <!-- 优惠券详情组件 -->
+    <order-coupon-detail ref="order_coupon_detail_ref"></order-coupon-detail>
+  </view>
+  
+  <!-- 加载状态 -->
+  <view class="loading-state flex-center" v-else>
+    <uni-icons type="spinner" size="40" color="#419D95" class="loading-icon"></uni-icons>
+    <view class="fs-28 color-999 ml-16">加载中...</view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { fetchOrder} from "../../api/user";
+import { onLoad } from "@dcloudio/uni-app";
+import { ref } from "vue";
+import OrderCouponDetail from "@/components/order-coupon-detail/order-coupon-detail.vue";
+import { to } from "@/utils/navigate";
+
+const list = ref<any[]>();
+const price = ref();
+const orderDetail = ref();
+const order_coupon_detail_ref = ref();
+
+const handleClickDesc = () => {
+  console.log("handleClickDesc")
+  let {startChargeSeq,discountType} = orderDetail.value;
+  order_coupon_detail_ref.value?.open(startChargeSeq,discountType);
+}
+
+// 复制文本
+const copyText = (text: string) => {
+  uni.setClipboardData({
+    data: text,
+    success() {
+      uni.showToast({
+        title: "复制成功",
+        icon: "success",
+        duration: 1500
+      });
+    }
+  });
+};
+
+
+
+onLoad((options: any) => {
+  fetchOrder(options.id)
+    .then((res) => {
+      orderDetail.value = res;
+      const totalMoney = (res.totalMoney / 100).toFixed(2);
+      const elecMoney = res.elecMoney ? (res.elecMoney / 100).toFixed(2) : '0.00';
+      const serviceMoney = res.serviceMoney ? (res.serviceMoney / 100).toFixed(2) : '0.00';
+      const discountAmount = (res.discountAmount / 100).toFixed(2);
+      const discountAmountText = res.rightsDesc;
+      const startChargeSeq = res.startChargeSeq;
+      const reg = new RegExp("\B(?=(\d{3})+(?!\d))", "g");
+      const start = new Date(res.startTime.replace(/-/g, "/"));
+      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
+          ? `${parseInt(`${min / 60}`)}小时${parseInt(
+              `${min - parseInt(`${min / 60}`) * 60}`
+            )}分钟`
+          : `${parseInt(`${diff / 60}`)}分钟`;
+      const _list = [
+        {
+          label: "充电站点",
+          value: `${res.stationName}`,
+        },
+        {
+          label: "累计充电量",
+          value: `${res.totalPower}度`,
+        },
+        {
+          label: "电费",
+          value: `${elecMoney.replace(reg, ",")}元`,
+        },
+        {
+          label: "服务费",
+          value: `${serviceMoney.replace(reg, ",")}元`,
+        },
+        {
+          label: "订单金额",
+          value: `${totalMoney.replace(reg, ",")}元`,
+        },
+       /* {
+          label: "折扣金额",
+          value: `${discountAmount.replace(reg, ",")}元`,
+          discount:true
+        },*/
+        {
+          label: "开始时间",
+          value: res.startTime,
+          color: "",
+        },
+        {
+          label: "结束时间",
+          value: res.chargeStatus === 2 ? "充电中" : res.endTime,
+        },
+        {
+          label: "充电用时",
+          value: time,
+        },  {
+          label: "订单号",
+          value: startChargeSeq,
+          copy:true
+        },
+      ];
+      if (Number(discountAmount) > 0) {
+        _list.splice(2, 0, {
+          label: "优惠金额",
+          value: `${
+            Number(discountAmount) <= 0 ? "" : "-"
+          }${discountAmount}元${discountAmountText}`,
+          color: "#F43636",
+          discount:true
+        });
+      }
+      list.value = _list;
+      price.value = (res.payAmount / 100).toFixed(2);
+    })
+    .catch(() => {
+      uni.showModal({
+        content: "出现错误,请退出重进",
+        success() {
+          uni.navigateBack();
+        },
+      });
+    });
+});
+</script>
+
+<style lang="scss">
+.page-container {
+  min-height: 100vh;
+  padding-bottom: 40rpx;
+}
+
+/* 金额卡片 */
+.amount-card {
+  background: linear-gradient(135deg, #419D95 0%, #368a84 100%);
+  transition: all 0.3s ease;
+  
+  &:hover {
+    transform: translateY(-4rpx);
+    box-shadow: 0 8rpx 24rpx rgba(65, 157, 149, 0.3);
+  }
+}
+
+/* 信息卡片 */
+.info-card {
+  transition: all 0.3s ease;
+  
+  &:hover {
+    transform: translateY(-2rpx);
+    box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
+  }
+}
+
+/* 充电详情网格 */
+.detail-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 16rpx;
+}
+
+.detail-item {
+  transition: all 0.3s ease;
+  
+  &:hover {
+    transform: translateY(-2rpx);
+    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
+  }
+}
+
+/* 操作按钮 */
+.btn-theme {
+  background: linear-gradient(135deg, #419D95 0%, #368a84 100%);
+  transition: all 0.3s ease;
+  box-shadow: 0 4rpx 12rpx rgba(65, 157, 149, 0.3);
+  
+  &:hover {
+    transform: translateY(-2rpx);
+    box-shadow: 0 6rpx 16rpx rgba(65, 157, 149, 0.4);
+  }
+  
+  &:active {
+    transform: translateY(0);
+  }
+}
+
+/* 加载状态 */
+.loading-state {
+  min-height: 60vh;
+}
+
+.loading-icon {
+  animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+/* 工具类 */
+.bg-f5f7fa {
+  background-color: #f5f7fa;
+}
+
+.bg-fff {
+  background-color: #ffffff;
+}
+
+.bg-primary {
+  background-color: #ff6b35;
+}
+
+.bg-primary-light {
+  background-color: rgba(255, 107, 53, 0.1);
+}
+
+.bg-success {
+  background-color: #4cd964;
+}
+
+.bg-success-light {
+  background-color: rgba(76, 217, 100, 0.1);
+}
+
+.bg-theme {
+  background-color: #419D95;
+}
+
+.bg-theme-light {
+  background-color: rgba(65, 157, 149, 0.1);
+}
+
+.br-12 {
+  border-radius: 12rpx;
+}
+
+.br-16 {
+  border-radius: 16rpx;
+}
+
+.br-20 {
+  border-radius: 20rpx;
+}
+
+.mx-32 {
+  margin-left: 32rpx;
+  margin-right: 32rpx;
+}
+
+.mt-28 {
+  margin-top: 28rpx;
+}
+
+.mt-32 {
+  margin-top: 32rpx;
+}
+
+.mb-16 {
+  margin-bottom: 16rpx;
+}
+
+.mb-24 {
+  margin-bottom: 24rpx;
+}
+
+.mb-40 {
+  margin-bottom: 40rpx;
+}
+
+.ml-12 {
+  margin-left: 12rpx;
+}
+
+.ml-16 {
+  margin-left: 16rpx;
+}
+
+.ml-24 {
+  margin-left: 24rpx;
+}
+
+.mt-8 {
+  margin-top: 8rpx;
+}
+
+.mt-16 {
+  margin-top: 16rpx;
+}
+
+.p-24 {
+  padding: 24rpx;
+}
+
+.p-36 {
+  padding: 36rpx;
+}
+
+.p-40 {
+  padding: 40rpx;
+}
+
+.pb-32 {
+  padding-bottom: 32rpx;
+}
+
+.pt-32 {
+  padding-top: 32rpx;
+}
+
+.py-20 {
+  padding-top: 20rpx;
+  padding-bottom: 20rpx;
+}
+
+.w-60 {
+  width: 60rpx;
+}
+
+.h-60 {
+  height: 60rpx;
+}
+
+.h-92 {
+  height: 92rpx;
+}
+
+.flex-center {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.flex-align-center {
+  display: flex;
+  align-items: center;
+}
+
+.flex-column {
+  display: flex;
+  flex-direction: column;
+}
+
+.flex-grow {
+  flex-grow: 1;
+}
+
+.justify-between {
+  justify-content: space-between;
+}
+
+.items-center {
+  align-items: center;
+}
+
+.border-b {
+  border-bottom: 1rpx solid #f0f0f0;
+}
+
+.border-t {
+  border-top: 1rpx solid #f0f0f0;
+}
+
+.border-eee {
+  border-color: #eeeeee;
+}
+
+.shadow-sm {
+  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
+}
+
+.shadow-md {
+  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
+}
+
+.color-333 {
+  color: #333333;
+}
+
+.color-666 {
+  color: #666666;
+}
+
+.color-999 {
+  color: #999999;
+}
+
+.color-fff {
+  color: #ffffff;
+}
+
+.color-primary {
+  color: #ff6b35;
+}
+
+.color-success {
+  color: #4cd964;
+}
+
+.color-theme {
+  color: #419D95;
+}
+
+.fs-24 {
+  font-size: 24rpx;
+}
+
+.fs-26 {
+  font-size: 26rpx;
+}
+
+.fs-28 {
+  font-size: 28rpx;
+}
+
+.fs-32 {
+  font-size: 32rpx;
+}
+
+.fs-48 {
+  font-size: 48rpx;
+}
+
+.fs-72 {
+  font-size: 72rpx;
+}
+
+.lh-72 {
+  line-height: 72rpx;
+}
+
+.fw-500 {
+  font-weight: 500;
+}
+
+.fw-600 {
+  font-weight: 600;
+}
+
+.opacity-80 {
+  opacity: 0.8;
+}
+
+.cursor-pointer {
+  cursor: pointer;
+}
+
+.w-full {
+  width: 100%;
+}
+</style>

+ 753 - 0
charge-front/src/pages-charge/ordering/ordering.vue

@@ -0,0 +1,753 @@
+<template>
+  <view class="container">
+    <image
+      class="bg"
+      src="/pages-charge/static/charge-ordering-bg.png"
+      mode="widthFix"
+    />
+    <view class="body">
+      <view class="iphonex-placeholder"></view>
+      <view
+        class="status flex-center transition"
+        :style="{
+          opacity: step >= 1 ? '1' : '0',
+        }"
+      >
+        <image
+          :class="['border', `${step <= 2 ? 'border-animation' : ''}`]"
+          src="/pages-charge/static/charge-ordering-border.png"
+          @load="onImgLoad"
+        />
+        <view class="timer flex-column flex-center">
+          <block v-if="step === 1">
+            <view>
+              <text class="fw-600" style="font-size: 100rpx">{{
+                status.time
+              }}</text>
+              <text class="fs-40 fw-500 ml-10">s</text>
+            </view>
+            <view class="fs-26">充电启动中</view>
+          </block>
+          <block v-else-if="step === 2">
+            <image
+              class="icon"
+              src="/pages-charge/static/charge-ordering-icon.png"
+            />
+            <view class="fs-26 mt-10">充电中...</view>
+          </block>
+          <block v-else-if="step === 3">
+            <image
+              class="icon"
+              src="/pages-charge/static/charge-ordering-finish.png"
+            />
+            <view class="fs-26 mt-10">充电结束</view>
+          </block>
+        </view>
+      </view>
+    </view>
+  </view>
+
+  <!-- 停止充电倒计时遮罩 -->
+  <view class="stop-countdown-mask" v-if="stoppingStatus.isStopping">
+    <view class="stop-countdown-modal">
+      <view class="stop-countdown-title">正在停止充电</view>
+      <view class="stop-countdown-time">
+        <text class="stop-countdown-number">{{ stoppingStatus.countdown }}</text>
+        <text class="stop-countdown-unit">秒</text>
+      </view>
+      <view class="stop-countdown-tip">请等待充电桩响应...</view>
+    </view>
+  </view>
+
+  <style-bottom-view>
+    <view
+      class="transition pl-60 pr-60 pb-40"
+      :style="{
+        opacity: step >= 1 ? '1' : '0',
+      }"
+    >
+      <view class="mb-40" v-if="step === 2 || step === 3">
+        <shadow-card :list="chargeInfo"></shadow-card>
+      </view>
+      <style-button
+        v-if="step === 1 || step === 2"
+        type="warning"
+        size="small"
+        @click="cancel"
+        >停止充电</style-button
+      >
+      <style-button
+        v-if="step === 3"
+        type="primary"
+        size="small"
+        @click="reLaunch"
+        >返回首页</style-button
+      >
+    </view>
+  </style-bottom-view>
+</template>
+
+<script setup lang="ts">
+import { onHide, onLoad, onShow } from "@dcloudio/uni-app";
+import { cancelCharge, fetchChargeStatus, startCharge } from "../../api/charge";
+import { ref } from "vue";
+import { format } from "../../utils/date";
+import { to, reLaunch } from "@/utils/navigate";
+
+let timer: any;
+let statusTimer: any;
+let stopCountdownTimer: any;
+const step = ref(0);
+const options = ref<any>();
+const status = ref({
+  time: 30,
+  start: false,
+  cancel: false,
+  error: false,
+});
+const chargeInfo = ref<any[]>([]);
+// 停止充电状态
+const stoppingStatus = ref({
+  isStopping: false,
+  countdown: 15,
+});
+const startStartTimer = () => {
+  if (status.value.start || status.value.cancel || status.value.error) {
+    timer && clearTimeout(timer);
+    return;
+  }
+  timer = setTimeout(() => {
+    if (status.value.time <= 1 && !status.value.start) {
+      uni.showModal({
+        title: "充电错误",
+        content: "出现问题,请重试",
+        showCancel: false,
+        confirmText: "知道了",
+        confirmColor: "#2d9e95",
+        success() {
+          uni.navigateBack();
+        },
+      });
+      return;
+    }
+    status.value.time = status.value.time - 1;
+    startStartTimer();
+  }, 1000);
+};
+const startStatusTimer = () => {
+  statusTimer = setTimeout(() => {
+    fetchChargeStatus()
+      .then((res) => {
+        if (status.value.cancel) {
+          return;
+        }
+        if (res.isStarted) {
+          setChargeData(res);
+        }
+        startStatusTimer();
+      })
+      .catch(() => {
+        startStatusTimer();
+      });
+  }, 30000);
+};
+const start = () => {
+  step.value = 1;
+  startStartTimer();
+  startCharge(options.value && options.value.connectorId ? options.value.connectorId : "")
+    .then(() => {
+      fetchStatus();
+    })
+    .catch((err) => {
+      setTimeout(() => {
+        status.value.error = true;
+        timer && clearTimeout(timer);
+        uni.showModal({
+          content: `${err.errMsg}`,
+          showCancel: false,
+          success: () => {
+            uni.navigateBack();
+          },
+        });
+      }, 300);
+    });
+};
+
+const cancel = () => {
+  uni.showModal({
+    title: "停止充电",
+    content: "是否停止充电",
+    cancelText: "停止",
+    cancelColor: "#999999",
+    confirmText: "点错了",
+    confirmColor: "#2d9e95",
+    success: (res) => {
+      if (res.cancel) {
+        clearTimeout(timer);
+        clearTimeout(statusTimer);
+        status.value.cancel = true;
+        if ([1, 2].includes(step.value)) {
+          if (status.value.start) {
+            finish();
+          } else {
+            // 显示停止充电倒计时遮罩
+            stoppingStatus.value.isStopping = true;
+            stoppingStatus.value.countdown = 15;
+            startStopCountdown();
+            cancelCharge(
+              options.value && options.value.connectorId ? options.value.connectorId : ""
+            )
+              .then(() => {
+                // 停止充电后,轮询等待充电桩真正停止
+                waitForChargeStopped();
+              })
+              .catch(() => {
+                clearStopCountdown();
+                uni.showToast({
+                  title: "停止充电失败,请重试",
+                  icon: "none",
+                });
+              });
+          }
+        }
+      }
+    },
+  });
+};
+
+/**
+ * 开始停止充电倒计时
+ */
+const startStopCountdown = () => {
+  stopCountdownTimer = setInterval(() => {
+    if (stoppingStatus.value.countdown > 1) {
+      stoppingStatus.value.countdown--;
+    }
+  }, 1000);
+};
+
+/**
+ * 清除停止充电倒计时
+ */
+const clearStopCountdown = () => {
+  stoppingStatus.value.isStopping = false;
+  if (stopCountdownTimer) {
+    clearInterval(stopCountdownTimer);
+    stopCountdownTimer = null;
+  }
+};
+
+/**
+ * 轮询等待充电桩真正停止
+ * 最多等待15秒,每2秒检查一次充电状态
+ */
+const waitForChargeStopped = () => {
+  const maxRetry = 8; // 最多重试8次,每2秒检查一次,约15秒
+  let retryCount = 0;
+  
+  const checkStopped = () => {
+    fetchChargeStatus()
+      .then((res) => {
+        // 充电状态:0:预约中 1:启动中 2:充电中 3:停止中 4:已结束 5:未知
+        // 如果状态为 4(已结束)或 5(未知)或没有返回数据,认为已停止
+        if (!res || [4, 5].includes(res.chargeStatus)) {
+          clearStopCountdown();
+          // 设置手工停止充电标识
+          getApp<any>().globalData.manualStop = true;
+          uni.showToast({
+            title: "已停止充电",
+            icon: "success",
+          });
+          setTimeout(() => {
+            uni.navigateBack();
+          }, 1500);
+          return;
+        }
+        
+        // 如果状态为 3(停止中),继续等待
+        if (res.chargeStatus === 3) {
+          retryCount++;
+          if (retryCount >= maxRetry) {
+            clearStopCountdown();
+            // 设置手工停止充电标识
+            getApp<any>().globalData.manualStop = true;
+            uni.showModal({
+              title: "温馨提示",
+              content: "充电桩响应较慢,已发送停止指令,请稍后确认充电状态",
+              showCancel: false,
+              confirmText: "知道了",
+              success: () => {
+                uni.navigateBack();
+              },
+            });
+            return;
+          }
+          // 继续轮询
+          setTimeout(checkStopped, 2000);
+          return;
+        }
+        
+        // 如果状态仍为 1 或 2(充电中/启动中),继续等待
+        if ([1, 2].includes(res.chargeStatus)) {
+          retryCount++;
+          if (retryCount >= maxRetry) {
+            clearStopCountdown();
+            // 设置手工停止充电标识
+            getApp<any>().globalData.manualStop = true;
+            uni.showModal({
+              title: "温馨提示",
+              content: "充电桩响应较慢,已发送停止指令,请稍后确认充电状态",
+              showCancel: false,
+              confirmText: "知道了",
+              success: () => {
+                uni.navigateBack();
+              },
+            });
+            return;
+          }
+          // 继续轮询
+          setTimeout(checkStopped, 2000);
+          return;
+        }
+        
+        // 其他状态,认为已停止
+        clearStopCountdown();
+        getApp<any>().globalData.manualStop = true;
+        uni.showToast({
+          title: "已停止充电",
+          icon: "success",
+        });
+        setTimeout(() => {
+          uni.navigateBack();
+        }, 1500);
+      })
+      .catch(() => {
+        retryCount++;
+        if (retryCount >= maxRetry) {
+          clearStopCountdown();
+          getApp<any>().globalData.manualStop = true;
+          uni.showToast({
+            title: "已停止充电",
+            icon: "none",
+          });
+          setTimeout(() => {
+            uni.navigateBack();
+          }, 1500);
+          return;
+        }
+        // 出错后继续轮询
+        setTimeout(checkStopped, 2000);
+      });
+  };
+  
+  checkStopped();
+};
+const finish = () => {
+  // 显示停止充电倒计时遮罩
+  stoppingStatus.value.isStopping = true;
+  stoppingStatus.value.countdown = 15;
+  startStopCountdown();
+  fetchChargeStatus()
+    .then((res) => {
+      if (res.isStarted) {
+        // 当前充电中
+        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}`);
+        const time =
+          min >= 60
+            ? `${parseInt(`${min / 60}`)}小时${parseInt(
+                `${min - parseInt(`${min / 60}`) * 60}`
+              )}分钟`
+            : `${parseInt(`${diff / 60}`)}分钟`;
+        const endFormat = format("y-M-d h:m:s");
+        const totalMoney = (res.totalMoney / 100).toFixed(2);
+        const _chargeInfo = [
+          {
+            label: "累计充电量",
+            value: `${res.totalPower}度`,
+          },
+          {
+            label: "累计费用",
+            value: `${totalMoney}元`,
+          },
+          {
+            label: "开始时间",
+            value: res.startTime || format("y-M-d h:m:s"),
+          },
+          {
+            label: "结束时间",
+            value: endFormat,
+          },
+          {
+            label: "累计用时",
+            value: time,
+          },
+          {
+            label: "订单号",
+            value: res.startChargeSeq,
+            copy:true
+          }
+        ];
+        if (res.soc) {
+          _chargeInfo.unshift({
+            label: "剩余电量",
+            value: `${res.soc}%`,
+          });
+        }
+        chargeInfo.value = _chargeInfo;
+        cancelCharge(options.value && options.value.connectorId ? options.value.connectorId : "")
+          .then(() => {
+            if (res.failReason) {
+              clearStopCountdown();
+              uni.showModal({
+                content: res.failReason,
+                showCancel: false,
+                success() {
+                  uni.navigateBack();
+                },
+              });
+              return;
+            }
+            // 停止充电后,轮询等待充电桩真正停止
+            waitForChargeStoppedAndShowResult();
+          })
+          .catch((err) => {
+            console.log(err);
+            clearStopCountdown();
+            uni.showToast({
+              title: "停止充电失败,请重试",
+              icon: "none",
+            });
+          });
+      }
+    })
+    .catch((err) => {
+      console.log(err);
+      clearStopCountdown();
+      uni.showModal({
+        title: "温馨提示",
+        content: "出现错误,请重试",
+        showCancel: false,
+        confirmText: "重试",
+        confirmColor: "#2d9e95",
+        success: () => {
+          finish();
+        },
+      });
+    });
+};
+
+/**
+ * 轮询等待充电桩真正停止,然后显示充电结束界面
+ * 用于 finish 函数调用
+ */
+const waitForChargeStoppedAndShowResult = () => {
+  const maxRetry = 8; // 最多重试8次,每2秒检查一次,约15秒
+  let retryCount = 0;
+  
+  const checkStopped = () => {
+    fetchChargeStatus()
+      .then((res) => {
+        // 充电状态:0:预约中 1:启动中 2:充电中 3:停止中 4:已结束 5:未知
+        // 如果状态为 4(已结束)或 5(未知)或没有返回数据,认为已停止
+        if (!res || [4, 5].includes(res.chargeStatus)) {
+          clearStopCountdown();
+          // 设置手工停止充电标识
+          getApp<any>().globalData.manualStop = true;
+          // 显示充电结束界面
+          step.value = 3;
+          return;
+        }
+        
+        // 如果状态为 3(停止中),继续等待
+        if (res.chargeStatus === 3) {
+          retryCount++;
+          if (retryCount >= maxRetry) {
+            clearStopCountdown();
+            // 设置手工停止充电标识
+            getApp<any>().globalData.manualStop = true;
+            // 超时也显示结束界面
+            step.value = 3;
+            return;
+          }
+          // 继续轮询
+          setTimeout(checkStopped, 2000);
+          return;
+        }
+        
+        // 如果状态仍为 1 或 2(充电中/启动中),继续等待
+        if ([1, 2].includes(res.chargeStatus)) {
+          retryCount++;
+          if (retryCount >= maxRetry) {
+            clearStopCountdown();
+            // 设置手工停止充电标识
+            getApp<any>().globalData.manualStop = true;
+            // 超时也显示结束界面
+            step.value = 3;
+            return;
+          }
+          // 继续轮询
+          setTimeout(checkStopped, 2000);
+          return;
+        }
+        
+        // 其他状态,认为已停止
+        clearStopCountdown();
+        getApp<any>().globalData.manualStop = true;
+        step.value = 3;
+      })
+      .catch(() => {
+        retryCount++;
+        if (retryCount >= maxRetry) {
+          clearStopCountdown();
+          getApp<any>().globalData.manualStop = true;
+          step.value = 3;
+          return;
+        }
+        // 出错后继续轮询
+        setTimeout(checkStopped, 2000);
+      });
+  };
+  
+  checkStopped();
+};
+const setChargeData = (res: any) => {
+  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}`);
+  const time =
+    min >= 60
+      ? `${parseInt(`${min / 60}`)}小时${parseInt(
+          `${min - parseInt(`${min / 60}`) * 60}`
+        )}分钟`
+      : `${parseInt(`${diff / 60}`)}分钟`;
+  const totalMoney = (res.totalMoney / 100).toFixed(2);
+  const _chargeInfo = [
+    {
+      label: "累计充电量",
+      value: `${res.totalPower}度`,
+    },
+    {
+      label: "累计费用",
+      value: `${totalMoney}元`,
+    },
+    {
+      label: "开始时间",
+      value: res.startTime || format("y-M-d h:m:s"),
+    },
+    {
+      label: "累计用时",
+      value: time,
+    },
+  ];
+  if (res.soc) {
+    _chargeInfo.unshift({
+      label: "剩余电量",
+      value: `${res.soc}%`,
+    });
+  }
+  chargeInfo.value = _chargeInfo;
+};
+const fetchStatus = (data?: any) => {
+  if (status.value.cancel) {
+    return;
+  }
+  const promise = data ? Promise.resolve(data) : fetchChargeStatus();
+  promise
+    .then((res) => {
+      if (res.isStarted) {
+        // 当前充电中
+        status.value.start = true;
+        step.value = 2;
+        setChargeData(res);
+        startStatusTimer();
+      }
+    })
+    .catch((err) => {
+      console.log(err);
+      setTimeout(() => {
+        fetchStatus(data);
+      }, 1000);
+    });
+};
+const onImgLoad = () => {
+  fetchChargeStatus()
+    .then((res) => {
+      if (res && res.isAppointment) {
+        to(`/pages-charge/appointment/appointment?connectorId=${options.value.connectorId}`);
+        return;
+      }
+      if (res && res.isStarted) {
+        if (options.value && options.value.connectorId === res.connectorId) {
+          fetchStatus(res);
+          return;
+        }
+        // 当前充电中
+        uni.showModal({
+          title: "温馨提示",
+          content: "当前有正在充电中的订单",
+          showCancel: false,
+          confirmText: "查看订单",
+          confirmColor: "#2d9e95",
+          success: () => {
+            fetchStatus(res);
+          },
+        });
+      } else {
+        start();
+      }
+    })
+    .catch((err) => {
+      console.log(err);
+      start();
+    });
+};
+
+onLoad((_options: any) => {
+  options.value = _options;
+});
+onHide(() => {
+  timer && clearTimeout(timer);
+  statusTimer && clearTimeout(statusTimer);
+});
+onShow(() => {
+  startStartTimer();
+});
+</script>
+
+<style lang="scss">
+.container {
+  height: 100vh;
+  width: 100%;
+  background-color: #fff;
+  position: relative;
+  overflow: hidden;
+
+  .bg {
+    width: 100%;
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%);
+    transform-origin: center;
+  }
+
+  .body {
+    position: absolute;
+    left: 0;
+    top: 0;
+    height: 100%;
+    width: 100%;
+
+    .iphonex-placeholder {
+      box-sizing: content-box;
+      padding-bottom: constant(safe-area-inset-bottom);
+      padding-bottom: env(safe-area-inset-bottom);
+    }
+
+    .status {
+      width: 420rpx;
+      height: 420rpx;
+      border-radius: 50%;
+      position: relative;
+      margin: 0 auto;
+      margin-top: 130rpx;
+
+      .border {
+        width: 100%;
+        height: 100%;
+      }
+
+      .border-animation {
+        animation: rot 3s linear infinite;
+      }
+
+      .timer {
+        position: absolute;
+        width: 340rpx;
+        height: 340rpx;
+        left: 50%;
+        top: 50%;
+        transform: translate(-50%, -50%);
+        background: linear-gradient(180deg, #ffffff 0%, #ecf3ff 100%);
+        box-shadow: 0px 18rpx 30rpx rgba(0, 0, 0, 0.1);
+        border-radius: 50%;
+        .icon {
+          width: 152rpx;
+          height: 152rpx;
+        }
+      }
+    }
+  }
+}
+
+@keyframes rot {
+  0% {
+    transform: rotate(0deg);
+  }
+
+  100% {
+    transform: rotate(360deg);
+  }
+}
+
+// 停止充电倒计时遮罩样式
+.stop-countdown-mask {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-color: rgba(0, 0, 0, 0.6);
+  z-index: 9999;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.stop-countdown-modal {
+  width: 400rpx;
+  background-color: #fff;
+  border-radius: 24rpx;
+  padding: 60rpx 40rpx;
+  text-align: center;
+}
+
+.stop-countdown-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 30rpx;
+}
+
+.stop-countdown-time {
+  display: flex;
+  align-items: baseline;
+  justify-content: center;
+  margin-bottom: 20rpx;
+}
+
+.stop-countdown-number {
+  font-size: 80rpx;
+  font-weight: 600;
+  color: #2d9e95;
+}
+
+.stop-countdown-unit {
+  font-size: 28rpx;
+  color: #666;
+  margin-left: 8rpx;
+}
+
+.stop-countdown-tip {
+  font-size: 24rpx;
+  color: #999;
+}
+</style>

+ 636 - 0
charge-front/src/pages-charge/orders/orders.vue

@@ -0,0 +1,636 @@
+<template>
+  <view class="page-container">
+    <!-- 订单列表 -->
+    <view class="order-list pl-30 pr-30 pt-20" v-if="infiniteScroller.list && infiniteScroller.list.length">
+      <block v-for="(item, index) in infiniteScroller.list" :key="index">
+        <view 
+          class="order-card bg-fff br-16 mb-24 shadow-sm" 
+          @click="detail(index)"
+          v-if="!item.hidden"
+        >
+          <!-- 订单头部 -->
+          <view class="order-header pl-30 pr-30 pt-28" :class="{ 'invoice-mode': isInvoice }">
+            <!-- 复选框 -->
+            <view class="checkbox-container" v-if="isInvoice">
+              <style-checkbox :checked="item.checked" @click.stop></style-checkbox>
+            </view>
+            <view class="flex-align-center justify-between">
+              <view 
+                class="order-id fs-24 fw-500" 
+                :style="{ opacity: item.invoiceStatus === 1 ? 0.5 : 1 }"
+              >
+                <uni-icons type="document" size="20" color="#666" class="mr-12"></uni-icons>
+                订单号:{{item.startChargeSeq}}
+              </view>
+              
+              <!-- 发票状态标签 -->
+              <view 
+                v-if="
+                  isInvoice &&
+                  (item.invoiceStatus === 1 ||
+                    item.invoiceStatus === 2 ||
+                    item.invoiceStatus === 3)
+                "
+                class="invoice-status-tag flex-center fs-20 br-6 px-16 py-8"
+                :class="{
+                  'status-invoiced': item.invoiceStatus === 1,
+                  'status-cancelled': item.invoiceStatus === 2,
+                  'status-processing': item.invoiceStatus === 3
+                }"
+              >
+                {{ isInvoiceTitles[item.invoiceStatus] }}
+              </view>
+            </view>
+          </view>
+          
+          <!-- 订单信息 -->
+          <view class="order-info pl-30 pr-30 pb-28" :class="{ 'invoice-mode': isInvoice }">
+            <view class="flex-align-center justify-between">
+              <view class="order-time fs-24 color-999">
+                <uni-icons type="time" size="18" color="#999" class="mr-12"></uni-icons>
+                {{item.endTime || '未结束'}}
+              </view>
+              <view class="order-amount flex-align-center">
+                <view class="fs-32 fw-600 color-theme mr-8">¥{{ item.payAmount }}</view>
+                <uni-icons type="right" size="16" color="#ccc" class="ml-8"></uni-icons>
+              </view>
+            </view>
+          </view>
+        </view>
+      </block>
+    </view>
+    
+    <!-- 空状态 -->
+    <view class="empty-state flex-center flex-column" v-if="infiniteScroller.list && infiniteScroller.list.length <= 0">
+      <uni-icons type="empty" size="80" color="#ccc"></uni-icons>
+      <view class="fs-30 color-999 mt-24">暂无订单数据</view>
+    </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-theme mr-8">{{ checkedCount }}</view>
+          <view class="fs-28 color-000">个订单,共</view>
+          <view class="fs-28 color-theme 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="flex-align-center ml-auto mr-20">
+          <view
+            class="cancel-btn mr-20 border border-theme height-80 br-80 fs-32 fw-500 color-theme flex-center"
+            style="width: 180rpx"
+            @click="cancelInvoice"
+            >取消</view
+          >
+          <view
+            class="mr-40 bg-theme height-80 br-80 fs-32 fw-500 color-fff flex-center"
+            style="width: 216rpx"
+            @click="nextInvoice"
+            >下一步</view
+          >
+        </view>
+      </view>
+    </style-bottom-view>
+  </view>
+</template>
+
+<script setup lang="ts">
+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";
+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;
+      });
+    }
+    console.log(res.list)
+    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;
+  }
+  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 cancelInvoice = () => {
+  isInvoice.value = false;
+  // 重置所有选中状态
+  infiniteScroller.list.forEach((item: any) => {
+    item.checked = false;
+  });
+  setCheckData();
+};
+// 获取发票历史
+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(() => {
+  infiniteScroller.next();
+});
+</script>
+
+<style lang="scss">
+.page-container {
+  background-color: #f5f7fa;
+  min-height: 100vh;
+}
+
+.order-list {
+  padding-bottom: 20rpx;
+}
+
+.order-card {
+  background-color: #ffffff;
+  border-radius: 16rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
+  transition: all 0.3s ease;
+  position: relative;
+  overflow: hidden;
+  
+  &:hover {
+    transform: translateY(-2rpx);
+    box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
+  }
+}
+
+.checkbox-container {
+  position: absolute;
+  left: 24rpx;
+  top: 28rpx;
+  z-index: 2;
+}
+
+.order-header {
+  border-bottom: 1rpx solid #f0f0f0;
+  padding-bottom: 20rpx;
+}
+
+.order-header.invoice-mode {
+  padding-left: 80rpx !important;
+}
+
+.order-info {
+  padding-top: 20rpx;
+}
+
+.order-info.invoice-mode {
+  padding-left: 80rpx !important;
+}
+
+.order-id {
+  color: #333;
+  display: flex;
+  align-items: center;
+  white-space: nowrap;
+  flex: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.invoice-status-tag {
+  font-size: 20rpx;
+  font-weight: 500;
+  white-space: nowrap;
+  min-width: 80rpx;
+  text-align: center;
+  
+  &.status-invoiced {
+    background-color: rgba(65, 157, 149, 0.1);
+    color: #419D95;
+  }
+  
+  &.status-cancelled {
+    background-color: #ffebee;
+    color: #f44336;
+  }
+  
+  &.status-processing {
+    background-color: rgba(65, 157, 149, 0.1);
+    color: #419D95;
+  }
+}
+
+.order-info {
+  padding-top: 20rpx;
+}
+
+.order-time {
+  display: flex;
+  align-items: center;
+}
+
+.order-amount {
+  color: #419D95;
+  font-weight: 600;
+  display: flex;
+  align-items: center;
+}
+
+.empty-state {
+  min-height: 60vh;
+  padding-top: 100rpx;
+}
+
+.invoice-placeholder {
+  height: 60rpx;
+}
+
+.foot-placeholder {
+  height: 112rpx;
+}
+
+.foot {
+  height: 112rpx;
+  border-top: 1px solid rgba(0, 0, 0, 0.1);
+  background-color: #ffffff;
+}
+
+/* 工具类 */
+.shadow-sm {
+  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
+}
+
+.bg-fff {
+  background-color: #ffffff;
+}
+
+.br-16 {
+  border-radius: 16rpx;
+}
+
+.br-12 {
+  border-radius: 12rpx;
+}
+
+.br-8 {
+  border-radius: 8rpx;
+}
+
+.br-6 {
+  border-radius: 6rpx;
+}
+
+.p-20 {
+  padding: 20rpx;
+}
+
+.px-16 {
+  padding-left: 16rpx;
+  padding-right: 16rpx;
+}
+
+.py-8 {
+  padding-top: 8rpx;
+  padding-bottom: 8rpx;
+}
+
+.mb-24 {
+  margin-bottom: 24rpx;
+}
+
+.mr-28 {
+  margin-right: 28rpx;
+}
+
+.mr-12 {
+  margin-right: 12rpx;
+}
+
+.mr-8 {
+  margin-right: 8rpx;
+}
+
+.ml-8 {
+  margin-left: 8rpx;
+}
+
+.ml-16 {
+  margin-left: 16rpx;
+}
+
+.ml-24 {
+  margin-left: 24rpx;
+}
+
+.mt-24 {
+  margin-top: 24rpx;
+}
+
+.mt-20 {
+  margin-top: 20rpx;
+}
+
+.flex-center {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.flex-align-center {
+  display: flex;
+  align-items: center;
+}
+
+.flex-column {
+  display: flex;
+  flex-direction: column;
+}
+
+.flex-grow {
+  flex-grow: 1;
+}
+
+.flex-inline {
+  display: inline-flex;
+}
+
+.justify-between {
+  justify-content: space-between;
+}
+
+.bg-fff {
+  background-color: #ffffff;
+}
+
+.color-333 {
+  color: #333333;
+}
+
+.color-666 {
+  color: #666666;
+}
+
+.color-999 {
+  color: #999999;
+}
+
+.color-primary {
+  color: #ff6b35;
+}
+
+.color-theme {
+  color: #419D95;
+}
+
+.color-fff {
+  color: #ffffff;
+}
+
+.bg-primary {
+  background-color: #ff6b35;
+}
+
+.bg-theme {
+  background-color: #419D95;
+}
+
+.cancel-btn {
+  transition: all 0.3s ease;
+  border: 2rpx solid #419D95;
+  background-color: transparent;
+  
+  &:hover {
+    background-color: rgba(65, 157, 149, 0.05);
+    transform: translateY(-2rpx);
+    box-shadow: 0 2rpx 8rpx rgba(65, 157, 149, 0.2);
+  }
+  
+  &:active {
+    background-color: rgba(65, 157, 149, 0.1);
+    transform: translateY(0);
+  }
+}
+
+.fs-20 {
+  font-size: 20rpx;
+}
+
+.fs-24 {
+  font-size: 24rpx;
+}
+
+.fs-28 {
+  font-size: 28rpx;
+}
+
+.fs-30 {
+  font-size: 30rpx;
+}
+
+.fs-32 {
+  font-size: 32rpx;
+}
+
+.fs-36 {
+  font-size: 36rpx;
+}
+
+.fw-500 {
+  font-weight: 500;
+}
+
+.fw-600 {
+  font-weight: 600;
+}
+
+.height-46 {
+  height: 46rpx;
+}
+
+.height-72 {
+  height: 72rpx;
+}
+
+.height-80 {
+  height: 80rpx;
+}
+
+.width-96 {
+  width: 96rpx;
+}
+
+.pl-30 {
+  padding-left: 30rpx;
+}
+
+.pr-30 {
+  padding-right: 30rpx;
+}
+
+.pt-20 {
+  padding-top: 20rpx;
+}
+
+.pt-28 {
+  padding-top: 28rpx;
+}
+
+.pb-28 {
+  padding-bottom: 28rpx;
+}
+
+</style>

+ 194 - 0
charge-front/src/pages-charge/search/search.vue

@@ -0,0 +1,194 @@
+<template>
+  <view style="opacity: 0; pointer-events: none">
+    <navigation-bar></navigation-bar>
+  </view>
+
+  <image
+      src="/static/images/back.png"
+      mode="widthFix"
+      class="back"
+      @click="back"
+      :style="backStyle"
+  ></image>
+
+  <view
+      class="search flex-align-center"
+      v-if="searchStyle"
+      :style="searchStyle"
+  >
+    <view class="search_icon flex-center flex-shrink">
+      <icon type="search" size="18" color="rgba(0,0,0,0.3)"></icon>
+    </view>
+    <input
+        class="search_input flex-grow"
+        :value="keyword"
+        type="text"
+        :focus="true"
+        placeholder="搜索附近充电站"
+        placeholder-class="input-placeholder"
+        @input="input"
+        @confirm="search"
+        confirm-type="search"
+    />
+    <view
+        class="search_close flex-align-center flex-shrink"
+        @click.stop="clear"
+        v-if="keyword"
+    >
+      <image class="icon" src="/static/images/search-close.png"></image>
+    </view>
+  </view>
+
+  <block v-if="list">
+    <view class="pl-20 pr-20" v-if="list.length">
+      <view class="mt-20" v-for="(item, index) in list" :key="index">
+        <charge-station
+            :title="item.stationName"
+            :tag="item.construction"
+            :price="item.totalFee"
+            :fast="item.fastEquipmentInfos"
+            :slow="item.slowEquipmentInfos"
+            :sId="item.StationID"
+            :address="item.address"
+            :latitude="item.location.stationLat"
+            :longitude="item.location.stationLng"
+            :distance="item.distance"
+            :availableParkingNum="item.availableParkingNum"
+            :parkingNum="item.parkingNum"
+            :border="true"
+        ></charge-station>
+      </view>
+    </view>
+    <view class="empty flex-center flex-column" v-else>
+      <image
+          class="image"
+          src="/static/images/search-empty.png"
+          mode="widthFix"
+      ></image>
+      <view class="tip">无搜索结果...</view>
+    </view>
+  </block>
+</template>
+
+<script setup lang="ts">
+import {_getDistance, searchStation} from "@/api/charge";
+import {onLoad} from "@dcloudio/uni-app";
+import {ref} from "vue";
+
+const searchStyle = ref<any>();
+const backStyle = ref<any>();
+const keyword = ref("");
+const list = ref<any[]>();
+
+onLoad(() => {
+  const menu = uni.getMenuButtonBoundingClientRect();
+  searchStyle.value = {
+    top: `${menu.top}px`,
+    height: `${menu.height}px`,
+  };
+  backStyle.value = {
+    top: `${menu.top + 8}px`,
+  };
+});
+
+const back = function () {
+  uni.navigateBack();
+};
+const clear = function () {
+  keyword.value = "";
+};
+const input = function (e: any) {
+  if (e.detail && e.detail.value) {
+    keyword.value = e.detail.value;
+  }
+};
+const search = function () {
+  if (!keyword.value) {
+    uni.showModal({
+      title: "温馨提示",
+      content: "请输入搜素内容",
+      showCancel: false,
+      confirmColor: "#2d9e95",
+    });
+    return;
+  }
+  uni.showLoading({
+    title: "搜索中",
+  });
+  searchStation(keyword.value)
+      .then((res) => {
+        console.log("search result", res)
+        uni.hideLoading();
+        if (res && res.length > 0) {
+          let {latitude, longitude} = getApp<any>().globalData.fetchLocation
+          res.forEach(item => {
+            item.stationLatDistance = _getDistance(
+                latitude||22.696779,
+                longitude||114.044805,
+                item.location.stationLat,
+                item.location.stationLng
+            );
+            item.totalFee = (Number(item.currentPolicyInfo?.servicePrice||0)+Number(item.currentPolicyInfo?.elecPrice||0)).toFixed(2)
+          })
+        }
+        list.value = res;
+      })
+      .catch(() => {
+        uni.hideLoading();
+        list.value = [];
+      });
+};
+</script>
+
+<style lang="scss">
+.back {
+  position: absolute;
+  width: 18rpx;
+  left: 34rpx;
+}
+
+.search {
+  position: absolute;
+  left: 80rpx;
+  width: 450rpx;
+  border-radius: 32rpx;
+  background-color: var(--color-sec);
+
+  &_icon {
+    width: 72rpx;
+  }
+
+  &_input {
+    width: 100%;
+    background-color: rgba(0, 0, 0, 0);
+    font-size: 28rpx;
+  }
+
+  .input-placeholder {
+    color: rgba(0, 0, 0, 0.3);
+  }
+
+  &_close {
+    width: 50rpx;
+
+    .icon {
+      height: 30rpx;
+      width: 30rpx;
+    }
+  }
+}
+
+.empty {
+  margin-top: 240rpx;
+
+  .image {
+    width: 368rpx;
+  }
+
+  .tip {
+    font-size: 24rpx;
+    color: rgba(0, 0, 0, 0.4);
+    margin-top: 20rpx;
+  }
+}
+</style>

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


BIN
charge-front/src/pages-charge/static/charge-input-head.png


BIN
charge-front/src/pages-charge/static/charge-ordering-bg.png


BIN
charge-front/src/pages-charge/static/charge-ordering-border.png


BIN
charge-front/src/pages-charge/static/charge-ordering-finish.png


BIN
charge-front/src/pages-charge/static/charge-ordering-icon.png


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


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


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


BIN
charge-front/src/pages-charge/static/discount-rights-card.png


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


BIN
charge-front/src/pages-charge/static/machines-banner-address.png


BIN
charge-front/src/pages-charge/static/machines-banner-nav.png


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


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


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


+ 85 - 0
charge-front/src/pages-common/activity/activity.vue

@@ -0,0 +1,85 @@
+<template>
+  <view class="page">
+    <block v-if="activity">
+      <!--      <navigation-bar
+              :title="activity.name"
+              :color="'#fff'"
+              :placeholder="false"
+              :background="'#fff'"
+            ></navigation-bar>-->
+      <image
+          v-if="activity.activityDescPage"
+          style="width: 100%"
+          mode="widthFix"
+          :src="activity.activityDescPage"
+      ></image>
+    </block>
+    <view class="desc" v-if="desc&&desc.length>0">
+      <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 class="desc">
+      <view v-html="activity && activity.activityDesc"></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) => {
+    //TODO 改为活动接口查询
+    const a = res.activityList.find((item: any) => item.id == options.id);
+    // desc.value = a?.activityDesc?.split("\n");
+    activity.value = a;
+    if (a && a.name) {
+      uni.setNavigationBarTitle({
+        title: a.name
+      });
+    }
+    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: 40rpx;
+
+    img {
+      max-width: 95%;
+    }
+  }
+}
+</style>

+ 148 - 0
charge-front/src/pages-common/faq/faq.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-customer.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
charge-front/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>

+ 73 - 0
charge-front/src/pages-user/collect/collect.vue

@@ -0,0 +1,73 @@
+<template>
+  <view class="pl-20 pr-20" v-if="list && list.length">
+    <view class="mt-20" v-for="(item, index) in list" :key="index">
+      <charge-station
+        :title="item.stationName"
+        :tag="item.construction"
+        :price="item.totalFee"
+        :fast="item.fastEquipmentInfos"
+        :slow="item.slowEquipmentInfos"
+        :sId="item.StationID"
+        :address="item.address"
+        :latitude="item.location.stationLat"
+        :longitude="item.location.stationLng"
+        :distance="item.distance"
+        :availableParkingNum="item.availableParkingNum"
+        :parkingNum="item.parkingNum"
+      ></charge-station>
+    </view>
+  </view>
+
+  <view
+    class="pt-40 flex-center fs-30"
+    style="color: rgba(0, 0, 0, 0.6)"
+    v-if="list && list.length <= 0"
+    >暂无数据</view
+  >
+
+  <view class="pt-40 flex-center fs-28" style="opacity: 0.6" v-if="!list"
+    >加载中</view
+  >
+</template>
+
+<script setup lang="ts">
+import {_fetchStations, _getDistance, fetchStationByIds} from "../../api/charge";
+import { fetchCollectList } from "../../api/user";
+import { onLoad } from "@dcloudio/uni-app";
+import { ref } from "vue";
+const list = ref<any[]>();
+onLoad(() => {
+  fetchCollectList().then((stationIdList) => {
+    if (stationIdList) {
+      let {latitude, longitude} = getApp<any>().globalData.fetchLocation
+      return _fetchStations(1, 100, latitude, longitude,  stationIdList)
+    } else {
+      return Promise.reject([])
+    }
+
+    // fetchStationByIds(collectIds || []).then((res: any) => {
+    //   list.value = res;
+    // });
+  }).then(res=>{
+    if (res && res.length > 0) {
+      let {latitude, longitude} = getApp<any>().globalData.fetchLocation
+      res.forEach(item => {
+        item.stationLatDistance = _getDistance(
+            latitude||22.696779,
+            longitude||114.044805,
+            item.location.stationLat,
+            item.location.stationLng
+        );
+        item.totalFee = (Number(item.currentPolicyInfo?.servicePrice||0)+Number(item.currentPolicyInfo?.elecPrice||0)).toFixed(2)
+      })
+    }
+    list.value = res;
+  });
+});
+</script>
+
+<style lang="scss">
+page {
+  background-color: #f5f5f5;
+}
+</style>

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

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

+ 294 - 0
charge-front/src/pages-user/profile/profile.vue

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

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


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


+ 276 - 0
charge-front/src/pages-user/wallet-recharge/wallet-recharge.vue

@@ -0,0 +1,276 @@
+<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)">
+          <image
+              v-if="payOptionsDiscount[index]"
+              src="/static/images/coupon-center.png"
+              mode="widthFix"
+              class="tag fs-24 color-fff fw-500  width-40"
+          />
+          <!--          <view
+                        class="tag fs-24 color-fff fw-500"
+                        v-if="payOptionsDiscount[index]">{{ payOptionsDiscount[index] }}
+                    </view>-->
+          {{ item }}
+        </view>
+      </view>
+      <view v-if="activityStationList&&activityStationList.length>0&&payOptionsDiscount[payOption]" class="flex-align-center" @click="handleShowActivityStationDialog">
+        <view class="fs-30 color-666 pd-10" style="padding: 10px 0;">
+          参与活动站点:{{activityStationList[0]}}
+        </view>
+        <view
+            v-if="activityStationList.length>1"
+            class="ml-12 color-primary fs-30">
+          <text>更多</text>
+        </view>
+      </view>
+
+      <view v-if="payOptionsDiscountDay[payOption]" class="flex-align-center">
+        <view class="fs-30 color-666">
+          {{ (payOptionsDiscount[payOption] && payOptionsDiscount[payOption] > 0) ? `享服务费${payOptionsDiscount[payOption]}折,` : '' }}权益有效期{{
+            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-dialog
+  v-if="stationVisible"
+  title="活动站点"
+  @close="stationVisible=false">
+    <view class="activity-station">
+      <view v-for="station in activityStationList" :key="station" class="activity-station-item">
+        <text>{{station}}</text>
+      </view>
+    </view>
+
+  </style-dialog>
+
+  <style-bottom-view v-if="!stationVisible">
+    <view class="pl-60 pr-60 pb-20">
+      <style-button size="small" type="primary" @click="debounceConfirm">充值</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 {back, to} from "@/utils/navigate";
+import {debounce} from "@/utils/util";
+import StyleDialog from "@/components/style-dialog/style-dialog.vue";
+
+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 rechargeRightsId = ref();
+const rechargeRightsList = ref([]);
+const needBack = ref(false);
+const activityStationList = ref([]);
+const stationVisible = ref(false)
+
+
+const handleShowActivityStationDialog = () => {
+  console.log(stationVisible)
+  if(activityStationList.value&&activityStationList.value.length>0&&payOptionsDiscount.value[payOption.value]){
+    stationVisible.value = true;
+    console.log(stationVisible)
+  }
+}
+
+const input = (e: any) => {
+  payValue.value = e.value;
+  rechargeRightsId.value = 0;
+};
+const changeOption = (index: number) => {
+  payValue.value = 0;
+  payOption.value = index;
+  let find = rechargeRightsList.value.find((k:any) => (k.amountMin || 0) / 100 === payOptions.value[index]);
+  if (find) {
+    rechargeRightsId.value = find?.id;
+  }
+};
+
+const debounceConfirm = debounce(() => {
+  confirm();
+}, 500)
+
+const confirm = () => {
+  if (payValue.value && !/^[0-9]*(\.\d{1,2})?$/.test(`${payValue.value}`)) {
+    uni.showModal({
+      title: "温馨提示",
+      content: "请输入正确的金额",
+      showCancel: false,
+      confirmColor: "#2d9e95",
+    });
+    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: "#2d9e95",
+    });
+    return;
+  }
+  uni.showLoading({
+    title: "加载中",
+  });
+  insertMoney(params, rechargeRightsId.value)
+      .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) => {
+  console.log(options)
+  if (options.value) {
+    payOption.value = payOptions.value.findIndex(
+        (item) => item === Number((Number(options.value) / 100).toFixed(2))
+    );
+    needBack.value = !!options.back;
+  }
+
+  if (options.discount) {
+    payValue.value = Number((Number(options.discount) / 100).toFixed(2));
+  }
+  fetchProfile().then((res) => {
+    // console.log(res);
+    balance.value = res.balance;
+    if (res && res.activityList && res.activityList.length) {
+      let stationList = res.activityList[0].stationList;
+      if(stationList&&stationList.length>0){
+        activityStationList.value = stationList.map(k=>k.stationName)
+      }
+
+      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.discount || 0) / 10).toFixed(1);
+          payOptionsDiscountDay.value[fi] = item.validity > 0 ? item.validity : 1;
+        }
+      });
+      if (res.activityList && res.activityList[0].rechargeRightsList && res.activityList[0].rechargeRightsList.length > 0) {
+        rechargeRightsList.value = res?.activityList[0]?.rechargeRightsList
+      }
+      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: -9px;
+      right: -10px;
+      padding: 2rpx 2rpx;
+    }
+  }
+
+  .option-active {
+    color: var(--color-primary);
+    //border-color: var(--color-primary);
+    //color: #fff;
+    font-weight: 700;
+  }
+}
+
+.activity-station{
+  min-height: 450px;
+  width: 100%;
+  background-color: #f6f7fa;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  align-content: center;
+
+  &-item{
+    margin: 8rpx 0;
+      font-size: 15px;
+  }
+}
+</style>

+ 180 - 0
charge-front/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>

+ 369 - 0
charge-front/src/pages-user/wallet/wallet.vue

@@ -0,0 +1,369 @@
+<template>
+  <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>
+    <view class="body flex-grow flex-column">
+      <view class="tabs flex-shrink flex-align-center">
+        <view
+            v-for="(item, index) in tabs"
+            :key="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 dataList"
+            :key="index"
+            @click="detail(index)"
+        >
+          <view>
+            <view class="fs-30 fw-500" key="title" duration="300">{{
+                typeMap[item.type - 1]
+              }}
+            </view>
+            <view v-if="item.type!==2">
+              <view class="fs-24" style="color: rgba(0, 0, 0, 0.4)">余额</view>
+            </view>
+            <view v-else>
+              <text class="refund-status" :style="refundStyle(item.status)">{{ refundLabel(item.status) }}</text>
+            </view>
+
+          </view>
+          <view class="ml-auto" style="text-align: right">
+            <view class="fs-30 fw-500">
+              <text>{{ item.type == 3 ? "- " : "" }}{{ 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 class="ml-32" v-else>&nbsp;&nbsp;&nbsp;</view>
+        </view>
+      </view>
+      <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>
+</template>
+
+<script setup lang="ts">
+import {useInfiniteScroll} from "../../utils/infinite-scroll";
+import {onReachBottom, onPullDownRefresh, onShow} from "@dcloudio/uni-app";
+import {fetchWallet, listRefund} from "../../api/user";
+import {to} from "../../utils/navigate";
+import {ref} from "vue";
+import {rpxToPx} from "@/utils/device";
+
+const user = ref();
+
+const dataList = ref([])
+const tab = ref(0);
+const tabs = ref([
+  {
+    label: "全部",
+    value: 0,
+  },
+  {
+    label: "充值",
+    value: 1,
+  },
+  {
+    label: "消费",
+    value: 3,
+  },
+  {
+    label: "退款",
+    value: 2,
+  },
+]);
+
+const refundStatusMap = ref([
+  {label: '退款成功 ', value: 'SUCCESS', color: '#4cd964'},
+  {label: '退款关闭  ', value: 'CLOSED', color: '#000000'},
+  {label: '退款处理中 ', value: 'PROCESSING', color: '#007aff'},
+  {label: '退款异常  ', value: 'ABNORMAL', color: '#dd524d'},
+  {label: '退款已申请  ', value: 'NEW', color: '#f0ad4e'},
+])
+
+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);
+      });
+    }
+    return res;
+  });
+});*/
+const scrollViewHeight = ref(0);
+
+onShow(() => {
+  // infiniteScroller.refresh();
+  loadData();
+  if (getApp<any>().globalData.user) {
+    user.value = getApp<any>().globalData.user;
+  }
+})
+
+onReachBottom(() => {
+  // if(tab.value!==2){
+  //   infiniteScroller.next();
+  // }
+  loadData();
+});
+
+onPullDownRefresh(() => {
+  // if(tab.value!==2){
+  //   infiniteScroller.refresh();
+  // }
+  loadData();
+});
+
+const ready = (e: any) => {
+  scrollViewHeight.value =
+      getApp<any>().globalData.device.windowHeight -
+      (e.detail.navigationBarHeight + rpxToPx(380));
+};
+
+const refundLabel = (value: string) => {
+  return refundStatusMap.value.find((item: any) => item.value === value)?.label
+}
+
+const refundStyle = (value: string) => {
+  let rf = refundStatusMap.value.find((item: any) => item.value === value);
+  return setupLabelColorStyle(rf?.color)
+}
+
+const setupLabelColorStyle = (hex: string = "#000000", opacity = 0.2) => {
+  if (!hex) {
+    hex = "#000000"
+  }
+  hex = hex.replace("#", "");
+  // Convert the hex value to RGB values
+  const r = parseInt(hex.substring(0, 2), 16);
+  const g = parseInt(hex.substring(2, 4), 16);
+  const b = parseInt(hex.substring(4, 6), 16);
+
+  return {
+    /*    'text-shadow': `2px 2px 3px rgba(${r},${g},${b},${opacity})`,
+        'background-color': `rgba(${r},${g},${b},${opacity})`,*/
+    'color': `rgb(${r},${g},${b})`
+  }
+}
+
+const changeTab = (index: number) => {
+  tab.value = tabs.value[index].value;
+  loadData();
+  /*  if (tab.value === 2) {
+      let user = getApp<any>().globalData.user;
+      if (user) {
+        listRefund({userId: user.id,pageSize:1000}).then((res: any) => {
+          console.log(res)
+          let  dataList = (res.list || []).map(item => {
+            item.amount = (Number(item.refund) / 100).toFixed(2);
+            item.type = 2;
+            if (item.successTime) {
+              item.transactionTime = item.successTime.slice(0, 3).join("-") + " " + item.successTime.slice(3, 6).join(":")
+            }
+            return item;
+          })
+          infiniteScroller.setList(dataList);
+        })
+      } else {
+        infiniteScroller.refresh();
+      }
+
+    }*/
+}
+
+const loadData = () => {
+  if (!user) return;
+  if (tab.value !== 2) {
+    fetchWallet(tab.value, 1, 1000).then((res: any) => {
+      if (res && res.length) {
+        res.forEach((item: any) => {
+          item.amount = (Number(item.amount) / 100).toFixed(2);
+        });
+      }
+      dataList.value = res;
+    });
+  } else {
+    listRefund({userId: user.value.id, pageSize: 1000}).then((res: any) => {
+      console.log(res)
+      let list = (res.list || []).map(item => {
+        item.amount = (Number(item.refund) / 100).toFixed(2);
+        item.type = 2;
+        if (item.successTime) {
+          item.transactionTime = item.successTime.slice(0, 3).join("-") + " " + item.successTime.slice(3, 6).join(":")
+        }
+        return item;
+      })
+      dataList.value = list;
+    })
+  }
+}
+
+const detail = (index: number) => {
+  if (!dataList.value) {
+    return;
+  }
+  if (dataList.value[index].type === 3) {
+    uni.navigateTo({
+      url: `/pages-charge/order/order?id=${dataList.value[index]?.orderNo}`,
+    });
+  }
+};
+</script>
+
+<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;
+        }
+      }
+    }
+  }
+
+  .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: 130rpx;
+        border-bottom: 1rpx solid rgba(0, 0, 0, 0.1);
+
+        &:last-child {
+          border-bottom: none;
+        }
+      }
+    }
+  }
+}
+
+.refund-status {
+  display: inline-block;
+  //padding: 8rpx;
+  font-size: 22rpx;
+  font-weight: 500;
+}
+</style>

+ 209 - 0
charge-front/src/pages.json

@@ -0,0 +1,209 @@
+{
+  "entryPagePath": "pages/map/map",
+  "pages": [
+    {
+      "path": "pages/map/map",
+      "style": {
+        "disableScroll": true
+      }
+    },
+    {
+      "path": "pages/list/list"
+    },
+    {
+      "path": "pages/user/user"
+    },
+    {
+      "path": "pages/index/index"
+    }
+  ],
+  "subPackages": [
+    {
+      "root": "pages-charge",
+      "pages": [
+        {
+          "path": "machines/machines",
+          "style": {
+            "navigationBarTitleText": "",
+            "navigationStyle": "default",
+            "navigationBarBackgroundColor": "#ffffff"
+          }
+        },
+        {
+          "path": "codeing/codeing",
+          "style": {
+            "navigationBarTitleText": "编码充电",
+            "navigationStyle": "default"
+          }
+        },
+        {
+          "path": "order/order",
+          "style": {
+            "navigationStyle": "default",
+            "navigationBarTitleText": "充电费用"
+          }
+        },
+        {
+          "path": "ordering/ordering",
+          "style": {
+            "navigationBarTitleText": "充电",
+            "navigationStyle": "default",
+            "disableScroll": true
+          }
+        },
+        {
+          "path": "orders/orders",
+          "style": {
+            "navigationStyle": "default",
+            "navigationBarTitleText": "充电订单",
+            "enablePullDownRefresh": true
+          }
+        },
+        {
+          "path": "search/search"
+        },
+        {
+          "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
+          }
+        },
+        {
+          "path": "card/card",
+          "style": {
+            "navigationStyle": "default",
+            "navigationBarTitleText": "我的卡包",
+            "enablePullDownRefresh": true
+          }
+        },
+        {
+          "path": "coupon-center/coupon-center",
+          "style": {
+            "navigationStyle": "default",
+            "navigationBarTitleText": "领券中心"
+          }
+        }
+      ]
+    },
+    {
+      "root": "pages-user",
+      "pages": [
+        {
+          "path": "collect/collect",
+          "style": {
+            "navigationStyle": "default",
+            "navigationBarTitleText": "我的收藏",
+            "navigationBarBackgroundColor": "#F5F5F5"
+          }
+        },
+        {
+          "path": "profile/profile",
+          "style": {
+            "navigationStyle": "default",
+            "navigationBarTitleText": "个人信息"
+          }
+        },
+        {
+          "path": "profile-edit/profile-edit",
+          "style": {
+            "navigationStyle": "default",
+            "navigationBarTitleText": ""
+          }
+        },
+        {
+          "path": "wallet/wallet"
+
+        },
+        {
+          "path": "wallet-refund/wallet-refund",
+          "style": {
+            "disableScroll": true
+          }
+        },
+        {
+          "path": "wallet-recharge/wallet-recharge",
+          "style": {
+            "navigationStyle": "default",
+            "navigationBarTitleText": "我的钱包",
+            "enablePullDownRefresh": true
+          }
+        }
+      ]
+    },
+    {
+      "root": "pages-common",
+      "pages": [
+        {
+          "path": "faq/faq",
+          "style": {
+            "navigationStyle": "default",
+            "navigationBarTitleText": "常见问题",
+            "navigationBarBackgroundColor": "#ffffff"
+          }
+        },
+        {
+          "path": "invoice/invoice",
+          "style": {
+            "navigationStyle": "default",
+            "navigationBarTitleText": "发票详情",
+            "navigationBarBackgroundColor": "#F6F7FA"
+          }
+        },
+        {
+          "path": "activity/activity",
+          "style": {
+            "navigationStyle": "default",
+            "navigationBarTitleText": "活动明细"
+          }
+        }
+      ]
+    }
+  ],
+  "preloadRule": {
+    "pages/map/map": {
+      "network": "all",
+      "packages": ["pages-charge"]
+    }
+  },
+  "globalStyle": {
+    "navigationBarTextStyle": "black",
+    "navigationBarTitleText": "uni-app",
+    "navigationBarBackgroundColor": "#ffffff",
+    "backgroundColor": "#ffffff",
+    "navigationStyle": "custom",
+    "usingComponents": {
+      "navigation-bar": "./wxcomponents/navigation-bar/index"
+    }
+  },
+  "easycom": {
+    "autoscan": true
+  },
+  "tabBar": {
+    "custom": true,
+    "list": [
+      {
+        "pagePath": "pages/map/map"
+      },
+      {
+        "pagePath": "pages/user/user"
+      }
+    ]
+  }
+}

+ 29 - 0
charge-front/src/pages/index/index.vue

@@ -0,0 +1,29 @@
+<template>
+  <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",
+  });
+});
+</script>
+
+<style></style>

+ 69 - 0
charge-front/src/pages/list/list.vue

@@ -0,0 +1,69 @@
+<template>
+  <view class="list">
+    <navigation-bar title="充电站列表"></navigation-bar>
+    <view class="pl-20 pr-20" v-if="infiniteScroller.list">
+      <view
+        class="mt-20"
+        v-for="(item, index) in infiniteScroller.list"
+        :key="index"
+      >
+        <charge-station
+          :title="item.stationName"
+          :tag="item.construction"
+          :price="item.totalFee"
+          :fast="item.fastEquipmentInfos"
+          :slow="item.slowEquipmentInfos"
+          :sId="item.StationID"
+          :distance="item.distance"
+          :address="item.address"
+          :latitude="item.location.stationLat"
+          :longitude="item.location.stationLng"
+        ></charge-station>
+      </view>
+    </view>
+    <view
+      class="flex-center flex-column pt-79"
+      v-if="infiniteScroller.list && infiniteScroller.list.length <= 0"
+    >
+      <image class="empty" src="/static/images/map-empty.png" mode="widthFix" />
+      <view class="fs-22 mt-14" style="color: rgba(0, 0, 0, 0.5)"
+        >暂无充电站信息</view
+      >
+    </view>
+    <view class="flex-center flex-column pt-30" v-if="!infiniteScroller.list">
+      <view class="fs-28 mt-14" style="color: rgba(0, 0, 0, 0.5)">加载中</view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+// 废弃页面
+import { fetchStations } from "../../api/charge";
+import { fetchCollectList } from "../../api/user";
+import { useInfiniteScroll } from "../../utils/infinite-scroll";
+import { onLoad, onReachBottom } from "@dcloudio/uni-app";
+const infiniteScroller = useInfiniteScroll(6, (page) => {
+  let {latitude, longitude} = getApp<any>().globalData.fetchLocation
+  return fetchStations(page, 6,latitude, longitude);
+});
+onLoad(() => {
+  fetchCollectList().then(() => {
+    infiniteScroller.refresh();
+  });
+});
+onReachBottom(() => {
+  infiniteScroller.next();
+});
+</script>
+
+<style>
+.list {
+  width: 100w;
+  min-height: 100vh;
+  background-color: #f5f5f5;
+}
+
+.empty {
+  width: 80px;
+}
+</style>

+ 996 - 0
charge-front/src/pages/map/map.vue

@@ -0,0 +1,996 @@
+<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: 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"
+      min-scale="1"
+      :scale="mapProps.scale"
+      @regionchange="mapChange"
+      @updated="mapUpdated"
+      @labeltap="tapMarker"
+      @markertap="tapMarker"
+      :show-scale="true"
+    ></map>
+    <block v-else>
+      <view
+        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-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>
+    </block>
+    <view
+      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="touchCardStart"
+      @touchmove="touchCardMove"
+    >
+      <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.distance"
+              :latitude="item.location.stationLat"
+              :longitude="item.location.stationLng"
+              :availableParkingNum="item.availableParkingNum"
+              :parkingNum="item.parkingNum"
+              :activity-list="item.activityList"
+              :fromMap="true"
+            ></charge-station>
+          </view>
+          <view class="station-placeholder"></view>
+          <view class="station-iphonex"></view>
+        </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].distance"
+              :latitude="station[markersIndex].location.stationLat"
+              :longitude="station[markersIndex].location.stationLng"
+              :availableParkingNum="station[markersIndex].availableParkingNum"
+              :parkingNum="station[markersIndex].parkingNum"
+              :activity-list="station[markersIndex].activityList"
+              :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>
+      </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">
+    <button open-type="getPhoneNumber" @getphonenumber="loginMask" class="full">
+      登录按钮
+    </button>
+  </view>
+</template>
+
+<script setup lang="ts">
+import {debounce, throttle} from "@/utils/util";
+
+const defaulDistance = 3;
+const defaultScale = 12;
+const pointSize = {
+  width: 30,
+  height: 55,
+  fontSize: 7,
+  iconPath: "/static/images/map-point.png",
+  currentWidth: 42,
+  currentHeight: 72,
+  currentFontSize: 10,
+  currentIconPath: "/static/images/map-point-current.png",
+  androidX:-4,
+  androidCurrentX: -7,
+};
+import { fetchHomeBanner } from "@/api";
+import { deCode } from "../../utils/code";
+import { rpxToPx } from "../../utils/device";
+import { fetchToken, login, onLogin } from "@/api/auth";
+import { fetchStations, fetchChargeStatus } from "@/api/charge";
+import { fetchCollectList } from "@/api/user";
+import { fetchLocation } from "@/utils/location";
+import { to } from "@/utils/navigate";
+import { onLoad, onShow } from "@dcloudio/uni-app";
+import { ref } from "vue";
+
+const currentLocation = ref(null)
+const isIOS = ref(false);
+const token = ref<string>();
+const ready = ref(false);
+const loading = ref(false);
+const styleData = ref({
+  dialog: "",
+  dialogHeight: 0,
+  dialogPlaceHolderHeight: 0,
+  cardHeight: 0,
+  navHeight: 0,
+});
+
+const filterDialog = ref({
+  discounts: false,
+  distanceSelector: false,
+  distanceRange: [
+    {
+      value: 1,
+      scale: defaultScale + 3,
+    },
+    {
+      value: 2,
+      scale: defaultScale + 1,
+    },
+    {
+      value: 3,
+      scale: defaultScale,
+    },
+    {
+      value: 5,
+      scale: defaultScale - 0.2,
+    },
+    {
+      value: 10,
+      scale: defaultScale - 0.5,
+    },
+    {
+      value: 20,
+      scale: defaultScale - 1.5,
+    },
+    {
+      value: 30,
+      scale: defaultScale - 2,
+    },
+    {
+      value: 50,
+      scale: defaultScale - 2.5,
+    },
+    {
+      value: 100,
+      scale: defaultScale - 3.5,
+    },
+    {
+      value: 200,
+      scale: defaultScale - 4.5,
+    },
+  ],
+  options: {
+    distance: defaulDistance,
+    status: 0,
+  },
+  status: [
+    {
+      title: "全部",
+    },
+    {
+      title: "空闲",
+    },
+    {
+      title: "忙碌",
+    },
+  ],
+});
+
+const stationPage = ref({
+  page: 1,
+  pageSize: 6,
+  hasNext: false,
+});
+const station = ref<any[]>([]);
+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;
+
+const refreshStation = (location: any) => {
+  let length = 0;
+  let available = 0;
+  const { latitude, longitude } = location;
+  if (!token.value) {
+    return;
+  }
+  return fetchStations(
+    stationPage.value.page,
+    stationPage.value.pageSize,
+    latitude,
+    longitude,
+    mapProps.value.selflatitude,
+    mapProps.value.selflongitude,
+    {
+      distance: filterDialog.value.options.distance,
+      status: filterDialog.value.options.status,
+    }
+  ).then((res) => {
+    const _markersIndex = stationPage.value.page === 1 ? 0 : markersIndex.value;
+    // console.log("xxxxx",res)
+    const _markers: any[] = res.map((item, index) => {
+      item.totalFee =(Number(item.currentPolicyInfo?.servicePrice||0)+Number(item.currentPolicyInfo?.elecPrice||0)).toFixed(2)
+      length = 0;
+      available = 0;
+      // item.equipmentInfos &&
+      //   item.equipmentInfos.forEach((eq: any) => {
+      //     eq.connectorInfos &&
+      //       eq.connectorInfos.forEach((co: any) => {
+      //         length += 1;
+      //         if (
+      //           co.connectorStatusInfo &&
+      //           co.connectorStatusInfo.status === 1
+      //         ) {
+      //           available += 1;
+      //         }
+      //       });
+      //   });
+      return {
+        id: Number(item.StationID),
+        // (Number()+Number(item.electricityFee)||0).toFixed(2),
+        latitude: item.location.stationLat,
+        longitude: item.location.stationLng,
+        iconPath:
+          index === _markersIndex
+            ? pointSize.currentIconPath
+            : pointSize.iconPath,
+        width:
+          index === _markersIndex ? pointSize.currentWidth : pointSize.width,
+        height:
+          index === _markersIndex ? pointSize.currentHeight : pointSize.height,
+        label: {
+          content: `${item.availableParkingNum||0}/${item.parkingNum||0}`,
+          // content: `1/10`,
+          color: "#ffffff",
+          fontSize:
+            index === _markersIndex
+              ? pointSize.currentFontSize
+              : pointSize.fontSize,
+          textAlign: isIOS.value ? "center" : "left",
+          anchorX: isIOS.value
+            ? 0
+            : index === _markersIndex
+            ? -17 // 针对安卓设备调整 anchorX 实现居中
+            : -10,
+          anchorY: -(
+            (index === _markersIndex
+              ? pointSize.currentHeight
+              : pointSize.height- 3)
+          ),
+        },
+      };
+    });
+    if (stationPage.value.page === 1) {
+      _markers.push({
+        id: -1,
+        latitude: mapProps.value.selflatitude,
+        longitude: mapProps.value.selflongitude,
+        iconPath: "/static/images/map-current.png",
+        width: 34,
+        height: 34,
+      });
+    }
+    isIgnoreChangeLocation = markers.value.length !== _markers.length;
+    stationPage.value.hasNext = res.length >= stationPage.value.pageSize;
+    // empty.value = stationPage.value.page === 1 ? res.length <= 0 : false;
+    station.value =
+      stationPage.value.page === 1 ? res : [...station.value, ...res];
+    markersIndex.value = _markersIndex;
+    markers.value = _markers;
+    return res;
+  });
+};
+
+const refresh = () => {
+  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;
+  fetchLocation()
+    .then((res: any) => {
+      mapProps.value.latitude = res.latitude;
+      mapProps.value.longitude = res.longitude;
+      mapProps.value.selflatitude = res.latitude;
+      mapProps.value.selflongitude = res.longitude;
+      isIgnoreChangeLocation = true;
+      return refreshStation(res);
+    })
+    .then(() => {
+      uni.hideLoading();
+      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);
+  // 扫普通码
+  if (query.q) {
+    console.log("扫普通码", decodeURIComponent(query.q));
+    getApp<any>().globalData.normalCode = decodeURIComponent(query.q); // 获取到二维码原始链接内容
+  }
+  const device = uni.getSystemInfoSync();
+  isIOS.value = device.osName === "ios";
+
+  setTimeout(() => {
+    token.value = getApp<any>().globalData.token || "";
+    if (!token.value) {
+      isIgnoreChangeLocation = true;
+      fetchLocation().then((res: any) => {
+        mapProps.value.latitude = res.latitude;
+        mapProps.value.longitude = res.longitude;
+        mapProps.value.selflatitude = res.latitude;
+        mapProps.value.selflongitude = res.longitude;
+        markers.value = [
+          {
+            id: -1,
+            latitude: res.latitude,
+            longitude: res.longitude,
+            iconPath: "/static/images/map-current.png",
+            width: 34,
+            height: 34,
+          },
+        ];
+      });
+      onLogin((_token) => {
+        if (getApp<any>().globalData.normalCode) {
+          const code: string = getApp<any>().globalData.normalCode;
+          getApp<any>().globalData.normalCode = "";
+          deCode(code);
+        }
+        token.value = _token;
+        fetchCollectList().then(() => {
+          refresh();
+          fetchCharging();
+          fetchBanner();
+        });
+      });
+      return;
+    }
+    fetchCollectList().then(() => {
+      if (getApp<any>().globalData.normalCode) {
+        const code: string = getApp<any>().globalData.normalCode;
+        getApp<any>().globalData.normalCode = "";
+        deCode(code);
+      }
+      refresh();
+      fetchCharging();
+      fetchBanner();
+    });
+  }, 300);
+});
+
+onShow(() => {
+  if (token.value) {
+    fetchBanner();
+    charging.value = undefined;
+    setTimeout(() => {
+      fetchCharging();
+    }, 2000);
+  }
+});
+
+const fetchCharging = () => {
+  //手工停止充电后,暂时不查询充电状态
+  if (getApp<any>().globalData.manualStop) {
+    return;
+  }
+  fetchChargeStatus().then((res) => {
+    if (res && [0, 1, 2, 3].includes(res.chargeStatus)) {
+      charging.value = res;
+    } else {
+      // 充电已结束或无充电数据,重置手工停止标识
+      if (getApp<any>().globalData.manualStop) {
+        getApp<any>().globalData.manualStop = false;
+      }
+      charging.value = undefined;
+    }
+  }).catch(() => {
+    // 查询失败时不重置 manualStop,保持当前状态
+  });
+};
+
+const fetchBanner = () => {
+  fetchHomeBanner().then((res) => {
+    mapBanner.value = res.filter((item: any) => item.status === 1);
+  });
+};
+
+// 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 changeFilterDistance = (index: number) => {
+  filterDialog.value.options.distance =
+    filterDialog.value.distanceRange[index].value;
+  if (mapProps.value.scale === filterDialog.value.distanceRange[index].scale) {
+    refresh();
+    return;
+  }
+  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");
+    const { latitude, longitude } = res;
+    mapCtx.moveToLocation({
+      latitude,
+      longitude,
+    });
+    mapProps.value.scale = defaultScale;
+  });
+};
+
+const mapUpdated = (e: any) => {
+  // console.log('map updated', isIgnoreChangeLocation)
+  setTimeout(() => {
+    isIgnoreChangeLocation = false;
+  }, 500);
+};
+const mapChange = (e: any) => {
+  if (isIgnoreChangeLocation) {
+    return;
+  }
+  if (e.type === "end" && markers.value.length) {
+    //TODO 防抖500ms触发一次
+    console.log("throttle")
+    currentLocation.value = e.target.centerLocation
+    // throttle(()=>{
+    //
+    // },500)
+    // console.log("map change end", {
+    //   ...e.detail.centerLocation,
+    // });
+    debounceRefreshStation()
+  }
+};
+
+
+const debounceRefreshStation = throttle(() => {
+  const { latitude, longitude } = currentLocation.value;
+  stationPage.value.page = 1;
+  console.log("debounceRefreshStation run...")
+  refreshStation({
+    latitude,
+    longitude,
+  });
+}, 500)
+
+const _changeMarker = (current: number) => {
+  const _markers = JSON.parse(JSON.stringify(markers.value));
+  const markersNewIndex = current;
+  _markers[markersIndex.value].iconPath = pointSize.iconPath;
+  _markers[markersIndex.value].width = pointSize.width;
+  _markers[markersIndex.value].height = pointSize.height;
+  _markers[markersIndex.value].label.fontSize = pointSize.fontSize;
+  _markers[markersIndex.value].label.anchorY = -(pointSize.height - 3);
+  _markers[markersIndex.value].label.textAlign = isIOS.value ? "center" : "left";
+  _markers[markersIndex.value].label.anchorX = isIOS.value ? 0 : -10;
+
+  _markers[markersNewIndex].iconPath = pointSize.currentIconPath;
+  _markers[markersNewIndex].width = pointSize.currentWidth;
+  _markers[markersNewIndex].height = pointSize.currentHeight;
+  _markers[markersNewIndex].label.fontSize = pointSize.currentFontSize;
+  _markers[markersNewIndex].label.textAlign = isIOS.value ? "center" : "left";
+  _markers[markersNewIndex].label.anchorY = -(pointSize.currentHeight - 3);
+  _markers[markersNewIndex].label.anchorX = isIOS.value ? 0 : -17;
+
+  isIgnoreChangeLocation = true;
+  markers.value = _markers;
+  markersIndex.value = markersNewIndex;
+  if (stationPage.value.hasNext && markersNewIndex >= _markers.length - 2) {
+    stationPage.value.page += 1;
+    refreshStation({
+      latitude: mapProps.value.selflatitude,
+      longitude: mapProps.value.selflongitude,
+    });
+  }
+};
+const tapMarker = (e: any) => {
+  if (e.detail.markerId === -1) {
+    return;
+  }
+  const findIndex = station.value.findIndex(
+    (item) => Number(item.StationID) === Number(e.detail.markerId)
+  );
+  if (findIndex >= 0) {
+    _changeMarker(findIndex);
+  }
+};
+const loginMask = (e: any) => {
+  login(e);
+};
+
+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 = 200;
+  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?connectorId=${charging.value.connectorId}`
+      : `/pages-charge/ordering/ordering?sn=${charging.value.connectorId}&start=1&connectorId=${charging.value.connectorId}`
+  );
+};
+</script>
+
+<style lang="scss">
+page {
+  background-color: #ffffff;
+}
+
+.container {
+  position: relative;
+  height: 100vh;
+  width: 100vw;
+  background-color: #ffffff;
+}
+
+.mock-map {
+  position: relative;
+  width: 100vw;
+  background-color: #ffffff;
+  overflow: hidden;
+}
+
+.login-mask {
+  position: fixed;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 999999;
+  opacity: 0;
+
+  .full {
+    width: 100%;
+    height: 100%;
+  }
+}
+
+.dialog {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  z-index: 1000;
+
+  &_logo {
+    width: 156rpx;
+    height: 100%;
+    background-position: center;
+    background-size: 100% auto;
+    background-repeat: no-repeat;
+    background-image: url("/static/images/map-logo.png");
+  }
+
+  &_search {
+    width: 352rpx;
+    height: 100%;
+    background-position: center;
+    background-size: 100% auto;
+    background-repeat: no-repeat;
+    background-image: url("/static/images/map-input.png");
+  }
+
+  &_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%;
+      }
+    }
+  }
+
+  &_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 {
+    position: relative;
+    height: 12rpx;
+    width: 100%;
+    border-radius: 8rpx;
+    background-color: var(--color-sec);
+    margin-top: 10rpx;
+
+    &_active {
+      position: absolute;
+      left: 0;
+      top: 0;
+      background-color: var(--color-primary);
+      border-radius: 8rpx;
+      height: 12rpx;
+      width: 0%;
+    }
+
+    &_block {
+      position: absolute;
+      width: 40rpx;
+      height: 40rpx;
+      border-radius: 50%;
+      top: -15rpx;
+      left: 0;
+      background: rgba(255, 255, 255, 1);
+      box-shadow: 0px 4rpx 6rpx rgba(52, 125, 255, 0.4);
+    }
+
+    &_wx {
+      position: absolute;
+      width: 100%;
+      left: 0px;
+      top: -5px;
+      margin: 0;
+      opacity: 0;
+    }
+  } */
+}
+
+.card {
+  position: fixed;
+  width: 100%;
+  left: 0px;
+  z-index: 1000;
+  transition: all 0.3s;
+  box-shadow: 0px 8rpx 20rpx rgba(0, 0, 0, 0.2);
+  background-color: #fff;
+
+  &_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;
+  }
+
+  &_line {
+    height: 1rpx;
+    background-color: rgba(0, 0, 0, 0.1);
+    margin-left: 30rpx;
+    margin-right: 30rpx;
+  }
+
+  &_empty {
+    width: 100%;
+    height: 100%;
+    .image {
+      width: 160rpx;
+    }
+  }
+
+  .station-placeholder {
+    min-height: 104rpx;
+  }
+  .station-iphonex {
+    box-sizing: content-box;
+    padding-bottom: constant(safe-area-inset-bottom);
+    padding-bottom: env(safe-area-inset-bottom);
+  }
+}
+
+.charging {
+  position: fixed;
+  z-index: 1000;
+  right: 0;
+  bottom: 164rpx;
+  width: 186rpx;
+  height: 88rpx;
+  border-radius: 88rpx 0px 0px 88rpx;
+  background: #2d9e95;
+  margin-bottom: constant(safe-area-inset-bottom);
+  margin-bottom: env(safe-area-inset-bottom);
+}
+</style>

+ 232 - 0
charge-front/src/pages/user/user.vue

@@ -0,0 +1,232 @@
+<template>
+  <navigation-bar title="个人中心"></navigation-bar>
+
+  <image src="/static/images/user-bg.png" mode="widthFix" class="bg" />
+
+  <block v-if="user && user.mobilePhone">
+    <view class="container" :style="containerStyle">
+      <view class="header flex-column">
+        <view class="flex-grow flex-center">
+          <image
+            src="/static/images/user/round.png"
+            mode="heightFix"
+            style="height: 100%"
+          />
+        </view>
+        <view class="main flex-shrink">
+          <view
+            class="avatar"
+            @click="toPage(2)"
+            :style="{
+              'background-image': `url(${user.avatar})`,
+            }"
+          ></view>
+          <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>
+        </view>
+      </view>
+
+      <view class="body">
+        <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+'.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>
+        </block>
+      </view>
+    </view>
+  </block>
+</template>
+
+<script setup lang="ts">
+import { fetchContact } from "@/api";
+import { fetchProfile } from "@/api/user";
+import { onLoad, onShow } from "@dcloudio/uni-app";
+import { ref } from "vue";
+const containerStyle = ref({});
+const user = ref<any>({
+  avatar: "",
+});
+const service = ref("");
+const menu = ref([
+  {
+    title: "我的卡包",
+    path: "/pages-charge/card/card",
+  },
+  {
+    title: "充电订单",
+    path: "/pages-charge/orders/orders",
+  },
+  {
+    title: "我的收藏",
+    path: "/pages-user/collect/collect",
+  },
+  {
+    title: "个人信息",
+    path: "/pages-user/profile/profile",
+  },
+  {
+    title: "联系我们",
+    path: "",
+  },
+  {
+    title: "常见问题",
+    path: "/pages-common/faq/faq",
+  },
+  {
+    title: "电站列表",
+    path: "/pages/list/list",
+  },
+]);
+const toPage = (index: number) => {
+  if (index < 0) {
+    uni.navigateTo({
+      url: "/pages-user/wallet/wallet",
+    });
+    return;
+  }
+  if (index === 4) {
+    uni.makePhoneCall({
+      phoneNumber: service.value,
+    });
+    return;
+  }
+  const item = menu.value[index];
+  uni.navigateTo({
+    url: item.path + (index === 5 ? `?service=${service.value}` : ""),
+  });
+};
+
+onLoad(() => {
+  const bound = uni.getMenuButtonBoundingClientRect();
+  containerStyle.value = {
+    top: `${bound.bottom + 10}px`,
+  };
+  fetchContact().then((res) => {
+    if (res && res.mobile) {
+      service.value = res.mobile;
+    }
+  });
+});
+onShow(() => {
+  fetchProfile().then((res) => {
+    res.mobilePhoneFormat =
+      res.mobilePhone.slice(0, 3) + "****" + res.mobilePhone.slice(7);
+    res.avatar = res.avatar
+      ? res.avatar
+      : "https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0";
+    user.value = res;
+  });
+});
+</script>
+
+<style lang="scss">
+@import "../../styles/dialog.scss";
+.bg {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 660rpx;
+}
+
+.container {
+  position: absolute;
+  left: 0;
+  top: 0;
+  height: 100vh;
+  width: 100%;
+  overflow-y: auto;
+}
+
+.header {
+  height: 400rpx;
+  width: 100%;
+
+  .main {
+    height: 334rpx;
+    background: rgba(254, 255, 255, 0.7);
+    border-radius: 40rpx 40rpx 0 0;
+    position: relative;
+    text-align: center;
+
+    .avatar {
+      position: absolute;
+      left: 50%;
+      transform: translateX(-50%);
+      top: -50rpx;
+      width: 110rpx;
+      height: 110rpx;
+      border: 2rpx solid #ffffff;
+      filter: drop-shadow(0px 4rpx 8rpx rgba(0, 24, 60, 0.1));
+      background-size: cover;
+      background-repeat: no-repeat;
+      border-radius: 50%;
+    }
+
+    .phone {
+      padding-top: 78rpx;
+      color: #000;
+    }
+
+    .money {
+      width: 690rpx;
+      height: 118rpx;
+      background: #feffff;
+      border-radius: 120rpx;
+      margin: 0 auto;
+      margin-top: 30rpx;
+      color: #000000;
+      padding: 0 24rpx 0 40rpx;
+    }
+  }
+}
+
+.body {
+  width: 100%;
+  background-color: #fff;
+  padding: 0rpx 30rpx;
+
+  .menu {
+    background-color: #fff;
+    height: 120rpx;
+    border-bottom: 1rpx solid rgba(0, 0, 0, 0.1);
+
+    image {
+      width: 52rpx;
+      height: 52rpx;
+      margin-right: 20rpx;
+    }
+
+    &:last-child {
+      border-bottom: none;
+    }
+  }
+}
+</style>

+ 6 - 0
charge-front/src/shime-uni.d.ts

@@ -0,0 +1,6 @@
+export {}
+
+declare module "vue" {
+  type Hooks = App.AppInstance & Page.PageInstance;
+  interface ComponentCustomOptions extends Hooks {}
+}

BIN
charge-front/src/static/images/back.png


BIN
charge-front/src/static/images/contact-customer.png


BIN
charge-front/src/static/images/coupon-center.png


BIN
charge-front/src/static/images/custom-tab-bar/1-1.png


BIN
charge-front/src/static/images/custom-tab-bar/1.png


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


BIN
charge-front/src/static/images/custom-tab-bar/3-1.png


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


BIN
charge-front/src/static/images/custom-tab-bar/4-1.png


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


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


BIN
charge-front/src/static/images/discount-bg.png


BIN
charge-front/src/static/images/discount-coupon.png


BIN
charge-front/src/static/images/discount-rights-card.png


BIN
charge-front/src/static/images/icon-nav.png


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


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


BIN
charge-front/src/static/images/map-current.png


BIN
charge-front/src/static/images/map-empty.png


Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.