Commit 028b6cc7 authored by 陈帅's avatar 陈帅

auto insert pro code

parent 41ad482a
......@@ -5,6 +5,7 @@ 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'];
......@@ -123,3 +124,6 @@ const installBlock = async () => {
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';
});
// 从组件中导入 CopyBlock
body.splice(
index,
0,
parseCode(`import CopyBlock from '@/components/CopyBlock';
`),
);
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);
JSXFragment.children.push(parseCode(` <CopyBlock />`).expression);
node.argument = 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`);
};
......@@ -5,7 +5,7 @@ const t = require('@babel/types');
const fs = require('fs');
const prettier = require('prettier');
const getNewRouteCode = (configPath, newRoute, absSrcPath) => {
const getNewRouteCode = (configPath, newRoute) => {
const ast = parser.parse(fs.readFileSync(configPath, 'utf-8'), {
sourceType: 'module',
plugins: ['typescript'],
......
import React from 'react';
import { Icon, Typography, Popover } from 'antd';
import styles from './index.less';
import { connect } from 'dva';
import * as H from 'history';
const firstUpperCase = (pathString: string) => {
return pathString
.replace('.', '')
......@@ -22,7 +24,12 @@ const BlockCodeView: React.SFC<{
);
};
export default ({ url }: { url: string }) => {
type RoutingType = { location: H.Location };
export default connect(({ routing }: { routing: RoutingType }) => ({
location: routing.location,
}))(({ location }: RoutingType) => {
const url = location.pathname;
return (
<Popover
title="下载此页面到本地项目"
......@@ -35,4 +42,4 @@ export default ({ url }: { url: string }) => {
</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/lib/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';
import router from 'umi/router';
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,
});
}
};
onMenuClick = (event: ClickParam) => {
const { onMenuClick } = this.props;
if (onMenuClick) {
onMenuClick(event);
return;
}
const { key } = event;
if (key === 'logout') {
const { dispatch } = this.props;
dispatch!({
type: 'login/logout',
});
return;
}
router.push(`/account/${key}`);
};
render() {
const { currentUser, fetchingNotices, onNoticeVisibleChange, theme } = this.props;
const menu = (
<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>
);
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
......@@ -155,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"
......@@ -165,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);
import { ConnectState, ConnectProps } from '@/models/connect';
import RightContent from '@/components/GlobalHeader/RightContent';
import CopyBlock from '@/components/CopyBlock';
import { connect } from 'dva';
import React, { useState } from 'react';
import logo from '../assets/logo.svg';
......@@ -11,30 +10,26 @@ import {
BasicLayoutProps as BasicLayoutComponentsProps,
MenuDataItem,
Settings,
SettingDrawer,
} from '@ant-design/pro-layout';
import Link from 'umi/link';
import { isAntDesignProOrDev } from '@/utils/utils';
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;
};
};
/**
* use Authorized check all menu item
*/
const menuDataRender = (menuList: MenuDataItem[]): MenuDataItem[] => {
return menuList.map(item => {
const localItem = {
...item,
children: item.children ? menuDataRender(item.children) : [],
};
const localItem = { ...item, children: item.children ? menuDataRender(item.children) : [] };
return Authorized.check(item.authority, localItem, null) as MenuDataItem;
});
};
......@@ -44,57 +39,52 @@ const BasicLayout: React.FC<BasicLayoutProps> = 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 });
dispatch!({
type: 'global/changeLayoutCollapsed',
payload,
});
return (
<>
<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>
{isAntDesignProOrDev() && (
<SettingDrawer
settings={settings}
onSettingChange={config =>
dispatch!({
type: 'settings/changeSetting',
payload: config,
})
}
/>
)}
{isAntDesignProOrDev() && <CopyBlock url={location!.pathname} />}
</>
<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>
);
};
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment