排序
点击表头可以对该列进行排序,支持升序、降序和取消排序。
配置说明
ts
interface Column {
sort?: {
sortDirections: ('ascend' | 'descend')[];
sortOrder: 'ascend' | 'descend' | boolean;
sorter: boolean | ((a: ListItem, b: ListItem, extra: { id: string; direction: 'ascend' | 'descend' }) => number);
sortMode?: 'button' | 'toggle';
sortOnHeaderClick?: boolean;
sortIcon?: (data: {
direction: 'ascend' | 'descend';
isActive: boolean;
onClick: () => void;
}) => VNode | JSX.Element;
};
}sortDirections: 支持的排序方向数组sortOrder: 初始排序状态sorter:true: 使用默认排序(按字段值排序)- 函数: 自定义排序函数
sortMode: 排序模式- 默认为
button模式 button: 同时显示升序降序按钮,两个按钮能分开点击toggle: 默认不显示升序降序按钮,点表头第一下是升序,展示升序按钮,点第二下是降序,展示降序按钮,点第三下取消排序,排序icon 消失
- 默认为
sortOnHeaderClick: 控制点击按钮之外的表头其他位置是否触发排序true: 点击表头可触发排序(默认:toggle模式为true,button模式为false)false: 点击表头不触发排序
sortIcon: 自定义排序图标/按钮渲染函数- 函数接收参数:
{ direction: 'ascend' | 'descend', isActive: boolean, onClick: () => void } - 返回
VNode或JSX.Element - 如果不提供,则使用默认的箭头图标
- 函数接收参数:
默认排序配置
通过表格全局配置 defaultSort 可以指定表格初始化时的默认排序:
ts
interface TableOptions {
defaultSort?: {
field: string; // 排序的列字段
order: 'ascend' | 'descend'; // 排序方向
sorter?: ( // 自定义排序函数(可选)
a: ListItem,
b: ListItem,
extra: { field: string; direction: 'ascend' | 'descend' }
) => number;
};
}field: 指定要排序的列字段名order: 指定排序方向,ascend升序,descend降序sorter: 可选的自定义排序函数- 如果不提供,会优先使用列配置中的
sorter - 如果列配置也没有,则使用默认排序(按字段值比较)
- 如果不提供,会优先使用列配置中的
使用示例:
vue
<GridTable
:list="list"
:defaultSort="{
field: 'salary',
order: 'descend',
sorter: (a, b) => a.salary - b.salary,
}"
>
<!-- columns -->
</GridTable>按钮模式 (button)
源码
vue
<template>
<div class="base-view">
<div style="width: 100%; height: 600px; border: 2px solid var(--el-color-border)">
<Grid :columns="columns" :list="list"></Grid>
</div>
</div>
</template>
<script setup lang="ts">
import { Grid, type Column, type ListItem } from 'vue-virt-grid';
const generateList = (length = 50) =>
Array.from({ length }).map((_, rowIndex) => {
const names = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十'];
const departments = ['技术部', '产品部', '设计部', '运营部', '市场部'];
const statuses = ['在职', '离职', '试用期'];
const nameIndex = rowIndex % names.length;
const nameSuffix = Math.floor(rowIndex / names.length) > 0 ? Math.floor(rowIndex / names.length) : '';
return {
id: `row-${rowIndex}`,
parentId: null,
name: names[nameIndex] + nameSuffix,
age: 20 + (rowIndex % 30),
department: departments[rowIndex % departments.length],
salary: 5000 + (rowIndex % 20) * 1000,
status: statuses[rowIndex % statuses.length],
joinDate: `202${rowIndex % 4}-${String((rowIndex % 12) + 1).padStart(2, '0')}-${String((rowIndex % 28) + 1).padStart(2, '0')}`,
};
});
const columns: Column[] = [
{
field: 'name',
title: '姓名',
width: 120,
sort: {
sortDirections: ['ascend', 'descend'],
sortOrder: false,
sorter: true,
sortMode: 'button',
},
},
{
field: 'age',
title: '年龄',
width: 100,
sort: {
sortDirections: ['ascend', 'descend'],
sortOrder: false,
sorter: (a: ListItem, b: ListItem, _extra: { id: string; direction: 'ascend' | 'descend' }) => {
return (a.age as number) - (b.age as number);
},
sortMode: 'button',
},
},
{
field: 'department',
title: '部门',
width: 120,
sort: {
sortDirections: ['ascend', 'descend'],
sortOrder: false,
sorter: true,
sortMode: 'button',
},
},
{
field: 'salary',
title: '薪资',
width: 120,
align: 'right',
sort: {
sortDirections: ['ascend', 'descend'],
sortOrder: false,
sorter: (a: ListItem, b: ListItem, _extra: { id: string; direction: 'ascend' | 'descend' }) => {
return (a.salary as number) - (b.salary as number);
},
sortMode: 'button',
},
},
];
const list: ListItem[] = generateList(50);
</script>
<style lang="scss">
.base-view {
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
</style>切换模式 (toggle)
源码
vue
<template>
<div class="base-view">
<div style="width: 100%; height: 600px; border: 2px solid var(--el-color-border)">
<Grid :columns="columns" :list="list"></Grid>
</div>
</div>
</template>
<script setup lang="ts">
import { Grid, type Column, type ListItem } from 'vue-virt-grid';
const generateList = (length = 50) =>
Array.from({ length }).map((_, rowIndex) => {
const names = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十'];
const departments = ['技术部', '产品部', '设计部', '运营部', '市场部'];
const statuses = ['在职', '离职', '试用期'];
const nameIndex = rowIndex % names.length;
const nameSuffix = Math.floor(rowIndex / names.length) > 0 ? Math.floor(rowIndex / names.length) : '';
return {
id: `row-${rowIndex}`,
parentId: null,
name: names[nameIndex] + nameSuffix,
age: 20 + (rowIndex % 30),
department: departments[rowIndex % departments.length],
salary: 5000 + (rowIndex % 20) * 1000,
status: statuses[rowIndex % statuses.length],
joinDate: `202${rowIndex % 4}-${String((rowIndex % 12) + 1).padStart(2, '0')}-${String((rowIndex % 28) + 1).padStart(2, '0')}`,
};
});
const columns: Column[] = [
{
field: 'name',
title: '姓名',
width: 120,
sort: {
sortDirections: ['ascend', 'descend'],
sortOrder: false,
sorter: true,
sortMode: 'toggle',
},
},
{
field: 'age',
title: '年龄',
width: 100,
sort: {
sortDirections: ['ascend', 'descend'],
sortOrder: false,
sorter: (a: ListItem, b: ListItem, _extra: { id: string; direction: 'ascend' | 'descend' }) => {
return (a.age as number) - (b.age as number);
},
sortMode: 'toggle',
},
},
{
field: 'department',
title: '部门',
width: 120,
sort: {
sortDirections: ['ascend', 'descend'],
sortOrder: false,
sorter: true,
sortMode: 'toggle',
},
},
{
field: 'salary',
title: '薪资',
width: 120,
align: 'right',
sort: {
sortDirections: ['ascend', 'descend'],
sortOrder: false,
sorter: (a: ListItem, b: ListItem, _extra: { id: string; direction: 'ascend' | 'descend' }) => {
return (a.salary as number) - (b.salary as number);
},
sortMode: 'toggle',
},
},
];
const list: ListItem[] = generateList(50);
</script>
<style lang="scss">
.base-view {
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
</style>控制表头点击行为
源码
vue
<template>
<div class="base-view">
<div style="width: 100%; height: 600px; border: 2px solid var(--el-color-border)">
<Grid :columns="columns" :list="list"></Grid>
</div>
</div>
</template>
<script setup lang="ts">
import { Grid, type Column, type ListItem } from 'vue-virt-grid';
const generateList = (length = 50) =>
Array.from({ length }).map((_, rowIndex) => {
const names = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十'];
const departments = ['技术部', '产品部', '设计部', '运营部', '市场部'];
const statuses = ['在职', '离职', '试用期'];
const nameIndex = rowIndex % names.length;
const nameSuffix = Math.floor(rowIndex / names.length) > 0 ? Math.floor(rowIndex / names.length) : '';
return {
id: `row-${rowIndex}`,
parentId: null,
name: names[nameIndex] + nameSuffix,
age: 20 + (rowIndex % 30),
department: departments[rowIndex % departments.length],
salary: 5000 + (rowIndex % 20) * 1000,
status: statuses[rowIndex % statuses.length],
joinDate: `202${rowIndex % 4}-${String((rowIndex % 12) + 1).padStart(2, '0')}-${String((rowIndex % 28) + 1).padStart(2, '0')}`,
};
});
const columns: Column[] = [
{
field: 'name',
title: '姓名(按钮模式,点击表头可排序)',
width: 180,
sort: {
sortDirections: ['ascend', 'descend'],
sortOrder: false,
sorter: true,
sortMode: 'button',
sortOnHeaderClick: true,
},
},
{
field: 'age',
title: '年龄(按钮模式,点击表头不排序)',
width: 200,
sort: {
sortDirections: ['ascend', 'descend'],
sortOrder: false,
sorter: (_a: ListItem, _b: ListItem, _extra: { id: string; direction: 'ascend' | 'descend' }) => {
return (_a.age as number) - (_b.age as number);
},
sortMode: 'button',
sortOnHeaderClick: false,
},
},
{
field: 'department',
title: '部门(切换模式,点击表头不排序)',
width: 200,
sort: {
sortDirections: ['ascend', 'descend'],
sortOrder: false,
sorter: true,
sortMode: 'toggle',
sortOnHeaderClick: false,
},
},
{
field: 'salary',
title: '薪资(切换模式,点击表头可排序)',
width: 200,
align: 'right',
sort: {
sortDirections: ['ascend', 'descend'],
sortOrder: false,
sorter: (_a: ListItem, _b: ListItem, _extra: { id: string; direction: 'ascend' | 'descend' }) => {
return (_a.salary as number) - (_b.salary as number);
},
sortMode: 'toggle',
sortOnHeaderClick: true,
},
},
];
const list: ListItem[] = generateList(50);
</script>
<style lang="scss">
.base-view {
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
</style>自定义排序图标/按钮
源码
vue
<template>
<div class="base-view">
<div style="width: 100%; height: 600px; border: 2px solid var(--el-color-border)">
<Grid :columns="columns" :list="list"></Grid>
</div>
</div>
</template>
<script setup lang="ts">
import { h } from 'vue';
import { Grid, type Column, type ListItem } from 'vue-virt-grid';
const generateList = (length = 50) =>
Array.from({ length }).map((_, rowIndex) => {
const names = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十'];
const departments = ['技术部', '产品部', '设计部', '运营部', '市场部'];
const nameIndex = rowIndex % names.length;
const nameSuffix = Math.floor(rowIndex / names.length) > 0 ? Math.floor(rowIndex / names.length) : '';
return {
id: `row-${rowIndex}`,
parentId: null,
name: names[nameIndex] + nameSuffix,
age: 20 + (rowIndex % 30),
department: departments[rowIndex % departments.length],
salary: 5000 + (rowIndex % 20) * 1000,
};
});
const customSortIcon = (data: {
direction: 'ascend' | 'descend';
isActive: boolean;
onClick: () => void;
}) => {
return h(
'span',
{
class: [
'custom-sort-icon',
{
'is-active': data.isActive,
'is-clickable': !!data.onClick,
},
],
onClick: data.onClick ? (e: Event) => {
e.stopPropagation();
data.onClick();
} : undefined,
style: {
display: 'inline-block',
width: '16px',
height: '16px',
lineHeight: '16px',
textAlign: 'center',
marginLeft: '4px',
cursor: data.onClick ? 'pointer' : 'default',
color: data.isActive ? '#1890ff' : '#bfbfbf',
transition: 'color 0.2s',
fontSize: '12px',
},
},
data.direction === 'ascend' ? '▲' : '▼'
);
};
const columns: Column[] = [
{
field: 'name',
title: '姓名(自定义图标)',
width: 150,
sort: {
sortDirections: ['ascend', 'descend'],
sortOrder: false,
sorter: true,
sortMode: 'button',
sortIcon: customSortIcon,
},
},
{
field: 'age',
title: '年龄(默认图标)',
width: 150,
sort: {
sortDirections: ['ascend', 'descend'],
sortOrder: false,
sorter: (_a: ListItem, _b: ListItem, _extra: { id: string; direction: 'ascend' | 'descend' }) => {
return (_a.age as number) - (_b.age as number);
},
sortMode: 'button',
},
},
{
field: 'department',
title: '部门(切换模式+自定义图标)',
width: 200,
sort: {
sortDirections: ['ascend', 'descend'],
sortOrder: false,
sorter: true,
sortMode: 'toggle',
sortIcon: customSortIcon,
},
},
{
field: 'salary',
title: '薪资(自定义按钮样式)',
width: 180,
align: 'right',
sort: {
sortDirections: ['ascend', 'descend'],
sortOrder: false,
sorter: (_a: ListItem, _b: ListItem, _extra: { id: string; direction: 'ascend' | 'descend' }) => {
return (_a.salary as number) - (_b.salary as number);
},
sortMode: 'button',
sortIcon: (data) => {
return h(
'button',
{
class: [
'custom-sort-button',
{
'is-active': data.isActive,
},
],
onClick: data.onClick ? (e: Event) => {
e.stopPropagation();
data.onClick();
} : undefined,
style: {
padding: '2px 6px',
marginLeft: '4px',
border: '1px solid #d9d9d9',
borderRadius: '2px',
background: data.isActive ? '#1890ff' : '#fff',
color: data.isActive ? '#fff' : '#333',
cursor: data.onClick ? 'pointer' : 'default',
fontSize: '12px',
transition: 'all 0.2s',
},
},
data.direction === 'ascend' ? '↑' : '↓'
);
},
},
},
];
const list: ListItem[] = generateList(50);
</script>
<style lang="scss">
.base-view {
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
</style>完整示例
包含索引、边框、斑马纹、自定义表头 icon、按钮控制排序、自定义列排序规则、带操作按钮列、左右冻结列、默认排序(按薪资降序) 于一体的完整示例:
源码
vue
<template>
<div class="base-view">
<div style="width: 100%; height: 600px; border: 2px solid var(--el-color-border)">
<div style="padding: 16px; display: flex; align-items: center; gap: 16px; flex-wrap: wrap;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 14px; color: #333;">指定列:</span>
<ElSelect
:model-value="selectedColumnId || undefined"
placeholder="请选择列"
style="width: 200px;"
clearable
@change="handleColumnChange"
>
<ElOption
v-for="col in sortableColumns"
:key="col.field"
:label="col.title"
:value="col.field"
/>
</ElSelect>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 14px; color: #333;">排序:</span>
<ElButtonGroup>
<ElButton
:type="sortDirection === 'ascend' ? 'primary' : 'default'"
:disabled="!selectedColumnId"
@click="handleSort('ascend')"
>
升序
</ElButton>
<ElButton
:type="sortDirection === 'descend' ? 'primary' : 'default'"
:disabled="!selectedColumnId"
@click="handleSort('descend')"
>
降序
</ElButton>
<ElButton
:disabled="!selectedColumnId || !sortDirection"
@click="handleSort(null)"
>
取消排序
</ElButton>
</ElButtonGroup>
</div>
<ElButton type="success" @click="handlePrintData">
打印数据
</ElButton>
</div>
<hr>
<GridTable
ref="gridTableRef"
:list="sortedList"
border
stripe
:showTreeLine="false"
:minRowHeight="50"
:defaultSort="defaultSortConfig"
>
<GridTableColumn
type="index"
field="index"
title="#"
:width="60"
fixed="left"
align="center"
/>
<GridTableColumn
field="name"
title="姓名"
:width="150"
fixed="left"
:resizable="true"
:minWidth="100"
:maxWidth="220"
:sort="{
sortDirections: ['ascend', 'descend'],
sortOrder: false,
sorter: true,
sortMode: 'button',
}"
>
<template #sortIcon="{ direction, isActive, onClick }">
<span
:class="[
'custom-sort-icon',
{
'is-active': isActive,
'is-clickable': onClick !== undefined,
},
]"
:style="{
display: 'inline-block',
width: '14px',
height: '14px',
lineHeight: '14px',
textAlign: 'center',
marginLeft: '4px',
cursor: onClick !== undefined ? 'pointer' : 'default',
color: isActive ? '#1890ff' : '#bfbfbf',
transition: 'color 0.2s',
fontSize: '11px',
fontWeight: 'bold',
}"
@click.stop="onClick?.()"
>
{{ direction === 'ascend' ? '▲' : '▼' }}
</span>
</template>
<template #default="{ row }">
<div style="display: flex; align-items: center; gap: 8px; padding: 4px 0;">
<div
style="
width: 32px;
height: 32px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 14px;
font-weight: bold;
flex-shrink: 0;
"
>
{{ row.name.charAt(0) }}
</div>
<div>
<div style="font-size: 14px; font-weight: 500; color: #333;">
{{ row.name }}
</div>
<div style="font-size: 12px; color: #999;">
ID: {{ row.id }}
</div>
</div>
</div>
</template>
</GridTableColumn>
<GridTableColumn
field="age"
title="年龄"
:width="100"
align="center"
:resizable="true"
:minWidth="60"
:maxWidth="150"
:sort="{
sortDirections: ['ascend', 'descend'],
sortOrder: false,
sorter: ageSorter,
sortMode: 'button',
}"
>
<template #sortIcon="{ direction, isActive, onClick }">
<span
:class="[
'custom-sort-icon',
{
'is-active': isActive,
'is-clickable': onClick !== undefined,
},
]"
:style="{
display: 'inline-block',
width: '14px',
height: '14px',
lineHeight: '14px',
textAlign: 'center',
marginLeft: '4px',
cursor: onClick !== undefined ? 'pointer' : 'default',
color: isActive ? '#1890ff' : '#bfbfbf',
transition: 'color 0.2s',
fontSize: '11px',
fontWeight: 'bold',
}"
@click.stop="onClick?.()"
>
{{ direction === 'ascend' ? '▲' : '▼' }}
</span>
</template>
</GridTableColumn>
<GridTableColumn
field="department"
title="部门"
:width="150"
:resizable="true"
:minWidth="100"
:maxWidth="250"
:sort="{
sortDirections: ['ascend', 'descend'],
sortOrder: false,
sorter: true,
sortMode: 'button',
sortOnHeaderClick: true,
}"
>
<template #default="{ row }">
<div style="display: flex; flex-direction: column; gap: 4px; padding: 4px 0;">
<div style="font-weight: 500; font-size: 14px;">
{{ row.department }}
</div>
<div style="display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px;">
<span
v-for="tag in row.tags"
:key="tag"
style="
display: inline-block;
padding: 2px 8px;
background: #f0f0f0;
border-radius: 4px;
font-size: 11px;
color: #666;
"
>
{{ tag }}
</span>
</div>
</div>
</template>
</GridTableColumn>
<GridTableColumn
field="city"
title="城市"
:width="100"
:resizable="true"
:minWidth="60"
:maxWidth="150"
:sort="{
sortDirections: ['ascend', 'descend'],
sortOrder: false,
sorter: true,
sortMode: 'button',
}"
/>
<GridTableColumn
field="inputValue"
title="输入框"
:width="180"
:resizable="true"
:minWidth="120"
:maxWidth="250"
>
<template #cover="{ row }">
<ElInput
v-model="row.inputValue"
style="width: 100%;"
placeholder="请输入"
size="small"
/>
</template>
</GridTableColumn>
<GridTableColumn
field="selectValue"
title="下拉选择"
:width="180"
:resizable="true"
:minWidth="120"
:maxWidth="250"
>
<template #cover="{ row }">
<ElSelect
v-model="row.selectValue"
style="width: 100%;"
size="small"
placeholder="请选择"
>
<ElOption label="选项一" value="option1" />
<ElOption label="选项二" value="option2" />
<ElOption label="选项三" value="option3" />
</ElSelect>
</template>
</GridTableColumn>
<GridTableColumn
field="checkboxValue"
title="复选框"
:width="100"
align="center"
:resizable="true"
:minWidth="80"
:maxWidth="150"
>
<template #default="{ row }">
<ElCheckbox v-model="row.checkboxValue" />
</template>
</GridTableColumn>
<GridTableColumn
field="radioValue"
title="单选框"
:width="150"
align="center"
:resizable="true"
:minWidth="100"
:maxWidth="200"
>
<template #default="{ row }">
<ElRadioGroup v-model="row.radioValue" size="small">
<ElRadio value="radio1">选项1</ElRadio>
<ElRadio value="radio2">选项2</ElRadio>
<ElRadio value="radio3">选项3</ElRadio>
</ElRadioGroup>
</template>
</GridTableColumn>
<GridTableColumn
field="switchValue"
title="开关"
:width="100"
align="center"
:resizable="true"
:minWidth="80"
:maxWidth="150"
>
<template #default="{ row }">
<ElSwitch v-model="row.switchValue" />
</template>
</GridTableColumn>
<GridTableColumn
field="dateValue"
title="日期选择"
:width="180"
:resizable="true"
:minWidth="140"
:maxWidth="250"
>
<template #default="{ row }">
<div style="padding: 4px 0; color: #333; font-size: 13px;">
{{ row.dateValue }}
</div>
</template>
<template #cover="{ row }">
<ElDatePicker
v-model="row.dateValue"
value-format="YYYY-MM-DD"
style="width: 100%;"
size="small"
placeholder="选择日期"
type="date"
/>
</template>
</GridTableColumn>
<GridTableColumn
field="complex"
title="复合组件"
:width="280"
:resizable="true"
:minWidth="200"
:maxWidth="400"
>
<template #cover="{ row }">
<div style="display: flex; flex-direction: column; gap: 8px; padding: 4px;">
<div style="display: flex; align-items: center; gap: 8px;">
<ElInput
v-model="row.inputValue"
placeholder="输入框"
size="small"
style="flex: 1;"
/>
<ElSelect
v-model="row.selectValue"
size="small"
style="width: 120px;"
placeholder="选择"
>
<ElOption label="选项一" value="option1" />
<ElOption label="选项二" value="option2" />
<ElOption label="选项三" value="option3" />
</ElSelect>
</div>
<div style="display: flex; align-items: center; gap: 12px;">
<ElCheckbox v-model="row.checkboxValue">复选框</ElCheckbox>
<ElSwitch v-model="row.switchValue" />
<ElDatePicker
v-model="row.dateValue"
value-format="YYYY-MM-DD"
size="small"
style="flex: 1;"
type="date"
/>
</div>
<div style="display: flex; gap: 8px;">
<ElButton size="small" type="primary">
保存
</ElButton>
<ElButton size="small">取消</ElButton>
</div>
</div>
</template>
</GridTableColumn>
<GridTableColumn
field="salary"
title="薪资"
:width="120"
align="right"
:resizable="true"
:minWidth="80"
:maxWidth="200"
:sort="{
sortDirections: ['ascend', 'descend'],
sortOrder: false,
sorter: salarySorter,
sortMode: 'button',
}"
/>
<GridTableColumn
field="status"
title="状态"
:width="120"
align="center"
:resizable="true"
:minWidth="80"
:maxWidth="180"
:sort="{
sortDirections: ['ascend', 'descend'],
sortOrder: false,
sorter: statusSorter,
sortMode: 'button',
}"
>
<template #default="{ row }">
<div
:style="{
display: 'inline-block',
padding: '4px 12px',
borderRadius: '12px',
fontSize: '12px',
fontWeight: '500',
color: getStatusConfig(row.status).color,
background: getStatusConfig(row.status).bg,
border: `1px solid ${getStatusConfig(row.status).color}33`,
}"
>
{{ row.status }}
</div>
</template>
</GridTableColumn>
<GridTableColumn
field="description"
title="工作描述"
:width="250"
:resizable="true"
:minWidth="150"
:maxWidth="400"
>
<template #default="{ row }">
<div style="padding: 8px 0; line-height: 1.6; font-size: 13px; color: #333;">
{{ row.description || '' }}
</div>
</template>
</GridTableColumn>
<GridTableColumn
field="skills"
title="技能"
:width="200"
:resizable="true"
:minWidth="120"
:maxWidth="300"
>
<template #default="{ row }">
<div style="display: flex; flex-wrap: wrap; gap: 6px; padding: 6px 0;">
<span
v-for="skill in row.skills"
:key="skill"
style="
display: inline-block;
padding: 4px 10px;
background: #e6f7ff;
border: 1px solid #91d5ff;
border-radius: 12px;
font-size: 12px;
color: #1890ff;
"
>
{{ skill }}
</span>
</div>
</template>
</GridTableColumn>
<GridTableColumn
field="email"
title="联系方式"
:width="220"
:resizable="true"
:minWidth="150"
:maxWidth="350"
>
<template #default="{ row }">
<div style="display: flex; flex-direction: column; gap: 6px; padding: 6px 0;">
<div style="font-size: 13px; color: #1890ff;">
📧 {{ row.email }}
</div>
<div style="font-size: 13px; color: #666;">
📱 {{ row.phone }}
</div>
</div>
</template>
</GridTableColumn>
<GridTableColumn
field="projects"
title="参与项目"
:width="180"
:resizable="true"
:minWidth="120"
:maxWidth="280"
>
<template #default="{ row }">
<div v-if="!row.projects || row.projects.length === 0" style="padding: 6px 0; color: #999; font-size: 12px; font-style: italic;">
暂无项目
</div>
<div v-else style="display: flex; flex-direction: column; gap: 4px; padding: 6px 0;">
<div
v-for="project in row.projects"
:key="project"
style="display: flex; align-items: center; gap: 6px; font-size: 13px;"
>
<span
style="
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: #52c41a;
"
/>
<span>{{ project }}</span>
</div>
</div>
</template>
</GridTableColumn>
<GridTableColumn
field="joinDate"
title="入职日期"
:width="120"
:resizable="true"
:minWidth="100"
:maxWidth="180"
:sort="{
sortDirections: ['ascend', 'descend'],
sortOrder: false,
sorter: joinDateSorter,
sortMode: 'button',
}"
/>
<GridTableColumn
field="departmentSelect"
title="部门选择器"
:width="180"
:resizable="true"
:minWidth="140"
:maxWidth="250"
:sort="{
sortDirections: ['ascend', 'descend'],
sortOrder: false,
sorter: true,
sortMode: 'button',
}"
>
<template #cover="{ row }">
<ElSelect
v-model="row.departmentSelect"
size="small"
style="width: 100%;"
placeholder="请选择部门"
filterable
clearable
>
<ElOption
v-for="dept in departments"
:key="dept"
:label="dept"
:value="dept"
/>
</ElSelect>
</template>
</GridTableColumn>
<GridTableColumn
field="actions"
title="操作"
:width="180"
fixed="right"
align="center"
>
<template #default="{ row }">
<div style="display: flex; gap: 8px; justify-content: center;">
<ElButton size="small" type="primary" @click="() => handleEdit(row)">
编辑
</ElButton>
<ElButton size="small" type="danger" @click="() => handleDelete(row)">
删除
</ElButton>
</div>
</template>
</GridTableColumn>
</GridTable>
</div>
</div>
</template>
<script setup lang="tsx">
import { ref, computed } from 'vue';
import { GridTable, GridTableColumn, type ListItem } from 'vue-virt-grid';
import {
ElInput,
ElSelect,
ElOption,
ElCheckbox,
ElRadio,
ElRadioGroup,
ElDatePicker,
ElSwitch,
ElButton,
ElButtonGroup,
} from 'element-plus';
import 'element-plus/dist/index.css';
// 部门选项
const departments = ['技术部', '产品部', '设计部', '运营部', '市场部', '人事部', '财务部'];
const generateList = (length = 100) =>
Array.from({ length }).map((_, rowIndex) => {
const names = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十', '郑十一', '王十二'];
const statuses = ['在职', '离职', '试用期'];
const cities = ['北京', '上海', '广州', '深圳', '杭州', '成都'];
const tags = ['前端', '后端', '全栈', '架构师', '高级', '资深', '专家'];
const descriptions = [
'负责核心业务系统开发,参与技术方案设计',
'负责前端架构优化,提升用户体验和性能',
'负责后端服务开发,处理高并发场景',
'负责全栈开发,包括前端和后端系统',
'负责系统架构设计,技术选型和团队管理',
'负责产品功能开发,与产品经理协作',
'负责代码审查,技术培训和团队建设',
];
const nameIndex = rowIndex % names.length;
const nameSuffix = Math.floor(rowIndex / names.length) > 0 ? Math.floor(rowIndex / names.length) : '';
const tagCount = (rowIndex % 4) + 1;
const descriptionIndex = rowIndex % descriptions.length;
const hasLongDescription = rowIndex % 3 === 0;
return {
id: `row-${rowIndex}`,
parentId: null,
name: names[nameIndex] + nameSuffix,
age: 20 + (rowIndex % 30),
department: departments[rowIndex % departments.length],
salary: 5000 + (rowIndex % 20) * 1000,
status: statuses[rowIndex % statuses.length],
city: cities[rowIndex % cities.length],
email: `user${rowIndex}@example.com`,
phone: `138${String(rowIndex).padStart(8, '0')}`,
joinDate: `202${rowIndex % 4}-${String((rowIndex % 12) + 1).padStart(2, '0')}-${String((rowIndex % 28) + 1).padStart(2, '0')}`,
tags: tags.slice(0, tagCount),
description: hasLongDescription
? `${descriptions[descriptionIndex]}。这是一个比较长的描述内容,用来展示单元格可以包含多行文本,并且不同行的内容长度不同,从而产生不同的行高。`
: descriptions[descriptionIndex],
skills: ['Vue', 'React', 'TypeScript', 'Node.js', 'Python', 'Java'].slice(0, (rowIndex % 3) + 2),
projects: rowIndex % 5 === 0 ? ['项目A', '项目B', '项目C'] : rowIndex % 3 === 0 ? ['项目A'] : [],
inputValue: `输入值${rowIndex}`,
selectValue: rowIndex % 3 === 0 ? 'option1' : rowIndex % 3 === 1 ? 'option2' : 'option3',
checkboxValue: rowIndex % 2 === 0,
radioValue: rowIndex % 3 === 0 ? 'radio1' : rowIndex % 3 === 1 ? 'radio2' : 'radio3',
switchValue: rowIndex % 2 === 0,
dateValue: `202${rowIndex % 4}-${String((rowIndex % 12) + 1).padStart(2, '0')}-${String((rowIndex % 28) + 1).padStart(2, '0')}`,
departmentSelect: departments[rowIndex % departments.length], // 用于部门选择器
};
});
const ageSorter = (_a: ListItem, _b: ListItem) => {
return (_a.age as number) - (_b.age as number);
};
const salarySorter = (_a: ListItem, _b: ListItem) => {
const salaryA = _a.salary as number;
const salaryB = _b.salary as number;
if (salaryA === salaryB) return 0;
return salaryA > salaryB ? 1 : -1;
};
const statusSorter = (_a: ListItem, _b: ListItem) => {
const statusOrder = { '在职': 1, '试用期': 2, '离职': 3 };
const orderA = statusOrder[_a.status as keyof typeof statusOrder] || 0;
const orderB = statusOrder[_b.status as keyof typeof statusOrder] || 0;
return orderA - orderB;
};
const joinDateSorter = (_a: ListItem, _b: ListItem) => {
const dateA = new Date(_a.joinDate as string).getTime();
const dateB = new Date(_b.joinDate as string).getTime();
return dateA - dateB;
};
/**
* 默认排序配置
* 表格初始化时会自动应用此排序配置
*
* - field: 指定要排序的列字段
* - order: 排序方向,'ascend' 升序 | 'descend' 降序
* - sorter: 可选,自定义排序函数。如果不提供,会使用列配置中的 sorter 或默认排序
*/
const defaultSortConfig = {
field: 'salary', // 按薪资列排序
order: 'descend' as const, // 降序排列(薪资从高到低)
sorter: salarySorter, // 使用自定义排序函数
};
const getStatusConfig = (status: string) => {
const statusConfig: Record<string, { color: string; bg: string }> = {
'在职': { color: '#52c41a', bg: '#f6ffed' },
'试用期': { color: '#faad14', bg: '#fffbe6' },
'离职': { color: '#ff4d4f', bg: '#fff1f0' },
};
return statusConfig[status] || { color: '#666', bg: '#f5f5f5' };
};
const handleEdit = (row: any) => {
alert(`编辑: ${row.name}`);
};
const handleDelete = (row: any) => {
alert(`删除: ${row.name}`);
};
const list: ListItem[] = generateList(100);
const gridTableRef = ref<InstanceType<typeof GridTable> | null>(null);
// 排序控制
const selectedColumnId = ref<string | null>(null);
const sortDirection = ref<'ascend' | 'descend' | null>(null);
const sortedList = ref<ListItem[]>(list);
// 可排序的列配置(使用 field 作为标识)
const sortableColumnsConfig = [
{ field: 'name', title: '姓名', sorter: undefined },
{ field: 'age', title: '年龄', sorter: ageSorter },
{ field: 'department', title: '部门', sorter: undefined },
{ field: 'city', title: '城市', sorter: undefined },
{ field: 'salary', title: '薪资', sorter: salarySorter },
{ field: 'status', title: '状态', sorter: statusSorter },
{ field: 'joinDate', title: '入职日期', sorter: joinDateSorter },
];
// 获取所有可排序的列
const sortableColumns = computed(() => {
return sortableColumnsConfig;
});
// 列选择变化
const handleColumnChange = (value: string | null) => {
selectedColumnId.value = value;
if (!selectedColumnId.value) {
sortDirection.value = null;
sortedList.value = [...list];
} else {
// 如果之前有排序,保持排序方向
if (!sortDirection.value) {
sortDirection.value = null;
}
}
};
// 执行排序
const handleSort = (direction: 'ascend' | 'descend' | null) => {
if (!selectedColumnId.value) return;
if (direction === null) {
// 取消排序
sortedList.value = [...list];
sortDirection.value = null;
} else {
// 执行排序
const column = sortableColumnsConfig.find((col) => col.field === selectedColumnId.value);
if (!column) return;
const sorted = [...list].sort((a, b) => {
let result = 0;
if (column.sorter) {
result = column.sorter(a, b);
} else {
// 默认排序
const aVal = a[column.field as keyof ListItem];
const bVal = b[column.field as keyof ListItem];
if (aVal === bVal) return 0;
result = aVal > bVal ? 1 : -1;
}
return direction === 'ascend' ? result : -result;
});
sortedList.value = sorted;
sortDirection.value = direction;
}
};
// 打印当前数据
const handlePrintData = () => {
try {
const currentData = sortedList.value;
console.log('当前表格数据:', currentData);
console.log('数据条数:', currentData.length);
console.table(currentData.slice(0, 10)); // 打印前10条数据
alert(`已打印数据到控制台,共 ${currentData.length} 条数据`);
} catch (error) {
console.error('打印数据失败:', error);
alert('打印数据失败,请查看控制台');
}
};
</script>
<style lang="scss">
.base-view {
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
</style>