未验证 提交 41719aa5 编写于 作者: 陈帅 提交者: GitHub

V4 fetch block (#4238)

* 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 button
上级 5a30e60e
......@@ -5,9 +5,12 @@
"stylelint-config-rational-order",
"stylelint-config-prettier"
],
"plugins": ["stylelint-order", "stylelint-declaration-block-no-ignored-properties"],
"plugins": [
"stylelint-order",
"stylelint-declaration-block-no-ignored-properties"
],
"rules": {
"no-descending-specificity": null,
"plugin/declaration-block-no-ignored-properties": true
}
}
}
\ No newline at end of file
......@@ -4,7 +4,7 @@ import MergeLessPlugin from 'antd-pro-merge-less';
import AntDesignThemePlugin from 'antd-theme-webpack-plugin';
import path from 'path';
function getModulePackageName(module) {
function getModulePackageName(module: { context: string }) {
if (!module.context) return null;
const nodeModulesPath = path.join(__dirname, '../node_modules/');
......@@ -14,16 +14,16 @@ function getModulePackageName(module) {
const moduleRelativePath = module.context.substring(nodeModulesPath.length);
const [moduleDirName] = moduleRelativePath.split(path.sep);
let packageName = moduleDirName;
let packageName: string | null = moduleDirName;
// handle tree shaking
if (packageName.match('^_')) {
if (packageName && packageName.match('^_')) {
// eslint-disable-next-line prefer-destructuring
packageName = packageName.match(/^_(@?[^@]+)/)[1];
packageName = packageName.match(/^_(@?[^@]+)/)![1];
}
return packageName;
}
export default config => {
export default (config: any) => {
// preview.pro.ant.design only do not use in your production ; preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。
if (
process.env.ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site' ||
......@@ -62,18 +62,19 @@ export default config => {
minSize: 0,
cacheGroups: {
vendors: {
test: module => {
test: (module: { context: string }) => {
const packageName = getModulePackageName(module);
if (packageName) {
return ['bizcharts', '@antv_data-set'].indexOf(packageName) >= 0;
}
return false;
},
name(module) {
name(module: { context: string }) {
const packageName = getModulePackageName(module);
if (['bizcharts', '@antv_data-set'].indexOf(packageName) >= 0) {
return 'viz'; // visualization package
if (packageName) {
if (['bizcharts', '@antv_data-set'].indexOf(packageName) >= 0) {
return 'viz'; // visualization package
}
}
return 'misc';
},
......
......@@ -13,6 +13,7 @@
"docker:dev": "docker-compose -f ./docker/docker-compose.dev.yml up",
"docker:push": "npm run docker-hub:build && npm run docker:tag && docker push antdesign/ant-design-pro",
"docker:tag": "docker tag ant-design-pro antdesign/ant-design-pro",
"fetch:blocks": "node ./scripts/fetch-blocks.js",
"functions:build": "npm run generateMock && netlify-lambda build ./lambda",
"functions:run": "npm run generateMock && cross-env NODE_ENV=dev netlify-lambda serve ./lambda",
"generateMock": "node ./scripts/generateMock",
......@@ -26,7 +27,7 @@
"lint:style": "stylelint --fix 'src/**/*.less' --syntax less",
"lint:ts": "tslint -p . -c tslint.yml",
"prettier": " check-prettier write",
"site": "umi build && npm run functions:build",
"site": "npm run fetch:blocks && umi build && npm run functions:build",
"start": "umi dev",
"start:no-mock": "cross-env MOCK=none umi dev",
"test": "umi test",
......@@ -49,18 +50,17 @@
"dependencies": {
"@ant-design/pro-layout": "^4.2.0",
"@antv/data-set": "^0.10.1",
"@types/qs": "^6.5.3",
"antd": "^3.16.1",
"bizcharts": "^3.5.3-beta.0",
"bizcharts-plugin-slider": "^2.1.1-beta.1",
"classnames": "^2.2.6",
"dva": "^2.4.0",
"lodash": "^4.17.10",
"lodash-decorators": "^6.0.0",
"memoize-one": "^5.0.0",
"moment": "^2.22.2",
"numeral": "^2.0.6",
"omit.js": "^1.0.0",
"path-to-regexp": "^2.4.0",
"prop-types": "^15.7.2",
"qs": "^6.7.0",
"rc-animate": "^2.4.4",
"react": "^16.8.5",
......@@ -68,9 +68,14 @@
"react-copy-to-clipboard": "^5.0.1",
"react-document-title": "^2.0.3",
"react-dom": "^16.7.0",
"react-fittext": "^1.0.0",
"react-media": "^1.9.2",
"react-media-hook2": "^1.0.2"
"react-media-hook2": "^1.0.2",
"umi": "^2.7.0-beta.2",
"umi-plugin-ga": "^1.1.3",
"umi-plugin-locale": "^2.8.0-beta.1",
"umi-plugin-pro-block": "^1.3.0",
"umi-plugin-react": "^1.8.0-beta.1",
"umi-request": "^1.0.7"
},
"devDependencies": {
"@types/classnames": "^2.2.7",
......@@ -106,6 +111,7 @@
"merge-umi-mock-data": "^1.0.4",
"mockjs": "^1.0.1-beta3",
"netlify-lambda": "^1.4.3",
"node-fetch": "^2.6.0",
"prettier": "^1.17.0",
"serverless-http": "^2.0.1",
"slash2": "^2.0.0",
......@@ -119,12 +125,7 @@
"tslint": "^5.12.1",
"tslint-config-prettier": "^1.17.0",
"tslint-eslint-rules": "^5.4.0",
"tslint-react": "^3.6.0",
"umi": "^2.7.0-beta.2",
"umi-plugin-ga": "^1.1.3",
"umi-plugin-pro-block": "^1.3.0",
"umi-plugin-react": "^1.8.0-beta.1",
"umi-request": "^1.0.0"
"tslint-react": "^3.6.0"
},
"optionalDependencies": {
"puppeteer": "^1.12.1"
......
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();
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`);
};
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;
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',
},
],
},
],
},
];
.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;
}
}
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>
);
});
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);
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);
import { ConnectProps, ConnectState } from '@/models/connect';
import { NoticeItem } from '@/models/global';
import { CurrentUser } from '@/models/user';
import React, { Component } from 'react';
import { Spin, Tag, Menu, Icon, Avatar, Tooltip, message } from 'antd';
import { ClickParam } from 'antd/es/menu';
import { FormattedMessage, formatMessage } from 'umi-plugin-react/locale';
import moment from 'moment';
import groupBy from 'lodash/groupBy';
import NoticeIcon from '../NoticeIcon';
import { Icon, Tooltip } from 'antd';
import { formatMessage } from 'umi-plugin-react/locale';
import HeaderSearch from '../HeaderSearch';
import HeaderDropdown from '../HeaderDropdown';
import SelectLang from '../SelectLang';
import styles from './index.less';
import Avatar from './AvatarDropdown';
import { connect } from 'dva';
export type SiderTheme = 'light' | 'dark';
export interface GlobalHeaderRightProps extends ConnectProps {
notices?: NoticeItem[];
currentUser?: CurrentUser;
fetchingNotices?: boolean;
onNoticeVisibleChange?: (visible: boolean) => void;
onMenuClick?: (param: ClickParam) => void;
onNoticeClear?: (tabName?: string) => void;
theme?: SiderTheme;
layout: 'sidemenu' | 'topmenu';
}
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, onMenuClick, theme } = this.props;
const menu = (
<Menu className={styles.menu} selectedKeys={[]} onClick={onMenuClick}>
<Menu.Item key="userCenter">
<Icon type="user" />
<FormattedMessage id="menu.account.center" defaultMessage="account center" />
</Menu.Item>
<Menu.Item key="userinfo">
<Icon type="setting" />
<FormattedMessage id="menu.account.settings" defaultMessage="account settings" />
</Menu.Item>
<Menu.Item key="triggerError">
<Icon type="close-circle" />
<FormattedMessage id="menu.account.trigger" defaultMessage="Trigger Error" />
</Menu.Item>
<Menu.Divider />
<Menu.Item key="logout">
<Icon type="logout" />
<FormattedMessage id="menu.account.logout" defaultMessage="logout" />
</Menu.Item>
</Menu>
);
const noticeData = this.getNoticeData();
const unreadMsg = this.getUnreadData(noticeData);
const { theme, layout } = this.props;
let className = styles.right;
if (theme === 'dark') {
if (theme === 'dark' && layout === 'topmenu') {
className = `${styles.right} ${styles.dark}`;
}
return (
<div className={className}>
<HeaderSearch
className={`${styles.action} ${styles.search}`}
placeholder={formatMessage({ id: 'component.globalHeader.search' })}
placeholder={formatMessage({
id: 'component.globalHeader.search',
})}
dataSource={[
formatMessage({ id: 'component.globalHeader.search.example1' }),
formatMessage({ id: 'component.globalHeader.search.example2' }),
formatMessage({ id: 'component.globalHeader.search.example3' }),
formatMessage({
id: 'component.globalHeader.search.example1',
}),
formatMessage({
id: 'component.globalHeader.search.example2',
}),
formatMessage({
id: 'component.globalHeader.search.example3',
}),
]}
onSearch={value => {
console.log('input', value); // tslint:disable-line no-console
......@@ -141,7 +47,11 @@ class GlobalHeaderRight extends Component<GlobalHeaderRightProps> {
console.log('enter', value); // tslint:disable-line no-console
}}
/>
<Tooltip title={formatMessage({ id: 'component.globalHeader.help' })}>
<Tooltip
title={formatMessage({
id: 'component.globalHeader.help',
})}
>
<a
target="_blank"
href="https://pro.ant.design/docs/getting-started"
......@@ -151,71 +61,14 @@ class GlobalHeaderRight extends Component<GlobalHeaderRightProps> {
<Icon type="question-circle-o" />
</a>
</Tooltip>
<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>
{currentUser && currentUser.name ? (
<HeaderDropdown overlay={menu}>
<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 }} />
)}
<Avatar />
<SelectLang className={styles.action} />
</div>
);
}
}
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,
export default connect(({ settings }: ConnectState) => ({
theme: settings.navTheme,
layout: settings.layout,
}))(GlobalHeaderRight);
......@@ -33,7 +33,7 @@ describe('Homepage', () => {
jest.setTimeout(1000000);
await page.setCacheEnabled(false);
});
const routers = formatter(RouterConfig[1].routes);
const routers = formatter(RouterConfig);
routers.forEach(route => {
it(`test pages ${route}`, testPage(route));
});
......
......@@ -16,74 +16,81 @@ import {
BasicLayoutProps as BasicLayoutComponentsProps,
MenuDataItem,
Settings,
SettingDrawer,
} from '@ant-design/pro-layout';
import Link from 'umi/link';
export interface BasicLayoutProps extends BasicLayoutComponentsProps, ConnectProps {
breadcrumbNameMap: { [path: string]: MenuDataItem };
breadcrumbNameMap: {
[path: string]: MenuDataItem;
};
settings: Settings;
}
export type BasicLayoutContext = { [K in 'location']: BasicLayoutProps[K] } & {
breadcrumbNameMap: { [path: string]: MenuDataItem };
breadcrumbNameMap: {
[path: string]: MenuDataItem;
};
};
/**
* default menuLocal
* use Authorized check all menu item
*/
const filterMenuData = (menuList: MenuDataItem[], locale: boolean) => {
const menuDataRender = (menuList: MenuDataItem[]): MenuDataItem[] => {
return menuList.map(item => {
const localItem = {
...item,
name: item.locale && locale ? formatMessage({ id: item.locale }) : item.name,
};
const localItem = { ...item, children: item.children ? menuDataRender(item.children) : [] };
return Authorized.check(item.authority, localItem, null) as MenuDataItem;
});
};
const BasicLayout: React.FC<BasicLayoutProps> = props => {
const { dispatch, children, settings } = props;
const { dispatch, children, settings, location } = props;
/**
* constructor
*/
useState(() => {
dispatch!({ type: 'user/fetchCurrent' });
dispatch!({ type: 'settings/getSetting' });
dispatch!({
type: 'user/fetchCurrent',
});
dispatch!({
type: 'settings/getSetting',
});
});
/**
* init variables
*/
const handleMenuCollapse = (payload: boolean) =>
dispatch!({ type: 'global/changeLayoutCollapsed', payload });
const {
menu: { locale },
} = settings;
dispatch!({
type: 'global/changeLayoutCollapsed',
payload,
});
return (
<>
<BasicLayoutComponents
logo={logo}
onCollapse={handleMenuCollapse}
menuItemRender={(menuItemProps, defaultDom) => {
return <Link to={menuItemProps.path}>{defaultDom}</Link>;
}}
filterMenuData={menuList => filterMenuData(menuList, locale)}
rightContentRender={rightProps => <RightContent {...rightProps} />}
{...props}
{...settings}
>
{children}
</BasicLayoutComponents>
<SettingDrawer
settings={settings}
onSettingChange={config =>
dispatch!({
type: 'settings/changeSetting',
payload: config,
})
}
/>
</>
<BasicLayoutComponents
logo={logo}
onCollapse={handleMenuCollapse}
menuItemRender={(menuItemProps, defaultDom) => {
return <Link to={menuItemProps.path}>{defaultDom}</Link>;
}}
breadcrumbRender={(routers = []) => {
return [
{
path: '/',
breadcrumbName: formatMessage({
id: 'menu.home',
defaultMessage: 'Home',
}),
},
...routers,
];
}}
menuDataRender={menuDataRender}
formatMessage={formatMessage}
rightContentRender={rightProps => <RightContent {...rightProps} />}
{...props}
{...settings}
>
{children}
</BasicLayoutComponents>
);
};
......
......@@ -10,7 +10,7 @@ export default {
'layout.user.link.help': 'Help',
'layout.user.link.privacy': 'Privacy',
'layout.user.link.terms': 'Terms',
'app.home.introduce': 'introduce',
'app.preview.down.block': 'Download this page to your local project',
...globalHeader,
...menu,
...settingDrawer,
......
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',
};
......@@ -10,7 +10,7 @@ export default {
'layout.user.link.help': 'ajuda',
'layout.user.link.privacy': 'política de privacidade',
'layout.user.link.terms': 'termos de serviços',
'app.home.introduce': 'introduzir',
'app.preview.down.block': 'Download this page to your local project',
...globalHeader,
...menu,
...settingDrawer,
......
......@@ -2,8 +2,50 @@ export default {
'menu.welcome': 'Welcome',
'menu.more-blocks': 'More Blocks',
'menu.home': 'Início',
'menu.login': 'Login',
'menu.register': 'Registro',
'menu.register.result': 'Resultado de registro',
'menu.dashboard': 'Dashboard',
'menu.dashboard.analysis': 'Análise',
'menu.dashboard.monitor': 'Monitor',
'menu.dashboard.workplace': 'Ambiente de Trabalho',
'menu.exception.403': '403',
'menu.exception.404': '404',
'menu.exception.500': '500',
'menu.form': 'Formulário',
'menu.form.basic-form': 'Formulário Básico',
'menu.form.step-form': 'Formulário Assistido',
'menu.form.step-form.info': 'Formulário Assistido(gravar informações de transferência)',
'menu.form.step-form.confirm': 'Formulário Assistido(confirmar informações de transferência)',
'menu.form.step-form.result': 'Formulário Assistido(finalizado)',
'menu.form.advanced-form': 'Formulário Avançado',
'menu.list': 'Lista',
'menu.list.table-list': 'Tabela de Busca',
'menu.list.basic-list': 'Lista Básica',
'menu.list.card-list': 'Lista de Card',
'menu.list.search-list': 'Lista de Busca',
'menu.list.search-list.articles': 'Lista de Busca(artigos)',
'menu.list.search-list.projects': 'Lista de Busca(projetos)',
'menu.list.search-list.applications': 'Lista de Busca(aplicações)',
'menu.profile': 'Perfil',
'menu.profile.basic': 'Perfil Básico',
'menu.profile.advanced': 'Perfil Avançado',
'menu.result': 'Resultado',
'menu.result.success': 'Sucesso',
'menu.result.fail': 'Falha',
'menu.exception': 'Exceção',
'menu.exception.not-permission': '403',
'menu.exception.not-find': '404',
'menu.exception.server-error': '500',
'menu.exception.trigger': 'Disparar',
'menu.account': 'Conta',
'menu.account.center': 'Central da Conta',
'menu.account.settings': 'Configurar Conta',
'menu.account.trigger': 'Disparar Erro',
'menu.account.logout': 'Sair',
'menu.editor': 'Graphic Editor',
'menu.editor.flow': 'Flow Editor',
'menu.editor.mind': 'Mind Editor',
'menu.editor.koni': 'Koni Editor',
};
......@@ -10,7 +10,7 @@ export default {
'layout.user.link.help': '帮助',
'layout.user.link.privacy': '隐私',
'layout.user.link.terms': '条款',
'app.home.introduce': '介绍',
'app.preview.down.block': '下载此页面到本地项目',
...globalHeader,
...menu,
...settingDrawer,
......
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': '拓扑编辑器',
};
......@@ -10,10 +10,7 @@ export default {
'layout.user.link.help': '幫助',
'layout.user.link.privacy': '隱私',
'layout.user.link.terms': '條款',
'app.home.introduce': '介紹',
'app.forms.basic.title': '基礎表單',
'app.forms.basic.description':
'表單頁用於向用戶收集或驗證信息,基礎表單常見於數據項較少的表單場景。',
'app.preview.down.block': '下載此頁面到本地項目',
...globalHeader,
...menu,
...settingDrawer,
......
......@@ -2,8 +2,50 @@ export default {
'menu.welcome': '歡迎',
'menu.more-blocks': '更多區塊',
'menu.home': '首頁',
'menu.login': '登錄',
'menu.exception.403': '403',
'menu.exception.404': '404',
'menu.exception.500': '500',
'menu.register': '註冊',
'menu.register.resultt': '註冊結果',
'menu.dashboard': 'Dashboard',
'menu.dashboard.analysis': '分析頁',
'menu.dashboard.monitor': '監控頁',
'menu.dashboard.workplace': '工作臺',
'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.account': '個人頁',
'menu.account.center': '個人中心',
'menu.account.settings': '個人設置',
'menu.account.trigger': '觸發報錯',
'menu.account.logout': '退出登錄',
'menu.exception': '异常页',
'menu.exception.not-permission': '403',
'menu.exception.not-find': '404',
'menu.exception.server-error': '500',
'menu.exception.trigger': '触发错误',
'menu.editor': '圖形編輯器',
'menu.editor.flow': '流程編輯器',
'menu.editor.mind': '腦圖編輯器',
'menu.editor.koni': '拓撲編輯器',
};
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;
......@@ -4,3 +4,15 @@ const reg = /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(
export function isUrl(path: string) {
return reg.test(path);
}
// 给官方演示站点用,用于关闭真实开发环境不需要使用的特性
export function isAntDesignProOrDev() {
const { NODE_ENV } = process.env;
if (ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site') {
return true;
}
if (NODE_ENV === 'development') {
return true;
}
return window.location.hostname === 'preview.pro.ant.design';
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册