diff --git a/packages/devui-vue/devui/table/src/body/body.scss b/packages/devui-vue/devui/table/src/body/body.scss index ea2657279dfcdaccd4f275b9704afe884975afe3..26e8dac0b5bcf46c90589316c39ae6b9f1efb97b 100644 --- a/packages/devui-vue/devui/table/src/body/body.scss +++ b/packages/devui-vue/devui/table/src/body/body.scss @@ -8,7 +8,7 @@ border-bottom: 1px solid $devui-dividing-line; background-color: $devui-global-bg-normal; - &:hover { + &.hover-enabled:hover { background-color: $devui-list-item-hover-bg; } @@ -18,3 +18,11 @@ } } } + + +.devui-sticky-cell { + position: sticky; + z-index: 5; + background-color: inherit; + box-shadow: rgba(0,0,0,.05) $devui-shadow-length-slide-right; +} \ No newline at end of file diff --git a/packages/devui-vue/devui/table/src/body/body.tsx b/packages/devui-vue/devui/table/src/body/body.tsx index 9b02b8679897d1deb3d3347c868d72b6e5ad4934..9e79d142cd01568f63d84cacef52c9d99cbdd7b6 100644 --- a/packages/devui-vue/devui/table/src/body/body.tsx +++ b/packages/devui-vue/devui/table/src/body/body.tsx @@ -1,33 +1,46 @@ -import { defineComponent } from 'vue'; +import { defineComponent, inject, computed } from 'vue'; import { TableBodyProps, TableBodyPropsTypes } from './body.type' -import { useTableBody } from './use-body'; +import { TABLE_TOKEN } from '../table.type'; +import { Checkbox } from '../../../checkbox'; + import './body.scss'; export default defineComponent({ name: 'DTableBody', - props: TableBodyProps, + // props: TableBodyProps, setup(props: TableBodyPropsTypes) { - const { rowColumns } = useTableBody(props); + const parent = inject(TABLE_TOKEN); + const { + _data: data, + _columns: columns, + _checkList: checkList + } = parent.store.states; + + // 移动到行上是否高亮 + const hoverEnabled = computed(() => parent.props.rowHoveredHighlight); - return { rowColumns }; - }, - render() { - const { rowColumns } = this; + // 行前的 checkbox + const renderCheckbox = (index: number) => parent.props.checkable ? ( + + + + ) : null; - return ( + return () => ( - {rowColumns.map((row, rowIndex) => { + {data.value.map((row, rowIndex) => { return ( - - {row.columns.map((column, index) => { + + {renderCheckbox(rowIndex)} + {columns.value.map((column, index) => { return ( - {column.renderCell({ row, column, $index: index })} + {column.renderCell(row, index)} ); })} ); })} - ); - }, + ) + } }); \ No newline at end of file diff --git a/packages/devui-vue/devui/table/src/body/body.type.ts b/packages/devui-vue/devui/table/src/body/body.type.ts index c1febde00fb051c2b818c58fde03ba5004b0a021..b80250e0c5a7fb926ae6959b26754bc98fecbe98 100644 --- a/packages/devui-vue/devui/table/src/body/body.type.ts +++ b/packages/devui-vue/devui/table/src/body/body.type.ts @@ -1,10 +1,6 @@ import { ExtractPropTypes } from 'vue'; export const TableBodyProps = { - store: { - type: Object, - default: {}, - }, -}; +} as const; export type TableBodyPropsTypes = ExtractPropTypes; diff --git a/packages/devui-vue/devui/table/src/body/use-body.ts b/packages/devui-vue/devui/table/src/body/use-body.ts index 1657a06b50646cc594957432254062c6035cd1a2..34f68c25c3bac87e888411916f5010cd53ea96ce 100644 --- a/packages/devui-vue/devui/table/src/body/use-body.ts +++ b/packages/devui-vue/devui/table/src/body/use-body.ts @@ -1,13 +1,19 @@ -import { computed } from 'vue'; +import { computed, ComputedRef } from 'vue'; +import { Column } from '../column/column.type'; import { TableBodyPropsTypes } from './body.type' -export function useTableBody(props: TableBodyPropsTypes): any { - const storeData = props.store.states; +interface Data { + rowColumns: ComputedRef<(Record & { columns: Column[]; })[]> +} + +export const useTableBody = (props: TableBodyPropsTypes): Data => { + const states = props.store.states; const rowColumns = computed(() => { - return storeData._data.value.map((row) => { - const obj = Object.assign({}, row); - obj.columns = storeData._columns.value; - return obj; + return states._data.value.map((row) => { + return { + ...row, + columns: states._columns.value + }; }); }); diff --git a/packages/devui-vue/devui/table/src/colgroup/colgroup.tsx b/packages/devui-vue/devui/table/src/colgroup/colgroup.tsx index 93e7197d8466045978538007fbb68379a8a8992b..fa16eaea3b96fbd52197dc9af84ebbe681fa2253 100644 --- a/packages/devui-vue/devui/table/src/colgroup/colgroup.tsx +++ b/packages/devui-vue/devui/table/src/colgroup/colgroup.tsx @@ -1,23 +1,22 @@ import { inject, defineComponent } from 'vue'; -import { Table } from '../table.type'; -import { Column } from '../column/column.type'; +import { TABLE_TOKEN } from '../table.type'; export default defineComponent({ name: 'DColGroup', setup() { - const parent: Table = inject('table'); - const columns: Column[] = parent.store.states._columns; + const parent = inject(TABLE_TOKEN); + const columns = parent.store.states._columns; + return () => ( + parent.props.tableLayout === 'fixed' ? ( + + {/* 如果是 checkable,那么需要指定 col */} + {parent.props.checkable ? : null} + {columns.value.map((column, index) => { + return ; + })} + + ) : null - return { columns }; - }, - render() { - const { columns } = this; - return ( - - {columns.map((column, index) => { - return ; - })} - ); - }, + } }); \ No newline at end of file diff --git a/packages/devui-vue/devui/table/src/column/column.tsx b/packages/devui-vue/devui/table/src/column/column.tsx index 60ef5e04cf8e85ae459d43673fd1ce60219978ab..bf92f239ec1c4a1c6f1cb405089950dec5252870 100644 --- a/packages/devui-vue/devui/table/src/column/column.tsx +++ b/packages/devui-vue/devui/table/src/column/column.tsx @@ -1,30 +1,34 @@ -import { inject, defineComponent, onBeforeMount, onMounted } from 'vue'; +import { inject, defineComponent, onBeforeUnmount, onMounted, toRefs, watch } from 'vue'; import { Column, TableColumnProps, TableColumnPropsTypes, } from './column.type' -import { Table } from '../table.type'; -import { useRender } from './use-column'; +import { TABLE_TOKEN } from '../table.type'; +import { createColumn } from './use-column'; export default defineComponent({ name: 'DColumn', props: TableColumnProps, - setup(props: TableColumnPropsTypes) { - const column: Column = { - field: props.field, - header: props.header, - }; - const parent: Table = inject('table'); - const { setColumnWidth, setColumnRender } = useRender(props); + setup(props: TableColumnPropsTypes, ctx) { + /* + ctx.slots : { + customFilterTemplate: Slot + } + */ + const column = createColumn(toRefs(props), ctx.slots); - onBeforeMount(() => { - setColumnWidth(column); - setColumnRender(column); - }); + const parent = inject(TABLE_TOKEN); onMounted(() => { parent.store.insertColumn(column); + watch(() => column.order, () => { + parent.store.sortColumn(); + }); + }); + + onBeforeUnmount(() => { + parent.store.removeColumn(column); }); }, render() { diff --git a/packages/devui-vue/devui/table/src/column/column.type.ts b/packages/devui-vue/devui/table/src/column/column.type.ts index cdab04098202602701a11228536e6d9339695253..451b3be5d8ea2df79e4989b12d79126c7e84315a 100644 --- a/packages/devui-vue/devui/table/src/column/column.type.ts +++ b/packages/devui-vue/devui/table/src/column/column.type.ts @@ -1,4 +1,4 @@ -import { PropType, ExtractPropTypes, VNode } from 'vue'; +import { PropType, ExtractPropTypes, VNode, Slot } from 'vue'; export const TableColumnProps = { header: { @@ -18,21 +18,70 @@ export const TableColumnProps = { default: 80, }, formatter: { - type: Function as PropType< - (row: any, column: Column, cellValue, index: number) => VNode - >, + type: Function as PropType, }, + order: { + type: Number, + default: 0 + }, + sortable: { + type: Boolean, + default: false + }, + compareFn: { + type: Function as PropType, + default: (field: string, a: any, b: any): boolean => a[field] < b[field] + }, + filterable: { + type: Boolean, + default: false + }, + filterMultiple: { + type: Boolean, + default: false + }, + filterList: { + type: Array as PropType, + default: [] + } }; export type TableColumnPropsTypes = ExtractPropTypes; -export interface Column { +export type Formatter = (row: T, cellValue: R, index: number) => VNode[]; + +export type CompareFn = (field: string, a: T, b: T) => boolean; + +export type FilterResults = (string | number)[]; + +export interface CustomFilterProps { + value: FilterResults + onChange: (value: FilterResults) => void +} + +export type CustomFilterSlot = (props: CustomFilterProps) => VNode[]; + +export interface FilterConfig { + id: number | string + name: string + value: any + checked?: boolean +} +export interface Column = any> { field?: string width?: number minWidth?: number realWidth?: number header?: string + order?: number + sortable?: boolean + filterable?: boolean + filterMultiple?: boolean + filterList?: FilterConfig[] renderHeader?: () => void - renderCell?: (data: any) => void - formatter?: (row: any, column: Column, cellValue, index: number) => VNode + renderCell?: (row: T, index: number) => void + formatter?: Formatter + compareFn?: CompareFn + customFilterTemplate?: CustomFilterSlot + subColumns?: Slot } \ No newline at end of file diff --git a/packages/devui-vue/devui/table/src/column/use-column.ts b/packages/devui-vue/devui/table/src/column/use-column.ts index 7c5fc4ce546f06d6694a7e4f318cb9b0309565bb..708b180fa086e3c4fca7915b52d7569c7365d1f0 100644 --- a/packages/devui-vue/devui/table/src/column/use-column.ts +++ b/packages/devui-vue/devui/table/src/column/use-column.ts @@ -1,45 +1,71 @@ -import { ref } from 'vue'; +import { watch, reactive, onBeforeMount, ToRefs, Slots, h } from 'vue'; import { Column, TableColumnPropsTypes } from './column.type' import { formatWidth, formatMinWidth } from '../utils'; -export function useRender(props: TableColumnPropsTypes): any { - const formatedWidth = ref(formatWidth(props.width)); - const formatedMinWidth = ref(formatMinWidth(props.minWidth)); - const setColumnWidth = (column: Column) => { - column.width = formatedWidth.value; - column.minWidth = formatedMinWidth.value; + +export function createColumn = any>( + props: ToRefs, + templates: Slots +): Column { + const { + field, + header, + sortable, + width, + minWidth, + formatter, + compareFn, + filterable, + filterList, + filterMultiple, + order + } = props; + const column: Column = reactive({}); + + watch([field, header, order], ([field, header, order]) => { + column.field = field; + column.header = header; + column.order = order; + }, { immediate: true }); + + watch([sortable, compareFn], ([sortable, compareFn]) => { + column.sortable = sortable; + column.compareFn = compareFn; + }) + + watch([ + filterable, + filterList, + filterMultiple, + ], ([filterable, filterList, filterMultiple]) => { + column.filterable = filterable; + column.filterMultiple = filterMultiple; + column.filterList = filterList; + }, { immediate: true }) + + + onBeforeMount(() => { + column.width = formatWidth(width.value); + column.minWidth = formatMinWidth(minWidth.value); column.realWidth = column.width || column.minWidth; - return column; - }; - - const setColumnRender = (column: Column) => { - column.renderHeader = () => { - return defaultRenderHeader(column); - }; - column.renderCell = (data) => { - return defaultRenderCell(data); - }; - }; - - return { setColumnWidth, setColumnRender }; + column.renderHeader = defaultRenderHeader; + column.renderCell = defaultRenderCell; + column.formatter = formatter.value; + column.customFilterTemplate = templates.customFilterTemplate; + column.subColumns = templates.subColumns; + }); + + return column; } -function defaultRenderHeader(column: Column) { - return column.header; +function defaultRenderHeader(this: Column) { + return h('span', { class: 'title' }, this.header); } -function defaultRenderCell({ - row, - column, - $index, -}: { - row: any - column: Column - $index: number -}) { - const value = row[column.field]; - if (column.formatter) { - return column.formatter(row, column, value, $index); +function defaultRenderCell>(this: Column, rowData: T, index: number) { + const value = rowData[this.field]; + if (this.formatter) { + return this.formatter(rowData, value, index); } - return value?.toString?.() || ''; -} \ No newline at end of file + return value?.toString?.() ?? ''; +} diff --git a/packages/devui-vue/devui/table/src/header/filter/filter.scss b/packages/devui-vue/devui/table/src/header/filter/filter.scss new file mode 100644 index 0000000000000000000000000000000000000000..97d9907c17d366003d97ae3e0a43ef843dd4ac5f --- /dev/null +++ b/packages/devui-vue/devui/table/src/header/filter/filter.scss @@ -0,0 +1,143 @@ +@import '../../../../styles-var/devui-var.scss'; + +.data-table-column-filter-content { + background: $devui-connected-overlay-bg; + border-radius: $devui-border-radius; + box-sizing: content-box; + .drop-down-item { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + + .line { + width: calc(100% - 20px); + margin-left: 10px; + } + + & > .filter-options > d-checkbox { + height: 30px; + padding: 0 10px; + } + + .checkbox-group { + padding: 0 10px; + } +} + +.filter-content-hidden { + display: none !important; +} + +.line { + height: 1px; + background: $devui-dividing-line; + margin-top: 5px; + margin-bottom: 5px; +} + +.checkbox-group { + height: 30px; +} + +.button-style { + display: inline-block; + width: 45%; + font-size: $devui-font-size; + color: $devui-text; + text-align: center; + line-height: 20px; + cursor: pointer; +} + +.button-style:hover { + color: $devui-brand; +} + + +.normal-filter-list-container { + overflow-y: auto; +} + +.icon-filter-style { + color: $devui-dividing-line; + cursor: pointer; + margin-left: 20px; +} + +.label-style { + font-size: $devui-font-size; + color: $devui-text; + letter-spacing: 0; + margin-left: 10px; +} + +.email-style { + font-size: $devui-font-size; + color: $devui-placeholder;// TODO: Color-Question + letter-spacing: 0; + margin-left: 10px; +} + +.overlay-mask { + position: fixed; + top: 0; + bottom: 0; + right: 0; + left: 0; + z-index: 9; +} + +.drop-down-item { + padding: 0 10px; + height: 30px; + line-height: 30px; + cursor: pointer; + + &:hover { + background: $devui-list-item-hover-bg; + color: $devui-list-item-hover-text; + } +} + +.filter-item-active { + background: $devui-list-item-active-bg; + color: $devui-list-item-active-text; +} + +.edit-padding-fix { + margin-top: -6px; + margin-bottom: -6px; +} + +.filter-icon { + display: inline-block; + vertical-align: middle; + cursor: pointer; + height: 16px; + margin-top: -1px; // 解决漏斗形状视觉效果不局中 + + > svg g { + fill: $devui-dividing-line; + } + + &:hover { + > svg g { + fill: $devui-icon-fill-active-hover; + } + } +} + +.filter-icon-active { + visibility: visible !important; + + > svg g { + fill: $devui-icon-fill-active; + } + + &:hover { + > svg g { + fill: $devui-icon-fill-active-hover; + } + } +} \ No newline at end of file diff --git a/packages/devui-vue/devui/table/src/header/filter/filter.tsx b/packages/devui-vue/devui/table/src/header/filter/filter.tsx new file mode 100644 index 0000000000000000000000000000000000000000..62581158e2f6454ee5775452b46a10b7e2218e8d --- /dev/null +++ b/packages/devui-vue/devui/table/src/header/filter/filter.tsx @@ -0,0 +1,102 @@ +import { defineComponent, PropType, ref, computed } from "vue"; +import { CustomFilterSlot, FilterConfig, FilterResults } from "../../column/column.type"; +import { Dropdown } from '../../../../dropdown'; +import { Checkbox } from "../../../../checkbox"; + +import './filter.scss'; + + +export const Filter = defineComponent({ + props: { + modelValue: { + type: Array as PropType, + default: [] + }, + 'onUpdate:modelValue': { + type: Function as PropType<(v: FilterResults) => void> + }, + customTemplate: { + type: Function as PropType + }, + filterList: { + type: Array as PropType, + required: true + }, + filterMultiple: { + type: Boolean, + default: true + }, + }, + emits: ['update:modelValue'], + setup(props) { + const filterOrigin = ref(null); + + // 多选逻辑 + const onUpdateChecked = (config: FilterConfig, value: boolean) => { + const checkedList = props.modelValue; + const update = props['onUpdate:modelValue']; + const contained = !!checkedList.find(item => item === config.value); + if (value && !contained) { + update?.([...checkedList, config.value]); + } else if (!value && contained) { + update?.(checkedList.filter(item => config.value !== item)); + } + } + + // 单选逻辑 + const updateSingleChecked = (config: FilterConfig) => { + props['onUpdate:modelValue']?.([config.value]); + } + + + const dropdownContent = computed(() => { + const checkedList = props.modelValue; + const isContained = (config: FilterConfig) => !!checkedList.find(item => item === config.value); + return () => ( +
    + {props.filterList.map((item, index) => { + return ( +
  • 0 ? 'margin-top:10px' : ''}> + {props.filterMultiple ? ( + onUpdateChecked(item, value)} + > + {item.name} + + ) : ( + updateSingleChecked(item)}>{item.name} + )} +
  • + ) + })} +
+ ); + }); + return () => ( + <> + + + + + + + + + + + + + {props.customTemplate?.({ value: props.modelValue, onChange: props["onUpdate:modelValue"] }) ?? dropdownContent.value()} + + + ) + } +}); diff --git a/packages/devui-vue/devui/table/src/header/filter/index.ts b/packages/devui-vue/devui/table/src/header/filter/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..998668a1df5fa7e132f27c1ca1efae4effb292f5 --- /dev/null +++ b/packages/devui-vue/devui/table/src/header/filter/index.ts @@ -0,0 +1 @@ +export * from './filter'; \ No newline at end of file diff --git a/packages/devui-vue/devui/table/src/header/header.scss b/packages/devui-vue/devui/table/src/header/header.scss index 21e88bf495006c749a79c1782c1d840f70b134b0..9a467e994f131cd5fa68a710cc92a4f694652141 100644 --- a/packages/devui-vue/devui/table/src/header/header.scss +++ b/packages/devui-vue/devui/table/src/header/header.scss @@ -11,10 +11,28 @@ th { text-align: left; - padding: 12px 10px; + padding: 0; border: none; } } + + .header-container { + position: relative; + display: flex; + align-items: center; + padding-left: 2px; + padding-right: 8px; + + .title { + display: inline-block; + line-height: 36px; + vertical-align: middle; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + cursor: default; + } + } } .header-bg { diff --git a/packages/devui-vue/devui/table/src/header/header.tsx b/packages/devui-vue/devui/table/src/header/header.tsx index a05a80b7ae6f8189134368ea1d71f4a2cb582f9f..653f6a372764279e5dd0c50b870beb520a2d1881 100644 --- a/packages/devui-vue/devui/table/src/header/header.tsx +++ b/packages/devui-vue/devui/table/src/header/header.tsx @@ -1,28 +1,80 @@ -import { defineComponent, toRefs } from 'vue'; -import { TableHeaderProps, TableHeaderPropsTypes } from './header.type' +import { defineComponent, inject, computed, ref, shallowRef, PropType, watch, toRefs } from 'vue'; +import { TABLE_TOKEN } from '../table.type'; +import { Column, FilterResults } from '../column/column.type'; + +import { Checkbox } from '../../../checkbox'; +import { Sort } from './sort'; +import { Filter } from './filter'; + import './header.scss'; +import '../body/body.scss'; +import { useFliter, useSort } from './use-header'; + export default defineComponent({ name: 'DTableHeader', - props: TableHeaderProps, - setup(props: TableHeaderPropsTypes) { - const { store } = toRefs(props) - const columns = store.value.states._columns.value; + setup() { + const table = inject(TABLE_TOKEN); + const { + _checkAll: checkAll, + _halfChecked: halfChecked, + _columns: columns + } = table.store.states; + + const checkbox = computed(() => table.props.checkable ? ( + + + + ) : null); + + return () => { + return ( + + + {checkbox.value} + {columns.value.map((column, index) => ( + + ))} + + + ) + } + } +}); - return { - columns, +const Th = defineComponent({ + props: { + column: { + type: Object as PropType, + required: true } }, - render() { - const { columns } = this - return ( - - - {columns.map((column, index) => { - return {column.renderHeader()}; - })} - - + setup(props: { column: Column; }) { + const table = inject(TABLE_TOKEN); + const { column } = toRefs(props); + + // 排序功能 + const directionRef = useSort(table.store, column); + + // 过滤器 + const filteredRef = useFliter(table.store, column); + + return () => ( + +
+ {props.column.renderHeader()} + {props.column.filterable && } +
+ {props.column.sortable && } + ) } }); \ No newline at end of file diff --git a/packages/devui-vue/devui/table/src/header/header.type.ts b/packages/devui-vue/devui/table/src/header/header.type.ts index 3369aa9d76063355dcfc33c108438a50279b2226..e51c2ad264452e1ca745036ec58dbe4468e28056 100644 --- a/packages/devui-vue/devui/table/src/header/header.type.ts +++ b/packages/devui-vue/devui/table/src/header/header.type.ts @@ -1,10 +1,6 @@ import { ExtractPropTypes } from 'vue'; export const TableHeaderProps = { - store: { - type: Object, - default: {}, - }, -}; +} as const; export type TableHeaderPropsTypes = ExtractPropTypes; diff --git a/packages/devui-vue/devui/table/src/header/sort/index.ts b/packages/devui-vue/devui/table/src/header/sort/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..72099fcbdd2abb2025a317a5ed5e26e0995e4983 --- /dev/null +++ b/packages/devui-vue/devui/table/src/header/sort/index.ts @@ -0,0 +1 @@ +export { Sort } from './sort'; \ No newline at end of file diff --git a/packages/devui-vue/devui/table/src/header/sort/sort.scss b/packages/devui-vue/devui/table/src/header/sort/sort.scss new file mode 100644 index 0000000000000000000000000000000000000000..203b367d286d39a1daa534ae0d600ce1bc8d7708 --- /dev/null +++ b/packages/devui-vue/devui/table/src/header/sort/sort.scss @@ -0,0 +1,56 @@ +@import '../../../../styles-var/devui-var.scss'; + +.sort-clickable { + cursor: pointer; + position: absolute; + top: -16px; + left: 50%; + transform: translateX(-50%); + padding: 8px; + line-height: 20px; +} + +.sort-icon-default { + > svg g use { + fill: $devui-dividing-line; + } + + > svg g polygon { + fill: $devui-icon-bg; + } + + &:hover { + > svg g use { + fill: $devui-icon-fill-active-hover; + } + } +} + +.sort-icon-asc, +.sort-icon-desc { + > svg g use { + fill: $devui-icon-fill-active; + } + + > svg g polygon { + fill: $devui-icon-bg; + } + + &:hover { + > svg g use { + fill: $devui-icon-fill-active-hover; + } + } +} + +.sort-icon-asc { + > svg g polygon:last-of-type { + opacity: 0.3; + } +} + +.sort-icon-desc { + > svg g polygon:first-of-type { + opacity: 0.3; + } +} diff --git a/packages/devui-vue/devui/table/src/header/sort/sort.tsx b/packages/devui-vue/devui/table/src/header/sort/sort.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5ceadb53cd4a16356032d97addc0bf1b0fb5ae73 --- /dev/null +++ b/packages/devui-vue/devui/table/src/header/sort/sort.tsx @@ -0,0 +1,69 @@ +import { defineComponent, PropType } from 'vue'; +import { SortDirection } from '../../table.type'; +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) { + const changeDirection = () => { + let direction = ''; + if (props.modelValue === 'ASC') { + direction = 'DESC'; + } else if (props.modelValue === 'DESC') { + direction = ''; + } else { + direction = 'ASC'; + } + ctx.emit('update:modelValue', direction); + } + + return () => ( + + + + + + + + + + + + + + + + + + + + ); + } +}) \ No newline at end of file diff --git a/packages/devui-vue/devui/table/src/header/use-header.ts b/packages/devui-vue/devui/table/src/header/use-header.ts new file mode 100644 index 0000000000000000000000000000000000000000..5953c57d8534af78ae8f5664c8ada4ba0710d3c2 --- /dev/null +++ b/packages/devui-vue/devui/table/src/header/use-header.ts @@ -0,0 +1,24 @@ +import { ref, watch, Ref, shallowRef } from 'vue'; +import { Column, FilterResults } from '../column/column.type'; +import { TableStore } from '../store'; +import { SortDirection } from '../table.type'; + +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); + } + }, { immediate: true }); + + return directionRef; +} + +export const useFliter = (store: TableStore, column: Ref): Ref => { + const filteredRef = shallowRef(); + watch(filteredRef, (results) => { + store.filterData(column.value.field, results); + }); + return filteredRef; +} diff --git a/packages/devui-vue/devui/table/src/store/index.ts b/packages/devui-vue/devui/table/src/store/index.ts index b7138064c7b394b6ba8e68e79d6a4c5c1cd194c2..c808643cd006c4321db91277d1bb137aa1ff6056 100644 --- a/packages/devui-vue/devui/table/src/store/index.ts +++ b/packages/devui-vue/devui/table/src/store/index.ts @@ -1,30 +1,206 @@ -import { ref, watch } from 'vue'; -import { TablePropsTypes } from '../table.type'; -import { Column } from '../column/column.type'; +import { watch, Ref, ref, computed } from 'vue'; +import { Column, CompareFn, FilterResults } from '../column/column.type'; +import { SortDirection } from '../table.type'; +export interface TableStore> { + states: { + _data: Ref + _columns: Ref + _checkList: Ref + _checkAll: Ref + _halfChecked: Ref + } + insertColumn(column: Column): void + sortColumn(): void + removeColumn(column: Column): void + getCheckedRows(): T[] + sortData(field: string, direction: SortDirection, compareFn: CompareFn): void + filterData(field: string, results: FilterResults): void + resetFilterData(): void +} -export function createStore(props: TablePropsTypes): any { - const _data = ref([]); - const _columns = ref([]); - updateData(); +export function createStore(dataSource: Ref): TableStore { + const _data: Ref = ref([]); + watch(dataSource, (value: T[]) => { + _data.value = [...value]; + }, { deep: true, immediate: true }); - watch(() => props.data, updateData, { deep: true }); + const { _columns, insertColumn, removeColumn, sortColumn } = createColumnGenerator(); + const { _checkAll, _checkList, _halfChecked, getCheckedRows } = createSelection(dataSource, _data); + const { sortData } = createSorter(dataSource, _data); + const { filterData, resetFilterData } = createFilter(dataSource, _data); - function updateData() { - _data.value = []; - props.data.forEach((item) => { - _data.value.push(item); - }); - } + return { + states: { + _data, + _columns, + _checkList, + _checkAll, + _halfChecked + }, + insertColumn, + sortColumn, + removeColumn, + getCheckedRows, + sortData, + filterData, + resetFilterData + }; +} + +/** + * 列生成器 + * @returns + */ +const createColumnGenerator = () => { + const _columns: Ref = ref([]); + /** + * 插入当前列 + * @param {Column} column + */ const insertColumn = (column: Column) => { _columns.value.push(column); + // 实际上就是插入排序 + _columns.value.sort((a, b) => a.order > b.order ? 1 : -1); }; + /** + * 对 column 进行排序 + */ + const sortColumn = () => { + _columns.value.sort((a, b) => a.order > b.order ? 1 : -1); + } + + /** + * 移除当前列 + * @param {Column} column + * @returns + */ + const removeColumn = (column: Column) => { + const i = _columns.value.findIndex((v) => v === column); + if (i === -1) { + return; + } + _columns.value.splice(i, 1); + } + return { _columns, insertColumn, removeColumn, sortColumn }; +} + +/** + * 选择功能 + * @param dataSource + * @param _data + * @returns + */ +const createSelection = (dataSource: Ref, _data: Ref) => { + const _checkList: Ref = ref([]); + const _checkAllRecord: Ref = ref(false); + const _checkAll: Ref = computed({ + get: () => _checkAllRecord.value, + set: (val: boolean) => { + _checkAllRecord.value = val; + // 只有在 set 的时候变更 _checkList 的数据 + for (let i = 0; i < _checkList.value.length; i++) { + _checkList.value[i] = val; + } + } + }); + const _halfChecked = ref(false); + + watch(dataSource, (value: T[]) => { + _checkList.value = new Array(value.length).fill(false); + }, { deep: true, immediate: true }); + + // checkList 只有全为true的时候 + watch(_checkList, (list) => { + if (list.length === 0) { + return; + } + let allTrue = true; + let allFalse = true; + for (let i = 0; i < list.length; i++) { + allTrue &&= list[i]; + allFalse &&= !list[i]; + } + + _checkAllRecord.value = allTrue; + _halfChecked.value = !(allFalse || allTrue); + + }, { immediate: true, deep: true }); + + /** + * 获取当前已选数据 + * @returns {T[]} + */ + const getCheckedRows = (): T[] => { + return _data.value.filter((_, index) => _checkList.value[index]); + } + return { - insertColumn, - states: { - _data, - _columns, - }, + _checkList, + _checkAll, + _halfChecked, + getCheckedRows }; -} \ No newline at end of file +} + +/** + * 排序功能 + * @template T + * @param dataSource + * @param _data + */ +const createSorter = (dataSource: Ref, _data: Ref) => { + /** + * 对数据进行排序 + * @param {string} field + * @param {SortDirection} direction + * @param {CompareFn} compareFn + */ + const sortData = ( + field: string, + direction: SortDirection, + compareFn: CompareFn = (field: string, a: T, b: T) => a[field] > b[field] + ) => { + if (direction === 'ASC') { + _data.value = _data.value.sort((a, b) => compareFn(field, a, b) ? 1 : -1); + } else if (direction === 'DESC') { + _data.value = _data.value.sort((a, b) => !compareFn(field, a, b) ? 1 : -1); + } else { + _data.value = [...dataSource.value]; + } + } + return { sortData }; +} + +/** + * 过滤功能 + * @template T + * @param dataSource + * @param _data + * @returns + */ +const createFilter = (dataSource: Ref, _data: Ref) => { + // 过滤数据所需要的 + const fieldSet = new Set(); + /** + * 过滤数据 + */ + const filterData = (field: string, results: FilterResults) => { + fieldSet.add(field); + const fields = [...fieldSet]; + _data.value = dataSource.value.filter(item => { + return fields.reduce((prev, field) => { + return prev && (results.indexOf(item[field]) !== -1) + }, true); + }); + } + /** + * 重置数据为最开始的状态 + */ + const resetFilterData = () => { + fieldSet.clear(); + _data.value = [...dataSource.value]; + } + return { filterData, resetFilterData }; +} diff --git a/packages/devui-vue/devui/table/src/table.scss b/packages/devui-vue/devui/table/src/table.scss index 48d9e7cf3180dea7646f67175ebe7ba3268b574f..125f13cc1f246a333059eb53fae0cdded2686eec 100644 --- a/packages/devui-vue/devui/table/src/table.scss +++ b/packages/devui-vue/devui/table/src/table.scss @@ -6,10 +6,13 @@ width: 100%; border-spacing: 0; border: none; + margin: 0; + padding: 0; &-wrapper { width: 100%; overflow-x: auto; + padding-top: 8px; } &-striped { @@ -23,6 +26,22 @@ font-size: $devui-font-size; text-align: center; } + + &-view { + display: flex; + flex-direction: column; + position: relative; + height: 100%; + + & .scroll-view { + flex: 1; + overflow: scroll; + } + } +} + +.table-layout-auto { + table-layout: auto; } .table-layout-auto { diff --git a/packages/devui-vue/devui/table/src/table.tsx b/packages/devui-vue/devui/table/src/table.tsx index 83146778b7d6595233b33ac52a0b792113a2ab9f..4e52c19a62217c8b368dbfbb6c68b1ef2ddef476 100644 --- a/packages/devui-vue/devui/table/src/table.tsx +++ b/packages/devui-vue/devui/table/src/table.tsx @@ -1,36 +1,68 @@ -import { provide, defineComponent, getCurrentInstance } from 'vue'; -import { Table, TableProps, TablePropsTypes } from './table.type'; +import { provide, defineComponent, getCurrentInstance, computed, toRef } from 'vue'; +import { Table, TableProps, TablePropsTypes, TABLE_TOKEN } from './table.type'; import { useTable } from './use-table'; import { createStore } from './store'; import ColGroup from './colgroup/colgroup'; import TableHeader from './header/header'; import TableBody from './body/body'; + import './table.scss'; + export default defineComponent({ name: 'DTable', props: TableProps, - setup(props: TablePropsTypes) { + setup(props: TablePropsTypes, ctx) { const table = getCurrentInstance() as Table; - const store = createStore(props); + const store = createStore(toRef(props, 'data')); table.store = store; - const { classes } = useTable(props); - provide('table', table); - - return { classes, store }; - }, - render() { - const { classes, data, store, $slots } = this; - return ( -
- {$slots.default()} - + provide(TABLE_TOKEN, table); + + const { classes, style } = useTable(props); + + const isEmpty = computed(() => props.data.length === 0); + + const fixHeaderCompo = computed(() => { + return ( +
+
+
+ + +
+
+
+ + + {!isEmpty.value && } +
+
+ + ); + }); + + const normalHeaderCompo = computed(() => { + return ( + - - {!!data.length && } + + {!isEmpty.value && }
- {!data.length &&
No Data
} + ) + }); + + ctx.expose({ + getCheckedRows() { + return store.getCheckedRows(); + } + }); + + return () => ( +
+ {ctx.slots.default()} + {props.fixHeader ? fixHeaderCompo.value : normalHeaderCompo.value} + {isEmpty.value &&
No Data
}
); - }, + } }); diff --git a/packages/devui-vue/devui/table/src/table.type.ts b/packages/devui-vue/devui/table/src/table.type.ts index b9132f45ec9a57251db60d09b475cab66b56bf1c..369441f95e8bcf231ba9b4cba75163371a9697b5 100644 --- a/packages/devui-vue/devui/table/src/table.type.ts +++ b/packages/devui-vue/devui/table/src/table.type.ts @@ -1,26 +1,94 @@ -import { PropType, ExtractPropTypes, ComponentInternalInstance } from 'vue'; +import { PropType, ExtractPropTypes, ComponentInternalInstance, InjectionKey } from 'vue'; +import { TableStore } from './store'; + +export type TableSize = 'sm' | 'md' | 'lg'; export const TableProps = { data: { - type: Array as PropType, + type: Array as PropType[]>, default: [], }, striped: { type: Boolean, default: false, }, - headerBg:{ + scrollable: { type: Boolean, default: false }, - tableLayout:{ + maxWidth: { + type: String, + }, + maxHeight: { + type: String, + }, + tableWidth: { + type: String, + }, + tableHeight: { + type: String, + }, + size: { + type: String as PropType, + validator(value: string): boolean { + return value === 'sm' || value === 'md' || value === 'lg'; + } + }, + rowHoveredHighlight: { + type: Boolean, + default: true + }, + fixHeader: { + type: Boolean, + default: false + }, + checkable: { + type: Boolean, + default: true + }, + tableLayout: { type: String as PropType<'fixed' | 'auto'>, - default: 'fixed' + default: 'auto', + validator(v: string) { + return v === 'fixed' || v === 'auto'; + } + }, + showLoading: { + type: Boolean, + default: false + }, + headerBg: { + type: Boolean, + default: false } }; export type TablePropsTypes = ExtractPropTypes; -export interface Table extends ComponentInternalInstance { - store: any +export interface Table> extends ComponentInternalInstance { + store: TableStore + props: TablePropsTypes } + +// export interface TableCheckStatusArg { +// pageAllChecked?: boolean; // 全选 +// pageHalfChecked?: boolean; // 半选 +// } + +// export interface RowToggleStatusEventArg { +// rowItem: any; // 行数据 +// open: boolean; // 子表格是否展开 +// } + +export interface TableMethods> { + getCheckedRows(): T[] + // setRowCheckStatus(arg: TableCheckStatusArg): void + // setTableCheckStatus(arg: RowToggleStatusEventArg): void + // setRowChildToggleStatus(): void + // setTableChildrenToggleStatus(): void + // cancelEditingStatus(): void +} + +export const TABLE_TOKEN: InjectionKey = Symbol(); + +export type SortDirection = 'ASC' | 'DESC' | ''; diff --git a/packages/devui-vue/devui/table/src/use-table.ts b/packages/devui-vue/devui/table/src/use-table.ts index e375cbbb4ca06fb68921d87c421407cb381883f2..06dc72a4043d85e04225ada1c7057c8380bbbcdb 100644 --- a/packages/devui-vue/devui/table/src/use-table.ts +++ b/packages/devui-vue/devui/table/src/use-table.ts @@ -1,13 +1,23 @@ -import { computed } from 'vue'; +import { computed, ComputedRef, CSSProperties } from 'vue'; import { TablePropsTypes } from './table.type'; -export function useTable(props: TablePropsTypes): any { +interface TableConfig { + classes: ComputedRef> + style: ComputedRef +} + +export function useTable(props: TablePropsTypes): TableConfig { const classes = computed(() => ({ 'devui-table': true, 'devui-table-striped': props.striped, 'header-bg': props.headerBg, 'table-layout-auto': props.tableLayout === 'auto' })); - - return { classes }; + const style: ComputedRef = computed(() => ({ + maxHeight: props.maxHeight, + maxWidth: props.maxWidth, + height: props.tableHeight, + width: props.tableWidth + })); + return {classes, style}; } \ No newline at end of file diff --git a/packages/devui-vue/docs/components/table/index.md b/packages/devui-vue/docs/components/table/index.md index fb305b27c2e127afe91f737d63027c7a10b902aa..d68739292d332ee336b219f23638e16bc3797ddc 100644 --- a/packages/devui-vue/docs/components/table/index.md +++ b/packages/devui-vue/docs/components/table/index.md @@ -14,7 +14,7 @@ ```vue