新增商品弹框组件的实现(Demo)

792 阅读6分钟

技术栈:

umi + antd + ts

先看效果:

1.点击+符号,弹出新增商品组件

2.新增商品弹框内容

文件目录

文件解析

const.ts
export const formLayout = {
  labelCol: { // 标签大小
    sm: { span: 6 },
    xs: { span: 24 }
  },
  wrapperCol: { // 输入控件的大小
    sm: { span: 16 },
    xs: { span: 24 }
  }
}

index.tsx

实现效果

import React, { useState } from 'react'
import { Tooltip } from 'antd'
import { PlusCircleOutlined } from '@ant-design/icons'

import './index.styl' // 引入样式
import AddShopModal from './addShop' // 引入新增商品组件

export default () => {
  const [state, setState] = useState({ // 控制商品组件是否 可见 或者 可编辑
    isEditAddShop: false,
    addShopVisible: false
  })

  /**
 - 打开/关闭新增商品组件弹窗
 - @param visible
 - @param isEdit
 - ?:可选参数--可有可无
   */
  const handleToggleAddShop = (visible: boolean, isEdit?: boolean) => {
    // ...state 设置初始状态,如果有参数传入,值发送变化,则 isEditAddShop: isEdit, addShopVisible: visible 覆盖state的值
    setState({ ...state, isEditAddShop: isEdit, addShopVisible: visible })
  }

  return (
    <> // 最外层 div 可以省略
      <div>
        <Tooltip title='新增商品'>
          <PlusCircleOutlined onClick={() => handleToggleAddShop(true, false)} />
        </Tooltip>
      </div>
      <AddShopModal
        isEdit={state.isEditAddShop} // 新增商品 是否可编辑 通过 prop 传递 state.isEditAddShop的值给 新增商品组件从而控制是否编辑状态 
        visible={state.addShopVisible} // 同理上面
        onClose={() => handleToggleAddShop(false)}
      />
    </>
  )
}
addShop.tsx

分步骤解析

  • 文件导入

// 新建商品组件

import React, { useState, useEffect } from 'react' 

import { Button, Modal, Form, Input, Select, Upload, message } from 'antd' 

import { PlusCircleOutlined, DeleteOutlined, DownloadOutlined, UploadOutlined } from '@ant-design/icons' 

import _ from 'lodash' 

import { generate } from 'shortid' 

import { Store } from 'antd/lib/form/interface' 

import { formLayout } from './const' // 用来定义label标签和输入控件的大小与间隔 

import { ShopComponent } from '@/interfaces/shop-component' 

import { uploadAddShopFile } from '@/services/shop-component/addShop' // 新增商品接口 

import { useCommit, useLoading, useModelState } from '@/models/shop-component/addShop-model' // 调用接口/数据存储的实现方法

规范注意点:

1.从外部导入的文件/插件等,统一放在最上面,一起放 

2.从内部导入的文件/插件等,统一放在外部导入的文件/插件 的 下面 (可以隔开一格来区分)

这里引用了shortid 里面的 generate,用于生成唯一的 key,这样我们点击 添加参数 时,就可以不断新增不重复的项(key不同),因为React中,每一项都需要一个 唯一的key 去区分不同的项

formLayout 放在这里使用

ShopComponent 为 Interface接口 定义 数据类型命名和契约

export declare namespace ShopComponent {
  // 商品类型参数
  interface SParamsType {
    key?: string,
    name: string,
    anName: string,
    value: string,
    type: string
  }
}

这里 declare声明 了 namespace命名空间

  • 定义好从父组件传递过来的数据的类型

    interface InitProps { isEdit: boolean visible: boolean onClose: () => void }

在这里插入图片描述

void

  • 文本域和选择器选项

const { TextArea } = Input // 引用 文本域 组件 

const { Option } = Select // 引用 选择器的 选项

商品弹框组件编写

第一步:初始化数据,定义state

export default function AddShopModal(props: InitProps) { // props 接受父组件传过来的数据
  
  const initParams: ShopComponent.SParamsType = { // 定义类型 并 初始化 ShopComponent 商品类型参数
    key: generate(), // 随机生成唯一的id,作为key
    name: '',
    anName: '',
    value: '',
    type: 'input'
  }

  const { onClose, visible, isEdit } = props // 对象解构
  const [form] = Form.useForm() // 表单form常用功能,要调用form里面的功能,需要定义下
  const [uploading, setUploading] = useState(false) // 上传按钮loading状态的state
  const [filePath, setFilePath] = useState('') // 设置文件路径state
  const [dataParams, setDataParams] = useState([initParams]) // 设置商品类型-添加参数的state
}

第二步:编写组件需要使用到的方法

1.关闭当前弹窗并清空列表

 function handleClose() {
    onClose()
    form.resetFields() // 置空form里面的内容
    setFilePath('') // 把文件路径state变为''
    setUploading(false) // 上传状态修改为false
    setDataParams([initParams]) // 商品类型的添加参数变为初始值--initParams
 }

2.商品类型-添加参数-点击添加参数按钮时,先深拷贝(拷贝出来的都是与之前的不相等,不同的栈中,独立的)当前数组,再push(向数组的末尾添加一个或多个元素),这样就形成新的数组

function handleAddParams() {
    const newDataParams = _.cloneDeep(dataParams) // 深拷贝
    newDataParams.push(initParams)
    setDataParams(newDataParams)
}

3.删除商品类型-添加参数,先深拷贝当前数组,通过过滤,把当前点击的数组子项过滤掉,这样便形成新的数组,从而删除掉选中项

function handleDeleteParams(key: string) {
    let newDataParams = _.cloneDeep(dataParams)
    newDataParams = _.filter(newDataParams, item => item.key !== key)
    setDataParams(newDataParams)
 }

4.新增或编辑商品类型-添加参数里面的内容,通过遍历添加参数里面的每一项,拿到当前编辑的项的key与遍历到的key对比,如果相同,则更新为最新的值

  /**
   * 存储商品类型内容变化
   * @param key 每行唯一标识
   * @param field 字段
   * @param value 值
   */
  function handleChangeParams(key: string, field: string, value: string) {
    const newDataParams = _.map(dataParams, data => {
      if(data.key === key) {
        data[field] = value
      }
      return data
    })
    setDataParams(newDataParams)
  }

5.上传文件前校验

  /**
   * @param file
   */
  function handleUploadBefore(file: File) {
    const { name } = file // 工资条.zip
    const suffixFile = _.last(_.split(name,'.')) // zip  切割后,变为一个数组,再去最后一项即可
    if(!_.isEqual(suffixFile, 'zip')) { // 比较后缀是否和设定的格式相等,如果不相等,则提示
      message.error('只支持上传 .zip后缀文件')
      return false
    }
    return true
  } 

上传的文件:

在这里插入图片描述

6.文件上传方法

async function handleUpload(options) { // 这里使用了异步方法,不阻塞
    setUploading(true) // 当选择文件好后,开启加载中的状态,即转圈圈
    const { file } = options // 获取文件信息
    const { success, result }: any = await uploadAddShopFile(file) // 上传文件接口--后面的博客再讲解,获取成功和结果信息
    setUploading(false) // 上传成功后,把加载中的状态取消
    const { name } = file // 文件名字
    if(!success) return message.error(`文件上传失败:${name}`) // 不成功则提示
    const { path } = result // 文件的路径
    form.setFieldsValue({ // 在form表单中设置从后端返回的文件名
      filename: name
    })
    setFilePath(path) // 设置后端返回的文件路径
    message.success(`文件上传成功:${name}`) 
}

7.渲染商品类型-添加参数表单内容

  /**
   * @param dataParams 表单数据
   */
function renderAddParamsForm(dataParams: ShopComponent.SParamsType) {
    const { name, value, key, anName, type } = dataParams
    return (
      <div 
        key={key}
      >
        <Input 
          placeholder='别名'
          value={anName}
          onChange={(e: React.ChangeEvent) => { 
            const val = _.get(e, 'target.value', '') // 获取修改后的值
            handleChangeParams(key, 'anName', val) // 值改变
          }}
        />
        <Input
          placeholder='名称'
          value={name}
          onChange={(e: React.ChangeEvent) => {
            const val = _.get(e, 'target.value', '') 
            handleChangeParams(key, 'name',val) 
          }}
        />
        <Input
          placeholder='默认值'
          value={value}
          onChange={(e: React.ChangeEvent) => {
            const val = _.get(e, 'target.value', '')
            handleParamsChange(key, 'value', val)
          }}
        />
        <Input 
          placeholder='类型'
          value={type}
          hidden={true}
        />
        {
          <DeleteOutlined onClick={() => handleDeleteParams(key)}></DeleteOutlined>
        }
      </div>
    )
}

注意:渲染的商品类型-添加参数的每一项,都需要一个不同的key

8.提交方法

  /**
   * 提交
   */
function handleSubmit() {
    if(!filePath) return message.error('请先上传文件!') // 通过商品文件路径判断是否上传商品
    form.validateFields().then((values: Store): void => { // form 表单检验, 通过antd-form存储在Store里面的值  void 无返回值
      if(!!dataParams.length) { // 判断商品类型-添加参数的长度
        const booValue = _.some(dataParams, item => _.values(item).includes('')) // 判断是否有空值,Boolean
        if(booValue) {
          message.error('请添加参数')
          return 
        }
        // 当新建多个参数时,判断参数的名称是否重复
        if(dataParams.length !== 1) {
          const nameArr = _.map(dataParams, item => item.name);
          if(_.uniq(nameArr).length !== nameArr.length) { // 去重后对比,如果名称有重复,则提示错误
            message.error('参数名称不能重复!')
            return
          }
        }
      }
      const reqParams: ShopComponent.IAddShopParams = { // 定义 form 请求的数据类型,已经获取值
        name: values.name,
        alias: values.alias,
        shopPath: values.shopPath,
        shopId: values.shopId,
        description: values.description,
        filename: values.name,
        filePath: filePath,
        params: dataParams
      }
      if (!_.isEmpty(externalDetails)) { // 如果是编辑的时候,则商品的类型不变,通过uuid来控制
        reqParams.uuid = externalDetails.uuid
        delete reqParams.filePath
      }
      commit('addOrUpdateExternal',[reqParams, handleClose]) // 新增或编辑接口
    })
}

页面元素展示:

return (
    <Modal 
    title={`${isEdit ? '编辑' : '新建'}商品组件`} // 通过判断 isEdit 的 Boolean 值 来 显示标题
    destroyOnClose // 	关闭时销毁 Modal 里的子元素
    width={600}
    visible={visible} // 判断是否可见
    onOk={handleSubmit}
    onCancel={handleClose}
    confirmLoading={loading} // 确定按钮 loading 在提交前有个加载中的状态,确保先提交后确定
    >
      <Form form={form} {...formLayout}> // form 经 Form.useForm() 创建的 form 控制实例,不提供时会自动创建
        <Form.Item
          label='商品组件名称'
          required
          name='alias'
          rules={[
            { required: true, message: '请输入商品组件名称' },
            { max: 50, message: '商品组件名称不能超过50个字符' }
          ]}
        >
          <Input placeholder='请输入商品组件名称' />
        </Form.Item>
        <Form.Item 
          label='商品组件标识' 
          required
          name='name'
          rules={[
            { required: true, message: '请输入商品组件标识' },
            { max: 50, message: '商品组件标识不能超过50个字符!' },
            { // 验证规则
              pattern: new RegExp('[^a-zA-Z1-9\-]', 'i'), // 正则表达式
              message: '商品组件标识需由小写英文字母、-、数字组成,不超过50个字符!',
              validator: (rule, value, callback) => {
                if (rule.pattern.test(value)) {
                  callback(rule.message as string)
                }
                callback()
              }
            }
          ]}
        >
          <Input placeholder='请输入商品组件标识' />
        </Form.Item>
        <Form.Item
          label='启动文件路径' 
          required
          name='shopPath'
          rules={[
            { required: true, message: '请输入启动文件路径' }
          ]}
        >
          <Input placeholder='请输入启动文件路径' />
        </Form.Item>
        <Form.Item 
          label='商品类型'
          required
          name='shopId'
          rules={[
            { required: true, message: '请选择商品类型' }
          ]}
        >
          <Select 
            placeholder='请选择商品类型'
            allowClear
          >
            {_.map(imageList, item => <Option key={item.uuid} value={item.uuid}>{item.alias}</Option>)}
          </Select>
        </Form.Item>
        <div>
          <Button type="ghost" shape="round" icon={<PlusCircleOutlined />} onClick={handleAddParams}>
            添加参数
          </Button>
          <div>
            <div>
               <span>别名</span>
               <span>名称</span>
               <span>默认值</span>
            </div>
            {
              _.map(dataParams, item => renderAddParamsForm(item))
            }
          </div>
        </div>
        <Form.Item 
          label='描述'
          required
          name='description'
          rules= {[
            { required: true, message: '请输入描述' }
          ]}
        >
          <TextArea rows={5} placeholder='请输入描述' />
        </Form.Item>
        <Form.Item
          label='文件名'
          required
          name='filename'
          rules= {[
            { required: true, message: '请输入文件名' }
          ]}
        >
          <Input disabled />
        </Form.Item>
        <div>
          <Upload
            accept='.zip'
            beforeUpload={file => handleUploadBefore(file)}
            customRequest={handleUpload}
            showUploadList={false}
            disabled={isEdit} // 编辑时禁止上传
          >
            <Button type='primary' icon={<UploadOutlined/>} loading={uploading} disabled={isEdit} >选择上传文件</Button>
          </Upload>
            <Button 
              onClick={() => window.open("#")} // 这里open本是一个接口,直接可下载示例文件
              type='primary'
              icon={<DownloadOutlined/>} 
            >
              下载示例文件
            </Button>
        </div>
      </Form>
    </Modal>
)