修改注册银行账户 地址以及 融合样式改动
This commit is contained in:
35
src/components/Authorized/Authorized.tsx
Normal file
35
src/components/Authorized/Authorized.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { Result } from 'antd';
|
||||
import check from './CheckPermissions';
|
||||
import type { IAuthorityType } from './CheckPermissions';
|
||||
import type AuthorizedRoute from './AuthorizedRoute';
|
||||
import type Secured from './Secured';
|
||||
|
||||
type 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;
|
33
src/components/Authorized/AuthorizedRoute.tsx
Normal file
33
src/components/Authorized/AuthorizedRoute.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { Redirect, Route } from 'umi';
|
||||
|
||||
import React from 'react';
|
||||
import Authorized from './Authorized';
|
||||
import type { IAuthorityType } from './CheckPermissions';
|
||||
|
||||
type AuthorizedRouteProps = {
|
||||
currentAuthority: string;
|
||||
component: React.ComponentClass<any, any>;
|
||||
render: (props: any) => React.ReactNode;
|
||||
redirectPath: string;
|
||||
authority: IAuthorityType;
|
||||
};
|
||||
|
||||
const AuthorizedRoute: React.SFC<AuthorizedRouteProps> = ({
|
||||
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;
|
88
src/components/Authorized/CheckPermissions.tsx
Normal file
88
src/components/Authorized/CheckPermissions.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
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);
|
||||
|
||||
/**
|
||||
* @en-US
|
||||
* General permission check method
|
||||
* Common check permissions method
|
||||
* @param {Permission judgment} authority
|
||||
* @param {Your permission | Your permission description} currentAuthority
|
||||
* @param {Passing components} target
|
||||
* @param {no pass components | no pass components} Exception
|
||||
* -------------------------------------------------------
|
||||
* @zh-CN
|
||||
* 通用权限检查方法 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 => {
|
||||
// No judgment permission. View all by default
|
||||
// Retirement authority, return target;
|
||||
if (!authority) {
|
||||
return target;
|
||||
}
|
||||
// Array processing
|
||||
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;
|
||||
}
|
||||
// Deal with 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;
|
||||
}
|
||||
// Deal with promise
|
||||
if (authority instanceof Promise) {
|
||||
return <PromiseRender<T, K> ok={target} error={Exception} promise={authority} />;
|
||||
}
|
||||
// Deal with function
|
||||
if (typeof authority === 'function') {
|
||||
const bool = authority(currentAuthority);
|
||||
// The return value after the function is executed is Promise
|
||||
if (bool instanceof Promise) {
|
||||
return <PromiseRender<T, K> ok={target} error={Exception} promise={bool} />;
|
||||
}
|
||||
if (bool) {
|
||||
return target;
|
||||
}
|
||||
return Exception;
|
||||
}
|
||||
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;
|
96
src/components/Authorized/PromiseRender.tsx
Normal file
96
src/components/Authorized/PromiseRender.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import React from 'react';
|
||||
import { Spin } from 'antd';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { isComponentClass } from './Secured';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
|
||||
type PromiseRenderProps<T, K> = {
|
||||
ok: T;
|
||||
error: K;
|
||||
promise: Promise<boolean>;
|
||||
};
|
||||
|
||||
type PromiseRenderState = {
|
||||
component: React.ComponentClass | React.FunctionComponent;
|
||||
};
|
||||
|
||||
export default class PromiseRender<T, K> extends React.Component<
|
||||
PromiseRenderProps<T, K>,
|
||||
PromiseRenderState
|
||||
> {
|
||||
state: PromiseRenderState = {
|
||||
component: () => null,
|
||||
};
|
||||
|
||||
componentDidMount(): void {
|
||||
this.setRenderComponent(this.props);
|
||||
}
|
||||
|
||||
shouldComponentUpdate = (
|
||||
nextProps: PromiseRenderProps<T, K>,
|
||||
nextState: PromiseRenderState,
|
||||
): boolean => {
|
||||
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>): void {
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
80
src/components/Authorized/Secured.tsx
Normal file
80
src/components/Authorized/Secured.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import CheckPermissions from './CheckPermissions';
|
||||
|
||||
/**
|
||||
* @en-US No pages can be accessed by default,default is "NULL"
|
||||
* @zh-CN 默认不能访问任何页面 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;
|
||||
};
|
||||
|
||||
/**
|
||||
* @en-US
|
||||
* Used to determine whether you have permission to access this view permission
|
||||
* authority supports incoming string, () => boolean | Promise
|
||||
* e.g.'user' Only user user can access
|
||||
* e.g.'user,admin' user and admin can access
|
||||
* e.g. ()=>boolean return true to access, return false to not access
|
||||
* e.g. Promise then can be accessed, catch can not be accessed
|
||||
* 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
|
||||
*-------------------------------------------------------------
|
||||
* @zh-CN
|
||||
* 用于判断是否拥有权限访问此 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 non-required parameter
|
||||
*/
|
||||
const authorize = (authority: string, error?: React.ReactNode) => {
|
||||
/**
|
||||
* @en-US
|
||||
* conversion into a class
|
||||
* Prevent the staticContext from being found to cause an error when the string is passed in
|
||||
* String parameters can cause staticContext not found error
|
||||
*-------------------------------------------------------------
|
||||
* @zh-CN
|
||||
* 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;
|
11
src/components/Authorized/index.tsx
Normal file
11
src/components/Authorized/index.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
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;
|
31
src/components/Authorized/renderAuthorize.ts
Normal file
31
src/components/Authorized/renderAuthorize.ts
Normal file
@ -0,0 +1,31 @@
|
||||
/* 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);
|
@ -16,7 +16,7 @@ interface CompanyInfoProps {
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
const CompanyInfo: React.FC<CompanyInfoProps> = (props) => {
|
||||
const { viewType = false, record = '' } = props;
|
||||
const { viewType = false, record = '999698' } = props;
|
||||
const intl = useIntl();
|
||||
// 切换tab
|
||||
const [subTab, setSubTab] = useState<string>('base');
|
||||
|
@ -5,7 +5,7 @@ import request from '@/utils/request';
|
||||
/**
|
||||
* 供应商基本信息
|
||||
*/
|
||||
export const coscoSupplierBase = (id: string) => request.get(`/coscoSupplierBase/${id}`);
|
||||
export const coscoSupplierBase = (id: string) => request.get(`/coscoSupplierBase/${id? id: '999698'}`);
|
||||
|
||||
/**
|
||||
* 资质分页列表
|
||||
|
88
src/components/GlobalHeader/AvatarDropdown.tsx
Normal file
88
src/components/GlobalHeader/AvatarDropdown.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { Avatar, Menu, Spin } from 'antd';
|
||||
import React from 'react';
|
||||
import type { ConnectProps } from 'umi';
|
||||
import { history, connect } from 'umi';
|
||||
import type { ConnectState } from '@/models/connect';
|
||||
import type { CurrentUser } from '@/models/user';
|
||||
import HeaderDropdown from '../HeaderDropdown';
|
||||
import styles from './index.less';
|
||||
|
||||
export type GlobalHeaderRightProps = {
|
||||
currentUser?: CurrentUser;
|
||||
menu?: boolean;
|
||||
} & Partial<ConnectProps>;
|
||||
|
||||
class AvatarDropdown extends React.Component<GlobalHeaderRightProps> {
|
||||
onMenuClick = (event: { key: React.Key; keyPath: React.Key[]; item: React.ReactInstance }) => {
|
||||
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} anticon`}>{currentUser.name}</span>
|
||||
</span>
|
||||
</HeaderDropdown>
|
||||
) : (
|
||||
<span className={`${styles.action} ${styles.account}`}>
|
||||
<Spin
|
||||
size="small"
|
||||
style={{
|
||||
marginLeft: 8,
|
||||
marginRight: 8,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(({ user }: ConnectState) => ({
|
||||
currentUser: user.currentUser,
|
||||
}))(AvatarDropdown);
|
168
src/components/GlobalHeader/NoticeIconView.tsx
Normal file
168
src/components/GlobalHeader/NoticeIconView.tsx
Normal file
@ -0,0 +1,168 @@
|
||||
import { Component } from 'react';
|
||||
import type { ConnectProps } from 'umi';
|
||||
import { connect } from 'umi';
|
||||
import { Tag, message } from 'antd';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import moment from 'moment';
|
||||
import type { NoticeItem } from '@/models/global';
|
||||
import type { CurrentUser } from '@/models/user';
|
||||
import type { ConnectState } from '@/models/connect';
|
||||
import NoticeIcon from '../NoticeIcon';
|
||||
import styles from './index.less';
|
||||
|
||||
export type GlobalHeaderRightProps = {
|
||||
notices?: NoticeItem[];
|
||||
currentUser?: CurrentUser;
|
||||
fetchingNotices?: boolean;
|
||||
onNoticeVisibleChange?: (visible: boolean) => void;
|
||||
onNoticeClear?: (tabName?: string) => void;
|
||||
} & Partial<ConnectProps>;
|
||||
|
||||
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(`${'Emptied'} ${title}`);
|
||||
|
||||
if (dispatch) {
|
||||
dispatch({
|
||||
type: 'global/clearNotices',
|
||||
payload: key,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
getNoticeData = (): Record<string, NoticeItem[]> => {
|
||||
const { notices = [] } = this.props;
|
||||
|
||||
if (!notices || notices.length === 0 || !Array.isArray(notices)) {
|
||||
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: Record<string, NoticeItem[]>) => {
|
||||
const unreadMsg: Record<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="Empty"
|
||||
viewMoreText="See 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="Notification"
|
||||
emptyText="You have viewed all notifications"
|
||||
showViewMore
|
||||
/>
|
||||
<NoticeIcon.Tab
|
||||
tabKey="message"
|
||||
count={unreadMsg.message}
|
||||
list={noticeData.message}
|
||||
title="Message"
|
||||
emptyText="You have read all messages"
|
||||
showViewMore
|
||||
/>
|
||||
<NoticeIcon.Tab
|
||||
tabKey="event"
|
||||
title="To do"
|
||||
emptyText="You have completed all to-dos"
|
||||
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);
|
89
src/components/GlobalHeader/RightContent.tsx
Normal file
89
src/components/GlobalHeader/RightContent.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import { Tooltip, Tag } from 'antd';
|
||||
import type { Settings as ProSettings } from '@ant-design/pro-layout';
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import React from 'react';
|
||||
import type { ConnectProps } from 'umi';
|
||||
import { connect, SelectLang } from 'umi';
|
||||
import type { ConnectState } from '@/models/connect';
|
||||
import Avatar from './AvatarDropdown';
|
||||
import HeaderSearch from '../HeaderSearch';
|
||||
import styles from './index.less';
|
||||
|
||||
// import userIcon from '@/assets/user.svg';
|
||||
// import homeIcon from '@/assets/home.svg';
|
||||
// import shutdownIcon from '@/assets/shutdown.svg';
|
||||
// import divider from '@/assets/divider.png';
|
||||
// const theme = JSON.parse(PROJECT_THEME);
|
||||
|
||||
export type GlobalHeaderRightProps = {
|
||||
theme?: ProSettings['navTheme'] | 'realDark';
|
||||
} & Partial<ConnectProps> &
|
||||
Partial<ProSettings>;
|
||||
|
||||
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 === 'top') {
|
||||
className = `${styles.right} ${styles.dark}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<HeaderSearch
|
||||
className={`${styles.action} ${styles.search}`}
|
||||
placeholder="Site Search"
|
||||
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',
|
||||
},
|
||||
]}
|
||||
// onSearch={value => {
|
||||
// //console.log('input', value);
|
||||
// }}
|
||||
/>
|
||||
<Tooltip title="Use documentation">
|
||||
<a
|
||||
style={{
|
||||
color: 'inherit',
|
||||
}}
|
||||
target="_blank"
|
||||
href="https://pro.ant.design/docs/getting-started"
|
||||
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((state: ConnectState) => ({
|
||||
theme: state.settings?.navTheme,
|
||||
layout: state.settings?.layout,
|
||||
}))(GlobalHeaderRight);
|
96
src/components/GlobalHeader/index.less
Normal file
96
src/components/GlobalHeader/index.less
Normal file
@ -0,0 +1,96 @@
|
||||
@import '~antd/es/style/themes/default.less';
|
||||
|
||||
@pro-header-hover-bg: rgba(0, 0, 0, 0.025);
|
||||
|
||||
.menu {
|
||||
:global(.anticon) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
:global(.ant-dropdown-menu-item) {
|
||||
min-width: 160px;
|
||||
}
|
||||
}
|
||||
.left-logo {
|
||||
top: 16px;
|
||||
|
||||
img {
|
||||
margin-left: 32px;
|
||||
margin-right: 12px;
|
||||
top: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.right-btns {
|
||||
color: inherit;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.right {
|
||||
display: flex;
|
||||
float: right;
|
||||
height: 48px;
|
||||
margin-left: auto;
|
||||
overflow: hidden;
|
||||
.action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding: 0 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
> span {
|
||||
vertical-align: middle;
|
||||
}
|
||||
&:hover {
|
||||
background: @pro-header-hover-bg;
|
||||
}
|
||||
&:global(.opened) {
|
||||
background: @pro-header-hover-bg;
|
||||
}
|
||||
}
|
||||
.search {
|
||||
padding: 0 12px;
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
.account {
|
||||
.avatar {
|
||||
margin: ~'calc((@{layout-header-height} - 24px) / 2)' 0;
|
||||
margin-right: 8px;
|
||||
color: @primary-color;
|
||||
vertical-align: top;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.action {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
> span {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
&: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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
16
src/components/HeaderDropdown/index.less
Normal file
16
src/components/HeaderDropdown/index.less
Normal file
@ -0,0 +1,16 @@
|
||||
@import '~antd/es/style/themes/default.less';
|
||||
|
||||
.container > * {
|
||||
background-color: @popover-bg;
|
||||
border-radius: 4px;
|
||||
box-shadow: @shadow-1-down;
|
||||
}
|
||||
|
||||
@media screen and (max-width: @screen-xs) {
|
||||
.container {
|
||||
width: 100% !important;
|
||||
}
|
||||
.container > * {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
}
|
17
src/components/HeaderDropdown/index.tsx
Normal file
17
src/components/HeaderDropdown/index.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import type { DropDownProps } from 'antd/es/dropdown';
|
||||
import { Dropdown } from 'antd';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from './index.less';
|
||||
|
||||
export type HeaderDropdownProps = {
|
||||
overlayClassName?: string;
|
||||
overlay: React.ReactNode | (() => React.ReactNode) | any;
|
||||
placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter';
|
||||
} & Omit<DropDownProps, 'overlay'>;
|
||||
|
||||
const HeaderDropdown: React.FC<HeaderDropdownProps> = ({ overlayClassName: cls, ...restProps }) => (
|
||||
<Dropdown overlayClassName={classNames(styles.container, cls)} {...restProps} />
|
||||
);
|
||||
|
||||
export default HeaderDropdown;
|
30
src/components/HeaderSearch/index.less
Normal file
30
src/components/HeaderSearch/index.less
Normal file
@ -0,0 +1,30 @@
|
||||
@import '~antd/es/style/themes/default.less';
|
||||
|
||||
.headerSearch {
|
||||
.input {
|
||||
width: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
transition: width 0.3s, margin-left 0.3s;
|
||||
:global(.ant-select-selection) {
|
||||
background: transparent;
|
||||
}
|
||||
input {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
border: 0;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
&,
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-bottom: 1px solid @border-color-base;
|
||||
}
|
||||
&.show {
|
||||
width: 210px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
105
src/components/HeaderSearch/index.tsx
Normal file
105
src/components/HeaderSearch/index.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { AutoComplete, Input } from 'antd';
|
||||
import useMergedState from 'rc-util/es/hooks/useMergedState';
|
||||
import type { AutoCompleteProps } from 'antd/es/auto-complete';
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import styles from './index.less';
|
||||
|
||||
export type HeaderSearchProps = {
|
||||
onSearch?: (value?: string) => void;
|
||||
onChange?: (value?: string) => void;
|
||||
onVisibleChange?: (b: boolean) => void;
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
options: AutoCompleteProps['options'];
|
||||
defaultOpen?: boolean;
|
||||
open?: boolean;
|
||||
defaultValue?: string;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
const HeaderSearch: React.FC<HeaderSearchProps> = (props) => {
|
||||
const {
|
||||
className,
|
||||
defaultValue,
|
||||
onVisibleChange,
|
||||
placeholder,
|
||||
open,
|
||||
defaultOpen,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const inputRef = useRef<Input | null>(null);
|
||||
|
||||
const [value, setValue] = useMergedState<string | undefined>(defaultValue, {
|
||||
value: props.value,
|
||||
onChange: props.onChange,
|
||||
});
|
||||
|
||||
const [searchMode, setSearchMode] = useMergedState(defaultOpen ?? false, {
|
||||
value: props.open,
|
||||
onChange: onVisibleChange,
|
||||
});
|
||||
|
||||
const inputClass = classNames(styles.input, {
|
||||
[styles.show]: searchMode,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(className, styles.headerSearch)}
|
||||
onClick={() => {
|
||||
setSearchMode(true);
|
||||
if (searchMode && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}}
|
||||
onTransitionEnd={({ propertyName }) => {
|
||||
if (propertyName === 'width' && !searchMode) {
|
||||
if (onVisibleChange) {
|
||||
onVisibleChange(searchMode);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SearchOutlined
|
||||
key="Icon"
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
/>
|
||||
<AutoComplete
|
||||
key="AutoComplete"
|
||||
className={inputClass}
|
||||
value={value}
|
||||
style={{
|
||||
height: 28,
|
||||
marginTop: -6,
|
||||
}}
|
||||
options={restProps.options}
|
||||
onChange={setValue}
|
||||
>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
defaultValue={defaultValue}
|
||||
aria-label={placeholder}
|
||||
placeholder={placeholder}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (restProps.onSearch) {
|
||||
restProps.onSearch(value);
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
setSearchMode(false);
|
||||
}}
|
||||
/>
|
||||
</AutoComplete>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderSearch;
|
Reference in New Issue
Block a user