上一个教程,我们的应用已经可以以“当前用户”的身份来调用数据。那么,其实我们的应用已经可以“当前用户”的身份来,创建、发布、删除的“事件”的能力了。
用户的 Token 其实是有使用范围的限制的,只是现在我们的应用已经获得的相关的授权,所以这里先不要考虑中。更多 OAuth 2.0 Scope 的介绍可以查看: OAuth Scopes
这一部分教程,我们改变一下编写代码的步骤。不再以页面的角度来一步一步进行开发,而是从代码层次的角度展开工作。
准备请求方法
首先,我们会根据 File-X Event 的 API 文档 ,将所有需要调用的接口编写成一个个可供调用的函数。
打开 src/requests.js
,开始添加方法
// create event
export const createEvent = async (data, token) => {
const url = `${API_ENDPOINT}/api/xfile/event/`
return await fetch(url, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `${token.token_type} ${token.access_token}`
}
}).then(resHelper)
}
// update event
export const updateEvent = async (id, data, token) => {
const url = `${API_ENDPOINT}/api/xfile/event/${id}/`
return await fetch(url, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `${token.token_type} ${token.access_token}`
},
body: JSON.stringify(data)
}).then(resHelper)
}
// publish event
export const publishEvent = async (id, token) => {
return updateEvent(id, { publish: true }, token)
}
// delete event
export const deleteEvent = async (id, token) => {
const url = `${API_ENDPOINT}/api/xfile/event/${id}/`
return await fetch(url, {
method: 'DELETE',
headers: {
Authorization: `${token.token_type} ${token.access_token}`,
Accept: 'application/json'
}
}).then(resHelper)
}
添加“创建事件”的功能
在刚开始的交互流程中,我们可以看到,用户可以通过点击“我的”页面上的“+”按钮,进入“创建事件“页面
添加“新建事件”功能
接下来,我们添加一个 NewButton
组件。新建 src/NewButton.js
,编辑:
import React from 'react'
import './NewButton.css'
function NewButton (props) {
return <button className='NewButton' onClick={props.onClick} />
}
export default NewButton
添加样式文件 src/NewButton.css
,并编辑:
.NewButton {
position: fixed;
z-index: 10;
bottom: 64px;
right: 24px;
background-color: #333;
width: 48px;
height: 48px;
border: none;
border-radius: 24px;
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.3);
transition: 0.3s ease all;
}
.NewButton:active {
transform: scale(0.95);
}
.NewButton::before,
.NewButton::after {
content: '';
position: absolute;
top: calc(50% - 1.5px);
left: calc(50% - 13px);
width: 26px;
height: 3px;
background-color: white;
}
.NewButton::after {
transform: rotate(90deg);
}
在“我的”页面中,添加按钮。打开 src/Me.js
并编辑
import NewButton from './NewButton'
function Me (props) {
// ...codes
;<div className='App__content'>
<EventList request={handleListRequest} showUserInfo={false} />
{/* 添加 NewButton 代码 */}
<NewButton
onClick={() => {
props.history.push('/event/new')
}}
/>
</div>
}
打开浏览器,访问“我的”页面,看到的效果如下图所示:
点击按钮,可以进入“创建事件”页面
实现“创建事件”页面
“创建事件”页面其实就是一个表单, 我们可以引入地方的库(如 Formik),来管理表单的状态。但是在这个教程里面,只是简单的 react 组件来进行表单设计。
在开始表单编辑之前,我们先继续文章开头的思路,将数据先接口,再去处理交互。所以我们先来修改一下 src/EventCreation.js
使其能够创建一个事件。
打开src/EventCreation.js
,并编辑:
import React, { useContext } from 'react'
import { createEvent, publishEvent } from './requests'
import { AuthInfo } from './AuthInfoProvider'
function EventCreation () {
const {
state: { token }
} = useContext(AuthInfo)
return (
<div>
<button
onClick={() => {
const date = new Date()
const formatted = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`
const payload = {
title: 'Demo',
content: 'DEMO CONTENT',
html_content: '<h1>Demo</h1><p>DEMO CONTENT</p>',
event_time: formatted
}
createEvent(payload, token)
.then(({ data }) => {
console.log(data)
})
.catch(err => {
global.alert(err.message)
})
}}
>
Demo 事件
</button>
</div>
)
}
export default EventCreation
我们在 EventCreation 页面添加了一个按钮,点击这个按钮后,应用会以“当前用户”的身份创建一个“事件”。当创建成功后,会在 console 中输出刚刚新建的“事件”。
接下来,我们开始进行“事件创建表单”的编写。 可以先把上面添加的带面注释掉。
import React, { useContext, useState } from 'react'
import { createEvent, publishEvent } from './requests'
import { AuthInfo } from './AuthInfoProvider'
const getCurrentDate = () => {
const date = new Date()
return `${date.getFullYear()}-${date.toLocaleDateString('en-US', {
month: '2-digit'
})}-${date.getDate()}`
}
function EventCreation (props) {
const {
state: { token }
} = useContext(AuthInfo)
const [formValues, setFormValues] = useState({
title: '',
content: '',
event_time: getCurrentDate()
})
const [isSubmitting, setIsSubmitting] = useState(false)
return (
<form
className='App'
onSubmit={e => {
e.preventDefault()
// TODO should validate data before submit.
setIsSubmitting(true)
createEvent(
{
...formValues,
html_content: `<h1>${formValues.title}</h1><p>${formValues.content}</p>`
},
token
)
.then(({ data }) => {
console.log(data)
return publishEvent(data.id, token)
// 返回上一页
})
.then(() => {
props.history.goBack()
})
.catch(err => {
global.alert(err.message)
setIsSubmitting(false)
})
}}
>
<div className='App__header'>
<div className='PageHeader'>
<div className='PageHeader__left pl_12'>
<button
className='button button_merge'
type='button'
onClick={() => {
props.history.goBack()
}}
disabled={isSubmitting}
>
取消
</button>
</div>
<h3 className='PageTitle'>新事件</h3>
<div className='PageHeader__right pr_12'>
<button
className='button button_merge'
type='submit'
disabled={isSubmitting}
>
发布
</button>
</div>
</div>
</div>
<div className='App__content Form'>
<div className='FormItem'>
<label htmlFor='event_time' className='FormLabel'>
日期
</label>
<input
type='date'
id='event_time'
name='event_time'
className='FormInput'
value={formValues.event_time}
onChange={e => {
setFormValues({
...formValues,
event_time: e.target.value
})
}}
/>
</div>
<div className='FormItem'>
<label htmlFor='title' className='FormLabel'>
标题
</label>
<input
type='text'
id='title'
name='title'
value={formValues.title}
className='FormInput'
onChange={e => {
setFormValues({
...formValues,
title: e.target.value
})
}}
/>
</div>
<div className='FormItem'>
<label htmlFor='content' className='FormLabel'>
内容
</label>
<textarea
id='content'
value={formValues.content}
name='content'
className='FormInput'
onChange={e => {
setFormValues({
...formValues,
content: e.target.value
})
}}
/>
</div>
</div>
</form>
)
}
export default EventCreation
打开 src/index.css
,添加表单相关样式
.Form {
padding-left: 12px;
}
.FormItem {
padding: 5px 12px 5px 3px;
border-bottom: 1px dashed #ccc;
}
.FormLabel {
font-size: 14px;
display: block;
margin-bottom: 5px;
}
.FormInput {
display: block;
width: 100%;
border: none;
background: #f3f3f3;
font-size: 16px;
padding: 3px;
resize: vertical;
box-sizing: border-box;
}
textarea.FormInput {
min-height: 3em;
}
编辑完成后,访问“我的”页面,看到的效果如下图所示:
输入内容后,点击“发布”按钮,我们就可以完成“事件”的创建,及发布。
注意:上面的代码中,我们在创建“事件”之后,立刻调用了“发布”接口,所以达到了点击“发布”按钮就可以发布事件的效果,跳过了原模型设计中的“草稿”功能。
添加 “删除事件” 功能
接下来我们要实现删除事件的功能,我们将在 EventItem
里面添加一个“删除”按钮。当“当前用户”为“事件”拥有者时,用户可以看到”删除“按钮。用户点击”删除”按钮后,页面弹出提示框要求用户进行确认。待用户确认删除“事件”后,应用向后台提交“删除”请求。 待后台返回删除结构后,更新页面列表数据。
打开 src/EventItem.js
,开始修改
import React, { useContext } from 'react'
import { AuthInfo } from './AuthInfoProvider'
import { deleteEvent } from './requests'
function EventItem (props) {
const {
state: { token, currentUser }
} = useContext(AuthInfo)
const canDelete = currentUser && currentUser.uid === data.user.uid
return (
<div className='EventItem'>
{/* ... */}
{canDelete && (
<button
className='EventItem__deleteBtn button button_dashed button_sm'
onClick={() => {
if (window.confirm('确定删除事件?')) {
deleteEvent(data.id, token)
.then(() => {
props.onDeleted(data)
})
.catch(err => {
alert(err.message)
})
}
}}
>
×
</button>
)}
</div>
)
}
上面的代码中,我们使用从 AuthInfoContext 中获取到了当前用户数据,并根据情况显示删除按钮。并且在调用 File-X.Event: delete 的API来删除事件,删除成功之后,我们需要通知上层的组件 EventList
,将 “事件”从列表中删除。
打开 src/EventList.js
,并编辑
// 找到 EventItem
<EventItem
data={item}
showUserInfo={showUserInfo}
key={item.id}
onDeleted={(data) => {
const filtered = state.items.filter((item) => item.id !== data.id);
setState({
...state,
items: filtered
})
}}
/>
编辑后的效果如下图:
小结
这一章节中,我们实现了两个功能点:1. 新建并发布事件 2. 删除事件。包含的知识点包含:
- 使用 React 制作简单的网页表单,并将数据提交到服务器上。
- 如何通过 props 的方式,将“子组件”的变化通知到“父组件”
额外任务
更新事件的功能,还没有实现,大家可以参考教程中提到的思路来尝试一下。
- 判断当前用户是否有权修改“事件”。如果“有”,显示修改按钮。用户点击后进入“事件编辑”页面
- 添加“事件编辑”路由
- 参考“事件创建”,新建表单,根据“事件”的数据,初始化表单
- 待“事件”更新后,通知“父组件”更新列表数据。