grid 欄格 —— 版面配置
Ce contenu n’est pas encore disponible dans votre langue.
欄格是版面配置的骨架:把頁面切成 24 列,內容按列寬比例排佈、按斷點響應式變化。lh-ui 的欄格基於 CSS Grid(不是 flex 的百分比寬度),定位精度更高、支援顯式起點定位。core/grid 管的是斷點判定、響應式回退、列定位的鉗制計算——CSS 本身算不了「向下回退」和「越界鉗制」。
- 各端實作
<LhRow>/<LhCol>:頁面主版面配置、表單兩列 / 三列排佈、卡片牆、儀表板。 - 需要響應式列寬(PC 三列、平板兩列、手機一列)。
- 不該用它:簡單兩元素左右對齊(用 flex 或
space更直接);不定寬的流式版面配置(grid 列寬固定比例反而彆扭)。
import { LH_GRID_COLUMNS, LH_GRID_BREAKPOINTS, LH_GRID_BREAKPOINT_ORDER, resolveBreakpoint, resolveResponsiveSize, getGridRowClassNames, normalizeColSize, clampSpan, getColStyle, getGridColClassNames,} from '@lh-ui/share/core/grid'import type { LhGridBreakpoint, LhGridSpan, LhColSize, LhBreakpointSizes,} from '@lh-ui/share/core/grid'CSS Grid 而非 flex 百分比:flex 欄格的每個 col 都是 width: x/24,行內換行靠 flex-wrap,列間距用負 margin 抵消——一堆 hack。CSS Grid 直接 grid-template-columns: repeat(24, 1fr),間距用 gap,col 用 grid-column 定位,語義乾淨。代價是 IE 不支援——lh-ui 的目標端(現代瀏覽器 + 小程序 webview)都已覆蓋。
斷點向下回退:resolveResponsiveSize(sizes, 'md') 從 md 檔開始往小找,命中第一個已宣告的檔位。使用者只寫了 { xs: 24, lg: 12 },在 md(768~1024)寬度下會回退到 xs 的 24——不是 undefined。這是響應式 API 的核心體驗:使用者只宣告關鍵斷點,中間檔位自動沿用小螢幕設定。
span = 0 是哨兵而非 bug:span: 0, offset: 6 表示「不佔列、只把後面內容推 6 列」。此時 getColStyle 輸出 7 / 25(延伸到末列)而非 7 / span 0——後者會讓元素塌縮成零寬度。
起點鉗制防撐破容器:offset 21 + span 8 算出起點 22,越界。getColStyle 把起點鉗到 24 + 1 - 8 = 17——元素整體左移到能放下為止。註解寫了原因:越界的 grid-column 會讓 Grid 生成隱式軌道,把容器寬度撐破。
auto-placement 與顯式定位的分水嶺:純 span 用 span N(不指定起點,Grid 自動排列);帶 offset/push/pull 用 start / span N(顯式起點,打斷自動排列)。註解記錄了一個真實陷阱:純 span 若寫成 1 / span N,同行所有 Col 都從列 1 開始,Grid 只能把後面的推到下一行——視覺上變成「直排版」。
類別名映射表而非直接拼接:justify="center" 和 align="center" 如果直接拼類別名會撞出兩個 lh-row--center(justify 走 justify-items、align 走 align-items,是兩個不同的 CSS 屬性)。用顯式映射表後:justify 的 center → lh-row--center,align 的 center → lh-row--middle(center 作 middle 的相容別名,對齊主流庫 API 習慣)。附帶好處:未知值不產出髒類別名。
| 函式 / 常數 | 簽名 | 傳回 | 作用 |
|---|---|---|---|
resolveBreakpoint |
(width: number) => LhGridBreakpoint |
斷點 | 視區寬度 → 檔位 |
resolveResponsiveSize |
(sizes, breakpoint) => LhColSize | undefined |
生效尺寸 | 向下回退到最近已宣告檔 |
getGridRowClassNames |
(state: { justify?, align?, wrap? }) => string[] |
類別名 | 對齊 / 換行 |
normalizeColSize |
(size?: LhColSize) => LhGridSpan |
描述物件 | 數字 → { span } |
clampSpan |
(span?: number) => number |
[0, 24] 整數 | 非有限值歸 0 |
getColStyle |
(size?: LhColSize) => Record<string, string> |
內聯樣式 | grid-column 定位 |
getGridColClassNames |
(size?: LhColSize) => string[] |
類別名 | span / offset |
常數:LH_GRID_COLUMNS = 24、LH_GRID_BREAKPOINTS(xs 0 / sm 640 / md 768 / lg 1024 / xl 1280)、LH_GRID_BREAKPOINT_ORDER(從大到小)。
export type LhGridBreakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
export interface LhGridSpan { span?: number // 佔據列數(0 = 不佔列,僅用於 offset 定位) offset?: number // 左側空出列數 push?: number // 起點右移 pull?: number // 起點左移}
export type LhColSize = number | LhGridSpanexport type LhBreakpointSizes = Partial<Record<LhGridBreakpoint, LhColSize>>欄格是純版面配置原語,本身沒有無障礙語義——不要給 Row / Col 加 role。內容語義由內部的元件提供。一條相關原則:
- 版面配置變化不得改變 DOM 順序:響應式重排時 Col 在 DOM 裡的順序固定,只是視覺列數變化。若需要不同斷點下內容順序不同(「主內容在 PC 左側、行動裝置頂部」),用原始碼順序 +
order樣式,不要切換 DOM 順序——讀屏軟體按 DOM 序播報,視覺與播報順序不一致會造成混亂。
import { resolveBreakpoint, resolveResponsiveSize, getColStyle, clampSpan,} from '@lh-ui/share/core/grid'
resolveBreakpoint(800) // 'md'resolveBreakpoint(600) // 'xs'(< 640)resolveBreakpoint(1300) // 'xl'
// 向下回退:md 檔未宣告 → 取 xsresolveResponsiveSize({ xs: 24, lg: 12 }, 'md') // 24resolveResponsiveSize({ xs: 24, lg: 12 }, 'lg') // 12resolveResponsiveSize({ xs: 24 }, 'xl') // 24resolveResponsiveSize(undefined, 'md') // undefined(無宣告)getColStyle(8) // { gridColumn: 'span 8' }(auto-placement)getColStyle({ span: 8, offset: 4 })// { gridColumn: '5 / span 8' }(起點 = offset+1,鉗制後未越界)getColStyle({ span: 0, offset: 6 })// { gridColumn: '7 / 25' }(哨兵:不佔列、延伸到末列)getColStyle({ span: 8, offset: 21 })// { gridColumn: '17 / span 8' }(越界鉗制:起點 22 → 17)getColStyle({ span: 8, push: 4 })// { gridColumn: '5 / span 8' }(push 4 = 起點右移 4)getColStyle({ span: 8, pull: 4 })// { gridColumn: '1 / span 8' }(起點 5-4=1,鉗制下限 1)getColStyle({}) // { gridColumn: 'span 24' }(無聲明佔滿)
clampSpan(8) // 8clampSpan(30) // 24(鉗到上限)clampSpan(-5) // 0clampSpan(NaN) // 0(非有限值歸 0)<!-- Vue3:響應式三列 → 兩列 → 一列 --><template> <LhRow :gutter="16"> <LhCol v-for="item in list" :key="item.id" :xs="24" :sm="12" :lg="8"> <LhCard>{{ item.title }}</LhCard> </LhCol> </LhRow></template><!-- 表單兩列布局 + offset 定位按鈕 --><template> <LhRow :gutter="20"> <LhCol :span="12"><LhFormItem label="姓名" prop="name" /></LhCol> <LhCol :span="12"><LhFormItem label="邮箱" prop="email" /></LhCol> <LhCol :span="12" :offset="12"> <LhButton type="primary" @click="submit">提交</LhButton> </LhCol> </LhRow></template>// React:斷點監聽由 UI 層用 resize 監聽或 useMediaQuery 驅動 state;// getColStyle 在 render 中純函數調用// uniapp:<view> + grid;小程序端無 window,斷點由 UI 層用 uni.getSystemInfoSync()// 的 safeArea/windowWidth 或 res.rpx 換算驅動,core 的 resolveBreakpoint 照用| 類別名 | 語義 |
|---|---|
.lh-row |
行容器(display: grid; grid-template-columns: repeat(24, 1fr)) |
.lh-row--{end|center|space-between|space-around} |
水平對齊(justify-items) |
.lh-row--{middle|bottom} |
垂直對齊(align-items) |
.lh-row--nowrap |
不換行 |
.lh-col |
列容器 |
.lh-col--{1..24} |
span 列數 |
.lh-col--offset-{1..24} |
左側偏移列數 |
間距令牌 --lh-row-gutter(行間距,由 gutter prop 覆蓋);列間透過 grid gap 實作。
gutter與 grid gap:Grid 的 gap 是行列同時生效的;只想要列間距不要行間距時,行容器要單獨覆蓋row-gap: 0。- 響應式斷點需 UI 層監聽:
resolveBreakpoint是純函式,UI 層要自己監聽 resize(或用matchMedia)把寬度餵進來。SSR 首屏無 window,預設渲染 xs 檔、用戶端激活後糾正。 - 不要混用 auto 與顯式定位:同行裡一個 Col 用純 span、另一個用 offset,顯式定位的那個會打斷 auto-placement——行內排列結果可能不直觀。要嘛全純 span,要嘛全顯式。
push/pull與offset的區別:三者都改起點,但語義不同——offset是「左側留空」(佔位語義),push/pull是「平移」(視覺排序)。實作在getColStyle裡都是改起點,類別名層面只有 offset 有對應類(push / pull 只走內聯樣式)。- 24 列座標系統是硬約束:
span 25會被鉗到 24;offset 24 + span 1起點鉗到 24。業務側若需要超過 24 的細粒度,改用LhRow巢狀而非加大列數。 - 巢狀欄格:
LhCol裡再放LhRow,內層重新從 24 列開始算——子欄格不繼承父級的 span 上下文。這是 CSS Grid 的天然行為。 - 類別名 span 與內聯樣式並存:
getGridColClassNames出的lh-col--8與getColStyle的grid-column是兩套機制(類別名供 SCSS 媒體查詢、內聯供 JS 響應式)。UI 層二選一,兩者同時寫會互相覆蓋(內聯樣式贏)。
@lh-ui/share/core/grid → getColStyle( )(填好参数后点运行)Copyright © 2024-2026 FengMo WenXue