Login & Register

This commit is contained in:
Limo Saplf
2019-09-01 21:56:21 +08:00
parent d9a8408535
commit 4480fc8e19
12 changed files with 231 additions and 23 deletions

View File

@@ -26,7 +26,7 @@ const AccountItem: React.FC<AccountItemProps> = ({
<div className={`${styles.item} ${className}`} onClick={onClick}> <div className={`${styles.item} ${className}`} onClick={onClick}>
<span className={styles.title}>{titleText}</span> <span className={styles.title}>{titleText}</span>
<span className={styles.message}>{message}</span> <span className={styles.message}>{message}</span>
<span className={styles.preview}>{preview}</span> {preview && <span className={styles.preview}>{preview}</span>}
{showGo && <Icon className={styles.go} type="right" />} {showGo && <Icon className={styles.go} type="right" />}
</div> </div>
); );

View File

@@ -26,6 +26,9 @@
flex-basis: 0; flex-basis: 0;
margin: 0 10px; margin: 0 10px;
color: @primary-color; color: @primary-color;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.preview { .preview {

View File

@@ -8,6 +8,7 @@ export default {
'common.phoneHolder': 'Please input phone number', 'common.phoneHolder': 'Please input phone number',
'common.pwdHolder': 'Please input password', 'common.pwdHolder': 'Please input password',
'common.codeHolder': '请输入验证码',
'common.next': 'Next', 'common.next': 'Next',
'common.pullToRefresh': 'Pull to refresh', 'common.pullToRefresh': 'Pull to refresh',
'common.releaseToRefresh': 'Release to refresh', 'common.releaseToRefresh': 'Release to refresh',
@@ -24,6 +25,17 @@ export default {
'check.msgArea': 'Please select area', 'check.msgArea': 'Please select area',
'check.msgPhone': 'Please input correct phone number', 'check.msgPhone': 'Please input correct phone number',
'check.msgPwd': '请输入正确的密码', 'check.msgPwd': '请输入正确的密码',
'check.msgCode': '请输入验证码',
'check.msgConfirm': '两次输入密码不一致',
'check.msgInviter': '请输入邀请码',
'check.titleRegister': '新用户注册',
'check.titleReset': '重置密码',
'check.getCode1': '获取验证码',
'check.getCode2': '{time}秒',
'check.getCode3': '重新获取',
'check.setPwd': '设置登录密码',
'check.confirmPwd': '确认登录密码',
'check.invite': '邀请码',
'home.tabHome': 'Home', 'home.tabHome': 'Home',
'home.tabEstate': '矿场', 'home.tabEstate': '矿场',

View File

@@ -8,6 +8,7 @@ export default {
'common.phoneHolder': '请输入手机号', 'common.phoneHolder': '请输入手机号',
'common.pwdHolder': '请输入密码', 'common.pwdHolder': '请输入密码',
'common.codeHolder': '请输入验证码',
'common.next': '下一步', 'common.next': '下一步',
'common.pullToRefresh': '下拉刷新', 'common.pullToRefresh': '下拉刷新',
'common.releaseToRefresh': '释放以刷新', 'common.releaseToRefresh': '释放以刷新',
@@ -24,6 +25,17 @@ export default {
'check.msgArea': '请选择区号', 'check.msgArea': '请选择区号',
'check.msgPhone': '请输入正确的手机号', 'check.msgPhone': '请输入正确的手机号',
'check.msgPwd': '请输入正确的密码', 'check.msgPwd': '请输入正确的密码',
'check.msgCode': '请输入验证码',
'check.msgConfirm': '两次输入密码不一致',
'check.msgInviter': '请输入邀请码',
'check.titleRegister': '新用户注册',
'check.titleReset': '重置密码',
'check.getCode1': '获取验证码',
'check.getCode2': '{time} 秒后重新发送',
'check.getCode3': '重新获取',
'check.setPwd': '设置登录密码',
'check.confirmPwd': '确认登录密码',
'check.invite': '邀请码',
'home.tabHome': '首页', 'home.tabHome': '首页',
'home.tabEstate': '矿场', 'home.tabEstate': '矿场',

View File

@@ -40,22 +40,35 @@ const Page: React.FC<DispatchProp & CheckModelState> = ({
); );
} }
let title;
if (pageStep === PageStep.Register) {
title = formatMessage({ id: 'check.titleRegister' });
} else if (pageStep === PageStep.Reset || pageStep === PageStep.ResetSuccess) {
title = formatMessage({ id: 'check.titleReset' });
}
return ( return (
<div className={styles.check}> <div className={styles.check}>
{ {
pageStep !== PageStep.Check && pageStep !== PageStep.Check &&
<Header <Header
title={title}
className={styles.header} className={styles.header}
onReturn={() => dispatch({ type: 'check/back' })} onReturn={() => dispatch({ type: 'check/back' })}
/> />
} }
<div className="cover"> {(
<div className="image"> pageStep === PageStep.Check ||
<FittedImage fit="contain" src={bgPic} /> pageStep === PageStep.Login
) && (
<div className="cover">
<div className="image">
<FittedImage fit="contain" src={bgPic} />
</div>
<span>{formatMessage({ id: 'app.name' })}</span>
</div> </div>
<span>{formatMessage({ id: 'app.name' })}</span> )}
</div>
{{ {{
[PageStep.Check]: <CheckPage />, [PageStep.Check]: <CheckPage />,

View File

@@ -21,7 +21,7 @@ const LoginPage: React.FC<DispatchProp & CheckModelState> = ({
/> />
<Button <Button
className="item gradient" className="item gradient submit"
type="primary" type="primary"
loading={submitting} loading={submitting}
onClick={() => dispatch({ type: 'check/submitLogin' })} onClick={() => dispatch({ type: 'check/submitLogin' })}

View File

@@ -5,7 +5,7 @@ import { delay } from 'dva/saga';
import { PageState, AreaCode, AreaCodes } from '@/types/common'; import { PageState, AreaCode, AreaCodes } from '@/types/common';
import { getAreaCodes, getUserInfo } from '@/services/common'; import { getAreaCodes, getUserInfo } from '@/services/common';
import { validPhoneInput, validPwdInput, validPhone, validPwd } from '@/utils/validate'; import { validPhoneInput, validPwdInput, validPhone, validPwd } from '@/utils/validate';
import { checkExistence, CheckResult, signIn } from './service'; import { checkExistence, CheckResult, signIn, signUp, sendCode } from './service';
import { formatMessage } from 'umi-plugin-locale'; import { formatMessage } from 'umi-plugin-locale';
export enum PageStep { export enum PageStep {
@@ -23,7 +23,12 @@ export interface CheckModelState {
areacode: AreaCode | undefined, // 选中的地区 areacode: AreaCode | undefined, // 选中的地区
phone: string; phone: string;
password: string; password: string;
pwdConfirm: string;
inviter: string;
verifyCode: string;
submitting: boolean; submitting: boolean;
sendingCode: boolean;
codeTimer: number;
} }
const model: Model = { const model: Model = {
@@ -36,7 +41,12 @@ const model: Model = {
areacode: undefined, areacode: undefined,
phone: '', phone: '',
password: '', password: '',
pwdConfirm: '',
inviter: '',
verifyCode: '',
submitting: false, submitting: false,
sendingCode: false,
codeTimer: -1,
} as CheckModelState, } as CheckModelState,
effects: { effects: {
@@ -71,6 +81,15 @@ const model: Model = {
} }
}, },
*timer({ payload }: any, { put }) {
yield put({ type: 'onUpdateState', codeTimer: payload });
yield delay(1000);
const next = payload - 1;
if (next >= 0) {
yield put({ type: 'timer', payload: next });
}
},
*submitCheck(_, { put, call, select }) { *submitCheck(_, { put, call, select }) {
const { areacode, phone } = yield select((state: any) => state.check); const { areacode, phone } = yield select((state: any) => state.check);
if (!areacode) return message.warn(formatMessage({ id: 'check.msgArea' })); if (!areacode) return message.warn(formatMessage({ id: 'check.msgArea' }));
@@ -97,26 +116,61 @@ const model: Model = {
const { areacode, phone, password } = yield select((state: any) => state.check); const { areacode, phone, password } = yield select((state: any) => state.check);
if (!areacode) return message.warn(formatMessage({ id: 'check.msgArea' })); if (!areacode) return message.warn(formatMessage({ id: 'check.msgArea' }));
if (!validPhone(phone)) return message.warn(formatMessage({ id: 'check.msgPhone' })); if (!validPhone(phone)) return message.warn(formatMessage({ id: 'check.msgPhone' }));
if (!validPwd(password)) return message.warn(formatMessage({ id: 'check.msgPwd' })) if (!validPwd(password)) return message.warn(formatMessage({ id: 'check.msgPwd' }));
yield put({ type: 'onUpdateState', submitting: true }); yield put({ type: 'onUpdateState', submitting: true });
try { try {
const token = yield call(signIn, areacode, phone, password); const token = yield call(signIn, areacode, phone, password);
yield put({ type: 'user/updateToken', payload: token }); yield put({ type: 'user/updateToken', payload: token });
yield delay(0);
const user = yield call(getUserInfo, true); const user = yield call(getUserInfo, true);
yield put({ type: 'user/updateUser', payload: user }); yield put({ type: 'user/updateUser', payload: user });
} catch(e) { } catch(e) {
message.error(e.message); message.error(e.message);
yield put({ type: 'user/logout' });
} finally { } finally {
yield put({ type: 'onUpdateState', submitting: false }); yield put({ type: 'onUpdateState', submitting: false });
} }
}, },
*submitRegister(_, { put }) { *submitRegister(_, { put, select, call }) {
const { areacode, phone, password, verifyCode, inviter, pwdConfirm } = yield select((state: any) => state.check);
if (!areacode) return message.warn(formatMessage({ id: 'check.msgArea' }));
if (!validPhone(phone)) return message.warn(formatMessage({ id: 'check.msgPhone' }));
if (!verifyCode) return message.warn(formatMessage({ id: 'check.msgCode' }));
if (!validPwd(password)) return message.warn(formatMessage({ id: 'check.msgPwd' }));
if (password !== pwdConfirm) return message.warn(formatMessage({ id: 'check.msgConfirm' }));
// if (!inviter) return message.warn(formatMessage({ id: 'check.msgInviter' }));
yield put({ type: 'onUpdateState', submitting: true }); yield put({ type: 'onUpdateState', submitting: true });
yield delay(800); try {
yield put({ type: 'onUpdateState', submitting: false }); const token = yield call(signUp, areacode, phone, password, verifyCode, inviter);
yield put({ type: 'onUpdateState', pageStep: PageStep.Check }); yield put({ type: 'user/updateToken', payload: token });
yield delay(0);
const user = yield call(getUserInfo, true);
yield put({ type: 'user/updateUser', payload: user });
} catch(e) {
message.error(e.message);
yield put({ type: 'user/logout' });
} finally {
yield put({ type: 'onUpdateState', submitting: false });
}
},
*sendCode(_, { select, put, call }) {
const { areacode, phone } = yield select((state: any) => state.check);
if (!areacode) return message.warn(formatMessage({ id: 'check.msgArea' }));
if (!validPhone(phone)) return message.warn(formatMessage({ id: 'check.msgPhone' }));
yield put({ type: 'onUpdateState', sendingCode: true });
try {
yield call(sendCode, areacode, phone);
yield put({ type: 'timer', payload: 60 });
} catch(e) {
message.error(e.message);
} finally {
yield put({ type: 'onUpdateState', sendingCode: false });
}
}, },
}, },

View File

@@ -4,30 +4,83 @@ import { connect, DispatchProp } from 'dva';
import { formatMessage } from 'umi-plugin-locale'; import { formatMessage } from 'umi-plugin-locale';
import { CheckModelState } from './model'; import { CheckModelState } from './model';
import styles from './style.less';
const RegisterPage: React.FC<DispatchProp & CheckModelState> = ({ const RegisterPage: React.FC<DispatchProp & CheckModelState> = ({
password, password,
inviter,
pwdConfirm,
verifyCode,
submitting, submitting,
dispatch, dispatch,
codeTimer,
sendingCode,
}) => { }) => {
let btnText;
if (codeTimer < 0) {
btnText = formatMessage({ id: 'check.getCode1' });
} else if (codeTimer === 0) {
btnText = formatMessage({ id: 'check.getCode3' });
} else {
btnText = formatMessage({ id: 'check.getCode2' }, { time: codeTimer })
}
return ( return (
<> <div className={styles.register}>
<Input.Group
className="item"
compact={true}
size="large"
>
<Input
style={{ width: '40%' }}
placeholder={formatMessage({ id: 'common.codeHolder' })}
value={verifyCode}
onChange={e => dispatch({ type: 'check/onUpdateState', verifyCode: e.target.value })}
/>
<Button
style={{ width: '60%' }}
size="large"
loading={sendingCode}
disabled={codeTimer > 0}
onClick={() => dispatch({ type: 'check/sendCode' })}
>{btnText}</Button>
</Input.Group>
<Input <Input
className="item" className="item"
placeholder={formatMessage({ id: 'common.pwdHolder' })} placeholder={formatMessage({ id: 'check.setPwd' })}
value={password} value={password}
onChange={e => dispatch({ type: 'check/onUpdatePassword', payload: e.target.value })} onChange={e => dispatch({ type: 'check/onUpdatePassword', payload: e.target.value })}
size="large" size="large"
type="password" type="password"
/> />
<Input
className="item"
placeholder={formatMessage({ id: 'check.confirmPwd' })}
value={pwdConfirm}
onChange={e => dispatch({ type: 'check/onUpdateState', pwdConfirm: e.target.value })}
size="large"
type="password"
/>
<Input
className="item"
placeholder={formatMessage({ id: 'check.invite' })}
value={inviter}
onChange={e => dispatch({ type: 'check/onUpdateState', inviter: e.target.value })}
size="large"
/>
<Button <Button
className="item gradient" className="item gradient submit"
type="primary" type="primary"
loading={submitting} loading={submitting}
onClick={() => dispatch({ type: 'check/submitRegister' })} onClick={() => dispatch({ type: 'check/submitRegister' })}
size="large" size="large"
>{formatMessage({ id: 'check.register' })}</Button> >{formatMessage({ id: 'check.register' })}</Button>
</> </div>
); );
} }

View File

@@ -28,3 +28,33 @@ export async function signIn(areacode: AreaCode, phone: string, password: string
} }
return data.token; return data.token;
} }
export async function signUp(areacode: AreaCode, phone: string, password: string, code: string, inviter: string): Promise<string> {
const { ok, msg, data } = await http.post(
'/sign/up',
{
phone: encodePhone(areacode, phone),
password,
inviter,
code,
},
);
if (!ok || !data || !data.token) {
throw Error(msg);
}
return data.token;
}
export async function sendCode(areacode: AreaCode, phone: string) {
const { ok, msg } = await http.post(
'/sign/send',
{
phone: encodePhone(areacode, phone),
type: 'signUp',
},
);
if (ok) {
return true;
}
throw Error(msg);
}

View File

@@ -11,7 +11,7 @@
width: auto; width: auto;
} }
.ant-btn { .submit {
margin-top: 30px; margin-top: 30px;
} }
@@ -54,3 +54,10 @@
line-height: 30; line-height: 30;
line-height: 80vh; line-height: 80vh;
} }
.register {
height: 100%;
padding-top: 160px;
display: flex;
flex-direction: column;
}

View File

@@ -14,6 +14,11 @@ export async function getAreaCodes(): Promise<AreaCodes> {
const res = await http.get<AreaCodes>('/sign/code'); const res = await http.get<AreaCodes>('/sign/code');
if (res.ok && res.data) { if (res.ok && res.data) {
res.data = res.data.map((it: any) => ({
id: it.id || it.pode,
areacode: it.areacode || it.itc.match(/\d+/)[0],
areaname: it.areaname || it.name_native || it.name_en,
}));
storeAreaCode(res.data); storeAreaCode(res.data);
return res.data; return res.data;
} }
@@ -24,7 +29,7 @@ export async function getAreaCodes(): Promise<AreaCodes> {
export async function getUserInfo(refresh = false): Promise<User> { export async function getUserInfo(refresh = false): Promise<User> {
if (!refresh) { if (!refresh) {
const userCache = loadUser() as User; const userCache = loadUser() as User;
if (userCache) { if (userCache && userCache.id) {
return userCache; return userCache;
} }
} }

View File

@@ -15,11 +15,29 @@ const successLabel = 'color:white;background-color:green;padding: 1px 2px';
const failureLabel = 'color:white;background-color:red;padding: 1px 2px'; const failureLabel = 'color:white;background-color:red;padding: 1px 2px';
const originLabel = 'color:unset;background-color:unset;padding:unset'; const originLabel = 'color:unset;background-color:unset;padding:unset';
const baseURL = 'http://yapi.faronear.org/mock/35'; const baseURL = 'https://server.fiv.tacever.org/v1';
// const baseURL = '//yapi.faronear.org/mock/20/v1';
const instance = axios.create({ const instance = axios.create({
baseURL, baseURL,
}); });
function replaceId(obj: any) {
if (obj instanceof Array) {
obj.forEach(replaceId);
} else if (typeof obj === 'object') {
const id = obj['_id'];
if (id) {
delete obj['_id'];
obj.id = id;
}
_.entries(obj).forEach(([_, value]) => {
if (typeof value === 'object') {
replaceId(value);
}
});
}
}
async function filter<T>( async function filter<T>(
method: keyof AxiosInstance, method: keyof AxiosInstance,
...args: any[] ...args: any[]
@@ -29,13 +47,14 @@ async function filter<T>(
const { user } = globalState().user; const { user } = globalState().user;
if (user && user.token) { if (user && user.token) {
instance.defaults.headers = { instance.defaults.headers = {
...instance(instance.defaults.headers), ...(instance.defaults.headers || {}),
Authorization: user.token, token: user.token,
}; };
} else { } else {
instance.defaults.headers = _.omit(instance.defaults.headers, 'Authorization'); instance.defaults.headers = _.omit(instance.defaults.headers, 'token');
} }
const { status, data } = await (instance[method] as (...args: any[]) => Promise<AxiosResponse<AppResp>>)(...args); const { status, data } = await (instance[method] as (...args: any[]) => Promise<AxiosResponse<AppResp>>)(...args);
replaceId(data);
ret = { ret = {
ok: status >= 200 && status < 300 && data && data.code === 0, ok: status >= 200 && status < 300 && data && data.code === 0,
code: data && data.code, code: data && data.code,