diff --git a/ts/web/nextjs/polaris/docs/plans/2026-10-19-navigation-additions.md b/ts/web/nextjs/polaris/docs/plans/2026-10-19-navigation-additions.md
new file mode 100644
index 0000000..787828a
--- /dev/null
+++ b/ts/web/nextjs/polaris/docs/plans/2026-10-19-navigation-additions.md
@@ -0,0 +1,30 @@
+# 新增导航与游戏资料分类图标
+
+## 范围与决定
+
+- 用户已明确要求执行;从干净的 `main` 新建 `feat/polaris-navigation-additions` 分支,不提交代码。
+- 新增 OpenRouter(AI 工具)、菜鸟教程(学习资料)、Lucide(设计资源),保持原有条目不变。
+- 新条目沿用现有数据结构,填写简短描述、关键词和分类内排序,不置顶;图标沿用既有 favicon 与首字母兜底,不新增依赖或资源。
+- 仅将游戏资料分类的 `icon` 改为 `book-search`,前端映射增加 Lucide 的 `BookSearch`,保留学习资料的 `BookOpen`。
+- 不调整布局、样式或其他分类图标。
+
+## 执行顺序与责任
+
+1. 协调者创建分支并记录计划(已完成)。
+2. fixer 负责 `src/data/links.json`、`src/data/categories.json`、`src/features/navigation/components/category-icon.tsx` 及必要的局部回归测试;编辑前确认编码、缩进与换行,阅读适用的 Next.js 本地文档。
+3. fixer 运行数据与图标相关测试、类型检查、修改文件的 lint 和格式检查,报告证据;协调者复核最终差异和关键行为。
+
+## 验证路径与预算
+
+- 新增导航:断言三个 ID/URL 唯一、分类正确、字段满足现有契约,现有条目不变。
+- 分类图标:断言游戏资料为 `book-search` 且渲染实际 `BookSearch`,学习资料仍为 `book-open`;类型检查确认当前 lucide-react 导出可用。
+- 工程检查:优先运行现有测试套件(若成本较低则全部)、`pnpm typecheck`、修改文件的 ESLint 与 Prettier 检查、`git diff --check`。
+- 不以外部站点的 favicon 网络可达性作为本次完成门槛;其失败沿用已有兜底机制。
+- 无框架/API/布局变更,不重复执行完整生产构建或截图验证,除非检查暴露相关风险。
+
+## 状态
+
+- 实施完成,未提交代码。
+- fixer 验证:全量测试 17 个文件、117 个用例通过;类型检查、修改文件 ESLint 与 Prettier 检查通过。环境使用 `corepack pnpm` 执行项目命令。
+- 协调者复核:原有 51 条链接与 HEAD 完全一致,恰好新增 3 条;分类数据除游戏资料的 `icon` 外与 HEAD 完全一致;图标组件测试覆盖 `BookSearch`、`BookOpen` 与默认兜底;`git diff --check` 通过。
+- 外部 favicon 可达性未测试,不影响本次数据及图标映射变更结论。
diff --git a/ts/web/nextjs/polaris/src/data/categories.json b/ts/web/nextjs/polaris/src/data/categories.json
index dad0b6f..df875fb 100644
--- a/ts/web/nextjs/polaris/src/data/categories.json
+++ b/ts/web/nextjs/polaris/src/data/categories.json
@@ -45,7 +45,7 @@
"id": "game-resources",
"title": "游戏资料",
"description": "游戏攻略维基、资料速查与实用规划工具。",
- "icon": "book-open",
+ "icon": "book-search",
"sort": 65
},
{
diff --git a/ts/web/nextjs/polaris/src/data/data.test.ts b/ts/web/nextjs/polaris/src/data/data.test.ts
index e80cd6a..e9d1753 100644
--- a/ts/web/nextjs/polaris/src/data/data.test.ts
+++ b/ts/web/nextjs/polaris/src/data/data.test.ts
@@ -30,9 +30,9 @@ describe("Polaris 静态数据", () => {
}
})
- it("包含 51 条合法链接且分类引用均存在", () => {
+ it("包含 54 条合法链接且分类引用均存在", () => {
const categoryIds = new Set(categories.map(({ id }) => id))
- expect(links).toHaveLength(51)
+ expect(links).toHaveLength(54)
expect(hasUniqueValues(links.map(({ id }) => id))).toBe(true)
for (const link of links) {
@@ -225,8 +225,8 @@ describe("Polaris 静态数据", () => {
},
] as const
- it("九条新链接全部存在且总数为 51", () => {
- expect(links).toHaveLength(51)
+ it("九条新链接全部存在且总数为 54", () => {
+ expect(links).toHaveLength(54)
expect(
expectedNewLinks.every(({ id }) =>
links.some((link) => link.id === id),
@@ -248,4 +248,48 @@ describe("Polaris 静态数据", () => {
},
)
})
+
+ describe("新增三条导航链接回归", () => {
+ const expectedNewLinks = [
+ {
+ id: "openrouter",
+ title: "OpenRouter",
+ url: "https://openrouter.ai",
+ categoryId: "ai",
+ },
+ {
+ id: "runoob",
+ title: "菜鸟教程",
+ url: "https://www.runoob.com/",
+ categoryId: "learning",
+ },
+ {
+ id: "lucide",
+ title: "Lucide",
+ url: "https://lucide.dev/",
+ categoryId: "design",
+ },
+ ] as const
+
+ it("三条新链接字段与分类精确匹配且沿用 favicon 兜底", () => {
+ for (const expected of expectedNewLinks) {
+ const link = links.find(({ id }) => id === expected.id)
+
+ expect(link).toBeDefined()
+ expect(link).toMatchObject({
+ id: expected.id,
+ title: expected.title,
+ url: expected.url,
+ categoryId: expected.categoryId,
+ pinned: false,
+ })
+ expect(link?.icon).toBeUndefined()
+ }
+ })
+
+ it("三条新链接 ID 与 URL 唯一", () => {
+ expect(hasUniqueValues(expectedNewLinks.map(({ id }) => id))).toBe(true)
+ expect(hasUniqueValues(expectedNewLinks.map(({ url }) => url))).toBe(true)
+ })
+ })
})
diff --git a/ts/web/nextjs/polaris/src/data/links.json b/ts/web/nextjs/polaris/src/data/links.json
index 7af3375..bbef66f 100644
--- a/ts/web/nextjs/polaris/src/data/links.json
+++ b/ts/web/nextjs/polaris/src/data/links.json
@@ -534,5 +534,35 @@
"pinned": false,
"keywords": ["阿里云", "控制台", "云服务", "管理"],
"sort": 100
+ },
+ {
+ "id": "openrouter",
+ "title": "OpenRouter",
+ "description": "统一大模型 API 聚合平台",
+ "url": "https://openrouter.ai",
+ "categoryId": "ai",
+ "pinned": false,
+ "keywords": ["ai", "大模型", "api", "聚合"],
+ "sort": 30
+ },
+ {
+ "id": "runoob",
+ "title": "菜鸟教程",
+ "description": "编程入门教程与在线学习网站",
+ "url": "https://www.runoob.com/",
+ "categoryId": "learning",
+ "pinned": false,
+ "keywords": ["教程", "编程", "学习", "入门"],
+ "sort": 20
+ },
+ {
+ "id": "lucide",
+ "title": "Lucide",
+ "description": "开源 SVG 图标库",
+ "url": "https://lucide.dev/",
+ "categoryId": "design",
+ "pinned": false,
+ "keywords": ["图标", "svg", "图标库", "开源"],
+ "sort": 50
}
]
diff --git a/ts/web/nextjs/polaris/src/features/navigation/components/category-icon.test.tsx b/ts/web/nextjs/polaris/src/features/navigation/components/category-icon.test.tsx
new file mode 100644
index 0000000..e649e54
--- /dev/null
+++ b/ts/web/nextjs/polaris/src/features/navigation/components/category-icon.test.tsx
@@ -0,0 +1,29 @@
+import { render } from "@testing-library/react"
+import { describe, expect, it } from "vitest"
+
+import { CategoryIcon } from "./category-icon"
+
+describe("CategoryIcon", () => {
+ it("为 book-search 渲染实际的 BookSearch 图标", () => {
+ const { container } = render()
+ const svg = container.querySelector("svg")
+
+ expect(svg).not.toBeNull()
+ expect(svg).toHaveClass("lucide-book-search")
+ })
+
+ it("book-open 仍渲染 BookOpen 而非 BookSearch", () => {
+ const { container } = render()
+ const svg = container.querySelector("svg")
+
+ expect(svg).not.toBeNull()
+ expect(svg).toHaveClass("lucide-book-open")
+ expect(svg).not.toHaveClass("lucide-book-search")
+ })
+
+ it("未知图标回退到 Sparkles", () => {
+ const { container } = render()
+
+ expect(container.querySelector("svg")).toHaveClass("lucide-sparkles")
+ })
+})
diff --git a/ts/web/nextjs/polaris/src/features/navigation/components/category-icon.tsx b/ts/web/nextjs/polaris/src/features/navigation/components/category-icon.tsx
index d7db875..8ddfebc 100644
--- a/ts/web/nextjs/polaris/src/features/navigation/components/category-icon.tsx
+++ b/ts/web/nextjs/polaris/src/features/navigation/components/category-icon.tsx
@@ -1,5 +1,6 @@
import {
BookOpen,
+ BookSearch,
CirclePlay,
Code2,
Coffee,
@@ -14,6 +15,7 @@ const CATEGORY_ICONS: Readonly> = {
code: Code2,
palette: Palette,
"book-open": BookOpen,
+ "book-search": BookSearch,
zap: Zap,
"circle-play": CirclePlay,
coffee: Coffee,
diff --git a/ts/web/nextjs/polaris/src/services/services.test.ts b/ts/web/nextjs/polaris/src/services/services.test.ts
index 5ae266e..27985a2 100644
--- a/ts/web/nextjs/polaris/src/services/services.test.ts
+++ b/ts/web/nextjs/polaris/src/services/services.test.ts
@@ -57,6 +57,7 @@ describe("静态数据 service", () => {
"haowallpaper",
"iodraw",
"pixiv",
+ "lucide",
])
expect(getLinksByCategory("life").map(({ id }) => id)).toEqual(["amap"])
})