页面编写
antd表单的使用
要使用antd,先下载antd
cnpm install antd --save
首先看一个表单的例子,在src下新建一个test文件夹,新建TextAntdForm.jsx,内容如下(先不管看得懂看不懂,后面解释)
import React from 'react'
import {Form, Input, Button} from 'antd'
function TextAntdForm(props) {
const {getFieldDecorator, validateFields} = props.form
const handleSubmit = (event) => {
event.preventDefault();
validateFields((error, values) => {
if (!error) {
console.log(values);
}
})
}
return(
<div style={{width: "300px", margin: "100px auto", fontFamily: "Consolas, '楷体'"}}>
<Form onSubmit={handleSubmit}>
<Form.Item label="用户名">
{getFieldDecorator('title', {
rules: [{
required: true,
message: '请输入用户名',
}],
})(
<Input size="large" placeholder="请输入用户名"/>
)}
</Form.Item>
<Form.Item label="密码">
{getFieldDecorator('password', {
rules: [{
required: true,
message: '请输入密码',
}],
})(
<Input.Password size="large" placeholder="请输入密码"/>
)}
</Form.Item>
<Form.Item>
<Button size="large" type="primary" htmlType="submit">提交</Button>
</Form.Item>
</Form>
</div>
)
}
export default Form.create()(TextAntdForm)
修改src/index.js渲染该组件(记得引入antd/dits/antd.css,否则antd组件没有样式)
import React from 'react'
import ReactDOM from 'react-dom';
// import router from './router'
import TestAntdForm from './test/TestAntdForm'
import 'antd/dist/antd.css'
import './common.css';
// ReactDOM.render(router(), document.getElementById("root"));
ReactDOM.render(<TestAntdForm />, document.getElementById("root"));
现在来解释上面的代码,首先看最后一行
Form.create()(TextAntdForm)
还记得高阶组件吗,Form.create()就是一个高阶组件,他会向组件的props中注入form,form提供了一些API,这里使用了两个:
- getFieldDecorator:用于和表单进行双向绑定
getFieldDecorator('title', {
rules: [{
required: true,
message: '请输入用户名',
}],
})(
<Input size="large" placeholder="请输入用户名"/>
)
上面将Input与表单项进行了绑定,getFieldDecorator接收两个参数,第一个参数是id,根据它可以获取输入控件的值或者设置输入控件的值,是必填项;第二个参数是options,里面可以有很多属性,这里使用了rules,定义了校验的规则,required表示是否必填,message表示未填时显示的消息文字。
- validateFields:校验
里面接受一个回调函数,回调函数接收两个参数,第一个参数为error,当不满足校验规则时error的值非空,第二个参数是values,会将绑定表单的值以对象的形式传给values,键就是在getFieldDecorator传入的id。
上面在提交表单后,会调用Form的onSubmit回调函数,在回调函数,我们对数据进行了校验,如果没有问题的话,我们可以将输入表单的键值对以对象的形式获取到values,并打印出来
页面数据
我们将数据设置为一个数组datas,它的格式如下
datas = [
{title: , brief: , isTop: , content: }
{title: , brief: , isTop: , content: }
]
Home组件根据datas展示数据,Edit和Display组件根据id和datas获取要展示的数据,由于多个组件都要用到数据,所以这里使用useContext和useReducer来分发数据。
在src下新建Provider.jsx,用来提供state和dispatch,内容如下
import React, {useReducer} from 'react'
export const Context = React.createContext();
const reducer = (state, action) => {
const tempDatas = state.datas
// 按道理case后面跟的都是常量,我这里为了简单
switch(action.type) {
case 'insertData':
tempDatas[tempDatas.length] = action.data;
return {...state, datas: tempDatas}
case 'updateData':
tempDatas[action.id] = action.data
return {...state, datas: tempDatas}
case 'deleteData':
tempDatas.splice(action.id, 1)
return {...state, datas: tempDatas}
case 'changeOperation':
return {...state, operation: action.operation}
default:
return state
}
}
const initState = {
// 随便写的数据用来实验 随后会删掉
datas: [{title: 'aaa', isTop: true, brief: 'hahah'}, content: '<p>123</p>'],
// 用来判断是添加文章还是编辑文章
operation: 'ADD'
}
function Provider(props) {
const [state, dispatch] = useReducer(reducer, initState)
const {children} = props
return (
<Context.Provider value={{state, dispatch}}>
{children}
</Context.Provider>
)
}
export default Provider
更改router.js,在最上面加上Provider
import Provider from './Provider'
// 上面没有变化,除了import Provider,故此省略
const router = () => {
return (
<Provider>
<Router history={history}>
<Switch>
{routes.map((item, id) => {
return RouteItem({ key: id, ...item, history: history })
})}
</Switch>
</Router>
</Provider>
);
};
export default router
Home
观察Home页面
发现Home是由这一个个Item组成,Item中的数据正是datas数组中每一个元素的内容,在Home中新建文件夹Item,并在Item中新建index.jsx和index.module.css。index.jsx:
import React from 'react'
import styles from './index.module.css'
import {Modal} from 'antd'
const {confirm} = Modal
function Item(props) {
const {
data,
login,
handleDelete,
index,
handleToEdit,
handleToDisplay
} = props;
const toDisplay = (event) => {
event.preventDefault();
handleToDisplay(index)
}
const toEdit = (event) => {
event.preventDefault();
handleToEdit(index)
}
const toDelete = (event) => {
event.preventDefault();
confirm({
title: `你确定要删除${data.title}`,
content: '删除后内容不可恢复',
onOk() {
handleDelete(index)
},
onCancel() {
},
});
}
return (
<div className={styles.item}>
<h2><a href={`/display/${data.id}`} onClick={toDisplay}>{data.title}</a></h2>
<hr />
<div className={styles.abstract}>
{data.brief}
</div>
<div className={styles.readmore}>
<a href={`/display/${data.id}`} onClick={toDisplay}>阅读更多</a>
</div>
{/* 当登录失显示编辑本文和删除本文 */}
{login ?
<div className={styles.edit}>
<a href={`/edit/${data.id}`} onClick={toEdit}>编辑本文</a>
</div> : ""}
{login ?
<div className={styles.delete}>
<a href="/delete" onClick={toDelete}>删除本文</a>
</div>: ""}
{/* 当isTop为1时显示置顶图标 */}
{data.isTop ?
<div className={styles.isTop}>
<svg viewBox="0 0 1024 1024">
<path d="M0 0h1024v1024z" fill="#7ED321"></path>
<path d="M571.733333 157.866667l17.066667-12.8-83.2-83.2L552.533333 14.933333l183.466667 183.466667-46.933333 46.933333-81.066667-81.066666-17.066667 12.8 100.266667 100.266666-14.933333 14.933334-102.4-102.4c-6.4 4.266667-10.666667 8.533333-17.066667 10.666666l72.533333 72.533334-110.933333 110.933333 36.266667 36.266667-14.933334 14.933333L313.6 209.066667l14.933333-14.933334 36.266667 36.266667 110.933333-110.933333 61.866667 61.866666c6.4-4.266667 10.666667-8.533333 17.066667-10.666666l-96-96 14.933333-14.933334 98.133333 98.133334z m-72.533333 209.066666l17.066667-17.066666-117.333334-117.333334-17.066666 17.066667 117.333333 117.333333z m27.733333-29.866666l14.933334-14.933334L426.666667 204.8l-14.933334 14.933333 115.2 117.333334z m27.733334-27.733334l17.066666-14.933333-117.333333-117.333333-17.066667 14.933333 117.333334 117.333333z m27.733333-25.6l14.933333-14.933333L482.133333 149.333333l-14.933333 14.933334 115.2 119.466666z m10.666667-202.666666L554.666667 44.8l-21.333334 21.333333 38.4 38.4 21.333334-23.466666z m57.6 57.6l-40.533334-40.533334-21.333333 21.333334 40.533333 40.533333 21.333334-21.333333zM704 192l-38.4-38.4-21.333333 21.333333L682.666667 213.333333l21.333333-21.333333zM571.733333 471.466667l12.8-21.333334c8.533333 10.666667 17.066667 19.2 25.6 27.733334 6.4 6.4 12.8 6.4 21.333334-2.133334l172.8-172.8-38.4-38.4 17.066666-17.066666 87.466667 87.466666-17.066667 17.066667-29.866666-29.866667-177.066667 177.066667c-14.933333 14.933333-29.866667 14.933333-44.8 0l-29.866667-27.733333z m302.933334 21.333333l-44.8 44.8c-27.733333 25.6-55.466667 40.533333-83.2 44.8-27.733333 2.133333-59.733333-6.4-96-25.6l6.4-25.6c34.133333 19.2 64 27.733333 87.466666 25.6 23.466667-4.266667 46.933333-14.933333 68.266667-36.266667l44.8-44.8 17.066667 17.066667z m132.266666-21.333333l-17.066666 19.2-55.466667-55.466667c-10.666667 8.533333-19.2 17.066667-29.866667 23.466667l51.2 51.2-119.466666 119.466666-17.066667-17.066666 102.4-102.4-76.8-76.8-104.533333 100.266666-17.066667-17.066666 121.6-121.6 42.666667 42.666666c10.666667-6.4 19.2-14.933333 29.866666-23.466666L861.866667 362.666667l17.066666-17.066667 128 125.866667zM802.133333 682.666667h-25.6c2.133333-25.6 2.133333-55.466667-2.133333-89.6h23.466667c4.266667 34.133333 4.266667 64 4.266666 89.6z" fill="#FFFFFF"></path>
</svg>
</div> : ""}
</div>
)
}
export default Item
注意到Item为展示组件,只负责数据的展示,而不负责数据的处理、获取,Item的数据、数据的操作都是从props中获取的,这些操作都由Item的容器组件Home来完成。
我们会将login保存在sessionStorage,login是一个布尔值,保存了是否登录的信息,true表示登录,false表示未登录,根据是否登录,决定是否将删除和编辑的操作暴露出来。同时我们也将根据data的isTop是否为true来显示是否置顶的svg图样(该图样来自CSDN的置顶图样)。
index.module.css
.item {
width: 100%;
height: 150px;
background-color: white;
margin-bottom: 40px;
border-radius: 4px;
padding-left: 25px;
padding-right: 20px;
padding-top: 20px;
position: relative;
box-shadow:0px 0px 6px 6px #FFF;
}
.item a {
text-decoration: none;
color: #40759b;
}
.item a:hover {
text-decoration: underline;
}
.abstract {
width: 100%;
padding-top: 30px;
}
.readmore {
position: absolute;
font-size: 14px;
bottom: 10px;
right: 10px;
}
.edit {
position: absolute;
font-size: 14px;
bottom: 10px;
right: 75px;
}
.delete {
position: absolute;
font-size: 14px;
bottom: 10px;
right: 140px;
}
.isTop {
position: absolute;
width: 50px;
top: 0;
right: 0;
}
现在进入Home组件,修改Home/index.jsx如下
import React, { useContext } from 'react'
import BasicLayout from './../../layouts/BasicLayout'
import Item from './components/Item'
import {withRouter} from 'react-router-dom'
import {Context} from './../../router'
import { message } from 'antd'
function Home(props) {
const {history} = props
// 使用useContext获取Provider提供的数据
const {state, dispatch} = useContext(Context);
const login = sessionStorage.getItem("login");
const datas = state.datas;
const handleDelete = (id) => {
dispatch({type: "deleteData", id});
message.success('删除成功');
}
const handleToEdit = (id) => {
dispatch({type: "changeOperation", operation: "EDIT"})
history.push(`/edit/${id}`);
}
const handleToDisplay = (id) => {
history.push(`display/${id}`);
}
return (
<div>
<BasicLayout>
{datas.map((data, index) => {
return <Item
index={index}
data={data}
key={index}
login={login}
handleDelete={handleDelete}
handleToEdit={handleToEdit}
handleToDisplay={handleToDisplay}
/>
})}
</BasicLayout>
</div>
)
}
export default withRouter(Home)
现在启动项目(npm start),观察到页面如下
说明Home页面已经成功了(由于在sessionStorage中没有login,所以删除本文和编辑本文均显示不出来,当点击阅读更多时,会跳转到Display的页面)。
Display
我们来观察Display的页面
发现Display页面有一个背景为白色的内容区和一个按钮,这个按钮根据是否有登录来决定是否暴露出来,所以我们在Display中新建一个components文件夹,在里面新建一个Content文件夹,在Content文件夹中新建index.jsx和index.module.css。首先Content是一个展示组件,所以它的数据全部都由Display提供,所有的数据操作也由Display传入回调函数进行处理。
index.jsx如下
import React from 'react'
import { Button } from 'antd'
import styles from './index.module.css'
function Content(props) {
const {login, htmlContent, handleToEdit} = props
const toEdit = () => {
handleToEdit();
}
return(
<div className={styles.display}>
<div className="braft-output-content" style={{minHeight: "425px", backgroundColor: "#FFF", padding: "50px 25px", fontSize: "16px", maxWidth: "850px"}} dangerouslySetInnerHTML={{__html: htmlContent}} >
</div>
{login &&
<div className={styles.edit}>
<Button type="primary" onClick={toEdit}>编辑文章</Button>
</div>
}
</div>
)
}
export default Content
想必上面的代码还是比较容易理解的,index.module.css的内容如下
.display {
position: relative;
}
.display ul, .display ol {
padding-left: 30px;
}
.edit {
position: absolute;
top: 10px;
right: 10px;
}
所以Display中的内容如下
import React, {useContext} from 'react'
import BasicLayout from './../../layouts/BasicLayout'
import {withRouter} from 'react-router-dom'
import Content from './components/Content'
import {Context} from './../../Provider'
import BraftEditor from 'braft-editor'
function Display(props) {
const {history} = props
// 获取传过来的id
const index = Number(history.location.pathname.split("/")[2])
const {state, dispatch} = useContext(Context)
const htmlContent = BraftEditor.createEditorState(state.datas[index].content).toHTML()
const login = sessionStorage.getItem("login")
const handleToEdit = () => {
dispatch({type: "changeOperation", operation: "EDIT"});
history.push(`/edit/${index}`);
}
return (
<BasicLayout>
<Content
htmlContent={htmlContent}
login = {login}
handleToEdit = {handleToEdit}
/>
</BasicLayout>
)
}
export default withRouter(Display)
至此Display页面设计完毕。
Edit
在写Edit页面之前,来改造一下RichText组件,我们要将RichText做成展示组件,所有的数据都由Edit提供,所有的数据处理也由Edit处理,修改如下
import React from 'react'
import BraftEditor from 'braft-editor'
import 'braft-editor/dist/index.css'
function RichText(props) {
const {value, onChange} = props
const handleEditorChange = (editorState) => {
onChange(editorState)
}
return (
<BraftEditor
value={value}
onChange={handleEditorChange}
/>
)
}
export default RichText
现在我们来看一下Edit页面的结构
我们使用antd的表单来做成这件事情,在前面已经介绍过antd表单的使用,所以这里不多加介绍,直接上代码
import React, {useEffect, useContext} from 'react'
import RichText from './components/RichText'
import { withRouter } from 'react-router-dom'
import { Form, Input, Button, message, Checkbox } from 'antd'
import BasicLayout from './../../layouts/BasicLayout'
import BraftEditor from 'braft-editor'
import {Context} from './../../Provider'
function Edit(props) {
const {history} = props
const FormItem = Form.Item;
const { getFieldDecorator, validateFieldsAndScroll } = props.form;
const {state, dispatch} = useContext(Context)
useEffect(() => {
// 当组件加载后 如果是编辑文章 根据id获取数据 然后显示
// 如果是添加操作,则不加载数据 直接显示空白内容
if(state.operation === 'EDIT') {
const index = Number(history.location.pathname.split("/")[2])
const data = state.datas[index]
// setFieldsValue为表单设置内容
props.form.setFieldsValue({
...data,
content: BraftEditor.createEditorState(data.content)
})
}
}, [])
const handleSubmit = (event) => {
event.preventDefault();
validateFieldsAndScroll((err, values) => {
if(!err) {
// 如果是通过添加按钮进来的 那么拿到数据保存 然后跳转到home页面
if(state.operation === 'ADD') {
dispatch({type: 'insertData', data: {
...values,
content: values.content.toRAW()
}});
message.success("添加成功");
history.push("/home");
//如果是编辑文章进来的,更新数据 然后跳转到home
} else if (state.operation === "EDIT") {
const id = Number(history.location.pathname.split("/")[2]);
dispatch({type: "updateData", id, data: {
...values,
content: values.content.toRAW(),
}});
message.success("更新成功");
history.push("/home");
}
}
})
}
return (
<BasicLayout>
<div>
<Form onSubmit={handleSubmit}>
<FormItem labelAlign="left" label="文章标题">
{getFieldDecorator('title', {
rules: [{
required: true,
message: '请输入标题',
}],
})(
<Input size="large" placeholder="请输入标题"/>
)}
</FormItem>
<FormItem size="large" label="文章摘要">
{getFieldDecorator('brief', {
rules: [{
required: true,
message: '请输入摘要',
}],
})(
<Input.TextArea style={{fontSize: "16px"}} placeholder="请输入摘要"/>
)}
</FormItem>
<FormItem>
{getFieldDecorator('isTop', {
valuePropName: 'checked',
})(
<Checkbox>
是否置顶
</Checkbox>,
)}
</FormItem>
<FormItem label="文章正文">
{getFieldDecorator('content', {
validateTrigger: 'onBlur',
rules: [{
required: true,
message: "请输入正文"
}],
})(
<RichText />
)}
</FormItem>
<FormItem>
<Button size="large" type="primary" htmlType="submit">提交</Button>
</FormItem>
</Form>
</div>
</BasicLayout>
)
}
// 为Edit注入form
export default withRouter(Form.create()(Edit))
上面的代码虽然有点长,但是都是比较容易理解的。注意,虽然我们没有为RichText传入value和onChange,但是由于RichText和表单项进行了双向绑定,所以表单会注入value和onChange。
Login
Login页面应该是最简单的,只要用我们在前面antd表单示例里面的表单就可以完成,所以直接上代码如下
import React from 'react'
import {withRouter} from 'react-router-dom'
import {Form, Button, Input, message } from 'antd'
import LoginLayout from './../../layouts/LoginLayout'
function Login(props) {
const { form, history } = props;
const FormItem = Form.Item;
const {getFieldDecorator, validateFields} = form
const handleSubmit = (event) => {
event.preventDefault();
validateFields((err, values) => {
if (!err) {
if (values.adminId === "123" && values.password === "123") {
sessionStorage.setItem("login", true);
message.success("登录成功");
history.push("/home")
} else {
message.error("用户名或密码错误")
}
}
})
}
return (
<LoginLayout>
<Form onSubmit={handleSubmit}>
<FormItem labelAlign="left" label="用户名">
{getFieldDecorator('adminId', {
rules: [{
required: true,
message: '请输入用户名',
}],
})(
<Input size="large" placeholder="请输入用户名"/>
)}
</FormItem>
<FormItem size="large" label="密码">
{getFieldDecorator('password', {
rules: [{
required: true,
message: '请输入密码',
}],
})(
<Input.Password size="large" placeholder="请输入密码"/>
)}
</FormItem>
<FormItem>
<Button size="large" type="primary" htmlType="submit">提交</Button>
</FormItem>
</Form>
</LoginLayout>
)
}
export default withRouter(Form.create()(Login))
收尾
在这里还有一个小地方没有处理,那就是Header,里面的a标签的点击事件没有处理,并且我们希望在登录的情况下显示”写博客”,以及在登录的情况下显示”退出登录”,在未登录的情况下显示”登录”,所以修改Header如下(由于要用到history,所以要在Layout里给Header传入history,但是Layout也没有history,所以要在Home, Edit, Display, Login中给用到的Layout传入history,这里的代码就不贴出了,想必这样的事情对现在的你应该已经很简单了)
import React, {useContext} from 'react'
import styles from './index.module.css'
import {Context} from './../../Provider'
function Header(props) {
const {history} = props;
const {dispatch} = useContext(Context);
const login = sessionStorage.getItem("login");
const toEdit = (e) => {
e.preventDefault();
dispatch({type: "changeOperation", operation: "ADD"});
history.push("/edit");
}
const toHome = (e) => {
e.preventDefault()
history.push("/home")
}
const logout = (e) => {
e.preventDefault()
sessionStorage.removeItem("login");
history.push("/login");
}
const login_ = (e) => {
e.preventDefault()
history.push("/login");
}
return (
<div className={styles.header}>
<div className={styles.nav}>
<ul>
<li><a href="/home" onClick={toHome}>首页</a></li>
{login && <li><a href="/edit" onClick={toEdit}>写博客</a></li>}
{login ? <li><a href="/login" onClick={logout}>退出登录</a></li> : <li><a href="/login" onClick={login_}>登录</a></li>}
</ul>
</div>
<div className={styles.desc}>
Coder
</div>
</div>
)
}
export default Header
接下来就是数据的持久化,我们希望将数据能够保存到localStorage,这样当页面刷新,关闭页面、浏览器,关机数据都能够保存。修改Provider.jsx
import React, {useReducer} from 'react'
export const Context = React.createContext();
const saveData = (datas) => {
localStorage.setItem("datas",JSON.stringify(datas) || [])
}
const loadData = () => {
// 如果datas没有内容,则为空数组,因为后面用到datas.map,放止报错
return JSON.parse(localStorage.getItem("datas")) || []
}
const reducer = (state, action) => {
const tempDatas = state.datas
// 每次对数据进行操作后都保存数据
switch(action.type) {
case 'insertData':
tempDatas[tempDatas.length] = action.data;
saveData(tempDatas)
return {...state, datas: tempDatas}
case 'updateData':
tempDatas[action.id] = action.data
saveData(tempDatas)
return {...state, datas: tempDatas}
case 'deleteData':
tempDatas.splice(action.id, 1)
saveData(tempDatas)
return {...state, datas: tempDatas}
case 'changeOperation':
return {...state, operation: action.operation}
default:
return state
}
}
const initState = {
// 初始化从localStorage中读取数据
datas: loadData(),
operation: 'ADD'
}
function Provider(props) {
const [state, dispatch] = useReducer(reducer, initState)
const {children} = props
return (
<Context.Provider value={{state, dispatch}}>
{children}
</Context.Provider>
)
}
export default Provider