feat(polaris): 细分游戏资料并完善站点导航

新增游戏资料分类及九个网站链接,清理三条旧链接。
调整侧边栏提示交互,并为阿里云控制台使用本地图标。
同步更新分类、链接和交互测试。
This commit is contained in:
2026-10-12 00:08:34 +08:00
parent 95e2799721
commit 6e839019f5
9 changed files with 385 additions and 60 deletions
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1791734628751" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5223" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M1020.586667 361.813333c0-92.16-75.093333-167.253333-167.253334-167.253333h-266.24l23.893334 95.573333 228.693333 51.2c20.48 3.413333 37.546667 23.893333 37.546667 44.373334v245.76c0 20.48-17.066667 40.96-37.546667 44.373333l-228.693333 51.2-27.306667 92.16h266.24c92.16 0 167.253333-75.093333 167.253333-167.253333 3.413333 0 3.413333-290.133333 3.413334-290.133334zM187.733333 672.426667c-20.48-3.413333-37.546667-23.893333-37.546666-44.373334v-245.76c0-20.48 17.066667-40.96 37.546666-44.373333l228.693334-51.2 23.893333-95.573333H174.08C81.92 191.146667 6.826667 266.24 6.826667 358.4V648.533333c0 92.16 75.093333 167.253333 167.253333 167.253334h266.24l-23.893333-95.573334c0 3.413333-228.693333-47.786667-228.693334-47.786666z m215.04-211.626667h218.453334v88.746667h-218.453334v-88.746667z" fill="#ff6b01" p-id="5224"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -6,7 +6,7 @@ import { getLinksByCategory } from "@/services/links"
import CategoryPage, { dynamicParams, generateStaticParams } from "./page" import CategoryPage, { dynamicParams, generateStaticParams } from "./page"
describe("CategoryPage", () => { describe("CategoryPage", () => {
it("为全部 7 个分类生成静态参数并关闭运行时动态参数", () => { it("为全部 8 个分类生成静态参数并关闭运行时动态参数", () => {
expect(generateStaticParams()).toEqual([ expect(generateStaticParams()).toEqual([
{ categoryId: "ai" }, { categoryId: "ai" },
{ categoryId: "dev" }, { categoryId: "dev" },
@@ -14,11 +14,35 @@ describe("CategoryPage", () => {
{ categoryId: "learning" }, { categoryId: "learning" },
{ categoryId: "efficiency" }, { categoryId: "efficiency" },
{ categoryId: "media" }, { categoryId: "media" },
{ categoryId: "game-resources" },
{ categoryId: "life" }, { categoryId: "life" },
]) ])
expect(dynamicParams).toBe(false) expect(dynamicParams).toBe(false)
}) })
it("归类边界:游戏资料页收录宝可梦维基,娱乐媒体页不再包含", async () => {
const gamePage = await CategoryPage({
params: Promise.resolve({ categoryId: "game-resources" }),
})
render(gamePage)
expect(
screen.getByRole("heading", { level: 1, name: "游戏资料" }),
).toBeInTheDocument()
expect(
screen.getByRole("link", { name: /神奇宝贝百科/ }),
).toBeInTheDocument()
expect(
screen.getByRole("link", { name: /星露谷物语中文维基/ }),
).toBeInTheDocument()
expect(
screen.queryByRole("link", { name: /萌娘百科/ }),
).not.toBeInTheDocument()
expect(getLinksByCategory("media").map(({ id }) => id)).not.toContain(
"wiki-52poke",
)
})
it("通过 service 组合开发工具分类页", async () => { it("通过 service 组合开发工具分类页", async () => {
const links = getLinksByCategory("dev") const links = getLinksByCategory("dev")
const page = await CategoryPage({ const page = await CategoryPage({
@@ -41,6 +41,13 @@
"icon": "circle-play", "icon": "circle-play",
"sort": 60 "sort": 60
}, },
{
"id": "game-resources",
"title": "游戏资料",
"description": "游戏攻略维基、资料速查与实用规划工具。",
"icon": "book-open",
"sort": 65
},
{ {
"id": "life", "id": "life",
"title": "生活服务", "title": "生活服务",
+137 -4
View File
@@ -9,9 +9,12 @@ const hasUniqueValues = (values: string[]) =>
new Set(values).size === values.length new Set(values).size === values.length
describe("Polaris 静态数据", () => { describe("Polaris 静态数据", () => {
it("包含 7 个唯一且结构完整的分类", () => { it("包含 8 个唯一且结构完整的分类", () => {
expect(categories).toHaveLength(7) expect(categories).toHaveLength(8)
expect(hasUniqueValues(categories.map(({ id }) => id))).toBe(true) expect(hasUniqueValues(categories.map(({ id }) => id))).toBe(true)
expect(hasUniqueValues(categories.map(({ sort }) => String(sort)))).toBe(
true,
)
for (const category of categories) { for (const category of categories) {
expect(category).toEqual({ expect(category).toEqual({
@@ -27,9 +30,9 @@ describe("Polaris 静态数据", () => {
} }
}) })
it("包含 45 条合法链接且分类引用均存在", () => { it("包含 51 条合法链接且分类引用均存在", () => {
const categoryIds = new Set(categories.map(({ id }) => id)) const categoryIds = new Set(categories.map(({ id }) => id))
expect(links).toHaveLength(45) expect(links).toHaveLength(51)
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) {
@@ -115,4 +118,134 @@ describe("Polaris 静态数据", () => {
} }
}) })
}) })
describe("归类边界回归", () => {
const linkIdsByCategory = (categoryId: string) =>
links
.filter((link) => link.categoryId === categoryId)
.map(({ id }) => id)
.sort()
it("游戏维基与攻略工具归入 game-resources", () => {
expect(linkIdsByCategory("game-resources")).toEqual(
expect.arrayContaining([
"wiki-52poke",
"wiki-blhx",
"wiki-fgo",
"wiki-stardew",
"wiki-terraria",
]),
)
})
it("娱乐媒体保留影音社区与 ACG 百科,不混入游戏资料", () => {
const mediaIds = linkIdsByCategory("media")
expect(mediaIds).toEqual(
expect.arrayContaining([
"youtube",
"bilibili",
"spotify",
"nexusmods",
"pokemon-showdown",
"expreview",
"chairo",
"wiki-moegirl",
]),
)
for (const gameId of [
"wiki-52poke",
"wiki-blhx",
"wiki-fgo",
"wiki-stardew",
"wiki-terraria",
]) {
expect(mediaIds).not.toContain(gameId)
}
})
it("Nexus Mods 与 Pokemon Showdown 等实测工具仍留在娱乐媒体", () => {
const mediaIds = linkIdsByCategory("media")
expect(mediaIds).toContain("nexusmods")
expect(mediaIds).toContain("pokemon-showdown")
expect(linkIdsByCategory("game-resources")).not.toContain("nexusmods")
expect(linkIdsByCategory("game-resources")).not.toContain(
"pokemon-showdown",
)
})
})
describe("新增九条链接回归", () => {
const expectedNewLinks = [
{
id: "kzo",
url: "https://kzo.moe",
categoryId: "media",
},
{
id: "manhuagui",
url: "https://www.manhuagui.com",
categoryId: "media",
},
{
id: "linovelib",
url: "https://www.linovelib.com",
categoryId: "media",
},
{
id: "steam",
url: "https://store.steampowered.com",
categoryId: "media",
},
{
id: "tieba",
url: "https://tieba.baidu.com",
categoryId: "media",
},
{
id: "manga-bilibili",
url: "https://manga.bilibili.com",
categoryId: "media",
},
{
id: "zhihu",
url: "https://www.zhihu.com",
categoryId: "media",
},
{
id: "pixiv",
url: "https://www.pixiv.net",
categoryId: "design",
},
{
id: "aliyun-unified-console",
url: "https://home.console.aliyun.com",
categoryId: "efficiency",
},
] as const
it("九条新链接全部存在且总数为 51", () => {
expect(links).toHaveLength(51)
expect(
expectedNewLinks.every(({ id }) =>
links.some((link) => link.id === id),
),
).toBe(true)
})
it.each(expectedNewLinks)(
"$id 归入 $categoryId 且 URL 正确",
(expected) => {
const link = links.find(({ id }) => id === expected.id)
expect(link).toBeDefined()
expect(link).toMatchObject({
url: expected.url,
categoryId: expected.categoryId,
pinned: false,
})
},
)
})
}) })
+98 -36
View File
@@ -266,47 +266,27 @@
"title": "神奇宝贝百科", "title": "神奇宝贝百科",
"description": "宝可梦中文资料维基", "description": "宝可梦中文资料维基",
"url": "https://wiki.52poke.com/wiki/%E4%B8%BB%E9%A1%B5", "url": "https://wiki.52poke.com/wiki/%E4%B8%BB%E9%A1%B5",
"categoryId": "media", "categoryId": "game-resources",
"pinned": false, "pinned": false,
"keywords": ["宝可梦", "口袋妖怪", "攻略", "资料"], "keywords": ["宝可梦", "口袋妖怪", "攻略", "资料"],
"sort": 80 "sort": 80
}, },
{
"id": "wiki-52poke-typechart",
"title": "宝可梦属性相克表",
"description": "宝可梦全属性克制关系速查",
"url": "https://wiki.52poke.com/wiki/%E5%B1%9E%E6%80%A7%E7%9B%B8%E5%85%8B%E8%A1%A8",
"categoryId": "media",
"pinned": false,
"keywords": ["宝可梦", "属性", "相克", "速查"],
"sort": 90
},
{ {
"id": "wiki-blhx", "id": "wiki-blhx",
"title": "碧蓝航线WIKI", "title": "碧蓝航线WIKI",
"description": "碧蓝航线官方中文百科", "description": "碧蓝航线官方中文百科",
"url": "https://wiki.biligame.com/blhx/%E9%A6%96%E9%A1%B5", "url": "https://wiki.biligame.com/blhx/%E9%A6%96%E9%A1%B5",
"categoryId": "media", "categoryId": "game-resources",
"pinned": false, "pinned": false,
"keywords": ["碧蓝航线", "手游", "攻略", "wiki"], "keywords": ["碧蓝航线", "手游", "攻略", "wiki"],
"sort": 100 "sort": 100
}, },
{
"id": "wiki-blhx-boss",
"title": "碧蓝航线月度BOSS解析",
"description": "大型作战月度 BOSS 攻略解析",
"url": "https://wiki.biligame.com/blhx/%E5%A4%A7%E5%9E%8B%E4%BD%9C%E6%88%98%E6%9C%88%E5%BA%A6BOSS%E8%A7%A3%E6%9E%90",
"categoryId": "media",
"pinned": false,
"keywords": ["碧蓝航线", "boss", "攻略", "大型作战"],
"sort": 110
},
{ {
"id": "wiki-fgo", "id": "wiki-fgo",
"title": "Mooncell", "title": "Mooncell",
"description": "FGO 中文维基与攻略资料站", "description": "FGO 中文维基与攻略资料站",
"url": "https://fgo.wiki/w/%E9%A6%96%E9%A1%B5", "url": "https://fgo.wiki/w/%E9%A6%96%E9%A1%B5",
"categoryId": "media", "categoryId": "game-resources",
"pinned": false, "pinned": false,
"keywords": ["fgo", "mooncell", "从者", "攻略"], "keywords": ["fgo", "mooncell", "从者", "攻略"],
"sort": 120 "sort": 120
@@ -316,7 +296,7 @@
"title": "星露谷物语中文维基", "title": "星露谷物语中文维基",
"description": "星露谷物语官方中文资料维基", "description": "星露谷物语官方中文资料维基",
"url": "https://zh.stardewvalleywiki.com", "url": "https://zh.stardewvalleywiki.com",
"categoryId": "media", "categoryId": "game-resources",
"pinned": false, "pinned": false,
"keywords": ["星露谷物语", "攻略", "作物", "wiki"], "keywords": ["星露谷物语", "攻略", "作物", "wiki"],
"sort": 130 "sort": 130
@@ -326,7 +306,7 @@
"title": "泰拉瑞亚中文维基", "title": "泰拉瑞亚中文维基",
"description": "泰拉瑞亚官方中文百科", "description": "泰拉瑞亚官方中文百科",
"url": "https://terraria.wiki.gg/zh", "url": "https://terraria.wiki.gg/zh",
"categoryId": "media", "categoryId": "game-resources",
"pinned": false, "pinned": false,
"keywords": ["泰拉瑞亚", "攻略", "物品", "wiki"], "keywords": ["泰拉瑞亚", "攻略", "物品", "wiki"],
"sort": 140 "sort": 140
@@ -341,16 +321,6 @@
"keywords": ["动漫", "acg", "百科", "二次元"], "keywords": ["动漫", "acg", "百科", "二次元"],
"sort": 150 "sort": 150
}, },
{
"id": "farmgamehub",
"title": "星露谷洒水器规划器",
"description": "星露谷物语洒水器布局可视化工具",
"url": "https://www.farmgamehub.com/zh/tools/stardew-sprinklers",
"categoryId": "media",
"pinned": false,
"keywords": ["星露谷物语", "洒水器", "布局", "工具"],
"sort": 160
},
{ {
"id": "iodraw", "id": "iodraw",
"title": "iodraw", "title": "iodraw",
@@ -406,7 +376,8 @@
"id": "aliyun-console", "id": "aliyun-console",
"title": "阿里云轻量控制台", "title": "阿里云轻量控制台",
"description": "阿里云轻量应用服务器控制台", "description": "阿里云轻量应用服务器控制台",
"url": "https://swas.console.aliyun.com", "url": "https://console.aliyun.com",
"icon": "local:ali-cloud",
"categoryId": "efficiency", "categoryId": "efficiency",
"pinned": false, "pinned": false,
"keywords": ["阿里云", "控制台", "轻量服务器", "云服务"], "keywords": ["阿里云", "控制台", "轻量服务器", "云服务"],
@@ -472,5 +443,96 @@
"pinned": false, "pinned": false,
"keywords": ["地图", "导航", "出行", "路线"], "keywords": ["地图", "导航", "出行", "路线"],
"sort": 10 "sort": 10
},
{
"id": "kzo",
"title": "Kmoe 漫画",
"description": "Kindle/EPUB 漫画下载与推送",
"url": "https://kzo.moe",
"categoryId": "media",
"pinned": false,
"keywords": ["漫画", "kindle", "epub", "推送"],
"sort": 160
},
{
"id": "manhuagui",
"title": "漫画柜",
"description": "在线漫画阅读站点",
"url": "https://www.manhuagui.com",
"categoryId": "media",
"pinned": false,
"keywords": ["漫画", "在线阅读", "动漫"],
"sort": 170
},
{
"id": "linovelib",
"title": "轻小说文库",
"description": "在线轻小说阅读站点",
"url": "https://www.linovelib.com",
"categoryId": "media",
"pinned": false,
"keywords": ["轻小说", "在线阅读", "文库"],
"sort": 180
},
{
"id": "steam",
"title": "Steam",
"description": "PC 游戏数字发行商店",
"url": "https://store.steampowered.com",
"categoryId": "media",
"pinned": false,
"keywords": ["游戏", "商店", "平台"],
"sort": 190
},
{
"id": "tieba",
"title": "百度贴吧",
"description": "百度旗下主题兴趣社区",
"url": "https://tieba.baidu.com",
"categoryId": "media",
"pinned": false,
"keywords": ["贴吧", "论坛", "社区", "百度"],
"sort": 200
},
{
"id": "manga-bilibili",
"title": "哔哩哔哩漫画",
"description": "哔哩哔哩正版漫画平台",
"url": "https://manga.bilibili.com",
"categoryId": "media",
"pinned": false,
"keywords": ["漫画", "哔哩哔哩", "正版", "阅读"],
"sort": 210
},
{
"id": "zhihu",
"title": "知乎",
"description": "中文问答社区",
"url": "https://www.zhihu.com",
"categoryId": "media",
"pinned": false,
"keywords": ["问答", "社区", "知识"],
"sort": 220
},
{
"id": "pixiv",
"title": "pixiv",
"description": "插画作品分享社区",
"url": "https://www.pixiv.net",
"categoryId": "design",
"pinned": false,
"keywords": ["插画", "绘画", "灵感", "二次元"],
"sort": 40
},
{
"id": "aliyun-unified-console",
"title": "阿里云统一控制台",
"description": "阿里云产品统一管理控制台",
"url": "https://home.console.aliyun.com",
"icon": "local:ali-cloud",
"categoryId": "efficiency",
"pinned": false,
"keywords": ["阿里云", "控制台", "云服务", "管理"],
"sort": 100
} }
] ]
@@ -72,6 +72,7 @@ describe("CategoryContent", () => {
"learning", "learning",
"efficiency", "efficiency",
"media", "media",
"game-resources",
"life", "life",
] as const)("%s 分类展示服务提供的数量", (categoryId) => { ] as const)("%s 分类展示服务提供的数量", (categoryId) => {
const category = getCategoryById(categoryId) const category = getCategoryById(categoryId)
@@ -82,4 +83,24 @@ describe("CategoryContent", () => {
expect(screen.getByText(`共 ${links.length} 个`)).toBeInTheDocument() expect(screen.getByText(`共 ${links.length} 个`)).toBeInTheDocument()
}) })
it("归类边界:游戏资料面板只渲染 5 个游戏维基卡片", () => {
const category = getCategoryById("game-resources")
if (!category) throw new Error("测试数据缺少 game-resources 分类")
const links = getLinksByCategory(category.id)
render(<CategoryContent category={category} links={links} />)
expect(links).toHaveLength(5)
const toolLinks = screen
.getByRole("list", { name: "工具列表" })
.querySelectorAll("a")
expect(toolLinks).toHaveLength(5)
expect(
screen.queryByRole("link", { name: /萌娘百科/ }),
).not.toBeInTheDocument()
expect(
screen.queryByRole("link", { name: /Nexus Mods/ }),
).not.toBeInTheDocument()
})
}) })
@@ -1,4 +1,5 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react" import { fireEvent, render, screen, waitFor } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import type { Category } from "@/types/navigation" import type { Category } from "@/types/navigation"
@@ -9,9 +10,18 @@ const categories: Category[] = [
{ id: "dev", title: "开发工具", description: "开发", icon: "code", sort: 10 }, { id: "dev", title: "开发工具", description: "开发", icon: "code", sort: 10 },
] ]
class ResizeObserverStub {
observe() {}
unobserve() {}
disconnect() {}
}
describe("AppShell", () => { describe("AppShell", () => {
beforeEach(() => window.localStorage.clear()) beforeEach(() => window.localStorage.clear())
afterEach(() => vi.restoreAllMocks()) afterEach(() => {
vi.restoreAllMocks()
vi.unstubAllGlobals()
})
it("切换侧栏并持久化状态", () => { it("切换侧栏并持久化状态", () => {
render( render(
@@ -198,4 +208,67 @@ describe("AppShell", () => {
"page", "page",
) )
}) })
it("展开时分类文字可见,悬停或聚焦不显示提示", async () => {
const user = userEvent.setup()
render(
<AppShell
categories={categories}
siteName="Polaris"
defaultCollapsed={false}
>
<main>首页内容</main>
</AppShell>,
)
const link = screen.getByRole("link", { name: "开发工具" })
expect(link.querySelector(".sidebar__label")).not.toHaveClass("sr-only")
await user.hover(link)
expect(link).not.toHaveAttribute("data-state")
expect(screen.queryByRole("tooltip")).not.toBeInTheDocument()
link.focus()
expect(screen.queryByRole("tooltip")).not.toBeInTheDocument()
})
it("收起时悬停分类图标显示分类名提示", async () => {
vi.stubGlobal("ResizeObserver", ResizeObserverStub)
const user = userEvent.setup()
render(
<AppShell
categories={categories}
siteName="Polaris"
defaultCollapsed={true}
>
<main>首页内容</main>
</AppShell>,
)
const link = screen.getByRole("link", { name: "开发工具" })
expect(link.querySelector(".sidebar__label")).toHaveClass("sr-only")
await user.hover(link)
expect(await screen.findByRole("tooltip")).toHaveTextContent("开发工具")
})
it("收起时键盘聚焦分类图标显示分类名提示", async () => {
vi.stubGlobal("ResizeObserver", ResizeObserverStub)
const user = userEvent.setup()
render(
<AppShell
categories={categories}
siteName="Polaris"
defaultCollapsed={true}
>
<main>首页内容</main>
</AppShell>,
)
await user.tab()
await user.tab()
await user.tab()
await user.tab()
const link = screen.getByRole("link", { name: "开发工具" })
expect(link).toHaveFocus()
expect(await screen.findByRole("tooltip")).toHaveTextContent("开发工具")
})
}) })
@@ -81,11 +81,9 @@ export function Sidebar({
<div className="sidebar__categories"> <div className="sidebar__categories">
{categories.map((category) => { {categories.map((category) => {
const isActive = category.id === activeCategoryId const isActive = category.id === activeCategoryId
const categoryLink = (
return (
<Tooltip key={category.id}>
<TooltipTrigger asChild>
<Link <Link
key={category.id}
className={cn( className={cn(
"sidebar__nav-item", "sidebar__nav-item",
isActive && "sidebar__nav-item--active", isActive && "sidebar__nav-item--active",
@@ -94,15 +92,19 @@ export function Sidebar({
aria-current={isActive ? "page" : undefined} aria-current={isActive ? "page" : undefined}
> >
<CategoryIcon name={category.icon} /> <CategoryIcon name={category.icon} />
<span <span className={cn("sidebar__label", collapsed && "sr-only")}>
className={cn("sidebar__label", collapsed && "sr-only")}
>
{category.title} {category.title}
</span> </span>
</Link> </Link>
</TooltipTrigger> )
return collapsed ? (
<Tooltip key={category.id}>
<TooltipTrigger asChild>{categoryLink}</TooltipTrigger>
<TooltipContent side="right">{category.title}</TooltipContent> <TooltipContent side="right">{category.title}</TooltipContent>
</Tooltip> </Tooltip>
) : (
categoryLink
) )
})} })}
</div> </div>
@@ -16,6 +16,7 @@ describe("静态数据 service", () => {
"learning", "learning",
"efficiency", "efficiency",
"media", "media",
"game-resources",
"life", "life",
]) ])
expect(first).not.toBe(second) expect(first).not.toBe(second)
@@ -55,6 +56,7 @@ describe("静态数据 service", () => {
"iconfont", "iconfont",
"haowallpaper", "haowallpaper",
"iodraw", "iodraw",
"pixiv",
]) ])
expect(getLinksByCategory("life").map(({ id }) => id)).toEqual(["amap"]) expect(getLinksByCategory("life").map(({ id }) => id)).toEqual(["amap"])
}) })