OpenSpec 項目實戰(四) | 佈局改造:給長列表裝上導航骨架

作者:術哥無界
日期:2026年5月17日 上午9:50
來源:WeChat 原文

整理版優先睇

速讀 5 個重點 高亮

Tab 導航取代長列表,一個檔案搞掂分類篩選

整理版摘要

呢篇文章係術哥分享佢喺 OpenSpec 項目入面,點樣將首頁嘅長列表改造成有 Tab 導航嘅佈局。佢係一個專注 AI 編程同開源嘅技術實踐者,今次嘅改造係因為原本按分類平鋪嘅頁面,隨住工具增加用戶要靠滾動揾嘢,唔夠方便。佢嘅結論係:用 useState 做過濾顯示嘅 Tab 導航方案,喺目前 4 個分類、5 個工具嘅規模下最簡單直接,而且只改咗 2 個檔案就完成。

文章記錄咗成個工作流程:由 Explore 階段比較四種方案(粘性 Tab、側邊導航、錨點跳轉、分類吸頂),揀咗 Tab 導航;到 Propose 階段自動產出提案、規格、設計同任務;Apply 階段實際改 Home.tsx,加咗 useState 控制 activeTab,用 Set 去重抽分類,仲提取咗 ToolCard 組件;最後 Verify 同 Archive 確認冇問題。

呢個案例好明顯展示咗前期基建嘅價值:因為之前做咗工具註冊中心同設計系統,今次只需要改一個核心檔案,TopNavLayout、catalog.ts 全部唔使鬱。作者特別提醒,apply 完成後一定要人手確認檔案有冇改到,避免 AI 跳過寫檔案嘅 bug。

  • Tab 導航方案最適合目前規模:4 個分類、5 個工具,用 useState + 過濾顯示,夠簡單直接
  • 對比咗四種導航方案(粘性 Tab、側邊導航、錨點跳轉、分類吸頂),最後揀咗 Tab 欄跟內容滾動、唔吸頂
  • 實作只改咗 2 個檔案Home.tsx 加咗 Tab 邏輯同 ToolCard 組件,Home.test.tsx 加測試
  • 前期做好工具註冊中心同設計系統,令後期改動可以集中喺單一檔案,體現「前期投入,後期收益
  • TDD 流程今次終於行得通:先寫測試(RED),再寫實作(GREEN),最後 30 個測試全部通過
整理重點

問題:長列表用落就覺唔夠好

上期做完 UI 重設計之後,頁面係好睇咗,但所有工具仲係按分類平鋪喺一個長頁面。當初 catalog.ts 得 5 個工具、4 個分類,仲可以接受;但諗深一層,之後每加一個工具,首頁就多一張卡片,用戶就要靠滾動揾嘢,冇分類導航、冇錨點跳轉、冇篩選,成個頁面似列表多過似工具平台。

所以今期做佈局改造,個需求好簡單:畀首頁加上分類導航機制,令用戶可以快速跳到目標分類,唔使喺長列表入面碌嚟碌去。作者用 OpenSpec 嘅工作流,由 Explore 開始,行咗完整嘅 5 步流程。

作者喺 Explore 階段用 /opsx:explore 命令,AI 自動畀咗 4 個候選方案嘅 ASCII

  1. 1 方案 A:粘性頂部 Tab 導航,直觀點擊即達,但分類多時會逼夾
  2. 2 方案 B:左側迷你導航(桌面)/ 抽屜式(流動),唔佔內容空間,但流動端交互成本高
  3. 3 方案 C:錨點式快速跳轉,侵入性最低,但需要內容區有清晰錨點
  4. 4 方案 D:分類標題吸頂 + 滾動進度,上下文感強,但失去全局概覽

作者揀咗方案 A,因為目前得 4 個分類,夠用;側邊欄太重,未來超過 8 個分類先再考慮換。佢仲決定要「全部Tab 做默認,Tab 欄放喺首頁內容區頂部,唔改 TopNav(嗰個係全局導航),唔需要獨立 URL。

整理重點

Propose:自動生成 5 個工件,再 Apply:改一個檔案解決問題

作者確認咗方案之後,AI 自動執行咗 Propose 階段,一次過產出咗 5 個工件:proposal.md、design.md、specs、review.md、tasks.md。proposal 講清楚 WhyWhat Changes,design 記錄咗三個核心決策:用 useState 管理 Tab 狀態、切換時 scroll to top、用 Flexbox 同下劃線高亮活躍 Tab。

Apply 階段,AI 自動執行咗 12 個 task,最終只改咗 2 個檔案:src/app/views/Home.tsxHome.test.tsxTopNav.tsx、Layout.tsx、router、catalog.ts 全部冇鬱,完全符合 Explore 階段嘅決策。

Home.tsx 核心變更:Tab 導航邏輯 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);

作者仲提取咗 ToolCard 子組件,將卡片渲染邏輯獨立出來複用。Tab 欄用 nav + button 渲染,活躍態用 border-b-2 下劃線高亮。條件渲染分兩種模式:「全部」時按分類分組展示,單分類時用扁平網格。

實際實現用 activeTab 而非 activeCategory,分類列表用 Set 去重而非從 grouped 對象取

Tab 樣式用下劃線 border-b-2 而非藍色填充背景,因為設計決策留到 apply 階段決定

整理重點

Verify 同 Archive:雙重確認,避開之前嘅坑

Apply 完成之後,作者行咗 /opsx:verify,三個維度都 pass:Completeness 話 12 個 task 全部 commit 曬,Correctness 話 7 個 spec scenario 全部覆蓋,Coherence 話 design decision 有跟足。不過作者提醒,verify 冇做 apply 前後嘅 diff 對比,所以除咗跑 verify,仲要手動 confirm 關鍵檔案真係改咗。

作者跟住行 npm run build,零 error 零 warning;再 npm run dev 開瀏覽器逐項 check:默認「全部Tab 激活,點「開發工具」淨係顯示「代碼解釋」一張卡,點「文本處理」顯示兩張卡,切返「全部」恢復曬,Console 冇報錯。

  • 作者強調:apply 完成後一定要人手確認檔案有冇改到,因為 AI 有時會跳過實際寫檔案
  • 今次只改 2 個檔案,原因係 catalog.ts 已經有 getTools() 同分類數據,design tokens 定義曬顏色變量,TopNavLayout 唔需要鬱
  • 作者話呢個就係「前期投入,後期收益」嘅體現

零 error 零 warning,npm run dev 逐項檢查 console 冇報錯

🚩 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,一句話需求:幫首頁加上分類導航機制,等用戶可以快速定位到目標分類,唔使喺長列表入面碌嚟碌去。

完整流程不變:

Explore  →  Propose  →  Apply  →  Verify  →  Archive
   ↓           ↓          ↓         ↓          ↓
  澄清       生成       按任務      驗證       歸檔
  需求       5 工件      執行       檢查      change

2. Explore:揀邊種導航方案

上期係視覺變更,今期返到功能變更——唔改樣式,改佈局結構。

盤點當前佈局

先睇首頁代碼嘅核心邏輯。第 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 變更。目前首頁係按分類平鋪嘅長列表,冇導航結構。需要加入分類導航機制,等用戶可以快速定位到目標分類。

第 1 輪:4 個方案 + 4 個探索方向

執行 /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 事)。

單級分類,短期內唔會有二級。

第 2 輪:3 個實現細節

AI 追問咗三個實現細節。

Tab 切換行為?

選項
行為
A
過濾顯示
 — 只渲染該分類嘅卡片,其他 DOM 移除。切返去「全部」就恢復全部顯示。
B
滾動定位
 — 頁面有全部內容,㩒 Tab 就碌到對應分類標題位置。Tab 欄吸頂方便切返。

方案 A 適合內容少、靜態嘅場景,DOM 簡單。方案 B 適合內容多、首次只加載首屏 lazy 嘅場景。

滾動狀態? 切到「文本處理」→ 碌返頂部,定係保持當前滾動位置?

Tab 欄位置? AI 畫咗一個結構圖確認 Tab 喺內容區入面嘅位置:

┌──────────────────────────────────────────────────────┐
│                    TopNav (全局)                    │
├──────────────────────────────────────────────────────┤
│                                                      │
│  ┌─ Content Area ─────────────────────────────────┐  │
│  │  [全部] [文本處理] [數據轉換] [開發工具] [內容創作] │  │
│  │  ─────────────────────────────────────────────  │  │
│  │                                                │  │
│  │  ┌─────┐  ┌─────┐  ┌─────┐                    │  │
│  │  │卡片1 │  │卡片2 │  │卡片3 │                    │  │
│  │  └─────┘  └─────┘  └─────┘                    │  │
│  │                                                │  │
│  │  ...                                           │  │
│  └────────────────────────────────────────────────┘  │
│                                                      │
└──────────────────────────────────────────────────────┘

我嘅回答:

  1. Tab 切換行為:揀 A(過濾顯示)。㩒「文本處理」淨係渲染該分類卡片,其他 DOM 移除。切返「全部」恢復全部顯示。

  2. 滾動狀態:切換 Tab 時返返頂部。因為切換後內容變咗,保持舊滾動位置冇意思。

  3. Tab 欄位置:唔固定,跟住內容滾動。Tab 欄就喺內容區頂部,唔需要 sticky。目前得 5 個工具,一屏就睇得曬,唔需要吸頂。

AI 畀咗總結確認:

┌──────────────────────────────────────────────────────┐
│  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:改一個文件解決問題

執行:

/opsx:apply

AI 自動執行咗 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`.

實際修改嘅文件

文件
操作
說明
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 子組件,將卡片渲染邏輯獨立出嚟複用:

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 過程中測試先 run 咗 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 標記咗任務完成但 skip 咗實際寫文件操作——呢個坑踩過一次就夠。

首頁改造後的 Tab 導航效果 - 全部 Tab 默認狀態
首頁改造之後嘅 Tab 導航效果 - 全部 Tab 默認狀態

圖 2:首頁內容區頂部分類 Tab 欄 — 「全部」+ 4 個分類動態生成,活躍 Tab 用底線高亮

5. Verify:構建同瀏覽器雙重確認

/opsx:verify 三維度檢查

執行:

/opsx:verify

verify 嘅三維度檢查結果:

## 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 對比。所以除咗 run 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 啓動之後逐項檢查:

  • 首頁加載:Tab 欄正常顯示喺標題區域下面,包含「全部」「內容創作」「開發工具」「數據轉換」「文本處理」5 個 Tab
  • 默認狀態:「全部」Tab 處於激活態(底線高亮),下面按分類分組顯示全部 5 張工具卡片
  • 㩒「開發工具」:頁面只顯示「代碼解釋」1 張卡片(Beta 標籤),其他分類消失
  • 㩒「文本處理」:頁面顯示「文本摘要」同「Markdown 表格」兩張卡片(Markdown 表格帶 Planned 灰色標籤)
  • 㩒「全部」:恢復顯示所有分類,返到默認狀態
  • Console:冇報錯、冇警告
Tab 切換篩選效果 - 開發工具
Tab 切換篩選效果 - 開發工具

圖 3:㩒「開發工具」Tab — 底線高亮,只顯示該分類下嘅工具卡片

切回「全部」恢復效果
切返「全部」恢復效果

圖 4:切返「全部」— 恢復顯示所有分類

6. Archive:歸檔

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 個步驟。

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.ts 的 getTools() 同分類數據已經準備好,design tokens 嘅 CSS 變量體系可以直接重用,TopNav 同 Layout 唔需要鬱。

講到尾,OpenSpec 嘅結構化流程令每期集中喺一個變更範圍。第 2 期做註冊中心,第 3 期做設計系統,第 4 期做導航,每期唔發散。如果一開始就隨便加功能,而家可能要同時改 catalog、路由、佈局、樣式——嗰啲先真係頭痛。

Tab 導航嘅限制

目前嘅 Tab 方案喺分類少(4-6 個)時體驗唔錯。但有兩個限制要注意。

第一,Tab 唔支援多級分類。如果將來工具多咗需要「文本處理 > 格式轉換」呢種層級結構,Tab 搞唔掂,要換成樹形側邊欄。

第二,Tab 狀態唔喺 URL 入面。用戶篩選咗「開發工具」之後 refresh 頁面,會返返「全部」。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 視覺優化,令頁面由「功能齊全」變成「好用靚仔」。

好喇,多謝你睇我嘅文章,如果鍾意可以點讚轉發俾需要嘅朋友,我哋下一期再見!敬請期待!


🚩 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,一句話需求:給首頁加上分類導航機制,讓用戶快速定位到目標分類,不用在長列表裏滾來滾去。

完整流程不變:

Explore  →  Propose  →  Apply  →  Verify  →  Archive
   ↓           ↓          ↓         ↓          ↓
  澄清       生成       按任務      驗證       歸檔
  需求       5 工件      執行       檢查      change

2. Explore:選哪種導航方案

上期是視覺變更,這期回到功能變更——不改樣式,改佈局結構。

盤點當前佈局

先看首頁代碼的核心邏輯。第 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 變更。當前首頁是按分類平鋪的長列表,沒有導航結構。需要加入分類導航機制,讓用戶能快速定位到目標分類。

第 1 輪:4 個方案 + 4 個探索方向

執行 /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)。

單級分類,近期不會有二級。

第 2 輪:3 個實現細節

AI 追問了三個實現細節。

Tab 切換行為?

選項
行為
A
過濾顯示
 — 只渲染該分類的卡片,其他 DOM 移除。切換回「全部」恢復全部顯示。
B
滾動定位
 — 頁面有全部內容,點擊 Tab 滾動到對應分類標題位置。Tab 欄吸頂以方便切回。

方案 A 適合內容少、靜態的場景,DOM 簡單。方案 B 適合內容多、首次只加載首屏 lazy 的場景。

滾動狀態? 切到「文本處理」→ 滾動到頂部,還是保持當前滾動位置?

Tab 欄位置? AI 畫了一個結構圖確認 Tab 在內容區中的位置:

┌──────────────────────────────────────────────────────┐
│                    TopNav (全局)                    │
├──────────────────────────────────────────────────────┤
│                                                      │
│  ┌─ Content Area ─────────────────────────────────┐  │
│  │  [全部] [文本處理] [數據轉換] [開發工具] [內容創作] │  │
│  │  ─────────────────────────────────────────────  │  │
│  │                                                │  │
│  │  ┌─────┐  ┌─────┐  ┌─────┐                    │  │
│  │  │卡片1 │  │卡片2 │  │卡片3 │                    │  │
│  │  └─────┘  └─────┘  └─────┘                    │  │
│  │                                                │  │
│  │  ...                                           │  │
│  └────────────────────────────────────────────────┘  │
│                                                      │
└──────────────────────────────────────────────────────┘

我的回答:

  1. Tab 切換行為:選 A(過濾顯示)。點擊「文本處理」只渲染該分類卡片,其他 DOM 移除。切回「全部」恢復全部顯示。

  2. 滾動狀態:切換 Tab 時回到頂部。因為切換後內容變了,保持舊滾動位置沒意義。

  3. Tab 欄位置:不固定,跟隨內容滾動。Tab 欄就在內容區頂部,不需要 sticky。當前只有 5 個工具,一屏就能看完,不需要吸頂。

AI 給了總結確認:

┌──────────────────────────────────────────────────────┐
│  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:改一個文件解決問題

執行:

/opsx:apply

AI 自動執行了 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`.

實際修改的文件

文件
操作
說明
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 子組件,把卡片渲染邏輯獨立出來複用:

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 期做了設計系統,這期加導航就能只改一個文件。

Apply 完成後的人工抽查

吸取上期 NotFound.tsx 漏改的教訓,這次 apply 完成後手動打開 Home.tsx 確認代碼確實改了。上期的根因是 AI 標記了任務完成但跳過了實際寫文件操作——這個坑踩過一次就夠了。

首頁改造後的 Tab 導航效果 - 全部 Tab 默認狀態
首頁改造後的 Tab 導航效果 - 全部 Tab 默認狀態

圖 2:首頁內容區頂部分類 Tab 欄 — 「全部」+ 4 個分類動態生成,活躍 Tab 用下劃線高亮

5. Verify:構建和瀏覽器雙確認

/opsx:verify 三維度檢查

執行:

/opsx:verify

verify 的三維度檢查結果:

## 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 啓動後逐項檢查:

  • 首頁加載:Tab 欄正常顯示在標題區域下方,包含「全部」「內容創作」「開發工具」「數據轉換」「文本處理」5 個 Tab
  • 默認狀態:「全部」Tab 處於激活態(下劃線高亮),下方按分類分組顯示全部 5 張工具卡片
  • 點擊「開發工具」:頁面只顯示「代碼解釋」1 張卡片(Beta 標籤),其他分類消失
  • 點擊「文本處理」:頁面顯示「文本摘要」和「Markdown 表格」兩張卡片(Markdown 表格帶 Planned 灰色標籤)
  • 點擊「全部」:恢復顯示所有分類,回到默認狀態
  • Console:無報錯、無警告
Tab 切換篩選效果 - 開發工具
Tab 切換篩選效果 - 開發工具

圖 3:點擊「開發工具」Tab — 下劃線高亮,只顯示該分類下的工具卡片

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

圖 4:切回「全部」— 恢復顯示所有分類

6. Archive:歸檔

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 個步驟。

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.ts 的 getTools() 和分類數據已經就緒,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 視覺優化,讓頁面從「功能齊全」變成「好用好看」。

好啦,謝謝你觀看我的文章,如果喜歡可以點贊轉發給需要的朋友,我們下一期再見!敬請期待!