Vue+CSS 做出的LED时钟太酷了!还能倒计时,代码全开源

前言

大家好,我是大华!

今天给大家分享一个超酷的LED数字时钟效果。这个时钟不仅显示当前时间,还有倒计时功能,而且完全由Vue和CSS实现!

为了方便理解,把代码分成了基础款升级款两种,都有完整的源码。

先看看效果:

基础款:

升级款(支持倒计时、颜色参数的配置):

这就是我们今天要做的:七段数码管LED时钟

接下来我们一步步展示讲解实现思路和实现,完整源码在后面。


理解七段数码管

千万不要被它的名字吓到了。

"七段" = 7个发光的小条,它们编号是1到7,位置如下:

复制代码
   ┌───1───┐
   │       │
   6       2
   │       │
   └───7───┘
   │       │
   5       3
   │       │
   └───4───┘

比如要显示数字 1,只需要点亮第2和第3段。

要显示 8,7段全部点亮!

所以,核心思路是:

给每个数字,定义好它该亮哪几段。

然后根据当前时间,动态点亮对应的段。


版本一:基础版

逻辑比较简单,先贴完整代码:

html 复制代码
<template>
  <div class="clock">
    <!-- 小时 -->
    <div class="digit hours">
      <div
        v-for="i in 7"
        :key="'h1-' + i"
        class="segment"
        :class="{ on: getActiveSegments(parseInt(time.hours[0])).includes(i) }"
      ></div>
    </div>
    <div class="digit hours">
      <div
        v-for="i in 7"
        :key="'h2-' + i"
        class="segment"
        :class="{ on: getActiveSegments(parseInt(time.hours[1])).includes(i) }"
      ></div>
    </div>

    <div class="separator"></div>

    <!-- 分钟 -->
    <div class="digit minutes">
      <div
        v-for="i in 7"
        :key="'m1-' + i"
        class="segment"
        :class="{ on: getActiveSegments(parseInt(time.minutes[0])).includes(i) }"
      ></div>
    </div>
    <div class="digit minutes">
      <div
        v-for="i in 7"
        :key="'m2-' + i"
        class="segment"
        :class="{ on: getActiveSegments(parseInt(time.minutes[1])).includes(i) }"
      ></div>
    </div>

    <div class="separator"></div>

    <!-- 秒钟 -->
    <div class="digit seconds">
      <div
        v-for="i in 7"
        :key="'s1-' + i"
        class="segment"
        :class="{ on: getActiveSegments(parseInt(time.seconds[0])).includes(i) }"
      ></div>
    </div>
    <div class="digit seconds">
      <div
        v-for="i in 7"
        :key="'s2-' + i"
        class="segment"
        :class="{ on: getActiveSegments(parseInt(time.seconds[1])).includes(i) }"
      ></div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';

// 七段数码管数字对应的段亮起编号 (1-7)
const digitSegments = [
  [1, 2, 3, 4, 5, 6],     // 0
  [2, 3],                  // 1
  [1, 2, 7, 5, 4],         // 2
  [1, 2, 7, 3, 4],         // 3
  [6, 7, 2, 3],            // 4
  [1, 6, 7, 3, 4],         // 5
  [1, 3, 4, 5, 6, 7],      // 6 
  [1, 2, 3],               // 7
  [1, 2, 3, 4, 5, 6, 7],   // 8
  [1, 2, 3, 4, 6, 7]       // 9
];

const time = ref({
  hours: '00',
  minutes: '00',
  seconds: '00'
});

let intervalId = null;

const updateTime = () => {
  const now = new Date();
  time.value = {
    hours: String(now.getHours()).padStart(2, '0'),
    minutes: String(now.getMinutes()).padStart(2, '0'),
    seconds: String(now.getSeconds()).padStart(2, '0')
  };
};

const getActiveSegments = (digit) => {
  return digitSegments[digit] || [];
};

// 启动定时器
onMounted(() => {
  updateTime();
  intervalId = setInterval(updateTime, 1000);
});

// 清理定时器
onUnmounted(() => {
  if (intervalId) {
    clearInterval(intervalId);
  }
});
</script>

<style scoped>
/* 样式保持不变 */
.clock {
  height: 200px;
  position: absolute;
  top: 50%;
  left: 50%;
  width: 900px;
  margin-left: -450px;
  margin-top: -100px;
  text-align: center;
}

.digit {
  width: 120px;
  height: 200px;
  margin: 0 5px;
  position: relative;
  display: inline-block;
}

.segment {
  background: #c00;
  border-radius: 5px;
  position: absolute;
  opacity: 0.15;
  transition: opacity 0.2s;
}

.segment.on,
.separator {
  opacity: 1;
  box-shadow: 0 0 50px rgba(255, 0, 0, 0.7);
  transition: opacity 0s;
}

.separator {
  width: 20px;
  height: 20px;
  background: #c00;
  border-radius: 50%;
  display: inline-block;
  position: relative;
  top: -90px;
}

.digit .segment:nth-child(1) {
  top: 10px;
  left: 20px;
  right: 20px;
  height: 10px;
}

.digit .segment:nth-child(2) {
  top: 20px;
  right: 10px;
  width: 10px;
  height: calc(50% - 25px);
}

.digit .segment:nth-child(3) {
  bottom: 20px;
  right: 10px;
  width: 10px;
  height: calc(50% - 25px);
}

.digit .segment:nth-child(4) {
  bottom: 10px;
  right: 20px;
  height: 10px;
  left: 20px;
}

.digit .segment:nth-child(5) {
  bottom: 20px;
  left: 10px;
  width: 10px;
  height: calc(50% - 25px);
}

.digit .segment:nth-child(6) {
  top: 20px;
  left: 10px;
  width: 10px;
  height: calc(50% - 25px);
}

.digit .segment:nth-child(7) {
  bottom: calc(50% - 5px);
  right: 20px;
  left: 20px;
  height: 10px;
}
</style>

下面来简单讲解一下

1. 数据结构:怎么存"亮哪几段"?

js 复制代码
const digitSegments = [
  [1, 2, 3, 4, 5, 6],     // 0
  [2, 3],                  // 1
  [1, 2, 7, 5, 4],         // 2
  ...
];

很简单吧?
digitSegments[0] 就是数字0要亮的段。
digitSegments[1] 就是数字1要亮的段。

2. 怎么让"段"亮起来?

每个"段"就是一个 div。

html 复制代码
<div class="segment" v-for="i in 7"></div>

然后判断:当前数字对应的段列表里,有没有这个 i?

js 复制代码
:class="{ on: getActiveSegments(2).includes(i) }"

如果有,就加上 on 类,让它变亮!

3. 时间怎么更新?

setInterval,每秒更新一次。

js 复制代码
onMounted(() => {
  updateTime(); // 先更新一次
  intervalId = setInterval(updateTime, 1000); // 每秒更新
});

updateTime 函数获取当前时间,格式化成 HH:mm:ss,存到 time 变量里。

4. 样式怎么实现"发光"?

靠 CSS 的 box-shadow

css 复制代码
.segment.on {
  opacity: 1;
  box-shadow: 0 0 50px rgba(255, 0, 0, 0.7);
}

一亮起来,就加个红色光晕。

瞬间就有那味儿了!

5. 数字怎么摆?

position: absolute 把7个段拼成一个"8"字。

比如第1段(上面的横):

css 复制代码
.digit .segment:nth-child(1) {
  top: 10px;
  left: 20px;
  right: 20px;
  height: 10px;
}

其他段同理,上下左右定位。

基础版小结:代码清晰,逻辑简单。

这时如果想换个颜色?改CSS。

想变大一点?改CSS。

想用在别处?得复制粘贴+改一堆。

所以,我们来做一个升级版!

版本二:升级版(专业级组件)

这个版本,直接做成一个可配置的Vue组件

1. 支持传参!

defineProps 接收外部配置:

js 复制代码
const props = defineProps({
  type: { default: 'clock' }, // clock 或 countdown
  countdownTime: { default: 60 },
  segmentWidth: { default: 10 },
  digitColor: { default: '#c00' },
  glowColor: { default: 'rgba(255,0,0,0.7)' },
  fontSize: { default: 200 }
})

这意味着,你可以这样用:

vue 复制代码
<LedClock 
  type="countdown" 
  countdownTime="300" 
  digitColor="green" 
  fontSize="150"
/>

直接显示一个 绿色的、倒计时5分钟 的LED钟!

2. 支持倒计时模式!

不只是显示时间。

还能倒着数!

js 复制代码
if (props.type === 'countdown') {
  // 启动倒计时逻辑
  remainingSeconds.value = props.countdownTime;
  intervalId = setInterval(() => {
    remainingSeconds.value--;
    updateTimeFromCountdown(); // 转成 HH:MM:SS
  }, 1000);
}

倒计时归零,自动停止。

3. 样式全部动态!

以前CSS写死像素。

现在全用 calc(v-bind(fontSize) / 200 * xx)

比如:

css 复制代码
height: v-bind(fontSize + 'px');
margin: 0 calc(v-bind(fontSize) / 200 * 5px);

意思是:

所有尺寸,都按 fontSize 的比例来缩放!

fontSize=100,整个钟就变小。

fontSize=300,就变大!

完全响应式!

4. 颜色也能换!

css 复制代码
background: v-bind(digitColor);
box-shadow: 0 0 ... v-bind(glowColor);

v-bind() 直接把JS变量注入CSS。

想红就红,想蓝就蓝。

5. 自动重置逻辑

watch 监听 typecountdownTime 变化:

js 复制代码
watch(
  () => [props.type, props.countdownTime],
  () => {
    startInterval(); // 参数一变,立刻重启定时器
  }
)

比如你从"时钟"切到"倒计时",

或者改了倒计时时间,

组件自动重新初始化!

升级版完整源码

html 复制代码
<template>
  <div class="clock" :style="{ height: fontSize + 'px' }">
    <!-- 小时 -->
    <div class="digit">
      <div
        v-for="i in 7"
        :key="'h1-' + i"
        class="segment"
        :class="{ on: getActiveSegments(time.hours[0]).includes(i) }"
      ></div>
    </div>
    <div class="digit">
      <div
        v-for="i in 7"
        :key="'h2-' + i"
        class="segment"
        :class="{ on: getActiveSegments(time.hours[1]).includes(i) }"
      ></div>
    </div>

    <div class="separator"></div>

    <!-- 分钟 -->
    <div class="digit">
      <div
        v-for="i in 7"
        :key="'m1-' + i"
        class="segment"
        :class="{ on: getActiveSegments(time.minutes[0]).includes(i) }"
      ></div>
    </div>
    <div class="digit">
      <div
        v-for="i in 7"
        :key="'m2-' + i"
        class="segment"
        :class="{ on: getActiveSegments(time.minutes[1]).includes(i) }"
      ></div>
    </div>

    <div class="separator"></div>

    <!-- 秒钟 -->
    <div class="digit">
      <div
        v-for="i in 7"
        :key="'s1-' + i"
        class="segment"
        :class="{ on: getActiveSegments(time.seconds[0]).includes(i) }"
      ></div>
    </div>
    <div class="digit">
      <div
        v-for="i in 7"
        :key="'s2-' + i"
        class="segment"
        :class="{ on: getActiveSegments(time.seconds[1]).includes(i) }"
      ></div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, watch, defineEmits } from 'vue';

// ----------------------
// Props 定义
// ----------------------
const props = defineProps({
  type: {
    type: String,
    default: 'clock',
    validator: (value) => ['clock', 'countdown'].includes(value)
  },
  countdownTime: {
    type: Number,
    default: 60 // 默认倒计时 60 秒
  },
  segmentWidth: {
    type: Number,
    default: 10
  },
  digitColor: {
    type: String,
    default: '#c00'
  },
  glowColor: {
    type: String,
    default: 'rgba(255,0,0,0.7)'
  },
  fontSize: {
    type: Number,
    default: 200
  }
});

// ----------------------
// 七段数码管定义
// ----------------------
const digitSegments = [
  [1, 2, 3, 4, 5, 6],     // 0
  [2, 3],                  // 1
  [1, 2, 7, 5, 4],         // 2
  [1, 2, 7, 3, 4],         // 3
  [6, 7, 2, 3],            // 4
  [1, 6, 7, 3, 4],         // 5
  [1, 3, 4, 5, 6, 7],      // 6 (修正:确保段2不亮)
  [1, 2, 3],               // 7
  [1, 2, 3, 4, 5, 6, 7],   // 8
  [1, 2, 3, 4, 6, 7]       // 9 (修正:补全底横段4)
];

const getActiveSegments = (digit) => {
  const num = parseInt(digit);
  return isNaN(num) ? [] : digitSegments[num];
};

// ----------------------
// 时间状态
// ----------------------
const time = ref({
  hours: '00',
  minutes: '00',
  seconds: '00'
});

let intervalId = null;

// 倒计时剩余秒数
const remainingSeconds = ref(0);

// 初始化倒计时
const initCountdown = () => {
  remainingSeconds.value = props.countdownTime;
  updateTimeFromCountdown();
};

// 根据剩余秒数更新显示
const updateTimeFromCountdown = () => {
  const h = Math.floor(remainingSeconds.value / 3600);
  const m = Math.floor((remainingSeconds.value % 3600) / 60);
  const s = remainingSeconds.value % 60;

  time.value = {
    hours: String(h).padStart(2, '0'),
    minutes: String(m).padStart(2, '0'),
    seconds: String(s).padStart(2, '0')
  };
};

// 实时时钟更新
const updateClock = () => {
  const now = new Date();
  time.value = {
    hours: String(now.getHours()).padStart(2, '0'),
    minutes: String(now.getMinutes()).padStart(2, '0'),
    seconds: String(now.getSeconds()).padStart(2, '0')
  };
};

// ----------------------
// 定时器逻辑
// ----------------------
const startInterval = () => {
  if (intervalId) clearInterval(intervalId);

  if (props.type === 'countdown') {
    initCountdown();
    intervalId = setInterval(() => {
      if (remainingSeconds.value <= 0) {
        clearInterval(intervalId);
        emit('finish'); // 倒计时结束触发事件
        return;
      }
      remainingSeconds.value--;
      updateTimeFromCountdown();
    }, 1000);
  } else {
    updateClock();
    intervalId = setInterval(updateClock, 1000);
  }
};

// ----------------------
// 生命周期 & 监听
// ----------------------
onMounted(() => {
  startInterval();
});

onUnmounted(() => {
  if (intervalId) clearInterval(intervalId);
});

// 当 type 或 countdownTime 改变时重新启动
watch(
  () => [props.type, props.countdownTime],
  () => {
    startInterval();
  }
);

// 定义事件
const emit = defineEmits(['finish']);
</script>

<style scoped>
.clock {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 900px;
  margin-left: -450px;
  margin-top: calc(v-bind(fontSize) / -2 + 'px');
  text-align: center;
}

.digit {
  width: calc(v-bind(fontSize) / 200 * 120px);
  height: v-bind(fontSize + 'px');
  margin: 0 calc(v-bind(fontSize) / 200 * 5px);
  position: relative;
  display: inline-block;
}

.segment {
  background: v-bind(digitColor);
  border-radius: calc(v-bind(fontSize) / 200 * 5px);
  position: absolute;
  opacity: 0.15;
  transition: opacity 0.2s;
}

.segment.on,
.separator {
  opacity: 1;
  box-shadow: 0 0 calc(v-bind(fontSize) * 0.5px) v-bind(glowColor);
  transition: opacity 0s;
}

.separator {
  width: calc(v-bind(fontSize) / 200 * 20px);
  height: calc(v-bind(fontSize) / 200 * 20px);
  background: v-bind(digitColor);
  border-radius: 50%;
  display: inline-block;
  position: relative;
  top: calc(v-bind(fontSize) / 200 * -90px);
}

/* 动态调整段的尺寸 */
.digit .segment:nth-child(1) {
  top: calc(v-bind(fontSize) / 200 * 10px);
  left: calc(v-bind(fontSize) / 200 * 20px);
  right: calc(v-bind(fontSize) / 200 * 20px);
  height: calc(v-bind(fontSize) / 200 * 10px);
}

.digit .segment:nth-child(2),
.digit .segment:nth-child(3) {
  top: calc(v-bind(fontSize) / 200 * 20px);
  right: calc(v-bind(fontSize) / 200 * 10px);
  width: calc(v-bind(fontSize) / 200 * 10px);
  height: calc(v-bind(fontSize) / 400 * 150px);
}

.digit .segment:nth-child(3) {
  top: auto;
  bottom: calc(v-bind(fontSize) / 200 * 20px);
}

.digit .segment:nth-child(4) {
  bottom: calc(v-bind(fontSize) / 200 * 10px);
  left: calc(v-bind(fontSize) / 200 * 20px);
  right: calc(v-bind(fontSize) / 200 * 20px);
  height: calc(v-bind(fontSize) / 200 * 10px);
}

.digit .segment:nth-child(5),
.digit .segment:nth-child(6) {
  bottom: calc(v-bind(fontSize) / 200 * 20px);
  left: calc(v-bind(fontSize) / 200 * 10px);
  width: calc(v-bind(fontSize) / 200 * 10px);
  height: calc(v-bind(fontSize) / 400 * 150px);
}

.digit .segment:nth-child(6) {
  bottom: auto;
  top: calc(v-bind(fontSize) / 200 * 20px);
}

.digit .segment:nth-child(7) {
  bottom: calc(v-bind(fontSize) / 200 * 95px);
  left: calc(v-bind(fontSize) / 200 * 20px);
  right: calc(v-bind(fontSize) / 200 * 20px);
  height: calc(v-bind(fontSize) / 200 * 10px);
}
</style>

两个版本对比

功能 基础版 升级版
显示时间
倒计时
自定义大小
自定义颜色
可复用
代码复杂度 ⭐⭐⭐

关键技术点总结

1. 七段编码表

本质是"数字→段编号"的映射。

可以背下来,也可以画图推导。

2. 动态类名 + includes

:class="{ on: array.includes(i) }"

是判断"是否包含"的极简写法。

3. v-bind() in CSS

Vue 3 的黑科技!

让CSS也能用JS变量。

实现真正的动态样式。

4. watch + setInterval

参数变了,定时器也要重置。

避免内存泄漏和逻辑错乱。

5. padStart(2, '0')

9'09'
15'15'

保证两位数显示,必备技巧!

你能用它做什么?

  • 做一个炫酷的网页时钟
  • 当作倒计时器(发布会、考试)
  • 做数字动画(得分、排行榜)
  • 甚至做成一个"黑客风"仪表盘

只要改改颜色、加点音效,

瞬间科技感拉满!

如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发。

公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!

📌往期精彩

《工作 5 年没碰过分布式锁,是我太菜还是公司太稳?网友:太真实了!》

《90%的人不知道!Spring官方早已不推荐@Autowired?这3种注入方式你用对了吗?》

《写给小公司前端的 UI 规范:别让页面丑得自己都看不下去》

《终于找到 Axios 最优雅的封装方式了,再也不用写重复代码了》

相关推荐
m0_564914923 小时前
点击EDGE浏览器下载的PDF文件总在EDGE中打开
前端·edge·pdf
@大迁世界3 小时前
JavaScript 2.0?当 Bun、Deno 与 Edge 运行时重写执行范式
开发语言·前端·javascript·ecmascript
red润3 小时前
Day.js 是一个轻量级的 JavaScript 日期处理库,以下是常用用法:
前端·javascript
JIngJaneIL3 小时前
记账本|基于SSM的家庭记账本小程序设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·家庭记账本小程序
Ting-yu3 小时前
Nginx快速入门
java·服务器·前端·nginx
我是日安3 小时前
从零到一打造 Vue3 响应式系统 Day 17 - 性能处理:无限循环
前端·vue.js
user94051035547173 小时前
Uniapp 3D 轮播图 轮播视频 可循环组件
前端
前端付豪3 小时前
12、为什么在 <script> 里写 export 会报错?
前端·javascript
Junsen3 小时前
electron窗口层级与dock窗口列表
前端·electron