PersonalWebApplication/Client/src/components/StartPage.tsx

135 lines
4.9 KiB
TypeScript
Raw Normal View History

import { useEffect, useState } from 'react'
import { motion } from 'framer-motion'
import { fadeUp, stagger } from './motion'
import { useWeather } from '../hooks/useWeather'
import { Greeting } from './start/Greeting'
import { SearchBar } from './start/SearchBar'
import { CategoryDock } from './start/CategoryDock'
import { WeatherFx } from './start/WeatherFx'
// 背景图:放 src/assets/startpage/ 下。按昼夜选 light / dark
// 缺这两张时回退到目录里排序第一张,再无则用渐变。说明见该目录 README。
const bgFiles = import.meta.glob<string>('../assets/startpage/*.{jpg,jpeg,png,webp}', {
eager: true,
query: '?url',
import: 'default',
})
function bgByName(name: string): string | undefined {
return Object.entries(bgFiles).find(([p]) => p.toLowerCase().includes(`/${name}.`))?.[1]
}
const lightBg = bgByName('light')
const darkBg = bgByName('dark')
const anyBg = Object.entries(bgFiles)
.sort(([a], [b]) => a.localeCompare(b))
.map(([, url]) => url)[0]
// 和风图标码中能区分昼夜的子集(晴/多云/阵雨)。其余码无昼夜之分。
const NIGHT_ICONS = new Set(['150', '151', '152', '153', '350', '351', '456', '457'])
const DAY_ICONS = new Set(['100', '101', '102', '103', '300', '301', '400', '401'])
// 昼夜判断图标码优先判断不了的码用本地时间兜底18:0006:00 为夜)。
function isNight(icon?: string): boolean {
if (icon) {
if (NIGHT_ICONS.has(icon)) return true
if (DAY_ICONS.has(icon)) return false
}
const h = new Date().getHours()
return h < 6 || h >= 18
}
// 动态效果类型:只做雨/雪两类,其余静态。
function fxKind(icon?: string): 'rain' | 'snow' | 'none' {
const n = icon ? Number.parseInt(icon, 10) : NaN
if (Number.isNaN(n)) return 'none'
if ((n >= 400 && n <= 499) || n === 901) return 'snow'
if (n >= 300 && n <= 399) return 'rain'
return 'none'
}
function isThunder(icon?: string): boolean {
return icon ? ['302', '303', '304'].includes(icon) : false
}
export function StartPage() {
const wx = useWeather()
// 每分钟一拍,让昼夜随时间自动翻(即便天气没刷新)。
const [, setTick] = useState(0)
useEffect(() => {
const id = setInterval(() => setTick((t) => t + 1), 60_000)
return () => clearInterval(id)
}, [])
const icon = wx.data && wx.data.ok ? wx.data.icon : undefined
// 开发预览(仅 DEV?bg=light|dark 强制背景;?fx=rain|snow|thunder|none 强制动效。
// 例http://localhost:5173/?bg=dark&fx=snow
const ov = import.meta.env.DEV ? new URLSearchParams(window.location.search) : null
const ovBg = ov?.get('bg')
const ovFx = ov?.get('fx')
const night = ovBg ? ovBg === 'dark' : isNight(icon)
const bg = (night ? darkBg : lightBg) ?? lightBg ?? darkBg ?? anyBg
let kind = fxKind(icon)
let thunder = isThunder(icon)
if (ovFx === 'snow') {
kind = 'snow'
thunder = false
} else if (ovFx === 'thunder') {
kind = 'rain'
thunder = true
} else if (ovFx === 'rain') {
kind = 'rain'
thunder = false
} else if (ovFx === 'none') {
kind = 'none'
thunder = false
}
return (
<div className="relative min-h-svh w-full overflow-hidden">
{/* 背景层:有图用图,无图回退渐变 */}
{bg ? (
<div
className="absolute inset-0 -z-20 bg-cover bg-center"
style={{ backgroundImage: `url(${bg})` }}
/>
) : (
<div className="absolute inset-0 -z-20 bg-gradient-to-br from-[#243140] via-[#36475a] to-[#566578]" />
)}
{/* scrim上下压暗保证玻璃与文字在任意图片上都可读 */}
<div className="absolute inset-0 -z-10 bg-gradient-to-b from-black/45 via-black/15 to-black/55" />
{/* ▼▼ 半透明黑遮罩:整体压暗背景。调透明度改下面的 0.50=全透明1=纯黑)▼▼ */}
<div className="absolute inset-0 -z-10" style={{ backgroundColor: 'rgba(0, 0, 0, 0.5)' }} />
{/* 天气动态层:雨/雪粒子(克制),位于遮罩之上、内容之下 */}
<WeatherFx kind={kind} thunder={thunder} />
<motion.main
variants={stagger(0.15, 0.12)}
initial="hidden"
animate="show"
className="relative z-10 mx-auto flex min-h-svh max-w-2xl flex-col items-center justify-center gap-9 px-6"
>
<motion.div variants={fadeUp} className="w-full">
<Greeting wx={wx} />
</motion.div>
<motion.div variants={fadeUp} className="w-full">
<SearchBar />
</motion.div>
{/* /
CategoryDock */}
<div aria-hidden className="h-44 w-full shrink-0 sm:h-48" />
</motion.main>
{/* 底部分类抽屉:按类型分组的快捷入口,悬停某类时菜单向上抽出 */}
<CategoryDock />
</div>
)
}