clock/src/components/FlipDigit.tsx
ysm 9cbae137a8 修复翻页时钟偏移并优化时间显示布局
- 修复翻页时钟动画开始时的位置偏移问题
- 将AM/PM显示移至时间数字左上角
- 添加日期(年月日星期)显示在时间数字右上角
- 统一数码管、翻页、滚动三种时钟的显示样式
- 调整AM/PM和日期字体为数字大小的20%
- 修复翻页和滚动时钟AM/PM显示错误(上午下午颠倒)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 13:24:27 +08:00

189 lines
5.5 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import './FlipDigit.css';
interface FlipDigitProps {
value: number;
size: number;
}
export function FlipDigit({ value, size }: FlipDigitProps) {
// Background top value (updates immediately at animation start)
const [bgTopValue, setBgTopValue] = useState(value);
// Background bottom value (updates after bottom flip completes)
const [bgBottomValue, setBgBottomValue] = useState(value);
// Flip top value (holds old value at animation start)
const [flipTopValue, setFlipTopValue] = useState(value);
// Flip bottom value (holds new value)
const [flipBottomValue, setFlipBottomValue] = useState<number | null>(null);
const animating = useRef(false);
const halfHeight = size * 0.5;
const cardWidth = size * 0.75;
const fontSize = size * 0.75;
// Text offset for vertical alignment
const topTextOffset = halfHeight * 0.75;
const bottomTextOffset = -halfHeight * 0.75;
// Animation state
const [topRotation, setTopRotation] = useState(0);
const [bottomRotation, setBottomRotation] = useState(-90);
const [isAnimating, setIsAnimating] = useState(false);
// Finish animation
const finishAnimation = () => {
setIsAnimating(false);
requestAnimationFrame(() => {
setTopRotation(0);
setBottomRotation(-90);
setFlipBottomValue(null);
animating.current = false;
});
};
// Start bottom animation after top completes
const startBottomAnimation = (targetValue: number) => {
// 上半片翻转完成后,更新上半翻转片的值为新值
// 这样当 finishAnimation 重置 rotation 时,上半翻转片显示正确的值
setFlipTopValue(targetValue);
// 第二阶段:下半片从-90°向下翻转90°到0°
requestAnimationFrame(() => {
setBottomRotation(0);
});
// 下半片翻转完成后,背景下半更新为新值
setTimeout(() => {
setBgBottomValue(targetValue);
setTimeout(() => {
finishAnimation();
}, 50);
}, 300);
};
// When value changes, trigger animation
useEffect(() => {
if (value !== bgTopValue && !animating.current) {
animating.current = true;
// Save current value as flip top value
setFlipTopValue(bgTopValue);
// Set flip bottom value to new value
setFlipBottomValue(value);
// Update bgTop immediately (revealed when top flips away)
setBgTopValue(value);
// First, ensure transition is disabled before resetting rotation
setIsAnimating(false);
// Reset rotation states instantly (without transition)
setTopRotation(0);
setBottomRotation(-90);
// Wait for browser to apply the reset (without transition)
requestAnimationFrame(() => {
// Now enable transition for the actual animation
setIsAnimating(true);
// Wait one more frame to ensure transition is enabled
requestAnimationFrame(() => {
// Start top animation: flip from 0° to 90°
setTopRotation(90);
});
});
// After top animation completes, start bottom animation
setTimeout(() => {
startBottomAnimation(value);
}, 300);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value, bgTopValue]);
const containerStyle = {
width: `${cardWidth}px`,
height: `${size}px`,
};
const halfHeightStyle = {
height: `${halfHeight}px`,
};
const topTextStyle = {
fontSize: `${fontSize}px`,
transform: `translateY(${topTextOffset}px)`,
};
const bottomTextStyle = {
fontSize: `${fontSize}px`,
transform: `translateY(${bottomTextOffset}px)`,
};
const topFlipStyle = {
height: `${halfHeight}px`,
transform: `rotateX(${topRotation}deg)`,
};
const bottomFlipStyle = {
height: `${halfHeight}px`,
transform: `rotateX(${bottomRotation}deg)`,
};
const centerLineStyle = {
top: `${halfHeight - 0.5}px`,
};
return (
<div className="flip-digit-container" style={containerStyle}>
{/* Background top - shows new value's top half */}
<div className="flip-digit-bg-top" style={halfHeightStyle}>
<span className="flip-digit-text flip-digit-top-text" style={topTextStyle}>
{bgTopValue}
</span>
</div>
{/* Background bottom - shows old value's bottom half */}
<div className="flip-digit-bg-bottom" style={halfHeightStyle}>
<span className="flip-digit-text flip-digit-bottom-text" style={bottomTextStyle}>
{bgBottomValue}
</span>
</div>
{/* Top flip flap - shows old value's top half, flips down */}
<div
className="flip-digit-flip-top"
style={{
...halfHeightStyle,
...topFlipStyle,
transition: isAnimating ? 'transform 300ms ease-in' : 'none',
}}
>
<span className="flip-digit-text flip-digit-top-text" style={topTextStyle}>
{flipTopValue}
</span>
</div>
{/* Bottom flip flap - shows new value's bottom half, flips down */}
{flipBottomValue !== null && (
<div
className="flip-digit-flip-bottom"
style={{
...halfHeightStyle,
...bottomFlipStyle,
transition: isAnimating ? 'transform 300ms ease-out' : 'none',
}}
>
<span className="flip-digit-text flip-digit-bottom-text" style={bottomTextStyle}>
{flipBottomValue}
</span>
</div>
)}
{/* Center divider line */}
<div className="flip-digit-center-line" style={centerLineStyle} />
</div>
);
}