grid 栅格 —— 布局
栅格是布局的骨架:把页面切成 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 风默文学 | 宁ICP备2024006574号 |
苏公网安备32040002010537