Skip to content

列表组件

列表组件提供了完整的列表页面解决方案,包括传统列表和卡片网格两种展示方式。

导入

typescript
import {
  DuxCardPage,
  DuxListPage
} from '@duxweb/dvha-pro'

组件总览

DVHA Pro 提供以下列表组件:

  • DuxListPage - 传统列表页面组件
  • DuxCardPage - 卡片网格页面组件

DuxListPage 传统列表页面

传统列表页面组件,基于 DuxListLayout 扩展,提供垂直排列的列表布局,适用于文章列表、订单列表等场景。

属性

DuxListPage 继承了 DuxListLayout 的所有属性:

属性名类型默认值说明
rowKeystring'id'行键字段
pathstring-数据接口路径
filterRecord<string, any>-筛选条件
filterSchemaJsonSchemaNode[]-筛选表单配置
paginationboolean/objecttrue分页配置
tabsTabItem[]-标签页配置
toolsListPageTools-工具栏配置
actionsUseActionItem[][]操作配置
checkableboolean-是否支持选择
sideLeftTitlestring''左侧栏标题
sideRightTitlestring''右侧栏标题

插槽

插槽名说明参数
default列表项内容{ item, isChecked, toggleChecked }
actions操作按钮区域-
tools工具栏扩展-
bottom底部扩展区域-
sideLeft左侧栏内容-
sideRight右侧栏内容-

基础用法

vue
<script setup>
import { DuxListPage } from '@duxweb/dvha-pro'
import { NButton, NCheckbox, NTag } from 'naive-ui'
import { ref } from 'vue'

const filter = ref({})

const filterSchema = [
  {
    title: '状态',
    tag: 'NSelect',
    attrs: {
      placeholder: '请选择状态',
      options: [
        { label: '全部', value: '' },
        { label: '已发布', value: 'published' },
        { label: '草稿', value: 'draft' }
      ]
    }
  }
]

const actions = [
  {
    label: '新增文章',
    icon: 'i-tabler:plus',
    onClick: () => {
      console.log('新增文章')
    }
  }
]
</script>

<template>
  <DuxListPage
    path="/api/articles"
    :filter="filter"
    :filter-schema="filterSchema"
    :actions="actions"
    checkable
  >
    <template #default="{ item, isChecked, toggleChecked }">
      <div class="p-4 border rounded-lg flex items-center gap-4">
        <NCheckbox
          :checked="isChecked(item.id)"
          @update:checked="() => toggleChecked(item.id)"
        />

        <div class="flex-1">
          <div class="flex items-center gap-2 mb-2">
            <h3 class="text-lg font-medium">
              {{ item.title }}
            </h3>
            <NTag :type="item.status === 'published' ? 'success' : 'warning'">
              {{ item.status === 'published' ? '已发布' : '草稿' }}
            </NTag>
          </div>

          <p class="text-gray-600 mb-2">
            {{ item.description }}
          </p>

          <div class="text-sm text-gray-500">
            作者:{{ item.author }} | 发布时间:{{ item.publishedAt }}
          </div>
        </div>

        <div class="flex gap-2">
          <NButton size="small">
            编辑
          </NButton>
          <NButton size="small" type="primary">
            查看
          </NButton>
        </div>
      </div>
    </template>
  </DuxListPage>
</template>

订单列表示例

vue
<script setup>
import { DuxCard, DuxListPage, DuxMedia } from '@duxweb/dvha-pro'
import { NButton, NCheckbox, NTag } from 'naive-ui'
import { ref } from 'vue'

const filter = ref({})

const filterSchema = [
  {
    title: '订单状态',
    tag: 'NSelect',
    attrs: {
      placeholder: '请选择状态',
      options: [
        { label: '全部', value: '' },
        { label: '待付款', value: 'pending' },
        { label: '已付款', value: 'paid' },
        { label: '已发货', value: 'shipped' },
        { label: '已完成', value: 'completed' }
      ]
    }
  }
]
</script>

<template>
  <DuxListPage
    path="/api/orders"
    :filter="filter"
    :filter-schema="filterSchema"
    checkable
  >
    <template #default="{ item, isChecked, toggleChecked }">
      <DuxCard divide>
        <template #header>
          <div class="flex items-center justify-between">
            <div class="flex items-center gap-3">
              <NCheckbox
                :checked="isChecked(item.id)"
                @update:checked="() => toggleChecked(item.id)"
              />

              <div class="flex flex-col">
                <div class="flex gap-2 items-center">
                  <div class="text-base font-bold">
                    订单号:# {{ item.orderNo }}
                  </div>
                  <NTag type="success" size="small">
                    已付款
                  </NTag>
                </div>
                <div class="text-sm text-muted">
                  {{ item.products.length }} 个商品 | {{ item.customerName }} | {{ item.createdAt }}
                </div>
              </div>
            </div>

            <div class="flex gap-2">
              <NButton>下载账单</NButton>
              <NButton type="primary">
                查看详情
              </NButton>
            </div>
          </div>
        </template>

        <div class="grid gap-4" style="grid-template-columns: repeat(auto-fit, minmax(250px, 250px))">
          <DuxMedia
            v-for="product in item.products"
            :key="product.id"
            :title="product.title"
            :image="product.image"
            :image-width="60"
            :desc="[
              `数量:${product.quantity}x ${product.price}元`,
              `颜色:${product.color}`,
            ]"
          />
        </div>
      </DuxCard>
    </template>
  </DuxListPage>
</template>

DuxCardPage 卡片网格页面

卡片网格页面组件,基于 DuxListLayout 扩展,提供响应式网格布局,适用于商品展示、用户卡片、文章卡片等场景。

属性

DuxCardPage 在 DuxListLayout 基础上增加了以下属性:

属性名类型默认值说明
colWidthnumber320卡片最小宽度(px)
rowsnumber4基础行数
maxRowsnumber10最大行数
sideLeftTitlestring''左侧栏标题
sideRightTitlestring''右侧栏标题

其他属性继承自 DuxListLayout。

插槽

插槽名说明参数
default卡片内容{ item, isChecked, toggleChecked }
actions操作按钮区域-
tools工具栏扩展-
bottom底部扩展区域-
sideLeft左侧栏内容-
sideRight右侧栏内容-

基础用法

vue
<script setup>
import { DuxCardPage } from '@duxweb/dvha-pro'
import { NButton, NCard, NCheckbox, NTag } from 'naive-ui'
import { ref } from 'vue'

const filter = ref({})

const filterSchema = [
  {
    title: '分类',
    tag: 'NSelect',
    attrs: {
      placeholder: '请选择分类',
      options: [
        { label: '全部', value: '' },
        { label: '电子产品', value: 'electronics' },
        { label: '服装', value: 'clothing' },
        { label: '家居', value: 'home' }
      ]
    }
  }
]
</script>

<template>
  <DuxCardPage
    path="/api/products"
    :filter="filter"
    :filter-schema="filterSchema"
    :col-width="280"
    :rows="3"
    checkable
  >
    <template #default="{ item, isChecked, toggleChecked }">
      <NCard size="small" hoverable>
        <template #header>
          <div class="flex items-center justify-between">
            <NCheckbox
              :checked="isChecked(item.id)"
              @update:checked="() => toggleChecked(item.id)"
            />
            <NTag size="small">
              {{ item.category }}
            </NTag>
          </div>
        </template>

        <div class="space-y-3">
          <img
            :src="item.image"
            :alt="item.name"
            class="w-full h-40 object-cover rounded"
          >

          <div>
            <h3 class="font-medium truncate">
              {{ item.name }}
            </h3>
            <p class="text-sm text-gray-500 line-clamp-2">
              {{ item.description }}
            </p>
          </div>

          <div class="flex items-center justify-between">
            <span class="text-lg font-bold text-red-500">¥{{ item.price }}</span>
            <NButton size="small" type="primary">
              加入购物车
            </NButton>
          </div>
        </div>
      </NCard>
    </template>
  </DuxCardPage>
</template>

用户卡片示例

vue
<script setup>
import { DuxCard, DuxCardPage, DuxMedia } from '@duxweb/dvha-pro'
import { NButton, NCheckbox } from 'naive-ui'
import { ref } from 'vue'

const filter = ref({})

const filterSchema = [
  {
    title: '用户状态',
    tag: 'NSelect',
    attrs: {
      placeholder: '请选择状态',
      options: [
        { label: '全部', value: '' },
        { label: '活跃', value: 'active' },
        { label: '禁用', value: 'disabled' }
      ]
    }
  }
]
</script>

<template>
  <DuxCardPage
    path="/api/users"
    :filter="filter"
    :filter-schema="filterSchema"
    :col-width="300"
    checkable
  >
    <template #default="{ item, isChecked, toggleChecked }">
      <DuxCard size="none" class="relative group" divide bordered :shadow="false">
        <div class="absolute top-2 left-2 z-10 group-hover:block" :class="[isChecked(item.id) ? 'block' : 'hidden']">
          <NCheckbox :checked="isChecked(item.id)" @update:checked="() => toggleChecked(item.id)" />
        </div>

        <template #header>
          <div class="flex gap-2 justify-between items-center">
            <DuxMedia
              :image="item.avatar"
              :image-width="38"
              avatar
              :title="item.nickname"
              :desc="item.email"
            />
            <NButton circle quaternary>
              <template #icon>
                <i class="i-tabler:dots-vertical" />
              </template>
            </NButton>
          </div>
        </template>

        <div class="flex flex-col gap-1 text-muted">
          <div class="flex flex-row gap-2 justify-between">
            <div class="text-sm">
              注册时间
            </div>
            <div class="text-sm">
              {{ item.registeredAt }}
            </div>
          </div>
          <div class="flex flex-row gap-2 justify-between">
            <div class="text-sm">
              登录时间
            </div>
            <div class="text-sm">
              {{ item.lastLoginAt }}
            </div>
          </div>
        </div>

        <template #footer>
          <div class="grid grid-cols-2 gap-2">
            <NButton secondary>
              资料
            </NButton>
            <NButton secondary type="primary">
              日志
            </NButton>
          </div>
        </template>
      </DuxCard>
    </template>
  </DuxCardPage>
</template>

文章卡片示例

vue
<script setup>
import { DuxCard, DuxCardPage } from '@duxweb/dvha-pro'
import { NButton, NCheckbox, NDropdown, NTag, NTooltip } from 'naive-ui'
import { h, ref } from 'vue'

const filter = ref({})

const filterSchema = [
  {
    title: '文章状态',
    tag: 'NSelect',
    attrs: {
      placeholder: '请选择状态',
      options: [
        { label: '全部', value: '' },
        { label: '已发布', value: 'published' },
        { label: '草稿', value: 'draft' }
      ]
    }
  }
]

const dropdownOptions = [
  {
    label: '编辑',
    key: 'edit',
    icon: () => h('i', { class: 'i-tabler:edit' }),
  },
  {
    label: '删除',
    key: 'delete',
    icon: () => h('i', { class: 'i-tabler:trash' }),
  },
]
</script>

<template>
  <DuxCardPage
    path="/api/articles"
    :filter="filter"
    :filter-schema="filterSchema"
    :col-width="350"
    checkable
  >
    <template #default="{ item, isChecked, toggleChecked }">
      <DuxCard
        :key="item.id"
        size="none"
        class="relative group bg-elevated"
        bordered
        :shadow="false"
        divide
        header-size="none"
      >
        <div class="absolute top-2 left-2 z-10 group-hover:block" :class="[isChecked(item.id) ? 'block' : 'hidden']">
          <NCheckbox :checked="isChecked(item.id)" @update:checked="() => toggleChecked(item.id)" />
        </div>

        <template #header>
          <div class="h-50 rounded-t overflow-hidden">
            <img :src="item.coverImage" :alt="item.title" class="w-full h-full object-cover">
          </div>
        </template>

        <div class="flex flex-col gap-2">
          <div class="text-sm text-muted">
            {{ item.createdAt }} | {{ item.readTime }} 分钟阅读
          </div>
          <div class="text-base font-bold">
            {{ item.title }}
          </div>
          <div class="text-sm text-muted line-clamp-2">
            {{ item.description }}
          </div>
        </div>

        <template #footer>
          <div class="flex flex-row items-center justify-between gap-2">
            <div>
              <NTag :type="item.status === 'published' ? 'success' : 'warning'">
                {{ item.status === 'published' ? '发布' : '草稿' }}
              </NTag>
            </div>
            <div class="flex flex-row gap-2">
              <NTooltip>
                <template #trigger>
                  <NButton secondary size="small" circle>
                    <template #icon>
                      <i class="i-tabler:chart-bar" />
                    </template>
                  </NButton>
                </template>
                分析
              </NTooltip>
              <NTooltip>
                <template #trigger>
                  <NButton secondary type="primary" size="small" circle>
                    <template #icon>
                      <i class="i-tabler:eye" />
                    </template>
                  </NButton>
                </template>
                预览
              </NTooltip>
              <NDropdown
                trigger="click"
                :options="dropdownOptions"
              >
                <NButton secondary size="small" circle>
                  <template #icon>
                    <i class="i-tabler:dots-vertical" />
                  </template>
                </NButton>
              </NDropdown>
            </div>
          </div>
        </template>
      </DuxCard>
    </template>
  </DuxCardPage>
</template>

设计特性

响应式布局

两种列表组件都具有良好的响应式特性:

  • DuxListPage - 在小屏幕上自动调整布局,隐藏不重要的元素
  • DuxCardPage - 根据屏幕宽度自动调整列数,确保卡片始终保持合适的尺寸

数据管理

  • 内置分页功能,支持自定义分页配置
  • 支持筛选和搜索功能
  • 提供选择和批量操作能力
  • 自动处理加载状态和空状态

交互体验

  • 支持键盘导航和快捷键
  • 提供丰富的操作按钮和工具栏
  • 支持侧边栏扩展功能
  • 响应式设计,适配各种设备

最佳实践

选择合适的列表类型

  • 使用 DuxListPage 当需要展示详细信息,如订单列表、用户管理、文章管理等
  • 使用 DuxCardPage 当需要展示图片或卡片式内容,如商品展示、用户头像、相册等

性能优化

  • 合理设置分页大小,避免一次加载过多数据
  • 使用虚拟滚动处理大量数据(可配合第三方库)
  • 对图片进行懒加载处理

用户体验

  • 提供清晰的筛选和搜索功能
  • 保持一致的卡片尺寸和间距
  • 使用合适的加载状态指示器
  • 提供批量操作功能以提高效率

常见问题

Q: 如何自定义卡片的列数?

A: 通过设置 colWidth 属性来控制卡片的最小宽度,系统会根据容器宽度自动计算列数。

Q: 如何处理空数据状态?

A: 组件内置了空数据处理,会自动显示 DuxPageEmpty 组件。你也可以通过插槽自定义空状态。

Q: 如何实现无限滚动?

A: 可以监听滚动事件,当接近底部时调用 fetchNextPage 方法加载更多数据。

Q: 如何自定义筛选表单?

A: 通过 filterSchema 属性配置筛选表单,支持各种表单组件和验证规则。