Compare commits
No commits in common. "2a40d05cacae607694826521a9f7ba8f901758d6" and "4518e2e57e84adb0d6dd07d6ac64865c62079eea" have entirely different histories.
2a40d05cac
...
4518e2e57e
@ -5,7 +5,6 @@
|
|||||||
## 项目概述
|
## 项目概述
|
||||||
|
|
||||||
一个精简的 React + TypeScript + Vite 入门项目。
|
一个精简的 React + TypeScript + Vite 入门项目。
|
||||||
主要功能是在页面上显示数码风格的时钟。
|
|
||||||
|
|
||||||
- **Node 版本**: v24.14.0(在 `.nvmrc` 中指定)
|
- **Node 版本**: v24.14.0(在 `.nvmrc` 中指定)
|
||||||
- **包管理器**: npm
|
- **包管理器**: npm
|
||||||
@ -67,8 +66,3 @@ src/
|
|||||||
## 构建输出
|
## 构建输出
|
||||||
|
|
||||||
生产构建输出到 `dist/` 目录(ESLint 会忽略该目录)。
|
生产构建输出到 `dist/` 目录(ESLint 会忽略该目录)。
|
||||||
|
|
||||||
## 本地存储
|
|
||||||
|
|
||||||
本地存储使用localStorage,用于保存用户设置。
|
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +0,0 @@
|
|||||||
# 翻页
|
|
||||||
|
|
||||||
这里是关于翻页的设计文档
|
|
||||||
|
|
||||||
翻页样式是模拟常见于机场的用于显示航班信息的老式机械翻页机。
|
|
||||||
|
|
||||||
基本样式就是上下两个方块,分别显示数字的上半部分和下半部分。
|
|
||||||
|
|
||||||
每次翻页时,上半部分会沿着上半部分的下边缘向下翻转,显示出后面背景的下一个数字的上半部分。然后,下半部分会沿着下半部分的上边缘向下翻转,显示出后面背景的下一个数字的下半部分。
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
source ~/.nvm/nvm.sh
|
|
||||||
nvm use
|
|
||||||
|
|
||||||
134
src/App.css
134
src/App.css
@ -8,11 +8,26 @@
|
|||||||
.app {
|
.app {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
width: 100%;
|
gap: 40px;
|
||||||
padding-top: 40px;
|
}
|
||||||
box-sizing: border-box;
|
|
||||||
|
.clock-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Digital clock style (default) */
|
||||||
|
.digital-clock {
|
||||||
|
font-family: 'DSEG7', monospace;
|
||||||
|
font-size: 15vw;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 0 0 20px rgba(255, 255, 255, 0.5);
|
||||||
|
letter-spacing: 0.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Style selector */
|
/* Style selector */
|
||||||
@ -23,113 +38,6 @@
|
|||||||
padding: 8px;
|
padding: 8px;
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
z-index: 10;
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-bottom: 60px;
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clock-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0;
|
|
||||||
width: 100%;
|
|
||||||
padding: 0 20px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.digital-clock-outer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.digital-clock-side-panel {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
justify-content: center;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.digital-clock-wrapper {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.digital-clock-header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: baseline;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.digital-clock-header-left {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.digital-clock-period {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #fff;
|
|
||||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.digital-clock-date {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #fff;
|
|
||||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
|
||||||
line-height: 1;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.format-toggle-side {
|
|
||||||
padding: 4px 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #888;
|
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.format-toggle-side:hover {
|
|
||||||
color: #fff;
|
|
||||||
background-color: rgba(255, 255, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.clock-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex: 1;
|
|
||||||
max-width: 100vw;
|
|
||||||
overflow: hidden;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.digital-clock {
|
|
||||||
font-family: 'DSEG7', monospace;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #fff;
|
|
||||||
text-shadow: 0 0 20px rgba(255, 255, 255, 0.5);
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
white-space: nowrap;
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.style-button {
|
.style-button {
|
||||||
@ -156,6 +64,10 @@
|
|||||||
|
|
||||||
/* Responsive adjustments */
|
/* Responsive adjustments */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
.digital-clock {
|
||||||
|
font-size: 12vw;
|
||||||
|
}
|
||||||
|
|
||||||
.style-selector {
|
.style-selector {
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|||||||
126
src/App.tsx
126
src/App.tsx
@ -5,65 +5,10 @@ import './App.css'
|
|||||||
|
|
||||||
type ClockStyle = 'digital' | 'flip' | 'roll'
|
type ClockStyle = 'digital' | 'flip' | 'roll'
|
||||||
|
|
||||||
interface DateInfo {
|
function DigitalClock({ time }: { time: string }) {
|
||||||
dateString: string
|
|
||||||
weekday: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDate(date: Date): DateInfo {
|
|
||||||
const year = date.getFullYear()
|
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
|
||||||
const day = String(date.getDate()).padStart(2, '0')
|
|
||||||
const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
|
|
||||||
const weekday = weekdays[date.getDay()]
|
|
||||||
|
|
||||||
return {
|
|
||||||
dateString: `${year}-${month}-${day}`,
|
|
||||||
weekday
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function DigitalClock({ time, size, period, is24Hour, onToggleFormat, dateInfo }: { time: string; size: number; period?: string; is24Hour: boolean; onToggleFormat: () => void; dateInfo: DateInfo }) {
|
|
||||||
const fontSize = Math.floor(size * 0.95)
|
|
||||||
const periodSize = Math.floor(size * 0.20)
|
|
||||||
const dateSize = Math.floor(size * 0.20)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="digital-clock-outer">
|
<div className="digital-clock">
|
||||||
<div className="digital-clock-side-panel">
|
{time}
|
||||||
<button
|
|
||||||
className="format-toggle-side"
|
|
||||||
onClick={onToggleFormat}
|
|
||||||
>
|
|
||||||
{is24Hour ? '24H' : '12H'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="digital-clock-wrapper">
|
|
||||||
<div className="digital-clock-header">
|
|
||||||
<div className="digital-clock-header-left">
|
|
||||||
{!is24Hour && period && (
|
|
||||||
<span
|
|
||||||
className="digital-clock-period"
|
|
||||||
style={{ fontSize: `${periodSize}px` }}
|
|
||||||
>
|
|
||||||
{period}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
className="digital-clock-date"
|
|
||||||
style={{ fontSize: `${dateSize}px` }}
|
|
||||||
>
|
|
||||||
{dateInfo.dateString} {dateInfo.weekday}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="digital-clock"
|
|
||||||
style={{ fontSize: `${fontSize}px`, lineHeight: '1' }}
|
|
||||||
>
|
|
||||||
{time}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -71,8 +16,6 @@ function DigitalClock({ time, size, period, is24Hour, onToggleFormat, dateInfo }
|
|||||||
function App() {
|
function App() {
|
||||||
const [time, setTime] = useState(new Date())
|
const [time, setTime] = useState(new Date())
|
||||||
const [clockStyle, setClockStyle] = useState<ClockStyle>('digital')
|
const [clockStyle, setClockStyle] = useState<ClockStyle>('digital')
|
||||||
const [clockSize, setClockSize] = useState(120)
|
|
||||||
const [is24Hour, setIs24Hour] = useState(true)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
@ -81,67 +24,26 @@ function App() {
|
|||||||
return () => clearInterval(timer)
|
return () => clearInterval(timer)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Responsive clock size - fit within screen width
|
const timeString = `${String(time.getHours()).padStart(2, '0')}:${String(time.getMinutes()).padStart(2, '0')}:${String(time.getSeconds()).padStart(2, '0')}`
|
||||||
useEffect(() => {
|
|
||||||
const updateSize = () => {
|
|
||||||
const width = window.innerWidth
|
|
||||||
// More conservative for small screens
|
|
||||||
const margin = width < 480 ? 40 : 60
|
|
||||||
const availableWidth = width - margin
|
|
||||||
// Use larger divisor for small screens to prevent overflow
|
|
||||||
const divisor = width < 480 ? 7 : 6
|
|
||||||
const newSize = Math.floor(availableWidth / divisor)
|
|
||||||
setClockSize(Math.min(newSize, 200))
|
|
||||||
}
|
|
||||||
updateSize()
|
|
||||||
window.addEventListener('resize', updateSize)
|
|
||||||
return () => window.removeEventListener('resize', updateSize)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Format time based on 12/24 hour setting
|
|
||||||
const formatTime = () => {
|
|
||||||
let hours = time.getHours()
|
|
||||||
const minutes = String(time.getMinutes()).padStart(2, '0')
|
|
||||||
const seconds = String(time.getSeconds()).padStart(2, '0')
|
|
||||||
let period: string | undefined
|
|
||||||
|
|
||||||
if (!is24Hour) {
|
|
||||||
period = hours >= 12 ? 'PM' : 'AM'
|
|
||||||
hours = hours % 12 || 12
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
timeString: `${String(hours).padStart(2, '0')}:${minutes}:${seconds}`,
|
|
||||||
period
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { timeString, period } = formatTime()
|
|
||||||
const dateInfo = formatDate(time)
|
|
||||||
|
|
||||||
const renderClock = () => {
|
const renderClock = () => {
|
||||||
switch (clockStyle) {
|
switch (clockStyle) {
|
||||||
case 'flip':
|
case 'flip':
|
||||||
return <FlipClock time={timeString} size={clockSize} dateInfo={dateInfo} period={period} />
|
return <FlipClock time={timeString} size={120} />
|
||||||
case 'roll':
|
case 'roll':
|
||||||
return <RollClock time={timeString} size={clockSize} dateInfo={dateInfo} period={period} />
|
return <RollClock time={timeString} size={120} />
|
||||||
case 'digital':
|
case 'digital':
|
||||||
default:
|
default:
|
||||||
return (
|
return <DigitalClock time={timeString} />
|
||||||
<DigitalClock
|
|
||||||
time={timeString}
|
|
||||||
size={clockSize}
|
|
||||||
period={period}
|
|
||||||
is24Hour={is24Hour}
|
|
||||||
onToggleFormat={() => setIs24Hour(!is24Hour)}
|
|
||||||
dateInfo={dateInfo}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
|
<div className="clock-container">
|
||||||
|
{renderClock()}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="style-selector">
|
<div className="style-selector">
|
||||||
<button
|
<button
|
||||||
className={`style-button ${clockStyle === 'digital' ? 'active' : ''}`}
|
className={`style-button ${clockStyle === 'digital' ? 'active' : ''}`}
|
||||||
@ -162,12 +64,6 @@ function App() {
|
|||||||
滚动
|
滚动
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="clock-wrapper">
|
|
||||||
<div className="clock-container">
|
|
||||||
{renderClock()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,72 +1,3 @@
|
|||||||
.flip-clock-outer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flip-clock-side-panel {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
justify-content: center;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flip-clock-period {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #fff;
|
|
||||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flip-format-toggle {
|
|
||||||
padding: 4px 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #888;
|
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flip-format-toggle:hover {
|
|
||||||
color: #fff;
|
|
||||||
background-color: rgba(255, 255, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.flip-clock-main {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flip-clock-header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: baseline;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flip-clock-header-left {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flip-clock-period {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #fff;
|
|
||||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flip-clock-container {
|
.flip-clock-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -74,15 +5,6 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flip-clock-date {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #fff;
|
|
||||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
|
||||||
line-height: 1;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flip-clock-digit-group {
|
.flip-clock-digit-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
@ -1,97 +1,52 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { FlipDigit } from './FlipDigit';
|
import { FlipDigit } from './FlipDigit';
|
||||||
import './FlipClock.css';
|
import './FlipClock.css';
|
||||||
|
|
||||||
interface DateInfo {
|
|
||||||
dateString: string;
|
|
||||||
weekday: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FlipClockProps {
|
interface FlipClockProps {
|
||||||
time: string; // "HH:MM:SS" format
|
time: string; // "HH:MM:SS" format
|
||||||
size: number;
|
size: number;
|
||||||
dateInfo: DateInfo;
|
|
||||||
period?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FlipClock({ time, size, dateInfo, period }: FlipClockProps) {
|
export function FlipClock({ time, size }: FlipClockProps) {
|
||||||
const [is24Hour, setIs24Hour] = useState(true);
|
const [hours, minutes, seconds] = time.split(':');
|
||||||
|
|
||||||
// Parse time - it's already in 12h format when period is provided
|
|
||||||
const [hoursStr, minutes, seconds] = time.split(':');
|
|
||||||
const displayHours = hoursStr;
|
|
||||||
|
|
||||||
const periodSize = size * 0.20;
|
|
||||||
const dateSize = size * 0.20;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flip-clock-outer">
|
<div className="flip-clock-container">
|
||||||
<div className="flip-clock-side-panel">
|
{/* Hours */}
|
||||||
<button
|
<div className="flip-clock-digit-group">
|
||||||
className="flip-format-toggle"
|
<FlipDigit value={parseInt(hours[0], 10)} size={size} />
|
||||||
onClick={() => setIs24Hour(!is24Hour)}
|
<FlipDigit value={parseInt(hours[1], 10)} size={size} />
|
||||||
>
|
|
||||||
{is24Hour ? '24H' : '12H'}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flip-clock-main">
|
|
||||||
<div className="flip-clock-header">
|
|
||||||
<div className="flip-clock-header-left">
|
|
||||||
{period && (
|
|
||||||
<span
|
|
||||||
className="flip-clock-period"
|
|
||||||
style={{ fontSize: `${periodSize}px` }}
|
|
||||||
>
|
|
||||||
{period}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
className="flip-clock-date"
|
|
||||||
style={{ fontSize: `${dateSize}px` }}
|
|
||||||
>
|
|
||||||
{dateInfo.dateString} {dateInfo.weekday}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flip-clock-container">
|
|
||||||
{/* Hours */}
|
|
||||||
<div className="flip-clock-digit-group">
|
|
||||||
<FlipDigit value={parseInt(displayHours[0], 10)} size={size} />
|
|
||||||
<FlipDigit value={parseInt(displayHours[1], 10)} size={size} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span
|
<span
|
||||||
className="flip-clock-separator"
|
className="flip-clock-separator"
|
||||||
style={{
|
style={{
|
||||||
fontSize: `${size * 0.5}px`,
|
fontSize: `${size * 0.5}px`,
|
||||||
marginTop: `${-size * 0.08}px`
|
marginTop: `${-size * 0.08}px`
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
:
|
:
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* Minutes */}
|
{/* Minutes */}
|
||||||
<div className="flip-clock-digit-group">
|
<div className="flip-clock-digit-group">
|
||||||
<FlipDigit value={parseInt(minutes[0], 10)} size={size} />
|
<FlipDigit value={parseInt(minutes[0], 10)} size={size} />
|
||||||
<FlipDigit value={parseInt(minutes[1], 10)} size={size} />
|
<FlipDigit value={parseInt(minutes[1], 10)} size={size} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
className="flip-clock-separator"
|
className="flip-clock-separator"
|
||||||
style={{
|
style={{
|
||||||
fontSize: `${size * 0.5}px`,
|
fontSize: `${size * 0.5}px`,
|
||||||
marginTop: `${-size * 0.08}px`
|
marginTop: `${-size * 0.08}px`
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
:
|
:
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* Seconds */}
|
{/* Seconds */}
|
||||||
<div className="flip-clock-digit-group">
|
<div className="flip-clock-digit-group">
|
||||||
<FlipDigit value={parseInt(seconds[0], 10)} size={size} />
|
<FlipDigit value={parseInt(seconds[0], 10)} size={size} />
|
||||||
<FlipDigit value={parseInt(seconds[1], 10)} size={size} />
|
<FlipDigit value={parseInt(seconds[1], 10)} size={size} />
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
perspective: 1000px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.flip-digit-bg-top {
|
.flip-digit-bg-top {
|
||||||
@ -70,8 +69,7 @@
|
|||||||
border-top-left-radius: 4px;
|
border-top-left-radius: 4px;
|
||||||
border-top-right-radius: 4px;
|
border-top-right-radius: 4px;
|
||||||
backface-visibility: hidden;
|
backface-visibility: hidden;
|
||||||
z-index: 11;
|
z-index: 10;
|
||||||
/* 旋转轴在底部边缘(卡片中线位置) */
|
|
||||||
transform-origin: center bottom;
|
transform-origin: center bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +87,6 @@
|
|||||||
border-bottom-right-radius: 4px;
|
border-bottom-right-radius: 4px;
|
||||||
backface-visibility: hidden;
|
backface-visibility: hidden;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
/* 旋转轴在顶部边缘(卡片中线位置) */
|
|
||||||
transform-origin: center top;
|
transform-origin: center top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -33,28 +33,23 @@ export function FlipDigit({ value, size }: FlipDigitProps) {
|
|||||||
|
|
||||||
// Finish animation
|
// Finish animation
|
||||||
const finishAnimation = () => {
|
const finishAnimation = () => {
|
||||||
|
setTopRotation(0);
|
||||||
|
setBottomRotation(-90);
|
||||||
|
setFlipBottomValue(null);
|
||||||
setIsAnimating(false);
|
setIsAnimating(false);
|
||||||
|
animating.current = false;
|
||||||
requestAnimationFrame(() => {
|
|
||||||
setTopRotation(0);
|
|
||||||
setBottomRotation(-90);
|
|
||||||
setFlipBottomValue(null);
|
|
||||||
animating.current = false;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start bottom animation after top completes
|
// Start bottom animation after top completes
|
||||||
const startBottomAnimation = (targetValue: number) => {
|
const startBottomAnimation = (targetValue: number) => {
|
||||||
// 上半片翻转完成后,更新上半翻转片的值为新值
|
|
||||||
// 这样当 finishAnimation 重置 rotation 时,上半翻转片显示正确的值
|
|
||||||
setFlipTopValue(targetValue);
|
setFlipTopValue(targetValue);
|
||||||
|
|
||||||
// 第二阶段:下半片从-90°向下翻转90°到0°
|
// Animate bottom flap from -90deg to 0deg
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
setBottomRotation(0);
|
setBottomRotation(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 下半片翻转完成后,背景下半更新为新值
|
// After bottom animation completes, update bgBottom and finish
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setBgBottomValue(targetValue);
|
setBgBottomValue(targetValue);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -67,6 +62,7 @@ export function FlipDigit({ value, size }: FlipDigitProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value !== bgTopValue && !animating.current) {
|
if (value !== bgTopValue && !animating.current) {
|
||||||
animating.current = true;
|
animating.current = true;
|
||||||
|
setIsAnimating(true);
|
||||||
|
|
||||||
// Save current value as flip top value
|
// Save current value as flip top value
|
||||||
setFlipTopValue(bgTopValue);
|
setFlipTopValue(bgTopValue);
|
||||||
@ -75,23 +71,13 @@ export function FlipDigit({ value, size }: FlipDigitProps) {
|
|||||||
// Update bgTop immediately (revealed when top flips away)
|
// Update bgTop immediately (revealed when top flips away)
|
||||||
setBgTopValue(value);
|
setBgTopValue(value);
|
||||||
|
|
||||||
// First, ensure transition is disabled before resetting rotation
|
// Reset rotation states
|
||||||
setIsAnimating(false);
|
|
||||||
|
|
||||||
// Reset rotation states instantly (without transition)
|
|
||||||
setTopRotation(0);
|
setTopRotation(0);
|
||||||
setBottomRotation(-90);
|
setBottomRotation(-90);
|
||||||
|
|
||||||
// Wait for browser to apply the reset (without transition)
|
// Start top animation after a brief delay to ensure state is set
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
// Now enable transition for the actual animation
|
setTopRotation(90);
|
||||||
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
|
// After top animation completes, start bottom animation
|
||||||
@ -123,12 +109,14 @@ export function FlipDigit({ value, size }: FlipDigitProps) {
|
|||||||
|
|
||||||
const topFlipStyle = {
|
const topFlipStyle = {
|
||||||
height: `${halfHeight}px`,
|
height: `${halfHeight}px`,
|
||||||
transform: `rotateX(${topRotation}deg)`,
|
transform: `perspective(1000px) translateY(${halfHeight * 0.5}px) rotateX(${topRotation}deg) translateY(${-halfHeight * 0.5}px)`,
|
||||||
|
opacity: topRotation > 60 ? 0 : 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const bottomFlipStyle = {
|
const bottomFlipStyle = {
|
||||||
height: `${halfHeight}px`,
|
height: `${halfHeight}px`,
|
||||||
transform: `rotateX(${bottomRotation}deg)`,
|
transform: `perspective(1000px) translateY(${-halfHeight * 0.5}px) rotateX(${bottomRotation}deg) translateY(${halfHeight * 0.5}px)`,
|
||||||
|
opacity: bottomRotation > -60 ? 1 : 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const centerLineStyle = {
|
const centerLineStyle = {
|
||||||
@ -157,7 +145,7 @@ export function FlipDigit({ value, size }: FlipDigitProps) {
|
|||||||
style={{
|
style={{
|
||||||
...halfHeightStyle,
|
...halfHeightStyle,
|
||||||
...topFlipStyle,
|
...topFlipStyle,
|
||||||
transition: isAnimating ? 'transform 300ms ease-in' : 'none',
|
transition: isAnimating ? 'transform 300ms ease-in, opacity 300ms ease-in' : 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flip-digit-text flip-digit-top-text" style={topTextStyle}>
|
<span className="flip-digit-text flip-digit-top-text" style={topTextStyle}>
|
||||||
@ -172,7 +160,7 @@ export function FlipDigit({ value, size }: FlipDigitProps) {
|
|||||||
style={{
|
style={{
|
||||||
...halfHeightStyle,
|
...halfHeightStyle,
|
||||||
...bottomFlipStyle,
|
...bottomFlipStyle,
|
||||||
transition: isAnimating ? 'transform 300ms ease-out' : 'none',
|
transition: isAnimating ? 'transform 300ms ease-out, opacity 300ms ease-out' : 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flip-digit-text flip-digit-bottom-text" style={bottomTextStyle}>
|
<span className="flip-digit-text flip-digit-bottom-text" style={bottomTextStyle}>
|
||||||
|
|||||||
@ -1,78 +1,9 @@
|
|||||||
.roll-clock-outer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roll-clock-side-panel {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
justify-content: center;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roll-clock-period {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #fff;
|
|
||||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roll-format-toggle {
|
|
||||||
padding: 4px 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #888;
|
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roll-format-toggle:hover {
|
|
||||||
color: #fff;
|
|
||||||
background-color: rgba(255, 255, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.roll-clock-wrapper {
|
.roll-clock-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.roll-clock-main {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roll-clock-header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: baseline;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roll-clock-header-left {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roll-clock-period {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #fff;
|
|
||||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roll-clock-container {
|
.roll-clock-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -80,15 +11,6 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.roll-clock-date {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #fff;
|
|
||||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
|
||||||
line-height: 1;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roll-clock-digit-group {
|
.roll-clock-digit-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
@ -2,30 +2,17 @@ import { useState } from 'react';
|
|||||||
import { RollDigit } from './RollDigit';
|
import { RollDigit } from './RollDigit';
|
||||||
import './RollClock.css';
|
import './RollClock.css';
|
||||||
|
|
||||||
interface DateInfo {
|
|
||||||
dateString: string;
|
|
||||||
weekday: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RollClockProps {
|
interface RollClockProps {
|
||||||
time: string; // "HH:MM:SS" format
|
time: string; // "HH:MM:SS" format
|
||||||
size: number;
|
size: number;
|
||||||
dateInfo: DateInfo;
|
|
||||||
period?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RollClock({ time, size, dateInfo, period }: RollClockProps) {
|
export function RollClock({ time, size }: RollClockProps) {
|
||||||
// Roll direction: 'down' = roll down, 'up' = roll up
|
// Roll direction: 'down' = roll down, 'up' = roll up
|
||||||
const [direction, setDirection] = useState<'down' | 'up'>('down');
|
const [direction, setDirection] = useState<'down' | 'up'>('down');
|
||||||
const [is24Hour, setIs24Hour] = useState(true);
|
|
||||||
|
|
||||||
// Parse time - it's already in 12h format when period is provided
|
|
||||||
const [hoursStr, minutes, seconds] = time.split(':');
|
|
||||||
const displayHours = hoursStr;
|
|
||||||
|
|
||||||
|
const [hours, minutes, seconds] = time.split(':');
|
||||||
const buttonSize = size * 0.25;
|
const buttonSize = size * 0.25;
|
||||||
const periodSize = size * 0.20;
|
|
||||||
const dateSize = size * 0.20;
|
|
||||||
|
|
||||||
// Toggle direction
|
// Toggle direction
|
||||||
const toggleDirection = () => {
|
const toggleDirection = () => {
|
||||||
@ -33,95 +20,65 @@ export function RollClock({ time, size, dateInfo, period }: RollClockProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="roll-clock-outer">
|
<div className="roll-clock-wrapper">
|
||||||
<div className="roll-clock-side-panel">
|
<div className="roll-clock-container">
|
||||||
<button
|
{/* Hours */}
|
||||||
className="roll-format-toggle"
|
<div className="roll-clock-digit-group">
|
||||||
onClick={() => setIs24Hour(!is24Hour)}
|
<RollDigit value={parseInt(hours[0], 10)} size={size} direction={direction} />
|
||||||
>
|
<RollDigit value={parseInt(hours[1], 10)} size={size} direction={direction} />
|
||||||
{is24Hour ? '24H' : '12H'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="roll-clock-wrapper">
|
|
||||||
<div className="roll-clock-main">
|
|
||||||
<div className="roll-clock-header">
|
|
||||||
<div className="roll-clock-header-left">
|
|
||||||
{!is24Hour && period && (
|
|
||||||
<span
|
|
||||||
className="roll-clock-period"
|
|
||||||
style={{ fontSize: `${periodSize}px` }}
|
|
||||||
>
|
|
||||||
{period}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
className="roll-clock-date"
|
|
||||||
style={{ fontSize: `${dateSize}px` }}
|
|
||||||
>
|
|
||||||
{dateInfo.dateString} {dateInfo.weekday}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="roll-clock-container">
|
|
||||||
{/* Hours */}
|
|
||||||
<div className="roll-clock-digit-group">
|
|
||||||
<RollDigit value={parseInt(displayHours[0], 10)} size={size} direction={direction} />
|
|
||||||
<RollDigit value={parseInt(displayHours[1], 10)} size={size} direction={direction} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span
|
|
||||||
className="roll-clock-separator"
|
|
||||||
style={{
|
|
||||||
fontSize: `${size * 0.5}px`,
|
|
||||||
marginTop: `${-size * 0.08}px`
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
:
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* Minutes */}
|
|
||||||
<div className="roll-clock-digit-group">
|
|
||||||
<RollDigit value={parseInt(minutes[0], 10)} size={size} direction={direction} />
|
|
||||||
<RollDigit value={parseInt(minutes[1], 10)} size={size} direction={direction} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span
|
|
||||||
className="roll-clock-separator"
|
|
||||||
style={{
|
|
||||||
fontSize: `${size * 0.5}px`,
|
|
||||||
marginTop: `${-size * 0.08}px`
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
:
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* Seconds */}
|
|
||||||
<div className="roll-clock-digit-group">
|
|
||||||
<RollDigit value={parseInt(seconds[0], 10)} size={size} direction={direction} />
|
|
||||||
<RollDigit value={parseInt(seconds[1], 10)} size={size} direction={direction} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Direction toggle button */}
|
<span
|
||||||
<button
|
className="roll-clock-separator"
|
||||||
className="roll-clock-button"
|
|
||||||
style={{
|
style={{
|
||||||
width: `${buttonSize}px`,
|
fontSize: `${size * 0.5}px`,
|
||||||
height: `${buttonSize}px`,
|
marginTop: `${-size * 0.08}px`
|
||||||
borderRadius: `${buttonSize / 2}px`,
|
|
||||||
marginLeft: '8px',
|
|
||||||
}}
|
}}
|
||||||
onClick={toggleDirection}
|
|
||||||
>
|
>
|
||||||
<span
|
:
|
||||||
className="roll-clock-button-text"
|
</span>
|
||||||
style={{ fontSize: `${buttonSize * 0.6}px` }}
|
|
||||||
>
|
{/* Minutes */}
|
||||||
{direction === 'down' ? '↓' : '↑'}
|
<div className="roll-clock-digit-group">
|
||||||
</span>
|
<RollDigit value={parseInt(minutes[0], 10)} size={size} direction={direction} />
|
||||||
</button>
|
<RollDigit value={parseInt(minutes[1], 10)} size={size} direction={direction} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span
|
||||||
|
className="roll-clock-separator"
|
||||||
|
style={{
|
||||||
|
fontSize: `${size * 0.5}px`,
|
||||||
|
marginTop: `${-size * 0.08}px`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
:
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Seconds */}
|
||||||
|
<div className="roll-clock-digit-group">
|
||||||
|
<RollDigit value={parseInt(seconds[0], 10)} size={size} direction={direction} />
|
||||||
|
<RollDigit value={parseInt(seconds[1], 10)} size={size} direction={direction} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Direction toggle button */}
|
||||||
|
<button
|
||||||
|
className="roll-clock-button"
|
||||||
|
style={{
|
||||||
|
width: `${buttonSize}px`,
|
||||||
|
height: `${buttonSize}px`,
|
||||||
|
borderRadius: `${buttonSize / 2}px`,
|
||||||
|
marginLeft: '8px',
|
||||||
|
}}
|
||||||
|
onClick={toggleDirection}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="roll-clock-button-text"
|
||||||
|
style={{ fontSize: `${buttonSize * 0.6}px` }}
|
||||||
|
>
|
||||||
|
{direction === 'down' ? '↓' : '↑'}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,9 +51,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
width: 100%;
|
width: 1126px;
|
||||||
|
max-width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
border-inline: 1px solid var(--border);
|
||||||
min-height: 100svh;
|
min-height: 100svh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user