一步一步構(gòu)件react后臺(tái)系統(tǒng)4 之注冊(cè)頁面
添加注冊(cè)頁面
- 添加頁面
- views/register/index
這里依然采用ant里面的Form注冊(cè)組件
其他新增的或修改的會(huì)打上標(biāo)記, 沒打標(biāo)記的梢褐, 都是ant的form注冊(cè)組件
import React from 'react' // +
import { Form, Input, Tooltip, Icon, Cascader, Select, Row, Col, Checkbox, Button, AutoComplete } from 'antd';
import { validatorPhone } from '@/utils/index' // +
import './index.css' // +
const FormItem = Form.Item;
const Option = Select.Option;
const AutoCompleteOption = AutoComplete.Option;
class RegistrationForm extends React.Component {
state = {
confirmDirty: false,
autoCompleteResult: []
};
handleSubmit = (e) => {
e.preventDefault();
this.props.form.validateFieldsAndScroll((err, values) => {
if (!err) {
console.log('Received values of form: ', values);
}
});
}
handleConfirmBlur = (e) => {
const value = e.target.value;
this.setState({ confirmDirty: this.state.confirmDirty || !!value });
}
compareToFirstPassword = (rule, value, callback) => {
const form = this.props.form;
if (value && value !== form.getFieldValue('password')) {
callback('Two passwords that you enter is inconsistent!');
} else {
callback();
}
}
validateToNextPassword = (rule, value, callback) => {
const form = this.props.form;
if (value && this.state.confirmDirty) {
form.validateFields(['confirm'], { force: true });
}
callback();
}
handleWebsiteChange = (value) => {
let autoCompleteResult;
if (!value) {
autoCompleteResult = [];
} else {
autoCompleteResult = ['.com', '.org', '.net'].map(domain => `${value}${domain}`);
}
this.setState({ autoCompleteResult });
}
// 獲取驗(yàn)證碼, 傳遞給父組件
getCaptcha = (e) => { // +
var num = Math.floor(Math.random()*10000)
this.props.handleCaptcha(num)
}
render() {
const { getFieldDecorator } = this.props.form;
const { autoCompleteResult } = this.state;
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 8 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 16 },
},
};
const tailFormItemLayout = {
wrapperCol: {
xs: {
span: 24,
offset: 0,
},
sm: {
span: 16,
offset: 8,
},
},
};
const prefixSelector = getFieldDecorator('prefix', {
initialValue: '86',
})(
<Select style={{ width: 70 }}>
<Option value="86">+86</Option>
<Option value="87">+87</Option>
</Select>
);
const websiteOptions = autoCompleteResult.map(website => (
<AutoCompleteOption key={website}>{website}</AutoCompleteOption>
));
// +- 里面的組件都修改了。
return (
<div className="register">
<div className="register-form">
<Form onSubmit={this.handleSubmit} >
<FormItem
{...formItemLayout}
label="用戶別名"
>
{getFieldDecorator('username', {
rules: [{
message: '請(qǐng)輸入用戶名稱',
}, {
required: true, message: '用戶名稱必填',
}],
})(
<Input />
)}
</FormItem>
<FormItem
{...formItemLayout}
label="用戶密碼"
>
{getFieldDecorator('password', {
rules: [{
required: true, message: '請(qǐng)輸入用戶密碼!',
}, {
validator: this.validateToNextPassword,
}],
})(
<Input type="password" />
)}
</FormItem>
<FormItem
{...formItemLayout}
label="確認(rèn)密碼"
>
{getFieldDecorator('confirm', {
rules: [{
required: true, message: '請(qǐng)驗(yàn)證密碼',
}, {
validator: this.compareToFirstPassword,
}],
})(
<Input type="password" onBlur={this.handleConfirmBlur} />
)}
</FormItem>
<FormItem
{...formItemLayout}
label="手機(jī)號(hào)碼"
>
{getFieldDecorator('phone', {
rules: [{ required: true, message: '請(qǐng)輸入正確的手機(jī)號(hào)碼' },
{validator: validatorPhone.bind(this)()} // + 添加了驗(yàn)證手機(jī)正則
],
})(
<Input addonBefore={prefixSelector} style={{ width: '100%' }} />
)}
</FormItem>
<FormItem
{...formItemLayout}
label="驗(yàn)證碼"
extra="點(diǎn)擊獲取驗(yàn)證碼级历, 驗(yàn)證碼將會(huì)自動(dòng)填寫"
>
<Row gutter={8}>
<Col span={12}>
{getFieldDecorator('captcha', {
rules: [{ required: true, message: '請(qǐng)輸入正確的驗(yàn)證碼' }],
})(
<Input disabled/> // + 只讀
)}
</Col>
<Col span={12}>
<Button onClick={this.getCaptcha}>獲取驗(yàn)證碼</Button> // +- 添加驗(yàn)證碼事件
</Col>
</Row>
</FormItem>
<FormItem
{...formItemLayout}
label={(
<span> 電子郵箱</span>
)}
>
{getFieldDecorator('email', {
rules: [{ required: false, message: '請(qǐng)輸入電子郵箱', whitespace: true }],
})(
<Input />
)}
</FormItem>
<FormItem {...tailFormItemLayout}>
<Button type="primary" htmlType="submit">注冊(cè)</Button>
</FormItem>
</Form>
</div>
</div>
);
}
}
// 當(dāng)input發(fā)生改變時(shí)诗舰, 調(diào)用父?jìng)鬟f過來的事件。
const onFieldsChange = (props, changeFields) => {
props.onChange(changeFields)
}
// 接受到值, 并注入Form組件內(nèi)
const mapPropsToFields = props => {
let captcha = props.captcha
captcha && captcha.value && captcha.errors && delete captcha.errors // 當(dāng)存在值的時(shí)候语卤, 刪除errors
return {
username: Form.createFormField({
...props.username,
}),
password: Form.createFormField({
...props.password
}),
confirm: Form.createFormField({
...props.confirm
}),
phone: Form.createFormField({
...props.phone
}),
captcha: Form.createFormField({
...props.captcha
}),
email: Form.createFormField({
...props.email
})
}
}
// value改變事件
const onValuesChange = (_, values) => {
}
const WrappedRegistrationForm = Form.create({onFieldsChange, mapPropsToFields, onValuesChange})(RegistrationForm);
// 傳遞參數(shù)給form組件
class Register extends React.Component {
state = {
fields: {
username: {
value: '我是useranme默認(rèn)值'
}
}
}
// 當(dāng)input的value發(fā)生改變, 就改變值,
handleFormChange = (changedFields) => {
this.setState(({fields}) => {
return {
fields: {...fields, ...changedFields}
}
})
}
// 點(diǎn)擊獲取驗(yàn)證碼事件粹舵。
handleCaptcha = (captchaValue) => {
this.setState(({fields}) => {
let captcha = { // 合并captcha對(duì)象钮孵, 修改value
...((typeof fields.captcha === 'object') ? fields.captcha : {}),
value: captchaValue
}
return {
fields: {...fields, captcha}
}
})
}
render () {
let fields = this.state.fields
return (
<div>
<WrappedRegistrationForm {...fields} onChange={this.handleFormChange} handleCaptcha={this.handleCaptcha}></WrappedRegistrationForm>
</div>
)
}
}
export default Register
- views/register/index.css
.register {
display: -ms-flexbox;
display: flex;
-ms-flex-pack: center;
justify-content: center;
-ms-flex-align: center;
align-items: center;
height: 100%;
background: #f3f3f3;
}
.register>.register-form {
width: 60%;
height: auto;
padding: 36px;
-webkit-box-shadow: 0 0 100px rgba(0, 0, 0, 0.08);
box-shadow: 0 0 100px rgba(0, 0, 0, 0.08);
background: #fff;
}
#root{
height: 100%;
}
.App{
height: 100%;
}
- utils/index
export const validatorPhone = function () {
return (rule, value, callback) => {
const form = this.props.form;
if (value && !(/^1[3|4|5|8][0-9]\d{4,8}$/.test(form.getFieldValue('phone'))) ) {
callback('請(qǐng)輸入正確的手機(jī)號(hào)碼'); <Link to="/register">還沒有賬號(hào)? 去注冊(cè)</Link>
} else {
callback();
}
}
}
- views/login/index.js
添加一個(gè)注冊(cè)入口
<Link to="/register">還沒有賬號(hào)眼滤? 去注冊(cè)</Link>
- router/index.js
別忘了巴席, 添加到路由配置里面
注意: 這里添加了一個(gè)meta對(duì)象, 用來自定義一些參數(shù)诅需。
@params: isAuth<boolean> 是否不需要驗(yàn)證(是否登錄)
import React from 'react'
import Login from '@/views/login/index'
import Index from '@/views/index/index'
import Register from '@/views/register/index' // +
import { RenderRoutes } from '@/router/utils'
const Ui = ({routes}) => (<div>
<h3>Ui
</h3>
<RenderRoutes routes={routes}></RenderRoutes>
</div>)
const Button = () => <h3>Button</h3>
const Icon = () => <h3>Icon</h3>
const Animation = () => <h3>Animation</h3>
const From = () => <h3>From</h3>
export const menus = [ // 菜單相關(guān)路由
{ path: '/index/UI', name: 'UI', icon:'video-camera', component: Ui , routes: [
{path: '/index/UI/button', name: '按鈕', icon: 'video-camera', component: Button },
{path: '/index/UI/Icon', name: '圖標(biāo)', icon: 'video-camera', component: Icon }
]
},
{ path: '/index/animation', name: '動(dòng)畫', icon: 'video-camera', component: Animation },
{ path: '/index/form', name: '表格', icon: 'video-camera', component: From },
]
// isAuth 表示不用驗(yàn)證是否登錄
export const main = [
{ path: '/login', exact: true, name: '登錄', component: Login, meta: { // +-
isAuth: true
} },
{ path: '/register', exact: true, name: '注冊(cè)', component: Register, meta: { // +
isAuth: true
} },
{ path: '/', exact: true, name: '首頁', Redirect: '/index'},
{
path: '/index', name: '首頁', component: Index,
routes: menus
}
]
export const routerConfig = {
main, menus
}
- router/utils.js
修改渲染組件情妖,
當(dāng)路徑是 不需要驗(yàn)證是否登錄的頁面 的時(shí)候, 就不進(jìn)行判斷是否重定向到登錄頁面了诱担。
// 渲染當(dāng)前組件
export const RouteWithSubRoutes = route => (<Route
path={route.path}
exact={route.exact}
render={props =>{
var isAuthenticated = sessionStorage.getItem('isAuthenticated')
if ( !(typeof route.meta === 'object' && route.meta.isAuth) && !isAuthenticated ) { // +-
return <Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
}
return (
route &&( route.Redirect ? (<Redirect to={route.Redirect}></Redirect>) :
(<route.component {...props} routes={route.routes} />))
)
}}
/>
);
就這樣毡证, 頁面做好了。
查看github代碼
切換Log 到
添加注冊(cè)頁面蔫仙, 修改路由配置料睛, 修改路由渲染組件, 修改重定向登錄規(guī)則
即可看到當(dāng)前代碼摇邦。
- 添加后臺(tái)接口
注冊(cè)頁面做好了恤煞, 但是需要后臺(tái)接口, 這里添加后臺(tái)接口施籍。
先打開后臺(tái)項(xiàng)目居扒。
修改package.json 添加打開項(xiàng)目快捷方式
"nodemon": "nodemon bin/www" // +
然后運(yùn)行項(xiàng)目
npm run nodemon
- 修改routes/index.js
// 添加接口
const utils = require ('../utils/index')
let { isEmpty } = utils
router.post('/register', async (ctx, next) => {
let body = ctx.request.body
let username = body.username;
let password = body.password;
let captcha = body.captcha;
let phone = body.phone;
console.log(body)
if (isEmpty(username)) {
ctx.body = {
code: 0,
message: "用戶名不能為空",
}
}
else if (isEmpty(password)) {
ctx.body = {
code: 0,
message: "密碼不能為空",
}
}
else if (isEmpty(captcha)) {
ctx.body = {
code: 0,
message: "驗(yàn)證碼不能為空",
}
}
else if (isEmpty(phone)) {
ctx.body = {
code: 0,
message: "手機(jī)號(hào)碼不能為空",
}
} else {
ctx.body = {
code: 200,
message: "注冊(cè)成功",
}
}
})
- /utils/index.js
新增工具
const isEmpty = (text) => {
if (text) {
return false
}
return true
}
module.exports = {
isEmpty
}
好了 后臺(tái)接口做好了。
- 添加接口請(qǐng)求
- views/register/index
這里把 connect 的參數(shù)都放到connect里面丑慎, 因?yàn)椋看味夹枰胊ction 喜喂,太過繁瑣,放到一個(gè)頁面統(tǒng)一管理也很方便)
this.props.hanleRegister(values)
執(zhí)行傳遞過來的接口事件
import {connect} from "react-redux"; // +
import { mapReigster } from '@/reducer/connect.js' // +
handleSubmit = (e) => {
e.preventDefault();
this.props.form.validateFieldsAndScroll((err, values) => {
if (!err) {
console.log('Received values of form: ', values);
this.props.hanleRegister(values) // +
}
});
}
const WrappedRegistrationForm = connect( mapReigster.mapStateToProps, mapReigster.mapDispatchToProps )(Form.create({onFieldsChange, mapPropsToFields, onValuesChange})(RegistrationForm)); // +-
- reducer/connect.js
這里把創(chuàng)建action創(chuàng)建函數(shù)都封裝起來import { receive } from '@/reducer/actionCreate.js'
封裝一個(gè)fetchPosts
事件竿裂, 用來請(qǐng)求數(shù)據(jù)并切換數(shù)據(jù)玉吁。
添加 mapReigster
添加 mapLogin (這里順便把登錄里面的connect參數(shù)也提取出來了)
import { action_slidecollapsed, routerConfig, action_login } from '@/reducer/action.js'
import http from '@/api/http.js' // +
import { receive } from '@/reducer/actionCreate.js' // +
import { ACTION_LOGIN, ACTION_REGISTER } from '@/reducer/action.js' // +
export const mapStateToProps = (state) => {
console.log(state)
return {slidecollapsed: state.slidecollapsed,
isSlide: false,
}
}
export const mapDispatchToProps = (dispatch) => {
return {onSlidecollapsed: () => dispatch(action_slidecollapsed), getRouterConfig: () => {
return dispatch(routerConfig)
}, toggleSlide: () => {
dispatch({type: action_slidecollapsed.type})
}}
}
export const crumbsMap = {
mapStateToProps (state) {
return { routerConfig: state.routerConfig }
},
mapDispatchToProps (dispatch) {
return {getRouterConfig: () => {
return dispatch(routerConfig)
}}
}
}
@params: url<string> 接口路徑
@params: actionType<string> action.type
@params: subreddit<string> 數(shù)據(jù)名稱
@params: data<object> 數(shù)據(jù)
function fetchPosts(url, actionType, subreddit, data) { // +
return dispatch => {
dispatch(receive(actionType, subreddit, '暫無數(shù)據(jù)'))
return http.post(url, data)
.then(res => {
dispatch(receive(actionType, subreddit, res))
})
}
}
// 注冊(cè)
export const mapReigster = {
mapStateToProps (state) {
return state.getReigster || state
},
mapDispatchToProps (dispatch) {
return {hanleRegister: (data) => {
return dispatch(fetchPosts('/register', ACTION_REGISTER,'reigsterData', data))
}}
}
}
// 登錄
export const mapLogin = {
mapStateToProps (state) {
return state.getLogin
},
mapDispatchToProps (dispatch) {
return {handleLogin: (data) => {
return dispatch(fetchPosts('/login', ACTION_LOGIN, 'loginData', data))
}}
}
}
- reducer/actionCreate.js
import { ACTION_LOGIN, ACTION_REGISTER } from '@/reducer/action.js'
export const receiveLogin = (dataName, data) => {
return {
type: ACTION_LOGIN,
[dataName]: data
}
}
// 封裝個(gè)通用的 actionCreate
export const receive = ( typeName, dataName, data) => {
return {
type: typeName,
[dataName]: data
}
}
- reducer/action.js
添加action
export const SLIDECOLLAPSED = 'slidecollapsed'
export const ROUTERCONFIG = 'routerConfig'
export const ACTION_LOGIN = 'getLogin'
export const ACTION_REGISTER = 'ACTION_REGISTER' // +
export const action_slidecollapsed = {type: SLIDECOLLAPSED}
export const routerConfig = { type: ROUTERCONFIG }
export const action_login = { type: ACTION_LOGIN }
export const action_register = { type: ACTION_REGISTER } // +
- api/http.js
修改http.js, 添加響應(yīng)提示
import axios from 'axios'
import { message } from 'antd';
let loadingInstance = {
close: () =>{}
}
// process.env.NODE_ENV === 'production' ? 'http://123.207.49.214:8028' : 'http://123.207.49.214:8028'
// 創(chuàng)建axios實(shí)例
const service = axios.create({
baseURL: "http://localhost:4000", // api的base_url
timeout: 5000, // 請(qǐng)求超時(shí)時(shí)間
//設(shè)置默認(rèn)請(qǐng)求頭腻异,使post請(qǐng)求發(fā)送的是formdata格式數(shù)據(jù)// axios的header默認(rèn)的Content-Type好像是'application/json;charset=UTF-8',我的項(xiàng)目都是用json格式傳輸进副,如果需要更改的話,可以用這種方式修改
// headers: {
// "Content-Type": "application/x-www-form-urlencoded"
// },
// withCredentials: true, // 允許攜帶cookie
})
function cloneLoading () {
loadingInstance.close()
}
// request攔截器
service.interceptors.request.use(config => {
return config
}, error => {
cloneLoading()
// Do something with request error
Promise.reject(error)
})
// respone攔截器
service.interceptors.response.use(
response => {
cloneLoading()
if (response.data && response.data.code === 0) {
message.error(response.data.message, 1.5)
} else if (response.data && response.data.code === 200) {
message.success(response.data.message, 1.5)
}
return response.data
}, error => {
console.log('err' + error)// for debug
cloneLoading()
if (error && error.response) {
switch (error.response.status) {
case 400:
error.desc = '請(qǐng)求錯(cuò)誤'
break;
case 401:
error.desc = '未授權(quán)悔常,請(qǐng)登錄'
break;
case 403:
error.desc = '拒絕訪問'
break;
case 404:
error.desc = `請(qǐng)求地址出錯(cuò): ${error.response.config.url}`
break;
case 408:
error.desc = '請(qǐng)求超時(shí)'
break;
case 500:
error.desc = '服務(wù)器內(nèi)部錯(cuò)誤'
break;
case 501:
error.desc = '服務(wù)未實(shí)現(xiàn)'
break;
case 502:
error.desc = '網(wǎng)關(guān)錯(cuò)誤'
break;
case 503:
error.desc = '服務(wù)不可用'
break;
case 504:
error.desc = '網(wǎng)關(guān)超時(shí)'
break;
case 505:
error.desc = 'HTTP版本不受支持'
break;
}
message.error(error.desc)
}
return Promise.reject(error)
})
export default service
- views/login/index.js
既然提取了login的 connect參數(shù)影斑, 這里就要修改 login頁面
刪除 fetchPosts, loginMap
import { mapLogin } from '@/reducer/connect.js'
export default connect(mapLogin.mapStateToProps, mapLogin.mapDispatchToProps)(Form.create()(Login)); // +
function fetchPosts(subreddit, data) { // -
return dispatch => {
dispatch(receiveLogin(subreddit, '暫無數(shù)據(jù)'))
return http.post(`/login`, data)
.then(res => {
dispatch(receiveLogin(subreddit, res))
})
}
}
export const loginMap = { // -
mapStateToProps (state) {
return state.getLogin
},
mapDispatchToProps (dispatch) {
return {handleLogin: (data) => {
return dispatch(fetchPosts('loginData', data))
}}
}
}
- reducer/reduxs.js
新增 ACTION_REGISTER机打, getReigster與導(dǎo)出 新的allReducer
import {SLIDECOLLAPSED, ROUTERCONFIG, ACTION_LOGIN, ACTION_REGISTER} from '@/reducer/action.js'
const getReigster = (state = {}, action) => {
switch (action.type) {
case ACTION_REGISTER:
return {...state, ...action}
default :
return state
}
}
export const allReducer = combineReducers({
slidecollapsed: slidecollapsedFuc, routerConfig: getRouterConfig, getLogin: getLoginFun, getReigster
})
這樣矫户, 就修改完成了。
- 驗(yàn)證接口是否成功
之前在http.js修改了提示姐帚, 不管請(qǐng)求成功或是失敗吏垮, 都會(huì)有彈框出來提示障涯。
當(dāng)然罐旗, 也可以查看內(nèi)容是否注入到組件內(nèi)
在register組件內(nèi)膳汪, 在render內(nèi)打印出 this.pros出來,
還記得我們?cè)?connect.js內(nèi)添加的內(nèi)容么九秀?
mapDispatchToProps (dispatch) {
return {hanleRegister: (data) => {
return dispatch(fetchPosts('/register', ACTION_REGISTER,'reigsterData', data))
}}
}
這里的reigsterData
就是注入到組件內(nèi)的內(nèi)容遗嗽。 查看是否有該內(nèi)容, 如果有鼓蜒, 查看是否是請(qǐng)求后的返回值痹换。
里面包含這個(gè)參數(shù), 表示注冊(cè)成功都弹。
{
regsterData: {
code : 200
message : "注冊(cè)成功"
}
}
- 注冊(cè)完畢后娇豫, 自動(dòng)登錄
- 修改 reducer/connect.js
添加 handleLogin 登錄事件
export const mapReigster = {
mapStateToProps (state) {
return state.getReigster || state
},
mapDispatchToProps (dispatch) {
return {hanleRegister: (data) => {
return dispatch(fetchPosts('/register', ACTION_REGISTER,'reigsterData', data))
},
handleLogin: (data) => {
return dispatch(fetchPosts('/login', ACTION_REGISTER, 'loginData', data))
}
}
}
}
- views/register/index.js
state = {
confirmDirty: false,
autoCompleteResult: [],
formValue: [], // 添加保存內(nèi)容
isLoginLoading: false // 添加是否已經(jīng)請(qǐng)求
};
handleSubmit = (e) => {
e.preventDefault();
this.props.form.validateFieldsAndScroll((err, values) => {
if (!err) {
console.log('Received values of form: ', values);
this.props.hanleRegister(values)
this.setState({ // 注冊(cè)完畢, 保存內(nèi)容畅厢,并修改狀態(tài)冯痢。
formValue: values,
isLoginLoading: false
})
}
});
}
// render內(nèi)
let { reigsterData, loginData } = this.props // 新增注入的數(shù)據(jù)
// 注冊(cè)成功, 自動(dòng)登錄
if (typeof reigsterData === 'object' && reigsterData.code === 200) {
if (!this.state.isLoginLoading) { // 判斷是否已經(jīng)請(qǐng)求過框杜,
this.props.handleLogin(this.state.formValue) // 請(qǐng)求登錄浦楣。
this.setState({
isLoginLoading: true
})
}
if (typeof loginData === 'object' && loginData.code === 200) { // 登錄成功, 跳轉(zhuǎn)頁面咪辱。
sessionStorage.setItem('isAuthenticated', true)
let from = {}
from.pathname = '/';
return <Redirect to={from} />;
}
}
這樣振劳, 注冊(cè)與注冊(cè)完畢后自動(dòng)登錄就已經(jīng)完成。
需要注意的是油狂, 這里是自己寫的一個(gè)后臺(tái)历恐, 登錄賬號(hào)與密碼已經(jīng)寫死, admin 123456专筷, 所以夹供, 這里注冊(cè)的時(shí)候, 雖然隨便輸入什么都能夠注冊(cè)成功仁堪, 但是登陸的時(shí)候哮洽, 只有admin 123456才能夠登錄成功。