添加/修改组件
添加商品 onClick={() => this.props.history.push(’/product/addupdate’)
修改商品 onClick={() => this.props.history.push(’/product/addupdate’, product)
共用一个路由组件,该组件涉及较多的antd控件,来细讲一下。
Form组件
组件介绍
前文提过: 在 label 对应的 Form.Item 上不要在指定 name 属性,这个 Item 只作为布局作用。
当你为 Form.Item 设置 name 属性后,子组件会转为受控模式。因而 defaultValue 不会生效。你需要在 Form 上通过 initialValues 设置默认值。
先完成这一部分,红圈分别涉及(逻辑,TextArea,Input属性,Cascader组件)
Cascader级联列表
这里使用动态加载版的级联
本质不难,就是根据一个数组来展示数据的。children就是代表下一级的选项。
const options = [
{
value: 'zhejiang',
label: 'Zhejiang',
children: [
{
value: 'hangzhou',
label: 'Hangzhou',
children: [
{
value: 'xihu',
label: 'West Lake',
},
],
},
],
},
动态加载的情况,每个数据都有isLeaf: false, 属性,false表示非叶子节点,即还有下一级。true表示没有下一级。初始的数据的children 属性没有值,表示非叶子节点。
动态意思是,点击下一层的箭头按钮时,系统会搜索下一级有什么数据,然后在添加到children 属性,最后更新数据。Cascader组件就会根据数据展示下一级选项。
动态的核心就是往原本的选项添加children 属性的值。
selectedOptions就是当前的一级选项,列表类型,通常只有一个元素
const targetOption = selectedOptions[selectedOptions.length - 1];
其中一个难点(复杂而已),就是初始化商品信息时,options 只有第一层级,它无法显示商品的第二级类别。(方法:针对该类别搜索该父类别的全部子类别——children 属性,其余父类别的二级类别的等到要查看时,才更新children 属性)
开始布局
const title = (
<span>
<LinkButton onClick={() => this.props.history.goBack()}>
<ArrowLeftOutlined style={{ fontSize: 20 }} />
</LinkButton>
{isUpdate ? '修改商品' : '添加商品'}
</span>
)
const formItemLayout = {
labelCol: { span: 2 },
wrapperCol: { span: 8 }
}
const tailLayout = {
wrapperCol: { offset: 2, span: 8 }
}
return (
<Card title={title}>
<Form ref={this.formRef}
{...formItemLayout}
onFinish={this.onFinish}
>
<Item label='商品名称'>
<Item name='name'
noStyle
initialValue={product.name}
rules={[{ required: true, message: "商品名称必须输入" },]}
>
<Input placeholder='请输入商品名称' />
</Item>
</Item>
<Item label='商品描述'>
<Item name='desc'
noStyle
initialValue={product.desc}
rules={[{ required: true, message: "商品描述必须输入" },]}
>
<TextArea
placeholder="请输入商品描述"
autoSize={{ minRows: 2, maxRows: 6 }}
/>
</Item>
</Item>
<Item label='商品价格'>
<Item name='price'
noStyle
initialValue={product.price}
rules={[
{ required: true, message: "商品价格必须输入" },
{ validator: this.validatePrice }
]}
validateFirst="false"
>
<Input type='number' placeholder='请输入商品价格'
style={{ width: 160 }} addonAfter='元' />
</Item>
</Item>
<Item label='商品分类'>
<Item name='categoryIds'
initialValue={categoryIds}
rules={[{ required: true, message: "商品分类必须输入" },]}
>
<Cascader placeholder="请选择" options={options}
loadData={this.loadData}
/>
</Item>
</Item>
<Item {...tailLayout}>
<Button type='primary'
htmlType="submit"
>
submit
</Button>
</Item>
</Form>
</Card>
)
准备数据
如果是添加按钮跳转,product为undefined,需要赋值一个空对象,否则product.name报错。
isUpdate控制为添加界面还是修改界面,区别就是组件是否有product数据。
Cascader 初始值要为数组,元素表示第几级选项。但前提是Cascader 组件的options有这几级的数据,即option中有元素,元素又有children属性值。
const product = this.props.location.state
this.product = product || {}
this.isUpdate = !!product
const { pCategoryId, categoryId } = product
const { options } = this.state
const categoryIds = []
逻辑函数
主要还是为Cascader 组件服务,初始化options值。这里要考虑上面讲的问题,如果该product有两级分类,就要预加载第一级分类所包含的第二级分类。
componentDidMount() {
this.getCategorys('0')
}
getCategorys = async (parentId) => {return categorys}
const subCategorys = await this.getCategorys(targetOption.value);
getCategorys只有初始化使才查询第一级分类——parentId===‘0’。如果查第二级分类,则返回一个promise对象,因为需要得到二级分类的数据。
主要照着打代码容易出小错误,这四个组件并不难。后面的Q&A的问题,花了2个小时才查出来,也不是很清楚为什么要加nostyle,
添加 noStyle 作为纯粹的无样式绑定组件。为 true 时不带样式,作为纯字段控件使用。
但Cascader 那里千万别加
最后的界面: 暂时表单提交的回调函数 不发请求,直接打印收集的表单数据
Q&A
遇到的问题:在添加/修改路由界面跳转回商品主界面时报错 检查了setState函数,而且文件也没有其他异步函数在执行,都不是这两个问题。
最后检查表单元素,逐个注释看看哪个控件出错,最后发现是这一段代码。
<Item label='商品分类'>
<Item name='categoryIds'
noStyle
initialValue={categoryIds}
rules={[{ required: true, message: "商品分类必须输入" },]}
>
<Cascader placeholder="请选择" options={options}
loadData={this.loadData}
/>
</Item>
</Item>
具体又不是Cascader 自身问题,而是我用Item 包裹时,设置了noStyle,当我注释掉时,就没错了。 猜测是有些样式不能不带样式,但具体不清楚为什么不带样式会导致:还有异步程序没清除。
新的组件 Upload
Upload组件(antd)
上传图片的API接口,传送参数,响应参数 form-data二进制数据
Upload组件的参数
accept 接受上传的文件类型 name=‘image’ //请求参数名 fileList={fileList} // 所有已上传图片文件对象的数组
file 当前操作的文件对象。
{
uid: 'uid',
name: 'xx.png'
status: 'done',
response: '{"status": "success"}',
}
当图片file上传之后,就成为ileList中的元素,变成UploadFile类型,继承自 File,附带额外属性用于渲染。
多了几个属性:
url:' https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
如果后台有开启服务,你应该是可以在浏览器输入图片地址url直接访问图片的。
<Upload
action="/manage/img/upload"
accept='image/*'
listType="picture-card" //卡片格式
name={UPLOAD_IMG_NAME} // 'image' //ajax请求参数名
fileList={fileList} // 所有已上传图片文件对象的数组
onPreview={this.handlePreview}
onChange={this.handleChange}
>
{fileList.length >= 5 ? null : uploadButton}
</Upload>
onPreview={this.handlePreview} 展示大图
上传文件onChange(核心)
onChange={this.handleChange} 在一次上传过程中会触发多次。(因为文件不是一次发生完)
上传和删除图片都会调用这个函数。
说明: file: 当前操作文件信息对象 fileList: 所有文件信息对象的数组
上传时,在触发这个回调函数之前,file已经克隆了一份,放在了fileList数组的最后面。克隆意思是,两个文件虽然内容一样,但是存在不同的变量里。
删除时,在触发这个回调函数之前,当前的文件信息已经在fileList中删除,所以fileList中是找不到与file内容相同的文件
handleChange = async ({ file, fileList }) => {
}
打印一下file, fileList
response: '{"status": "success"}', // 服务端响应内容 ,即由后台决定返回什么内容。
第一个name是本地图片名称,或者组件自己随机命名 第二个name是服务器(后台)保存的图片名称
第一步: 一旦上传成功,将当前上传的file信息修正(name, url)
handleChange = async ({ file, fileList }) => {
if (file.status === 'done') {
const result = file.response
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('上传失败了')
}
}
}
修改后this.state.fileList 就存储了图片信息(在后台的位置,图片名称),表单就要收集此信息。因为最终是以fileList展示,而file克隆给fileList就任务结束了。
表单只需要图片名称: 分清:file.status是文件的上传情况 result.status是ajax请求后台服务器的响应情况
第二步: 父组件想得到子组件数据的方法:
子组件调用父组件的方法: 将父组件的方法以函数属性的形式传递给子组件,子组件就可以调用。
父组件调用子组件的方法 :在父组件中通过ref得到子组件标签对象(也就是组件对象),调用其方法。
三步走:创造保存ref标识的标签对象的容器——> 给标签组件定义ref属性(此时容器就存放了当前标签对象)——> this.pw.current 属性就能访问该对象。
this.pw = React.createRef();
onFinish = async (values) => {
const imgs = this.pw.current.getImgs();
}
<PicturesWall ref={this.pw} imgs={imgs} />
第三步:
handleChange = async ({ file, fileList }) => {
if (file.status === 'done') {
const result = file.response
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') {
const result = await reqDeleteImg(file.name)
if (result.status === 0) {
message.success('删除图片成功')
} else {
message.error('删除图片失败')
}
}
this.setState({ fileList })
}
修改页面
展示商品图片时,图片已经在服务器里了,界面要展示已上传的图片,实际就是初始化FileList数组。
constructor(props) {
super(props)
let fileList = []
const imgs = this.props.imgs
if (imgs && imgs.length > 0) {
fileList = imgs.map((img, index) => ({
uid: -index,
name: img,
status: 'done',
url: BASE_IMG_PATH + img,
}))
}
this.state = {
previewVisible: false,
previewImage: '',
previewTitle: '',
fileList: fileList
};
}
Upload组件总结
上传图片到服务器的这个请求是组件帮我们完成的。 而删除图片只是在前端界面上删除,图片的status属性为remove,需要我们自己去发请求删除服务器里面的图片。
结果图
表单提交的回调函数也能得到新的图片名称 后台服务器也下载了新的图片
下一节才能实现真正的添加商品。。。
|