diff --git a/devui/table/__tests__/table.spec.ts b/devui/table/__tests__/table.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..35e37e75c5b4de5a4f51b7262d1b64e5eef3a8dc --- /dev/null +++ b/devui/table/__tests__/table.spec.ts @@ -0,0 +1,8 @@ +import { mount } from '@vue/test-utils'; +import { Table } from '../index'; + +describe('table test', () => { + it('table init render', async () => { + // todo + }) +}) diff --git a/devui/table/index.ts b/devui/table/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..c40ee5ced62faca537baca02ee222613846a4a4f --- /dev/null +++ b/devui/table/index.ts @@ -0,0 +1,19 @@ +import type { App } from 'vue' +import Table from './src/table' +import Column from './src/column/column' + +Table.install = function(app: App): void { + app.component(Table.name, Table) + app.component(Column.name, Column) +} + +export { Table, Column } + +export default { + title: 'Table 表格', + category: '数据展示', + status: undefined, // TODO: 组件若开发完成则填入"已完成",并删除该注释 + install(app: App): void { + app.use(Table as any) + } +} diff --git a/devui/table/src/body/body.scss b/devui/table/src/body/body.scss new file mode 100644 index 0000000000000000000000000000000000000000..6417799221226608b49e6c629169296611cb348f --- /dev/null +++ b/devui/table/src/body/body.scss @@ -0,0 +1,20 @@ +@import '@devui/styles-var/devui-var.scss'; + +.devui-tbody { + tr { + font-size: $devui-font-size-card-title; + color: $devui-text; + border: none; + border-bottom: 1px solid $devui-dividing-line; + background-color: $devui-global-bg-normal; + + &:hover { + background-color: $devui-list-item-hover-bg; + } + + td { + padding: 10px; + border: none; + } + } +} diff --git a/devui/table/src/body/body.tsx b/devui/table/src/body/body.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9b02b8679897d1deb3d3347c868d72b6e5ad4934 --- /dev/null +++ b/devui/table/src/body/body.tsx @@ -0,0 +1,33 @@ +import { defineComponent } from 'vue'; +import { TableBodyProps, TableBodyPropsTypes } from './body.type' +import { useTableBody } from './use-body'; +import './body.scss'; + +export default defineComponent({ + name: 'DTableBody', + props: TableBodyProps, + setup(props: TableBodyPropsTypes) { + const { rowColumns } = useTableBody(props); + + return { rowColumns }; + }, + render() { + const { rowColumns } = this; + + return ( + + {rowColumns.map((row, rowIndex) => { + return ( + + {row.columns.map((column, index) => { + return ( + {column.renderCell({ row, column, $index: index })} + ); + })} + + ); + })} + + ); + }, +}); \ No newline at end of file diff --git a/devui/table/src/body/body.type.ts b/devui/table/src/body/body.type.ts new file mode 100644 index 0000000000000000000000000000000000000000..c1febde00fb051c2b818c58fde03ba5004b0a021 --- /dev/null +++ b/devui/table/src/body/body.type.ts @@ -0,0 +1,10 @@ +import { ExtractPropTypes } from 'vue'; + +export const TableBodyProps = { + store: { + type: Object, + default: {}, + }, +}; + +export type TableBodyPropsTypes = ExtractPropTypes; diff --git a/devui/table/src/body/use-body.ts b/devui/table/src/body/use-body.ts new file mode 100644 index 0000000000000000000000000000000000000000..1657a06b50646cc594957432254062c6035cd1a2 --- /dev/null +++ b/devui/table/src/body/use-body.ts @@ -0,0 +1,15 @@ +import { computed } from 'vue'; +import { TableBodyPropsTypes } from './body.type' + +export function useTableBody(props: TableBodyPropsTypes): any { + const storeData = 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 { rowColumns }; +} \ No newline at end of file diff --git a/devui/table/src/colgroup/colgroup.tsx b/devui/table/src/colgroup/colgroup.tsx new file mode 100644 index 0000000000000000000000000000000000000000..93e7197d8466045978538007fbb68379a8a8992b --- /dev/null +++ b/devui/table/src/colgroup/colgroup.tsx @@ -0,0 +1,23 @@ +import { inject, defineComponent } from 'vue'; +import { Table } from '../table.type'; +import { Column } from '../column/column.type'; + +export default defineComponent({ + name: 'DColGroup', + setup() { + const parent: Table = inject('table'); + const columns: Column[] = parent.store.states._columns; + + return { columns }; + }, + render() { + const { columns } = this; + return ( + + {columns.map((column, index) => { + return ; + })} + + ); + }, +}); \ No newline at end of file diff --git a/devui/table/src/column/column.tsx b/devui/table/src/column/column.tsx new file mode 100644 index 0000000000000000000000000000000000000000..60ef5e04cf8e85ae459d43673fd1ce60219978ab --- /dev/null +++ b/devui/table/src/column/column.tsx @@ -0,0 +1,33 @@ +import { inject, defineComponent, onBeforeMount, onMounted } from 'vue'; +import { + Column, + TableColumnProps, + TableColumnPropsTypes, +} from './column.type' +import { Table } from '../table.type'; +import { useRender } 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); + + onBeforeMount(() => { + setColumnWidth(column); + setColumnRender(column); + }); + + onMounted(() => { + parent.store.insertColumn(column); + }); + }, + render() { + return null; + }, +}); \ No newline at end of file diff --git a/devui/table/src/column/column.type.ts b/devui/table/src/column/column.type.ts new file mode 100644 index 0000000000000000000000000000000000000000..cdab04098202602701a11228536e6d9339695253 --- /dev/null +++ b/devui/table/src/column/column.type.ts @@ -0,0 +1,38 @@ +import { PropType, ExtractPropTypes, VNode } from 'vue'; + +export const TableColumnProps = { + header: { + type: String, + default: '', + }, + field: { + type: String, + default: '', + }, + width: { + type: [String, Number], + default: '', + }, + minWidth: { + type: [String, Number], + default: 80, + }, + formatter: { + type: Function as PropType< + (row: any, column: Column, cellValue, index: number) => VNode + >, + }, +}; + +export type TableColumnPropsTypes = ExtractPropTypes; + +export interface Column { + field?: string + width?: number + minWidth?: number + realWidth?: number + header?: string + renderHeader?: () => void + renderCell?: (data: any) => void + formatter?: (row: any, column: Column, cellValue, index: number) => VNode +} \ No newline at end of file diff --git a/devui/table/src/column/use-column.ts b/devui/table/src/column/use-column.ts new file mode 100644 index 0000000000000000000000000000000000000000..7c5fc4ce546f06d6694a7e4f318cb9b0309565bb --- /dev/null +++ b/devui/table/src/column/use-column.ts @@ -0,0 +1,45 @@ +import { ref } 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; + 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 }; +} + +function defaultRenderHeader(column: Column) { + return column.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); + } + return value?.toString?.() || ''; +} \ No newline at end of file diff --git a/devui/table/src/header/header.scss b/devui/table/src/header/header.scss new file mode 100644 index 0000000000000000000000000000000000000000..b2886eee13301a184943c62f14810f963c2ff2d9 --- /dev/null +++ b/devui/table/src/header/header.scss @@ -0,0 +1,18 @@ +@import '@devui/styles-var/devui-var.scss'; + +.devui-thead { + tr { + font-size: $devui-font-size-card-title; + color: $devui-text; + font-weight: 700; + border: none; + border-bottom: 1px solid $devui-line; + background-color: $devui-global-bg-normal; + + th { + text-align: left; + padding: 12px 10px; + border: none; + } + } +} diff --git a/devui/table/src/header/header.tsx b/devui/table/src/header/header.tsx new file mode 100644 index 0000000000000000000000000000000000000000..88c8001390e0de5c23bc5ccaa6e900cd2a3d8456 --- /dev/null +++ b/devui/table/src/header/header.tsx @@ -0,0 +1,22 @@ +import { defineComponent, toRefs } from 'vue'; +import { TableHeaderProps, TableHeaderPropsTypes } from './header.type' +import './header.scss'; + +export default defineComponent({ + name: 'DTableHeader', + props: TableHeaderProps, + setup(props: TableHeaderPropsTypes) { + const { store } = toRefs(props) + const columns = store.value.states._columns.value; + + return ( + + + {columns.map((column, index) => { + return {column.renderHeader()}; + })} + + + ) + } +}); \ No newline at end of file diff --git a/devui/table/src/header/header.type.ts b/devui/table/src/header/header.type.ts new file mode 100644 index 0000000000000000000000000000000000000000..3369aa9d76063355dcfc33c108438a50279b2226 --- /dev/null +++ b/devui/table/src/header/header.type.ts @@ -0,0 +1,10 @@ +import { ExtractPropTypes } from 'vue'; + +export const TableHeaderProps = { + store: { + type: Object, + default: {}, + }, +}; + +export type TableHeaderPropsTypes = ExtractPropTypes; diff --git a/devui/table/src/store/index.ts b/devui/table/src/store/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..b7138064c7b394b6ba8e68e79d6a4c5c1cd194c2 --- /dev/null +++ b/devui/table/src/store/index.ts @@ -0,0 +1,30 @@ +import { ref, watch } from 'vue'; +import { TablePropsTypes } from '../table.type'; +import { Column } from '../column/column.type'; + +export function createStore(props: TablePropsTypes): any { + const _data = ref([]); + const _columns = ref([]); + updateData(); + + watch(() => props.data, updateData, { deep: true }); + + function updateData() { + _data.value = []; + props.data.forEach((item) => { + _data.value.push(item); + }); + } + + const insertColumn = (column: Column) => { + _columns.value.push(column); + }; + + return { + insertColumn, + states: { + _data, + _columns, + }, + }; +} \ No newline at end of file diff --git a/devui/table/src/table-types.ts b/devui/table/src/table-types.ts new file mode 100644 index 0000000000000000000000000000000000000000..1f9cbcf673a594df6b5455b103d11e8783f27593 --- /dev/null +++ b/devui/table/src/table-types.ts @@ -0,0 +1,14 @@ +import type { PropType, ExtractPropTypes } from 'vue' + +type TableData = Array<{ + [key: string]: any +}> + +export const tableProps = { + data: { + type: Array as PropType, + required: true, + } +} as const + +export type TableProps = ExtractPropTypes diff --git a/devui/table/src/table.scss b/devui/table/src/table.scss new file mode 100644 index 0000000000000000000000000000000000000000..9709244d058cbcb7bbdb2a00e8dcb53eee9aa9a8 --- /dev/null +++ b/devui/table/src/table.scss @@ -0,0 +1,26 @@ +@import '@devui/styles-var/devui-var.scss'; + +.devui-table { + display: table; + table-layout: fixed; + width: 100%; + border-spacing: 0; + border: none; + + &-wrapper { + width: 100%; + overflow-x: auto; + } + + &-striped { + tbody tr:nth-child(even) { + background-color: $devui-list-item-strip-bg; + } + } + + &-empty { + width: 100%; + font-size: $devui-font-size; + text-align: center; + } +} diff --git a/devui/table/src/table.tsx b/devui/table/src/table.tsx new file mode 100644 index 0000000000000000000000000000000000000000..83146778b7d6595233b33ac52a0b792113a2ab9f --- /dev/null +++ b/devui/table/src/table.tsx @@ -0,0 +1,36 @@ +import { provide, defineComponent, getCurrentInstance } from 'vue'; +import { Table, TableProps, TablePropsTypes } 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) { + const table = getCurrentInstance() as Table; + const store = createStore(props); + table.store = store; + const { classes } = useTable(props); + provide('table', table); + + return { classes, store }; + }, + render() { + const { classes, data, store, $slots } = this; + return ( +
+ {$slots.default()} + + + + {!!data.length && } +
+ {!data.length &&
No Data
} +
+ ); + }, +}); diff --git a/devui/table/src/table.type.ts b/devui/table/src/table.type.ts new file mode 100644 index 0000000000000000000000000000000000000000..563b9bb6a9cb2a2dfbf67c0a9e4b5c04754488bb --- /dev/null +++ b/devui/table/src/table.type.ts @@ -0,0 +1,18 @@ +import { PropType, ExtractPropTypes, ComponentInternalInstance } from 'vue'; + +export const TableProps = { + data: { + type: Array as PropType, + default: [], + }, + striped: { + type: Boolean, + default: false, + }, +}; + +export type TablePropsTypes = ExtractPropTypes; + +export interface Table extends ComponentInternalInstance { + store: any +} diff --git a/devui/table/src/use-table.ts b/devui/table/src/use-table.ts new file mode 100644 index 0000000000000000000000000000000000000000..c5e9f1e65d0bd212f4b92ab8de7ab32a374173f0 --- /dev/null +++ b/devui/table/src/use-table.ts @@ -0,0 +1,11 @@ +import { computed } from 'vue'; +import { TablePropsTypes } from './table.type'; + +export function useTable(props: TablePropsTypes): any { + const classes = computed(() => ({ + 'devui-table': true, + 'devui-table-striped': props.striped, + })); + + return { classes }; +} \ No newline at end of file diff --git a/devui/table/src/utils.ts b/devui/table/src/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..9043fe1a3f676a500a0ef664a033b131c1bb12a3 --- /dev/null +++ b/devui/table/src/utils.ts @@ -0,0 +1,11 @@ +export function formatWidth(width: number | string): number { + if (typeof width === 'number') { + return width; + } + + return parseInt(width, 10) || 0; +} + +export function formatMinWidth(minWidth: number | string): number { + return formatWidth(minWidth) || 80; +} \ No newline at end of file diff --git a/docs/.vitepress/devui-theme/styles/demo-block.scss b/docs/.vitepress/devui-theme/styles/demo-block.scss index 3c7b4b881d28f2ba93bac9cd5fe79fed557f128c..facd32f70602369c4a4a63da17179b36131600b1 100644 --- a/docs/.vitepress/devui-theme/styles/demo-block.scss +++ b/docs/.vitepress/devui-theme/styles/demo-block.scss @@ -23,4 +23,11 @@ .meta { border-top: solid 1px $devui-dividing-line !important; + background-color: $devui-area !important; +} + +.description { + border: solid 1px $devui-dividing-line !important; + color: $devui-text !important; + background-color: $devui-base-bg !important; } diff --git a/docs/.vitepress/devui-theme/styles/layout.scss b/docs/.vitepress/devui-theme/styles/layout.scss index a25c86d6cdb49d5caeb7a24f28f21251e8021d16..8f0ef7a58e631aea3bf8bca270f8aa2745a379d9 100644 --- a/docs/.vitepress/devui-theme/styles/layout.scss +++ b/docs/.vitepress/devui-theme/styles/layout.scss @@ -159,6 +159,14 @@ ol { padding-left: 1.25em; } +ul { + list-style: disc; +} + +ol { + list-style: decimal; +} + li > ul, li > ol { margin: 0; diff --git a/docs/components/table/index.md b/docs/components/table/index.md new file mode 100644 index 0000000000000000000000000000000000000000..07cf4c4905d61fc443981556e7cda931463e3329 --- /dev/null +++ b/docs/components/table/index.md @@ -0,0 +1,210 @@ +# Table 表格 + +展示行列数据。 + +### 何时使用 + +1. 当有大量结构化的数据需要展现时; +2. 当需要对数据进行排序、过滤、自定义操作等复杂行为时。 + +### 基本用法 + +:::demo 简单表格,`d-table`组件上的`data`属性传入要展示的数据,`d-table-column`组件上通过`field`传入对应列内容的字段名,`header`传入对应列的标题。 + +```vue + + + +``` + +::: + +### 斑马纹表格 + +:::demo 通过`d-table`组件上的`striped`属性,可设置带斑马纹的表格,更容易区分不同行的数据。 + +```vue + + + + + + +``` + +::: + +### 空数据模板 + +:::demo 当传入的数据为空时,默认展示空数据模板。 + +```vue + + + +``` + +::: + + +### d-table Props + +| 参数 | 类型 | 默认值 | 说明 | +| ------- | --------- | ------- | ------------------ | +| data | `Array` | `[]` | 显示的数据 | +| striped | `Boolean` | `false` | 是否显示斑马纹间隔 | + +### d-column Props + +| 参数 | 类型 | 默认值 | 说明 | +| ------ | ------------------ | ------ | ---------------------- | +| header | `String` | `-` | 对应列的标题 | +| field | `String` | `-` | 对应列内容的字段名 | +| width | `String \| Number` | `-` | 对应列的宽度,单位`px` | +| min-width | `String \| Number` | `-` | 对应列的最小宽度,单位`px` |