feat(polaris): 新增三个导航网站并更新游戏资料图标
Polaris CI / verify (push) Successful in 3m40s
Polaris CI / publish (push) Successful in 5m7s

This commit is contained in:
2026-10-19 11:35:53 +08:00
parent 306107af83
commit b72b14a818
7 changed files with 141 additions and 5 deletions
@@ -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 可达性未测试,不影响本次数据及图标映射变更结论。
@@ -45,7 +45,7 @@
"id": "game-resources", "id": "game-resources",
"title": "游戏资料", "title": "游戏资料",
"description": "游戏攻略维基、资料速查与实用规划工具。", "description": "游戏攻略维基、资料速查与实用规划工具。",
"icon": "book-open", "icon": "book-search",
"sort": 65 "sort": 65
}, },
{ {
+48 -4
View File
@@ -30,9 +30,9 @@ describe("Polaris 静态数据", () => {
} }
}) })
it("包含 51 条合法链接且分类引用均存在", () => { it("包含 54 条合法链接且分类引用均存在", () => {
const categoryIds = new Set(categories.map(({ id }) => id)) const categoryIds = new Set(categories.map(({ id }) => id))
expect(links).toHaveLength(51) expect(links).toHaveLength(54)
expect(hasUniqueValues(links.map(({ id }) => id))).toBe(true) expect(hasUniqueValues(links.map(({ id }) => id))).toBe(true)
for (const link of links) { for (const link of links) {
@@ -225,8 +225,8 @@ describe("Polaris 静态数据", () => {
}, },
] as const ] as const
it("九条新链接全部存在且总数为 51", () => { it("九条新链接全部存在且总数为 54", () => {
expect(links).toHaveLength(51) expect(links).toHaveLength(54)
expect( expect(
expectedNewLinks.every(({ id }) => expectedNewLinks.every(({ id }) =>
links.some((link) => link.id === 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)
})
})
}) })
+30
View File
@@ -534,5 +534,35 @@
"pinned": false, "pinned": false,
"keywords": ["阿里云", "控制台", "云服务", "管理"], "keywords": ["阿里云", "控制台", "云服务", "管理"],
"sort": 100 "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
} }
] ]
@@ -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(<CategoryIcon name="book-search" />)
const svg = container.querySelector("svg")
expect(svg).not.toBeNull()
expect(svg).toHaveClass("lucide-book-search")
})
it("book-open 仍渲染 BookOpen 而非 BookSearch", () => {
const { container } = render(<CategoryIcon name="book-open" />)
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(<CategoryIcon name="not-exist" />)
expect(container.querySelector("svg")).toHaveClass("lucide-sparkles")
})
})
@@ -1,5 +1,6 @@
import { import {
BookOpen, BookOpen,
BookSearch,
CirclePlay, CirclePlay,
Code2, Code2,
Coffee, Coffee,
@@ -14,6 +15,7 @@ const CATEGORY_ICONS: Readonly<Record<string, LucideIcon>> = {
code: Code2, code: Code2,
palette: Palette, palette: Palette,
"book-open": BookOpen, "book-open": BookOpen,
"book-search": BookSearch,
zap: Zap, zap: Zap,
"circle-play": CirclePlay, "circle-play": CirclePlay,
coffee: Coffee, coffee: Coffee,
@@ -57,6 +57,7 @@ describe("静态数据 service", () => {
"haowallpaper", "haowallpaper",
"iodraw", "iodraw",
"pixiv", "pixiv",
"lucide",
]) ])
expect(getLinksByCategory("life").map(({ id }) => id)).toEqual(["amap"]) expect(getLinksByCategory("life").map(({ id }) => id)).toEqual(["amap"])
}) })