cjd-parkball
Version:
> 中后台业务组件库,中后台就像公园,进入需要买门票(登录),所以以 Parkball(公园球) 命名,公园内必定捕获!作为一个组件库,提供使用方法文档,方便开发者的调用
691 lines (684 loc) • 21.2 kB
JavaScript
/**
* 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