Parcourir la source

feat: 小程序新增洗车价格公示 — 站点详情和设备启停页面展示服务单价

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline il y a 1 jour
Parent
commit
d47dd7e36d

+ 36 - 0
car-wash-entity/src/main/java/com/kym/entity/vo/DevicePriceVo.java

@@ -0,0 +1,36 @@
+package com.kym.entity.vo;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+/**
+ * 设备价格公示 VO — 面向用户展示的洗车服务单价
+ *
+ * @author skyline
+ * @since 2026-06-20
+ */
+@Data
+@Accessors(chain = true)
+public class DevicePriceVo {
+
+    /** 场地费(分/分钟) */
+    private Integer priceSpace;
+    /** 清水(分/分钟) */
+    private Integer priceWater;
+    /** 泡沫(分/分钟) */
+    private Integer priceFoam;
+    /** 吸尘(分/分钟) */
+    private Integer priceCleaner;
+    /** 洗手(分/分钟) */
+    private Integer priceTap;
+    /** 扩展项目(分/分钟) */
+    private Integer priceUserExt;
+    /** 镀膜(分/分钟) */
+    private Integer priceCoat;
+    /** 吹气(分/分钟) */
+    private Integer priceBlow;
+    /** 最低消费(分) */
+    private Integer amountMinLimit;
+    /** 最高消费/预扣金额(分) */
+    private Integer prepayMoney;
+}

+ 5 - 0
car-wash-entity/src/main/java/com/kym/entity/vo/WashDeviceVo.java

@@ -106,4 +106,9 @@ public class WashDeviceVo extends BaseEntity {
      */
     private Integer sequence;
 
+    /**
+     * 价格公示信息
+     */
+    private DevicePriceVo price;
+
 }

+ 161 - 0
car-wash-mp/src/components/price-table/index.vue

@@ -0,0 +1,161 @@
+<template>
+  <view class="price-card" v-if="hasAnyPrice">
+    <view class="card-header">
+      <view class="header-dot"></view>
+      <text class="header-title">收费标准</text>
+    </view>
+
+    <view class="price-grid">
+      <view class="price-item" v-for="item in priceItems" :key="item.label">
+        <text class="price-label">{{ item.label }}</text>
+        <text class="price-value">{{ fmtYuanPerMin(item.value) }}</text>
+      </view>
+    </view>
+
+    <view class="price-footer" v-if="amountMinLimit != null || prepayMoney != null">
+      <view class="footer-item" v-if="amountMinLimit != null && amountMinLimit > 0">
+        <text class="footer-label">最低消费</text>
+        <text class="footer-value">¥{{ (amountMinLimit / 100).toFixed(2) }}</text>
+      </view>
+      <view class="footer-item" v-if="prepayMoney != null && prepayMoney > 0">
+        <text class="footer-label">单次上限</text>
+        <text class="footer-value">¥{{ (prepayMoney / 100).toFixed(2) }}</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts" name="PriceTable">
+import { computed } from "vue";
+
+const PRICE_LABELS: Record<string, string> = {
+  priceSpace: "场地费",
+  priceWater: "清水",
+  priceFoam: "泡沫",
+  priceCleaner: "吸尘",
+  priceTap: "洗手",
+  priceUserExt: "扩展项目",
+  priceCoat: "镀膜",
+  priceBlow: "吹气",
+};
+
+const props = defineProps({
+  price: {
+    type: Object,
+    default: null,
+  },
+});
+
+const amountMinLimit = computed(() => props.price?.amountMinLimit);
+const prepayMoney = computed(() => props.price?.prepayMoney);
+
+const hasAnyPrice = computed(() => {
+  if (!props.price) return false;
+  return Object.keys(PRICE_LABELS).some((key) => props.price[key] != null);
+});
+
+const priceItems = computed(() => {
+  if (!props.price) return [];
+  return Object.entries(PRICE_LABELS)
+    .filter(([key]) => props.price[key] != null)
+    .map(([key, label]) => ({ label, value: props.price[key] }));
+});
+
+const fmtYuanPerMin = (fen: number) => {
+  if (fen == null) return "";
+  const yuan = fen / 100;
+  return yuan % 1 === 0 ? `¥${yuan}/分钟` : `¥${yuan.toFixed(2)}/分钟`;
+};
+</script>
+
+<style lang="scss" scoped>
+.price-card {
+  margin: 0 30rpx 24rpx;
+  @include card-interactive(24rpx);
+  overflow: hidden;
+  cursor: default;
+
+  &:active {
+    transform: none;
+    box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
+  }
+
+  .card-header {
+    display: flex;
+    align-items: center;
+    gap: 12rpx;
+    padding: 28rpx 28rpx 0;
+
+    .header-dot {
+      width: 10rpx;
+      height: 10rpx;
+      background: $uni-color-primary;
+      border-radius: 50%;
+    }
+
+    .header-title {
+      font-size: 30rpx;
+      font-weight: $uni-font-weight-semibold;
+      color: $uni-text-color-dark;
+    }
+  }
+
+  .price-grid {
+    display: flex;
+    flex-wrap: wrap;
+    padding: 24rpx 28rpx 12rpx;
+
+    .price-item {
+      width: 50%;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      padding: 14rpx 0;
+      box-sizing: border-box;
+
+      &:nth-child(odd) {
+        padding-right: 20rpx;
+      }
+
+      &:nth-child(even) {
+        padding-left: 20rpx;
+      }
+
+      .price-label {
+        font-size: 26rpx;
+        color: $uni-text-color-hint;
+      }
+
+      .price-value {
+        font-size: 26rpx;
+        color: $uni-color-primary;
+        font-weight: $uni-font-weight-semibold;
+      }
+    }
+  }
+
+  .price-footer {
+    display: flex;
+    gap: 32rpx;
+    padding: 16rpx 28rpx 28rpx;
+    border-top: 1rpx solid $uni-border-color-light;
+
+    .footer-item {
+      display: flex;
+      align-items: center;
+      gap: 8rpx;
+
+      .footer-label {
+        font-size: 24rpx;
+        color: $uni-text-color-tertiary;
+      }
+
+      .footer-value {
+        font-size: 24rpx;
+        color: $uni-text-color-dark;
+        font-weight: $uni-font-weight-medium;
+      }
+    }
+  }
+}
+</style>

+ 4 - 0
car-wash-mp/src/pages-wash/device/index.vue

@@ -26,6 +26,9 @@
         </view>
       </view>
 
+      <!-- 价格公示 -->
+      <PriceTable :price="state.device?.price" />
+
       <!-- 停车费提示 -->
       <view class="parking-notice" v-if="state.parkingFee">
         <view class="parking-notice-left">
@@ -117,6 +120,7 @@ import {get, post} from "@/utils/https";
 import {checkLogin, fetchToken, tryLogin} from "@/utils/auth";
 import {fmtMoney} from "@/utils/common";
 import LoginBar from "@/components/login-bar/index.vue";
+import PriceTable from "@/components/price-table/index.vue";
 
 const guideSteps = [
   '点击上方【启动设备】按钮启动设备',

+ 13 - 0
car-wash-mp/src/pages-wash/station/index.vue

@@ -25,6 +25,9 @@
         <WashStation :item="state.station" ref="station_ref" :showPhone="true"></WashStation>
       </view>
 
+      <!-- 价格公示 -->
+      <PriceTable :price="unifiedPrice" />
+
       <!-- 设备列表 -->
       <view class="device-section">
         <view class="section-header">
@@ -117,6 +120,7 @@ import { onLoad } from "@dcloudio/uni-app";
 import { get } from "@/utils/https";
 import { fmtDictName } from "@/utils/common";
 import WashStation from "@/components/station/index.vue";
+import PriceTable from "@/components/price-table/index.vue";
 
 const station_ref = ref();
 const deviceLoading = ref(false);
@@ -136,6 +140,15 @@ const swiperImages = computed(() => {
   return pictures.split("|").filter(Boolean);
 });
 
+// 站点统一价格(去重后若全部设备共用一个配置则返回该价格,否则返回 null)
+const unifiedPrice = computed(() => {
+  const devices = state.deviceList;
+  if (!devices.length) return null;
+  const configIds = new Set(devices.map((d: any) => d.deviceConfigId).filter(Boolean));
+  if (configIds.size !== 1) return null;
+  return devices[0].price || null;
+});
+
 onLoad((options: any) => {
   const id = options?.id;
   const cached = getApp<any>().globalData.last?.station;

+ 45 - 0
car-wash-service/src/main/java/com/kym/service/impl/WashDeviceServiceImpl.java

@@ -9,6 +9,7 @@ import com.kym.entity.WashDevice;
 import com.kym.entity.WashOrder;
 import com.kym.entity.common.PageBean;
 import com.kym.entity.queryParams.DeviceQueryParams;
+import com.kym.entity.vo.DevicePriceVo;
 import com.kym.entity.vo.WashDeviceVo;
 import com.kym.mapper.WashDeviceMapper;
 import com.kym.service.DeviceConfigService;
@@ -70,6 +71,13 @@ public class WashDeviceServiceImpl extends MyBaseServiceImpl<WashDeviceMapper, W
                 .orderByAsc(WashDevice::getState)
                 .orderByAsc(WashDevice::getDeviceName)
                 .list();
+
+        if (list.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        var configMap = loadDeviceConfigMap(list);
+
         var voList = new ArrayList<WashDeviceVo>();
         //TODO 根据订单查询占用中的设备
 //        var currentUserId = StpUtil.getLoginIdAsLong();
@@ -78,6 +86,7 @@ public class WashDeviceServiceImpl extends MyBaseServiceImpl<WashDeviceMapper, W
 //            .setCurrentUserId(currentUserId);
             BeanUtils.copyProperties(washDevice, vo);
             vo.setShortId(KymCache.INSTANCE.getShortIdByProductKeyAndDeviceName(washDevice.getProductKey(), washDevice.getDeviceName()));
+            vo.setPrice(toDevicePriceVo(configMap.get(washDevice.getDeviceConfigId())));
             voList.add(vo);
         }
         return voList;
@@ -109,6 +118,11 @@ public class WashDeviceServiceImpl extends MyBaseServiceImpl<WashDeviceMapper, W
 
             var vo = new WashDeviceVo().setCurrentUserId(StpUtil.getLoginIdAsLong());
             BeanUtils.copyProperties(washDevice, vo.setShortId(shortId));
+
+            if (washDevice != null && washDevice.getDeviceConfigId() != null) {
+                var config = deviceConfigService.getById(washDevice.getDeviceConfigId());
+                vo.setPrice(toDevicePriceVo(config));
+            }
             return vo;
         } else {
             throw new BusinessException("设备他人使用中!");
@@ -248,4 +262,35 @@ public class WashDeviceServiceImpl extends MyBaseServiceImpl<WashDeviceMapper, W
 
     //endregion
 
+    //region 价格公示
+
+    private java.util.Map<Long, DeviceConfig> loadDeviceConfigMap(List<WashDevice> devices) {
+        var configIds = devices.stream().map(WashDevice::getDeviceConfigId).filter(id -> id != null).toList();
+        if (configIds.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        return deviceConfigService.lambdaQuery()
+                .in(DeviceConfig::getId, configIds)
+                .list().stream().collect(Collectors.toMap(DeviceConfig::getId, Function.identity()));
+    }
+
+    private DevicePriceVo toDevicePriceVo(DeviceConfig config) {
+        if (config == null) {
+            return null;
+        }
+        return new DevicePriceVo()
+                .setPriceSpace(config.getPriceSpace())
+                .setPriceWater(config.getPriceWater())
+                .setPriceFoam(config.getPriceFoam())
+                .setPriceCleaner(config.getPriceCleaner())
+                .setPriceTap(config.getPriceTap())
+                .setPriceUserExt(config.getPriceUserExt())
+                .setPriceCoat(config.getPriceCoat())
+                .setPriceBlow(config.getPriceBlow())
+                .setAmountMinLimit(config.getAmountMinLimit())
+                .setPrepayMoney(config.getPrepayMoney());
+    }
+
+    //endregion
+
 }