feat(start): 快捷入口改为底部分类抽屉
- 移除搜索框下方的快捷入口宫格(QuickLinks) - 新增 CategoryDock:底部玻璃分类胶囊(社交/娱乐/开发/游戏), 悬停时菜单以 transform-origin:bottom 从胶囊向上抽出,链接错位浮现; 含悬停桥、点击/焦点开合、reduced-motion 降级 - StartPage 加透明占位,保持问候/搜索框删除前的垂直位置与比例 - 文档:startpage-quicklinks → startpage-category-dock 并重写; 修正 weather 部署文档过期的 /mnt/f 路径 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a3075133d1
commit
0b3b85c528
30
Client/package-lock.json
generated
30
Client/package-lock.json
generated
@ -1413,9 +1413,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
|
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
|
||||||
"version": "7.8.4",
|
"version": "7.8.5",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||||
"integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
|
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
@ -1544,9 +1544,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.10.37",
|
"version": "2.10.38",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz",
|
||||||
"integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==",
|
"integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
@ -1689,9 +1689,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.375",
|
"version": "1.5.376",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.375.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.376.tgz",
|
||||||
"integrity": "sha512-ZWP5eB4BVPW/ZYo9252hQZHZ5XavtsTgpbhcmMmRwymavC5AsLWQWBPaKMeNd2LW0KGby5HPXvj7+sr4ta5j/Q==",
|
"integrity": "sha512-cUVA7/RvbFTEuw/i3obUwDTRIXojaxkResf+ibByPFxjc6XK3VNtcQXV0NSbAlJ0FMjcJGgftVVB4Qo184EXvA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
@ -2595,9 +2595,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.12",
|
"version": "3.3.14",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.14.tgz",
|
||||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
"integrity": "sha512-U9kYi5bpVMEI31yC8iw4bJJp0avcHXA0W8/wNfLfnvJYzihQo2ZRPYPvpAAd570HAcCBjCTN7vnr+v4StKl1IQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@ -2621,9 +2621,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.47",
|
"version": "2.0.48",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz",
|
||||||
"integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==",
|
"integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { fadeUp, stagger } from './motion'
|
|||||||
import { useWeather } from '../hooks/useWeather'
|
import { useWeather } from '../hooks/useWeather'
|
||||||
import { Greeting } from './start/Greeting'
|
import { Greeting } from './start/Greeting'
|
||||||
import { SearchBar } from './start/SearchBar'
|
import { SearchBar } from './start/SearchBar'
|
||||||
import { QuickLinks } from './start/QuickLinks'
|
import { CategoryDock } from './start/CategoryDock'
|
||||||
import { WeatherFx } from './start/WeatherFx'
|
import { WeatherFx } from './start/WeatherFx'
|
||||||
|
|
||||||
// 背景图:放 src/assets/startpage/ 下。按昼夜选 light / dark;
|
// 背景图:放 src/assets/startpage/ 下。按昼夜选 light / dark;
|
||||||
@ -122,10 +122,13 @@ export function StartPage() {
|
|||||||
<SearchBar />
|
<SearchBar />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.div variants={fadeUp} className="w-full">
|
{/* 占位:补回快捷入口腾出的高度,让问候/搜索框保持删除前的垂直位置与比例,
|
||||||
<QuickLinks />
|
不因居中重排而被撑到页面正中。底部入口改由 CategoryDock 承载。 */}
|
||||||
</motion.div>
|
<div aria-hidden className="h-44 w-full shrink-0 sm:h-48" />
|
||||||
</motion.main>
|
</motion.main>
|
||||||
|
|
||||||
|
{/* 底部分类抽屉:按类型分组的快捷入口,悬停某类时菜单向上抽出 */}
|
||||||
|
<CategoryDock />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
252
Client/src/components/start/CategoryDock.tsx
Normal file
252
Client/src/components/start/CategoryDock.tsx
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
import { useState, type FocusEvent, type ReactNode } from 'react'
|
||||||
|
import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'
|
||||||
|
|
||||||
|
// 底部分类抽屉:一排玻璃分类胶囊,悬停某类时菜单从胶囊里向上抽出。
|
||||||
|
// 数据结构:每个分类 { id, label, glyph, links },link = { label, href, icon }。
|
||||||
|
interface SiteLink {
|
||||||
|
label: string
|
||||||
|
href: string
|
||||||
|
icon: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Category {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
glyph: ReactNode
|
||||||
|
links: SiteLink[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 品牌图标:单 path,fill=currentColor,跟随文字色 ───────────── */
|
||||||
|
const GithubIcon = (
|
||||||
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor" aria-hidden>
|
||||||
|
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const DeepSeekIcon = (
|
||||||
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor" aria-hidden>
|
||||||
|
<path d="M23.748 4.651c-.254-.124-.364.113-.512.233-.051.04-.094.09-.137.137-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.155-.708-.311-.955-.65-.172-.24-.219-.509-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.094.172.187.129.323-.082.28-.18.553-.266.833-.055.179-.137.218-.328.14a5.5 5.5 0 0 1-1.737-1.179c-.857-.828-1.631-1.743-2.597-2.46a12 12 0 0 0-.689-.47c-.985-.957.13-1.743.387-1.836.27-.098.094-.433-.778-.428-.872.003-1.67.295-2.687.685a3 3 0 0 1-.465.136 9.6 9.6 0 0 0-2.883-.101c-1.885.21-3.39 1.1-4.497 2.622C.082 8.776-.231 10.854.152 13.02c.403 2.284 1.568 4.175 3.36 5.653 1.857 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.132-.284 4.994-1.86.47.234.962.328 1.78.398.629.058 1.235-.031 1.705-.129.735-.155.684-.836.418-.961-2.155-1.004-1.682-.595-2.112-.926 1.095-1.295 2.768-3.598 3.284-6.733.05-.346.115-.834.108-1.114-.004-.171.035-.238.23-.257a4.2 4.2 0 0 0 1.545-.475c1.397-.763 1.96-2.016 2.093-3.517.02-.23-.004-.467-.247-.588M11.58 18.168c-2.088-1.642-3.101-2.183-3.52-2.16-.39.024-.32.472-.234.763.09.288.207.487.371.74.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.168-1.361-.801-2.5-1.86-3.301-3.306-.775-1.393-1.225-2.888-1.299-4.482-.02-.385.094-.522.477-.592a4.7 4.7 0 0 1 1.53-.038c2.131.311 3.946 1.264 5.467 2.774.868.86 1.525 1.887 2.202 2.89.72 1.066 1.494 2.082 2.48 2.915.348.291.626.513.892.677-.802.09-2.14.109-3.055-.615zm1.001-6.44a.306.306 0 0 1 .415-.287.3.3 0 0 1 .113.074.3.3 0 0 1 .086.214c0 .17-.136.307-.308.307a.303.303 0 0 1-.306-.307m3.11 1.596c-.2.081-.4.151-.591.16a1.25 1.25 0 0 1-.798-.254c-.274-.23-.47-.358-.551-.758a1.7 1.7 0 0 1 .015-.588c.07-.327-.007-.537-.238-.727-.188-.156-.426-.199-.689-.199a.6.6 0 0 1-.254-.078.253.253 0 0 1-.114-.358 1 1 0 0 1 .192-.21c.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.392.451.462.576.685.915.176.264.336.536.446.848.066.194-.02.353-.25.45" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const BilibiliIcon = (
|
||||||
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor" aria-hidden>
|
||||||
|
<path d="M17.813 4.653h.854c1.51.054 2.769.578 3.773 1.574 1.004.995 1.524 2.249 1.56 3.76v7.36c-.036 1.51-.556 2.769-1.56 3.773s-2.262 1.524-3.773 1.56H5.333c-1.51-.036-2.769-.556-3.773-1.56S.036 18.858 0 17.347v-7.36c.036-1.511.556-2.765 1.56-3.76 1.004-.996 2.262-1.52 3.773-1.574h.774l-1.174-1.12a1.234 1.234 0 0 1-.373-.906c0-.356.124-.658.373-.907l.027-.027c.267-.249.573-.373.92-.373.347 0 .653.124.92.373L9.653 4.44c.071.071.134.142.187.213h4.267a.836.836 0 0 1 .16-.213l2.853-2.747c.267-.249.573-.373.92-.373.347 0 .662.151.929.4.267.249.391.551.391.907 0 .355-.124.657-.373.906zM5.333 7.24c-.746.018-1.373.276-1.88.773-.506.498-.769 1.13-.786 1.894v7.52c.017.764.28 1.395.786 1.893.507.498 1.134.756 1.88.773h13.334c.746-.017 1.373-.275 1.88-.773.506-.498.769-1.129.786-1.893v-7.52c-.017-.765-.28-1.396-.786-1.894-.507-.497-1.134-.755-1.88-.773zM8 11.107c.373 0 .684.124.933.373.25.249.383.569.4.96v1.173c-.017.391-.15.711-.4.96-.249.25-.56.374-.933.374s-.684-.125-.933-.374c-.25-.249-.383-.569-.4-.96V12.44c0-.373.129-.689.386-.947.258-.257.574-.386.947-.386zm8 0c.373 0 .684.124.933.373.25.249.383.569.4.96v1.173c-.017.391-.15.711-.4.96-.249.25-.56.374-.933.374s-.684-.125-.933-.374c-.25-.249-.383-.569-.4-.96V12.44c.017-.391.15-.711.4-.96.249-.249.56-.373.933-.373Z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const ZhihuIcon = (
|
||||||
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor" aria-hidden>
|
||||||
|
<path d="M5.721 0C2.251 0 0 2.25 0 5.719V18.28C0 21.751 2.252 24 5.721 24h12.56C21.751 24 24 21.75 24 18.281V5.72C24 2.249 21.75 0 18.281 0zm1.964 4.078c-.271.73-.5 1.434-.68 2.11h4.587c.545-.006.445 1.168.445 1.171H9.384a58.104 58.104 0 01-.112 3.797h2.712c.388.023.393 1.251.393 1.266H9.183a9.223 9.223 0 01-.408 2.102l.757-.604c.452.456 1.512 1.712 1.906 2.177.473.681.063 2.081.063 2.081l-2.794-3.382c-.653 2.518-1.845 3.607-1.845 3.607-.523.468-1.58.82-2.64.516 2.218-1.73 3.44-3.917 3.667-6.497H4.491c0-.015.197-1.243.806-1.266h2.71c.024-.32.086-3.254.086-3.797H6.598c-.136.406-.158.447-.268.753-.594 1.095-1.603 1.122-1.907 1.155.906-1.821 1.416-3.6 1.591-4.064.425-1.124 1.671-1.125 1.671-1.125zM13.078 6h6.377v11.33h-2.573l-2.184 1.373-.401-1.373h-1.219zm1.313 1.219v8.86h.623l.263.937 1.455-.938h1.456v-8.86z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const XIcon = (
|
||||||
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden>
|
||||||
|
<path d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const YoutubeIcon = (
|
||||||
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor" aria-hidden>
|
||||||
|
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const SteamIcon = (
|
||||||
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor" aria-hidden>
|
||||||
|
<path d="M11.979 0C5.678 0 .511 4.86.022 11.037l6.432 2.658c.545-.371 1.203-.59 1.912-.59.063 0 .125.004.188.006l2.861-4.142V8.91c0-2.495 2.028-4.524 4.524-4.524 2.494 0 4.524 2.031 4.524 4.527s-2.03 4.525-4.524 4.525h-.105l-4.076 2.911c0 .052.004.105.004.159 0 1.875-1.515 3.396-3.39 3.396-1.635 0-3.016-1.173-3.331-2.727L.436 15.27C1.862 20.307 6.486 24 11.979 24c6.627 0 11.999-5.373 11.999-12S18.605 0 11.979 0zM7.54 18.21l-1.473-.61c.262.543.714.999 1.314 1.25 1.297.539 2.793-.076 3.332-1.375.263-.63.264-1.319.005-1.949s-.75-1.121-1.377-1.383c-.624-.26-1.29-.249-1.878-.03l1.523.63c.956.4 1.409 1.5 1.009 2.455-.397.957-1.497 1.41-2.454 1.012H7.54zm11.415-9.303c0-1.662-1.353-3.015-3.015-3.015-1.665 0-3.015 1.353-3.015 3.015 0 1.665 1.35 3.015 3.015 3.015 1.663 0 3.015-1.35 3.015-3.015zm-5.273-.005c0-1.252 1.013-2.266 2.265-2.266 1.249 0 2.266 1.014 2.266 2.266 0 1.251-1.017 2.265-2.266 2.265-1.253 0-2.265-1.014-2.265-2.265z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
// 无可靠 simple-icons path 的站点:用字标,跟其余图标同色同风格。
|
||||||
|
const Wordmark = (text: string) => (
|
||||||
|
<span className="text-[13px] font-bold leading-none tracking-tight" aria-hidden>
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
|
||||||
|
/* ── 分类小图标:通用线性图形,非品牌,安全自绘(18px stroke)─────── */
|
||||||
|
const strokeProps = {
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
width: 17,
|
||||||
|
height: 17,
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
strokeWidth: 1.9,
|
||||||
|
strokeLinecap: 'round' as const,
|
||||||
|
strokeLinejoin: 'round' as const,
|
||||||
|
'aria-hidden': true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const SocialGlyph = (
|
||||||
|
<svg {...strokeProps}>
|
||||||
|
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const FunGlyph = (
|
||||||
|
<svg {...strokeProps}>
|
||||||
|
<circle cx="12" cy="12" r="9" />
|
||||||
|
<path d="M10 8.5 16 12l-6 3.5z" fill="currentColor" stroke="none" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const DevGlyph = (
|
||||||
|
<svg {...strokeProps}>
|
||||||
|
<path d="m16 18 6-6-6-6" />
|
||||||
|
<path d="m8 6-6 6 6 6" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const GameGlyph = (
|
||||||
|
<svg {...strokeProps}>
|
||||||
|
<path d="M7 12h4M9 10v4" />
|
||||||
|
<circle cx="15.5" cy="11" r="0.6" fill="currentColor" />
|
||||||
|
<circle cx="17.5" cy="13" r="0.6" fill="currentColor" />
|
||||||
|
<rect x="2.5" y="6.5" width="19" height="11" rx="5.5" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
// 分类顺序即从左到右的展示顺序。每类的 links 即抽屉里的内容。
|
||||||
|
const CATEGORIES: Category[] = [
|
||||||
|
{
|
||||||
|
id: 'social',
|
||||||
|
label: '社交',
|
||||||
|
glyph: SocialGlyph,
|
||||||
|
links: [
|
||||||
|
{ label: '知乎', href: 'https://www.zhihu.com', icon: ZhihuIcon },
|
||||||
|
{ label: '微博', href: 'https://weibo.com', icon: Wordmark('微博') },
|
||||||
|
{ label: 'X', href: 'https://x.com', icon: XIcon },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fun',
|
||||||
|
label: '娱乐',
|
||||||
|
glyph: FunGlyph,
|
||||||
|
links: [
|
||||||
|
{ label: 'Bilibili', href: 'https://www.bilibili.com', icon: BilibiliIcon },
|
||||||
|
{ label: 'YouTube', href: 'https://www.youtube.com', icon: YoutubeIcon },
|
||||||
|
{ label: 'FMHY', href: 'https://fm-hy.top', icon: Wordmark('FMHY') },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dev',
|
||||||
|
label: '开发',
|
||||||
|
glyph: DevGlyph,
|
||||||
|
links: [
|
||||||
|
{ label: 'GitHub', href: 'https://github.com', icon: GithubIcon },
|
||||||
|
{ label: 'DeepSeek', href: 'https://chat.deepseek.com', icon: DeepSeekIcon },
|
||||||
|
{ label: 'Rust 文档', href: 'https://rustwiki.org/zh-CN/book/', icon: Wordmark('Rs') },
|
||||||
|
{ label: 'C++ 文档', href: 'https://en.cppreference.com/', icon: Wordmark('C++') },
|
||||||
|
{ label: 'C# 文档', href: 'https://learn.microsoft.com/dotnet/csharp/', icon: Wordmark('C#') },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'game',
|
||||||
|
label: '游戏',
|
||||||
|
glyph: GameGlyph,
|
||||||
|
links: [
|
||||||
|
{ label: 'Steam', href: 'https://store.steampowered.com', icon: SteamIcon },
|
||||||
|
{ label: 'Epic Games', href: 'https://store.epicgames.com', icon: Wordmark('Epic') },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export function CategoryDock() {
|
||||||
|
// 同一时刻只展开一个抽屉。悬停/聚焦/点击都改这一个状态。
|
||||||
|
const [active, setActive] = useState<string | null>(null)
|
||||||
|
const reduce = useReducedMotion()
|
||||||
|
|
||||||
|
// 焦点移出整个分类块(含抽屉内链接)才收起,方向键/Tab 浏览不会闪。
|
||||||
|
function handleBlur(id: string, e: FocusEvent<HTMLDivElement>) {
|
||||||
|
if (!e.currentTarget.contains(e.relatedTarget as Node | null)) {
|
||||||
|
setActive((cur) => (cur === id ? null : cur))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
aria-label="网站分类"
|
||||||
|
className="pointer-events-none fixed inset-x-0 bottom-6 z-20 flex justify-center px-4"
|
||||||
|
>
|
||||||
|
<div className="pointer-events-auto flex items-end gap-2 sm:gap-3">
|
||||||
|
{CATEGORIES.map((cat) => {
|
||||||
|
const open = active === cat.id
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={cat.id}
|
||||||
|
className="relative"
|
||||||
|
onMouseEnter={() => setActive(cat.id)}
|
||||||
|
onMouseLeave={() => setActive((cur) => (cur === cat.id ? null : cur))}
|
||||||
|
onFocus={() => setActive(cat.id)}
|
||||||
|
onBlur={(e) => handleBlur(cat.id, e)}
|
||||||
|
>
|
||||||
|
{/* 抽屉:从胶囊上沿向上抽出。pb-2.5 既留视觉间隙,又把间隙纳入悬停命中区。 */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{open && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.12 }}
|
||||||
|
className="absolute bottom-full left-1/2 w-44 -translate-x-1/2 pb-2.5"
|
||||||
|
>
|
||||||
|
<motion.ul
|
||||||
|
role="menu"
|
||||||
|
aria-label={cat.label}
|
||||||
|
initial={reduce ? { opacity: 0 } : { opacity: 0, y: 10, scaleY: 0.82 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scaleY: 1 }}
|
||||||
|
exit={reduce ? { opacity: 0 } : { opacity: 0, y: 10, scaleY: 0.82 }}
|
||||||
|
transition={{ type: 'spring', stiffness: 460, damping: 32 }}
|
||||||
|
style={{ transformOrigin: 'bottom' }}
|
||||||
|
className="glass-panel flex flex-col gap-0.5 rounded-2xl p-1.5"
|
||||||
|
>
|
||||||
|
{cat.links.map((link, i) => (
|
||||||
|
<motion.li
|
||||||
|
key={link.href}
|
||||||
|
role="none"
|
||||||
|
initial={reduce ? false : { opacity: 0, y: 6 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: reduce ? 0 : 0.04 + i * 0.035 }}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
role="menuitem"
|
||||||
|
href={link.href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="flex items-center gap-3 rounded-xl px-2.5 py-2 text-sm text-white/75 transition-colors hover:bg-white/12 hover:text-white"
|
||||||
|
>
|
||||||
|
<span className="flex h-5 w-7 shrink-0 items-center justify-center">
|
||||||
|
{link.icon}
|
||||||
|
</span>
|
||||||
|
<span className="truncate">{link.label}</span>
|
||||||
|
</a>
|
||||||
|
</motion.li>
|
||||||
|
))}
|
||||||
|
</motion.ul>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* 分类胶囊(抽屉的把手)。展开时高亮,并把下方圆角拍平贴合抽屉。 */}
|
||||||
|
<motion.button
|
||||||
|
type="button"
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-expanded={open}
|
||||||
|
onClick={() => setActive((cur) => (cur === cat.id ? null : cat.id))}
|
||||||
|
whileTap={{ scale: 0.96 }}
|
||||||
|
className={`glass-panel flex items-center gap-2 rounded-full px-3.5 py-2.5 text-sm font-medium transition-colors sm:px-4 ${
|
||||||
|
open ? 'text-white' : 'text-white/80 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="shrink-0">{cat.glyph}</span>
|
||||||
|
<span>{cat.label}</span>
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,97 +0,0 @@
|
|||||||
import { motion } from 'framer-motion'
|
|
||||||
import type { ReactNode } from 'react'
|
|
||||||
|
|
||||||
// 快捷入口:第一行 5 个网站,第二行 3 个文档。每项 { icon, label, href }。
|
|
||||||
interface Shortcut {
|
|
||||||
label?: string
|
|
||||||
href?: string
|
|
||||||
icon?: ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
// 品牌图标:单 path,fill=currentColor,跟随文字颜色。
|
|
||||||
const GithubIcon = (
|
|
||||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden>
|
|
||||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
|
|
||||||
const DeepSeekIcon = (
|
|
||||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden>
|
|
||||||
<path d="M23.748 4.651c-.254-.124-.364.113-.512.233-.051.04-.094.09-.137.137-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.155-.708-.311-.955-.65-.172-.24-.219-.509-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.094.172.187.129.323-.082.28-.18.553-.266.833-.055.179-.137.218-.328.14a5.5 5.5 0 0 1-1.737-1.179c-.857-.828-1.631-1.743-2.597-2.46a12 12 0 0 0-.689-.47c-.985-.957.13-1.743.387-1.836.27-.098.094-.433-.778-.428-.872.003-1.67.295-2.687.685a3 3 0 0 1-.465.136 9.6 9.6 0 0 0-2.883-.101c-1.885.21-3.39 1.1-4.497 2.622C.082 8.776-.231 10.854.152 13.02c.403 2.284 1.568 4.175 3.36 5.653 1.857 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.132-.284 4.994-1.86.47.234.962.328 1.78.398.629.058 1.235-.031 1.705-.129.735-.155.684-.836.418-.961-2.155-1.004-1.682-.595-2.112-.926 1.095-1.295 2.768-3.598 3.284-6.733.05-.346.115-.834.108-1.114-.004-.171.035-.238.23-.257a4.2 4.2 0 0 0 1.545-.475c1.397-.763 1.96-2.016 2.093-3.517.02-.23-.004-.467-.247-.588M11.58 18.168c-2.088-1.642-3.101-2.183-3.52-2.16-.39.024-.32.472-.234.763.09.288.207.487.371.74.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.168-1.361-.801-2.5-1.86-3.301-3.306-.775-1.393-1.225-2.888-1.299-4.482-.02-.385.094-.522.477-.592a4.7 4.7 0 0 1 1.53-.038c2.131.311 3.946 1.264 5.467 2.774.868.86 1.525 1.887 2.202 2.89.72 1.066 1.494 2.082 2.48 2.915.348.291.626.513.892.677-.802.09-2.14.109-3.055-.615zm1.001-6.44a.306.306 0 0 1 .415-.287.3.3 0 0 1 .113.074.3.3 0 0 1 .086.214c0 .17-.136.307-.308.307a.303.303 0 0 1-.306-.307m3.11 1.596c-.2.081-.4.151-.591.16a1.25 1.25 0 0 1-.798-.254c-.274-.23-.47-.358-.551-.758a1.7 1.7 0 0 1 .015-.588c.07-.327-.007-.537-.238-.727-.188-.156-.426-.199-.689-.199a.6.6 0 0 1-.254-.078.253.253 0 0 1-.114-.358 1 1 0 0 1 .192-.21c.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.392.451.462.576.685.915.176.264.336.536.446.848.066.194-.02.353-.25.45" />
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
|
|
||||||
const BilibiliIcon = (
|
|
||||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden>
|
|
||||||
<path d="M17.813 4.653h.854c1.51.054 2.769.578 3.773 1.574 1.004.995 1.524 2.249 1.56 3.76v7.36c-.036 1.51-.556 2.769-1.56 3.773s-2.262 1.524-3.773 1.56H5.333c-1.51-.036-2.769-.556-3.773-1.56S.036 18.858 0 17.347v-7.36c.036-1.511.556-2.765 1.56-3.76 1.004-.996 2.262-1.52 3.773-1.574h.774l-1.174-1.12a1.234 1.234 0 0 1-.373-.906c0-.356.124-.658.373-.907l.027-.027c.267-.249.573-.373.92-.373.347 0 .653.124.92.373L9.653 4.44c.071.071.134.142.187.213h4.267a.836.836 0 0 1 .16-.213l2.853-2.747c.267-.249.573-.373.92-.373.347 0 .662.151.929.4.267.249.391.551.391.907 0 .355-.124.657-.373.906zM5.333 7.24c-.746.018-1.373.276-1.88.773-.506.498-.769 1.13-.786 1.894v7.52c.017.764.28 1.395.786 1.893.507.498 1.134.756 1.88.773h13.334c.746-.017 1.373-.275 1.88-.773.506-.498.769-1.129.786-1.893v-7.52c-.017-.765-.28-1.396-.786-1.894-.507-.497-1.134-.755-1.88-.773zM8 11.107c.373 0 .684.124.933.373.25.249.383.569.4.96v1.173c-.017.391-.15.711-.4.96-.249.25-.56.374-.933.374s-.684-.125-.933-.374c-.25-.249-.383-.569-.4-.96V12.44c0-.373.129-.689.386-.947.258-.257.574-.386.947-.386zm8 0c.373 0 .684.124.933.373.25.249.383.569.4.96v1.173c-.017.391-.15.711-.4.96-.249.25-.56.374-.933.374s-.684-.125-.933-.374c-.25-.249-.383-.569-.4-.96V12.44c.017-.391.15-.711.4-.96.249-.249.56-.373.933-.373Z" />
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
|
|
||||||
const ZhihuIcon = (
|
|
||||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden>
|
|
||||||
<path d="M5.721 0C2.251 0 0 2.25 0 5.719V18.28C0 21.751 2.252 24 5.721 24h12.56C21.751 24 24 21.75 24 18.281V5.72C24 2.249 21.75 0 18.281 0zm1.964 4.078c-.271.73-.5 1.434-.68 2.11h4.587c.545-.006.445 1.168.445 1.171H9.384a58.104 58.104 0 01-.112 3.797h2.712c.388.023.393 1.251.393 1.266H9.183a9.223 9.223 0 01-.408 2.102l.757-.604c.452.456 1.512 1.712 1.906 2.177.473.681.063 2.081.063 2.081l-2.794-3.382c-.653 2.518-1.845 3.607-1.845 3.607-.523.468-1.58.82-2.64.516 2.218-1.73 3.44-3.917 3.667-6.497H4.491c0-.015.197-1.243.806-1.266h2.71c.024-.32.086-3.254.086-3.797H6.598c-.136.406-.158.447-.268.753-.594 1.095-1.603 1.122-1.907 1.155.906-1.821 1.416-3.6 1.591-4.064.425-1.124 1.671-1.125 1.671-1.125zM13.078 6h6.377v11.33h-2.573l-2.184 1.373-.401-1.373h-1.219zm1.313 1.219v8.86h.623l.263.937 1.455-.938h1.456v-8.86z" />
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
|
|
||||||
// FMHY 不在 simple-icons 图标库里,用字标,跟其余图标同色同风格。
|
|
||||||
const FmhyIcon = (
|
|
||||||
<span className="text-[15px] font-bold leading-none tracking-tight" aria-hidden>
|
|
||||||
FMHY
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
|
|
||||||
const RustIcon = (
|
|
||||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden>
|
|
||||||
<path d="M23.8346 11.7033l-1.0073-.6236a13.7268 13.7268 0 00-.0283-.2936l.8656-.8069a.3483.3483 0 00-.1154-.578l-1.1066-.414a8.4958 8.4958 0 00-.087-.2856l.6904-.9587a.3462.3462 0 00-.2257-.5446l-1.1663-.1894a9.3574 9.3574 0 00-.1407-.2622l.49-1.0761a.3437.3437 0 00-.0274-.3361.3486.3486 0 00-.3006-.154l-1.1845.0416a6.7444 6.7444 0 00-.1873-.2268l.2723-1.153a.3472.3472 0 00-.417-.4172l-1.1532.2724a14.0183 14.0183 0 00-.2278-.1873l.0415-1.1845a.3442.3442 0 00-.49-.328l-1.076.491c-.0872-.0476-.1742-.0952-.2623-.1407l-.1903-1.1673A.3483.3483 0 0016.256.955l-.9597.6905a8.4867 8.4867 0 00-.2855-.086l-.414-1.1066a.3483.3483 0 00-.5781-.1154l-.8069.8666a9.2936 9.2936 0 00-.2936-.0284L12.2946.1683a.3462.3462 0 00-.5892 0l-.6236 1.0073a13.7383 13.7383 0 00-.2936.0284L9.9803.3374a.3462.3462 0 00-.578.1154l-.4141 1.1065c-.0962.0274-.1903.0567-.2855.086L7.744.955a.3483.3483 0 00-.5447.2258L7.009 2.348a9.3574 9.3574 0 00-.2622.1407l-1.0762-.491a.3462.3462 0 00-.49.328l.0416 1.1845a7.9826 7.9826 0 00-.2278.1873L3.8413 3.425a.3472.3472 0 00-.4171.4171l.2713 1.1531c-.0628.075-.1255.1509-.1863.2268l-1.1845-.0415a.3462.3462 0 00-.328.49l.491 1.0761a9.167 9.167 0 00-.1407.2622l-1.1662.1894a.3483.3483 0 00-.2258.5446l.6904.9587a13.303 13.303 0 00-.087.2855l-1.1065.414a.3483.3483 0 00-.1155.5781l.8656.807a9.2936 9.2936 0 00-.0283.2935l-1.0073.6236a.3442.3442 0 000 .5892l1.0073.6236c.008.0982.0182.1964.0283.2936l-.8656.8079a.3462.3462 0 00.1155.578l1.1065.4141c.0273.0962.0567.1914.087.2855l-.6904.9587a.3452.3452 0 00.2268.5447l1.1662.1893c.0456.088.0922.1751.1408.2622l-.491 1.0762a.3462.3462 0 00.328.49l1.1834-.0415c.0618.0769.1235.1528.1873.2277l-.2713 1.1541a.3462.3462 0 00.4171.4161l1.153-.2713c.075.0638.151.1255.2279.1863l-.0415 1.1845a.3442.3442 0 00.49.327l1.0761-.49c.087.0486.1741.0951.2622.1407l.1903 1.1662a.3483.3483 0 00.5447.2268l.9587-.6904a9.299 9.299 0 00.2855.087l.414 1.1066a.3452.3452 0 00.5781.1154l.8079-.8656c.0972.0111.1954.0203.2936.0294l.6236 1.0073a.3472.3472 0 00.5892 0l.6236-1.0073c.0982-.0091.1964-.0183.2936-.0294l.8069.8656a.3483.3483 0 00.578-.1154l.4141-1.1066a8.4626 8.4626 0 00.2855-.087l.9587.6904a.3452.3452 0 00.5447-.2268l.1903-1.1662c.088-.0456.1751-.0931.2622-.1407l1.0762.49a.3472.3472 0 00.49-.327l-.0415-1.1845a6.7267 6.7267 0 00.2267-.1863l1.1531.2713a.3472.3472 0 00.4171-.416l-.2713-1.1542c.0628-.0749.1255-.1508.1863-.2278l1.1845.0415a.3442.3442 0 00.328-.49l-.49-1.076c.0475-.0872.0951-.1742.1407-.2623l1.1662-.1893a.3483.3483 0 00.2258-.5447l-.6904-.9587.087-.2855 1.1066-.414a.3462.3462 0 00.1154-.5781l-.8656-.8079c.0101-.0972.0202-.1954.0283-.2936l1.0073-.6236a.3442.3442 0 000-.5892zm-6.7413 8.3551a.7138.7138 0 01.2986-1.396.714.714 0 11-.2997 1.396zm-.3422-2.3142a.649.649 0 00-.7715.5l-.3573 1.6685c-1.1035.501-2.3285.7795-3.6193.7795a8.7368 8.7368 0 01-3.6951-.814l-.3574-1.6684a.648.648 0 00-.7714-.499l-1.473.3158a8.7216 8.7216 0 01-.7613-.898h7.1676c.081 0 .1356-.0141.1356-.088v-2.536c0-.074-.0536-.0881-.1356-.0881h-2.0966v-1.6077h2.2677c.2065 0 1.1065.0587 1.394 1.2088.0901.3533.2875 1.5044.4232 1.8729.1346.413.6833 1.2381 1.2685 1.2381h3.5716a.7492.7492 0 00.1296-.0131 8.7874 8.7874 0 01-.8119.9526zM6.8369 20.024a.714.714 0 11-.2997-1.396.714.714 0 01.2997 1.396zM4.1177 8.9972a.7137.7137 0 11-1.304.5791.7137.7137 0 011.304-.579zm-.8352 1.9813l1.5347-.6824a.65.65 0 00.33-.8585l-.3158-.7147h1.2432v5.6025H3.5669a8.7753 8.7753 0 01-.2834-3.348zm6.7343-.5437V8.7836h2.9601c.153 0 1.0792.1772 1.0792.8697 0 .575-.7107.7815-1.2948.7815zm10.7574 1.4862c0 .2187-.008.4363-.0243.651h-.9c-.09 0-.1265.0586-.1265.1477v.413c0 .973-.5487 1.1846-1.0296 1.2382-.4576.0517-.9648-.1913-1.0275-.4717-.2704-1.5186-.7198-1.8436-1.4305-2.4034.8817-.5599 1.799-1.386 1.799-2.4915 0-1.1936-.819-1.9458-1.3769-2.3153-.7825-.5163-1.6491-.6195-1.883-.6195H5.4682a8.7651 8.7651 0 014.907-2.7699l1.0974 1.151a.648.648 0 00.9182.0213l1.227-1.1743a8.7753 8.7753 0 016.0044 4.2762l-.8403 1.8982a.652.652 0 00.33.8585l1.6178.7188c.0283.2875.0425.577.0425.8717zm-9.3006-9.5993a.7128.7128 0 11.984 1.0316.7137.7137 0 01-.984-1.0316zm8.3389 6.71a.7107.7107 0 01.9395-.3625.7137.7137 0 11-.9405.3635z" />
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
|
|
||||||
const CppIcon = (
|
|
||||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden>
|
|
||||||
<path d="M22.394 6c-.167-.29-.398-.543-.652-.69L12.926.22c-.509-.294-1.34-.294-1.848 0L2.26 5.31c-.508.293-.923 1.013-.923 1.6v10.18c0 .294.104.62.271.91.167.29.398.543.652.69l8.816 5.09c.508.293 1.34.293 1.848 0l8.816-5.09c.254-.147.485-.4.652-.69.167-.29.27-.616.27-.91V6.91c.003-.294-.1-.62-.268-.91zM12 19.11c-3.92 0-7.109-3.19-7.109-7.11 0-3.92 3.19-7.11 7.11-7.11a7.133 7.133 0 016.156 3.553l-3.076 1.78a3.567 3.567 0 00-3.08-1.78A3.56 3.56 0 008.444 12 3.56 3.56 0 0012 15.555a3.57 3.57 0 003.08-1.778l3.078 1.78A7.135 7.135 0 0112 19.11zm7.11-6.715h-.79v.79h-.79v-.79h-.79v-.79h.79v-.79h.79v.79h.79zm2.962 0h-.79v.79h-.79v-.79h-.79v-.79h.79v-.79h.79v.79h.79z" />
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
|
|
||||||
const CsharpIcon = (
|
|
||||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden>
|
|
||||||
<path d="M1.194 7.543v8.913c0 1.103.588 2.122 1.544 2.674l7.718 4.456a3.086 3.086 0 0 0 3.088 0l7.718-4.456a3.087 3.087 0 0 0 1.544-2.674V7.543a3.084 3.084 0 0 0-1.544-2.673L13.544.414a3.086 3.086 0 0 0-3.088 0L2.738 4.87a3.085 3.085 0 0 0-1.544 2.673Zm5.403 2.914v3.087a.77.77 0 0 0 .772.772.773.773 0 0 0 .772-.772.773.773 0 0 1 1.317-.546.775.775 0 0 1 .226.546 2.314 2.314 0 1 1-4.631 0v-3.087c0-.615.244-1.203.679-1.637a2.312 2.312 0 0 1 3.274 0c.434.434.678 1.023.678 1.637a.769.769 0 0 1-.226.545.767.767 0 0 1-1.091 0 .77.77 0 0 1-.226-.545.77.77 0 0 0-.772-.772.771.771 0 0 0-.772.772Zm12.35 3.087a.77.77 0 0 1-.772.772h-.772v.772a.773.773 0 0 1-1.544 0v-.772h-1.544v.772a.773.773 0 0 1-1.317.546.775.775 0 0 1-.226-.546v-.772H12a.771.771 0 1 1 0-1.544h.772v-1.543H12a.77.77 0 1 1 0-1.544h.772v-.772a.773.773 0 0 1 1.317-.546.775.775 0 0 1 .226.546v.772h1.544v-.772a.773.773 0 0 1 1.544 0v.772h.772a.772.772 0 0 1 0 1.544h-.772v1.543h.772a.776.776 0 0 1 .772.772Zm-3.088-2.315h-1.544v1.543h1.544v-1.543Z" />
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
|
|
||||||
const SHORTCUTS: Shortcut[] = [
|
|
||||||
{ label: 'DeepSeek', href: 'https://chat.deepseek.com', icon: DeepSeekIcon },
|
|
||||||
{ label: 'GitHub', href: 'https://github.com', icon: GithubIcon },
|
|
||||||
{ label: 'Bilibili', href: 'https://www.bilibili.com', icon: BilibiliIcon },
|
|
||||||
{ label: '知乎', href: 'https://www.zhihu.com', icon: ZhihuIcon },
|
|
||||||
{ label: 'FMHY', href: 'https://fm-hy.top', icon: FmhyIcon },
|
|
||||||
{ label: 'Rust 文档', href: 'https://rustwiki.org/zh-CN/book/', icon: RustIcon },
|
|
||||||
{ label: 'C++ 文档', href: 'https://en.cppreference.com/', icon: CppIcon },
|
|
||||||
{ label: 'C# 文档', href: 'https://learn.microsoft.com/dotnet/csharp/', icon: CsharpIcon },
|
|
||||||
]
|
|
||||||
|
|
||||||
export function QuickLinks() {
|
|
||||||
return (
|
|
||||||
// grid-cols-5:8 个入口自然排成第一行 5 个、第二行 3 个(左对齐)。
|
|
||||||
<div className="mx-auto grid w-full max-w-xl grid-cols-5 gap-3 sm:gap-4">
|
|
||||||
{SHORTCUTS.map((item, i) => (
|
|
||||||
<motion.a
|
|
||||||
key={i}
|
|
||||||
href={item.href}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
aria-label={item.label}
|
|
||||||
title={item.label}
|
|
||||||
whileHover={{ scale: 1.12, y: -4 }}
|
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
transition={{ type: 'spring', stiffness: 400, damping: 22 }}
|
|
||||||
// 第二行第一个入口从第 2 列起,3 个占第 2/3/4 列即居中。
|
|
||||||
className={`glass-panel flex aspect-square items-center justify-center rounded-2xl text-white/75 transition-colors hover:text-white${
|
|
||||||
i === 5 ? ' col-start-2' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{item.icon}
|
|
||||||
</motion.a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
74
docs/startpage-category-dock.md
Normal file
74
docs/startpage-category-dock.md
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# 起始页:搜索框 + 底部分类抽屉(已实现)
|
||||||
|
|
||||||
|
导航起始页竖直居中的是问候语 / 天气行下方的**搜索框**;原先搜索框下方那组
|
||||||
|
快捷入口宫格已**移除**,改为屏幕**底部的分类抽屉**(`CategoryDock`)——一排玻璃
|
||||||
|
分类胶囊,鼠标悬停某类时菜单从胶囊里向上「抽」出来。
|
||||||
|
|
||||||
|
- 前端:
|
||||||
|
- `Client/src/components/start/SearchBar.tsx`:搜索框 + 引擎下拉
|
||||||
|
- `Client/src/components/start/CategoryDock.tsx`:底部分类抽屉
|
||||||
|
- `Client/src/components/StartPage.tsx`:装配三块(问候 / 搜索框 / 占位),并在页底渲染 `CategoryDock`
|
||||||
|
|
||||||
|
## 搜索框(`SearchBar.tsx`)
|
||||||
|
|
||||||
|
- 内置 4 个引擎,默认 **Bing**:Bing / Google / 百度 / DuckDuckGo。
|
||||||
|
每个引擎是 `{ name, url }`,`url` 后直接拼 `encodeURIComponent(query)`。
|
||||||
|
- 选中的引擎存 `localStorage`(键 `ip.startpage.engine`),下次进页面记住。
|
||||||
|
- 提交后 `window.location.href` 跳到引擎搜索结果(当前标签页)。
|
||||||
|
- 要加引擎:往 `ENGINES` 里加一项即可,下拉会自动多一行。
|
||||||
|
|
||||||
|
## 底部分类抽屉(`CategoryDock.tsx`)
|
||||||
|
|
||||||
|
固定在视口底部居中(`fixed inset-x-0 bottom-6`,外层 `pointer-events-none`、
|
||||||
|
仅胶囊 `pointer-events-auto`,不挡页面其它点击)。数据是 `CATEGORIES: Category[]`,
|
||||||
|
顺序即从左到右,每类 `{ id, label, glyph, links }`,`link = { label, href, icon }`。
|
||||||
|
|
||||||
|
当前分类与内容:
|
||||||
|
|
||||||
|
| 分类 | 站点 |
|
||||||
|
|------|------|
|
||||||
|
| 社交 | 知乎、微博、X |
|
||||||
|
| 娱乐 | Bilibili、YouTube、FMHY |
|
||||||
|
| 开发 | GitHub、DeepSeek、Rust 文档、C++ 文档、C# 文档 |
|
||||||
|
| 游戏 | Steam、Epic Games |
|
||||||
|
|
||||||
|
### 交互(抽屉动画)
|
||||||
|
|
||||||
|
- **悬停展开**:同一时刻只展开一个,状态是单个 `active: string | null`。
|
||||||
|
鼠标进入某类的包裹层 → `setActive(id)`,离开 → 清空。
|
||||||
|
- **从胶囊里向上抽出**:抽屉 `absolute bottom-full`(下沿贴住胶囊上沿),
|
||||||
|
动画 `transformOrigin: 'bottom'` + 弹簧 `scaleY 0.82→1` / `y 10→0`,
|
||||||
|
里面的链接再按 `delay` 逐个错位浮现(`role="menu"` / `menuitem`)。
|
||||||
|
- **悬停桥**:抽屉容器留 `pb-2.5`,把胶囊与菜单之间的视觉间隙纳入同一悬停命中区,
|
||||||
|
鼠标从胶囊移到菜单时不会断开。
|
||||||
|
- **触屏 / 键盘**:点击胶囊 `toggle`(触屏可开合);`focus`/`blur`(带
|
||||||
|
`relatedTarget` 包含判断)支持键盘聚焦展开、移出收起。
|
||||||
|
- **降级**:`prefers-reduced-motion` 下抽屉与列表项退化为纯淡入。
|
||||||
|
|
||||||
|
### 图标
|
||||||
|
|
||||||
|
- 品牌 logo 用 [simple-icons](https://simpleicons.org/) 的单 `path`,内联成 SVG、
|
||||||
|
`fill=currentColor` 跟随文字色(默认 `white/75`、悬停纯白),与玻璃风统一、零依赖。
|
||||||
|
- 没有可靠官方 `path` 的站点(微博 / Rust / C++ / C# / Epic)用紧凑字标
|
||||||
|
`Wordmark('…')` 占位(同色同风格,沿用原 FMHY 字标做法)。
|
||||||
|
- 分类胶囊左侧的小图标(社交 / 娱乐 / 开发 / 游戏)是**通用线性图形**(聊天气泡 /
|
||||||
|
播放三角 / `</>` / 手柄),非品牌、安全自绘。
|
||||||
|
|
||||||
|
### 增 / 删一个分类或站点
|
||||||
|
|
||||||
|
1. **加站点**:若是品牌站,去 simple-icons 复制 `path d="…"` 照现有 `XxxIcon`
|
||||||
|
写一个内联 SVG 常量(`width/height≈20`、`fill=currentColor`);没有官方图标就
|
||||||
|
用 `Wordmark('字标')`。然后往对应分类的 `links` 加 `{ label, href, icon }`。
|
||||||
|
2. **加分类**:往 `CATEGORIES` 加一项 `{ id, label, glyph, links }`,`glyph` 仿现有
|
||||||
|
线性小图标自绘即可;数组顺序就是底部从左到右的顺序。
|
||||||
|
|
||||||
|
## 起始页布局占位
|
||||||
|
|
||||||
|
`StartPage.tsx` 在搜索框下方保留了一块透明占位 `<div className="h-44 sm:h-48">`:
|
||||||
|
补回原快捷入口宫格腾出的高度,让居中的问候 / 搜索框停在删除前的垂直位置与比例,
|
||||||
|
不因居中重排被撑到页面正中。底部入口已交由 `CategoryDock` 承载。
|
||||||
|
|
||||||
|
## 部署
|
||||||
|
|
||||||
|
纯前端改动,按 `docs/startpage-weather-location.md`「只改前端」一节:
|
||||||
|
WSL 里 `npm run build` → 清空服务器 `static/` → 上传 `dist/*`,后端进程不用动。
|
||||||
@ -1,55 +0,0 @@
|
|||||||
# 起始页:搜索框 + 快捷入口(已实现)
|
|
||||||
|
|
||||||
导航起始页在问候语 / 天气行下方有两块:可切换搜索引擎的**搜索框**,
|
|
||||||
以及一组**快捷入口**(第一行 5 个网站、第二行 3 个文档,居中)。
|
|
||||||
|
|
||||||
- 前端:
|
|
||||||
- `Client/src/components/start/SearchBar.tsx`:搜索框 + 引擎下拉
|
|
||||||
- `Client/src/components/start/QuickLinks.tsx`:快捷入口宫格
|
|
||||||
|
|
||||||
## 搜索框(`SearchBar.tsx`)
|
|
||||||
|
|
||||||
- 内置 4 个引擎,默认 **Bing**:Bing / Google / 百度 / DuckDuckGo。
|
|
||||||
每个引擎是 `{ name, url }`,`url` 后直接拼 `encodeURIComponent(query)`。
|
|
||||||
- 选中的引擎存 `localStorage`(键 `ip.startpage.engine`),下次进页面记住。
|
|
||||||
- 提交后 `window.location.href` 跳到引擎搜索结果(当前标签页)。
|
|
||||||
- 要加引擎:往 `ENGINES` 里加一项即可,下拉会自动多一行。
|
|
||||||
|
|
||||||
## 快捷入口(`QuickLinks.tsx`)
|
|
||||||
|
|
||||||
- 数据是一个 `SHORTCUTS: Shortcut[]`,每项 `{ label, href, icon }`。
|
|
||||||
当前 8 个,每个用 `<motion.a target="_blank">` 新标签页打开:
|
|
||||||
|
|
||||||
| 行 | 入口 | 链接 |
|
|
||||||
|----|------|------|
|
|
||||||
| 一 | DeepSeek | `https://chat.deepseek.com` |
|
|
||||||
| 一 | GitHub | `https://github.com` |
|
|
||||||
| 一 | Bilibili | `https://www.bilibili.com` |
|
|
||||||
| 一 | 知乎 | `https://www.zhihu.com` |
|
|
||||||
| 一 | FMHY | `https://fm-hy.top` |
|
|
||||||
| 二 | Rust 文档 | `https://rustwiki.org/zh-CN/book/` |
|
|
||||||
| 二 | C++ 文档 | `https://en.cppreference.com/` |
|
|
||||||
| 二 | C# 文档 | `https://learn.microsoft.com/dotnet/csharp/` |
|
|
||||||
|
|
||||||
- **布局**:容器是 `grid-cols-5`,8 个元素自然排成「第一行 5 个、第二行 3 个」。
|
|
||||||
第二行第一个入口加 `col-start-2`,让那 3 个落在第 2/3/4 列即**居中**。
|
|
||||||
- **图标**:品牌 logo 用 [simple-icons](https://simpleicons.org/) 的单 `path`,
|
|
||||||
内联成 SVG、`fill=currentColor` 跟随文字颜色(默认 `white/75`、悬停纯白),
|
|
||||||
和玻璃风、天气图标统一,零依赖。
|
|
||||||
- **FMHY** 不在 simple-icons 里,用紧凑 `FMHY` 字标占位(同色同风格)。
|
|
||||||
- **C#** 用 simple-icons 的 `csharp`(六边形 + C#),不是单独的 `#`(sharp)。
|
|
||||||
- 悬停有 framer-motion 弹簧放大上浮动效(与其它起始页元素一致)。
|
|
||||||
|
|
||||||
### 增 / 删一个入口
|
|
||||||
|
|
||||||
1. 若是品牌站,去 simple-icons 复制对应图标的 `path d="…"`,照现有 `XxxIcon`
|
|
||||||
写一个内联 SVG 常量(`width/height=22`、`fill=currentColor`);没有官方图标就
|
|
||||||
仿 `FmhyIcon` 用 `<span>` 字标。
|
|
||||||
2. 往 `SHORTCUTS` 加 `{ label, href, icon }`。
|
|
||||||
3. 调整居中:第二行第一个入口要带 `col-start-N`。当前规则写死在 `i === 5`
|
|
||||||
(第 6 个起是第二行);若每行个数变了,按新的「第二行起始下标 / 居中起始列」改这两处。
|
|
||||||
|
|
||||||
## 部署
|
|
||||||
|
|
||||||
纯前端改动,按 `docs/startpage-weather-location.md`「只改前端」一节:
|
|
||||||
`npm run build` → 清空服务器 `static/` → 上传 `dist/*`,后端进程不用动。
|
|
||||||
@ -103,7 +103,7 @@ PORT=8080 cargo run
|
|||||||
## 部署:改了后端时(重新编译)
|
## 部署:改了后端时(重新编译)
|
||||||
|
|
||||||
改了 `Server/` 下代码时上线需要**重新编译 Rust 二进制**:
|
改了 `Server/` 下代码时上线需要**重新编译 Rust 二进制**:
|
||||||
1. WSL 里 `cd /mnt/f/Code/InternetProject/Server && cargo build --release`
|
1. WSL 里 `cd /home/cc/InternetProject/Server && cargo build --release`
|
||||||
2. scp 覆盖服务器 `/opt/internet-project/RustServer` + 前端 `dist/*`
|
2. scp 覆盖服务器 `/opt/internet-project/RustServer` + 前端 `dist/*`
|
||||||
3. 私钥与密钥建议写进服务器上的一个文件再统一加载,避免出现在命令历史里。
|
3. 私钥与密钥建议写进服务器上的一个文件再统一加载,避免出现在命令历史里。
|
||||||
例如服务器放 `/opt/internet-project/qweather.pem` 和 `/opt/internet-project/weather.env`:
|
例如服务器放 `/opt/internet-project/qweather.pem` 和 `/opt/internet-project/weather.env`:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user