diff --git a/README.md b/README.md index 25e1d7d..38d5931 100644 --- a/README.md +++ b/README.md @@ -221,15 +221,15 @@ render(); ``` -- TableView +- CentralContent - 请尽量使用该组件代替Descriptions组件。该组件比Descriptions组件添加了数据格式化和灵活的空判断和自定义空展示,并且优化了排列,可以实现任何栅格大小的数据项复杂组合。实现了尾行优化,使你不必担心末尾项的宽度问题,程序会自动计算并占满该行。 - _InfoPage(@kne/current-lib_info-page),(@kne/current-lib_info-page/dist/index.css),antd(antd) ```jsx -const { TableView } = _InfoPage; +const { CentralContent } = _InfoPage; const BaseExample = () => { - return ( { name: 'description', title: '描述' }, { name: 'description2', title: '描述' - }, { - name: 'end', title: '尾行优化' }]} />); }; @@ -272,6 +270,99 @@ render(); ``` +- TableView +- +- _InfoPage(@kne/current-lib_info-page),(@kne/current-lib_info-page/dist/index.css),antd(antd) + +```jsx +const { TableView } = _InfoPage; +const { Flex } = antd; +const { useState } = React; + +const dataSource = [{ + id: 'RC00101', + name: '张三', + birthday: '2020-03-03', + addTime: new Date(), + count: 2000.1322, + count2: 0.01234565, + count3: 1234523, + description: `描述描述描述描述描述描述描述描述` +}, { + id: 'RC00102', + name: '李四', + birthday: '2020-03-03', + addTime: new Date(), + count: 2000.1322, + count2: 0.01234565, + count3: 1234523, + description: `描述描述描述描述描述描述描述描述` +}, { + id: 'RC00103', + name: '王五', + birthday: '2020-03-03', + addTime: new Date(), + count: 2000.1322, + count2: 0.01234565, + count3: 1234523, + description: `描述描述描述描述描述描述描述描述` +}, { + id: 'RC00104', + name: '马七', + birthday: '2020-03-03', + addTime: new Date(), + count: 2000.1322, + count2: 0.01234565, + count3: 1234523, + description: `描述描述描述描述描述描述描述描述` +}]; + +const columns = [{ + name: 'id', title: 'ID' +}, { + name: 'name', title: '姓名' +}, { + name: 'birthday', title: '出生日期', format: 'date' +}, { + name: 'addTime', title: '添加时间', format: 'datetime' +}, { + name: 'count', title: '数量', format: 'number' +}, { + name: 'description', title: '描述', span: 10 +}]; + +const WithCheckbox = () => { + const [selectKeys, setSelectKeys] = useState([]); + return ; +}; + +const WithSelected = () => { + const [selectKeys, setSelectKeys] = useState([]); + return ; +}; + +const BaseExample = () => { + return + + + + +
+ +
+
; +}; + +render(); + +``` + ### API diff --git a/doc/central-content.js b/doc/central-content.js new file mode 100644 index 0000000..e0e4cf7 --- /dev/null +++ b/doc/central-content.js @@ -0,0 +1,41 @@ +const { CentralContent } = _InfoPage; + +const BaseExample = () => { + return ( `${value}万元` + }, { + name: 'empty', title: '空值显示' + }, { + name: 'empty2', title: '空值显示2', placeholder: '空' + }, { + name: 'empty3', title: '空值显示3', emptyIsPlaceholder: false + }, { + name: 'description', title: '描述' + }, { + name: 'description2', title: '描述' + }]} />); +}; + +render(); diff --git a/doc/example.json b/doc/example.json index 581f49b..1199ccc 100644 --- a/doc/example.json +++ b/doc/example.json @@ -56,8 +56,26 @@ ] }, { - "title": "TableView", + "title": "CentralContent", "description": "请尽量使用该组件代替Descriptions组件。该组件比Descriptions组件添加了数据格式化和灵活的空判断和自定义空展示,并且优化了排列,可以实现任何栅格大小的数据项复杂组合。实现了尾行优化,使你不必担心末尾项的宽度问题,程序会自动计算并占满该行。", + "code": "./central-content.js", + "scope": [ + { + "name": "_InfoPage", + "packageName": "@kne/current-lib_info-page" + }, + { + "packageName": "@kne/current-lib_info-page/dist/index.css" + }, + { + "name": "antd", + "packageName": "antd" + } + ] + }, + { + "title": "TableView", + "description": "", "code": "./table-view.js", "scope": [ { diff --git a/doc/table-view.js b/doc/table-view.js index 8415287..168bfaf 100644 --- a/doc/table-view.js +++ b/doc/table-view.js @@ -1,43 +1,85 @@ const { TableView } = _InfoPage; +const { Flex } = antd; +const { useState } = React; + +const dataSource = [{ + id: 'RC00101', + name: '张三', + birthday: '2020-03-03', + addTime: new Date(), + count: 2000.1322, + count2: 0.01234565, + count3: 1234523, + description: `描述描述描述描述描述描述描述描述` +}, { + id: 'RC00102', + name: '李四', + birthday: '2020-03-03', + addTime: new Date(), + count: 2000.1322, + count2: 0.01234565, + count3: 1234523, + description: `描述描述描述描述描述描述描述描述` +}, { + id: 'RC00103', + name: '王五', + birthday: '2020-03-03', + addTime: new Date(), + count: 2000.1322, + count2: 0.01234565, + count3: 1234523, + description: `描述描述描述描述描述描述描述描述` +}, { + id: 'RC00104', + name: '马七', + birthday: '2020-03-03', + addTime: new Date(), + count: 2000.1322, + count2: 0.01234565, + count3: 1234523, + description: `描述描述描述描述描述描述描述描述` +}]; + +const columns = [{ + name: 'id', title: 'ID' +}, { + name: 'name', title: '姓名' +}, { + name: 'birthday', title: '出生日期', format: 'date' +}, { + name: 'addTime', title: '添加时间', format: 'datetime' +}, { + name: 'count', title: '数量', format: 'number' +}, { + name: 'description', title: '描述', span: 10 +}]; + +const WithCheckbox = () => { + const [selectKeys, setSelectKeys] = useState([]); + return ; +}; + +const WithSelected = () => { + const [selectKeys, setSelectKeys] = useState([]); + return ; +}; const BaseExample = () => { - return ( `${value}万元` - }, { - name: 'empty', title: '空值显示' - }, { - name: 'empty2', title: '空值显示2', placeholder: '空' - }, { - name: 'empty3', title: '空值显示3', emptyIsPlaceholder: false - }, { - name: 'description', title: '描述' - }, { - name: 'description2', title: '描述' - }, { - name: 'end', title: '尾行优化' - }]} />); + return + + + + +
+ +
+
; }; render(); diff --git a/package.json b/package.json index 22c6dac..e78fe07 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kne/info-page", - "version": "0.1.6", + "version": "0.1.7", "description": "一般用在复杂的详情展示页面,InfoPage提供了一个标准的展示信息的格式", "syntax": { "esmodules": true @@ -77,7 +77,6 @@ "devDependencies": { "@kne/microbundle": "^0.15.5", "@kne/modules-dev": "^2.0.14", - "classnames": "^2.5.1", "cross-env": "^7.0.3", "husky": "^9.0.11", "npm-run-all": "^4.1.5", @@ -87,6 +86,8 @@ }, "dependencies": { "@kne/is-empty": "^1.0.1", - "dayjs": "^1.11.13" + "classnames": "^2.5.1", + "dayjs": "^1.11.13", + "lodash": "^4.17.21" } } diff --git a/src/TableView/boxComputed.js b/src/CentralContent/boxComputed.js similarity index 100% rename from src/TableView/boxComputed.js rename to src/CentralContent/boxComputed.js diff --git a/src/CentralContent/index.js b/src/CentralContent/index.js new file mode 100644 index 0000000..63d3eb5 --- /dev/null +++ b/src/CentralContent/index.js @@ -0,0 +1,109 @@ +import React, { useMemo } from 'react'; +import { isEmpty } from '@kne/is-empty'; +import { Col, Row } from 'antd'; +import get from 'lodash/get'; +import classnames from 'classnames'; +import boxComputed from './boxComputed'; +import style from './style.module.scss'; +import formatView from '../formatView'; + +const TableView = props => { + const { dataSource, columns, col, valueIsEmpty, emptyIsPlaceholder, placeholder, className } = Object.assign( + { + dataSource: {}, //数据 + columns: [], //列定义 + col: 2, //展示列数 + valueIsEmpty: isEmpty, + placeholder: '-', + emptyIsPlaceholder: true + }, + props + ); + + const renderColumns = useMemo(() => { + return boxComputed( + columns + .map(item => { + const itemValue = typeof item.getValueOf === 'function' ? item.getValueOf(dataSource, { column: item }) : get(dataSource, item.name); + const displayValue = (value => { + if (typeof item.format === 'function') { + return item.format(value, { dataSource, column: item }); + } + if (typeof item.format === 'string') { + const formatValue = formatView(value, item.format, { dataSource, column: item }); + if (formatValue) { + return formatValue; + } + } + return value; + })(itemValue); + + const itemIsEmpty = (item.valueIsEmpty || valueIsEmpty)(itemValue); + + if ( + item.display === false || + (typeof item.display === 'function' && + item.display(itemValue, { + dataSource, + column: item + }) === false) + ) { + return null; + } + + if (!(item.hasOwnProperty('emptyIsPlaceholder') ? item.emptyIsPlaceholder : emptyIsPlaceholder) && itemIsEmpty) { + return null; + } + + return Object.assign({}, item, { isEmpty: itemIsEmpty, value: displayValue }); + }) + .filter(item => !!item), + col + ); + }, [columns, col]); + + return ( + + {renderColumns.map((item, index) => { + return ( + + + + {item.title} + + + {item.isEmpty + ? typeof item.renderPlaceholder === 'function' + ? item.renderPlaceholder({ + column: item, + dataSource, + placeholder + }) + : item.placeholder || placeholder + : typeof item.render === 'function' + ? item.render(item.value, { + column: item, + dataSource + }) + : item.value} + + + + ); + })} + + ); +}; + +export default TableView; diff --git a/src/CentralContent/style.module.scss b/src/CentralContent/style.module.scss new file mode 100644 index 0000000..a5229ff --- /dev/null +++ b/src/CentralContent/style.module.scss @@ -0,0 +1,41 @@ +.table-view { + border-bottom: 1px solid #eeeeee; + border-right: 1px solid #eeeeee; + line-height: 20px; + + &:not(:last-child) { + border-bottom: none; + } +} + +.table-view-col { + flex: 0 0 var(--col-width); + max-width: var(--col-width); +} + +.table-view-item { + height: 100%; +} + +.table-view-label, +.table-view-content { + font-size: var(--font-size-default); + padding: 8px 16px; + border-top: 1px solid #eeeeee; + border-left: 1px solid #eeeeee; + line-height: 20px; + word-break: break-all; +} + +.table-view-label { + background: var(--bg-color-grey-1); + color: var(--font-color-grey); + flex: 0 0 var(--col-label-width); + max-width: var(--col-label-width); +} + +.table-view-content { + word-break: break-all; + white-space: pre-line; + flex: 1; +} diff --git a/src/Label.js b/src/Label.js new file mode 100644 index 0000000..cbe76d0 --- /dev/null +++ b/src/Label.js @@ -0,0 +1,28 @@ +import React, { useLayoutEffect, useRef } from 'react'; +import useRefCallback from '@kne/use-ref-callback'; + +const Label = ({ className, children, onChange }) => { + const ref = useRef(null); + const handlerChange = useRefCallback(onChange); + useLayoutEffect(() => { + const computed = () => { + if (!ref.current) { + return; + } + handlerChange(ref.current.getBoundingClientRect()); + }; + const resizeObserver = new ResizeObserver(computed); + resizeObserver.observe(ref.current); + computed(); + return () => { + resizeObserver.disconnect(); + }; + }, [handlerChange]); + return ( + + {children} + + ); +}; + +export default Label; diff --git a/src/TableView/Header.js b/src/TableView/Header.js new file mode 100644 index 0000000..7f8c66a --- /dev/null +++ b/src/TableView/Header.js @@ -0,0 +1,92 @@ +import React from 'react'; +import { Checkbox, Col, Row } from 'antd'; +import classnames from 'classnames'; +import get from 'lodash/get'; +import Label from '../Label'; +import style from './style.module.scss'; +import { CheckOutlined } from '@ant-design/icons'; + +const Header = p => { + const { dataSource, columns, defaultSpan, rowKey, rowSelection, colsSize, setColsSize, sticky } = Object.assign( + {}, + { + rowKey: 'id' + }, + p + ); + return ( + + {rowSelection && rowSelection.type === 'checkbox' && ( + + + {rowSelection.allowSelectedAll ? ( + (() => { + const checkedAll = rowSelection.isSelectedAll || dataSource.every(item => rowSelection.selectedRowKeys && rowSelection.selectedRowKeys.indexOf(get(item, typeof rowKey === 'function' ? rowKey(item) : rowKey)) > -1); + return ( + 0 && !checkedAll} + onChange={e => { + const checked = e.target.checked; + if (!checked) { + typeof rowSelection.onIsSelectAllChange === 'function' ? rowSelection.onIsSelectAllChange(false) : rowSelection.onChange([]); + } else { + typeof rowSelection.onIsSelectAllChange === 'function' + ? rowSelection.onIsSelectAllChange(true) + : rowSelection.onChange( + dataSource.map(item => { + return get(item, typeof rowKey === 'function' ? rowKey(item) : rowKey); + }) + ); + } + }} + /> + ); + })() + ) : ( + + )} + + + )} + + + {columns.map(column => { + const { name, title, span, width } = column; + return ( + + + + ); + })} + + + {rowSelection && rowSelection.type !== 'checkbox' && } + + ); +}; + +export default Header; diff --git a/src/TableView/index.js b/src/TableView/index.js index a178c8a..c4fc163 100644 --- a/src/TableView/index.js +++ b/src/TableView/index.js @@ -1,108 +1,164 @@ -import React, { useMemo } from 'react'; -import { isEmpty } from '@kne/is-empty'; -import { Col, Row } from 'antd'; -import get from 'lodash/get'; +import React, { useMemo, useState } from 'react'; +import Header from './Header'; +import { Checkbox, Col, Empty, Row } from 'antd'; +import { CheckOutlined } from '@ant-design/icons'; import classnames from 'classnames'; -import boxComputed from './boxComputed'; +import get from 'lodash/get'; +import formatView from '../formatView'; +import { isEmpty } from '@kne/is-empty'; import style from './style.module.scss'; -import { formatView } from '../defaultFormat'; -const TableView = props => { - const { dataSource, columns, col, valueIsEmpty, emptyIsPlaceholder, placeholder, className } = Object.assign( +const TableView = p => { + const [colsSize, setColsSize] = useState({}); + const props = Object.assign( + {}, { - dataSource: {}, //数据 - columns: [], //列定义 - col: 2, //展示列数 + rowKey: 'id', valueIsEmpty: isEmpty, placeholder: '-', - emptyIsPlaceholder: true + emptyIsPlaceholder: true, + empty: }, - props + p ); + const { className, dataSource, columns, rowKey, rowSelection, valueIsEmpty, emptyIsPlaceholder, placeholder, empty, onRowSelect } = props; - const renderColumns = useMemo(() => { - return boxComputed( - columns - .map(item => { - const itemValue = typeof item.getValueOf === 'function' ? item.getValueOf(dataSource, { column: item }) : get(dataSource, item.name); - const displayValue = (value => { - if (typeof item.format === 'function') { - return item.format(value, { dataSource, column: item }); - } - if (typeof item.format === 'string') { - const formatValue = formatView(value, item.format, { dataSource, column: item }); - if (formatValue) { - return formatValue; - } - } - return value; - })(itemValue); - - const itemIsEmpty = (item.valueIsEmpty || valueIsEmpty)(itemValue); + const defaultSpan = useMemo(() => { + const assignedSpan = columns.reduce((a, b) => { + return a + (b.span || 0); + }, 0); + const undistributedColCount = columns.filter(item => !item.span).length; - if ( - item.display === false || - (typeof item.display === 'function' && - item.display(itemValue, { - dataSource, - column: item - }) === false) - ) { - return null; - } + return Math.round(Math.max(24 - assignedSpan, 0) / undistributedColCount); + }, [columns]); - if (!(item.hasOwnProperty('emptyIsPlaceholder') ? item.emptyIsPlaceholder : emptyIsPlaceholder) && itemIsEmpty) { - return null; - } + const header =
; - return Object.assign({}, item, { isEmpty: itemIsEmpty, value: displayValue }); - }) - .filter(item => !!item), - col - ); - }, [columns, col]); - - return ( - - {renderColumns.map((item, index) => { + const body = + dataSource.length > 0 ? ( + dataSource.map(item => { + const id = get(item, typeof rowKey === 'function' ? rowKey(item) : rowKey); + const isChecked = rowSelection?.selectedRowKeys && rowSelection.selectedRowKeys.indexOf(id) > -1; return ( - { + if (item.disabled) { + return; + } + onRowSelect && onRowSelect(item, { columns, dataSource }); + if (!rowSelection) { + return; + } + if (rowSelection.isSelectedAll) { + return; + } + if (rowSelection.type === 'checkbox') { + const selectedRowKeys = (rowSelection.selectedRowKeys || []).slice(0); + isChecked ? selectedRowKeys.splice(rowSelection.selectedRowKeys.indexOf(id), 1) : selectedRowKeys.push(id); + rowSelection.onChange(selectedRowKeys); + } else { + rowSelection.onChange(rowSelection.selectedRowKeys.length && rowSelection.selectedRowKeys[0] === id ? [] : [id]); + } }} > - - - {item.title} + {rowSelection && rowSelection.type === 'checkbox' && ( + + + + - - {item.isEmpty - ? typeof item.renderPlaceholder === 'function' - ? item.renderPlaceholder({ - column: item, - dataSource, - placeholder - }) - : item.placeholder || placeholder - : typeof item.render === 'function' - ? item.render(item.value, { - column: item, - dataSource - }) - : item.value} - - - + )} + + + {columns.map(column => { + const { name, span } = column; + const colItem = (item => { + const itemValue = + typeof item.getValueOf === 'function' + ? item.getValueOf(item, { + dataSource, + columns, + column, + target: item + }) + : get(item, column.name); + + const displayValue = (value => { + if (typeof column.format === 'function') { + return column.format(value, { dataSource, columns, column, target: item }); + } + if (typeof column.format === 'string') { + const formatValue = formatView(value, column.format, { dataSource, columns, column, target: item }); + if (formatValue) { + return formatValue; + } + } + return value; + })(itemValue); + + const itemIsEmpty = (column.valueIsEmpty || valueIsEmpty)(itemValue); + + if (!(column.hasOwnProperty('emptyIsPlaceholder') ? column.emptyIsPlaceholder : emptyIsPlaceholder) && itemIsEmpty) { + return null; + } + return Object.assign({}, column, { isEmpty: itemIsEmpty, value: displayValue }); + })(item); + + return ( + + + {colItem.isEmpty + ? typeof colItem.renderPlaceholder === 'function' + ? colItem.renderPlaceholder({ + column, + dataSource, + columns, + placeholder, + target: item + }) + : colItem.placeholder || placeholder + : typeof colItem.render === 'function' + ? colItem.render(colItem.value, { + column, + columns, + dataSource, + target: item + }) + : colItem.value} + + + ); + })} + + + {rowSelection && rowSelection.type !== 'checkbox' && {isChecked && }} + ); - })} - + }) + ) : ( +
{empty}
+ ); + return ( +
+ {header} + {body} +
); }; diff --git a/src/TableView/style.module.scss b/src/TableView/style.module.scss index a5229ff..d408522 100644 --- a/src/TableView/style.module.scss +++ b/src/TableView/style.module.scss @@ -1,41 +1,74 @@ -.table-view { - border-bottom: 1px solid #eeeeee; - border-right: 1px solid #eeeeee; - line-height: 20px; +.table { + position: relative; +} + +.header { + border-left: 1px solid #f0f0f0; + + &.sticky { + position: sticky; + top: 0; + z-index: 1; + } - &:not(:last-child) { - border-bottom: none; + .col { + background: var(--bg-color-grey-1); + transition: background-color 300ms; + white-space: nowrap; } } -.table-view-col { - flex: 0 0 var(--col-width); - max-width: var(--col-width); +.body { + border-left: 1px solid #f0f0f0; + + .col { + cursor: pointer; + } + + &.is-selected-all { + cursor: not-allowed; + background: var(--bg-color-grey-3); + + .col { + pointer-events: none; + color: var(--font-color-grey-2); + } + } + + &.is-selected { + background: var(--primary-color-1); + color: var(--primary-color); + } + + &.is-disabled { + cursor: not-allowed; + + .col { + pointer-events: none; + color: var(--font-color-grey-2); + } + } + + &:hover:not(.is-selected-all):not(.is-selected):not(.is-disabled) .col { + background: var(--bg-color-grey-1); + } } -.table-view-item { - height: 100%; +.col { + border-bottom: 1px solid #f0f0f0; + flex: var(--col-span) 0 var(--col-width); } -.table-view-label, -.table-view-content { - font-size: var(--font-size-default); - padding: 8px 16px; - border-top: 1px solid #eeeeee; - border-left: 1px solid #eeeeee; - line-height: 20px; - word-break: break-all; +.col-content { + padding: 8px; + display: inline-block; } -.table-view-label { - background: var(--bg-color-grey-1); - color: var(--font-color-grey); - flex: 0 0 var(--col-label-width); - max-width: var(--col-label-width); +.single-checked { + flex: 0 0 30px; + align-content: center; } -.table-view-content { - word-break: break-all; - white-space: pre-line; - flex: 1; +.empty { + padding: 8px; } diff --git a/src/defaultFormat.js b/src/formatView.js similarity index 92% rename from src/defaultFormat.js rename to src/formatView.js index 31daea8..6c8ff7d 100644 --- a/src/defaultFormat.js +++ b/src/formatView.js @@ -1,7 +1,7 @@ import dayjs from 'dayjs'; import { isEmpty } from '@kne/is-empty'; -const defaultFormat = { +export const defaultFormat = { date: (value, { args }) => { const template = args[0] || 'YYYY-MM-DD'; return dayjs(value).format(template); @@ -50,7 +50,10 @@ const defaultFormat = { } }; -export const formatView = (value, format, context) => { +const formatView = (value, format, context) => { + if (!format) { + return value; + } const formatList = format.split(' ').filter(item => !!item); if (formatList.length > 0) { return formatList.reduce((value, format) => { @@ -63,4 +66,4 @@ export const formatView = (value, format, context) => { } }; -export default defaultFormat; +export default formatView; diff --git a/src/index.js b/src/index.js index 4daf6ab..6a2ce56 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,6 @@ export { default } from './InfoPage'; export { default as Content } from './Content'; export { default as Descriptions } from './Descriptions'; +export { default as CentralContent } from './CentralContent'; export { default as TableView } from './TableView'; -export * from './defaultFormat'; +export { default as formatView, defaultFormat } from './formatView';