技术栈:
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>
)