
🚩 2026 年「术哥无界」系列实战文档 X 篇原创计划 第 114 篇,OpenSpec 项目实战「2026」系列第 4 篇
大家好,欢迎来到 术哥无界 | ShugeX | 运维有术。
我是术哥,一名专注于 AI 编程、AI 智能体、Agent Skills、MCP、云原生、AIOps、Milvus 向量数据库的技术实践者与开源布道者!
Talk is cheap, let's explore。无界探索,有术而行。

图 1:从单页长列表到有导航结构的页面 - 本期要做的事

说明:本文内容基于 OpenSpec(Fission-AI/OpenSpec)v1.3.1 和 React 19 + TypeScript + Tailwind CSS v4 的实际操作记录整理而成,所有命令和代码均在 shuge AI Toolbox 项目中实际验证。文中的配置模板和参数建议仅供参考,实际效果请以你的业务数据和环境测试结果为准。如果有实际使用经验,欢迎在评论区分享交流。
第 3 期做完了 UI 重设计。CSS 变量体系从零搭起来了,77 行 tokens.css 定义了完整的品牌色、中性色、字体、间距体系。路由包裹 bug 也修了——点进工具页,导航栏不会消失了。
打开 npm run dev 看一眼:品牌色(蓝色系)生效,卡片样式统一,Beta 和 Planned 标签颜色一致。好看是好看。
但用着用着就发现一个问题:所有工具还是按分类平铺在一个长页面上。catalog.ts 里目前 5 个工具、4 个分类,还能接受。但后续每加一个工具,首页就多一张卡片。到 15 个工具、8 个分类的时候,用户只能靠滚动找工具。
没有分类导航,没有锚点跳转,没有筛选。页面像列表页,不像工具平台。
这期做布局改造。change name: layout-restructure,一句话需求:给首页加上分类导航机制,让用户快速定位到目标分类,不用在长列表里滚来滚去。
完整流程不变:
Explore → Propose → Apply → Verify → Archive
↓ ↓ ↓ ↓ ↓
澄清 生成 按任务 验证 归档
需求 5 工件 执行 检查 change上期是视觉变更,这期回到功能变更——不改样式,改布局结构。
先看首页代码的核心逻辑。第 3 期 apply 后的 src/app/views/Home.tsx,展示核心逻辑,卡片渲染和样式细节省略:
export default function Home() {
const tools = getTools();
const grouped = tools.reduce<Record<string, typeof tools>>((acc, tool) => {
if (!acc[tool.category]) {
acc[tool.category] = [];
}
acc[tool.category].push(tool);
return acc;
}, {});
const sortedCategories = Object.keys(grouped).sort();
return (
<div className="space-y-12 px-6 py-8">
<div className="text-center py-12">
<h1 className="text-3xl font-bold mb-3"
style={{ color: 'var(--color-neutral-900)' }}>
shuge AI Toolbox
</h1>
<p style={{ color: 'var(--color-neutral-500)' }}>
探索 AI 工具,提升工作效率
</p>
</div>
{sortedCategories.map((category) => {
const categoryTools = grouped[category].sort((a, b) => {
const priorityDiff = stagePriority[a.stage] - stagePriority[b.stage];
if (priorityDiff !== 0) return priorityDiff;
return a.name.localeCompare(b.name);
});
return (
<section key={category}>
<h2 className="text-xl font-semibold mb-4"
style={{ color: 'var(--color-neutral-800)' }}>
{category}
</h2>
{/* 卡片网格 — 每张卡片显示工具名称、状态标签、描述 */}
</section>
);
})}
</div>
);
}sortedCategories 按字母排序,map 逐个渲染 section。每个 section 是分类标题 + 工具卡片网格。逻辑清晰,数据流也简单。但页面结构是线性的——从上到下一串 section,中间没有导航机制。
TopNav 呢?逐字引用 src/layout/TopNav.tsx:
<div className="flex gap-4">
<Link
to="/"
className={`px-3 py-1 rounded-md transition-colors ${
isHome ? 'font-bold' : 'hover:opacity-80'
}`}
style={{
color: isHome ? 'var(--color-primary-500)' : 'var(--color-neutral-600)',
}}
>
首页
</Link>
</div>导航栏只有一个「首页」链接。而 catalog.ts 里明明有 getToolsByCategory() 函数和完整的分类数据,TopNav 完全没用上。
逐字引用 src/tool-registry/catalog.ts 的数据部分:
const tools: ToolManifest[] = [
{
id: 'text-summary',
name: '文本摘要',
route: '/tools/text-summary',
category: '文本处理',
description: '快速提取长文本的核心观点',
stage: 'active',
},
{
id: 'json-formatter',
name: 'JSON 格式化',
route: '/tools/json-formatter',
category: '数据转换',
description: '美化 JSON 数据结构',
stage: 'active',
},
{
id: 'code-explainer',
name: '代码解释',
route: '/tools/code-explainer',
category: '开发工具',
description: '解释代码片段的功能和逻辑',
stage: 'beta',
},
{
id: 'image-generator',
name: '图片生成',
route: '/tools/image-generator',
category: '内容创作',
description: '根据描述生成图片',
stage: 'planned',
},
{
id: 'markdown-table',
name: 'Markdown 表格',
route: '/tools/markdown-table',
category: '文本处理',
description: '将数据转换为 Markdown 表格',
stage: 'planned',
},
];4 个分类:文本处理、数据转换、开发工具、内容创作。5 个工具。后续会持续增长。
执行 /opsx:explore,输入描述:
本期做一个 layout-restructure 变更。当前首页是按分类平铺的长列表,没有导航结构。需要加入分类导航机制,让用户能快速定位到目标分类。
执行 /opsx:explore,输入描述:
本期做一个 layout-restructure 变更。当前首页是按分类平铺的长列表,没有导航结构。需要加入分类导航机制,让用户能快速定位到目标分类。
AI 先问了几个探索方向,然后给出了 4 个候选方案的 ASCII 图:
┌─────────────────────────────────────────────────────────┐
│ 方案 A: 粘性顶部 Tab 导航 │
│ ┌──────────────────────────────────────────────────┐ │
│ │ [全部] [分类A] [分类B] [分类C] [分类D] ... │ │
│ └──────────────────────────────────────────────────┘ │
│ 内容区域(滚动时导航栏固定) │
│ │
│ 优点:直观,点击即达 │
│ 缺点:分类多时拥挤,侵占垂直空间 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 方案 B: 左侧迷你导航(桌面端)/ 抽屉式(移动端) │
│ │
│ ┌────┐ ┌──────────────────────────────────────────┐ │
│ │ A │ │ │ │
│ │ B │ │ 内容区 │ │
│ │ C │ │ │ │
│ │ D │ │ │ │
│ └────┘ └──────────────────────────────────────────┘ │
│ │
│ 优点:不占内容区空间 │
│ 缺点:移动端交互成本高 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 方案 C: 锚点式快速跳转 │
│ │
│ 页面内滚动时出现浮动导航: │
│ │
│ ● [🔝]│
│ ● [🔝]│
│ │
│ 点击跳转,滚动时淡入淡出 │
│ │
│ 优点:侵入性最小 │
│ 缺点:需要内容区有清晰锚点 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 方案 D: 分类标题吸顶 + 滚动进度 │
│ │
│ 滚动时当前分类标题固定在顶部: │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 当前分类:████████ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ 优点:上下文感强,不占额外空间 │
│ 缺点:失去全局概览 │
└─────────────────────────────────────────────────────────┘我回答了项目背景:
当前 4 个分类(文本处理、数据转换、开发工具、内容创作)、5 个工具,单级分类,卡片网格布局,数据静态(catalog.ts)。用户主要找特定分类。
选方案 A(Tab 导航)。4 个分类刚好够用,侧边栏太重。未来超 8 个再考虑换。
需要「全部」Tab 作为默认状态,和当前效果一致。
Tab 放在首页内容区顶部,不改 TopNav(那是全局导航)。
不需要独立 URL(分类筛选是临时操作,不涉及 SEO)。
单级分类,近期不会有二级。
AI 追问了三个实现细节。
Tab 切换行为?
选项 | 行为 |
|---|---|
A | 过滤显示 — 只渲染该分类的卡片,其他 DOM 移除。切换回「全部」恢复全部显示。 |
B | 滚动定位 — 页面有全部内容,点击 Tab 滚动到对应分类标题位置。Tab 栏吸顶以方便切回。 |
方案 A 适合内容少、静态的场景,DOM 简单。方案 B 适合内容多、首次只加载首屏 lazy 的场景。
滚动状态? 切到「文本处理」→ 滚动到顶部,还是保持当前滚动位置?
Tab 栏位置? AI 画了一个结构图确认 Tab 在内容区中的位置:
┌──────────────────────────────────────────────────────┐
│ TopNav (全局) │
├──────────────────────────────────────────────────────┤
│ │
│ ┌─ Content Area ─────────────────────────────────┐ │
│ │ [全部] [文本处理] [数据转换] [开发工具] [内容创作] │ │
│ │ ───────────────────────────────────────────── │ │
│ │ │ │
│ │ ┌─────┐ ┌─────┐ ┌─────┐ │ │
│ │ │卡片1 │ │卡片2 │ │卡片3 │ │ │
│ │ └─────┘ └─────┘ └─────┘ │ │
│ │ │ │
│ │ ... │ │
│ └────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────┘我的回答:
Tab 切换行为:选 A(过滤显示)。点击「文本处理」只渲染该分类卡片,其他 DOM 移除。切回「全部」恢复全部显示。
滚动状态:切换 Tab 时回到顶部。因为切换后内容变了,保持旧滚动位置没意义。
Tab 栏位置:不固定,跟随内容滚动。Tab 栏就在内容区顶部,不需要 sticky。当前只有 5 个工具,一屏就能看完,不需要吸顶。
AI 给了总结确认:
┌──────────────────────────────────────────────────────┐
│ TopNav (全局导航) │
├──────────────────────────────────────────────────────┤
│ │
│ Content Area: │
│ [全部] [文本处理] [数据转换] [开发工具] [内容创作] │
│ ───────────────────────────────────────────────── │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │卡片1 │ │卡片2 │ │卡片3 │ (过滤显示) │
│ └─────┘ └─────┘ └─────┘ │
│ │
│ 切换 Tab → 回到顶部 │
│ Tab 栏跟随内容滚动,不吸顶 │
│ │
└──────────────────────────────────────────────────────┘已确认:过滤逻辑 A 方案、切换时 scroll to top、「全部」= 全量其他分类 = 过滤。待细化:活跃 Tab 样式、切换动画、工具分布均匀性。
AI 问:要我现在创建 layout-restructure 变更并生成提案吗?
我回复:确认 创建。
Explore 最后一轮 AI 自动询问是否创建变更,确认后 5 个工件一次生成,不需要单独执行 propose 命令。
项目配置(openspec/config.yaml)指定了 schema: with-review,产出顺序固定:proposal → specs → design → review → tasks。
AI 完成后输出了工件清单:
工件 | 路径 |
|---|---|
提案 |
|
设计 |
|
规格 |
|
审查 |
|
任务 |
|
核心决策已记录:
useState + 过滤显示proposal 的核心内容是 Why 和 What Changes。
Why:首页当前为分类平铺的长列表,用户需要手动滚动查找目标分类,效率低。
What Changes:
说实话,proposal.md 里有些内容不完全准确——比如它引用了 src/pages/home-page/ 这个不存在的路径,还提到了 Zustand 做状态管理(实际用的是 useState)。这是 OpenSpec 生成工件时的已知局限:proposal 基于项目结构推断,不一定和实际代码完全对应。核心需求描述是对的,细节以 design.md 和实际代码为准。
specs 用场景化的方式定义了 Tab 导航的行为要求。核心场景:
WHEN 用户打开首页 → 应显示「全部」Tab 处于激活状态,下方展示所有分类的工具卡片。
WHEN 用户点击某个分类 Tab → 该 Tab 切换为激活态(下划线高亮),下方只展示该分类下的工具卡片。其他分类的 section 不渲染。
WHEN 用户再次点击「全部」Tab → 恢复显示所有分类 section,和默认状态一致。
WHEN catalog 中新增分类 → Tab 栏自动出现新的分类 Tab,不需要手动添加。分类列表从 getTools() 动态提取。
design.md 有三个设计决策。
决策 1:Tab 状态管理 — 用 useState 管理当前激活的分类。不用 URL 参数(useSearchParams),理由是分类筛选是临时操作,用户不会分享「筛选到开发工具」的链接。URL 保持干净。
决策 2:滚动重置 — Tab 切换时执行 window.scrollTo({ top: 0, behavior: 'smooth' })。因为切换后内容完全不同,保持旧滚动位置没有意义。
决策 3:Tab 栏样式 — Flexbox 横向布局,活跃 Tab 使用下划线或背景色高亮。具体实现留到 apply 阶段决定。
design.md 中的 Non-Goals 部分明确了本期不做的事:不改 TopNav 和 Layout 结构、不改 catalog.ts 接口、不新增路由、不做侧边栏。
review 的审查结果:
维度 | 关注点 | 结论 |
|---|---|---|
边界条件 | 空分类、数据异常、仅一个分类时 | ⚠️ 警告 |
回滚方案 | 组件内 state + 单文件变更,Git 回滚 | ✅ 通过 |
测试覆盖 | Tab 渲染 + 点击切换 + 筛选结果 | ✅ 通过 |
向后兼容 | 不改 catalog 接口和路由 | ✅ 通过 |
任务粒度 | tasks.md 未生成时的状态 | ⚠️ 待评估 |
边界条件的 ⚠️ 指出:缺少异常场景处理——空分类时 Tab 是否隐藏、catalog 数据异常时如何兜底、仅一个分类时是否还需要 Tab 栏。这些是有价值的审查发现,不过本期分类数量固定且数据来自静态 catalog.ts,风险可控,先不处理。
实际产出 4 个任务组,遵循 TDD 流程(先写失败测试,再写实现):
任务组 1:Tab 导航组件测试(TDD RED 阶段) — 先编写 Tab 交互测试用例,预期全部失败。
任务组 2:实现 Tab 导航 UI(TDD GREEN 阶段) — 编写实现代码,让测试从红变绿。
任务组 3:优化卡片列表渲染 — 提取 ToolCard 子组件复用卡片渲染逻辑。
任务组 4:验收测试 — 全量测试确认所有功能正常。
每个 task 包含完整的测试代码和实现代码,遵循项目 config.yaml 中的 TDD 规则。
执行:
/opsx:applyAI 自动执行了 12 个 task,完成后输出报告:
## Implementation Complete
**Change:** layout-restructure
**Schema:** with-review
**Progress:** 12/12 tasks complete ✓
### Commits This Session
- `132cb45` test: add Tab navigation tests to Home
- `de953b9` feat: add Tab navigation to Home page
- `49f533a` feat: complete Tab navigation for Home page
### What Was Built
- Tab 导航栏位于内容区顶部(全部 + 4 个分类 Tab)
- 点击分类 Tab 过滤显示该分类工具卡片
- 「全部」Tab 显示全部分类分组
- Tab 切换时 smooth scroll 到顶部
- 活跃 Tab 使用 `border-b-2 border-accent-500` 高亮
- 提取 `ToolCard` 组件复用
### Test Results
Test Files 6 passed (6)
Tests 30 passed (30)
All tasks complete! You can archive this change with `/opsx:archive`.文件 | 操作 | 说明 |
|---|---|---|
| 修改 | 添加分类 Tab 栏 + 筛选逻辑 + ToolCard 组件 |
| 修改 | 添加 Tab 交互测试用例 |
只改了 2 个文件。TopNav.tsx、Layout.tsx、router/index.tsx、catalog.ts 都没动,符合 Explore 阶段的决策。
改造前的逻辑是 sortedCategories.map() 直接渲染所有分类 section。改造后多了几层:
首先,提取了 ToolCard 子组件,把卡片渲染逻辑独立出来复用:
function ToolCard({ tool }: { tool: ReturnType<typeof getTools>[number] }) {
return (
<Link
to={`/tools/${tool.id}`}
className={`block p-5 rounded-xl border transition-all ${
tool.stage === 'planned' ? 'opacity-60' : 'hover:shadow-md'
}`}
style={{
backgroundColor: 'var(--color-neutral-50)',
borderColor: 'var(--color-neutral-200)',
}}
>
<div className="flex items-start justify-between mb-2">
<h3 className="font-medium" style={{ color: 'var(--color-neutral-900)' }}>
{tool.name}
</h3>
{tool.stage === 'planned' && (
<span className="text-xs px-2 py-0.5 rounded"
style={{ backgroundColor: 'var(--color-neutral-200)', color: 'var(--color-neutral-600)' }}>
Planned
</span>
)}
{tool.stage === 'beta' && (
<span className="text-xs px-2 py-0.5 rounded"
style={{ backgroundColor: 'var(--color-accent-100)', color: 'var(--color-accent-600)' }}>
Beta
</span>
)}
</div>
<p className="text-sm" style={{ color: 'var(--color-neutral-500)' }}>
{tool.description}
</p>
</Link>
);
}然后在 Home 组件中,用 Set 去重获取分类列表,useState 管理 activeTab:
const tools = getTools();
const categories = [...new Set(tools.map((t) => t.category))].sort();
const [activeTab, setActiveTab] = useState<string>('全部');
const handleTabClick = (tab: string) => {
setActiveTab(tab);
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const filteredTools =
activeTab === '全部' ? tools : tools.filter((t) => t.category === activeTab);Tab 栏用 nav + button 渲染,活跃态用 border-b-2 下划线高亮:
<nav className="flex gap-2 border-b border-neutral-200">
<button onClick={() => handleTabClick('全部')}
className={`px-4 py-2 text-sm font-medium transition-colors ${
activeTab === '全部' ? 'border-b-2 border-accent-500 text-accent-600' : 'text-neutral-500 hover:text-neutral-900'
}`}>全部</button>
{categories.map((cat) => (
<button key={cat} onClick={() => handleTabClick(cat)}
className={`px-4 py-2 text-sm font-medium transition-colors ${
activeTab === cat ? 'border-b-2 border-accent-500 text-accent-600' : 'text-neutral-500 hover:text-neutral-900'
}`}>{cat}</button>
))}
</nav>条件渲染分两种模式:「全部」时按分类分组展示,单分类时扁平网格:
{activeTab === '全部' ? (
categories.map((category) => {
const categoryTools = filteredTools.filter((t) => t.category === category).sort((a, b) => {
const priorityDiff = stagePriority[a.stage] - stagePriority[b.stage];
if (priorityDiff !== 0) return priorityDiff;
return a.name.localeCompare(b.name);
});
return (
<section key={category}>
<h2 className="text-xl font-semibold mb-4" style={{ color: 'var(--color-neutral-800)' }}>{category}</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{categoryTools.map((tool) => (<ToolCard key={tool.id} tool={tool} />))}
</div>
</section>
);
})
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredTools.sort((a, b) => {
const priorityDiff = stagePriority[a.stage] - stagePriority[b.stage];
if (priorityDiff !== 0) return priorityDiff;
return a.name.localeCompare(b.name);
}).map((tool) => (<ToolCard key={tool.id} tool={tool} />))}
</div>
)}和草稿预期的差异挺大的。实际实现用的是 activeTab 而非 activeCategory,分类列表用 Set 去重而非从 grouped 对象取,Tab 样式用下划线 border-b-2 而非蓝色填充背景,还提取了 ToolCard 子组件。
值得一提的是 TDD 流程这期终于走对了。apply 过程中测试先跑了 6 个 FAIL(RED 阶段),实现代码写入后变成 10 PASS(GREEN 阶段),最后全量验收 30 PASS。前 3 期的 TDD 流程形同虚设,这期首次出现了真实的 RED-GREEN 循环。
上期改了 8 个文件(新建 tokens.css + 7 个修改),这期只改了 2 个。不是偷懒,是前几期的基础设施到位了。
catalog.ts 已经有 getTools() 和分类数据,不需要新增接口。design tokens 已经定义了所有需要的颜色变量,不需要新增 CSS。TopNav 和 Layout 不需要动——分类导航是内容层的交互,不涉及全局布局。路由也不需要动——不新增页面,只是同一个页面内的筛选。
这大概就是「前期投入,后期收益」的体现。第 2 期做了工具注册中心,第 3 期做了设计系统,这期加导航就能只改一个文件。
吸取上期 NotFound.tsx 漏改的教训,这次 apply 完成后手动打开 Home.tsx 确认代码确实改了。上期的根因是 AI 标记了任务完成但跳过了实际写文件操作——这个坑踩过一次就够了。

图 2:首页内容区顶部分类 Tab 栏 — 「全部」+ 4 个分类动态生成,活跃 Tab 用下划线高亮
/opsx:verify 三维度检查执行:
/opsx:verifyverify 的三维度检查结果:
## Verification Report: layout-restructure
### Summary
| Dimension | Status |
|--------------|------------------------------|
| Completeness | ✅ All 12 tasks committed |
| Correctness | ✅ All 7 spec scenarios covered |
| Coherence | ✅ Design decisions followed |verify 检查了 spec 覆盖和 design 遵循,引用了具体代码行号(比如 useState<string>('全部') 在 Home.tsx:56,tools.filter(...) 在 Home.tsx:64),映射了 7 个 spec scenario 到具体实现位置。但它没有做 apply 前后的文件 diff 对比。所以除了跑 verify,还得手动确认关键文件确实被修改了。
npm run build实际输出:
> shuge-ai-toolbox@0.0.0 build
> tsc -b && vite build
vite v8.0.12 building client environment for production...
✓ 31 modules transformed.
rendering chunks...
computing gzip size...
dist/index.html 0.46 kB │ gzip: 0.30 kB
dist/assets/index-HNIv7IuC.css 17.34 kB │ gzip: 4.22 kB
dist/assets/index-D0MO6XrT.js 290.38 kB │ gzip: 92.34 kB
✓ built in 105ms零 error 零 warning。
npm run dev 启动后逐项检查:

图 3:点击「开发工具」Tab — 下划线高亮,只显示该分类下的工具卡片

图 4:切回「全部」— 恢复显示所有分类
Verify 通过后执行:
/opsx:archive实际输出:
## Archive Complete
**Change:** layout-restructure
**Schema:** with-review
**Archived to:** `openspec/changes/archive/2026-05-16-layout-restructure/`
**Specs:** No delta specs (new capability, no existing main spec to sync)
All artifacts complete. All tasks complete.吸取上期 archive 假成功的教训,执行后立刻检查:
ls openspec/changes/确认 layout-restructure 目录已经从 changes 移到了 archive 目录。上期的坑是 AI 输出了 Archive Complete 但没执行文件操作——这次亲眼看到文件确实移走了才放心。
归档目录:openspec/changes/archive/2026-05-16-layout-restructure/,包含 5 个工件的完整快照。
至此,layout-restructure 这个 change 完整走完了 5 个步骤。
OpenSpec 工作流本身不包含代码提交。但 apply 过程自动产生了 3 个 commit:
132cb45 test: add Tab navigation tests to Homede953b9 feat: add Tab navigation to Home page49f533a feat: complete Tab navigation for Home page这 3 个 commit 是 apply 执行过程中按 TDD 阶段自动提交的(test → feat → feat complete),不是手动 git commit 的。
给 AI 编程助手指令推送到 GitHub:
把这次的改动推送到 GitHub。
AI 执行了 git push,推送到:https://github.com/shuge-x/shuge-ai-toolbox
这期改动量不大——核心只改了 Home.tsx 一个文件,加了 Tab 栏和筛选逻辑。但走完完整的 Explore → Propose → Apply → Verify → Archive 流程,还是有收获。
Explore 阶段讨论了四种导航方案:粘性 Tab、左侧栏、锚点跳转、分类标题吸顶。4 个分类用 Tab 刚好,侧边栏太重。但这个判断是有时间窗口的——如果后续分类增长到 10 个以上,Tab 挤成一排,可能要重新换成侧边栏。这个决策记录在 proposal.md 里,后续翻归档就能看到当初为什么选 Tab。
Propose 阶段 specs.md 用场景化的方式定义了 Tab 的行为——点击什么、显示什么、切回「全部」恢复什么。比「加个 Tab」这种模糊描述精确得多。如果不用 OpenSpec,这些行为可能只在脑子里有个大概,到写代码的时候再临时决定。
这期能只改一个文件,全靠前几期搭好的基础设施。catalog.ts 的 getTools() 和分类数据已经就绪,design tokens 的 CSS 变量体系可以直接复用,TopNav 和 Layout 不需要动。
说到底,OpenSpec 的结构化流程让每期聚焦一个变更范围。第 2 期做注册中心,第 3 期做设计系统,第 4 期做导航,每期不发散。如果一开始就随便加功能,现在可能要同时改 catalog、路由、布局、样式——那才是真的头疼。
当前的 Tab 方案在分类少(4-6 个)时体验不错。但有两个局限需要注意。
其一,Tab 不支持多级分类。如果未来工具多了需要「文本处理 > 格式转换」这种层级结构,Tab 搞不定,得换成树形侧边栏。
其二,Tab 状态不在 URL 里。用户筛选了「开发工具」后刷新页面,会回到「全部」。Explore 阶段就决定不做 URL 参数,但如果后续需要,加 useSearchParams 改动量不大。
你做工具平台的时候,Tab 和侧边栏会选哪个?欢迎在评论区聊聊。
上期踩了两个坑(NotFound.tsx 漏改、archive 假成功),这期吸取了教训:apply 后手动抽查关键文件,archive 后检查目录结构。习惯养成之后,验证成本不高,但能避免上期那种「AI 标了完成但实际没改」的问题。
这个教训可以提炼成一句话:artifact 记录的是预期做什么,不是已经做了什么。 上期 NotFound.tsx 的坑暴露了 verify 的盲区——它检查了 spec 覆盖和 design 遵循,但没有做 apply 前后的文件 diff。这期手动抽查就是在弥补这个盲区。
话说回来,这种验证习惯不限于 OpenSpec 工作流。任何 AI 编程工具都存在同样的风险——AI 说做了,不代表真的做了。抽查是低成本高回报的操作。
布局有了骨架,导航有了 Tab,首页不再是无限滚动的长列表。但页面视觉上还有提升空间——图标系统缺位、交互动效为零、配色细节还可以打磨。下一期做 UI 视觉优化,让页面从「功能齐全」变成「好用好看」。
好啦,谢谢你观看我的文章,如果喜欢可以点赞转发给需要的朋友,我们下一期再见!敬请期待!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。