Browse Source
* ✨ feat: use ProForm replace compents form
* up version
* fix lint error
pull/7588/head
committed by
GitHub
15 changed files with 250 additions and 648 deletions
@ -0,0 +1,23 @@ |
|||||
|
export default { |
||||
|
'pages.layouts.userLayout.title': 'Ant Design 是西湖区最具影响力的 Web 设计规范', |
||||
|
'pages.login.accountLogin.tab': '账户密码登录', |
||||
|
'pages.login.accountLogin.errorMessage': '错误的用户名和密码(admin/ant.design)', |
||||
|
'pages.login.username.placeholder': '用户名: admin or user', |
||||
|
'pages.login.username.required': '用户名是必填项!', |
||||
|
'pages.login.password.placeholder': '密码: ant.design', |
||||
|
'pages.login.password.required': '密码是必填项!', |
||||
|
'pages.login.phoneLogin.tab': '手机号登录', |
||||
|
'pages.login.phoneLogin.errorMessage': '验证码错误', |
||||
|
'pages.login.phoneNumber.placeholder': '请输入手机号!', |
||||
|
'pages.login.phoneNumber.required': '手机号是必填项!', |
||||
|
'pages.login.phoneNumber.invalid': '不合法的手机号!', |
||||
|
'pages.login.captcha.placeholder': '请输入验证码!', |
||||
|
'pages.login.captcha.required': '验证码是必填项!', |
||||
|
'pages.login.phoneLogin.getVerificationCode': '获取验证码', |
||||
|
'pages.getCaptchaSecondText': '秒后重新获取', |
||||
|
'pages.login.rememberMe': '自动登录', |
||||
|
'pages.login.forgotPassword': '忘记密码 ?', |
||||
|
'pages.login.submit': '提交', |
||||
|
'pages.login.loginWith': '其他登录方式 :', |
||||
|
'pages.login.registerAccount': '注册账户', |
||||
|
}; |
||||
@ -1,13 +0,0 @@ |
|||||
import { createContext } from 'react'; |
|
||||
|
|
||||
export interface LoginContextProps { |
|
||||
tabUtil?: { |
|
||||
addTab: (id: string) => void; |
|
||||
removeTab: (id: string) => void; |
|
||||
}; |
|
||||
updateActive?: (activeItem: { [key: string]: string } | string) => void; |
|
||||
} |
|
||||
|
|
||||
const LoginContext: React.Context<LoginContextProps> = createContext({}); |
|
||||
|
|
||||
export default LoginContext; |
|
||||
@ -1,177 +0,0 @@ |
|||||
import { Button, Col, Input, Row, Form, message } from 'antd'; |
|
||||
import React, { useState, useCallback, useEffect } from 'react'; |
|
||||
import omit from 'omit.js'; |
|
||||
import { FormItemProps } from 'antd/es/form/FormItem'; |
|
||||
import { getFakeCaptcha } from '@/services/login'; |
|
||||
import { FormattedMessage } from 'umi'; |
|
||||
|
|
||||
import ItemMap from './map'; |
|
||||
import LoginContext, { LoginContextProps } from './LoginContext'; |
|
||||
import styles from './index.less'; |
|
||||
|
|
||||
export type WrappedLoginItemProps = LoginItemProps; |
|
||||
export type LoginItemKeyType = keyof typeof ItemMap; |
|
||||
export interface LoginItemType { |
|
||||
UserName: React.FC<WrappedLoginItemProps>; |
|
||||
Password: React.FC<WrappedLoginItemProps>; |
|
||||
Mobile: React.FC<WrappedLoginItemProps>; |
|
||||
Captcha: React.FC<WrappedLoginItemProps>; |
|
||||
} |
|
||||
|
|
||||
export interface LoginItemProps extends Partial<FormItemProps> { |
|
||||
name?: string; |
|
||||
style?: React.CSSProperties; |
|
||||
placeholder?: string; |
|
||||
buttonText?: React.ReactNode; |
|
||||
countDown?: number; |
|
||||
getCaptchaButtonText?: string; |
|
||||
getCaptchaSecondText?: string; |
|
||||
updateActive?: LoginContextProps['updateActive']; |
|
||||
type?: string; |
|
||||
defaultValue?: string; |
|
||||
customProps?: { [key: string]: unknown }; |
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void; |
|
||||
tabUtil?: LoginContextProps['tabUtil']; |
|
||||
} |
|
||||
|
|
||||
const FormItem = Form.Item; |
|
||||
|
|
||||
const getFormItemOptions = ({ |
|
||||
onChange, |
|
||||
defaultValue, |
|
||||
customProps = {}, |
|
||||
rules, |
|
||||
}: LoginItemProps) => { |
|
||||
const options: { |
|
||||
rules?: LoginItemProps['rules']; |
|
||||
onChange?: LoginItemProps['onChange']; |
|
||||
initialValue?: LoginItemProps['defaultValue']; |
|
||||
} = { |
|
||||
rules: rules || (customProps.rules as LoginItemProps['rules']), |
|
||||
}; |
|
||||
if (onChange) { |
|
||||
options.onChange = onChange; |
|
||||
} |
|
||||
if (defaultValue) { |
|
||||
options.initialValue = defaultValue; |
|
||||
} |
|
||||
return options; |
|
||||
}; |
|
||||
|
|
||||
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
|
|
||||
const { |
|
||||
onChange, |
|
||||
customProps, |
|
||||
defaultValue, |
|
||||
rules, |
|
||||
name, |
|
||||
getCaptchaButtonText, |
|
||||
getCaptchaSecondText, |
|
||||
updateActive, |
|
||||
type, |
|
||||
tabUtil, |
|
||||
...restProps |
|
||||
} = props; |
|
||||
|
|
||||
const onGetCaptcha = useCallback(async (mobile: string) => { |
|
||||
const result = await getFakeCaptcha(mobile); |
|
||||
if (result === false) { |
|
||||
return; |
|
||||
} |
|
||||
message.success('获取验证码成功!验证码为:1234'); |
|
||||
setTiming(true); |
|
||||
}, []); |
|
||||
|
|
||||
useEffect(() => { |
|
||||
let interval: number = 0; |
|
||||
const { countDown } = props; |
|
||||
if (timing) { |
|
||||
interval = window.setInterval(() => { |
|
||||
setCount((preSecond) => { |
|
||||
if (preSecond <= 1) { |
|
||||
setTiming(false); |
|
||||
clearInterval(interval); |
|
||||
// 重置秒数
|
|
||||
return countDown || 60; |
|
||||
} |
|
||||
return preSecond - 1; |
|
||||
}); |
|
||||
}, 1000); |
|
||||
} |
|
||||
return () => clearInterval(interval); |
|
||||
}, [timing]); |
|
||||
if (!name) { |
|
||||
return null; |
|
||||
} |
|
||||
// get getFieldDecorator props
|
|
||||
const options = getFormItemOptions(props); |
|
||||
const otherProps = restProps || {}; |
|
||||
|
|
||||
if (type === 'Captcha') { |
|
||||
const inputProps = omit(otherProps, ['onGetCaptcha', 'countDown']); |
|
||||
|
|
||||
return ( |
|
||||
<FormItem shouldUpdate noStyle> |
|
||||
{({ getFieldValue }) => ( |
|
||||
<Row gutter={8}> |
|
||||
<Col span={16}> |
|
||||
<FormItem name={name} {...options}> |
|
||||
<Input {...customProps} {...inputProps} /> |
|
||||
</FormItem> |
|
||||
</Col> |
|
||||
<Col span={8}> |
|
||||
<Button |
|
||||
disabled={timing} |
|
||||
className={styles.getCaptcha} |
|
||||
size="large" |
|
||||
onClick={() => { |
|
||||
const value = getFieldValue('mobile'); |
|
||||
onGetCaptcha(value); |
|
||||
}} |
|
||||
> |
|
||||
{timing ? ( |
|
||||
`${count} 秒` |
|
||||
) : ( |
|
||||
<FormattedMessage |
|
||||
id="pages.login.phoneLogin.getVerificationCode" |
|
||||
defaultMessage="获取验证码" |
|
||||
/> |
|
||||
)} |
|
||||
</Button> |
|
||||
</Col> |
|
||||
</Row> |
|
||||
)} |
|
||||
</FormItem> |
|
||||
); |
|
||||
} |
|
||||
return ( |
|
||||
<FormItem name={name} {...options}> |
|
||||
<Input {...customProps} {...otherProps} /> |
|
||||
</FormItem> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
const LoginItems: Partial<LoginItemType> = {}; |
|
||||
|
|
||||
Object.keys(ItemMap).forEach((key) => { |
|
||||
const item = ItemMap[key]; |
|
||||
LoginItems[key] = (props: LoginItemProps) => ( |
|
||||
<LoginContext.Consumer> |
|
||||
{(context) => ( |
|
||||
<LoginItem |
|
||||
customProps={item.props} |
|
||||
rules={item.rules} |
|
||||
{...props} |
|
||||
type={key} |
|
||||
{...context} |
|
||||
updateActive={context.updateActive} |
|
||||
/> |
|
||||
)} |
|
||||
</LoginContext.Consumer> |
|
||||
); |
|
||||
}); |
|
||||
|
|
||||
export default LoginItems as LoginItemType; |
|
||||
@ -1,23 +0,0 @@ |
|||||
import { Button, Form } from 'antd'; |
|
||||
|
|
||||
import { ButtonProps } from 'antd/es/button'; |
|
||||
import React from 'react'; |
|
||||
import classNames from 'classnames'; |
|
||||
import styles from './index.less'; |
|
||||
|
|
||||
const FormItem = Form.Item; |
|
||||
|
|
||||
interface LoginSubmitProps extends ButtonProps { |
|
||||
className?: string; |
|
||||
} |
|
||||
|
|
||||
const LoginSubmit: React.FC<LoginSubmitProps> = ({ className, ...rest }) => { |
|
||||
const clsString = classNames(styles.submit, className); |
|
||||
return ( |
|
||||
<FormItem> |
|
||||
<Button size="large" className={clsString} type="primary" htmlType="submit" {...rest} /> |
|
||||
</FormItem> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
export default LoginSubmit; |
|
||||
@ -1,45 +0,0 @@ |
|||||
import React, { useEffect } from 'react'; |
|
||||
import { Tabs } from 'antd'; |
|
||||
import LoginContext, { LoginContextProps } from './LoginContext'; |
|
||||
|
|
||||
const { TabPane } = Tabs; |
|
||||
|
|
||||
const generateId = (() => { |
|
||||
let i = 0; |
|
||||
return (prefix = '') => { |
|
||||
i += 1; |
|
||||
return `${prefix}${i}`; |
|
||||
}; |
|
||||
})(); |
|
||||
|
|
||||
type TabPaneProps = Parameters<typeof Tabs.TabPane>[0]; |
|
||||
|
|
||||
interface LoginTabProps extends TabPaneProps { |
|
||||
tabUtil: LoginContextProps['tabUtil']; |
|
||||
active?: boolean; |
|
||||
} |
|
||||
|
|
||||
const LoginTab: React.FC<LoginTabProps> = (props) => { |
|
||||
useEffect(() => { |
|
||||
const uniqueId = generateId('login-tab-'); |
|
||||
const { tabUtil } = props; |
|
||||
if (tabUtil) { |
|
||||
tabUtil.addTab(uniqueId); |
|
||||
} |
|
||||
}, []); |
|
||||
const { children } = props; |
|
||||
return <TabPane {...props}>{props.active && children}</TabPane>; |
|
||||
}; |
|
||||
|
|
||||
const WrapContext: React.FC<TabPaneProps> & { |
|
||||
typeName: string; |
|
||||
} = (props) => ( |
|
||||
<LoginContext.Consumer> |
|
||||
{(value) => <LoginTab tabUtil={value.tabUtil} {...props} />} |
|
||||
</LoginContext.Consumer> |
|
||||
); |
|
||||
|
|
||||
// 标志位 用来判断是不是自定义组件
|
|
||||
WrapContext.typeName = 'LoginTab'; |
|
||||
|
|
||||
export default WrapContext; |
|
||||
@ -1,49 +0,0 @@ |
|||||
@import '~antd/es/style/themes/default.less'; |
|
||||
|
|
||||
.login { |
|
||||
:global { |
|
||||
.ant-tabs .ant-tabs-bar { |
|
||||
margin-bottom: 24px; |
|
||||
text-align: center; |
|
||||
border-bottom: 0; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.getCaptcha { |
|
||||
display: block; |
|
||||
width: 100%; |
|
||||
} |
|
||||
|
|
||||
.icon { |
|
||||
margin-left: 16px; |
|
||||
color: rgba(0, 0, 0, 0.2); |
|
||||
font-size: 24px; |
|
||||
vertical-align: middle; |
|
||||
cursor: pointer; |
|
||||
transition: color 0.3s; |
|
||||
|
|
||||
&:hover { |
|
||||
color: @primary-color; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.other { |
|
||||
margin-top: 24px; |
|
||||
line-height: 22px; |
|
||||
text-align: left; |
|
||||
|
|
||||
.register { |
|
||||
float: right; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.prefixIcon { |
|
||||
color: @disabled-color; |
|
||||
font-size: @font-size-base; |
|
||||
} |
|
||||
|
|
||||
.submit { |
|
||||
width: 100%; |
|
||||
margin-top: 24px; |
|
||||
} |
|
||||
} |
|
||||
@ -1,119 +0,0 @@ |
|||||
import { Tabs, Form } from 'antd'; |
|
||||
import React, { useState } from 'react'; |
|
||||
import useMergeValue from 'use-merge-value'; |
|
||||
import classNames from 'classnames'; |
|
||||
import { FormInstance } from 'antd/es/form'; |
|
||||
import { LoginParamsType } from '@/services/login'; |
|
||||
|
|
||||
import LoginContext from './LoginContext'; |
|
||||
import LoginItem, { LoginItemProps } from './LoginItem'; |
|
||||
import LoginSubmit from './LoginSubmit'; |
|
||||
import LoginTab from './LoginTab'; |
|
||||
import styles from './index.less'; |
|
||||
|
|
||||
export interface LoginProps { |
|
||||
activeKey?: string; |
|
||||
onTabChange?: (key: string) => void; |
|
||||
style?: React.CSSProperties; |
|
||||
onSubmit?: (values: LoginParamsType) => void; |
|
||||
className?: string; |
|
||||
from?: FormInstance; |
|
||||
children: React.ReactElement<typeof LoginTab>[]; |
|
||||
} |
|
||||
|
|
||||
interface LoginType extends React.FC<LoginProps> { |
|
||||
Tab: typeof LoginTab; |
|
||||
Submit: typeof LoginSubmit; |
|
||||
UserName: React.FunctionComponent<LoginItemProps>; |
|
||||
Password: React.FunctionComponent<LoginItemProps>; |
|
||||
Mobile: React.FunctionComponent<LoginItemProps>; |
|
||||
Captcha: React.FunctionComponent<LoginItemProps>; |
|
||||
} |
|
||||
|
|
||||
const Login: LoginType = (props) => { |
|
||||
const { className } = props; |
|
||||
const [tabs, setTabs] = useState<string[]>([]); |
|
||||
const [active, setActive] = useState({}); |
|
||||
const [type, setType] = useMergeValue('', { |
|
||||
value: props.activeKey, |
|
||||
onChange: props.onTabChange, |
|
||||
}); |
|
||||
const TabChildren: React.ReactComponentElement<typeof LoginTab>[] = []; |
|
||||
const otherChildren: React.ReactElement<unknown>[] = []; |
|
||||
React.Children.forEach( |
|
||||
props.children, |
|
||||
(child: React.ReactComponentElement<typeof LoginTab> | React.ReactElement<unknown>) => { |
|
||||
if (!child) { |
|
||||
return; |
|
||||
} |
|
||||
if ((child.type as { typeName: string }).typeName === 'LoginTab') { |
|
||||
TabChildren.push(child as React.ReactComponentElement<typeof LoginTab>); |
|
||||
} else { |
|
||||
otherChildren.push(child); |
|
||||
} |
|
||||
}, |
|
||||
); |
|
||||
return ( |
|
||||
<LoginContext.Provider |
|
||||
value={{ |
|
||||
tabUtil: { |
|
||||
addTab: (id) => { |
|
||||
setTabs([...tabs, id]); |
|
||||
}, |
|
||||
removeTab: (id) => { |
|
||||
setTabs(tabs.filter((currentId) => currentId !== id)); |
|
||||
}, |
|
||||
}, |
|
||||
updateActive: (activeItem) => { |
|
||||
if (!active) return; |
|
||||
if (active[type]) { |
|
||||
active[type].push(activeItem); |
|
||||
} else { |
|
||||
active[type] = [activeItem]; |
|
||||
} |
|
||||
setActive(active); |
|
||||
}, |
|
||||
}} |
|
||||
> |
|
||||
<div className={classNames(className, styles.login)}> |
|
||||
<Form |
|
||||
form={props.from} |
|
||||
onFinish={(values) => { |
|
||||
if (props.onSubmit) { |
|
||||
props.onSubmit(values as LoginParamsType); |
|
||||
} |
|
||||
}} |
|
||||
> |
|
||||
{tabs.length ? ( |
|
||||
<React.Fragment> |
|
||||
<Tabs |
|
||||
destroyInactiveTabPane |
|
||||
animated={false} |
|
||||
className={styles.tabs} |
|
||||
activeKey={type} |
|
||||
onChange={(activeKey) => { |
|
||||
setType(activeKey); |
|
||||
}} |
|
||||
> |
|
||||
{TabChildren} |
|
||||
</Tabs> |
|
||||
{otherChildren} |
|
||||
</React.Fragment> |
|
||||
) : ( |
|
||||
props.children |
|
||||
)} |
|
||||
</Form> |
|
||||
</div> |
|
||||
</LoginContext.Provider> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
Login.Tab = LoginTab; |
|
||||
Login.Submit = LoginSubmit; |
|
||||
|
|
||||
Login.UserName = LoginItem.UserName; |
|
||||
Login.Password = LoginItem.Password; |
|
||||
Login.Mobile = LoginItem.Mobile; |
|
||||
Login.Captcha = LoginItem.Captcha; |
|
||||
|
|
||||
export default Login; |
|
||||
@ -1,72 +0,0 @@ |
|||||
import { LockTwoTone, MailTwoTone, MobileTwoTone, UserOutlined } from '@ant-design/icons'; |
|
||||
import React from 'react'; |
|
||||
import styles from './index.less'; |
|
||||
|
|
||||
export default { |
|
||||
UserName: { |
|
||||
props: { |
|
||||
size: 'large', |
|
||||
id: 'userName', |
|
||||
prefix: ( |
|
||||
<UserOutlined |
|
||||
style={{ |
|
||||
color: '#1890ff', |
|
||||
}} |
|
||||
className={styles.prefixIcon} |
|
||||
/> |
|
||||
), |
|
||||
placeholder: 'admin', |
|
||||
}, |
|
||||
rules: [ |
|
||||
{ |
|
||||
required: true, |
|
||||
message: 'Please enter username!', |
|
||||
}, |
|
||||
], |
|
||||
}, |
|
||||
Password: { |
|
||||
props: { |
|
||||
size: 'large', |
|
||||
prefix: <LockTwoTone className={styles.prefixIcon} />, |
|
||||
type: 'password', |
|
||||
id: 'password', |
|
||||
placeholder: '888888', |
|
||||
}, |
|
||||
rules: [ |
|
||||
{ |
|
||||
required: true, |
|
||||
message: 'Please enter password!', |
|
||||
}, |
|
||||
], |
|
||||
}, |
|
||||
Mobile: { |
|
||||
props: { |
|
||||
size: 'large', |
|
||||
prefix: <MobileTwoTone className={styles.prefixIcon} />, |
|
||||
placeholder: 'mobile number', |
|
||||
}, |
|
||||
rules: [ |
|
||||
{ |
|
||||
required: true, |
|
||||
message: 'Please enter mobile number!', |
|
||||
}, |
|
||||
{ |
|
||||
pattern: /^1\d{10}$/, |
|
||||
message: 'Wrong mobile number format!', |
|
||||
}, |
|
||||
], |
|
||||
}, |
|
||||
Captcha: { |
|
||||
props: { |
|
||||
size: 'large', |
|
||||
prefix: <MailTwoTone className={styles.prefixIcon} />, |
|
||||
placeholder: 'captcha', |
|
||||
}, |
|
||||
rules: [ |
|
||||
{ |
|
||||
required: true, |
|
||||
message: 'Please enter Captcha!', |
|
||||
}, |
|
||||
], |
|
||||
}, |
|
||||
}; |
|
||||
Loading…
Reference in new issue