Sfoglia il codice sorgente

```
feat: 新增景区模块和车辆管理功能

- 新增 WdImg 组件声明到组件类型定义文件
- 在首页导航列表中添加景区入口
- 新增 chargeAddPlate 和 chargePlateList 页面用于车辆管理
- 添加 subPack-attractions 子包支持景区门票功能
- 在充电模块首页添加我的车辆管理和添加车辆入口
- 更新页面路由配置和类型定义
```

zouzexu 1 giorno fa
parent
commit
e0a6a9ca4c

+ 1 - 0
src/components.d.ts

@@ -27,6 +27,7 @@ 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']

+ 30 - 0
src/pages.json

@@ -389,6 +389,14 @@
     {
       "root": "subPack-charge",
       "pages": [
+        {
+          "path": "chargeAddPlate/chargeAddPlate",
+          "name": "charge-add-plate",
+          "islogin": false,
+          "style": {
+            "navigationBarTitleText": "添加车牌"
+          }
+        },
         {
           "path": "chargeBuyaTicketList/chargeBuyaTicketList",
           "name": "charge-buy-a-ticket-list",
@@ -442,6 +450,14 @@
             "navigationStyle": "custom"
           }
         },
+        {
+          "path": "chargePlateList/chargePlateList",
+          "name": "charge-plate-list",
+          "islogin": true,
+          "style": {
+            "navigationBarTitleText": "车牌管理"
+          }
+        },
         {
           "path": "chargeSearchList/chargeSearchList",
           "name": "cahrge-search-list",
@@ -624,6 +640,20 @@
           }
         }
       ]
+    },
+    {
+      "root": "subPack-attractions",
+      "pages": [
+        {
+          "path": "commonTab/index",
+          "name": "attractions-tabbar",
+          "islogin": false,
+          "style": {
+            "navigationBarTitleText": "景区门票",
+            "navigationStyle": "custom"
+          }
+        }
+      ]
     }
   ]
 }

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

@@ -60,6 +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-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 },

+ 78 - 0
src/subPack-charge/chargeAddPlate/chargeAddPlate.vue

@@ -0,0 +1,78 @@
+<script setup lang="ts">
+import { ref } from 'vue'
+import addPlate from '@/subPack-charge/components/plate/index.vue'
+
+definePage({
+  name: 'charge-add-plate',
+  islogin: false,
+  style: {
+    navigationBarTitleText: '添加车牌',
+  },
+})
+
+// 车牌号数据
+const plateNumber = ref<(string | number)[]>(Array.from({ length: 8 }, () => ''))
+
+// 子组件引用
+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',
+    })
+    return
+  }
+
+  // TODO: 调用添加车牌接口
+  const plateString = plateRef.value?.getPlateString()
+  console.log('保存车牌:', plateString)
+
+  uni.showToast({
+    title: '保存成功',
+    icon: 'success',
+  })
+
+  // 返回上一页
+  setTimeout(() => {
+    uni.navigateBack()
+  }, 1500)
+}
+</script>
+
+<template>
+  <view class="box-border px24rpx">
+    <view class="h-20rpx" />
+    <view class="rounded-16rpx bg-#FFF p-20rpx shadow-[0_4rpx_20rpx_0_rgba(101,108,106,0.1)]">
+      <view class="mb-24rpx">
+        <text class="text-32rpx font-600">
+          车牌号码
+        </text>
+        <text class="text-#9ED605">
+          *
+        </text>
+      </view>
+      <addPlate
+        ref="plateRef"
+        :plate-number="plateNumber"
+        @my-plate-change="handlePlateChange"
+      />
+      <view
+        class="mt-24rpx h-80rpx w-664rpx rounded-16rpx bg-[linear-gradient(90deg,#DBFC81_0%,#9ED605_100%)] text-center text-28rpx font-800 line-height-[80rpx] shadow-[inset_0rpx_6rpx_20rpx_2rpx_#FFFFFF]"
+        @click="handleSave"
+      >
+        保存
+      </view>
+    </view>
+  </view>
+</template>
+
+<style lang="scss" scoped></style>

+ 58 - 0
src/subPack-charge/chargePlateList/chargePlateList.vue

@@ -0,0 +1,58 @@
+<script setup lang="ts">
+import router from '@/router'
+
+definePage({
+  name: 'charge-plate-list',
+  islogin: true,
+  style: {
+    navigationBarTitleText: '车牌管理',
+  },
+})
+</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 class="ml-20rpx text-32rpx font-bold">
+          贵AV88888
+        </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>
+          <!-- <view>设为默认</view> -->
+          <view class="mt-20rpx">
+            <text>🗑 删除</text>
+          </view>
+        </view>
+      </view>
+      <StatusTip tip="暂未绑定车辆" />
+    </view>
+    <view class="mt-100rpx">
+      <view class="text-32rpx font-bold">
+        规则说明
+      </view>
+      <view class="mt-16rpx text-28rpx text-#666666">
+        <view class="mt-10rpx">
+          1.绑定车牌后,将按订单充电时长+30分钟离场时间进行减免停车费;(例如:充电时长60分钟,系统自动延长30分钟离场时间,即离场时减免90分钟停车费)
+        </view>
+        <view class="mt-10rpx">
+          2.绑定多个车牌时,请在充电开始前,确认充电车辆已设为当前默认充电车辆后,再开始充电,否则无法进行减免停车费。
+        </view>
+        <view class="mt-10rpx">
+          3.车牌绑定错误或默认车牌未对应现场充电车辆导致无法减免停车费,因此产生的一切损失与本平台无关。
+        </view>
+      </view>
+    </view>
+    <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]"
+      @click="router.push({ name: 'charge-add-plate' })"
+    >
+      添加车辆
+    </view>
+  </view>
+</template>
+
+<style lang="scss" scoped></style>

+ 392 - 0
src/subPack-charge/components/plate/index.vue

@@ -0,0 +1,392 @@
+<script setup lang="ts">
+import { computed, ref, watch } from 'vue'
+
+// Props 定义
+const props = defineProps({
+  plateNumber: {
+    type: Array as () => (string | number)[],
+    default: () => Array.from({ length: 8 }, () => ''),
+  },
+})
+
+// Emits 定义
+const emit = defineEmits<{
+  myPlateChange: [plateNumber: (string | number)[]]
+}>()
+
+// 响应式数据
+const show = ref(false)
+const index = ref(-1)
+const areaDatas: string[] = [
+  '京',
+  '津',
+  '渝',
+  '沪',
+  '冀',
+  '晋',
+  '辽',
+  '吉',
+  '黑',
+  '苏',
+  '浙',
+  '皖',
+  '闽',
+  '赣',
+  '鲁',
+  '豫',
+  '鄂',
+  '湘',
+  '粤',
+  '琼',
+  '川',
+  '贵',
+  '云',
+  '陕',
+  '甘',
+  '青',
+  '蒙',
+  '桂',
+  '宁',
+  '新',
+  '藏',
+  '使',
+  '领',
+  '',
+  '',
+  '',
+  '',
+  '',
+  '',
+]
+const characterDatas: (number | string)[] = [
+  0,
+  1,
+  2,
+  3,
+  4,
+  5,
+  6,
+  7,
+  8,
+  9,
+  'A',
+  'B',
+  'C',
+  'D',
+  'E',
+  'F',
+  'G',
+  'H',
+  'J',
+  'K',
+  'L',
+  'M',
+  'N',
+  'P',
+  'Q',
+  'R',
+  'S',
+  'T',
+  'U',
+  'V',
+  'W',
+  'X',
+  'Y',
+  'Z',
+  '挂',
+  '警',
+  '学',
+  '港',
+  '澳',
+]
+
+// 内部车牌数组(用于本地操作)
+const localPlateNumber = ref<(string | number)[]>([...props.plateNumber])
+
+// 计算属性
+const currentDatas = computed(() => {
+  return index.value === 0 ? areaDatas : characterDatas
+})
+
+// 方法
+function handleChange(idx: number) {
+  index.value = idx
+  show.value = true
+}
+
+function handleClickKeyBoard(item: string | number, idx: number) {
+  // 索引为1时不能选择数字(0-9)
+  if (index.value === 1 && idx < 10)
+    return
+  // 索引2-5时不能选择特殊字符(索引>33)
+  if (index.value > 1 && index.value < 6 && idx > 33)
+    return
+
+  if (index.value < 8) {
+    localPlateNumber.value[index.value] = item
+    emit('myPlateChange', [...localPlateNumber.value])
+  }
+  if (index.value < 7) {
+    index.value++
+  }
+}
+
+// 重置
+function handleReset() {
+  index.value = 0
+  for (let i = 0; i < 8; i++) {
+    localPlateNumber.value[i] = ''
+  }
+  emit('myPlateChange', [...localPlateNumber.value])
+}
+
+// 删除
+function handleDelete() {
+  localPlateNumber.value[index.value] = ''
+  emit('myPlateChange', [...localPlateNumber.value])
+  if (index.value > 0) {
+    index.value--
+  }
+}
+
+// 获取完整车牌号字符串
+function getPlateString() {
+  let plate = ''
+  localPlateNumber.value.forEach((item, idx) => {
+    if (idx === 1) {
+      plate = `${plate}${item}·`
+    }
+    else {
+      plate += item
+    }
+  })
+  return plate
+}
+
+// 车牌号格式校验
+function validatePlateNumber() {
+  const plateNumber = getPlateString().replace(/·/g, '')
+
+  if (!plateNumber) {
+    return { valid: false, message: '请输入车牌号' }
+  }
+
+  // 长度校验:普通车牌7位,新能源8位
+  if (plateNumber.length < 7) {
+    return { valid: false, message: '车牌号长度不足,请输入完整车牌号' }
+  }
+  if (plateNumber.length > 8) {
+    return { valid: false, message: '车牌号长度超出限制' }
+  }
+
+  // 第一位必须是省份简称
+  const firstChar = plateNumber.charAt(0)
+  if (!areaDatas.includes(firstChar)) {
+    return { valid: false, message: '车牌号首位必须是省份简称' }
+  }
+
+  // 第二位必须是大写字母
+  const secondChar = plateNumber.charAt(1)
+  if (!/^[A-Z]$/.test(secondChar)) {
+    return { valid: false, message: '车牌号第二位必须是字母' }
+  }
+
+  // 普通车牌正则:省份+字母+5位字母数字(含特殊车牌字符)
+  const normalPlateReg = /^[\u4E00-\u9FA5][A-Z][A-Z0-9]{4}[A-Z0-9挂港澳使领警]$/
+  // 新能源车牌正则:省份+字母+6位字母数字
+  const newEnergyPlateReg = /^[\u4E00-\u9FA5][A-Z][A-Z0-9]{6}$/
+
+  if (!normalPlateReg.test(plateNumber) && !newEnergyPlateReg.test(plateNumber)) {
+    return { valid: false, message: '车牌号格式不正确' }
+  }
+
+  return { valid: true, message: '' }
+}
+
+// 暴露方法给父组件
+defineExpose({
+  validatePlateNumber,
+  getPlateString,
+  reset: handleReset,
+})
+
+// 监听 props 变化同步到本地
+watch(() => props.plateNumber, (newVal) => {
+  localPlateNumber.value = [...newVal]
+}, { deep: true })
+</script>
+
+<template>
+  <view>
+    <view class="plate" :class="{ show }">
+      <view class="item" :class="{ active: index === 0 }" @click="handleChange(0)">
+        {{ localPlateNumber[0] }}
+        <text class="triangle" />
+      </view>
+      <view class="item" :class="{ active: index === 1 }" @click="handleChange(1)">
+        {{ localPlateNumber[1] }}
+      </view>
+      <view class="point">
+        ●
+      </view>
+      <view class="item" :class="{ active: index === 2 }" @click="handleChange(2)">
+        {{ localPlateNumber[2] }}
+      </view>
+      <view class="item" :class="{ active: index === 3 }" @click="handleChange(3)">
+        {{ localPlateNumber[3] }}
+      </view>
+      <view class="item" :class="{ active: index === 4 }" @click="handleChange(4)">
+        {{ localPlateNumber[4] }}
+      </view>
+      <view class="item" :class="{ active: index === 5 }" @click="handleChange(5)">
+        {{ localPlateNumber[5] }}
+      </view>
+      <view class="item" :class="{ active: index === 6 }" @click="handleChange(6)">
+        {{ localPlateNumber[6] }}
+      </view>
+      <view class="item new-energy" :class="{ active: index === 7 }" @click="handleChange(7)">
+        <view v-if="localPlateNumber[7] || localPlateNumber[7] === 0">
+          <text>{{ localPlateNumber[7] }}</text>
+        </view>
+        <wd-icon v-else name="add" size="13px" color="#9ED605" />
+      </view>
+    </view>
+    <section class="panel" :class="{ show }">
+      <view class="header">
+        <view @click="handleReset">
+          重置
+        </view>
+        <view @click="show = false">
+          完成
+        </view>
+      </view>
+      <view class="panelList">
+        <view v-for="(item, idx) of currentDatas" :key="idx" class="item">
+          <view
+            v-if="item !== ''"
+            :class="{ disabled: (index === 1 && idx < 10) || (index > 1 && index < 6 && idx > 33) }"
+            @click="handleClickKeyBoard(item, idx)"
+          >
+            {{ item }}
+          </view>
+        </view>
+        <view class="item backspace" :class="{ special: index === 0 }" @click="handleDelete">
+          ×
+        </view>
+      </view>
+    </section>
+  </view>
+</template>
+
+<style scoped lang='less'>
+    .plate {
+        display: flex;
+        justify-content: space-between;
+        .item {
+            width: 64rpx;
+            height: 80rpx;
+            background-color: #F3F4F7;
+            border-radius: 8rpx;
+            text-align: center;
+            line-height: 80rpx;
+            font-size: 32rpx;
+            color: rgba(0,0,0,0.90);
+            font-weight: bold;
+            position: relative;
+            &.active {
+                background-color: #bbbbbb;
+            }
+        }
+        .new-energy {
+            box-sizing: border-box;
+            border: 2rpx dashed #9ED605;
+            font-weight: bold;
+            uni-icons {
+                display: flex;
+                align-items: center;
+                justify-content: center;
+            }
+        }
+        .point {
+            height: 80rpx;
+            text-align: center;
+            line-height: 80rpx;
+            color: #BDC4CC;
+            font-size: 18rpx;
+        }
+        .triangle {
+            width: 0;
+            height: 0;
+            border: 6rpx solid transparent;
+            border-right-color: #00C69D;
+            border-bottom-color: #00C69D;
+            border-radius: 1rpx 2rpx 1rpx;
+            position: absolute;
+            right: 6rpx;
+            bottom: 6rpx;
+        }
+    }
+    .panel {
+        position: fixed;
+        left: 0;
+        width: 100%;
+        bottom: 0;
+        z-index: 999;
+        box-sizing: border-box;
+        background-color: #F5F5F5;
+        transition: all 0.3s ease;
+        transform: translateY(100%);
+        &.show {
+            transform: translateX(0);
+        }
+        .header {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            padding: 0 24rpx;
+            height: 96rpx;
+            color: #9ED605;
+            font-size: 34rpx;
+        }
+        .panelList {
+            padding: 0 19rpx 20rpx;
+            .item {
+                display: inline-block;
+                width: calc(~"(100% - 72rpx) / 10");
+                height: 84rpx;
+                margin-right: 8rpx;
+                margin-bottom: 8rpx;
+                vertical-align: top;
+                view {
+                    width: 100%;
+                    height: 84rpx;
+                    line-height: 84rpx;
+                    border-radius: 6rpx;
+                    background: #FEFFFE;
+                    font-size: 32rpx;
+                    color: rgba(0,0,0,0.90);
+                    font-weight: bold;
+                    text-align: center;
+                    &.disabled {
+                        background-color: rgba(254, 255, 254, 0.6);
+                        color: rgba(0, 0, 0, 0.23);
+                    }
+                }
+                &:nth-of-type(10n) {
+                    margin-right: 0;
+                }
+            }
+            .backspace {
+                vertical-align: top;
+                font-size: 48rpx;
+                font-weight: bold;
+                text-align: center;
+                height: 84rpx;
+                line-height: 84rpx;
+                border-radius: 6rpx;
+                background: #FEFFFE;
+                color: rgba(0,0,0,0.90);
+            }
+        }
+    }
+</style>

+ 18 - 3
src/subPack-charge/index/index.vue

@@ -228,9 +228,24 @@ function refund() {
           </view>
         </view>
       </view>
-      <!-- <view class="mt-24rpx">
-        <wd-swiper :list="swiperList" :height="100" :indicator="false" value-key="advertImg" />
-      </view> -->
+      <view class="mt-24rpx flex items-center justify-between rounded-16rpx bg-#FFF p-20rpx">
+        <view class="text-32rpx font-bold">
+          我的车辆
+        </view>
+        <view class="flex items-center gap-10rpx" @click="router.push({ name: 'charge-plate-list' })">
+          <view class="text-26rpx">
+            添加车辆,享更多权益
+          </view>
+          <view class="h-50rpx w-50rpx rounded-50% bg-[linear-gradient(90deg,#DBFC81_0%,#9ED605_100%)] text-center text-30rpx text-#FFF font-bold line-height-[50rpx]">
+            +
+          </view>
+        </view>
+        <!-- <view class="flex items-center gap-50rpx text-26rpx text-#9ED605">
+          <view>贵AV88888</view>
+          <view>管理>></view>
+        </view> -->
+        <!-- <wd-swiper :list="swiperList" :height="100" :indicator="false" value-key="advertImg" /> -->
+      </view>
       <view class="mt-24rpx flex items-center gap-20rpx">
         <view
           v-for="option in filterOptions"

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

@@ -35,12 +35,14 @@ interface NavigateToOptions {
        "/subPack-film/order-detail/index" |
        "/subPack-film/select-time/index" |
        "/subPack-film/submit-order/index" |
+       "/subPack-charge/chargeAddPlate/chargeAddPlate" |
        "/subPack-charge/chargeBuyaTicketList/chargeBuyaTicketList" |
        "/subPack-charge/chargeDetail/chargeDetail" |
        "/subPack-charge/chargeing/chargeing" |
        "/subPack-charge/chargeMap/chargeMap" |
        "/subPack-charge/chargeOrderDetail/chargeOrderDetail" |
        "/subPack-charge/chargeOrderList/chargeOrderList" |
+       "/subPack-charge/chargePlateList/chargePlateList" |
        "/subPack-charge/chargeSearchList/chargeSearchList" |
        "/subPack-charge/chargeSiteDetail/chargeSiteDetail" |
        "/subPack-charge/chargeStart/chargeStart" |
@@ -59,7 +61,8 @@ interface NavigateToOptions {
        "/subPack-djk/welfare/index" |
        "/subPack-refueling/commonTab/index" |
        "/subPack-refueling/orderDetaile/index" |
-       "/subPack-refueling/webView/index";
+       "/subPack-refueling/webView/index" |
+       "/subPack-attractions/commonTab/index";
 }
 interface RedirectToOptions extends NavigateToOptions {}
 

+ 1 - 0
vite.config.ts

@@ -34,6 +34,7 @@ export default async () => {
           'subPack-videoRights',
           'subPack-djk',
           'subPack-refueling',
+          'subPack-attractions',
         ],
         /**
          * 排除的页面,相对于 dir 和 subPackages