diff --git a/Client/package-lock.json b/Client/package-lock.json
index 7dff91a..7e5927a 100644
--- a/Client/package-lock.json
+++ b/Client/package-lock.json
@@ -1413,9 +1413,9 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
- "version": "7.8.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
- "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -1544,9 +1544,9 @@
}
},
"node_modules/baseline-browser-mapping": {
- "version": "2.10.37",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz",
- "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==",
+ "version": "2.10.38",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz",
+ "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -1689,9 +1689,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.5.375",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.375.tgz",
- "integrity": "sha512-ZWP5eB4BVPW/ZYo9252hQZHZ5XavtsTgpbhcmMmRwymavC5AsLWQWBPaKMeNd2LW0KGby5HPXvj7+sr4ta5j/Q==",
+ "version": "1.5.376",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.376.tgz",
+ "integrity": "sha512-cUVA7/RvbFTEuw/i3obUwDTRIXojaxkResf+ibByPFxjc6XK3VNtcQXV0NSbAlJ0FMjcJGgftVVB4Qo184EXvA==",
"dev": true,
"license": "ISC"
},
@@ -2595,9 +2595,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
- "version": "3.3.12",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
- "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "version": "3.3.14",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.14.tgz",
+ "integrity": "sha512-U9kYi5bpVMEI31yC8iw4bJJp0avcHXA0W8/wNfLfnvJYzihQo2ZRPYPvpAAd570HAcCBjCTN7vnr+v4StKl1IQ==",
"dev": true,
"funding": [
{
@@ -2621,9 +2621,9 @@
"license": "MIT"
},
"node_modules/node-releases": {
- "version": "2.0.47",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz",
- "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==",
+ "version": "2.0.48",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz",
+ "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==",
"dev": true,
"license": "MIT",
"engines": {
diff --git a/Client/src/components/StartPage.tsx b/Client/src/components/StartPage.tsx
index 509f141..b56eb5a 100644
--- a/Client/src/components/StartPage.tsx
+++ b/Client/src/components/StartPage.tsx
@@ -4,7 +4,7 @@ import { fadeUp, stagger } from './motion'
import { useWeather } from '../hooks/useWeather'
import { Greeting } from './start/Greeting'
import { SearchBar } from './start/SearchBar'
-import { QuickLinks } from './start/QuickLinks'
+import { CategoryDock } from './start/CategoryDock'
import { WeatherFx } from './start/WeatherFx'
// 背景图:放 src/assets/startpage/ 下。按昼夜选 light / dark;
@@ -122,10 +122,13 @@ export function StartPage() {
-
-
-
+ {/* 占位:补回快捷入口腾出的高度,让问候/搜索框保持删除前的垂直位置与比例,
+ 不因居中重排而被撑到页面正中。底部入口改由 CategoryDock 承载。 */}
+
+
+ {/* 底部分类抽屉:按类型分组的快捷入口,悬停某类时菜单向上抽出 */}
+
)
}
diff --git a/Client/src/components/start/CategoryDock.tsx b/Client/src/components/start/CategoryDock.tsx
new file mode 100644
index 0000000..df9e180
--- /dev/null
+++ b/Client/src/components/start/CategoryDock.tsx
@@ -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 = (
+
+)
+
+const DeepSeekIcon = (
+
+)
+
+const BilibiliIcon = (
+
+)
+
+const ZhihuIcon = (
+
+)
+
+const XIcon = (
+
+)
+
+const YoutubeIcon = (
+
+)
+
+const SteamIcon = (
+
+)
+
+// 无可靠 simple-icons path 的站点:用字标,跟其余图标同色同风格。
+const Wordmark = (text: string) => (
+
+ {text}
+
+)
+
+/* ── 分类小图标:通用线性图形,非品牌,安全自绘(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 = (
+
+)
+
+const FunGlyph = (
+
+)
+
+const DevGlyph = (
+
+)
+
+const GameGlyph = (
+
+)
+
+// 分类顺序即从左到右的展示顺序。每类的 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(null)
+ const reduce = useReducedMotion()
+
+ // 焦点移出整个分类块(含抽屉内链接)才收起,方向键/Tab 浏览不会闪。
+ function handleBlur(id: string, e: FocusEvent) {
+ if (!e.currentTarget.contains(e.relatedTarget as Node | null)) {
+ setActive((cur) => (cur === id ? null : cur))
+ }
+ }
+
+ return (
+
+ )
+}
diff --git a/Client/src/components/start/QuickLinks.tsx b/Client/src/components/start/QuickLinks.tsx
deleted file mode 100644
index cbf980e..0000000
--- a/Client/src/components/start/QuickLinks.tsx
+++ /dev/null
@@ -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 = (
-
-)
-
-const DeepSeekIcon = (
-
-)
-
-const BilibiliIcon = (
-
-)
-
-const ZhihuIcon = (
-
-)
-
-// FMHY 不在 simple-icons 图标库里,用字标,跟其余图标同色同风格。
-const FmhyIcon = (
-
- FMHY
-
-)
-
-const RustIcon = (
-
-)
-
-const CppIcon = (
-
-)
-
-const CsharpIcon = (
-
-)
-
-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 个(左对齐)。
-
- {SHORTCUTS.map((item, i) => (
-
- {item.icon}
-
- ))}
-
- )
-}
diff --git a/docs/startpage-category-dock.md b/docs/startpage-category-dock.md
new file mode 100644
index 0000000..8dac0b4
--- /dev/null
+++ b/docs/startpage-category-dock.md
@@ -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` 在搜索框下方保留了一块透明占位 ``:
+补回原快捷入口宫格腾出的高度,让居中的问候 / 搜索框停在删除前的垂直位置与比例,
+不因居中重排被撑到页面正中。底部入口已交由 `CategoryDock` 承载。
+
+## 部署
+
+纯前端改动,按 `docs/startpage-weather-location.md`「只改前端」一节:
+WSL 里 `npm run build` → 清空服务器 `static/` → 上传 `dist/*`,后端进程不用动。
diff --git a/docs/startpage-quicklinks.md b/docs/startpage-quicklinks.md
deleted file mode 100644
index 67602c0..0000000
--- a/docs/startpage-quicklinks.md
+++ /dev/null
@@ -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 个,每个用 `` 新标签页打开:
-
- | 行 | 入口 | 链接 |
- |----|------|------|
- | 一 | 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` 用 `` 字标。
-2. 往 `SHORTCUTS` 加 `{ label, href, icon }`。
-3. 调整居中:第二行第一个入口要带 `col-start-N`。当前规则写死在 `i === 5`
- (第 6 个起是第二行);若每行个数变了,按新的「第二行起始下标 / 居中起始列」改这两处。
-
-## 部署
-
-纯前端改动,按 `docs/startpage-weather-location.md`「只改前端」一节:
-`npm run build` → 清空服务器 `static/` → 上传 `dist/*`,后端进程不用动。
diff --git a/docs/startpage-weather-location.md b/docs/startpage-weather-location.md
index bba8c0b..e075c30 100644
--- a/docs/startpage-weather-location.md
+++ b/docs/startpage-weather-location.md
@@ -103,7 +103,7 @@ PORT=8080 cargo run
## 部署:改了后端时(重新编译)
改了 `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/*`
3. 私钥与密钥建议写进服务器上的一个文件再统一加载,避免出现在命令历史里。
例如服务器放 `/opt/internet-project/qweather.pem` 和 `/opt/internet-project/weather.env`: