UniApp充电桩小程序业务实现

# UniApp充电桩小程序业务实现

# 概述

本文档详细介绍基于UniApp框架开发的充电桩服务小程序的完整实现方案,包括充电桩查找、预约充电、充电监控、支付结算等核心功能模块的技术实现。

# 项目架构

# 技术栈

  • 前端框架: UniApp
  • 开发语言: Vue.js + TypeScript
  • UI框架: uni-ui + uView
  • 状态管理: Pinia
  • 网络请求: uni.request + 拦截器
  • 地图服务: 高德地图API
  • 实时通信: WebSocket
  • 支付: 微信支付/支付宝

# 项目结构

charging-station-miniapp/
├── pages/                    # 页面目录
│   ├── index/               # 首页
│   ├── map/                 # 地图找桩
│   ├── station/             # 充电站详情
│   ├── charging/            # 充电中页面
│   ├── reservation/         # 预约页面
│   ├── order/               # 订单管理
│   ├── wallet/              # 钱包页面
│   └── profile/             # 个人中心
├── components/              # 组件目录
│   ├── station-card/        # 充电站卡片
│   ├── charging-gun/        # 充电枪组件
│   ├── real-time-chart/     # 实时图表
│   └── payment-modal/       # 支付弹窗
├── static/                  # 静态资源
├── stores/                  # Pinia状态管理
├── utils/                   # 工具函数
├── api/                     # API接口
├── types/                   # TypeScript类型定义
└── config/                  # 配置文件

# 核心功能模块

# 1. 充电站地图模块

# 地图找桩页面

<!-- pages/map/map.vue -->
<template>
  <view class="map-container">
    <!-- 搜索栏 -->
    <view class="search-bar">
      <uni-search-bar 
        v-model="searchKeyword"
        placeholder="搜索充电站"
        @confirm="searchStations"
        @clear="clearSearch"
      />
      <view class="filter-btn" @tap="showFilterModal">
        <uni-icons type="tune" size="20" />
      </view>
    </view>
    
    <!-- 地图 -->
    <map 
      id="chargingMap"
      :longitude="mapCenter.longitude"
      :latitude="mapCenter.latitude"
      :scale="mapScale"
      :markers="stationMarkers"
      :show-location="true"
      @markertap="onMarkerTap"
      @regionchange="onRegionChange"
      @tap="onMapTap"
      class="map"
    >
      <!-- 定位按钮 -->
      <cover-view class="location-btn" @tap="getCurrentLocation">
        <cover-image src="/static/icons/location.png" class="location-icon" />
      </cover-view>
      
      <!-- 图例 -->
      <cover-view class="map-legend">
        <cover-view class="legend-item">
          <cover-image src="/static/icons/available.png" class="legend-icon" />
          <cover-text class="legend-text">可用</cover-text>
        </cover-view>
        <cover-view class="legend-item">
          <cover-image src="/static/icons/busy.png" class="legend-icon" />
          <cover-text class="legend-text">繁忙</cover-text>
        </cover-view>
        <cover-view class="legend-item">
          <cover-image src="/static/icons/offline.png" class="legend-icon" />
          <cover-text class="legend-text">离线</cover-text>
        </cover-view>
      </cover-view>
    </map>
    
    <!-- 充电站列表 -->
    <view class="station-list" :class="{ expanded: showStationList }">
      <view class="list-header" @tap="toggleStationList">
        <text class="list-title">附近充电站 ({{ nearbyStations.length }})</text>
        <uni-icons 
          :type="showStationList ? 'chevron-down' : 'chevron-up'" 
          size="16" 
        />
      </view>
      
      <scroll-view v-if="showStationList" scroll-y class="station-scroll">
        <station-card 
          v-for="station in nearbyStations" 
          :key="station.id"
          :station="station"
          @tap="selectStation(station)"
          @navigate="navigateToStation"
          @reserve="reserveStation"
        />
      </scroll-view>
    </view>
    
    <!-- 筛选弹窗 -->
    <uni-popup ref="filterPopup" type="bottom">
      <view class="filter-modal">
        <view class="filter-header">
          <text class="filter-title">筛选条件</text>
          <text class="filter-reset" @tap="resetFilter">重置</text>
        </view>
        
        <view class="filter-content">
          <!-- 充电类型 -->
          <view class="filter-section">
            <text class="section-title">充电类型</text>
            <view class="option-group">
              <view 
                v-for="type in chargingTypes" 
                :key="type.value"
                :class="['option-item', { selected: filterOptions.chargingType.includes(type.value) }]"
                @tap="toggleChargingType(type.value)"
              >
                {{ type.label }}
              </view>
            </view>
          </view>
          
          <!-- 功率范围 -->
          <view class="filter-section">
            <text class="section-title">功率范围</text>
            <uni-slider 
              v-model="filterOptions.powerRange"
              :min="0"
              :max="350"
              :step="10"
              show-value
              @change="onPowerRangeChange"
            />
          </view>
          
          <!-- 距离范围 -->
          <view class="filter-section">
            <text class="section-title">距离范围</text>
            <view class="distance-options">
              <view 
                v-for="distance in distanceOptions" 
                :key="distance.value"
                :class="['distance-item', { selected: filterOptions.distance === distance.value }]"
                @tap="selectDistance(distance.value)"
              >
                {{ distance.label }}
              </view>
            </view>
          </view>
        </view>
        
        <view class="filter-footer">
          <button class="confirm-btn" @tap="applyFilter">确定</button>
        </view>
      </view>
    </uni-popup>
  </view>
</template>

<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { useStationStore } from '@/stores/station'
import { useLocationStore } from '@/stores/location'
import StationCard from '@/components/station-card/station-card.vue'
import type { ChargingStation, MapCenter, FilterOptions } from '@/types/station'

const stationStore = useStationStore()
const locationStore = useLocationStore()

// 响应式数据
const searchKeyword = ref('')
const showStationList = ref(false)
const mapScale = ref(16)
const selectedStation = ref<ChargingStation | null>(null)

const mapCenter = reactive<MapCenter>({
  longitude: 116.397428,
  latitude: 39.90923
})

const filterOptions = reactive<FilterOptions>({
  chargingType: [],
  powerRange: [0, 350],
  distance: 5000,
  availability: 'all'
})

// 筛选选项
const chargingTypes = [
  { label: '直流快充', value: 'dc_fast' },
  { label: '交流慢充', value: 'ac_slow' },
  { label: '超级快充', value: 'super_fast' }
]

const distanceOptions = [
  { label: '1公里内', value: 1000 },
  { label: '3公里内', value: 3000 },
  { label: '5公里内', value: 5000 },
  { label: '10公里内', value: 10000 }
]

// 计算属性
const nearbyStations = computed(() => stationStore.nearbyStations)

const stationMarkers = computed(() => {
  return nearbyStations.value.map(station => ({
    id: station.id,
    longitude: station.longitude,
    latitude: station.latitude,
    iconPath: getStationIcon(station),
    width: 32,
    height: 32,
    callout: {
      content: `${station.name}\n可用: ${station.availableGuns}/${station.totalGuns}`,
      fontSize: 12,
      borderRadius: 4,
      bgColor: '#ffffff',
      padding: 8,
      display: 'BYCLICK'
    }
  }))
})

// 生命周期
onMounted(() => {
  getCurrentLocation()
  loadNearbyStations()
})

// 方法
const getCurrentLocation = () => {
  uni.getLocation({
    type: 'gcj02',
    success: (res) => {
      mapCenter.longitude = res.longitude
      mapCenter.latitude = res.latitude
      locationStore.updateLocation({
        longitude: res.longitude,
        latitude: res.latitude
      })
      loadNearbyStations()
    },
    fail: (error) => {
      console.error('获取位置失败:', error)
      uni.showToast({
        title: '获取位置失败',
        icon: 'none'
      })
    }
  })
}

const loadNearbyStations = async () => {
  try {
    await stationStore.loadNearbyStations({
      longitude: mapCenter.longitude,
      latitude: mapCenter.latitude,
      radius: filterOptions.distance,
      ...filterOptions
    })
  } catch (error) {
    console.error('加载充电站失败:', error)
  }
}

const getStationIcon = (station: ChargingStation): string => {
  const availabilityRate = station.availableGuns / station.totalGuns
  
  if (station.status === 'offline') {
    return '/static/icons/station-offline.png'
  } else if (availabilityRate > 0.5) {
    return '/static/icons/station-available.png'
  } else if (availabilityRate > 0) {
    return '/static/icons/station-busy.png'
  } else {
    return '/static/icons/station-full.png'
  }
}

const onMarkerTap = (e: any) => {
  const stationId = e.detail.markerId
  const station = nearbyStations.value.find(s => s.id === stationId)
  if (station) {
    selectStation(station)
  }
}

const selectStation = (station: ChargingStation) => {
  selectedStation.value = station
  
  // 移动地图中心到选中的充电站
  mapCenter.longitude = station.longitude
  mapCenter.latitude = station.latitude
  
  // 显示充电站详情
  uni.navigateTo({
    url: `/pages/station/detail?stationId=${station.id}`
  })
}

const searchStations = async () => {
  if (!searchKeyword.value.trim()) return
  
  try {
    await stationStore.searchStations({
      keyword: searchKeyword.value,
      longitude: mapCenter.longitude,
      latitude: mapCenter.latitude
    })
  } catch (error) {
    console.error('搜索失败:', error)
  }
}

const showFilterModal = () => {
  ;(this.$refs.filterPopup as any).open()
}

const applyFilter = async () => {
  ;(this.$refs.filterPopup as any).close()
  await loadNearbyStations()
}

const toggleStationList = () => {
  showStationList.value = !showStationList.value
}

const navigateToStation = (station: ChargingStation) => {
  // 调用地图导航
  uni.openLocation({
    longitude: station.longitude,
    latitude: station.latitude,
    name: station.name,
    address: station.address
  })
}

const reserveStation = (station: ChargingStation) => {
  uni.navigateTo({
    url: `/pages/reservation/reservation?stationId=${station.id}`
  })
}
</script>

# 2. 充电站详情模块

# 充电站详情页面

<!-- pages/station/detail.vue -->
<template>
  <view class="station-detail">
    <!-- 充电站基本信息 -->
    <view class="station-info">
      <view class="info-header">
        <text class="station-name">{{ stationDetail.name }}</text>
        <view class="station-status" :class="stationDetail.status">
          {{ getStatusText(stationDetail.status) }}
        </view>
      </view>
      
      <view class="info-content">
        <view class="info-item">
          <uni-icons type="location" size="16" color="#666" />
          <text class="info-text">{{ stationDetail.address }}</text>
        </view>
        
        <view class="info-item">
          <uni-icons type="phone" size="16" color="#666" />
          <text class="info-text">{{ stationDetail.phone }}</text>
        </view>
        
        <view class="info-item">
          <uni-icons type="clock" size="16" color="#666" />
          <text class="info-text">{{ stationDetail.operatingHours }}</text>
        </view>
      </view>
      
      <view class="action-buttons">
        <button class="nav-btn" @tap="navigateToStation">
          <uni-icons type="map" size="16" />
          导航
        </button>
        <button class="call-btn" @tap="callStation">
          <uni-icons type="phone" size="16" />
          电话
        </button>
        <button class="reserve-btn" @tap="showReservationModal">
          <uni-icons type="calendar" size="16" />
          预约
        </button>
      </view>
    </view>
    
    <!-- 充电枪列表 -->
    <view class="charging-guns">
      <view class="section-header">
        <text class="section-title">充电枪状态</text>
        <text class="available-count">可用 {{ availableGunsCount }}/{{ stationDetail.chargingGuns?.length || 0 }}</text>
      </view>
      
      <view class="guns-grid">
        <charging-gun
          v-for="gun in stationDetail.chargingGuns" 
          :key="gun.id"
          :gun="gun"
          @select="selectChargingGun"
        />
      </view>
    </view>
    
    <!-- 实时数据 -->
    <view class="real-time-data">
      <view class="section-header">
        <text class="section-title">实时数据</text>
        <text class="update-time">更新时间: {{ formatTime(lastUpdateTime) }}</text>
      </view>
      
      <view class="data-cards">
        <view class="data-card">
          <text class="data-value">{{ stationDetail.totalPower }}kW</text>
          <text class="data-label">总功率</text>
        </view>
        <view class="data-card">
          <text class="data-value">{{ stationDetail.currentLoad }}%</text>
          <text class="data-label">当前负载</text>
        </view>
        <view class="data-card">
          <text class="data-value">{{ stationDetail.todayEnergy }}kWh</text>
          <text class="data-label">今日充电量</text>
        </view>
        <view class="data-card">
          <text class="data-value">{{ stationDetail.todayOrders }}</text>
          <text class="data-label">今日订单</text>
        </view>
      </view>
    </view>
    
    <!-- 价格信息 -->
    <view class="pricing-info">
      <view class="section-header">
        <text class="section-title">计费标准</text>
      </view>
      
      <view class="pricing-table">
        <view class="pricing-row header">
          <text class="time-col">时段</text>
          <text class="price-col">电费(元/kWh)</text>
          <text class="service-col">服务费(元/kWh)</text>
        </view>
        
        <view 
          v-for="price in stationDetail.pricingRules" 
          :key="price.id"
          class="pricing-row"
        >
          <text class="time-col">{{ price.timeRange }}</text>
          <text class="price-col">{{ price.electricityPrice }}</text>
          <text class="service-col">{{ price.servicePrice }}</text>
        </view>
      </view>
    </view>
    
    <!-- 用户评价 -->
    <view class="reviews">
      <view class="section-header">
        <text class="section-title">用户评价</text>
        <view class="rating-summary">
          <uni-rate :value="stationDetail.averageRating" readonly size="16" />
          <text class="rating-text">{{ stationDetail.averageRating }}/5.0</text>
        </view>
      </view>
      
      <view class="review-list">
        <view 
          v-for="review in stationDetail.reviews" 
          :key="review.id"
          class="review-item"
        >
          <view class="review-header">
            <text class="reviewer-name">{{ review.userName }}</text>
            <uni-rate :value="review.rating" readonly size="12" />
            <text class="review-time">{{ formatTime(review.createTime) }}</text>
          </view>
          <text class="review-content">{{ review.content }}</text>
        </view>
      </view>
    </view>
    
    <!-- 预约弹窗 -->
    <uni-popup ref="reservationPopup" type="bottom">
      <view class="reservation-modal">
        <view class="modal-header">
          <text class="modal-title">预约充电</text>
          <uni-icons type="close" size="20" @tap="closeReservationModal" />
        </view>
        
        <view class="reservation-form">
          <view class="form-item">
            <text class="form-label">选择充电枪</text>
            <picker 
              :range="availableGuns" 
              range-key="name"
              @change="onGunChange"
            >
              <view class="picker-value">
                {{ selectedGun?.name || '请选择充电枪' }}
              </view>
            </picker>
          </view>
          
          <view class="form-item">
            <text class="form-label">预约时间</text>
            <picker 
              mode="datetime"
              :value="reservationTime"
              @change="onTimeChange"
            >
              <view class="picker-value">
                {{ formatDateTime(reservationTime) }}
              </view>
            </picker>
          </view>
          
          <view class="form-item">
            <text class="form-label">预计充电时长</text>
            <picker 
              :range="durationOptions" 
              range-key="label"
              @change="onDurationChange"
            >
              <view class="picker-value">
                {{ selectedDuration?.label || '请选择时长' }}
              </view>
            </picker>
          </view>
        </view>
        
        <view class="modal-footer">
          <button class="cancel-btn" @tap="closeReservationModal">取消</button>
          <button class="confirm-btn" @tap="confirmReservation">确认预约</button>
        </view>
      </view>
    </uni-popup>
  </view>
</template>

<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { useStationStore } from '@/stores/station'
import ChargingGun from '@/components/charging-gun/charging-gun.vue'
import type { ChargingStation, ChargingGun as Gun } from '@/types/station'

const props = defineProps<{
  stationId: string
}>()

const stationStore = useStationStore()

// 响应式数据
const stationDetail = ref<ChargingStation>({} as ChargingStation)
const lastUpdateTime = ref(new Date())
const selectedGun = ref<Gun | null>(null)
const reservationTime = ref('')
const selectedDuration = ref<any>(null)

const durationOptions = [
  { label: '30分钟', value: 30 },
  { label: '1小时', value: 60 },
  { label: '2小时', value: 120 },
  { label: '3小时', value: 180 }
]

// 计算属性
const availableGunsCount = computed(() => {
  return stationDetail.value.chargingGuns?.filter(gun => gun.status === 'available').length || 0
})

const availableGuns = computed(() => {
  return stationDetail.value.chargingGuns?.filter(gun => gun.status === 'available') || []
})

// 生命周期
onMounted(() => {
  loadStationDetail()
  startRealTimeUpdate()
})

// 方法
const loadStationDetail = async () => {
  try {
    const detail = await stationStore.getStationDetail(props.stationId)
    stationDetail.value = detail
  } catch (error) {
    console.error('加载充电站详情失败:', error)
    uni.showToast({
      title: '加载失败',
      icon: 'none'
    })
  }
}

const startRealTimeUpdate = () => {
  // 每30秒更新一次实时数据
  setInterval(() => {
    updateRealTimeData()
  }, 30000)
}

const updateRealTimeData = async () => {
  try {
    const realTimeData = await stationStore.getRealTimeData(props.stationId)
    Object.assign(stationDetail.value, realTimeData)
    lastUpdateTime.value = new Date()
  } catch (error) {
    console.error('更新实时数据失败:', error)
  }
}

const selectChargingGun = (gun: Gun) => {
  if (gun.status !== 'available') {
    uni.showToast({
      title: '该充电枪不可用',
      icon: 'none'
    })
    return
  }
  
  // 直接开始充电
  uni.navigateTo({
    url: `/pages/charging/charging?stationId=${props.stationId}&gunId=${gun.id}`
  })
}

const navigateToStation = () => {
  uni.openLocation({
    longitude: stationDetail.value.longitude,
    latitude: stationDetail.value.latitude,
    name: stationDetail.value.name,
    address: stationDetail.value.address
  })
}

const callStation = () => {
  uni.makePhoneCall({
    phoneNumber: stationDetail.value.phone
  })
}

const showReservationModal = () => {
  if (availableGuns.value.length === 0) {
    uni.showToast({
      title: '暂无可用充电枪',
      icon: 'none'
    })
    return
  }
  
  ;(this.$refs.reservationPopup as any).open()
}

const closeReservationModal = () => {
  ;(this.$refs.reservationPopup as any).close()
}

const confirmReservation = async () => {
  if (!selectedGun.value || !reservationTime.value || !selectedDuration.value) {
    uni.showToast({
      title: '请完善预约信息',
      icon: 'none'
    })
    return
  }
  
  try {
    uni.showLoading({ title: '预约中...' })
    
    await stationStore.createReservation({
      stationId: props.stationId,
      gunId: selectedGun.value.id,
      reservationTime: reservationTime.value,
      duration: selectedDuration.value.value
    })
    
    uni.showToast({
      title: '预约成功',
      icon: 'success'
    })
    
    closeReservationModal()
    
    // 跳转到预约详情页
    setTimeout(() => {
      uni.navigateTo({
        url: '/pages/reservation/list'
      })
    }, 1500)
  } catch (error) {
    console.error('预约失败:', error)
    uni.showToast({
      title: '预约失败',
      icon: 'none'
    })
  } finally {
    uni.hideLoading()
  }
}

const getStatusText = (status: string): string => {
  const statusMap: Record<string, string> = {
    online: '正常运营',
    offline: '暂停服务',
    maintenance: '维护中'
  }
  return statusMap[status] || '未知状态'
}

const formatTime = (time: string | Date): string => {
  const date = new Date(time)
  return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
}

const formatDateTime = (time: string): string => {
  if (!time) return '请选择时间'
  const date = new Date(time)
  return `${date.getMonth() + 1}${date.getDate()}${formatTime(date)}`
}
</script>

# 3. 充电监控模块

# 充电中页面

<!-- pages/charging/charging.vue -->
<template>
  <view class="charging-container">
    <!-- 充电状态卡片 -->
    <view class="charging-status">
      <view class="status-header">
        <text class="station-name">{{ chargingInfo.stationName }}</text>
        <text class="gun-number">{{ chargingInfo.gunNumber }}号枪</text>
      </view>
      
      <view class="status-circle">
        <view class="circle-progress" :style="{ '--progress': chargingProgress }">
          <view class="circle-content">
            <text class="progress-text">{{ Math.round(chargingProgress) }}%</text>
            <text class="status-text">{{ getChargingStatusText() }}</text>
          </view>
        </view>
      </view>
      
      <view class="charging-info">
        <view class="info-row">
          <text class="info-label">当前功率</text>
          <text class="info-value">{{ chargingInfo.currentPower }}kW</text>
        </view>
        <view class="info-row">
          <text class="info-label">充电电压</text>
          <text class="info-value">{{ chargingInfo.voltage }}V</text>
        </view>
        <view class="info-row">
          <text class="info-label">充电电流</text>
          <text class="info-value">{{ chargingInfo.current }}A</text>
        </view>
        <view class="info-row">
          <text class="info-label">已充电量</text>
          <text class="info-value">{{ chargingInfo.chargedEnergy }}kWh</text>
        </view>
      </view>
    </view>
    
    <!-- 充电时间和费用 -->
    <view class="charging-summary">
      <view class="summary-item">
        <text class="summary-label">充电时长</text>
        <text class="summary-value">{{ formatDuration(chargingInfo.duration) }}</text>
      </view>
      <view class="summary-item">
        <text class="summary-label">当前费用</text>
        <text class="summary-value cost">¥{{ chargingInfo.currentCost.toFixed(2) }}</text>
      </view>
    </view>
    
    <!-- 实时图表 -->
    <view class="chart-section">
      <view class="chart-tabs">
        <view 
          v-for="(tab, index) in chartTabs" 
          :key="index"
          :class="['chart-tab', { active: currentChartTab === index }]"
          @tap="switchChartTab(index)"
        >
          {{ tab.name }}
        </view>
      </view>
      
      <view class="chart-container">
        <real-time-chart 
          :type="chartTabs[currentChartTab].type"
          :data="chartData"
          :options="chartOptions"
        />
      </view>
    </view>
    
    <!-- 充电设置 -->
    <view class="charging-settings">
      <view class="setting-item">
        <text class="setting-label">目标电量</text>
        <view class="setting-control">
          <uni-slider 
            v-model="targetSOC"
            :min="chargingInfo.currentSOC"
            :max="100"
            :step="5"
            show-value
            @change="updateTargetSOC"
          />
        </view>
      </view>
      
      <view class="setting-item">
        <text class="setting-label">充电模式</text>
        <picker 
          :range="chargingModes" 
          range-key="label"
          :value="selectedModeIndex"
          @change="onModeChange"
        >
          <view class="picker-value">
            {{ chargingModes[selectedModeIndex]?.label }}
          </view>
        </picker>
      </view>
    </view>
    
    <!-- 操作按钮 -->
    <view class="action-buttons">
      <button 
        v-if="chargingInfo.status === 'charging'"
        class="pause-btn"
        @tap="pauseCharging"
      >
        暂停充电
      </button>
      <button 
        v-else-if="chargingInfo.status === 'paused'"
        class="resume-btn"
        @tap="resumeCharging"
      >
        继续充电
      </button>
      <button class="stop-btn" @tap="showStopConfirm">结束充电</button>
    </view>
    
    <!-- 结束充电确认弹窗 -->
    <uni-popup ref="stopConfirmPopup" type="dialog">
      <uni-popup-dialog 
        type="confirm"
        title="确认结束充电"
        content="确定要结束充电吗?结束后将进行费用结算。"
        @confirm="stopCharging"
        @close="closeStopConfirm"
      />
    </uni-popup>
  </view>
</template>

<script setup lang="ts">
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import { useChargingStore } from '@/stores/charging'
import RealTimeChart from '@/components/real-time-chart/real-time-chart.vue'
import type { ChargingInfo } from '@/types/charging'

const props = defineProps<{
  stationId: string
  gunId: string
  orderId?: string
}>()

const chargingStore = useChargingStore()

// 响应式数据
const chargingInfo = ref<ChargingInfo>({} as ChargingInfo)
const targetSOC = ref(80)
const selectedModeIndex = ref(0)
const currentChartTab = ref(0)
const chartData = ref([])
const wsConnection = ref<any>(null)

const chartTabs = [
  { name: '功率', type: 'power' },
  { name: '电压', type: 'voltage' },
  { name: '电流', type: 'current' },
  { name: '温度', type: 'temperature' }
]

const chargingModes = [
  { label: '快速充电', value: 'fast' },
  { label: '标准充电', value: 'standard' },
  { label: '慢速充电', value: 'slow' }
]

const chartOptions = {
  responsive: true,
  animation: false,
  scales: {
    x: {
      type: 'time',
      time: {
        unit: 'minute'
      }
    }
  }
}

// 计算属性
const chargingProgress = computed(() => {
  if (!chargingInfo.value.currentSOC || !chargingInfo.value.targetSOC) return 0
  return (chargingInfo.value.currentSOC / chargingInfo.value.targetSOC) * 100
})

// 生命周期
onMounted(() => {
  initCharging()
  connectWebSocket()
})

onUnmounted(() => {
  disconnectWebSocket()
})

// 方法
const initCharging = async () => {
  try {
    if (props.orderId) {
      // 从订单恢复充电状态
      const info = await chargingStore.getChargingInfo(props.orderId)
      chargingInfo.value = info
    } else {
      // 开始新的充电
      const result = await chargingStore.startCharging({
        stationId: props.stationId,
        gunId: props.gunId,
        targetSOC: targetSOC.value
      })
      chargingInfo.value = result.chargingInfo
    }
  } catch (error) {
    console.error('初始化充电失败:', error)
    uni.showToast({
      title: '充电启动失败',
      icon: 'none'
    })
  }
}

const connectWebSocket = () => {
  const wsUrl = `wss://api.charging.com/ws/charging/${chargingInfo.value.orderId}`
  
  wsConnection.value = uni.connectSocket({
    url: wsUrl,
    success: () => {
      console.log('WebSocket连接成功')
    },
    fail: (error) => {
      console.error('WebSocket连接失败:', error)
    }
  })
  
  wsConnection.value.onMessage((message: any) => {
    try {
      const data = JSON.parse(message.data)
      updateChargingData(data)
    } catch (error) {
      console.error('解析WebSocket消息失败:', error)
    }
  })
  
  wsConnection.value.onError((error: any) => {
    console.error('WebSocket错误:', error)
  })
  
  wsConnection.value.onClose(() => {
    console.log('WebSocket连接关闭')
    // 尝试重连
    setTimeout(() => {
      if (chargingInfo.value.status === 'charging') {
        connectWebSocket()
      }
    }, 5000)
  })
}

const disconnectWebSocket = () => {
  if (wsConnection.value) {
    wsConnection.value.close()
    wsConnection.value = null
  }
}

const updateChargingData = (data: any) => {
  // 更新充电信息
  Object.assign(chargingInfo.value, data.chargingInfo)
  
  // 更新图表数据
  if (data.chartData) {
    chartData.value = data.chartData
  }
}

const pauseCharging = async () => {
  try {
    uni.showLoading({ title: '暂停中...' })
    
    await chargingStore.pauseCharging(chargingInfo.value.orderId)
    
    uni.showToast({
      title: '已暂停充电',
      icon: 'success'
    })
  } catch (error) {
    console.error('暂停充电失败:', error)
    uni.showToast({
      title: '暂停失败',
      icon: 'none'
    })
  } finally {
    uni.hideLoading()
  }
}

const resumeCharging = async () => {
  try {
    uni.showLoading({ title: '恢复中...' })
    
    await chargingStore.resumeCharging(chargingInfo.value.orderId)
    
    uni.showToast({
      title: '已恢复充电',
      icon: 'success'
    })
  } catch (error) {
    console.error('恢复充电失败:', error)
    uni.showToast({
      title: '恢复失败',
      icon: 'none'
    })
  } finally {
    uni.hideLoading()
  }
}

const showStopConfirm = () => {
  ;(this.$refs.stopConfirmPopup as any).open()
}

const closeStopConfirm = () => {
  ;(this.$refs.stopConfirmPopup as any).close()
}

const stopCharging = async () => {
  try {
    uni.showLoading({ title: '结束充电中...' })
    
    const result = await chargingStore.stopCharging(chargingInfo.value.orderId)
    
    disconnectWebSocket()
    
    uni.showToast({
      title: '充电已结束',
      icon: 'success'
    })
    
    // 跳转到结算页面
    setTimeout(() => {
      uni.redirectTo({
        url: `/pages/payment/settlement?orderId=${chargingInfo.value.orderId}`
      })
    }, 1500)
  } catch (error) {
    console.error('结束充电失败:', error)
    uni.showToast({
      title: '结束失败',
      icon: 'none'
    })
  } finally {
    uni.hideLoading()
    closeStopConfirm()
  }
}

const updateTargetSOC = async () => {
  try {
    await chargingStore.updateChargingSettings({
      orderId: chargingInfo.value.orderId,
      targetSOC: targetSOC.value
    })
  } catch (error) {
    console.error('更新目标电量失败:', error)
  }
}

const switchChartTab = (index: number) => {
  currentChartTab.value = index
}

const getChargingStatusText = (): string => {
  const statusMap: Record<string, string> = {
    charging: '充电中',
    paused: '已暂停',
    completed: '已完成',
    error: '异常'
  }
  return statusMap[chargingInfo.value.status] || '未知状态'
}

const formatDuration = (seconds: number): string => {
  const hours = Math.floor(seconds / 3600)
  const minutes = Math.floor((seconds % 3600) / 60)
  const secs = seconds % 60
  
  return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
</script>

# 4. 订单管理模块

# 订单列表页面

<!-- pages/order/list.vue -->
<template>
  <view class="order-list-container">
    <!-- 筛选标签 -->
    <view class="filter-tabs">
      <view 
        v-for="(tab, index) in filterTabs" 
        :key="index"
        :class="['tab-item', { active: currentTab === index }]"
        @tap="switchTab(index)"
      >
        {{ tab.name }}
        <text v-if="tab.count > 0" class="tab-count">{{ tab.count }}</text>
      </view>
    </view>
    
    <!-- 订单列表 -->
    <scroll-view 
      scroll-y 
      class="order-scroll"
      @scrolltolower="loadMore"
      :refresher-enabled="true"
      :refresher-triggered="refreshing"
      @refresherrefresh="onRefresh"
    >
      <view v-if="orderList.length === 0" class="empty-state">
        <image src="/static/images/empty-order.png" class="empty-image" />
        <text class="empty-text">暂无订单记录</text>
      </view>
      
      <view v-else>
        <view 
          v-for="order in orderList" 
          :key="order.id"
          class="order-item"
          @tap="viewOrderDetail(order.id)"
        >
          <!-- 订单头部 -->
          <view class="order-header">
            <view class="station-info">
              <text class="station-name">{{ order.stationName }}</text>
              <text class="gun-number">{{ order.gunNumber }}号枪</text>
            </view>
            <view class="order-status" :class="order.status">
              {{ getOrderStatusText(order.status) }}
            </view>
          </view>
          
          <!-- 订单内容 -->
          <view class="order-content">
            <view class="order-info">
              <view class="info-row">
                <text class="info-label">订单号:</text>
                <text class="info-value">{{ order.orderNo }}</text>
              </view>
              <view class="info-row">
                <text class="info-label">充电时间:</text>
                <text class="info-value">{{ formatDateTime(order.startTime) }}</text>
              </view>
              <view class="info-row">
                <text class="info-label">充电时长:</text>
                <text class="info-value">{{ formatDuration(order.duration) }}</text>
              </view>
              <view class="info-row">
                <text class="info-label">充电电量:</text>
                <text class="info-value">{{ order.chargedEnergy }}kWh</text>
              </view>
            </view>
            
            <view class="order-amount">
              <text class="amount-label">总费用</text>
              <text class="amount-value">¥{{ order.totalAmount.toFixed(2) }}</text>
            </view>
          </view>
          
          <!-- 订单操作 -->
          <view class="order-actions">
            <button 
              v-if="order.status === 'charging'"
              class="action-btn primary"
              @tap.stop="continueCharging(order)"
            >
              继续充电
            </button>
            <button 
              v-if="order.status === 'completed' && !order.reviewed"
              class="action-btn"
              @tap.stop="reviewOrder(order)"
            >
              评价
            </button>
            <button 
              v-if="order.status === 'completed'"
              class="action-btn"
              @tap.stop="downloadInvoice(order)"
            >
              下载发票
            </button>
            <button 
              v-if="order.status === 'cancelled' || order.status === 'failed'"
              class="action-btn"
              @tap.stop="deleteOrder(order)"
            >
              删除
            </button>
          </view>
        </view>
      </view>
      
      <view v-if="hasMore" class="loading-more">
        <uni-load-more :status="loadingStatus" />
      </view>
    </scroll-view>
  </view>
</template>

<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { useOrderStore } from '@/stores/order'
import type { ChargingOrder } from '@/types/order'

const orderStore = useOrderStore()

// 响应式数据
const currentTab = ref(0)
const orderList = ref<ChargingOrder[]>([])
const page = ref(1)
const pageSize = ref(10)
const hasMore = ref(true)
const refreshing = ref(false)
const loadingStatus = ref('more')

const filterTabs = reactive([
  { name: '全部', status: '', count: 0 },
  { name: '充电中', status: 'charging', count: 0 },
  { name: '已完成', status: 'completed', count: 0 },
  { name: '已取消', status: 'cancelled', count: 0 }
])

// 生命周期
onMounted(() => {
  loadOrderList()
  loadOrderCounts()
})

// 方法
const switchTab = (index: number) => {
  currentTab.value = index
  resetList()
  loadOrderList()
}

const resetList = () => {
  orderList.value = []
  page.value = 1
  hasMore.value = true
}

const loadOrderList = async () => {
  try {
    loadingStatus.value = 'loading'
    
    const params = {
      page: page.value,
      pageSize: pageSize.value,
      status: filterTabs[currentTab.value].status
    }
    
    const res = await orderStore.getOrderList(params)
    
    if (res.success) {
      const newOrders = res.data.list || []
      
      if (page.value === 1) {
        orderList.value = newOrders
      } else {
        orderList.value.push(...newOrders)
      }
      
      hasMore.value = newOrders.length === pageSize.value
      loadingStatus.value = hasMore.value ? 'more' : 'noMore'
    }
  } catch (error) {
    console.error('加载订单失败:', error)
    loadingStatus.value = 'more'
  } finally {
    refreshing.value = false
  }
}

const loadOrderCounts = async () => {
  try {
    const counts = await orderStore.getOrderCounts()
    filterTabs.forEach(tab => {
      if (tab.status) {
        tab.count = counts[tab.status] || 0
      } else {
        tab.count = Object.values(counts).reduce((sum: number, count: number) => sum + count, 0)
      }
    })
  } catch (error) {
    console.error('加载订单统计失败:', error)
  }
}

const loadMore = () => {
  if (hasMore.value && loadingStatus.value !== 'loading') {
    page.value++
    loadOrderList()
  }
}

const onRefresh = () => {
  refreshing.value = true
  resetList()
  loadOrderList()
  loadOrderCounts()
}

const viewOrderDetail = (orderId: string) => {
  uni.navigateTo({
    url: `/pages/order/detail?orderId=${orderId}`
  })
}

const continueCharging = (order: ChargingOrder) => {
  uni.navigateTo({
    url: `/pages/charging/charging?orderId=${order.id}`
  })
}

const reviewOrder = (order: ChargingOrder) => {
  uni.navigateTo({
    url: `/pages/review/review?orderId=${order.id}`
  })
}

const downloadInvoice = async (order: ChargingOrder) => {
  try {
    uni.showLoading({ title: '生成发票中...' })
    
    const result = await orderStore.generateInvoice(order.id)
    
    if (result.success) {
      uni.downloadFile({
        url: result.data.invoiceUrl,
        success: (res) => {
          uni.showToast({
            title: '发票下载成功',
            icon: 'success'
          })
        },
        fail: (error) => {
          console.error('下载发票失败:', error)
          uni.showToast({
            title: '下载失败',
            icon: 'none'
          })
        }
      })
    }
  } catch (error) {
    console.error('生成发票失败:', error)
    uni.showToast({
      title: '生成发票失败',
      icon: 'none'
    })
  } finally {
    uni.hideLoading()
  }
}

const deleteOrder = (order: ChargingOrder) => {
  uni.showModal({
    title: '确认删除',
    content: '确定要删除这个订单吗?',
    success: async (res) => {
      if (res.confirm) {
        try {
          await orderStore.deleteOrder(order.id)
          
          uni.showToast({
            title: '删除成功',
            icon: 'success'
          })
          
          // 刷新列表
          onRefresh()
        } catch (error) {
          console.error('删除订单失败:', error)
          uni.showToast({
            title: '删除失败',
            icon: 'none'
          })
        }
      }
    }
  })
}

const getOrderStatusText = (status: string): string => {
  const statusMap: Record<string, string> = {
    pending: '待支付',
    charging: '充电中',
    completed: '已完成',
    cancelled: '已取消',
    failed: '失败'
  }
  return statusMap[status] || '未知状态'
}

const formatDateTime = (time: string): string => {
  const date = new Date(time)
  return `${date.getMonth() + 1}${date.getDate()}${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
}

const formatDuration = (seconds: number): string => {
  const hours = Math.floor(seconds / 3600)
  const minutes = Math.floor((seconds % 3600) / 60)
  
  if (hours > 0) {
    return `${hours}小时${minutes}分钟`
  } else {
    return `${minutes}分钟`
  }
}
</script>

# 状态管理 (Pinia)

# 充电站状态管理

// stores/station.ts
import { defineStore } from 'pinia'
import type { ChargingStation, StationFilter } from '@/types/station'
import api from '@/api'

export const useStationStore = defineStore('station', {
  state: () => ({
    nearbyStations: [] as ChargingStation[],
    currentStation: null as ChargingStation | null,
    searchResults: [] as ChargingStation[],
    loading: false,
    error: null as string | null
  }),
  
  getters: {
    availableStations: (state) => {
      return state.nearbyStations.filter(station => 
        station.status === 'online' && station.availableGuns > 0
      )
    },
    
    stationById: (state) => {
      return (id: string) => state.nearbyStations.find(station => station.id === id)
    }
  },
  
  actions: {
    async loadNearbyStations(params: {
      longitude: number
      latitude: number
      radius: number
      filter?: StationFilter
    }) {
      try {
        this.loading = true
        this.error = null
        
        const response = await api.station.getNearbyStations(params)
        
        if (response.success) {
          this.nearbyStations = response.data
        } else {
          this.error = response.message
        }
      } catch (error) {
        this.error = '加载充电站失败'
        console.error('加载充电站失败:', error)
      } finally {
        this.loading = false
      }
    },
    
    async getStationDetail(stationId: string): Promise<ChargingStation> {
      try {
        const response = await api.station.getStationDetail(stationId)
        
        if (response.success) {
          this.currentStation = response.data
          return response.data
        } else {
          throw new Error(response.message)
        }
      } catch (error) {
        console.error('获取充电站详情失败:', error)
        throw error
      }
    },
    
    async searchStations(params: {
      keyword: string
      longitude: number
      latitude: number
    }) {
      try {
        const response = await api.station.searchStations(params)
        
        if (response.success) {
          this.searchResults = response.data
        }
      } catch (error) {
        console.error('搜索充电站失败:', error)
      }
    },
    
    async createReservation(params: {
      stationId: string
      gunId: string
      reservationTime: string
      duration: number
    }) {
      try {
        const response = await api.reservation.create(params)
        
        if (response.success) {
          return response.data
        } else {
          throw new Error(response.message)
        }
      } catch (error) {
        console.error('创建预约失败:', error)
        throw error
      }
    },
    
    async getRealTimeData(stationId: string) {
      try {
        const response = await api.station.getRealTimeData(stationId)
        
        if (response.success) {
          return response.data
        }
      } catch (error) {
        console.error('获取实时数据失败:', error)
      }
    }
  }
})

# API接口封装

// api/station.ts
import request from '@/utils/request'
import type { ChargingStation, StationFilter } from '@/types/station'

export const stationApi = {
  // 获取附近充电站
  getNearbyStations(params: {
    longitude: number
    latitude: number
    radius: number
    filter?: StationFilter
  }) {
    return request({
      url: '/api/station/nearby',
      method: 'GET',
      data: params
    })
  },
  
  // 获取充电站详情
  getStationDetail(stationId: string) {
    return request({
      url: `/api/station/${stationId}`,
      method: 'GET'
    })
  },
  
  // 搜索充电站
  searchStations(params: {
    keyword: string
    longitude: number
    latitude: number
  }) {
    return request({
      url: '/api/station/search',
      method: 'GET',
      data: params
    })
  },
  
  // 获取实时数据
  getRealTimeData(stationId: string) {
    return request({
      url: `/api/station/${stationId}/realtime`,
      method: 'GET'
    })
  }
}

// api/charging.ts
export const chargingApi = {
  // 开始充电
  startCharging(params: {
    stationId: string
    gunId: string
    targetSOC: number
  }) {
    return request({
      url: '/api/charging/start',
      method: 'POST',
      data: params
    })
  },
  
  // 获取充电信息
  getChargingInfo(orderId: string) {
    return request({
      url: `/api/charging/${orderId}`,
      method: 'GET'
    })
  },
  
  // 暂停充电
  pauseCharging(orderId: string) {
    return request({
      url: `/api/charging/${orderId}/pause`,
      method: 'POST'
    })
  },
  
  // 恢复充电
  resumeCharging(orderId: string) {
    return request({
      url: `/api/charging/${orderId}/resume`,
      method: 'POST'
    })
  },
  
  // 停止充电
  stopCharging(orderId: string) {
    return request({
      url: `/api/charging/${orderId}/stop`,
      method: 'POST'
    })
  }
}

# 工具函数

# 网络请求封装

// utils/request.ts
interface RequestOptions {
  url: string
  method: 'GET' | 'POST' | 'PUT' | 'DELETE'
  data?: any
  header?: Record<string, string>
  timeout?: number
}

interface ApiResponse<T = any> {
  success: boolean
  data: T
  message: string
  code: number
}

const BASE_URL = 'https://api.charging.com'
const DEFAULT_TIMEOUT = 10000

// 请求拦截器
const requestInterceptor = (options: RequestOptions) => {
  // 添加认证token
  const token = uni.getStorageSync('access_token')
  if (token) {
    options.header = {
      ...options.header,
      'Authorization': `Bearer ${token}`
    }
  }
  
  // 添加公共请求头
  options.header = {
    'Content-Type': 'application/json',
    ...options.header
  }
  
  return options
}

// 响应拦截器
const responseInterceptor = (response: any): ApiResponse => {
  const { statusCode, data } = response
  
  if (statusCode === 200) {
    return data
  } else if (statusCode === 401) {
    // token过期,跳转到登录页
    uni.removeStorageSync('access_token')
    uni.navigateTo({
      url: '/pages/login/login'
    })
    throw new Error('登录已过期')
  } else {
    throw new Error(data?.message || '请求失败')
  }
}

export default function request<T = any>(options: RequestOptions): Promise<ApiResponse<T>> {
  return new Promise((resolve, reject) => {
    const requestOptions = requestInterceptor({
      timeout: DEFAULT_TIMEOUT,
      ...options,
      url: BASE_URL + options.url
    })
    
    uni.request({
      ...requestOptions,
      success: (response) => {
        try {
          const result = responseInterceptor(response)
          resolve(result)
        } catch (error) {
          reject(error)
        }
      },
      fail: (error) => {
        console.error('请求失败:', error)
        reject(new Error('网络请求失败'))
      }
    })
  })
}

# 地理位置工具

// utils/location.ts
export interface LocationInfo {
  longitude: number
  latitude: number
  address?: string
  city?: string
}

// 获取当前位置
export const getCurrentLocation = (): Promise<LocationInfo> => {
  return new Promise((resolve, reject) => {
    uni.getLocation({
      type: 'gcj02',
      success: (res) => {
        resolve({
          longitude: res.longitude,
          latitude: res.latitude
        })
      },
      fail: (error) => {
        reject(error)
      }
    })
  })
}

// 计算两点间距离(米)
export const calculateDistance = (
  lat1: number, 
  lon1: number, 
  lat2: number, 
  lon2: number
): number => {
  const R = 6371e3 // 地球半径(米)
  const φ1 = lat1 * Math.PI / 180
  const φ2 = lat2 * Math.PI / 180
  const Δφ = (lat2 - lat1) * Math.PI / 180
  const Δλ = (lon2 - lon1) * Math.PI / 180

  const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) +
          Math.cos(φ1) * Math.cos(φ2) *
          Math.sin(Δλ/2) * Math.sin(Δλ/2)
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))

  return R * c
}

// 格式化距离显示
export const formatDistance = (distance: number): string => {
  if (distance < 1000) {
    return `${Math.round(distance)}m`
  } else {
    return `${(distance / 1000).toFixed(1)}km`
  }
}

# TypeScript类型定义

// types/station.ts
export interface ChargingStation {
  id: string
  name: string
  address: string
  longitude: number
  latitude: number
  phone: string
  operatingHours: string
  status: 'online' | 'offline' | 'maintenance'
  totalGuns: number
  availableGuns: number
  chargingGuns: ChargingGun[]
  totalPower: number
  currentLoad: number
  todayEnergy: number
  todayOrders: number
  pricingRules: PricingRule[]
  reviews: Review[]
  averageRating: number
  distance?: number
}

export interface ChargingGun {
  id: string
  name: string
  type: 'dc_fast' | 'ac_slow' | 'super_fast'
  maxPower: number
  status: 'available' | 'charging' | 'offline' | 'reserved'
  currentPower?: number
  voltage?: number
  current?: number
}

export interface PricingRule {
  id: string
  timeRange: string
  electricityPrice: number
  servicePrice: number
}

export interface Review {
  id: string
  userName: string
  rating: number
  content: string
  createTime: string
}

export interface StationFilter {
  chargingType: string[]
  powerRange: [number, number]
  distance: number
  availability: 'all' | 'available' | 'fast'
}

// types/charging.ts
export interface ChargingInfo {
  orderId: string
  stationId: string
  stationName: string
  gunId: string
  gunNumber: string
  status: 'charging' | 'paused' | 'completed' | 'error'
  startTime: string
  duration: number
  currentPower: number
  voltage: number
  current: number
  chargedEnergy: number
  currentSOC: number
  targetSOC: number
  currentCost: number
  estimatedCost: number
}

export interface ChargingOrder {
  id: string
  orderNo: string
  stationId: string
  stationName: string
  gunId: string
  gunNumber: string
  status: 'pending' | 'charging' | 'completed' | 'cancelled' | 'failed'
  startTime: string
  endTime?: string
  duration: number
  chargedEnergy: number
  totalAmount: number
  electricityFee: number
  serviceFee: number
  reviewed: boolean
}

# 最佳实践

# 1. 性能优化

  • 地图优化: 使用地图聚合功能,避免同时显示过多标记点
  • 数据缓存: 缓存充电站基础信息,减少重复请求
  • 图片懒加载: 充电站图片使用懒加载技术
  • 分页加载: 订单列表采用分页加载,提升加载速度

# 2. 用户体验

  • 离线提示: 网络断开时显示友好提示
  • 加载状态: 所有异步操作都有加载状态提示
  • 错误处理: 完善的错误处理和用户提示
  • 操作反馈: 重要操作提供明确的成功/失败反馈

# 3. 安全考虑

  • 数据加密: 敏感数据传输使用HTTPS加密
  • 身份验证: 完善的用户身份验证机制
  • 权限控制: 基于角色的权限控制
  • 数据校验: 前端和后端双重数据校验

# 4. 代码规范

  • TypeScript: 全面使用TypeScript提供类型安全
  • 组件化: 合理拆分组件,提高代码复用性
  • 状态管理: 使用Pinia进行统一状态管理
  • 错误边界: 设置错误边界,防止应用崩溃

# 部署配置

# 1. 小程序配置

// manifest.json
{
  "name": "充电桩服务",
  "appid": "your_app_id",
  "description": "智能充电桩服务小程序",
  "versionName": "1.0.0",
  "versionCode": "100",
  "transformPx": false,
  "app-plus": {
    "usingComponents": true,
    "nvueStyleCompiler": "uni-app",
    "compilerVersion": 3,
    "splashscreen": {
      "alwaysShowBeforeRender": true,
      "waiting": true,
      "autoclose": true,
      "delay": 0
    }
  },
  "mp-weixin": {
    "appid": "your_wechat_appid",
    "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": false,
      "checkInvalidKey": true,
      "checkSiteMap": true,
      "uploadWithSourceMap": true,
      "compileHotReLoad": false,
      "lazyloadPlaceholderEnable": false,
      "useMultiFrameRuntime": true,
      "useApiHook": true,
      "useApiHostProcess": true,
      "babelSetting": {
        "ignore": [],
        "disablePlugins": [],
        "outputPath": ""
      }
    },
    "usingComponents": true,
    "permission": {
      "scope.userLocation": {
        "desc": "您的位置信息将用于查找附近的充电站"
      }
    },
    "requiredPrivateInfos": [
      "getLocation"
    ]
  }
}

# 2. 页面配置

// pages.json
{
  "pages": [
    {
      "path": "pages/index/index",
      "style": {
        "navigationBarTitleText": "充电桩服务",
        "enablePullDownRefresh": true
      }
    },
    {
      "path": "pages/map/map",
      "style": {
        "navigationBarTitleText": "地图找桩",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/station/detail",
      "style": {
        "navigationBarTitleText": "充电站详情"
      }
    },
    {
      "path": "pages/charging/charging",
      "style": {
        "navigationBarTitleText": "充电中",
        "navigationStyle": "custom"
      }
    }
  ],
  "tabBar": {
    "color": "#7A7E83",
    "selectedColor": "#3cc51f",
    "borderStyle": "black",
    "backgroundColor": "#ffffff",
    "list": [
      {
        "pagePath": "pages/index/index",
        "iconPath": "static/tab-icons/home.png",
        "selectedIconPath": "static/tab-icons/home-active.png",
        "text": "首页"
      },
      {
        "pagePath": "pages/map/map",
        "iconPath": "static/tab-icons/map.png",
        "selectedIconPath": "static/tab-icons/map-active.png",
        "text": "地图"
      },
      {
        "pagePath": "pages/order/list",
        "iconPath": "static/tab-icons/order.png",
        "selectedIconPath": "static/tab-icons/order-active.png",
        "text": "订单"
      },
      {
        "pagePath": "pages/profile/profile",
        "iconPath": "static/tab-icons/profile.png",
        "selectedIconPath": "static/tab-icons/profile-active.png",
        "text": "我的"
      }
    ]
  },
  "globalStyle": {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "充电桩服务",
    "navigationBarBackgroundColor": "#F8F8F8",
    "backgroundColor": "#F8F8F8"
  }
}

# 总结

本文档详细介绍了基于UniApp开发充电桩服务小程序的完整实现方案,涵盖了从项目架构设计到具体功能实现的各个方面。通过模块化的设计和完善的状态管理,确保了应用的可维护性和扩展性。

主要特性包括:

  • 实时地图找桩功能
  • 充电站详情展示
  • 实时充电监控
  • 完整的订单管理
  • 优秀的用户体验
  • 完善的错误处理

该方案可以作为充电桩行业小程序开发的参考模板,开发者可以根据具体业务需求进行定制和扩展。