如何优雅的使用TypeScript封装一个ApiFox测试组件

335 阅读2分钟

效果展示

APIFox原图

正常展示状态 image.png 选择状态

image.png

组件模仿结果

正常展示状态

image.png

选择状态

image.png

过程

类型、tsx、less是在编写组件的过程中一起写出来的,没有先写后写的区分,写好组件以后,在components目录下创建types文件统一导出

目录结构

│
├─ApiOperator
│      index.less
│      index.tsx
│      type.ts
│
└─types
        type.d.ts

components统一导出类型(type.d.ts)

// 通用组件类型统一导出
export * from '@/components/types/type'

1. 类型ts

export type Method =
	| 'GET'
	| 'POST'
	| 'PUT'
	| 'DELETE'
	| 'OPTIONS'
	| 'HEAD'
	| 'PATCH'

export interface ApiOptReqOptType {
	label: string
	value: Method
	colorClassName?: string
}

// 组件属性描述
export type ApiOptProps = {
	// 子元素
	children?: React.ReactNode
	// 自定义右侧内容宽度(默认为225px)
	rightWidth?: string
	// 输入框placeholder
	placeholder?: string
	// 接口选项信息
	methodOptions?: ApiOptReqOptType[]
	// 默认展示的接口信息
	defaultMethod?: ApiOptReqOptType
	// 接口信息值
	methodValue?: ApiOptReqOptType
	// 输入框值
	inputValue?: string
	// 下拉菜单和选择器同宽。默认将设置 min-width,当值小于选择框宽度时会被忽略。
	popupMatchSelectWidth?: number
	// 左侧选项改变事件
	onOptionChange?: (value: ApiOptReqOptType) => void
	// 输入框内容改变事件
	onInputChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
}

组件tsx

import React, { memo, useState } from 'react'
import { Input, Select, Space } from 'antd'

import type { ApiOptReqOptType, ApiOptProps } from './type'
import './index.less'

// 默认请求类型信息
const methodOptions: ApiOptReqOptType[] = [
  { label: 'GET', value: 'GET', colorClassName: 'color-get' },
  {
    label: 'POST',
    value: 'POST',
    colorClassName: 'color-post'
  },
  { label: 'PUT', value: 'PUT', colorClassName: 'color-put' },
  { label: 'DELETE', value: 'DELETE', colorClassName: 'color-delete' },
  { label: 'OPTIONS', value: 'OPTIONS', colorClassName: 'color-options' },
  { label: 'HEAD', value: 'HEAD', colorClassName: 'color-head' },
  { label: 'PATCH', value: 'PATCH', colorClassName: 'color-patch' }
]

const ApiOperator: React.FunctionComponent<ApiOptProps> = memo((props) => {
  const [selectVisible, setSelectVisible] = useState(false)

  // 手动渲染请求方式下拉列表
  function getDropDownEle(): React.ReactElement {
    return (
      <ul className="method-select">
        {methodOptions.map((item, index) => (
          <li
            className={['method-item', item.colorClassName].join(' ')}
            onClick={() => onReqMethodChange(item)}
            key={index}
          >
            {item.label}
          </li>
        ))}
      </ul>
    )
  }

  // 选择请求方式点击事件
  function onReqMethodChange(req: ApiOptReqOptType): void {
    setSelectVisible(false)
    if (props.onOptionChange) props.onOptionChange(req)
  }

  // 输入框改变事件
  function onInputChange(e: React.ChangeEvent<HTMLInputElement>): void {
    if (props.onInputChange) props.onInputChange(e)
  }

  return (
    <div className="api-operator">
      <div
        className={['left-info', props.methodValue?.colorClassName].join(' ')}
      >
        <Space.Compact block>
          <Select
            open={selectVisible}
            onDropdownVisibleChange={(visible) => setSelectVisible(visible)}
            defaultValue={props.defaultMethod}
            value={props.methodValue}
            options={props.methodOptions}
            popupMatchSelectWidth={props.popupMatchSelectWidth}
            dropdownRender={getDropDownEle}
            onSelect={(e) => console.log(e)}
          />
          <Input
            value={props.inputValue}
            onChange={(e) => onInputChange(e)}
            placeholder={props.placeholder}
          />
        </Space.Compact>
      </div>
      <div className="right-warp" style={{ width: props.rightWidth }}>
        {props.children}
      </div>
    </div>
  )
})

ApiOperator.defaultProps = {
  rightWidth: '225px',
  placeholder: '接口地址',
  methodOptions: methodOptions,
  defaultMethod: methodOptions[0],
  methodValue: methodOptions[0],
  popupMatchSelectWidth: 100,
  inputValue: ''
}

export default ApiOperator

样式less

@aop-color-get: #49aa19;
@aop-color-post: #d87a16;
@aop-color-put: #176ddc;
@aop-color-delete: #d84a1e;
@aop-color-options: #176ddc;
@aop-color-head: #176ddc;
@aop-color-patch: #cf2f86;

.api-operator {
  display: flex;
  .left-info {
    flex: 1;

    // 选中的样式
    .ant-select-selection-item {
      font-weight: 700;
    }
  }

  .color-get {
    .ant-select-selection-item {
      color: @aop-color-get;
    }
  }

  .color-post {
    .ant-select-selection-item {
      color: @aop-color-post;
    }
  }

  .color-put {
    .ant-select-selection-item {
      color: @aop-color-put;
    }
  }
  .color-delete {
    .ant-select-selection-item {
      color: @aop-color-delete;
    }
  }
  .color-options {
    .ant-select-selection-item {
      color: @aop-color-options;
    }
  }
  .color-head {
    .ant-select-selection-item {
      color: @aop-color-head;
    }
  }
  .color-patch {
    .ant-select-selection-item {
      color: @aop-color-patch;
    }
  }

  .right-warp {
    display: flex;
    .btn {
      margin-left: 10px;
    }
  }
}

.method-select {
  padding: 3px;
  .method-item {
    padding-left: 5px;
    font-weight: 700;
    height: 30px;
    line-height: 30px;
    font-size: 16px;
    border-radius: 5px;
    transition: all 250ms ease;
    &:hover {
      background-color: rgba(0, 0, 0, 0.05);
      cursor: pointer;
    }
  }

  .color-get {
    color: @aop-color-get;
  }

  .color-post {
    color: @aop-color-post;
  }

  .color-put {
    color: @aop-color-put;
  }
  .color-delete {
    color: @aop-color-delete;
  }
  .color-options {
    color: @aop-color-options;
  }
  .color-head {
    color: @aop-color-head;
  }
  .color-patch {
    color: @aop-color-patch;
  }
}