浏览代码

```
feat(charge): 新增车辆管理功能

- 添加车辆相关API定义,包括获取默认车辆、车辆列表、添加车辆、
设置默认车辆、删除车辆等接口
- 定义UserVehicleVO数据类型,包含车辆品牌、颜色、车牌号、
车辆类型等属性
- 实现添加车牌页面,集成车辆添加功能
- 实现车牌管理列表页面,支持设置默认车辆和删除车辆操作
- 在充电首页展示用户默认车辆信息

feat(attractions): 新增景区详情页面并优化景区tab页

- 添加景区详情页面,配置路由和页面参数
- 优化景区tab页面结构,分离首页和订单列表组件
- 实现页面滚动监听,动态调整导航栏透明度
- 更新景区图标资源,提升视觉效果

fix(charge): 修复订单详情金额计算问题

- 修改订单详情页面金额计算逻辑,正确显示实际支付金额
- 使用actualTotal减去platformVolume计算最终价格

refactor: 优化项目配置和组件引用

- 移除未使用的WdImg组件引用
- 修正manifest.json文件格式问题
- 修复页面路由参数传递中的空值问题
```

zouzexu 13 小时之前
父节点
当前提交
1914678b23

+ 45 - 0
src/api/api.type.d.ts

@@ -2435,6 +2435,7 @@ namespace Api {
      */
     actualTotal?: number
     platformVolume?: number
+
   }
 
   export interface RechargeLevel {
@@ -2498,6 +2499,50 @@ namespace Api {
     [property: string]: any
   }
 
+  export interface UserVehicleVO {
+    /**
+     * 车辆品牌
+     */
+    brand?: string
+    /**
+     * 车辆颜色
+     */
+    color?: string
+    /**
+     * 创建时间
+     */
+    createTime?: string
+    /**
+     * 车辆ID
+     */
+    id?: number
+    /**
+     * 是否默认车辆(0-否 1-是)
+     */
+    isDefault?: number
+    /**
+     * 车牌号
+     */
+    licensePlate?: string
+    /**
+     * 车辆型号
+     */
+    model?: string
+    /**
+     * 备注
+     */
+    remark?: string
+    /**
+     * 用户ID
+     */
+    userId?: number
+    /**
+     * 车辆类型(1-新能源 2-燃油车 3-混合动力)
+     */
+    vehicleType?: number
+    [property: string]: any
+  }
+
   interface videoRightHomePage {
     list?: VideoProductVo[]
   }

+ 5 - 0
src/api/apiDefinitions.ts

@@ -104,6 +104,11 @@ export default {
   'charge.wxJsApiPay': ['POST', '/smqjh-oms/service/pay/jsapi'],
   'charge.getPurchaseRecordPage': ['POST', '/smqjh-system/applet/v1/purchaseRecord/getPurchaseRecordPage'],
   'charge.userCouponRefund':['GET','/smqjh-oms/api/v1/order_cd/userCouponRefund'],
+  'charge.default':['GET','/smqjh-system/applet/v1/vehicle/default'],
+  'charge.vehicleList':['GET','/smqjh-system/applet/v1/vehicle/list'],
+  'charge.addVehicle':['POST','/smqjh-system/applet/v1/vehicle'],
+  'charge.setDefault':['PUT','/smqjh-system/applet/v1/vehicle/default/{id}'],
+  'charge.deleteVehicle':['DELETE','/smqjh-system/applet/v1/vehicle/{id}'],
 
   'videoRight.findAppByPage': ['GET', '/smqjh-pms/app-api/v1/videoProduct/findAppByPage'],
   'videoRight.goodsDetail': ['GET', '/smqjh-pms/app-api/v1/videoProduct/findById'],

+ 45 - 0
src/api/globals.d.ts

@@ -973,6 +973,51 @@ declare global {
       >(
         config: Config
       ): Alova2Method<apiResData<any>, 'charge.userCouponRefund', Config>;
+
+      default<
+        Config extends Alova2MethodConfig<apiResData<Api.UserVehicleVO>> & {}
+      >(
+        config: Config
+      ): Alova2Method<apiResData<Api.UserVehicleVO>, 'charge.default', Config>;
+
+      vehicleList<
+        Config extends Alova2MethodConfig<apiResData<Api.UserVehicleVO[]>> & {}
+      >(
+        config: Config
+      ): Alova2Method<apiResData<Api.UserVehicleVO[]>, 'charge.vehicleList', Config>;
+
+      addVehicle<
+        Config extends Alova2MethodConfig<listData<any>> & {
+          data: {
+            /**
+             * 车牌号
+             */
+            licensePlate: string;
+            /**
+            * 用户ID
+            */
+            userId: number;
+          }
+        }
+      >(
+        config: Config
+      ): Alova2Method<listData<any>, 'charge.addVehicle', Config>;
+
+      setDefault<
+        Config extends Alova2MethodConfig<apiResData<any>> & {
+          pathParams: { id: string };
+        }
+      >(
+        config: Config
+      ): Alova2Method<apiResData<any>, 'charge.setDefault', Config>;
+
+      deleteVehicle<
+        Config extends Alova2MethodConfig<apiResData<any>> & {
+          pathParams: { id: string };
+        }
+      >(
+        config: Config
+      ): Alova2Method<apiResData<any>, 'charge.deleteVehicle', Config>;
     }
 
     videoRight: {

+ 0 - 1
src/components.d.ts

@@ -27,7 +27,6 @@ declare module 'vue' {
     WdCountDown: typeof import('wot-design-uni/components/wd-count-down/wd-count-down.vue')['default']
     WdDivider: typeof import('wot-design-uni/components/wd-divider/wd-divider.vue')['default']
     WdIcon: typeof import('wot-design-uni/components/wd-icon/wd-icon.vue')['default']
-    WdImg: typeof import('wot-design-uni/components/wd-img/wd-img.vue')['default']
     WdInput: typeof import('wot-design-uni/components/wd-input/wd-input.vue')['default']
     WdInputNumber: typeof import('wot-design-uni/components/wd-input-number/wd-input-number.vue')['default']
     WdLoading: typeof import('wot-design-uni/components/wd-loading/wd-loading.vue')['default']

+ 10 - 1
src/pages.json

@@ -644,12 +644,21 @@
     {
       "root": "subPack-attractions",
       "pages": [
+        {
+          "path": "attractionsDetail/attractionsDetail",
+          "name": "attractions-detail",
+          "islogin": false,
+          "style": {
+            "navigationBarTitleText": "",
+            "navigationStyle": "custom"
+          }
+        },
         {
           "path": "commonTab/index",
           "name": "attractions-tabbar",
           "islogin": false,
           "style": {
-            "navigationBarTitleText": "景区门票",
+            "navigationBarTitleText": "",
             "navigationStyle": "custom"
           }
         }

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

@@ -60,7 +60,7 @@ const navList = computed(() => {
     { icon: `${StaticUrl}/smqjh-sp.png`, title: '电影演出', name: 'film-index', show: true },
     { icon: `${StaticUrl}/smqjh-vip.png`, title: '视频权益', name: 'video-rights-tabbar', show: !isOnlineAudit.value },
     { icon: `${StaticUrl}/smqjh-djk.png`, title: '大健康', name: 'djk-homeTabbar', show: true },
-    { icon: `${StaticUrl}/smqjh-djk.png`, title: '景区', name: 'attractions-tabbar', show: true },
+    { icon: `${StaticUrl}/smqjh-attractions.png`, title: '景区', name: 'attractions-tabbar', show: true },
     { icon: `${StaticUrl}/smqjh-diancan.png`, title: '大牌点餐', name: '', show: !isOnlineAudit.value },
     { icon: `${StaticUrl}/smqjh-jiayou.png`, title: '加油', name: '', show: !isOnlineAudit.value }, // refueling-tabbar
     { icon: `${StaticUrl}/smqjh-jiudian.png`, title: '酒店民宿', name: '', show: !isOnlineAudit.value },

+ 230 - 0
src/subPack-attractions/attractionsDetail/attractionsDetail.vue

@@ -0,0 +1,230 @@
+<script setup lang="ts">
+import DatePicker from '../components/DatePicker.vue'
+import { StaticUrl } from '@/config'
+import router from '@/router'
+
+const { statusBarHeight, opcity } = storeToRefs(useSysStore())
+
+// 轮播图数据
+const swiperList = ref([
+  'https://picsum.photos/400/300?random=1',
+  'https://picsum.photos/400/300?random=2',
+  'https://picsum.photos/400/300?random=3',
+])
+const currentSwiper = ref(0)
+
+// 选中的日期
+const selectedDate = ref(new Date(2026, 2, 21)) // 默认选中2026年3月21日
+
+// 日历数据(按月份存储,key为 'YYYY-MM')
+const calendarDataMap = ref<Record<string, Array<{ day: number, status?: string, price?: number, selected?: boolean }>>>({
+  '2026-03': [
+    { day: 21, status: '充足', price: 290 },
+    { day: 27, status: '售罄', price: 290 },
+  ],
+  '2026-04': [
+    { day: 1, status: '充足', price: 280 },
+    { day: 5, status: '紧张', price: 300 },
+    { day: 10, status: '售罄', price: 280 },
+  ],
+})
+
+// 当前显示的月份
+const currentYearMonth = ref({ year: 2026, month: 3 })
+
+// 获取当前月份的日期数据
+const currentMonthDays = computed(() => {
+  const key = `${currentYearMonth.value.year}-${String(currentYearMonth.value.month).padStart(2, '0')}`
+  return calendarDataMap.value[key] || []
+})
+
+// 景区信息
+const attractionInfo = ref({
+  name: '贵州黄果树风景名胜区',
+  address: '镇宁布依族苗族自治县-黄果树镇贵黄公路',
+  price: 290,
+  title: '日场门票+观光车+飞越黄果树观影票+吉祥物+冰箱贴 经典必打卡',
+})
+
+definePage({
+  name: 'attractions-detail',
+  islogin: false,
+  style: {
+    navigationBarTitleText: '',
+    navigationStyle: 'custom',
+  },
+})
+
+onMounted(() => {
+  opcity.value = 0
+})
+
+onPageScroll((e) => {
+  const calculatedOpacity = e.scrollTop / 100
+  opcity.value = Math.min(1, Math.max(0.1, calculatedOpacity))
+})
+
+function handleNav() {
+  uni.openLocation({
+    latitude: 25.9913,
+    longitude: 105.6687,
+    name: attractionInfo.value.name,
+    address: attractionInfo.value.address,
+  })
+}
+
+function handleService() {
+  uni.makePhoneCall({
+    phoneNumber: '400-123-4567',
+  })
+}
+
+function handleOrder() {
+  router.push({ name: 'attractions-order' })
+}
+
+function handleBook() {
+  useGlobalToast().show('立即预定')
+}
+
+// 处理日期选择
+function handleDateSelect(e: { date: Date, day: number, item?: any }) {
+  selectedDate.value = e.date
+  console.log('选中日期:', e.date, '日期数据:', e.item)
+}
+
+// 处理月份变化
+function handleMonthChange(e: { year: number, month: number }) {
+  currentYearMonth.value = { year: e.year, month: e.month }
+  console.log('切换月份:', e.year, '年', e.month, '月')
+}
+</script>
+
+<template>
+  <view class="min-h-screen bg-#F5F5F5">
+    <wd-navbar
+      title="景区门票"
+      :custom-style="`background-color: rgba(226, 255, 145, ${opcity})`"
+      :bordered="false"
+      :z-index="9999"
+      safe-area-inset-top
+      left-arrow
+      fixed
+      @click-left="router.back()"
+    />
+    <!-- 轮播图 -->
+    <wd-swiper
+      v-model:current="currentSwiper"
+      :list="swiperList"
+      :indicator="false"
+      :height="300"
+      class="w-full"
+    />
+
+    <!-- 价格标题区域 -->
+    <view class="relative z-10 rounded-t-32rpx bg-white px24rpx pt30rpx -mt-30rpx">
+      <view class="flex items-baseline text-#FF4D3A">
+        <text class="text-24rpx">
+          ¥
+        </text>
+        <text class="mx4rpx text-48rpx font-bold">
+          {{ attractionInfo.price }}
+        </text>
+        <text class="text-24rpx text-gray">
+          起
+        </text>
+      </view>
+      <view class="mt16rpx text-32rpx font-semibold line-height-[1.4]">
+        {{ attractionInfo.title }}
+      </view>
+
+      <!-- 地址导航卡片 -->
+      <view
+        class="mt24rpx h160rpx flex items-center justify-between bg-cover bg-center px24rpx"
+        :style="{ backgroundImage: `url(${StaticUrl}/djk-shop-nav-bg.png)` }"
+      >
+        <view class="w450rpx">
+          <view class="line-clamp-1 text-32rpx font-semibold">
+            {{ attractionInfo.name }}
+          </view>
+          <view class="line-clamp-1 mt12rpx flex items-center text-24rpx text-gray">
+            {{ attractionInfo.address }}
+          </view>
+        </view>
+        <view class="flex flex-col items-center justify-center" @click="handleNav">
+          <image
+            :src="`${StaticUrl}/djk-shop-dh.png`"
+            class="h40rpx w40rpx"
+          />
+          <view class="mt8rpx text-24rpx">
+            导航
+          </view>
+        </view>
+      </view>
+
+      <!-- 选择日期 -->
+      <DatePicker
+        v-model="selectedDate"
+        title="选择日期"
+        :day-data="currentMonthDays"
+        class="mt24rpx"
+        @select="handleDateSelect"
+        @month-change="handleMonthChange"
+      />
+
+      <!-- 预定须知 -->
+      <view class="mt24rpx pb40rpx">
+        <view class="text-32rpx font-semibold">
+          预定须知
+        </view>
+        <view class="mt16rpx text-28rpx font-semibold">
+          退款说明
+        </view>
+        <view class="mt12rpx text-26rpx text-gray line-height-[1.6]">
+          <view>使用日期当天23:59(含)之前申请取消,不收取损失费</view>
+          <view class="mt8rpx">
+            使用日期当天23:59之后申请取消,收取100%损失费;激活后不可退
+          </view>
+          <view class="mt8rpx">
+            如使用优惠,则损失费用按照优惠前金额的比例收取,最高不超过实付金额
+          </view>
+          <view class="mt8rpx">
+            产品不支持部分退
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 底部占位 -->
+    <view class="h120rpx" />
+
+    <!-- 底部操作栏 -->
+    <view
+      class="fixed bottom-0 left-0 z-100 box-border w-full flex items-center justify-between border-t border-#eee bg-white px24rpx py16rpx"
+      :style="{ paddingBottom: `${(Number(statusBarHeight) || 44) - 20}px` }"
+    >
+      <view class="flex items-center border-t-#EEEEEE">
+        <view class="mr-40rpx flex flex-col items-center" @click="handleService">
+          <wd-icon name="service" size="22px" color="#666" />
+          <text class="mt4rpx text-22rpx text-#666">
+            客服
+          </text>
+        </view>
+        <view class="flex flex-col items-center" @click="handleOrder">
+          <wd-icon name="list" size="22px" color="#666" />
+          <text class="mt4rpx text-22rpx text-#666">
+            订单
+          </text>
+        </view>
+      </view>
+      <view
+        class="h80rpx w-400rpx flex items-center justify-center rounded-40rpx bg-#9ED605 px80rpx text-28rpx text-#FFF font-semibold"
+        @click="handleBook"
+      >
+        立即预定
+      </view>
+    </view>
+  </view>
+</template>
+
+<style lang="scss" scoped></style>

+ 80 - 0
src/subPack-attractions/commonTab/components/homeList.vue

@@ -0,0 +1,80 @@
+<script setup lang="ts">
+import router from '@/router'
+import { StaticUrl } from '@/config'
+
+const { statusBarHeight, MenuButtonHeight, opcity } = storeToRefs(useSysStore())
+
+onMounted(() => {
+  opcity.value = 0
+})
+</script>
+
+<template>
+  <view class="attractions-home-page">
+    <wd-navbar
+      title="景区门票"
+      :custom-style="`background-color: rgba(226, 255, 145, ${opcity})`"
+      :bordered="false"
+      :z-index="9999"
+      safe-area-inset-top
+      left-arrow
+      fixed
+      @click-left="router.back()"
+    />
+
+    <view class="relative h-624rpx w-full">
+      <image class="h-full w-full" :src="`${StaticUrl}/attractions-home-bg.png`" />
+      <view :style="{ height: `${(Number(statusBarHeight) || 44) + MenuButtonHeight + 12}px` }" />
+
+      <view class="absolute left-24rpx right-24rpx top-198rpx">
+        <view class="h-60rpx w-full flex items-center justify-between rounded-40rpx bg-white pr-6rpx">
+          <view class="flex items-center pb-14rpx pl-24rpx pt-16rpx">
+            <wd-icon name="search" size="14" color="#ccc" />
+            <view class="ml-12rpx text-24rpx text-gray">
+              霸王茶姬
+            </view>
+          </view>
+          <view
+            class="h-50rpx w-96rpx flex items-center justify-center rounded-26rpx bg-[var(--them-color)] text-24rpx text-white font-semibold"
+          >
+            搜索
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <view class="relative px-24rpx">
+      <view>
+        <view v-for="value in 12" :key="value" class="mb-20rpx flex items-center gap-24rpx rounded-16rpx bg-#FFF p-24rpx" @click="router.push({ name: 'attractions-detail' })">
+          <view class="h-160rpx w-160rpx rounded-16rpx">
+            icon
+          </view>
+          <view class="flex-1">
+            <view class="text-32rpx font-bold">
+              日场门票+观光车+飞越黄果树观影票+吉祥物+冰箱贴
+            </view>
+            <view class="mt-24rpx flex items-center justify-between">
+              <view>
+                <text class="text-26rpx text-#FF4D3A">
+                  ¥
+                </text>
+                <text class="text-36rpx text-#FF4D3A font-bold">
+                  290
+                </text>
+                <text class="text-24rpx text-#AAA">
+                  起
+                </text>
+              </view>
+              <image
+                class="h-48rpx w-116rpx"
+                :src="`${StaticUrl}/attractions-home-btn.png`"
+              />
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<style lang="scss" scoped></style>

+ 16 - 0
src/subPack-attractions/commonTab/components/orderList.vue

@@ -0,0 +1,16 @@
+<script setup lang="ts">
+const { statusBarHeight, MenuButtonHeight } = storeToRefs(useSysStore())
+</script>
+
+<template>
+  <view class="video-rights-order-page">
+    <wd-navbar
+      title="订单列表" custom-style="background-color:#FFF" :bordered="false" :z-index="99"
+      safe-area-inset-top fixed
+    />
+    <view :style="{ paddingTop: `${(Number(statusBarHeight) || 44) + MenuButtonHeight + 12}px` }" />
+    订单
+  </view>
+</template>
+
+<style lang="scss" scoped></style>

+ 20 - 3
src/subPack-attractions/commonTab/index.vue

@@ -1,29 +1,46 @@
 <script setup lang="ts">
+import homeList from './components/homeList.vue'
+import orderList from './components/orderList.vue'
 import { StaticUrl } from '@/config'
 
 const tabbar = ref(0)
+const { opcity } = storeToRefs(useSysStore())
+
 definePage({
   name: 'attractions-tabbar',
   islogin: false,
   style: {
-    navigationBarTitleText: '景区门票',
+    navigationBarTitleText: '',
     navigationStyle: 'custom',
   },
 })
+
+// 页面级滚动监听 - 必须在页面组件中才能生效
+onPageScroll((e) => {
+  // 只在首页 tab 时更新透明度
+  if (tabbar.value === 0) {
+    const calculatedOpacity = e.scrollTop / 100
+    opcity.value = Math.min(1, Math.max(0.1, calculatedOpacity))
+  }
+})
 </script>
 
 <template>
   <view class="">
+    <home-list v-if="tabbar === 0" />
+    <order-list v-if="tabbar === 1" />
     <view class="">
       <wd-tabbar v-model="tabbar" safe-area-inset-bottom placeholder fixed :bordered="false" :z-index="99999">
         <wd-tabbar-item title="列表" icon="goods">
           <template #icon>
-            <wd-img height="40rpx" width="40rpx" :src="`${StaticUrl}/attractions-home-tabbar.png`" />
+            <image v-if="tabbar === 0" class="h-40rpx w-40rpx" :src="`${StaticUrl}/attractions-selHome-tabbar.png`" />
+            <image v-else class="h-40rpx w-40rpx" :src="`${StaticUrl}/attractions-home-tabbar.png`" />
           </template>
         </wd-tabbar-item>
         <wd-tabbar-item title="订单记录" icon="list">
           <template #icon>
-            <wd-img height="40rpx" width="40rpx" :src="`${StaticUrl}/attractions-order-tabbar.png`" />
+            <image v-if="tabbar === 1" class="h-40rpx w-40rpx" :src="`${StaticUrl}/attractions-selOrder-tabbar.png`" />
+            <image v-else class="h-40rpx w-40rpx" :src="`${StaticUrl}/attractions-order-tabbar.png`" />
           </template>
         </wd-tabbar-item>
       </wd-tabbar>

+ 237 - 0
src/subPack-attractions/components/DatePicker.vue

@@ -0,0 +1,237 @@
+<script setup lang="ts">
+/**
+ * 日期选择器组件
+ * @description 支持年月切换、日期选择、价格/库存状态展示
+ */
+
+interface DayItem {
+  day: number
+  status?: string
+  price?: number
+  selected?: boolean
+  disabled?: boolean
+}
+
+interface Props {
+  /** 当前选中的日期 */
+  modelValue?: Date
+  /** 标题 */
+  title?: string
+  /** 日期数据 */
+  dayData?: DayItem[]
+  /** 是否显示星期标题 */
+  showWeekHeader?: boolean
+  /** 自定义类名 */
+  customClass?: string
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  modelValue: () => new Date(),
+  title: '选择日期',
+  dayData: () => [],
+  showWeekHeader: true,
+  customClass: '',
+})
+
+const emit = defineEmits<{
+  /** 选中日期变化 */
+  (e: 'update:modelValue', value: Date): void
+  /** 选中日期 */
+  (e: 'select', value: { date: Date, day: number, item?: DayItem }): void
+  /** 月份变化 */
+  (e: 'monthChange', value: { year: number, month: number }): void
+}>()
+
+// 星期标题
+const weekDays = ['一', '二', '三', '四', '五', '六', '日']
+
+// 当前显示的日期
+const currentDate = ref(new Date(props.modelValue))
+
+// 当前年月显示文本
+const currentMonthText = computed(() => {
+  return `${currentDate.value.getFullYear()}年${currentDate.value.getMonth() + 1}月`
+})
+
+// 当前年月值
+const currentYear = computed(() => currentDate.value.getFullYear())
+const currentMonth = computed(() => currentDate.value.getMonth())
+
+// 计算当前月日历数据
+const calendarDays = computed(() => {
+  const year = currentYear.value
+  const month = currentMonth.value
+
+  // 获取当月第一天是星期几 (0=周日, 1=周一...)
+  const firstDayOfMonth = new Date(year, month, 1).getDay()
+  // 转换为周一开头 (0=周一, 6=周日)
+  const firstDayIndex = firstDayOfMonth === 0 ? 6 : firstDayOfMonth - 1
+
+  // 获取当月天数
+  const daysInMonth = new Date(year, month + 1, 0).getDate()
+
+  // 获取上月天数(用于填充前置空白)
+  const daysInPrevMonth = new Date(year, month, 0).getDate()
+
+  const days: Array<{ day: number, isCurrentMonth: boolean, item?: DayItem, date: Date }> = []
+
+  // 填充上月日期(灰色显示)
+  for (let i = firstDayIndex - 1; i >= 0; i--) {
+    const day = daysInPrevMonth - i
+    days.push({
+      day,
+      isCurrentMonth: false,
+      date: new Date(year, month - 1, day),
+    })
+  }
+
+  // 填充当月日期
+  for (let day = 1; day <= daysInMonth; day++) {
+    // 查找对应的日期数据
+    const dayItem = props.dayData.find(d => d.day === day)
+    const date = new Date(year, month, day)
+
+    // 判断是否选中
+    const isSelected = props.modelValue
+      && date.getFullYear() === props.modelValue.getFullYear()
+      && date.getMonth() === props.modelValue.getMonth()
+      && date.getDate() === props.modelValue.getDate()
+
+    days.push({
+      day,
+      isCurrentMonth: true,
+      item: dayItem ? { ...dayItem, selected: isSelected } : undefined,
+      date,
+    })
+  }
+
+  // 填充下月日期(补全到6行或5行)
+  const remainingCells = 42 - days.length // 6行 x 7列 = 42
+  for (let day = 1; day <= remainingCells; day++) {
+    days.push({
+      day,
+      isCurrentMonth: false,
+      date: new Date(year, month + 1, day),
+    })
+  }
+
+  return days
+})
+
+// 切换到上月
+function prevMonth() {
+  const newDate = new Date(currentDate.value)
+  newDate.setMonth(newDate.getMonth() - 1)
+  currentDate.value = newDate
+  emit('monthChange', { year: newDate.getFullYear(), month: newDate.getMonth() + 1 })
+}
+
+// 切换到下月
+function nextMonth() {
+  const newDate = new Date(currentDate.value)
+  newDate.setMonth(newDate.getMonth() + 1)
+  currentDate.value = newDate
+  emit('monthChange', { year: newDate.getFullYear(), month: newDate.getMonth() + 1 })
+}
+
+// 选择日期
+function selectDay(dayInfo: typeof calendarDays.value[0]) {
+  if (!dayInfo.isCurrentMonth)
+    return
+
+  const newDate = new Date(currentDate.value)
+  newDate.setDate(dayInfo.day)
+
+  emit('update:modelValue', newDate)
+  emit('select', {
+    date: newDate,
+    day: dayInfo.day,
+    item: dayInfo.item,
+  })
+}
+
+// 监听外部modelValue变化
+watch(() => props.modelValue, (newVal) => {
+  if (newVal) {
+    currentDate.value = new Date(newVal)
+  }
+})
+</script>
+
+<template>
+  <view class="rounded-16rpx bg-#F6F6F6 p-20rpx" :class="customClass">
+    <view class="text-32rpx font-semibold">
+      {{ title }}
+    </view>
+
+    <!-- 年月切换 -->
+    <view class="mt20rpx flex items-center justify-between">
+      <view class="h60rpx w60rpx flex items-center justify-center" @click="prevMonth">
+        <wd-icon name="arrow-left" size="20px" color="#999" />
+      </view>
+      <view class="rounded-24rpx bg-#F5F5F5 px32rpx py12rpx text-28rpx">
+        {{ currentMonthText }}
+      </view>
+      <view class="h60rpx w60rpx flex items-center justify-center" @click="nextMonth">
+        <wd-icon name="arrow-right" size="20px" color="#999" />
+      </view>
+    </view>
+
+    <!-- 星期标题 -->
+    <view v-if="showWeekHeader" class="mt20rpx flex items-center justify-between px12rpx">
+      <view
+        v-for="day in weekDays"
+        :key="day"
+        class="h60rpx w60rpx flex items-center justify-center text-28rpx"
+        :class="day === '六' || day === '日' ? 'text-#52c41a' : 'text-#333'"
+      >
+        {{ day }}
+      </view>
+    </view>
+
+    <!-- 日期网格 -->
+    <view class="mt12rpx flex flex-wrap">
+      <view
+        v-for="(item, index) in calendarDays"
+        :key="index"
+        class="mb16rpx h90rpx w-14.28% flex flex-col items-center justify-center"
+        @click="selectDay(item)"
+      >
+        <view
+          class="h100rpx w76rpx flex flex-col items-center justify-center rounded-16rpx"
+          :class="[
+            item.item?.selected ? 'bg-#E2FF91' : '',
+            item.item?.status === '售罄' || item.item?.disabled ? 'bg-#F5F5F5' : '',
+            !item.isCurrentMonth ? 'opacity-30' : '',
+          ]"
+        >
+          <text
+            class="text-28rpx"
+            :class="[
+              item.item?.selected ? 'text-#333 font-semibold' : 'text-#333',
+              item.item?.status === '售罄' || item.item?.disabled ? 'text-#999' : '',
+              !item.isCurrentMonth ? 'text-#999' : '',
+            ]"
+          >
+            {{ item.day }}
+          </text>
+          <text
+            v-if="item.item?.status && item.isCurrentMonth"
+            class="mt4rpx text-20rpx"
+            :class="item.item.status === '充足' ? 'text-#52c41a' : 'text-#999'"
+          >
+            {{ item.item.status }}
+          </text>
+          <text
+            v-else-if="item.item?.price && item.isCurrentMonth && item.item.status !== '售罄'"
+            class="mt4rpx text-20rpx text-#FF4D3A"
+          >
+            ¥{{ item.item.price }}
+          </text>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<style lang="scss" scoped></style>

+ 14 - 18
src/subPack-charge/chargeAddPlate/chargeAddPlate.vue

@@ -1,7 +1,9 @@
 <script setup lang="ts">
 import { ref } from 'vue'
 import addPlate from '@/subPack-charge/components/plate/index.vue'
+import router from '@/router'
 
+const globalToast = useGlobalToast()
 definePage({
   name: 'charge-add-plate',
   islogin: false,
@@ -10,41 +12,35 @@ definePage({
   },
 })
 
-// 车牌号数据
 const plateNumber = ref<(string | number)[]>(Array.from({ length: 8 }, () => ''))
 
-// 子组件引用
+const { userInfo } = storeToRefs(useUserStore())
+
 const plateRef = ref<InstanceType<typeof addPlate> | null>(null)
 
-// 车牌号变化处理
 function handlePlateChange(value: (string | number)[]) {
   plateNumber.value = value
 }
 
-// 保存车牌
+// 保存
 async function handleSave() {
   const result = plateRef.value?.validatePlateNumber()
   if (!result?.valid) {
-    uni.showToast({
-      title: result?.message || '请输入正确的车牌号',
-      icon: 'none',
-    })
+    globalToast.warning(result?.message || '请输入正确的车牌号')
     return
   }
 
-  // TODO: 调用添加车牌接口
-  const plateString = plateRef.value?.getPlateString()
-  console.log('保存车牌:', plateString)
+  const plateString = plateRef.value?.getPlateString()?.replace(/·/g, '') || ''
 
-  uni.showToast({
-    title: '保存成功',
-    icon: 'success',
+  await Apis.charge.addVehicle({
+    data: {
+      licensePlate: plateString,
+      userId: userInfo.value.id!,
+    },
   })
-
-  // 返回上一页
   setTimeout(() => {
-    uni.navigateBack()
-  }, 1500)
+    router.back()
+  }, 500)
 }
 </script>
 

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

@@ -169,7 +169,7 @@ async function getOrderDetail() {
             积分扣减
           </view>
           <view class="text-24rpx text-#F44033">
-            {{ chargeOrderDetail?.realCost || '--' }}元
+            {{ chargeOrderDetail ? (chargeOrderDetail.actualTotal || 0) - (chargeOrderDetail.platformVolume || 0) : '--' }}元
           </view>
         </view>
         <view class="mt-28rpx flex items-center justify-between">

+ 57 - 9
src/subPack-charge/chargePlateList/chargePlateList.vue

@@ -1,5 +1,6 @@
 <script setup lang="ts">
 import router from '@/router'
+import { createGlobalLoadingMiddleware } from '@/api/core/middleware'
 
 definePage({
   name: 'charge-plate-list',
@@ -8,27 +9,71 @@ definePage({
     navigationBarTitleText: '车牌管理',
   },
 })
+
+const { data: plateList, refresh } = usePagination(() => Apis.charge.vehicleList({}), {
+  immediate: false,
+  pageNum: 1,
+  pageSize: 10,
+  initialData: [],
+  data: res => res.data,
+  append: true,
+  middleware: createGlobalLoadingMiddleware(),
+})
+
+onShow(() => {
+  refresh()
+})
+
+// 设置默认车辆
+async function handleSetDefault(id: number) {
+  useGlobalMessage().confirm({
+    title: '提示',
+    msg: '确定要设置该车辆为默认车辆吗?',
+    success: async () => {
+      await Apis.charge.setDefault({
+        pathParams: { id: String(id) },
+      })
+      await refresh()
+    },
+  })
+}
+
+// 删除车辆
+function handleDelete(id: number) {
+  useGlobalMessage().confirm({
+    title: '提示',
+    msg: '确定要删除该车辆吗?',
+    success: async () => {
+      await Apis.charge.deleteVehicle({
+        pathParams: { id: String(id) },
+      })
+      await refresh()
+    },
+  })
+}
 </script>
 
 <template>
   <view class="box-border px24rpx">
     <view>
       <view class="h-20rpx" />
-      <view class="relative h-180rpx w-full flex items-center justify-between rounded-16rpx bg-[#9ED605]/30">
+      <view v-for="item in plateList" :key="item.id" class="relative mb-20rpx h-180rpx w-full flex items-center justify-between rounded-16rpx bg-[#9ED605]/30">
         <view class="ml-20rpx text-32rpx font-bold">
-          贵AV88888
+          {{ item.licensePlate }}
         </view>
         <view class="mr-20rpx text-26rpx text-#666666">
-          <view class="absolute right-0 top-0 rounded-[0rpx_16rpx_0rpx_16rpx] bg-#9ED605 px-24rpx py-8rpx text-#FFF">
+          <view v-if="item.isDefault === 1" class="absolute right-0 top-0 rounded-[0rpx_16rpx_0rpx_16rpx] bg-#9ED605 px-24rpx py-8rpx text-#FFF">
             默认
           </view>
-          <!-- <view>设为默认</view> -->
-          <view class="mt-20rpx">
+          <view v-else @click="handleSetDefault(item.id!)">
+            设为默认
+          </view>
+          <view class="mt-20rpx" @click="handleDelete(item.id!)">
             <text>🗑 删除</text>
           </view>
         </view>
       </view>
-      <StatusTip tip="暂未绑定车辆" />
+      <StatusTip v-if="!plateList.length" tip="暂未绑定车辆" />
     </view>
     <view class="mt-100rpx">
       <view class="text-32rpx font-bold">
@@ -36,13 +81,16 @@ definePage({
       </view>
       <view class="mt-16rpx text-28rpx text-#666666">
         <view class="mt-10rpx">
-          1.绑定车牌后,将按订单充电时长+30分钟离场时间进行减免停车费;(例如:充电时长60分钟,系统自动延长30分钟离场时间,即离场时减免90分钟停车费)
+          1.先绑定车牌再开始充电,才能享受充电停车费减免,充电过程中绑定车牌的无法减免停车费;
+        </view>
+        <view class="mt-10rpx">
+          2.绑定车牌后,将按订单充电时长+30分钟离场时间进行减免停车费(例如:充电时长60分钟,系统自动延长30分钟离场时间,即离场时减免90分钟停车费);
         </view>
         <view class="mt-10rpx">
-          2.绑定多个车牌时,请在充电开始前,确认充电车辆已设为当前默认充电车辆后,再开始充电,否则无法进行减免停车费。
+          3.绑定多个车牌时,请在充电开始前,确认充电车辆已设为当前默认充电车辆后再开始充电,否则无法进行减免充电停车费;
         </view>
         <view class="mt-10rpx">
-          3.车牌绑定错误或默认车牌未对应现场充电车辆导致无法减免停车费,因此产生的一切损失与本平台无关。
+          4.车牌绑定未按正确操作流程或车牌未对应现场充电车辆导致无法减免停车费,因此产生的一切损失与本平台无关。
         </view>
       </view>
     </view>

+ 16 - 5
src/subPack-charge/index/index.vue

@@ -49,6 +49,7 @@ onMounted(() => {
 onShow(() => {
   refresh()
   getUserAccountInfo()
+  getDefaultVehicle()
 })
 onPageScroll((e) => {
   const calculatedOpacity = e.scrollTop / 100
@@ -68,6 +69,15 @@ async function getUserAccountInfo() {
   const res = await Apis.charge.getMemberInfo({})
   userAccountInfo.value = res.data
 }
+
+/**
+ * 获取用户默认车辆
+ */
+const defaultVehicle = ref<Api.UserVehicleVO>()
+async function getDefaultVehicle() {
+  const res = await Apis.charge.default({})
+  defaultVehicle.value = res.data
+}
 // 处理筛选项点击的方法
 function handleFilterClick(filterKey: number) {
   activeFilter.value = filterKey
@@ -232,7 +242,8 @@ function refund() {
         <view class="text-32rpx font-bold">
           我的车辆
         </view>
-        <view class="flex items-center gap-10rpx" @click="router.push({ name: 'charge-plate-list' })">
+        <!-- 无车辆时显示添加提示 -->
+        <view v-if="!defaultVehicle" class="flex items-center gap-10rpx" @click="router.push({ name: 'charge-plate-list' })">
           <view class="text-26rpx">
             添加车辆,享更多权益
           </view>
@@ -240,11 +251,11 @@ function refund() {
             +
           </view>
         </view>
-        <!-- <view class="flex items-center gap-50rpx text-26rpx text-#9ED605">
-          <view>贵AV88888</view>
+        <!-- 有车辆时显示车牌和管理入口 -->
+        <view v-else class="flex items-center gap-50rpx text-26rpx text-#9ED605" @click="router.push({ name: 'charge-plate-list' })">
+          <view>{{ defaultVehicle.licensePlate }}</view>
           <view>管理>></view>
-        </view> -->
-        <!-- <wd-swiper :list="swiperList" :height="100" :indicator="false" value-key="advertImg" /> -->
+        </view>
       </view>
       <view class="mt-24rpx flex items-center gap-20rpx">
         <view

+ 1 - 1
src/subPack-videoRights/commonTab/components/home.vue

@@ -56,7 +56,7 @@ function clearSearch() {
     <view class="box-border px24rpx">
       <scroll-view scroll-y type="custom">
         <grid-view type="masonry" cross-axis-count="2" main-axis-gap="10" cross-axis-gap="10">
-          <view v-for="item in videoDataList" :key="item?.id" class="mt-18rpx rounded-16rpx bg-#FFF" @click="router.push({ name: 'video-rights-detail', params: { id: item.id } })">
+          <view v-for="item in videoDataList" :key="item?.id" class="mt-18rpx rounded-16rpx bg-#FFF" @click="router.push({ name: 'video-rights-detail', params: { id: item.id || '' } })">
             <view class="image-wrapper h-342rpx w-342rpx">
               <image
                 class="h-full w-full rounded-16rpx object-c"

+ 1 - 1
src/subPack-videoRights/commonTab/components/order.vue

@@ -55,7 +55,7 @@ function handleClick(e: any) {
         <wd-tab :title="item">
           <view class="box-border bg-#f6f6f6 px24rpx">
             <view class="h-4rpx" />
-            <view v-for=" order in orderList" :key="order.orderNumber" class="mt-28rpx rounded-16rpx bg-#FFF p-24rpx" @click="router.push({ name: 'video-rights-order-info', params: { orderNo: order.orderNumber } })">
+            <view v-for=" order in orderList" :key="order.orderNumber" class="mt-28rpx rounded-16rpx bg-#FFF p-24rpx" @click="router.push({ name: 'video-rights-order-info', params: { orderNo: order.orderNumber || '' } })">
               <view class="flex items-center justify-between">
                 <view class="text-28rpx">
                   {{ order.createTime }}

+ 1 - 0
src/uni-pages.d.ts

@@ -62,6 +62,7 @@ interface NavigateToOptions {
        "/subPack-refueling/commonTab/index" |
        "/subPack-refueling/orderDetaile/index" |
        "/subPack-refueling/webView/index" |
+       "/subPack-attractions/attractionsDetail/attractionsDetail" |
        "/subPack-attractions/commonTab/index";
 }
 interface RedirectToOptions extends NavigateToOptions {}