3 条作品
- 夏日口袋Summer Pockets
Key / 株式会社ビジュアルアーツ
- CLANNAD
Key / 株式会社ビジュアルアーツ
- 亚托莉 -我挚爱的时光-ATRI -My Dear Moments-
Frontwing Co., Ltd. / 枕 / ANIPLEX.EXE / iMel / Syawase Works / JAST USA
<script setup lang="ts">
import { DataList, Image, Stack, Text, Tooltip } from '@hina-ui/vue'
import { dataListDemo } from '../../data-list'
const items = dataListDemo('zh-CN')
</script>
<template>
<DataList
:items="items.slice(0, 3)"
item-key="id"
item-title="title"
item-description="subtitle"
:media-ratio="3 / 4"
layout-toggle
grid-min="10rem"
label="作品"
class="max-w-2xl"
>
<template #header><Text size="sm" tone="muted">3 条作品</Text></template>
<template #media="{ item }">
<Image :src="item.cover.src" alt="" fit="cover" class="size-full" />
</template>
<template #meta="{ item }">
<Stack gap="xs" class="min-w-0 w-full">
<Tooltip :content="item.developer">
<Text size="xs" tone="muted" truncate class="w-fit max-w-full">{{ item.developer }}</Text>
</Tooltip>
<Text as="time" :datetime="item.released" size="xs" tone="muted">{{ item.released }}</Text>
</Stack>
</template>
</DataList>
</template>
使用
DataList 提供媒体、标题、描述、元信息和操作区的条目结构。同一份内容可以切换为横向列表或纵向卡片;分页、加载占位和空态由组件协调。它保留列表语义,不附加整行点击或选择行为。需要表头和列对齐时使用 DataTable。
传入 items、唯一且稳定的 itemKey 和标题字段 itemTitle,即可渲染基本列表。标题和描述也接受函数。
<DataList :items="items" item-key="id" item-title="name" item-description="description" />
按需提供 #media、#title、#description、#meta 和 #actions。这些插槽接收 { item, index, key, layout },保留完整条目类型。开启分页时,index 包含当前页偏移量。提供默认插槽可替换整个条目内容,同时保留列表容器、分页和状态管理。
示例使用 Hikarinagi 的公开作品数据快照(2026-09-18),包含名称、开发商、发行日期与封面;详情链接指向原始条目。分页、筛选和远程分页示例使用 1,000 条,虚拟滚动示例使用 5,000 条;排版示例展示其中少量条目。封面由 Image 展示,操作使用 Button,长文本提示使用 Tooltip。
示例
条目结构与布局
layout-toggle 显示内置布局切换器,也可以通过 v-model:layout 或状态插槽的 setLayout 控制。切换时保留仍在渲染范围内的条目节点。
列表中媒体位于文字前方,操作区在宽容器中靠后、窄容器中移至文字下方;卡片中媒体在上,操作区在下。标题和描述默认完整换行,元信息自动换行。需要截断时,可在对应插槽中使用 Text 的截断能力。
mediaRatio 统一两种布局的媒体比例,省略时列表默认正方形,卡片默认 16:10。gridMin 设置最小卡片宽度,容器不足时使用单列;gridGap 设置间距。示例通过 Link 自定义标题,通过 Toggle 添加操作。
<script setup lang="ts">
import { reactive } from 'vue'
import { Bookmark } from '@lucide/vue'
import { DataList, Image, Link, Stack, Text, Toggle, Tooltip } from '@hina-ui/vue'
import { dataListDemo } from '../../data-list'
const items = dataListDemo('zh-CN')
const saved = reactive<Record<number, boolean>>({})
</script>
<template>
<DataList
:items="items.slice(3, 6)"
item-key="id"
item-description="subtitle"
layout="grid"
layout-toggle
grid-min="10rem"
label="自定义条目"
class="max-w-2xl"
>
<template #header><Text size="sm" tone="muted">3 条作品</Text></template>
<template #title="{ item }">
<Link :href="item.url" target="_blank" rel="noopener noreferrer" tone="neutral">
{{ item.title }}
</Link>
</template>
<template #media="{ item }">
<Image :src="item.cover.src" alt="" fit="cover" class="size-full" />
</template>
<template #meta="{ item }">
<Stack gap="xs" class="min-w-0 w-full">
<Tooltip :content="item.developer">
<Text size="xs" tone="muted" truncate class="w-fit max-w-full">{{ item.developer }}</Text>
</Tooltip>
<Text as="time" :datetime="item.released" size="xs" tone="muted">{{ item.released }}</Text>
</Stack>
</template>
<template #actions="{ item }">
<Toggle v-model="saved[item.id]" size="sm" :label="`标记 ${item.title}`" :tooltip="false">
<template #icon="{ pressed }">
<Bookmark :class="pressed ? 'fill-current' : undefined" />
</template>
{{ saved[item.id] ? '已标记' : '标记' }}
</Toggle>
</template>
</DataList>
</template>
本地分页
开启 pagination 后传入完整数组。组件截取当前页,并使用 Pagination 渲染分页。通过 v-model:page 和 v-model:page-size 控制页码与每页条数;数据减少导致页码越界时,自动回到最后一页。只有一页或没有数据时,默认分页自动隐藏;自定义 #pagination 不受此限制。
#footer 与分页共用底部一行,空间不足时换行。翻页只重置列表自身的滚动位置,不滚动外层页面。
状态插槽提供 setPage 和 setPageSize,处理加载状态和页码边界;修改每页条数会回到第一页。示例使用 Select 修改条数。
共 1000 条
- 夏日口袋Summer Pockets
2018-06-29
- CLANNAD
2004-04-28
- 亚托莉 -我挚爱的时光-ATRI -My Dear Moments-
2020-06-19
- 星之终途終のステラ
2022-09-30
- 星之梦planetarian ~ちいさなほしのゆめ~
2004-11-29
- 命运石之门STEINS;GATE
2009-10-15
- Summer Pockets REFLECTION BLUE
2020-06-26
- Harmonia -ハルモニア-
2016-09-23
- 时廻者LOOPERS -ルーパーズ-
2021-05-28
- 命运石之门 0STEINS;GATE 0
2015-12-10
<script setup lang="ts">
import { ref } from 'vue'
import { DataList, Select, Text } from '@hina-ui/vue'
import { dataListDemo } from '../../data-list'
const items = dataListDemo('zh-CN')
const page = ref(1)
const pageSize = ref(10)
const sizes = [10, 20, 50].map(value => ({ value, label: `${value} 条 / 页` }))
</script>
<template>
<DataList
v-model:page="page"
v-model:page-size="pageSize"
:items="items"
item-key="id"
item-title="title"
item-description="subtitle"
pagination
class="max-w-2xl"
>
<template #header="{ pageSize: size, setPageSize }">
<Text size="sm" tone="muted">共 {{ items.length }} 条</Text>
<Select
:model-value="size"
:options="sizes"
size="sm"
class="w-36 max-w-full"
aria-label="每页条数"
@update:model-value="value => setPageSize(Number(value))"
/>
</template>
<template #meta="{ item }">
<Text size="xs" tone="muted">{{ item.released }}</Text>
</template>
</DataList>
</template>
筛选与排序
筛选和排序由调用方计算,再将结果传给 items。查询变化时重置页码。示例组合 SearchInput 与 Select。
- 战国恋姬BRAVE二 ~战乱的九州、岛津篇~戦国†恋姫BRAVE弐 ~戦乱の九州、島津編~
2026-08-28
- ユリアリス
2026-08-16
- 両腕が折れたのでHなお世話をされる話
2026-07-31
- ネモフィリア-We pass each other-
2026-07-31
- ぼっちな魔王と俺の塔
2026-07-31
- お隣の天使様にいつの間にか駄目人間にされていた件 Memorial Vacation
2026-07-23
- 年上彼女2
2026-06-26
- as:9-nine- ARTEISIA
2026-06-26
- マガルミナ
2026-06-26
- Relirium - レリリウム - 遺跡と出逢いと冒険と
2026-05-29
共 1000 条结果
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { Button, DataList, Empty, SearchInput, Select, Text } from '@hina-ui/vue'
import { dataListDemo } from '../../data-list'
const items = dataListDemo('zh-CN')
const query = ref('')
const order = ref<string | number | null>('release')
const page = ref(1)
const options = [
{ value: 'release', label: '发行时间降序' },
{ value: 'title', label: '名称排序' },
]
const filtered = computed(() =>
items
.filter(item =>
`${item.title} ${item.originalTitle} ${item.developer}`
.toLocaleLowerCase()
.includes(query.value.trim().toLocaleLowerCase()),
)
.toSorted((a, b) =>
order.value === 'release'
? b.released.localeCompare(a.released)
: a.title.localeCompare(b.title),
),
)
watch([query, order], () => {
page.value = 1
})
</script>
<template>
<DataList
v-model:page="page"
:items="filtered"
item-key="id"
item-title="title"
item-description="subtitle"
pagination
:page-size="10"
label="筛选结果"
class="max-w-2xl"
>
<template #header>
<SearchInput
v-model="query"
placeholder="搜索名称或开发商"
aria-label="搜索"
class="w-full @lg/hn-data-list:min-w-0 @lg/hn-data-list:flex-1"
/>
<Select
v-model="order"
:options="options"
aria-label="排序方式"
class="w-full @lg/hn-data-list:w-44"
/>
</template>
<template #meta="{ item }">
<Text size="xs" tone="muted">{{ item.released }}</Text>
</template>
<template #empty>
<Empty title="没有匹配的作品" size="sm" :icon="false">
<template #actions>
<Button variant="outline" tone="neutral" size="sm" @click="query = ''">清空搜索</Button>
</template>
</Empty>
</template>
<template #footer="{ total }">
<Text size="xs" tone="muted">共 {{ total }} 条结果</Text>
</template>
</DataList>
</template>
远程分页
同时开启 manual 和 pagination 时,items 是服务端返回的当前页,组件不会再次切片。提供 total 显示完整分页;总数未知时省略它,并传入 hasNextPage,分页将只显示上一页、下一页和当前页码。
示例实际请求按页保存的静态 JSON 快照,可运行于静态部署;它不实时查询作品库,也没有人为等待。使用 Switch 切换已知/未知总数。请求期间保留上一页,取消过期请求;失败时通过 Empty 显示重试入口。
<script setup lang="ts">
import { onMounted, onScopeDispose, ref, watch } from 'vue'
import { useRuntimeConfig } from '#app'
import { Button, DataList, Empty, Switch, Text } from '@hina-ui/vue'
import type { DataListDemoItem } from '../../data-list'
const baseURL = useRuntimeConfig().app.baseURL
const page = ref(1)
const rows = ref<DataListDemoItem[]>([])
const total = ref<number>()
const loading = ref(true)
const knownTotal = ref(true)
const hasNextPage = ref(false)
const failed = ref(false)
let controller: AbortController | undefined
async function load() {
controller?.abort()
const request = new AbortController()
controller = request
loading.value = true
failed.value = false
try {
const response = await fetch(`${baseURL}demo/data-list/page-${page.value}.json`, {
signal: request.signal,
})
if (!response.ok) throw new Error(String(response.status))
const data = (await response.json()) as {
items: Omit<DataListDemoItem, 'subtitle'>[]
total: number
hasNextPage: boolean
}
if (request.signal.aborted) return
rows.value = data.items.map(item => ({
...item,
title: item.title,
subtitle: item.title === item.originalTitle ? '' : item.originalTitle,
}))
total.value = data.total
hasNextPage.value = data.hasNextPage
} catch {
if (!request.signal.aborted) {
rows.value = []
failed.value = true
}
} finally {
if (!request.signal.aborted) loading.value = false
}
}
onMounted(load)
watch(page, load)
onScopeDispose(() => controller?.abort())
</script>
<template>
<DataList
v-model:page="page"
:items="rows"
item-key="id"
item-title="title"
item-description="subtitle"
pagination
manual
:page-size="10"
:total="knownTotal ? total : undefined"
:has-next-page="hasNextPage"
:loading="loading"
:min-height="400"
class="max-w-2xl"
label="远程条目"
>
<template #header>
<Switch v-model="knownTotal" :disabled="loading">已知总条数</Switch>
<Button variant="ghost" tone="neutral" size="sm" :disabled="loading" @click="load">
刷新
</Button>
</template>
<template #meta="{ item }">
<Text size="xs" tone="muted">{{ item.released }}</Text>
</template>
<template #empty>
<Empty :title="failed ? '加载失败' : '暂无数据'" size="sm">
<template v-if="failed" #actions><Button size="sm" @click="load">重试</Button></template>
</Empty>
</template>
</DataList>
</template>
加载与空态
首次加载默认使用与条目结构对应的 Skeleton,placeholderCount 控制数量。默认只为已配置的内容区生成占位;虚拟模式默认渲染 3 个占位,普通分页默认与每页条数一致。自定义整个条目时,可用 #placeholder="{ index, layout }" 提供对应骨架,不需要伪造业务数据。
已有条目时,loading 会保留内容、禁止内容交互和翻页,并通过 LoadingOverlay 延迟显示加载提示。#loading 可替换整片首次加载内容以及刷新提示;#empty 或 emptyText 自定义空态。
minHeight 为内容区预留最小高度,自动高度列表在清空数据加载时保留上一轮高度;height 则固定内容区高度,并在内容超出时使用 ScrollArea。
<script setup lang="ts">
import { computed, ref } from 'vue'
import { DataList, Image, SegmentedControl, Stack, Text } from '@hina-ui/vue'
import { dataListDemo } from '../../data-list'
const items = dataListDemo('zh-CN')
const state = ref('initial')
const options = [
{ value: 'initial', label: '首次加载' },
{ value: 'ready', label: '内容' },
{ value: 'refresh', label: '刷新' },
{ value: 'empty', label: '空态' },
]
const rows = computed(() =>
state.value === 'initial' || state.value === 'empty' ? [] : items.slice(0, 2),
)
</script>
<template>
<Stack class="w-full max-w-2xl">
<SegmentedControl v-model="state" :options="options" size="sm" aria-label="列表状态" />
<DataList
:items="rows"
item-key="id"
item-title="title"
item-description="subtitle"
:loading="state === 'initial' || state === 'refresh'"
:placeholder-count="2"
:media-ratio="3 / 4"
:height="280"
label="条目"
>
<template #media="{ item }">
<Image :src="item.cover.src" alt="" fit="cover" class="size-full" />
</template>
<template #meta="{ item }">
<Text size="xs" tone="muted">{{ item.released }}</Text>
</template>
</DataList>
</Stack>
</template>
自定义分页
#pagination 接收与头部、页脚相同的状态和操作方法,可以接入 Pagination 的附属选项。
<DataList :items="items" item-key="id" item-title="name" pagination>
<template #pagination="{ page, pageSize, total, loading, setPage, setPageSize }">
<Pagination
:model-value="page"
:page-size="pageSize"
:total="total ?? 0"
:pending="loading"
:page-size-options="[10, 20, 50]"
show-info
@change="value => value.pageSize === pageSize ? setPage(value.page) : setPageSize(value.pageSize)"
/>
</template>
</DataList>
虚拟滚动
virtualize 支持列表和网格,与 VirtualList 复用窗口计算能力。网格按行虚拟化,以该行最高条目作为行高,列数随容器宽度调整。estimateSize 是列表行或网格整行的估算高度;overscan 是窗口前后额外保留的行数。高度会在渲染后动态测量。
height 指定滚动视口高度,虚拟模式默认 320px;百分比高度需要父容器具有明确高度。与分页组合时只虚拟化当前页。需要跨卸载保留的条目状态应按 ID 存在组件外部。
下方使用 5,000 条不同的真实条目,在列表与网格间切换,并显示当前可见范围。仅渲染当前窗口及缓冲区内的条目。
- 夏日口袋Summer Pockets
2018-06-29
- CLANNAD
2004-04-28
- 亚托莉 -我挚爱的时光-ATRI -My Dear Moments-
2020-06-19
- 星之终途終のステラ
2022-09-30
- 星之梦planetarian ~ちいさなほしのゆめ~
2004-11-29
可见范围:1–4 / 5000
<script setup lang="ts">
import { ref } from 'vue'
import { DataList, Image, Text, type DataListLayout } from '@hina-ui/vue'
import { dataListVirtualDemo } from '../../data-list-virtual'
const items = dataListVirtualDemo('zh-CN')
const layout = ref<DataListLayout>('list')
const range = ref({ startIndex: -1, endIndex: -1 })
</script>
<template>
<DataList
v-model:layout="layout"
:items="items"
item-key="id"
item-title="title"
item-description="subtitle"
layout-toggle
:media-ratio="layout === 'grid' ? 16 / 10 : 3 / 4"
grid-min="12rem"
:height="400"
:virtualize="{ estimateSize: layout === 'grid' ? 280 : 128, overscan: 1 }"
body-class="border-line rounded-lg border"
content-class="p-4"
label="虚拟条目"
class="max-w-2xl"
@range-change="range = $event"
>
<template #media="{ item }">
<Image :src="item.cover.src" alt="" fit="cover" class="size-full" />
</template>
<template #meta="{ item }">
<Text size="xs" tone="muted">{{ item.released }}</Text>
</template>
<template #footer>
<Text size="xs" tone="muted">
可见范围:{{ range.startIndex + 1 }}–{{ range.endIndex + 1 }} / {{ items.length }}
</Text>
</template>
</DataList>
</template>
SSR
列表、卡片、分页和首次加载骨架都支持服务端渲染。虚拟模式输出可读的初始窗口,挂载后校准视口和行高,不需要 client-only 包装。
虚拟网格可通过 initialColumns 指定服务端估算列数,默认 1。服务端和客户端初始值必须一致;实际列数由 CSS 和容器宽度决定。初始窗口之外的条目仍需在滚动后渲染。
API
属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
items | readonly T[] | 必填 | 完整数组;手动模式下为当前页 |
itemKey | 键字段或 (item, index) => string | number | 必填 | 唯一、稳定的键 |
itemTitle / itemDescription | 文本字段或 (item, index) => string | number | null | undefined | — | 标题/描述;对应插槽优先 |
mediaRatio | number | — | 媒体宽高比 |
layoutToggle | boolean | false | 显示布局切换器 |
gridMin | string | '14rem' | 期望的网格最小列宽 |
gridGap | 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | 网格间距 |
size | 'sm' | 'md' | 'lg' | 'md' | 列表媒体大小、条目间距及卡片内边距 |
divided | boolean | true | 列表分隔线 |
pagination | boolean | false | 显示分页 |
manual | boolean | false | 当前页数据不再切片 |
total | number | — | 远程总条数;本地模式使用数组长度 |
hasNextPage | boolean | false | 总数未知时是否有下一页 |
loading | boolean | false | 加载状态,禁止内容交互和分页 |
placeholderCount | number | 3 或每页条数 | 首次加载骨架数量 |
emptyText | string | 语言包 | 默认空态文字 |
label | string | — | 列表与滚动区的可访问名称 |
virtualize | boolean | DataListVirtualOptions | false | 启用列表或网格虚拟化 |
height | number | string | 自动;虚拟模式 320 | 内容区固定高度 |
minHeight | number | string | 160 | 未设置固定高度时的内容区最小高度 |
class | string | — | 根节点类名 |
bodyClass | string | — | 内容区外框,涵盖加载与空态 |
contentClass | string | — | 所有布局和渲染模式下均作用于条目列表容器 |
itemClass | string | ((item, index) => string | undefined) | — | 单个条目类名 |
DataListVirtualOptions 包含 estimateSize(列表默认 112px、网格默认 280px)、overscan(默认 3 行)和 initialColumns(默认 1)。其他属性,包括 dir 和 style,透传到根节点。
双向绑定
| 模型 | 类型 | 默认值 |
|---|---|---|
layout | 'list' | 'grid' | 'list' |
page | number | 1 |
pageSize | number | 10 |
插槽
| 插槽 | 参数 | 说明 |
|---|---|---|
media / title / description / meta / actions | DataListItemSlot<T> | 条目对应区域 |
default | DataListItemSlot<T> | 替换整个条目内容 |
placeholder | { index, layout } | 单个首次加载骨架 |
header / footer | DataListState<T> | 列表上方/底部与分页同一行 |
pagination | DataListState<T> | 替换分页 |
empty | DataListState<T> | 非加载状态下的空态 |
loading | DataListState<T> | 整片首次加载内容/刷新指示 |
DataListState<T> 包含当前页 items、page、pageSize、total、pageCount、layout、loading、refreshing、hasPreviousPage、hasNextPage、setPage(page)、setPageSize(size) 和 setLayout(layout)。未知总数时 total 和 pageCount 为 undefined。
事件与实例
| 名称 | 参数/类型 | 说明 |
|---|---|---|
pageChange | { page, pageSize } | 分页操作或越界修正 |
rangeChange | { startIndex, endIndex } | 虚拟模式可见范围,包含分页偏移;无范围时为 -1 |
viewport | HTMLElement | undefined | 设置高度或虚拟化时的滚动容器 |
scrollToIndex(index, options?) | align?: 'start' | 'center' | 'end' | 'auto'; behavior?: ScrollBehavior | 滚动到当前页内的条目,index 使用全局下标 |
scrollToIndex 在存在内部滚动容器时只滚动该容器;没有内部容器时,这个显式调用才会滚动外层祖先,让目标条目进入视口。
直接修改分页模型不会再次触发 pageChange;远程请求应监听模型。所有数据类型均从包根导出,包括 DataListProps<T>、DataListItemSlot<T>、DataListState<T>、DataListVirtualOptions 和 DataListExpose。