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 { |
export default { |
||||
'menu.welcome': 'Welcome', |
'menu.welcome': 'Welcome', |
||||
'menu.more-blocks': 'More Blocks', |
'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.center': 'Account Center', |
||||
'menu.account.settings': 'Account Settings', |
'menu.account.settings': 'Account Settings', |
||||
'menu.account.trigger': 'Trigger Error', |
'menu.account.trigger': 'Trigger Error', |
||||
'menu.account.logout': 'Logout', |
'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 { |
export default { |
||||
'menu.welcome': '欢迎', |
'menu.welcome': '欢迎', |
||||
'menu.more-blocks': '更多区块', |
'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.center': '个人中心', |
||||
'menu.account.settings': '个人设置', |
'menu.account.settings': '个人设置', |
||||
'menu.account.trigger': '触发报错', |
'menu.account.trigger': '触发报错', |
||||
'menu.account.logout': '退出登录', |
'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