index.jsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. /**
  2. * 权限管理
  3. */
  4. import React from 'react'
  5. import { Tabs, Layout, Row, Col, Input, Menu, Table, Checkbox, message, Button, Icon } from 'antd'
  6. import { connect } from 'dva'
  7. import Loading from '../common/loading/index'
  8. import './index.less'
  9. const { Sider, Header, Content } = Layout
  10. const { Search } = Input
  11. const { TabPane } = Tabs
  12. class Authority extends React.Component {
  13. componentDidMount() {
  14. const { dispatch } = this.props;
  15. dispatch({ type: 'userGroup/fetchList' });
  16. dispatch({ type: 'user/fetchList' });
  17. dispatch({ type: 'dashboard/remoteMenuTree' });
  18. }
  19. checkIndeterminate = (treeList) => {
  20. const { authority } = this.props;
  21. const { dashboardList, dashboardFilterLabel } = authority;
  22. let arr = treeList && treeList.length > 0 ? [ ...treeList ] : null;
  23. for(let i = (arr ? (arr.length - 1) : 0); arr && i >= 0; i--) {
  24. let l = arr[i];
  25. if(l.type === 'menu') {
  26. l.children = this.checkIndeterminate(l.children);
  27. let checkedCount = l.children ? l.children.filter(c => c.type === 'dashboard').map(c => c.checked).filter(c => !!c).length : 0;
  28. l.indeterminate = l.children ? (checkedCount > 0 && checkedCount < l.children.filter(c => c.type === 'dashboard').length) : false;
  29. l.checked = l.children ? checkedCount !== 0 && checkedCount === l.children.filter(c => c.type === 'dashboard').length : false;
  30. if((!l.children || l.children.length === 0) && !!dashboardFilterLabel && l.name.toLowerCase().indexOf(dashboardFilterLabel.toLowerCase()) === -1) {
  31. arr.splice(i, 1);
  32. }
  33. }else {
  34. l.children = null;
  35. l.indeterminate = false;
  36. l.checked = dashboardList.findIndex(d => d === l.code) > -1;
  37. if(!!dashboardFilterLabel && l.name.toLowerCase().indexOf(dashboardFilterLabel.toLowerCase()) === -1) {
  38. arr.splice(i, 1);
  39. }
  40. }
  41. }
  42. return arr;
  43. }
  44. changeTab = (key) => {
  45. const { dispatch } = this.props;
  46. dispatch({ type: 'authority/setFields', fields: [
  47. { name: 'tabActiveKey', value: key },
  48. { name: 'menuSelectedKeys', value: [] },
  49. { name: 'dashboardList', value: [] },
  50. ] });
  51. }
  52. render() {
  53. const { dashboard, authority, userGroup, user, dispatch } = this.props;
  54. const { panelLoading, menuSelectedKeys, tabActiveKey, groupFilterLabel, userFilterLabel, dashboardFilterLabel, groupLimit, userLimit } = authority;
  55. const dashboardTreeList = this.checkIndeterminate(JSON.parse(JSON.stringify(dashboard.menuTree)));
  56. return <Layout className='layout-authority'>
  57. <Loading visible={panelLoading} />
  58. <Sider>
  59. <Tabs
  60. className='tabs-authority'
  61. type='card'
  62. tabPosition='top'
  63. defaultActiveKey="userGroup"
  64. activeKey={tabActiveKey}
  65. onChange={this.changeTab}
  66. >
  67. <TabPane tab="用户组" key="userGroup" >
  68. <MenuList
  69. refName='group'
  70. selectedKeys={menuSelectedKeys}
  71. list={userGroup.list}
  72. filterLabel={groupFilterLabel}
  73. filterLabelChange={(val) => {
  74. dispatch({ type: 'authority/setFields', fields: [
  75. { name: 'groupLimit', value: 30 },
  76. { name: 'groupFilterLabel', value: val },
  77. ] })
  78. }}
  79. displayField='name'
  80. valueField='code'
  81. onItemClick={(item) => {
  82. dispatch({ type: 'authority/batchActions', actions: [
  83. { type: 'setFields', fields: [
  84. { name: 'menuSelectedKeys', value: [item.code] },
  85. ] },
  86. { type: 'fetchDashboardTree', dtype: '0', code: item.code }
  87. ] });
  88. }}
  89. limit={groupLimit}
  90. onScrollToBottom={() => {
  91. window.clearTimeout(this.groupLimitKey);
  92. this.groupLimitKey = window.setTimeout(() => {
  93. dispatch({ type: 'authority/setFields', fields: [
  94. { name: 'groupLimit', value: (groupLimit + 10 > userGroup.list.length ? userGroup.list.length : groupLimit + 10) },
  95. ] })
  96. }, 500);
  97. }}
  98. />
  99. </TabPane>
  100. <TabPane tab="用户" key="user">
  101. <MenuList
  102. refName='user'
  103. selectedKeys={menuSelectedKeys}
  104. list={user.list}
  105. filterLabel={userFilterLabel}
  106. filterLabelChange={(val) => {
  107. dispatch({ type: 'authority/setFields', fields: [
  108. { name: 'userLimit', value: 30 },
  109. { name: 'userFilterLabel', value: val },
  110. ] })
  111. }}
  112. displayField='fullName'
  113. valueField='code'
  114. onItemClick={(item) => {
  115. dispatch({ type: 'authority/batchActions', actions: [
  116. { type: 'setFields', fields: [ { name: 'menuSelectedKeys', value: [item.code] }, ]},
  117. { type: 'fetchDashboardTree', dtype: '1', code: item.code }
  118. ] });
  119. }}
  120. limit={userLimit}
  121. onScrollToBottom={() => {
  122. window.clearTimeout(this.userLimitKey);
  123. this.userLimitKey = window.setTimeout(() => {
  124. dispatch({ type: 'authority/setFields', fields: [
  125. { name: 'userLimit', value: (userLimit + 10 > user.list.length ? user.list.length : userLimit + 10) },
  126. ] })
  127. }, 500);
  128. }}
  129. />
  130. </TabPane>
  131. </Tabs>
  132. </Sider>
  133. <Content>
  134. <Header>
  135. <Row className='search-bar' type='flex' justify='end'>
  136. <Col className='search'>
  137. <Button className='btn-refresh' onClick={() => {
  138. dispatch({ type: 'authority/setField', name: 'panelLoading', value: true });
  139. dispatch({ type: 'userGroup/fetchList', mandatory: true });
  140. dispatch({ type: 'user/fetchList', mandatory: true });
  141. dispatch({ type: 'dashboard/remoteMenuTree', mandatory: true }).then(() => {
  142. dispatch({ type: 'authority/setField', name: 'panelLoading', value: false });
  143. });
  144. }}>
  145. <Icon type='sync'/>
  146. </Button>
  147. <Search
  148. placehodler='请输入关键字'
  149. value={dashboardFilterLabel}
  150. onChange={e => {
  151. let value = e.target.value;
  152. dispatch({ type: 'authority/setFields', fields: [
  153. { name: 'dashboardFilterLabel', value },
  154. ] })
  155. }}
  156. />
  157. </Col>
  158. </Row>
  159. </Header>
  160. <DashBoardTree filterLabel={dashboardFilterLabel} dataSource={dashboardTreeList} onRowCheckChange={(record, checked) => {
  161. if(!!menuSelectedKeys[0]) {
  162. if(record.type === 'menu') {
  163. let dashboardCodes = record.children.filter(c => c.type === 'dashboard').map(c => c.code)
  164. dispatch({ type: 'authority/' + (checked ? 'addAll': 'removeAll'), dtype: tabActiveKey === 'user' ? 1 : 0, code: menuSelectedKeys[0], dashboardCodes });
  165. }else {
  166. dispatch({ type: 'authority/' + (checked ? 'add': 'remove'), dtype: tabActiveKey === 'user' ? 1 : 0, code: menuSelectedKeys[0], dashboardCode: record.code });
  167. }
  168. }else {
  169. message.error('请先选择左侧用户组/用户');
  170. }
  171. }}/>
  172. </Content>
  173. </Layout>
  174. }
  175. }
  176. class MenuList extends React.Component {
  177. componentDidMount() {
  178. this.addEvents();
  179. }
  180. addEvents = () => {
  181. const { refName } = this.props;
  182. let tabs = this['menuList-' + refName];
  183. let menu = tabs.getElementsByClassName('ant-menu')[0];
  184. menu.removeEventListener('scroll', this.onMenuScroll);
  185. menu.addEventListener('scroll', this.onMenuScroll);
  186. }
  187. onMenuScroll = (e) => {
  188. const { onScrollToBottom } = this.props;
  189. let target = e.target;
  190. if(target.scrollHeight - target.offsetHeight === target.scrollTop) { // 滚动到底了
  191. typeof onScrollToBottom === 'function' && onScrollToBottom()
  192. }
  193. }
  194. render() {
  195. const { refName, selectedKeys, list, filterLabel: pFilterLabel, filterLabelChange, displayField, valueField, onItemClick, limit } = this.props;
  196. const reg = new RegExp('([+ \\- & | ! ( ) { } \\[ \\] ^ \" ~ * ? : ( ) \/])', 'g'); // 需要转义的字符
  197. let filterLabel = (pFilterLabel || '').replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1'); // 添加转义符号
  198. return <div ref={node => this['menuList-' + refName] = node} className='menu-list'>
  199. <div className='search-area'>
  200. <Search
  201. value={pFilterLabel}
  202. placeholder="请输入关键字"
  203. onChange={e => {
  204. let val = e.target.value + '';
  205. typeof filterLabelChange === 'function' && filterLabelChange(val);
  206. }}
  207. />
  208. </div>
  209. <Menu
  210. selectedKeys={selectedKeys}
  211. >
  212. {list.filter(l => {
  213. let reg = new RegExp('(' + filterLabel + '){1}', 'ig');
  214. return ((l[displayField] || '').search(reg) !== -1 || l[valueField] === selectedKeys[0]);
  215. }).slice(0, limit).map(l => (
  216. <Menu.Item key={l[valueField]+''} onClick={() => {
  217. typeof onItemClick === 'function' && onItemClick(l);
  218. }} >
  219. <div className='item-title'>
  220. <span className='label' title={l[displayField]}>
  221. { filterLabel ?
  222. ((l[displayField] || '').split(new RegExp(`(${filterLabel})`, 'i')).map((fragment, i) => {
  223. return (
  224. fragment.toLowerCase().replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1') === filterLabel.toLowerCase() ?
  225. <span key={i} style={{fontWeight: 'bold', color: 'red'}} className="highlight">{fragment}</span> :
  226. fragment
  227. )
  228. }
  229. )) : <span>{l[displayField]}</span>
  230. }
  231. </span>
  232. </div>
  233. </Menu.Item>
  234. ))}
  235. </Menu>
  236. </div>
  237. }
  238. }
  239. class DashBoardTree extends React.Component {
  240. constructor(props) {
  241. super(props);
  242. this.state = {
  243. tableScrollHeight: 0,
  244. allExpanded: false,
  245. expandedRowKeys: [],
  246. }
  247. }
  248. componentDidMount() {
  249. this.tableSize();
  250. window.addEventListener('resize', this.tableSize);
  251. }
  252. componentWillUnmount() {
  253. window.removeEventListener('resize', this.tableSize);
  254. }
  255. tableSize = () => {
  256. const content = document.getElementsByClassName('content-setting')[0];
  257. const tableWrapper = content.getElementsByClassName('ant-table-wrapper')[0];
  258. const tableContent = tableWrapper.getElementsByClassName('ant-spin-nested-loading')[0];
  259. const tableHeader = tableContent.querySelector('thead');
  260. const padding = tableContent.getBoundingClientRect().top - tableWrapper.getBoundingClientRect().top;
  261. // 容器高度 - padding * 2 - 表头高度 - 边框线
  262. let tableScrollHeight = tableWrapper.offsetHeight - padding * 2 - tableHeader.offsetHeight - 2;
  263. this.setState({
  264. tableScrollHeight,
  265. });
  266. }
  267. onExpandedRowsChange = (expandedRowKeys) => {
  268. this.setState({
  269. expandedRowKeys
  270. });
  271. }
  272. onExpandAll = () => {
  273. const { dataSource } = this.props;
  274. let expandedRowKeys = this.getAllKeys(dataSource);
  275. this.setState({
  276. allExpanded: true,
  277. expandedRowKeys
  278. });
  279. }
  280. collapseAll = () => {
  281. this.setState({
  282. allExpanded: false,
  283. expandedRowKeys: []
  284. });
  285. }
  286. getAllKeys = (list) => {
  287. let keyArr = [];
  288. for(let i = 0;!!list && i < list.length; i++) {
  289. if(!!list[i].children) {
  290. keyArr.push(list[i].key);
  291. keyArr = keyArr.concat(this.getAllKeys(list[i].children, keyArr));
  292. }
  293. }
  294. return keyArr
  295. }
  296. render() {
  297. const { dataSource, onRowCheckChange, filterLabel } = this.props;
  298. const { tableScrollHeight, expandedRowKeys, allExpanded } = this.state;
  299. const reg = new RegExp('([+ \\- & | ! ( ) { } \\[ \\] ^ \" ~ * ? : ( ) \/])', 'g'); // 需要转义的字符
  300. const columns = [{
  301. title: <span><Icon type='menu' title='展开/折叠全部' style={{ marginRight: '4px' }} onClick={allExpanded ? this.collapseAll : this.onExpandAll}/>报表目录</span>,
  302. dataIndex: 'name',
  303. key: 'name',
  304. width: '80%',
  305. render: (text, record) => {
  306. return <span style={{ fontWeight: record.type === 'dashboard' ? 'bold' : 'normal' }}>
  307. {
  308. filterLabel ?
  309. ((text || '').split(new RegExp(`(${filterLabel})`, 'i')).map((fragment, i) => {
  310. return (
  311. fragment.toLowerCase().replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1') === filterLabel.toLowerCase() ?
  312. <span key={i} style={{fontWeight: 'bold', color: 'red'}} className="highlight">{fragment}</span> :
  313. fragment
  314. )
  315. }
  316. )) : text
  317. }
  318. </span>
  319. }
  320. }, {
  321. title: '查看',
  322. dataIndex: 'view',
  323. key: 'view',
  324. width: '20%',
  325. render: (text, record, index) => {
  326. return <Checkbox disabled={record.type === 'menu' && (!record.children || record.children.filter(c => c.type === 'dashboard').length === 0)} indeterminate={record.indeterminate} checked={record.checked} onChange={(e) => {
  327. let checked = e.target.checked;
  328. typeof onRowCheckChange === 'function' && onRowCheckChange(record, checked);
  329. }} />
  330. }
  331. }];
  332. return <Table columns={columns} dataSource={dataSource} scroll={{x: false, y: tableScrollHeight}}
  333. size='small'
  334. pagination={false}
  335. expandedRowKeys={expandedRowKeys}
  336. onExpandedRowsChange={this.onExpandedRowsChange}
  337. />
  338. }
  339. }
  340. export default connect(({ present: { authority, userGroup, user, dashboard } }) => ({ authority, userGroup, user, dashboard }))(Authority)