Преглед на файлове

fix: 修复车辆删除无效及输入法反复调起问题

用单隐藏输入框替代多个 input,焦点不切换避免键盘反复收起重开;
退格键可连续操作。后端 deleteCar/setDefault 改用 updateById
替代 lambdaUpdate.set,修复 Boolean 字段更新无效导致软删除失败。
新增车牌重复添加提示,save 接口返回 isNew 标识前端区分提示。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline преди 1 ден
родител
ревизия
56222384b8

+ 2 - 2
car-wash-miniapp/src/main/java/com/kym/miniapp/controller/CarsController.java

@@ -35,8 +35,8 @@ public class CarsController {
     public R<?> save(@RequestBody Map<String, String> body) {
         var userId = StpUtil.getLoginIdAsLong();
         var plateNo = body.get("plateNo");
-        carsService.saveCar(userId, plateNo);
-        return R.success();
+        boolean isNew = carsService.saveCar(userId, plateNo);
+        return R.success(Map.of("isNew", isNew));
     }
 
     @PostMapping("/setDefault/{id}")

+ 102 - 128
car-wash-mp/src/pages-user/profile/index.vue

@@ -2,77 +2,63 @@
   <view class="plate-page">
     <uv-navbar title="我的车辆" bgColor="#C6171E" leftIconColor="#FFFFFF" :titleStyle="{ color: '#FFFFFF' }" :autoBack="true" :placeholder="true"></uv-navbar>
 
-    <!-- 车牌输入区域 -->
     <view class="plate-input-section">
       <view class="section-header">
         <text class="section-title">绑定新车牌</text>
       </view>
-
       <view class="plate-input-hint">
         <text>添加车牌后部分站点可享停车减免权益</text>
       </view>
 
       <!-- 车牌输入框 -->
       <view class="plate-input-container">
-        <input class="plate-item plate-region"
-               v-model="plateChars[0]"
-               maxlength="1"
-               type="text"
-               inputmode="none"
-               readonly
-               @click="toggleRegionPicker"
-               @touchstart.prevent="toggleRegionPicker"/>
-
-        <input class="plate-item"
-               v-model="plateChars[1]"
-               :focus="focusIndex === 1"
-               maxlength="1"
-               type="text"
-               inputmode="text"
-               @input="onPlateInput(1)"
-               @focus="onPlateFocus(1)"
-               @blur="onPlateBlur"/>
-
-        <view class="plate-divider">
-          <text>·</text>
+        <!-- 省份 -->
+        <view class="plate-item plate-region"
+              :class="{ 'plate-focus': cursorSlot === 0 }"
+              @click="toggleRegionPicker">
+          <text>{{ plateChars[0] || '' }}</text>
         </view>
 
-        <template v-for="(char, index) in plateChars.slice(2)" :key="index + 2">
-          <!-- 最后一位:带闪电图标的新能源标识位 -->
-          <view v-if="index === 5"
-                class="plate-item new-energy"
-                :class="{ 'plate-focus': focusIndex === (index + 2) }">
-            <input class="energy-input"
-                   :focus="focusIndex === (index + 2)"
-                   v-model="plateChars[index + 2]"
-                   maxlength="1"
-                   type="text"
-                   inputmode="text"
-                   @input="onPlateInput(index + 2)"
-                   @focus="onPlateFocus(index + 2)"
-                   @blur="onPlateBlur"/>
-            <image v-if="!plateChars[index + 2]" class="energy-icon-inside" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzUyQzQxQSI+PHBhdGggZD0iTTEzIDJMMyAxNGg2bC0xIDggMTAtMTJoLTZsMS04eiIvPjwvc3ZnPg==" mode="aspectFit" />
-          </view>
-          <!-- 前5位普通输入 -->
-          <input v-else
-                 class="plate-item"
-                 :class="{ 'plate-focus': focusIndex === (index + 2) }"
-                 :focus="focusIndex === (index + 2)"
-                 v-model="plateChars[index + 2]"
-                 maxlength="1"
-                 placeholder-class="placeholder"
-                 type="text"
-                 inputmode="text"
-                 @input="onPlateInput(index + 2)"
-                 @focus="onPlateFocus(index + 2)"
-                 @blur="onPlateBlur"/>
-        </template>
+        <!-- 城市 + 5位字母数字 = 6个字符区 + 分隔符 + 闪电位 -->
+        <view class="plate-item"
+              :class="{ 'plate-focus': cursorSlot === 1 }"
+              @click="focusSlot(1)">
+          <text>{{ plateChars[1] || '' }}</text>
+        </view>
+
+        <view class="plate-divider"><text>·</text></view>
+
+        <view v-for="i in 5" :key="i"
+              class="plate-item"
+              :class="{ 'plate-focus': cursorSlot === (i + 1) }"
+              @click="focusSlot(i + 1)">
+          <text>{{ plateChars[i + 1] || '' }}</text>
+        </view>
+
+        <!-- 第8位:新能源闪电标识位 -->
+        <view class="plate-item new-energy"
+              :class="{ 'plate-focus': cursorSlot === 7 }"
+              @click="focusSlot(7)">
+          <text v-if="plateChars[7]" class="energy-char">{{ plateChars[7] }}</text>
+          <image v-else class="energy-icon-inside" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzUyQzQxQSI+PHBhdGggZD0iTTEzIDJMMyAxNGg2bC0xIDggMTAtMTJoLTZsMS04eiIvPjwvc3ZnPg==" mode="aspectFit" />
+        </view>
       </view>
 
+      <!-- 隐藏输入框:统一接管键盘输入 -->
+      <input class="ghost-input"
+             v-model="restStr"
+             :focus="inputFocused"
+             :maxlength="7"
+             adjust-position
+             @input="onRestInput"
+             @focus="onRestFocus"
+             @blur="onRestBlur"/>
+
       <view class="save-btn-container">
         <uv-button type="primary" color="#C6171E" shape="circle" :disabled="!isPlateValid" @click="savePlate">保存</uv-button>
       </view>
 
+      <!-- 地区选择器 -->
       <view v-if="showRegionPicker" class="region-picker-mask" @click="toggleRegionPicker">
         <view class="region-picker-container" @click.stop>
           <view class="region-picker-header">
@@ -111,18 +97,12 @@
           <view class="car-info">
             <view class="car-plate-row">
               <text class="car-plate">{{ car.plateNo }}</text>
-              <view v-if="car.isDefault" class="default-tag">
-                <text>默认</text>
-              </view>
+              <view v-if="car.isDefault" class="default-tag"><text>默认</text></view>
             </view>
           </view>
           <view class="car-actions">
-            <view v-if="!car.isDefault" class="action-btn set-default-btn" @click="handleSetDefault(car)">
-              <text>设为默认</text>
-            </view>
-            <view class="action-btn delete-btn" @click="handleDelete(car)">
-              <text>删除</text>
-            </view>
+            <view v-if="!car.isDefault" class="action-btn set-default-btn" @click="handleSetDefault(car)"><text>设为默认</text></view>
+            <view class="action-btn delete-btn" @click="handleDelete(car)"><text>删除</text></view>
           </view>
         </view>
       </view>
@@ -135,42 +115,47 @@ import {onLoad, onShow} from "@dcloudio/uni-app";
 import {ref, computed, nextTick} from "vue";
 import {body, get, del} from "@/utils/https";
 
-const plateChars = ref(['粤', 'B', '', '', '', '', '', '']);
-const focusIndex = ref(2);
+const plateChars = ref(['粤', '', '', '', '', '', '', '']);
+const restStr = ref('');
+const cursorSlot = ref(1);
+const inputFocused = ref(false);
 const showRegionPicker = ref(false);
 const carList = ref([]);
 
 const regions = ['京', '津', '渝', '沪', '冀', '晋', '辽', '吉', '黑', '苏', '浙', '皖', '闽', '赣', '鲁', '豫', '鄂', '湘', '粤', '琼', '川', '贵', '云', '陕', '甘', '青', '蒙', '桂', '宁', '新', '藏', '使', '领', '警', '学', '港', '澳'];
 
+const filledCount = computed(() => {
+  return plateChars.value.filter(c => c !== '').length;
+});
+
 const isPlateValid = computed(() => {
-  const validChars = plateChars.value.filter(char => char !== '');
-  return validChars.length >= 7;
+  return filledCount.value >= 7;
 });
 
-let autoAdvancing = false;
-
-const onPlateInput = (index) => {
-  plateChars.value[index] = plateChars.value[index].toUpperCase();
-  if (plateChars.value[index] !== '') {
-    if (index < 7) {
-      autoAdvancing = true;
-      focusIndex.value = -1;
-      nextTick(() => {
-        focusIndex.value = index + 1;
-        autoAdvancing = false;
-      });
-    }
+const syncPlateFromRest = (str) => {
+  const s = str || restStr.value;
+  for (let i = 1; i <= 7; i++) {
+    plateChars.value[i] = s[i - 1] || '';
   }
+  cursorSlot.value = Math.min(s.length + 1, 7);
 };
 
-const onPlateFocus = (index) => {
-  focusIndex.value = index;
+const onRestInput = () => {
+  restStr.value = restStr.value.toUpperCase();
+  syncPlateFromRest(restStr.value);
 };
 
-const onPlateBlur = () => {
-  if (!autoAdvancing) {
-    focusIndex.value = -1;
-  }
+const focusSlot = (slot) => {
+  cursorSlot.value = slot;
+  inputFocused.value = true;
+};
+
+const onRestFocus = () => {
+  inputFocused.value = true;
+};
+
+const onRestBlur = () => {
+  inputFocused.value = false;
 };
 
 const toggleRegionPicker = () => {
@@ -180,25 +165,30 @@ const toggleRegionPicker = () => {
 const selectRegion = (region) => {
   plateChars.value[0] = region;
   toggleRegionPicker();
+  cursorSlot.value = 1;
   nextTick(() => {
-    focusIndex.value = plateChars.value[1] ? 2 : 1;
+    inputFocused.value = true;
   });
 };
 
 const savePlate = () => {
-  const validChars = plateChars.value.filter(char => char !== '');
-  if (validChars.length < 7) {
+  const plateNo = plateChars.value[0] + restStr.value;
+  if (plateNo.length < 7) {
     uni.showToast({ title: '请输入完整车牌号', icon: 'none' });
     return;
   }
-  const newPlateNo = validChars.join('');
 
   uni.showLoading({ title: "保存中" });
-  body(`/cars/save`, { plateNo: newPlateNo }).then(() => {
+  body(`/cars/save`, { plateNo }).then((data) => {
     uni.hideLoading();
-    uni.showToast({ icon: "success", title: "保存成功" });
-    plateChars.value = ['粤', 'B', '', '', '', '', '', ''];
-    focusIndex.value = 2;
+    if (data && data.isNew === false) {
+      uni.showToast({ icon: "none", title: "车牌已存在,已设为默认" });
+    } else {
+      uni.showToast({ icon: "success", title: "保存成功" });
+    }
+    plateChars.value = ['粤', '', '', '', '', '', '', ''];
+    restStr.value = '';
+    cursorSlot.value = 1;
     fetchCarList();
   }).catch(() => {
     uni.hideLoading();
@@ -221,9 +211,7 @@ const handleSetDefault = (car) => {
     uni.hideLoading();
     uni.showToast({ icon: "success", title: "已设为默认" });
     fetchCarList();
-  }).catch(() => {
-    uni.hideLoading();
-  });
+  }).catch(() => uni.hideLoading());
 };
 
 const handleDelete = (car) => {
@@ -237,21 +225,14 @@ const handleDelete = (car) => {
           uni.hideLoading();
           uni.showToast({ icon: "success", title: "已删除" });
           fetchCarList();
-        }).catch(() => {
-          uni.hideLoading();
-        });
+        }).catch(() => uni.hideLoading());
       }
     }
   });
 };
 
-onLoad(() => {
-  fetchCarList();
-});
-
-onShow(() => {
-  fetchCarList();
-});
+onLoad(() => fetchCarList());
+onShow(() => fetchCarList());
 </script>
 
 <style lang="scss" scoped>
@@ -281,7 +262,6 @@ onShow(() => {
   color: $uni-text-color-hint;
 }
 
-// 车牌输入区
 .plate-input-section {
   background-color: $uni-bg-color-card;
   padding: 28rpx;
@@ -321,7 +301,6 @@ onShow(() => {
   color: $uni-text-color;
   background-color: $uni-bg-color-card;
   box-sizing: border-box;
-  text-align: center;
   transition: border-color 0.15s ease, box-shadow 0.15s ease;
 }
 
@@ -335,7 +314,6 @@ onShow(() => {
   border-color: #52C41A;
   background-color: rgba(82, 196, 26, 0.06);
   position: relative;
-  overflow: hidden;
 }
 
 .plate-item.new-energy.plate-focus {
@@ -343,13 +321,9 @@ onShow(() => {
   box-shadow: 0 0 0 4rpx rgba(82, 196, 26, 0.16);
 }
 
-.energy-input {
-  width: 100%;
-  height: 100%;
-  text-align: center;
-  font-size: 34rpx;
-  font-weight: 600;
-  color: $uni-text-color;
+.energy-char {
+  position: relative;
+  z-index: 1;
 }
 
 .energy-icon-inside {
@@ -360,19 +334,12 @@ onShow(() => {
   width: 48rpx;
   height: 48rpx;
   pointer-events: none;
-  z-index: 0;
 }
 
 .plate-region {
   background-color: $uni-bg-color-page;
 }
 
-.placeholder {
-  text-align: center;
-  font-size: 22rpx;
-  color: $uni-text-color-hint;
-}
-
 .plate-divider {
   width: 20rpx;
   height: 88rpx;
@@ -383,7 +350,16 @@ onShow(() => {
   color: $uni-text-color-hint;
 }
 
-// 保存按钮
+// Hidden input capturing all keyboard input
+.ghost-input {
+  position: fixed;
+  top: -200rpx;
+  left: -200rpx;
+  width: 1rpx;
+  height: 1rpx;
+  opacity: 0;
+}
+
 .save-btn-container {
   margin-top: 8rpx;
 }
@@ -399,7 +375,7 @@ onShow(() => {
   color: $uni-text-color-hint;
 }
 
-// 地区选择器
+// Region picker
 .region-picker-mask {
   position: fixed;
   top: 0;
@@ -477,7 +453,7 @@ onShow(() => {
   transform: scale(0.95);
 }
 
-// 车辆列表
+// Car list
 .car-list-section {
   background-color: $uni-bg-color-card;
   padding: 28rpx;
@@ -485,7 +461,6 @@ onShow(() => {
   border-radius: 24rpx;
 }
 
-// 空状态
 .empty-state {
   display: flex;
   flex-direction: column;
@@ -512,7 +487,6 @@ onShow(() => {
   color: $uni-text-color-hint;
 }
 
-// 车辆列表项
 .car-item {
   display: flex;
   align-items: center;

+ 1 - 1
car-wash-service/src/main/java/com/kym/service/CarsService.java

@@ -18,7 +18,7 @@ public interface CarsService extends MPJBaseService<Cars> {
 
     List<Map<String, Object>> listUserCars(Long userId);
 
-    void saveCar(Long userId, String plateNo);
+    boolean saveCar(Long userId, String plateNo);
 
     void setDefault(Long userId, Long carId);
 

+ 13 - 21
car-wash-service/src/main/java/com/kym/service/impl/CarsServiceImpl.java

@@ -39,16 +39,15 @@ public class CarsServiceImpl extends MPJBaseServiceImpl<CarsMapper, Cars> implem
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void saveCar(Long userId, String plateNo) {
+    public boolean saveCar(Long userId, String plateNo) {
         var existing = lambdaQuery()
                 .eq(Cars::getUserId, userId)
                 .eq(Cars::getPlateNo, plateNo)
                 .eq(Cars::getIsDelete, false)
                 .one();
         if (existing != null) {
-            // already exists, set as default
             setDefault(userId, existing.getId());
-            return;
+            return false;
         }
         long count = countUserCars(userId);
         var car = new Cars();
@@ -57,6 +56,7 @@ public class CarsServiceImpl extends MPJBaseServiceImpl<CarsMapper, Cars> implem
         car.setIsDelete(false);
         car.setIsDefault(count == 0);
         save(car);
+        return true;
     }
 
     private long countUserCars(Long userId) {
@@ -69,17 +69,15 @@ public class CarsServiceImpl extends MPJBaseServiceImpl<CarsMapper, Cars> implem
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void setDefault(Long userId, Long carId) {
-        // clear all defaults for this user
         lambdaUpdate()
                 .eq(Cars::getUserId, userId)
                 .set(Cars::getIsDefault, false)
                 .update();
-        // set the target as default
-        lambdaUpdate()
-                .eq(Cars::getId, carId)
-                .eq(Cars::getUserId, userId)
-                .set(Cars::getIsDefault, true)
-                .update();
+        var car = lambdaQuery().eq(Cars::getId, carId).one();
+        if (car != null) {
+            car.setIsDefault(true);
+            updateById(car);
+        }
     }
 
     @Override
@@ -91,13 +89,9 @@ public class CarsServiceImpl extends MPJBaseServiceImpl<CarsMapper, Cars> implem
                 .one();
         if (car == null) return;
         boolean wasDefault = car.getIsDefault() != null && car.getIsDefault();
-        // soft delete
-        lambdaUpdate()
-                .eq(Cars::getId, carId)
-                .set(Cars::getIsDelete, true)
-                .set(Cars::getIsDefault, false)
-                .update();
-        // if deleted car was default, assign a new default
+        car.setIsDelete(true);
+        car.setIsDefault(false);
+        updateById(car);
         if (wasDefault) {
             var first = lambdaQuery()
                     .eq(Cars::getUserId, userId)
@@ -106,10 +100,8 @@ public class CarsServiceImpl extends MPJBaseServiceImpl<CarsMapper, Cars> implem
                     .last("limit 1")
                     .one();
             if (first != null) {
-                lambdaUpdate()
-                        .eq(Cars::getId, first.getId())
-                        .set(Cars::getIsDefault, true)
-                        .update();
+                first.setIsDefault(true);
+                updateById(first);
             }
         }
     }