0%

《React后台管理系统实战:五》产品管理(二):产品添加页面及验证等、富文本编辑器、提交商品

详情请点阅读全文

一、产品添加基础部分

1 home.jsx点添加按钮动作跳转到添加商品页

点击:onClick={() => this.props.history.push('/product/add-update')}>

1
2
3
4
5
6
7
//card右侧内容
const extra=(
<Button type='primary' onClick={() => this.props.history.push('/product/add-update')}>
<Icon type='plus'/>
添加商品
</Button>
)

2 静态页面 add-update.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import React,{Component} from 'react'
import {
Card,
Icon,
Form,
Input,
Cascader,//级联组件
Upload, //上传组件
Button,
message,
} from 'antd'
import LinkButton from '../../../components/link-button'
const {Item}=Form
const {TextArea}=Input

export default class AddUpdate extends Component{
render(){
//card左
const title=(
<span>
<LinkButton>
<Icon type='arrow-left' style={{fontSize:20}} />
</LinkButton>
<span>添加商品</span>
</span>
)

//form内的Item的布局样式
const formItemLayout = {
labelCol: {span: 2}, //左侧label标签的宽度占2个格栅
wrapperCol: {span: 8 }, //右侧(输入框外面有一层包裹)占8个格栅
};
return(
<Card title={title} extra=''>
{/* 使用组件的扩展属性语法 */}
<Form {...formItemLayout}>
{/* label指定商品前面标签名,placeholder指定输入框提示内容 */}
<Item label='商品名称'>
<Input placeholder='输入商品名' />
</Item>

<Item label='商品描述'>
{/* autoSize指定文本域最小高度和最大高度 */}
<TextArea placeholder='输入商品描述' autoSize={{ minRows: 2, maxRows: 6 }} />
</Item>

<Item label='商品价格'>
<Input type='number' placeholder='输入商品价格' addonAfter="元" />
</Item>

<Item label='商品分类'>
<Input placeholder='输入商品分类' />
</Item>

<Item label='商品图片'>
<Input placeholder='输入商品图片' />
</Item>

<Item label='商品详情'>
<Input placeholder='输入商品详情' />
</Item>

<Item >
<Button type='primary'>提交</Button>
</Item>

</Form>
</Card>
)
}
}

效果:http://localhost:3000/product/add-update

在这里插入图片描述

3 表单验证add-update.jsx

【0】包装当前类使得到form的的强大函数
【0-1】解构得到from的getFieldDecorator
【1】商品名规则
【2】商品描述验证规则
【3】商品价格验证规则:知识点自自定义验证函数
【5】表单提交验证
【6】自定义验证规则要求价格大于0
【7】自定义验证:商品价格大于0函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import React,{Component} from 'react'
import {
Card,
Icon,
Form,
Input,
Cascader,//级联组件
Upload, //上传组件
Button,
message,
} from 'antd'
import LinkButton from '../../../components/link-button'
const {Item}=Form
const {TextArea}=Input

class AddUpdate extends Component{
//【5】表单提交验证
submit=()=>{
this.props.form.validateFields((err,v)=>{
if(!err){
alert('产品添加中')
}
})
}
//【7】自定义验证:商品价格大于0函数
valiPrice=(rule, value, callback)=>{
console.log(value,typeof(value)) //在价格输入-1即显示是string类型
if(value*1>0){ //字符串*1:将字符串转化为数字类型
callback('验证通过') //每个节点必须调用否validateFields则会验证失败
}else{
callback('价格必须大于0')
}
}
render(){
//card左
const title=(
<span>
<LinkButton>
<Icon type='arrow-left' style={{fontSize:20}} />
</LinkButton>
<span>添加商品</span>
</span>
)
//card右

//form内的Item的布局样式
const formItemLayout = {
labelCol: {span: 2}, //左侧label标签的宽度占2个格栅
wrapperCol: {span: 8 }, //右侧(输入框外面有一层包裹)占8个格栅
};
//【0-1】获取from的getFieldDecorator
const {getFieldDecorator}=this.props.form
return(
<Card title={title} extra=''>
{/* 使用组件的扩展属性语法 */}
<Form {...formItemLayout}>
{/* label指定商品前面标签名,placeholder指定输入框提示内容 */}
<Item label='商品名称'>
{//【1】商品名规则
getFieldDecorator('name',{
initialValue:'',
rules:[
{required:true,message:'商品名称必须填写'}
]
})(<Input placeholder='输入商品名' />)
}

</Item>

<Item label='商品描述'>
{//【2】
getFieldDecorator('desc',{
initialValue:'',
rules:[
{required:true,message:'商品描述必须输入'}
]
})(<TextArea placeholder='输入商品描述' autoSize={{ minRows: 2, maxRows: 6 }} />)
}
{/* autoSize指定文本域最小高度和最大高度 */}

</Item>

<Item label='商品价格'>
{//【3】
getFieldDecorator('price',{
initialValue:'',
rules:[
{required:true,message:'价格必须输入'},
{validator:this.valiPrice},//【6】自定义验证规则要求价格大于0
]
})(<Input type='number' placeholder='输入商品价格' addonAfter="元" />)
}

</Item>

<Item label='商品分类'>
<Input placeholder='输入商品分类' />
</Item>

<Item label='商品图片'>
<Input placeholder='输入商品图片' />
</Item>

<Item label='商品详情'>
<Input placeholder='输入商品详情' />
</Item>

<Item >
<Button type='primary' onClick={this.submit}>提交</Button>
</Item>

</Form>
</Card>
)
}
}
export default Form.create()(AddUpdate) //【0】包装当前类使得到form的的强大函数

效果:http://localhost:3000/product/add-update

在这里插入图片描述

4 商品分类:Cascader级联组件

代码add-update.jsx

【0】定义状态选项
【1】级联商品分类
【2】获取categorys
【3】把获取到的categorys解析为options
【4】加载categorys并初始化为
【5】加载二级分类列表函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
import React,{Component} from 'react'
import {
Card,
Icon,
Form,
Input,
Cascader,//级联组件
Upload, //上传组件
Button,
message,
} from 'antd'
import LinkButton from '../../../components/link-button'
import {reqCategorys} from '../../../api'
const {Item}=Form
const {TextArea}=Input

// 定义选项
// const options = [
// {
// value: 'zhejiang',
// label: 'Zhejiang',
// isLeaf: false,
// },
// {
// value: 'jiangsu',
// label: 'Jiangsu',
// isLeaf: false,
// },
// ];


class AddUpdate extends Component{
state={
options:[], //【0】定义状态选项
}

//表单提交验证
submit=()=>{
this.props.form.validateFields((err,v)=>{
if(!err){
alert('产品添加中')
}
})
}

//【3】把获取到的categorys解析为options
initOptions=(categorys)=>{
const options = categorys.map((v,k)=>({ //返回一个字典,要额外加一个括号
value: v._id,
label: v.name,
isLeaf: false,
}))

this.setState({options})
}

//【2】获取categorys
getCategorys= async (parentId)=>{
const result = await reqCategorys(parentId)
if(result.status===0){
const categorys = result.data
// 如果是一级分类列表
if (parentId==='0') {
this.initOptions(categorys)
} else { // 二级列表
return categorys // 返回二级列表 ==> 当前async函数返回的promsie就会成功且value为categorys
}
}else{
message.error('产品分类获取失败请刷新重试')
}
}

//自定义验证:商品价格大于0函数
valiPrice=(rule, value, callback)=>{
console.log(value,typeof(value)) //在价格输入-1即显示是string类型
if(value*1>0){ //字符串*1:将字符串转化为数字类型
callback('验证通过')
}else{
callback('价格必须大于0')
}
}

//【5】加载二级分类列表函数
loadData = async selectedOptions => {
const targetOption = selectedOptions[0];
targetOption.loading = true;

// 根据选中的分类, 请求获取二级分类列表
const subCategorys = await this.getCategorys(targetOption.value)
// 隐藏loading
targetOption.loading = false
// 二级分类数组有数据
if (subCategorys && subCategorys.length>0) {
// 生成一个二级列表的options
const childOptions = subCategorys.map(c => ({
value: c._id,
label: c.name,
isLeaf: true
}))
// 关联到当前option上
targetOption.children = childOptions
} else { // 当前选中的分类没有二级分类
targetOption.isLeaf = true
}

// 更新options状态
this.setState({
options: [...this.state.options],
})
};

componentDidMount(){
this.getCategorys('0') //【4】加载categorys并初始化为
}

render(){
//card左
const title=(
<span>
<LinkButton>
<Icon type='arrow-left' style={{fontSize:20}} />
</LinkButton>
<span>添加商品</span>
</span>
)
//card右

//form内的Item的布局样式
const formItemLayout = {
labelCol: {span: 2}, //左侧label标签的宽度占2个格栅
wrapperCol: {span: 8 }, //右侧(输入框外面有一层包裹)占8个格栅
};

//获取from的getFieldDecorator
const {getFieldDecorator}=this.props.form
return(
<Card title={title} extra=''>
{/* 使用组件的扩展属性语法 */}
<Form {...formItemLayout}>
{/* label指定商品前面标签名,placeholder指定输入框提示内容 */}
<Item label='商品名称'>
{//商品名规则
getFieldDecorator('name',{
initialValue:'',
rules:[
{required:true,message:'商品名称必须填写'}
]
})(<Input placeholder='输入商品名' />)
}

</Item>

<Item label='商品描述'>
{//
getFieldDecorator('desc',{
initialValue:'',
rules:[
{required:true,message:'商品描述必须输入'}
]
})(<TextArea placeholder='输入商品描述' autoSize={{ minRows: 2, maxRows: 6 }} />)
}
{/* autoSize指定文本域最小高度和最大高度 */}

</Item>

<Item label='商品价格'>
{//
getFieldDecorator('price',{
initialValue:'',
rules:[
{required:true,message:'价格必须输入'},
{validator:this.valiPrice},//自定义验证规则要求价格大于0
]
})(<Input type='number' placeholder='输入商品价格' addonAfter="元" />)
}

</Item>

<Item label='商品分类'>
{/*【1】级联商品分类 */}
<Cascader
placeholder='请选择'
options={this.state.options}
loadData={this.loadData}
/>
</Item>

<Item label='商品图片'>
<Input placeholder='输入商品图片' />
</Item>

<Item label='商品详情'>
<Input placeholder='输入商品详情' />
</Item>

<Item >
<Button type='primary' onClick={this.submit}>提交</Button>
</Item>

</Form>
</Card>
)
}
}
export default Form.create()(AddUpdate) //包装当前类使得到form的的强大函数

效果:http://localhost:3000/product/add-update

在这里插入图片描述

5 图片上传部分

图片上传因为代码较多所以单独写个组件,引入进来

.5.1图片上传组件编写pictures-walls.jsx

【1】上传图片的接口地址
【2】只接受图片格式
【3】请求参数名,来自api说明上传图片的参数类型
【5】file: 当前操作的图片文件(上传/删除)
fileList: 所有已上传图片文件对象的数组
官方文档:https://ant.design/components/upload-cn/#onChange
【6】 一旦上传成功, 将当前上传的file的信息修正成最新的(name, url)
【7】在操作(上传/删除)过程中不断更新fileList状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import React,{Component} from 'react'
import { Upload, Icon, Modal,message } from 'antd';

function getBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = error => reject(error);
});
}

export default class PicturesWall extends Component {
state = {
previewVisible: false,
previewImage: '',
fileList: [
// {
// uid: '-1',
// name: 'image.png',
// status: 'done',
// url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
// }

],
};

handleCancel = () => this.setState({ previewVisible: false });

handlePreview = async file => {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj);
}

this.setState({
previewImage: file.url || file.preview,
previewVisible: true,
});
};

/*
【5】file: 当前操作的图片文件(上传/删除)
fileList: 所有已上传图片文件对象的数组
官方文档:https://ant.design/components/upload-cn/#onChange
*/
handleChange = ({ file,fileList }) => {
console.log('handlechange:',file.status, fileList.length, file===fileList[fileList.length-1])

//【6】 一旦上传成功, 将当前上传的file的信息修正最新的(name, url)
if(file.status==='done'){
const result = file.response // {status: 0, data: {name: 'xxx.jpg', url: '图片地址'}}
if(result.status===0){
message.success('上传成功')
const {name, url} = result.data
file = fileList[fileList.length-1]
file.name = name
file.url = url
}else{
message.error('上传错误')
}
}

// 【7】在操作(上传/删除)过程中不断更新fileList状态
this.setState({ fileList })
}

render() {
const { previewVisible, previewImage, fileList } = this.state;
const uploadButton = (
<div>
<Icon type="plus" />
<div className="ant-upload-text">Upload</div>
</div>
);
return (
<div className="clearfix">
<Upload
action="/manage/img/upload" /**【1】上传图片的接口地址 */
accept='image/*' /**【2】只接受图片格式 */
name='image' /**【3】请求参数名,来自api说明上传图片的参数类型 */
listType="picture-card" /*卡片样式:text, picture 和 picture-card*/
fileList={fileList} /*所有已上传图片文件对象的数组*/
onPreview={this.handlePreview} /**显示图片预览函数 */
onChange={this.handleChange} /**上传/删除图片函数 */
>
{//控制图片上传按钮最多5个
fileList.length >= 5 ? null : uploadButton}
</Upload>
<Modal visible={previewVisible} footer={null} onCancel={this.handleCancel}>
<img alt="example" style={{ width: '100%' }} src={previewImage} />
</Modal>
</div>
);
}
}

.5.2 获取子组件的图片信息:父子组件传值

ref的官方教程:https://zh-hans.reactjs.org/docs/refs-and-the-dom.html
首先,从add-update.jsx内引入picture-wall.jsx

.5.2.1 picture-wall.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
constructor(props){
super(props)

this.state={
previewVisible: false,
previewImage: '',
fileList: []
}
}

/*
【1】获取所有已上传图片文件名的数组
*/
getImgs = () => {
//返回状态中的文件列表中每个文件的文件名
return this.state.fileList.map(file => file.name)
}

.5.2.1 add-update.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    constructor(props){
super(props)

this.state={
options:[], //定义状态选项
}

//【1】创建用于存放指定ref标识的标签对象容器
this.pw=React.createRef()
}

//表单提交验证
submit=()=>{
this.props.form.validateFields((error,values)=>{
if(!error){
//【2】获取子组件的相关图片名数组信息
const imgs=this.pw.current.getImgs()
alert('成功')
//【3】输出看看
console.log('表单,图片:',values,imgs)
}else{
console.log('失败')
}
})
}

//【4】render()内组件,ref标记为[1]处的容器
<Item label='商品图片'>
<PicturesWall ref={this.pw} />
</Item>

效果:提交后可看到获取到的数据

在这里插入图片描述

6 图片删除(移除前端并删除服务器对应图片)

第一步:编写删除api函数src/api/index.js

删除服务器上指定名称图片

1
2
3
4
5
6
7
8
import ajax from './ajax'
import jsonp from 'jsonp'
import {message} from 'antd'
// const BASE = 'http://localhost:5000'
const BASE = ''

// 删除服务器上指定名称图片
export const reqDeletPic=(name)=>ajax(BASE+'/manage/img/delete',{name},'POST')

第二步 执行删除 src/pages/product/pictures-wall.jsx

修改函数:handleChange加入删除对应图片代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import {reqDeletPic} from '../../../api' //【1】

/*
file: 当前操作的图片文件(上传/删除)
fileList: 所有已上传图片文件对象的数组
官方文档:https://ant.design/components/upload-cn/#onChange
*/
handleChange = async ({ file,fileList }) => { //【3】async
console.log('handlechange:',file.status, fileList.length, file===fileList[fileList.length-1])

// 一旦上传成功, 将当前上传的file的信息修正成最新的(name, url)
if(file.status==='done'){
const result = file.response // {status: 0, data: {name: 'xxx.jpg', url: '图片地址'}}
if(result.status===0){
message.success('上传成功')
const {name, url} = result.data
file = fileList[fileList.length-1]
file.name = name
file.url = url
}else{
message.error('上传错误')
}
}else if(file.status==='removed'){//【2】如果文件的状态为移除,则删除服务器上对应图片名图片
const result=await reqDeletPic(file.name)
if(result.status===0){
message.success('图片删除成功:'+file.name)
}else{
message.error('图片删除失败:'+file.name)
}
}

// 在操作(上传/删除)过程中不断更新fileList状态
this.setState({ fileList })
}

效果:

在这里插入图片描述

二、商品详情:富文本编辑器

1 使用基于react的富文本编程器插件库: react-draft-wysiwyg

官方文档:https://github.com/jpuri/react-draft-wysiwyg
官方实例:https://jpuri.github.io/react-draft-wysiwyg/#/demo
安装1:cnpm install --save react-draft-wysiwyg draft-js
安装2:cnpm i --save draftjs-to-html
安装3:cnpm i --save html-to-draftjs

第一步,建新组件src/pages/product/rich-text.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import React, { Component } from 'react';
import { EditorState, convertToRaw } from 'draft-js';
import { Editor } from 'react-draft-wysiwyg';
import draftToHtml from 'draftjs-to-html';
import htmlToDraft from 'html-to-draftjs';
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css' //【1】引入编辑器样式,否则会乱七八糟


export default class RichText extends Component {
state = {
editorState: EditorState.createEmpty(),
}

onEditorStateChange=(editorState) => { //【2】标签写法改成如左写法
this.setState({
editorState,
});
};

render() {
const { editorState } = this.state;
return (
<div>
<Editor
editorState={editorState}
wrapperClassName="demo-wrapper"
editorClassName="demo-editor"
onEditorStateChange={this.onEditorStateChange}
/*【3】给编辑器加内置样式,边框,高等*/
editorStyle={{border: '1px solid black', minHeight: 200, paddingLeft: 10}}
/>
<textarea
disabled
value={draftToHtml(convertToRaw(editorState.getCurrentContent()))}
/>
</div>
);
}
}

第二步,引入编辑器组件

在add-updgate.jsx内引入组件

1
2
3
4
5
6
import RichText from './rich-text'

//render()内
<Item label='商品详情'>
<RichText />
</Item>

第三步,编辑器尺寸改大

开始编辑器尺寸非常小,原因是/src/pages/product/add-update.jsx里定义了表单的格栅尺寸:

1
2
3
4
5
//form内的Item的布局样式
const formItemLayout = {
labelCol: {span: 2}, //左侧label标签的宽度占2个格栅
wrapperCol: {span: 8 }, //右侧(输入框外面有一层包裹)占8个格栅
};

修改编辑器大小/src/pages/product/add-update.jsx

1
2
3
<Item label='商品详情' labelCol={{span: 2}} wrapperCol={{span: 20}}>
<RichText />
</Item>

2 父子传表单数据:点提交时获取富文本编辑器的内容

第一步,在rich-text.jsx内建立返回带html标签的字符串数据函数

1
2
3
4
//【1】让父组件获取到当前组件的信息(state之下建立即可)
getDetail=()=>{
return draftToHtml(convertToRaw(this.state.editorState.getCurrentContent()))
}

第二步,在父组件add-update.jsx内接收子组件的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
    constructor(props){
super(props)
//创建用于存放指定ref标识的标签对象容器
this.pw=React.createRef()
//【1】建立空容器
this.editor=React.createRef()

this.state={
options:[], //定义状态选项
}
}



//表单提交验证
submit=()=>{
this.props.form.validateFields((error,values)=>{
if(!error){
//获取子组件的相关信息
const imgs=this.pw.current.getImgs()
//【3】获取子组件商品详情的带html标签的字符串数据
const detail=this.editor.current.getDetail()
alert('成功')
//【4】输出看看
console.log('表单,图片,详情:',values,imgs,detail)
}else{
console.log('失败')
}
})
}


//render(){}内
<Item label='商品详情' labelCol={{span: 2}} wrapperCol={{span: 20}}>
{/**【2】指定把richtext对象装进editor里 */}
<RichText ref={this.editor} />
</Item>

效果:填写商品信息,提交后,控制台输出

1
2
表单,图片,详情: {name: "游客131071427", desc: "a", price: "2", categoryIds: Array(1)} [] 
<p><strong>你好</strong>在 <span style="color: rgb(226,80,65);">地地地柑</span> <del>工 城</del>&nbsp;&nbsp;</p>

3 图片上传:没有按钮,加个,并实现功能rich-text.jsx

【1】图片上传按钮配置
【2】图片上传加一个上传按钮功能实现函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//【2】图片上传加一个上传按钮功能实现函数
uploadImageCallBack = (file) => {
return new Promise(
(resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('POST', '/manage/img/upload')
const data = new FormData()
data.append('image', file)
xhr.send(data)
xhr.addEventListener('load', () => {
const response = JSON.parse(xhr.responseText)
const url = response.data.url // 得到图片的url
resolve({data: {link: url}}) //返回图片的地址
})
xhr.addEventListener('error', () => {
const error = JSON.parse(xhr.responseText)
reject(error)
})
}
)
}


//render(){}
render() {
const { editorState } = this.state;
return (
<div>
<Editor
editorState={editorState}
wrapperClassName="demo-wrapper"
editorClassName="demo-editor"
editorStyle={{border: '1px solid black', minHeight: 200, paddingLeft: 10}}
onEditorStateChange={this.onEditorStateChange}
/**【1】图片上传按钮配置*/
toolbar={{
image: { uploadCallback: this.uploadImageCallBack, alt: { present: true, mandatory: true } },
}}
/>
<textarea
disabled
value={draftToHtml(convertToRaw(editorState.getCurrentContent()))}
/>
</div>
);
}

效果:点富文本内的图片上传按钮可成功上传图片:

注意 alt 内容必须填个,否则无法上传图片
在这里插入图片描述

三、提交添加商品请求

第一步:编写api请求函数(src/api/index.js)

添加商品/修改商品:因为参数相同所以组成二合一接口,请求地址为按条件拼接,如果参数存在._id则为修改商品,否则为添加商品

1
2
//添加商品/修改商品:二合一接口,如果参数存在._id则为修改商品,否则为添加商品
export const reqAddUpdatePro=(product)=>ajax(BASE+'/manage/product/'+(product._id?'update':'add'),product,'POST')

第二步:收集表单数据(src/pages/product/add-update.jsx)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//表单提交验证
submit=()=>{
this.props.form.validateFields((error,values)=>{

//【2】调用接口请求函数去添加/更新
//【3】根据结果提示是否添加/更新成功
if(!error){
//【1】收集数据, 并封装成product对象
const {name,desc,price,categoryIds}=values
let pCategoryId,categoryId
if(categoryIds.length===1){
pCategoryId='0'
categoryId=categoryIds[0]
}else{
pCategoryId=categoryIds[0]
categoryId=categoryIds[1]
}
//获取子组件的相关信息
const imgs=this.pw.current.getImgs()
//获取子组件商品详情的带html标签的字符串数据
const detail=this.editor.current.getDetail()
//将收集到的表单数据封闭成product对象
const product={name,desc,price,imgs,detail,pCategoryId,categoryId}
//输出看看
console.log(product)
}else{
console.log('失败')
}


})
}

2步效果:填写完成添加产品表单,点提交按钮:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//只有一级分类的产品数据
categoryId: "5e4154e725a557082c18f430"
desc: "工"
detail: "<p>手镯</p>↵<img src="http://localhost:5000/upload/image-1582702969070.jpg" alt="顶替" style="height: auto;width: auto"/>↵<p></p>↵"
imgs: Array(2)
0: "image-1582702950591.jpg"
1: "image-1582702955364.jpg"
length: 2
__proto__: Array(0)
name: "游客131071427"
pCategoryId: "0"
price: "12"
__proto__: Object

//二级分类的产品数据
categoryId: "5e4771a418da331714a18693"
desc: "工"
detail: "<p>手镯</p>↵<img src="http://localhost:5000/upload/image-1582702969070.jpg" alt="顶替" style="height: auto;width: auto"/>↵<p></p>↵"
imgs: Array(2)
0: "image-1582702950591.jpg"
1: "image-1582702955364.jpg"
length: 2
__proto__: Array(0)
name: "游客131071427"
pCategoryId: "5e41549925a557082c18f426"
price: "12"
__proto__: Object

第三步:提交数据(添加商品)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import {reqCategorys,reqAddUpdatePro} from '../../../api' //【0】引入添加修改产品函数

//产品表单提交
submit=()=>{
this.props.form.validateFields(async(error,values)=>{

if(!error){
//【1】解构values收集数据, 并封装成product对象
const {name,desc,price,categoryIds}=values
let pCategoryId,categoryId
if(categoryIds.length===1){//如果长度为1说明只有一级产品分类
pCategoryId='0'
categoryId=categoryIds[0]
}else{//否则说明有二级产品分类
pCategoryId=categoryIds[0]
categoryId=categoryIds[1]
}
//获取子组件的相关信息
const imgs=this.pw.current.getImgs()
//获取子组件商品详情的带html标签的字符串数据
const detail=this.editor.current.getDetail()
//封闭成product对象
const product={name,desc,price,imgs,detail,pCategoryId,categoryId}
//输出看看
console.log(product)

//【2】调用接口请求函数去添加/更新
const result=await reqAddUpdatePro(product)
if(result.status===0){//【3】根据结果提示是否添加/更新成功
message.success('添加产品成功')
}else{
message.error('添加产品失败')
}

}else{
console.log('验证失败,请检查产品数据')
}

})



}

3步效果:全部完成点提交,将显示添加产品成功

附:完整代码

1.商品管理主页src/pages/product/home.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
import React,{Component} from 'react'
import {
Card,
Select,
Input,
Table,
Icon,
Button,
message
} from 'antd'
import LinkButton from '../../../components/link-button'
import {reqProducts} from '../../../api/' //【0】引入产品列表请求
import {PAGE_SIZE} from '../../../utils/constans' //【0.1】引入常量


const Option=Select.Option

export default class Home extends Component{
state={
//商品列表
products:[
// {
// "status": 1,
// "imgs": [
// "image-1559402396338.jpg"
// ],
// "_id": "5ca9e05db49ef916541160cd",
// "name": "联想ThinkPad 翼4809",
// "desc": "年度重量级新品,X390、T490全新登场 更加轻薄机身设计9",
// "price": 65999,
// "pCategoryId": "5ca9d6c0b49ef916541160bb",
// "categoryId": "5ca9db9fb49ef916541160cc",
// "detail": "<p><span style=\"color: rgb(228,57,60);background-color: rgb(255,255,255);font-size: 12px;\">想你所需,超你所想!精致外观,轻薄便携带光驱,内置正版office杜绝盗版死机,全国联保两年!</span> 222</p>\n<p><span style=\"color: rgb(102,102,102);background-color: rgb(255,255,255);font-size: 16px;\">联想(Lenovo)扬天V110 15.6英寸家用轻薄便携商务办公手提笔记本电脑 定制 2G独显 内置</span></p>\n<p><span style=\"color: rgb(102,102,102);background-color: rgb(255,255,255);font-size: 16px;\">99999</span></p>\n",
// "__v": 0
// },
// {
// "status": 1,
// "imgs": [
// "image-1559402448049.jpg",
// "image-1559402450480.jpg"
// ],
// "_id": "5ca9e414b49ef916541160ce",
// "name": "华硕(ASUS) 飞行堡垒",
// "desc": "15.6英寸窄边框游戏笔记本电脑(i7-8750H 8G 256GSSD+1T GTX1050Ti 4G IPS)",
// "price": 6799,
// "pCategoryId": "5ca9d6c0b49ef916541160bb",
// "categoryId": "5ca9db8ab49ef916541160cb",
// "detail": "<p><span style=\"color: rgb(102,102,102);background-color: rgb(255,255,255);font-size: 16px;\">华硕(ASUS) 飞行堡垒6 15.6英寸窄边框游戏笔记本电脑(i7-8750H 8G 256GSSD+1T GTX1050Ti 4G IPS)火陨红黑</span>&nbsp;</p>\n<p><span style=\"color: rgb(228,57,60);background-color: rgb(255,255,255);font-size: 12px;\">1T+256G高速存储组合!超窄边框视野无阻,强劲散热一键启动!</span>&nbsp;</p>\n",
// "__v": 0
// },
// {
// "status": 2,
// "imgs": [
// "image-1559402436395.jpg"
// ],
// "_id": "5ca9e4b7b49ef916541160cf",
// "name": "你不知道的JS(上卷)",
// "desc": "图灵程序设计丛书: [You Don't Know JS:Scope & Closures] JavaScript开发经典入门图书 打通JavaScript的任督二脉",
// "price": 35,
// "pCategoryId": "0",
// "categoryId": "5ca9d6c9b49ef916541160bc",
// "detail": "<p style=\"text-align:start;\"><span style=\"color: rgb(102,102,102);background-color: rgb(255,255,255);font-size: 16px;\">图灵程序设计丛书:你不知道的JavaScript(上卷)</span> <span style=\"color: rgb(102,102,102);background-color: rgb(255,255,255);font-size: 16px;\"><strong>[You Don't Know JS:Scope &amp; Closures]</strong></span></p>\n<p style=\"text-align:start;\"><span style=\"color: rgb(227,57,60);background-color: rgb(255,255,255);font-size: 12px;\">JavaScript开发经典入门图书 打通JavaScript的任督二脉 领略语言内部的绝美风光</span>&nbsp;</p>\n",
// "__v": 0
// },
// {
// "status": 2,
// "imgs": [
// "image-1554638240202.jpg"
// ],
// "_id": "5ca9e5bbb49ef916541160d0",
// "name": "美的(Midea) 213升-BCD-213TM",
// "desc": "爆款直降!大容量三口之家优选! *节能养鲜,自动低温补偿,36分贝静音呵护",
// "price": 1388,
// "pCategoryId": "5ca9d695b49ef916541160ba",
// "categoryId": "5ca9d9cfb49ef916541160c4",
// "detail": "<p style=\"text-align:start;\"><span style=\"color: rgb(102,102,102);background-color: rgb(255,255,255);font-size: 16px;font-family: Arial, \"microsoft yahei;\">美的(Midea) 213升 节能静音家用三门小冰箱 阳光米 BCD-213TM(E)</span></p>\n<p><span style=\"color: rgb(228,57,60);background-color: rgb(255,255,255);font-size: 12px;font-family: tahoma, arial, \"Microsoft YaHei\", \"Hiragino Sans GB\", u5b8bu4f53, sans-serif;\">爆款直降!大容量三口之家优选! *节能养鲜,自动低温补偿,36分贝静音呵护! *每天不到一度电,省钱又省心!</span>&nbsp;</p>\n",
// "__v": 0
// },
// {
// "status": 1,
// "imgs": [
// "image-1554638403550.jpg"
// ],
// "_id": "5ca9e653b49ef916541160d1",
// "name": "美的(Midea)KFR-35GW/WDAA3",
// "desc": "正1.5匹 变频 智弧 冷暖 智能壁挂式卧室空调挂机",
// "price": 2499,
// "pCategoryId": "5ca9d695b49ef916541160ba",
// "categoryId": "5ca9da1ab49ef916541160c6",
// "detail": "<p style=\"text-align:start;\"><span style=\"color: rgb(102,102,102);background-color: rgb(255,255,255);font-size: 16px;\">美的(Midea)正1.5匹 变频 智弧 冷暖 智能壁挂式卧室空调挂机 KFR-35GW/WDAA3@</span></p>\n<p style=\"text-align:start;\"></p>\n<p><span style=\"color: rgb(228,57,60);background-color: rgb(255,255,255);font-size: 12px;\">提前加入购物车!2299元成交价!前50名下单送赠品加湿型电风扇,赠完即止!8日0点开抢!</span><a href=\"https://sale.jd.com/mall/LKHdqZUIYk.html\" target=\"_blank\"><span style=\"color: rgb(94,105,173);background-color: rgb(255,255,255);font-size: 12px;\">更有无风感柜挂组合套购立减500元!猛戳!!</span></a>&nbsp;</p>\n",
// "__v": 0
// }
],
loading:false,
}
//Table的列名及对应显示的内容渲染
initColumns=()=>{
this.columns=[
{
title:'商品名称',
dataIndex:'name'
},
{
title:'商品描述',
dataIndex:'desc'
},
{
title:'价格',
dataIndex:'price',
render:(price)=>'¥'+price //把price渲染进对应的行,并加上¥符号
},
{
width:100,
title:'商品状态',
dataIndex:'status',
render:(status)=>{
return(
<span>
<Button type='primary'>{status===1 ? '下架' : '上架'}</Button>
<span>{status===1 ? '在售':'已下架'}</span>
</span>
)
}
},
{
width:100,
title:'操作',

render:(proObj)=>{
return(
<span>
<LinkButton>详情</LinkButton>
<LinkButton>修改</LinkButton>
</span>
)
}
},
]
}

// addPro=async()=>{
// const result=await reqAddPro('5e41549925a557082c18f426','0','桔子','desc','price','detail',[])
// console.log(result)
// if (result.status===0){
// message.success('产品添加成功')
// }else{
// message.error(result.msg)
// }
// }

//【1】请求产品列表放入state,后台分页
getProducts=async(pageNum)=>{//pageNum为请求页码
this.setState({loading:true}) //设置加载动画开始显示
this.pageNum=pageNum //接收参数
const result = await reqProducts({pageNum,PAGE_SIZE})
this.setState({loading:true}) //关闭加载动画
if(result.status===0){
console.log(result.data.list)
//this.setState({products:result.data})
}else{
message.error('加载产品失败,请刷新页面重试')
}
}

componentWillMount(){
//Table列名初始化函数调用,用于准备表格列名及显示内容
this.initColumns()
//this.addPro()
}

componentDidMount(){
this.getProducts(1)

}

render(){
//state数据解构,简化使用
const {products}=this.state

//card左侧内容
const title=(
<span>
<Select value='1' style={{width:150,}}>
<Option value='1'>按名称搜索</Option>
<Option value='2'>按描述搜索</Option>
</Select>
<Input placeholder='关键字' style={{width:150,margin:'0 8px'}} />
<Button type='primary'>搜索</Button>
</span>
)
//card右侧内容
const extra=(
<Button type='primary' onClick={() => this.props.history.push('/product/add-update')}>
<Icon type='plus'/>
添加商品
</Button>
)
return(
<Card title={title} extra={extra}>
<Table
bordered
rowKey='_id'
dataSource={products}
columns={this.columns} />
</Card>
)
}
}

商品管理主页效果http://localhost:3000/product

在这里插入图片描述

2.商品添加组件src/pages/product/add-update.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
import React,{Component} from 'react'
import {
Card,
Icon,
Form,
Input,
Cascader,//级联组件
Button,
message,
} from 'antd'
import LinkButton from '../../../components/link-button'
import {reqCategorys,reqAddUpdatePro} from '../../../api' //【0】引入添加修改产品函数
import PicturesWall from './pictures-wall'
import RichText from './rich-text'

const {Item}=Form
const {TextArea}=Input


class AddUpdate extends Component{
constructor(props){
super(props)
//创建用于存放指定ref标识的标签对象容器
this.pw=React.createRef()
//
this.editor=React.createRef()

this.state={
options:[], //定义状态选项
}
}


//把获取到的categorys解析为options
initOptions=(categorys)=>{
const options = categorys.map((v,k)=>({ //返回一个字典,要额外加一个括号
value: v._id,
label: v.name,
isLeaf: false,
}))

this.setState({
options
})
}

//获取categorys
getCategorys= async (parentId)=>{
const result = await reqCategorys(parentId)
if(result.status===0){
const categorys = result.data
// 如果是一级分类列表
if (parentId==='0') {
this.initOptions(categorys)
} else { // 二级列表
return categorys // 返回二级列表 ==> 当前async函数返回的promsie就会成功且value为categorys
}
}else{
message.error('产品分类获取失败请刷新重试')
}
}

//自定义验证:商品价格大于0函数
valiPrice=(rule, value, callback)=>{
//console.log(value,typeof(value)) //在价格输入-1即显示是string类型
if(value*1>0){ //字符串*1:将字符串转化为数字类型
callback()
}else{
callback('价格必须大于0')
}
}


onChange = (value, selectedOptions) => {
console.log(value, selectedOptions);
}


//加载二级分类列表函数
loadData = async selectedOptions => {
const targetOption = selectedOptions[0];
targetOption.loading = true

// 根据选中的分类, 请求获取二级分类列表
const subCategorys = await this.getCategorys(targetOption.value)
// 隐藏loading
targetOption.loading = false
// 二级分类数组有数据
if (subCategorys && subCategorys.length>0) {
// 生成一个二级列表的options
const childOptions = subCategorys.map(c => ({
value: c._id,
label: c.name,
isLeaf: true
}))
// 关联到当前option上
targetOption.children = childOptions
} else { // 当前选中的分类没有二级分类
targetOption.isLeaf = true
}

// 更新options状态
this.setState({
options: [...this.state.options],
})
}

//产品表单提交
submit=()=>{
this.props.form.validateFields(async(error,values)=>{


if(!error){
//【1】收集数据, 并封装成product对象
const {name,desc,price,categoryIds}=values
let pCategoryId,categoryId
if(categoryIds.length===1){//如果长度为1说明只有一级产品分类
pCategoryId='0'
categoryId=categoryIds[0]
}else{//否则说明有二级产品分类
pCategoryId=categoryIds[0]
categoryId=categoryIds[1]
}
//获取子组件的相关信息
const imgs=this.pw.current.getImgs()
//获取子组件商品详情的带html标签的字符串数据
const detail=this.editor.current.getDetail()

const product={name,desc,price,imgs,detail,pCategoryId,categoryId}
//输出看看
console.log(product)


//【2】调用接口请求函数去添加/更新
const result=await reqAddUpdatePro(product)
if(result.status===0){//【3】根据结果提示是否添加/更新成功
message.success('添加产品成功')
}else{
message.error('添加产品失败')
}

}else{
console.log('验证失败,请检查产品数据')
}


})





}




componentDidMount(){
this.getCategorys('0') //加载categorys并初始化为
}

render(){
//card左
const title=(
<span>
<LinkButton>
<Icon type='arrow-left' style={{fontSize:20}} />
</LinkButton>
<span>添加商品</span>
</span>
)
//card右

//form内的Item的布局样式
const formItemLayout = {
labelCol: {span: 2}, //左侧label标签的宽度占2个格栅
wrapperCol: {span: 8 }, //右侧(输入框外面有一层包裹)占8个格栅
};

//获取from的getFieldDecorator
const {getFieldDecorator}=this.props.form
return(
<Card title={title} extra=''>
{/* 使用组件的扩展属性语法 */}
<Form {...formItemLayout}>
{/* label指定商品前面标签名,placeholder指定输入框提示内容 */}
<Item label='商品名称'>
{//商品名规则
getFieldDecorator('name',{
initialValue:'',
rules:[
{required:true,message:'商品名称必须填写'}
]
})(<Input placeholder='输入商品名' />)
}

</Item>

<Item label='商品描述'>
{//autoSize指定文本域最小高度和最大高度
getFieldDecorator('desc',{
initialValue:'',
rules:[
{required:true,message:'商品描述必须输入'}
]
})(<TextArea placeholder='输入商品描述' autoSize={{ minRows: 2, maxRows: 6 }} />)
}
</Item>

<Item label='商品价格'>
{//validator自定义验证规则要求价格大于0
getFieldDecorator('price',{
initialValue:'',
rules:[
{required:true,message:'价格必须输入'},
{validator:(rule,value,callback)=>{
if(value*1>0){ //字符串*1:将字符串转化为数字类型
callback() //此处必须进行回调函数调用,否则将无法通过验证
}else{
callback('价格必须大于0')
}
}},
]
})(<Input type='number' placeholder='输入商品价格' addonAfter="元" />)
}
</Item>

<Item label="商品分类">
{
getFieldDecorator('categoryIds', {
initialValue: [],
rules: [
{required: true, message: '必须指定商品分类'},
]
})(<Cascader
placeholder='请指定商品分类'
options={this.state.options} /*需要显示的列表数据数组*/
loadData={this.loadData} /*当选择某个列表项, 加载下一级列表的监听回调*/
/>
)
}

</Item>

<Item label='商品图片'>
<PicturesWall ref={this.pw} />
</Item>

<Item label='商品详情' labelCol={{span: 2}} wrapperCol={{span: 20}}>
{/**指定把richtext对象装进editor里 */}
<RichText ref={this.editor} />
</Item>

<Item >
<Button type='primary' onClick={this.submit}>提交</Button>
</Item>

</Form>
</Card>
)
}
}
export default Form.create()(AddUpdate) //包装当前类使得到form的的强大函数

商品添加页效果:http://localhost:3000/product/add-update

在这里插入图片描述

3.异步请求函数src/api/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import ajax from './ajax'
import jsonp from 'jsonp'
import {message} from 'antd' //借用antd返回信息组件
// const BASE = 'http://localhost:5000'
const BASE = ''

//导出一个函数,第1种写法
//登录接口函数
// export function reqLogin(username,password){
// return ajax('login',{username,password},'POST')
// }

//导出一个函数,第2种写法
// 登录接口函数
export const reqLogin=(username,password)=>ajax(BASE+'login',{username,password},'POST')


//获取产品一级/二级分类列表接口
export const reqCategorys=(parentId)=>ajax(BASE+'/manage/category/list',{parentId})
//添加产品分类接口
export const reqAddCategory=(parentId,categoryName)=>ajax(BASE+'/manage/category/add',{parentId,categoryName},'POST')
//修改产品分类接口
export const reqUpdateCategory=({categoryId,categoryName})=>ajax(BASE+'/manage/category/update',{categoryId,categoryName},'POST')


//获取产品列表
export const reqProducts=({pageNum,pageSize})=>ajax(BASE+'/manage/product/list',{pageNum,pageSize})
//添加商品/修改商品:二合一接口,如果参数存在._id则为修改商品,否则为添加商品
export const reqAddUpdatePro=(product)=>ajax(BASE+'/manage/product/'+(product._id?'update':'add'),product,'POST')


// 删除服务器上指定名称图片
export const reqDeletPic=(name)=>ajax(BASE+'/manage/img/delete',{name},'POST')







// 天气接口
export const reqWeather=(city) => {
const url = `http://api.map.baidu.com/telematics/v3/weather?location=${city}&output=json&ak=3p49MVra6urFRGOT9s8UBWr2`
//返回一个promise函数
return new Promise((resolve,reject) => {
//发送一个jsonp请求
jsonp(url,{},(err,data) => {
//输出请求的数据到控制台
console.log('jsonp()', err, data)
//如果请求成功
if(!err && data.status==='success'){
//从数据中解构取出图片、天气
const {dayPictureUrl,weather}=data.results[0].weather_data[0]
//异步返回图片、天气给调用函数者
resolve({dayPictureUrl,weather})
}else{//如果请求失败
message.error('天气信息获取失败')
}
})
})
}
//reqWeather('上海')

4.商品路由页src/page/pruduct/index.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React,{Component} from 'react'
import './index.less'
import {Switch,Route,Redirect} from 'react-router-dom'

import Home from './home'
import AddUpdate from './add-update'
import Detail from './detail'

export default class Product extends Component{
render(){
return(
<Switch>
{/* 为防止不能匹配到product/xxx,加上exact */}
<Route exact path='/product' component={Home} />
<Route path='/product/add-update' component={AddUpdate} />
<Route path='/product/detail' component={Detail} />
{/* 如果以上都不匹配则跳转到产品首页 */}
<Redirect to='/product' />
</Switch>
)
}
}

5.富文件编辑组件rich-text.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import React, { Component } from 'react';
import { EditorState, convertToRaw } from 'draft-js';
import { Editor } from 'react-draft-wysiwyg';
import draftToHtml from 'draftjs-to-html';
import htmlToDraft from 'html-to-draftjs';
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css' //引入编辑器样式,否则会乱七八糟


export default class RichText extends Component {
state = {
editorState: EditorState.createEmpty(),
}

onEditorStateChange=(editorState) => { //标签写法改成如左写法
this.setState({
editorState,
});
};

//让父组件获取到当前组件的信息
getDetail=()=>{
return draftToHtml(convertToRaw(this.state.editorState.getCurrentContent()))
}

//【2】图片上传加一个上传按钮
uploadImageCallBack = (file) => {
return new Promise(
(resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('POST', '/manage/img/upload')
const data = new FormData()
data.append('image', file)
xhr.send(data)
xhr.addEventListener('load', () => {
const response = JSON.parse(xhr.responseText)
const url = response.data.url // 得到图片的url
resolve({data: {link: url}})
})
xhr.addEventListener('error', () => {
const error = JSON.parse(xhr.responseText)
reject(error)
})
}
)
}

render() {
const { editorState } = this.state;
return (
<div>
<Editor
editorState={editorState}
wrapperClassName="demo-wrapper"
editorClassName="demo-editor"
editorStyle={{border: '1px solid black', minHeight: 200, paddingLeft: 10}}
onEditorStateChange={this.onEditorStateChange}
/**【1】图片上传按钮配置*/
toolbar={{
image: { uploadCallback: this.uploadImageCallBack, alt: { present: true, mandatory: true } },
}}
/>
<textarea
disabled
value={draftToHtml(convertToRaw(editorState.getCurrentContent()))}
/>
</div>
);
}
}

6.图片上传组件src/page/product/pictures-wall.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import React,{Component} from 'react'
import { Upload, Icon, Modal,message } from 'antd';
import {reqDeletPic} from '../../../api' //【1】

function getBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = error => reject(error);
});
}

export default class PicturesWall extends Component {
constructor(props){
super(props)

this.state={
previewVisible: false,
previewImage: '',
fileList: []
}
}

/*
获取所有已上传图片文件名的数组
*/
getImgs = () => {
//返回状态中的文件列表中每个文件的文件名
return this.state.fileList.map(file => file.name)
}
// state = {
// previewVisible: false,
// previewImage: '',
// fileList: [
// // {
// // uid: '-1',
// // name: 'image.png',
// // status: 'done',
// // url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
// // }

// ],
// };

handleCancel = () => this.setState({ previewVisible: false });

handlePreview = async file => {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj);
}

this.setState({
previewImage: file.url || file.preview,
previewVisible: true,
});
};

/*
file: 当前操作的图片文件(上传/删除)
fileList: 所有已上传图片文件对象的数组
官方文档:https://ant.design/components/upload-cn/#onChange
*/
handleChange = async ({ file,fileList }) => { //【3】async
console.log('handlechange:',file.status, fileList.length, file===fileList[fileList.length-1])

// 一旦上传成功, 将当前上传的file的信息修正成最新的(name, url)
if(file.status==='done'){
const result = file.response // {status: 0, data: {name: 'xxx.jpg', url: '图片地址'}}
if(result.status===0){
message.success('上传成功')
const {name, url} = result.data
file = fileList[fileList.length-1]
file.name = name
file.url = url
}else{
message.error('上传错误')
}
}else if(file.status==='removed'){//【2】如果文件的状态为移除,则删除服务器上对应图片名图片
const result=await reqDeletPic(file.name)
if(result.status===0){
message.success('图片删除成功:'+file.name)
}else{
message.error('图片删除失败:'+file.name)
}
}

// 在操作(上传/删除)过程中不断更新fileList状态
this.setState({ fileList })
}

render() {
const { previewVisible, previewImage, fileList } = this.state;
const uploadButton = (
<div>
<Icon type="plus" />
<div className="ant-upload-text">Upload</div>
</div>
);
return (
<div className="clearfix">
<Upload
action="/manage/img/upload" /**上传图片的接口地址 */
accept='image/*' /**只接受图片格式 */
name='image' /**请求参数名,来自api说明上传图片的参数类型 */
listType="picture-card" /*卡片样式:text, picture 和 picture-card*/
fileList={fileList} /*所有已上传图片文件对象的数组*/
onPreview={this.handlePreview} /**显示图片预览函数 */
onChange={this.handleChange} /**上传/删除图片函数 */
>
{//控制图片上传按钮最多5个
fileList.length >= 5 ? null : uploadButton}
</Upload>
<Modal visible={previewVisible} footer={null} onCancel={this.handleCancel}>
<img alt="example" style={{ width: '100%' }} src={previewImage} />
</Modal>
</div>
);
}
}