表格 Table
这是一个表格,用于展示有行、列结构的内容。
WARNING
为确保行展开和选择行功能正常工作,需要这些功能时,请务必确保 data 数组中的每个数据项都包含与 rowKey(默认为 'key')参数值同名的字段,且该字段的值在所有数据中保持唯一。
如果需要进行后端分页,请参考 异步分页。
如果需要进行跨页全选,请参考 跨页全选。
基础使用
data 属性为表格当前数据,columns 属性设置表格列。
请为 columns 中的元素配置 key 属性作为行的唯一标识。
请为 data 中的元素配置 key 属性作为行的唯一标识,这个唯一标识字段可以通过 rowKey 控制。
Name | Age | Email |
|---|---|---|
Alice | 25 | alice@example.com |
Bob | 30 | bob@example.com |
Charlie | 35 | charlie@example.com |
<template>
<px-table :data="data" :columns="columns"></px-table>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const data = ref([
{ key: 1, name: 'Alice', age: 25, email: 'alice@example.com' },
{ key: 2, name: 'Bob', age: 30, email: 'bob@example.com' },
{ key: 3, name: 'Charlie', age: 35, email: 'charlie@example.com' }
])
const columns = [
{
key: 'name',
label: 'Name',
field: 'name'
},
{
key: 'age',
label: 'Age',
field: 'age'
},
{
key: 'email',
label: 'Email',
field: 'email'
}
]
</script>对齐方式
columns[].align 配置单元格文本的对齐方式,默认为 'left'。
Name | Age | Email |
|---|---|---|
Olivia | 28 | olivia@example.com |
James | 32 | james@example.com |
Sophia | 24 | sophia@example.com |
William | 29 | william@example.com |
Emma | 31 | emma@example.com |
<template>
<px-table :data="data" :columns="columns"></px-table>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const data = ref([
{ key: 1, name: 'Olivia', age: 28, email: 'olivia@example.com' },
{ key: 2, name: 'James', age: 32, email: 'james@example.com' },
{ key: 3, name: 'Sophia', age: 24, email: 'sophia@example.com' },
{ key: 4, name: 'William', age: 29, email: 'william@example.com' },
{ key: 5, name: 'Emma', age: 31, email: 'emma@example.com' }
])
const columns = [
{
key: 'name',
label: 'Name',
field: 'name',
align: 'left'
},
{
key: 'age',
label: 'Age',
field: 'age',
align: 'center'
},
{
key: 'email',
label: 'Email',
field: 'email',
align: 'right'
}
]
</script>自定义单元格
可以用以下方式自定义单元格:
columns[].slotName配置单元格内容插槽。columns[].labelSlotName配置表头单元格内容插槽。columns[].render配置单元格内容渲染函数。columns[].labelRender配置表头单元格内容渲染函数。columns[].cellProps配置单元格属性。columns[].labelCellProps配置表头单元格属性。columns[].contentProps配置单元格属性。columns[].labelContentProps配置表头单元格属性。
Name | Age | Email | Status Active / Inactive | Actions |
|---|---|---|---|---|
Emma Johnson | 32 | emma.johnson@example.com | active | |
James Miller | 45 | jmiller@test.net | inactive | |
Sophia Chen | 28 | schen@demo.org | active | |
William Brown | 61 | wbrown55@sample.co | inactive | |
Olivia Davis | 39 | odavis@mail.io | active |
<template>
<px-table :data="data" :columns="columns">
<template #status="{ record }">
<px-tag :theme="record.status === 'active' ? 'success' : 'danger'">{{
record.status
}}</px-tag>
</template>
<template #status-label>
Status
<px-tag style="margin-left: 8px" theme="success">Active</px-tag>
/
<px-tag theme="danger">Inactive</px-tag>
</template>
</px-table>
</template>
<script setup lang="ts">
import { Button } from '@pixelium/web-vue'
import { h, ref } from 'vue'
const data = ref([
{
key: 1,
name: 'Emma Johnson',
age: 32,
email: 'emma.johnson@example.com',
status: 'active'
},
{
key: 2,
name: 'James Miller',
age: 45,
email: 'jmiller@test.net',
status: 'inactive'
},
{
key: 3,
name: 'Sophia Chen',
age: 28,
email: 'schen@demo.org',
status: 'active'
},
{
key: 4,
name: 'William Brown',
age: 61,
email: 'wbrown55@sample.co',
status: 'inactive'
},
{
key: 5,
name: 'Olivia Davis',
age: 39,
email: 'odavis@mail.io',
status: 'active'
}
])
const columns = [
{
key: 'name',
label: 'Name',
field: 'name'
},
{
key: 'age',
label: 'Age',
field: 'age'
},
{
key: 'email',
label: 'Email',
field: 'email',
cellProps: {
style: {
backgroundColor: 'wheat',
color: 'var(--px-neutral-10)'
}
}
},
{
key: 'status',
labelSlotName: 'status-label',
slotName: 'status'
},
{
key: 'action',
render: () => {
return h(Button, { size: 'small' }, { default: () => 'Detail' })
},
labelRender: () => {
return 'Actions'
}
}
]
</script>列宽度
columns[].width 和 columns[].minWidth 配置单元格宽度。
设置 columns[].width 时,请至少留下一列不设置,以自适应表格实际宽度。
Name | Role | Email | Address |
|---|---|---|---|
Olivia Martinez | Admin | olivia.martinez@company.com | 123 Maple Street, New York, NY 10001 |
James Wilson | Developer | james.wilson@company.com | 456 Oak Avenue, San Francisco, CA 94102 |
Sophia Chen | Designer | sophia.chen@company.com | 789 Pine Road, Chicago, IL 60601 |
William Taylor | Manager | william.taylor@company.com | 321 Elm Boulevard, Austin, TX 73301 |
Emma Johnson | Analyst | emma.johnson@company.com | 654 Cedar Lane, Seattle, WA 98101 |
<template>
<px-table :data="data" :columns="columns"></px-table>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const data = ref([
{
key: 1,
name: 'Olivia Martinez',
role: 'Admin',
email: 'olivia.martinez@company.com',
address: '123 Maple Street, New York, NY 10001'
},
{
key: 2,
name: 'James Wilson',
role: 'Developer',
email: 'james.wilson@company.com',
address: '456 Oak Avenue, San Francisco, CA 94102'
},
{
key: 3,
name: 'Sophia Chen',
role: 'Designer',
email: 'sophia.chen@company.com',
address: '789 Pine Road, Chicago, IL 60601'
},
{
key: 4,
name: 'William Taylor',
role: 'Manager',
email: 'william.taylor@company.com',
address: '321 Elm Boulevard, Austin, TX 73301'
},
{
key: 5,
name: 'Emma Johnson',
role: 'Analyst',
email: 'emma.johnson@company.com',
address: '654 Cedar Lane, Seattle, WA 98101'
}
])
const columns = [
{
key: 'name',
label: 'Name',
field: 'name',
minWidth: 250
},
{
key: 'role',
label: 'Role',
field: 'role',
width: 120
},
{
key: 'email',
label: 'Email',
field: 'email',
minWidth: 300
},
{
key: 'address',
label: 'Address',
field: 'address',
minWidth: 300
}
]
</script>边框和底纹
bordered 设置表格边框,默认展示所有边框。variant 设置表格背景样式变体,默认纯色背景('normal')。
Name | Age | Email |
|---|---|---|
Alice | 25 | alice@example.com |
Bob | 30 | bob@example.com |
Charlie | 35 | charlie@example.com |
<template>
<px-space direction="vertical">
<div style="display: flex; align-items: center">
<px-checkbox v-model="threeLineTable" @change="threeLineTableCheckChangeHandler">
Three-Line Table
</px-checkbox>
</div>
<div style="display: flex; align-items: center">
Bordered:
<px-checkbox-group
style="margin-left: 16px"
v-model="bordered"
:options="borderedOptions"
@change="borderedCheckChangeHandler"
></px-checkbox-group>
</div>
<div style="display: flex; align-items: center">
Variant:
<px-radio-group
style="margin-left: 16px"
v-model="variant"
:options="variantOptions"
></px-radio-group>
</div>
<px-table
:data="data"
:columns="columns"
:variant="variant"
:bordered="borderedConfig"
></px-table>
</px-space>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
const data = ref([
{ key: 1, name: 'Alice', age: 25, email: 'alice@example.com' },
{ key: 2, name: 'Bob', age: 30, email: 'bob@example.com' },
{ key: 3, name: 'Charlie', age: 35, email: 'charlie@example.com' }
])
const columns = [
{
key: 'name',
label: 'Name',
field: 'name'
},
{
key: 'age',
label: 'Age',
field: 'age'
},
{
key: 'email',
label: 'Email',
field: 'email'
}
]
const borderedOptions = [
{ label: 'Table', value: 'table' },
{ label: 'Side', value: 'side' },
{ label: 'Head', value: 'head' },
{ label: 'Row', value: 'row' },
{ label: 'Col', value: 'col' }
]
const bordered = ref<string[]>(['table', 'head'])
const threeLineTable = ref(true)
const borderedCheckChangeHandler = (val: string[]) => {
if (val.length === 2 && val.includes('table') && val.includes('head')) {
threeLineTable.value = true
} else {
threeLineTable.value = false
}
}
const threeLineTableCheckChangeHandler = (val: boolean) => {
if (val) {
bordered.value = ['table', 'head']
}
}
const borderedConfig = computed(() => {
return {
table: bordered.value.includes('table'),
side: bordered.value.includes('side'),
head: bordered.value.includes('head'),
row: bordered.value.includes('row'),
col: bordered.value.includes('col')
}
})
const variantOptions = [
{ label: 'Normal', value: 'normal' },
{ label: 'Striped', value: 'striped' },
{ label: 'Checkered', value: 'checkered' }
]
const variant = ref<string>('normal')
</script>表格高度
由于 display: table 的特性,当为该 Table 组件最外层元素设置较小的 height 或者 max-height 时,会得到预期外的效果,<table> 的高度仍然会被子元素撑开。
我们推荐使用 tableAreaProps 属性,设置固定或者动态的表格高度,例如 :table-area-props="{ style: 'max-height: 400px' }"。
Name | Role | Level | Salary | Address |
|---|---|---|---|---|
Aric Ironfist | Blacksmith | 42 | 2800 | Iron Forge Street 12 |
Lyra Swiftarrow | Hunter | 36 | 2200 | Forest Edge Cottage |
Borin Stonebeard | Miner | 28 | 1800 | Mountain Hall 7 |
Elara Moonshadow | Alchemist | 51 | 3200 | Apothecary Lane 3 |
Garrick Stormblade | Knight | 65 | 4500 | Castle Barracks |
Mira Whisperwind | Bard | 24 | 1500 | Tavern Square 5 |
Thrain Forgeheart | Engineer | 47 | 2900 | Workshop District 9 |
Sylas Greenleaf | Druid | 58 | 3800 | Ancient Grove |
Kaelen Firehand | Pyromancer | 61 | 4200 | Mage Tower East |
Freya Shieldmaiden | Guard Captain | 54 | 3600 | Guard Post Central |
Jorin Goldfinder | Merchant | 33 | 2500 | Market Square 8 |
Nyssa Lightfoot | Rogue | 39 | 2700 | Shadow Alley 2 |
Torin Boulderbreaker | Stone Mason | 31 | 2100 | Quarry Road 4 |
Liana Starweaver | Astrologer | 45 | 3100 | Observatory Hill |
Finn Riverstride | Fisherman | 22 | 1400 | Dockside Hut |
Rowan Oakenshield | Carpenter | 27 | 1700 | Timber Yard 6 |
Vera Nightshade | Herbalist | 34 | 2300 | Herb Garden Cottage |
Draven Bloodedge | Mercenary | 49 | 3300 | Fighter's Guild |
Mara Swiftcurrent | Sailor | 26 | 1600 | Harbor Watch 3 |
Orin Deepdelver | Archaeologist | 43 | 3000 | Explorer's Lodge |
<template>
<px-table
:pagination="false"
:data="data"
:columns="columns"
row-key="name"
:table-area-props="{ style: 'max-height: 400px' }"
></px-table>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const data = ref([
{
name: 'Aric Ironfist',
role: 'Blacksmith',
level: 42,
salary: 2800,
address: 'Iron Forge Street 12'
},
{
name: 'Lyra Swiftarrow',
role: 'Hunter',
level: 36,
salary: 2200,
address: 'Forest Edge Cottage'
},
{
name: 'Borin Stonebeard',
role: 'Miner',
level: 28,
salary: 1800,
address: 'Mountain Hall 7'
},
{
name: 'Elara Moonshadow',
role: 'Alchemist',
level: 51,
salary: 3200,
address: 'Apothecary Lane 3'
},
{
name: 'Garrick Stormblade',
role: 'Knight',
level: 65,
salary: 4500,
address: 'Castle Barracks'
},
{
name: 'Mira Whisperwind',
role: 'Bard',
level: 24,
salary: 1500,
address: 'Tavern Square 5'
},
{
name: 'Thrain Forgeheart',
role: 'Engineer',
level: 47,
salary: 2900,
address: 'Workshop District 9'
},
{
name: 'Sylas Greenleaf',
role: 'Druid',
level: 58,
salary: 3800,
address: 'Ancient Grove'
},
{
name: 'Kaelen Firehand',
role: 'Pyromancer',
level: 61,
salary: 4200,
address: 'Mage Tower East'
},
{
name: 'Freya Shieldmaiden',
role: 'Guard Captain',
level: 54,
salary: 3600,
address: 'Guard Post Central'
},
{
name: 'Jorin Goldfinder',
role: 'Merchant',
level: 33,
salary: 2500,
address: 'Market Square 8'
},
{
name: 'Nyssa Lightfoot',
role: 'Rogue',
level: 39,
salary: 2700,
address: 'Shadow Alley 2'
},
{
name: 'Torin Boulderbreaker',
role: 'Stone Mason',
level: 31,
salary: 2100,
address: 'Quarry Road 4'
},
{
name: 'Liana Starweaver',
role: 'Astrologer',
level: 45,
salary: 3100,
address: 'Observatory Hill'
},
{
name: 'Finn Riverstride',
role: 'Fisherman',
level: 22,
salary: 1400,
address: 'Dockside Hut'
},
{
name: 'Rowan Oakenshield',
role: 'Carpenter',
level: 27,
salary: 1700,
address: 'Timber Yard 6'
},
{
name: 'Vera Nightshade',
role: 'Herbalist',
level: 34,
salary: 2300,
address: 'Herb Garden Cottage'
},
{
name: 'Draven Bloodedge',
role: 'Mercenary',
level: 49,
salary: 3300,
address: "Fighter's Guild"
},
{
name: 'Mara Swiftcurrent',
role: 'Sailor',
level: 26,
salary: 1600,
address: 'Harbor Watch 3'
},
{
name: 'Orin Deepdelver',
role: 'Archaeologist',
level: 43,
salary: 3000,
address: "Explorer's Lodge"
}
])
const columns = [
{
key: 'name',
label: 'Name',
field: 'name'
},
{
key: 'role',
label: 'Role',
field: 'role'
},
{
key: 'level',
label: 'Level',
field: 'level'
},
{
key: 'salary',
label: 'Salary',
field: 'salary'
},
{
label: 'Address',
field: 'address',
key: 'address'
}
]
</script>滚动区域
scroll.x 配置滚动区域宽度。Table 组件会在内部根据列配置计算出一个最小的滚动区域宽度,当表格实际宽度小于它和 scroll.x 的最大值时,就会展示横向滚动条。
Name | Age | Email |
|---|---|---|
Alice | 25 | alice@example.com |
Bob | 30 | bob@example.com |
Charlie | 35 | charlie@example.com |
<template>
<px-table :data="data" :columns="columns" :scroll="{ x: 1200 }"></px-table>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const data = ref([
{ key: 1, name: 'Alice', age: 25, email: 'alice@example.com' },
{ key: 2, name: 'Bob', age: 30, email: 'bob@example.com' },
{ key: 3, name: 'Charlie', age: 35, email: 'charlie@example.com' }
])
const columns = [
{
key: 'name',
label: 'Name',
field: 'name'
},
{
key: 'age',
label: 'Age',
field: 'age'
},
{
key: 'email',
label: 'Email',
field: 'email'
}
]
</script>合并单元格
通过 spanMethod 属性配置合并单元格。
BuildingType | Coord | Level | Income | Maintain |
|---|---|---|---|---|
Blacksmith | (23, 18) | 3 | 1800 | 450 |
(25, 20) | 4 | 2400 | 600 | |
Tavern | (34, 27) | 2 | 1600 | 400 |
(36, 25) | 5 | 3200 | 800 | |
Market | (41, 33) | 3 | 2100 | 525 |
(43, 35) | 6 | 4000 | 1000 | |
Farm | (15, 12) | 2 | 900 | 225 |
(18, 14) | 4 | 2000 | 500 | |
Barracks | (9, 39) | 3 | 1500 | 375 |
(11, 37) | 5 | 2800 | 700 |
<template>
<px-table :data="data" :columns="columns" :spanMethod="spanMethod"></px-table>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { TableOptionsArg } from '@pixelium/web-vue'
// If on-demand import
// import type { TableOptionsArg } from '@pixelium/web-vue/es'
const data = ref([
{
buildingType: 'Blacksmith',
coord: '(23, 18)',
level: 3,
income: 1800,
maintain: 450
},
{
buildingType: 'Blacksmith',
coord: '(25, 20)',
level: 4,
income: 2400,
maintain: 600
},
{
buildingType: 'Tavern',
coord: '(34, 27)',
level: 2,
income: 1600,
maintain: 400
},
{
buildingType: 'Tavern',
coord: '(36, 25)',
level: 5,
income: 3200,
maintain: 800
},
{
buildingType: 'Market',
coord: '(41, 33)',
level: 3,
income: 2100,
maintain: 525
},
{
buildingType: 'Market',
coord: '(43, 35)',
level: 6,
income: 4000,
maintain: 1000
},
{
buildingType: 'Farm',
coord: '(15, 12)',
level: 2,
income: 900,
maintain: 225
},
{
buildingType: 'Farm',
coord: '(18, 14)',
level: 4,
income: 2000,
maintain: 500
},
{
buildingType: 'Barracks',
coord: '(9, 39)',
level: 3,
income: 1500,
maintain: 375
},
{
buildingType: 'Barracks',
coord: '(11, 37)',
level: 5,
income: 2800,
maintain: 700
},
{
buildingType: 'Town Hall',
coord: '(12, 45)',
level: 5
}
])
const columns = [
{
key: 'buildingType',
label: 'BuildingType',
field: 'buildingType'
},
{
key: 'coord',
label: 'Coord',
field: 'coord'
},
{
key: 'level',
label: 'Level',
field: 'level'
},
{
key: 'income',
label: 'Income',
field: 'income'
},
{
label: 'Maintain',
field: 'maintain',
key: 'maintain'
}
]
const spanMethod = ({ rowIndex, colIndex, record, column }: TableOptionsArg) => {
const curBuildingType = record.buildingType
if (colIndex === 0) {
if (data.value[rowIndex - 1]?.buildingType === curBuildingType) {
return
}
let rowspan = 0
for (let i = rowIndex; i < data.value.length; i++) {
if (data.value[i].buildingType !== curBuildingType) {
break
}
rowspan++
}
return {
rowspan
}
}
if (curBuildingType === 'Town Hall' && colIndex === 2) {
return { colspan: 3 }
}
}
</script>固定表头和固定列
Table 默认固定表头,可通过 fixedHead 属性配置。
在 columns 属性的子元素中设置 fixed: 'left' 或者 fixed: 'right' 进行固定列。
WARNING
固定的列在展示时,如果位于表格中间,会移动到相应的固定的两侧。
固定列配置在多级表头的情况下,只对根节点有效。
Id | Name | Age | Phone | Email | Address |
|---|---|---|---|---|---|
1 | John Smith | 28 | +1-555-101-2001 | john.smith@example.com | 123 Main St, Springfield, IL 62704 |
2 | Jane Johnson | 34 | +1-555-102-2002 | jane.johnson@example.com | 456 Oak Ave, Shelbyville, IL 62706 |
3 | Bob Williams | 22 | +1-555-103-2003 | bob.williams@example.com | 789 Pine Rd, Capital City, IL 62708 |
4 | Alice Brown | 45 | +1-555-104-2004 | alice.brown@example.com | 101 Maple Dr, Metropolis, IL 62710 |
5 | Charlie Jones | 31 | +1-555-105-2005 | charlie.jones@example.com | 202 Cedar Ln, Smallville, IL 62712 |
6 | Diana Garcia | 29 | +1-555-106-2006 | diana.garcia@example.com | 303 Birch St, Gotham, NY 10001 |
7 | Eve Miller | 38 | +1-555-107-2007 | eve.miller@example.com | 404 Elm Pl, Star City, NY 10003 |
8 | Frank Davis | 26 | +1-555-108-2008 | frank.davis@example.com | 505 Walnut Way, Central City, NY 10005 |
9 | Grace Rodriguez | 41 | +1-555-109-2009 | grace.rodriguez@example.com | 606 Spruce Ct, Coast City, CA 90210 |
10 | Henry Martinez | 33 | +1-555-110-2010 | henry.martinez@example.com | 707 Ash Blvd, Emerald City, CA 90212 |
11 | Ivy Hernandez | 27 | +1-555-111-2011 | ivy.hernandez@example.com | 808 Poplar Ave, Keystone City, CA 90214 |
12 | Jack Lopez | 36 | +1-555-112-2012 | jack.lopez@example.com | 909 Fir St, National City, TX 75001 |
13 | Kate Gonzalez | 24 | +1-555-113-2013 | kate.gonzalez@example.com | 111 Redwood Dr, Opal City, TX 75003 |
14 | Leo Wilson | 39 | +1-555-114-2014 | leo.wilson@example.com | 222 Sequoia Ln, Ivy Town, TX 75005 |
15 | Mia Anderson | 30 | +1-555-115-2015 | mia.anderson@example.com | 333 Magnolia Rd, Hub City, FL 32003 |
16 | Nick Thomas | 42 | +1-555-116-2016 | nick.thomas@example.com | 444 Willow Cir, Blue Valley, FL 32005 |
17 | Olivia Taylor | 35 | +1-555-117-2017 | olivia.taylor@example.com | 555 Cypress St, Starling City, FL 32007 |
18 | Paul Moore | 23 | +1-555-118-2018 | paul.moore@example.com | 666 Juniper Way, Fawcett City, WA 98001 |
19 | Quinn Jackson | 37 | +1-555-119-2019 | quinn.jackson@example.com | 777 Sycamore Pl, Gateway City, WA 98003 |
20 | Ryan Martin | 32 | +1-555-120-2020 | ryan.martin@example.com | 888 Laurel Ave, Coral City, WA 98005 |
<template>
<px-table
:data="data"
:columns="columns"
row-key="id"
:table-area-props="{ style: 'max-height: 400px' }"
:scroll="{ x: 1200 }"
:pagination="false"
></px-table>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const data = ref([
{
id: 1,
name: 'John Smith',
age: 28,
phone: '+1-555-101-2001',
email: 'john.smith@example.com',
address: '123 Main St, Springfield, IL 62704'
},
{
id: 2,
name: 'Jane Johnson',
age: 34,
phone: '+1-555-102-2002',
email: 'jane.johnson@example.com',
address: '456 Oak Ave, Shelbyville, IL 62706'
},
{
id: 3,
name: 'Bob Williams',
age: 22,
phone: '+1-555-103-2003',
email: 'bob.williams@example.com',
address: '789 Pine Rd, Capital City, IL 62708'
},
{
id: 4,
name: 'Alice Brown',
age: 45,
phone: '+1-555-104-2004',
email: 'alice.brown@example.com',
address: '101 Maple Dr, Metropolis, IL 62710'
},
{
id: 5,
name: 'Charlie Jones',
age: 31,
phone: '+1-555-105-2005',
email: 'charlie.jones@example.com',
address: '202 Cedar Ln, Smallville, IL 62712'
},
{
id: 6,
name: 'Diana Garcia',
age: 29,
phone: '+1-555-106-2006',
email: 'diana.garcia@example.com',
address: '303 Birch St, Gotham, NY 10001'
},
{
id: 7,
name: 'Eve Miller',
age: 38,
phone: '+1-555-107-2007',
email: 'eve.miller@example.com',
address: '404 Elm Pl, Star City, NY 10003'
},
{
id: 8,
name: 'Frank Davis',
age: 26,
phone: '+1-555-108-2008',
email: 'frank.davis@example.com',
address: '505 Walnut Way, Central City, NY 10005'
},
{
id: 9,
name: 'Grace Rodriguez',
age: 41,
phone: '+1-555-109-2009',
email: 'grace.rodriguez@example.com',
address: '606 Spruce Ct, Coast City, CA 90210'
},
{
id: 10,
name: 'Henry Martinez',
age: 33,
phone: '+1-555-110-2010',
email: 'henry.martinez@example.com',
address: '707 Ash Blvd, Emerald City, CA 90212'
},
{
id: 11,
name: 'Ivy Hernandez',
age: 27,
phone: '+1-555-111-2011',
email: 'ivy.hernandez@example.com',
address: '808 Poplar Ave, Keystone City, CA 90214'
},
{
id: 12,
name: 'Jack Lopez',
age: 36,
phone: '+1-555-112-2012',
email: 'jack.lopez@example.com',
address: '909 Fir St, National City, TX 75001'
},
{
id: 13,
name: 'Kate Gonzalez',
age: 24,
phone: '+1-555-113-2013',
email: 'kate.gonzalez@example.com',
address: '111 Redwood Dr, Opal City, TX 75003'
},
{
id: 14,
name: 'Leo Wilson',
age: 39,
phone: '+1-555-114-2014',
email: 'leo.wilson@example.com',
address: '222 Sequoia Ln, Ivy Town, TX 75005'
},
{
id: 15,
name: 'Mia Anderson',
age: 30,
phone: '+1-555-115-2015',
email: 'mia.anderson@example.com',
address: '333 Magnolia Rd, Hub City, FL 32003'
},
{
id: 16,
name: 'Nick Thomas',
age: 42,
phone: '+1-555-116-2016',
email: 'nick.thomas@example.com',
address: '444 Willow Cir, Blue Valley, FL 32005'
},
{
id: 17,
name: 'Olivia Taylor',
age: 35,
phone: '+1-555-117-2017',
email: 'olivia.taylor@example.com',
address: '555 Cypress St, Starling City, FL 32007'
},
{
id: 18,
name: 'Paul Moore',
age: 23,
phone: '+1-555-118-2018',
email: 'paul.moore@example.com',
address: '666 Juniper Way, Fawcett City, WA 98001'
},
{
id: 19,
name: 'Quinn Jackson',
age: 37,
phone: '+1-555-119-2019',
email: 'quinn.jackson@example.com',
address: '777 Sycamore Pl, Gateway City, WA 98003'
},
{
id: 20,
name: 'Ryan Martin',
age: 32,
phone: '+1-555-120-2020',
email: 'ryan.martin@example.com',
address: '888 Laurel Ave, Coral City, WA 98005'
}
])
const columns = [
{
key: 'id',
label: 'Id',
field: 'id',
fixed: 'left'
},
{
key: 'name',
label: 'Name',
field: 'name'
},
{
key: 'age',
label: 'Age',
field: 'age'
},
{
key: 'phone',
label: 'Phone',
field: 'phone'
},
{
key: 'email',
label: 'Email',
field: 'email'
},
{
label: 'Address',
field: 'address',
key: 'address',
fixed: 'right',
width: 300
}
]
</script>多级表头
在 columns 属性的子元素中设置 children 开启多级表头。
多级表头情况下,表头区域会展示单元格边框。
Basic Information | Contact Information | Work Information | Status | ||||||
|---|---|---|---|---|---|---|---|---|---|
ID | Name | Contact Details | Address | Department | Salary Information | ||||
Phone | Email | Base Salary | Bonus | Total | |||||
1 | John Smith | +1 (212) 555-0198 | john.smith@example.com | 123 Main St, New York, NY | Sales | 5000 | 1000 | 6000 | Active |
2 | Emily Johnson | +44 20 7946 0958 | emily.j@example.com | 456 Park Ave, Los Angeles, CA | Marketing | 4500 | 800 | 5300 | Active |
3 | Michael Brown | +61 2 9876 5432 | m.brown@example.com | 789 Broadway, Chicago, IL | Engineering | 7000 | 1500 | 8500 | Active |
4 | Sarah Davis | +49 30 12345678 | sarah.davis@example.com | 101 Oak St, Houston, TX | HR | 4800 | 600 | 5400 | On Leave |
5 | David Wilson | +33 1 2345 6789 | d.wilson@example.com | 202 Pine Rd, Phoenix, AZ | Finance | 6500 | 1200 | 7700 | Active |
6 | Jennifer Taylor | +81 3 1234 5678 | jennifer.t@example.com | 303 Elm St, Philadelphia, PA | Sales | 5200 | 900 | 6100 | Active |
7 | Robert Miller | +1 (415) 555-0134 | robert.m@example.com | 404 Cedar Ave, San Antonio, TX | Engineering | 7200 | 1600 | 8800 | Active |
8 | Lisa Anderson | +44 131 496 0600 | lisa.a@example.com | 505 Maple Dr, San Diego, CA | Marketing | 4700 | 700 | 5400 | Inactive |
9 | William Thomas | +61 8 8123 4567 | william.t@example.com | 606 Birch Ln, Dallas, TX | Finance | 6800 | 1300 | 8100 | Active |
10 | Maria Jackson | +49 89 98765432 | maria.j@example.com | 707 Walnut St, San Jose, CA | HR | 4900 | 650 | 5550 | Active |
<template>
<px-table
:data="data"
:columns="columns"
:scroll="{ x: 1500 }"
:table-area-props="{ style: 'max-height: 400px' }"
></px-table>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const columns = [
{
label: 'Basic Information',
align: 'center',
key: 'base-info',
children: [
{
label: 'ID',
field: 'id',
width: 80,
align: 'center',
key: 'id'
},
{
label: 'Name',
field: 'name',
width: 120,
key: 'name'
}
]
},
{
label: 'Contact Information',
key: 'contactInformation',
children: [
{
label: 'Contact Details',
key: 'contactDetails',
children: [
{
key: 'phone',
label: 'Phone',
field: 'phone',
minWidth: 120
},
{
key: 'email',
label: 'Email',
field: 'email',
minWidth: 180
}
]
},
{
label: 'Address',
field: 'address',
key: 'address',
minWidth: 200
}
]
},
{
label: 'Work Information',
key: 'workInformation',
children: [
{
label: 'Department',
field: 'department',
key: 'department',
width: 120
},
{
label: 'Salary Information',
key: 'salaryInformation',
children: [
{
label: 'Base Salary',
field: 'baseSalary',
key: 'baseSalary',
width: 100,
align: 'right'
},
{
label: 'Bonus',
field: 'bonus',
key: 'bonus',
width: 100,
align: 'right'
},
{
label: 'Total',
field: 'totalSalary',
key: 'totalSalary',
width: 100,
align: 'right'
}
]
}
]
},
{
label: 'Status',
field: 'status',
width: 80,
fixed: 'right',
key: 'status',
align: 'center'
}
]
const data = ref([
{
id: 1,
name: 'John Smith',
phone: '+1 (212) 555-0198',
email: 'john.smith@example.com',
address: '123 Main St, New York, NY',
department: 'Sales',
baseSalary: 5000,
bonus: 1000,
totalSalary: 6000,
status: 'Active'
},
{
id: 2,
name: 'Emily Johnson',
phone: '+44 20 7946 0958',
email: 'emily.j@example.com',
address: '456 Park Ave, Los Angeles, CA',
department: 'Marketing',
baseSalary: 4500,
bonus: 800,
totalSalary: 5300,
status: 'Active'
},
{
id: 3,
name: 'Michael Brown',
phone: '+61 2 9876 5432',
email: 'm.brown@example.com',
address: '789 Broadway, Chicago, IL',
department: 'Engineering',
baseSalary: 7000,
bonus: 1500,
totalSalary: 8500,
status: 'Active'
},
{
id: 4,
name: 'Sarah Davis',
phone: '+49 30 12345678',
email: 'sarah.davis@example.com',
address: '101 Oak St, Houston, TX',
department: 'HR',
baseSalary: 4800,
bonus: 600,
totalSalary: 5400,
status: 'On Leave'
},
{
id: 5,
name: 'David Wilson',
phone: '+33 1 2345 6789',
email: 'd.wilson@example.com',
address: '202 Pine Rd, Phoenix, AZ',
department: 'Finance',
baseSalary: 6500,
bonus: 1200,
totalSalary: 7700,
status: 'Active'
},
{
id: 6,
name: 'Jennifer Taylor',
phone: '+81 3 1234 5678',
email: 'jennifer.t@example.com',
address: '303 Elm St, Philadelphia, PA',
department: 'Sales',
baseSalary: 5200,
bonus: 900,
totalSalary: 6100,
status: 'Active'
},
{
id: 7,
name: 'Robert Miller',
phone: '+1 (415) 555-0134',
email: 'robert.m@example.com',
address: '404 Cedar Ave, San Antonio, TX',
department: 'Engineering',
baseSalary: 7200,
bonus: 1600,
totalSalary: 8800,
status: 'Active'
},
{
id: 8,
name: 'Lisa Anderson',
phone: '+44 131 496 0600',
email: 'lisa.a@example.com',
address: '505 Maple Dr, San Diego, CA',
department: 'Marketing',
baseSalary: 4700,
bonus: 700,
totalSalary: 5400,
status: 'Inactive'
},
{
id: 9,
name: 'William Thomas',
phone: '+61 8 8123 4567',
email: 'william.t@example.com',
address: '606 Birch Ln, Dallas, TX',
department: 'Finance',
baseSalary: 6800,
bonus: 1300,
totalSalary: 8100,
status: 'Active'
},
{
id: 10,
name: 'Maria Jackson',
phone: '+49 89 98765432',
email: 'maria.j@example.com',
address: '707 Walnut St, San Jose, CA',
department: 'HR',
baseSalary: 4900,
bonus: 650,
totalSalary: 5550,
status: 'Active'
}
])
</script>行选择
通过 selection 配置行选择。selectedKey 控制选择项(受控模式),不传或为 undefined 时,为非受控模式,可通过 defaultSelectedKey 配置默认值。
设置 columns[].disabled 禁用该行的选择器。
当存在左侧固定的列时,行选择列也会固定在左侧。
ID | Name | Email | City | Address | |
|---|---|---|---|---|---|
1001 | Emma Johnson | emma.johnson@example.com | New York | 123 Main Street, Apt 4B | |
1002 | James Wilson | j.wilson@example.com | Los Angeles | 456 Oak Avenue | |
1003 | Sophia Chen | sophia.chen@example.com | Chicago | 789 Pine Road, Suite 12 | |
1004 | Michael Brown | m.brown@example.com | Miami | 321 Beach Boulevard | |
1005 | Olivia Davis | olivia.davis@example.com | Seattle | 654 Lakeview Drive |
<template>
<div>
<px-switch
style="margin-bottom: 16px"
active-label="Multiple"
v-model="selection.multiple"
></px-switch>
<px-table
:data="data"
:columns="columns"
row-key="id"
:selection="selection"
:scroll="{ x: 1500 }"
v-model:selected-keys="selectedKeys"
></px-table>
<div style="margin-top: 16px">selectedKeys: {{ selectedKeys }}</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const data = ref([
{
id: 1001,
name: 'Emma Johnson',
email: 'emma.johnson@example.com',
city: 'New York',
address: '123 Main Street, Apt 4B'
},
{
id: 1002,
name: 'James Wilson',
email: 'j.wilson@example.com',
city: 'Los Angeles',
address: '456 Oak Avenue',
disabled: true
},
{
id: 1003,
name: 'Sophia Chen',
email: 'sophia.chen@example.com',
city: 'Chicago',
address: '789 Pine Road, Suite 12'
},
{
id: 1004,
name: 'Michael Brown',
email: 'm.brown@example.com',
city: 'Miami',
address: '321 Beach Boulevard'
},
{
id: 1005,
name: 'Olivia Davis',
email: 'olivia.davis@example.com',
city: 'Seattle',
address: '654 Lakeview Drive'
}
])
const columns = [
{
key: 'id',
label: 'ID',
field: 'id',
fixed: 'left'
},
{
key: 'name',
label: 'Name',
field: 'name'
},
{
key: 'email',
label: 'Email',
field: 'email'
},
{
key: 'city',
label: 'City',
field: 'city'
},
{
key: 'address',
label: 'Address',
field: 'address'
}
]
const selection = ref({
multiple: false,
showSelectAll: true
})
const selectedKeys = ref<number[]>([])
</script>展开行
通过 expandable 配置行选择。expandedKey 控制展开的行(受控模式),不传或为 undefined 时,为非受控模式,可通过 defaultExpandedKey 配置默认值。
展开行内容,通过 columns[].expand 或者 expand 插槽设置,为空时(设置了 expand 插槽情况下则为 false)不展示展开按钮。
当存在左侧固定的列时,展开按钮列也会固定在左侧。
ID | Name | |
|---|---|---|
1001 | Emma Johnson | |
1002 | James Wilson | |
1003 | Sophia Chen | |
1004 | Michael Brown | |
1005 | Olivia Davis |
ID | Name | |
|---|---|---|
1001 | Emma Johnson | |
1002 | James Wilson | |
1003 | Sophia Chen | |
1004 | Michael Brown | |
1005 | Olivia Davis |
<template>
<div>
<px-table
:data="data0"
:columns="columns"
row-key="id"
expandable
v-model:expanded-keys="expandedKeys"
></px-table>
<px-table
style="margin-top: 16px"
:data="data1"
:columns="columns"
row-key="id"
expandable
v-model:expanded-keys="expandedKeys"
>
<template #expand="{ record }">
<div>
<div v-for="key in keys" style="display: flex; align-items: center; gap: 8px">
<div style="color: gray; margin-right: 16px">{{ key }}:</div>
<div>{{ record[key] }}</div>
</div>
</div>
</template>
</px-table>
<div style="margin-top: 16px">expandedKeys: {{ expandedKeys }}</div>
</div>
</template>
<script setup lang="ts">
import type { TableData } from '@pixelium/web-vue'
// When On-demand Import
// import type { TableData } from '@pixelium/web-vue/es'
import { h, ref } from 'vue'
const keys = ['email', 'city', 'address']
const expandRender = ({ record }: { record: TableData }) => {
return h(
'div',
{},
keys.map((e) => {
return h('div', { style: 'display: flex; align-items: center; gap: 8px' }, [
h('div', { style: 'color: gray; margin-right: 16px' }, e + ': '),
h('div', {}, record[e])
])
})
)
}
const data0 = ref([
{
id: 1001,
name: 'Emma Johnson',
email: 'emma.johnson@example.com',
city: 'New York',
address: '123 Main Street, Apt 4B',
expand: expandRender
},
{
id: 1002,
name: 'James Wilson',
email: 'j.wilson@example.com',
city: 'Los Angeles',
address: '456 Oak Avenue',
expand: expandRender
},
{
id: 1003,
name: 'Sophia Chen',
email: 'sophia.chen@example.com',
city: 'Chicago',
address: '789 Pine Road, Suite 12'
},
{
id: 1004,
name: 'Michael Brown',
email: 'm.brown@example.com',
city: 'Miami',
address: '321 Beach Boulevard',
expand: expandRender
},
{
id: 1005,
name: 'Olivia Davis',
email: 'olivia.davis@example.com',
city: 'Seattle',
address: '654 Lakeview Drive',
expand: expandRender
}
])
const data1 = ref([
{
id: 1001,
name: 'Emma Johnson',
email: 'emma.johnson@example.com',
city: 'New York',
address: '123 Main Street, Apt 4B'
},
{
id: 1002,
name: 'James Wilson',
email: 'j.wilson@example.com',
city: 'Los Angeles',
address: '456 Oak Avenue',
expand: false
},
{
id: 1003,
name: 'Sophia Chen',
email: 'sophia.chen@example.com',
city: 'Chicago',
address: '789 Pine Road, Suite 12'
},
{
id: 1004,
name: 'Michael Brown',
email: 'm.brown@example.com',
city: 'Miami',
address: '321 Beach Boulevard'
},
{
id: 1005,
name: 'Olivia Davis',
email: 'olivia.davis@example.com',
city: 'Seattle',
address: '654 Lakeview Drive'
}
])
const columns = [
{
key: 'id',
label: 'ID',
field: 'id',
fixed: 'left'
},
{
key: 'name',
label: 'Name',
field: 'name'
}
]
const expandedKeys = ref<number[]>([])
</script>排序
通过 columns[].sortable 配置排序。sortOrder 控制每列排序状态(受控模式),不传或为 undefined 时,为非受控模式,可通过 defaultSortOrder 配置默认值。
columns[].sortable 中的 defaultSortOrder 属性也可以配置该列的默认值。
其中,sortMethod 属性设置为 'custom' 时,没有排序效果。可以通过监听 sortOrderChange 事件进行后端排序。
不配置 sortMethod 属性时将使用默认的比较器,基于 JS 原生的大于小于运算逻辑。
Name | Age | Email |
|---|---|---|
Emma | 31 | emma@example.com |
James | 32 | james@example.com |
Olivia | 28 | olivia@example.com |
Sophia | 24 | sophia@example.com |
William | 29 | william@example.com |
<template>
<px-table :data="data" :columns="columns" v-model:sort-order="sortOrder"></px-table>
<div style="margin-top: 16px">sortOrder: {{ sortOrder }}</div>
</template>
<script setup lang="ts">
import type { SortOrder, TableData } from '@pixelium/web-vue'
// If On-demand Import
// import type { SortOrder, TableData } from '@pixelium/web-vue/es'
import { ref } from 'vue'
const data = ref([
{ key: 1, name: 'Olivia', age: 28, email: 'olivia@example.com' },
{ key: 2, name: 'James', age: 32, email: 'james@example.com' },
{ key: 3, name: 'Sophia', age: 24, email: 'sophia@example.com' },
{ key: 4, name: 'William', age: 29, email: 'william@example.com' },
{ key: 5, name: 'Emma', age: 31, email: 'emma@example.com' }
])
const columns = [
{
key: 'name',
label: 'Name',
field: 'name',
sortable: {
orders: ['asc', 'desc'] as const,
sortMethod: (a: TableData, b: TableData, order: 'asc' | 'desc', field?: string) => {
const res = a.name.length - b.name.length
return order === 'desc' ? -res : res
},
defaultSortOrder: 'asc' as const
}
},
{
key: 'age',
label: 'Age',
field: 'age',
sortable: {
orders: ['asc'] as const
}
},
{
key: 'email',
label: 'Email',
field: 'email',
sortable: {
orders: ['asc', 'desc'] as const
}
}
]
const sortOrder = ref<SortOrder>({})
</script>多级排序
columns[].sortable 的 sortMethod 设置属性 multiple 为 true,开启多级排序,此时可以通过 priority 属性设置列的优先级,数值越大越优先。
WARNING
多级排序和单级排序是互斥的,触发其中一种排序方式的时候,会清空另一种排序方式的选择。
Name | Age | Email |
|---|---|---|
Emma | 31 | emma@example.com |
James | 32 | james@example.com |
Olivia | 28 | olivia@example.com |
Sophia | 24 | sophia@example.com |
William | 29 | william@example.com |
<template>
<px-table :data="data" :columns="columns" v-model:sort-order="sortOrder"></px-table>
<div style="margin-top: 16px">sortOrder: {{ sortOrder }}</div>
</template>
<script setup lang="ts">
import type { SortOrder, TableData } from '@pixelium/web-vue'
// If On-demand Import
// import type { SortOrder, TableData } from '@pixelium/web-vue/es'
import { ref } from 'vue'
const data = ref([
{ key: 1, name: 'Olivia', age: 28, email: 'olivia@example.com' },
{ key: 2, name: 'James', age: 32, email: 'james@example.com' },
{ key: 3, name: 'Sophia', age: 24, email: 'sophia@example.com' },
{ key: 4, name: 'William', age: 29, email: 'william@example.com' },
{ key: 5, name: 'Emma', age: 31, email: 'emma@example.com' }
])
const columns = [
{
key: 'name',
label: 'Name',
field: 'name',
sortable: {
orders: ['asc', 'desc'] as const,
sortMethod: (a: TableData, b: TableData, order: 'asc' | 'desc', field?: string) => {
const res = a.name.length - b.name.length
return order === 'desc' ? -res : res
},
defaultSortOrder: 'asc' as const,
multiple: true,
priority: 2
}
},
{
key: 'age',
label: 'Age',
field: 'age',
sortable: {
orders: ['asc'] as const,
multiple: true
}
},
{
key: 'email',
label: 'Email',
field: 'email',
sortable: {
orders: ['asc', 'desc'] as const,
multiple: true,
priority: 1
}
}
]
const sortOrder = ref<SortOrder>({})
</script>筛选
columns[].filterable 配置筛选。filterValue 控制每列排序状态(受控模式),不传或为 undefined 时,为非受控模式,可通过 defaultFilterValue 配置默认值。
columns[].filterable 中的 defaultFilterValue 属性也可以配置该列的默认值。
不配置 filterMethod 属性时将使用默认的比较器,基于 JS 原生的 === 运算逻辑。
Name | Base Salary | Bonus | Total | Status |
|---|---|---|---|---|
James Wilson | 8500 | 420 | 8920 | active |
Michael Brown | 11200 | 150 | 11350 | active |
Robert Taylor | 9800 | 920 | 10820 | active |
Jennifer Miller | 6300 | 310 | 6610 | active |
Lisa Martinez | 4500 | 230 | 4730 | active |
Amanda Clark | 2900 | 0 | 2900 | active |
<template>
<px-table :data="data" :columns="columns" v-model:filter-value="filterValue"></px-table>
<div style="margin-top: 16px">filterValue: {{ filterValue }}</div>
</template>
<script setup lang="ts">
import type { FilterValue, TableData } from '@pixelium/web-vue'
// If On-demand Import
// import type { FilterValue, TableData } from '@pixelium/web-vue/es'
import { ref } from 'vue'
const columns = [
{
label: 'Name',
field: 'name',
key: 'name'
},
{
label: 'Base Salary',
field: 'baseSalary',
key: 'baseSalary',
filterable: {
filterOptions: [{ label: '> 7000', value: 7000 }],
filterMethod: (value: number[], record: TableData, field?: string) => {
return value[0] ? record.baseSalary > value[0] : true
}
}
},
{
label: 'Bonus',
field: 'bonus',
key: 'bonus',
filterable: {
filterOptions: [{ label: '> 500', value: 500 }],
filterMethod: (value: number[], record: TableData, field?: string) => {
return value[0] ? record.bonus > value[0] : true
}
}
},
{
label: 'Total',
field: 'total',
key: 'total',
filterable: {
filterOptions: [{ label: '> 10000', value: 500 }],
filterMethod: (value: number[], record: TableData, field?: string) => {
return value[0] ? record.total > value[0] : true
}
}
},
{
label: 'Status',
field: 'status',
key: 'status',
filterable: {
multiple: true,
filterOptions: [
{ label: 'Active', value: 'active' },
{ label: 'Inactive', value: 'inactive' }
],
defaultFilterValue: ['active']
}
}
]
const data = ref([
{
name: 'James Wilson',
baseSalary: 8500,
bonus: 420,
total: 8920,
status: 'active'
},
{
name: 'Sarah Johnson',
baseSalary: 5200,
bonus: 780,
total: 5900,
status: 'inactive'
},
{
name: 'Michael Brown',
baseSalary: 11200,
bonus: 150,
total: 11350,
status: 'active'
},
{
name: 'Emily Davis',
baseSalary: 3400,
bonus: 0,
total: 3400,
status: 'inactive'
},
{
name: 'Robert Taylor',
baseSalary: 9800,
bonus: 920,
total: 10820,
status: 'active'
},
{
name: 'Jennifer Miller',
baseSalary: 6300,
bonus: 310,
total: 6610,
status: 'active'
},
{
name: 'David Anderson',
baseSalary: 7600,
bonus: 650,
total: 8250,
status: 'inactive'
},
{
name: 'Lisa Martinez',
baseSalary: 4500,
bonus: 230,
total: 4730,
status: 'active'
},
{
name: 'William Thomas',
baseSalary: 10500,
bonus: 870,
total: 11370,
status: 'inactive'
},
{
name: 'Amanda Clark',
baseSalary: 2900,
bonus: 0,
total: 2900,
status: 'active'
}
])
const filterValue = ref<FilterValue>({})
</script>总结行
summary 属性配置总结行。
summary.data设置总结行内容。summary.summaryText可以设置总结行第一列的文本。summary.placement用于调整总结行的位置,可选'start'和'end'(默认),让总结行位于数据区域头部或者脚部。summary.fixed配置总结行是否固定,默认固定总结行。summary.spanMethod配置总结行区域的单元格合并。
WARNING
总结行位于数据区域头部时,固定总结行需要固定表头生效方可生效。
Name | Base Salary | Bonus | Total | Status |
|---|---|---|---|---|
Emma Johnson | 8500 | 420 | 8920 | active |
David Smith | 6500 | 780 | 7280 | inactive |
Sophia Williams | 11200 | 150 | 11350 | active |
Michael Brown | 4200 | 0 | 4200 | inactive |
Olivia Davis | 9800 | 920 | 10720 | active |
James Wilson | 7300 | 310 | 7610 | active |
Sarah Miller | 10500 | 850 | 11350 | inactive |
Robert Taylor | 4200 | 190 | 4390 | active |
Jennifer Anderson | 8900 | 0 | 8900 | inactive |
Thomas Clark | 11500 | 970 | 12470 | active |
Sum | 82600 | 4590 | 87190 | |
Average | 8260 | 459 | 8719 | |
<template>
<div style="display: flex; align-items: center">
<px-switch
active-label="Fixed"
inactive-label="Unfixed"
v-model="summaryFixed"
style="margin-right: 16px"
></px-switch>
Placement:
<px-switch
active-label="Start"
inactive-label="End"
v-model="summaryStart"
style="margin-left: 8px"
></px-switch>
</div>
<px-table
:data="data"
:columns="columns"
:summary="summary"
style="margin-top: 16px"
:table-area-props="{ style: 'max-height: 300px' }"
></px-table>
</template>
<script setup lang="ts">
import type { TableOptionsArg } from '@pixelium/web-vue'
// If on-demand import
// import type { TableOptionsArg } from '@pixelium/web-vue/es'
import { computed, ref } from 'vue'
const columns = [
{
label: 'Name',
field: 'name',
key: 'name'
},
{
label: 'Base Salary',
field: 'baseSalary',
key: 'baseSalary'
},
{
label: 'Bonus',
field: 'bonus',
key: 'bonus'
},
{
label: 'Total',
field: 'total',
key: 'total'
},
{
label: 'Status',
field: 'status',
key: 'status'
}
]
const data = ref([
{
name: 'Emma Johnson',
baseSalary: 8500,
bonus: 420,
total: 8920,
status: 'active'
},
{
name: 'David Smith',
baseSalary: 6500,
bonus: 780,
total: 7280,
status: 'inactive'
},
{
name: 'Sophia Williams',
baseSalary: 11200,
bonus: 150,
total: 11350,
status: 'active'
},
{
name: 'Michael Brown',
baseSalary: 4200,
bonus: 0,
total: 4200,
status: 'inactive'
},
{
name: 'Olivia Davis',
baseSalary: 9800,
bonus: 920,
total: 10720,
status: 'active'
},
{
name: 'James Wilson',
baseSalary: 7300,
bonus: 310,
total: 7610,
status: 'active'
},
{
name: 'Sarah Miller',
baseSalary: 10500,
bonus: 850,
total: 11350,
status: 'inactive'
},
{
name: 'Robert Taylor',
baseSalary: 4200,
bonus: 190,
total: 4390,
status: 'active'
},
{
name: 'Jennifer Anderson',
baseSalary: 8900,
bonus: 0,
total: 8900,
status: 'inactive'
},
{
name: 'Thomas Clark',
baseSalary: 11500,
bonus: 970,
total: 12470,
status: 'active'
}
])
const summaryData = [
{
baseSalary: 0,
bonus: 0,
total: 0
},
{
baseSalary: 0,
bonus: 0,
total: 0
}
]
data.value.forEach((record) => {
summaryData[0].baseSalary += record.baseSalary
summaryData[0].bonus += record.bonus
summaryData[0].total += record.total
})
summaryData[1].baseSalary = parseFloat(
(summaryData[0].baseSalary / data.value.length).toFixed(4)
)
summaryData[1].bonus = parseFloat((summaryData[0].bonus / data.value.length).toFixed(4))
summaryData[1].total = parseFloat((summaryData[0].total / data.value.length).toFixed(4))
const summaryFixed = ref(false)
const summaryStart = ref(false)
const summary = computed(() => {
return {
data: summaryData,
summaryText: ['Sum', 'Average'],
fixed: summaryFixed.value,
placement: summaryStart.value ? 'start' : 'end',
spanMethod: ({ rowIndex, colIndex, record, column }: TableOptionsArg) => {
if (colIndex === 3) {
return {
colspan: 2
}
}
}
}
})
</script>加载状态
通过设置 loading 配置表格的加载状态。
Name | Age | Email |
|---|---|---|
Alice | 25 | alice@example.com |
Bob | 30 | bob@example.com |
Charlie | 35 | charlie@example.com |
<template>
<px-switch active-tip="loading" inactive-tip="not loading" v-model="loading"></px-switch>
<px-table
:data="data"
:columns="columns"
:loading="loading"
style="margin-top: 16px"
></px-table>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const loading = ref(false)
const data = ref([
{ key: 1, name: 'Alice', age: 25, email: 'alice@example.com' },
{ key: 2, name: 'Bob', age: 30, email: 'bob@example.com' },
{ key: 3, name: 'Charlie', age: 35, email: 'charlie@example.com' }
])
const columns = [
{
key: 'name',
label: 'Name',
field: 'name'
},
{
key: 'age',
label: 'Age',
field: 'age'
},
{
key: 'email',
label: 'Email',
field: 'email'
}
]
</script>前端分页
表格组件默认支持对传入的数据自动分页,可以通过 pagination 属性设置分页配置。
ID | User | Email | Register |
|---|---|---|---|
1 | John White | john.white@example.com | 2025-09-27 |
2 | Emma Harris | emma.harris@example.com | 2025-12-19 |
3 | Robert Martinez | robert.martinez@example.com | 2025-08-30 |
4 | Stephanie Rodriguez | stephanie.rodriguez@example.com | 2026-02-02 |
5 | Jessica Anderson | jessica.anderson@example.com | 2025-08-24 |
6 | Matthew Rodriguez | matthew.rodriguez@example.com | 2025-11-11 |
7 | Melissa Wilson | melissa.wilson@example.com | 2025-05-09 |
8 | Robert Martin | robert.martin@example.com | 2025-09-23 |
9 | Michelle Miller | michelle.miller@example.com | 2025-04-23 |
10 | Laura White | laura.white@example.com | 2025-07-29 |
<template>
<px-table
:data="data"
:columns="columns"
:pagination="{
showTotal: true,
showJumper: true,
showSize: true
}"
v-model:page-size="pageSize"
v-model:page="page"
></px-table>
<div style="margin-top: 16px">page: {{ page }}; pageSize: {{ pageSize }}</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const pageSize = ref(10)
const page = ref(1)
const columns = [
{
key: 'id',
label: 'ID',
field: 'id'
},
{
key: 'user',
label: 'User',
field: 'user'
},
{
key: 'email',
label: 'Email',
field: 'email'
},
{
key: 'register',
label: 'Register',
field: 'register'
}
]
const firstNames = [
'John',
'Emma',
'Michael',
'Sarah',
'David',
'Lisa',
'Robert',
'Jennifer',
'William',
'Jessica',
'James',
'Amanda',
'Christopher',
'Melissa',
'Daniel',
'Stephanie',
'Matthew',
'Laura',
'Joshua',
'Michelle'
]
const lastNames = [
'Smith',
'Johnson',
'Williams',
'Brown',
'Jones',
'Miller',
'Davis',
'Garcia',
'Rodriguez',
'Wilson',
'Martinez',
'Anderson',
'Taylor',
'Thomas',
'Jackson',
'White',
'Harris',
'Martin',
'Thompson',
'Moore'
]
function generateRandomData(count: number) {
const data: any[] = []
const currentDate = new Date()
for (let i = 1; i <= count; i++) {
const firstName = firstNames[Math.floor(Math.random() * firstNames.length)]
const lastName = lastNames[Math.floor(Math.random() * lastNames.length)]
const fullName = `${firstName} ${lastName}`
const email = `${firstName.toLowerCase()}.${lastName.toLowerCase()}@example.com`
const randomDaysAgo = Math.floor(Math.random() * 365)
const registerDate = new Date(currentDate)
registerDate.setDate(registerDate.getDate() - randomDaysAgo)
const registerDateStr = registerDate.toISOString().split('T')[0]
data.push({
id: i,
user: fullName,
email: email,
register: registerDateStr
})
}
return data
}
const data = ref(generateRandomData(50))
</script>异步分页
设置 pagination.paginateMethod 为 'custom 时,表格不会进行自动排序,常用于后端分页的情况。通过 pagination.total 设置数据总数。
ID | User | Email | Register |
|---|---|---|---|
1 | William Harris | william.harris@example.com | 2025-03-26 |
2 | James Moore | james.moore@example.com | 2025-06-30 |
3 | William Jones | william.jones@example.com | 2025-03-14 |
4 | Sarah Miller | sarah.miller@example.com | 2025-09-13 |
5 | David Brown | david.brown@example.com | 2025-11-16 |
6 | James Garcia | james.garcia@example.com | 2026-01-05 |
7 | John Harris | john.harris@example.com | 2025-08-26 |
8 | Matthew Garcia | matthew.garcia@example.com | 2025-06-06 |
9 | John Brown | john.brown@example.com | 2025-03-10 |
10 | Sarah Jackson | sarah.jackson@example.com | 2025-06-17 |
<template>
<px-table
:data="data"
:columns="columns"
:pagination="{
showTotal: true,
showJumper: true,
showSize: true,
total: 1000,
paginateMethod: 'custom'
}"
v-model:page-size="pageSize"
v-model:page="page"
:loading="loading"
></px-table>
<div style="margin-top: 16px">page: {{ page }}; pageSize: {{ pageSize }}</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
const pageSize = ref(10)
const page = ref(1)
const loading = ref(false)
watch([page, pageSize], () => {
loading.value = true
setTimeout(() => {
data.value = generateRandomData(pageSize.value, pageSize.value * (page.value - 1))
loading.value = false
}, 3000)
})
const columns = [
{
key: 'id',
label: 'ID',
field: 'id'
},
{
key: 'user',
label: 'User',
field: 'user'
},
{
key: 'email',
label: 'Email',
field: 'email'
},
{
key: 'register',
label: 'Register',
field: 'register'
}
]
const firstNames = [
'John',
'Emma',
'Michael',
'Sarah',
'David',
'Lisa',
'Robert',
'Jennifer',
'William',
'Jessica',
'James',
'Amanda',
'Christopher',
'Melissa',
'Daniel',
'Stephanie',
'Matthew',
'Laura',
'Joshua',
'Michelle'
]
const lastNames = [
'Smith',
'Johnson',
'Williams',
'Brown',
'Jones',
'Miller',
'Davis',
'Garcia',
'Rodriguez',
'Wilson',
'Martinez',
'Anderson',
'Taylor',
'Thomas',
'Jackson',
'White',
'Harris',
'Martin',
'Thompson',
'Moore'
]
function generateRandomData(count: number, startId: number) {
const data: any[] = []
const currentDate = new Date()
for (let i = 1; i <= count; i++) {
const firstName = firstNames[Math.floor(Math.random() * firstNames.length)]
const lastName = lastNames[Math.floor(Math.random() * lastNames.length)]
const fullName = `${firstName} ${lastName}`
const email = `${firstName.toLowerCase()}.${lastName.toLowerCase()}@example.com`
const randomDaysAgo = Math.floor(Math.random() * 365)
const registerDate = new Date(currentDate)
registerDate.setDate(registerDate.getDate() - randomDaysAgo)
const registerDateStr = registerDate.toISOString().split('T')[0]
data.push({
id: i + startId,
user: fullName,
email: email,
register: registerDateStr
})
}
return data
}
const data = ref(generateRandomData(pageSize.value, pageSize.value * (page.value - 1)))
</script>单页时隐藏分页
设置 pagination.hideWhenSinglePage 设置单页时隐藏分页。
Name | Age | Email |
|---|---|---|
Alice | 25 | alice@example.com |
Bob | 30 | bob@example.com |
Charlie | 35 | charlie@example.com |
<template>
<px-table
:data="data"
:columns="columns"
:pagination="{ hideWhenSinglePage: true }"
></px-table>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const data = ref([
{ key: 1, name: 'Alice', age: 25, email: 'alice@example.com' },
{ key: 2, name: 'Bob', age: 30, email: 'bob@example.com' },
{ key: 3, name: 'Charlie', age: 35, email: 'charlie@example.com' }
])
const columns = [
{
key: 'name',
label: 'Name',
field: 'name'
},
{
key: 'age',
label: 'Age',
field: 'age'
},
{
key: 'email',
label: 'Email',
field: 'email'
}
]
</script>实例方法
需要注意的是,为了受控模式下的状态同步,选中、展开、筛选、排序等实例方法均采用异步设计,每次调用后可能需要等待一个微任务的时间才能生效。
ID | Name | ||
|---|---|---|---|
1001 | Isabella Walker | ||
1002 | Olivia Wilson | ||
1003 | William Harris | ||
1004 | Liam Brown | ||
1005 | Michael Garcia | ||
1006 | Evelyn Thompson | ||
1007 | Benjamin Walker | ||
1008 | Ethan Wilson | ||
1009 | Ethan Walker | ||
1010 | Emma Walker |
<template>
<div>
<px-space>
<px-button @click="logCurrentData" theme="info">Log Current Data</px-button>
<px-button @click="logPaginatedData" theme="info">Log Paginated Data</px-button>
</px-space>
<px-space style="margin-top: 16px">
<px-button @click="expand1stRow">Expand 1st Row</px-button>
<px-button @click="clearExpand" theme="warning">Clear Expand</px-button>
</px-space>
<px-space style="margin-top: 16px">
<px-button @click="select2ndRow">Select 2nd Row</px-button>
<px-button @click="clearSelect" theme="warning">Clear Select</px-button>
<px-button @click="selectAll">Select All</px-button>
</px-space>
<px-space style="margin-top: 16px">
<px-button @click="sort1stCol">Sort 1st Col</px-button>
<px-button @click="clearSort" theme="warning">Clear Sort</px-button>
<px-button @click="filter2stCol">FIlter 2st Col</px-button>
<px-button @click="clearFilter" theme="warning">Clear Filter</px-button>
</px-space>
<div style="margin-top: 16px">expandedKeys: {{ expandedKeys }}</div>
<div style="margin-top: 16px">selectedKeys: {{ selectedKeys }}</div>
<div style="margin-top: 16px">sortOrder: {{ sortOrder }}</div>
<div style="margin-top: 16px">filterValue: {{ filterValue }}</div>
<px-table
style="margin-top: 16px"
:data="data"
:columns="columns"
row-key="id"
expandable
v-model:expanded-keys="selectedKeys"
v-model:selected-keys="expandedKeys"
v-model:sort-order="sortOrder"
v-model:filter-value="filterValue"
:selection="{
multiple: true,
showSelectAll: true
}"
ref="tableRef"
></px-table>
</div>
</template>
<script setup lang="ts">
import type { TableData, Table, SortOrder, FilterValue, TableColumn } from '@pixelium/web-vue'
// When On-demand Import
// import type { TableData, Table, SortOrder, FilterValue } from '@pixelium/web-vue/es'
import { h, ref } from 'vue'
const tableRef = ref<null | InstanceType<typeof Table>>(null)
const logCurrentData = () => {
console.log(tableRef.value?.getCurrentData())
}
const logPaginatedData = () => {
console.log(tableRef.value?.getPaginatedData())
}
const filter2stCol = () => {
tableRef.value?.filter('name', ['C'])
}
const clearFilter = () => {
tableRef.value?.clearFilter()
}
const sort1stCol = () => {
tableRef.value?.sort('id', 'desc')
}
const clearSort = () => {
tableRef.value?.clearSort()
}
const select2ndRow = () => {
tableRef.value?.select(1002, true)
}
const clearSelect = () => {
tableRef.value?.clearSelect()
}
const selectAll = () => {
tableRef.value?.selectAll(true)
}
const expand1stRow = () => {
tableRef.value?.expand(1001, true)
}
const clearExpand = () => {
tableRef.value?.clearExpand()
}
const keys = ['email', 'city', 'address']
const expandRender = ({ record }: { record: TableData }) => {
return h(
'div',
{},
keys.map((e) => {
return h('div', { style: 'display: flex; align-items: center; gap: 8px' }, [
h('div', { style: 'color: gray; margin-right: 16px' }, e + ': '),
h('div', {}, record[e])
])
})
)
}
function generateData(count: number, startId: number = 1001) {
const firstNames = [
'Emma',
'James',
'Sophia',
'Michael',
'Olivia',
'Liam',
'Ava',
'Noah',
'Isabella',
'William',
'Mia',
'Ethan',
'Charlotte',
'Alexander',
'Amelia',
'Benjamin',
'Harper',
'Daniel',
'Evelyn',
'Matthew'
]
const lastNames = [
'Johnson',
'Wilson',
'Chen',
'Brown',
'Davis',
'Miller',
'Garcia',
'Rodriguez',
'Martinez',
'Taylor',
'Anderson',
'Thomas',
'Jackson',
'White',
'Harris',
'Martin',
'Thompson',
'Moore',
'Walker',
'Clark'
]
const cities = [
'New York',
'Los Angeles',
'Chicago',
'Miami',
'Seattle',
'Houston',
'Phoenix',
'Philadelphia',
'San Antonio',
'San Diego',
'Dallas',
'San Jose',
'Austin',
'Jacksonville',
'Fort Worth',
'Columbus',
'Charlotte',
'San Francisco',
'Indianapolis',
'Denver'
]
const streetNames = [
'Main Street',
'Oak Avenue',
'Pine Road',
'Beach Boulevard',
'Lakeview Drive',
'Maple Lane',
'Cedar Street',
'Elm Avenue',
'Hill Road',
'Park Boulevard',
'River Drive',
'Sunset Avenue',
'Mountain Road',
'Valley Drive',
'Ocean Boulevard',
'Forest Lane',
'Spring Avenue',
'Summer Road',
'Winter Drive',
'Autumn Lane'
]
const data = []
for (let i = 0; i < count; i++) {
const firstName = firstNames[Math.floor(Math.random() * firstNames.length)]
const lastName = lastNames[Math.floor(Math.random() * lastNames.length)]
const city = cities[Math.floor(Math.random() * cities.length)]
const streetName = streetNames[Math.floor(Math.random() * streetNames.length)]
const streetNumber = Math.floor(Math.random() * 900) + 100
const aptTypes = ['Apt', 'Suite', 'Unit', '#', null]
const aptType = aptTypes[Math.floor(Math.random() * aptTypes.length)]
const aptNumber = aptType
? Math.floor(Math.random() * 20) +
1 +
(Math.random() > 0.5 ? String.fromCharCode(65 + Math.floor(Math.random() * 5)) : '')
: ''
let address = `${streetNumber} ${streetName}`
if (aptType && aptNumber) {
address += `, ${aptType} ${aptNumber}`
}
const item = {
id: startId + i,
name: `${firstName} ${lastName}`,
email: `${firstName.toLowerCase()}.${lastName.toLowerCase()}@example.com`,
city: city,
address: address,
expand: expandRender
}
data.push(item)
}
return data
}
const data = ref(generateData(50))
const columns: TableColumn[] = [
{
key: 'id',
label: 'ID',
field: 'id',
fixed: 'left',
sortable: {
orders: ['asc', 'desc']
}
},
{
key: 'name',
label: 'Name',
field: 'name',
filterable: {
filterOptions: Array.from({ length: 26 }, (_, i) => ({
label: `${String.fromCharCode(65 + i)}...`,
value: String.fromCharCode(65 + i)
})),
filterMethod: (value: string[], record: TableData) => {
if (value.length) {
return record.name[0] === value[0]
} else {
return true
}
}
}
}
]
const expandedKeys = ref<number[]>([])
const selectedKeys = ref<number[]>([])
const sortOrder = ref<SortOrder>({})
const filterValue = ref<FilterValue>({})
</script>跨页全选
selection.selectAllMethod 可以异步地自定义全选时更新的 selectedKeys 值。
selection.supersetSelectAllRef 配置全选复选框选中状态所参考的超集:'current'(当前页数据,默认)、'all'(所有页的数据)。
ID | User | Email | Register | |
|---|---|---|---|---|
1 | Michelle Rodriguez | michelle.rodriguez@example.com | 2025-04-19 | |
2 | James Taylor | james.taylor@example.com | 2025-02-17 | |
3 | Laura White | laura.white@example.com | 2025-07-26 | |
4 | Joshua Wilson | joshua.wilson@example.com | 2025-10-15 | |
5 | Sarah Jackson | sarah.jackson@example.com | 2025-07-10 | |
6 | Daniel Harris | daniel.harris@example.com | 2025-09-30 | |
7 | Robert Rodriguez | robert.rodriguez@example.com | 2025-03-14 | |
8 | Jennifer Brown | jennifer.brown@example.com | 2025-09-16 | |
9 | Sarah Brown | sarah.brown@example.com | 2025-03-18 | |
10 | William Garcia | william.garcia@example.com | 2025-09-17 |
<template>
<div style="display: flex; align-items: center">
Superset for Select All State Reference
<px-switch
style="margin-left: 16px"
v-model="supersetWasAllData"
active-label="All Data"
></px-switch>
</div>
<px-table
style="margin-top: 16px"
:data="data"
rowKey="id"
:columns="columns"
:pagination="{
showTotal: true,
showSize: true
}"
v-model:page-size="pageSize"
v-model:page="page"
:selection="{
selectAllMethod,
supersetSelectAllRef,
multiple: true
}"
v-model:selected-keys="selectedKeys"
ref="tableRef"
>
<template #footer>
<div
v-if="!crossPageSelectAll && currentPageSelectAll"
style="color: var(--px-neutral-8)"
>
Select all on current page
</div>
<div v-if="crossPageSelectAll" style="color: var(--px-primary-6)">
Select all across all pages
</div>
</template>
</px-table>
</template>
<script lang="ts" setup>
import type { DialogReturn, TableData } from '@pixelium/web-vue'
import { Button, Dialog, Table } from '@pixelium/web-vue'
// On-demand import
// import type { DialogReturn, TableData } from '@pixelium/web-vue/es'
// import { Button, Dialog, Table } from '@pixelium/web-vue/es'
import { ref, h, computed, watch, nextTick } from 'vue'
function intersection<T>(arr1: T[], arr2: T[]): T[] {
const map = new Map<any, { value: T; count: number }>()
const len1 = arr1.length
for (let i = 0; i < len1; i++) {
const key = arr1[i]
const gotVal = map.get(key)
if (!gotVal) {
map.set(key, { value: arr1[i], count: 1 })
} else {
gotVal.count += 1
}
}
const len2 = arr2.length
for (let i = 0; i < len2; i++) {
const key = arr2[i]
const gotVal = map.get(key)
if (!gotVal) {
map.set(key, { value: arr2[i], count: 1 })
} else {
gotVal.count += 1
}
}
const ans: T[] = []
for (const data of map.values()) {
if (data.count > 1) {
ans.push(data.value)
}
}
return ans
}
function union<T>(arr1: T[], arr2: T[]): T[] {
const map = new Map<any, T>()
const len1 = arr1.length
for (let i = 0; i < len1; i++) {
const key = arr1[i]
if (!map.has(key)) {
map.set(key, arr1[i])
}
}
const len2 = arr2.length
for (let i = 0; i < len2; i++) {
const key = arr2[i]
if (!map.has(key)) {
map.set(key, arr2[i])
}
}
return [...map.values()]
}
function difference<T>(arr1: T[], arr2: T[]): T[] {
const map = new Map<any, { value: T; count: number }>()
const len1 = arr1.length
for (let i = 0; i < len1; i++) {
const key = arr1[i]
if (!map.has(key)) {
map.set(key, { value: arr1[i], count: 1 })
}
}
const len2 = arr2.length
for (let i = 0; i < len2; i++) {
const key = arr2[i]
const gotVal = map.get(key)
if (gotVal) {
gotVal.count++
}
}
const ans: T[] = []
for (const data of map.values()) {
if (data.count === 1) {
ans.push(data.value)
}
}
return ans
}
const getKeys = (data: TableData[]) => data.filter((e) => !e.disabled).map((e) => e.id)
const pageSize = ref(10)
const page = ref(1)
const supersetWasAllData = ref(false)
const supersetSelectAllRef = computed(() => {
return supersetWasAllData.value ? 'all' : 'current'
})
const tableRef = ref<InstanceType<typeof Table> | null>(null)
const selectedKeys = ref<number[]>([])
const currentPageSelectAll = ref(false)
const crossPageSelectAll = ref(false)
watch([page, pageSize, selectedKeys], () => {
nextTick(() => {
const currentData = tableRef.value?.getCurrentData() || []
const paginatedData = tableRef.value?.getPaginatedData() || []
currentPageSelectAll.value =
difference(getKeys(paginatedData), selectedKeys.value).length === 0
crossPageSelectAll.value = difference(getKeys(currentData), selectedKeys.value).length === 0
})
})
const selectAllMethod = async (
value: boolean,
preState: { value: boolean; indeterminate: boolean },
extra: {
originData: TableData[]
currentData: TableData[]
paginatedData: TableData[]
selectedKeys: any[]
page: number
pageSize: number
}
) => {
const paginatedDataKeys = getKeys(extra.paginatedData)
const currentDataKeys = getKeys(extra.currentData)
let selectedKeys: any[] = []
const clearPageHandler = (dialogReturn: DialogReturn) => {
dialogReturn.close()
selectedKeys = difference(extra.selectedKeys, paginatedDataKeys)
}
const pageOnlyHandler = (dialogReturn: DialogReturn) => {
dialogReturn.close()
selectedKeys = getKeys(extra.paginatedData)
}
const pageAddHandler = (dialogReturn: DialogReturn) => {
dialogReturn.close()
selectedKeys = union(extra.selectedKeys, paginatedDataKeys)
}
const selectAllHandler = (dialogReturn: DialogReturn) => {
dialogReturn.close()
selectedKeys = union(extra.selectedKeys, currentDataKeys)
}
const clearAllHandler = (dialogReturn: DialogReturn) => {
dialogReturn.close()
selectedKeys = difference(extra.selectedKeys, currentDataKeys)
}
let dialogReturn: DialogReturn
if (value) {
const currentPageHasIntersection =
extra.selectedKeys.length &&
intersection(paginatedDataKeys, extra.selectedKeys).length > 0
const allDataHasIntersection =
supersetWasAllData.value &&
extra.selectedKeys.length &&
intersection(currentDataKeys, extra.selectedKeys).length > 0
dialogReturn = Dialog.warning({
title: 'Select All',
content: 'Select one action to perform:',
footer: () => {
return h('div', {}, [
currentPageHasIntersection && allDataHasIntersection
? h(
'div',
{
style:
'display: flex; justify-content: end; align-items: center; gap: 8px; margin-bottom: 8px'
},
{
default: () => [
currentPageHasIntersection
? h(
Button,
{
theme: 'info',
onClick: () => clearPageHandler(dialogReturn),
pollSizeChange: true
},
{ default: () => 'Clear Page' }
)
: null,
allDataHasIntersection
? h(
Button,
{
theme: 'warning',
onClick: () => clearAllHandler(dialogReturn),
pollSizeChange: true
},
{ default: () => 'Clear All' }
)
: null
]
}
)
: null,
h(
'div',
{ style: 'display: flex; justify-content: end; align-items: center; gap: 8px;' },
{
default: () => [
h(
Button,
{
theme: 'info',
onClick: () => pageOnlyHandler(dialogReturn),
pollSizeChange: true
},
{ default: () => 'This Page Only' }
),
h(
Button,
{
theme: 'primary',
onClick: () => pageAddHandler(dialogReturn),
pollSizeChange: true
},
{ default: () => 'Add This Page' }
),
h(
Button,
{
theme: 'primary',
onClick: () => selectAllHandler(dialogReturn),
pollSizeChange: true
},
{ default: () => 'Select All' }
)
]
}
)
])
}
})
} else {
dialogReturn = Dialog.warning({
title: 'Clear Select All',
content: 'Select one action to perform:',
footer: () => {
return h(
'div',
{ style: 'display: flex; justify-content: end; align-items: center; gap: 8px' },
{
default: () => [
h(
Button,
{
theme: 'info',
onClick: () => clearPageHandler(dialogReturn),
pollSizeChange: true
},
{ default: () => 'Clear Page' }
),
h(
Button,
{
theme: 'warning',
onClick: () => clearAllHandler(dialogReturn),
pollSizeChange: true
},
{ default: () => 'Clear All' }
)
]
}
)
}
})
}
await dialogReturn
return selectedKeys
}
const columns = [
{
key: 'id',
label: 'ID',
field: 'id'
},
{
key: 'user',
label: 'User',
field: 'user'
},
{
key: 'email',
label: 'Email',
field: 'email'
},
{
key: 'register',
label: 'Register',
field: 'register'
}
]
const firstNames = [
'John',
'Emma',
'Michael',
'Sarah',
'David',
'Lisa',
'Robert',
'Jennifer',
'William',
'Jessica',
'James',
'Amanda',
'Christopher',
'Melissa',
'Daniel',
'Stephanie',
'Matthew',
'Laura',
'Joshua',
'Michelle'
]
const lastNames = [
'Smith',
'Johnson',
'Williams',
'Brown',
'Jones',
'Miller',
'Davis',
'Garcia',
'Rodriguez',
'Wilson',
'Martinez',
'Anderson',
'Taylor',
'Thomas',
'Jackson',
'White',
'Harris',
'Martin',
'Thompson',
'Moore'
]
function generateRandomData(count: number) {
const data: any[] = []
const currentDate = new Date()
for (let i = 1; i <= count; i++) {
const firstName = firstNames[Math.floor(Math.random() * firstNames.length)]
const lastName = lastNames[Math.floor(Math.random() * lastNames.length)]
const fullName = `${firstName} ${lastName}`
const email = `${firstName.toLowerCase()}.${lastName.toLowerCase()}@example.com`
const randomDaysAgo = Math.floor(Math.random() * 365)
const registerDate = new Date(currentDate)
registerDate.setDate(registerDate.getDate() - randomDaysAgo)
const registerDateStr = registerDate.toISOString().split('T')[0]
data.push({
id: i,
user: fullName,
email: email,
register: registerDateStr
})
}
return data
}
const data = ref(generateRandomData(50))
</script>API
TableProps
| 属性 | 类型 | 可选 | 默认值 | 描述 | 版本 |
|---|---|---|---|---|---|
| data | TableData[] | 是 | [] | 表格的数据源。 | 0.1.0 |
| columns | TableColumn[] | 是 | [] | 表格的列配置集合。 | 0.1.0 |
| bordered | boolean | TableBordered | 是 | true | 是否显示表格边框及其相关样式。 | 0.1.0 |
| variant | 'normal' | 'striped' | 'checkered' | 是 | 'normal' | 表格的展示风格。 | 0.1.0 |
| fixedHead | boolean | 是 | true | 是否固定表头。 | 0.1.0 |
| spanMethod | (options: TableOptionsArg) => void | { colspan?: number, rowspan?: number} | 是 | | 用于合并行或列的计算方法。 | 0.1.0 |
| rowKey | string | 是 | 'key' | 用于标识行数据的唯一键名。 | 0.1.0 |
| scroll | { x?: number | string } | 是 | | 表格的滚动配置(如横向滚动设置)。 | 0.1.0 |
| selection | boolean | TableSelection | 是 | false | 是否启用行选择及其配置。 | 0.1.0 |
| selectedKeys | any[] | null | 是 | | 当前选中的行键集合(受控)。 | 0.1.0 |
| defaultSelectedKeys | any[] | null | 是 | | 默认选中的行键集合(非受控)。 | 0.1.0 |
| expandable | boolean | TableExpandable | 是 | false | 是否启用可展开行及其配置。 | 0.1.0 |
| expandedKeys | any[] | null | 是 | | 当前展开的行键集合(受控)。 | 0.1.0 |
| defaultExpandedKeys | any[] | null | 是 | | 默认展开的行键集合(非受控)。 | 0.1.0 |
| summary | TableSummary | 是 | | 表格汇总行的配置项。 | 0.1.0 |
| filterValue | FilterValue | null | 是 | | 当前的筛选值(受控)。 | 0.1.0 |
| defaultFilterValue | FilterValue | null | 是 | | 默认的筛选值(非受控)。 | 0.1.0 |
| sortOrder | SortOrder | null | 是 | | 当前的排序信息(受控)。 | 0.1.0 |
| defaultSortOrder | SortOrder | null | 是 | | 默认的排序信息(非受控)。 | 0.1.0 |
| loading | boolean | 是 | false | 表格是否处于加载状态。 | 0.1.0 |
| pagination | TablePagination | 是 | true | 表格分页配置。 | 0.1.0 |
| page | number | null | 是 | | 当前的页码(受控模式)。 | 0.1.0 |
| defaultPage | number | null | 是 | 1 | 当前的页码默认值(非受控模式)。 | 0.1.0 |
| pageSize | number | null | 是 | | 当前的页面容量(受控模式)。 | 0.1.0 |
| defaultPageSize | number | null | 是 | 10 | 当前的页面容量默认值(非受控模式)。 | 0.1.0 |
| tableAreaProps | RestAttrs | 是 | | 表格区域属性对象,作用的元素你可以通过 .px-table-area 找到。 | 0.1.0 |
| borderRadius | number | 是 | | 表格的圆角设置。 | 0.1.0 |
| pollSizeChange | boolean | 是 | | 开启轮询组件尺寸变化,可能会影响性能,常用于被容器元素影响尺寸,进而 canvas 绘制异常的情况。该属也会作用域内部单选框、多选框、分页子组件。 | 0.1.0 |
TableEvents
| 事件 | 参数 | 描述 | 版本 |
|---|---|---|---|
| update:selectedKey | value: any[] | 更新 selectedKeys 的回调。 | 0.1.0 |
| select | value: boolean, key: any, record: TableData, event: InputEvent | 选择某一行的回调。 | 0.1.0 |
| selectAll | value: boolean, event: InputEvent | 全选的回调。 | 0.1.0 |
| selectedChange | value: any[] | 选择的行变化的回调。 | 0.1.0 |
| update:expandedKeys | value: any[] | 更新 expandedKeys 的回调。 | 0.1.0 |
| expand | value: boolean, key: any, record: TableData, event: MouseEvent | 展开某一行的回调。 | 0.1.0 |
| expandedChange | value: any[] | 展开的行变化的回调。 | 0.1.0 |
| update:filterValue | value: FilterValue | 更新 filterValue 的回调。 | 0.1.0 |
| filterSelect | value: any[], key: string | number | symbol, option: TableFilterOption | string, column: TableColumn, event: InputEvent | 选择筛选器中选项时的回调。 | 0.1.0 |
| filterConfirm | key: string | number | symbol, event: MouseEvent | 确认筛选器选择时的回调。 | 0.1.0 |
| filterReset | key: string | number | symbol, event: MouseEvent | 重置筛选器选择时的回调。 | 0.1.0 |
| filterChange | value: FilterValue | 筛选器选择改变时的回调。 | 0.1.0 |
| update:sortOrder | value: SortOrder | 更新 sortOrder 的回调。 | 0.1.0 |
| sortSelect | value: 'asc' | 'desc' | 'none', key: string | number | symbol, column: TableColumn, event: MouseEvent | 选择排序方向时的回调。 | 0.1.0 |
| sortOrderChange | value: SortOrder | 排序改变时的回调。 | 0.1.0 |
| cellMouseenter | column: TableColumn, record: TableData, colIndex: number, rowIndex: number, event: MouseEvent | 鼠标移入单元格的回调。 | 0.1.0 |
| cellMouseleave | column: TableColumn, record: TableData, colIndex: number, rowIndex: number, event: MouseEvent | 鼠标移出单元格的回调。 | 0.1.0 |
| cellClick | column: TableColumn, record: TableData, colIndex: number, rowIndex: number, event: MouseEvent | 点击数据区域单元格的回调。 | 0.1.0 |
| cellDblclick | column: TableColumn, record: TableData, colIndex: number, rowIndex: number, event: MouseEvent | 双击数据区域单元格的回调。 | 0.1.0 |
| cellContextmenu | column: TableColumn, record: TableData, colIndex: number, rowIndex: number, event: MouseEvent | 右键点击数据区域单元格的回调。 | 0.1.0 |
| headCellClick | column: TableColumn, indexPath: number[], event: MouseEvent | 点击表头单元格的回调。 | 0.1.0 |
| headCellDblclick | column: TableColumn, indexPath: number[], event: MouseEvent | 双击表头单元格的回调。 | 0.1.0 |
| headCellContextmenu | column: TableColumn, indexPath: number[], event: MouseEvent | 右键点击表头单元格的回调。 | 0.1.0 |
| rowClick | record: TableData, rowIndex: number, event: MouseEvent | 点击数据区域行的回调。 | 0.1.0 |
| rowDblclick | record: TableData, rowIndex: number, event: MouseEvent | 双击数据区域行的回调。 | 0.1.0 |
| rowContextmenu | record: TableData, rowIndex: number, event: MouseEvent | 右键点击数据区域行的回调。 | 0.1.0 |
| update:page | value: number | 更新 page 的回调。 | 0.1.0 |
| update:pageSize | value: number | 更新 pageSize 的回调。 | 0.1.0 |
TableSlots
| 插槽 | 参数 | 描述 | 版本 |
|---|---|---|---|
| [columns[].labelSlotName] | rowIndex: number, colIndex: number, column: TableColumn | 表格头部单元格自定义内容的插槽。 | 0.1.0 |
| [columns[].slotName] | colIndex: number, rowIndex: number, column: TableColumn, record: TableData | 表格单元格自定义内容的插槽。 | 0.1.0 |
| expand | rowIndex: number, record: TableData | 表格展开行内容的插槽。 | 0.1.0 |
TableExpose
| 属性 | 类型 | 可选 | 默认值 | 描述 | 版本 |
|---|---|---|---|---|---|
| getCurrentData | () => TableData[] | 否 | | 获取表格经过排序和筛选的当前展示的数据。 | 0.1.0 |
| getPaginatedData | () => TableData[] | 否 | | 获取表格分页后的当前展示的数据。 | 0.1.0 |
| select | (key: any | any[], value: boolean) => Promise<void> | 否 | | 操作表格行选择状态。 | 0.1.0 |
| selectAll | (value: boolean, crossPage?: boolean, ignoreDisabled?: boolean) => Promise<void> | 否 | | 全选 / 全不选表格行,crossPage 表示跨页选择,默认 false,ignoreDisabled 控制是否忽略 disabled 状态的行,默认 true。 | 0.1.0 |
| clearSelect | () => Promise<void> | 否 | | 清除所有行的选择状态。 | 0.1.0 |
| expand | (key: any | any[], value: boolean) => Promise<void> | 否 | | 操作表格行展开状态。 | 0.1.0 |
| clearExpand | () => Promise<void> | 否 | | 清除所有行的展开状态。 | 0.1.0 |
| filter | (key: number | string | symbol, value: any[]) => Promise<void> | 否 | | 操作列的筛选状态。 | 0.1.0 |
| clearFilter | () => Promise<void> | 否 | | 清除所有列的筛选状态。 | 0.1.0 |
| sort | (key: number | string | symbol, value: 'none' | 'asc' | 'desc') => Promise<void> | 否 | | 操作列的排序状态。 | 0.1.0 |
| clearSort | () => Promise<void> | 否 | | 清除所有列的排序状态。 | 0.1.0 |
TableData
| 属性 | 类型 | 可选 | 默认值 | 描述 | 版本 |
|---|---|---|---|---|---|
| expand | boolean | string | (( arg: Pick<TableOptionsArg, 'record' | 'rowIndex'>) => VNode | string | JSX.Element | null | void) | 是 | | 展开行内容配置。 | 0.1.0 |
| disabled | boolean | 是 | false | 是否禁用行选择。 | 0.1.0 |
| [x: string | number | symbol] | any | 是 | | 其他属性。 | 0.1.0 |
TableColumn
| 属性 | 类型 | 可选 | 默认值 | 描述 | 版本 |
|---|---|---|---|---|---|
| key | number | string | symbol | 否 | | 列的唯一标识。 | 0.1.0 |
| label | string | 是 | | 表头显示文本。 | 0.1.0 |
| field | string | 是 | | 单元格对应的数据字段名。 | 0.1.0 |
| width | number | 是 | 80 | 列宽设置。 | 0.1.0 |
| minWidth | number | 是 | 0 | 列的最小宽度设置。 | 0.1.0 |
| align | 'left' | 'center' | 'right' | 是 | 'left' | 单元格文本对齐方式。 | 0.1.0 |
| fixed | 'left' | 'right' | 'none' | 是 | 'none' | 列的固定位置设置。 | 0.1.0 |
| slotName | string | 是 | | 单元格内容插槽名。 | 0.1.0 |
| render | string | ((arg: TableOptionsArg) => VNode | string | JSX.Element | null | void) | 是 | | 单元格内容渲染函数或名称。 | 0.1.0 |
| labelSlotName | string | 是 | | 表头单元格内容插槽名。 | 0.1.0 |
| labelRender | string | ((arg: Omit<TableOptionsArg, 'record'>) => VNode | string | JSX.Element | null | void) | 是 | | 表头单元格内容渲染函数或名称。 | 0.1.0 |
| children | TableColumn[] | 是 | | 子列集合,用于多级表头。 | 0.1.0 |
| filterable | TableFilterable | 是 | | 列的筛选配置。 | 0.1.0 |
| sortable | TableSortable | 是 | | 列的排序配置。 | 0.1.0 |
| cellProps | RestAttrs | 是 | | 单元格属性对象。 | 0.1.0 |
| labelCellProps | RestAttrs | 是 | | 表头单元格属性对象。 | 0.1.0 |
| contentProps | RestAttrs | 是 | | 单元格内容属性对象。 | 0.1.0 |
| labelContentProps | RestAttrs | 是 | | 表头内容属性对象。 | 0.1.0 |
TableBordered
| 属性 | 类型 | 可选 | 默认值 | 描述 | 版本 |
|---|---|---|---|---|---|
| table | boolean | 是 | true | 表格外围边框。 | 0.1.0 |
| row | boolean | 是 | true | 表格行水平边框。 | 0.1.0 |
| col | boolean | 是 | true | 表格列竖直边框。 | 0.1.0 |
| head | boolean | 是 | true | 表头区域和内容区域之间的边框。 | 0.1.0 |
| side | boolean | 是 | true | 表格左右两侧的边框,仅 bordered.table 或 bordered 为 true 时生效。 | 0.1.0 |
TableSelection
| 属性 | 类型 | 可选 | 默认值 | 描述 | 版本 |
|---|---|---|---|---|---|
| multiple | boolean | 是 | false | 是否为多选。 | 0.1.0 |
| showSelectAll | boolean | 是 | true | 是否为展示全选按钮,仅 selection.multiple 为 true 时生效。 | 0.1.0 |
| selectAllMethod | (value: boolean, preState: { value: boolean, indeterminate: boolean }, arg: { originData: TableData[], currentData: TableData[], paginatedData: TableData[], page: number, pageSize: number }) => any[] | Promise<any[]> | 是 | | 自定义全选时更新的 selectedKeys 值。 | 0.1.0 |
| supersetSelectAllRef | 'current' | 'all' | 是 | 'current' | 配置全选复选框选中状态所参考的超集。 | 0.1.0 |
| label | string | 是 | | 行选择器列的表头文本。 | 0.1.0 |
| width | number | 是 | 48 | 行选择器列宽度。 | 0.1.0 |
| minWidth | number | 是 | 0 | 行选择器列最小宽度。 | 0.1.0 |
| fixed | boolean | 是 | false | 行选择器列是否固定,存在左侧固定列时,强制左侧固定。 | 0.1.0 |
| onlyCurrent | boolean | 是 | false | 已选中行中,是否只包含当前 data 属性中的行。 | 0.1.0 |
| cellProps | RestAttrs | 是 | | 列单元格属性对象。 | 0.1.0 |
| labelCellProps | RestAttrs | 是 | | 列表头单元格属性对象。 | 0.1.0 |
| contentProps | RestAttrs | 是 | | 列单元格内容属性对象。 | 0.1.0 |
| labelContentProps | RestAttrs | 是 | | 列表头内容属性对象。 | 0.1.0 |
TableExpandable
| 属性 | 类型 | 可选 | 默认值 | 描述 | 版本 |
|---|---|---|---|---|---|
| defaultExpandAllRows | boolean | 是 | false | 是否默认展开所有行。 | 0.1.0 |
| label | string | 是 | | 展开按钮列的表头文本。 | 0.1.0 |
| width | number | 是 | 48 | 展开按钮列宽度。 | 0.1.0 |
| minWidth | number | 是 | 0 | 展开按钮列最小宽度。 | 0.1.0 |
| fixed | boolean | 是 | false | 展开按钮列是否固定,存在左侧固定列时,强制左侧固定。 | 0.1.0 |
| cellProps | RestAttrs | 是 | | 列单元格属性对象。 | 0.1.0 |
| labelCellProps | RestAttrs | 是 | | 列表头单元格属性对象。 | 0.1.0 |
| contentProps | RestAttrs | 是 | | 列单元格内容属性对象。 | 0.1.0 |
| labelContentProps | RestAttrs | 是 | | 列表头内容属性对象。 | 0.1.0 |
TableSummary
| 属性 | 类型 | 可选 | 默认值 | 描述 | 版本 |
|---|---|---|---|---|---|
| data | TableData | TableData[] | 是 | | 总结行数据。 | 0.1.0 |
| placement | 'end' | 'start' | 是 | 'end' | 总结行位置。 | 0.1.0 |
| summaryText | string | string[] | 是 | | 总结行首列的文本,为数组时,作用于对应下标的总结行,为字符串值时作用于所有总结行。 | 0.1.0 |
| fixed | boolean | 是 | true | 总结行是否固定,当总结行位于数据区域头部时,总结行固定需要 fixedHead 为 true 才生效。 | 0.1.0 |
| spanMethod | (options: TableOptionsArg) => void | { colspan?: number, rowspan?: number } | 是 | | 总结行合并单元格方法。 | 0.1.0 |
TableFilterable
| 属性 | 类型 | 可选 | 默认值 | 描述 | 版本 |
|---|---|---|---|---|---|
| filterOptions | (string | TableFilterOption)[] | 是 | | 列的筛选选项。 | 0.1.0 |
| filterMethod | (filteredValue: any[], record: TableData, field?: string) => boolean | 是 | | 用于判断记录是否匹配所选筛选项的函数。 | 0.1.0 |
| defaultFilterValue | any[] | null | 是 | | 默认筛选值。 | 0.1.0 |
| multiple | boolean | 是 | false | 是否允许选择多选。 | 0.1.0 |
| popoverProps | Omit<PopoverProps, 'visible' | 'content'> & EmitEvent<PopoverEvents> | 是 | | 筛选弹出框的属性。 | 0.1.0 |
TableSortable
| 属性 | 类型 | 可选 | 默认值 | 描述 | 版本 |
|---|---|---|---|---|---|
| orders | ('asc' | 'desc')[] | Readonly<('asc' | 'desc')[]> | 是 | | 列的可供选择的的排序方向。 | 0.1.0 |
| sortMethod | 'custom' | ((a: TableData, b: TableData, order: 'asc' | 'desc', field?: string) => number) | 是 | | 列的排序方法。 | 0.1.0 |
| defaultSortOrder | 'asc' | 'desc' | 'none' | null | 是 | | 列的排序方向的默认值。 | 0.1.0 |
| multiple | boolean | 是 | false | 是否为多级排序。 | 0.1.0 |
| priority | number | 是 | 0 | 多级排序的优先级,数值较大者优先。 | 0.1.0 |
TablePagination
paginateMethod 字段控制表格组件内部的分页方法,'auto' 时,表格会根据页面容量对数据进行分页。为 'custom' 则不会有此类行为,这个配置常用于后端分页。
export type TablePagination = {
paginateMethod?: 'custom' | 'auto'
} & PaginationProps &
EmitEvent<PaginationEvents> &
RestAttrsTableOptionsArg
export type TableOptionsArg = {
rowIndex: number
colIndex: number
record: TableData
column: TableColumn
}Option
export interface Option<T = any> {
value: T
label: string
}TableFilterOption
export interface TableFilterOption<T = any> extends Option<T> {
disabled?: boolean
key?: string | number | symbol
}SortOrder, FilterValue
export type SortOrder = {
[key: string | number | symbol]: 'asc' | 'desc' | 'none' | null | undefined
}
export type FilterValue = {
[key: string | number | symbol]: any[] | null | undefined
}RestAttrs
import type { StyleValue } from 'vue'
export type VueClassValue = string | Record<string, any> | VueClassValue[]
export type VueStyleValue = StyleValue
export type RestAttrs = {
style?: VueStyleValue | null
class?: VueClassValue | null
[x: string]: any
}EmitEvent
export type EmitEvent<T extends Record<string, any>> = {
[K in keyof T as `on${Capitalize<K & string>}`]?: (...args: T[K]) => void
}