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