Skip to content

表格 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
1
<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
1
<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
1
<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[].widthcolumns[].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
1
<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')。

Bordered:
Variant:
Name
Age
Email
Alice
25
alice@example.com
Bob
30
bob@example.com
Charlie
35
charlie@example.com
1
<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
1
<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
1
2
<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
1
<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
1
selectedKeys: []
<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
1
ID
Name
1001
Emma Johnson
1002
James Wilson
1003
Sophia Chen
1004
Michael Brown
1005
Olivia Davis
1
expandedKeys: []
<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
1
sortOrder: { "name": "asc" }
<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[].sortablesortMethod 设置属性 multipletrue,开启多级排序,此时可以通过 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
1
sortOrder: { "name": "asc" }
<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
1
filterValue: { "status": [ "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

总结行位于数据区域头部时,固定总结行需要固定表头生效方可生效。

Placement:
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
1
<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
1
<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
Total 50
1
2
3
4
5
10 / Page
Go to
page: 1; pageSize: 10
<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
Total 1000
1
2
3
4
5
6
7
...
100
10 / Page
Go to
page: 1; pageSize: 10
<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>

实例方法

需要注意的是,为了受控模式下的状态同步,选中、展开、筛选、排序等实例方法均采用异步设计,每次调用后可能需要等待一个微任务的时间才能生效。

expandedKeys: []
selectedKeys: []
sortOrder: {}
filterValue: {}
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
1
2
3
4
5
<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'(所有页的数据)。

Superset for Select All State Reference
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
Total 50
1
2
3
4
5
10 / Page
<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

属性类型可选默认值描述版本
dataTableData[][]表格的数据源。0.1.0
columnsTableColumn[][]表格的列配置集合。0.1.0
borderedboolean | TableBorderedtrue是否显示表格边框及其相关样式。0.1.0
variant'normal' | 'striped' | 'checkered''normal'表格的展示风格。0.1.0
fixedHeadbooleantrue是否固定表头。0.1.0
spanMethod(options: TableOptionsArg) => void | { colspan?: number, rowspan?: number}用于合并行或列的计算方法。0.1.0
rowKeystring'key'用于标识行数据的唯一键名。0.1.0
scroll{ x?: number | string }表格的滚动配置(如横向滚动设置)。0.1.0
selectionboolean | TableSelectionfalse是否启用行选择及其配置。0.1.0
selectedKeysany[] | null当前选中的行键集合(受控)。0.1.0
defaultSelectedKeysany[] | null默认选中的行键集合(非受控)。0.1.0
expandableboolean | TableExpandablefalse是否启用可展开行及其配置。0.1.0
expandedKeysany[] | null当前展开的行键集合(受控)。0.1.0
defaultExpandedKeysany[] | null默认展开的行键集合(非受控)。0.1.0
summaryTableSummary表格汇总行的配置项。0.1.0
filterValueFilterValue | null当前的筛选值(受控)。0.1.0
defaultFilterValueFilterValue | null默认的筛选值(非受控)。0.1.0
sortOrderSortOrder | null当前的排序信息(受控)。0.1.0
defaultSortOrderSortOrder | null默认的排序信息(非受控)。0.1.0
loadingbooleanfalse表格是否处于加载状态。0.1.0
paginationTablePaginationtrue表格分页配置。0.1.0
pagenumber | null当前的页码(受控模式)。0.1.0
defaultPagenumber | null1当前的页码默认值(非受控模式)。0.1.0
pageSizenumber | null当前的页面容量(受控模式)。0.1.0
defaultPageSizenumber | null10当前的页面容量默认值(非受控模式)。0.1.0
tableAreaPropsRestAttrs表格区域属性对象,作用的元素你可以通过 .px-table-area 找到。0.1.0
borderRadiusnumber表格的圆角设置。0.1.0
pollSizeChangeboolean开启轮询组件尺寸变化,可能会影响性能,常用于被容器元素影响尺寸,进而 canvas 绘制异常的情况。该属也会作用域内部单选框、多选框、分页子组件。0.1.0

TableEvents

事件参数描述版本
update:selectedKeyvalue: any[]更新 selectedKeys 的回调。0.1.0
selectvalue: boolean, key: any, record: TableData, event: InputEvent选择某一行的回调。0.1.0
selectAllvalue: boolean, event: InputEvent全选的回调。0.1.0
selectedChangevalue: any[]选择的行变化的回调。0.1.0
update:expandedKeysvalue: any[]更新 expandedKeys 的回调。0.1.0
expandvalue: boolean, key: any, record: TableData, event: MouseEvent展开某一行的回调。0.1.0
expandedChangevalue: any[]展开的行变化的回调。0.1.0
update:filterValuevalue: FilterValue更新 filterValue 的回调。0.1.0
filterSelectvalue: any[], key: string | number | symbol, option: TableFilterOption | string, column: TableColumn, event: InputEvent选择筛选器中选项时的回调。0.1.0
filterConfirmkey: string | number | symbol, event: MouseEvent确认筛选器选择时的回调。0.1.0
filterResetkey: string | number | symbol, event: MouseEvent重置筛选器选择时的回调。0.1.0
filterChangevalue: FilterValue筛选器选择改变时的回调。0.1.0
update:sortOrdervalue: SortOrder更新 sortOrder 的回调。0.1.0
sortSelectvalue: 'asc' | 'desc' | 'none', key: string | number | symbol, column: TableColumn, event: MouseEvent选择排序方向时的回调。0.1.0
sortOrderChangevalue: SortOrder排序改变时的回调。0.1.0
cellMouseentercolumn: TableColumn, record: TableData, colIndex: number, rowIndex: number, event: MouseEvent鼠标移入单元格的回调。0.1.0
cellMouseleavecolumn: TableColumn, record: TableData, colIndex: number, rowIndex: number, event: MouseEvent鼠标移出单元格的回调。0.1.0
cellClickcolumn: TableColumn, record: TableData, colIndex: number, rowIndex: number, event: MouseEvent点击数据区域单元格的回调。0.1.0
cellDblclickcolumn: TableColumn, record: TableData, colIndex: number, rowIndex: number, event: MouseEvent双击数据区域单元格的回调。0.1.0
cellContextmenucolumn: TableColumn, record: TableData, colIndex: number, rowIndex: number, event: MouseEvent右键点击数据区域单元格的回调。0.1.0
headCellClickcolumn: TableColumn, indexPath: number[], event: MouseEvent点击表头单元格的回调。0.1.0
headCellDblclickcolumn: TableColumn, indexPath: number[], event: MouseEvent双击表头单元格的回调。0.1.0
headCellContextmenucolumn: TableColumn, indexPath: number[], event: MouseEvent右键点击表头单元格的回调。0.1.0
rowClickrecord: TableData, rowIndex: number, event: MouseEvent点击数据区域行的回调。0.1.0
rowDblclickrecord: TableData, rowIndex: number, event: MouseEvent双击数据区域行的回调。0.1.0
rowContextmenurecord: TableData, rowIndex: number, event: MouseEvent右键点击数据区域行的回调。0.1.0
update:pagevalue: number更新 page 的回调。0.1.0
update:pageSizevalue: 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
expandrowIndex: 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 表示跨页选择,默认 falseignoreDisabled 控制是否忽略 disabled 状态的行,默认 true0.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

属性类型可选默认值描述版本
expandboolean | string | (( arg: Pick<TableOptionsArg, 'record' | 'rowIndex'>) => VNode | string | JSX.Element | null | void)展开行内容配置。0.1.0
disabledbooleanfalse是否禁用行选择。0.1.0
[x: string | number | symbol]any其他属性。0.1.0

TableColumn

属性类型可选默认值描述版本
keynumber | string | symbol列的唯一标识。0.1.0
labelstring表头显示文本。0.1.0
fieldstring单元格对应的数据字段名。0.1.0
widthnumber80列宽设置。0.1.0
minWidthnumber0列的最小宽度设置。0.1.0
align'left' | 'center' | 'right''left'单元格文本对齐方式。0.1.0
fixed'left' | 'right' | 'none''none'列的固定位置设置。0.1.0
slotNamestring单元格内容插槽名。0.1.0
renderstring | ((arg: TableOptionsArg) => VNode | string | JSX.Element | null | void)单元格内容渲染函数或名称。0.1.0
labelSlotNamestring表头单元格内容插槽名。0.1.0
labelRenderstring | ((arg: Omit<TableOptionsArg, 'record'>) => VNode | string | JSX.Element | null | void)表头单元格内容渲染函数或名称。0.1.0
childrenTableColumn[]子列集合,用于多级表头。0.1.0
filterableTableFilterable列的筛选配置。0.1.0
sortableTableSortable列的排序配置。0.1.0
cellPropsRestAttrs单元格属性对象。0.1.0
labelCellPropsRestAttrs表头单元格属性对象。0.1.0
contentPropsRestAttrs单元格内容属性对象。0.1.0
labelContentPropsRestAttrs表头内容属性对象。0.1.0

TableBordered

属性类型可选默认值描述版本
tablebooleantrue表格外围边框。0.1.0
rowbooleantrue表格行水平边框。0.1.0
colbooleantrue表格列竖直边框。0.1.0
headbooleantrue表头区域和内容区域之间的边框。0.1.0
sidebooleantrue表格左右两侧的边框,仅 bordered.tableborderedtrue 时生效。0.1.0

TableSelection

属性类型可选默认值描述版本
multiplebooleanfalse是否为多选。0.1.0
showSelectAllbooleantrue是否为展示全选按钮,仅 selection.multipletrue 时生效。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
labelstring行选择器列的表头文本。0.1.0
widthnumber48行选择器列宽度。0.1.0
minWidthnumber0行选择器列最小宽度。0.1.0
fixedbooleanfalse行选择器列是否固定,存在左侧固定列时,强制左侧固定。0.1.0
onlyCurrentbooleanfalse已选中行中,是否只包含当前 data 属性中的行。0.1.0
cellPropsRestAttrs列单元格属性对象。0.1.0
labelCellPropsRestAttrs列表头单元格属性对象。0.1.0
contentPropsRestAttrs列单元格内容属性对象。0.1.0
labelContentPropsRestAttrs列表头内容属性对象。0.1.0

TableExpandable

属性类型可选默认值描述版本
defaultExpandAllRowsbooleanfalse是否默认展开所有行。0.1.0
labelstring展开按钮列的表头文本。0.1.0
widthnumber48展开按钮列宽度。0.1.0
minWidthnumber0展开按钮列最小宽度。0.1.0
fixedbooleanfalse展开按钮列是否固定,存在左侧固定列时,强制左侧固定。0.1.0
cellPropsRestAttrs列单元格属性对象。0.1.0
labelCellPropsRestAttrs列表头单元格属性对象。0.1.0
contentPropsRestAttrs列单元格内容属性对象。0.1.0
labelContentPropsRestAttrs列表头内容属性对象。0.1.0

TableSummary

属性类型可选默认值描述版本
dataTableData | TableData[]总结行数据。0.1.0
placement'end' | 'start''end'总结行位置。0.1.0
summaryTextstring | string[]总结行首列的文本,为数组时,作用于对应下标的总结行,为字符串值时作用于所有总结行。0.1.0
fixedbooleantrue总结行是否固定,当总结行位于数据区域头部时,总结行固定需要 fixedHeadtrue 才生效。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
defaultFilterValueany[] | null默认筛选值。0.1.0
multiplebooleanfalse是否允许选择多选。0.1.0
popoverPropsOmit<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
multiplebooleanfalse是否为多级排序。0.1.0
prioritynumber0多级排序的优先级,数值较大者优先。0.1.0

TablePagination

paginateMethod 字段控制表格组件内部的分页方法,'auto' 时,表格会根据页面容量对数据进行分页。为 'custom' 则不会有此类行为,这个配置常用于后端分页。

ts
export type TablePagination = {
	paginateMethod?: 'custom' | 'auto'
} & PaginationProps &
	EmitEvent<PaginationEvents> &
	RestAttrs

TableOptionsArg

ts
export type TableOptionsArg = {
	rowIndex: number
	colIndex: number
	record: TableData
	column: TableColumn
}

Option

ts
export interface Option<T = any> {
	value: T
	label: string
}

TableFilterOption

ts
export interface TableFilterOption<T = any> extends Option<T> {
	disabled?: boolean
	key?: string | number | symbol
}

SortOrder, FilterValue

ts
export type SortOrder = {
	[key: string | number | symbol]: 'asc' | 'desc' | 'none' | null | undefined
}

export type FilterValue = {
	[key: string | number | symbol]: any[] | null | undefined
}

RestAttrs

ts
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

ts
export type EmitEvent<T extends Record<string, any>> = {
	[K in keyof T as `on${Capitalize<K & string>}`]?: (...args: T[K]) => void
}