2026-06-18 20:13:18 +08:00
|
|
|
|
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'
|
2026-06-21 21:23:12 +08:00
|
|
|
|
import { CategoryDock } from './start/CategoryDock'
|
2026-06-18 20:13:18 +08:00
|
|
|
|
import { WeatherFx } from './start/WeatherFx'
|
|
|
|
|
|
|
2026-06-22 22:18:47 +08:00
|
|
|
|
// 背景图:放 src/assets/startpage/ 下。6:00–24:00 从 No.1~No.5 里随机取一张;
|
|
|
|
|
|
// 0:00–6:00 用 Night。缺图时互相回退,再无则用渐变。说明见该目录 README。
|
2026-06-18 20:13:18 +08:00
|
|
|
|
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]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 22:18:47 +08:00
|
|
|
|
// 白天候选:No.1~No.5(按编号排序);夜间:Night。
|
|
|
|
|
|
const dayBgs = Object.entries(bgFiles)
|
|
|
|
|
|
.filter(([p]) => /\/no\.\d+\./i.test(p))
|
2026-06-18 20:13:18 +08:00
|
|
|
|
.sort(([a], [b]) => a.localeCompare(b))
|
2026-06-22 22:18:47 +08:00
|
|
|
|
.map(([, url]) => url)
|
|
|
|
|
|
const nightBg = bgByName('night')
|
2026-06-18 20:13:18 +08:00
|
|
|
|
|
2026-06-22 22:18:47 +08:00
|
|
|
|
// 昼夜判断:纯按本地时间,0:00–6:00 为夜。
|
|
|
|
|
|
function isNight(): boolean {
|
|
|
|
|
|
return new Date().getHours() < 6
|
2026-06-18 20:13:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 动态效果类型:只做雨/雪两类,其余静态。
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-20 20:45:30 +08:00
|
|
|
|
export function StartPage() {
|
2026-06-18 20:13:18 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-06-22 22:18:47 +08:00
|
|
|
|
// 白天随机选一张,整页生命周期内固定,避免每分钟一拍时背景乱跳。
|
|
|
|
|
|
const [dayIdx] = useState(() => Math.floor(Math.random() * Math.max(dayBgs.length, 1)))
|
|
|
|
|
|
|
2026-06-18 20:13:18 +08:00
|
|
|
|
// 开发预览(仅 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')
|
|
|
|
|
|
|
2026-06-22 22:18:47 +08:00
|
|
|
|
const night = ovBg ? ovBg === 'dark' : isNight()
|
|
|
|
|
|
const dayBg = dayBgs[dayIdx] ?? dayBgs[0]
|
|
|
|
|
|
const bg = (night ? nightBg : dayBg) ?? dayBg ?? nightBg
|
2026-06-18 20:13:18 +08:00
|
|
|
|
|
|
|
|
|
|
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.5(0=全透明,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>
|
|
|
|
|
|
|
2026-06-21 21:23:12 +08:00
|
|
|
|
{/* 占位:补回快捷入口腾出的高度,让问候/搜索框保持删除前的垂直位置与比例,
|
|
|
|
|
|
不因居中重排而被撑到页面正中。底部入口改由 CategoryDock 承载。 */}
|
|
|
|
|
|
<div aria-hidden className="h-44 w-full shrink-0 sm:h-48" />
|
2026-06-18 20:13:18 +08:00
|
|
|
|
</motion.main>
|
2026-06-21 21:23:12 +08:00
|
|
|
|
|
|
|
|
|
|
{/* 底部分类抽屉:按类型分组的快捷入口,悬停某类时菜单向上抽出 */}
|
|
|
|
|
|
<CategoryDock />
|
2026-06-18 20:13:18 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|