فهرست منبع

```
feat(charge): 添加充电功能相关页面和组件

- 新增充电详情页、地图页、站点详情页、启动充电页和充电中页面
- 实现扫码充电功能,支持扫描充电桩二维码开始充电
- 添加充电状态管理和实时更新功能
- 集成地图标记显示充电站点位置
- 实现充电订单统计和展示
- 添加充电终端状态格式化显示
- 修复JSON文件末尾多余逗号问题
- 优化页面布局和样式调整
```

zouzexu 3 روز پیش
والد
کامیت
aee4e1cdc5

+ 25 - 11
src/subPack-charge/chargeDetail/chargeDetail.vue

@@ -1,4 +1,5 @@
 <script setup lang="ts">
+import { formatStatusName } from '../utils/index'
 import router from '@/router'
 import { StaticUrl } from '@/config'
 
@@ -44,6 +45,8 @@ function getStatusImageByStatus(deviceStatus: number) {
       return 'kx'
     case 2: // 占用
       return 'zy'
+    case 3: // 占用
+      return 'zy'
     case 0: // 离线
       return 'lx'
     default:
@@ -71,7 +74,7 @@ function getTabStyle(tab: string) {
     />
     <view :style="{ paddingTop: `${(Number(statusBarHeight) || 44) + MenuButtonHeight + 12}px` }" />
     <view class="content-page box-border px24rpx">
-      <view class="flex items-center gap-24rpx">
+      <view class="flex items-center justify-between">
         <view>
           <view class="text-32rpx font-bold">
             {{ priceDetail?.stationName }}
@@ -81,10 +84,7 @@ function getTabStyle(tab: string) {
           </view>
         </view>
         <view>
-          <image
-            class="h-132rpx w-140rpx"
-            :src="`${StaticUrl}/site-name-icon.png`"
-          />
+          <image class="h-132rpx w-140rpx" :src="`${StaticUrl}/site-name-icon.png`" />
         </view>
       </view>
       <view class="items-centerrounded-16rpx mt-20rpx flex bg-#FFF p-20rpx">
@@ -132,7 +132,11 @@ function getTabStyle(tab: string) {
         </view>
       </view>
       <view v-if="activeTab == 'price'">
-        <view v-for="item in priceDetail?.priceList" :key="item.timePeriod" class="mt-20rpx rounded-16rpx bg-#FFF p-24rpx" :style="{ border: item.currentPeriod ? '2rpx solid #9ED605' : '' }">
+        <view
+          v-for="item in priceDetail?.priceList" :key="item.timePeriod"
+          class="mt-20rpx rounded-16rpx bg-#FFF p-24rpx"
+          :style="{ border: item.currentPeriod ? '2rpx solid #9ED605' : '' }"
+        >
           <view class="relative flex items-center justify-between">
             <view class="flex items-center gap-20rpx">
               <view
@@ -156,7 +160,10 @@ function getTabStyle(tab: string) {
               <view class="text-24rpx text-#222222">
                 抵扣券电价
               </view>
-              <view class="text-24rpx" :style="{ color: item.currentPeriod ? '#FF6464' : '', fontWeight: item.currentPeriod ? '800' : '' }">
+              <view
+                class="text-24rpx"
+                :style="{ color: item.currentPeriod ? '#FF6464' : '', fontWeight: item.currentPeriod ? '800' : '' }"
+              >
                 <text>{{ item.totalPrice }}</text>
                 <text class="text-#AAA">
                   元/度
@@ -167,13 +174,18 @@ function getTabStyle(tab: string) {
         </view>
       </view>
       <view v-if="activeTab == 'terminal'">
-        <view v-for="item in connectorsDetail?.connectorList" :key="item.connectorId" class="mt-20rpx flex items-center gap-20rpx rounded-16rpx bg-#FFF p-20rpx">
+        <view
+          v-for="item in connectorsDetail?.connectorList" :key="item.connectorId"
+          class="mt-20rpx flex items-center gap-20rpx rounded-16rpx bg-#FFF p-20rpx"
+          @click="router.push({ name: 'charge-start', params: { connectorCode: item.connectorCode } })"
+        >
           <view
-            class="h-116rpx w-116rpx text-center line-height-[116rpx]"
+            class="h-116rpx w-116rpx text-center"
             :style="{ backgroundImage: `url(${StaticUrl}/site-status-${getStatusImageByStatus(item.status)}.png)`, backgroundSize: 'cover', backgroundPosition: 'center' }"
           >
+            <image class="mt-20rpx h-38rpx w-27.18rpx" :src="`${StaticUrl}/terminal-icon.png`" />
             <view class="text-24rpx font-bold">
-              {{ item.statusName }}
+              {{ formatStatusName(item.statusName) }}
             </view>
           </view>
           <view>
@@ -191,7 +203,9 @@ function getTabStyle(tab: string) {
       </view>
     </view>
     <view class="h-180rpx" />
-    <view class="fixed bottom-66rpx left-24rpx h-100rpx w-702rpx rounded-16rpx bg-[linear-gradient(90deg,#DBFC81_0%,#9ED605_100%)] text-center text-28rpx font-800 line-height-[100rpx] shadow-[inset_0rpx_6rpx_20rpx_2rpx_#FFFFFF]">
+    <view
+      class="fixed bottom-66rpx left-24rpx h-100rpx w-702rpx rounded-16rpx bg-[linear-gradient(90deg,#DBFC81_0%,#9ED605_100%)] text-center text-28rpx font-800 line-height-[100rpx] shadow-[inset_0rpx_6rpx_20rpx_2rpx_#FFFFFF]"
+    >
       扫码充电
     </view>
   </view>

+ 85 - 45
src/subPack-charge/chargeMap/chargeMap.vue

@@ -18,23 +18,29 @@ onMounted(() => {
   getStationInfoMapList()
 })
 const markersData = ref<any[]>([])
+const markerMapList = ref<any[]>([])
 async function getStationInfoMapList() {
   const res = await Apis.charge.stationInfoMapList({ data: { longitude: Location.value.longitude, latitude: Location.value.latitude } })
+  markerMapList.value = res.data
   res.data.forEach((item: any) => {
     markersData.value.push({
       id: item.stationId,
       latitude: item.latitude,
       longitude: item.longitude,
-      iconPath: '',
+      iconPath: `${StaticUrl}/marker-tag.png`,
       width: 30,
-      height: 30,
+      height: 34,
     })
   })
 }
-const markerShow = ref(true)
+const markerShow = ref(false)
+const selectData = ref<any>({})
 function openMarkerTap(item: any) {
-  console.log(item, '点击标点')
-  markerShow.value = true
+  const selected = markerMapList.value.find((station: any) => station.stationId === item.detail.markerId)
+  if (selected) {
+    selectData.value = selected
+    markerShow.value = true
+  }
 }
 </script>
 
@@ -53,7 +59,6 @@ function openMarkerTap(item: any) {
         </view>
       </template>
     </wd-navbar>
-    <!-- <view :style="{ paddingTop: `${(Number(statusBarHeight) || 44) + MenuButtonHeight + 12}px` }" /> -->
     <view class="relative">
       <view
         class="absolute z-1 box-border px24rpx"
@@ -70,60 +75,95 @@ function openMarkerTap(item: any) {
           :latitude="Location.latitude || 23.129163" :markers="markersData" @markertap.stop="openMarkerTap"
         />
       </view>
-      <view class="absolute top-900rpx px-24rpx">
-        <view class="h-432rpx w-702rpx rounded-32rpx bg-#FFF p-24rpx">
-          <view class="flex items-center justify-between">
-            <view class="text-32rpx font-800">
-              贵阳花果园购物中心充电站
+      <transition name="slide-up">
+        <view v-if="markerShow" class="absolute top-960rpx px-24rpx">
+          <view class="h-432rpx w-660rpx rounded-32rpx bg-#FFF p-24rpx" @click="router.push({ name: 'charge-site-detail', params: { stationId: String(selectData.stationId) } })">
+            <view class="" @click.stop="markerShow = false">
+              <wd-icon name="close" size="16px" color="#9ED605" />
             </view>
-            <view
-              class="ml-150rpx h-44rpx w-148rpx flex items-center border-2rpx border-#9ED605 rounded-34rpx border-solid line-height-[44rpx]"
-            >
-              <view class="w-44rpx rounded-[34rpx_0rpx_0rpx_34rpx] bg-#9ED605 text-center">
-                <wd-icon name="location" size="16px" color="#FFF" />
+            <view class="flex items-center justify-between">
+              <view class="text-32rpx font-800">
+                {{ selectData.stationName }}
               </view>
-              <view class="text-24rpx text-#9ED605">
-                1.55km
+              <view
+                class="ml-150rpx h-44rpx w-148rpx flex items-center border-2rpx border-#9ED605 rounded-34rpx border-solid line-height-[44rpx]"
+              >
+                <view class="w-44rpx rounded-[34rpx_0rpx_0rpx_34rpx] bg-#9ED605 text-center">
+                  <wd-icon name="location" size="16px" color="#FFF" />
+                </view>
+                <view class="text-24rpx text-#9ED605">
+                  {{ selectData.distance }}km
+                </view>
               </view>
             </view>
-          </view>
-          <view class="mt-20rpx text-24rpx text-#AAA">
-            贵州省贵阳市观山湖区贵安新区群航新能源责任有限公司
-          </view>
-          <view class="flex items-center">
-            <view class="h-120rpx w-204rpx rounded-8rpx bg-[linear-gradient(180deg,#45E67D_0%,rgba(218,249,229,0)_100%)] text-center">
-              <view class="h-40rpx w-100rpx rounded-8rpx bg-[linear-gradient(180deg,#4FEF86_0%,#00AA3A_100%)]">
-                快充
-              </view>
-              <view>12/12</view>
+            <view class="mt-20rpx text-24rpx text-#AAA">
+              {{ selectData.address }}
             </view>
-            <view>
-              <view>快充</view>
-              <view>12/12</view>
+            <view class="mt-20rpx flex items-center justify-between">
+              <view class="h-120rpx w-204rpx flex flex-col items-center justify-center rounded-8rpx bg-[linear-gradient(180deg,#45E67D_0%,rgba(218,249,229,0)_100%)]">
+                <view class="mt-16rpx h-40rpx w-100rpx rounded-8rpx bg-[linear-gradient(180deg,#4FEF86_0%,#00AA3A_100%)] text-center text-24rpx text-#FFF font-bold line-height-[40rpx]">
+                  快充
+                </view>
+                <view class="mt-10rpx text-32rpx font-bold">
+                  {{ selectData.fastCharging }}
+                </view>
+              </view>
+              <view class="h-120rpx w-204rpx flex flex-col items-center justify-center rounded-8rpx bg-[linear-gradient(180deg,#BED2FF_0%,rgba(62,82,128,0)_100%)]">
+                <view class="mt-16rpx h-40rpx w-100rpx rounded-8rpx bg-[linear-gradient(180deg,#8EB1FF_0%,#3071FF_100%)] text-center text-24rpx text-#FFF font-bold line-height-[40rpx]">
+                  慢充
+                </view>
+                <view class="mt-10rpx text-32rpx font-bold">
+                  {{ selectData.slowCharging }}
+                </view>
+              </view>
             </view>
-            <view>
-              <view>快充</view>
-              <view>12/12</view>
+            <view class="mt-20rpx h-72rpx w-654rpx flex items-center justify-between bg-#F6FAFF">
+              <view>
+                <text class="text-48rpx text-#FF6464 font-800">
+                  {{ selectData.platformPrice }}
+                </text>
+                <text class="text-24rpx">
+                  元/度
+                </text>
+              </view>
+              <view class="28rpx">
+                {{ selectData.peakValue }}:{{ selectData.peakTime }}
+              </view>
             </view>
-          </view>
-          <view>
-            <view>
-              <text>1.0026</text>
-              <text>元/度</text>
+            <view class="mt-20rpx flex items-center gap-16rpx">
+              <view class="h-30rpx w-30rpx rounded-4rpx bg-#5BE7FF text-center text-24rpx text-#FFF line-height-[30rpx]">
+                P
+              </view>
+              <view class="text-24rpx text-#AAA">
+                {{ selectData.tips }}
+              </view>
             </view>
-            <view>峰:10:00-13:00</view>
-          </view>
-          <view>
-            <view>P</view>
-            <view>充电减免2小时停车费,超出部分按每小时3元计算</view>
           </view>
         </view>
-      </view>
+      </transition>
     </view>
     <chargeFooter />
   </view>
 </template>
 
 <style lang="scss" scoped>
+.slide-up-enter-active, .slide-up-leave-active {
+  transition: transform 0.3s ease;
+}
+
+.slide-up-enter-from {
+  transform: translateY(100%);
+}
 
+.slide-up-enter-to {
+  transform: translateY(0%);
+}
+
+.slide-up-leave-from {
+  transform: translateY(0%);
+}
+
+.slide-up-leave-to {
+  transform: translateY(100%);
+}
 </style>

+ 30 - 3
src/subPack-charge/chargeSiteDetail/chargeSiteDetail.vue

@@ -1,8 +1,8 @@
 <script setup lang="ts">
+import { ScanCodeUtil, formatStatusName } from '../utils/index'
 import router from '@/router'
 import { StaticUrl } from '@/config'
 
-// const swiperList = ['https://www.keaitupian.cn/cjpic/frombd/2/253/1659552792/3869332496.jpg']
 const { statusBarHeight, MenuButtonHeight } = storeToRefs(useSysStore())
 const { Location } = storeToRefs(useAddressStore())
 const { opcity } = storeToRefs(useSysStore())
@@ -102,6 +102,33 @@ function openMap() {
     longitude: Number(stationDetail.value?.longitude),
   })
 }
+
+async function getDeviceInfo(connectorCode: string) {
+  useGlobalLoading().loading({})
+  const res = await Apis.charge.connectorDetail({ data: { connectorCode } })
+  useGlobalLoading().close()
+  if (res.data.status === 0 || res.data.status === 255) {
+    useGlobalMessage().alert('此设备异常或被占用,请更换其他设备')
+  }
+  else {
+    router.push({ name: 'charge-start', params: { connectorCode } })
+  }
+}
+
+async function scanCode() {
+  try {
+    const connectorCode = await ScanCodeUtil.scanAndGetConnectorCode()
+    if (!connectorCode) {
+      useGlobalMessage().alert('二维码不正确')
+      return
+    }
+    // 获取设备信息
+    getDeviceInfo(connectorCode)
+  }
+  catch (error) {
+    console.error('扫码失败:', error)
+  }
+}
 </script>
 
 <template>
@@ -230,7 +257,7 @@ function openMap() {
                 :src="`${StaticUrl}/terminal-icon.png`"
               />
               <view class="text-24rpx font-bold">
-                {{ item?.statusName }}
+                {{ formatStatusName(item.statusName) }}
               </view>
             </view>
             <view>
@@ -269,7 +296,7 @@ function openMap() {
               元/度
             </text>
           </view>
-          <view class="scan-qrcode">
+          <view class="scan-qrcode" @click="scanCode">
             扫码充电
           </view>
         </view>

+ 6 - 3
src/subPack-charge/chargeStart/chargeStart.vue

@@ -41,12 +41,15 @@ const fromData = ref<Api.invokeChargeList>({
   consigneeMobile: userInfo.value?.mobile,
 })
 async function launchCharge() {
-  router.push({ name: 'chargeing' })
-  return
+  // router.push({ name: 'chargeing' })
+  // return
   fromData.value.equipmentId = connectorDetailInfo.value?.equipmentId
   fromData.value.stationId = connectorDetailInfo.value?.stationId
   fromData.value.connectorId = connectorDetailInfo.value?.connectorId
-  await Apis.charge.invokeCharge({ data: fromData.value })
+  const res = await Apis.charge.invokeCharge({ data: fromData.value })
+  if (res.code === '00000') {
+    router.push({ name: 'chargeing', params: { orderNo: res.data.orderNo } })
+  }
 }
 </script>
 

+ 13 - 8
src/subPack-charge/chargeing/chargeing.vue

@@ -18,8 +18,19 @@ definePage({
     navigationStyle: 'custom',
   },
 })
+const chargeingDetail = ref<Api.chargeingCostList>()
+const orderNo = ref()
+const { resume, pause } = useIntervalFn(() => {
+  getChargeingCost()
+}, 5000)
+
+onLoad((options: any) => {
+  orderNo.value = options.orderNo
+})
+
 onMounted(() => {
   getChargeingCost()
+  resume()
   opcity.value = 0
 })
 
@@ -27,14 +38,8 @@ onPageScroll((e) => {
   const calculatedOpacity = e.scrollTop / 100
   opcity.value = Math.min(1, Math.max(0.1, calculatedOpacity))
 })
-
-const chargeingDetail = ref<Api.chargeingCostList>()
-
-// const { pause } = useIntervalFn(async () => {
-//   await getChargeingCost()
-// }, 5000)
 async function getChargeingCost() {
-  const res = await Apis.charge.chargeingCost({ data: { orderNo: '' } })
+  const res = await Apis.charge.chargeingCost({ data: { orderNo: orderNo.value } })
   chargeingDetail.value = res.data
 }
 
@@ -67,7 +72,7 @@ async function initiatedStopCharge() {
   useGlobalLoading().loading({})
   const res: any = await Apis.charge.stopCharge({ data: { chargeOrderNo: chargeingDetail.value?.chargeOrderNo } })
   if (res.code === '00000') {
-    // pause()
+    pause()
     clearInterval(longPressTimer.value)
     useGlobalToast().success('停止充电成功')
     stopCharge.value = false // 关闭弹窗

+ 31 - 1
src/subPack-charge/components/charge-tab.vue

@@ -1,11 +1,40 @@
 <script lang="ts" setup>
+import { ScanCodeUtil } from '../utils/index'
 import { StaticUrl } from '@/config'
+import router from '@/router'
+
+async function getDeviceInfo(connectorCode: string) {
+  useGlobalLoading().loading({})
+  const res = await Apis.charge.connectorDetail({ data: { connectorCode } })
+  useGlobalLoading().close()
+  if (res.data.status === 0 || res.data.status === 255) {
+    useGlobalMessage().alert('此设备异常或被占用,请更换其他设备')
+  }
+  else {
+    router.push({ name: 'charge-start', params: { connectorCode } })
+  }
+}
+
+async function scanCode() {
+  try {
+    const connectorCode = await ScanCodeUtil.scanAndGetConnectorCode()
+    if (!connectorCode) {
+      useGlobalMessage().alert('二维码不正确')
+      return
+    }
+    // 获取设备信息
+    getDeviceInfo(connectorCode)
+  }
+  catch (error) {
+    console.error('扫码失败:', error)
+  }
+}
 </script>
 
 <template>
   <view class="charge-footer">
     <view class="ios flex items-center justify-center pb-20rpx -mt-26rpx">
-      <view class="img-box">
+      <view class="img-box" @click="scanCode">
         <image class="h-120rpx w-120rpx" :src="`${StaticUrl}/charge-qrCode.png`" />
       </view>
     </view>
@@ -20,6 +49,7 @@ import { StaticUrl } from '@/config'
   border-radius: 32rpx 32rpx 0rpx 0rpx;
   background: #FFFFFF;
   box-shadow: 0rpx -6rpx 12rpx 2rpx rgba(0, 0, 0, 0.05);
+  z-index: 9999;
   .img-box{
     border-radius: 50%;
     width: 140rpx;

+ 1 - 1
src/subPack-charge/index/index.vue

@@ -129,7 +129,7 @@ function handleFilterClick(filterKey: number) {
                 充电订单
               </view>
               <view class="mt-24rpx text-28rpx text-#9ED605 font-500">
-                956
+                {{ userAccountInfo?.chargingOrderCount || '--' }}
               </view>
             </view>
             <image class="h-80rpx w-80rpx" :src="`${StaticUrl}/charge-order.png`" />

+ 74 - 0
src/subPack-charge/utils/index.ts

@@ -0,0 +1,74 @@
+/**
+ * @param statusName 去掉括号及括号内的内容
+ */
+export function formatStatusName(statusName: string) {
+  return statusName.replace(/\([^)]*\)/g, '')
+}
+
+/**
+ * 扫码工具类
+ */
+export class ScanCodeUtil {
+  /**
+   * 调用扫码统一处理
+   * @param opts 参数配置,详细查看 https://uniapp.dcloud.net.cn/api/system/barcode.html
+   */
+  static scan(opts: any = {}) {
+    return new Promise((resolve, reject) => {
+      uni.scanCode(
+        Object.assign(
+          {},
+          {
+            success: (res: any) => resolve(res),
+            fail: reject,
+          },
+          opts,
+        ),
+      )
+    })
+  }
+
+  /**
+   * 解析 URL 参数
+   * @param url URL 字符串
+   */
+  static parseUrlParams(url: string) {
+    console.log(url, '二维码数据格式')
+
+    try {
+      const urlObj = new URL(url)
+      const params = new URLSearchParams(urlObj.search)
+      return Object.fromEntries(params.entries())
+    }
+    catch (error) {
+      console.warn('URL 构造函数不可用,使用手动解析:', error)
+      const queryString = url.split('?')[1]
+      if (!queryString)
+        return {}
+      const params: Record<string, string> = {}
+      queryString.split('&').forEach((pair) => {
+        const [key, value] = pair.split('=')
+        if (key) {
+          params[decodeURIComponent(key)] = decodeURIComponent(value || '')
+        }
+      })
+      return params
+    }
+  }
+
+  /**
+   * 扫码并提取 connectorCode
+   * @param opts 参数配置
+   */
+  static async scanAndGetConnectorCode(opts: any = {}): Promise<string | null> {
+    try {
+      const res: any = await this.scan(opts)
+      const params = this.parseUrlParams(res.result)
+      return params.connectorCode || null
+    }
+    catch (error) {
+      console.error('扫码或解析失败:', error)
+      throw error
+    }
+  }
+}