使用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>
相关推荐
能源革命2 分钟前
Vue 网格拖拽布局组件全景指南:7 大方案深度对比
前端·javascript·vue.js
知野小兔15 分钟前
JavaScript中Map和Set详解
javascript
deli00725 分钟前
Skill 怎么写才被 Agent 命中?18 条规则做成 SKILL.md 校验器,规范样例 92 分
前端
Hum8le26 分钟前
CTF题目《easy_web》(安洵杯 2019 变种 Web)
前端·安全·web安全
coding漫漫长路26 分钟前
二值化后还剩1212个黑点,OCR却认成了乱码
前端
不可能片场28 分钟前
AI视频口型对齐 用音频驱动说话镜头
前端·electron
Lank_M28 分钟前
OCR一个字没认错,倾斜2.5°却把行序排乱了
前端
daols8832 分钟前
vue 表格组件 vxe-table 实现自定义多行编辑功能
前端·javascript·vue.js·vxe-table
不要试图纠正别人36 分钟前
别让大模型算钱:我把优惠券组合优化写成了确定性算法
前端
志尊宝36 分钟前
Vue3 零基础每日笔记(075):图片懒加载与骨架屏
javascript·vue.js·笔记