Design Pattern: Tree View
Visual basis: vortex-1.0 — verify behavior / copy / structure, not pixel style.
Why we're building this
Multiple platform features — Site Management, Floor Plan, Device lists, Custom Views, Access Control — render hierarchical data as a tree, but each built its own expand/collapse and selection behaviour, so the experience is inconsistent. The immediate trigger: Site Management moved from three levels to five (Site > Area × 4), and the existing fixed-depth tree implementations cannot support it.
This pattern defines one reusable, depth-agnostic tree view: node behaviour is driven by node kind, not depth; selection and expansion are independent; "expand all" stops at a configurable kind. It ships first in the Site Management sidebar, then replaces the Floor Plan tree, and is available to other modules (device picker, views) later.
節點結構與狀態
一個通用樹狀列表元件,顯示巢狀階層資料。元件本身不認得「Site / Area / Camera」,只認得「節點」與「種類(kind)」——icon、空狀態文案、業務語意都由使用端透過資料與設定傳入。
下圖為 Tree View 的預設狀態(以 Sites 為例):
節點類型 Node types
| 類型 | 外觀 |
|---|---|
| 可展開(無 icon) | chevron(16×16)+ label |
| 可展開(有 icon) | chevron(16×16)+ icon(20×20)+ label |
| 葉節點(無 icon) | 無箭頭,label 左側 12px padding 對齊 |
| 葉節點(有 icon) | 無箭頭,icon(20×20)+ label,左側 12px padding |
| 空提示行 Empty Hint Row | Regular 400 文字,無 icon/chevron,disabled 不可點;文字由使用端依 kind 傳入(如「No cameras」) |
節點狀態 Node states
| 狀態 | 外觀(VORTEX 1.0 token) |
|---|---|
| 預設 | 文字 @color-text01、icon @color-icon03、透明背景 |
| Hover | 文字 @color-text01、背景 @color-surface02(內層容器圓角 8px) |
| 選取 Active | 文字 @color-text01、背景 @color-surface04(內層容器圓角 8px) |
| 空容器 Empty Container | 文字 @color-text20 / icon @color-icon29(變淡);可展開、可選取;hover / selected 時恢復一般色 |
| 停用 Disabled | 文字 @color-text06 / icon @color-icon01,不可點擊 |
輔助線 Indent guides
| 狀態 | 外觀 |
|---|---|
| 預設 | 不顯示 |
| Hover | 該節點所屬最頂層群組底下所有展開層級出現微弱垂直線(@color-outline19) |
| Active | 選取節點展開後,直接子層出現較明顯垂直線(@color-outline04);收合時父層出現 |
| Active + Hover | 較明顯的線優先 |
尺寸 Dimensions
| 項目 | 值 |
|---|---|
| Node height | 40px |
| Node horizontal padding | 8px |
| Node internal gap | 4px |
| Chevron | 16×16px |
| Node icon | 20×20px |
| Indent per level | 16px |
| Leaf 左側 padding | 12px |
| 選取內層容器圓角 | 8px |
| Level-1 群組間距 | 8px |
| Container padding | 8px |
| 輔助線寬度 | 1px |
字體:節點文字 Segoe UI 16px Semibold(600) line-height 1;空提示行 Segoe UI 16px Regular(400) line-height 1(不用 italic)。
邊界 Boundaries
| 情境 | 行為 |
|---|---|
| 沒有任何資料 | 不顯示樹,由使用端處理 empty state |
| 展開的節點沒有子項目 | 顯示空提示行(Empty Hint Row),文字由使用端依 kind 提供 |
Token gap:
@color-icon29(空容器 icon 變淡)尚未加入theme-color/index.less,以 OpenSpecadd-tree-view-component為準。
TODO / 未解問題
-
@color-icon29token 尚未落地,需與前端確認補上。
選取
規則 Behaviour
- 點擊節點 label(或 icon)= 選取;一次只能選一個(single-select)。
- 為了讓被選節點露出,選取只會**往上展開它的祖先(上層)**中被收合的節點;節點自己與往下的子節點展開狀態一律不動(詳見下方「不會做的事」)。
- 若節點不在捲動可見區域,平滑捲動到可見位置。
- 選取 emit 事件通知使用端(
select/update:selectedId)。 - 點擊 chevron 只切換展開/收合、不影響選取(見 Section 03)。
下面兩張圖分別為「選取態」與「選取 + hover 同時」(hover 為微暗背景、選取為微亮背景):
選取「不會」做的事 釐清邊界
選取只「往上」展開祖先讓節點可見,除此之外的展開/收合狀態都不碰:
- 不改變被選取節點本身的展開/收合狀態(原本收合就維持收合)。
- 不展開被選取節點的子節點。
- 不收合其他已展開的節點。
驗收條件 QA · Given-When-Then
- Given 點擊某節點 label,Then 該節點成為唯一選取(前一個取消)。
- Given 選取一個位於收合祖先下的節點,Then 自動展開其所有祖先使其可見。
- Given 被選取節點不在可見區,Then 平滑捲動到可見位置。
- Given 點擊 chevron,Then 只切換展開/收合、選取不變。
元件與 Token 決策
- 選取態:文字
@color-text01、背景@color-surface04(內層容器圓角 8px)。 - Emits:
select {nodeId, node}、update:selectedId(支援 v-model)。
展開與收合
點擊分離 Click separation
[chevron] [icon] [label] [counter]
↑ 展開/收合 ↑ 選取
- chevron 區:只切換展開/收合,不影響選取。
- label / icon / counter 區:只選取,不影響展開/收合。
- 整列 hover:整列背景變化。
全部展開/全部收合
頂部節點計數列(「N Sites」)右側的單顆按鈕,即為「全部展開/全部收合」切換:
| 控制 | 行為 |
|---|---|
| 展開按鈕 icon | 全收合時顯示 chevrons-down,tooltip「Expand All」 |
| 收合按鈕 icon | 有展開節點時顯示 chevrons-up,tooltip「Collapse All」 |
| Expand ceiling | 「全部展開」停在 expandCeilingKind 指定的種類(如展開到 Floorplan,Camera 維持收合) |
| 初始展開 | 由 defaultExpandToKind 決定初次載入展開到哪一種類 |
驗收條件 QA · Given-When-Then
- Given 點擊 chevron,Then 切換該節點展開/收合、選取不變。
- Given 點「全部展開」,Then 展開到
expandCeilingKind指定的種類為止。 - Given 已有任一節點展開,Then 全部按鈕顯示為 Collapse All(
chevrons-up)。
元件與 Token 決策
- Props:
expandedIds(v-model)、defaultExpandToKind、expandCeilingKind。 - Emits:
toggle {nodeId, expanded}、update:expandedIds。 - Methods:
expandAll()(受 ceiling 限制)、collapseAll()。
搜尋
搜尋過濾、highlight、No Matches Found 由使用端處理,不是元件本身的責任。此處定義互動規則,供使用端實作出一致行為。
搜尋流程 Flow
- 使用者輸入 → 首次輸入時快照當前展開狀態。
- 即時過濾(不分大小寫)。
- 自動展開命中節點的所有祖先。
- 命中文字以 highlight 顯示;無關分支隱藏。
過濾規則 Filter rules
| 節點 | 行為 |
|---|---|
| 命中 | 名稱含關鍵字(不分大小寫),正常可選 |
| 命中的祖先 | 顯示且可選(正常樣式、不灰化),提供階層 context |
| 命中的子節點 | 預設不展開;父層也命中才展開 |
| 完全無關分支 | 隱藏 |
清除搜尋 Clear
| 操作 | 行為 |
|---|---|
| 點清除(X) | 清除搜尋文字 |
| 展開狀態 | 恢復搜尋前的快照 |
| 選取狀態 | 搜尋前有選取則恢復;搜尋中有新選取則保留新選取 |
| highlight | 全部移除 |
狀態與邊界
| 情境 | 行為 |
|---|---|
| 無命中 No matches | 於 tree 區顯示「No matches found」訊息,隱藏所有節點 |
| disabled 節點被命中 | 顯示但維持 disabled、不可選 |
| 選取節點仍在結果中 | 維持該節點的選取態 |
| 選取節點被過濾掉 | 選取在邏輯上保留、但不可見;清除搜尋後恢復顯示 |
| 搜尋中選了新節點 | 清除後保留新選取 |
| 搜尋中外部觸發 reveal | 先清除搜尋、恢復展開,再 reveal(見 Section 05) |
驗收條件 QA · Given-When-Then
- Given 輸入關鍵字,Then 只顯示命中節點及其祖先,其餘隱藏。
- Given 命中節點,Then 自動展開其祖先,且命中文字 highlight。
- Given 清除搜尋,Then 展開狀態恢復成搜尋前的快照。
- Given disabled 節點被命中,Then 顯示但維持不可選。
- Given 搜尋無任何命中,Then 顯示「No matches found」並隱藏所有節點。
- Given 選取的節點被搜尋過濾掉,Then 選取邏輯上保留,清除搜尋後恢復可見。
外部觸發選取
外部事件(例如點擊地圖上的 camera marker)可觸發 tree 選取特定節點。
流程 Flow
- 外部呼叫
revealAndSelect(nodeId)。 - 若在搜尋中,先清除搜尋、恢復展開狀態。
- 找到目標節點 → 展開所有祖先 → 選取 → 平滑捲動到可見區。
狀態與邊界
| 情境 | 行為 |
|---|---|
| 目標已可見且已選取 | 不做任何事 |
| 目標已可見但未選取 | 選取,必要時捲動 |
| 目標不存在 | 靜默失敗,不做任何事 |
| 搜尋中觸發 | 先清除搜尋、恢復展開,再 reveal |
驗收條件 QA · Given-When-Then
- Given 對一個收合的深層節點呼叫
revealAndSelect,Then 展開其祖先、選取、捲動到可見。 - Given 目標節點不存在,Then 靜默失敗(無錯誤、畫面不變)。
- Given 搜尋中觸發 reveal,Then 先清除搜尋並恢復展開,再執行 reveal。
元件與 Token 決策
- Exposed method:
revealAndSelect(nodeId)。
TODO / 未解問題
- 鍵盤導航(Arrow / Enter / Space)目前不實作,元件結構預留擴充空間(interaction-spec §5)。
使用場景
同一個元件、不同資料與設定,撐起平台上數種樹狀情境。元件不綁定業務語意——icon 由使用端透過 node-icon slot 依 node.kind 傳入。
Icon 規則:Site / Area 節點一律無 icon;icon 只用來區分不同「種類」的節點。因此:Site Management 只有 site/area → 完全無 icon;View 多了 Camera 種類 → Camera 有 icon;Floorplans 有 Floor plan + Camera 兩種 → 兩者各有 icon。
Site Management Sidebar 五層、無 icon
Site > Area × 4,純文字節點(無 icon)。這是 tree-view 的首發場景。
View Sidebar 最多六層、Camera 有 icon
Site > Area × 4 > Camera——Camera 直接掛在最深的 Area 底下(無 Floor plan 層);Camera 由使用端依 node.kind 傳入 icon。
Floorplans Sidebar 最多七層、Floor plan / Camera 有 icon
Site > Area × 4 > Floor plan > Camera——比 View 多一層 Floor plan;Floor plan 與 Camera 由使用端依 node.kind 傳入對應 icon。
狀態與邊界
| 情境 | 說明 |
|---|---|
| 層數不固定 | 節點行為由 kind 決定,不由深度決定;同一 kind 可出現在不同深度 |
| 未來取代 | 之後取代 Floor Plan 專案現有寫死層數的 tree 實作 |
驗收條件 QA · Given-When-Then
- Given Site Management 資料(最多五層、無 icon),Then 正確渲染且無 icon。
- Given View 資料(最多六層,Camera 有 icon),Then 依
node.kind顯示 Camera icon。 - Given Floorplans 資料(最多七層,Floor plan / Camera 有 icon),Then 依
node.kind顯示對應 icon。