You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by ju...@apache.org on 2020/05/21 03:37:32 UTC
[incubator-apisix-dashboard] 01/02: feat: remove unused files
This is an automated email from the ASF dual-hosted git repository.
juzhiyuan pushed a commit to branch feat-layout
in repository https://gitbox.apache.org/repos/asf/incubator-apisix-dashboard.git
commit 7a5400e0f8108b7d9dc31399386760df86b85e89
Author: juzhiyuan <jj...@gmail.com>
AuthorDate: Thu May 21 11:34:39 2020 +0800
feat: remove unused files
---
config/config.ts | 8 +-
src/app.tsx | 19 ++
src/components/Authorized/Authorized.tsx | 35 ----
src/components/Authorized/AuthorizedRoute.tsx | 33 ---
src/components/Authorized/CheckPermissions.tsx | 83 --------
src/components/Authorized/PromiseRender.tsx | 93 ---------
src/components/Authorized/Secured.tsx | 66 ------
src/components/Authorized/index.tsx | 11 -
src/components/Authorized/renderAuthorize.ts | 30 ---
src/components/Footer/index.tsx | 17 ++
src/components/GlobalHeader/AvatarDropdown.tsx | 86 --------
src/components/GlobalHeader/NoticeIconView.tsx | 171 ----------------
src/components/GlobalHeader/RightContent.tsx | 56 ------
src/components/RightContent/AvatarDropdown.tsx | 99 +++++++++
.../{GlobalHeader => RightContent}/index.less | 35 +---
src/components/RightContent/index.tsx | 72 +++++++
src/components/SelectLang/index.less | 24 ---
src/components/SelectLang/index.tsx | 50 -----
src/layouts/BasicLayout.tsx | 155 --------------
src/layouts/BlankLayout.tsx | 5 -
src/layouts/SecurityLayout.tsx | 57 ------
src/layouts/UserLayout.less | 71 -------
src/layouts/UserLayout.tsx | 77 -------
src/models/connect.d.ts | 31 ---
src/models/global.ts | 137 -------------
src/models/login.ts | 87 --------
src/models/setting.ts | 37 ----
src/models/user.ts | 85 --------
src/pages/Authorized.tsx | 36 ----
.../user/login/components/Login/LoginItem.tsx | 10 +-
src/pages/user/login/components/Login/LoginTab.tsx | 6 +-
src/pages/user/login/components/Login/index.tsx | 17 +-
src/pages/user/login/index.tsx | 224 +++++++++++++++------
src/pages/user/login/style.less | 70 +++++++
src/utils/Authorized.ts | 19 --
src/utils/authority.ts | 32 ---
36 files changed, 462 insertions(+), 1682 deletions(-)
diff --git a/config/config.ts b/config/config.ts
index 7524365..8339d2f 100644
--- a/config/config.ts
+++ b/config/config.ts
@@ -21,12 +21,16 @@ export default defineConfig({
targets: {
ie: 11,
},
+ layout: {
+ name: 'APISIX Dashboard',
+ locale: true,
+ },
base: '/dashboard/',
publicPath: '/',
routes: [
{
path: '/user',
- component: '../layouts/UserLayout',
+ layout: false,
routes: [
{
name: 'login',
@@ -37,11 +41,9 @@ export default defineConfig({
},
{
path: '/',
- component: '../layouts/SecurityLayout',
routes: [
{
path: '/',
- component: '../layouts/BasicLayout',
authority: ['admin', 'user'],
routes: [
{
diff --git a/src/app.tsx b/src/app.tsx
index 1d66ae6..7a5b6af 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -1,6 +1,25 @@
+import React from 'react';
import { notification } from 'antd';
import { RequestConfig } from 'umi';
+import { BasicLayoutProps, Settings as LayoutSettings } from '@ant-design/pro-layout';
+
import { getAdminAPIConfig } from '@/utils/setting';
+import RightContent from '@/components/RightContent';
+import Footer from '@/components/Footer';
+
+export const layout = ({
+ initialState,
+}: {
+ initialState: { settings?: LayoutSettings };
+}): BasicLayoutProps => {
+ return {
+ rightContentRender: () => <RightContent />,
+ disableContentMargin: false,
+ footerRender: () => <Footer />,
+ menuHeaderRender: undefined,
+ ...initialState?.settings,
+ };
+};
const codeMessage = {
200: '服务器成功返回请求的数据。',
diff --git a/src/components/Authorized/Authorized.tsx b/src/components/Authorized/Authorized.tsx
deleted file mode 100644
index 36af4c6..0000000
--- a/src/components/Authorized/Authorized.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import React from 'react';
-import { Result } from 'antd';
-import check, { IAuthorityType } from './CheckPermissions';
-
-import AuthorizedRoute from './AuthorizedRoute';
-import Secured from './Secured';
-
-interface AuthorizedProps {
- authority: IAuthorityType;
- noMatch?: React.ReactNode;
-}
-
-type IAuthorizedType = React.FunctionComponent<AuthorizedProps> & {
- Secured: typeof Secured;
- check: typeof check;
- AuthorizedRoute: typeof AuthorizedRoute;
-};
-
-const Authorized: React.FunctionComponent<AuthorizedProps> = ({
- children,
- authority,
- noMatch = (
- <Result
- status={403}
- title="403"
- subTitle="Sorry, you are not authorized to access this page."
- />
- ),
-}) => {
- const childrenRender: React.ReactNode = typeof children === 'undefined' ? null : children;
- const dom = check(authority, childrenRender, noMatch);
- return <>{dom}</>;
-};
-
-export default Authorized as IAuthorizedType;
diff --git a/src/components/Authorized/AuthorizedRoute.tsx b/src/components/Authorized/AuthorizedRoute.tsx
deleted file mode 100644
index 7743eae..0000000
--- a/src/components/Authorized/AuthorizedRoute.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { Redirect, Route } from 'umi';
-
-import React from 'react';
-import Authorized from './Authorized';
-import { IAuthorityType } from './CheckPermissions';
-
-interface AuthorizedRoutePops {
- currentAuthority: string;
- component: React.ComponentClass<any, any>;
- render: (props: any) => React.ReactNode;
- redirectPath: string;
- authority: IAuthorityType;
-}
-
-const AuthorizedRoute: React.SFC<AuthorizedRoutePops> = ({
- component: Component,
- render,
- authority,
- redirectPath,
- ...rest
-}) => (
- <Authorized
- authority={authority}
- noMatch={<Route {...rest} render={() => <Redirect to={{ pathname: redirectPath }} />} />}
- >
- <Route
- {...rest}
- render={(props: any) => (Component ? <Component {...props} /> : render(props))}
- />
- </Authorized>
-);
-
-export default AuthorizedRoute;
diff --git a/src/components/Authorized/CheckPermissions.tsx b/src/components/Authorized/CheckPermissions.tsx
deleted file mode 100644
index caa15a3..0000000
--- a/src/components/Authorized/CheckPermissions.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-import React from 'react';
-import { CURRENT } from './renderAuthorize';
-// eslint-disable-next-line import/no-cycle
-import PromiseRender from './PromiseRender';
-
-export type IAuthorityType =
- | undefined
- | string
- | string[]
- | Promise<boolean>
- | ((currentAuthority: string | string[]) => IAuthorityType);
-
-/**
- * 通用权限检查方法
- * Common check permissions method
- * @param { 权限判定 | Permission judgment } authority
- * @param { 你的权限 | Your permission description } currentAuthority
- * @param { 通过的组件 | Passing components } target
- * @param { 未通过的组件 | no pass components } Exception
- */
-const checkPermissions = <T, K>(
- authority: IAuthorityType,
- currentAuthority: string | string[],
- target: T,
- Exception: K,
-): T | K | React.ReactNode => {
- // 没有判定权限.默认查看所有
- // Retirement authority, return target;
- if (!authority) {
- return target;
- }
- // 数组处理
- if (Array.isArray(authority)) {
- if (Array.isArray(currentAuthority)) {
- if (currentAuthority.some(item => authority.includes(item))) {
- return target;
- }
- } else if (authority.includes(currentAuthority)) {
- return target;
- }
- return Exception;
- }
- // string 处理
- if (typeof authority === 'string') {
- if (Array.isArray(currentAuthority)) {
- if (currentAuthority.some(item => authority === item)) {
- return target;
- }
- } else if (authority === currentAuthority) {
- return target;
- }
- return Exception;
- }
- // Promise 处理
- if (authority instanceof Promise) {
- return <PromiseRender<T, K> ok={target} error={Exception} promise={authority} />;
- }
- // Function 处理
- if (typeof authority === 'function') {
- try {
- const bool = authority(currentAuthority);
- // 函数执行后返回值是 Promise
- if (bool instanceof Promise) {
- return <PromiseRender<T, K> ok={target} error={Exception} promise={bool} />;
- }
- if (bool) {
- return target;
- }
- return Exception;
- } catch (error) {
- throw error;
- }
- }
- throw new Error('unsupported parameters');
-};
-
-export { checkPermissions };
-
-function check<T, K>(authority: IAuthorityType, target: T, Exception: K): T | K | React.ReactNode {
- return checkPermissions<T, K>(authority, CURRENT, target, Exception);
-}
-
-export default check;
diff --git a/src/components/Authorized/PromiseRender.tsx b/src/components/Authorized/PromiseRender.tsx
deleted file mode 100644
index 25f2597..0000000
--- a/src/components/Authorized/PromiseRender.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import React from 'react';
-import { Spin } from 'antd';
-import isEqual from 'lodash/isEqual';
-import { isComponentClass } from './Secured';
-// eslint-disable-next-line import/no-cycle
-
-interface PromiseRenderProps<T, K> {
- ok: T;
- error: K;
- promise: Promise<boolean>;
-}
-
-interface PromiseRenderState {
- component: React.ComponentClass | React.FunctionComponent;
-}
-
-export default class PromiseRender<T, K> extends React.Component<
- PromiseRenderProps<T, K>,
- PromiseRenderState
-> {
- state: PromiseRenderState = {
- component: () => null,
- };
-
- componentDidMount() {
- this.setRenderComponent(this.props);
- }
-
- shouldComponentUpdate = (nextProps: PromiseRenderProps<T, K>, nextState: PromiseRenderState) => {
- const { component } = this.state;
- if (!isEqual(nextProps, this.props)) {
- this.setRenderComponent(nextProps);
- }
- if (nextState.component !== component) return true;
- return false;
- };
-
- // set render Component : ok or error
- setRenderComponent(props: PromiseRenderProps<T, K>) {
- const ok = this.checkIsInstantiation(props.ok);
- const error = this.checkIsInstantiation(props.error);
- props.promise
- .then(() => {
- this.setState({
- component: ok,
- });
- return true;
- })
- .catch(() => {
- this.setState({
- component: error,
- });
- });
- }
-
- // Determine whether the incoming component has been instantiated
- // AuthorizedRoute is already instantiated
- // Authorized render is already instantiated, children is no instantiated
- // Secured is not instantiated
- checkIsInstantiation = (
- target: React.ReactNode | React.ComponentClass,
- ): React.FunctionComponent => {
- if (isComponentClass(target)) {
- const Target = target as React.ComponentClass;
- return (props: any) => <Target {...props} />;
- }
- if (React.isValidElement(target)) {
- return (props: any) => React.cloneElement(target, props);
- }
- return () => target as React.ReactNode & null;
- };
-
- render() {
- const { component: Component } = this.state;
- const { ok, error, promise, ...rest } = this.props;
-
- return Component ? (
- <Component {...rest} />
- ) : (
- <div
- style={{
- width: '100%',
- height: '100%',
- margin: 'auto',
- paddingTop: 50,
- textAlign: 'center',
- }}
- >
- <Spin size="large" />
- </div>
- );
- }
-}
diff --git a/src/components/Authorized/Secured.tsx b/src/components/Authorized/Secured.tsx
deleted file mode 100644
index 0bdbbe4..0000000
--- a/src/components/Authorized/Secured.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import React from 'react';
-import CheckPermissions from './CheckPermissions';
-
-/**
- * 默认不能访问任何页面
- * default is "NULL"
- */
-const Exception403 = () => 403;
-
-export const isComponentClass = (component: React.ComponentClass | React.ReactNode): boolean => {
- if (!component) return false;
- const proto = Object.getPrototypeOf(component);
- if (proto === React.Component || proto === Function.prototype) return true;
- return isComponentClass(proto);
-};
-
-// Determine whether the incoming component has been instantiated
-// AuthorizedRoute is already instantiated
-// Authorized render is already instantiated, children is no instantiated
-// Secured is not instantiated
-const checkIsInstantiation = (target: React.ComponentClass | React.ReactNode) => {
- if (isComponentClass(target)) {
- const Target = target as React.ComponentClass;
- return (props: any) => <Target {...props} />;
- }
- if (React.isValidElement(target)) {
- return (props: any) => React.cloneElement(target, props);
- }
- return () => target;
-};
-
-/**
- * 用于判断是否拥有权限访问此 view 权限
- * authority 支持传入 string, () => boolean | Promise
- * e.g. 'user' 只有 user 用户能访问
- * e.g. 'user,admin' user 和 admin 都能访问
- * e.g. ()=>boolean 返回true能访问,返回false不能访问
- * e.g. Promise then 能访问 catch不能访问
- * e.g. authority support incoming string, () => boolean | Promise
- * e.g. 'user' only user user can access
- * e.g. 'user, admin' user and admin can access
- * e.g. () => boolean true to be able to visit, return false can not be accessed
- * e.g. Promise then can not access the visit to catch
- * @param {string | function | Promise} authority
- * @param {ReactNode} error 非必需参数
- */
-const authorize = (authority: string, error?: React.ReactNode) => {
- /**
- * conversion into a class
- * 防止传入字符串时找不到staticContext造成报错
- * String parameters can cause staticContext not found error
- */
- let classError: boolean | React.FunctionComponent = false;
- if (error) {
- classError = (() => error) as React.FunctionComponent;
- }
- if (!authority) {
- throw new Error('authority is required');
- }
- return function decideAuthority(target: React.ComponentClass | React.ReactNode) {
- const component = CheckPermissions(authority, target, classError || Exception403);
- return checkIsInstantiation(component);
- };
-};
-
-export default authorize;
diff --git a/src/components/Authorized/index.tsx b/src/components/Authorized/index.tsx
deleted file mode 100644
index 6703a46..0000000
--- a/src/components/Authorized/index.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import Authorized from './Authorized';
-import Secured from './Secured';
-import check from './CheckPermissions';
-import renderAuthorize from './renderAuthorize';
-
-Authorized.Secured = Secured;
-Authorized.check = check;
-
-const RenderAuthorize = renderAuthorize(Authorized);
-
-export default RenderAuthorize;
diff --git a/src/components/Authorized/renderAuthorize.ts b/src/components/Authorized/renderAuthorize.ts
deleted file mode 100644
index df00875..0000000
--- a/src/components/Authorized/renderAuthorize.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-/* eslint-disable eslint-comments/disable-enable-pair */
-/* eslint-disable import/no-mutable-exports */
-let CURRENT: string | string[] = 'NULL';
-
-type CurrentAuthorityType = string | string[] | (() => typeof CURRENT);
-/**
- * use authority or getAuthority
- * @param {string|()=>String} currentAuthority
- */
-const renderAuthorize = <T>(Authorized: T): ((currentAuthority: CurrentAuthorityType) => T) => (
- currentAuthority: CurrentAuthorityType,
-): T => {
- if (currentAuthority) {
- if (typeof currentAuthority === 'function') {
- CURRENT = currentAuthority();
- }
- if (
- Object.prototype.toString.call(currentAuthority) === '[object String]' ||
- Array.isArray(currentAuthority)
- ) {
- CURRENT = currentAuthority as string[];
- }
- } else {
- CURRENT = 'NULL';
- }
- return Authorized;
-};
-
-export { CURRENT };
-export default <T>(Authorized: T) => renderAuthorize<T>(Authorized);
diff --git a/src/components/Footer/index.tsx b/src/components/Footer/index.tsx
new file mode 100644
index 0000000..0f6648b
--- /dev/null
+++ b/src/components/Footer/index.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import { GithubOutlined } from '@ant-design/icons';
+import { DefaultFooter } from '@ant-design/pro-layout';
+
+export default () => (
+ <DefaultFooter
+ copyright="2020 Apache APISIX"
+ links={[
+ {
+ key: 'GitHub',
+ title: <GithubOutlined />,
+ href: 'https://github.com/apache/incubator-apisix',
+ blankTarget: true,
+ },
+ ]}
+ />
+);
diff --git a/src/components/GlobalHeader/AvatarDropdown.tsx b/src/components/GlobalHeader/AvatarDropdown.tsx
deleted file mode 100644
index 4cf5340..0000000
--- a/src/components/GlobalHeader/AvatarDropdown.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
-import { Avatar, Menu, Spin } from 'antd';
-import { ClickParam } from 'antd/es/menu';
-import React from 'react';
-import { history, connect } from 'umi';
-import { ConnectProps, ConnectState } from '@/models/connect';
-import { CurrentUser } from '@/models/user';
-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;
-
- if (dispatch) {
- dispatch({
- type: 'login/logout',
- });
- }
-
- return;
- }
-
- history.push(`/account/${key}`);
- };
-
- render(): React.ReactNode {
- const {
- currentUser = {
- avatar: '',
- name: '',
- },
- menu,
- } = this.props;
- const menuHeaderDropdown = (
- <Menu className={styles.menu} selectedKeys={[]} onClick={this.onMenuClick}>
- {menu && (
- <Menu.Item key="center">
- <UserOutlined />
- 个人中心
- </Menu.Item>
- )}
- {menu && (
- <Menu.Item key="settings">
- <SettingOutlined />
- 个人设置
- </Menu.Item>
- )}
- {menu && <Menu.Divider />}
-
- <Menu.Item key="logout">
- <LogoutOutlined />
- 退出登录
- </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);
diff --git a/src/components/GlobalHeader/NoticeIconView.tsx b/src/components/GlobalHeader/NoticeIconView.tsx
deleted file mode 100644
index f450087..0000000
--- a/src/components/GlobalHeader/NoticeIconView.tsx
+++ /dev/null
@@ -1,171 +0,0 @@
-import React, { Component } from 'react';
-import { Tag, message } from 'antd';
-import { connect } from 'umi';
-import groupBy from 'lodash/groupBy';
-import moment from 'moment';
-import { NoticeItem } from '@/models/global';
-import { CurrentUser } from '@/models/user';
-import { ConnectProps, ConnectState } from '@/models/connect';
-import NoticeIcon from '../NoticeIcon';
-import styles from './index.less';
-
-export interface GlobalHeaderRightProps extends ConnectProps {
- notices?: NoticeItem[];
- currentUser?: CurrentUser;
- fetchingNotices?: boolean;
- onNoticeVisibleChange?: (visible: boolean) => void;
- onNoticeClear?: (tabName?: string) => void;
-}
-
-class GlobalHeaderRight extends Component<GlobalHeaderRightProps> {
- componentDidMount() {
- const { dispatch } = this.props;
-
- if (dispatch) {
- dispatch({
- type: 'global/fetchNotices',
- });
- }
- }
-
- changeReadState = (clickedItem: NoticeItem): void => {
- const { id } = clickedItem;
- const { dispatch } = this.props;
-
- if (dispatch) {
- dispatch({
- type: 'global/changeNoticeReadState',
- payload: id,
- });
- }
- };
-
- handleNoticeClear = (title: string, key: string) => {
- const { dispatch } = this.props;
- message.success(`${'清空了'} ${title}`);
-
- if (dispatch) {
- dispatch({
- type: 'global/clearNotices',
- payload: key,
- });
- }
- };
-
- 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.keys(noticeData).forEach(key => {
- const value = noticeData[key];
-
- if (!unreadMsg[key]) {
- unreadMsg[key] = 0;
- }
-
- if (Array.isArray(value)) {
- unreadMsg[key] = value.filter(item => !item.read).length;
- }
- });
- return unreadMsg;
- };
-
- 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="清空"
- viewMoreText="查看更多"
- onClear={this.handleNoticeClear}
- onPopupVisibleChange={onNoticeVisibleChange}
- onViewMore={() => message.info('Click on view more')}
- clearClose
- >
- <NoticeIcon.Tab
- tabKey="notification"
- count={unreadMsg.notification}
- list={noticeData.notification}
- title="通知"
- emptyText="你已查看所有通知"
- showViewMore
- />
- <NoticeIcon.Tab
- tabKey="message"
- count={unreadMsg.message}
- list={noticeData.message}
- title="消息"
- emptyText="您已读完所有消息"
- showViewMore
- />
- <NoticeIcon.Tab
- tabKey="event"
- title="待办"
- emptyText="你已完成所有待办"
- 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);
diff --git a/src/components/GlobalHeader/RightContent.tsx b/src/components/GlobalHeader/RightContent.tsx
deleted file mode 100644
index 9841729..0000000
--- a/src/components/GlobalHeader/RightContent.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { Tooltip, Tag } from 'antd';
-import { QuestionCircleOutlined } from '@ant-design/icons';
-import React from 'react';
-import { connect } from 'umi';
-import { ConnectProps, ConnectState } from '@/models/connect';
-import Avatar from './AvatarDropdown';
-import SelectLang from '../SelectLang';
-import styles from './index.less';
-
-export type SiderTheme = 'light' | 'dark';
-export interface GlobalHeaderRightProps extends ConnectProps {
- theme?: SiderTheme;
- layout: 'sidemenu' | 'topmenu';
-}
-
-const ENVTagColor = {
- dev: 'orange',
- test: 'green',
- pre: '#87d068',
-};
-
-const GlobalHeaderRight: React.SFC<GlobalHeaderRightProps> = props => {
- const { theme, layout } = props;
- let className = styles.right;
-
- if (theme === 'dark' && layout === 'topmenu') {
- className = `${styles.right} ${styles.dark}`;
- }
-
- return (
- <div className={className}>
- <Tooltip title="Documentation">
- <a
- target="_blank"
- href="https://github.com/apache/incubator-apisix"
- rel="noopener noreferrer"
- className={styles.action}
- >
- <QuestionCircleOutlined />
- </a>
- </Tooltip>
- <Avatar />
- {REACT_APP_ENV && (
- <span>
- <Tag color={ENVTagColor[REACT_APP_ENV]}>{REACT_APP_ENV}</Tag>
- </span>
- )}
- <SelectLang className={styles.action} />
- </div>
- );
-};
-
-export default connect(({ settings }: ConnectState) => ({
- theme: settings.navTheme,
- layout: settings.layout,
-}))(GlobalHeaderRight);
diff --git a/src/components/RightContent/AvatarDropdown.tsx b/src/components/RightContent/AvatarDropdown.tsx
new file mode 100644
index 0000000..5555355
--- /dev/null
+++ b/src/components/RightContent/AvatarDropdown.tsx
@@ -0,0 +1,99 @@
+import React, { useCallback } from 'react';
+import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
+import { Avatar, Menu, Spin } from 'antd';
+import { ClickParam } from 'antd/es/menu';
+import { history, useModel } from 'umi';
+import { getPageQuery } from '@/utils/utils';
+
+import { stringify } from 'querystring';
+import HeaderDropdown from '../HeaderDropdown';
+import styles from './index.less';
+
+export interface GlobalHeaderRightProps {
+ menu?: boolean;
+}
+
+/**
+ * 退出登录,并且将当前的 url 保存
+ */
+const loginOut = async () => {
+ const { redirect } = getPageQuery();
+ // Note: There may be security issues, please note
+ if (window.location.pathname !== '/user/login' && !redirect) {
+ history.replace({
+ pathname: '/user/login',
+ search: stringify({
+ redirect: window.location.href,
+ }),
+ });
+ }
+};
+
+const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
+ const { initialState, setInitialState } = useModel('@@initialState');
+
+ const onMenuClick = useCallback((event: ClickParam) => {
+ const { key } = event;
+ if (key === 'logout') {
+ setInitialState({ ...initialState, currentUser: undefined });
+ loginOut();
+ return;
+ }
+ history.push(`/account/${key}`);
+ }, []);
+
+ const loading = (
+ <span className={`${styles.action} ${styles.account}`}>
+ <Spin
+ size="small"
+ style={{
+ marginLeft: 8,
+ marginRight: 8,
+ }}
+ />
+ </span>
+ );
+
+ if (!initialState) {
+ return loading;
+ }
+
+ const { currentUser } = initialState;
+
+ if (!currentUser || !currentUser.name) {
+ return loading;
+ }
+
+ const menuHeaderDropdown = (
+ <Menu className={styles.menu} selectedKeys={[]} onClick={onMenuClick}>
+ {menu && (
+ <Menu.Item key="center">
+ <UserOutlined />
+ 个人中心
+ </Menu.Item>
+ )}
+ {menu && (
+ <Menu.Item key="settings">
+ <SettingOutlined />
+ 个人设置
+ </Menu.Item>
+ )}
+ {menu && <Menu.Divider />}
+
+ <Menu.Item key="logout">
+ <LogoutOutlined />
+ 退出登录
+ </Menu.Item>
+ </Menu>
+ );
+ return (
+ <HeaderDropdown overlay={menuHeaderDropdown}>
+ <span className={`${styles.action} ${styles.account}`}>
+ <Avatar size="small" className={styles.avatar} src={currentUser.avatar} alt="avatar" />
+ <span className={`${styles.name} anticon`}>{currentUser.name}</span>
+ </span>
+ </HeaderDropdown>
+ );
+};
+
+export default AvatarDropdown;
diff --git a/src/components/GlobalHeader/index.less b/src/components/RightContent/index.less
similarity index 66%
rename from src/components/GlobalHeader/index.less
rename to src/components/RightContent/index.less
index 6a156db..ef2549e 100644
--- a/src/components/GlobalHeader/index.less
+++ b/src/components/RightContent/index.less
@@ -14,20 +14,17 @@
.right {
display: flex;
float: right;
- height: @layout-header-height;
+ height: 48px;
margin-left: auto;
overflow: hidden;
.action {
display: flex;
align-items: center;
- height: 100%;
+ height: 48px;
padding: 0 12px;
cursor: pointer;
transition: all 0.3s;
- > span {
- color: @text-color;
- vertical-align: middle;
- }
+
&:hover {
background: @pro-header-hover-bg;
}
@@ -43,7 +40,6 @@
}
.account {
.avatar {
- margin: ~'calc((@{layout-header-height} - 24px) / 2)' 0;
margin-right: 8px;
color: @primary-color;
vertical-align: top;
@@ -54,30 +50,11 @@
.dark {
.action {
- color: rgba(255, 255, 255, 0.85);
- > span {
- color: rgba(255, 255, 255, 0.85);
+ &:hover {
+ background: #252a3d;
}
- &:hover,
&:global(.opened) {
- background: @primary-color;
- }
- }
-}
-
-:global(.ant-pro-global-header) {
- .dark {
- .action {
- color: @text-color;
- > span {
- color: @text-color;
- }
- &:hover {
- color: rgba(255, 255, 255, 0.85);
- > span {
- color: rgba(255, 255, 255, 0.85);
- }
- }
+ background: #252a3d;
}
}
}
diff --git a/src/components/RightContent/index.tsx b/src/components/RightContent/index.tsx
new file mode 100644
index 0000000..8879e41
--- /dev/null
+++ b/src/components/RightContent/index.tsx
@@ -0,0 +1,72 @@
+import { Tooltip, Tag, Space } from 'antd';
+import { QuestionCircleOutlined } from '@ant-design/icons';
+import React from 'react';
+import { useModel, SelectLang } from 'umi';
+import Avatar from './AvatarDropdown';
+import HeaderSearch from '../HeaderSearch';
+import styles from './index.less';
+
+export type SiderTheme = 'light' | 'dark';
+
+const ENVTagColor = {
+ dev: 'orange',
+ test: 'green',
+ pre: '#87d068',
+};
+
+const GlobalHeaderRight: React.FC<{}> = () => {
+ const { initialState } = useModel('@@initialState');
+
+ if (!initialState || !initialState.settings) {
+ return null;
+ }
+
+ const { navTheme, layout } = initialState.settings;
+ let className = styles.right;
+
+ if ((navTheme === 'dark' && layout === 'top') || layout === 'mix') {
+ className = `${styles.right} ${styles.dark}`;
+ }
+ return (
+ <Space className={className}>
+ <HeaderSearch
+ className={`${styles.action} ${styles.search}`}
+ placeholder="站内搜索"
+ defaultValue="umi ui"
+ options={[
+ { label: <a href="https://umijs.org/zh/guide/umi-ui.html">umi ui</a>, value: 'umi ui' },
+ {
+ label: <a href="next.ant.design">Ant Design</a>,
+ value: 'Ant Design',
+ },
+ {
+ label: <a href="https://protable.ant.design/">Pro Table</a>,
+ value: 'Pro Table',
+ },
+ {
+ label: <a href="https://prolayout.ant.design/">Pro Layout</a>,
+ value: 'Pro Layout',
+ },
+ ]}
+ />
+ <Tooltip title="使用文档">
+ <span
+ className={styles.action}
+ onClick={() => {
+ window.location.href = 'https://pro.ant.design/docs/getting-started';
+ }}
+ >
+ <QuestionCircleOutlined />
+ </span>
+ </Tooltip>
+ <Avatar />
+ {REACT_APP_ENV && (
+ <span>
+ <Tag color={ENVTagColor[REACT_APP_ENV]}>{REACT_APP_ENV}</Tag>
+ </span>
+ )}
+ <SelectLang className={styles.action} />
+ </Space>
+ );
+};
+export default GlobalHeaderRight;
diff --git a/src/components/SelectLang/index.less b/src/components/SelectLang/index.less
deleted file mode 100644
index c0da9b4..0000000
--- a/src/components/SelectLang/index.less
+++ /dev/null
@@ -1,24 +0,0 @@
-@import '~antd/es/style/themes/default.less';
-
-.menu {
- :global(.anticon) {
- margin-right: 8px;
- }
- :global(.ant-dropdown-menu-item) {
- min-width: 160px;
- }
-}
-
-.dropDown {
- line-height: @layout-header-height;
- vertical-align: top;
- cursor: pointer;
- > span {
- font-size: 16px !important;
- transform: none !important;
- svg {
- position: relative;
- top: -1px;
- }
- }
-}
diff --git a/src/components/SelectLang/index.tsx b/src/components/SelectLang/index.tsx
deleted file mode 100644
index fbb6658..0000000
--- a/src/components/SelectLang/index.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import { GlobalOutlined } from '@ant-design/icons';
-import { Menu } from 'antd';
-import { getLocale, setLocale } from 'umi';
-import { ClickParam } from 'antd/es/menu';
-import React from 'react';
-import classNames from 'classnames';
-import HeaderDropdown from '../HeaderDropdown';
-import styles from './index.less';
-
-interface SelectLangProps {
- className?: string;
-}
-
-const SelectLang: React.FC<SelectLangProps> = props => {
- const { className } = props;
- const selectedLang = getLocale();
-
- const changeLang = ({ key }: ClickParam): void => setLocale(key);
-
- const locales = ['zh-CN', 'en-US'];
- const languageLabels = {
- 'zh-CN': '简体中文',
- 'en-US': 'English',
- };
- const languageIcons = {
- 'zh-CN': '🇨🇳',
- 'en-US': '🇺🇸',
- };
- const langMenu = (
- <Menu className={styles.menu} selectedKeys={[selectedLang]} onClick={changeLang}>
- {locales.map(locale => (
- <Menu.Item key={locale}>
- <span role="img" aria-label={languageLabels[locale]}>
- {languageIcons[locale]}
- </span>{' '}
- {languageLabels[locale]}
- </Menu.Item>
- ))}
- </Menu>
- );
- return (
- <HeaderDropdown overlay={langMenu} placement="bottomRight">
- <span className={classNames(styles.dropDown, className)}>
- <GlobalOutlined title="语言" />
- </span>
- </HeaderDropdown>
- );
-};
-
-export default SelectLang;
diff --git a/src/layouts/BasicLayout.tsx b/src/layouts/BasicLayout.tsx
deleted file mode 100644
index 6f68a6a..0000000
--- a/src/layouts/BasicLayout.tsx
+++ /dev/null
@@ -1,155 +0,0 @@
-import ProLayout, {
- MenuDataItem,
- BasicLayoutProps as ProLayoutProps,
- Settings,
- DefaultFooter,
-} from '@ant-design/pro-layout';
-import React, { useEffect } from 'react';
-import { Link, useIntl, connect, Dispatch } from 'umi';
-import { GithubOutlined } from '@ant-design/icons';
-import { Result, Button } from 'antd';
-import Authorized from '@/utils/Authorized';
-import RightContent from '@/components/GlobalHeader/RightContent';
-import { ConnectState } from '@/models/connect';
-import { getAuthorityFromRouter } from '@/utils/utils';
-import logo from '../assets/logo.svg';
-
-const noMatch = (
- <Result
- status={403}
- title="403"
- subTitle="Sorry, you are not authorized to access this page."
- extra={
- <Button type="primary">
- <Link to="/user/login">Go Login</Link>
- </Button>
- }
- />
-);
-export interface BasicLayoutProps extends ProLayoutProps {
- breadcrumbNameMap: {
- [path: string]: MenuDataItem;
- };
- route: ProLayoutProps['route'] & {
- authority: string[];
- };
- settings: Settings;
- dispatch: Dispatch;
-}
-export type BasicLayoutContext = { [K in 'location']: BasicLayoutProps[K] } & {
- breadcrumbNameMap: {
- [path: string]: MenuDataItem;
- };
-};
-/**
- * use Authorized check all menu item
- */
-
-const menuDataRender = (menuList: MenuDataItem[]): MenuDataItem[] =>
- menuList.map(item => {
- const localItem = { ...item, children: item.children ? menuDataRender(item.children) : [] };
- return Authorized.check(item.authority, localItem, null) as MenuDataItem;
- });
-
-const defaultFooterDom = (
- <DefaultFooter
- copyright="2020 Apache APISIX"
- links={[
- {
- key: 'GitHub',
- title: <GithubOutlined />,
- href: 'https://github.com/apache/incubator-apisix',
- blankTarget: true,
- },
- ]}
- />
-);
-
-const BasicLayout: React.FC<BasicLayoutProps> = props => {
- const {
- dispatch,
- children,
- settings,
- location = {
- pathname: '/',
- },
- } = props;
- /**
- * constructor
- */
-
- useEffect(() => {
- if (dispatch) {
- dispatch({
- type: 'user/fetchCurrent',
- });
- }
- }, []);
- /**
- * init variables
- */
-
- const handleMenuCollapse = (payload: boolean): void => {
- if (dispatch) {
- dispatch({
- type: 'global/changeLayoutCollapsed',
- payload,
- });
- }
- }; // get children authority
-
- const authorized = getAuthorityFromRouter(props.route.routes, location.pathname || '/') || {
- authority: undefined,
- };
- const { formatMessage } = useIntl();
-
- return (
- <ProLayout
- logo={logo}
- formatMessage={formatMessage}
- menuHeaderRender={(logoDom, titleDom) => (
- <Link to="/">
- {logoDom}
- {titleDom}
- </Link>
- )}
- onCollapse={handleMenuCollapse}
- menuItemRender={(menuItemProps, defaultDom) => {
- if (menuItemProps.isUrl || menuItemProps.children || !menuItemProps.path) {
- return defaultDom;
- }
-
- return <Link to={menuItemProps.path}>{defaultDom}</Link>;
- }}
- breadcrumbRender={(routers = []) => [
- {
- path: '/',
- breadcrumbName: formatMessage({ id: 'menu.home' }),
- },
- ...routers,
- ]}
- itemRender={(route, params, routes, paths) => {
- const first = routes.indexOf(route) === 0;
- return first ? (
- <Link to={paths.join('/')}>{route.breadcrumbName}</Link>
- ) : (
- <span>{route.breadcrumbName}</span>
- );
- }}
- footerRender={() => defaultFooterDom}
- menuDataRender={menuDataRender}
- rightContentRender={() => <RightContent />}
- {...props}
- {...settings}
- >
- <Authorized authority={authorized!.authority} noMatch={noMatch}>
- {children}
- </Authorized>
- </ProLayout>
- );
-};
-
-export default connect(({ global, settings }: ConnectState) => ({
- collapsed: global.collapsed,
- settings,
-}))(BasicLayout);
diff --git a/src/layouts/BlankLayout.tsx b/src/layouts/BlankLayout.tsx
deleted file mode 100644
index cdc55b0..0000000
--- a/src/layouts/BlankLayout.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import React from 'react';
-
-const Layout: React.FC = ({ children }) => <>{children}</>;
-
-export default Layout;
diff --git a/src/layouts/SecurityLayout.tsx b/src/layouts/SecurityLayout.tsx
deleted file mode 100644
index 6024b65..0000000
--- a/src/layouts/SecurityLayout.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import React from 'react';
-import { PageLoading } from '@ant-design/pro-layout';
-import { Redirect, connect } from 'umi';
-import { stringify } from 'querystring';
-import { ConnectState, ConnectProps } from '@/models/connect';
-import { CurrentUser } from '@/models/user';
-
-interface SecurityLayoutProps extends ConnectProps {
- loading?: boolean;
- currentUser?: CurrentUser;
-}
-
-interface SecurityLayoutState {
- isReady: boolean;
-}
-
-class SecurityLayout extends React.Component<SecurityLayoutProps, SecurityLayoutState> {
- state: SecurityLayoutState = {
- isReady: false,
- };
-
- componentDidMount() {
- this.setState({
- isReady: true,
- });
- const { dispatch } = this.props;
- if (dispatch) {
- dispatch({
- type: 'user/fetchCurrent',
- });
- }
- }
-
- render() {
- const { isReady } = this.state;
- const { children, loading, currentUser } = this.props;
- // You can replace it to your authentication rule (such as check token exists)
- // 你可以把它替换成你自己的登录认证规则(比如判断 token 是否存在)
- const isLogin = currentUser && currentUser.userid;
- const queryString = stringify({
- redirect: window.location.href,
- });
-
- if ((!isLogin && loading) || !isReady) {
- return <PageLoading />;
- }
- if (!isLogin && window.location.pathname !== '/user/login') {
- return <Redirect to={`/user/login?${queryString}`} />;
- }
- return children;
- }
-}
-
-export default connect(({ user, loading }: ConnectState) => ({
- currentUser: user.currentUser,
- loading: loading.models.user,
-}))(SecurityLayout);
diff --git a/src/layouts/UserLayout.less b/src/layouts/UserLayout.less
deleted file mode 100755
index cdc207e..0000000
--- a/src/layouts/UserLayout.less
+++ /dev/null
@@ -1,71 +0,0 @@
-@import '~antd/es/style/themes/default.less';
-
-.container {
- display: flex;
- flex-direction: column;
- height: 100vh;
- overflow: auto;
- background: @layout-body-background;
-}
-
-.lang {
- width: 100%;
- height: 40px;
- line-height: 44px;
- text-align: right;
- :global(.ant-dropdown-trigger) {
- margin-right: 24px;
- }
-}
-
-.content {
- flex: 1;
- padding: 32px 0;
-}
-
-@media (min-width: @screen-md-min) {
- .container {
- background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
- background-repeat: no-repeat;
- background-position: center 110px;
- background-size: 100%;
- }
-
- .content {
- padding: 32px 0 24px;
- }
-}
-
-.top {
- text-align: center;
-}
-
-.header {
- height: 44px;
- line-height: 44px;
- a {
- text-decoration: none;
- }
-}
-
-.logo {
- height: 44px;
- margin-right: 16px;
- vertical-align: top;
-}
-
-.title {
- position: relative;
- top: 2px;
- color: @heading-color;
- font-weight: 600;
- font-size: 33px;
- font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif;
-}
-
-.desc {
- margin-top: 12px;
- margin-bottom: 40px;
- color: @text-color-secondary;
- font-size: @font-size-base;
-}
diff --git a/src/layouts/UserLayout.tsx b/src/layouts/UserLayout.tsx
deleted file mode 100644
index c759ab3..0000000
--- a/src/layouts/UserLayout.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import { DefaultFooter, MenuDataItem, getMenuData, getPageTitle } from '@ant-design/pro-layout';
-import { Helmet, HelmetProvider } from 'react-helmet-async';
-import { Link, useIntl, ConnectProps, connect } from 'umi';
-import React from 'react';
-import { GithubOutlined } from '@ant-design/icons';
-import SelectLang from '@/components/SelectLang';
-import { ConnectState } from '@/models/connect';
-import logo from '../assets/logo.svg';
-import styles from './UserLayout.less';
-
-export interface UserLayoutProps extends Partial<ConnectProps> {
- breadcrumbNameMap: {
- [path: string]: MenuDataItem;
- };
-}
-
-const UserLayout: React.FC<UserLayoutProps> = props => {
- const {
- route = {
- routes: [],
- },
- } = props;
- const { routes = [] } = route;
- const {
- children,
- location = {
- pathname: '',
- },
- } = props;
- const { formatMessage } = useIntl();
- const { breadcrumb } = getMenuData(routes);
- const title = getPageTitle({
- pathname: location.pathname,
- formatMessage,
- breadcrumb,
- ...props,
- });
- return (
- <HelmetProvider>
- <Helmet>
- <title>{title}</title>
- <meta name="description" content={title} />
- </Helmet>
-
- <div className={styles.container}>
- <div className={styles.lang}>
- <SelectLang />
- </div>
- <div className={styles.content}>
- <div className={styles.top}>
- <div className={styles.header}>
- <Link to="/">
- <img alt="logo" className={styles.logo} src={logo} />
- <span className={styles.title}>APISIX Dashboard</span>
- </Link>
- </div>
- <div className={styles.desc}>Cloud-Native Microservices API Gateway</div>
- </div>
- {children}
- </div>
- <DefaultFooter
- copyright="2020 Apache APISIX"
- links={[
- {
- key: 'GitHub',
- title: <GithubOutlined />,
- href: 'https://github.com/apache/incubator-apisix',
- blankTarget: true,
- },
- ]}
- />
- </div>
- </HelmetProvider>
- );
-};
-
-export default connect(({ settings }: ConnectState) => ({ ...settings }))(UserLayout);
diff --git a/src/models/connect.d.ts b/src/models/connect.d.ts
deleted file mode 100644
index a0696dc..0000000
--- a/src/models/connect.d.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { MenuDataItem } from '@ant-design/pro-layout';
-import { GlobalModelState } from './global';
-import { DefaultSettings as SettingModelState } from '../../config/defaultSettings';
-import { UserModelState } from './user';
-import { StateType } from './login';
-
-export { GlobalModelState, SettingModelState, UserModelState };
-
-export interface Loading {
- global: boolean;
- effects: { [key: string]: boolean | undefined };
- models: {
- global?: boolean;
- menu?: boolean;
- setting?: boolean;
- user?: boolean;
- login?: boolean;
- };
-}
-
-export interface ConnectState {
- global: GlobalModelState;
- loading: Loading;
- settings: SettingModelState;
- user: UserModelState;
- login: StateType;
-}
-
-export interface Route extends MenuDataItem {
- routes?: Route[];
-}
diff --git a/src/models/global.ts b/src/models/global.ts
deleted file mode 100644
index a9432f6..0000000
--- a/src/models/global.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-import { Subscription, Effect, Reducer } from 'umi';
-
-import { queryNotices } from '@/services/user';
-import { ConnectState } from './connect.d';
-
-export interface NoticeItem extends API.NoticeIconData {
- id: string;
- type: string;
- status: string;
-}
-
-export interface GlobalModelState {
- collapsed: boolean;
- notices: NoticeItem[];
-}
-
-export interface GlobalModelType {
- namespace: 'global';
- state: GlobalModelState;
- effects: {
- fetchNotices: Effect;
- clearNotices: Effect;
- changeNoticeReadState: Effect;
- };
- reducers: {
- changeLayoutCollapsed: Reducer<GlobalModelState>;
- saveNotices: Reducer<GlobalModelState>;
- saveClearedNotices: Reducer<GlobalModelState>;
- };
- subscriptions: { setup: Subscription };
-}
-
-const GlobalModel: GlobalModelType = {
- namespace: 'global',
-
- state: {
- collapsed: false,
- notices: [],
- },
-
- effects: {
- *fetchNotices(_, { call, put, select }) {
- const data = yield call(queryNotices);
- yield put({
- type: 'saveNotices',
- payload: data,
- });
- const unreadCount: number = yield select(
- (state: ConnectState) => state.global.notices.filter((item) => !item.read).length,
- );
- yield put({
- type: 'user/changeNotifyCount',
- payload: {
- totalCount: data.length,
- unreadCount,
- },
- });
- },
- *clearNotices({ payload }, { put, select }) {
- yield put({
- type: 'saveClearedNotices',
- payload,
- });
- const count: number = yield select((state: ConnectState) => state.global.notices.length);
- const unreadCount: number = yield select(
- (state: ConnectState) => state.global.notices.filter((item) => !item.read).length,
- );
- yield put({
- type: 'user/changeNotifyCount',
- payload: {
- totalCount: count,
- unreadCount,
- },
- });
- },
- *changeNoticeReadState({ payload }, { put, select }) {
- const notices: NoticeItem[] = yield select((state: ConnectState) =>
- state.global.notices.map((item) => {
- const notice = { ...item };
- if (notice.id === payload) {
- notice.read = true;
- }
- return notice;
- }),
- );
-
- yield put({
- type: 'saveNotices',
- payload: notices,
- });
-
- yield put({
- type: 'user/changeNotifyCount',
- payload: {
- totalCount: notices.length,
- unreadCount: notices.filter((item) => !item.read).length,
- },
- });
- },
- },
-
- reducers: {
- changeLayoutCollapsed(state = { notices: [], collapsed: true }, { payload }): GlobalModelState {
- return {
- ...state,
- collapsed: payload,
- };
- },
- saveNotices(state, { payload }): GlobalModelState {
- return {
- collapsed: false,
- ...state,
- notices: payload,
- };
- },
- saveClearedNotices(state = { notices: [], collapsed: true }, { payload }): GlobalModelState {
- return {
- collapsed: false,
- ...state,
- notices: state.notices.filter((item): boolean => item.type !== payload),
- };
- },
- },
-
- subscriptions: {
- setup({ history }): void {
- // Subscribe history(url) change, trigger `load` action if pathname is `/`
- history.listen(({ pathname, search }): void => {
- if (typeof window.ga !== 'undefined') {
- window.ga('send', 'pageview', pathname + search);
- }
- });
- },
- },
-};
-
-export default GlobalModel;
diff --git a/src/models/login.ts b/src/models/login.ts
deleted file mode 100644
index 446f5ac..0000000
--- a/src/models/login.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import { stringify } from 'querystring';
-import { history, Effect, Reducer } from 'umi';
-
-import { fakeAccountLogin } from '@/services/login';
-import { setAuthority } from '@/utils/authority';
-import { getPageQuery } from '@/utils/utils';
-
-export interface StateType {
- status?: 'ok' | 'error';
- type?: string;
- currentAuthority?: 'user' | 'guest' | 'admin';
-}
-
-export interface LoginModelType {
- namespace: string;
- state: StateType;
- effects: {
- login: Effect;
- logout: Effect;
- };
- reducers: {
- changeLoginStatus: Reducer<StateType>;
- };
-}
-
-const Model: LoginModelType = {
- namespace: 'login',
-
- state: {
- status: undefined,
- },
-
- effects: {
- *login({ payload }, { call, put }) {
- const response = yield call(fakeAccountLogin, payload);
- yield put({
- type: 'changeLoginStatus',
- payload: response,
- });
- // Login successfully
- if (response.status === 'ok') {
- const urlParams = new URL(window.location.href);
- const params = getPageQuery();
- let { redirect } = params as { redirect: string };
- if (redirect) {
- const redirectUrlParams = new URL(redirect);
- if (redirectUrlParams.origin === urlParams.origin) {
- redirect = redirect.substr(urlParams.origin.length);
- if (redirect.match(/^\/.*#/)) {
- redirect = redirect.substr(redirect.indexOf('#') + 1);
- }
- } else {
- window.location.href = '/';
- return;
- }
- }
- history.replace(redirect || '/');
- }
- },
-
- logout() {
- const { redirect } = getPageQuery();
- // Note: There may be security issues, please note
- if (window.location.pathname !== '/user/login' && !redirect) {
- history.replace({
- pathname: '/user/login',
- search: stringify({
- redirect: window.location.href,
- }),
- });
- }
- },
- },
-
- reducers: {
- changeLoginStatus(state, { payload }) {
- setAuthority(payload.currentAuthority);
- return {
- ...state,
- status: payload.status,
- type: payload.type,
- };
- },
- },
-};
-
-export default Model;
diff --git a/src/models/setting.ts b/src/models/setting.ts
deleted file mode 100644
index 59fa70a..0000000
--- a/src/models/setting.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { Reducer } from 'umi';
-import defaultSettings, { DefaultSettings } from '../../config/defaultSettings';
-
-export interface SettingModelType {
- namespace: 'settings';
- state: DefaultSettings;
- reducers: {
- changeSetting: Reducer<DefaultSettings>;
- };
-}
-
-const updateColorWeak: (colorWeak: boolean) => void = colorWeak => {
- const root = document.getElementById('root');
- if (root) {
- root.className = colorWeak ? 'colorWeak' : '';
- }
-};
-
-const SettingModel: SettingModelType = {
- namespace: 'settings',
- state: defaultSettings,
- reducers: {
- changeSetting(state = defaultSettings, { payload }) {
- const { colorWeak, contentWidth } = payload;
-
- if (state.contentWidth !== contentWidth && window.dispatchEvent) {
- window.dispatchEvent(new Event('resize'));
- }
- updateColorWeak(!!colorWeak);
- return {
- ...state,
- ...payload,
- };
- },
- },
-};
-export default SettingModel;
diff --git a/src/models/user.ts b/src/models/user.ts
deleted file mode 100644
index ff5160d..0000000
--- a/src/models/user.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-import { Effect, Reducer } from 'umi';
-
-import { queryCurrent, query as queryUsers } from '@/services/user';
-
-export interface CurrentUser {
- avatar?: string;
- name?: string;
- title?: string;
- group?: string;
- signature?: string;
- tags?: {
- key: string;
- label: string;
- }[];
- userid?: string;
- unreadCount?: number;
-}
-
-export interface UserModelState {
- currentUser?: CurrentUser;
-}
-
-export interface UserModelType {
- namespace: 'user';
- state: UserModelState;
- effects: {
- fetch: Effect;
- fetchCurrent: Effect;
- };
- reducers: {
- saveCurrentUser: Reducer<UserModelState>;
- changeNotifyCount: Reducer<UserModelState>;
- };
-}
-
-const UserModel: UserModelType = {
- namespace: 'user',
-
- state: {
- currentUser: {},
- },
-
- effects: {
- *fetch(_, { call, put }) {
- const response = yield call(queryUsers);
- yield put({
- type: 'save',
- payload: response,
- });
- },
- *fetchCurrent(_, { call, put }) {
- const response = yield call(queryCurrent);
- yield put({
- type: 'saveCurrentUser',
- payload: response,
- });
- },
- },
-
- reducers: {
- saveCurrentUser(state, action) {
- return {
- ...state,
- currentUser: action.payload || {},
- };
- },
- changeNotifyCount(
- state = {
- currentUser: {},
- },
- action,
- ) {
- return {
- ...state,
- currentUser: {
- ...state.currentUser,
- notifyCount: action.payload.totalCount,
- unreadCount: action.payload.unreadCount,
- },
- };
- },
- },
-};
-
-export default UserModel;
diff --git a/src/pages/Authorized.tsx b/src/pages/Authorized.tsx
deleted file mode 100644
index 468543d..0000000
--- a/src/pages/Authorized.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import React from 'react';
-import { Redirect, connect, ConnectProps } from 'umi';
-import Authorized from '@/utils/Authorized';
-import { getRouteAuthority } from '@/utils/utils';
-import { ConnectState, UserModelState } from '@/models/connect';
-
-interface AuthComponentProps extends ConnectProps {
- user: UserModelState;
-}
-
-const AuthComponent: React.FC<AuthComponentProps> = ({
- children,
- route = {
- routes: [],
- },
- location = {
- pathname: '',
- },
- user,
-}) => {
- const { currentUser } = user;
- const { routes = [] } = route;
- const isLogin = currentUser && currentUser.name;
- return (
- <Authorized
- authority={getRouteAuthority(location.pathname, routes) || ''}
- noMatch={isLogin ? <Redirect to="/exception/403" /> : <Redirect to="/user/login" />}
- >
- {children}
- </Authorized>
- );
-};
-
-export default connect(({ user }: ConnectState) => ({
- user,
-}))(AuthComponent);
diff --git a/src/pages/user/login/components/Login/LoginItem.tsx b/src/pages/user/login/components/Login/LoginItem.tsx
index bd027b0..dd591ea 100644
--- a/src/pages/user/login/components/Login/LoginItem.tsx
+++ b/src/pages/user/login/components/Login/LoginItem.tsx
@@ -57,7 +57,7 @@ const getFormItemOptions = ({
return options;
};
-const LoginItem: React.FC<LoginItemProps> = props => {
+const LoginItem: React.FC<LoginItemProps> = (props) => {
const [count, setCount] = useState<number>(props.countDown || 0);
const [timing, setTiming] = useState(false);
// 这么写是为了防止restProps中 带入 onChange, defaultValue, rules props tabUtil
@@ -89,7 +89,7 @@ const LoginItem: React.FC<LoginItemProps> = props => {
const { countDown } = props;
if (timing) {
interval = window.setInterval(() => {
- setCount(preSecond => {
+ setCount((preSecond) => {
if (preSecond <= 1) {
setTiming(false);
clearInterval(interval);
@@ -113,7 +113,7 @@ const LoginItem: React.FC<LoginItemProps> = props => {
const inputProps = omit(otherProps, ['onGetCaptcha', 'countDown']);
return (
- <FormItem shouldUpdate>
+ <FormItem shouldUpdate noStyle>
{({ getFieldValue }) => (
<Row gutter={8}>
<Col span={16}>
@@ -148,11 +148,11 @@ const LoginItem: React.FC<LoginItemProps> = props => {
const LoginItems: Partial<LoginItemType> = {};
-Object.keys(ItemMap).forEach(key => {
+Object.keys(ItemMap).forEach((key) => {
const item = ItemMap[key];
LoginItems[key] = (props: LoginItemProps) => (
<LoginContext.Consumer>
- {context => (
+ {(context) => (
<LoginItem
customProps={item.props}
rules={item.rules}
diff --git a/src/pages/user/login/components/Login/LoginTab.tsx b/src/pages/user/login/components/Login/LoginTab.tsx
index 935f2fd..adbed2a 100644
--- a/src/pages/user/login/components/Login/LoginTab.tsx
+++ b/src/pages/user/login/components/Login/LoginTab.tsx
@@ -18,7 +18,7 @@ interface LoginTabProps extends TabPaneProps {
active?: boolean;
}
-const LoginTab: React.FC<LoginTabProps> = props => {
+const LoginTab: React.FC<LoginTabProps> = (props) => {
useEffect(() => {
const uniqueId = generateId('login-tab-');
const { tabUtil } = props;
@@ -32,9 +32,9 @@ const LoginTab: React.FC<LoginTabProps> = props => {
const WrapContext: React.FC<TabPaneProps> & {
typeName: string;
-} = props => (
+} = (props) => (
<LoginContext.Consumer>
- {value => <LoginTab tabUtil={value.tabUtil} {...props} />}
+ {(value) => <LoginTab tabUtil={value.tabUtil} {...props} />}
</LoginContext.Consumer>
);
diff --git a/src/pages/user/login/components/Login/index.tsx b/src/pages/user/login/components/Login/index.tsx
index 4148c25..c0ea0c5 100644
--- a/src/pages/user/login/components/Login/index.tsx
+++ b/src/pages/user/login/components/Login/index.tsx
@@ -30,10 +30,10 @@ interface LoginType extends React.FC<LoginProps> {
Captcha: React.FunctionComponent<LoginItemProps>;
}
-const Login: LoginType = props => {
+const Login: LoginType = (props) => {
const { className } = props;
const [tabs, setTabs] = useState<string[]>([]);
- const [active, setActive] = useState();
+ const [active, setActive] = useState({});
const [type, setType] = useMergeValue('', {
value: props.activeKey,
onChange: props.onTabChange,
@@ -57,14 +57,15 @@ const Login: LoginType = props => {
<LoginContext.Provider
value={{
tabUtil: {
- addTab: id => {
+ addTab: (id) => {
setTabs([...tabs, id]);
},
- removeTab: id => {
- setTabs(tabs.filter(currentId => currentId !== id));
+ removeTab: (id) => {
+ setTabs(tabs.filter((currentId) => currentId !== id));
},
},
- updateActive: activeItem => {
+ updateActive: (activeItem) => {
+ if (!active) return;
if (active[type]) {
active[type].push(activeItem);
} else {
@@ -77,7 +78,7 @@ const Login: LoginType = props => {
<div className={classNames(className, styles.login)}>
<Form
form={props.from}
- onFinish={values => {
+ onFinish={(values) => {
if (props.onSubmit) {
props.onSubmit(values as LoginParamsType);
}
@@ -89,7 +90,7 @@ const Login: LoginType = props => {
animated={false}
className={styles.tabs}
activeKey={type}
- onChange={activeKey => {
+ onChange={(activeKey) => {
setType(activeKey);
}}
>
diff --git a/src/pages/user/login/index.tsx b/src/pages/user/login/index.tsx
index 0690290..5010685 100644
--- a/src/pages/user/login/index.tsx
+++ b/src/pages/user/login/index.tsx
@@ -1,19 +1,14 @@
-import { Alert, Checkbox } from 'antd';
+import { AlipayCircleOutlined, TaobaoCircleOutlined, WeiboCircleOutlined } from '@ant-design/icons';
+import { Alert, Checkbox, message } from 'antd';
import React, { useState } from 'react';
-import { connect, Dispatch, useIntl, FormattedMessage } from 'umi';
-import { StateType } from '@/models/login';
-import { LoginParamsType } from '@/services/login';
-import { ConnectState } from '@/models/connect';
-import LoginForm from './components/Login';
-
+import { Link, SelectLang, history, useModel } from 'umi';
+import { getPageQuery } from '@/utils/utils';
+import logo from '@/assets/logo.svg';
+import { LoginParamsType, fakeAccountLogin } from '@/services/login';
+import LoginFrom from './components/Login';
import styles from './style.less';
-const { Tab, UserName, Password, Submit } = LoginForm;
-interface LoginProps {
- dispatch: Dispatch;
- userLogin: StateType;
- submitting?: boolean;
-}
+const { Tab, UserName, Password, Mobile, Captcha, Submit } = LoginFrom;
const LoginMessage: React.FC<{
content: string;
@@ -28,67 +23,162 @@ const LoginMessage: React.FC<{
/>
);
-const Login: React.FC<LoginProps> = (props) => {
- const { userLogin = {}, submitting } = props;
- const { status, type: loginType } = userLogin;
+/**
+ * 此方法会跳转到 redirect 参数所在的位置
+ */
+const replaceGoto = () => {
+ const urlParams = new URL(window.location.href);
+ const params = getPageQuery();
+ let { redirect } = params as { redirect: string };
+ if (redirect) {
+ const redirectUrlParams = new URL(redirect);
+ if (redirectUrlParams.origin === urlParams.origin) {
+ redirect = redirect.substr(urlParams.origin.length);
+ if (redirect.match(/^\/.*#/)) {
+ redirect = redirect.substr(redirect.indexOf('#') + 1);
+ }
+ } else {
+ window.location.href = '/';
+ return;
+ }
+ }
+ history.replace(redirect || '/');
+};
+
+const Login: React.FC<{}> = () => {
+ const [userLoginState, setUserLoginState] = useState<API.LoginStateType>({});
+ const [submitting, setSubmitting] = useState(false);
+
+ const { refresh } = useModel('@@initialState');
const [autoLogin, setAutoLogin] = useState(true);
- const [type, setType] = useState('account');
- const { formatMessage } = useIntl();
+ const [type, setType] = useState<string>('account');
- const handleSubmit = (values: LoginParamsType) => {
- const { dispatch } = props;
- dispatch({
- type: 'login/login',
- payload: { ...values, type },
- });
+ const handleSubmit = async (values: LoginParamsType) => {
+ setSubmitting(true);
+ try {
+ // 登录
+ const msg = await fakeAccountLogin({ ...values, type });
+ if (msg.status === 'ok') {
+ message.success('登陆成功!');
+ replaceGoto();
+ setTimeout(() => {
+ refresh();
+ }, 0);
+ return;
+ }
+ // 如果失败去设置用户错误信息
+ setUserLoginState(msg);
+ } catch (error) {
+ message.error('登陆失败,请重试!');
+ }
+ setSubmitting(false);
};
+
+ const { status, type: loginType } = userLoginState;
+
return (
- <div className={styles.main}>
- <LoginForm activeKey={type} onTabChange={setType} onSubmit={handleSubmit}>
- <Tab key="account" tab={formatMessage({ id: 'component.user.loginByPassword' })}>
- {status === 'error' && loginType === 'account' && !submitting && (
- <LoginMessage
- content={formatMessage({ id: 'component.user.wrongUsernameOrPassword' })}
- />
- )}
+ <div className={styles.container}>
+ <div className={styles.lang}>
+ <SelectLang />
+ </div>
+ <div className={styles.content}>
+ <div className={styles.top}>
+ <div className={styles.header}>
+ <Link to="/">
+ <img alt="logo" className={styles.logo} src={logo} />
+ <span className={styles.title}>Ant Design</span>
+ </Link>
+ </div>
+ <div className={styles.desc}>Ant Design 是西湖区最具影响力的 Web 设计规范</div>
+ </div>
+
+ <div className={styles.main}>
+ <LoginFrom activeKey={type} onTabChange={setType} onSubmit={handleSubmit}>
+ <Tab key="account" tab="账户密码登录">
+ {status === 'error' && loginType === 'account' && !submitting && (
+ <LoginMessage content="账户或密码错误(admin/ant.design)" />
+ )}
- <UserName
- name="userName"
- placeholder={formatMessage({ id: 'component.user.username' })}
- defaultValue="admin"
- rules={[
- {
- required: true,
- message: formatMessage({ id: 'component.user.inputUsername' }),
- },
- ]}
- />
- <Password
- name="password"
- placeholder={formatMessage({ id: 'component.user.password' })}
- defaultValue="123456"
- rules={[
- {
- required: true,
- message: formatMessage({ id: 'component.user.inputPassword' }),
- },
- ]}
- />
- </Tab>
- <div>
- <Checkbox checked={autoLogin} onChange={(e) => setAutoLogin(e.target.checked)}>
- <FormattedMessage id="component.user.rememberMe" />
- </Checkbox>
+ <UserName
+ name="userName"
+ placeholder="用户名: admin or user"
+ rules={[
+ {
+ required: true,
+ message: '请输入用户名!',
+ },
+ ]}
+ />
+ <Password
+ name="password"
+ placeholder="密码: ant.design"
+ rules={[
+ {
+ required: true,
+ message: '请输入密码!',
+ },
+ ]}
+ />
+ </Tab>
+ <Tab key="mobile" tab="手机号登录">
+ {status === 'error' && loginType === 'mobile' && !submitting && (
+ <LoginMessage content="验证码错误" />
+ )}
+ <Mobile
+ name="mobile"
+ placeholder="手机号"
+ rules={[
+ {
+ required: true,
+ message: '请输入手机号!',
+ },
+ {
+ pattern: /^1\d{10}$/,
+ message: '手机号格式错误!',
+ },
+ ]}
+ />
+ <Captcha
+ name="captcha"
+ placeholder="验证码"
+ countDown={120}
+ getCaptchaButtonText=""
+ getCaptchaSecondText="秒"
+ rules={[
+ {
+ required: true,
+ message: '请输入验证码!',
+ },
+ ]}
+ />
+ </Tab>
+ <div>
+ <Checkbox checked={autoLogin} onChange={(e) => setAutoLogin(e.target.checked)}>
+ 自动登录
+ </Checkbox>
+ <a
+ style={{
+ float: 'right',
+ }}
+ >
+ 忘记密码
+ </a>
+ </div>
+ <Submit loading={submitting}>登录</Submit>
+ <div className={styles.other}>
+ 其他登录方式
+ <AlipayCircleOutlined className={styles.icon} />
+ <TaobaoCircleOutlined className={styles.icon} />
+ <WeiboCircleOutlined className={styles.icon} />
+ <Link className={styles.register} to="/user/register">
+ 注册账户
+ </Link>
+ </div>
+ </LoginFrom>
</div>
- <Submit loading={submitting}>
- <FormattedMessage id="component.user.login" />
- </Submit>
- </LoginForm>
+ </div>
</div>
);
};
-export default connect(({ login, loading }: ConnectState) => ({
- userLogin: login,
- submitting: loading.effects['login/login'],
-}))(Login);
+export default Login;
diff --git a/src/pages/user/login/style.less b/src/pages/user/login/style.less
index d9bbbf3..64358ec 100644
--- a/src/pages/user/login/style.less
+++ b/src/pages/user/login/style.less
@@ -1,5 +1,75 @@
@import '~antd/es/style/themes/default.less';
+.container {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ overflow: auto;
+ background: @layout-body-background;
+}
+
+.lang {
+ width: 100%;
+ height: 40px;
+ line-height: 44px;
+ text-align: right;
+ :global(.ant-dropdown-trigger) {
+ margin-right: 24px;
+ }
+}
+
+.content {
+ flex: 1;
+ padding: 32px 0;
+}
+
+@media (min-width: @screen-md-min) {
+ .container {
+ background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
+ background-repeat: no-repeat;
+ background-position: center 110px;
+ background-size: 100%;
+ }
+
+ .content {
+ padding: 32px 0 24px;
+ }
+}
+
+.top {
+ text-align: center;
+}
+
+.header {
+ height: 44px;
+ line-height: 44px;
+ a {
+ text-decoration: none;
+ }
+}
+
+.logo {
+ height: 44px;
+ margin-right: 16px;
+ vertical-align: top;
+}
+
+.title {
+ position: relative;
+ top: 2px;
+ color: @heading-color;
+ font-weight: 600;
+ font-size: 33px;
+ font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif;
+}
+
+.desc {
+ margin-top: 12px;
+ margin-bottom: 40px;
+ color: @text-color-secondary;
+ font-size: @font-size-base;
+}
+
.main {
width: 368px;
margin: 0 auto;
diff --git a/src/utils/Authorized.ts b/src/utils/Authorized.ts
deleted file mode 100644
index e3b813a..0000000
--- a/src/utils/Authorized.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import RenderAuthorize from '../components/Authorized';
-import { getAuthority } from './authority';
-/* eslint-disable eslint-comments/disable-enable-pair */
-/* eslint-disable import/no-mutable-exports */
-let Authorized = RenderAuthorize(getAuthority());
-
-// Reload the rights component
-const reloadAuthorized = (): void => {
- Authorized = RenderAuthorize(getAuthority());
-};
-
-/**
- * hard code
- * block need it。
- */
-window.reloadAuthorized = reloadAuthorized;
-
-export { reloadAuthorized };
-export default Authorized;
diff --git a/src/utils/authority.ts b/src/utils/authority.ts
deleted file mode 100644
index d99659d..0000000
--- a/src/utils/authority.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { reloadAuthorized } from './Authorized';
-
-// use localStorage to store the authority info, which might be sent from server in actual project.
-export function getAuthority(str?: string): string | string[] {
- const authorityString =
- typeof str === 'undefined' && localStorage ? localStorage.getItem('antd-pro-authority') : str;
- // authorityString could be admin, "admin", ["admin"]
- let authority;
- try {
- if (authorityString) {
- authority = JSON.parse(authorityString);
- }
- } catch (e) {
- authority = authorityString;
- }
- if (typeof authority === 'string') {
- return [authority];
- }
- // preview.pro.ant.design only do not use in your production.
- // preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。
- if (!authority && ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site') {
- return ['admin'];
- }
- return authority;
-}
-
-export function setAuthority(authority: string | string[]): void {
- const proAuthority = typeof authority === 'string' ? [authority] : authority;
- localStorage.setItem('antd-pro-authority', JSON.stringify(proAuthority));
- // auto reload
- reloadAuthorized();
-}