PersonalWebApplication/Client/src/components/StartPage.tsx
cc 5b60185f94 feat(start): 背景按时段选图(6-24 随机 No.1~5,0-6 用 Night),标题改 CCStart
- StartPage 背景改为纯本地时间:白天从 No.1~No.5 随机取一张并固定整页生命周期,夜间(0-6点)用 Night
- 登录页 h2 与 index.html 标签页标题由 Sign in 改为 CCStart
- 起始页背景图压成 WebP(限宽 2560,质量 ~82),单图从 2.5~4.7MB 降到 113~375KB,适配 3Mbps 带宽
- 删除旧 dark.png/light.jpg
- 部署文档补充前端单独重发流程与 WSL 实际路径

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 22:18:47 +08:00

131 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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/ 下。6:0024:00 从 No.1~No.5 里随机取一张;
// 0:006:00 用 Night。缺图时互相回退再无则用渐变。说明见该目录 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]
}
// 白天候选No.1~No.5按编号排序夜间Night。
const dayBgs = Object.entries(bgFiles)
.filter(([p]) => /\/no\.\d+\./i.test(p))
.sort(([a], [b]) => a.localeCompare(b))
.map(([, url]) => url)
const nightBg = bgByName('night')
// 昼夜判断纯按本地时间0:006:00 为夜。
function isNight(): boolean {
return new Date().getHours() < 6
}
// 动态效果类型:只做雨/雪两类,其余静态。
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
// 白天随机选一张,整页生命周期内固定,避免每分钟一拍时背景乱跳。
const [dayIdx] = useState(() => Math.floor(Math.random() * Math.max(dayBgs.length, 1)))
// 开发预览(仅 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()
const dayBg = dayBgs[dayIdx] ?? dayBgs[0]
const bg = (night ? nightBg : dayBg) ?? dayBg ?? nightBg
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>
)
}