list.jsx 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. import React from 'react'
  2. import { Layout, Row, Col, Input, Button, Table, Icon, Menu, Dropdown, Card, Breadcrumb, Popover, Tree, Tag } from 'antd'
  3. import { connect } from 'dva'
  4. import { dateFormat } from '../../utils/baseUtils'
  5. import GroupSelector from './groupSelector'
  6. import TransferBox from '../common/selectUserBox/selectUserBox'
  7. import DeleteBox from '../common/deleteBox/deleteBox'
  8. import DataPreview from '../common/dataPreview/dataPreview'
  9. import './list.less'
  10. const { Content } = Layout
  11. const { Search } = Input
  12. const { TreeNode } = Tree
  13. class DataSource extends React.Component {
  14. constructor(props) {
  15. super(props);
  16. this.state = {
  17. selectedRecord: null, // 当前选中的dataSource
  18. visibleGroupMenu: false, // 显示分组菜单
  19. visibleSetGroupMenu: false, //
  20. visibleTransferBox: false,
  21. visibleDeleteBox: false,
  22. visibleDataPreviewBox: false, // 显示数据预览
  23. groupEditing: false, // 是否处于编辑状态
  24. }
  25. };
  26. hideTransferBox = () => {
  27. this.setState({ visibleTransferBox: false})
  28. }
  29. componentDidMount() {
  30. const { dispatch } = this.props;
  31. this.setScrollTableHeight();
  32. dispatch({ type: 'dataSource/fetchList' });
  33. dispatch({ type: 'dataSource/remoteGroupList' });
  34. }
  35. /**
  36. * 根据视图设置表格高度以呈现滚动条
  37. */
  38. setScrollTableHeight() {
  39. const mainContent = document.getElementsByClassName('main-content')[0];
  40. const toolbar = mainContent.getElementsByClassName('datasource-tools')[0];
  41. const tableHeader = mainContent.getElementsByClassName('ant-table-header')[0];
  42. const tableBody = mainContent.getElementsByClassName('ant-table-body')[0];
  43. tableBody.style.maxHeight=`${mainContent.offsetHeight - toolbar.offsetHeight - tableHeader.offsetHeight - 58}px`;
  44. }
  45. onGroup() {
  46. const { dataSource } = this.props;
  47. const groupList = dataSource.groupList;
  48. const list = dataSource.list;
  49. const currentGroup = dataSource.currentGroup;
  50. let groupFilter = groupList.concat({ code: '-1', label: '未分组' }).filter(g => (
  51. currentGroup[0].code === 'all' ||
  52. (
  53. currentGroup[0].code === '-1' ? g.code === '-1' : (
  54. currentGroup[1] ? (g.code === currentGroup[1].code) :
  55. (
  56. g.code === currentGroup[0].code ||
  57. g.pcode === currentGroup[0].code
  58. )
  59. )
  60. )
  61. )).map(g => g.code);
  62. return list.filter(l => groupFilter.indexOf(l.groupCode+'') !== -1);
  63. }
  64. onSearch(list, text) {
  65. const reg = new RegExp('([+ \\- & | ! ( ) { } \\[ \\] ^ \" ~ * ? : ( ) \/])', 'g'); // 需要转义的字符
  66. let filterLabel = (text || '').replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1'); // 添加转义符号
  67. return list.map(l => {
  68. let o = Object.assign({}, l);
  69. let reg = new RegExp('('+ filterLabel +'){1}', 'ig');
  70. if(o.name && o.name.search(reg) !== -1) {
  71. return o;
  72. }else if(o.description && o.description.search(reg) !== -1) {
  73. return o;
  74. }else {
  75. return null
  76. }
  77. }).filter(a => a!==null);
  78. }
  79. onSort(list) {
  80. return list.sort((a, b) => {
  81. return new Date(b.createTime) - new Date(a.createTime);
  82. });
  83. }
  84. handleVisibleChange = (flag) => {
  85. this.setState({ visibleGroupMenu: flag });
  86. }
  87. createGroupMenu = (selectedRecord) => {
  88. const { dataSource, dispatch } = this.props;
  89. const groupList = dataSource.groupList;
  90. const pGroups = groupList.filter(d => d.pcode === '-1').sort((a, b) => a.index - b.index);
  91. const cGroups = groupList.filter(d => d.pcode !== '-1');
  92. let allGroups = selectedRecord ? [
  93. { code: '-1', label: '未分组' }
  94. ].concat(pGroups) : [
  95. { code: 'all', label: '全部分组' },
  96. { code: '-1', label: '未分组' }
  97. ].concat(pGroups);
  98. return allGroups.map(p => {
  99. let c = cGroups.filter(c => c.pcode === p.code).sort((a, b) => a.index - b.index);
  100. return c.length > 0 ? (
  101. <Menu.SubMenu key={p.code} title={<span style={{ fontWeight: selectedRecord ?
  102. (p.code + '' === selectedRecord.groupCode + '' ? 'bold' : (
  103. c.find(ch => ch.code + '' === selectedRecord.groupCode + '') && c.find(ch => ch.code + '' === selectedRecord.groupCode + '').pcode === p.code ? 'bold' : 'normal'
  104. ))
  105. : dataSource.currentGroup[0].code === p.code ? 'bold' : 'normal'
  106. }}>{p.label}</span>} onTitleClick={(item) => {
  107. // dispatch({ type: 'dataSource/setCurrentGroup', group1: p }); // 尝试禁用分组后跳转逻辑
  108. if (selectedRecord) {
  109. dispatch({ type: 'dataSource/remoteSetDataSourceGroup', dataSource: selectedRecord, group: p });
  110. }
  111. this.hideGroupMenu();
  112. }}>
  113. {c.map(c => {
  114. return (<Menu.Item key={c.code} onClick={(item) => {
  115. // dispatch({ type: 'dataSource/setCurrentGroup', group1: p, group2: c }); // 尝试禁用分组后跳转逻辑
  116. if(selectedRecord) {
  117. dispatch({ type: 'dataSource/remoteSetDataSourceGroup', dataSource: selectedRecord, group: c });
  118. }
  119. }}><span style={{ fontWeight: selectedRecord ? (
  120. selectedRecord.groupCode+'' === c.code+'' ? 'bold' : 'normal'
  121. ) : (dataSource.currentGroup[1] && (dataSource.currentGroup[1].code === c.code) ? 'bold' : 'normal') }}>{c.label}</span></Menu.Item>)
  122. })}
  123. </Menu.SubMenu>
  124. ) : (
  125. <Menu.Item key={p.code} onClick={() => {
  126. // dispatch({ type: 'dataSource/setCurrentGroup', group1: p }); // 尝试禁用分组后跳转逻辑
  127. if(selectedRecord) {
  128. dispatch({ type: 'dataSource/remoteSetDataSourceGroup', dataSource: selectedRecord, group: p });
  129. }
  130. this.hideGroupMenu();
  131. }}><span className={selectedRecord ? (
  132. selectedRecord.groupCode+'' === p.code+'' ? 'selected' : ''
  133. ) : dataSource.currentGroup[0] && (dataSource.currentGroup[0].code === p.code) ? 'selected' : ''} style={{ fontWeight: selectedRecord ? (
  134. selectedRecord.groupCode+'' === p.code+'' ? 'bold' : 'normal'
  135. ) : dataSource.currentGroup[0] && (dataSource.currentGroup[0].code === p.code) ? 'bold' : 'normal' }}>{p.label}</span></Menu.Item>
  136. );
  137. });
  138. }
  139. createSubGroupMenu = () => {
  140. const { dataSource, dispatch } = this.props;
  141. const groupList = dataSource.groupList;
  142. const parentGroup = dataSource.currentGroup[0];
  143. const children = groupList.filter(d => d.pcode === parentGroup.code);
  144. const subGroup = dataSource.currentGroup[1];
  145. return children.map(c => {
  146. return (
  147. <Menu.Item key={c.code} onClick={() => {
  148. dispatch({ type: 'dataSource/setCurrentGroup', group1: parentGroup, group2: c });
  149. }}><span className={subGroup && (subGroup.code === c.code) ? 'selected' : ''} style={{ fontWeight: subGroup && (subGroup.code === c.code) ? 'bold' : 'normal' }}>{c.label}</span></Menu.Item>
  150. );
  151. })
  152. }
  153. createGroupTree(modify) {
  154. const { dispatch, dataSource } = this.props;
  155. const { groupEditing } = this.state;
  156. const groupList = dataSource.groupList;
  157. let parent = groupList.filter(d => d.pcode === '-1').sort((a, b) => a.index - b.index);
  158. let children = groupList.filter(d => d.pcode !== '-1');
  159. let groupTree = parent.map(p => {
  160. return (
  161. <TreeNode disabled={groupEditing} title={
  162. modify ? (<div><Icon style={{ cursor: 'move' }} type='drag'/>
  163. <Input value={p.label} size='small' focus={'true'} onFocus={() => {
  164. this.setState({
  165. groupEditing: true
  166. });
  167. }} onChange={(e) => {
  168. dispatch({ type: 'dataSource/modifyGroup', group: {...p, label:e.target.value} });
  169. }} onBlur={(e) => {
  170. this.setState({
  171. groupEditing: false
  172. });
  173. dispatch({ type: 'dataSource/remoteModifyGroup', group: {...p, label:e.target.value} });
  174. }} onPressEnter={(e) => {
  175. dispatch({ type: 'dataSource/remoteModifyGroup', group: {...p, label:e.target.value} });
  176. }} /><Icon type='plus-circle-o' onClick={() => {
  177. dispatch({ type: 'dataSource/remoteAddGroup', pgroup: p });
  178. }}/><Icon type='minus-circle' onClick={() => {
  179. dispatch({ type: 'dataSource/remoteDeleteGroup', group: p });
  180. }}/></div>) : p.label} key={p.code}>
  181. {
  182. children.filter(c => c.pcode === p.code).sort((a, b) => a.index - b.index).map(c => {
  183. return (
  184. <TreeNode disabled={groupEditing} title={
  185. modify ? (<div><Icon style={{ cursor: 'move' }} type='drag'/>
  186. <Input value={c.label} size='small' onFocus={() => {
  187. this.setState({
  188. groupEditing: true
  189. });
  190. }} onChange={(e) => {
  191. dispatch({ type: 'dataSource/modifyGroup', group: {...c, label:e.target.value} });
  192. }} onBlur={(e) => {
  193. this.setState({
  194. groupEditing: false
  195. });
  196. dispatch({ type: 'dataSource/remoteModifyGroup', group: {...c, label:e.target.value} });
  197. }} onPressEnter={(e) => {
  198. dispatch({ type: 'dataSource/remoteModifyGroup', group: {...c, label:e.target.value} });
  199. }} onCompositionEnd={(e) => {
  200. }}/><Icon type='minus-circle' onClick={() => {
  201. dispatch({ type: 'dataSource/remoteDeleteGroup', group: c });
  202. }}/></div>) : p.label
  203. } key={c.code} />
  204. )
  205. })
  206. }
  207. </TreeNode>
  208. )
  209. });
  210. return groupTree;
  211. }
  212. hideGroupMenu = () => {
  213. this.setState({
  214. visibleGroupMenu: false
  215. });
  216. }
  217. onDrop = (info) => {
  218. const { dispatch } = this.props;
  219. const dropCode = info.node.props.eventKey;
  220. const dragCode = info.dragNode.props.eventKey;
  221. const dropPos = info.node.props.pos.split('-');
  222. const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]); // -1/0/1 -> 兄/子/弟
  223. dispatch({ type: 'dataSource/remoteMoveGroup', dragCode, dropCode, dropPosition });
  224. }
  225. render() {
  226. const { main, dataSource, dispatch } = this.props;
  227. const { selectedRecord, visibleTransferBox, visibleDeleteBox, visibleDataPreviewBox } = this.state;
  228. const { currentUser } = main;
  229. const reg = new RegExp('([+ \\- & | ! ( ) { } \\[ \\] ^ \" ~ * ? : ( ) \/])', 'g'); // 需要转义的字符
  230. let filterLabel = dataSource.filterLabel.replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1'); // 添加转义符号
  231. const TAG_COLOR = ['blue'];
  232. // const TAG_COLOR1 = ['magenta', 'red', 'volcano', 'orange', 'gold', 'lime', 'green', 'cyan', 'blue', 'geekblue', 'purple'];
  233. const moreOperatingMenu = (
  234. <Menu className='operationmenu' visible={true}>
  235. <Menu.Item
  236. onClick={() => {
  237. dispatch({ type: 'chartDesigner/remoteQucikAdd', dataSource: selectedRecord });
  238. }}
  239. >
  240. <Icon type="file-add" />创建图表
  241. </Menu.Item>
  242. {/* {selectedRecord && currentUser.code === selectedRecord.creatorCode && <Menu.Item onClick={(e) => {
  243. dispatch({ type: 'dataSource/resetNewModel' });
  244. let selectedModel = dataSource.list.find((i) => { return i.code === selectedRecord.code })
  245. dispatch({type: 'main/redirect', path: {pathname: '/datasource/'+ selectedModel.type +'/' + selectedModel.code + '/base'}});
  246. }}>
  247. <Icon type="info-circle-o" />属性设置
  248. </Menu.Item>} */}
  249. <Menu.Item onClick={() => {
  250. this.setState({
  251. visibleDataPreviewBox: true
  252. });
  253. }}><Icon type="search" />预览数据</Menu.Item>
  254. <Menu.Divider />
  255. { selectedRecord && currentUser.code === selectedRecord.creatorCode && <Menu.SubMenu className='setgroupmenu' title={<div><Icon style={{ marginRight: '6px' }} type='profile' />移动到</div>}>
  256. {this.createGroupMenu(selectedRecord)}
  257. </Menu.SubMenu>}
  258. <Menu.Divider />
  259. { selectedRecord && currentUser.code === selectedRecord.creatorCode && <Menu.Item
  260. onClick={()=>{
  261. this.setState({ visibleTransferBox: true})
  262. }}
  263. >
  264. <Icon type="swap" />移交
  265. </Menu.Item>}
  266. { selectedRecord && currentUser.code === selectedRecord.creatorCode && <Menu.Item
  267. onClick={(e) => {
  268. this.setState({ visibleDeleteBox: true})
  269. }}
  270. >
  271. <Icon type="delete" />删除
  272. </Menu.Item>}
  273. </Menu>
  274. );
  275. const dataSourceColumns = [{
  276. title: '名称',
  277. dataIndex: 'name',
  278. key: 'name',
  279. width: 100,
  280. render: (text, record) => {
  281. return <div className='datasource-name'>
  282. <div className={`datasource-type`}>
  283. {record.type === 'database' ? <Icon type="database" theme="outlined" /> : <Icon type="file-excel" theme="outlined" />}
  284. </div>
  285. <div>
  286. <span style={{ color: '#1890ff', cursor: 'pointer' }} onClick={() => {
  287. dispatch({ type: 'dataSource/resetNewModel' });
  288. dispatch({type: 'main/redirect', path: {pathname: '/datasource/'+ record.type +'/' + record.code + '/base'}});
  289. }}>
  290. { filterLabel ?
  291. ((text || '').split(new RegExp(`(${filterLabel})`, 'i')).map((fragment, i) => {
  292. return (
  293. fragment.toLowerCase().replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1') === filterLabel.toLowerCase() ?
  294. <span key={i} style={{fontWeight: 'bold', color: 'red'}} className="highlight">{fragment}</span> :
  295. fragment
  296. )
  297. }
  298. )) : text
  299. }
  300. </span>
  301. </div>
  302. </div>
  303. }
  304. }, {
  305. title: '说明',
  306. dataIndex: 'description',
  307. key: 'description',
  308. width: 200,
  309. render: (text, record) => {
  310. return (
  311. <span>
  312. { filterLabel ?
  313. ((text || '').split(new RegExp(`(${filterLabel})`, 'i')).map((fragment, i) => {
  314. return (
  315. fragment.toLowerCase().replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1') === filterLabel.toLowerCase() ?
  316. <span key={i} style={{fontWeight: 'bold', color: 'red'}} className="highlight">{fragment}</span> :
  317. fragment
  318. )
  319. }
  320. )) : text
  321. }
  322. </span>
  323. )
  324. }
  325. }, {
  326. title: '创建人',
  327. dataIndex: 'creatorName',
  328. key: 'creatorName',
  329. width: 100
  330. }, {
  331. title: '创建时间',
  332. dataIndex: 'createTime',
  333. key: 'createTime',
  334. render: (text, record) => dateFormat(text, 'yyyy-MM-dd hh:mm:ss'),
  335. width: 100
  336. }, {
  337. title: '操作',
  338. key: 'action',
  339. render: (text, record, index) => (
  340. <Dropdown code={record.code} overlay={moreOperatingMenu} trigger={['click']} >
  341. <Icon type="setting" />
  342. </Dropdown>
  343. ),
  344. width: 50
  345. }];
  346. return (
  347. <Layout className='datasource-view'>
  348. <Content>
  349. <Card className='datasource-body' title={
  350. <Row className='datasource-tools' type='flex' justify='space-between'>
  351. <Col style={{ display: 'flex' }}>
  352. <Popover overlayClassName='popover-group' title={
  353. <Row className='grouptree-title' type='flex' justify='space-between'>
  354. <Col>
  355. 分组管理
  356. </Col>
  357. <Col>
  358. <div className='create-group' onClick={() => {
  359. dispatch({ type: 'dataSource/remoteAddGroup' });
  360. }}>添加分组<Icon type="plus-circle-o" /></div>
  361. </Col>
  362. </Row>
  363. } trigger="click" placement="bottomLeft" content={(
  364. <Tree
  365. className='tree-group'
  366. showLine
  367. defaultExpandAll
  368. draggable
  369. onDragStart={this.onDragStart}
  370. onDrop={this.onDrop}
  371. >
  372. {
  373. this.createGroupTree(true)
  374. }
  375. </Tree>
  376. )}>
  377. <Icon type="bars" />
  378. </Popover>
  379. <Breadcrumb className='group' separator=">">
  380. <Breadcrumb.Item>
  381. <GroupSelector model={dataSource} modelName='dataSource'>
  382. <Tag color={TAG_COLOR[Math.ceil(Math.random()*TAG_COLOR.length) - 1]} >
  383. {dataSource.currentGroup[0].label}
  384. </Tag>
  385. </GroupSelector>
  386. </Breadcrumb.Item>
  387. {dataSource.currentGroup[1] && (<Breadcrumb.Item>
  388. <GroupSelector model={dataSource} modelName='dataSource'>
  389. <Tag color={TAG_COLOR[Math.ceil(Math.random()*TAG_COLOR.length) - 1]}>
  390. {dataSource.currentGroup[1].label}
  391. </Tag>
  392. </GroupSelector>
  393. </Breadcrumb.Item>)}
  394. </Breadcrumb>
  395. </Col>
  396. <Col className='search'>
  397. <Col>
  398. <Search
  399. value={dataSource.filterLabel}
  400. placeholder="请输入关键字"
  401. onChange={e => {
  402. dispatch({ type: 'dataSource/setFilterLabel', label: e.target.value });
  403. }}
  404. />
  405. </Col>
  406. <Col>
  407. <Dropdown overlay={(
  408. <Menu onClick={(item, key, keyPath) => {
  409. const type = item.key;
  410. dispatch({ type: 'dataSource/resetNewModel' });
  411. dispatch({ type: 'dataConnect/resetSelected' });
  412. dispatch({ type: 'dataSource/setNewModelField', name: 'type', value: type });
  413. dispatch({type: 'main/redirect', path: {pathname: '/datasource/'+ type +'/create/base'}});
  414. }}>
  415. { currentUser.role === 'admin' && <Menu.Item key='database'>来自数据库</Menu.Item>}
  416. <Menu.Item disabled key='file'>来自文件</Menu.Item>
  417. </Menu>
  418. )} trigger={['click']}>
  419. <Button>
  420. <Icon type="plus" />添加数据源
  421. </Button>
  422. </Dropdown>
  423. </Col>
  424. </Col>
  425. </Row>
  426. }>
  427. <Table
  428. className='datasource-table'
  429. columns={dataSourceColumns}
  430. dataSource={
  431. this.onSort(
  432. this.onSearch(this.onGroup(), dataSource.filterLabel)
  433. )
  434. }
  435. size='small'
  436. scroll={{x: false, y: true}}
  437. pagination={false}
  438. onRow={(record) => {
  439. return {
  440. onClick: () => {this.setState({ selectedRecord: record})}
  441. }
  442. }}
  443. />
  444. <TransferBox
  445. visibleBox={visibleTransferBox}
  446. title='选择移交对象'
  447. okHandler={(user) => {
  448. dispatch({ type: 'dataSource/transfer', dataSourceCode: this.state.selectedRecord.code, userCode: user.code });
  449. }}
  450. hideBox={() => {
  451. this.setState({
  452. visibleTransferBox: false
  453. })
  454. }}
  455. onlyAdmin={true}
  456. />
  457. {visibleDeleteBox && <DeleteBox
  458. visibleBox={visibleDeleteBox}
  459. text={<div><span>确定要删除数据源【{selectedRecord.name}】吗?</span><br/><span style={{ color: 'red' }}>(此操作将会导致使用该数据源的图表失效!)</span></div>}
  460. hideBox={() => {
  461. this.setState({
  462. visibleDeleteBox: false
  463. })
  464. }}
  465. okHandler={() =>{
  466. dispatch({ type: 'dataSource/remoteDelete', code: selectedRecord.code }).then(() => {
  467. dispatch({ type: 'chart/fetchList', mandatory: true });
  468. })
  469. }}
  470. />}
  471. {visibleDataPreviewBox && <DataPreview
  472. visibleBox={visibleDataPreviewBox}
  473. hideBox={() => {
  474. this.setState({
  475. visibleDataPreviewBox: false
  476. });
  477. }}
  478. fetchFunction={(page, pageSize) => {
  479. dispatch({ type: 'dataSource/remoteDataList', code: selectedRecord.code, page, pageSize });
  480. }}
  481. />}
  482. </Card>
  483. </Content>
  484. </Layout>
  485. )
  486. }
  487. }
  488. function mapStateToProps({present: {main, dataSource, dataConnect}}) {
  489. return { main, dataSource, dataConnect }
  490. }
  491. export default connect(mapStateToProps)(DataSource)