166 lines
5.3 KiB
TypeScript
166 lines
5.3 KiB
TypeScript
import React, { useState, useEffect } from 'react'
|
|
import { motion } from 'framer-motion'
|
|
|
|
// 倒计时组件的属性接口
|
|
interface CountdownProps {
|
|
startDate: string
|
|
firstPeriodDays: number
|
|
secondPeriodDays: number
|
|
extraHours: number
|
|
}
|
|
|
|
// 文章接口
|
|
interface Article {
|
|
id: number
|
|
title: string
|
|
content: string
|
|
created_at: string
|
|
type: 'activity' | 'news'
|
|
}
|
|
|
|
// 倒计时组件
|
|
function Countdown({ startDate, firstPeriodDays, secondPeriodDays, extraHours }: CountdownProps) {
|
|
const [remainingDays, setRemainingDays] = useState(0)
|
|
const [isFirstPeriod, setIsFirstPeriod] = useState(true)
|
|
|
|
useEffect(() => {
|
|
const calculateCountdown = () => {
|
|
const totalDays = firstPeriodDays + secondPeriodDays
|
|
const currentTime = new Date().getTime()
|
|
const elapsedDays = Math.floor((currentTime - new Date(startDate).getTime()) / (1000 * 60 * 60 * 24))
|
|
const currentPeriod = elapsedDays % totalDays
|
|
|
|
if (currentPeriod < firstPeriodDays) {
|
|
setIsFirstPeriod(true)
|
|
setRemainingDays(firstPeriodDays - currentPeriod)
|
|
} else {
|
|
setIsFirstPeriod(false)
|
|
setRemainingDays(totalDays - currentPeriod)
|
|
}
|
|
}
|
|
|
|
calculateCountdown()
|
|
const timer = setInterval(calculateCountdown, 1000 * 60 * 60) // 每小时更新一次
|
|
|
|
return () => clearInterval(timer)
|
|
}, [startDate, firstPeriodDays, secondPeriodDays])
|
|
|
|
return (
|
|
<motion.div
|
|
className="icon text-center bg-white rounded-lg shadow-lg p-6 w-full sm:w-64"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5 }}
|
|
>
|
|
<p className="text-lg font-semibold mb-2">{isFirstPeriod ? '距离下一次大休还有' : '距离开学还有'}</p>
|
|
<motion.a
|
|
className={`text-6xl font-bold ${isFirstPeriod ? 'text-blue-500' : 'text-red-500'}`}
|
|
initial={{ scale: 1 }}
|
|
animate={{ scale: [1, 1.1, 1] }}
|
|
transition={{ duration: 1, repeat: Infinity, repeatType: "reverse" }}
|
|
>
|
|
{remainingDays}
|
|
</motion.a>
|
|
<p className="text-lg mt-2">天</p>
|
|
</motion.div>
|
|
)
|
|
}
|
|
|
|
// 文章组件
|
|
function Articles() {
|
|
const [articles, setArticles] = useState<Article[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
const fetchArticles = async () => {
|
|
try {
|
|
const response = await fetch('https://ez-api.mei.lv:55233/public/home')
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch articles')
|
|
}
|
|
const data = await response.json()
|
|
setArticles(data)
|
|
setLoading(false)
|
|
} catch (err) {
|
|
setError('Failed to load articles. Please try again later.')
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
fetchArticles()
|
|
}, [])
|
|
|
|
const activities = articles.filter(article => article.type === 'activity')
|
|
const news = articles.filter(article => article.type === 'news')
|
|
|
|
const renderArticles = (articles: Article[]) => {
|
|
return articles.map((article, index) => (
|
|
<motion.div
|
|
key={article.id}
|
|
className="mb-6 bg-white rounded-lg shadow-md p-6"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5, delay: index * 0.1 }}
|
|
>
|
|
<a href={`/article/${article.id}.html`} className="text-xl font-semibold hover:text-blue-600 transition-colors duration-200">
|
|
{article.title}
|
|
</a>
|
|
<p className="mt-2 text-gray-600 truncate">{article.content}</p>
|
|
<small className="text-gray-500">发布于: {article.created_at}</small>
|
|
</motion.div>
|
|
))
|
|
}
|
|
|
|
if (loading) {
|
|
return <div className="text-center">加载中...</div>
|
|
}
|
|
|
|
if (error) {
|
|
return <div className="text-center text-red-500">{error}</div>
|
|
}
|
|
|
|
return (
|
|
<div className="articles grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
<div className="left-column">
|
|
<h2 className="text-2xl font-bold mb-4 text-blue-600">活动一览</h2>
|
|
{renderArticles(activities)}
|
|
</div>
|
|
<div className="right-column">
|
|
<h2 className="text-2xl font-bold mb-4 text-green-600">新闻动态</h2>
|
|
{renderArticles(news)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// 主应用组件
|
|
export default function App() {
|
|
return (
|
|
<div className="min-h-screen bg-gray-100 py-12 px-4 sm:px-6 lg:px-8">
|
|
<div className="max-w-7xl mx-auto">
|
|
<div className="flex flex-col items-center mb-12">
|
|
<div className="icons grid grid-cols-1 sm:grid-cols-2 gap-6 w-full max-w-2xl mb-8">
|
|
<Countdown
|
|
startDate="2024-10-06T16:25:00"
|
|
firstPeriodDays={12}
|
|
secondPeriodDays={2}
|
|
extraHours={0}
|
|
/>
|
|
<motion.div
|
|
className="icon text-center bg-white rounded-lg shadow-lg p-6 w-full sm:w-64"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5, delay: 0.2 }}
|
|
>
|
|
<p className="text-lg font-semibold mb-2">今天油二</p>
|
|
<button className="text-4xl font-bold text-green-500">没有爆炸</button>
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
<div className="w-full h-px bg-gray-300 my-12"></div>
|
|
<Articles />
|
|
</div>
|
|
</div>
|
|
)
|
|
} |