Compare commits

..
36 Commits
Author SHA1 Message Date
zuiidea ed8555f15e Update README.md 2017-04-28 17:01:27 +08:00
zuiidea 33d4f1679e Merge branch '4.2-dev' 2017-04-28 16:42:59 +08:00
zuiidea 37dd7d011e merge 2017-04-28 16:42:31 +08:00
zuiidea 5641cc61c4 user mocj 2017-04-28 16:34:08 +08:00
zuiidea 30f14769f0 responsive 2017-04-27 20:40:47 +08:00
zuiidea 1b7c076057 filters 2017-04-27 20:26:30 +08:00
zuiidea fbd0fcb5c4 user detail 2017-04-27 17:59:39 +08:00
zuiidea 19ee8eacce user API restful 2017-04-27 17:29:50 +08:00
zuiidea 0b0c7c3f91 update some texts and configs 2017-04-27 14:40:36 +08:00
zuiidea 5d6abb67b6 Merge pull request #219 from HeChengjun/master
修正 arrayToTree 函数中 children 拼写错误
2017-04-26 16:55:17 +08:00
HeChengjun 1a58dcac95 修正 arrayToTree 函数中 children 拼写错误 2017-04-26 16:16:35 +08:00
zuiidea 3d2d524898 Update README.md 2017-04-25 16:17:25 +08:00
zuiidea 2869ead7bd Merge pull request #215 from zuiidea/4.2-dev
fix
2017-04-25 12:34:10 +08:00
zuiidea 25f7a65897 fix 2017-04-25 12:32:42 +08:00
zuiidea 75e6284873 Merge pull request #214 from zuiidea/4.2-dev
Menu defaultSelected
2017-04-25 12:19:03 +08:00
zuiidea 5fbb1b338a Menu defaultSelected 2017-04-25 12:09:20 +08:00
zuiidea ed2b0b25bd Merge pull request #212 from HeChengjun/patch-1
menu.js line 96 : lineChart -> barChart
2017-04-24 17:57:56 +08:00
HeChengjun a4922c7541 menu.js line 96 : lineChart -> barChart 2017-04-24 17:17:10 +08:00
zuiidea 4566171265 Merge pull request #209 from zuiidea/4.2-dev
hot update
2017-04-24 10:10:55 +08:00
zuiidea 6c48480031 hot update 2017-04-24 10:09:10 +08:00
zuiidea 3401215d41 Merge pull request #202 from zuiidea/4.2-dev
fix userInfo
2017-04-21 18:37:36 +08:00
zuiidea 7284cad8aa fix userInfo 2017-04-21 18:36:35 +08:00
zuiidea 53b8ee6e63 Update README.md 2017-04-21 11:06:23 +08:00
zuiidea b5423129f7 Merge pull request #198 from zuiidea/4.2-dev
4.2 dev
2017-04-21 10:49:31 +08:00
zuiidea 4c6deb49d3 fix menu open 2017-04-21 10:47:55 +08:00
zuiidea b6c3cb8a53 rebuild bread,menu,router 2017-04-20 19:26:17 +08:00
zuiidea a42e749c22 Update README.md 2017-04-19 19:27:23 +08:00
zuiidea cfd0172204 error message 2017-04-19 19:10:20 +08:00
zuiidea 4a694b6df8 fix prop-types 2017-04-18 09:56:15 +08:00
zuiidea 3d03fa0695 Merge pull request #184 from zlotus/remote_dev
fix mock error caused by invalid username
2017-04-15 18:45:25 +08:00
ZealotuS 69fbb4742e fix mock error caused by invalid username 2017-04-15 18:13:56 +08:00
zuiidea db8b283def Merge branch '4.1-dev' 2017-04-14 19:47:55 +08:00
zuiidea 4f673b6e3a merge 2017-04-14 19:47:33 +08:00
zuiidea f7616d524b update request 2017-04-01 17:53:06 +08:00
zuiidea 08fdd0d04f Merge pull request #159 from zuiidea/4.0-dev
fix getUserInfo
2017-03-31 21:05:51 +08:00
zuiidea 847f003cd8 fix getUserInfo 2017-03-31 21:04:33 +08:00
59 changed files with 5177 additions and 665 deletions
+21
View File
@@ -11,6 +11,9 @@
演示地址 <http://zuiidea.github.io/antd-admin/>
备用地址 <http://47.92.30.98:8000>
## 特性
- 基于[react](https://github.com/facebook/react),[ant-design](https://github.com/ant-design/ant-design),[dva](https://github.com/dvajs/dva),[Mock](https://github.com/nuysoft/Mock) 企业级后台管理系统最佳实践
@@ -19,8 +22,26 @@
- 基于[dva](https://github.com/dvajs/dva)动态加载 Model 和路由,按需加载
- 浅度响应式设计
## 注意
如果使用到roadhog,且出现接口404
- 保证`package.json`里没有`roadhog`
- 删除`node_modules`
- `npm i`
- `npm i roadhog@0.6.0-beta.3 -g`
## 更新日志
### 4.2
`2017-04-28`
-   修改user相关API使用`Restful`风格
-   增加user页面多条件查询[#266](https://github.com/zuiidea/antd-admin/issues/226)
-   修复菜单默认高亮[#201](https://github.com/zuiidea/antd-admin/issues/201)
`2017-04-21`
-   重写Menu,Bread组件及配置文件,[说明](https://github.com/zuiidea/antd-admin/wiki/%E8%8F%9C%E5%8D%95%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6)
### 4.1
`2017-04-14`
+4 -10
View File
@@ -7,6 +7,7 @@
"dva": "^1.1.0",
"dva-loading": "^0.2.0",
"lodash": "^4.17.4",
"nprogress": "^0.2.0",
"qs": "^6.2.0",
"react": "^15.4.1",
"react-countup": "1.3.0",
@@ -34,6 +35,7 @@
"jsonp": "^0.2.1",
"less-vars-to-js": "^1.1.2",
"mockjs": "^1.0.1-beta3",
"path-to-regexp": "^1.7.0",
"rc-tween-one": "^1.0.0",
"redbox-react": "^1.2.10"
},
@@ -41,16 +43,8 @@
"lint"
],
"scripts": {
"dev": "set BROWSER=none&&roadhog server",
"dev": "set BROWSER=none&&set HOST=0.0.0.0&&roadhog server",
"lint": "eslint --fix --ext .js src",
"build": "roadhog build"
},
"theme": "./src/theme.js",
"standard": {
"parser": "babel-eslint",
"globals": [
"location",
"localStorage"
]
"build": "roadhog build --analyze"
}
}
+2 -1
View File
@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import { TweenOneGroup } from 'rc-tween-one'
const enterAnim = [
+2 -1
View File
@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import { Table } from 'antd'
import { request } from '../../utils'
import lodash from 'lodash'
+2 -1
View File
@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import { Dropdown, Button, Icon, Menu } from 'antd'
const DropOption = ({ onMenuClick, menuOptions = [], buttonStyle, dropdownProps }) => {
+29
View File
@@ -0,0 +1,29 @@
import React from 'react'
import PropTypes from 'prop-types'
import styles from './FilterItem.less'
const FilterItem = ({
label = '',
children,
}) => {
const labelArray = label.split('')
return (
<div className={styles.filterItem}>
{labelArray.length > 0
? <div className={styles.labelWrap}>
{labelArray.map((item, index) => <span className="labelText" key={index}>{item}</span>)}
</div>
: ''}
<div className={styles.item}>
{children}
</div>
</div>
)
}
FilterItem.propTypes = {
label: PropTypes.string,
children: PropTypes.element.isRequired,
}
export default FilterItem
+17
View File
@@ -0,0 +1,17 @@
.filterItem {
display: flex;
justify-content: space-between;
.labelWrap {
width: 64px;
line-height: 28px;
margin-right: 12px;
justify-content: space-between;
display: flex;
overflow: hidden;
}
.item {
flex: 1;
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"name": "FilterItem",
"version": "0.0.0",
"private": true,
"main": "./FilterItem.js"
}
+2 -1
View File
@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import './iconfont.less'
const Iconfont = ({ type }) => <span
+36 -39
View File
@@ -1,47 +1,50 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import { Breadcrumb, Icon } from 'antd'
import { Link } from 'dva/router'
import styles from './Bread.less'
import { menu } from '../../utils'
import pathToRegexp from 'path-to-regexp'
import { queryArray } from '../../utils'
let pathSet = []
const getPathSet = function (menuArray, parentPath) {
parentPath = parentPath || '/'
menuArray.forEach(item => {
pathSet[(parentPath + item.key).replace(/\//g, '-').hyphenToHump()] = {
path: parentPath + item.key,
name: item.name,
icon: item.icon || '',
clickable: item.clickable === undefined,
const Bread = ({ menu }) => {
// 匹配当前路由
let pathArray = []
let current
for (let index in menu) {
if (menu[index].router && pathToRegexp(menu[index].router).exec(location.pathname)) {
current = menu[index]
break
}
if (item.child) {
getPathSet(item.child, `${parentPath}${item.key}/`)
}
})
}
getPathSet(menu)
}
function Bread ({ location }) {
let pathNames = []
location.pathname.substr(1).split('/').forEach((item, key) => {
if (key > 0) {
pathNames.push((`${pathNames[key - 1]}-${item}`).hyphenToHump())
} else {
pathNames.push((`-${item}`).hyphenToHump())
const getPathArray = (item) => {
pathArray.unshift(item)
if (item.bpid) {
getPathArray(queryArray(menu, item.bpid, 'id'))
}
})
}
const breadsArray = pathNames.filter(item => (item in pathSet))
const breads = breadsArray.map((item, key) => {
if (!current) {
pathArray.push(menu[0])
pathArray.push({
id: 404,
name: 'Not Found',
})
} else {
getPathArray(current)
}
// 递归查找父级
const breads = pathArray.map((item, key) => {
const content = (
<span>{pathSet[item].icon
? <Icon type={pathSet[item].icon} style={{ marginRight: 4 }} />
: ''}{pathSet[item].name}</span>
<span>{item.icon
? <Icon type={item.icon} style={{ marginRight: 4 }} />
: ''}{item.name}</span>
)
return (
<Breadcrumb.Item key={key}>
{((breadsArray.length - 1) !== key && pathSet[item].clickable)
? <Link to={pathSet[item].path}>
{((pathArray.length - 1) !== key)
? <Link to={item.router}>
{content}
</Link>
: content}
@@ -52,12 +55,6 @@ function Bread ({ location }) {
return (
<div className={styles.bread}>
<Breadcrumb>
<Breadcrumb.Item >
<Link to="/dashboard">
<Icon type="home" />
<span>Home</span>
</Link>
</Breadcrumb.Item>
{breads}
</Breadcrumb>
</div>
@@ -65,7 +62,7 @@ function Bread ({ location }) {
}
Bread.propTypes = {
location: PropTypes.object,
menu: PropTypes.array,
}
export default Bread
+6 -3
View File
@@ -1,13 +1,15 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import { Menu, Icon, Popover } from 'antd'
import styles from './Header.less'
import Menus from './Menu'
const SubMenu = Menu.SubMenu
function Header ({ user, logout, switchSider, siderFold, isNavbar, menuPopoverVisible, location, switchMenuPopover, navOpenKeys, changeOpenKeys }) {
function Header ({ user, logout, switchSider, siderFold, isNavbar, menuPopoverVisible, location, switchMenuPopover, navOpenKeys, changeOpenKeys, menu }) {
let handleClickMenu = e => e.key === 'logout' && logout()
const menusProps = {
menu,
siderFold: false,
darkTheme: false,
isNavbar,
@@ -38,7 +40,7 @@ function Header ({ user, logout, switchSider, siderFold, isNavbar, menuPopoverVi
{user.username} < /span>}
>
<Menu.Item key="logout">
<a>注销</a>
Sign out
</Menu.Item>
</SubMenu>
</Menu>
@@ -48,6 +50,7 @@ function Header ({ user, logout, switchSider, siderFold, isNavbar, menuPopoverVi
}
Header.propTypes = {
menu: PropTypes.array,
user: PropTypes.object,
logout: PropTypes.func,
switchSider: PropTypes.func,
+77 -27
View File
@@ -1,36 +1,60 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import { Menu, Icon } from 'antd'
import { Link } from 'dva/router'
import { menu } from '../../utils'
import { arrayToTree, queryArray } from '../../utils'
import pathToRegexp from 'path-to-regexp'
const topMenus = menu.map(item => item.key)
const getMenus = function (menuArray, siderFold, parentPath = '/') {
return menuArray.map(item => {
const linkTo = parentPath + item.key
if (item.child) {
const Menus = ({ siderFold, darkTheme, location, handleClickNavMenu, navOpenKeys, changeOpenKeys, menu }) => {
// 生成树状
const menuTree = arrayToTree(menu.filter(_ => _.mpid !== -1), 'id', 'mpid')
const levelMap = {}
// 递归生成菜单
const getMenus = (menuTreeN, siderFoldN) => {
return menuTreeN.map(item => {
if (item.children) {
if (item.mpid) {
levelMap[item.id] = item.mpid
}
return (
<Menu.SubMenu
key={item.id}
title={<span>
{item.icon && <Icon type={item.icon} />}
{(!siderFoldN || menuTree.indexOf(item) < 0) && item.name}
</span>}
>
{getMenus(item.children, siderFoldN)}
</Menu.SubMenu>
)
}
return (
<Menu.SubMenu key={linkTo} title={<span>{item.icon ? <Icon type={item.icon} /> : ''}{siderFold && topMenus.indexOf(item.key) >= 0 ? '' : item.name}</span>}>
{getMenus(item.child, siderFold, `${linkTo}/`)}
</Menu.SubMenu>
<Menu.Item key={item.id}>
<Link to={item.router}>
{item.icon && <Icon type={item.icon} />}
{(!siderFoldN || menuTree.indexOf(item) < 0) && item.name}
</Link>
</Menu.Item>
)
}
return (
<Menu.Item key={linkTo}>
<Link to={linkTo}>
{item.icon ? <Icon type={item.icon} /> : ''}
{siderFold && topMenus.indexOf(item.key) >= 0 ? '' : item.name}
</Link>
</Menu.Item>
)
})
}
function Menus ({ siderFold, darkTheme, location, handleClickNavMenu, navOpenKeys, changeOpenKeys }) {
const menuItems = getMenus(menu, siderFold)
})
}
const menuItems = getMenus(menuTree, siderFold)
// 保持选中
const getAncestorKeys = (key) => {
const map = {
'/navigation/navigation2': ['/navigation'],
let map = {}
const getParent = (index) => {
const result = [String(levelMap[index])]
if (levelMap[result[0]]) {
result.unshift(getParent(result[0])[0])
}
return result
}
for (let index in levelMap) {
if ({}.hasOwnProperty.call(levelMap, index)) {
map[index] = getParent(index)
}
}
return map[key] || []
}
@@ -53,13 +77,38 @@ function Menus ({ siderFold, darkTheme, location, handleClickNavMenu, navOpenKey
openKeys: navOpenKeys,
} : {}
// 寻找选中路由
let currentMenu
let defaultSelectedKeys
for (let item of menu) {
if (item.router && pathToRegexp(item.router).exec(location.pathname)) {
currentMenu = item
break
}
}
const getPathArray = (array, current, pid, id) => {
let result = [String(current[id])]
const getPath = (item) => {
if (item && item[pid]) {
result.unshift(String(item[pid]))
getPath(queryArray(array, item[pid], id))
}
}
getPath(current)
return result
}
if (currentMenu) {
defaultSelectedKeys = getPathArray(menu, currentMenu, 'mpid', 'id')
}
return (
<Menu
{...menuProps}
mode={siderFold ? 'vertical' : 'inline'}
theme={darkTheme ? 'dark' : 'light'}
onClick={handleClickNavMenu}
defaultSelectedKeys={[location.pathname !== '/' ? location.pathname : '/dashboard']}
defaultSelectedKeys={defaultSelectedKeys}
>
{menuItems}
</Menu>
@@ -67,6 +116,7 @@ function Menus ({ siderFold, darkTheme, location, handleClickNavMenu, navOpenKey
}
Menus.propTypes = {
menu: PropTypes.array,
siderFold: PropTypes.bool,
darkTheme: PropTypes.bool,
location: PropTypes.object,
+7 -4
View File
@@ -1,11 +1,13 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import { Icon, Switch } from 'antd'
import styles from './Layout.less'
import { config } from '../../utils'
import Menus from './Menu'
function Sider ({ siderFold, darkTheme, location, changeTheme, navOpenKeys, changeOpenKeys }) {
function Sider ({ siderFold, darkTheme, location, changeTheme, navOpenKeys, changeOpenKeys, menu }) {
const menusProps = {
menu,
siderFold,
darkTheme,
location,
@@ -20,14 +22,15 @@ function Sider ({ siderFold, darkTheme, location, changeTheme, navOpenKeys, chan
</div>
<Menus {...menusProps} />
{!siderFold ? <div className={styles.switchtheme}>
<span><Icon type="bulb" />切换主题</span>
<Switch onChange={changeTheme} defaultChecked={darkTheme} checkedChildren="黑" unCheckedChildren="白" />
<span><Icon type="bulb" />Switch Theme</span>
<Switch onChange={changeTheme} defaultChecked={darkTheme} checkedChildren="Dark" unCheckedChildren="Light" />
</div> : ''}
</div>
)
}
Sider.propTypes = {
menu: PropTypes.array,
siderFold: PropTypes.bool,
darkTheme: PropTypes.bool,
location: PropTypes.object,
+2 -1
View File
@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import styles from './Search.less'
import { Input, Select, Button, Icon } from 'antd'
+2
View File
@@ -3,6 +3,7 @@ import DropOption from './DropOption'
import Iconfont from './Iconfont'
import Search from './Search'
import Editor from './Editor'
import FilterItem from './FilterItem'
import * as Layout from './Layout/index.js'
import layer from './layer'
@@ -14,5 +15,6 @@ export {
Iconfont,
Search,
Editor,
FilterItem,
layer,
}
+2 -1
View File
@@ -3,13 +3,14 @@ import 'babel-polyfill'
import dva from 'dva'
import createLoading from 'dva-loading'
import { browserHistory } from 'dva/router'
import { message } from 'antd'
// 1. Initialize
const app = dva({
...createLoading(),
history: browserHistory,
onError (error) {
console.error('app onError -- ', error)
message.error(error.message)
},
})
+217
View File
@@ -0,0 +1,217 @@
const qs = require('qs')
const Mock = require('mockjs')
const config = require('../utils/config')
const { apiPrefix } = config
let usersListData = Mock.mock({
'data|80-100': [
{
id: '@id',
name: '@name',
nickName: '@last',
phone: /^1[34578]\d{9}$/,
'age|11-99': 1,
address: '@county(true)',
isMale: '@boolean',
email: '@email',
createTime: '@datetime',
avatar () {
return Mock.Random.image('100x100', Mock.Random.color(), '#757575', 'png', this.nickName.substr(0, 1))
},
},
],
})
let database = usersListData.data
const userPermission = {
DEFAULT: [
'dashboard', 'chart',
],
ADMIN: [
'dashboard', 'users', 'UIElement', 'UIElementIconfont', 'chart',
],
DEVELOPER: ['dashboard', 'users', 'UIElement', 'UIElementIconfont', 'chart'],
}
const adminUsers = [
{
id: 0,
username: 'admin',
password: 'admin',
permissions: userPermission.ADMIN,
}, {
id: 1,
username: 'guest',
password: 'guest',
permissions: userPermission.DEFAULT,
}, {
id: 2,
username: '吴彦祖',
password: '123456',
permissions: userPermission.DEVELOPER,
},
]
const queryArray = (array, key, keyAlias = 'key') => {
if (!(array instanceof Array)) {
return null
}
let data
for (let item of array) {
if (item[keyAlias] === key) {
data = item
break
}
}
if (data) {
return data
}
return null
}
const NOTFOUND = {
message: 'Not Found',
documentation_url: 'http://localhost:8000/request',
}
module.exports = {
[`POST ${apiPrefix}/user/login`] (req, res) {
const { username, password } = req.body
const user = adminUsers.filter((item) => item.username === username)
if (user && user[0].password === password) {
const now = new Date()
now.setDate(now.getDate() + 1)
res.cookie('token', JSON.stringify({ id: user[0].id, deadline: now.getTime() }), {
maxAge: 900000,
httpOnly: true,
})
res.json({ success: true, message: 'Ok' })
} else {
res.status(400).end()
}
},
[`GET ${apiPrefix}/user/logout`] (req, res) {
res.clearCookie('token')
res.status(200).end()
},
[`GET ${apiPrefix}/user`] (req, res) {
const cookie = req.headers.cookie || ''
const cookies = qs.parse(cookie.replace(/\s/g, ''), { delimiter: ';' })
const response = {}
const user = {}
if (!cookies.token) {
res.status(200).send({ message: 'Not Login' })
return
}
const token = JSON.parse(cookies.token)
if (token) {
response.success = token.deadline > new Date().getTime()
}
if (response.success) {
const userItem = adminUsers.filter(_ => _.id === token.id)
if (userItem.length > 0) {
user.permissions = userItem[0].permissions
user.username = userItem[0].username
user.id = userItem[0].id
}
}
response.user = user
res.json(response)
},
[`GET ${apiPrefix}/users`] (req, res) {
const { query } = req
let { pageSize, page, ...other } = query
pageSize = pageSize || 10
page = page || 1
let newData = database
for (let key in other) {
if ({}.hasOwnProperty.call(other, key)) {
newData = newData.filter((item) => {
if ({}.hasOwnProperty.call(item, key)) {
if (key === 'address') {
return other[key].every(iitem => item[key].indexOf(iitem) > -1)
} else if (key === 'createTime') {
const start = new Date(other[key][0]).getTime()
const end = new Date(other[key][1]).getTime()
const now = new Date(item[key]).getTime()
if (start && end) {
return now >= start && now <= end
}
return true
}
return String(item[key]).trim().indexOf(decodeURI(other[key]).trim()) > -1
}
return true
})
}
}
res.status(200).json({
data: newData.slice((page - 1) * pageSize, page * pageSize),
total: newData.length,
})
},
[`POST ${apiPrefix}/user`] (req, res) {
const newData = req.body
newData.createTime = Mock.mock('@now')
newData.avatar = newData.avatar || Mock.Random.image('100x100', Mock.Random.color(), '#757575', 'png', newData.nickName.substr(0, 1))
newData.id = Mock.mock('@id')
database.unshift(newData)
res.status(200).end()
},
[`GET ${apiPrefix}/user/:id`] (req, res) {
const { id } = req.params
const data = queryArray(database, id, 'id')
if (data) {
res.status(200).json(data)
} else {
res.status(404).json(NOTFOUND)
}
},
[`DELETE ${apiPrefix}/user/:id`] (req, res) {
const { id } = req.params
const data = queryArray(database, id, 'id')
if (data) {
database = database.filter((item) => item.id !== id)
res.status(204).end()
} else {
res.status(404).json(NOTFOUND)
}
},
[`PATCH ${apiPrefix}/user/:id`] (req, res) {
const { id } = req.params
const editItem = req.body
let isExist = false
database = database.map((item) => {
if (item.id === id) {
isExist = true
return Object.assign({}, item, editItem)
}
return item
})
if (isExist) {
res.status(201).end()
} else {
res.status(404).json(NOTFOUND)
}
},
}
-186
View File
@@ -1,186 +0,0 @@
const qs = require('qs')
const Mock = require('mockjs')
const config = require('../utils/config')
const { apiPrefix } = config
let usersListData = Mock.mock({
'data|100': [
{
'id|+1': 1,
name: '@cname',
nickName: '@last',
phone: /^1[34578]\d{9}$/,
'age|11-99': 1,
address: '@county(true)',
isMale: '@boolean',
email: '@email',
createTime: '@datetime',
avatar () {
return Mock.Random.image('100x100', Mock.Random.color(), '#757575', 'png', this.nickName.substr(0, 1))
},
},
],
page: {
total: 100,
current: 1,
},
})
const userPermission = {
DEFAULT: [
'dashboard', 'chart',
],
ADMIN: [
'dashboard', 'users', 'UIElement', 'UIElementIconfont', 'chart',
],
DEVELOPER: ['dashboard', 'users', 'UIElement', 'UIElementIconfont', 'chart'],
}
const adminUsers = [
{
id: 0,
username: 'admin',
password: 'admin',
permissions: userPermission.ADMIN,
}, {
id: 1,
username: 'guest',
password: 'guest',
permissions: userPermission.DEFAULT,
}, {
id: 2,
username: '吴彦祖',
password: '123456',
permissions: userPermission.DEVELOPER,
},
]
module.exports = {
[`POST ${apiPrefix}/user/login`] (req, res) {
const { username, password } = req.body
const user = adminUsers.filter((item) => item.username === username)
if (user && user[0].password === password) {
const now = new Date()
now.setDate(now.getDate() + 1)
res.cookie('token', JSON.stringify({ id: user[0].id, deadline: now.getTime() }), {
maxAge: 900000,
httpOnly: true,
})
res.json({ success: true, message: 'Ok' })
} else {
res.status(400).send({ message: 'Bad Request' })
}
},
[`GET ${apiPrefix}/user/logout`] (req, res) {
res.clearCookie('token')
res.status(200).send({ message: 'OK' })
},
[`GET ${apiPrefix}/userInfo`] (req, res) {
const cookies = qs.parse(req.headers.cookie, { delimiter: ';' })
const response = {}
const user = {}
if (!cookies.token) {
res.status(200).send({ message: 'Not Login' })
return
}
const token = JSON.parse(cookies.token)
if (token) {
response.success = token.deadline > new Date().getTime()
}
if (response.success) {
const userItem = adminUsers.filter(_ => _.id === token.id)
if (userItem.length > 0) {
user.permissions = userItem[0].permissions
user.username = userItem[0].username
user.id = userItem[0].id
}
}
response.user = user
res.json(response)
},
[`GET ${apiPrefix}/users`] (req, res) {
const page = req.query
const pageSize = page.pageSize || 10
const currentPage = page.page || 1
let data
let newPage
let newData = usersListData.data.concat()
if (page.field) {
const d = newData.filter((item) => {
return item[page.field].indexOf(decodeURI(page.keyword)) > -1
})
data = d.slice((currentPage - 1) * pageSize, currentPage * pageSize)
newPage = {
current: currentPage * 1,
total: d.length,
}
} else {
data = usersListData.data.slice((currentPage - 1) * pageSize, currentPage * pageSize)
usersListData.page.current = currentPage * 1
newPage = usersListData.page
}
res.json({
success: true,
data,
page: {
...newPage,
pageSize: Number(pageSize),
},
})
},
[`POST ${apiPrefix}/users`] (req, res) {
const newData = req.body
newData.createTime = Mock.mock('@now')
newData.avatar = Mock.Random.image('100x100', Mock.Random.color(), '#757575', 'png', newData.nickName.substr(0, 1))
newData.id = usersListData.data.length + 1
usersListData.data.unshift(newData)
usersListData.page.total = usersListData.data.length
usersListData.page.current = 1
res.json({ success: true, data: usersListData.data, page: usersListData.page })
},
[`DELETE ${apiPrefix}/users`] (req, res) {
const deleteItem = req.body
usersListData.data = usersListData.data.filter((item) => {
if (item.id === deleteItem.id) {
return false
}
return true
})
usersListData.page.total = usersListData.data.length
res.json({ success: true, data: usersListData.data, page: usersListData.page })
},
[`PUT ${apiPrefix}/users`] (req, res) {
const editItem = req.body
editItem.createTime = Mock.mock('@now')
editItem.avatar = Mock.Random.image('100x100', Mock.Random.color(), '#757575', 'png', editItem.nickName.substr(0, 1))
usersListData.data = usersListData.data.map((item) => {
if (item.id === editItem.id) {
return editItem
}
return item
})
res.json({ success: true, data: usersListData.data, page: usersListData.page })
},
}
+3 -4
View File
@@ -1,4 +1,4 @@
import { getUserInfo, logout } from '../services/app'
import { query, logout } from '../services/app'
import { routerRedux } from 'dva/router'
import { parse } from 'qs'
import { config } from '../utils'
@@ -13,7 +13,7 @@ export default {
siderFold: localStorage.getItem(`${prefix}siderFold`) === 'true',
darkTheme: localStorage.getItem(`${prefix}darkTheme`) === 'true',
isNavbar: document.body.clientWidth < 769,
navOpenKeys: [],
navOpenKeys: JSON.parse(localStorage.getItem(`${prefix}navOpenKeys`)) || [],
},
subscriptions: {
setup ({ dispatch }) {
@@ -27,7 +27,7 @@ export default {
*queryUser ({
payload,
}, { call, put }) {
const data = yield call(getUserInfo, parse(payload))
const data = yield call(query, parse(payload))
if (data.success && data.user) {
yield put({
type: 'queryUserSuccess',
@@ -133,7 +133,6 @@ export default {
}
},
handleNavOpenKeys (state, { payload: navOpenKeys }) {
console.log(navOpenKeys)
return {
...state,
...navOpenKeys,
+2 -1
View File
@@ -79,8 +79,9 @@ let zuimei = {
return name
},
getWeatherByCode (num) {
getWeatherByCode (number) {
let wea = ''
let num = Number(number)
if (num === 0) {
wea = '晴'
} else if (num === 1) {
+30 -39
View File
@@ -1,9 +1,11 @@
import { create, remove, update, query } from '../services/users'
import { create, remove, update } from '../services/user'
import { query } from '../services/users'
import { routerRedux } from 'dva/router'
import { parse } from 'qs'
export default {
namespace: 'users',
namespace: 'user',
state: {
list: [],
@@ -23,7 +25,7 @@ export default {
subscriptions: {
setup ({ dispatch, history }) {
history.listen(location => {
if (location.pathname === '/users') {
if (location.pathname === '/user') {
dispatch({
type: 'query',
payload: location.query,
@@ -34,65 +36,54 @@ export default {
},
effects: {
*requery ({ payload }, { put }) {
yield put(routerRedux.push({
pathname: location.pathname,
query: parse(location.search.substr(1)),
}))
},
*query ({ payload }, { call, put }) {
const data = yield call(query, parse(payload))
const data = yield call(query, payload)
if (data) {
yield put({
type: 'querySuccess',
payload: {
list: data.data,
pagination: data.page,
pagination: {
current: Number(payload.page) || 1,
pageSize: Number(payload.pageSize) || 10,
total: data.total,
},
},
})
}
},
*'delete' ({ payload }, { call, put }) {
const data = yield call(remove, { id: payload })
if (data && data.success) {
yield put({
type: 'querySuccess',
payload: {
list: data.data,
pagination: {
total: data.page.total,
current: data.page.current,
},
},
})
if (data.success) {
yield put({ type: 'requery' })
} else {
throw data
}
},
*create ({ payload }, { call, put }) {
yield put({ type: 'hideModal' })
const data = yield call(create, payload)
if (data && data.success) {
yield put({
type: 'querySuccess',
payload: {
list: data.data,
pagination: {
total: data.page.total,
current: data.page.current,
},
},
})
if (data.success) {
yield put({ type: 'requery' })
} else {
throw data
}
},
*update ({ payload }, { select, call, put }) {
yield put({ type: 'hideModal' })
const id = yield select(({ users }) => users.currentItem.id)
const id = yield select(({ user }) => user.currentItem.id)
const newUser = { ...payload, id }
const data = yield call(update, newUser)
if (data && data.success) {
yield put({
type: 'querySuccess',
payload: {
list: data.data,
pagination: {
total: data.page.total,
current: data.page.current,
},
},
})
if (data.success) {
yield put({ type: 'requery' })
} else {
throw data
}
},
*switchIsMotion ({
+51
View File
@@ -0,0 +1,51 @@
import pathToRegexp from 'path-to-regexp'
import { query } from '../../services/user'
export default {
namespace: 'userDetail',
state: {
data: {},
},
subscriptions: {
setup ({ dispatch, history }) {
history.listen(() => {
const match = pathToRegexp('/user/:id').exec(location.pathname)
if (match) {
dispatch({ type: 'query', payload: { id: match[1] } })
}
})
},
},
effects: {
*query ({
payload,
}, { call, put }) {
const data = yield call(query, payload)
const { success, message, status, ...other } = data
if (success) {
yield put({
type: 'querySuccess',
payload: {
data: other,
},
})
} else {
throw data
}
},
},
reducers: {
querySuccess (state, { payload }) {
const { data } = payload
return {
...state,
data,
}
},
},
}
View File
+15 -8
View File
@@ -1,12 +1,11 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import { Router } from 'dva/router'
import App from './routes/app'
const cached = {}
const registerModel = (app, model) => {
if (!cached[model.namespace]) {
if (!(app._models.filter(m => m.namespace === model.namespace).length === 1)) {
app.model(model)
cached[model.namespace] = 1
}
}
@@ -31,12 +30,20 @@ const Routers = function ({ history, app }) {
}, 'dashboard')
},
}, {
path: 'users',
path: 'user',
getComponent (nextState, cb) {
require.ensure([], require => {
registerModel(app, require('./models/users'))
cb(null, require('./routes/users/'))
}, 'users')
registerModel(app, require('./models/user'))
cb(null, require('./routes/user/'))
}, 'user')
},
}, {
path: 'user/:id',
getComponent (nextState, cb) {
require.ensure([], require => {
registerModel(app, require('./models/user/detail'))
cb(null, require('./routes/user/detail/'))
}, 'user-detail')
},
}, {
path: 'login',
+13 -5
View File
@@ -1,9 +1,11 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'dva'
import { Layout } from '../components'
import { classnames, config } from '../utils'
import { classnames, config, menu } from '../utils'
import { Helmet } from 'react-helmet'
import '../themes/index.less'
const { prefix } = config
const { Header, Bread, Footer, Sider, styles } = Layout
@@ -11,6 +13,7 @@ const App = ({ children, location, dispatch, app }) => {
const { user, siderFold, darkTheme, isNavbar, menuPopoverVisible, navOpenKeys } = app
const headerProps = {
menu,
user,
siderFold,
location,
@@ -32,6 +35,7 @@ const App = ({ children, location, dispatch, app }) => {
}
const siderProps = {
menu,
siderFold,
darkTheme,
location,
@@ -40,11 +44,15 @@ const App = ({ children, location, dispatch, app }) => {
dispatch({ type: 'app/changeTheme' })
},
changeOpenKeys (openKeys) {
localStorage.setItem('navOpenKeys', JSON.stringify(openKeys))
localStorage.setItem(`${prefix}navOpenKeys`, JSON.stringify(openKeys))
dispatch({ type: 'app/handleNavOpenKeys', payload: { navOpenKeys: openKeys } })
},
}
const breadProps = {
menu,
}
if (config.openPages && config.openPages.indexOf(location.pathname) > -1) {
return <div>{children}</div>
}
@@ -63,7 +71,7 @@ const App = ({ children, location, dispatch, app }) => {
</aside> : ''}
<div className={styles.main}>
<Header {...headerProps} />
<Bread location={location} />
<Bread {...breadProps} location={location} />
<div className={styles.container}>
<div className={styles.content}>
{children}
@@ -83,4 +91,4 @@ App.propTypes = {
app: PropTypes.object,
}
export default connect(({ app }) => ({ app }))(App)
export default connect(({ app, loading }) => ({ app, loading }))(App)
+2 -1
View File
@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import styles from './Container.less'
import { ResponsiveContainer } from 'recharts'
+2 -1
View File
@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import { Row, Col, Card, Button } from 'antd'
import Container from '../Container'
import {
+2 -1
View File
@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import { Row, Col, Card, Button } from 'antd'
import Container from '../Container'
import {
+2 -1
View File
@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import { Table, Tag } from 'antd'
import styles from './browser.less'
import { color } from '../../../utils'
+2 -1
View File
@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import { Table, Tag } from 'antd'
import styles from './comments.less'
import { color } from '../../../utils'
+2 -1
View File
@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import styles from './completed.less'
import classnames from 'classnames'
import { color } from '../../../utils'
+2 -1
View File
@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import styles from './cpu.less'
import { color } from '../../../utils'
import CountUp from 'react-countup'
@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import { Icon, Card } from 'antd'
import CountUp from 'react-countup'
import styles from './numberCard.less'
+2 -1
View File
@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import styles from './quote.less'
function Quote ({ name, content, title, avatar }) {
@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import { Table, Tag } from 'antd'
import styles from './recentSales.less'
import { color } from '../../../utils'
+2 -1
View File
@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import styles from './sales.less'
import classnames from 'classnames'
import { color } from '../../../utils'
+2 -1
View File
@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import { Button } from 'antd'
import styles from './user.less'
import CountUp from 'react-countup'
+2 -1
View File
@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import styles from './weather.less'
function Weather ({ city, icon, dateTime, temperature, name }) {
+2 -1
View File
@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'dva'
import { Row, Col, Card } from 'antd'
import { NumberCard, Quote, Sales, Weather, RecentSales, Comments, Completed, Browser, Cpu, User } from './components'
+7 -8
View File
@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'dva'
import { Button, Row, Form, Input } from 'antd'
import { config } from '../../utils'
@@ -37,28 +38,26 @@ const Login = ({
rules: [
{
required: true,
message: '请填写用户名',
},
],
})(<Input size="large" onPressEnter={handleOk} placeholder="用户名" />)}
})(<Input size="large" onPressEnter={handleOk} placeholder="Username" />)}
</FormItem>
<FormItem hasFeedback>
{getFieldDecorator('password', {
rules: [
{
required: true,
message: '请填写密码',
},
],
})(<Input size="large" type="password" onPressEnter={handleOk} placeholder="密码" />)}
})(<Input size="large" type="password" onPressEnter={handleOk} placeholder="Password" />)}
</FormItem>
<Row>
<Button type="primary" size="large" onClick={handleOk} loading={loginLoading}>
登录
Sign in
</Button>
<p>
<span>账号:guest</span>
<span>密码:guest</span>
<span>Username:guest</span>
<span>Password:guest</span>
</p>
</Row>
+20 -25
View File
@@ -11,36 +11,42 @@ import {
Button,
} from 'antd'
const { api, baseURL } = config
const { userInfo, dashboard, users, userLogin } = api
const { dashboard, users, userLogin, user } = api
const requestOptions = [
{
url: baseURL + userInfo,
url: baseURL + user.replace('/:id', ''),
desc: 'intercept request by mock.js',
},
{
url: baseURL + dashboard,
desc: 'intercept request by mock.js',
},
{
url: baseURL + users,
desc: 'intercept request by mock.js',
},
{
url: baseURL + userLogin,
method: 'post',
data: {
username: 'admin',
password: 'admin6',
username: 'guest',
password: 'guest',
},
desc: 'intercept request by mock.js',
},
{
url: baseURL + users,
desc: 'intercept request by mock.js',
},
{
url: baseURL + user,
desc: 'intercept request by mock.js',
data: Mock.mock({
id: '@id',
}),
},
{
url: baseURL + user.replace('/:id', ''),
desc: 'intercept request by mock.js',
method: 'post',
data: Mock.mock({
'id|+1': 1000,
name: '@cname',
nickName: '@last',
phone: /^1[34578]\d{9}$/,
@@ -48,37 +54,26 @@ const requestOptions = [
address: '@county(true)',
isMale: '@boolean',
email: '@email',
createTime: '@datetime',
avatar () {
return Mock.Random.image('100x100', Mock.Random.color(), '#757575', 'png', this.nickName.substr(0, 1))
},
}),
},
{
url: baseURL + users,
url: baseURL + user,
desc: 'intercept request by mock.js',
method: 'put',
method: 'patch',
data: Mock.mock({
id: 1,
id: '@id',
name: '@cname',
nickName: '@last',
phone: /^1[34578]\d{9}$/,
'age|11-99': 1,
address: '@county(true)',
isMale: '@boolean',
email: '@email',
createTime: '@datetime',
avatar () {
return Mock.Random.image('100x100', Mock.Random.color(), '#757575', 'png', this.nickName.substr(0, 1))
},
}),
},
{
url: baseURL + users,
url: baseURL + user,
desc: 'intercept request by mock.js',
method: 'delete',
data: Mock.mock({
id: 10,
id: '@id',
}),
},
{
+129
View File
@@ -0,0 +1,129 @@
import React from 'react'
import PropTypes from 'prop-types'
import moment from 'moment'
import { FilterItem } from '../../components'
import { Form, Button, Row, Col, DatePicker, Input, Cascader, Switch } from 'antd'
import city from '../../utils/city'
const Search = Input.Search
const { RangePicker } = DatePicker
const ColProps = {
xs: 24,
sm: 12,
style: {
marginBottom: 16,
},
}
const TwoColProps = {
...ColProps,
xl: 96,
}
const UserFilter = ({
onAdd,
isMotion,
switchIsMotion,
onFilterChange,
filter,
form: {
getFieldDecorator,
getFieldsValue,
setFieldsValue,
},
}) => {
const handleFields = (fields) => {
const { createTime } = fields
if (createTime.length) {
fields.createTime = [createTime[0].format('YYYY-MM-DD'), createTime[1].format('YYYY-MM-DD')]
}
return fields
}
const handleSubmit = () => {
let fields = getFieldsValue()
fields = handleFields(fields)
console.log(fields)
onFilterChange(fields)
}
const handleReset = () => {
const fields = getFieldsValue()
for (let item in fields) {
if ({}.hasOwnProperty.call(fields, item)) {
if (fields[item] instanceof Array) {
fields[item] = []
} else {
fields[item] = undefined
}
}
}
setFieldsValue(fields)
handleSubmit()
}
const handleChange = (key, values) => {
let fields = getFieldsValue()
fields[key] = values
fields = handleFields(fields)
onFilterChange(fields)
}
const { name, address } = filter
let initialCreateTime = []
if (filter.createTime && filter.createTime[0]) {
initialCreateTime[0] = moment(filter.createTime[0])
}
if (filter.createTime && filter.createTime[1]) {
initialCreateTime[1] = moment(filter.createTime[1])
}
return (
<Row gutter={24}>
<Col {...ColProps} xl={{ span: 4 }} md={{ span: 8 }}>
{getFieldDecorator('name', { initialValue: name })(<Search placeholder="Search Name" size="large" onSearch={handleSubmit} />)}
</Col>
<Col {...ColProps} xl={{ span: 4 }} md={{ span: 8 }}>
{getFieldDecorator('address', { initialValue: address })(
<Cascader
size="large"
style={{ width: '100%' }}
options={city}
placeholder="Please pick an address"
onChange={handleChange.bind(null, 'address')}
/>)}
</Col>
<Col {...ColProps} xl={{ span: 6 }} md={{ span: 8 }} sm={{ span: 12 }}>
<FilterItem label="Createtime">
{getFieldDecorator('createTime', { initialValue: initialCreateTime })(
<RangePicker style={{ width: '100%' }} size="large" onChange={handleChange.bind(null, 'createTime')} />
)}
</FilterItem>
</Col>
<Col {...TwoColProps} xl={{ span: 10 }} md={{ span: 24 }} sm={{ span: 24 }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div >
<Button type="primary" size="large" className="margin-right" onClick={handleSubmit}>Search</Button>
<Button size="large" onClick={handleReset}>Reset</Button>
</div>
<div>
<Switch style={{ marginRight: 16 }} size="large" defaultChecked={isMotion} onChange={switchIsMotion} checkedChildren={'Motion'} unCheckedChildren={'Motion'} />
<Button size="large" type="ghost" onClick={onAdd}>Create</Button>
</div>
</div>
</Col>
</Row>
)
}
UserFilter.propTypes = {
onAdd: PropTypes.func,
isMotion: PropTypes.bool,
switchIsMotion: PropTypes.func,
form: PropTypes.object,
filter: PropTypes.object,
onFilterChange: PropTypes.func,
}
export default Form.create()(UserFilter)
@@ -1,10 +1,11 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import { Table, Modal } from 'antd'
import styles from './UserList.less'
import classnames from 'classnames'
import AnimTableBody from '../../components/DataTable/AnimTableBody'
import { DropOption } from '../../components'
import { Link } from 'dva/router'
const confirm = Modal.confirm
@@ -14,7 +15,7 @@ function list ({ loading, dataSource, pagination, onPageChange, onDeleteItem, on
onEditItem(record)
} else if (e.key === '2') {
confirm({
title: '您确定要删除这条记录吗?',
title: 'Are you sure delete this record?',
onOk () {
onDeleteItem(record.id)
},
@@ -24,50 +25,50 @@ function list ({ loading, dataSource, pagination, onPageChange, onDeleteItem, on
const columns = [
{
title: '头像',
title: 'Avatar',
dataIndex: 'avatar',
key: 'avatar',
width: 64,
className: styles.avatar,
render: (text) => <img alt={'avatar'} width={24} src={text} />,
}, {
title: '姓名',
title: 'Name',
dataIndex: 'name',
key: 'name',
render: (text, record) => <Link to={`user/${record.id}`}>{text}</Link>,
}, {
title: '昵称',
title: 'NickName',
dataIndex: 'nickName',
key: 'nickName',
}, {
title: '年龄',
title: 'Age',
dataIndex: 'age',
key: 'age',
render: (text) => <span>{text}岁</span>,
}, {
title: '性别',
title: 'Gender',
dataIndex: 'isMale',
key: 'isMale',
render: (text) => <span>{text
? '男'
: '女'}</span>,
? 'Male'
: 'Female'}</span>,
}, {
title: '电话',
title: 'Phone',
dataIndex: 'phone',
key: 'phone',
}, {
title: '邮箱',
title: 'Email',
dataIndex: 'email',
key: 'email',
}, {
title: '住址',
title: 'Address',
dataIndex: 'address',
key: 'address',
}, {
title: '创建时间',
title: 'CreateTime',
dataIndex: 'createTime',
key: 'createTime',
}, {
title: '操作',
title: 'Operation',
key: 'operation',
width: 100,
render: (text, record) => {
@@ -1,5 +1,8 @@
import React, { PropTypes } from 'react'
import { Form, Input, InputNumber, Radio, Modal } from 'antd'
import React from 'react'
import PropTypes from 'prop-types'
import { Form, Input, InputNumber, Radio, Modal, Cascader } from 'antd'
import city from '../../utils/city'
const FormItem = Form.Item
const formItemLayout = {
@@ -32,12 +35,13 @@ const modal = ({
...getFieldsValue(),
key: item.key,
}
data.address = data.address.join(' ')
onOk(data)
})
}
const modalOpts = {
title: `${type === 'create' ? '新建用户' : '修改用户'}`,
title: `${type === 'create' ? 'Create User' : 'Update User'}`,
visible,
onOk: handleOk,
onCancel,
@@ -47,89 +51,89 @@ const modal = ({
return (
<Modal {...modalOpts}>
<Form layout="horizontal">
<FormItem label="姓名:" hasFeedback {...formItemLayout}>
<FormItem label="Name" hasFeedback {...formItemLayout}>
{getFieldDecorator('name', {
initialValue: item.name,
rules: [
{
required: true,
message: '姓名未填写',
},
],
})(<Input />)}
</FormItem>
<FormItem label="昵称:" hasFeedback {...formItemLayout}>
<FormItem label="NickName" hasFeedback {...formItemLayout}>
{getFieldDecorator('nickName', {
initialValue: item.nickName,
rules: [
{
required: true,
message: '昵称未填写',
},
],
})(<Input />)}
</FormItem>
<FormItem label="性别" hasFeedback {...formItemLayout}>
<FormItem label="Gender" hasFeedback {...formItemLayout}>
{getFieldDecorator('isMale', {
initialValue: item.isMale,
rules: [
{
required: true,
type: 'boolean',
message: '请选择性别',
},
],
})(
<Radio.Group>
<Radio value>男</Radio>
<Radio value={false}>女</Radio>
<Radio value>Male</Radio>
<Radio value={false}>Female</Radio>
</Radio.Group>
)}
</FormItem>
<FormItem label="年龄:" hasFeedback {...formItemLayout}>
<FormItem label="Age" hasFeedback {...formItemLayout}>
{getFieldDecorator('age', {
initialValue: item.age,
rules: [
{
required: true,
type: 'number',
message: '年龄未填写',
},
],
})(<InputNumber min={18} max={100} />)}
</FormItem>
<FormItem label="电话:" hasFeedback {...formItemLayout}>
<FormItem label="Phone" hasFeedback {...formItemLayout}>
{getFieldDecorator('phone', {
initialValue: item.phone,
rules: [
{
required: true,
message: '不能为空',
pattern: /^1[34578]\d{9}$/,
},
],
})(<Input />)}
</FormItem>
<FormItem label="邮箱:" hasFeedback {...formItemLayout}>
<FormItem label="Email" hasFeedback {...formItemLayout}>
{getFieldDecorator('email', {
initialValue: item.email,
rules: [
{
required: true,
message: '不能为空',
pattern: /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(.[a-zA-Z0-9_-])+/,
},
],
})(<Input />)}
</FormItem>
<FormItem label="住址:" hasFeedback {...formItemLayout}>
<FormItem label="Address" hasFeedback {...formItemLayout}>
{getFieldDecorator('address', {
initialValue: item.address,
initialValue: item.address && item.address.split(' '),
rules: [
{
required: true,
message: '不能为空',
},
],
})(<Input />)}
})(<Cascader
size="large"
style={{ width: '100%' }}
options={city}
placeholder="Pick an address"
/>)}
</FormItem>
</Form>
</Modal>
+29
View File
@@ -0,0 +1,29 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'dva'
import styles from './index.less'
const Detail = ({ userDetail }) => {
const { data } = userDetail
const content = []
for (let key in data) {
if ({}.hasOwnProperty.call(data, key)) {
content.push(<div key={key} className={styles.item}>
<div>{key}</div>
<div>{String(data[key])}</div>
</div>)
}
}
return (<div className="content-inner">
<div className={styles.content}>
{content}
</div>
</div>)
}
Detail.propTypes = {
userDetail: PropTypes.object,
loading: PropTypes.bool,
}
export default connect(({ userDetail, loading }) => ({ userDetail, loading: loading.models.userDetail }))(Detail)
+14
View File
@@ -0,0 +1,14 @@
.content {
line-height: 2.4;
font-size: 13px;
.item {
display: flex;
& > div {
&:first-child {
width: 100px;
}
}
}
}
@@ -1,13 +1,14 @@
import React, { PropTypes } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import { routerRedux } from 'dva/router'
import { connect } from 'dva'
import UserList from './UserList'
import UserFilter from './UserFilter'
import UserModal from './UserModal'
function Users ({ location, dispatch, users, loading }) {
const { list, pagination, currentItem, modalVisible, modalType, isMotion } = users
const { field, keyword } = location.query
const User = ({ location, dispatch, user, loading }) => {
const { list, pagination, currentItem, modalVisible, modalType, isMotion } = user
const { pageSize } = pagination
const userModalProps = {
item: modalType === 'create' ? {} : currentItem,
@@ -15,13 +16,13 @@ function Users ({ location, dispatch, users, loading }) {
visible: modalVisible,
onOk (data) {
dispatch({
type: `users/${modalType}`,
type: `user/${modalType}`,
payload: data,
})
},
onCancel () {
dispatch({
type: 'users/hideModal',
type: 'user/hideModal',
})
},
}
@@ -45,13 +46,13 @@ function Users ({ location, dispatch, users, loading }) {
},
onDeleteItem (id) {
dispatch({
type: 'users/delete',
type: 'user/delete',
payload: id,
})
},
onEditItem (item) {
dispatch({
type: 'users/showModal',
type: 'user/showModal',
payload: {
modalType: 'update',
currentItem: item,
@@ -61,30 +62,41 @@ function Users ({ location, dispatch, users, loading }) {
}
const userFilterProps = {
field,
keyword,
isMotion,
filter: {
...location.query,
},
onFilterChange (value) {
dispatch(routerRedux.push({
pathname: location.pathname,
query: {
...value,
page: 1,
pageSize,
},
}))
},
onSearch (fieldsValue) {
fieldsValue.keyword.length ? dispatch(routerRedux.push({
pathname: '/users',
pathname: '/user',
query: {
field: fieldsValue.field,
keyword: fieldsValue.keyword,
},
})) : dispatch(routerRedux.push({
pathname: '/users',
pathname: '/user',
}))
},
onAdd () {
dispatch({
type: 'users/showModal',
type: 'user/showModal',
payload: {
modalType: 'create',
},
})
},
switchIsMotion () {
dispatch({ type: 'users/switchIsMotion' })
dispatch({ type: 'user/switchIsMotion' })
},
}
@@ -100,11 +112,11 @@ function Users ({ location, dispatch, users, loading }) {
)
}
Users.propTypes = {
users: PropTypes.object,
User.propTypes = {
user: PropTypes.object,
location: PropTypes.object,
dispatch: PropTypes.func,
loading: PropTypes.bool,
}
export default connect(({ users, loading }) => ({ users, loading: loading.models.users }))(Users)
export default connect(({ user, loading }) => ({ user, loading: loading.models.user }))(User)
-49
View File
@@ -1,49 +0,0 @@
import React, { PropTypes } from 'react'
import { Form, Button, Row, Col, Switch } from 'antd'
import { Search } from '../../components'
const UserFilter = ({
field,
keyword,
onSearch,
onAdd,
isMotion,
switchIsMotion,
}) => {
const searchGroupProps = {
field,
keyword,
size: 'large',
select: true,
selectOptions: [{ value: 'name', name: '姓名' }, { value: 'address', name: '地址' }],
selectProps: {
defaultValue: field || 'name',
},
onSearch: (value) => {
onSearch(value)
},
}
return (
<Row gutter={24}>
<Col lg={8} md={12} sm={16} xs={24} style={{ marginBottom: 16 }}>
<Search {...searchGroupProps} />
</Col>
<Col lg={{ offset: 8, span: 8 }} md={12} sm={8} xs={24} style={{ marginBottom: 16, textAlign: 'right' }}>
{/* <Switch style={{ marginRight: 16 }} defaultChecked={isMotion} onChange={switchIsMotion} checkedChildren={'动画开'} unCheckedChildren={'动画关'} /> */}
<Button size="large" type="ghost" onClick={onAdd}>添加</Button>
</Col>
</Row>
)
}
UserFilter.propTypes = {
form: PropTypes.object.isRequired,
onSearch: PropTypes.func,
onAdd: PropTypes.func,
field: PropTypes.string,
keyword: PropTypes.string,
isMotion: PropTypes.bool,
switchIsMotion: PropTypes.func,
}
export default Form.create()(UserFilter)
+4 -4
View File
@@ -1,10 +1,10 @@
import { request, config } from '../utils'
const { api } = config
const { userInfo, userLogout } = api
const { user, userLogout, userLogin } = api
export async function login (params) {
return request({
url: '/api/login',
url: userLogin,
method: 'post',
data: params,
})
@@ -18,9 +18,9 @@ export async function logout (params) {
})
}
export async function getUserInfo (params) {
export async function query (params) {
return request({
url: userInfo,
url: user.replace('/:id', ''),
method: 'get',
data: params,
})
+35
View File
@@ -0,0 +1,35 @@
import { request, config } from '../utils'
const { api } = config
const { user } = api
export async function query (params) {
return request({
url: user,
method: 'get',
data: params,
})
}
export async function create (params) {
return request({
url: user.replace('/:id', ''),
method: 'post',
data: params,
})
}
export async function remove (params) {
return request({
url: user,
method: 'delete',
data: params,
})
}
export async function update (params) {
return request({
url: user,
method: 'patch',
data: params,
})
}
-24
View File
@@ -9,27 +9,3 @@ export async function query (params) {
data: params,
})
}
export async function create (params) {
return request({
url: users,
method: 'post',
data: params,
})
}
export async function remove (params) {
return request({
url: users,
method: 'delete',
data: params,
})
}
export async function update (params) {
return request({
url: users,
method: 'put',
data: params,
})
}
+7 -14
View File
@@ -113,21 +113,14 @@ body {
}
}
@media (min-width: 1600px) {
.ant-col-xl-48 {
width: 20%;
}
:global {
.ant-col-xl-48 {
width: 20%;
}
.ant-col-xl-96 {
width: 40%;
}
}
@media (min-width: 1600px) {
.ant-col-xl-48 {
width: 20%;
}
.ant-col-xl-96 {
width: 40%;
.ant-col-xl-96 {
width: 40%;
}
}
}
@media (max-width: 767px) {
+4073
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -1,12 +1,12 @@
module.exports = {
name: 'AntD Admin',
prefix: 'antdAdmin',
footerText: 'Ant Design Admin 版权所有 © 2016 由 zuiidea 支持',
footerText: 'Ant Design Admin © 2017 zuiidea',
logo: 'https://t.alipayobjects.com/images/T1QUBfXo4fXXXXXXXX.png',
iconFontUrl: '//at.alicdn.com/t/font_c4y7asse3q1cq5mi.js',
baseURL: 'http://localhost:8000/api/v1',
YQL: ['http://www.zuimeitianqi.com'],
CORS: ['http://localhost:7001'],
CORS: ['http://localhost:7001', 'http://192.168.1.110:8000'],
openPages: ['/login'],
apiPrefix: '/api/v1',
api: {
@@ -14,6 +14,7 @@ module.exports = {
userLogout: '/user/logout',
userInfo: '/userInfo',
users: '/users',
user: '/user/:id',
dashboard: '/dashboard',
},
}
+49
View File
@@ -3,6 +3,7 @@ import menu from './menu'
import request from './request'
import classnames from 'classnames'
import { color } from './theme'
import lodash from 'lodash'
// 连字符转驼峰
String.prototype.hyphenToHump = function () {
@@ -52,6 +53,52 @@ const queryURL = (name) => {
return null
}
/**
* 数组内查询
* @param {array} array
* @param {String} id
* @param {String} keyAlias
* @return {Array}
*/
const queryArray = (array, key, keyAlias = 'key') => {
if (!(array instanceof Array)) {
return null
}
const item = array.filter(_ => _[keyAlias] === key)
if (item.length) {
return item[0]
}
return null
}
/**
* 数组格式转树状结构
* @param {array} array
* @param {String} id
* @param {String} pid
* @param {String} children
* @return {Array}
*/
const arrayToTree = (array, id = 'id', pid = 'pid', children = 'children') => {
let data = lodash.cloneDeep(array)
let result = []
let hash = {}
data.forEach((item, index) => {
hash[data[index][id]] = data[index]
})
data.forEach((item) => {
let hashVP = hash[item[pid]]
if (hashVP) {
!hashVP[children] && (hashVP[children] = [])
hashVP[children].push(item)
} else {
result.push(item)
}
})
return result
}
module.exports = {
config,
menu,
@@ -59,4 +106,6 @@ module.exports = {
color,
classnames,
queryURL,
queryArray,
arrayToTree,
}
+122 -77
View File
@@ -1,102 +1,147 @@
module.exports = [
{
key: 'dashboard',
name: 'Dashboard',
id: 1,
icon: 'laptop',
name: 'Dashboard',
router: '/dashboard',
},
{
key: 'users',
id: 2,
bpid: 1,
name: 'User Manage',
icon: 'user',
router: '/user',
},
{
key: 'request',
id: 21,
mpid: -1,
bpid: 2,
name: 'User Detail',
router: '/user/:id',
},
{
id: 3,
bpid: 1,
name: 'Request',
icon: 'api',
router: '/request',
},
{
key: 'UIElement',
id: 4,
bpid: 1,
name: 'UI Element',
icon: 'camera-o',
clickable: false,
child: [
{
key: 'iconfont',
name: 'IconFont',
icon: 'heart-o',
},
{
key: 'dataTable',
name: 'DataTable',
icon: 'database',
},
{
key: 'dropOption',
name: 'DropOption',
icon: 'bars',
},
{
key: 'search',
name: 'Search',
icon: 'search',
},
{
key: 'editor',
name: 'Editor',
icon: 'edit',
},
{
key: 'layer',
name: 'layer (Function)',
icon: 'credit-card',
},
],
},
{
key: 'chart',
id: 41,
bpid: 4,
mpid: 4,
name: 'IconFont',
icon: 'heart-o',
router: '/UIElement/iconfont',
},
{
id: 42,
bpid: 4,
mpid: 4,
name: 'DataTable',
icon: 'database',
router: '/UIElement/dataTable',
},
{
id: 43,
bpid: 4,
mpid: 4,
name: 'DropOption',
icon: 'bars',
router: '/UIElement/dropOption',
},
{
id: 44,
bpid: 4,
mpid: 4,
name: 'Search',
icon: 'search',
router: '/UIElement/search',
},
{
id: 45,
bpid: 4,
mpid: 4,
name: 'Editor',
icon: 'edit',
router: '/UIElement/editor',
},
{
id: 46,
bpid: 4,
mpid: 4,
name: 'layer (Function)',
icon: 'credit-card',
router: '/UIElement/layer',
},
{
id: 5,
bpid: 1,
name: 'Recharts',
icon: 'code-o',
child: [
{
key: 'lineChart',
name: 'LineChart',
icon: 'line-chart',
},
{
key: 'barChart',
name: 'BarChart',
icon: 'bar-chart',
},
{
key: 'areaChart',
name: 'AreaChart',
icon: 'area-chart',
},
],
},
{
key: 'navigation',
id: 51,
bpid: 5,
mpid: 5,
name: 'LineChart',
icon: 'line-chart',
router: '/chart/lineChart',
},
{
id: 52,
bpid: 5,
mpid: 5,
name: 'BarChart',
icon: 'bar-chart',
router: '/chart/barChart',
},
{
id: 53,
bpid: 5,
mpid: 5,
name: 'AreaChart',
icon: 'area-chart',
router: '/chart/areaChart',
},
{
id: 6,
bpid: 1,
name: 'Test Navigation',
icon: 'setting',
child: [
{
key: 'navigation1',
name: 'Test Navigation1',
},
{
key: 'navigation2',
name: 'Test Navigation2',
child: [
{
key: 'navigation21',
name: 'Test Navigation21',
},
{
key: 'navigation22',
name: 'Test Navigation22',
},
],
},
],
},
{
id: 61,
bpid: 6,
mpid: 6,
name: 'Test Navigation1',
router: '/navigation/navigation1',
},
{
id: 62,
bpid: 6,
mpid: 6,
name: 'Test Navigation2',
router: '/navigation/navigation2',
},
{
id: 621,
bpid: 62,
mpid: 62,
name: 'Test Navigation21',
router: '/navigation/navigation2/navigation1',
},
{
id: 622,
bpid: 62,
mpid: 62,
name: 'Test Navigation22',
router: '/navigation/navigation2/navigation2',
},
]
+38 -12
View File
@@ -3,6 +3,8 @@ import qs from 'qs'
import { YQL, CORS, baseURL } from './config'
import jsonp from 'jsonp'
import lodash from 'lodash'
import pathToRegexp from 'path-to-regexp'
import { message } from 'antd'
axios.defaults.baseURL = baseURL
@@ -14,6 +16,26 @@ const fetch = (options) => {
url,
} = options
const cloneData = lodash.cloneDeep(data)
try {
let domin = ''
if (url.match(/[a-zA-z]+:\/\/[^/]*/)) {
domin = url.match(/[a-zA-z]+:\/\/[^/]*/)[0]
url = url.slice(domin.length)
}
const match = pathToRegexp.parse(url)
url = pathToRegexp.compile(url)(data)
for (let item of match) {
if (item instanceof Object && item.name in cloneData) {
delete cloneData[item.name]
}
}
url = domin + url
} catch (e) {
message.error(e.message)
}
if (fetchType === 'JSONP') {
return new Promise((resolve, reject) => {
jsonp(url, {
@@ -34,17 +56,19 @@ const fetch = (options) => {
switch (method.toLowerCase()) {
case 'get':
return axios.get(`${url}${!lodash.isEmpty(data) ? `?${qs.stringify(data)}` : ''}`)
return axios.get(url, {
params: cloneData,
})
case 'delete':
return axios.delete(url, { data })
case 'head':
return axios.head(url, data)
return axios.delete(url, {
data: cloneData,
})
case 'post':
return axios.post(url, data)
return axios.post(url, cloneData)
case 'put':
return axios.put(url, data)
return axios.put(url, cloneData)
case 'patch':
return axios.patch(url, data)
return axios.patch(url, cloneData)
default:
return axios(options)
}
@@ -75,16 +99,18 @@ export default function request (options) {
}
}).catch((error) => {
const { response } = error
let message
let msg
let status
let otherData = {}
if (response) {
status = response.status
const { data, statusText } = response
message = data.message || statusText
otherData = data
status = response.status
msg = data.message || statusText
} else {
status = 600
message = 'Network Error'
msg = 'Network Error'
}
return { success: false, status, message }
return { success: false, status, message: msg, ...otherData }
})
}
-50
View File
@@ -1,50 +0,0 @@
const webpack = require('atool-build/lib/webpack')
module.exports = function (webpackConfig, env) {
webpackConfig.babel.plugins.push('transform-runtime')
webpackConfig.babel.plugins.push(['import', {
libraryName: 'antd',
style: true,
}])
// Support hmr
if (env === 'development') {
webpackConfig.devtool = '#eval'
webpackConfig.babel.plugins.push(['dva-hmr', {
entries: [
'./src/index.js',
],
}])
} else {
webpackConfig.babel.plugins.push('dev-expression')
webpackConfig.entry = { index: './src/_index.js' }
}
// Don't extract common.js and common.css
webpackConfig.plugins = webpackConfig.plugins.filter((plugin) => {
return !(plugin instanceof webpack.optimize.CommonsChunkPlugin)
})
// Support CSS Modules
// Parse all less files as css module.
webpackConfig.module.loaders.forEach((loader) => {
if (typeof loader.test === 'function' && loader.test.toString().indexOf('\\.less$') > -1) {
loader.include = /node_modules/
loader.test = /\.less$/
}
if (loader.test.toString() === '/\\.module\\.less$/') {
loader.exclude = /node_modules/
loader.test = /\.less$/
}
if (typeof loader.test === 'function' && loader.test.toString().indexOf('\\.css$') > -1) {
loader.include = /node_modules/
loader.test = /\.css$/
}
if (loader.test.toString() === '/\\.module\\.css$/') {
loader.exclude = /node_modules/
loader.test = /\.css$/
}
})
return webpackConfig
}