Sfoglia il codice sorgente

用户端小程序页面优化

skyline 1 settimana fa
parent
commit
bf863f7d55

+ 75 - 66
car-wash-mp/src/components/custom-service/index.vue

@@ -1,70 +1,71 @@
 <template>
-  <!-- 全屏拖拽热区 -->
   <movable-area class="movable-area">
     <movable-view
-        class="movable-view customer-service"
-        direction="all"
-        :x="pos.x"
-        :y="pos.y"
-        @change="onDrag"  >
-
-      <!-- 客服图标 -->
-      <button open-type="contact" @contact="handleContact" class="contact" send-message-title="YesWash洗车客服">
-        在线客服
-      </button>
-      <image src="/static/iconfont/cs.svg" mode="widthFix"/>
+      class="movable-view"
+      direction="all"
+      :x="snapPos.x"
+      :y="snapPos.y"
+      :animation="true"
+      @change="onDrag"
+      @touchend="onTouchEnd"
+    >
+      <view class="cs-button">
+        <uv-icon name="kefu-ermai" color="#FFFFFF" size="24"></uv-icon>
+      </view>
+      <button class="contact-btn" open-type="contact" @contact="handleContact" send-message-title="YesWash洗车客服" />
     </movable-view>
   </movable-area>
 </template>
 
-<script setup>
-import {ref} from 'vue'
-import {onLoad} from "@dcloudio/uni-app";
-import {rpxToPx} from "@/utils/device";
+<script setup lang="ts">
+import { ref } from 'vue'
 
-/* 位置缓存 key */
 const POS_KEY = 'kefuPos'
 
-/* 响应式坐标 */
-const pos = ref({x: 600, y: 800})
+/* 实时拖拽位置(非响应式,避免与原生拖拽冲突) */
+let dragX = 600
+let dragY = 800
 
-/* 读取上次位置 */
+/* 初始位置从缓存读取 */
 try {
   const last = uni.getStorageSync(POS_KEY)
-  if (last) pos.value = last
-} catch {
-}
+  if (last) {
+    dragX = last.x
+    dragY = last.y
+  }
+} catch {}
 
-/* 拖拽结束实时缓存 */
-function onDrag(e) {
-  const {x, y} = e.detail
-  pos.value = {x, y}
-  uni.setStorageSync(POS_KEY, pos.value)
-}
+const snapPos = ref({ x: dragX, y: dragY })
+let windowInfo: any = null
 
-/* 点击客服 */
-function goService() {
-  uni.navigateTo({url: '/pages/service/service'})
-  // 或打开微信客服会话
-  // uni.openCustomerServiceConversation({ ... })
+function getWindowInfo() {
+  if (!windowInfo) {
+    windowInfo = uni.getWindowInfo()
+  }
+  return windowInfo
 }
 
+function onDrag(e: any) {
+  dragX = e.detail.x
+  dragY = e.detail.y
+}
 
-onLoad(() => {
-  let y = uni.getWindowInfo().windowHeight - 200;
-  let x = uni.getWindowInfo().windowWidth;
-  pos.value = {x, y}
-  uni.setStorageSync(POS_KEY, pos.value)
-})
+function onTouchEnd() {
+  const { windowWidth, windowHeight } = getWindowInfo()
+  const btnPx = windowWidth * 104 / 750
+  const maxX = windowWidth - btnPx
 
-const handleContact = () => {
+  const snapX = dragX < maxX / 2 ? 0 : maxX
+  const snapY = Math.max(0, Math.min(dragY, windowHeight - btnPx))
 
+  snapPos.value = { x: snapX, y: snapY }
+  uni.setStorageSync(POS_KEY, { x: snapX, y: snapY })
 }
 
+const handleContact = () => {}
 </script>
 
 <style scoped lang="scss">
-/* 热区占满屏幕但不拦截事件 */
 .movable-area {
   position: fixed;
   left: 0;
@@ -72,36 +73,44 @@ const handleContact = () => {
   width: 100vw;
   height: 100vh;
   pointer-events: none;
-  z-index: 999999 !important;
+  z-index: 999999;
 }
 
 .movable-view {
-  width: 100rpx;
-  height: 100rpx;
-  pointer-events: auto; /* 仅图标可点 */
-}
-
-.kefu image {
-  width: 100rpx;
-  height: 100rpx;
+  width: 104rpx;
+  height: 104rpx;
+  pointer-events: auto;
+  position: relative;
 }
 
-.customer-service {
-  height: 100rpx;
-  width: 100rpx;
-
+.cs-button {
+  width: 96rpx;
+  height: 96rpx;
+  border-radius: 50%;
+  background: #C6171E;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 4rpx 16rpx rgba(198, 23, 30, 0.28);
+  position: absolute;
+  left: 0;
+  top: 0;
+  transition: transform 0.15s ease;
 
-  image {
-    width: 80rpx !important;
-    height: 80rpx !important;
-    z-index: 100;
-    position: absolute;
-    margin-top: -60rpx;
+  &:active {
+    transform: scale(0.92);
   }
+}
 
-  .contact {
-    opacity: 0;
-    z-index: 9999;
-  }
+.contact-btn {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 104rpx;
+  height: 104rpx;
+  opacity: 0;
+  z-index: 2;
+  padding: 0;
+  margin: 0;
 }
-</style>
+</style>

+ 162 - 95
car-wash-mp/src/pages-order/detail/index.vue

@@ -1,19 +1,23 @@
 <template>
   <view class="page-container">
     <uv-navbar title="订单详情" bgColor="#C6171E" leftIconColor="#FFFFFF" :titleStyle="{ color: '#FFFFFF' }" :autoBack="true" :placeholder="true"></uv-navbar>
-    <scroll-view class="content-scroll" scroll-y="true">
-      <!-- 金额头部 -->
-      <view class="amount-header">
-        <view class="amount-box">
-          <text class="amount-symbol">¥</text>
-          <text class="amount-number">{{ ((state.detail.amount) / 100).toFixed(2) }}</text>
+    <scroll-view class="content-scroll" scroll-y="true" :enhanced="true" :show-scrollbar="false">
+      <!-- 金额概览 -->
+      <view class="summary-card">
+        <view class="summary-left">
+          <text class="summary-label">支付金额</text>
+          <view class="summary-amount">
+            <text class="summary-symbol">¥</text>
+            <text class="summary-number">{{ ((state.detail.amount) / 100).toFixed(2) }}</text>
+          </view>
         </view>
-        <text class="amount-type">洗车消费</text>
-        <view
-          class="status-badge"
-          :class="'status-' + (state.detail.payStatus == 0 ? 'primary' : (state.detail.payStatus == 1 ? 'success' : 'error'))"
-        >
-          {{ fmtDictName('Order.pay', state.detail.payStatus) }}
+        <view class="summary-right">
+          <view
+            class="summary-badge"
+            :class="'badge-' + (state.detail.payStatus == 0 ? 'primary' : (state.detail.payStatus == 1 ? 'success' : 'error'))"
+          >
+            {{ fmtDictName('Order.pay', state.detail.payStatus) }}
+          </view>
         </view>
       </view>
 
@@ -70,15 +74,15 @@
         </view>
 
         <view class="fee-list">
-          <view class="fee-item" v-for="item in state.detail.orderItems" :key="item.id">
-            <view class="fee-item-left">
+          <view class="fee-item" v-for="item in state.detail.orderItems" :key="item.name">
+            <view class="fee-item-top">
               <text class="fee-name">{{ item.name }}</text>
-              <text class="fee-duration">{{ item.seconds }}</text>
-            </view>
-            <view class="fee-item-right">
-              <text class="fee-price">{{ item.price }}</text>
               <text class="fee-amount">¥{{ item.amount }}</text>
             </view>
+            <view class="fee-item-bottom">
+              <text class="fee-meta">单价 ¥{{ item.price }}</text>
+              <text class="fee-meta">{{ item.seconds }}</text>
+            </view>
           </view>
         </view>
       </view>
@@ -110,14 +114,25 @@
         </view>
       </view>
 
+      <!-- 客服入口 -->
+      <button class="service-card" open-type="contact" send-message-title="YesWash洗车客服" session-from="order_detail">
+        <view class="service-left">
+          <view class="service-icon">
+            <uv-icon name="kefu-ermai" color="#FFFFFF" size="18"></uv-icon>
+          </view>
+          <text class="service-text">联系客服</text>
+        </view>
+        <uv-icon name="arrow-right" color="#C0C4CC" size="14"></uv-icon>
+      </button>
+
       <view style="height: 40rpx;"></view>
     </scroll-view>
   </view>
 </template>
 
 <script setup lang="ts">
-import { onHide, onLoad } from "@dcloudio/uni-app";
-import { reactive } from "vue";
+import { onLoad, onShow } from "@dcloudio/uni-app";
+import { reactive, ref } from "vue";
 import { get } from "@/utils/https";
 import { fmtDictName, fmtDateTime, fmtDuration, fmtMoney } from "@/utils/common";
 
@@ -129,8 +144,10 @@ const initState = () => ({
 });
 
 const state = reactive(initState());
+const orderNo = ref('');
+let loaded = false;
 
-onLoad((options: any) => {
+function loadData() {
   const user = getApp<any>().globalData.user;
   if (!user || !user.id) {
     uni.showToast({ title: '请先登录', icon: 'none' });
@@ -140,12 +157,7 @@ onLoad((options: any) => {
     return;
   }
 
-  const { orderNo } = options;
-  if (orderNo) {
-    state.detail.orderId = orderNo;
-  }
-
-  get(`/wash-order/detailWashOrder?userId=${user.id}&orderId=${orderNo}`)
+  get(`/wash-order/detailWashOrder?userId=${user.id}&orderId=${orderNo.value}`)
     .then((res: any) => {
       state.detail = { ...state.detail, ...res };
 
@@ -169,10 +181,21 @@ onLoad((options: any) => {
     .catch(() => {
       uni.showToast({ title: '加载失败', icon: 'none' });
     });
+}
+
+onLoad((options: any) => {
+  orderNo.value = options.orderNo || '';
+  if (orderNo.value) {
+    state.detail.orderId = orderNo.value;
+  }
+  loadData();
+  loaded = true;
 });
 
-onHide(() => {
-  Object.assign(state, initState());
+onShow(() => {
+  if (loaded) {
+    loadData();
+  }
 });
 
 const handleCopy = () => {
@@ -211,68 +234,79 @@ const fallbackCopy = (orderId: string) => {
   width: 100vw;
   height: 100vh;
   background: $uni-bg-color-page;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
 }
 
 .content-scroll {
-  height: 100%;
+  flex: 1;
   width: 100%;
+  overflow-y: auto;
 }
 
-// 金额头部
-.amount-header {
-  padding: 60rpx 30rpx 40rpx;
-  text-align: center;
-  background: linear-gradient(135deg, $uni-color-primary 0%, $uni-color-primary-light 100%);
-  position: relative;
+// 金额概览卡片
+.summary-card {
+  margin: 24rpx 30rpx;
+  background: $uni-bg-color-card;
+  border-radius: 24rpx;
+  padding: 36rpx 28rpx;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
 
-  .amount-box {
+  .summary-left {
     display: flex;
-    align-items: baseline;
-    justify-content: center;
-    margin-bottom: 16rpx;
+    flex-direction: column;
+    gap: 12rpx;
 
-    .amount-symbol {
-      font-size: 36rpx;
-      color: $uni-text-color-inverse;
-      font-weight: $uni-font-weight-medium;
-      margin-right: 8rpx;
+    .summary-label {
+      font-size: 24rpx;
+      color: $uni-text-color-hint;
     }
 
-    .amount-number {
-      font-size: 80rpx;
-      color: $uni-text-color-inverse;
-      font-weight: $uni-font-weight-semibold;
-      line-height: 1;
-    }
-  }
+    .summary-amount {
+      display: flex;
+      align-items: baseline;
+
+      .summary-symbol {
+        font-size: 32rpx;
+        color: $uni-text-color-dark;
+        font-weight: $uni-font-weight-semibold;
+        margin-right: 4rpx;
+      }
 
-  .amount-type {
-    font-size: 28rpx;
-    color: rgba(255, 255, 255, 0.9);
-    margin-bottom: 24rpx;
-    display: block;
+      .summary-number {
+        font-size: 60rpx;
+        color: $uni-text-color-dark;
+        font-weight: $uni-font-weight-bold;
+        line-height: 1;
+      }
+    }
   }
 
-  .status-badge {
-    display: inline-block;
-    padding: 8rpx 24rpx;
-    border-radius: 20rpx;
-    font-size: 24rpx;
-    font-weight: $uni-font-weight-medium;
+  .summary-right {
+    .summary-badge {
+      padding: 10rpx 24rpx;
+      border-radius: 20rpx;
+      font-size: 24rpx;
+      font-weight: $uni-font-weight-medium;
+      white-space: nowrap;
 
-    &.status-primary {
-      background: rgba(255, 255, 255, 0.2);
-      color: $uni-text-color-inverse;
-    }
+      &.badge-primary {
+        background: rgba($uni-color-primary, 0.08);
+        color: $uni-color-primary;
+      }
 
-    &.status-success {
-      background: $uni-text-color-inverse;
-      color: $uni-color-success;
-    }
+      &.badge-success {
+        background: rgba($uni-color-success, 0.08);
+        color: $uni-color-success;
+      }
 
-    &.status-error {
-      background: $uni-text-color-inverse;
-      color: $uni-color-error;
+      &.badge-error {
+        background: rgba($uni-color-error, 0.08);
+        color: $uni-color-error;
+      }
     }
   }
 }
@@ -384,9 +418,6 @@ const fallbackCopy = (orderId: string) => {
   padding: 0 28rpx;
 
   .fee-item {
-    display: flex;
-    justify-content: space-between;
-    align-items: flex-start;
     padding: 24rpx 0;
     border-bottom: 1rpx solid $uni-border-color-light;
 
@@ -394,11 +425,11 @@ const fallbackCopy = (orderId: string) => {
       border-bottom: none;
     }
 
-    .fee-item-left {
-      flex: 1;
+    .fee-item-top {
       display: flex;
-      flex-direction: column;
-      gap: 8rpx;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 10rpx;
 
       .fee-name {
         font-size: 28rpx;
@@ -406,28 +437,22 @@ const fallbackCopy = (orderId: string) => {
         font-weight: $uni-font-weight-medium;
       }
 
-      .fee-duration {
-        font-size: 24rpx;
-        color: $uni-text-color-hint;
+      .fee-amount {
+        font-size: 30rpx;
+        color: $uni-color-primary;
+        font-weight: $uni-font-weight-semibold;
       }
     }
 
-    .fee-item-right {
+    .fee-item-bottom {
       display: flex;
-      flex-direction: column;
-      align-items: flex-end;
-      gap: 8rpx;
+      justify-content: space-between;
+      align-items: center;
 
-      .fee-price {
+      .fee-meta {
         font-size: 24rpx;
         color: $uni-text-color-hint;
       }
-
-      .fee-amount {
-        font-size: 32rpx;
-        color: $uni-color-primary;
-        font-weight: $uni-font-weight-semibold;
-      }
     }
   }
 }
@@ -467,4 +492,46 @@ const fallbackCopy = (orderId: string) => {
     }
   }
 }
+
+// 客服入口
+.service-card {
+  margin: 24rpx 30rpx;
+  background: $uni-bg-color-card;
+  border-radius: 24rpx;
+  padding: 28rpx;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  border: none;
+  width: auto;
+  line-height: inherit;
+  font-size: inherit;
+  box-sizing: border-box;
+
+  &::after {
+    border: none;
+  }
+
+  .service-left {
+    display: flex;
+    align-items: center;
+    gap: 16rpx;
+
+    .service-icon {
+      width: 48rpx;
+      height: 48rpx;
+      border-radius: 50%;
+      background: #C6171E;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+
+    .service-text {
+      font-size: 28rpx;
+      color: $uni-text-color-dark;
+      font-weight: $uni-font-weight-medium;
+    }
+  }
+}
 </style>