首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >OpenSpec 项目实战(四) | 布局改造:给长列表装上导航骨架

OpenSpec 项目实战(四) | 布局改造:给长列表装上导航骨架

原创
作者头像
运维有术
发布2026-05-18 23:03:11
发布2026-05-18 23:03:11
1590
举报
文章被收录于专栏:运维有术运维有术

OpenSpec 项目实战(四) | 布局改造:给长列表装上导航骨架

🚩 2026 年「术哥无界」系列实战文档 X 篇原创计划 第 114 篇,OpenSpec 项目实战「2026」系列第 4

大家好,欢迎来到 术哥无界 | ShugeX | 运维有术

我是术哥,一名专注于 AI 编程、AI 智能体、Agent Skills、MCP、云原生、AIOps、Milvus 向量数据库的技术实践者与开源布道者

Talk is cheap, let's explore。无界探索,有术而行。

封面图 - 布局改造前后对比
封面图 - 布局改造前后对比

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

工作流总览:5 步从探索到归档
工作流总览:5 步从探索到归档

说明:本文内容基于 OpenSpec(Fission-AI/OpenSpec)v1.3.1 和 React 19 + TypeScript + Tailwind CSS v4 的实际操作记录整理而成,所有命令和代码均在 shuge AI Toolbox 项目中实际验证。文中的配置模板和参数建议仅供参考,实际效果请以你的业务数据和环境测试结果为准。如果有实际使用经验,欢迎在评论区分享交流。

1. 长列表还能用,但不够好

第 3 期做完了 UI 重设计。CSS 变量体系从零搭起来了,77 行 tokens.css 定义了完整的品牌色、中性色、字体、间距体系。路由包裹 bug 也修了——点进工具页,导航栏不会消失了。

打开 npm run dev 看一眼:品牌色(蓝色系)生效,卡片样式统一,Beta 和 Planned 标签颜色一致。好看是好看。

但用着用着就发现一个问题:所有工具还是按分类平铺在一个长页面上。catalog.ts 里目前 5 个工具、4 个分类,还能接受。但后续每加一个工具,首页就多一张卡片。到 15 个工具、8 个分类的时候,用户只能靠滚动找工具。

没有分类导航,没有锚点跳转,没有筛选。页面像列表页,不像工具平台。

这期做布局改造。change name: layout-restructure,一句话需求:给首页加上分类导航机制,让用户快速定位到目标分类,不用在长列表里滚来滚去。

完整流程不变:

代码语言:markdown
复制
Explore  →  Propose  →  Apply  →  Verify  →  Archive
   ↓           ↓          ↓         ↓          ↓
  澄清       生成       按任务      验证       归档
  需求       5 工件      执行       检查      change

2. Explore:选哪种导航方案

上期是视觉变更,这期回到功能变更——不改样式,改布局结构。

盘点当前布局

先看首页代码的核心逻辑。第 3 期 apply 后的 src/app/views/Home.tsx,展示核心逻辑,卡片渲染和样式细节省略:

代码语言: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

代码语言: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 的数据部分:

代码语言:tsx
复制
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 变更。当前首页是按分类平铺的长列表,没有导航结构。需要加入分类导航机制,让用户能快速定位到目标分类。

第 1 轮:4 个方案 + 4 个探索方向

执行 /opsx:explore,输入描述:

本期做一个 layout-restructure 变更。当前首页是按分类平铺的长列表,没有导航结构。需要加入分类导航机制,让用户能快速定位到目标分类。

AI 先问了几个探索方向,然后给出了 4 个候选方案的 ASCII 图:

代码语言:markdown
复制
┌─────────────────────────────────────────────────────────┐
│  方案 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)。

单级分类,近期不会有二级。

第 2 轮:3 个实现细节

AI 追问了三个实现细节。

Tab 切换行为?

选项

行为

A

过滤显示 — 只渲染该分类的卡片,其他 DOM 移除。切换回「全部」恢复全部显示。

B

滚动定位 — 页面有全部内容,点击 Tab 滚动到对应分类标题位置。Tab 栏吸顶以方便切回。

方案 A 适合内容少、静态的场景,DOM 简单。方案 B 适合内容多、首次只加载首屏 lazy 的场景。

滚动状态? 切到「文本处理」→ 滚动到顶部,还是保持当前滚动位置?

Tab 栏位置? AI 画了一个结构图确认 Tab 在内容区中的位置:

代码语言:markdown
复制
┌──────────────────────────────────────────────────────┐
│                    TopNav (全局)                    │
├──────────────────────────────────────────────────────┤
│                                                      │
│  ┌─ Content Area ─────────────────────────────────┐  │
│  │  [全部] [文本处理] [数据转换] [开发工具] [内容创作] │  │
│  │  ─────────────────────────────────────────────  │  │
│  │                                                │  │
│  │  ┌─────┐  ┌─────┐  ┌─────┐                    │  │
│  │  │卡片1 │  │卡片2 │  │卡片3 │                    │  │
│  │  └─────┘  └─────┘  └─────┘                    │  │
│  │                                                │  │
│  │  ...                                           │  │
│  └────────────────────────────────────────────────┘  │
│                                                      │
└──────────────────────────────────────────────────────┘

我的回答:

Tab 切换行为:选 A(过滤显示)。点击「文本处理」只渲染该分类卡片,其他 DOM 移除。切回「全部」恢复全部显示。

滚动状态:切换 Tab 时回到顶部。因为切换后内容变了,保持旧滚动位置没意义。

Tab 栏位置:不固定,跟随内容滚动。Tab 栏就在内容区顶部,不需要 sticky。当前只有 5 个工具,一屏就能看完,不需要吸顶。

AI 给了总结确认:

代码语言:markdown
复制
┌──────────────────────────────────────────────────────┐
│  TopNav (全局导航)                                   │
├──────────────────────────────────────────────────────┤
│                                                      │
│  Content Area:                                       │
│  [全部] [文本处理] [数据转换] [开发工具] [内容创作]     │
│  ─────────────────────────────────────────────────   │
│  ┌─────┐  ┌─────┐  ┌─────┐                          │
│  │卡片1 │  │卡片2 │  │卡片3 │   (过滤显示)           │
│  └─────┘  └─────┘  └─────┘                          │
│                                                      │
│  切换 Tab → 回到顶部                                 │
│  Tab 栏跟随内容滚动,不吸顶                           │
│                                                      │
└──────────────────────────────────────────────────────┘

已确认:过滤逻辑 A 方案、切换时 scroll to top、「全部」= 全量其他分类 = 过滤。待细化:活跃 Tab 样式、切换动画、工具分布均匀性。

AI 问:要我现在创建 layout-restructure 变更并生成提案吗?

我回复:确认 创建。

3. Propose:5 个工件产出

Explore 最后一轮 AI 自动询问是否创建变更,确认后 5 个工件一次生成,不需要单独执行 propose 命令。

项目配置(openspec/config.yaml)指定了 schema: with-review,产出顺序固定:proposal → specs → design → review → tasks。

AI 完成后输出了工件清单:

工件

路径

提案

openspec/changes/layout-restructure/proposal.md

设计

openspec/changes/layout-restructure/design.md

规格

openspec/changes/layout-restructure/specs/category-navigation/spec.md

审查

openspec/changes/layout-restructure/review.md

任务

openspec/changes/layout-restructure/tasks.md

核心决策已记录:

  • Tab 导航方案:useState + 过滤显示
  • 滚动重置:smooth scroll to top
  • Tab 栏样式:下划线高亮,Flexbox 横向布局
  • 不吸顶,不引入 URL 参数

proposal.md

proposal 的核心内容是 Why 和 What Changes。

Why:首页当前为分类平铺的长列表,用户需要手动滚动查找目标分类,效率低。

What Changes

  • 首页内容区顶部添加分类 Tab 栏
  • Tab 栏包含「全部」Tab + 动态生成的各分类 Tab
  • 点击 Tab 过滤展示对应分类的工具卡片
  • 不新增路由,不改现有接口

说实话,proposal.md 里有些内容不完全准确——比如它引用了 src/pages/home-page/ 这个不存在的路径,还提到了 Zustand 做状态管理(实际用的是 useState)。这是 OpenSpec 生成工件时的已知局限:proposal 基于项目结构推断,不一定和实际代码完全对应。核心需求描述是对的,细节以 design.md 和实际代码为准。

specs.md

specs 用场景化的方式定义了 Tab 导航的行为要求。核心场景:

WHEN 用户打开首页 → 应显示「全部」Tab 处于激活状态,下方展示所有分类的工具卡片。

WHEN 用户点击某个分类 Tab → 该 Tab 切换为激活态(下划线高亮),下方只展示该分类下的工具卡片。其他分类的 section 不渲染。

WHEN 用户再次点击「全部」Tab → 恢复显示所有分类 section,和默认状态一致。

WHEN catalog 中新增分类 → Tab 栏自动出现新的分类 Tab,不需要手动添加。分类列表从 getTools() 动态提取。

design.md

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.md

review 的审查结果:

维度

关注点

结论

边界条件

空分类、数据异常、仅一个分类时

⚠️ 警告

回滚方案

组件内 state + 单文件变更,Git 回滚

✅ 通过

测试覆盖

Tab 渲染 + 点击切换 + 筛选结果

✅ 通过

向后兼容

不改 catalog 接口和路由

✅ 通过

任务粒度

tasks.md 未生成时的状态

⚠️ 待评估

边界条件的 ⚠️ 指出:缺少异常场景处理——空分类时 Tab 是否隐藏、catalog 数据异常时如何兜底、仅一个分类时是否还需要 Tab 栏。这些是有价值的审查发现,不过本期分类数量固定且数据来自静态 catalog.ts,风险可控,先不处理。

tasks.md

实际产出 4 个任务组,遵循 TDD 流程(先写失败测试,再写实现):

任务组 1:Tab 导航组件测试(TDD RED 阶段) — 先编写 Tab 交互测试用例,预期全部失败。

任务组 2:实现 Tab 导航 UI(TDD GREEN 阶段) — 编写实现代码,让测试从红变绿。

任务组 3:优化卡片列表渲染 — 提取 ToolCard 子组件复用卡片渲染逻辑。

任务组 4:验收测试 — 全量测试确认所有功能正常。

每个 task 包含完整的测试代码和实现代码,遵循项目 config.yaml 中的 TDD 规则。

4. Apply:改一个文件解决问题

执行:

代码语言:bash
复制
/opsx:apply

AI 自动执行了 12 个 task,完成后输出报告:

代码语言:markdown
复制
## 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`.

实际修改的文件

文件

操作

说明

src/app/views/Home.tsx

修改

添加分类 Tab 栏 + 筛选逻辑 + ToolCard 组件

src/app/views/Home.test.tsx

修改

添加 Tab 交互测试用例

只改了 2 个文件。TopNav.tsxLayout.tsxrouter/index.tsxcatalog.ts 都没动,符合 Explore 阶段的决策。

Home.tsx 的核心变更

改造前的逻辑是 sortedCategories.map() 直接渲染所有分类 section。改造后多了几层:

首先,提取了 ToolCard 子组件,把卡片渲染逻辑独立出来复用:

代码语言:tsx
复制
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

代码语言:tsx
复制
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 下划线高亮:

代码语言:tsx
复制
<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>

条件渲染分两种模式:「全部」时按分类分组展示,单分类时扁平网格:

代码语言:tsx
复制
{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 期做了设计系统,这期加导航就能只改一个文件。

Apply 完成后的人工抽查

吸取上期 NotFound.tsx 漏改的教训,这次 apply 完成后手动打开 Home.tsx 确认代码确实改了。上期的根因是 AI 标记了任务完成但跳过了实际写文件操作——这个坑踩过一次就够了。

首页改造后的 Tab 导航效果 - 全部 Tab 默认状态
首页改造后的 Tab 导航效果 - 全部 Tab 默认状态

图 2:首页内容区顶部分类 Tab 栏 — 「全部」+ 4 个分类动态生成,活跃 Tab 用下划线高亮

5. Verify:构建和浏览器双确认

/opsx:verify 三维度检查

执行:

代码语言:bash
复制
/opsx:verify

verify 的三维度检查结果:

代码语言:markdown
复制
## 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,还得手动确认关键文件确实被修改了。

构建验证

代码语言:bash
复制
npm run build

实际输出:

代码语言:markdown
复制
> 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 启动后逐项检查:

  • 首页加载:Tab 栏正常显示在标题区域下方,包含「全部」「内容创作」「开发工具」「数据转换」「文本处理」5 个 Tab
  • 默认状态:「全部」Tab 处于激活态(下划线高亮),下方按分类分组显示全部 5 张工具卡片
  • 点击「开发工具」:页面只显示「代码解释」1 张卡片(Beta 标签),其他分类消失
  • 点击「文本处理」:页面显示「文本摘要」和「Markdown 表格」两张卡片(Markdown 表格带 Planned 灰色标签)
  • 点击「全部」:恢复显示所有分类,回到默认状态
  • Console:无报错、无警告
Tab 切换筛选效果 - 开发工具
Tab 切换筛选效果 - 开发工具

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

切回「全部」恢复效果
切回「全部」恢复效果

图 4:切回「全部」— 恢复显示所有分类

6. Archive:归档

Verify 通过后执行:

代码语言:bash
复制
/opsx:archive

实际输出:

代码语言:markdown
复制
## 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 假成功的教训,执行后立刻检查:

代码语言:bash
复制
ls openspec/changes/

确认 layout-restructure 目录已经从 changes 移到了 archive 目录。上期的坑是 AI 输出了 Archive Complete 但没执行文件操作——这次亲眼看到文件确实移走了才放心。

归档目录:openspec/changes/archive/2026-05-16-layout-restructure/,包含 5 个工件的完整快照。

至此,layout-restructure 这个 change 完整走完了 5 个步骤。

7. Git 推送

OpenSpec 工作流本身不包含代码提交。但 apply 过程自动产生了 3 个 commit:

  • 132cb45 test: add Tab navigation tests to Home
  • de953b9 feat: add Tab navigation to Home page
  • 49f533a 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

8. 回顾:本期学到了什么

小变更也有完整流程的价值

这期改动量不大——核心只改了 Home.tsx 一个文件,加了 Tab 栏和筛选逻辑。但走完完整的 Explore → Propose → Apply → Verify → Archive 流程,还是有收获。

Explore 阶段讨论了四种导航方案:粘性 Tab、左侧栏、锚点跳转、分类标题吸顶。4 个分类用 Tab 刚好,侧边栏太重。但这个判断是有时间窗口的——如果后续分类增长到 10 个以上,Tab 挤成一排,可能要重新换成侧边栏。这个决策记录在 proposal.md 里,后续翻归档就能看到当初为什么选 Tab。

Propose 阶段 specs.md 用场景化的方式定义了 Tab 的行为——点击什么、显示什么、切回「全部」恢复什么。比「加个 Tab」这种模糊描述精确得多。如果不用 OpenSpec,这些行为可能只在脑子里有个大概,到写代码的时候再临时决定。

依赖已有基础设施

这期能只改一个文件,全靠前几期搭好的基础设施。catalog.tsgetTools() 和分类数据已经就绪,design tokens 的 CSS 变量体系可以直接复用,TopNav 和 Layout 不需要动。

说到底,OpenSpec 的结构化流程让每期聚焦一个变更范围。第 2 期做注册中心,第 3 期做设计系统,第 4 期做导航,每期不发散。如果一开始就随便加功能,现在可能要同时改 catalog、路由、布局、样式——那才是真的头疼。

Tab 导航的局限

当前的 Tab 方案在分类少(4-6 个)时体验不错。但有两个局限需要注意。

其一,Tab 不支持多级分类。如果未来工具多了需要「文本处理 > 格式转换」这种层级结构,Tab 搞不定,得换成树形侧边栏。

其二,Tab 状态不在 URL 里。用户筛选了「开发工具」后刷新页面,会回到「全部」。Explore 阶段就决定不做 URL 参数,但如果后续需要,加 useSearchParams 改动量不大。

你做工具平台的时候,Tab 和侧边栏会选哪个?欢迎在评论区聊聊。

AI 编程验证的持续性

上期踩了两个坑(NotFound.tsx 漏改、archive 假成功),这期吸取了教训:apply 后手动抽查关键文件,archive 后检查目录结构。习惯养成之后,验证成本不高,但能避免上期那种「AI 标了完成但实际没改」的问题。

这个教训可以提炼成一句话:artifact 记录的是预期做什么,不是已经做了什么。 上期 NotFound.tsx 的坑暴露了 verify 的盲区——它检查了 spec 覆盖和 design 遵循,但没有做 apply 前后的文件 diff。这期手动抽查就是在弥补这个盲区。

话说回来,这种验证习惯不限于 OpenSpec 工作流。任何 AI 编程工具都存在同样的风险——AI 说做了,不代表真的做了。抽查是低成本高回报的操作。

9. 下期预告

布局有了骨架,导航有了 Tab,首页不再是无限滚动的长列表。但页面视觉上还有提升空间——图标系统缺位、交互动效为零、配色细节还可以打磨。下一期做 UI 视觉优化,让页面从「功能齐全」变成「好用好看」。

好啦,谢谢你观看我的文章,如果喜欢可以点赞转发给需要的朋友,我们下一期再见!敬请期待!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • OpenSpec 项目实战(四) | 布局改造:给长列表装上导航骨架
    • 1. 长列表还能用,但不够好
    • 2. Explore:选哪种导航方案
      • 盘点当前布局
      • 四种导航方案
      • 第 1 轮:4 个方案 + 4 个探索方向
      • 第 2 轮:3 个实现细节
    • 3. Propose:5 个工件产出
      • proposal.md
      • specs.md
      • design.md
      • review.md
      • tasks.md
    • 4. Apply:改一个文件解决问题
      • 实际修改的文件
      • Home.tsx 的核心变更
      • 为什么要改这么少?
      • Apply 完成后的人工抽查
    • 5. Verify:构建和浏览器双确认
      • /opsx:verify 三维度检查
      • 构建验证
      • 浏览器确认
    • 6. Archive:归档
    • 7. Git 推送
    • 8. 回顾:本期学到了什么
      • 小变更也有完整流程的价值
      • 依赖已有基础设施
      • Tab 导航的局限
      • AI 编程验证的持续性
    • 9. 下期预告
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档