diff --git a/packages/devui-vue/devui/table/__tests__/table.spec.tsx b/packages/devui-vue/devui/table/__tests__/table.spec.tsx index a3b507782e..ec50c8e7b9 100644 --- a/packages/devui-vue/devui/table/__tests__/table.spec.tsx +++ b/packages/devui-vue/devui/table/__tests__/table.spec.tsx @@ -8,27 +8,27 @@ let data: Array> = []; describe('d-table', () => { beforeEach(() => { data = [ - { - firstName: 'Mark', - lastName: 'Otto', - date: '1990/01/11', - gender: 'Male', - }, { firstName: 'Jacob', lastName: 'Thornton', gender: 'Female', date: '1990/01/12', }, + { + firstName: 'Mark', + lastName: 'Otto', + date: '1990/01/11', + gender: 'Male', + }, { firstName: 'Danni', lastName: 'Chen', - gender: 'Male', + gender: 'Female', date: '1990/01/13', }, { - firstName: 'green', - lastName: 'gerong', + firstName: 'Green', + lastName: 'Gerong', gender: 'Male', date: '1990/01/14', }, @@ -232,4 +232,44 @@ describe('d-table', () => { expect(tableHeader.findAll('tr')[0].findAll('th')[1].attributes('rowspan')).toBe('2'); wrapper.unmount(); }); + + it('sort', async () => { + const handleSortChange = jest.fn(); + const wrapper = mount({ + setup() { + const sortDateMethod = (a, b) => { + return a.date > b.date; + }; + return () => ( + + + + + + + ); + }, + }); + + await nextTick(); + await nextTick(); + + const table = wrapper.find('.devui-table'); + const tableHeader = table.find('.devui-table__thead'); + const lastTh = tableHeader.find('tr').findAll('th')[3]; + expect(lastTh.classes()).toContain('sort-active'); + + const tableBody = table.find('.devui-table__tbody'); + const lastTd = tableBody.find('tr').findAll('td')[3]; + expect(lastTd.text()).toBe('1990/01/11'); + + const sortIcon = lastTh.find('.sort-clickable'); + await sortIcon.trigger('click'); + expect(lastTd.text()).toBe('1990/01/14'); + expect(handleSortChange).toBeCalled(); + + await sortIcon.trigger('click'); + expect(lastTd.text()).toBe('1990/01/12'); + expect(handleSortChange).toBeCalled(); + }); }); diff --git a/packages/devui-vue/devui/table/src/components/body-td/body-td.tsx b/packages/devui-vue/devui/table/src/components/body-td/body-td.tsx index 63afeb21c5..4ea65f1313 100644 --- a/packages/devui-vue/devui/table/src/components/body-td/body-td.tsx +++ b/packages/devui-vue/devui/table/src/components/body-td/body-td.tsx @@ -2,7 +2,7 @@ import { defineComponent, toRef, inject } from 'vue'; import type { PropType } from 'vue'; import { Column } from '../column/column-types'; import { TABLE_TOKEN } from '../../table-types'; -import { useFixedColumn } from '../../composable/use-table'; +import { useFixedColumn } from '../../composables/use-table'; export default defineComponent({ name: 'DTableBodyTd', diff --git a/packages/devui-vue/devui/table/src/components/column/column-types.ts b/packages/devui-vue/devui/table/src/components/column/column-types.ts index b86a552d6f..4b2cf93fb3 100644 --- a/packages/devui-vue/devui/table/src/components/column/column-types.ts +++ b/packages/devui-vue/devui/table/src/components/column/column-types.ts @@ -5,10 +5,12 @@ import { TableStore } from '../../store/store-types'; // eslint-disable-next-line no-use-before-define export type Formatter = (row: DefaultRow, column: Column, cellValue: any, rowIndex: number) => VNode; -export type CompareFn = (field: string, a: T, b: T) => boolean; +export type SortMethod = (a: T, b: T) => boolean; export type ColumnType = 'checkable' | 'index' | ''; +export type SortDirection = 'ASC' | 'DESC' | ''; + export interface FilterConfig { id: number | string; name: string; @@ -47,9 +49,12 @@ export const tableColumnProps = { type: Boolean, default: false, }, - compareFn: { - type: Function as PropType, - default: (field: string, a: any, b: any): boolean => a[field] < b[field], + sortDirection: { + type: String as PropType, + default: '', + }, + sortMethod: { + type: Function as PropType, }, filterable: { type: Boolean, @@ -92,6 +97,7 @@ export interface Column { header?: string; order?: number; sortable?: boolean; + sortDirection: SortDirection; filterable?: boolean; filterMultiple?: boolean; filterList?: FilterConfig[]; @@ -100,7 +106,7 @@ export interface Column { renderHeader?: (column: Column, store: TableStore) => VNode; renderCell?: (rowData: DefaultRow, columnItem: Column, store: TableStore, rowIndex: number) => VNode; formatter?: Formatter; - compareFn?: CompareFn; + sortMethod: SortMethod; customFilterTemplate?: CustomFilterSlot; subColumns?: Slot; } diff --git a/packages/devui-vue/devui/table/src/components/column/use-column.ts b/packages/devui-vue/devui/table/src/components/column/use-column.ts index 49b209ed0d..44c7d5044e 100644 --- a/packages/devui-vue/devui/table/src/components/column/use-column.ts +++ b/packages/devui-vue/devui/table/src/components/column/use-column.ts @@ -12,10 +12,11 @@ export function createColumn(props: ToRefs, slots: Slots): Col field, header, sortable, + sortDirection, width, minWidth, formatter, - compareFn, + sortMethod, filterable, filterList, filterMultiple, @@ -51,10 +52,15 @@ export function createColumn(props: ToRefs, slots: Slots): Col ); // 排序功能 - watch([sortable, compareFn], ([sortableVal, compareFnVal]) => { - column.sortable = sortableVal; - column.compareFn = compareFnVal; - }); + watch( + [sortable, sortDirection, sortMethod], + ([sortableVal, sortDirectionVal, sortMethodVal]) => { + column.sortable = sortableVal; + column.sortDirection = sortDirectionVal; + column.sortMethod = sortMethodVal; + }, + { immediate: true } + ); // 过滤功能 watch( diff --git a/packages/devui-vue/devui/table/src/components/header-th/header-th.tsx b/packages/devui-vue/devui/table/src/components/header-th/header-th.tsx index bda79e50b3..3cc945e180 100644 --- a/packages/devui-vue/devui/table/src/components/header-th/header-th.tsx +++ b/packages/devui-vue/devui/table/src/components/header-th/header-th.tsx @@ -2,9 +2,9 @@ import { defineComponent, inject, toRefs } from 'vue'; import type { PropType } from 'vue'; import { Column } from '../column/column-types'; import { TABLE_TOKEN } from '../../table-types'; -import { Sort } from '../sort'; +import Sort from '../sort/sort'; import { Filter } from '../filter'; -import { useFixedColumn } from '../../composable/use-table'; +import { useFixedColumn } from '../../composables/use-table'; import { useSort, useFilter } from './use-header-th'; export default defineComponent({ @@ -15,22 +15,25 @@ export default defineComponent({ required: true, }, }, - setup(props: { column: Column }) { + setup(props: { column: Column }, { expose }) { const table = inject(TABLE_TOKEN); + const store = table.store; const { column } = toRefs(props); - const directionRef = useSort(table.store, column); - const filteredRef = useFilter(table.store, column); + const { direction, sortClass, handleSort, clearSortOrder } = useSort(column); + const filteredRef = useFilter(store, column); const { stickyClass, stickyStyle } = useFixedColumn(column); + expose({ clearSortOrder }); + return () => ( - +
- {props.column.renderHeader?.(column.value, table.store)} - {props.column.filterable && ( + {column.value.renderHeader?.(column.value, store)} + {column.value.filterable && ( )} + {column.value.sortable && }
- {props.column.sortable && } ); }, diff --git a/packages/devui-vue/devui/table/src/components/header-th/use-header-th.ts b/packages/devui-vue/devui/table/src/components/header-th/use-header-th.ts index ba631f20d2..7ccf17e7ef 100644 --- a/packages/devui-vue/devui/table/src/components/header-th/use-header-th.ts +++ b/packages/devui-vue/devui/table/src/components/header-th/use-header-th.ts @@ -1,22 +1,48 @@ -import { ref, watch, Ref, shallowRef } from 'vue'; -import { Column, FilterResults } from '../column/column-types'; +import { ref, watch, Ref, shallowRef, computed, getCurrentInstance, inject, onMounted } from 'vue'; +import type { ComputedRef } from 'vue'; +import { Column, FilterResults, SortDirection } from '../column/column-types'; import { TableStore } from '../../store/store-types'; -import { SortDirection } from '../../table-types'; +import { TABLE_TOKEN } from '../../table-types'; -export const useSort = (store: TableStore, column: Ref): Ref => { - // 排序功能 - const directionRef = ref('DESC'); - watch( - [directionRef, column], - ([direction, column]) => { - if (column.sortable) { - store.sortData(column.field, direction, column.compareFn); +interface UseSort { + direction: Ref; + sortClass: ComputedRef>; + handleSort: (val: SortDirection) => void; + clearSortOrder: () => void; +} + +export const useSort = (column: Ref): UseSort => { + const table = inject(TABLE_TOKEN); + const store = table.store; + const direction = ref(column.value.sortDirection); + const sortClass = computed(() => ({ + 'sort-active': Boolean(direction.value), + })); + const thInstance = getCurrentInstance(); + thInstance && store.states.thList.push(thInstance); + onMounted(() => { + column.value.sortable && column.value.sortDirection && store.sortData?.(direction.value, column.value.sortMethod); + }); + const execClearSortOrder = () => { + store.states.thList.forEach((th) => { + if (th !== thInstance) { + th.exposed?.clearSortOrder?.(); } - }, - { immediate: true } - ); + }); + }; + + const handleSort = (val: SortDirection) => { + direction.value = val; + execClearSortOrder(); + store.sortData?.(direction.value, column.value.sortMethod); + table.emit('sort-change', { field: column.value.field, direction: direction.value }); + }; + + const clearSortOrder = () => { + direction.value = ''; + }; - return directionRef; + return { direction, sortClass, handleSort, clearSortOrder }; }; export const useFilter = (store: TableStore, column: Ref): Ref => { diff --git a/packages/devui-vue/devui/table/src/components/header/header.scss b/packages/devui-vue/devui/table/src/components/header/header.scss index 7d1896f425..7931ab7174 100644 --- a/packages/devui-vue/devui/table/src/components/header/header.scss +++ b/packages/devui-vue/devui/table/src/components/header/header.scss @@ -13,6 +13,11 @@ border: none; border-bottom: 1px solid $devui-line; } + + .sort-active { + background-color: $devui-list-item-hover-bg; + border-radius: $devui-border-radius 0 0 $devui-border-radius; + } } .header-container { diff --git a/packages/devui-vue/devui/table/src/components/sort/index.ts b/packages/devui-vue/devui/table/src/components/sort/index.ts deleted file mode 100644 index 3577964f8a..0000000000 --- a/packages/devui-vue/devui/table/src/components/sort/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Sort } from './sort'; diff --git a/packages/devui-vue/devui/table/src/components/sort/sort-types.ts b/packages/devui-vue/devui/table/src/components/sort/sort-types.ts new file mode 100644 index 0000000000..521d0e8ba8 --- /dev/null +++ b/packages/devui-vue/devui/table/src/components/sort/sort-types.ts @@ -0,0 +1,11 @@ +import type { ExtractPropTypes, PropType } from 'vue'; +import { SortDirection } from '../column/column-types'; + +export const sortProps = { + sortDirection: { + type: String as PropType, + default: '', + }, +}; + +export type SortProps = ExtractPropTypes; diff --git a/packages/devui-vue/devui/table/src/components/sort/sort.tsx b/packages/devui-vue/devui/table/src/components/sort/sort.tsx index a1b7862ba0..cfb25a75fb 100644 --- a/packages/devui-vue/devui/table/src/components/sort/sort.tsx +++ b/packages/devui-vue/devui/table/src/components/sort/sort.tsx @@ -1,64 +1,47 @@ -import { defineComponent, PropType } from 'vue'; -import { SortDirection } from '../../table-types'; +import { defineComponent } from 'vue'; +import { sortProps, SortProps } from './sort-types'; import './sort.scss'; -export const Sort = defineComponent({ - props: { - modelValue: { - type: String as PropType, - default: '', - }, - 'onUpdate:modelValue': { - type: Function as PropType<(v: SortDirection) => void>, - }, - }, - emits: ['update:modelValue'], - setup(props, ctx) { +export default defineComponent({ + props: sortProps, + emits: ['sort'], + setup(props: SortProps, ctx) { + const directionMap = { + ASC: 'DESC', + DESC: '', + default: 'ASC', + }; const changeDirection = () => { - let direction = ''; - if (props.modelValue === 'ASC') { - direction = 'DESC'; - } else if (props.modelValue === 'DESC') { - direction = ''; - } else { - direction = 'ASC'; - } - ctx.emit('update:modelValue', direction); + ctx.emit('sort', directionMap[props.sortDirection || 'default']); }; return () => ( - + - + - - - - + + + + + values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.085309222 0" + type="matrix" + in="shadowBlurOuter1"> - - - - + + + + diff --git a/packages/devui-vue/devui/table/src/composable/use-table.ts b/packages/devui-vue/devui/table/src/composables/use-table.ts similarity index 100% rename from packages/devui-vue/devui/table/src/composable/use-table.ts rename to packages/devui-vue/devui/table/src/composables/use-table.ts diff --git a/packages/devui-vue/devui/table/src/store/index.ts b/packages/devui-vue/devui/table/src/store/index.ts index 94e5dd5c50..c3a6c0891e 100644 --- a/packages/devui-vue/devui/table/src/store/index.ts +++ b/packages/devui-vue/devui/table/src/store/index.ts @@ -1,6 +1,5 @@ -import { watch, Ref, ref, computed, unref } from 'vue'; -import { Column, CompareFn, FilterResults } from '../components/column/column-types'; -import { SortDirection } from '../table-types'; +import { watch, Ref, ref, computed, unref, ComponentInternalInstance } from 'vue'; +import { Column, SortMethod, FilterResults, SortDirection } from '../components/column/column-types'; import { TableStore } from './store-types'; function replaceColumn(array: any, column: any) { @@ -121,20 +120,18 @@ const createSelection = (dataSource: Ref, _data: Ref) => { }; const createSorter = (dataSource: Ref, _data: Ref) => { - const sortData = ( - field: string, - direction: SortDirection, - compareFn: CompareFn = (fieldKey: string, a: T, b: T) => a[fieldKey] > b[fieldKey] - ) => { + const sortData = (direction: SortDirection, sortMethod: SortMethod) => { if (direction === 'ASC') { - _data.value = _data.value.sort((a, b) => (compareFn(field, a, b) ? 1 : -1)); + _data.value = _data.value.sort((a, b) => (sortMethod ? (sortMethod(a, b) ? 1 : -1) : 0)); } else if (direction === 'DESC') { - _data.value = _data.value.sort((a, b) => (!compareFn(field, a, b) ? 1 : -1)); + _data.value = _data.value.sort((a, b) => (sortMethod ? (sortMethod(a, b) ? -1 : 1) : 0)); } else { _data.value = [...dataSource.value]; } }; - return { sortData }; + + const thList: ComponentInternalInstance[] = []; + return { sortData, thList }; }; const createFilter = (dataSource: Ref, _data: Ref) => { @@ -176,7 +173,7 @@ export function createStore(dataSource: Ref): TableStore { const { _columns, flatColumns, insertColumn, removeColumn, sortColumn, updateColumns } = createColumnGenerator(); const { _checkAll, _checkList, _halfChecked, getCheckedRows } = createSelection(dataSource, _data); - const { sortData } = createSorter(dataSource, _data); + const { sortData, thList } = createSorter(dataSource, _data); const { filterData, resetFilterData } = createFilter(dataSource, _data); const { isFixedLeft } = createFixedLogic(_columns); @@ -190,6 +187,7 @@ export function createStore(dataSource: Ref): TableStore { _checkAll, _halfChecked, isFixedLeft, + thList, }, insertColumn, sortColumn, diff --git a/packages/devui-vue/devui/table/src/store/store-types.ts b/packages/devui-vue/devui/table/src/store/store-types.ts index f8b426849e..7f4b979902 100644 --- a/packages/devui-vue/devui/table/src/store/store-types.ts +++ b/packages/devui-vue/devui/table/src/store/store-types.ts @@ -1,6 +1,5 @@ -import type { Ref } from 'vue'; -import { SortDirection } from '../table-types'; -import { Column, CompareFn, FilterResults } from '../components/column/column-types'; +import type { ComponentInternalInstance, Ref } from 'vue'; +import { Column, SortMethod, FilterResults, SortDirection } from '../components/column/column-types'; export interface TableStore> { states: { @@ -11,13 +10,14 @@ export interface TableStore> { _checkAll: Ref; _halfChecked: Ref; isFixedLeft: Ref; + thList: ComponentInternalInstance[]; }; insertColumn(column: Column, parent: any): void; sortColumn(): void; removeColumn(column: Column): void; updateColumns(): void; getCheckedRows(): T[]; - sortData(field: string, direction: SortDirection, compareFn: CompareFn): void; + sortData(field: string, direction: SortDirection, sortMethod: SortMethod): void; filterData(field: string, results: FilterResults): void; resetFilterData(): void; } diff --git a/packages/devui-vue/devui/table/src/table-types.ts b/packages/devui-vue/devui/table/src/table-types.ts index 84347d85cd..b810a1531d 100644 --- a/packages/devui-vue/devui/table/src/table-types.ts +++ b/packages/devui-vue/devui/table/src/table-types.ts @@ -110,5 +110,3 @@ export interface TableMethods> { } export const TABLE_TOKEN: InjectionKey = Symbol(); - -export type SortDirection = 'ASC' | 'DESC' | ''; diff --git a/packages/devui-vue/devui/table/src/table.tsx b/packages/devui-vue/devui/table/src/table.tsx index 3b4fb586ff..95af954e85 100644 --- a/packages/devui-vue/devui/table/src/table.tsx +++ b/packages/devui-vue/devui/table/src/table.tsx @@ -1,6 +1,6 @@ import { provide, defineComponent, getCurrentInstance, computed, toRef, ref, onMounted, nextTick } from 'vue'; import { Table, TableProps, TablePropsTypes, TABLE_TOKEN, DefaultRow } from './table-types'; -import { useTable } from './composable/use-table'; +import { useTable } from './composables/use-table'; import { createStore } from './store'; import FixHeader from './components/fix-header'; import NormalHeader from './components/normal-header'; @@ -16,6 +16,7 @@ export default defineComponent({ dLoading: Loading, }, props: TableProps, + emits: ['sort-change'], setup(props: TablePropsTypes, ctx) { const table = getCurrentInstance() as Table; const store = createStore(toRef(props, 'data')); diff --git a/packages/devui-vue/docs/components/table/index.md b/packages/devui-vue/docs/components/table/index.md index 2c9abfbec5..8a02f1996b 100644 --- a/packages/devui-vue/docs/components/table/index.md +++ b/packages/devui-vue/docs/components/table/index.md @@ -807,6 +807,70 @@ export default defineComponent({ ::: +### 列排序 + +:::demo `sortable`参数设置为`true`可以支持列排序;`sort-direction`设置初始化时的排序方式;`sort-method`用来定义每一列的排序方法;`sort-change`是排序的回调事件,返回该列的排序信息:`field`排序字段和`direction`排序方向。 + +```vue + + + +``` + +::: + ### Table 参数 | 参数名 | 类型 | 默认值 | 说明 | 跳转 Demo | @@ -826,6 +890,12 @@ export default defineComponent({ | span-method | [SpanMethod](#spanmethod) | -- | 可选,合并单元格的计算方法 | [合并单元格](#合并单元格) | | border-type | [BorderType](#bordertype) | '' | 可选,表格边框类型,默认有行边框;`bordered`: 全边框;`borderless`: 无边框 | [表格样式](#表格样式) | +### Table 事件 + +| 事件名 | 回调参数 | 说明 | 跳转 Demo | +| :---------- | :----------------------------------------------------------- | :----------------------------- | :---------------- | +| sort-change | `Function(obj: { field: string; direction: SortDirection })` | 排序回调事件,返回该列排序信息 | [列排序](#列排序) | + ### Table 方法 | 方法名 | 类型 | 说明 | @@ -834,16 +904,19 @@ export default defineComponent({ ### Column 参数 -| 参数名 | 类型 | 默认值 | 说明 | 跳转 Demo | -| :---------- | :------------------------ | :----- | :------------------------------------------ | :-------------------- | -| header | `string` | -- | 可选,对应列的标题 | [基本用法](#基本用法) | -| field | `string` | -- | 可选,对应列内容的字段名 | [基本用法](#基本用法) | -| type | [ColumnType](#columntype) | '' | 可选,列的类型,设置`checkable`会显示多选框 | [表格多选](#表格多选) | -| width | `string \| number` | -- | 可选,对应列的宽度,单位`px` | -| min-width | `string \| number` | -- | 可选,对应列的最小宽度,单位`px` | -| fixed-left | `string` | -- | 可选,该列固定到左侧的距离,如:'100px' | [固定列](#固定列) | -| fixed-right | `string` | -- | 可选,该列固定到右侧的距离,如:'100px' | [固定列](#固定列) | -| formatter | [Formatter](#formatter) | -- | 可选,格式化列内容 | +| 参数名 | 类型 | 默认值 | 说明 | 跳转 Demo | +| :------------- | :------------------------------ | :----- | :------------------------------------------ | :-------------------- | +| header | `string` | -- | 可选,对应列的标题 | [基本用法](#基本用法) | +| field | `string` | -- | 可选,对应列内容的字段名 | [基本用法](#基本用法) | +| type | [ColumnType](#columntype) | '' | 可选,列的类型,设置`checkable`会显示多选框 | [表格多选](#表格多选) | +| width | `string \| number` | -- | 可选,对应列的宽度,单位`px` | +| min-width | `string \| number` | -- | 可选,对应列的最小宽度,单位`px` | +| fixedLeft | `string` | -- | 可选,该列固定到左侧的距离,如:'100px' | [固定列](#固定列) | +| fixedRight | `string` | -- | 可选,该列固定到右侧的距离,如:'100px' | [固定列](#固定列) | +| formatter | [Formatter](#formatter) | -- | 可选,格式化列内容 | +| sortable | `boolean` | false | 可选,对行数据按照该列的顺序进行排序 | [列排序](#列排序) | +| sort-direction | [SortDirection](#sortdirection) | '' | 可选,设置该列的排序状态 | [列排序](#列排序) | +| sort-method | [SortMethod](#sortmethod) | -- | 可选,用于排序的比较函数 | [列排序](#列排序) | ### Column 插槽 @@ -894,3 +967,15 @@ type ColumnType = 'checkable' | 'index' | ''; ```ts type Formatter = (row: any, column: any, cellValue: any, rowIndex: number) => VNode; ``` + +#### SortDirection + +```ts +type SortDirection = 'ASC' | 'DESC' | ''; +``` + +#### SortMethod + +```ts +type SortMethod = (a: T, b: T) => boolean; +```