- 修复翻页时钟动画开始时的位置偏移问题 - 将AM/PM显示移至时间数字左上角 - 添加日期(年月日星期)显示在时间数字右上角 - 统一数码管、翻页、滚动三种时钟的显示样式 - 调整AM/PM和日期字体为数字大小的20% - 修复翻页和滚动时钟AM/PM显示错误(上午下午颠倒) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
189 lines
5.5 KiB
TypeScript
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>
|
|
);
|
|
}
|