CardList
卡片列表:布局、加载与多选手势一处收齐,每项由 #item 插槽给——写 Card 或整张自定义。插槽参数是 { item, index, itemKey, selected, active, selecting, toggle }(叫 itemKey 不叫 key:key 是 Vue 保留字,传进去会变成 vnode key)。
状态归列表、渲染归项:列表持有选中集合、高亮项与选择模式,经 provideListItemContext 逐项传下去,项组件用 useListItemContext 读本项状态、自己决定怎么画。XCard 是第一个接它的项组件,插槽里写 <XCard> 不必逐项绑定;hover 给列表统一的悬停效果,单张卡自己给了以卡片为准。容器 role=list,class / style 落在根上。
数据侧见 usePagedList,多选手势见 useCollectionSelect。
无限加载
has-more 时底部哨兵进视口发 load-more。没铺满且还有更多就接着要:列表变了、拉完一页、容器尺寸变了都主动量一次(IntersectionObserver 只在进出视口时回调,一页拉完哨兵仍在视口里不会再报);要过一页而列表原样没变就停,防空页死循环;容器在 display: none 里不拉。数据可以是同步的本地切片,不必有 loading。
首次加载(loading 且没有项)出骨架(skeleton 块数,或 #skeleton 插槽);error 独立于「没有更多」,显示重试、发 retry。
布局
grid纯 CSS 宫格:按min-item-width自动排列数,或columns固定列数。masonry瀑布流:按item-size(多是图的像素尺寸)的宽高比估高、放进最矮的一列,DOM 按列排,追加下一页时前面的不换列;卡片里固定高度的部分给item-extra。columnsCSS 多栏纵向流:先排满一栏再排下一栏。
gap 行列同一个数;min-columns 给列数下限,按实测的内容宽度(已扣滚动条)排,宿主不必按边线、内边距反推卡片宽度。
没有更多了
多选
selectable 打开,选中集合与选择模式是 v-model:selected(Set<string>)与 v-model:selecting。手势同 Finder 与 iOS 相册:点勾选圈或 ⌘ / Ctrl / Shift 点进入选择模式;模式里点一项加减、Shift 连选、按住一项划过去连成一段、从空白处拖框;⌘A 全选、Esc 退出。选择模式里卡片内的点击在捕获阶段拦下,宿主的「打开」不会触发。
批量操作栏由 #selection-bar 给({ count, total, selectAll, invert, exit }),组件不认识业务动作。无限加载时给 selectable-keys(全部可选的键,含没加载的):只按已加载的算,「全选」只选了一页,换筛选重新分页时后面几页的选中会被悄悄清掉。
点卡片「打开」:无
没有更多了
分页
paginate 换成底部分页器(v-model:page、page-size、total),不再无限加载。行视图也是卡片:plain + hover="tint",换视图时选中、高亮与加载状态不用换一套。scroll 关掉时列表随外层滚。
文案与边界
空态、到底、出错、重试、加载中与勾选圈名取 useUiLocale().cardList,props 可覆盖;空态与到底传空串即不显示。当前不做虚拟化,项全量渲染。布局判定的纯逻辑(columnCount / masonry / estimateHeight / wantsMore 等)已导出,宿主自写列表时复用同一套。
API
Props
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
items 必填 | T[] | — | — |
itemKey 必填 | ItemKey<T> | — | — |
layout | "columns" | "grid" | "masonry" | undefined | "grid" | `grid` 宫格、`masonry` 瀑布流(按 `itemSize` 估高放进最矮一列)、`columns` 多栏纵向流 |
minItemWidth | number | undefined | 160 | 卡片最小宽度(px):没给 `columns` 时按它排列数 |
columns | number | undefined | undefined | 固定列数 |
minColumns | number | undefined | 1 | 至少几列(没给 `columns` 时):按实测的内容宽度排,窄到放不下也不少于它,不靠宿主反推卡片宽度 |
gap | number | undefined | 8 | 卡片间距(px),行列同值 |
itemSize | ((item: T) => readonly [number, number] | undefined) | undefined | undefined | 瀑布流估高用的内容宽高(多是图的像素尺寸);缺省按正方形 |
itemExtra | number | undefined | 0 | 瀑布流估高时卡片里固定高度的部分(标题栏、信息栏,px) |
minItemHeight | number | undefined | 0 | 瀑布流估高时内容的最小高度(px):卡片给画面设了最小高度(扁图也要装下叠层)时与之一致 |
hover | "lift" | "none" | "ring" | "tint" | undefined | "ring" | 卡片统一的悬停效果(见 `XCard` 的 `hover`;缺省只描边,不上浮);单张卡自己给了以卡片为准 |
active | string | readonly string[] | undefined | undefined | 高亮的项(当前打开的、菜单开着的):描边淡一档 |
scroll | boolean | undefined | true | 根节点里的列表区就是滚动容器(缺省);关掉时随外层滚,无限加载的哨兵按视口算 |
loading | boolean | undefined | false | — |
hasMore | boolean | undefined | false | — |
error | unknown | undefined | 出错了(有值即是):显示 `errorText` 与重试,不当成「没有更多」 |
skeleton | number | undefined | 8 | 首次加载的骨架块数 |
paginate | boolean | undefined | false | 换成底部分页器(`v-model:page`),不再无限加载 |
total | number | undefined | undefined | — |
pageSize | number | undefined | 20 | — |
selectable | boolean | undefined | false | — |
selectableKeys | readonly string[] | undefined | undefined | 全部可选的键(含分页还没加载出来的):全选、反选、选择栏的总数、清理筛掉的选中按它算;缺省是已加载的项。 无限加载时要给,否则「全选」只选了一页 |
emptyText | string | undefined | undefined | 空态文案,缺省取 locale;传空串不显示 |
doneText | string | undefined | undefined | 到底文案,缺省取 locale;传空串不显示 |
errorText | string | undefined | undefined | — |
retryText | string | undefined | undefined | — |
selectLabel | string | undefined | undefined | 勾选圈的读屏名,缺省取 locale |
page | number | undefined | 1 | — |
selected | Set<string> | undefined | new Set() | — |
selecting | boolean | undefined | false | — |
Emits
| 事件 | 载荷 | 说明 |
|---|---|---|
load-more | [] | — |
retry | [] | — |
update:page | [value: number] | — |
update:selected | [value: Set<string>] | — |
update:selecting | [value: boolean] | — |
Slots
| 插槽 | 说明 |
|---|---|
item | — |
empty | — |
skeleton | — |
header | — |
selection-bar | — |