一、产品添加基础部分

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

点击: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>)

2 静态页面 add-update.jsx

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}=Inputexport 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函数

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}=Inputclass 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的getFieldDecoratorconst {getFieldDecorator}=this.props.formreturn(<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】加载二级分类列表函数

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解析为optionsinitOptions=(categorys)=>{const options = categorys.map((v,k)=>({ //返回一个字典,要额外加一个括号            value: v._id,label: v.name,isLeaf: false,             }))this.setState({options})}//【2】获取categorysgetCategorys= 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)// 隐藏loadingtargetOption.loading = false// 二级分类数组有数据if (subCategorys && subCategorys.length>0) {// 生成一个二级列表的optionsconst 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的getFieldDecoratorconst {getFieldDecorator}=this.props.formreturn(<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】级联商品分类 */}<Cascaderplaceholder='请选择'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状态

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.datafile = fileList[fileList.length-1]file.name = namefile.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"><Uploadaction="/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

 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

    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

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

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加入删除对应图片代码:

import {reqDeletPic} from '../../../api' //【1】/*file: 当前操作的图片文件(上传/删除)fileList: 所有已上传图片文件对象的数组官方文档:https://ant.design/components/upload-cn/#onChange*/handleChange = async ({ file,fileList }) => { //【3】asyncconsole.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.datafile = fileList[fileList.length-1]file.name = namefile.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

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><EditoreditorState={editorState}wrapperClassName="demo-wrapper"editorClassName="demo-editor"onEditorStateChange={this.onEditorStateChange}/*【3】给编辑器加内置样式,边框,高等*/editorStyle={{border: '1px solid black', minHeight: 200, paddingLeft: 10}}/><textareadisabledvalue={draftToHtml(convertToRaw(editorState.getCurrentContent()))}/></div>);}
}

第二步,引入编辑器组件

在add-updgate.jsx内引入组件

import RichText from './rich-text'//render()内
<Item label='商品详情'><RichText />
</Item>

第三步,编辑器尺寸改大

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

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

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

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

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

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

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

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

    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>&nbsp;&nbsp;</p>

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

【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 // 得到图片的urlresolve({data: {link: url}}) //返回图片的地址})xhr.addEventListener('error', () => {const error = JSON.parse(xhr.responseText)reject(error)})})}//render(){}
render() {const { editorState } = this.state;return (<div><EditoreditorState={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 } },}}/><textareadisabledvalue={draftToHtml(convertToRaw(editorState.getCurrentContent()))}/></div>);}

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

注意 alt 内容必须填个,否则无法上传图片

三、提交添加商品请求

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

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

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

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

//表单提交验证submit=()=>{this.props.form.validateFields((error,values)=>{//【2】调用接口请求函数去添加/更新//【3】根据结果提示是否添加/更新成功if(!error){//【1】收集数据, 并封装成product对象const {name,desc,price,categoryIds}=valueslet pCategoryId,categoryIdif(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步效果:填写完成添加产品表单,点提交按钮:

//只有一级分类的产品数据
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

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

import {reqCategorys,reqAddUpdatePro} from '../../../api' //【0】引入添加修改产品函数//产品表单提交submit=()=>{this.props.form.validateFields(async(error,values)=>{if(!error){//【1】解构values收集数据, 并封装成product对象const {name,desc,price,categoryIds}=valueslet pCategoryId,categoryIdif(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

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.Optionexport 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

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}=Inputclass AddUpdate extends Component{constructor(props){super(props)//创建用于存放指定ref标识的标签对象容器this.pw=React.createRef()//this.editor=React.createRef()this.state={options:[], //定义状态选项} }//把获取到的categorys解析为optionsinitOptions=(categorys)=>{const options = categorys.map((v,k)=>({ //返回一个字典,要额外加一个括号            value: v._id,label: v.name,isLeaf: false,             }))this.setState({options})}//获取categorysgetCategorys= 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)// 隐藏loadingtargetOption.loading = false// 二级分类数组有数据if (subCategorys && subCategorys.length>0) {// 生成一个二级列表的optionsconst 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}=valueslet pCategoryId,categoryIdif(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的getFieldDecoratorconst {getFieldDecorator}=this.props.formreturn(<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自定义验证规则要求价格大于0getFieldDecorator('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: '必须指定商品分类'},]})(<Cascaderplaceholder='请指定商品分类'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

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

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

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 // 得到图片的urlresolve({data: {link: url}})})xhr.addEventListener('error', () => {const error = JSON.parse(xhr.responseText)reject(error)})})}render() {const { editorState } = this.state;return (<div><EditoreditorState={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 } },}}/><textareadisabledvalue={draftToHtml(convertToRaw(editorState.getCurrentContent()))}/></div>);}
}

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

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】asyncconsole.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.datafile = fileList[fileList.length-1]file.name = namefile.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"><Uploadaction="/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>);}
}

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

  1. PHP:【商城后台管理系统】部署角色管理,角色添加,菜单权限,删除角色功能

    PHP:[商城后台管理系统]部署角色管理,角色添加,菜单权限,删除角色功能 一.角色管理界面 ①首页 ②角色添加 ③角色编辑 ④角色删除 二.部署流程 部署流程 后端采用thinkphp6.0框架,角 ...

  2. 《React后台管理系统实战:五》产品管理(一)

    一.概述 1.1目录结构及功能 src/pages/admin/product/add-update.jsx //添加及更新产品detail.jsx //产品详情home.jsx //产品默认页ind ...

  3. 《React后台管理系统实战:十一》可视图表及首页图表

    一.简介及环境安装 les135- 1.常用数据可视化图表库 1) echarts a. https://echarts.baidu.com/ b. 百度开源, 如果要在 react 项目中使用, 需 ...

  4. Serverless 实战 —— Serverless + Egg.js 后台管理系统实战

    Serverless + Egg.js 后台管理系统实战 作为一名前端开发者,在选择 Nodejs 后端服务框架时,第一时间会想到 Egg.js,不得不说 Egg.js 是一个非常优秀的企业级框架,它 ...

  5. 【开发篇】10分钟快速搭建React后台管理系统模板

    React后台管理系统模板 github 我又回来了!!!学完前端react,再学spring,这周或者下周写spring有关的!!! 一.准备React 1.建立react应用 npx create ...

  6. 【作业题】后台管理系统中的课程管理模块(后端篇)

    巩固web阶段知识,完成后台管理系统中的课程管理模块,该模块包含了添加课程.配置课程相关信息.管理课程章节等功能. 项目地址:课程管理模块 产品的原型图 课程管理 实现以下功能: 展示课程列表 根据课 ...

  7. 从头开始react后台管理系统-安装ant-Design

    从头开始react后台管理系统-安装ant-Design introduce 在做微信小程序, 想着顺便做一个管理系统的 随便扯扯 我是个菜鸡,很菜很菜的菜鸡,春招各家凉,现在还是0offer.心情很 ...

  8. React后台管理系统-品类选择器二级联动组件

    React后台管理系统-品类选择器二级联动组件 品类选择器二级联动 posted on 2018-06-26 22:21 gisery 阅读( ...) 评论( ...) 编辑 收藏

  9. 富文本编辑器Ueditor实战(二)-图片上传

    本文接前文富文本编辑器Ueditor实战(一),前文留了一个问题,就是图片上传功能无法使用,报后台配置错误的问题.那么应该如何配置图片上传功能呢?本文将使用jsp+tomcat的方式进行说明如何动态配 ...

最新文章

  1. AI探索(二)Tensorflow环境准备
  2. hibernate SQL查询COUNT函数
  3. 第一张信用卡,该选哪家的?
  4. OpenCV2和OpenCV3兼容安装
  5. 好戏连台,BCH独领风骚
  6. 10-9 重要的内置函数(zip、filter、map、sorted)
  7. net472无法建立到信任_是否还会信任,那个曾经背叛过自己的人
  8. php中session总结,PHP中SESSION使用中的一点经验总结
  9. Linux查看jdk版本、卸载jdk
  10. 使用NOPI做Excepl导入导出
  11. 热电厂sis系统服务器升中标结果,电厂SIS系统简介
  12. 句法分析(成分句法分析)(依存句法分析)
  13. 智能家居APP使用指南
  14. 瞬间移动(c(n, m))
  15. python判断火车票座位是否靠窗_Python查询火车票(一)
  16. php的seeder是什么,laravel利用seeder实现数据表中填充数据
  17. oracle 进程占cpu使用率,ORACLE进程占用CPU情况分析
  18. python二次函数拟合_Python实现——二次多项式回归(最小二乘法)
  19. mathcontext保留2位小数_Java BigDecimal subtract()用法及代码示例
  20. Exp5 MSF基础应用 20164323段钊阳

热门文章

  1. 科技云报道:大模型时代,AI基础软件机会何在?
  2. 一个事物两个方面的对比举例_《介绍一种事物》课堂实录
  3. 【面试题】 面试官:你觉得你最大的缺点是什么?
  4. firework 怎么导出html,由Fireworks导出的htm生成imageset文件
  5. python pexpect linux安装_Python Pexpect库的使用
  6. python编程入门贴吧_《Python编程从入门到实践》学习笔记10:文件和异常
  7. 两天学完Linux(第一天)
  8. 温度过高时蜂鸣器报警,自认为的简简单单的实验能有啥困难?(附DS18B20温度传感器的详细笔记和时序图)
  9. C4D模型工具—循环/路径切割
  10. redis为什么要使用skiplist跳表