Procházet zdrojové kódy

feat: 我的卡包页重设计——票券式卡片(面额区+打孔分隔+信息区)替代背景图贴片,钱包规范tab等宽居中带数量徽标(优惠券在左默认选中),空状态改图标+文案,失效卡整体置灰,导航栏底色统一#F4F7F6

skyline před 2 týdny
rodič
revize
39b9323331
2 změnil soubory, kde provedl 288 přidání a 212 odebrání
  1. 287 212
      charge-front/src/pages-charge/card/card.vue
  2. 1 0
      charge-front/src/pages.json

+ 287 - 212
charge-front/src/pages-charge/card/card.vue

@@ -1,173 +1,93 @@
 <template>
-  <view class="page pt-20 pr-40 pl-40">
-    <view class="tab-list flex-align-end">
-      <view class="fs-28 color-666 mt-20 w50" :class="mode===0?'active':''" @click="changeTab(0)">权益卡</view>
-      <view class="fs-28 color-666 mt-20 w50" :class="mode===1?'active':''" @click="changeTab(1)">优惠券</view>
-    </view>
-    <view v-if="mode===0">
+  <view class="page">
+    <!-- tabs:等宽居中 + 下划线过渡(同钱包页规范) -->
+    <view class="tabs">
       <view
-          class="item mt-20 "
-          v-for="(item, index) in list"
-          :class="item.status==0?'inactive':''"
-          :key="index"
+          v-for="tab in tabs"
+          :key="tab.value"
+          class="tab-item flex-center"
+          @click="changeTab(tab.value)"
       >
-        <image class="full-percent" src="../static/discount-bg.png"></image>
-        <view class="absolute-full flex pt-8 pb-8">
-          <view class="left  flex-justify-center pl-48">
-
-            <view class="item-content_left">
-              <view class="flex-align-end">
-                <view class="item-header__left">
-                  <image class="width-84" src="../static/discount-rights-card.png" mode="widthFix"></image>
-                  <view class="fs-48 lh-58 color-000 fw-500 ml-16">
-                    {{ (item.discount / 10).toFixed(1) }}折
-                  </view>
-                  <view class="fs-26 color-999 ml-16" v-if="mode === 0"
-                  >抵扣充电服务费
-                  </view>
-                </view>
-
-                <!--                <view class="item-header__right flex-grow">
-                                  <view class="item-status ml-10">{{ item.status == 0 ? '已失效' : '生效中' }}</view>
-                                </view>-->
-              </view>
-
-
-              <block>
-                <view class="fs-26 color-999 mt-8"
-                      @click.stop="to(`/pages-common/activity/activity?id=${item.activityId}`)">
-                  <view class="fs-26 color-999">到期时间:{{ item.endTime }}</view>
-                  <view class="flex-align-center" v-if="mode === 0">
-                    <view class="fs-26 color-999">权益余额:{{ fenToYuan(item.rightsBalance) }}元
-                      <text class="color-primary">详细规则</text>
-                      <text class="fs-24 ml-4 color-primary">>></text>
-                    </view>
-                  </view>
-                </view>
-              </block>
-            </view>
-            <view class="item-content_right">
-              <view class="item-status ml-10">{{ item.status == 0 ? '已失效' : '生效中' }}</view>
-            </view>
-
-          </view>
-          <!--          <view v-if="mode === 0" class="right width-80 flex-center">
-                      <style-checkbox
-                          :size="48"
-                          :checked="checkIndex === index"
-                          :iconsize="17"
-                      ></style-checkbox>
-                    </view>-->
-          <!--          <view
-                        v-if="mode === 1"
-                        class="right flex-center"
-                        :style="{
-                      width: '210rpx',
-                      borderLeft: '1px dashed rgba(28, 43, 41, 0.08)',
-                    }"
-                    >
-                      <view class="btn flex-center fs-28" @click.stop="toCharge(index)"
-                      >前往充值</view
-                      >
-                    </view>-->
-        </view>
-      </view>
-
-      <view style="width: 100%;text-align: center;">
-        <image v-if="!list||list.length==0" style="width: 240rpx;" mode="widthFix" src="/static/images/search-empty.png"></image>
+        <view
+            class="fs-30"
+            :style="{
+            color: mode === tab.value ? 'var(--text-1)' : 'var(--text-2)',
+            fontWeight: mode === tab.value ? '600' : '400',
+          }"
+        >{{ tab.label }}</view>
+        <view
+            v-if="tab.count"
+            class="tab-badge"
+            :class="{ 'tab-badge_off': mode !== tab.value }"
+        >{{ tab.count }}</view>
+        <view class="tab-underline" :style="{ opacity: mode === tab.value ? '1' : '0' }"></view>
       </view>
     </view>
 
-
-    <view v-else-if="mode===1">
+    <!-- :key 使 tab 切换时重新播放入场动效 -->
+    <view :key="mode">
       <view
-          class="item mt-20"
-          :class="item.usageStatus==1||item.status==0?'inactive':''"
-          v-for="(item, index) in couponList"
+          v-for="(item, index) in currentList"
           :key="index"
+          class="ticket mt-20 anim-fade-up"
+          :class="{ 'ticket-inactive': item.disabled }"
+          :style="{ animationDelay: Math.min(index, 5) * 0.04 + 's' }"
       >
-        <!--        <image
-                    class="full-percent"
-                    src="/pages-charge/static/discount-bg.png"
-                ></image>-->
-        <view class="absolute-full flex pt-8 pb-8">
-          <view class="left  flex-justify-center pl-48">
-            <view class="item-content_left">
-              <view class="flex-align-end">
-                <view class="item-header__left">
-
-                  <image
-                      class="width-84"
-                      src="../static/discount-coupon.png"
-                      mode="widthFix"
-                  ></image>
-                  <view></view>
-                  <view class="fs-48 lh-58 color-000 fw-500 ml-16" v-if="item.couponType=='FullDiscount'">
-                    {{ fenToYuan(item.discount) }}元
-                  </view>
-
-                  <view class="fs-48 lh-58 color-000 fw-500 ml-16" v-else>
-                    {{ (item.discount / 10).toFixed(1) }}折
-                  </view>
-
-                  <view class="fs-26 color-999 ml-16">
-                    充电服务费:{{ item.couponType == 'FullDiscount' ? '满减券' : '折扣券' }}
-                  </view>
-                </view>
-
-
-                <!--
-                                <view class="item-header__right flex-grow">
-                                  <view class="item-status ml-10" v-if="item.status==0">已失效</view>
-                                  <view class="item-status ml-10" v-else>{{ item.usageStatus == 0 ? '未使用' : '已使用' }}</view>
-                                </view>
-                -->
-
-
-              </view>
-
-              <block v-if="mode === 1">
-                <view class="fs-26 color-999 mt-8"
-                      @click.stop="to(`/pages-common/activity/activity?id=${item.activityId}`)">
-                  <view class="fs-26 color-999">到期时间:{{ item.endTime }}</view>
-                  <view class="flex-align-center">
-                    <view class="fs-26 color-999">服务费门槛:{{ fenToYuan(item.minServiceMoney) }}元
-                      <text class="color-primary">详细规则</text>
-                      <text class="fs-24 ml-4 color-primary">>></text>
-                    </view>
-                  </view>
-                </view>
-              </block>
-            </view>
-            <view class="item-content_right">
-              <view class="item-status ml-10" v-if="item.status==0">已失效</view>
-              <view class="item-status ml-10" v-else>{{ item.usageStatus == 0 ? '未使用' : '已使用' }}</view>
-            </view>
-
-
+        <!-- 票券左区:面额 -->
+        <view class="ticket-left">
+          <view class="flex-align-baseline">
+            <text class="ticket-num">{{ item.value }}</text>
+            <text class="ticket-unit ml-2">{{ item.unit }}</text>
+          </view>
+          <view class="ticket-type">{{ item.typeLabel }}</view>
+        </view>
+        <!-- 票券打孔分隔 -->
+        <view class="ticket-divider">
+          <view class="ticket-notch ticket-notch-top"></view>
+          <view class="ticket-notch ticket-notch-bottom"></view>
+        </view>
+        <!-- 票券右区:说明 -->
+        <view class="ticket-right">
+          <view class="flex-align-center">
+            <view class="fs-28 fw-600 color-333 ticket-title">{{ item.title }}</view>
+            <view class="ticket-status ml-auto flex-shrink">{{ item.statusText }}</view>
+          </view>
+          <view class="fs-24 color-999 mt-12">有效期至 {{ item.endTimeText }}</view>
+          <view class="fs-24 color-999 mt-8">{{ item.extra }}</view>
+          <view
+              class="ticket-link pressable"
+              hover-class="pressed"
+              hover-stay-time="80"
+              @click.stop="toActivity(item)"
+          >
+            详细规则
+            <uni-icons class="ml-4" type="right" size="12" color="#2d9e95"></uni-icons>
           </view>
         </view>
       </view>
 
-      <view style="width: 100%;text-align: center;">
-        <image v-if="!couponList||couponList.length==0" style="width: 240rpx;" mode="widthFix" src="/static/images/search-empty.png"></image>
+      <!-- 空状态 -->
+      <view v-if="!currentList.length" class="empty flex-column flex-center">
+        <view class="empty-icon flex-center">
+          <uni-icons type="wallet" size="30" color="#2d9e95"></uni-icons>
+        </view>
+        <view class="fs-26 color-999 mt-24">{{ mode === 0 ? "暂无权益卡" : "暂无优惠券" }}</view>
       </view>
     </view>
-
   </view>
 </template>
 
 <script setup lang="ts">
 import {onPullDownRefresh, onShow} from "@dcloudio/uni-app";
-import {ref} from "vue";
+import {computed, ref} from "vue";
 import {to} from "../../utils/navigate";
 import {fetchRightsAndCoupons} from "@/api/user";
-import { fenToYuan } from "@/utils/money";
+import {fenToYuan} from "@/utils/money";
+import {format, parseDate} from "@/utils/date";
 
-const mode = ref(0);
+const mode = ref(1);
 const list = ref<any[]>([]);
 const couponList = ref<any[]>([]);
-const checkIndex = ref(-1);
 
 onPullDownRefresh(() => {
   loadData();
@@ -192,105 +112,260 @@ const loadData = () => {
 
 onShow(() => {
   loadData();
+});
 
+/** 面额大数字去掉多余的 0:"20.00" -> "20"、"9.0" -> "9" */
+const stripZeros = (s: string) => s.replace(/\.?0+$/, "");
+
+const endTimeText = (item: any) =>
+    item.endTime ? format("y.M.d", parseDate(item.endTime).getTime()) : "--";
+
+interface TicketVM {
+  value: string;
+  unit: string;
+  typeLabel: string;
+  title: string;
+  statusText: string;
+  disabled: boolean;
+  endTimeText: string;
+  extra: string;
+  activityId: any;
+}
 
-});
+const rightsTickets = computed<TicketVM[]>(() =>
+    (list.value || []).map((item: any) => ({
+      value: stripZeros((Number(item.discount) / 10).toFixed(1)),
+      unit: "折",
+      typeLabel: "权益卡",
+      title: "抵扣充电服务费",
+      statusText: item.status == 0 ? "已失效" : "生效中",
+      disabled: item.status == 0,
+      endTimeText: endTimeText(item),
+      extra: `权益余额 ¥${fenToYuan(item.rightsBalance)}`,
+      activityId: item.activityId,
+    }))
+);
+
+const couponTickets = computed<TicketVM[]>(() =>
+    (couponList.value || []).map((item: any) => {
+      const full = item.couponType === "FullDiscount";
+      return {
+        value: full ? stripZeros(fenToYuan(item.discount)) : stripZeros((Number(item.discount) / 10).toFixed(1)),
+        unit: full ? "元" : "折",
+        typeLabel: full ? "满减券" : "折扣券",
+        title: "抵扣充电服务费",
+        statusText: item.status == 0 ? "已失效" : item.usageStatus == 1 ? "已使用" : "未使用",
+        disabled: item.status == 0 || item.usageStatus == 1,
+        endTimeText: endTimeText(item),
+        extra: `门槛 满 ¥${fenToYuan(item.minServiceMoney)} 可用`,
+        activityId: item.activityId,
+      };
+    })
+);
+
+const tabs = computed(() => [
+  {label: "优惠券", value: 1, count: couponTickets.value.length},
+  {label: "权益卡", value: 0, count: rightsTickets.value.length},
+]);
+
+const currentList = computed(() =>
+    mode.value === 0 ? rightsTickets.value : couponTickets.value
+);
+
+const toActivity = (item: TicketVM) => {
+  if (item.activityId) {
+    to(`/pages-common/activity/activity?id=${item.activityId}`);
+  }
+};
 </script>
 
 <style lang="scss" scoped>
 .page {
   min-height: 100vh;
   width: 100%;
-  background-color: #f6f7fa;
-
-  .item {
-    position: relative;
-    min-height: 220rpx;
-    border-radius: 20rpx;
-    overflow: hidden;
-    background: #fff;
-
-    .left {
-      flex-grow: 1;
-    }
-
-    .right {
-      flex-shrink: 0;
-
-      .btn {
-        border-radius: 8rpx;
-        border: 1px solid var(--color-primary);
-        color: var(--color-primary);
-        width: 144rpx;
-        height: 56rpx;
-      }
-    }
-  }
+  padding: 8rpx 30rpx 0;
+  background-color: var(--bg-page);
+}
 
-  .tab-list {
-    width: 100%;
+/* ============ tabs ============ */
+.tabs {
+  display: flex;
+  width: 100%;
+}
 
-    .w50 {
-      width: 50%;
-      text-align: center;
-    }
+.tab-item {
+  position: relative;
+  /* flex-basis 0 强制严格等分,不受文字宽度影响 */
+  flex: 1 1 0;
+  min-width: 0;
+  height: 96rpx;
+}
 
-    .active {
-      color: var(--color-primary);
-    }
-  }
+.tab-badge {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  min-width: 32rpx;
+  height: 32rpx;
+  padding: 0 10rpx;
+  margin-left: 10rpx;
+  border-radius: 16rpx;
+  background: var(--color-primary-tint-strong);
+  color: var(--color-primary);
+  font-size: 20rpx;
+  font-weight: 500;
+  box-sizing: border-box;
+}
+
+.tab-badge_off {
+  background: rgba(28, 43, 41, 0.06);
+  color: var(--text-3);
+}
+
+.tab-underline {
+  position: absolute;
+  left: 50%;
+  bottom: 4rpx;
+  transform: translateX(-50%);
+  width: 40rpx;
+  height: 6rpx;
+  border-radius: 6rpx;
+  background-color: var(--color-primary);
+  transition: opacity 0.22s cubic-bezier(0.22, 1, 0.36, 1);
+}
+
+/* ============ 票券卡片 ============ */
+.ticket {
+  display: flex;
+  align-items: stretch;
+  min-height: 176rpx;
+  border-radius: 20rpx;
+  background: #fff;
+  overflow: hidden;
+  box-shadow: 0 2rpx 8rpx rgba(28, 43, 41, 0.04);
 }
 
-.item-status {
+.ticket-left {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  width: 176rpx;
+  flex-shrink: 0;
+  background: var(--color-primary-tint);
+}
+
+.ticket-num {
+  font-size: 48rpx;
+  line-height: 52rpx;
+  font-weight: 600;
+  color: var(--color-primary);
+  font-variant-numeric: tabular-nums;
+}
+
+.ticket-unit {
   font-size: 24rpx;
-  padding: 0rpx 2rpx;
+  font-weight: 500;
   color: var(--color-primary);
-  border: 2rpx solid var(--color-primary);
-  border-radius: 6rpx;
+}
 
+.ticket-type {
+  margin-top: 8rpx;
+  font-size: 22rpx;
+  color: var(--color-primary);
+  opacity: 0.72;
 }
 
-.item-header__left {
-  display: inline-flex;
-  align-items: flex-end;
+.ticket-divider {
+  position: relative;
+  width: 0;
+  margin: 0 24rpx;
+  border-left: 2rpx dashed rgba(28, 43, 41, 0.12);
 }
 
-.item-header__right {
-  padding: 5rpx;
+.ticket-notch {
+  position: absolute;
+  left: -13rpx;
+  width: 24rpx;
+  height: 24rpx;
+  border-radius: 50%;
+  background: var(--bg-page);
+}
+
+.ticket-notch-top {
+  top: -12rpx;
+}
+
+.ticket-notch-bottom {
+  bottom: -12rpx;
+}
+
+.ticket-right {
   display: flex;
-  //justify-content: flex-end;
+  flex-direction: column;
+  flex: 1;
+  min-width: 0;
+  padding: 24rpx 24rpx 20rpx 0;
 }
 
-.inactive {
-  cursor: not-allowed;
+.ticket-title {
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.ticket-status {
+  display: inline-flex;
+  align-items: center;
+  height: 40rpx;
+  padding: 0 14rpx;
+  border-radius: 12rpx;
+  background: var(--color-primary-tint);
+  color: var(--color-primary);
+  font-size: 22rpx;
+}
+
+.ticket-link {
+  display: inline-flex;
+  align-items: center;
+  margin-top: auto;
+  padding-top: 12rpx;
+  font-size: 24rpx;
+  font-weight: 500;
+  color: var(--color-primary);
+  align-self: flex-start;
+}
+
+/* 失效/已使用:整体置灰 */
+.ticket-inactive {
   pointer-events: none;
 
-  .item-status {
-    font-size: 24rpx;
-    padding: 0rpx 2rpx;
-    color: #ccc !important;
-    border: 2rpx solid #ccc;
-    border-radius: 6rpx;
+  .ticket-left {
+    background: var(--color-sec);
+  }
 
+  .ticket-num,
+  .ticket-unit,
+  .ticket-type,
+  .ticket-link {
+    color: var(--text-3);
   }
 
-  .color-primary {
-    color: #ccc !important;
+  .ticket-status {
+    background: rgba(28, 43, 41, 0.06);
+    color: var(--text-3);
   }
 }
 
-.item-content_left {
-  flex-grow: 1;
-  display: flex;
-  flex-direction: column;
-  justify-content: space-around;
-
+/* ============ 空状态 ============ */
+.empty {
+  padding: 120rpx 0 160rpx;
 }
 
-.item-content_right {
-  width: 120rpx;
-  display: flex;
-  align-items: center;
-  align-content: center;
+.empty-icon {
+  width: 112rpx;
+  height: 112rpx;
+  border-radius: 50%;
+  background: var(--color-primary-tint);
 }
 </style>

+ 1 - 0
charge-front/src/pages.json

@@ -91,6 +91,7 @@
           "style": {
             "navigationStyle": "default",
             "navigationBarTitleText": "我的卡包",
+            "navigationBarBackgroundColor": "#F4F7F6",
             "enablePullDownRefresh": true
           }
         }