跟着做react项目(至P81)

导航

  • 添加/修改组件
    • Form组件
    • Cascader级联列表
    • 开始布局
    • 准备数据
    • 逻辑函数
  • Q&A
  • 新的组件 Upload
    • Upload组件的参数
    • 上传文件onChange(核心)
    • 修改页面
    • Upload组件总结
  • 结果图

添加/修改组件

添加商品
onClick={() => this.props.history.push(’/product/addupdate’)

修改商品
onClick={() => this.props.history.push(’/product/addupdate’, product)

共用一个路由组件,该组件涉及较多的antd控件,来细讲一下。

Form组件

组件介绍

前文提过: 在 label 对应的 Form.Item 上不要在指定 name 属性,这个 Item 只作为布局作用。
跟着做react项目(至P81)_第1张图片

当你为 Form.Item 设置 name 属性后,子组件会转为受控模式。因而 defaultValue 不会生效。你需要在 Form 上通过 initialValues 设置默认值。

先完成这一部分,红圈分别涉及(逻辑,TextArea,Input属性,Cascader组件)
跟着做react项目(至P81)_第2张图片

Cascader级联列表

这里使用动态加载版的级联
跟着做react项目(至P81)_第3张图片

本质不难,就是根据一个数组来展示数据的。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>
        &nbsp;&nbsp; {isUpdate ? '修改商品' : '添加商品'}
    </span>
)
// 指定 form 的 item 布局的对象, 页面分为24列栅格span
const formItemLayout = {
    labelCol: { span: 2 }, //左侧label宽度
    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'
                    // noStyle //这里不能写这个
                    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属性值。

// producthome传入的数据
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 那里千万别加

最后的界面:
跟着做react项目(至P81)_第4张图片
暂时表单提交的回调函数 不发请求,直接打印收集的表单数据
跟着做react项目(至P81)_第5张图片

Q&A

遇到的问题:在添加/修改路由界面跳转回商品主界面时报错
跟着做react项目(至P81)_第6张图片
检查了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

跟着做react项目(至P81)_第7张图片

Upload组件(antd)

上传图片的API接口,传送参数,响应参数
form-data二进制数据
跟着做react项目(至P81)_第8张图片

Upload组件的参数

accept 接受上传的文件类型
跟着做react项目(至P81)_第9张图片
name=‘image’ //请求参数名
在这里插入图片描述
fileList={fileList} // 所有已上传图片文件对象的数组

file 当前操作的文件对象。

{
   uid: 'uid',      // 文件唯一标识,建议设置为负数,防止和内部产生的 id 冲突
   name: 'xx.png'   // 文件名
   status: 'done', // 状态有:uploading done error removed,被 beforeUpload 拦截的文件没有 status 属性
   response: '{"status": "success"}', // 服务端响应内容
}

当图片file上传之后,就成为ileList中的元素,变成UploadFile类型,继承自 File,附带额外属性用于渲染。

多了几个属性:

url:' https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', //图片地址

如果后台有开启服务,你应该是可以在浏览器输入图片地址url直接访问图片的。

<Upload
    action="/manage/img/upload" // 上传的地址 API接口文档
    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} 在一次上传过程中会触发多次。(因为文件不是一次发生完)
跟着做react项目(至P81)_第10张图片

上传和删除图片都会调用这个函数。

说明:
file: 当前操作文件信息对象
fileList: 所有文件信息对象的数组

上传时,在触发这个回调函数之前,file已经克隆了一份,放在了fileList数组的最后面。克隆意思是,两个文件虽然内容一样,但是存在不同的变量里。

删除时,在触发这个回调函数之前,当前的文件信息已经在fileList中删除,所以fileList中是找不到与file内容相同的文件

handleChange = async ({ file, fileList }) => {
        //file !== fileList[fileList.length-1]
    }

打印一下file, fileList

response: '{"status": "success"}', // 服务端响应内容,即由后台决定返回什么内容。

第一个name是本地图片名称,或者组件自己随机命名
第二个name是服务器(后台)保存的图片名称
跟着做react项目(至P81)_第11张图片

第一步: 一旦上传成功,将当前上传的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就任务结束了。

表单只需要图片名称:
跟着做react项目(至P81)_第12张图片
分清: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} />

第三步:
跟着做react项目(至P81)_第13张图片

    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('删除图片失败')
            }
        }
        // 最后更新 fileList 状态
        this.setState({ fileList })
    }

修改页面

展示商品图片时,图片已经在服务器里了,界面要展示已上传的图片,实际就是初始化FileList数组。

constructor(props) {
    super(props)
    let fileList = []
    // 如果传入了 imgs, 生成一个对应的 fileList
    const imgs = this.props.imgs
    if (imgs && imgs.length > 0) {
        fileList = imgs.map((img, index) => ({
            uid: -index,
            name: img,
            status: 'done', // loading: 上传中, done: 上传完成, remove: 删除
            url: BASE_IMG_PATH + img, // 在后台的地址
        }))
    }
    //初始化状态
    this.state = {
        previewVisible: false, //标识是否大图预览
        previewImage: '', //大图的url
        previewTitle: '',
        fileList: fileList // 所有需要显示的图片信息对象的数组
    };
}

Upload组件总结

上传图片到服务器的这个请求是组件帮我们完成的。
而删除图片只是在前端界面上删除,图片的status属性为remove,需要我们自己去发请求删除服务器里面的图片。

结果图

跟着做react项目(至P81)_第14张图片
表单提交的回调函数也能得到新的图片名称
跟着做react项目(至P81)_第15张图片
后台服务器也下载了新的图片
跟着做react项目(至P81)_第16张图片

下一节才能实现真正的添加商品。。。

你可能感兴趣的:(前端项目,react.js,javascript,前端)