From 25791f92682a91ac40c14daa25f7dd0f0ba7a8dd Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 23 Jun 2026 13:52:28 +0800 Subject: [PATCH] =?UTF-8?q?perf(start):=20=E4=BC=98=E5=8C=96=E9=AB=98?= =?UTF-8?q?=E5=88=86=E5=B1=8F=E5=8D=A1=E9=A1=BF=20+=20=E8=83=8C=E6=99=AF?= =?UTF-8?q?=E5=9B=BE=E6=8C=89=E6=A8=AA=E7=AB=96=E5=B1=8F=E5=88=87=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 性能: - 光标圆环去掉 mixBlendMode:difference(跟随时触发全屏重合成, 高分屏掉帧主因),并删除位置弹簧平滑,改为 1:1 跟手 - .glass-panel 的 backdrop-filter 模糊 20px→10px,每帧重算开销减半 背景: - 横/竖图分别放 landscape//portrait/ 子目录(同名) - 新增 useMediaQuery hook,按 (orientation: portrait) 选图, 转屏实时切换;竖图缺失自动回退同名横图 - 更新 startpage/README 说明新结构 Co-Authored-By: Claude Opus 4.8 --- Client/src/assets/startpage/README.md | 44 ++++++++++------ .../startpage/{ => landscape}/Night.webp | Bin .../startpage/{ => landscape}/No.1.webp | Bin .../startpage/{ => landscape}/No.2.webp | Bin .../startpage/{ => landscape}/No.3.webp | Bin .../startpage/{ => landscape}/No.4.webp | Bin .../startpage/{ => landscape}/No.5.webp | Bin Client/src/components/CursorFollower.tsx | 9 ++-- Client/src/components/StartPage.tsx | 48 +++++++++++++----- Client/src/hooks/useMediaQuery.ts | 15 ++++++ Client/src/index.css | 6 ++- 11 files changed, 86 insertions(+), 36 deletions(-) rename Client/src/assets/startpage/{ => landscape}/Night.webp (100%) rename Client/src/assets/startpage/{ => landscape}/No.1.webp (100%) rename Client/src/assets/startpage/{ => landscape}/No.2.webp (100%) rename Client/src/assets/startpage/{ => landscape}/No.3.webp (100%) rename Client/src/assets/startpage/{ => landscape}/No.4.webp (100%) rename Client/src/assets/startpage/{ => landscape}/No.5.webp (100%) create mode 100644 Client/src/hooks/useMediaQuery.ts diff --git a/Client/src/assets/startpage/README.md b/Client/src/assets/startpage/README.md index a426fc3..0e7e7de 100644 --- a/Client/src/assets/startpage/README.md +++ b/Client/src/assets/startpage/README.md @@ -1,32 +1,42 @@ # Startpage 背景图 导航起始页(`src/components/StartPage.tsx`)的背景图放在这个目录里, -**按昼夜自动切换**: +**按昼夜 + 屏幕方向自动切换**。 -- 白天 → `light.*`(如 `light.jpg`) -- 夜晚 → `dark.*`(如 `dark.png`) +## 目录结构 -放好这两张文件即可生效,无需改代码。 +``` +startpage/ +├─ landscape/ 横屏图(电脑、手机横放) +│ ├─ No.1.webp ~ No.5.webp 白天候选 +│ └─ Night.webp 夜间 +└─ portrait/ 竖屏图(手机竖放),命名与 landscape 完全相同 + ├─ No.1.webp ~ No.5.webp + └─ Night.webp +``` + +放好文件即可生效,无需改代码。**竖图可以只放一部分**——缺的那张会自动 +回退到同名横图。 ## 工作方式 -`StartPage` 用 Vite 的 `import.meta.glob` 索引本目录,按文件名精确取 -`light.*` / `dark.*`,再按昼夜选用。 +`StartPage` 用 Vite 的 `import.meta.glob` 分别索引 `landscape/`、`portrait/`: -昼夜判断:**优先用天气接口返回的图标码**(晴/多云/阵雨类区分昼夜), -图标码判断不了的天气(雨、雪、雾、阴等无昼夜之分)则用**本地时间兜底** -(18:00–06:00 视为夜晚)。 +- **方向**:`useMediaQuery('(orientation: portrait)')` 判断竖屏(高 ≥ 宽)。 + 竖屏优先取 `portrait/` 的图,转屏/缩窗口会实时切换。 +- **白天**:从 `No.1`~`No.5` 里随机取一张(整页生命周期内固定,不乱跳)。 +- **夜间**:用 `Night`。判断为本地时间 `0:00–6:00`。 -回退顺序:选中的那张缺失 → 用另一张 → 都缺则用目录里排序第一张 → -再无则用中性渐变(构建始终成功)。 +回退顺序:竖图缺失 → 同名横图 → 横图里排序第一张 → 再无则用中性渐变 +(构建始终成功)。 ## 命名与规格 -- 必须命名为 `light.*` 和 `dark.*`(扩展名任意:`.jpg/.jpeg/.png/.webp`)。 - 其它名字(如 `dark33.png`)不会被选用,只会白白打进包里,建议删掉。 -- 格式:`webp`(最小)> `jpg` > `png` -- 横图、够大:≥ 1920×1080,最好 2560×1440+ -- 图上还会叠一层暗色 scrim + 半透明黑遮罩(保证文字可读),所以偏暗、 +- 白天图必须叫 `No.<数字>.*`(如 `No.1.webp`),夜间图叫 `Night.*`。 + 其它名字不会被选用,只会白白打进包里。 +- 扩展名任意:`.jpg/.jpeg/.png/.webp`,体积 `webp` > `jpg` > `png`。 +- 横图够大:≥ 1920×1080,最好 2560×1440+;竖图按手机比例(如 1080×2340)。 +- 图上还会叠一层暗色 scrim + 半透明黑遮罩(保证文字可读),偏暗、 低对比的图效果最好。 ## 本地预览(仅 `npm run dev`) @@ -37,3 +47,5 @@ - `?fx=rain` / `?fx=snow` / `?fx=thunder` / `?fx=none` —— 强制动态效果 例:`http://localhost:5173/?bg=dark&fx=snow` + +竖屏效果用浏览器开发者工具的设备模拟(Device Toolbar)切竖屏即可预览。 diff --git a/Client/src/assets/startpage/Night.webp b/Client/src/assets/startpage/landscape/Night.webp similarity index 100% rename from Client/src/assets/startpage/Night.webp rename to Client/src/assets/startpage/landscape/Night.webp diff --git a/Client/src/assets/startpage/No.1.webp b/Client/src/assets/startpage/landscape/No.1.webp similarity index 100% rename from Client/src/assets/startpage/No.1.webp rename to Client/src/assets/startpage/landscape/No.1.webp diff --git a/Client/src/assets/startpage/No.2.webp b/Client/src/assets/startpage/landscape/No.2.webp similarity index 100% rename from Client/src/assets/startpage/No.2.webp rename to Client/src/assets/startpage/landscape/No.2.webp diff --git a/Client/src/assets/startpage/No.3.webp b/Client/src/assets/startpage/landscape/No.3.webp similarity index 100% rename from Client/src/assets/startpage/No.3.webp rename to Client/src/assets/startpage/landscape/No.3.webp diff --git a/Client/src/assets/startpage/No.4.webp b/Client/src/assets/startpage/landscape/No.4.webp similarity index 100% rename from Client/src/assets/startpage/No.4.webp rename to Client/src/assets/startpage/landscape/No.4.webp diff --git a/Client/src/assets/startpage/No.5.webp b/Client/src/assets/startpage/landscape/No.5.webp similarity index 100% rename from Client/src/assets/startpage/No.5.webp rename to Client/src/assets/startpage/landscape/No.5.webp diff --git a/Client/src/components/CursorFollower.tsx b/Client/src/components/CursorFollower.tsx index 33bc389..113c06e 100644 --- a/Client/src/components/CursorFollower.tsx +++ b/Client/src/components/CursorFollower.tsx @@ -1,13 +1,12 @@ import { useEffect, useState } from 'react' -import { motion, useMotionValue, useSpring } from 'framer-motion' +import { motion, useMotionValue } from 'framer-motion' // 跟随鼠标的小圆环(仿 Few & Far)。 // 仅在精确指针设备启用;触摸设备与 reduced-motion 下不挂载。 export function CursorFollower() { + // 位置 1:1 跟手,不做弹簧平滑(平滑在掉帧时会显得发涩/拖影)。 const x = useMotionValue(-100) const y = useMotionValue(-100) - const springX = useSpring(x, { stiffness: 350, damping: 28, mass: 0.4 }) - const springY = useSpring(y, { stiffness: 350, damping: 28, mass: 0.4 }) const [enabled, setEnabled] = useState(false) const [hovering, setHovering] = useState(false) @@ -47,11 +46,11 @@ export function CursorFollower() { ('../assets/startpage/*.{jpg,jpeg,png,webp}', { +// 背景图:横图放 src/assets/startpage/landscape/,竖图放 src/assets/startpage/portrait/(同名)。 +// 6:00–24:00 从 No.1~No.5 里随机取一张;0:00–6:00 用 Night。 +// 竖屏优先用 portrait/ 的图,缺图则回退到同名横图,再无则用渐变。说明见该目录 README。 +const landscapeFiles = import.meta.glob('../assets/startpage/landscape/*.{jpg,jpeg,png,webp}', { + eager: true, + query: '?url', + import: 'default', +}) +const portraitFiles = import.meta.glob('../assets/startpage/portrait/*.{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] +interface BgSet { + dayBgs: string[] // 白天候选:No.1~No.5(按编号排序) + nightBg?: string // 夜间:Night } -// 白天候选: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') +// 从一组 glob 结果里拆出白天候选与夜间图。 +function buildSet(files: Record): BgSet { + const dayBgs = Object.entries(files) + .filter(([p]) => /\/no\.\d+\./i.test(p)) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([, url]) => url) + const nightBg = Object.entries(files).find(([p]) => p.toLowerCase().includes('/night.'))?.[1] + return { dayBgs, nightBg } +} + +const landscape = buildSet(landscapeFiles) +const portrait = buildSet(portraitFiles) // 昼夜判断:纯按本地时间,0:00–6:00 为夜。 function isNight(): boolean { @@ -56,8 +70,13 @@ export function StartPage() { const icon = wx.data && wx.data.ok ? wx.data.icon : undefined + // 竖屏(高≥宽)优先用竖版图,缺图回退到同名横图。 + const isPortrait = useMediaQuery('(orientation: portrait)') + // 白天随机选一张,整页生命周期内固定,避免每分钟一拍时背景乱跳。 - const [dayIdx] = useState(() => Math.floor(Math.random() * Math.max(dayBgs.length, 1))) + // 索引按两套候选的最大长度取,方向切换时也不越界。 + const dayCount = Math.max(landscape.dayBgs.length, portrait.dayBgs.length, 1) + const [dayIdx] = useState(() => Math.floor(Math.random() * dayCount)) // 开发预览(仅 DEV):?bg=light|dark 强制背景;?fx=rain|snow|thunder|none 强制动效。 // 例:http://localhost:5173/?bg=dark&fx=snow @@ -65,8 +84,11 @@ export function StartPage() { const ovBg = ov?.get('bg') const ovFx = ov?.get('fx') + const i = dayIdx % dayCount + const dayBg = (isPortrait ? portrait.dayBgs[i] : undefined) ?? landscape.dayBgs[i] ?? landscape.dayBgs[0] + const nightBg = (isPortrait ? portrait.nightBg : undefined) ?? landscape.nightBg + const night = ovBg ? ovBg === 'dark' : isNight() - const dayBg = dayBgs[dayIdx] ?? dayBgs[0] const bg = (night ? nightBg : dayBg) ?? dayBg ?? nightBg let kind = fxKind(icon) diff --git a/Client/src/hooks/useMediaQuery.ts b/Client/src/hooks/useMediaQuery.ts new file mode 100644 index 0000000..9366869 --- /dev/null +++ b/Client/src/hooks/useMediaQuery.ts @@ -0,0 +1,15 @@ +import { useSyncExternalStore } from 'react' + +// 订阅一条媒体查询,匹配状态变化时自动重渲染。 +// 典型用法:useMediaQuery('(orientation: portrait)') 判断竖屏。 +export function useMediaQuery(query: string): boolean { + return useSyncExternalStore( + (onChange) => { + const mql = window.matchMedia(query) + mql.addEventListener('change', onChange) + return () => mql.removeEventListener('change', onChange) + }, + () => window.matchMedia(query).matches, + () => false, // SSR/首帧兜底:当作不匹配 + ) +} diff --git a/Client/src/index.css b/Client/src/index.css index 1e82a1e..403d050 100644 --- a/Client/src/index.css +++ b/Client/src/index.css @@ -212,8 +212,10 @@ body { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22), 0 10px 30px rgba(0, 0, 0, 0.22); - backdrop-filter: blur(20px) saturate(140%); - -webkit-backdrop-filter: blur(20px) saturate(140%); + /* blur 半径直接决定每帧重算模糊的成本(∝ 半径×面积),高分屏下尤甚; + 20px→10px 观感几乎无差,开销减半。见 .glass-sheet 处的同类取舍。 */ + backdrop-filter: blur(10px) saturate(140%); + -webkit-backdrop-filter: blur(10px) saturate(140%); } /* ── reduced-motion 降级 ────────────────────────────────────── */