<script setup lang="ts">
|
import { ref, onMounted, onUnmounted } from "vue";
|
import dayjs from "dayjs";
|
import Flipper from "./flipper";
|
|
const props = defineProps({
|
height: {
|
type: Number,
|
default: 100
|
}
|
});
|
|
const timer = ref<number>(0);
|
const flipObjs: Flipper[] = [];
|
const contentRef = ref<HTMLDivElement>();
|
const seconds = ref(0);
|
const minutes = ref(0);
|
const isRunning = ref(false);
|
|
const startTimer = () => {
|
if (isRunning.value) return;
|
isRunning.value = true;
|
|
// 重置时间为0
|
seconds.value = 0;
|
minutes.value = 0;
|
|
// 更新翻牌显示
|
updateFlipDisplay();
|
|
// 启动计时器
|
timer.value = window.setInterval(() => {
|
seconds.value++;
|
if (seconds.value >= 60) {
|
seconds.value = 0;
|
minutes.value++;
|
}
|
updateFlipDisplay();
|
}, 1000);
|
};
|
|
const resetTimer = () => {
|
if (timer.value) {
|
window.clearInterval(timer.value);
|
timer.value = 0;
|
}
|
isRunning.value = false;
|
seconds.value = 0;
|
minutes.value = 0;
|
|
// 强制重置所有翻牌为0
|
const timeStr = "0000";
|
setTimeout(() => {
|
for (let i = 0; i < flipObjs.length; i++) {
|
flipObjs[i]._setFront(0, "number0");
|
flipObjs[i]._setBack(1, "number1");
|
}
|
}, 1200);
|
};
|
|
const updateFlipDisplay = () => {
|
const timeStr = `${String(minutes.value).padStart(2, "0")}${String(seconds.value).padStart(2, "0")}`;
|
|
// 正确处理分钟和秒的进位
|
let nextSeconds = seconds.value + 1;
|
let nextMinutes = minutes.value;
|
if (nextSeconds >= 60) {
|
nextSeconds = 0;
|
nextMinutes++;
|
}
|
|
const nextTimeStr = `${String(nextMinutes).padStart(2, "0")}${String(nextSeconds).padStart(2, "0")}`;
|
|
for (let i = 0; i < flipObjs.length; i++) {
|
if (timeStr[i] !== nextTimeStr[i]) {
|
flipObjs[i].flipDown(
|
Number(timeStr[i]),
|
`number${timeStr[i]}`,
|
Number(nextTimeStr[i]),
|
`number${nextTimeStr[i]}`
|
);
|
}
|
}
|
};
|
|
onMounted(() => {
|
if (contentRef.value) {
|
const flips = Array.from(contentRef.value.children).filter(itemNode =>
|
itemNode.className.includes("flip")
|
);
|
|
// 只保留4个翻牌元素(分和秒)
|
while (flips.length > 4) {
|
flips.pop();
|
}
|
|
// 初始化翻牌对象
|
const timeStr = "0000";
|
const nextTimeStr = "0000";
|
|
for (let i = 0; i < flips.length; i++) {
|
flipObjs.push(
|
new Flipper({
|
node: flips[i],
|
frontText: `number${timeStr[i]}`,
|
backText: `number${nextTimeStr[i]}`
|
})
|
);
|
}
|
}
|
});
|
|
onUnmounted(() => {
|
timer.value && window.clearInterval(timer.value);
|
});
|
|
defineExpose({
|
startTimer,
|
resetTimer
|
});
|
</script>
|
|
<template>
|
<div style="width: 100%">
|
<div class="container">
|
<div ref="contentRef" class="content">
|
<!-- 分钟十位 -->
|
<div class="flip down" @click.stop="startTimer">
|
<div class="digital front" />
|
<div class="digital back" />
|
</div>
|
<!-- 分钟个位 -->
|
<div class="flip down" @click.stop="startTimer">
|
<div class="digital front" />
|
<div class="digital back" />
|
</div>
|
<div class="dot">:</div>
|
<!-- 秒钟十位 -->
|
<div class="flip down" @click.stop="resetTimer">
|
<div class="digital front" />
|
<div class="digital back" />
|
</div>
|
<!-- 秒钟个位 -->
|
<div class="flip down" @click.stop="resetTimer">
|
<div class="digital front" />
|
<div class="digital back" />
|
</div>
|
</div>
|
<!-- <div class="controls">
|
<button @click="startTimer">开始</button>
|
<button @click="resetTimer">重置</button>
|
</div>-->
|
</div>
|
</div>
|
</template>
|
|
<style scoped lang="less">
|
.container {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
position: relative;
|
.content {
|
display: flex;
|
|
.flip {
|
width: 60px;
|
height: v-bind('props.height + "px"');
|
line-height: v-bind('props.height + "px"');
|
margin: 0 4px;
|
border: solid 1px var(--base-green);
|
border-radius: 10px;
|
background-color: var(--base-green);
|
font-size: 66px;
|
color: #fff;
|
box-shadow: 0 0 6px rgba(0, 0, 0, 0.5);
|
text-align: center;
|
position: relative;
|
|
.digital {
|
&::before,
|
&::after {
|
content: "";
|
position: absolute;
|
left: 0;
|
right: 0;
|
background-color: var(--base-green);
|
overflow: hidden;
|
color: #fff;
|
box-sizing: border-box;
|
}
|
|
&::before {
|
top: 0;
|
bottom: 50%;
|
border-radius: 10px 10px 0 0;
|
border-bottom: solid 1px var(--base-green);
|
}
|
|
&::after {
|
top: 50%;
|
bottom: 0;
|
border-radius: 0 0 10px 10px;
|
line-height: 0;
|
}
|
}
|
|
.front {
|
&::before {
|
z-index: 3;
|
}
|
|
&::after {
|
z-index: 1;
|
}
|
}
|
|
.back {
|
&::before {
|
z-index: 1;
|
}
|
|
&::after {
|
z-index: 2;
|
transform-origin: 50% 0%;
|
transform: perspective(160px) rotateX(180deg);
|
}
|
}
|
|
.generate-number-classes(10);
|
.generate-number-classes(@n, @i: 0) when (@i =< @n) {
|
.number@{i} {
|
&::before,
|
&::after {
|
content: "@{i}";
|
}
|
}
|
.generate-number-classes(@n, (@i + 1));
|
}
|
}
|
|
.down.go {
|
.front {
|
&::before {
|
transform-origin: 50% 100%;
|
animation: frontFlipDown 0.6s ease-in-out both;
|
box-shadow: 0 -2px 6px rgba(255, 255, 255, 0.3);
|
backface-visibility: hidden;
|
}
|
}
|
|
.back {
|
&::after {
|
animation: backFlipDown 0.6s ease-in-out both;
|
}
|
}
|
}
|
|
.dot {
|
height: v-bind('props.height + "px"');
|
line-height: v-bind('props.height + "px"');
|
margin: 0 10px;
|
font-size: 24px;
|
color: var(--base-green);
|
}
|
|
@keyframes frontFlipDown {
|
0% {
|
transform: perspective(160px) rotateX(0deg);
|
}
|
|
100% {
|
transform: perspective(160px) rotateX(-180deg);
|
}
|
}
|
|
@keyframes backFlipDown {
|
0% {
|
transform: perspective(160px) rotateX(180deg);
|
}
|
|
100% {
|
transform: perspective(160px) rotateX(0deg);
|
}
|
}
|
}
|
|
.controls {
|
display: flex;
|
gap: 10px;
|
|
button {
|
padding: 8px 16px;
|
border: none;
|
border-radius: 4px;
|
background-color: #007bff;
|
color: white;
|
cursor: pointer;
|
|
&:hover {
|
background-color: #0056b3;
|
}
|
}
|
}
|
}
|
</style>
|