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

feat: 预约时间选择改为滚轮样式

chips网格改为picker-view两列滚轮(日期+时间),时间行内显示该时段电价;打开时定位到最早可选时间(已预约则定位当前预约时间);切换日期自动钳制时间索引;保留24小时窗口与底部确认条
skyline преди 3 седмици
родител
ревизия
1d075a7ee5
променени са 1 файла, в които са добавени 65 реда и са изтрити 98 реда
  1. 65 98
      charge-front/src/pages-charge/appointment/appointment.vue

+ 65 - 98
charge-front/src/pages-charge/appointment/appointment.vue

@@ -271,30 +271,32 @@
     >
       <view class="time-picker">
         <view class="fs-22 color-999 tp-tip">仅可选择从现在起24小时内的时间</view>
-        <!-- 时段网格(30分钟粒度) -->
-        <scroll-view scroll-y class="tp-grid_scroll">
-          <block v-for="(group, gi) in timeGroups" :key="gi">
-            <view class="tp-group_title fs-26 color-999">{{ group.title }}</view>
-            <view class="tp-grid">
-              <view
-                  v-for="opt in group.options"
-                  :key="opt.ts"
-                  class="tp-cell pressable"
-                  hover-class="pressed"
-                  hover-stay-time="80"
-                  :class="{
-                  'tp-cell_active': selectedTs === opt.ts,
-                  'tp-cell_disabled': opt.disabled,
-                }"
-                  @click="pickOption(opt)"
-              >
-                <view class="fs-28">{{ opt.timeLabel }}</view>
-                <view class="fs-20 tp-cell_price" v-if="opt.price">{{ opt.price }}元/度</view>
-              </view>
+        <!-- 滚动选择:日期 + 时间 -->
+        <picker-view
+            class="tp-picker"
+            indicator-style="height: 88rpx;"
+            mask-style="background-size: 100% 176rpx; background-position: top, bottom; background-image: linear-gradient(180deg, rgba(244,247,246,0.9), rgba(244,247,246,0.4)), linear-gradient(0deg, rgba(244,247,246,0.9), rgba(244,247,246,0.4));"
+            :value="pickerIndex"
+            @change="onPickerChange"
+        >
+          <picker-view-column>
+            <view
+                v-for="(g, i) in timeGroups"
+                :key="i"
+                class="tp-picker_item fs-28"
+            >{{ g.title }}</view>
+          </picker-view-column>
+          <picker-view-column>
+            <view
+                v-for="(t, i) in currentTimeOptions"
+                :key="i"
+                class="tp-picker_item"
+            >
+              <text class="fs-32">{{ t.timeLabel }}</text>
+              <text class="fs-20 color-999 ml-8" v-if="t.price">{{ t.price }}元/度</text>
             </view>
-          </block>
-          <view style="height: 24rpx"></view>
-        </scroll-view>
+          </picker-view-column>
+        </picker-view>
         <!-- 底部确认 -->
         <view class="tp-footer">
           <view class="flex-column flex-grow" v-if="selected">
@@ -441,9 +443,27 @@ const WINDOW = 24 * 60 * 60 * 1000; // 只能选择从此时开始24小时内
 
 const timesInfo = ref<any>({
   groups: [] as any[],
-  flat: {} as Record<number, any>,
 });
-const selectedTs = ref(0);
+
+const timeGroups = computed(() => timesInfo.value.groups);
+/** 滚轮选中位置:[日期列索引, 时间列索引] */
+const pickerIndex = ref([0, 0]);
+const currentTimeOptions = computed(
+    () => timesInfo.value.groups[pickerIndex.value[0]]?.options || []
+);
+const selected = computed(
+    () => currentTimeOptions.value[pickerIndex.value[1]]
+);
+
+const onPickerChange = (e: any) => {
+  let [d, t] = e.detail.value;
+  // 切换日期时,钳制时间索引到该日期的可用范围
+  if (d !== pickerIndex.value[0]) {
+    const len = timesInfo.value.groups[d]?.options.length || 1;
+    t = Math.min(t, len - 1);
+  }
+  pickerIndex.value = [d, t];
+};
 
 const pad2 = (n: number) => (n < 10 ? `0${n}` : `${n}`);
 
@@ -454,7 +474,6 @@ const buildOption = (ts: number, now: number): any => {
     ts,
     timeLabel: hourStr,
     price: getHourPrice(`${pad2(d.getHours())}`),
-    disabled: ts <= now,
     get fullLabel() {
       const dayDiff = Math.floor(
           (new Date(ts).setHours(0, 0, 0, 0) - new Date(now).setHours(0, 0, 0, 0)) / DAY
@@ -468,11 +487,10 @@ const buildOption = (ts: number, now: number): any => {
 /** 生成24小时内的可选项:今天/明天两组,30分钟粒度 */
 const buildTimeOptions = () => {
   const now = Date.now();
-  // 网格首个时间:10分钟后取整到下一个半点
-  let first = Math.ceil((now + 10 * 60 * 1000) / STEP) * STEP;
+  // 首个可选时间:10分钟后取整到下一个半点
+  const first = Math.ceil((now + 10 * 60 * 1000) / STEP) * STEP;
   const last = now + WINDOW; // 窗口上限
   const groups: any[] = [];
-  const flat: Record<number, any> = {};
 
   // 按天分组
   const firstDay = new Date(first).setHours(0, 0, 0, 0);
@@ -487,9 +505,7 @@ const buildTimeOptions = () => {
       if (d.getMinutes() !== 0 && d.getMinutes() !== 30) {
         continue;
       }
-      const opt = buildOption(ts, now);
-      options.push(opt);
-      flat[opt.ts] = opt;
+      options.push(buildOption(ts, now));
     }
     if (!options.length) {
       continue;
@@ -500,21 +516,7 @@ const buildTimeOptions = () => {
       options,
     });
   }
-  timesInfo.value = { groups, flat };
-};
-
-const selected = computed(() => timesInfo.value.flat[selectedTs.value]);
-const timeGroups = computed(() => timesInfo.value.groups);
-
-const pickOption = (opt: any) => {
-  if (opt.disabled) {
-    uni.showToast({
-      title: "无法选择过去的时间",
-      icon: "none",
-    });
-    return;
-  }
-  selectedTs.value = opt.ts;
+  timesInfo.value = { groups };
 };
 
 const selectTime = () => {
@@ -533,16 +535,23 @@ const selectTime = () => {
     return;
   }
   buildTimeOptions();
-  // 已预约修改时间时回显当前预约时间;新预约由用户自主选择,不预选
+  // 已预约修改时间时滚轮定位到当前预约时间;新预约定位到最早可选时间
+  let dayIdx = 0;
+  let timeIdx = 0;
   if (appointmentData.value) {
     const start = new Date(
         appointmentData.value.startTime.replace(/-/g, "/")
     ).getTime();
     const aligned = Math.ceil(start / STEP) * STEP;
-    selectedTs.value = timesInfo.value.flat[aligned] ? aligned : 0;
-  } else {
-    selectedTs.value = 0;
+    timesInfo.value.groups.forEach((g: any, gi: number) => {
+      const idx = g.options.findIndex((o: any) => o.ts === aligned);
+      if (idx >= 0) {
+        dayIdx = gi;
+        timeIdx = idx;
+      }
+    });
   }
+  pickerIndex.value = [dayIdx, timeIdx];
   chargeTimeDialog.value = true;
 };
 
@@ -995,58 +1004,16 @@ onShow(() => {
   padding: 20rpx 30rpx 0;
 }
 
-.tp-grid_scroll {
-  height: 560rpx;
-  padding: 8rpx 30rpx 0;
-  box-sizing: border-box;
+.tp-picker {
+  height: 440rpx;
+  margin-top: 8rpx;
 }
 
-.tp-group_title {
-  padding: 24rpx 0 16rpx;
-  font-weight: 500;
-}
-
-.tp-grid {
-  display: grid;
-  grid-template-columns: repeat(4, 1fr);
-  gap: 12rpx;
-}
-
-.tp-cell {
+.tp-picker_item {
   display: flex;
-  flex-direction: column;
   align-items: center;
   justify-content: center;
-  height: 96rpx;
-  background: var(--color-sec);
-  border-radius: 16rpx;
-  color: #1c2b29;
-  transition: background-color 0.15s cubic-bezier(0.22, 1, 0.36, 1),
-    color 0.15s cubic-bezier(0.22, 1, 0.36, 1);
-}
-
-.tp-cell_price {
-  margin-top: 2rpx;
-  color: #8c9997;
-}
-
-.tp-cell_active {
-  background: var(--color-primary-tint-strong);
-  color: var(--color-primary);
-  font-weight: 600;
-
-  .tp-cell_price {
-    color: var(--color-primary);
-  }
-}
-
-.tp-cell_disabled {
-  color: #c4cfcd;
-  background: rgba(28, 43, 41, 0.03);
-
-  .tp-cell_price {
-    color: #d5dedc;
-  }
+  height: 88rpx;
 }
 
 .tp-footer {