list.jsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. /**
  2. * 图表列表
  3. */
  4. import React from 'react'
  5. import { Layout, Button, Icon, Input, Menu, Dropdown, Card, Col, Row, Popover, Breadcrumb, Tree, Tag } from 'antd'
  6. import { connect } from 'dva'
  7. import './list.less'
  8. import ChooseDataSourceBox from './chooseDataSourceBox'
  9. import { dateFormat } from '../../utils/baseUtils'
  10. import Ellipsis from 'ant-design-pro/lib/Ellipsis'
  11. import 'ant-design-pro/dist/ant-design-pro.css'
  12. import GroupSelector from '../datasource/groupSelector'
  13. import Thumbnail from './thumbnail'
  14. import DistributeBox from './distributeBox';
  15. import TransferBox from './transferBox'
  16. import DeleteBox from '../common/deleteBox'
  17. const { Content } = Layout
  18. const { Search } = Input
  19. const CardGrid = Card.Grid
  20. const { TreeNode } = Tree
  21. class ChartList extends React.Component {
  22. constructor(props) {
  23. super(props);
  24. this.state = {
  25. selectedRecord: null,
  26. visibleChooseDataSourceBox: false,
  27. visibleDistributeBox: false,
  28. visibleTransferBox: false,
  29. visibleGroupMenu: false, // 显示分组菜单
  30. visibleDeleteBox: false
  31. }
  32. }
  33. componentDidMount() {
  34. const { dispatch } = this.props;
  35. this.setBodyWidth();
  36. dispatch({ type: 'chart/fetchList' });
  37. dispatch({ type: 'chart/remoteGroupList' });
  38. }
  39. /**
  40. * 设置卡片容器宽度 = 每行最大卡片数量 * 卡片宽度
  41. */
  42. setBodyWidth() {
  43. const chartBody = document.getElementsByClassName('chart-body')[0]; // 卡片容器
  44. const parent = chartBody.parentNode; // 父级容器
  45. const pWidth = parent.offsetWidth; // 父级容器宽度
  46. const pPadding = 10 + 10; // 父级容器左右padding
  47. const cWidth = 207; // 每个卡片宽度
  48. const cMargin = 5 + 5; // 每个卡片左右margin
  49. const pTrueWidth = pWidth - pPadding; // 父容器实际可用宽度
  50. const cTrueWidth = cWidth + cMargin; // 卡片实际占用宽度
  51. const count = Math.floor(pTrueWidth/cTrueWidth); // 每行最大卡片数量
  52. chartBody.style.width = count * cTrueWidth + 'px';
  53. }
  54. generateCard() {
  55. const { chart, dispatch } = this.props;
  56. const groupList = chart.groupList;
  57. const { selectedRecord } = this.state;
  58. const list = chart.list;
  59. const currentGroup = chart.currentGroup;
  60. const reg = new RegExp('([+ \\- & | ! ( ) { } \\[ \\] ^ \" ~ * ? : ( ) \/])', 'g'); // 需要转义的字符
  61. let filterLabel = chart.filterLabel.replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1'); // 添加转义符号
  62. const operationMenu = (
  63. <Menu className='menu-operation'>
  64. <Menu.Item onClick={() => {
  65. const { selectedRecord } = this.state;
  66. // const selectedChartDataSourceCode = selectedRecord ? selectedRecord.code : '';
  67. const selectedChartCode = selectedRecord ? selectedRecord.code : '';
  68. dispatch({ type: 'chartPolicy/fetchList', chartCode: selectedChartCode });
  69. // dispatch({ type: 'chartDesigner/remoteDataColumn', code: });
  70. this.setState({visibleDistributeBox: true})
  71. }}>
  72. <Icon type='share-alt'/>分发
  73. </Menu.Item>
  74. <Menu.SubMenu className='setgroupmenu' title={<div><Icon style={{ marginRight: '6px' }} type='profile' />移动到</div>}>
  75. {this.createGroupMenu(selectedRecord)}
  76. </Menu.SubMenu>
  77. <Menu.Divider />
  78. <Menu.Item
  79. onClick={()=>{
  80. this.setState({ visibleTransferBox: true})
  81. }}
  82. >
  83. <Icon type="swap" />移交
  84. </Menu.Item>
  85. <Menu.Item
  86. onClick={(e) => {
  87. this.setState({ visibleDeleteBox: true})
  88. }}
  89. >
  90. <Icon type="delete" />删除
  91. </Menu.Item>
  92. </Menu>
  93. )
  94. let groupFilter = groupList.concat({ code: '-1', label: '未分组' }).filter(g => (
  95. currentGroup[0].code === 'all' ||
  96. (
  97. currentGroup[0].code === '-1' ? g.code === '-1' : (
  98. currentGroup[1] ? (g.code === currentGroup[1].code) :
  99. (
  100. g.code === currentGroup[0].code ||
  101. g.pcode === currentGroup[0].code
  102. )
  103. )
  104. )
  105. )).map(g => g.code);
  106. let cards = list.filter(l => groupFilter.indexOf(l.groupCode+'') !== -1).map(l => {
  107. let reg = new RegExp('(' + filterLabel + '){1}', 'ig');
  108. if((l.name || '').search(reg) !== -1 || (l.description || '').search(reg) !== -1) {
  109. return { ...l, bf: false };
  110. }else {
  111. return { ...l, bf: true };
  112. }
  113. }).sort((a, b) => {
  114. return new Date(b.createTime) - new Date(a.createTime)
  115. }).map( (l, i) => (
  116. <CardGrid className={`chart-card${l.bf?' chart-card-hide':''}`} key={i} onClick={() => {
  117. this.setState({ selectedRecord: l })
  118. }}>
  119. <Card
  120. title={
  121. <Row type='flex' justify='space-between'>
  122. <Col span={21} style={{ overflow: 'hidden', textOverflow: 'ellipsis' }} >
  123. { filterLabel ?
  124. ((l.name || '').split(new RegExp(`(${filterLabel})`, 'i')).map((fragment, i) => {
  125. return (
  126. fragment.toLowerCase().replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1') === filterLabel.toLowerCase() ?
  127. <span key={i} style={{fontWeight: 'bold', color: 'red'}} className="highlight">{fragment}</span> :
  128. fragment
  129. )
  130. }
  131. )) : l.name
  132. }
  133. </Col>
  134. <Col style={{ textAlign: 'right' }} span={3} >
  135. <Icon type='star-o'/>
  136. </Col>
  137. </Row>
  138. }
  139. cover={
  140. <Col className='cover-body'>
  141. <Row className='thumb' onClick={() => {
  142. dispatch({ type: 'chartDesigner/reset' });
  143. dispatch({ type: 'main/redirect', path: '/chart/' + l.code });
  144. }}>
  145. {/* <div className='deny-body'>
  146. <div className='deny-tip'>您没有对应数据源的权限</div>
  147. </div> */}
  148. <Thumbnail type={l.type} code={l.code} option={l.chartOption}/>
  149. </Row>
  150. <Row className='desc'>
  151. <Ellipsis tooltip={l.description&&l.description.length > 16} lines={2}>{
  152. <span>
  153. { filterLabel ?
  154. ((l.description || '').split(new RegExp(`(${filterLabel})`, 'i')).map((fragment, i) => {
  155. return (
  156. fragment.toLowerCase().replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1') === filterLabel.toLowerCase() ?
  157. <span key={i} style={{fontWeight: 'bold', color: 'red'}} className="highlight">{fragment}</span> :
  158. fragment
  159. )
  160. }
  161. )) : l.description
  162. }
  163. </span>
  164. }</Ellipsis>
  165. </Row>
  166. <Row className='footer' type='flex' justify='end' align='bottom'>
  167. <Col style={{ textAlign: 'left' }} span={22}>
  168. <Row>{l.creator} {dateFormat(l.createTime, 'yyyy-MM-dd')}</Row>
  169. </Col>
  170. <Col span={2} style={{ textAlign: 'right' }}>
  171. <Dropdown overlay={operationMenu} trigger={['click']}>
  172. <Icon type="ellipsis" />
  173. </Dropdown>
  174. </Col>
  175. </Row>
  176. </Col>
  177. }
  178. >
  179. </Card>
  180. </CardGrid>
  181. ));
  182. if(cards.length === 0) {
  183. cards = <div style={{ padding: '7px', textAlign: 'center', fontSize: '14px', color: 'rgba(0, 0, 0, 0.45)' }}>暂无数据</div>
  184. }
  185. return cards;
  186. }
  187. createGroupMenu = (selectedRecord) => {
  188. const { chart, dispatch } = this.props;
  189. const groupList = chart.groupList;
  190. const pGroups = groupList.filter(d => d.pcode === '-1').sort((a, b) => a.index - b.index);
  191. const cGroups = groupList.filter(d => d.pcode !== '-1');
  192. let allGroups = !!selectedRecord ? [
  193. { code: '-1', label: '未分组' }
  194. ].concat(pGroups) : [
  195. { code: 'all', label: '全部分组' },
  196. { code: '-1', label: '未分组' }
  197. ].concat(pGroups);
  198. return allGroups.map(p => {
  199. let c = cGroups.filter(c => c.pcode === p.code).sort((a, b) => a.index - b.index);
  200. return c.length > 0 ? (
  201. <Menu.SubMenu key={p.code} title={<span style={{ fontWeight: !!selectedRecord ?
  202. (p.code+'' === selectedRecord.groupCode+'' ? 'bold' : (
  203. c.find(ch => ch.code+'' === selectedRecord.groupCode+'') && c.find(ch => ch.code+'' === selectedRecord.groupCode+'').pcode === p.code ? 'bold' : 'normal'
  204. ))
  205. : chart.currentGroup[0].code === p.code ? 'bold' : 'normal' }}>{p.label}</span>} onTitleClick={(item) => {
  206. dispatch({ type: 'chart/setCurrentGroup', group1: p });
  207. if(selectedRecord) {
  208. dispatch({ type: 'chart/remoteSetChartGroup', chart: selectedRecord, group: p });
  209. }
  210. this.hideGroupMenu();
  211. }}>
  212. {c.map(c => {
  213. return (<Menu.Item key={c.code} onClick={(item) => {
  214. dispatch({ type: 'chart/setCurrentGroup', group1: p, group2: c });
  215. if(selectedRecord) {
  216. dispatch({ type: 'chart/remoteSetChartGroup', chart: selectedRecord, group: c });
  217. }
  218. }}><span style={{ fontWeight: !!selectedRecord ? (
  219. selectedRecord.groupCode+'' === c.code+'' ? 'bold' : 'normal'
  220. ) : (chart.currentGroup[1] && (chart.currentGroup[1].code === c.code) ? 'bold' : 'normal') }}>{c.label}</span></Menu.Item>)
  221. })}
  222. </Menu.SubMenu>
  223. ) : (
  224. <Menu.Item key={p.code} onClick={() => {
  225. dispatch({ type: 'chart/setCurrentGroup', group1: p });
  226. if(selectedRecord) {
  227. dispatch({ type: 'chart/remoteSetChartGroup', chart: selectedRecord, group: p });
  228. }
  229. this.hideGroupMenu();
  230. }}><span style={{ fontWeight: !!selectedRecord ? (
  231. selectedRecord.groupCode+'' === p.code+'' ? 'bold' : 'normal'
  232. ) : chart.currentGroup[0] && (chart.currentGroup[0].code === p.code) ? 'bold' : 'normal' }}>{p.label}</span></Menu.Item>
  233. );
  234. });
  235. }
  236. createSubGroupMenu = () => {
  237. const { chart, dispatch } = this.props;
  238. const groupList = chart.groupList;
  239. const parentGroup = chart.currentGroup[0];
  240. const children = groupList.filter(d => d.pcode === parentGroup.code);
  241. const subGroup = chart.currentGroup[1];
  242. return children.map(c => {
  243. return (
  244. <Menu.Item key={c.code} onClick={() => {
  245. dispatch({ type: 'chart/setCurrentGroup', group1: parentGroup, group2: c });
  246. }}><span style={{ fontWeight: subGroup && (subGroup.code === c.code) ? 'bold' : 'normal' }}>{c.label}</span></Menu.Item>
  247. );
  248. })
  249. }
  250. createGroupTree(modify) {
  251. const { dispatch, chart } = this.props;
  252. const { groupEditing } = this.state;
  253. const groupList = chart.groupList;
  254. let parent = groupList.filter(d => d.pcode === '-1').sort((a, b) => a.index - b.index);
  255. let children = groupList.filter(d => d.pcode !== '-1');
  256. let groupTree = parent.map(p => {
  257. return (
  258. <TreeNode disabled={groupEditing} title={
  259. modify ? (<div><Icon style={{ cursor: 'move' }} type='drag'/>
  260. <Input value={p.label} size='small' focus={'true'} onFocus={() => {
  261. this.setState({
  262. groupEditing: true
  263. });
  264. }} onChange={(e) => {
  265. dispatch({ type: 'chart/modifyGroup', group: {...p, label:e.target.value} });
  266. }} onBlur={(e) => {
  267. this.setState({
  268. groupEditing: false
  269. });
  270. dispatch({ type: 'chart/remoteModifyGroup', group: {...p, label:e.target.value} });
  271. }} onPressEnter={(e) => {
  272. dispatch({ type: 'chart/remoteModifyGroup', group: {...p, label:e.target.value} });
  273. }} /><Icon type='plus-circle-o' onClick={() => {
  274. dispatch({ type: 'chart/remoteAddGroup', pgroup: p });
  275. }}/><Icon type='minus-circle' onClick={() => {
  276. dispatch({ type: 'chart/remoteDeleteGroup', group: p });
  277. }}/></div>) : p.label} key={p.code}>
  278. {
  279. children.filter(c => c.pcode === p.code).sort((a, b) => a.index - b.index).map(c => {
  280. return (
  281. <TreeNode disabled={groupEditing} title={
  282. modify ? (<div><Icon style={{ cursor: 'move' }} type='drag'/>
  283. <Input value={c.label} size='small' onFocus={() => {
  284. this.setState({
  285. groupEditing: true
  286. });
  287. }} onChange={(e) => {
  288. dispatch({ type: 'chart/modifyGroup', group: {...c, label:e.target.value} });
  289. }} onBlur={(e) => {
  290. this.setState({
  291. groupEditing: false
  292. });
  293. dispatch({ type: 'chart/remoteModifyGroup', group: {...c, label:e.target.value} });
  294. }} onPressEnter={(e) => {
  295. dispatch({ type: 'chart/remoteModifyGroup', group: {...c, label:e.target.value} });
  296. }} onCompositionEnd={(e) => {
  297. console.log(e.target.value);
  298. }}/><Icon type='minus-circle' onClick={() => {
  299. dispatch({ type: 'chart/remoteDeleteGroup', group: c });
  300. }}/></div>) : p.label
  301. } key={c.code} />
  302. )
  303. })
  304. }
  305. </TreeNode>
  306. )
  307. });
  308. return groupTree;
  309. }
  310. handleVisibleChange = (flag) => {
  311. this.setState({ visibleGroupMenu: flag });
  312. }
  313. hideGroupMenu = () => {
  314. this.setState({
  315. visibleGrouMenu: false
  316. });
  317. }
  318. onDrop = (info) => {
  319. const { dispatch } = this.props;
  320. const dropCode = info.node.props.eventKey;
  321. const dragCode = info.dragNode.props.eventKey;
  322. const dropPos = info.node.props.pos.split('-');
  323. const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]); // -1/0/1 -> 兄/子/弟
  324. console.log(dragCode, dropCode, dropPosition);
  325. dispatch({ type: 'chart/remoteMoveGroup', dragCode, dropCode, dropPosition });
  326. }
  327. render() {
  328. const { visibleChooseDataSourceBox, visibleDistributeBox, visibleTransferBox, visibleDeleteBox, selectedRecord } = this.state;
  329. const { dispatch, chart } = this.props;
  330. const TAG_COLOR = ['blue'];
  331. return (
  332. <Layout className='chart-list'>
  333. <Content>
  334. <Card title={
  335. <Row className='tools' type='flex' justify='space-between'>
  336. <Col style={{ display: 'flex' }}>
  337. <Popover overlayClassName='popover-group' title={
  338. <Row className='grouptree-title' type='flex' justify='space-between'>
  339. <Col>
  340. 分组管理
  341. </Col>
  342. <Col>
  343. <div className='create-group' onClick={() => {
  344. dispatch({ type: 'chart/remoteAddGroup' });
  345. }}>添加分组<Icon type="plus-circle-o" /></div>
  346. </Col>
  347. </Row>
  348. } trigger="click" placement="bottomLeft" content={(
  349. <Tree
  350. className='tree-group'
  351. showLine
  352. defaultExpandAll
  353. draggable
  354. onDragStart={this.onDragStart}
  355. onDrop={this.onDrop}
  356. >
  357. {
  358. this.createGroupTree(true)
  359. }
  360. </Tree>
  361. )}>
  362. <Icon type="bars" />
  363. </Popover>
  364. <Breadcrumb className='group' separator=">">
  365. <Breadcrumb.Item>
  366. <GroupSelector model={chart} modelName='chart'>
  367. <Tag color={TAG_COLOR[Math.ceil(Math.random()*TAG_COLOR.length) - 1]} >
  368. {chart.currentGroup[0].label}
  369. </Tag>
  370. </GroupSelector>
  371. </Breadcrumb.Item>
  372. {chart.currentGroup[1] && (<Breadcrumb.Item>
  373. <GroupSelector model={chart} modelName='chart'>
  374. <Tag color={TAG_COLOR[Math.ceil(Math.random()*TAG_COLOR.length) - 1]}>
  375. {chart.currentGroup[1].label}
  376. </Tag>
  377. </GroupSelector>
  378. </Breadcrumb.Item>)}
  379. </Breadcrumb>
  380. </Col>
  381. <Col className='search'>
  382. <Col style={{ padding: '0 5px' }}>
  383. <Search
  384. placeholder="请输入关键字"
  385. value={chart.filterLabel}
  386. onChange={e => {
  387. dispatch({ type: 'chart/setFilterLabel', label: e.target.value });
  388. }}
  389. />
  390. </Col>
  391. <Col >
  392. <Button onClick={() => {
  393. dispatch({ type: 'dataSource/fetchList' });
  394. this.setState({
  395. visibleChooseDataSourceBox: true
  396. });
  397. }}>
  398. <Icon type="area-chart" />创建图表
  399. </Button>
  400. <ChooseDataSourceBox visibleChooseDataSourceBox={visibleChooseDataSourceBox} hideBox={() => {
  401. this.setState({
  402. visibleChooseDataSourceBox: false
  403. });
  404. }}/>
  405. </Col>
  406. </Col>
  407. </Row>
  408. }>
  409. <div className='chart-body'>
  410. { this.generateCard() }
  411. </div>
  412. </Card>
  413. </Content>
  414. <DistributeBox key={this.state.selectedRecord ? this.state.selectedRecord.code : 'notkey'} visibleDistributeBox={visibleDistributeBox} selectedRecord={this.state.selectedRecord} hideBox={() => {
  415. this.setState({
  416. visibleDistributeBox: false
  417. });
  418. }} />
  419. <TransferBox
  420. visibleTransferBox={visibleTransferBox}
  421. onOk={(obj) => {
  422. console.log(obj);
  423. }}
  424. hideBox={() => {
  425. this.setState({
  426. visibleTransferBox: false
  427. })
  428. }} />
  429. <DeleteBox
  430. visibleDeleteBox={visibleDeleteBox}
  431. type='chart'
  432. hideBox={() => {
  433. this.setState({
  434. visibleDeleteBox: false
  435. })
  436. }}
  437. selectedRecord={selectedRecord}
  438. onOk={() => {
  439. dispatch({ type: 'chart/remoteDelete', code: this.state.selectedRecord.code })
  440. this.setState({ visibleDeleteBox: false })
  441. }} />
  442. </Layout>
  443. )
  444. }
  445. }
  446. function mapStateToProps(state) {
  447. return { chart: state.present.chart }
  448. }
  449. export default connect(mapStateToProps)(ChartList)