2
0

3 Коммиты a08e7fbe1a ... 71a4e3b77f

Автор SHA1 Сообщение Дата
  skyline 71a4e3b77f feat: 常见问题页电话客服图标重绘为扁平化SVG(品牌青绿圆徽+白色客服耳机,矢量无损),移除旧位图 2 недель назад
  skyline 39b9323331 feat: 我的卡包页重设计——票券式卡片(面额区+打孔分隔+信息区)替代背景图贴片,钱包规范tab等宽居中带数量徽标(优惠券在左默认选中),空状态改图标+文案,失效卡整体置灰,导航栏底色统一#F4F7F6 2 недель назад
  skyline a048a8078c feat: 预约充电确认页重设计——倒计时升级为时/分/秒数字块主卡(含预计开始时间与等待开始标签),取消预约收入卡片底部,费用行与余额卡token化加按压反馈,页面底色统一#F4F7F6并加卡片入场动效 2 недель назад

+ 184 - 63
charge-front/src/pages-charge/appointment/appointment.vue

@@ -1,7 +1,7 @@
 <template>
   <view :class="['page']">
     <view v-if="station && priceInfo">
-      <view class="block">
+      <view class="block anim-fade-up">
         <view class="station">
           <ChargeMachine
               :title="'NO.' + equipment.shortId"
@@ -21,49 +21,49 @@
       </view>
       <!--      <view class="mt-40 ml-30 color-999 fs-32 fw-500">费用说明</view>-->
       <view
-          class="mt-20 block height-96 flex-align-center pl-30 pr-30"
+          class="mt-20 block height-96 flex-align-center pl-30 pr-30 pressable anim-fade-up"
+          style="animation-delay: 0.04s"
+          hover-class="pressed"
           @click="openPriceDesc"
       >
-        <view class="fs-28 color-000">充电费用</view>
-        <view class="ml-64 fs-26 color-333"
+        <view class="fs-28 color-333 fw-500">充电费用</view>
+        <view class="ml-auto fs-26 color-666 mr-8"
         >{{ priceInfo.minPrice }}~{{ priceInfo.maxPrice }}元/度
         </view
         >
-        <view class="ml-auto">
-          <uni-icons type="right" size="16" color="rgba(28,43,41,0.4)"></uni-icons>
-        </view>
+        <uni-icons type="right" size="16" color="rgba(28,43,41,0.4)"></uni-icons>
       </view>
       <template v-if="appointmentData">
-        <view class="mt-40 color-999 fs-32 fw-500">已预约充电</view>
-        <view class="mt-20 block flex-align-center" style="height: 180rpx">
-          <image
-              src="../static/icon-alarm.png"
-              mode="widthFix"
-              style="width: 32rpx; display: block"
-          ></image>
-          <view class="ml-24">
-            <view class="flex" v-if="appointmentCountDown">
-              <view class="fs-28 color-000">将在</view>
-              <view class="fs-44 lh-36 color-primary ml-8 mr-8">{{
-                  appointmentCountDown
-                }}
-              </view>
-              <view class="fs-28 color-000">后开始充电</view>
+        <view class="mt-20 block hero anim-fade-up" style="animation-delay: 0.08s">
+          <view class="flex-align-center pl-30 pr-30 pt-30">
+            <view class="hero-icon flex-center anim-breathe">
+              <uni-icons type="notification-filled" size="22" color="#2d9e95"></uni-icons>
             </view>
-            <view class="fs-24 color-999 mt-8"
-            >注意:启动充电前请勿拔插充电枪!
-            </view
-            >
+            <view class="ml-16 fs-30 fw-600 color-333">已预约充电</view>
+            <view class="hero-tag ml-auto">等待开始</view>
+          </view>
+          <view class="mt-28 fs-24 color-999 text-center">距开始充电</view>
+          <view class="mt-16 flex-center">
+            <view v-for="(part, i) in countdownParts" :key="i" class="hero-cell">
+              <view class="hero-num">{{ part }}</view>
+              <view class="hero-unit">{{ ["时", "分", "秒"][i] }}</view>
+            </view>
+          </view>
+          <view class="mt-20 fs-24 color-999 text-center" v-if="appointmentStartText"
+          >预计 {{ appointmentStartText }} 开始充电
+          </view>
+          <view class="hero-tip flex-align-center ml-30 mr-30 mt-24">
+            <uni-icons type="info" size="14" color="#2d9e95"></uni-icons>
+            <view class="ml-8 fs-24 color-666">启动充电前请勿拔插充电枪</view>
+          </view>
+          <view class="mt-30 hero-divider"></view>
+          <view class="flex-center pt-20 pb-24">
+            <view
+                class="hero-cancel pressable flex-center"
+                hover-class="pressed"
+                @click="cancelAppointment"
+            >取消预约</view>
           </view>
-        </view>
-        <view class="mt-40 flex-center">
-          <view
-              class="flex-center height-68 br-68 fs-26 color-666"
-              style="width: 184rpx; border: 1px solid #c9d4d2"
-              @click="cancelAppointment"
-          >取消预约
-          </view
-          >
         </view>
       </template>
       <template v-else>
@@ -175,39 +175,40 @@
       </template>
       <!-- 当前余额 + 低余额提醒,点击去充值(充值后返回本页) -->
       <view
-          class="mt-20 block flex-align-center pl-30 pr-30 pt-30 pb-30"
-          :style="isLowBalance ? 'background-color: #fff5f5; border: 1rpx solid rgba(244, 54, 54, 0.25);' : ''"
+          class="mt-20 block flex-align-center pl-30 pr-30 pt-30 pb-30 pressable anim-fade-up"
+          :class="{ 'balance-low': isLowBalance }"
+          style="animation-delay: 0.12s"
+          hover-class="pressed"
           @click="to('/pages-user/wallet-recharge/wallet-recharge?back=1')"
       >
-        <view class="flex-column">
-          <view class="flex-align-center">
-            <uni-icons type="wallet-filled" size="16" :color="isLowBalance ? '#f43636' : '#999'"></uni-icons>
-            <view class="fs-26 ml-8 color-333">当前余额</view>
-          </view>
-          <view class="mt-8">
-            <view class="fs-40 fw-600" :style="{ color: isLowBalance ? '#f43636' : '#333' }">{{ balanceText }}</view>
+        <view class="balance-icon flex-center flex-shrink">
+          <uni-icons
+              type="wallet-filled"
+              size="18"
+              :color="isLowBalance ? '#f43636' : '#2d9e95'"
+          ></uni-icons>
+        </view>
+        <view class="ml-20">
+          <view class="fs-24 color-999">当前余额</view>
+          <view class="flex-align-baseline mt-6">
+            <text class="fs-24 fw-500 color-666">¥</text>
+            <text
+                class="fs-44 fw-600 ml-4 lh-44"
+                :style="{ color: isLowBalance ? 'var(--color-error)' : 'var(--text-1)' }"
+            >{{ balanceValue }}
+            </text>
           </view>
           <view
               v-if="isLowBalance"
-              class="fs-24 mt-8 lh-30"
-              style="color: #f43636"
-          >余额仅 {{ balanceText }},可能无法完成本次充电
-          </view
-          >
-        </view>
-        <view class="ml-auto">
-          <view
-              class="flex-center fs-24 fw-500"
-              :style="isLowBalance
-                  ? 'height: 56rpx; border-radius: 56rpx; background-color: var(--color-error); color: #fff; padding: 0 28rpx;'
-                  : 'height: 56rpx; border-radius: 56rpx; border: 1rpx solid #c9d4d2; color: #666; padding: 0 28rpx;'"
-          >去充值
-          </view
-          >
+              class="fs-22 mt-4 lh-30"
+              style="color: var(--color-error)"
+          >余额较低,可能无法完成本次充电
+          </view>
         </view>
+        <view class="ml-auto recharge-pill flex-center fs-24 fw-500 flex-shrink">去充值</view>
       </view>
       <view class="pt-60 pb-80"></view>
-      <style-bottom-view>
+      <style-bottom-view :shadow="true">
         <view class="pt-20 pl-40 pr-40 pb-20 bg-fff flex">
           <template v-if="appointmentData">
             <view class="mr-10" style="width: 50%">
@@ -375,7 +376,7 @@ const LOW_BALANCE_THRESHOLD = 30;
 const isLowBalance = computed(
     () => Number(balance.value) > 0 && Number(balance.value) < LOW_BALANCE_THRESHOLD
 );
-const balanceText = computed(() => `${Number(balance.value).toFixed(2)}元`);
+const balanceValue = computed(() => Number(balance.value).toFixed(2));
 
 
 const refreshUserBalance = () => {
@@ -691,6 +692,25 @@ const formatAppointmentCountDown = (time: string) => {
   }:${secondTime < 10 ? "0" + secondTime : secondTime}`;
 };
 
+/** 倒计时分段:["01", "20", "52"] 对应 时/分/秒 */
+const countdownParts = computed(() => {
+  const cd = appointmentCountDown.value;
+  return cd ? String(cd).split(":") : ["--", "--", "--"];
+});
+
+/** 预约开始时间的展示文案:今天/明天 M月d日 HH:mm */
+const appointmentStartText = computed(() => {
+  if (!appointmentData.value?.startTime) {
+    return "";
+  }
+  const ts = parseDate(appointmentData.value.startTime).getTime();
+  const dayDiff = Math.round(
+      (new Date(ts).setHours(0, 0, 0, 0) - new Date().setHours(0, 0, 0, 0)) / DAY
+  );
+  const dayText = dayDiff <= 0 ? "今天" : dayDiff === 1 ? "明天" : format("M月d日", ts);
+  return `${dayText} ${format("h:m", ts)}`;
+});
+
 const submit = () => {
   if (
       chargeType.value === 0 &&
@@ -946,8 +966,8 @@ onUnload(() => {
 .page {
   min-height: 100vh;
   width: 100%;
-  padding: 20rpx;
-  background-color: #f6f7fa;
+  padding: 24rpx 30rpx 0;
+  background-color: var(--bg-page);
 }
 
 .block {
@@ -960,6 +980,107 @@ onUnload(() => {
   border-bottom: 1rpx solid var(--divider);
 }
 
+/* ============ 预约状态主卡 ============ */
+.hero-icon {
+  width: 72rpx;
+  height: 72rpx;
+  border-radius: 50%;
+  background: var(--color-primary-tint-strong);
+  flex-shrink: 0;
+}
+
+.hero-tag {
+  display: flex;
+  align-items: center;
+  height: 44rpx;
+  padding: 0 20rpx;
+  border-radius: 12rpx;
+  background: var(--color-primary-tint);
+  color: var(--color-primary);
+  font-size: 24rpx;
+  font-weight: 500;
+  flex-shrink: 0;
+}
+
+.hero-cell {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  width: 128rpx;
+  padding: 20rpx 0 14rpx;
+  border-radius: 12rpx;
+  background: var(--color-primary-tint);
+}
+
+.hero-cell + .hero-cell {
+  margin-left: 16rpx;
+}
+
+.hero-num {
+  font-size: 52rpx;
+  line-height: 56rpx;
+  font-weight: 600;
+  color: var(--color-primary);
+  font-variant-numeric: tabular-nums;
+}
+
+.hero-unit {
+  margin-top: 4rpx;
+  font-size: 20rpx;
+  color: var(--text-3);
+}
+
+.hero-tip {
+  height: 64rpx;
+  border-radius: 12rpx;
+  background: var(--color-sec);
+}
+
+.hero-divider {
+  border-top: 1rpx solid var(--divider);
+}
+
+.hero-cancel {
+  min-width: 240rpx;
+  height: 64rpx;
+  padding: 0 48rpx;
+  border: 1rpx solid var(--divider);
+  border-radius: 32rpx;
+  background: #fff;
+  font-size: 26rpx;
+  color: var(--text-2);
+}
+
+/* ============ 余额卡 ============ */
+.balance-icon {
+  width: 72rpx;
+  height: 72rpx;
+  border-radius: 50%;
+  background: var(--color-primary-tint-strong);
+}
+
+.balance-low {
+  background-color: rgba(244, 54, 54, 0.04);
+  border: 1rpx solid rgba(244, 54, 54, 0.25);
+}
+
+.balance-low .balance-icon {
+  background: rgba(244, 54, 54, 0.08);
+}
+
+.recharge-pill {
+  height: 56rpx;
+  padding: 0 28rpx;
+  border-radius: 28rpx;
+  background: var(--color-primary-tint);
+  color: var(--color-primary);
+}
+
+.balance-low .recharge-pill {
+  background: var(--color-error);
+  color: #fff;
+}
+
 /* ============ 时间选择器 ============ */
 .time-picker {
   background-color: #fff;

+ 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 - 1
charge-front/src/pages-common/faq/faq.vue

@@ -50,7 +50,7 @@
       <image
         class="width-96"
         mode="widthFix"
-        src="/static/images/contact-customer.png"
+        src="/static/images/contact-customer.svg"
       />
       <view class="mt-16 color-666 fs-28">电话客服</view>
     </view>

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

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

BIN
charge-front/src/static/images/contact-customer.png


+ 13 - 0
charge-front/src/static/images/contact-customer.svg

@@ -0,0 +1,13 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
+  <!-- 扁平化电话客服图标:品牌青绿实底圆徽 + 白色客服耳机(居中对称,线宽统一) -->
+  <circle cx="48" cy="48" r="44" fill="#2D9E95"/>
+  <!-- 头带:半圆弧,两端收进耳罩 -->
+  <path d="M29 44v-4a19 19 0 0 1 38 0v4" fill="none" stroke="#FFFFFF" stroke-width="5.5" stroke-linecap="round"/>
+  <!-- 左右耳罩:胶囊形 -->
+  <rect x="24" y="40" width="12" height="21" rx="6" fill="#FFFFFF"/>
+  <rect x="60" y="40" width="12" height="21" rx="6" fill="#FFFFFF"/>
+  <!-- 话务杆:自左耳罩底部平滑弯出 -->
+  <path d="M30 61c0 7.5 6.5 11.5 14 11.5h4.5" fill="none" stroke="#FFFFFF" stroke-width="5" stroke-linecap="round"/>
+  <!-- 话筒端点 -->
+  <circle cx="54" cy="72.5" r="5" fill="#FFFFFF"/>
+</svg>