index.vue
<template>
<div class="FlipClock">
<Flipper ref="flipperHour" />
<em>:</em>
<Flipper ref="flipperMinute" />
<em>:</em>
<Flipper ref="flipperSecond" />
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue"
import Flipper from "./Flipper.vue"
const timer = ref<any>(null)
const flipObjs = ref<any[]>([])
const flipperHour = ref()
const flipperMinute = ref()
const flipperSecond = ref()
function init() {
let now = new Date()
let nowTimeStr = formatDate(new Date(now.getTime()), "hh-ii-ss")
nowTimeStr = nowTimeStr.split("-")
for (let i = 0; i < flipObjs.value.length; i++) {
flipObjs.value[i].value.setFront(nowTimeStr[i])
}
}
function run() {
timer.value = setInterval(() => {
let now = new Date()
let nowTimeStr = formatDate(new Date(now.getTime() - 1000), "hh-ii-ss")
nowTimeStr = nowTimeStr.split("-")
let nextTimeStr = formatDate(now, "hh-ii-ss")
nextTimeStr = nextTimeStr.split("-")
for (let i = 0; i < flipObjs.value.length; i++) {
if (nowTimeStr[i] === nextTimeStr[i]) {
continue
}
flipObjs.value[i].value.flipDown(nowTimeStr[i], nextTimeStr[i])
}
}, 1000)
}
function formatDate(date, dateFormat) {
if (/(y+)/.test(dateFormat)) {
dateFormat = dateFormat.replace(
RegExp.$1,
(date.getFullYear() + "").substr(4 - RegExp.$1.length)
)
}
let o = {
"m+": date.getMonth() + 1,
"d+": date.getDate(),
"h+": date.getHours(),
"i+": date.getMinutes(),
"s+": date.getSeconds()
}
for (let k in o) {
if (new RegExp(`(${k})`).test(dateFormat)) {
let str = o[k] + ""
dateFormat = dateFormat.replace(RegExp.$1, RegExp.$1.length === 1 ? str : padLeftZero(str))
}
}
return dateFormat
}
function padLeftZero(str) {
return str.toString().padStart(2, "0")
}
onMounted(() => {
flipObjs.value = [flipperHour, flipperMinute, flipperSecond]
init()
run()
})
</script>
<style lang="scss" scoped>
.FlipClock {
text-align: center;
}
.FlipClock .M-Flipper {
margin: 0 3px;
}
.FlipClock em {
display: inline-block;
line-height: 102px;
font-size: 66px;
font-style: normal;
vertical-align: top;
}
</style>
Flipper.vue
<template>
<div class="M-Flipper" :class="[flipType, { go: isFlipping }]">
<div class="digital front" :class="_textClass(frontTextFromData)"></div>
<div class="digital back" :class="_textClass(backTextFromData)"></div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
const props = defineProps({
frontText: {
type: [Number, String],
default: "00"
},
backText: {
type: [Number, String],
default: "01"
},
duration: {
type: Number,
default: 600
}
})
const isFlipping = ref(false)
const flipType = ref("down")
const frontTextFromData = ref(props.frontText)
const backTextFromData = ref(props.backText)
function _textClass(number) {
return "number" + number
}
function _flip(type, front, back) {
if (isFlipping.value) {
return false
}
frontTextFromData.value = front
backTextFromData.value = back
flipType.value = type
isFlipping.value = true
setTimeout(() => {
isFlipping.value = false
frontTextFromData.value = back
}, props.duration)
}
function flipDown(front, back) {
_flip("down", front, back)
}
function flipUp(front, back) {
_flip("up", front, back)
}
function setFront(text) {
frontTextFromData.value = text
}
function setBack(text) {
backTextFromData.value = text
}
defineExpose({
flipDown,
flipUp,
setFront,
setBack
})
</script>
<style lang="scss" scoped>
.M-Flipper {
display: inline-block;
position: relative;
width: 120px;
height: 100px;
line-height: 100px;
border: solid 1px #000;
border-radius: 10px;
background: #fff;
font-size: 66px;
color: #fff;
box-shadow: 0 0 6px rgba(0, 0, 0, 0.5);
text-align: center;
font-family: "Helvetica Neue";
}
.M-Flipper .digital:before,
.M-Flipper .digital:after {
content: "";
position: absolute;
left: 0;
right: 0;
background: #000;
overflow: hidden;
box-sizing: border-box;
}
.M-Flipper .digital:before {
top: 0;
bottom: 50%;
border-radius: 10px 10px 0 0;
border-bottom: solid 1px #666;
}
.M-Flipper .digital:after {
top: 50%;
bottom: 0;
border-radius: 0 0 10px 10px;
line-height: 0;
}
.M-Flipper.down .front:before {
z-index: 3;
}
.M-Flipper.down .back:after {
z-index: 2;
transform-origin: 50% 0%;
transform: perspective(160px) rotateX(180deg);
}
.M-Flipper.down .front:after,
.M-Flipper.down .back:before {
z-index: 1;
}
.M-Flipper.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;
}
.M-Flipper.down.go .back:after {
animation: backFlipDown 0.6s ease-in-out both;
}
.M-Flipper.up .front:after {
z-index: 3;
}
.M-Flipper.up .back:before {
z-index: 2;
transform-origin: 50% 100%;
transform: perspective(160px) rotateX(-180deg);
}
.M-Flipper.up .front:before,
.M-Flipper.up .back:after {
z-index: 1;
}
.M-Flipper.up.go .front:after {
transform-origin: 50% 0;
animation: frontFlipUp 0.6s ease-in-out both;
box-shadow: 0 2px 6px rgba(255, 255, 255, 0.3);
backface-visibility: hidden;
}
.M-Flipper.up.go .back:before {
animation: backFlipUp 0.6s ease-in-out both;
}
@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);
}
}
@keyframes frontFlipUp {
0% {
transform: perspective(160px) rotateX(0deg);
}
100% {
transform: perspective(160px) rotateX(180deg);
}
}
@keyframes backFlipUp {
0% {
transform: perspective(160px) rotateX(-180deg);
}
100% {
transform: perspective(160px) rotateX(0deg);
}
}
.M-Flipper {
@for $i from 0 through 59 {
$classPrefix: if($i <= 9, "number0", "number");
$content: if($i <= 9, "0" + $i, $i);
.digital.#{$classPrefix}#{$i}:before,
.digital.#{$classPrefix}#{$i}:after {
// 你的样式
content: "#{$content}";
}
}
}
</style>
感谢大神_Nealyang_博客
感谢大神_Nealyang_Github