UNPKG

cjd-parkball

Version:

> 中后台业务组件库,中后台就像公园,进入需要买门票(登录),所以以 Parkball(公园球) 命名,公园内必定捕获!作为一个组件库,提供使用方法文档,方便开发者的调用

691 lines (684 loc) 21.2 kB
/** * CompareTable */ import React from 'react' import PropTypes from 'prop-types' import { Icon, Checkbox } from 'antd' import './index.scss' function analyseSource (cols, sourceData, relayData, relayKeys) { let renderableData = relayData || [] let allKeys = relayKeys || [] for (let i = 0; i < cols.length; i += 1) { const row = cols[i] const value = [] const { key, children } = row for (let j = 0; j < sourceData.length; j += 1) { value.push(sourceData[j][key]) } row.value = value renderableData.push(row) allKeys.push(key) if (children) { const { renderableData: d, allKeys: k } = analyseSource(children, sourceData, renderableData, allKeys) renderableData = d allKeys = k } } return { renderableData, allKeys } } function filterUnRender (unRender, sourceData, relayData) { if (!unRender.length) { return sourceData } let renderableData = relayData || [] for (let i = 0; i < sourceData.length; i += 1) { const { key, children } = sourceData[i] if (!unRender.includes(key)) { renderableData.push(sourceData[i]) } if (unRender.includes(key) && children) { filterUnRender(children.map(c => c.key), sourceData.slice(i), renderableData) } } return renderableData } function filterUnExpanded (unExpanded, sourceData, filterKeys = []) { if (!unExpanded.length) { return sourceData } for (let i = 0; i < sourceData.length; i += 1) { const { key, children } = sourceData[i] if (unExpanded.includes(key)) { for (let j = 0; j < children.length; j += 1) { const { key: k, children: c } = children[j] filterKeys.push(k) if (c) { filterUnExpanded(c.map(loop => loop.key), sourceData, filterKeys) } } } } return sourceData.filter(d => !filterKeys.includes(d.key)) } function offsetToWindow (ele) { const { left, top } = ele.getBoundingClientRect() return { left, top } } class CompareTable extends React.Component { static propTypes = { indent: PropTypes.bool, key: PropTypes.string, columns: PropTypes.array, dataSource: PropTypes.array, distribution: PropTypes.object, checkBoxGroup: PropTypes.array, } static defaultProps = { indent: false, key: 'name', columns: [ { key: 'name', label: '车名', }, { key: 'guidePrice', label: '厂商指导价', }, { key: 'referencePrice', label: '经销商参考价', }, { key: 'body', label: '车身信息', children: [ { key: 'length', label: '车身长度' }, { key: 'width', label: '车身宽度' }, { key: 'high', label: '车身高度' }, { key: 'distance', label: '轴距' }, ], }, { key: 'engine', label: '发动机配置', children: [ { key: 'type', label: '发动机型号' }, { key: 'displacement', label: '排量' }, { key: 'airIntake', label: '进气方式' }, { key: 'cylinders', label: '每缸气门数' }, ], }, { key: 'seat', label: '座椅参数', children: [ { key: 'texture', label: '材质' }, { key: 'cup', label: '杯架数' }, { key: 'handrail', label: '扶手配置' }, { key: 'memory', label: '座椅记忆' }, ], }, { key: 'config', label: '操作配置', children: [ { key: 'radio', label: '驻车雷达' }, { key: 'video', label: '倒车影像' }, { key: 'cruise', label: '定速巡航' }, ], }, { key: 'multimedia', label: '多媒体配置', children: [ { key: 'GPS', label: 'GPS' }, { key: 'screen', label: '彩色大屏' }, { key: 'blue', label: '蓝牙' }, ], }, { key: 'lamplight', label: '灯光配置', children: [ { key: 'autoHead', label: '自动头灯' }, { key: 'turnHelp', label: '转向辅助' }, { key: 'foglight', label: '雾灯' }, ], }, { key: 'guard', label: '防盗配置', children: [ { key: 'louver', label: '电动车窗' }, { key: 'rim', label: '铝合金轮圈' }, ], }, { key: 'safe', label: '安全配置', children: [ { key: 'gasbag', label: '前后排侧气囊' }, { key: 'curtain', label: '前后排头部气囊' }, ], }, { key: 'wheel', label: '车轮制动', children: [ { key: 'backup', label: '备胎配置' }, { key: 'front', label: '前轮胎规格' }, { key: 'rear', label: '后轮胎规格' }, ], }, ], dataSource: [{ name: '自行车', guidePrice: 666, referencePrice: 777, length: 5665, width: 1720, high: 1740, distance: 3480, type: 'V22', displacement: 2.2, airIntake: '自然吸气', cylinders: 4, texture: '真皮', cup: '单杯架', handrail: '中央扶手', memory: '主驾驶座椅记忆', distanceLv: '高级', memoryPower: '四向', radio: '无倒车雷达', video: '有倒车影像', cruise: '-', GPS: '高德 GPS', screen: '10英寸', blue: '4.0蓝牙', autoHead: '自动头灯', turnHelp: '360转向辅助', foglight: '前雾灯', louver: '全景电动车窗', rim: '铁制轮圈', gasbag: '前排双气囊', curtain: '主驾驶侧气帘', backup: '全尺寸备胎', front: '18寸', rear: '18寸', }, { name: '摩托车', guidePrice: 666, referencePrice: 777, length: 5665, width: 1722, high: 1750, distance: 3600, type: 'V21', displacement: 1.5, airIntake: '涡轮增压', cylinders: 3, texture: '纺织', cup: '三倍架', handrail: '后排扶手', memory: '主副座椅记忆', distanceLv: '中级', memoryPower: '双向', radio: '有倒车雷达', video: '360倒车影像', cruise: 'ACC', GPS: '百度 GPS', screen: '12英寸大屏', blue: '3.0蓝牙', autoHead: '非自动前灯', turnHelp: '180转向辅助', foglight: '后雾灯', louver: '电动车窗', rim: '铝合金', gasbag: '全车气囊', curtain: '前后双气帘', backup: '非全尺寸', front: '17英寸', rear: '17英寸', }], distribution: { controlCols: ['name'], fixedCols: ['name'], headCols: ['guidePrice', 'referencePrice'], }, checkBoxGroup: ['hiddenSame', 'lightDiff'], } constructor (props) { super(props) this.state = { renderableData: [], unExpanded: [], unRender: [], delKeys: [], checkBoxGroup: [], floorCurrent: '', navAreaStyle: {}, fixedTableStyle: { display: 'none' }, } this.scrollRoot = null; this.presetCheck = { hiddenSame: { name: 'hiddenSame', value: false, label: '隐藏相同项', onChange: (checked) => { const { renderableData } = this.state if (checked) { this.setState({ unRender: renderableData.filter((d) => { if (d.value) { const uniq = [...new Set(d.value)] if (uniq.length === 1 && (uniq[0] || uniq === 0)) { return true } } }).map(d => d.key) }) } if (!checked) { this.setState({ unRender: [] }) } }, }, lightDiff: { name: 'lightDiff', value: false, label: '高亮不同项', onChange: (checked) => { if (checked) { const { renderableData } = this.state this.setState({ renderableData: renderableData.map((d) => { const { render, value } = d const uniq = [...new Set(value)] if (uniq.length === 2) { const a = [uniq[0]] const b = [uniq[1]] let more for (let i = 0; i < value.length; i += 1) { const v = value[i] v === a[0] && a.push(v) v === b[0] && b.push(v) if (a.length > b.length) { more = a[0] } if (b.length > a.length) { more = b[0] } } if (more) { return { ...d, render: (v, value, data) => { const r = render ? render(v, value, data) : v return v === more ? <div className="high-light">{r}</div> : r } } } return d } return d }) }) } if (!checked) { const { delKeys } = this.state const { key, columns, dataSource } = this.props const { renderableData } = analyseSource(columns, dataSource.filter(d => !delKeys.includes(d[key]))) this.setState({ renderableData, }) } }, } } } floorCurrent = (e) => { const { columns } = this.props const { distribution: { bodyCols } } = this.state /** * trap tag * 导航深度只有一层 */ const floors = columns.filter(c => bodyCols.includes(c.key)).map(c => c.key) const floorCurrent = [] for (let i = 0; i < floors.length; i += 1) { const floor = floors[i] const nextFloor = floors[i + 1] const floorEle = this.floorNav.getElementsByClassName(`floor-single-${floor}`)[0] const { top: ft } = offsetToWindow(floorEle) const { top: tt } = offsetToWindow(this.bodyTable.getElementsByClassName(`foldable-tr-${floor}`)[0]) const { top: ntt } = nextFloor ? offsetToWindow(this.bodyTable.getElementsByClassName(`foldable-tr-${nextFloor}`)[0]) : { top: tt + 1 } if (ft >= tt && (ft - floorEle.offsetHeight) < ntt) { floorCurrent.push(floor) } } if (floorCurrent.length > 1) { console.log(e.target) } console.log(floorCurrent) return floorCurrent } onBodyWheel = (e) => { const { scrollTop: rootSt, scrollHeight: rootSh, offsetHeight: rootOh } = this.scrollRoot const { offsetTop: controlOt, offsetHeight: controlOh } = this.controlTable const { offsetHeight: headOh } = this.headTable || {} const { offsetTop: bodyOt } = this.bodyTable const { top: offsetTopWindow } = offsetToWindow(this.scrollRoot) const isOnRoot = !!e.path.filter(loop => loop.className && loop.className.includes('scroll-root')).length const isToBottom = (rootSt + rootOh === rootSh) this.setState((prevState) => { const { fixedTableStyle: { display }, floorCurrent } = prevState if (rootSt < controlOt) { return { navAreaStyle: { position: 'absolute', top: bodyOt }, fixedTableStyle: { display: 'none', }, } } if (rootSt >= controlOt) { let loop if (display === 'none' || !isOnRoot || isToBottom) { loop = { fixedTableStyle: { display: 'block', position: 'fixed', top: offsetTopWindow } } } return { floorCurrent: this.floorCurrent(e)[0] || floorCurrent, navAreaStyle: { position: 'fixed', top: offsetTopWindow + controlOh + (headOh || 0) }, ...loop } } }) } componentWillMount () { let { columns, dataSource, checkBoxGroup, distribution } = this.props checkBoxGroup = checkBoxGroup.map((c) => { if (typeof c === 'string') { return this.presetCheck[c] } return c }) const { renderableData, allKeys } = analyseSource(columns, dataSource) const bodyCols = distribution.bodyCols || Object.values(distribution).reduce((bodyCols, current) => bodyCols.filter(c => !current.includes(c)), allKeys) this.setState({ renderableData, checkBoxGroup, distribution: { ...distribution, bodyCols }, floorCurrent: bodyCols[0] }) } componentDidMount () { this.scrollRoot = this.compareTable.offsetParent this.scrollRoot.className = `${this.scrollRoot.className} scroll-root` document.body.addEventListener('wheel', this.onBodyWheel, true) this.setState({ navAreaStyle: { position: 'absolute', top: this.bodyTable.offsetTop } }) } componentWillReceiveProps (nextProps) { let { key, delKeys } = this.state let { key: nk, columns, dataSource, checkBoxGroup, distribution } = nextProps if (key !== nk) { delKeys = [] } checkBoxGroup = checkBoxGroup.map((c) => { if (typeof c === 'string') { return this.presetCheck[c] } return c }) const { renderableData, allKeys } = analyseSource(columns, dataSource.filter(d => !delKeys.includes(d[nk]))) const bodyCols = distribution.bodyCols || Object.values(distribution).reduce((bodyCols, current) => bodyCols.filter(c => !current.includes(c)), allKeys) this.setState({ renderableData, checkBoxGroup, distribution: { ...distribution, bodyCols } }) } componentDidUpdate (prevProps, prevState) { } buildFloorNav () { const { columns } = this.props const { floorCurrent, distribution: { bodyCols } } = this.state /** * trap tag * 导航深度只有一层 */ const lis = columns.filter(c => bodyCols.includes(c.key)).map((col) => { const { key, label } = col let className = '' if (floorCurrent === key) { className = `floor-single floor-single-${key} floor-current` } if (floorCurrent !== key) { className = `floor-single floor-single-${key}` } return (<li key={`${key}-floor`} className={className} onClick={() => { this.setState({ floorCurrent: key }) }} > {label} </li>) }) return (<ul ref={floorNav => this.floorNav = floorNav} className="floor-nav" > {lis} </ul>) } buildNavArea () { const { navAreaStyle } = this.state return (<div className="nav-area" style={navAreaStyle} > {this.buildFloorNav()} </div>) } buildControlTr (rowSpan) { const { checkBoxGroup, delKeys } = this.state const { key, columns, dataSource } = this.props const checkBoxes = (<div> {checkBoxGroup.map((ckb) => { const { name, label, value, onChange } = ckb return (<Checkbox key={name} checked={value} onChange={(e) => { const { checked } = e.target const group = [] const values = {} for (let i = 0; i < checkBoxGroup.length; i += 1) { const ck = checkBoxGroup[i] if (ck.name === name) { ck.value = checked } group.push(ck) values[name] = ck.value } this.setState({ checkBoxGroup: group }) onChange && onChange(checked, values) }}> {label} </Checkbox>) })} </div>) const tdsData = dataSource.filter(d => !delKeys.includes(d[key])) const tds = tdsData.map((d, i) => { const k = d[key] const { renderableData } = analyseSource(columns, tdsData.filter(d => d[key] !== k)) return (<td key={`del-td-${k}`}> <Icon type="close-square-o" onClick={() => { this.setState({ delKeys: [...delKeys, k], renderableData, }) }} /> </td>) }) return (<tr key="control-tr"> <th key="control-th" rowSpan={rowSpan + 2}>{checkBoxes}</th> {tds} </tr>) } buildMoveColTr () { const { delKeys } = this.state const { key, columns, dataSource } = this.props const seats = [] const tds = dataSource.filter((d, i) => { if (!delKeys.includes(d[key])) { seats.push(i) return true } }).map((d, i) => { const { renderableData } = analyseSource(columns, dataSource) let leftHandler = <Icon type="double-left" onClick={() => { const loop = dataSource.splice(seats[i - 1], 1, d)[0] dataSource.splice(seats[i], 1, loop) this.setState({ renderableData }) }} /> let rightHandler = <Icon type="double-right" onClick={() => { const loop = dataSource.splice(seats[i + 1], 1, d)[0] dataSource.splice(seats[i], 1, loop) this.setState({ renderableData }) }} /> if (i === 0) { leftHandler = '' } if (i === dataSource.length - 1) { rightHandler = '' } return (<td key={`move-td-${d[key]}`}> {leftHandler} {rightHandler} </td>) }) return (<tr key="move-tr">{tds}</tr>) } buildBodyTableTh ({ key, label, children }) { const { unExpanded } = this.state const isExpanded = !unExpanded.includes(key) let icon if (children) { icon = ( <Icon type={ isExpanded ? 'minus-square-o' : 'plus-square-o' } onClick={() => { this.setState((prevState) => { const { unExpanded } = prevState let un = [] if (unExpanded.includes(key)) { un = unExpanded.filter(l => l !== key) } else { un = [...unExpanded, key] } return { unExpanded: un } }) }} /> ) } return <th key={`${key}-th`}>{icon}{label}</th> } buildGeneralTds (d) { const tds = [] const { key, value, render } = d for (let i = 0; i < value.length; i += 1) { const v = value[i] tds.push(<td key={`${key}-td-${i}`}>{render ? render(v, value, d) : v}</td>) } return tds } buildBodyTable () { const { renderableData, unExpanded, unRender, distribution: { bodyCols } } = this.state let bodyData = renderableData.filter(d => bodyCols.includes(d.key)) bodyData = filterUnRender(unRender, bodyData) bodyData = filterUnExpanded(unExpanded, bodyData) const trs = bodyData.map((d) => { const { key, children } = d const className = children ? `foldable-tr foldable-tr-${key}` : '' const tr = [this.buildBodyTableTh(d), ...this.buildGeneralTds(d)] return <tr className={className} key={`${key}-tr`}>{tr}</tr> }) return ( <table className="body-table" ref={bodyTable => this.bodyTable = bodyTable} > <tbody>{trs}</tbody> </table> ) } buildHeadTable () { const { renderableData, delKeys, distribution: { headCols } } = this.state const headData = renderableData.filter(d => headCols.includes(d.key)) const trs = headData.map((d) => { let { key, label } = d const tr = [<th key={`${key}-th`}>{label}</th>, ...this.buildGeneralTds(d)] return <tr key={`${key}-tr`}>{tr}</tr> }) return ( <table className="head-table" ref={headTable => this.headTable = headTable} > <tbody>{trs}</tbody> </table> ) } buildFixedTable () { const { renderableData, fixedTableStyle, distribution: { fixedCols } } = this.state const fixedData = renderableData.filter(d => fixedCols.includes(d.key)) return (<table className="fixed-table" style={fixedTableStyle} ref={fixedTable => this.fixedTable = fixedTable} > <tbody> {this.buildControlTr(fixedData.length)} {this.buildMoveColTr()} </tbody> </table>) } buildControlTable () { const { renderableData, distribution: { controlCols } } = this.state const controlData = renderableData.filter(d => controlCols.includes(d.key)) return (<table className="control-table" ref={controlTable => this.controlTable = controlTable} > <tbody> {this.buildControlTr(controlData.length)} {this.buildMoveColTr()} </tbody> </table>) } buildTableArea () { return (<div className="table-area"> {this.buildControlTable()} {this.buildFixedTable()} {this.buildHeadTable()} {this.buildBodyTable()} </div>) } render () { return ( <div className="demo-wrap"> <div className="test"></div> <div className="compare-table" ref={compareTable => this.compareTable = compareTable} > {this.buildTableArea()} {this.buildNavArea()} </div> </div> ) } } export default CompareTable