点击:onClick={() => this.props.history.push('/product/add-update')}>
//card右侧内容
const extra=(
<Button type='primary' onClick={() => this.props.history.push('/product/add-update')}>
<Icon type='plus'/>
添加商品
</Button>
)
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>
)
}
}
【0】包装当前类使得到form的的强大函数
【0-1】解构得到from的getFieldDecorator
【1】商品名规则
【2】商品描述验证规则
【3】商品价格验证规则:知识点自自定义验证函数
【5】表单提交验证
【6】自定义验证规则要求价格大于0
【7】自定义验证:商品价格大于0函数
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的的强大函数
【0】定义状态选项
【1】级联商品分类
【2】获取categorys
【3】把获取到的categorys解析为options
【4】加载categorys并初始化为
【5】加载二级分类列表函数
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的的强大函数
图片上传因为代码较多所以单独写个组件,引入进来
【1】上传图片的接口地址
【2】只接受图片格式
【3】请求参数名,来自api说明上传图片的参数类型
【5】file: 当前操作的图片文件(上传/删除)
fileList: 所有已上传图片文件对象的数组
官方文档:https://ant.design/components/upload-cn/#onChange
【6】 一旦上传成功, 将当前上传的file的信息修正成最新的(name, url)
【7】在操作(上传/删除)过程中不断更新fileList状态
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>
);
}
}
ref的官方教程:https://zh-hans.reactjs.org/docs/refs-and-the-dom.html
首先,从add-update.jsx内引入picture-wall.jsx
constructor(props){
super(props)
this.state={
previewVisible: false,
previewImage: '',
fileList: []
}
}
/*
【1】获取所有已上传图片文件名的数组
*/
getImgs = () => {
//返回状态中的文件列表中每个文件的文件名
return this.state.fileList.map(file => file.name)
}
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>
删除服务器上指定名称图片
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')
修改函数:handleChange加入删除对应图片代码:
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 })
}
官方文档: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
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内引入组件
import RichText from './rich-text'
//render()内
<Item label='商品详情'>
<RichText />
</Item>
//form内的Item的布局样式
const formItemLayout = {
labelCol: {span: 2}, //左侧label标签的宽度占2个格栅
wrapperCol: {span: 8 }, //右侧(输入框外面有一层包裹)占8个格栅
};
<Item label='商品详情' labelCol={{span: 2}} wrapperCol={{span: 20}}>
<RichText />
</Item>
//【1】让父组件获取到当前组件的信息(state之下建立即可)
getDetail=()=>{
return draftToHtml(convertToRaw(this.state.editorState.getCurrentContent()))
}
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>
表单,图片,详情: {name: "游客131071427", desc: "a", price: "2", categoryIds: Array(1)} []
<p><strong>你好</strong>在 <span style="color: rgb(226,80,65);">地地地柑</span> <del>工 城</del> </p>
【1】图片上传按钮配置
【2】图片上传加一个上传按钮功能实现函数
//【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>
);
}
添加商品/修改商品:因为参数相同所以组成二合一接口,请求地址为按条件拼接,如果参数存在._id则为修改商品,否则为添加商品
//添加商品/修改商品:二合一接口,如果参数存在._id则为修改商品,否则为添加商品
export const reqAddUpdatePro=(product)=>ajax(BASE+'/manage/product/'+(product._id?'update':'add'),product,'POST')
//表单提交验证
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('失败')
}
})
}
//只有一级分类的产品数据
categoryId: "5e4154e725a557082c18f430"
desc: "工"
detail: "手镯
↵://localhost:5000/upload/image-1582702969070.jpg" alt="顶替" style="height: auto;width: auto"/>↵↵"
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: "手镯
↵://localhost:5000/upload/image-1582702969070.jpg" alt="顶替" style="height: auto;width: auto"/>↵↵"
imgs: Array(2)
0: "image-1582702950591.jpg"
1: "image-1582702955364.jpg"
length: 2
__proto__: Array(0)
name: "游客131071427"
pCategoryId: "5e41549925a557082c18f426"
price: "12"
__proto__: Object
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('验证失败,请检查产品数据')
}
})
}
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": "想你所需,超你所想!精致外观,轻薄便携带光驱,内置正版office杜绝盗版死机,全国联保两年! 222
\n联想(Lenovo)扬天V110 15.6英寸家用轻薄便携商务办公手提笔记本电脑 定制 2G独显 内置
\n99999
\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": "华硕(ASUS) 飞行堡垒6 15.6英寸窄边框游戏笔记本电脑(i7-8750H 8G 256GSSD+1T GTX1050Ti 4G IPS)火陨红黑
\n1T+256G高速存储组合!超窄边框视野无阻,强劲散热一键启动!
\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": "图灵程序设计丛书:你不知道的JavaScript(上卷) [You Don't Know JS:Scope & Closures]
\nJavaScript开发经典入门图书 打通JavaScript的任督二脉 领略语言内部的绝美风光
\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": "美的(Midea) 213升 节能静音家用三门小冰箱 阳光米 BCD-213TM(E)
\n爆款直降!大容量三口之家优选! *节能养鲜,自动低温补偿,36分贝静音呵护! *每天不到一度电,省钱又省心!
\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": "美的(Midea)正1.5匹 变频 智弧 冷暖 智能壁挂式卧室空调挂机 KFR-35GW/WDAA3@
\n\n提前加入购物车!2299元成交价!前50名下单送赠品加湿型电风扇,赠完即止!8日0点开抢!更有无风感柜挂组合套购立减500元!猛戳!!
\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>
)
}
}
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的的强大函数
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('上海')
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>
)
}
}
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>
);
}
}
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>
);
}
}