当前位置: 首页 > news >正文

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

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

前言

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

需求分析

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

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

技术选型

我们将使用以下技术栈:

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

基础倒计时组件实现

核心逻辑设计

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

<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);// 计时器IDlet 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>

鸿蒙系统适配工具函数

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

// utils/platform.ts/*** 检测当前环境是否为鸿蒙系统*/
export function isHarmonyOS(): boolean {// #ifdef APP-PLUSconst systemInfo = uni.getSystemInfoSync();const systemName = systemInfo.osName || '';const systemVersion = systemInfo.osVersion || '';// 鸿蒙系统识别return systemName.toLowerCase().includes('harmony') || (systemName === 'android' && systemVersion.includes('harmony'));// #endifreturn false;
}/*** 优化鸿蒙系统下的定时器性能*/
export function optimizeHarmonyTimer(): void {// #ifdef APP-PLUSif (!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-PLUSif (!isHarmonyOS()) return;try {if (plus && plus.device && plus.device.setWakeLock) {// 释放锁屏plus.device.setWakeLock(false);}} catch (e) {console.error('释放鸿蒙优化资源失败', e);}// #endif
}

高级倒计时功能

1. 秒杀倒计时组件

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

<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. 验证码倒计时按钮

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

<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)来启用硬件加速。

.countdown-item {transform: translateZ(0);backface-visibility: hidden;
}

2. UI适配

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

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

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

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

3. 系统API适配

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

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

应用场景

电商秒杀

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

验证码获取

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

考试计时

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

健身计时器

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

性能优化技巧

1. 使用防抖和节流

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

// 防抖函数
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在后台线程中进行,避免阻塞主线程:

// 创建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可以获得更好的性能:

let lastTime = 0;
let rafId = null;function animate(timestamp) {if (!lastTime) lastTime = timestamp;const deltaTime = timestamp - lastTime;if (deltaTime >= 16) { // 约60fpslastTime = 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计时器详解

相关文章:

  • 低损耗高效能100G O Band DWDM 10km光模块 | 支持密集波分复用
  • Elasticsearch 快速入门指南
  • ChromaDB 向量库优化技巧实战
  • SymPy | 使用SymPy求解多元非线性方程组
  • 合并两个有序数组的高效算法详解
  • 1.1 认识编程与C++
  • 黑马k8s(七)
  • 腾讯开源实时语音大模型VITA-audio,92mstoken极速响应,支持多语言~
  • 麒麟v10 部署 MySQL 5.6.10 完整步骤
  • javaSE.迭代器
  • AI Agent开发第67课-彻底消除RAG知识库幻觉-文档分块全技巧(1)
  • 密码学刷题小记录
  • QML学习01(设置宽度、高度、坐标点、标题,信号与槽,键盘事件)
  • 网页渲染的两条赛道
  • 【高斯拟合】不用库手写高斯拟合算法:从最小二乘到拟合参数推导
  • 牛客网NC22012:判断闰年问题详解
  • [c语言日寄]数据结构:栈
  • RAGFlow 中的 Rerank 和 Recall 解释
  • 大数据架构选型全景指南:核心架构对比与实战案例 解析
  • 吊舱热敏传感器抗干扰技术分析!
  • 李成钢:近期个别经济体实施所谓“对等关税”,严重违反世贸组织规则
  • 定制基因编辑疗法治愈罕见遗传病患儿
  • 董军同德国国防部长举行会谈
  • 终于越过萨巴伦卡这座高山,郑钦文感谢自己的耐心和专注
  • 沙青青评《通勤梦魇》︱“人机组合”的通勤之路
  • AI含量非常高,2025上海教育博览会将于本周五开幕