diff --git a/scripts/fetch-blocks.js b/scripts/fetch-blocks.js index f2c5af289c24eee38382791a6efccaf61e1036c8..8fcf44c396c5243b108194b3632af779c1442c53 100644 --- a/scripts/fetch-blocks.js +++ b/scripts/fetch-blocks.js @@ -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(); diff --git a/scripts/insertCode.js b/scripts/insertCode.js new file mode 100644 index 0000000000000000000000000000000000000000..ddf323dc0d8626ff930b473a53e2d7d5bfd304d2 --- /dev/null +++ b/scripts/insertCode.js @@ -0,0 +1,136 @@ +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 = ` + + 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(` `).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(``).expression); + node.argument.children.splice(index, 0, parseCode(``).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`); +}; diff --git a/scripts/repalceRouter.js b/scripts/repalceRouter.js index 4910d3b343a2b4e71dab4c022da60c9c0da937e4..4bb3c4f45ca91eb1b8b8b7d645221f99c89052b7 100644 --- a/scripts/repalceRouter.js +++ b/scripts/repalceRouter.js @@ -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'], diff --git a/src/components/CopyBlock/index.tsx b/src/components/CopyBlock/index.tsx index f1a1156a18f3fbae4ffabd8b0f604183831600c1..1d7f3893b6c3b76a5f4d2256cd595ecb644880d4 100644 --- a/src/components/CopyBlock/index.tsx +++ b/src/components/CopyBlock/index.tsx @@ -1,6 +1,8 @@ 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 ( { ); -}; +}); diff --git a/src/components/GlobalHeader/AvatarDropdown.tsx b/src/components/GlobalHeader/AvatarDropdown.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4454ccaceca1bb95a1300688242bda4358f7a546 --- /dev/null +++ b/src/components/GlobalHeader/AvatarDropdown.tsx @@ -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 { + 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 ( + + + {currentUser.name} + + ); + } + const menuHeaderDropdown = ( + + + + + + + + + + + + + + + + ); + + return currentUser && currentUser.name ? ( + + + + {currentUser.name} + + + ) : ( + + ); + } +} +export default connect(({ user }: ConnectState) => ({ + currentUser: user.currentUser, +}))(AvatarDropdown); diff --git a/src/components/GlobalHeader/NoticeIconView.tsx b/src/components/GlobalHeader/NoticeIconView.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f79429aaeb604198991c51c9889b020e2c11da46 --- /dev/null +++ b/src/components/GlobalHeader/NoticeIconView.tsx @@ -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 { + 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 = ( + + {newNotice.extra} + + ); + } + 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 ( + { + 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 + > + + + + + ); + } +} + +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); diff --git a/src/components/GlobalHeader/RightContent.tsx b/src/components/GlobalHeader/RightContent.tsx index 051baa54725428cc7b9df5dd4bd227b586cda06d..65fabe444faffdb241d90cd367b1f50bcf491f6c 100644 --- a/src/components/GlobalHeader/RightContent.tsx +++ b/src/components/GlobalHeader/RightContent.tsx @@ -1,152 +1,44 @@ 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 { - 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 = ( - - {newNotice.extra} - - ); - } - 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 = ( - - - - - - - - - - - - - - - - ); - 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 (
{ console.log('input', value); // tslint:disable-line no-console @@ -155,7 +47,11 @@ class GlobalHeaderRight extends Component { console.log('enter', value); // tslint:disable-line no-console }} /> - + { - - { - 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 - > - - - - - {currentUser && currentUser.name ? ( - - - - {currentUser.name} - - - ) : ( - - )} +
); } } -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); diff --git a/src/layouts/BasicLayout.tsx b/src/layouts/BasicLayout.tsx index 2cb64e0235b90242b9bb9ffd8285954b00756881..c83ef66f180ffc33a405eba663c0de20bf3d9e0d 100644 --- a/src/layouts/BasicLayout.tsx +++ b/src/layouts/BasicLayout.tsx @@ -1,6 +1,5 @@ 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 = 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 ( - <> - { - return {defaultDom}; - }} - breadcrumbRender={(routers = []) => { - return [ - { - path: '/', - breadcrumbName: formatMessage({ - id: 'menu.home', - defaultMessage: 'Home', - }), - }, - ...routers, - ]; - }} - menuDataRender={menuDataRender} - formatMessage={formatMessage} - rightContentRender={rightProps => } - {...props} - {...settings} - > - {children} - - {isAntDesignProOrDev() && ( - - dispatch!({ - type: 'settings/changeSetting', - payload: config, - }) - } - /> - )} - {isAntDesignProOrDev() && } - + { + return {defaultDom}; + }} + breadcrumbRender={(routers = []) => { + return [ + { + path: '/', + breadcrumbName: formatMessage({ + id: 'menu.home', + defaultMessage: 'Home', + }), + }, + ...routers, + ]; + }} + menuDataRender={menuDataRender} + formatMessage={formatMessage} + rightContentRender={rightProps => } + {...props} + {...settings} + > + {children} + ); };