倒计时 + 步骤条:订单页黄金组合

电商类小程序的订单详情页,几乎都长同一个样子:顶部一圈支付倒计时,下方一条订单状态步骤条。Vant 的 CountDown 和 Steps 两个组件正好覆盖这套组合,本文以订单支付流程为主线把两者讲透。

CountDown 基础:倒计时支付截止时间

核心属性只有一个 time,单位是毫秒。常用姿势是拿后端返回的截止时间戳换算剩余毫秒:

html order-pay.vue
<template>
  <div class="pay-header">
    <p>订单提交成功,请在</p>
    <van-count-down :time="remainMs" format="HH:mm:ss" @finish="onTimeout" />
    <p>内完成支付</p>
  </div>
</template>

<script setup>
import { ref, onMounted } from "vue";
import { useRouter } from "vue-router";

const router = useRouter();
const remainMs = ref(0);

onMounted(() => {
  // expireAt 为后端返回的支付截止时间戳(毫秒)
  const expireAt = 1799999999000;
  remainMs.value = Math.max(0, expireAt - Date.now());
});

const onTimeout = () => {
  // 倒计时结束:跳转回订单列表或提示订单已关闭
  router.replace("/order/closed");
};
</script>
⚠️
不要用「本地时间 + 固定 30 分钟」算剩余时间,手机本地时间可能不准。以服务端时间戳做差值是唯一可靠的方式。

format 与自定义内容

format 支持 DD、HH、mm、ss、S 等占位符;需要「时:分:秒 各自带样式」时用默认插槽:

html demo.vue
<!-- 自定义格式:天 + 时:分 -->
<van-count-down :time="remainMs" format="DD 天 HH 时 mm 分" />

<!-- 自定义样式:数字卡片风格 -->
<van-count-down :time="remainMs" :millisecond="false">
  <template #default="current">
    <span class="time-box">{{ pad(current.hours) }}</span>
    <i class="colon">:</i>
    <span class="time-box">{{ pad(current.minutes) }}</span>
    <i class="colon">:</i>
    <span class="time-box">{{ pad(current.seconds) }}</span>
  </template>
</van-count-down>

<script setup>
const pad = (n) => String(n).padStart(2, "0");
</script>

<style scoped>
.time-box {
  background: #323233; color: #fff;
  border-radius: 4px; padding: 2px 6px; font-weight: 600;
}
.colon { color: #323233; margin: 0 4px; }
</style>

change 与 finish 事件

change 每秒触发(开启 millisecond 后每帧触发),回调参数包含 total、days、hours、minutes、seconds;finish 在归零时触发一次,用来做「支付超时」跳转或刷新订单状态。

Steps:订单状态步骤条

van-steps 通过 active 属性指定当前进行到第几步(从 0 开始):

html order-status.vue
<template>
  <van-steps :active="stepIndex" active-color="#ee0a24" active-icon="checked">
    <van-step>提交订单</van-step>
    <van-step>付款成功</van-step>
    <van-step>商家发货</van-step>
    <van-step>确认收货</van-step>
  </van-steps>
</template>

<script setup>
import { computed, ref } from "vue";

// status: 1 待支付 2 待发货 3 待收货 4 已完成
const status = ref(2);
const stepIndex = computed(() => ({ 1: 0, 2: 1, 3: 2, 4: 3 }[status.value] ?? 0));
</script>

每一步显示时间或描述

van-step 支持在插槽里放多行内容,物流信息直接展示在步骤下:

html demo.vue
<van-steps :active="1" direction="vertical">
  <van-step>
    已签收
    <template #default>
      <p class="log">您的订单已送达小区驿站,凭取件码 8-2-3006 领取。</p>
      <p class="time">11-09 09:12</p>
    </template>
  </van-step>
  <van-step>
    运输中
    <template #default>
      <p class="log">包裹已到达【广州白云转运中心】</p>
      <p class="time">11-08 21:40</p>
    </template>
  </van-step>
  <van-step>
    已发货
    <template #default>
      <p class="log">商家已发货,快递单号 SF1388xxxx</p>
      <p class="time">11-08 10:05</p>
    </template>
  </van-step>
</van-steps>
属性(Steps)说明
active当前步骤索引,从 0 开始
directionhorizontal 横向 / vertical 纵向
active-color / inactive-color激活与未激活的颜色
active-icon / inactive-icon激活与未激活的图标
finish-icon已完成步骤的图标

组合实战:待支付订单详情页

把两个组件拼成完整的订单页骨架:倒计时归零自动切状态,步骤条随支付状态推进:

html order-detail.vue
<template>
  <header class="status-header" :class="{ expired: expired }">
    <template v-if="!expired">
      <p class="status-text">等待付款</p>
      <p class="count-line">
        剩余
        <van-count-down
          :time="remainMs"
          format="mm 分 ss 秒"
          style="--van-count-down-text-color: #fff"
          @finish="expired = true"
        />
        自动关闭订单
      </p>
    </template>
    <template v-else>
      <p class="status-text">订单已超时关闭</p>
    </template>
  </header>

  <van-steps :active="stepIndex" active-color="#07c160">
    <van-step>提交订单</van-step>
    <van-step>付款成功</van-step>
    <van-step>商家发货</van-step>
    <van-step>交易完成</van-step>
  </van-steps>

  <footer v-if="!expired">
    <van-button round type="danger" block @click="onPay">
      立即支付 ¥{{ amount }}
    </van-button>
  </footer>
</template>

<script setup>
import { computed, onMounted, ref } from "vue";
import { showToast } from "vant";

const props = defineProps({
  status: { type: Number, default: 1 },
  expireAt: { type: Number, required: true },
  amount: { type: String, default: "0.00" },
});

const expired = ref(false);
const remainMs = ref(0);

onMounted(() => {
  remainMs.value = Math.max(0, props.expireAt - Date.now());
  if (remainMs.value === 0) expired.value = true;
});

const stepIndex = computed(() =>
  ({ 1: 0, 2: 1, 3: 2, 4: 3 }[props.status] ?? 0)
);

const onPay = () => showToast("调起收银台...");
</script>
✅
关键点:倒计时 finish 与 expired 状态联动,超时后立即隐藏支付按钮并切换步骤条文案,避免用户对已关闭订单继续支付。

CountDown 属性速查

属性 / 事件说明
time倒计时时长,毫秒
format格式:DD 天 HH 时 mm 分 ss 秒
millisecond是否开启毫秒级渲染
auto-start是否自动开始,false 时配合 ref.start() 手动开始
@change / @finish每秒触发 / 归零触发

总结

CountDown 记住三件事:time 单位毫秒、剩余时间用服务端时间戳算差值、finish 里做超时兜底;Steps 记住一件事:active 是索引,用 computed 把后端状态码映射过去。两者组合出的订单页,就是移动端电商最常见的状态机界面。

把状态可视化,用户就不需要客服。