使用uniapp,实现根据时间倒计时执行进度条变化

一、功能清单(已全部实现)

2️⃣ v-model 控制开始 / 暂停 / 继续

  • v-model:running 控制进度条运行状态
  • 外部随时暂停 / 恢复

3️⃣ 可作为公共组件(uniapp / Vue2)

  • props + emit 规范
  • 可直接放到 components/ProgressBar/index.vue
  • 也可无痛迁移到 uni_modules

4️⃣ 主题化(高度 / 颜色 / icon 可配置)

  • 渐变色可配置
  • 危险阈值可配置
  • icon 可配置

二、组件使用方式示例(先看用法)

vue 复制代码
<ProgressBar
  v-model:running="running"
  :totalTime="180"
  icon="⚡"
  :dangerTime="15"
  :height="20"
/>

<button @click="running = !running">
  {{ running ? '暂停' : '继续' }}
</button>

三、完整公共组件(每一行都有注释

📁 components/ProgressBar/index.vue

vue 复制代码
<template>
  <!-- 整体容器,作为相对定位基准 -->
  <view class="progress-wrapper">

    <!-- 跟随进度移动的图标 -->
    <view
      class="progress-icon"
      :style="{ left: iconLeft + '%' }"
    >
      {{ icon }}
    </view>

    <!-- 进度条轨道 -->
    <view
      class="progress-track"
      :style="{ height: height + 'rpx' }"
    >
      <!-- 实际进度条 -->
      <view
        class="progress-bar"
        :class="{ danger: isDanger }"
        :style="{ width: progress + '%', background: barGradient }"
      >
        <!-- 高光流动效果 -->
        <view class="progress-shine"></view>
      </view>
    </view>

    <!-- 倒计时文本 -->
    <view class="time-text">
      {{ formatTime(remainTime) }}
    </view>

  </view>
</template>

<script>
export default {
  name: "ProgressBar",

  /* ================= props ================= */
  props: {
    // 总时长(秒)
    totalTime: {
      type: Number,
      default: 180,
    },

    // v-model:running 控制运行状态
    running: {
      type: Boolean,
      default: false,
    },

    // 危险时间阈值(秒)
    dangerTime: {
      type: Number,
      default: 15,
    },

    // 进度条高度(rpx)
    height: {
      type: Number,
      default: 20,
    },

    // 图标内容
    icon: {
      type: String,
      default: "⚡",
    },

    // 正常渐变色
    gradient: {
      type: String,
      default: "linear-gradient(90deg,#ff6a00,#ff9f00,#ff6a00)",
    },

    // 危险状态渐变色
    dangerGradient: {
      type: String,
      default: "linear-gradient(90deg,#ff3b30,#ff6a00)",
    },
  },

  /* ================= emits ================= */
  emits: ["update:running", "finish"],

  data() {
    return {
      // 剩余时间(毫秒)
      remainTime: this.totalTime * 1000,

      // requestAnimationFrame id
      rafId: null,

      // 开始时间戳
      startTime: 0,
    };
  },

  /* ================= 计算属性 ================= */
  computed: {
    // 当前进度百分比(0 → 100)
    progress() {
      const elapsed = this.totalTime * 1000 - this.remainTime;
      return Math.min((elapsed / (this.totalTime * 1000)) * 100, 100);
    },

    // 图标位置(防止超出边界)
    iconLeft() {
      return Math.min(Math.max(this.progress, 0), 100);
    },

    // 是否进入危险状态
    isDanger() {
      return this.remainTime / 1000 <= this.dangerTime;
    },

    // 当前进度条渐变色
    barGradient() {
      return this.isDanger ? this.dangerGradient : this.gradient;
    },
  },

  /* ================= 监听 ================= */
  watch: {
    // 外部控制 running
    running(val) {
      val ? this.start() : this.stop();
    },
  },

  mounted() {
    // 初始 running 为 true 则直接启动
    if (this.running) {
      this.start();
    }
  },

  beforeDestroy() {
    // 组件销毁时清理动画
    cancelAnimationFrame(this.rafId);
  },

  /* ================= 方法 ================= */
  methods: {
    // 启动倒计时
    start() {
      // 防止重复启动
      if (this.rafId) return;

      // 记录开始时间
      this.startTime = Date.now();

      // 帧循环
      const tick = () => {
        const elapsed = Date.now() - this.startTime;

        // 计算剩余时间
        this.remainTime = Math.max(
          this.remainTime - elapsed,
          0
        );

        // 更新时间基准
        this.startTime = Date.now();

        // 继续 or 结束
        if (this.remainTime > 0 && this.running) {
          this.rafId = requestAnimationFrame(tick);
        } else {
          this.finish();
        }
      };

      this.rafId = requestAnimationFrame(tick);
    },

    // 暂停倒计时
    stop() {
      cancelAnimationFrame(this.rafId);
      this.rafId = null;
    },

    // 结束
    finish() {
      this.stop();
      this.$emit("update:running", false);
      this.$emit("finish");
    },

    // 格式化时间 mm:ss
    formatTime(ms) {
      const s = Math.floor(ms / 1000);
      const m = Math.floor(s / 60);
      const sec = s % 60;
      return `${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
    },
  },
};
</script>

<style scoped lang="scss">
/* 外层容器 */
.progress-wrapper {
  position: relative;
  padding-top: 40rpx;
}

/* 进度条轨道 */
.progress-track {
  background: rgba(255, 255, 255, 0.15);
  border-radius: 999rpx;
  overflow: hidden;
}

/* 主进度条 */
.progress-bar {
  height: 100%;
  border-radius: 999rpx;
  position: relative;
  transition: width 0.08s linear;
}

/* 流动条纹 */
.progress-bar::after {
  content: "";
  position: absolute;
  inset: 0;
  background-size: 40rpx 40rpx;
  background-image: linear-gradient(
    -45deg,
    rgba(255,255,255,.25) 25%,
    transparent 25%,
    transparent 50%,
    rgba(255,255,255,.25) 50%,
    rgba(255,255,255,.25) 75%,
    transparent 75%
  );
  animation: stripe 2.5s linear infinite;
}

/* 危险状态加速 */
.progress-bar.danger::after {
  animation-duration: 1.2s;
}

/* 高光 */
.progress-shine {
  position: absolute;
  left: -40%;
  width: 40%;
  height: 100%;
  background: linear-gradient(
    120deg,
    transparent,
    rgba(255,255,255,.6),
    transparent
  );
  animation: shine 1.2s infinite;
}

/* 图标 */
.progress-icon {
  position: absolute;
  top: 0;
  transform: translate(-50%, -100%);
  font-size: 36rpx;
  transition: left 0.08s linear;
  animation: float 1.6s ease-in-out infinite;
}

/* 时间文字 */
.time-text {
  margin-top: 20rpx;
  text-align: center;
  font-size: 26rpx;
  color: #ff7a18;
}

/* 动画定义 */
@keyframes stripe {
  to { background-position: 80rpx 80rpx; }
}
@keyframes shine {
  to { left: 120%; }
}
@keyframes float {
  50% { transform: translate(-50%, -120%); }
}
</style>
相关推荐
科技D人生2 小时前
Vue.js 学习总结(19)—— Vue3 按钮防重复点击三种方案总结
前端·vue.js·uniapp·vue3 防重复提交·uniapp 防重复提交·前端防抖
指尖跳动的光2 小时前
前端视角-如何保证系统稳定性
前端
John_ToDebug2 小时前
Chromium WebUI 定制实践:从 C++ 注入到 JS 安全展示全链路解析
javascript·c++·chrome
fruge2 小时前
2025全栈技术深耕与实践:从框架融合到工程落地
前端
拼命_小李2 小时前
使用intro.js实现简单下一步引导demo
javascript
长不大的蜡笔小新2 小时前
私人健身房管理系统
java·javascript·spring boot
Hzsilvana2 小时前
踩坑日记:Uniapp项目定位偏差与依赖更新的真相
uni-app
秋4272 小时前
tomcat与web服务器
服务器·前端·tomcat
hdsoft_huge3 小时前
Java 实现高效查询海量 geometry 及 Protobuf 序列化与天地图前端分片加载
java·前端·状态模式