SwGallery
SwGallery is a non-scrolling cover-card container that wraps into an equal-height grid, complementary to SwRail (horizontal scrolling row). It only owns layout and fetch pacing — all card visuals (cover / badge / rating / episode count) are rendered by the business via the #default scoped slot (usually with SwImage); movie / comic / novel covers are all the same to the component.
Poster wall: minItemWidth adaptive columns + load-more button (with remaining count)
Column control (pick one)
| Mode | Usage | Fits |
|---|---|---|
| Fixed card width, adaptive (default) | :min-item-width="180" | Poster walls: column count follows window resizing (CSS Grid auto-fill + minmax) |
| Locked column count | :columns="3" or :columns="{ mobile: 1, tablet: 2, desktop: 3 }" | Designs with explicit column counts that must not change on ultra-wide screens (breakpoints align with framework: mobile <768 / desktop ≥1024; missing tiers fall back to the nearest) |
Both are pure CSS (media query + CSS variables) — zero JS, SSR-safe. Passing columns overrides minItemWidth.
columns breakpoint lock (1/2/3 columns) + custom #load-more button
loadMode auto: load on reaching bottom
total remaining count + auto finish
Load more (three-state loadMode, default button)
| loadMode | Behavior | Scenario |
|---|---|---|
'button' (default) | Bottom button; with total it shows a count ("加载更多 (N)" — "Load more (N)"): with pageSize also passed, N = min(pageSize, remaining); otherwise N = remaining | Full list pages |
'auto' | An IntersectionObserver sentinel near the bottom triggers automatically (autoThreshold lead distance; keeps filling until the first screen is full) | Infinite-scroll lists |
'none' | Pure grid | Multi-row content blocks on category home pages; paginated paging (with SwPagination, replace-mode swapping the whole items set) |
Append-mode data flow: on @load-more, append to items; an internal busy lock + the loading prop form a double lock against duplicate triggers (appending to items / finishing loading auto-unlocks, so failure cases can retry). Set finished when there is no more data (with total passed, remaining=0 is automatically treated as the end).
Failure & retry: when a request fails, the parent sets error: true and the load area becomes a "加载失败,点击重试" (load failed, tap to retry) button (clicking still emits @load-more; the parent clears error before retrying); in auto mode a failure removes the sentinel and switches to manual retry (preventing auto-retry loops) — once error is cleared and items are appended, auto loading resumes.
Failure & retry: error flag → retry button → clear and continue (with page-size button count)
loadMode none: category-home content block (pure grid)
Standard Usage
<script setup lang="ts">
const items = ref<Movie[]>([])
const total = ref(0)
const loading = ref(false)
async function loadMore() {
loading.value = true
try {
const r = await request<MovieList>('/movie/list', { body: { offset: items.value.length } })
items.value = [...items.value, ...r.list]
total.value = +r.total
} finally {
loading.value = false
}
}
await loadMore()
</script>
<template>
<SwGallery :items="items" :total="total" :loading="loading" :min-item-width="180" @load-more="loadMore">
<template #default="{ item }">
<SwLink :to="`/movie/${item.id}`" :track="{ /* card tracking goes through SwLink; the grid has none built in */ }">
<SwImage :src="item.img" :alt="item.name" ratio="2/3" />
<p>{{ item.name }}</p>
</SwLink>
</template>
</SwGallery>
</template>
Props
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
items | T[] | ✓ | — | Append-mode data source (generic; the component doesn't care about the shape) |
minItemWidth | number | string | 240 | Minimum card width (auto-fill mode). A number is treated as px; a string is passed through as-is, e.g. min(15.625rem, 46%) or var(--x) | |
columns | number | { mobile?, tablet?, desktop? } | undefined | Locked column count; overrides minItemWidth when passed | |
loadMode | 'button' | 'auto' | 'none' | 'button' | Load-more mode | |
loading | boolean | false | External loading state (one of the double locks) | |
finished | boolean | false | No more data | |
total | number | undefined | Total count → button count; remaining=0 is treated as the end | |
pageSize | number | undefined | Items per load → the button count shows min(pageSize, remaining); falls back to the remaining count when omitted | |
error | boolean | false | Last load failed (maintained by the parent); when true the load area becomes a "load failed, tap to retry" button | |
autoThreshold | number | string | 200 | Sentinel lead distance for auto mode. A number is treated as px; a string is passed straight to rootMargin, so % works for viewport-relative prefetching. Can be changed at runtime (the observer is rebuilt) | |
ariaLabel | string | undefined | Outputs role="region" + aria-label only when passed |
Events
| Name | Payload | Description |
|---|---|---|
load-more | — | Asks the parent to append items (de-duplicated inside the double lock) |
Slots
| Name | Scope | Description |
|---|---|---|
default | { item, index } | Each card (the business renders all visuals) |
load-more | { loading, remaining, error } | Custom load-more button area |
finished | — | End-of-list text (default 「已经到底了」, i.e. "You've reached the end") |
empty | — | Empty state when items is empty (default 「暂无内容」, i.e. "No content yet") |
Expose
| Name | Description |
|---|---|
loadMore() | Imperatively trigger one load (subject to the double lock) |
CSS Variables
| Variable | Default | Description |
|---|---|---|
--sw-gallery-gap | 1rem | Grid gap |
--sw-gallery-min | Written from minItemWidth | Minimum card width |
--sw-gallery-template | Falls back to minItemWidth auto-fill | Custom-breakpoint escape hatch: the columns prop only offers mobile/tablet/desktop. For more breakpoints, omit columns and set this variable inside your own media queries (its value is grid-template-columns). Mutually exclusive with columns — passing the prop overrides it |
Caveats
- No built-in tracking: card impression / click go through SwLink
:trackinside the card; the grid itself reports nothing. - Pager scenario: numbered paging is a replace-mode data flow — use
loadMode="none"+ SwPagination (framework#131); don't stuff page numbers into the Gallery. - For component selection (horizontal scroll / carousel / video feed / grid) see the SwRail selection table.
- Pass
:loadingfor retry-on-error too: botherrorandloadingare parent-maintained; on failure, passing only:errorwithout:loadingkeeps the internal load lock held and the retry click does nothing — pass:loadingas well, and clearerrorback tofalsebefore firing the retry request. - Pair
@load-morewith:finishedor:total: with neither, "reached the end" is never detected — the button stays put (button mode) or the sentinel keeps re-firing empty pages (an infinite loop in auto mode). Set:finished="true"when there's no more data, or pass:totalso the remaining count converges to zero.