鸿蒙OS&UniApp实现的倒计时功能与倒计时组件(鸿蒙系统适配版)#三方框架 #Uniapp

UniApp实现的倒计时功能与倒计时组件(鸿蒙系统适配版)

前言

在移动应用开发中,倒计时功能是一个常见而实用的需求,比如秒杀活动倒计时、验证码重发倒计时、限时优惠等场景。本文将详细介绍如何使用UniApp框架开发一个功能完善、性能优异的倒计时组件,并重点关注在鸿蒙系统下的特殊适配,确保组件在华为设备上也能完美运行。

需求分析

一个优秀的倒计时组件应具备以下功能:

  1. 精确的时间计算
  2. 多种格式的时间展示(天时分秒、时分秒、分秒等)
  3. 自定义样式
  4. 事件通知(开始、结束、每秒更新等)
  5. 支持暂停、恢复、重置等操作
  6. 适配多端,特别是鸿蒙系统

技术选型

我们将使用以下技术栈:

  • UniApp作为跨平台开发框架
  • Vue3组合式API提供响应式编程体验
  • TypeScript增强代码的类型安全
  • 秒级计时使用setTimeout实现,毫秒级计时使用requestAnimationFrame
  • 鸿蒙系统特有API调用

基础倒计时组件实现

核心逻辑设计

首先,我们来实现一个基础的倒计时组件:

vue 复制代码
<template>
  <view class="countdown-container" :class="{'harmony-countdown': isHarmony}">
    <text v-if="showDays && formattedTime.days > 0" class="countdown-item days">
      {{ formattedTime.days }}
    </text>
    <text v-if="showDays && formattedTime.days > 0" class="countdown-separator">天</text>
    
    <text class="countdown-item hours">{{ formattedTime.hours }}</text>
    <text class="countdown-separator">:</text>
    
    <text class="countdown-item minutes">{{ formattedTime.minutes }}</text>
    <text class="countdown-separator">:</text>
    
    <text class="countdown-item seconds">{{ formattedTime.seconds }}</text>
    
    <text v-if="showMilliseconds" class="countdown-separator">.</text>
    <text v-if="showMilliseconds" class="countdown-item milliseconds">
      {{ formattedTime.milliseconds }}
    </text>
  </view>
</template>

<script lang="ts">
import { defineComponent, ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue';
import { isHarmonyOS } from '@/utils/platform';

interface TimeObject {
  days: string;
  hours: string;
  minutes: string;
  seconds: string;
  milliseconds: string;
}

export default defineComponent({
  name: 'CountdownTimer',
  props: {
    endTime: {
      type: [Number, String, Date],
      required: true
    },
    format: {
      type: String,
      default: 'HH:mm:ss'
    },
    showDays: {
      type: Boolean,
      default: false
    },
    showMilliseconds: {
      type: Boolean,
      default: false
    },
    autoStart: {
      type: Boolean,
      default: true
    },
    millisecondPrecision: {
      type: Boolean,
      default: false
    }
  },
  emits: ['start', 'end', 'update', 'pause', 'resume'],
  setup(props, { emit }) {
    // 剩余时间(毫秒)
    const remainingTime = ref(0);
    // 倒计时状态
    const isPaused = ref(!props.autoStart);
    // 是否已结束
    const isEnded = ref(false);
    // 是否为鸿蒙系统
    const isHarmony = ref(false);
    
    // 计时器ID
    let timerId: number | null = null;
    let rafId: number | null = null;
    
    // 上一次更新时间
    let lastUpdateTime = 0;
    
    // 格式化后的时间对象
    const formattedTime = computed((): TimeObject => {
      const total = remainingTime.value;
      
      // 计算天、时、分、秒、毫秒
      const days = Math.floor(total / (1000 * 60 * 60 * 24));
      const hours = Math.floor((total % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
      const minutes = Math.floor((total % (1000 * 60 * 60)) / (1000 * 60));
      const seconds = Math.floor((total % (1000 * 60)) / 1000);
      const milliseconds = Math.floor((total % 1000) / 10);
      
      // 格式化
      return {
        days: days.toString().padStart(2, '0'),
        hours: hours.toString().padStart(2, '0'),
        minutes: minutes.toString().padStart(2, '0'),
        seconds: seconds.toString().padStart(2, '0'),
        milliseconds: milliseconds.toString().padStart(2, '0')
      };
    });
    
    // 计算结束时间的时间戳
    const getEndTimeStamp = (): number => {
      if (typeof props.endTime === 'number') {
        return props.endTime;
      } else if (typeof props.endTime === 'string') {
        return new Date(props.endTime).getTime();
      } else if (props.endTime instanceof Date) {
        return props.endTime.getTime();
      }
      return 0;
    };
    
    // 初始化倒计时
    const initCountdown = () => {
      const endTimeStamp = getEndTimeStamp();
      const now = Date.now();
      
      if (endTimeStamp <= now) {
        remainingTime.value = 0;
        isEnded.value = true;
        emit('end');
        return;
      }
      
      remainingTime.value = endTimeStamp - now;
      
      if (!isPaused.value) {
        startCountdown();
        emit('start');
      }
    };
    
    // 开始倒计时
    const startCountdown = () => {
      if (isPaused.value || isEnded.value) return;
      
      // 记录开始时间
      lastUpdateTime = Date.now();
      
      // 根据精度选择不同的计时方法
      if (props.millisecondPrecision) {
        requestAnimationFrameTimer();
      } else {
        setTimeoutTimer();
      }
    };
    
    // setTimeout计时器
    const setTimeoutTimer = () => {
      if (timerId !== null) clearTimeout(timerId);
      
      timerId = setTimeout(() => {
        updateTime();
        
        if (remainingTime.value > 0 && !isPaused.value) {
          setTimeoutTimer();
        }
      }, 1000) as unknown as number;
    };
    
    // requestAnimationFrame计时器
    const requestAnimationFrameTimer = () => {
      if (rafId !== null) cancelAnimationFrame(rafId);
      
      const frame = () => {
        updateTime();
        
        if (remainingTime.value > 0 && !isPaused.value) {
          rafId = requestAnimationFrame(frame);
        }
      };
      
      rafId = requestAnimationFrame(frame);
    };
    
    // 更新时间
    const updateTime = () => {
      const now = Date.now();
      const deltaTime = now - lastUpdateTime;
      lastUpdateTime = now;
      
      remainingTime.value -= deltaTime;
      
      if (remainingTime.value <= 0) {
        remainingTime.value = 0;
        isEnded.value = true;
        emit('end');
        
        if (timerId !== null) {
          clearTimeout(timerId);
          timerId = null;
        }
        if (rafId !== null) {
          cancelAnimationFrame(rafId);
          rafId = null;
        }
      } else {
        emit('update', formattedTime.value);
      }
    };
    
    // 暂停倒计时
    const pause = () => {
      if (isPaused.value || isEnded.value) return;
      
      isPaused.value = true;
      
      if (timerId !== null) {
        clearTimeout(timerId);
        timerId = null;
      }
      if (rafId !== null) {
        cancelAnimationFrame(rafId);
        rafId = null;
      }
      
      emit('pause');
    };
    
    // 恢复倒计时
    const resume = () => {
      if (!isPaused.value || isEnded.value) return;
      
      isPaused.value = false;
      lastUpdateTime = Date.now();
      startCountdown();
      
      emit('resume');
    };
    
    // 重置倒计时
    const reset = () => {
      pause();
      initCountdown();
      if (props.autoStart) {
        resume();
      }
    };
    
    // 暴露方法给父组件
    const publicMethods = {
      pause,
      resume,
      reset
    };
    
    // 监听endTime变化
    watch(() => props.endTime, reset);
    
    onMounted(() => {
      isHarmony.value = isHarmonyOS();
      initCountdown();
    });
    
    onUnmounted(() => {
      if (timerId !== null) {
        clearTimeout(timerId);
      }
      if (rafId !== null) {
        cancelAnimationFrame(rafId);
      }
    });
    
    return {
      remainingTime,
      formattedTime,
      isPaused,
      isEnded,
      isHarmony,
      ...publicMethods
    };
  }
});
</script>

<style>
.countdown-container {
  display: flex;
  align-items: center;
}

.countdown-item {
  background-color: #f1f1f1;
  padding: 4rpx 8rpx;
  border-radius: 6rpx;
  min-width: 40rpx;
  text-align: center;
  font-weight: bold;
}

.countdown-separator {
  margin: 0 6rpx;
}

/* 鸿蒙系统专属样式 */
.harmony-countdown .countdown-item {
  background: linear-gradient(to bottom, #f5f5f5, #e8e8e8);
  border-radius: 12rpx;
  box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1);
  color: #333;
  font-family: 'HarmonyOS Sans', sans-serif;
}
</style>

鸿蒙系统适配工具函数

为了更好地适配鸿蒙系统,我们需要一些工具函数来检测系统环境并进行相应调整:

ts 复制代码
// utils/platform.ts

/**
 * 检测当前环境是否为鸿蒙系统
 */
export function isHarmonyOS(): boolean {
  // #ifdef APP-PLUS
  const systemInfo = uni.getSystemInfoSync();
  const systemName = systemInfo.osName || '';
  const systemVersion = systemInfo.osVersion || '';
  
  // 鸿蒙系统识别
  return systemName.toLowerCase().includes('harmony') || 
         (systemName === 'android' && systemVersion.includes('harmony'));
  // #endif
  
  return false;
}

/**
 * 优化鸿蒙系统下的定时器性能
 */
export function optimizeHarmonyTimer(): void {
  // #ifdef APP-PLUS
  if (!isHarmonyOS()) return;
  
  try {
    // 在鸿蒙系统中优化定时器性能
    // 1. 使用plus API进行原生层优化
    if (plus && plus.device && plus.device.setWakeLock) {
      // 防止设备休眠影响计时器精度
      plus.device.setWakeLock(true);
    }
    
    // 2. 优化requestAnimationFrame循环
    if (typeof requestAnimationFrame !== 'undefined') {
      const originalRAF = window.requestAnimationFrame;
      window.requestAnimationFrame = function(callback) {
        return originalRAF.call(window, (timestamp) => {
          // 在鸿蒙系统上增加高精度时间戳处理
          if (window.performance && window.performance.now) {
            timestamp = window.performance.now();
          }
          return callback(timestamp);
        });
      };
    }
  } catch (e) {
    console.error('鸿蒙计时器优化失败', e);
  }
  // #endif
}

/**
 * 释放鸿蒙系统优化资源
 */
export function releaseHarmonyOptimization(): void {
  // #ifdef APP-PLUS
  if (!isHarmonyOS()) return;
  
  try {
    if (plus && plus.device && plus.device.setWakeLock) {
      // 释放锁屏
      plus.device.setWakeLock(false);
    }
  } catch (e) {
    console.error('释放鸿蒙优化资源失败', e);
  }
  // #endif
}

高级倒计时功能

1. 秒杀倒计时组件

秒杀倒计时通常需要展示天、时、分、秒,并且有特定的样式和动画效果:

vue 复制代码
<template>
  <view class="seckill-countdown">
    <view class="seckill-title">
      <text>{{title}}</text>
    </view>
    
    <view class="seckill-timer">
      <view class="time-block" v-if="showDays">
        <text class="time-num">{{formattedTime.days}}</text>
        <text class="time-unit">天</text>
      </view>
      
      <view class="time-block">
        <text class="time-num">{{formattedTime.hours}}</text>
        <text class="time-unit">时</text>
      </view>
      
      <view class="time-block">
        <text class="time-num">{{formattedTime.minutes}}</text>
        <text class="time-unit">分</text>
      </view>
      
      <view class="time-block">
        <text class="time-num">{{formattedTime.seconds}}</text>
        <text class="time-unit">秒</text>
      </view>
    </view>
    
    <view class="seckill-status">
      <text v-if="isEnded">活动已结束</text>
      <text v-else>火热抢购中</text>
    </view>
  </view>
</template>

<script>
import CountdownTimer from '@/components/CountdownTimer.vue';
import { computed, ref } from 'vue';

export default {
  name: 'SeckillCountdown',
  props: {
    title: {
      type: String,
      default: '限时秒杀'
    },
    endTime: {
      type: [Number, String, Date],
      required: true
    },
    showDays: {
      type: Boolean,
      default: true
    }
  },
  setup(props) {
    // 复用基础倒计时组件逻辑
    const countdownRef = ref(null);
    const remainingTime = ref(0);
    const isEnded = ref(false);
    
    // 格式化时间
    const formattedTime = computed(() => {
      const total = remainingTime.value;
      
      const days = Math.floor(total / (1000 * 60 * 60 * 24));
      const hours = Math.floor((total % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
      const minutes = Math.floor((total % (1000 * 60 * 60)) / (1000 * 60));
      const seconds = Math.floor((total % (1000 * 60)) / 1000);
      
      return {
        days: days.toString().padStart(2, '0'),
        hours: hours.toString().padStart(2, '0'),
        minutes: minutes.toString().padStart(2, '0'),
        seconds: seconds.toString().padStart(2, '0')
      };
    });
    
    // 更新剩余时间
    const updateRemainingTime = () => {
      const endTimeStamp = typeof props.endTime === 'number' ? 
                          props.endTime : new Date(props.endTime).getTime();
      const now = Date.now();
      
      if (endTimeStamp <= now) {
        remainingTime.value = 0;
        isEnded.value = true;
        return;
      }
      
      remainingTime.value = endTimeStamp - now;
      
      // 使用setTimeout模拟倒计时
      setTimeout(updateRemainingTime, 1000);
    };
    
    // 初始化
    updateRemainingTime();
    
    return {
      countdownRef,
      formattedTime,
      isEnded,
      showDays: props.showDays
    };
  }
};
</script>

<style>
.seckill-countdown {
  background: linear-gradient(135deg, #ff6b6b, #ff3366);
  padding: 20rpx;
  border-radius: 16rpx;
  color: #fff;
}

.seckill-title {
  font-size: 32rpx;
  font-weight: bold;
  margin-bottom: 12rpx;
}

.seckill-timer {
  display: flex;
  justify-content: center;
  margin: 16rpx 0;
}

.time-block {
  position: relative;
  margin: 0 8rpx;
  text-align: center;
}

.time-num {
  display: block;
  background-color: rgba(0, 0, 0, 0.2);
  border-radius: 8rpx;
  padding: 8rpx 12rpx;
  min-width: 60rpx;
  font-size: 32rpx;
  font-weight: bold;
}

.time-unit {
  display: block;
  font-size: 24rpx;
  margin-top: 4rpx;
}

.seckill-status {
  font-size: 28rpx;
  text-align: center;
  margin-top: 10rpx;
}

/* 动画效果 */
@keyframes pulse {
  0% { transform: scale(1); }
  50% { transform: scale(1.05); }
  100% { transform: scale(1); }
}

.time-num {
  animation: pulse 1s infinite;
}
</style>

2. 验证码倒计时按钮

验证码倒计时是移动应用中的另一个常见场景:

vue 复制代码
<template>
  <view class="verification-code">
    <input 
      type="text" 
      class="code-input" 
      placeholder="请输入验证码" 
      maxlength="6"
      v-model="code"
    />
    
    <button 
      class="send-btn"
      :disabled="isCounting || !isMobileValid"
      :class="{'counting': isCounting, 'harmony-btn': isHarmony}"
      @click="sendCode"
    >
      <text v-if="!isCounting">获取验证码</text>
      <text v-else>{{countdown}}秒后重发</text>
    </button>
  </view>
</template>

<script lang="ts">
import { defineComponent, ref, computed, onUnmounted } from 'vue';
import { isHarmonyOS } from '@/utils/platform';

export default defineComponent({
  name: 'VerificationCodeButton',
  props: {
    mobile: {
      type: String,
      default: ''
    },
    duration: {
      type: Number,
      default: 60
    }
  },
  emits: ['send'],
  setup(props, { emit }) {
    const code = ref('');
    const countdown = ref(props.duration);
    const isCounting = ref(false);
    const isHarmony = ref(false);
    
    let timer: number | null = null;
    
    // 验证手机号是否有效
    const isMobileValid = computed(() => {
      return /^1[3-9]\d{9}$/.test(props.mobile);
    });
    
    // 发送验证码
    const sendCode = () => {
      if (isCounting.value || !isMobileValid.value) return;
      
      // 触发发送事件
      emit('send', props.mobile);
      
      // 开始倒计时
      startCountdown();
    };
    
    // 开始倒计时
    const startCountdown = () => {
      isCounting.value = true;
      countdown.value = props.duration;
      
      timer = setInterval(() => {
        if (countdown.value <= 1) {
          stopCountdown();
        } else {
          countdown.value--;
        }
      }, 1000) as unknown as number;
    };
    
    // 停止倒计时
    const stopCountdown = () => {
      if (timer !== null) {
        clearInterval(timer);
        timer = null;
      }
      isCounting.value = false;
      countdown.value = props.duration;
    };
    
    // 检测系统环境
    onMounted(() => {
      isHarmony.value = isHarmonyOS();
    });
    
    // 组件销毁时清理定时器
    onUnmounted(() => {
      if (timer !== null) {
        clearInterval(timer);
      }
    });
    
    return {
      code,
      countdown,
      isCounting,
      isMobileValid,
      isHarmony,
      sendCode
    };
  }
});
</script>

<style>
.verification-code {
  display: flex;
  align-items: center;
  width: 100%;
}

.code-input {
  flex: 1;
  height: 80rpx;
  border: 1rpx solid #ddd;
  border-radius: 8rpx;
  padding: 0 20rpx;
  margin-right: 20rpx;
}

.send-btn {
  width: 220rpx;
  height: 80rpx;
  line-height: 80rpx;
  text-align: center;
  font-size: 28rpx;
  color: #fff;
  background-color: #007aff;
  border-radius: 8rpx;
}

.send-btn:disabled {
  background-color: #ccc;
  color: #fff;
}

.send-btn.counting {
  background-color: #f5f5f5;
  color: #999;
  border: 1rpx solid #ddd;
}

/* 鸿蒙系统特有样式 */
.harmony-btn {
  border-radius: 20rpx;
  background: linear-gradient(to bottom, #0091ff, #007aff);
  font-family: 'HarmonyOS Sans', sans-serif;
  box-shadow: 0 4rpx 12rpx rgba(0, 122, 255, 0.3);
}

.harmony-btn:disabled {
  background: #e1e1e1;
  box-shadow: none;
}

.harmony-btn.counting {
  background: #f8f8f8;
  color: #666;
  border: none;
  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
</style>

鸿蒙系统适配关键点

在为鸿蒙系统进行适配时,需要注意以下几个方面:

1. 性能优化

鸿蒙系统的渲染引擎与Android有所不同,因此在处理高频刷新的倒计时组件时需要特别注意性能优化:

  1. 使用requestAnimationFrame代替setInterval:对于毫秒级的倒计时,使用requestAnimationFrame可以获得更平滑的效果。

  2. 避免DOM频繁更新:减少不必要的DOM更新,可以使用计算属性缓存格式化后的时间。

  3. 使用硬件加速 :在样式中添加transform: translateZ(0)来启用硬件加速。

css 复制代码
.countdown-item {
  transform: translateZ(0);
  backface-visibility: hidden;
}

2. UI适配

鸿蒙系统有其独特的设计语言,需要对UI进行相应调整:

  1. 字体适配:鸿蒙系统推荐使用HarmonyOS Sans字体。

  2. 圆角和阴影:鸿蒙系统的设计更倾向于使用较大的圆角和更柔和的阴影。

  3. 渐变色:适当使用渐变色可以使UI更符合鸿蒙系统风格。

3. 系统API适配

鸿蒙系统提供了一些特有的API,可以利用这些API提升用户体验:

js 复制代码
// #ifdef APP-PLUS
if (isHarmonyOS()) {
  // 使用鸿蒙特有的震动反馈API
  if (plus.os.name === 'Android' && plus.device.vendor === 'HUAWEI') {
    try {
      // 调用华为设备的特殊振动API
      plus.device.vibrate(10);
    } catch (e) {
      console.error('震动API调用失败', e);
    }
  }
}
// #endif

应用场景

电商秒杀

在电商应用中,秒杀活动是一个典型的倒计时应用场景。用户需要清楚地看到活动开始还有多长时间,或者活动结束还有多长时间。

验证码获取

注册或登录时,发送验证码后通常需要倒计时防止用户频繁点击发送按钮。

考试计时

在在线教育应用中,考试时间倒计时是一个重要功能,需要精确的时间显示。

健身计时器

在健身应用中,可以用倒计时组件实现HIIT训练计时器、休息时间倒计时等功能。

性能优化技巧

1. 使用防抖和节流

对于用户交互频繁的场景,可以使用防抖和节流技术避免不必要的计算:

js 复制代码
// 防抖函数
function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

// 节流函数
function throttle(fn, interval) {
  let last = 0;
  return function(...args) {
    const now = Date.now();
    if (now - last >= interval) {
      last = now;
      fn.apply(this, args);
    }
  };
}

2. 使用Web Worker

对于复杂的时间计算,可以使用Web Worker在后台线程中进行,避免阻塞主线程:

js 复制代码
// 创建Worker
const worker = new Worker('/countdown-worker.js');

// 监听Worker消息
worker.onmessage = function(e) {
  remainingTime.value = e.data.remainingTime;
};

// 发送消息给Worker
worker.postMessage({
  action: 'start',
  endTime: getEndTimeStamp(),
  interval: 1000
});

// 组件销毁时终止Worker
onUnmounted(() => {
  worker.terminate();
});

3. 使用RAF优化动画

对于需要流畅动画效果的倒计时,使用requestAnimationFrame可以获得更好的性能:

js 复制代码
let lastTime = 0;
let rafId = null;

function animate(timestamp) {
  if (!lastTime) lastTime = timestamp;
  const deltaTime = timestamp - lastTime;
  
  if (deltaTime >= 16) { // 约60fps
    lastTime = timestamp;
    updateCountdown();
  }
  
  rafId = requestAnimationFrame(animate);
}

rafId = requestAnimationFrame(animate);

// 清理
function cleanup() {
  if (rafId) {
    cancelAnimationFrame(rafId);
    rafId = null;
  }
}

总结

本文详细介绍了如何使用UniApp实现功能完善的倒计时组件,并重点关注了鸿蒙系统的适配问题。通过合理的架构设计、性能优化和UI适配,我们可以开发出在各平台(特别是鸿蒙系统)上都能流畅运行的倒计时组件。

倒计时虽然是一个看似简单的功能,但要实现精确、稳定且性能良好的倒计时组件,需要考虑许多细节问题。希望本文对你在UniApp开发中实现倒计时功能有所帮助。

参考资源

  1. UniApp官方文档
  2. 鸿蒙系统设计规范
  3. Vue3官方文档
  4. JavaScript计时器详解
相关推荐
AIGC魔法师26 分钟前
AI 极客低代码平台快速上手 --生成Python代码
开发语言·harmonyos·低代码平台·ai极客
Bruce_Liuxiaowei2 小时前
HarmonyOS NEXT~鸿蒙系统与mPaaS三方框架集成指南
华为·harmonyos·鸿蒙·鸿蒙系统
知识点集锦4 小时前
4-5月份,思科,华为,微软,个别考试战报分享
网络·学习·安全·华为·云计算
通义灵码5 小时前
如何使用通义灵码辅助开发鸿蒙OS - AI编程助手提升效率
华为·ai编程·harmonyos·通义灵码
Bruce_Liuxiaowei6 小时前
HarmonyOS NEXT~鸿蒙系统与Uniapp跨平台开发实践指南
华为·uni-app·harmonyos
tryCbest7 小时前
uniapp如何设置uni.request可变请求ip地址
网络协议·tcp/ip·uni-app
英码科技9 小时前
AI筑基,新质跃升|英码科技亮相华为广东新质生产力创新峰会,发布大模型一体机新品,助力产业智能化转型
人工智能·科技·华为
陈杨_9 小时前
HarmonyOS5云服务技术分享--认证文档问题
华为·echarts·创业创新·harmonyos
老李不敲代码10 小时前
榕壹云上门家政系统:基于Spring Boot+MySQL+UniApp的全能解决方案
spring boot·mysql·微信小程序·小程序·uni-app