Browse Source

筛选组件逻辑完善/添加用户组限制优化/权限设置界面结构设计

zhuth 6 years ago
parent
commit
8962b4d823

+ 19 - 2
src/components/admin/userGroupDetailBox.jsx

@@ -7,7 +7,7 @@ class DetailBox extends React.Component {
     constructor(props) {
         super(props);
         this.state = {
-            okButtonDisabled: false
+            okButtonDisabled: true
         }
     }
 
@@ -78,7 +78,11 @@ class DetailBox extends React.Component {
                                     placeholder="请输入用户组名称"
                                     // value={newOne.name}
                                     onChange={(e) => {
-                                        dispatch({ type: 'userGroup/setNewModelField', name: 'name', value: e.target.value });
+                                        let value = e.target.value;
+                                        dispatch({ type: 'userGroup/setNewModelField', name: 'name', value });
+                                        me.setState({
+                                            okButtonDisabled: !!value.trim()
+                                        });
                                     }}
                                 >
                                 </Input>
@@ -89,6 +93,19 @@ class DetailBox extends React.Component {
                         {
                             getFieldDecorator('description', {
                                 initialValue: newOne.description,
+                                validateFirst: true,
+                                rules: [
+                                    { validator(rule, value, callback, source, options) {
+                                        let msg;
+                                        if(value.length > 500) {
+                                            msg = '用户组描述不能大于500个字符'
+                                        }
+                                        me.setState({
+                                            okButtonDisabled: !!msg
+                                        });
+                                        callback(msg);
+                                    } }
+                                ],
                             })(
                                 <Input.TextArea
                                     autosize={{ minRows: 2 }}

+ 106 - 0
src/components/authority/index.jsx

@@ -0,0 +1,106 @@
+import React from 'react'
+import { Layout, Input, Select, Spin } from 'antd'
+import { connect } from 'dva'
+import * as service from '../../services/index'
+import URLS from '../../constants/url'
+import './index.less'
+const { Header, Content } = Layout
+const InputGroup = Input.Group
+const Option = Select.Option
+
+class Authority extends React.Component {
+    constructor(props) {
+        super(props);
+        this.state = {
+            dataList: [],
+            fetching: false,
+        };
+    }
+
+    fetchData = (keyword) => {
+        this.setState({ dataList: [], fetching: true }, () => {
+            let condition = keyword || '';
+            service.fetch({
+                url: URLS.USER_QUERY,
+                method: 'GET',
+                body: {
+                    condition
+                },
+            }).then(r => {
+                if(!r.err && r.data.code > 0) {
+                    return r;
+                }else {
+                    let obj = {};
+                    throw obj;
+                }
+            }).then(r => {
+                const resData = r.data.data || [];
+                this.setState({
+                    dataList: resData.map(d => ({
+                        code: d.id + '',
+                        name: d.name,
+                        account: d.userName + '',
+                        password: d.passWord,
+                        role: d.role,
+                        department: d.department,
+                        post: d.post,
+                    })),
+                    fetching: false
+                });
+            }).catch(ex => {
+                this.setState({
+                    dataList: [],
+                    fetching: false
+                });
+                console.error('fetch error', ex);
+            });
+        });
+    }
+
+    generateOptions = () => {
+        const { dataList } = this.state;
+        let arr = dataList.filter(u => u.role !== 'super').map((s, i) => <Option key={s.code} value={s.code}>{s.name}</Option>)
+        console.log(arr);
+        return arr
+    }
+
+    render() {
+        const { authority, dispatch } = this.props;
+        const { fetching } = this.state;
+        return <Layout className='layout-authority'>
+            <Header
+                theme='light'
+            >
+                <InputGroup compact>
+                    <Select
+                        defaultValue="group"
+                    >
+                        <Option value="group">用户组</Option>
+                        <Option value="user">用户</Option>
+                    </Select>
+                    <Select
+                        notFoundContent={fetching ? <Spin size="small" /> : '无'}
+                        showSearch
+                        filterOption={false} 
+                        onSearch={(value) => {
+                            const timeout = this.timeout;
+                            timeout && window.clearTimeout(timeout);
+                            this.timeout = window.setTimeout(() => {
+                                this.fetchData(value)
+                            }, 500)
+                        }}
+                        onFocus={() => {
+                            this.fetchData('');
+                        }}
+                    >
+                        { this.generateOptions() }
+                    </Select>
+                </InputGroup>
+            </Header>
+            <Content>
+                Content
+            </Content>
+        </Layout>
+    }
+}
+export default connect(({ present: { authority } }) => ({ authority }))(Authority)

+ 9 - 0
src/components/authority/index.less

@@ -0,0 +1,9 @@
+.layout-authority {
+    .ant-layout-header {
+        .ant-input-group {
+            .ant-select {
+                min-width: 150px;
+            }
+        }
+    }
+}

+ 52 - 0
src/components/authority/index1.jsx

@@ -0,0 +1,52 @@
+import React from 'react'
+import { Layout } from 'antd'
+import { connect } from 'dva'
+import DashboardMenu from '../dashboard/menu'
+const { Sider, Content } = Layout
+
+class Authority extends React.Component {
+    constructor(props) {
+        super(props);
+        this.state = {};
+    }
+
+    render() {
+        const { authority, dispatch } = this.props;
+        console.log(authority);
+        return <Layout>
+            <Sider
+                theme='light'
+                width={300}
+            >
+                <DashboardMenu
+                    model={authority}
+                    editable={false}
+                    searchMenu={true}
+                    onlyMenu={false}
+                    hideHeader={false}
+                    hideEmptyMenu={true}
+                    onExpand={menus => {
+                        dispatch({ type: 'authority/setFields', fields: [
+                            { name: 'menuExpandedKeys', value: menus.map(m => m.code) },
+                            { name: 'menuAutoExpandParent', value: false },
+                        ] });
+                    }}
+                    onSelect={selectedMenu => {
+                        dispatch({ type: 'authority/setField', name: 'menuSelectedKeys', value: selectedMenu && selectedMenu.type === 'dashboard' ? [selectedMenu.code] : [] })
+                    }}
+                    onSearch={(keyword, expandedMenus) => {
+                        dispatch({ type: 'authority/setFields', fields: [
+                            { name: 'menuExpandedKeys', value: expandedMenus.map(m => m.code) },
+                            { name: 'menuFilterLabel', value: keyword },
+                            { name: 'menuAutoExpandParent', value: true },
+                        ] });
+                    }}
+                />
+            </Sider>
+            <Content>
+                
+            </Content>
+        </Layout>
+    }
+}
+export default connect(({ present: { authority } }) => ({ authority }))(Authority)

+ 1 - 1
src/components/chartDesigner/sections/barConfigForm.jsx

@@ -35,7 +35,7 @@ const BarConfigForm = ({ autoRefresh, chartDesigner, dispatch, formItemLayout })
 									fragment
 								)
 							}))
-							return <div>{label0}>{path[1].label}</div>
+							return <div>{label0}{path[1] ? '>' + path[1].label : ''}</div>
 						}
 					}}
 					options={columns.filter(c =>['ordinal', 'categorical', 'time'].indexOf(c.type) !== -1).map((c, i)=>{

+ 1 - 0
src/components/chartDesigner/sections/displayColumnBox.jsx

@@ -81,6 +81,7 @@ class DisplayColumnBox extends React.Component {
                 <Transfer
                     dataSource={allColumns.map(c => ({ ...c, key: c.name }))}
                     showSearch
+                    filterOption={(inputValue, option) => option.label.toLowerCase().indexOf(inputValue.toLowerCase()) > -1}
                     listStyle={{
                         width: 250,
                         height: 300,

+ 1 - 1
src/components/chartDesigner/sections/lineConfigForm.jsx

@@ -35,7 +35,7 @@ const LineConfigForm = ({ autoRefresh, chartDesigner, dispatch, formItemLayout }
 									fragment
 								)
 							}))
-							return <div>{label0}>{path[1].label}</div>
+							return <div>{label0}</div>
 						}
 					}}
 					options={columns.filter(c =>['time'].indexOf(c.type) !== -1).map((c, i)=>{

+ 1 - 1
src/components/chartDesigner/sections/pieConfigForm.jsx

@@ -35,7 +35,7 @@ const PieConfigForm = ({ autoRefresh, chartDesigner, dispatch, formItemLayout })
 									fragment
 								)
 							}))
-							return <div>{label0}>{path[1].label}</div>
+							return <div>{label0}{path[1] ? '>' + path[1].label : ''}</div>
 						}
 					}}
 					options={columns.filter(c =>['ordinal', 'categorical', 'time'].indexOf(c.type) !== -1).map((c, i)=>{

+ 1 - 1
src/components/chartDesigner/sections/scatterConfigForm.jsx

@@ -34,7 +34,7 @@ const ScatterConfigForm = ({ autoRefresh, chartDesigner, dispatch, formItemLayou
 									fragment
 								)
 							}))
-							return <div>{label0}>{path[1].label}</div>
+							return <div>{label0}{path[1] ? '>' + path[1].label : ''}</div>
 						}
 					}}
 					options={columns.filter(c =>['scale', 'ordinal'].indexOf(c.type) !== -1).map((c, i)=>{

+ 13 - 8
src/components/common/filterBox/filter.jsx

@@ -38,7 +38,7 @@ class Filter extends React.Component {
     generateValueItem = (filter, index) => {
         const { fetching, columnData, dropdownOpen } = this.state;
         let field;
-        const { type, operator } = filter;
+        const { key, type, operator } = filter;
         let defaultValue = type==='time' ? ( filter['value' + index] ? ( filter['value' + index].dynamic ? ( filter['value' + index] ) : moment(filter['value' + index]) ) : null ) : filter['value' + index]
         const commonProps = { defaultValue }
 
@@ -59,17 +59,22 @@ class Filter extends React.Component {
             field = <div className='cus-time-picker' >
                 <Input
                     key={Math.random()}
-                    suffix={<Icon style={{ color: 'rgba(0, 0, 0, 0.25)' }} type="calendar" />}
-                    defaultValue={ commonProps.defaultValue.dynamic ? commonProps.defaultValue.label : moment(commonProps.defaultValue).format('YYYY-MM-DD') }
-                        onFocus={() => {
+                    allowClear
+                    // suffix={<Icon style={{ color: 'rgba(0, 0, 0, 0.25)' }} type="calendar" />}
+                    defaultValue={ commonProps.defaultValue ? (commonProps.defaultValue.dynamic ? commonProps.defaultValue.label : moment(commonProps.defaultValue).format('YYYY-MM-DD')) : '' }
+                    onFocus={() => {
                         let obj = {};
                         obj['datePickerOpen' + index] = true;
                         this.setState(obj);
                     }}
+                    onChange={() => {
+                        // 因为onFocus方法弹出了浮层,onChange方法只在点击清楚图标时会调用(相当于不可编辑的input),所以这里只做清除
+                        this.changeFilterValue(filter, null, index)
+                    }}
                 />
                 <DatePicker
                     key={Math.random()}
-                    defaultValue={commonProps.defaultValue.dynamic ? null : commonProps.defaultValue}
+                    defaultValue={commonProps.defaultValue ? (commonProps.defaultValue.dynamic ? null : commonProps.defaultValue) : null}
                     showToday={false}
                     open={this.state['datePickerOpen' + index]}
                     onOpenChange={status => {
@@ -131,7 +136,7 @@ class Filter extends React.Component {
                 </Select>)
             }else { // 等于/不等于
                 field = (<Select
-                    key={Math.random()} 
+                    key={key} // 不定义这个key的话每次在box中更新了值后不能正确反映到filter中来,不信你把这行注释看看
                     { ...commonProps }
                     allowClear
                     mode='single'
@@ -160,7 +165,7 @@ class Filter extends React.Component {
 
     generateTimeTags = (filter, index, defaultValue) => {
         const timeTypes = [
-            { dynamic: true, label: '今天', name: 'today', format: () => {console.log(11);return 'today'} },
+            { dynamic: true, label: '今天', name: 'today' },
             { dynamic: true, label: '昨天', name: 'yesterday' },
             { dynamic: true, label: '本周', name: 'week' },
             { dynamic: true, label: '上周', name: 'lastweek' },
@@ -171,7 +176,7 @@ class Filter extends React.Component {
             { dynamic: true, label: '本年', name: 'year' },
             { dynamic: true, label: '上年', name: 'lastyear' },
         ]
-        return timeTypes.map(t => <Tag key={t.name} className={`cus-time-tag${defaultValue.name === t.name ? ' current' : ''}`} onClick={() => {
+        return timeTypes.map(t => <Tag key={t.name} className={`cus-time-tag${defaultValue && defaultValue.name === t.name ? ' current' : ''}`} onClick={() => {
             this.changeFilterValue(filter, t, index);
             let obj = {};
             obj['datePickerOpen' + index] = false;

+ 13 - 8
src/components/common/filterBox/filter2.jsx

@@ -34,7 +34,7 @@ class Filter extends React.Component {
 
     generateTimeTags = (filter, index, defaultValue) => {
         const timeTypes = [
-            { dynamic: true, label: '今天', name: 'today', format: () => {console.log(11);return 'today'} },
+            { dynamic: true, label: '今天', name: 'today' },
             { dynamic: true, label: '昨天', name: 'yesterday' },
             { dynamic: true, label: '本周', name: 'week' },
             { dynamic: true, label: '上周', name: 'lastweek' },
@@ -45,7 +45,7 @@ class Filter extends React.Component {
             { dynamic: true, label: '本年', name: 'year' },
             { dynamic: true, label: '上年', name: 'lastyear' },
         ]
-        return timeTypes.map(t => <Tag key={t.name} className={`cus-time-tag${defaultValue.name === t.name ? ' current' : ''}`} onClick={() => {
+        return timeTypes.map(t => <Tag key={t.name} className={`cus-time-tag${defaultValue && defaultValue.name === t.name ? ' current' : ''}`} onClick={() => {
             this.changeFilterValue(filter, t, index);
             let obj = {};
             obj['datePickerOpen' + index] = false;
@@ -56,7 +56,7 @@ class Filter extends React.Component {
     generateValueItem = (filter, index) => {
         const { fetching, columnData, dropdownOpen } = this.state;
         let field;
-        let { type, operator } = filter;
+        let { key, type, operator } = filter;
 
         let defaultValue = type==='time' ? ( filter['value' + index] ? ( filter['value' + index].dynamic ? ( filter['value' + index] ) : moment(filter['value' + index]) ) : null ) : filter['value' + index]
         const commonProps = { defaultValue }
@@ -77,17 +77,22 @@ class Filter extends React.Component {
             field = <div className='cus-time-picker' >
                 <Input
                     key={Math.random()}
-                    suffix={<Icon style={{ color: 'rgba(0, 0, 0, 0.25)' }} type="calendar" />}
-                    defaultValue={ commonProps.defaultValue.dynamic ? commonProps.defaultValue.label : moment(commonProps.defaultValue).format('YYYY-MM-DD') }
-                        onFocus={() => {
+                    allowClear
+                    // suffix={<Icon style={{ color: 'rgba(0, 0, 0, 0.25)' }} type="calendar" />}
+                    defaultValue={ commonProps.defaultValue ? (commonProps.defaultValue.dynamic ? commonProps.defaultValue.label : moment(commonProps.defaultValue).format('YYYY-MM-DD')) : '' }
+                    onFocus={(e) => {
                         let obj = {};
                         obj['datePickerOpen' + index] = true;
                         this.setState(obj);
                     }}
+                    onChange={() => {
+                        // 因为onFocus方法弹出了浮层,onChange方法只在点击清楚图标时会调用(相当于不可编辑的input),所以这里只做清除
+                        this.changeFilterValue(filter, null, index)
+                    }}
                 />
                 <DatePicker
                     key={Math.random()}
-                    defaultValue={commonProps.defaultValue.dynamic ? null : commonProps.defaultValue}
+                    defaultValue={commonProps.defaultValue ? (commonProps.defaultValue.dynamic ? null : commonProps.defaultValue) : null}
                     showToday={false}
                     open={this.state['datePickerOpen' + index]}
                     onOpenChange={status => {
@@ -146,7 +151,7 @@ class Filter extends React.Component {
                 </Select>)
             }else { // 等于/不等于
                 field = (<Select
-                    key={Math.random()}
+                    key={key}
                     { ...commonProps }
                     allowClear
                     mode='single'

+ 11 - 6
src/components/common/filterBox/filterBox.jsx

@@ -12,11 +12,11 @@ const FormItem = Form.Item
 const SelectOption = Select.Option
 const OptionGroup = Select.OptGroup 
 
-let uuid = 0;
+let uuid = Math.ceil(Math.random() * 1000);
 class FilterBox extends React.Component {
     
     constructor(props) {
-        uuid = 0;
+        uuid = Math.ceil(Math.random() * 1000);
         super(props);
         this.state = {
             columns: props.columns || [],
@@ -223,7 +223,7 @@ class FilterBox extends React.Component {
             { dynamic: true, label: '本年', name: 'year' },
             { dynamic: true, label: '上年', name: 'lastyear' },
         ]
-        return timeTypes.map(t => <Tag key={t.name} className={`cus-time-tag${defaultValue.name === t.name ? ' current' : ''}`} onClick={() => {
+        return timeTypes.map(t => <Tag key={t.name} className={`cus-time-tag${defaultValue && defaultValue.name === t.name ? ' current' : ''}`} onClick={() => {
             this.changeFilterValue(filter, t, index);
             let obj = {};
             obj['datePickerOpen' + index] = false;
@@ -253,17 +253,22 @@ class FilterBox extends React.Component {
             field = <div className='cus-time-picker' >
                 <Input
                     key={Math.random()}
-                    suffix={<Icon style={{ color: 'rgba(0, 0, 0, 0.25)' }} type="calendar" />}
-                    defaultValue={ commonProps.defaultValue.dynamic ? commonProps.defaultValue.label : moment(commonProps.defaultValue).format('YYYY-MM-DD') }
+                    allowClear
+                    // suffix={<Icon style={{ color: 'rgba(0, 0, 0, 0.25)' }} type="calendar" />}
+                    defaultValue={ commonProps.defaultValue ? (commonProps.defaultValue.dynamic ? commonProps.defaultValue.label : moment(commonProps.defaultValue).format('YYYY-MM-DD')) : '' }
                     onFocus={() => {
                         let obj = {};
                         obj['datePickerOpen' + index] = true;
                         this.setState(obj);
                     }}
+                    onChange={() => {
+                        // 因为onFocus方法弹出了浮层,onChange方法只在点击清楚图标时会调用(相当于不可编辑的input),所以这里只做清除
+                        this.changeFilterValue(filter, null, index)
+                    }}
                 />
                 <DatePicker
                     key={Math.random()}
-                    defaultValue={commonProps.defaultValue.dynamic ? null : commonProps.defaultValue}
+                    defaultValue={commonProps.defaultValue ? (commonProps.defaultValue.dynamic ? null : commonProps.defaultValue) : null}
                     showToday={false}
                     open={this.state['datePickerOpen' + index]}
                     onOpenChange={status => {

+ 11 - 6
src/components/common/filterBox/filterBox2.jsx

@@ -14,11 +14,11 @@ import moment from 'moment'
 const FormItem = Form.Item
 const SelectOption = Select.Option
 
-let uuid = 0;
+let uuid = Math.ceil(Math.random() * 1000);
 class FilterBox extends React.Component {
     
     constructor(props) {
-        uuid = 0;
+        uuid = Math.ceil(Math.random() * 1000);
         super(props);
         this.state = {
             dataSources: props.dataSources || [],
@@ -373,7 +373,7 @@ class FilterBox extends React.Component {
             { dynamic: true, label: '本年', name: 'year' },
             { dynamic: true, label: '上年', name: 'lastyear' },
         ]
-        return timeTypes.map(t => <Tag key={t.name} className={`cus-time-tag${defaultValue.name === t.name ? ' current' : ''}`} onClick={() => {
+        return timeTypes.map(t => <Tag key={t.name} className={`cus-time-tag${defaultValue && defaultValue.name === t.name ? ' current' : ''}`} onClick={() => {
             this.changeFilterValue(filter, t, index);
             let obj = {};
             obj['datePickerOpen' + index] = false;
@@ -399,17 +399,22 @@ class FilterBox extends React.Component {
             field = <div className='cus-time-picker' >
                 <Input
                     key={Math.random()}
-                    suffix={<Icon style={{ color: 'rgba(0, 0, 0, 0.25)' }} type="calendar" />}
-                    defaultValue={ commonProps.defaultValue.dynamic ? commonProps.defaultValue.label : moment(commonProps.defaultValue).format('YYYY-MM-DD') }
+                    allowClear
+                    // suffix={<Icon style={{ color: 'rgba(0, 0, 0, 0.25)' }} type="calendar" />}
+                    defaultValue={ commonProps.defaultValue ? (commonProps.defaultValue.dynamic ? commonProps.defaultValue.label : moment(commonProps.defaultValue).format('YYYY-MM-DD')) : '' }
                     onFocus={() => {
                         let obj = {};
                         obj['datePickerOpen' + index] = true;
                         this.setState(obj);
                     }}
+                    onChange={() => {
+                        // 因为onFocus方法弹出了浮层,onChange方法只在点击清楚图标时会调用(相当于不可编辑的input),所以这里只做清除
+                        this.changeFilterValue(filter, null, index)
+                    }}
                 />
                 <DatePicker
                     key={Math.random()}
-                    defaultValue={commonProps.defaultValue.dynamic ? null : commonProps.defaultValue}
+                    defaultValue={commonProps.defaultValue ? (commonProps.defaultValue.dynamic ? null : commonProps.defaultValue) : null}
                     showToday={false}
                     open={this.state['datePickerOpen' + index]}
                     onOpenChange={status => {

+ 33 - 10
src/components/dashboard/layout.jsx

@@ -7,20 +7,43 @@ import './layout.less'
 const { Sider, Content } = Layout
 
 class DashboardLayout extends React.Component {
-    constructor(props) {
-        super(props);
-        this.state = {
-
-        }
-    }
 
     render() {
-        const { loading } = this.props;
+        const { loading, dashboard, dispatch } = this.props;
         return <Layout
             className='layout-dashboard'
         >
             <Sider width={300}>
-                <DashboardMenu mode='manage'/>
+                <DashboardMenu
+                    model={dashboard}
+                    editable={true}
+                    searchMenu={true}
+                    onlyMenu={true}
+                    hideEmptyMenu={false}
+                    onExpand={menus => {
+                        dispatch({ type: 'dashboard/setFields', fields: [
+                            { name: 'menuExpandedKeys', value: menus.map(m => m.code) },
+                            { name: 'menuAutoExpandParent', value: false },
+                        ] });
+                    }}
+                    onSelect={selectedMenu => {
+                        dispatch({ type: 'dashboard/setField', name: 'menuSelectedKeys', value: !!selectedMenu ? [selectedMenu.code] : [] })
+                        if(!!selectedMenu) {
+                            if(selectedMenu.code === '-1') {
+                                dispatch({ type: 'dashboard/fetchList', mandatory: true });
+                            }else {
+                                dispatch({ type: 'dashboard/remoteMenuDashboardList', menuCode: selectedMenu.code });
+                            }
+                        }
+                    }}
+                    onSearch={(keyword, expandedMenus) => {
+                        dispatch({ type: 'dashboard/setFields', fields: [
+                            { name: 'menuExpandedKeys', value: expandedMenus.map(m => m.code) },
+                            { name: 'menuFilterLabel', value: keyword },
+                            { name: 'menuAutoExpandParent', value: true },
+                        ] });
+                    }}
+                />
                 <div style={{ display: loading ? 'block' : 'none', position: 'absolute', height: '100%', width: '100%', top: '0px', zIndex: '4', background: 'rgba(51,51,51,.1)' }}>
                     <Spin style={{ display: 'inline-block', position: 'absolute', top: '50%', left: '50%', margin: '-10px' }} indicator={<Icon type="loading" style={{ fontSize: 24 }} spin />}/>
                 </div>
@@ -31,7 +54,7 @@ class DashboardLayout extends React.Component {
         </Layout>
     }
 }
-function mapStateToProps({ present: { loading }}) {
+function mapStateToProps({ present: { loading, dashboard }}) {
     let effectsArr = ['dashboard/remoteMenuTree', 'dashboard/remoteAddMenu', 'dashboard/remoteDeleteMenu'];
     let flag = false;
     for(let i = 0; i < effectsArr.length; i++) {
@@ -41,6 +64,6 @@ function mapStateToProps({ present: { loading }}) {
         }
     }
 
-    return { loading: flag }
+    return { loading: flag, dashboard }
 }
 export default connect(mapStateToProps)(DashboardLayout)

+ 97 - 84
src/components/dashboard/menu.jsx

@@ -9,7 +9,6 @@ const { Search } = Input
 class DashboardMenu extends React.Component {
 
     constructor(props) {
-        console.log('create');
         super(props);
         this.state = {
             editingKey: null,
@@ -18,6 +17,14 @@ class DashboardMenu extends React.Component {
         };
     }
 
+    static defaultProps = { 
+        onlyMenu: true, // 仅展示目录
+        hideEmptyMenu: false, // 隐藏空目录
+        editable: true,
+        searchMenu: true,
+        hideHeader: false,
+    }
+
     componentDidMount() {
         const { dispatch } = this.props;
         dispatch({ type: 'dashboard/remoteMenuTree' });
@@ -35,41 +42,55 @@ class DashboardMenu extends React.Component {
         return arr;
     }
 
-    reduceTree = (mode, tree, regLabel) => {
+    reduceTree = (tree, regLabel) => {
+        const { onlyMenu, searchMenu, hideEmptyMenu } = this.props;
         let arr = tree ? [ ...tree ] : [];
         for(let i = arr.length - 1; i >= 0; i--) {
             let t = arr[i];
             if(t.children && t.children.length > 0) {
-                t.children = this.reduceTree(mode, t.children, regLabel);
+                t.children = this.reduceTree(t.children, regLabel);
             }
-            if(mode === 'view') {
-                if((!t.children || t.children.length === 0) && t.type !== 'dashboard') {
-                    arr.splice(i, 1);
-                }
-                if(t.type === 'dashboard' && !!regLabel && t.name.search(new RegExp('(' + regLabel + '){1}', 'ig')) === -1) {
-                    arr.splice(i, 1);
-                }
-            }else {
+
+            // 纯目录过滤
+            if(onlyMenu) {
                 if(t.type === 'dashboard') {
                     arr.splice(i, 1);
+                    continue;
                 }
-                if(t.type === 'menu' && !!regLabel && (t.name.search(new RegExp('(' + regLabel + '){1}', 'ig')) === -1) && (t.children.length === 0)) {
+            }
+            // 空目录过滤
+            if(hideEmptyMenu) {
+                // 当搜索包括目录时还是保留目录
+                // if(searchMenu && t.type === 'menu' && t.name.search(new RegExp('(' + regLabel + '){1}', 'ig')) >= -1) {
+
+                // }else if((!t.children || t.children.length === 0) && t.type === 'menu') {
+                //     arr.splice(i, 1);
+                //     continue;
+                // }
+
+                if(t.type === 'menu' && (!t.children || t.children.length === 0) && !(searchMenu && t.name.search(new RegExp('(' + regLabel + '){1}', 'ig')) >= -1)) {
                     arr.splice(i, 1);
+                    continue;
                 }
             }
+            // 关键字过滤
+            if(!!regLabel && t.name.search(new RegExp('(' + regLabel + '){1}', 'ig')) === -1 && (t.type === 'dashboard' || (t.children.length === 0))) {
+                arr.splice(i, 1);
+                continue;
+            }
         }
         return arr;
     }
 
     generateMenu(tree, regLabel) {
-        const { mode, dispatch } = this.props;
+        const { dispatch, onlyMenu, searchMenu, editable } = this.props;
         const { editingKey } = this.state;
         let ftree = this.cloneTree(tree);
-        ftree = this.reduceTree(mode, ftree, regLabel)
-        return ftree.filter(t => (mode === 'view' || t.type === 'menu')).sort((a, b) => a.index - b.index).map(t => {
+        ftree = this.reduceTree(ftree, regLabel)
+        return ftree.filter(t => (onlyMenu ? t.type === 'menu' : true)).sort((a, b) => a.index - b.index).map(t => {
             let title = <div className='node-title'>
                 <span>{ (t.code !== editingKey) ?
-                   ( regLabel ? ( <span style={{ fontWeight: t.type === 'dashboard' ? 'bold' : 'normal' }}>
+                   ( !!regLabel && (searchMenu || t.type === 'dashboard') ? (<div className='label'> <span title={t.name} style={{ fontWeight: t.type === 'dashboard' ? 'bold' : 'normal' }}>
                         { t.type === 'dashboard' && <Icon style={{ marginRight: '8px' }} type="pushpin" /> }
                         { (t.name || '').split(new RegExp(`(${regLabel})`, 'i')).map((fragment, i) => {
                             return (
@@ -78,13 +99,14 @@ class DashboardMenu extends React.Component {
                                 fragment
                             )
                         }) }                       
-                   </span> ) : <div className='label'>
-                        <span style={{ fontWeight: t.type === 'dashboard' ? 'bold' : 'normal' }}>
+                   </span> </div>  ) : <div className='label'>
+                        <span title={t.name} style={{ fontWeight: t.type === 'dashboard' ? 'bold' : 'normal' }}>
                             { t.type === 'dashboard' && <Icon style={{ marginRight: '8px' }} type="pushpin" /> }
                             { t.name }
                         </span>
                     </div> ) : ( <div className='input'>
                     <Input
+                        ref={(input) => { this['inputRef-' + t.code] = input }}
                         size="small"
                         defaultValue={t.name}
                         addonAfter={<Icon style={{ cursor: 'pointer', color: '#52C41A' }} type="check-circle" onClick={() => {
@@ -104,13 +126,18 @@ class DashboardMenu extends React.Component {
                     />
                 </div> )
                 }</span>
-                {mode !== 'view' && <div className='tools'>
+                {editable && <div className='tools'>
+                    {t.code !== editingKey && t.type === 'menu' && <Icon type='plus' onClick={() => {
+                        this.onAddClick(t)
+                    }}/>}
                     {t.code !== editingKey && <Icon type='edit' onClick={() => {
                         this.setState({
                             editingKey: t.code
+                        }, () => {
+                            this['inputRef-' + t.code].focus();
                         });
                     }}/>}
-                    {!(t.children && t.children.length > 0) && <Icon type='delete' onClick={() => {
+                    {t.code !== editingKey && !(t.children && t.children.length > 0) && <Icon type='delete' onClick={() => {
                         this.setState({
                             selectedMenu: t,
                             visibleDeleteBox: true,
@@ -125,58 +152,37 @@ class DashboardMenu extends React.Component {
         });
     }
 
-    onChange = (e) => {
-        const { dispatch } = this.props;
-        dispatch({ type: 'dashboard/setField', name: 'menuFilterLabel', value: e.target.value })
-    }
-
     onExpand = (keys) => {
-        const { mode, dispatch } = this.props;
-        if(mode === 'view') {
-            dispatch({ type: 'home/setFields', fields: [
-                { name: 'menuExpandedKeys', value: keys },
-                { name: 'menuAutoExpandParent', value: false },
-            ] });
-        }else {
-            dispatch({ type: 'dashboard/setFields', fields: [
-                { name: 'menuExpandedKeys', value: keys },
-                { name: 'menuAutoExpandParent', value: false },
-            ] });
+        const { dashboard, onExpand: propsOnExpand } = this.props;
+        const { menuList } = dashboard;
+        let expandedMenus = [];
+        for(let i = 0; i < keys.length; i++) {
+            let menu = [{ code: '-1', name: '全部目录' }].concat(menuList).find(l => l.code === keys[i]);
+            expandedMenus.push(menu);
+        }
+        if(typeof propsOnExpand === 'function') {
+            propsOnExpand(expandedMenus);
         }
     }
 
     onSelect = (selectedKeys, info) => {
-        const { mode, dashboard, dispatch } = this.props;
+        const { dashboard, onSelect: propsOnSelect } = this.props;
         const { menuList } = dashboard;
-        if(mode === 'view') {
-            // dispatch({ type: 'home/setField', name: 'menuSelectedKeys', value: selectedKeys })
-            let selectedMenu = menuList.find(l => l.code === selectedKeys[0]);
-            if(selectedMenu && selectedMenu.type === 'dashboard') {
-                dispatch({ type: 'home/openTab', tab: {
-                    code: selectedMenu.code,
-                    name: selectedMenu.name
-                } })
-            }
-        }else {
-            dispatch({ type: 'dashboard/setField', name: 'menuSelectedKeys', value: selectedKeys })
-            if(selectedKeys.length === 1) {
-                if(selectedKeys[0] === '-1') {
-                    dispatch({ type: 'dashboard/fetchList', mandatory: true });
-                }else {
-                    dispatch({ type: 'dashboard/remoteMenuDashboardList', menuCode: selectedKeys[0] });
-                }
-            }
+        let selectedMenu = [{ code: '-1', name: '全部目录' }].concat(menuList).find(l => l.code === selectedKeys[0]);
+
+        if(typeof propsOnSelect === 'function') {
+            propsOnSelect(selectedMenu);
         }
     }
 
     onSearch = (value) => {
-        const { dashboard, dispatch } = this.props;
-        const expandedKeys = this.findExpandedKeys(dashboard.menuTree, {code: '-1'}, value);
-        dispatch({ type: 'dashboard/setFields', fields: [
-            { name: 'menuExpandedKeys', value: expandedKeys },
-            { name: 'menuFilterLabel', value },
-            { name: 'menuAutoExpandParent', value: true },
-        ] });
+        const { dashboard, onSearch: propsOnSearch } = this.props;
+        // 获得需要展开的目录(存在包含关键字的报表或目录)
+        const expandedMenus = this.findExpandedKeys(dashboard.menuTree, {code: '-1'}, value);
+
+        if(typeof propsOnSearch === 'function') {
+            propsOnSearch(value, expandedMenus);
+        }
     }
 
     findExpandedKeys = (tree, parent, filterLabel) => {
@@ -184,7 +190,7 @@ class DashboardMenu extends React.Component {
         if(tree && tree.length > 0) {
             tree.forEach(t => {
                 if(t.name.toLowerCase().indexOf(filterLabel.toLowerCase()) > -1 && result.indexOf(parent.code) === -1) {
-                    result.push(parent.code);
+                    result.push(parent);
                 }
                 result = result.concat(this.findExpandedKeys(t.children, t, filterLabel));
             });
@@ -192,21 +198,24 @@ class DashboardMenu extends React.Component {
         return result;
     }
 
-    onAddClick = () => {
-        const { dashboard, dispatch } = this.props;
-        const { menuList, menuTree, menuSelectedKeys, menuExpandedKeys } = dashboard;
-        const pmenu = [{ code: '-1', name: '全部目录', pcode: '-1', childrenCount: menuTree.length }].concat(menuList).find(l => l.code === menuSelectedKeys[0]);
+    onAddClick = (pmenu) => {
+        const { dashboard, dispatch, onExpand } = this.props;
+        const { menuList, menuExpandedKeys } = dashboard;
         const menu = {
             index: pmenu.childrenCount,
             name: '新目录',
             pcode: pmenu.code
         }
         dispatch({ type: 'dashboard/remoteAddMenu', menu }).then(res => {
-            if(res) {
-                dispatch({ type: 'dashboard/setFields', fields: [
-                    { name: 'menuExpandedKeys', value: menuExpandedKeys.concat([pmenu.code]) },
-                    { name: 'autoExpandParent', value: true },
-                ] });
+            if(res && typeof onExpand === 'function') {
+                let expandedMenus = [];
+                [{ code: '-1', name: '全部目录' }].concat(menuList).forEach(m => {
+                    if(menuExpandedKeys.indexOf(m.code) > -1) {
+                        expandedMenus.push(m); 
+                    }
+                });
+                expandedMenus.push(pmenu);
+                onExpand(expandedMenus);
             }
         });
     }
@@ -222,24 +231,19 @@ class DashboardMenu extends React.Component {
     }
 
     render() {
-        const { mode, home, dashboard, dispatch } = this.props;
+        const { dashboard, dispatch, hideHeader, editable, model } = this.props;
         const { visibleDeleteBox, selectedMenu } = this.state;
         const { menuTree } = dashboard;
-        const { menuFilterLabel, menuExpandedKeys, menuSelectedKeys, menuAutoExpandParent } = mode === 'view' ? home : dashboard;
+        const { menuFilterLabel, menuExpandedKeys, menuSelectedKeys, menuAutoExpandParent } = model;
         const reg = new RegExp('([+ \\- & | ! ( ) { } \\[ \\] ^ \" ~ * ? : ( ) \/])', 'g'); // 需要转义的字符
         const regLabel = menuFilterLabel.replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1'); // 添加转义符号
 
-        return <div className='menu-container' style={ mode === 'view' ? { paddingTop: 0 } : { paddingTop: '88px' } }>
+        return <div className='menu-container' style={ hideHeader ? { paddingTop: 0 } : { paddingTop: '48px' } }>
             
-            {mode !== 'view' && <div className='menu-buttons'>
-                <Button disabled={menuSelectedKeys.length !== 1} onClick={this.onAddClick}>新增目录</Button>
-                {/* <Button disabled={menuSelectedKeys.length !== 1} onClick={this.onModifyClick}>修改目录</Button> */}
-            </div>}
-            {mode !== 'view' && <div className='menu-search'>
+            {!hideHeader && <div className='menu-search'>
                 <Search
                     defaultValue={menuFilterLabel}
                     placeholder='搜索'
-                    // onChange={this.onChange}
                     onSearch={this.onSearch}
                 />
                 <Button onClick={() => {
@@ -255,7 +259,16 @@ class DashboardMenu extends React.Component {
                     onSelect={this.onSelect}
                     autoExpandParent={menuAutoExpandParent}
                 >
-                    <TreeNode title='全部目录' key='-1'>
+                    <TreeNode title={
+                        <div className='node-title'>
+                            <span><div className='label'><span>全部目录</span></div></span>
+                            {editable && <div className='tools'>
+                                <Icon type='plus' onClick={() => {
+                                    this.onAddClick({ code: '-1', name: '全部目录', pcode: '-1', childrenCount: menuTree.length })
+                                }}/>
+                            </div>}
+                        </div>
+                    } key='-1'>
                         { this.generateMenu(menuTree, regLabel) }
                     </TreeNode>
                 </Tree>
@@ -276,7 +289,7 @@ class DashboardMenu extends React.Component {
     }
 }
 
-function mapStateToProps({ present: { dashboard, home }}) {
-    return { dashboard, home }
+function mapStateToProps({ present: { dashboard }}) {
+    return { dashboard }
 }
 export default connect(mapStateToProps)(DashboardMenu)

+ 15 - 9
src/components/dashboard/menu.less

@@ -1,19 +1,20 @@
 .menu-container {
     height: 100%;
     // padding-top: 88px;
-    .menu-buttons {
-        height: 32px;
-        padding: 0 8px;
-        margin-top: -80px;
-        margin-bottom: 8px;
-        button {
-            margin-right: 8px;
-        }
-    }
+    // .menu-buttons {
+    //     height: 32px;
+    //     padding: 0 8px;
+    //     margin-top: -32px;
+    //     margin-bottom: 8px;
+    //     button {
+    //         margin-right: 8px;
+    //     }
+    // }
     .menu-search {
         display: flex;
         height: 32px;
         padding: 0 8px;
+        margin-top: -40px;
         margin-bottom: 8px;
         .ant-input-search {
             margin-right: 8px;
@@ -32,6 +33,11 @@
             .node-title {
                 display: flex;
                 justify-content: space-between;
+                .label {
+                    width: 150px;
+                    overflow: hidden;
+                    text-overflow: ellipsis;
+                }
                 .tools {
                     .anticon {
                         display: none;

+ 24 - 2
src/components/homePage/sider.jsx

@@ -46,12 +46,34 @@ class MenuLayout extends React.Component {
                 />
                 <Button className='refresh' onClick={() => {
                     dispatch({ type: 'dashboard/remoteMenuTree', mandatory: true });
-                    dispatch({ type: 'dashboard/fetchCollectionDashboardList', mandatory: true });
+                    dispatch({ type: 'home/fetchCollectionDashboardList', mandatory: true });
                 }}><Icon type='sync' /></Button>
             </Header>
             <Content>
                 <DashboardCollection />
-                <DashboardMenu mode='view' />
+                <DashboardMenu
+                    model={home}
+                    editable={false}
+                    searchMenu={false}
+                    onlyMenu={false}
+                    hideHeader={true}
+                    hideEmptyMenu={true}
+                    onExpand={menus => {
+                        dispatch({ type: 'home/setFields', fields: [
+                            { name: 'menuExpandedKeys', value: menus.map(m => m.code) },
+                            { name: 'menuAutoExpandParent', value: false },
+                        ] });
+                    }}
+                    onSelect={selectedMenu => {
+                        // dispatch({ type: 'home/setField', name: 'menuSelectedKeys', value: selectedKeys })
+                        if(selectedMenu && selectedMenu.type === 'dashboard') {
+                            dispatch({ type: 'home/openTab', tab: {
+                                code: selectedMenu.code,
+                                name: selectedMenu.name
+                            } })
+                        }
+                    }}
+                />
             </Content>
         </Layout>;
     }

+ 10 - 4
src/components/homePage/toolbar.less

@@ -24,14 +24,20 @@
         .ant-modal-close {
             width: 0;
             height: 0;
-            border-width: 16px;
+            border-width: 20px;
             border-style: solid;
             border-color: #DEDEDE #DEDEDE transparent transparent;
+            transform: rotate(135deg);
+            right: -18px;
+            top: -20px;
             .ant-modal-close-x {
-                width: 16px;
-                height: 16px;
+                font-size: 14px;
+                width: 14px;
+                height: 14px;
                 line-height: 0;
-                margin-top: -14px;
+                margin-top: -10px;
+                margin-left: 4px;
+                transform: rotate(45deg);
             }
         }
         .ant-modal-body {

+ 5 - 3
src/components/setting/index.jsx

@@ -3,8 +3,9 @@ import { Link } from 'react-router-dom'
 import { Route, Switch, Redirect } from 'dva/router'
 import { Layout, Button } from 'antd'
 import { connect } from 'dva'
-import Admin from '../../components/admin/admin'
-import Logs from '../../components/logs/logs'
+import Admin from '../admin/admin'
+import Authority from '../authority/index'
+import Logs from '../logs/logs'
 import './index.less'
 const { Sider, Content } = Layout
 
@@ -32,12 +33,13 @@ class Setting extends React.Component {
                 className='sider-setting'
             >
                 <Link to='/setting/admin'><Button className='ant-btn-block' type={(paths[1] === 'admin' || !paths[1]) ? 'primary' : 'default'} >用户管理</Button></Link>
-                <Link to=''><Button disabled className='ant-btn-block' type={paths[1] === 'datasource' ? 'primary' : 'default'} >权限管理</Button></Link>
+                <Link to='/setting/authority'><Button disabled className='ant-btn-block' type={paths[1] === 'authority' ? 'primary' : 'default'} >权限管理</Button></Link>
                 <Link to='/setting/logs'><Button className='ant-btn-block' type={paths[1] === 'logs' ? 'primary' : 'default'} >操作日志</Button></Link>
             </Sider>
             <Content className='content-setting'>
                 <Switch>
                     <Route sensitive path='/setting/admin' component={Admin}/>
+                    <Route sensitive path='/setting/authority' component={Authority}/>
                     <Route sensitive path='/setting/logs' component={Logs}/>
                     <Route path='/' component={() => (<Redirect to={{ pathname: '/setting/admin' }} ></Redirect>)}/>
                 </Switch>

+ 2 - 0
src/index.js

@@ -15,6 +15,7 @@ import user from './models/user'
 import chartPolicy from './models/chartPolicy'
 import dataSourcePolicy from './models/dataSourcePolicy'
 import dataList from './models/dataList'
+import authority from './models/authority'
 import logs from './models/logs'
 import recent from './models/recent'
 import './utils/baseUtils'
@@ -51,6 +52,7 @@ app.model(chartPolicy); // 图表策略
 app.model(dataSourcePolicy); // 数据源策略
 app.model(dataList); // 数据列表
 app.model(recent); // 最近访问记录
+app.model(authority); // 权限控制
 app.model(logs); // 操作日志
 
 // 4. Router

+ 40 - 0
src/models/authority.js

@@ -0,0 +1,40 @@
+import { message } from 'antd'
+import * as service from '../services/index'
+import URLS from '../constants/url'
+
+export default {
+    namespace: 'authority',
+    state: {
+        originData: {
+            menuExpandedKeys: ['-1'],
+            menuSelectedKeys: [],
+            menuFilterLabel: '',
+            menuAutoExpandParent: true,
+        },
+    },
+    reducers: {
+        setField(state, action) {
+            const { name, value } = action;
+            let obj = {};
+            obj[name] = value;
+            return Object.assign({}, state, obj);
+        },
+        setFields(state, action) {
+            const { fields } = action;
+            let obj = {};
+            fields.map(f => (obj[f.name] = f.value));
+            return Object.assign({}, state, obj);
+        },
+        reset(state, action) {
+            return Object.assign({}, state, state.originData);
+        },
+    },
+    effects: {
+
+    },
+    subscriptions: {
+        setup({ dispatch, history }) {
+            dispatch({ type: 'reset' });
+        }
+    }
+}

+ 1 - 1
src/models/parseChartOption.js

@@ -295,7 +295,7 @@ function dataViewOption(data, dataViewConfig) {
         columns: columns.map(c => ({
             title: c.label,
             dataIndex: c.name,
-            render: c.type === 'time' ? (v, r, i) => moment(v).format('YYYY-MM-DD') : void(0)
+            render: c.type === 'time' ? ((v, r, i) => moment(v).isValid() ? moment(v).format('YYYY-MM-DD') : v) : void(0)
         })),
         dataSource: dataSource.map((d, i) => {
             return { ...d, key: i}