Browse Source
* first step * fix local error * commit * test serve * fix test * sort package.json * fix typo * fix pageHeader error * new style * change script name * fix copy block url style * new copy code style * add loginout * auto insert pro code * use new layout * add locale to copy buttonpull/4130/head
committed by
GitHub
24 changed files with 1253 additions and 252 deletions
@ -0,0 +1,129 @@ |
|||
const path = require('path'); |
|||
const fs = require('fs'); |
|||
const fetch = require('node-fetch'); |
|||
const exec = require('child_process').exec; |
|||
const getNewRouteCode = require('./repalceRouter'); |
|||
const router = require('./router.config'); |
|||
const chalk = require('chalk'); |
|||
const insertCode = require('./insertCode'); |
|||
|
|||
const fetchGithubFiles = async () => { |
|||
const ignoreFile = ['_scripts']; |
|||
const data = await fetch(`https://api.github.com/repos/ant-design/pro-blocks/git/trees/master`); |
|||
if (data.status !== 200) { |
|||
return; |
|||
} |
|||
const { tree } = await data.json(); |
|||
const files = tree.filter(file => file.type === 'tree' && !ignoreFile.includes(file.path)); |
|||
return Promise.resolve(files); |
|||
}; |
|||
|
|||
const relativePath = path.join(__dirname, '../config/config.ts'); |
|||
|
|||
const findAllInstallRouter = router => { |
|||
let routers = []; |
|||
router.forEach(item => { |
|||
if (item.component && item.path) { |
|||
if (item.path !== '/user' || item.path !== '/') { |
|||
routers.push({ |
|||
...item, |
|||
routes: !!item.routes, |
|||
}); |
|||
} |
|||
} |
|||
if (item.routes) { |
|||
routers = routers.concat(findAllInstallRouter(item.routes)); |
|||
} |
|||
}); |
|||
return routers; |
|||
}; |
|||
|
|||
const filterParentRouter = (router, layout) => { |
|||
return [...router] |
|||
.map(item => { |
|||
if (item.routes && (!router.component || layout)) { |
|||
return { ...item, routes: filterParentRouter(item.routes, false) }; |
|||
} |
|||
if (item.redirect) { |
|||
return item; |
|||
} |
|||
return null; |
|||
}) |
|||
.filter(item => item); |
|||
}; |
|||
const firstUpperCase = pathString => { |
|||
return pathString |
|||
.replace('.', '') |
|||
.split(/\/|\-/) |
|||
.map(s => s.toLowerCase().replace(/( |^)[a-z]/g, L => L.toUpperCase())) |
|||
.filter(s => s) |
|||
.join(''); |
|||
}; |
|||
|
|||
const execCmd = shell => { |
|||
return new Promise((resolve, reject) => { |
|||
exec(shell, { encoding: 'utf8' }, (error, statusbar) => { |
|||
if (error) { |
|||
console.log(error); |
|||
return reject(error); |
|||
} |
|||
console.log(statusbar); |
|||
resolve(); |
|||
}); |
|||
}); |
|||
}; |
|||
|
|||
// replace router config
|
|||
const parentRouter = filterParentRouter(router, true); |
|||
const { routesPath, code } = getNewRouteCode(relativePath, parentRouter); |
|||
// write ParentRouter
|
|||
fs.writeFileSync(routesPath, code); |
|||
|
|||
const installBlock = async () => { |
|||
let gitFiles = await fetchGithubFiles(); |
|||
const installRouters = findAllInstallRouter(router); |
|||
const installBlockIteration = async i => { |
|||
const item = installRouters[i]; |
|||
|
|||
if (!item || !item.path) { |
|||
return Promise.resolve(); |
|||
} |
|||
const gitPath = firstUpperCase(item.path); |
|||
// 如果这个区块在 git 上存在
|
|||
if (gitFiles.find(file => file.path === gitPath)) { |
|||
console.log('install ' + chalk.green(item.name) + ' to: ' + chalk.yellow(item.path)); |
|||
gitFiles = gitFiles.filter(file => file.path !== gitPath); |
|||
const skipModifyRouter = item.routes ? '--skip-modify-routes' : ''; |
|||
const cmd = `umi block add https://github.com/ant-design/pro-blocks/tree/master/${gitPath} --npm-client=cnpm --path=${ |
|||
item.path |
|||
} ${skipModifyRouter}`;
|
|||
try { |
|||
await execCmd(cmd); |
|||
console.log(`install ${chalk.hex('#1890ff')(item.name)} success`); |
|||
} catch (error) { |
|||
console.error(error); |
|||
} |
|||
} |
|||
return installBlockIteration(i + 1); |
|||
}; |
|||
// 安装路由中设置的区块
|
|||
await installBlockIteration(0); |
|||
|
|||
const installGitFile = async i => { |
|||
const item = gitFiles[i]; |
|||
if (!item || !item.path) { |
|||
return Promise.resolve(); |
|||
} |
|||
console.log('install ' + chalk.green(item.path)); |
|||
const cmd = `umi block add https://github.com/ant-design/pro-blocks/tree/master/${item.path}`; |
|||
await execCmd(cmd); |
|||
return installBlockIteration(1); |
|||
}; |
|||
|
|||
// 安装 router 中没有的剩余区块.
|
|||
installGitFile(0); |
|||
}; |
|||
installBlock(); |
|||
|
|||
// 插入 pro 需要的演示代码
|
|||
insertCode(); |
|||
@ -0,0 +1,161 @@ |
|||
const parser = require('@babel/parser'); |
|||
const traverse = require('@babel/traverse'); |
|||
const generate = require('@babel/generator'); |
|||
const t = require('@babel/types'); |
|||
const fs = require('fs'); |
|||
const path = require('path'); |
|||
const prettier = require('prettier'); |
|||
const chalk = require('chalk'); |
|||
|
|||
const parseCode = code => { |
|||
return parser.parse(code, { |
|||
sourceType: 'module', |
|||
plugins: ['typescript', 'jsx'], |
|||
}).program.body[0]; |
|||
}; |
|||
|
|||
/** |
|||
* 生成代码 |
|||
* @param {*} ast |
|||
*/ |
|||
function generateCode(ast) { |
|||
const newCode = generate.default(ast, {}).code; |
|||
return prettier.format(newCode, { |
|||
// format same as ant-design-pro
|
|||
singleQuote: true, |
|||
trailingComma: 'es5', |
|||
printWidth: 100, |
|||
parser: 'typescript', |
|||
}); |
|||
} |
|||
|
|||
const SettingCodeString = ` |
|||
<SettingDrawer |
|||
settings={settings} |
|||
onSettingChange={config => |
|||
dispatch!({ |
|||
type: 'settings/changeSetting', |
|||
payload: config, |
|||
}) |
|||
} |
|||
/> |
|||
`;
|
|||
|
|||
const mapAst = (configPath, callBack) => { |
|||
const ast = parser.parse(fs.readFileSync(configPath, 'utf-8'), { |
|||
sourceType: 'module', |
|||
plugins: ['typescript', 'jsx'], |
|||
}); |
|||
// 查询当前配置文件是否导出 routes 属性
|
|||
traverse.default(ast, { |
|||
Program({ node }) { |
|||
const { body } = node; |
|||
callBack(body); |
|||
}, |
|||
}); |
|||
return generateCode(ast); |
|||
}; |
|||
|
|||
const insertBasicLayout = configPath => { |
|||
return mapAst(configPath, body => { |
|||
const index = body.findIndex(item => { |
|||
return item.type !== 'ImportDeclaration'; |
|||
}); |
|||
|
|||
body.forEach(item => { |
|||
// 从包中导出 SettingDrawer
|
|||
if (item.type === 'ImportDeclaration') { |
|||
if (item.source.value === '@ant-design/pro-layout') { |
|||
item.specifiers.push(parseCode(`SettingDrawer`).expression); |
|||
} |
|||
} |
|||
if (item.type === 'VariableDeclaration') { |
|||
const { |
|||
id, |
|||
init: { body }, |
|||
} = item.declarations[0]; |
|||
// 给 BasicLayout 中插入 button 和 设置抽屉
|
|||
if (id.name === `BasicLayout`) { |
|||
body.body.forEach(node => { |
|||
if (node.type === 'ReturnStatement') { |
|||
const JSXFragment = parseCode(`<></>`).expression; |
|||
JSXFragment.children.push({ ...node.argument }); |
|||
JSXFragment.children.push(parseCode(SettingCodeString).expression); |
|||
node.argument = JSXFragment; |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
}); |
|||
}); |
|||
}; |
|||
|
|||
const insertBlankLayout = configPath => { |
|||
return mapAst(configPath, body => { |
|||
const index = body.findIndex(item => { |
|||
return item.type !== 'ImportDeclaration'; |
|||
}); |
|||
// 从组件中导入 CopyBlock
|
|||
body.splice( |
|||
index, |
|||
0, |
|||
parseCode(`import CopyBlock from '@/components/CopyBlock';
|
|||
`),
|
|||
); |
|||
body.forEach(item => { |
|||
if (item.type === 'VariableDeclaration') { |
|||
const { id, init } = item.declarations[0]; |
|||
// 给 BasicLayout 中插入 button 和 设置抽屉
|
|||
if (id.name === `Layout`) { |
|||
const JSXFragment = parseCode(`<></>`).expression; |
|||
JSXFragment.children.push({ ...init.body }); |
|||
JSXFragment.children.push(parseCode(` <CopyBlock id={Date.now()}/>`).expression); |
|||
init.body = JSXFragment; |
|||
} |
|||
} |
|||
}); |
|||
}); |
|||
}; |
|||
|
|||
const insertRightContent = configPath => { |
|||
return mapAst(configPath, body => { |
|||
const index = body.findIndex(item => { |
|||
return item.type !== 'ImportDeclaration'; |
|||
}); |
|||
// 从组件中导入 CopyBlock
|
|||
body.splice(index, 0, parseCode(`import NoticeIconView from './NoticeIconView';`)); |
|||
|
|||
body.forEach(item => { |
|||
if (item.type === 'ClassDeclaration') { |
|||
const classBody = item.body.body[0].body; |
|||
classBody.body.forEach(node => { |
|||
if (node.type === 'ReturnStatement') { |
|||
const index = node.argument.children.findIndex(item => { |
|||
if (item.type === 'JSXElement') { |
|||
if (item.openingElement.name.name === 'Avatar') { |
|||
return true; |
|||
} |
|||
} |
|||
}); |
|||
node.argument.children.splice(index, 1, parseCode(`<Avatar menu />`).expression); |
|||
node.argument.children.splice(index, 0, parseCode(`<NoticeIconView />`).expression); |
|||
} |
|||
}); |
|||
} |
|||
}); |
|||
}); |
|||
}; |
|||
|
|||
module.exports = () => { |
|||
const basicLayoutPath = path.join(__dirname, '../src/layouts/BasicLayout.tsx'); |
|||
fs.writeFileSync(basicLayoutPath, insertBasicLayout(basicLayoutPath)); |
|||
console.log(`insert ${chalk.hex('#1890ff')('BasicLayout')} success`); |
|||
|
|||
const rightContentPath = path.join(__dirname, '../src/components/GlobalHeader/RightContent.tsx'); |
|||
fs.writeFileSync(rightContentPath, insertRightContent(rightContentPath)); |
|||
console.log(`insert ${chalk.hex('#1890ff')('RightContent')} success`); |
|||
|
|||
const blankLayoutPath = path.join(__dirname, '../src/layouts/BlankLayout.tsx'); |
|||
fs.writeFileSync(blankLayoutPath, insertBlankLayout(blankLayoutPath)); |
|||
console.log(`insert ${chalk.hex('#1890ff')('blankLayoutPath')} success`); |
|||
}; |
|||
@ -0,0 +1,77 @@ |
|||
const parser = require('@babel/parser'); |
|||
const traverse = require('@babel/traverse'); |
|||
const generate = require('@babel/generator'); |
|||
const t = require('@babel/types'); |
|||
const fs = require('fs'); |
|||
const prettier = require('prettier'); |
|||
|
|||
const getNewRouteCode = (configPath, newRoute) => { |
|||
const ast = parser.parse(fs.readFileSync(configPath, 'utf-8'), { |
|||
sourceType: 'module', |
|||
plugins: ['typescript'], |
|||
}); |
|||
let routesNode = null; |
|||
const importModules = []; |
|||
// 查询当前配置文件是否导出 routes 属性
|
|||
traverse.default(ast, { |
|||
Program({ node }) { |
|||
// find import
|
|||
const { body } = node; |
|||
body.forEach(item => { |
|||
if (t.isImportDeclaration(item)) { |
|||
const { specifiers } = item; |
|||
const defaultEpecifier = specifiers.find(s => { |
|||
return t.isImportDefaultSpecifier(s) && t.isIdentifier(s.local); |
|||
}); |
|||
if (defaultEpecifier && t.isStringLiteral(item.source)) { |
|||
importModules.push({ |
|||
identifierName: defaultEpecifier.local.name, |
|||
modulePath: item.source.value, |
|||
}); |
|||
} |
|||
} |
|||
}); |
|||
}, |
|||
ObjectExpression({ node, parent }) { |
|||
// find routes on object, like { routes: [] }
|
|||
if (t.isArrayExpression(parent)) { |
|||
// children routes
|
|||
return; |
|||
} |
|||
const { properties } = node; |
|||
properties.forEach(p => { |
|||
const { key, value } = p; |
|||
if (t.isObjectProperty(p) && t.isIdentifier(key) && key.name === 'routes') { |
|||
if (value) { |
|||
// find json file program expression
|
|||
(p.value = parser.parse(JSON.stringify(newRoute)).program.body[0].expression), |
|||
(routesNode = value); |
|||
} |
|||
} |
|||
}); |
|||
}, |
|||
}); |
|||
if (routesNode) { |
|||
const code = generateCode(ast); |
|||
return { code, routesPath: configPath }; |
|||
} else { |
|||
throw new Error('route array config not found.'); |
|||
} |
|||
}; |
|||
|
|||
/** |
|||
* 生成代码 |
|||
* @param {*} ast |
|||
*/ |
|||
function generateCode(ast) { |
|||
const newCode = generate.default(ast, {}).code; |
|||
return prettier.format(newCode, { |
|||
// format same as ant-design-pro
|
|||
singleQuote: true, |
|||
trailingComma: 'es5', |
|||
printWidth: 100, |
|||
parser: 'typescript', |
|||
}); |
|||
} |
|||
|
|||
module.exports = getNewRouteCode; |
|||
@ -0,0 +1,236 @@ |
|||
module.exports = [ |
|||
{ |
|||
path: '/', |
|||
component: '../layouts/BlankLayout', |
|||
routes: [ |
|||
// user
|
|||
{ |
|||
path: '/user', |
|||
component: '../layouts/UserLayout', |
|||
routes: [ |
|||
{ path: '/user/login', name: 'login', component: './User/Login' }, |
|||
{ path: '/user/register', name: 'register', component: './User/Register' }, |
|||
{ |
|||
path: '/user/register-result', |
|||
name: 'register.result', |
|||
component: './User/RegisterResult', |
|||
}, |
|||
{ path: '/user', redirect: '/user/login' }, |
|||
{ |
|||
component: '404', |
|||
}, |
|||
], |
|||
}, |
|||
// app
|
|||
{ |
|||
path: '/', |
|||
component: '../layouts/BasicLayout', |
|||
Routes: ['src/pages/Authorized'], |
|||
authority: ['admin', 'user'], |
|||
routes: [ |
|||
// dashboard
|
|||
{ |
|||
path: '/dashboard', |
|||
name: 'dashboard', |
|||
icon: 'dashboard', |
|||
routes: [ |
|||
{ |
|||
path: '/dashboard/analysis', |
|||
name: 'analysis', |
|||
component: './Dashboard/Analysis', |
|||
}, |
|||
{ |
|||
path: '/dashboard/monitor', |
|||
name: 'monitor', |
|||
component: './Dashboard/Monitor', |
|||
}, |
|||
{ |
|||
path: '/dashboard/workplace', |
|||
name: 'workplace', |
|||
component: './Dashboard/Workplace', |
|||
}, |
|||
], |
|||
}, |
|||
// forms
|
|||
{ |
|||
path: '/form', |
|||
icon: 'form', |
|||
name: 'form', |
|||
routes: [ |
|||
{ |
|||
path: '/form/basic-form', |
|||
name: 'basicform', |
|||
component: './Form/BasicForm', |
|||
}, |
|||
{ |
|||
path: '/form/step-form', |
|||
name: 'stepform', |
|||
component: './Form/StepForm', |
|||
}, |
|||
{ |
|||
path: '/form/advanced-form', |
|||
name: 'advancedform', |
|||
authority: ['admin'], |
|||
component: './Form/AdvancedForm', |
|||
}, |
|||
], |
|||
}, |
|||
// list
|
|||
{ |
|||
path: '/list', |
|||
icon: 'table', |
|||
name: 'list', |
|||
routes: [ |
|||
{ |
|||
path: '/list/table-list', |
|||
name: 'searchtable', |
|||
component: './list/Tablelist', |
|||
}, |
|||
{ |
|||
path: '/list/basic-list', |
|||
name: 'basiclist', |
|||
component: './list/Basiclist', |
|||
}, |
|||
{ |
|||
path: '/list/card-list', |
|||
name: 'cardlist', |
|||
component: './list/Cardlist', |
|||
}, |
|||
{ |
|||
path: '/list/search', |
|||
name: 'search-list', |
|||
component: './list/search', |
|||
routes: [ |
|||
{ |
|||
path: '/list/search/articles', |
|||
name: 'articles', |
|||
component: './list/Articles', |
|||
}, |
|||
{ |
|||
path: '/list/search/projects', |
|||
name: 'projects', |
|||
component: './list/Projects', |
|||
}, |
|||
{ |
|||
path: '/list/search/applications', |
|||
name: 'applications', |
|||
component: './list/Applications', |
|||
}, |
|||
{ |
|||
path: '/list/search', |
|||
redirect: '/list/search/articles', |
|||
}, |
|||
], |
|||
}, |
|||
], |
|||
}, |
|||
{ |
|||
path: '/profile', |
|||
name: 'profile', |
|||
icon: 'profile', |
|||
routes: [ |
|||
// profile
|
|||
{ |
|||
path: '/profile/basic', |
|||
name: 'basic', |
|||
component: './Profile/BasicProfile', |
|||
}, |
|||
{ |
|||
path: '/profile/basic/:id', |
|||
hideInMenu: true, |
|||
component: './Profile/BasicProfile', |
|||
}, |
|||
{ |
|||
path: '/profile/advanced', |
|||
name: 'advanced', |
|||
authority: ['admin'], |
|||
component: './Profile/AdvancedProfile', |
|||
}, |
|||
], |
|||
}, |
|||
{ |
|||
name: 'result', |
|||
icon: 'check-circle-o', |
|||
path: '/result', |
|||
routes: [ |
|||
// result
|
|||
{ |
|||
path: '/result/success', |
|||
name: 'success', |
|||
component: './Result/Success', |
|||
}, |
|||
{ path: '/result/fail', name: 'fail', component: './Result/Error' }, |
|||
], |
|||
}, |
|||
{ |
|||
name: 'exception', |
|||
icon: 'warning', |
|||
path: '/exception', |
|||
routes: [ |
|||
// exception
|
|||
{ |
|||
path: '/exception/403', |
|||
name: 'not-permission', |
|||
component: './Exception/403', |
|||
}, |
|||
{ |
|||
path: '/exception/404', |
|||
name: 'not-find', |
|||
component: './Exception/404', |
|||
}, |
|||
{ |
|||
path: '/exception/500', |
|||
name: 'server-error', |
|||
component: './Exception/500', |
|||
}, |
|||
], |
|||
}, |
|||
{ |
|||
name: 'account', |
|||
icon: 'user', |
|||
path: '/account', |
|||
routes: [ |
|||
{ |
|||
path: '/account/center', |
|||
name: 'center', |
|||
component: './Account/Center/Center', |
|||
}, |
|||
{ |
|||
path: '/account/settings', |
|||
name: 'settings', |
|||
component: './Account/Settings/Info', |
|||
}, |
|||
], |
|||
}, |
|||
// editor
|
|||
{ |
|||
name: 'editor', |
|||
icon: 'highlight', |
|||
path: '/editor', |
|||
routes: [ |
|||
{ |
|||
path: '/editor/flow', |
|||
name: 'flow', |
|||
component: './Editor/GGEditor/Flow', |
|||
}, |
|||
{ |
|||
path: '/editor/mind', |
|||
name: 'mind', |
|||
component: './Editor/GGEditor/Mind', |
|||
}, |
|||
{ |
|||
path: '/editor/koni', |
|||
name: 'koni', |
|||
component: './Editor/GGEditor/Koni', |
|||
}, |
|||
], |
|||
}, |
|||
{ path: '/', redirect: '/dashboard/analysis', authority: ['admin', 'user'] }, |
|||
{ |
|||
component: '404', |
|||
}, |
|||
], |
|||
}, |
|||
], |
|||
}, |
|||
]; |
|||
@ -0,0 +1,29 @@ |
|||
.copy-block { |
|||
position: fixed; |
|||
right: 80px; |
|||
bottom: 40px; |
|||
z-index: 99; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
width: 40px; |
|||
height: 40px; |
|||
font-size: 20px; |
|||
background: #fff; |
|||
border-radius: 40px; |
|||
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14), |
|||
0 1px 10px 0 rgba(0, 0, 0, 0.12); |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.copy-block-view { |
|||
position: relative; |
|||
.copy-block-code { |
|||
display: inline-block; |
|||
margin: 0 0.2em; |
|||
padding: 0.2em 0.4em 0.1em; |
|||
font-size: 85%; |
|||
border-radius: 3px; |
|||
} |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
import React from 'react'; |
|||
import { Icon, Typography, Popover } from 'antd'; |
|||
import styles from './index.less'; |
|||
import { connect } from 'dva'; |
|||
import * as H from 'history'; |
|||
import { FormattedMessage } from 'umi-plugin-react/locale'; |
|||
|
|||
const firstUpperCase = (pathString: string) => { |
|||
return pathString |
|||
.replace('.', '') |
|||
.split(/\/|\-/) |
|||
.map(s => s.toLowerCase().replace(/( |^)[a-z]/g, L => L.toUpperCase())) |
|||
.filter(s => s) |
|||
.join(''); |
|||
}; |
|||
const BlockCodeView: React.SFC<{ |
|||
url: string; |
|||
}> = ({ url }) => { |
|||
const blockUrl = `npx umi block add ant-design-pro/${firstUpperCase(url)} --path=${url}`; |
|||
return ( |
|||
<div className={styles['copy-block-view']}> |
|||
<Typography.Paragraph copyable> |
|||
<code className={styles['copy-block-code']}>{blockUrl}</code> |
|||
</Typography.Paragraph> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
type RoutingType = { location: H.Location }; |
|||
|
|||
export default connect(({ routing }: { routing: RoutingType }) => ({ |
|||
location: routing.location, |
|||
}))(({ location }: RoutingType) => { |
|||
const url = location.pathname; |
|||
return ( |
|||
<Popover |
|||
title={<FormattedMessage id="app.preview.down.block" defaultMessage="下载此页面到本地项目" />} |
|||
placement="topLeft" |
|||
content={<BlockCodeView url={url} />} |
|||
trigger="click" |
|||
> |
|||
<div className={styles['copy-block']}> |
|||
<Icon type="download" /> |
|||
</div> |
|||
</Popover> |
|||
); |
|||
}); |
|||
@ -0,0 +1,72 @@ |
|||
import React from 'react'; |
|||
import { Avatar, Menu, Spin, Icon } from 'antd'; |
|||
import { FormattedMessage } from 'umi-plugin-react/locale'; |
|||
import { ClickParam } from 'antd/lib/menu'; |
|||
import { ConnectProps, ConnectState } from '@/models/connect'; |
|||
import { CurrentUser } from '@/models/user'; |
|||
import { connect } from 'dva'; |
|||
import router from 'umi/router'; |
|||
import HeaderDropdown from '../HeaderDropdown'; |
|||
import styles from './index.less'; |
|||
|
|||
export interface GlobalHeaderRightProps extends ConnectProps { |
|||
currentUser?: CurrentUser; |
|||
menu?: boolean; |
|||
} |
|||
|
|||
class AvatarDropdown extends React.Component<GlobalHeaderRightProps> { |
|||
onMenuClick = (event: ClickParam) => { |
|||
const { key } = event; |
|||
|
|||
if (key === 'logout') { |
|||
const { dispatch } = this.props; |
|||
dispatch!({ |
|||
type: 'login/logout', |
|||
}); |
|||
return; |
|||
} |
|||
router.push(`/account/${key}`); |
|||
}; |
|||
render() { |
|||
const { currentUser = {}, menu } = this.props; |
|||
if (!menu) { |
|||
return ( |
|||
<span className={`${styles.action} ${styles.account}`}> |
|||
<Avatar size="small" className={styles.avatar} src={currentUser.avatar} alt="avatar" /> |
|||
<span className={styles.name}>{currentUser.name}</span> |
|||
</span> |
|||
); |
|||
} |
|||
const menuHeaderDropdown = ( |
|||
<Menu className={styles.menu} selectedKeys={[]} onClick={this.onMenuClick}> |
|||
<Menu.Item key="center"> |
|||
<Icon type="user" /> |
|||
<FormattedMessage id="menu.account.center" defaultMessage="account center" /> |
|||
</Menu.Item> |
|||
<Menu.Item key="settings"> |
|||
<Icon type="setting" /> |
|||
<FormattedMessage id="menu.account.settings" defaultMessage="account settings" /> |
|||
</Menu.Item> |
|||
<Menu.Divider /> |
|||
<Menu.Item key="logout"> |
|||
<Icon type="logout" /> |
|||
<FormattedMessage id="menu.account.logout" defaultMessage="logout" /> |
|||
</Menu.Item> |
|||
</Menu> |
|||
); |
|||
|
|||
return currentUser && currentUser.name ? ( |
|||
<HeaderDropdown overlay={menuHeaderDropdown}> |
|||
<span className={`${styles.action} ${styles.account}`}> |
|||
<Avatar size="small" className={styles.avatar} src={currentUser.avatar} alt="avatar" /> |
|||
<span className={styles.name}>{currentUser.name}</span> |
|||
</span> |
|||
</HeaderDropdown> |
|||
) : ( |
|||
<Spin size="small" style={{ marginLeft: 8, marginRight: 8 }} /> |
|||
); |
|||
} |
|||
} |
|||
export default connect(({ user }: ConnectState) => ({ |
|||
currentUser: user.currentUser, |
|||
}))(AvatarDropdown); |
|||
@ -0,0 +1,145 @@ |
|||
import { ConnectProps, ConnectState } from '@/models/connect'; |
|||
import { NoticeItem } from '@/models/global'; |
|||
import { CurrentUser } from '@/models/user'; |
|||
import React, { Component } from 'react'; |
|||
import { Tag, message } from 'antd'; |
|||
import { formatMessage } from 'umi-plugin-react/locale'; |
|||
import moment from 'moment'; |
|||
import groupBy from 'lodash/groupBy'; |
|||
import NoticeIcon from '../NoticeIcon'; |
|||
import styles from './index.less'; |
|||
import { connect } from 'dva'; |
|||
|
|||
export interface GlobalHeaderRightProps extends ConnectProps { |
|||
notices?: NoticeItem[]; |
|||
currentUser?: CurrentUser; |
|||
fetchingNotices?: boolean; |
|||
onNoticeVisibleChange?: (visible: boolean) => void; |
|||
onNoticeClear?: (tabName?: string) => void; |
|||
} |
|||
|
|||
class GlobalHeaderRight extends Component<GlobalHeaderRightProps> { |
|||
getNoticeData = (): { [key: string]: NoticeItem[] } => { |
|||
const { notices = [] } = this.props; |
|||
if (notices.length === 0) { |
|||
return {}; |
|||
} |
|||
const newNotices = notices.map(notice => { |
|||
const newNotice = { ...notice }; |
|||
if (newNotice.datetime) { |
|||
newNotice.datetime = moment(notice.datetime as string).fromNow(); |
|||
} |
|||
if (newNotice.id) { |
|||
newNotice.key = newNotice.id; |
|||
} |
|||
if (newNotice.extra && newNotice.status) { |
|||
const color = { |
|||
todo: '', |
|||
processing: 'blue', |
|||
urgent: 'red', |
|||
doing: 'gold', |
|||
}[newNotice.status]; |
|||
newNotice.extra = ( |
|||
<Tag color={color} style={{ marginRight: 0 }}> |
|||
{newNotice.extra} |
|||
</Tag> |
|||
); |
|||
} |
|||
return newNotice; |
|||
}); |
|||
return groupBy(newNotices, 'type'); |
|||
}; |
|||
|
|||
getUnreadData = (noticeData: { [key: string]: NoticeItem[] }) => { |
|||
const unreadMsg: { [key: string]: number } = {}; |
|||
Object.entries(noticeData).forEach(([key, value]) => { |
|||
if (!unreadMsg[key]) { |
|||
unreadMsg[key] = 0; |
|||
} |
|||
if (Array.isArray(value)) { |
|||
unreadMsg[key] = value.filter(item => !item.read).length; |
|||
} |
|||
}); |
|||
return unreadMsg; |
|||
}; |
|||
|
|||
changeReadState = (clickedItem: NoticeItem) => { |
|||
const { id } = clickedItem; |
|||
const { dispatch } = this.props; |
|||
dispatch!({ |
|||
type: 'global/changeNoticeReadState', |
|||
payload: id, |
|||
}); |
|||
}; |
|||
componentDidMount() { |
|||
const { dispatch } = this.props; |
|||
dispatch!({ |
|||
type: 'global/fetchNotices', |
|||
}); |
|||
} |
|||
handleNoticeClear = (title: string, key: string) => { |
|||
const { dispatch } = this.props; |
|||
message.success(`${formatMessage({ id: 'component.noticeIcon.cleared' })} ${title}`); |
|||
if (dispatch) { |
|||
dispatch({ |
|||
type: 'global/clearNotices', |
|||
payload: key, |
|||
}); |
|||
} |
|||
}; |
|||
render() { |
|||
const { currentUser, fetchingNotices, onNoticeVisibleChange } = this.props; |
|||
const noticeData = this.getNoticeData(); |
|||
const unreadMsg = this.getUnreadData(noticeData); |
|||
|
|||
return ( |
|||
<NoticeIcon |
|||
className={styles.action} |
|||
count={currentUser && currentUser.unreadCount} |
|||
onItemClick={item => { |
|||
this.changeReadState(item as NoticeItem); |
|||
}} |
|||
loading={fetchingNotices} |
|||
clearText={formatMessage({ id: 'component.noticeIcon.clear' })} |
|||
viewMoreText={formatMessage({ id: 'component.noticeIcon.view-more' })} |
|||
onClear={this.handleNoticeClear} |
|||
onPopupVisibleChange={onNoticeVisibleChange} |
|||
onViewMore={() => message.info('Click on view more')} |
|||
clearClose |
|||
> |
|||
<NoticeIcon.Tab |
|||
tabKey="notification" |
|||
count={unreadMsg.notification} |
|||
list={noticeData.notification} |
|||
title={formatMessage({ id: 'component.globalHeader.notification' })} |
|||
emptyText={formatMessage({ id: 'component.globalHeader.notification.empty' })} |
|||
showViewMore |
|||
/> |
|||
<NoticeIcon.Tab |
|||
tabKey="message" |
|||
count={unreadMsg.message} |
|||
list={noticeData.message} |
|||
title={formatMessage({ id: 'component.globalHeader.message' })} |
|||
emptyText={formatMessage({ id: 'component.globalHeader.message.empty' })} |
|||
showViewMore |
|||
/> |
|||
<NoticeIcon.Tab |
|||
tabKey="event" |
|||
title={formatMessage({ id: 'component.globalHeader.event' })} |
|||
emptyText={formatMessage({ id: 'component.globalHeader.event.empty' })} |
|||
count={unreadMsg.event} |
|||
list={noticeData.event} |
|||
showViewMore |
|||
/> |
|||
</NoticeIcon> |
|||
); |
|||
} |
|||
} |
|||
|
|||
export default connect(({ user, global, loading }: ConnectState) => ({ |
|||
currentUser: user.currentUser, |
|||
collapsed: global.collapsed, |
|||
fetchingMoreNotices: loading.effects['global/fetchMoreNotices'], |
|||
fetchingNotices: loading.effects['global/fetchNotices'], |
|||
notices: global.notices, |
|||
}))(GlobalHeaderRight); |
|||
@ -1,9 +1,50 @@ |
|||
export default { |
|||
'menu.welcome': 'Welcome', |
|||
'menu.more-blocks': 'More Blocks', |
|||
|
|||
'menu.home': 'Home', |
|||
'menu.login': 'Login', |
|||
'menu.register': 'Register', |
|||
'menu.register.result': 'Register Result', |
|||
'menu.dashboard': 'Dashboard', |
|||
'menu.dashboard.analysis': 'Analysis', |
|||
'menu.dashboard.monitor': 'Monitor', |
|||
'menu.dashboard.workplace': 'Workplace', |
|||
'menu.exception.403': '403', |
|||
'menu.exception.404': '404', |
|||
'menu.exception.500': '500', |
|||
'menu.form': 'Form', |
|||
'menu.form.basic-form': 'Basic Form', |
|||
'menu.form.step-form': 'Step Form', |
|||
'menu.form.step-form.info': 'Step Form(write transfer information)', |
|||
'menu.form.step-form.confirm': 'Step Form(confirm transfer information)', |
|||
'menu.form.step-form.result': 'Step Form(finished)', |
|||
'menu.form.advanced-form': 'Advanced Form', |
|||
'menu.list': 'List', |
|||
'menu.list.table-list': 'Search Table', |
|||
'menu.list.basic-list': 'Basic List', |
|||
'menu.list.card-list': 'Card List', |
|||
'menu.list.search-list': 'Search List', |
|||
'menu.list.search-list.articles': 'Search List(articles)', |
|||
'menu.list.search-list.projects': 'Search List(projects)', |
|||
'menu.list.search-list.applications': 'Search List(applications)', |
|||
'menu.profile': 'Profile', |
|||
'menu.profile.basic': 'Basic Profile', |
|||
'menu.profile.advanced': 'Advanced Profile', |
|||
'menu.result': 'Result', |
|||
'menu.result.success': 'Success', |
|||
'menu.result.fail': 'Fail', |
|||
'menu.exception': 'Exception', |
|||
'menu.exception.not-permission': '403', |
|||
'menu.exception.not-find': '404', |
|||
'menu.exception.server-error': '500', |
|||
'menu.exception.trigger': 'Trigger', |
|||
'menu.account': 'Account', |
|||
'menu.account.center': 'Account Center', |
|||
'menu.account.settings': 'Account Settings', |
|||
'menu.account.trigger': 'Trigger Error', |
|||
'menu.account.logout': 'Logout', |
|||
'menu.editor': 'Graphic Editor', |
|||
'menu.editor.flow': 'Flow Editor', |
|||
'menu.editor.mind': 'Mind Editor', |
|||
'menu.editor.koni': 'Koni Editor', |
|||
}; |
|||
|
|||
@ -1,8 +1,50 @@ |
|||
export default { |
|||
'menu.welcome': '欢迎', |
|||
'menu.more-blocks': '更多区块', |
|||
'menu.home': '首页', |
|||
'menu.login': '登录', |
|||
'menu.register': '注册', |
|||
'menu.register.result': '注册结果', |
|||
'menu.dashboard': 'Dashboard', |
|||
'menu.dashboard.analysis': '分析页', |
|||
'menu.dashboard.monitor': '监控页', |
|||
'menu.dashboard.workplace': '工作台', |
|||
'menu.exception.403': '403', |
|||
'menu.exception.404': '404', |
|||
'menu.exception.500': '500', |
|||
'menu.form': '表单页', |
|||
'menu.form.basic-form': '基础表单', |
|||
'menu.form.step-form': '分步表单', |
|||
'menu.form.step-form.info': '分步表单(填写转账信息)', |
|||
'menu.form.step-form.confirm': '分步表单(确认转账信息)', |
|||
'menu.form.step-form.result': '分步表单(完成)', |
|||
'menu.form.advanced-form': '高级表单', |
|||
'menu.list': '列表页', |
|||
'menu.list.table-list': '查询表格', |
|||
'menu.list.basic-list': '标准列表', |
|||
'menu.list.card-list': '卡片列表', |
|||
'menu.list.search-list': '搜索列表', |
|||
'menu.list.search-list.articles': '搜索列表(文章)', |
|||
'menu.list.search-list.projects': '搜索列表(项目)', |
|||
'menu.list.search-list.applications': '搜索列表(应用)', |
|||
'menu.profile': '详情页', |
|||
'menu.profile.basic': '基础详情页', |
|||
'menu.profile.advanced': '高级详情页', |
|||
'menu.result': '结果页', |
|||
'menu.result.success': '成功页', |
|||
'menu.result.fail': '失败页', |
|||
'menu.exception': '异常页', |
|||
'menu.exception.not-permission': '403', |
|||
'menu.exception.not-find': '404', |
|||
'menu.exception.server-error': '500', |
|||
'menu.exception.trigger': '触发错误', |
|||
'menu.account': '个人页', |
|||
'menu.account.center': '个人中心', |
|||
'menu.account.settings': '个人设置', |
|||
'menu.account.trigger': '触发报错', |
|||
'menu.account.logout': '退出登录', |
|||
'menu.editor': '图形编辑器', |
|||
'menu.editor.flow': '流程编辑器', |
|||
'menu.editor.mind': '脑图编辑器', |
|||
'menu.editor.koni': '拓扑编辑器', |
|||
}; |
|||
|
|||
@ -0,0 +1,64 @@ |
|||
import { routerRedux } from 'dva/router'; |
|||
import { Reducer } from 'redux'; |
|||
import { EffectsCommandMap } from 'dva'; |
|||
import { AnyAction } from 'redux'; |
|||
import { stringify, parse } from 'qs'; |
|||
|
|||
export function getPageQuery() { |
|||
return parse(window.location.href.split('?')[1]); |
|||
} |
|||
|
|||
export interface IStateType {} |
|||
|
|||
export type Effect = ( |
|||
action: AnyAction, |
|||
effects: EffectsCommandMap & { select: <T>(func: (state: IStateType) => T) => T }, |
|||
) => void; |
|||
|
|||
export interface ModelType { |
|||
namespace: string; |
|||
state: IStateType; |
|||
effects: { |
|||
logout: Effect; |
|||
}; |
|||
reducers: { |
|||
changeLoginStatus: Reducer<IStateType>; |
|||
}; |
|||
} |
|||
|
|||
const Model: ModelType = { |
|||
namespace: 'login', |
|||
|
|||
state: { |
|||
status: undefined, |
|||
}, |
|||
|
|||
effects: { |
|||
*logout(_, { put }) { |
|||
const { redirect } = getPageQuery(); |
|||
// redirect
|
|||
if (window.location.pathname !== '/user/login' && !redirect) { |
|||
yield put( |
|||
routerRedux.replace({ |
|||
pathname: '/user/login', |
|||
search: stringify({ |
|||
redirect: window.location.href, |
|||
}), |
|||
}), |
|||
); |
|||
} |
|||
}, |
|||
}, |
|||
|
|||
reducers: { |
|||
changeLoginStatus(state, { payload }) { |
|||
return { |
|||
...state, |
|||
status: payload.status, |
|||
type: payload.type, |
|||
}; |
|||
}, |
|||
}, |
|||
}; |
|||
|
|||
export default Model; |
|||
Loading…
Reference in new issue