HeaderConfigModal.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. import React from 'react'
  2. import classnames from 'classnames'
  3. import { uuid } from 'utils/util'
  4. import { fontWeightOptions, fontStyleOptions, fontFamilyOptions, fontSizeOptions, DefaultTableCellStyle } from '../constants'
  5. import { ITableHeaderConfig } from './types'
  6. import { Icon, Row, Col, Modal, Input, Button, Radio, Select, Table, message, Tooltip } from 'antd'
  7. const ButtonGroup = Button.Group
  8. const RadioGroup = Radio.Group
  9. const RadioButton = Radio.Button
  10. import { TableRowSelection, ColumnProps } from 'antd/lib/table'
  11. import ColorPicker from 'components/ColorPicker'
  12. import { fromJS } from 'immutable'
  13. import styles from './styles.less'
  14. import stylesConfig from '../styles.less'
  15. interface IHeaderConfigModalProps {
  16. visible: boolean
  17. config: ITableHeaderConfig[]
  18. onCancel: () => void
  19. onSave: (config: ITableHeaderConfig[]) => void
  20. }
  21. interface IHeaderConfigModalStates {
  22. localConfig: ITableHeaderConfig[]
  23. currentEditingConfig: ITableHeaderConfig
  24. currentSelectedKeys: string[]
  25. mapHeader: { [key: string]: ITableHeaderConfig }
  26. mapHeaderParent: { [key: string]: ITableHeaderConfig }
  27. }
  28. class HeaderConfigModal extends React.PureComponent<IHeaderConfigModalProps, IHeaderConfigModalStates> {
  29. private headerNameInput = React.createRef<Input>()
  30. public constructor (props: IHeaderConfigModalProps) {
  31. super(props)
  32. const localConfig = fromJS(props.config).toJS()
  33. const [mapHeader, mapHeaderParent] = this.getMapHeaderKeyAndConfig(localConfig)
  34. this.state = {
  35. localConfig,
  36. currentEditingConfig: null,
  37. mapHeader,
  38. mapHeaderParent,
  39. currentSelectedKeys: []
  40. }
  41. }
  42. public componentWillReceiveProps (nextProps: IHeaderConfigModalProps) {
  43. if (nextProps.config === this.props.config) { return }
  44. const localConfig = fromJS(nextProps.config).toJS()
  45. const [mapHeader, mapHeaderParent] = this.getMapHeaderKeyAndConfig(localConfig)
  46. this.setState({
  47. localConfig,
  48. mapHeader,
  49. mapHeaderParent,
  50. currentSelectedKeys: []
  51. })
  52. }
  53. private getMapHeaderKeyAndConfig (config: ITableHeaderConfig[]): [{ [key: string]: ITableHeaderConfig }, { [key: string]: ITableHeaderConfig }] {
  54. const map: { [key: string]: ITableHeaderConfig } = {}
  55. const mapParent: { [key: string]: ITableHeaderConfig } = {}
  56. config.forEach((c) => this.traverseHeaderConfig(c, null, (cursorConfig, parentConfig) => {
  57. map[cursorConfig.key] = cursorConfig
  58. mapParent[cursorConfig.key] = parentConfig
  59. return false
  60. }))
  61. return [map, mapParent]
  62. }
  63. private moveUp = () => {
  64. const { localConfig, mapHeaderParent, currentSelectedKeys } = this.state
  65. if (currentSelectedKeys.length <= 0) {
  66. message.warning('请勾选要上移的列')
  67. return
  68. }
  69. currentSelectedKeys.forEach((key) => {
  70. const parent = mapHeaderParent[key]
  71. const siblings = parent ? parent.children : localConfig
  72. const idx = siblings.findIndex((s) => s.key === key)
  73. if (idx < 1) { return }
  74. const temp = siblings[idx - 1]
  75. siblings[idx - 1] = siblings[idx]
  76. siblings[idx] = temp
  77. })
  78. this.setState({
  79. localConfig: [...localConfig]
  80. })
  81. }
  82. private moveDown = () => {
  83. const { localConfig, mapHeaderParent, currentSelectedKeys } = this.state
  84. if (currentSelectedKeys.length <= 0) {
  85. message.warning('请勾选要下移的列')
  86. return
  87. }
  88. currentSelectedKeys.forEach((key) => {
  89. const parent = mapHeaderParent[key]
  90. const siblings = parent ? parent.children : localConfig
  91. const idx = siblings.findIndex((s) => s.key === key)
  92. if (idx >= siblings.length - 1) { return }
  93. const temp = siblings[idx]
  94. siblings[idx] = siblings[idx + 1]
  95. siblings[idx + 1] = temp
  96. })
  97. this.setState({
  98. localConfig: [...localConfig]
  99. })
  100. }
  101. private mergeColumns = () => {
  102. const { localConfig, mapHeader, mapHeaderParent, currentSelectedKeys } = this.state
  103. if (currentSelectedKeys.length <= 0) {
  104. message.warning('请勾选要合并的列')
  105. return
  106. }
  107. const ancestors = []
  108. currentSelectedKeys.forEach((key) => {
  109. let cursorConfig = mapHeader[key]
  110. while (true) {
  111. if (currentSelectedKeys.includes(cursorConfig.key)) {
  112. const parent = mapHeaderParent[cursorConfig.key]
  113. if (!parent) { break }
  114. cursorConfig = parent
  115. } else {
  116. break
  117. }
  118. }
  119. if (ancestors.findIndex((c) => c.key === cursorConfig.key) < 0) {
  120. ancestors.push(cursorConfig)
  121. }
  122. })
  123. const isTop = ancestors.every((config) => !mapHeaderParent[config.key])
  124. if (!isTop) {
  125. message.warning('勾选的列应是当前最上级列')
  126. return
  127. }
  128. const insertConfig: ITableHeaderConfig = {
  129. key: uuid(5),
  130. headerName: `新建合并列`,
  131. alias: null,
  132. visualType: null,
  133. isGroup: true,
  134. style: {
  135. ...DefaultTableCellStyle,
  136. justifyContent: 'center'
  137. },
  138. children: ancestors
  139. }
  140. let minIdx = localConfig.length - ancestors.length
  141. minIdx = ancestors.reduce((min, config) => Math.min(min,
  142. localConfig.findIndex((c) => c.key === config.key)), minIdx)
  143. const ancestorKeys = ancestors.map((c) => c.key)
  144. const newLocalConfig = localConfig.filter((c) => !ancestorKeys.includes(c.key))
  145. newLocalConfig.splice(minIdx, 0, insertConfig)
  146. const [newMapHeader, newMapHeaderParent] = this.getMapHeaderKeyAndConfig(newLocalConfig)
  147. this.setState({
  148. localConfig: newLocalConfig,
  149. mapHeader: newMapHeader,
  150. mapHeaderParent: newMapHeaderParent,
  151. currentEditingConfig: insertConfig,
  152. currentSelectedKeys: []
  153. }, () => {
  154. this.headerNameInput.current.focus()
  155. this.headerNameInput.current.select()
  156. })
  157. }
  158. private cancel = () => {
  159. this.props.onCancel()
  160. }
  161. private save = () => {
  162. this.props.onSave(this.state.localConfig)
  163. }
  164. private traverseHeaderConfig (
  165. config: ITableHeaderConfig,
  166. parentConfig: ITableHeaderConfig,
  167. cb: (cursorConfig: ITableHeaderConfig, parentConfig?: ITableHeaderConfig) => boolean
  168. ) {
  169. let hasFound = cb(config, parentConfig)
  170. if (hasFound) { return hasFound }
  171. hasFound = Array.isArray(config.children) &&
  172. config.children.some((c) => this.traverseHeaderConfig(c, config, cb))
  173. return hasFound
  174. }
  175. private propChange = (record: ITableHeaderConfig, propName) => (e) => {
  176. const value = e.target ? e.target.value : e
  177. const { localConfig } = this.state
  178. const { key } = record
  179. const cb = (cursorConfig: ITableHeaderConfig) => {
  180. const isTarget = key === cursorConfig.key
  181. if (isTarget) {
  182. cursorConfig.style[propName] = value
  183. }
  184. return isTarget
  185. }
  186. localConfig.some((config) => this.traverseHeaderConfig(config, null, cb))
  187. this.setState({
  188. localConfig: [...localConfig]
  189. })
  190. }
  191. private editHeaderName = (key: string) => () => {
  192. const { localConfig } = this.state
  193. localConfig.some((config) => (
  194. this.traverseHeaderConfig(config, null, (cursorConfig) => {
  195. const hasFound = cursorConfig.key === key
  196. if (hasFound) {
  197. this.setState({
  198. currentEditingConfig: cursorConfig
  199. }, () => {
  200. this.headerNameInput.current.focus()
  201. this.headerNameInput.current.select()
  202. })
  203. }
  204. return hasFound
  205. })
  206. ))
  207. }
  208. private deleteHeader = (key: string) => () => {
  209. const { localConfig, mapHeader, mapHeaderParent } = this.state
  210. localConfig.some((config) => (
  211. this.traverseHeaderConfig(config, null, (cursorConfig) => {
  212. const hasFound = cursorConfig.key === key
  213. if (hasFound) {
  214. const parent = mapHeaderParent[cursorConfig.key]
  215. let idx
  216. if (parent) {
  217. idx = parent.children.findIndex((c) => c.key === cursorConfig.key)
  218. parent.children.splice(idx, 1, ...cursorConfig.children)
  219. } else {
  220. idx = localConfig.findIndex((c) => c.key === cursorConfig.key)
  221. localConfig.splice(idx, 1, ...cursorConfig.children)
  222. }
  223. }
  224. return hasFound
  225. })
  226. ))
  227. const [newMapHeader, newMapHeaderParent] = this.getMapHeaderKeyAndConfig(localConfig)
  228. this.setState({
  229. mapHeader: newMapHeader,
  230. mapHeaderParent: newMapHeaderParent,
  231. localConfig
  232. })
  233. }
  234. private saveEditingHeaderName = () => {
  235. const value = this.headerNameInput.current.input.value
  236. if (!value) {
  237. message.warning('请输入和并列名称')
  238. return
  239. }
  240. const { localConfig, currentEditingConfig } = this.state
  241. localConfig.some((config) => (
  242. this.traverseHeaderConfig(config, null, (cursorConfig) => {
  243. const hasFound = cursorConfig.key === currentEditingConfig.key
  244. if (hasFound) {
  245. cursorConfig.headerName = value
  246. }
  247. return hasFound
  248. })
  249. ))
  250. this.setState({
  251. localConfig: [...localConfig],
  252. currentEditingConfig: null
  253. })
  254. }
  255. private columns: Array<ColumnProps<any>> = [{
  256. title: '表格列',
  257. dataIndex: 'headerName',
  258. key: 'headerName',
  259. render: (_, record: ITableHeaderConfig) => {
  260. const { currentEditingConfig } = this.state
  261. const { key, headerName, alias, isGroup } = record
  262. if (!currentEditingConfig || currentEditingConfig.key !== key) {
  263. return isGroup ? (
  264. <span className={styles.tableEditCell}>
  265. <label>{alias || headerName}</label>
  266. <Icon type="edit" onClick={this.editHeaderName(key)} />
  267. <Icon type="delete" onClick={this.deleteHeader(key)} />
  268. </span>
  269. ) : (<label>{alias || headerName}</label>)
  270. }
  271. const { headerName: currentEditingHeaderName } = currentEditingConfig
  272. return (
  273. <>
  274. <Input
  275. ref={this.headerNameInput}
  276. size="small"
  277. className={styles.tableInput}
  278. defaultValue={currentEditingHeaderName}
  279. />
  280. <Button
  281. type="primary"
  282. size="small"
  283. onClick={this.saveEditingHeaderName}
  284. >
  285. 确定
  286. </Button>
  287. </>
  288. )
  289. }
  290. }, {
  291. title: '背景色',
  292. dataIndex: 'backgroundColor',
  293. key: 'backgroundColor',
  294. width: 60,
  295. render: (_, record: ITableHeaderConfig) => {
  296. const { style } = record
  297. const { backgroundColor } = style
  298. return (
  299. <Row type="flex" justify="center">
  300. <Col>
  301. <ColorPicker
  302. className={stylesConfig.color}
  303. value={backgroundColor}
  304. onChange={this.propChange(record, 'backgroundColor')}
  305. />
  306. </Col>
  307. </Row>
  308. )
  309. }
  310. }, {
  311. title: '字体',
  312. dataIndex: 'font',
  313. key: 'font',
  314. width: 285,
  315. render: (_, record: ITableHeaderConfig) => {
  316. const { style } = record
  317. const { fontSize, fontFamily, fontColor, fontStyle, fontWeight } = style
  318. return (
  319. <>
  320. <Row gutter={8} type="flex" align="middle" className={stylesConfig.rowBlock}>
  321. <Col span={14}>
  322. <Select
  323. size="small"
  324. className={stylesConfig.colControl}
  325. placeholder="字体"
  326. value={fontFamily}
  327. onChange={this.propChange(record, 'fontFamily')}
  328. >
  329. {fontFamilyOptions}
  330. </Select>
  331. </Col>
  332. <Col span={6}>
  333. <Select
  334. size="small"
  335. className={stylesConfig.colControl}
  336. placeholder="文字大小"
  337. value={fontSize}
  338. onChange={this.propChange(record, 'fontSize')}
  339. >
  340. {fontSizeOptions}
  341. </Select>
  342. </Col>
  343. <Col span={4}>
  344. <ColorPicker
  345. className={stylesConfig.color}
  346. value={fontColor}
  347. onChange={this.propChange(record, 'fontColor')}
  348. />
  349. </Col>
  350. </Row>
  351. <Row gutter={8} type="flex" align="middle" className={stylesConfig.rowBlock}>
  352. <Col span={12}>
  353. <Select
  354. size="small"
  355. className={stylesConfig.colControl}
  356. value={fontStyle}
  357. onChange={this.propChange(record, 'fontStyle')}
  358. >
  359. {fontStyleOptions}
  360. </Select>
  361. </Col>
  362. <Col span={12}>
  363. <Select
  364. size="small"
  365. className={stylesConfig.colControl}
  366. value={fontWeight}
  367. onChange={this.propChange(record, 'fontWeight')}
  368. >
  369. {fontWeightOptions}
  370. </Select>
  371. </Col>
  372. </Row>
  373. </>
  374. )
  375. }
  376. }, {
  377. title: '对齐',
  378. dataIndex: 'justifyContent',
  379. key: 'justifyContent',
  380. width: 180,
  381. render: (_, record: ITableHeaderConfig) => {
  382. const { style } = record
  383. const { justifyContent } = style
  384. return (
  385. <RadioGroup size="small" value={justifyContent} onChange={this.propChange(record, 'justifyContent')}>
  386. <RadioButton value="flex-start">左对齐</RadioButton>
  387. <RadioButton value="center">居中</RadioButton>
  388. <RadioButton value="flex-end">右对齐</RadioButton>
  389. </RadioGroup>
  390. )
  391. }
  392. }]
  393. private modalFooter = [(
  394. <Button
  395. key="cancel"
  396. size="large"
  397. onClick={this.cancel}
  398. >
  399. 取 消
  400. </Button>
  401. ), (
  402. <Button
  403. key="submit"
  404. size="large"
  405. type="primary"
  406. onClick={this.save}
  407. >
  408. 保 存
  409. </Button>
  410. )]
  411. private tableRowSelection: TableRowSelection<ITableHeaderConfig> = {
  412. hideDefaultSelections: true,
  413. onChange: (selectedRowKeys: string[]) => {
  414. this.setState({
  415. currentSelectedKeys: selectedRowKeys
  416. })
  417. }
  418. // @FIXME data columns do not allow check
  419. // getCheckboxProps: (record) => ({
  420. // disabled: !record.isGroup
  421. // })
  422. }
  423. public render () {
  424. const { visible } = this.props
  425. const { localConfig, currentSelectedKeys } = this.state
  426. const rowSelection: TableRowSelection<ITableHeaderConfig> = {
  427. ...this.tableRowSelection,
  428. selectedRowKeys: currentSelectedKeys
  429. }
  430. const wrapTableCls = classnames({
  431. [stylesConfig.rows]: true,
  432. [styles.headerTable]: true
  433. })
  434. return (
  435. <Modal
  436. title="表头样式与分组"
  437. width={1000}
  438. maskClosable={false}
  439. footer={this.modalFooter}
  440. visible={visible}
  441. onCancel={this.cancel}
  442. onOk={this.save}
  443. >
  444. <div className={stylesConfig.rows}>
  445. <Row gutter={8} className={stylesConfig.rowBlock} type="flex" align="middle">
  446. <Col span={4}>
  447. <Button type="primary" onClick={this.mergeColumns}>合并</Button>
  448. </Col>
  449. <Col span={19}>
  450. <Row gutter={8} type="flex" justify="end" align="middle">
  451. <ButtonGroup>
  452. <Button onClick={this.moveUp}><Icon type="arrow-up" />上移</Button>
  453. <Button onClick={this.moveDown}>下移<Icon type="arrow-down" /></Button>
  454. </ButtonGroup>
  455. </Row>
  456. </Col>
  457. <Col span={1}>
  458. <Row type="flex" justify="end">
  459. <Tooltip
  460. title="表格数据列请在外部拖拽以更改顺序"
  461. >
  462. <Icon type="info-circle" />
  463. </Tooltip>
  464. </Row>
  465. </Col>
  466. </Row>
  467. </div>
  468. <div className={wrapTableCls}>
  469. <Row gutter={8} className={stylesConfig.rowBlock}>
  470. <Col span={24}>
  471. <Table
  472. bordered={true}
  473. pagination={false}
  474. columns={this.columns}
  475. dataSource={localConfig}
  476. rowSelection={rowSelection}
  477. />
  478. </Col>
  479. </Row>
  480. </div>
  481. </Modal>
  482. )
  483. }
  484. }
  485. export default HeaderConfigModal