J'Blog

后台系统开发记录

后台系统开发记录

react antd

基础组件不涉及数据都是从外层属性带过来,业务组件数据针对层级嵌套比较多的组件,数据传输可以利用数据流而不是通过属性传递;针对数据获取如果不需要重新渲染的组件,数据获取可以通过ref获取

拖拽排序组件

// 基于react-beautiful-dnd插件
/**
 * 拖拽排序列表组件
 * @param dropId 拖拽id,用于区分不同列表
 * @param list 列表数据
 * @param children 列表子数据ReactNode,自定义样式
 * @param changeHandle 拖拽回调函数
 * @param showDragIcon 是否展示拖拽图标
 * @returns sort list
 */

// index.tsx
import React, { useEffect, useState } from "react"
import { DragDropContext, Draggable } from "react-beautiful-dnd"
// react18存在兼容性问题,所以需要StrictModeDroppable
import { StrictModeDroppable } from "./StrictModeDroppable"
import Item from "./Item"
import "./index.less"

interface DragSortListProps {
  dropId?: string  // 设置拖拽区域唯一id
  list: any[] // 包含唯一id,用于区分每个item,作为key
  children: any  // 每个可拖拽的子元素,自定义样式
  changeHandle?: (list: any[]) => void // 拖拽后callback
  showDragIcon?: boolean // 是否显示拖拽图标
}

const DragSortList = ({
  dropId,
  list,
  children,
  changeHandle,
  showDragIcon,
}: DragSortListProps) => {
  const [data, setData] = useState(list)

  // 拖拽后执行函数
  const dragEnded = (param: any) => {
    const { source, destination } = param
    const _arr = [...data]
    const _item = _arr.splice(source.index, 1)[0]
    _arr.splice(destination.index, 0, _item)
    setData(_arr)
    changeHandle && changeHandle(_arr)
  }

  useEffect(() => {
    setData(list)
  }, [list])
  return (
    <div className='drag-sort-list'>
      <DragDropContext onDragEnd={dragEnded}>
        <StrictModeDroppable droppableId={dropId || "wrapper"}>
          {(provided, snapshot) => (
	    <ul ref={provided.innerRef} {...provided.droppableProps} className="list-box">
              {data && data.map((item, index) => {
                return (
                  <Draggable draggableId={`${item.id}`} index={index} key={item.id}>
                    {(_provided, _snapshot) => (
                      <Item
                        ref={_provided.innerRef}
                        showDragIcon={showDragIcon}
                        dragHandleProps={_provided.dragHandleProps}
                        {..._provided.draggableProps}
                        snapshot={_snapshot}
                        {...item}
                        dataItem={item}
                        itemContent={children}></Item>
                    )}
                  </Draggable>
                )
              })}
              {provided.placeholder}
            </ul>
          )}
        </StrictModeDroppable>
      </DragDropContext>
    </div>
  )
}

export default DragSortList

// StrictModeDroppable.tsx
import { useEffect, useState } from "react";
import { Droppable, DroppableProps } from "react-beautiful-dnd";

export const StrictModeDroppable = ({ children, ...props }: DroppableProps) => {
  const [enabled, setEnabled] = useState(false);

  useEffect(() => {
    const animation = requestAnimationFrame(() => setEnabled(true));

    return () => {
      cancelAnimationFrame(animation);
      setEnabled(false);
    };
  }, []);

  if (!enabled) {
    return null;
  }

  return <Droppable {...props}>{children}</Droppable>;
};

// Item.tsx
import React, { forwardRef, ReactNode } from "react"
import { DragOutlined } from "@ant-design/icons"

interface IProps {
  dataItem: any
  itemContent: (data: any, dragHandleProps?: any) => ReactNode
  dragHandleProps: any
  snapshot: any
  showDragIcon?: boolean
}

const Item = forwardRef(
  (
    { dataItem, itemContent, dragHandleProps, snapshot, showDragIcon = true, ...props }: IProps,
    ref: any,
  ) => {
    return (
      <li ref={ref} {...props || {}} className={"item-box " + (snapshot?.isDragging ? "hovering" : "")}>
        <div className='item'>
          <span {...dragHandleProps} className='drag-icon'>
            {showDragIcon && <DragOutlined />}
          </span>
          {itemContent(dataItem, dragHandleProps)}
        </div>
      </li>
    )
  },
)

export default Item

// 使用组件
import { useEffect, useState } from "react"
import { Input } from "antd"
import { DeleteOutlined, PlusCircleOutlined } from "@ant-design/icons"
import "./index.less"
import { useDebounceFn } from "ahooks"
import DragSortList from "@/components/ui/DragSortList"
import ObjectID from "bson-objectid"

interface IProps {
  defaultValue?: string[] // 初始值
  changeCb?: (list: any[]) => void // 值发生变化时回调
}

const PersonalizedTags = ({ defaultValue, changeCb }: IProps) => {
  const [tags, setTags] = useState<any>([])

  const Item = (data: any) => {
    const { tag, id } = data

    return (
      <div className='tag-item' key={tag}>
        <Input
          placeholder='请输入标签'
          defaultValue={tag}
          data-id={id}
          onChange={changeValueHandle.run}
          onBlur={changeValue}
          allowClear
        />
        <DeleteOutlined onClick={() => removeHandle(id)} />
      </div>
    )
  }

  const addHandle = () => {
    const newTags = [...tags, { id: ObjectID(Date.now()).id, tag: "" }]
    setTags([...newTags])
    changeCb && changeCb([...newTags])
  }

  const removeHandle = (id: any) => {
    const newTags = tags.filter((item: any) => item.id !== id)
    setTags(newTags)
    changeCb && changeCb(newTags)
  }

  const changeValue = () => {
    changeCb && changeCb([...tags])
  }

  const changeValueHandle = useDebounceFn(
    (e: any) => {
      const {
        dataset: { id },
      } = e.currentTarget
      const curVal = e.target.value
      const curTag = tags.find((item: any) => item.id === id)
      curTag["tag"] = curVal
    },
    { wait: 500 },
  )

  const changeDragCheckedList = (checkedList: any) => {
    setTags(checkedList)
    changeCb && changeCb(checkedList)
  }

  useEffect(() => {
    setTags(
      defaultValue?.map((item: any) => ({
        id: ObjectID(Date.now()).id,
        tag: item,
      })) || [],
    )
  }, [defaultValue])
  return (
    <div className='personalized-tags'>
      <DragSortList
        dropId='personalizedTags-wrapper'
        list={tags}
        showDragIcon={true}
        changeHandle={changeDragCheckedList}>
        {Item}
      </DragSortList>

      <div className='add-btn' onClick={addHandle}>
        <PlusCircleOutlined />
        添加
      </div>
    </div>
  )
}

export default PersonalizedTags

手写穿梭框

// 支持左侧滚动刷新,右侧拖拽排序
/**
 * 穿梭框组件
 * @param leftData 左边数据
 * @param rightData 右边数据
 * @param showMore 加载更多数据
 * @param goLeftHandle 移动到左边回调函数
 * @param goRightHandle 移动到又边回调函数
 * @param getDragCheckedList 获取右边拖拽后列表的回调函数
 * @returns
 */
import { LeftOutlined, RightOutlined } from "@ant-design/icons"
import Item from "./item"
import "./index.less"
import { useState } from "react"

interface IProps {
  leftData: any[]
  rightData: any[]
  showMore?: () => any
  goLeftHandle: (checkedList: string[]) => void
  goRightHandle: (checkedList: string[]) => void
  getDragCheckedList?: (list: any[]) => void
}

const Transfer = ({ leftData, rightData, showMore, goLeftHandle, goRightHandle, getDragCheckedList }: IProps) => {
  const [leftCheckedList, setLeftCheckedList] = useState<string[]>([])
  const [rightCheckedList, setRightCheckedList] = useState<string[]>([])

  // 移动到左边
  const goLeft = () => {
    goLeftHandle(rightCheckedList)
  }

  // 移动到右边
  const goRight = () => {
    goRightHandle(leftCheckedList)
  }

  // 获取选中的数据
  const getCheckedList = (checkedList: string[], type: "left" | "right") => {
    if (type === "left") {
      setLeftCheckedList(checkedList)
    } else {
      setRightCheckedList(checkedList)
    }
  }

  return (
    <div className='transfer'>
      <Item type={"left"} data={leftData} showMore={showMore} getCheckedList={getCheckedList} />
      <div className='btns'>
        <div className='left-btn' onClick={goLeft}>
          <LeftOutlined />
        </div>
        <div className='right-btn' onClick={goRight}>
          <RightOutlined />
        </div>
      </div>
      <Item type={"right"} data={rightData} getCheckedList={getCheckedList} getDragCheckedList={getDragCheckedList} />
    </div>
  )
}

export default Transfer

// Item.tsx
import { useInViewport } from "ahooks"
import { Checkbox } from "antd"
import { useEffect, useRef, useState } from "react"
import { CheckboxChangeEvent } from "antd/lib/checkbox"
import DragSortList from "../DragSortList"

const CheckboxGroup = Checkbox.Group

interface IProps {
  type: "left" | "right"
  data: any[]
  showMore?: () => any
  getCheckedList: (list: string[], type: IProps["type"]) => void
  getDragCheckedList?: any
}

const Item = ({ type, data, showMore, getCheckedList, getDragCheckedList }: IProps) => {
  const [checkedList, setCheckedList] = useState<string[]>([])
  const [loadMore, setLoadMore] = useState(type === "left" ? true : false)
  const checkAll = data.length === checkedList.length && data?.length ? true : false
  const indeterminate = checkedList.length > 0 && checkedList.length < data.length
  const title = type === "left" ? "未关联" : "已关联"
  // const loadMore = type === "left" ? true : false
  const loadMoreRef = useRef<HTMLDivElement>(null)
  const rootRef = useRef<any>(null)

  const [inViewport] = useInViewport(loadMoreRef, {
    root: rootRef,
  })

  const dataOps = data.map((item) => {
    return {
      id: item.id,
      label: item.name,
      value: item.id,
    }
  })

  const Item = (data: any, dragHandleProps?: any) => {
    const { value, label } = data

    return (
      <Checkbox key={value} value={value}>
        <span {...dragHandleProps}>{label}</span>
      </Checkbox>
    )
  }

  const onCheckAllChange = (e: CheckboxChangeEvent) => {
    setCheckedList(e.target.checked ? dataOps.map((item) => item.value) : [])
  }

  const onChange = (list: string[]) => {
    setCheckedList(list)
  }

  const changeDragCheckedList = (list: any[]) => {
    console.log("获取排序后的列表", list)
    getDragCheckedList(list)
  }

  useEffect(() => {
    if (inViewport) {
      showMore &&
        showMore().then((res: any) => {
          setLoadMore(res)
        })
    }
  }, [inViewport])

  useEffect(() => {
    getCheckedList(checkedList, type)
  }, [checkedList])

  useEffect(() => {
    setCheckedList([])
  }, [data])

  return (
    <div className='transfer-item'>
      <div className='header'>
        <Checkbox indeterminate={indeterminate} checked={checkAll} onChange={onCheckAllChange}>
          {title}
        </Checkbox>
        <span>
          {checkedList.length}/{data.length}
        </span>
      </div>

      <div className='content' ref={rootRef}>
        <CheckboxGroup value={checkedList} onChange={onChange}>
          {type === "right" ? (
            <DragSortList
              dropId='transfer-wrapper'
              list={dataOps}
              showDragIcon={false}
              changeHandle={changeDragCheckedList}>
              {Item}
            </DragSortList>
          ) : (
            dataOps.map((item) => {
              return Item(item)
            })
          )}
        </CheckboxGroup>
        {loadMore && (
          <div ref={loadMoreRef} className='load-more'>
            加载更多...
          </div>
        )}
      </div>
    </div>
  )
}

export default Item

图文混排组件

// 添加图片上传组件或文本输入框,支持拖拽排序功能
// 通过数据驱动组件更新,更新list: {id: string, type: 'IMAGE' | 'TEXT', value: string}[] 
// 唯一id: import ObjectID from "bson-objectid" new ObjectID(Date.now()).id 在组件内部消化,提交数据时不带id
// 代码片段
<DragSortList dropId='image-text-wrapper' list={list} changeHandle={changeHandle}>
   {Item}
</DragSortList>

const Item = (data: any) => {
    const { id, type, value } = data
    // input change时更新值,但不调用changeCb,因为调用了之后组件重新渲染,input会失去焦点,影响继续输入文本,所以在input blur的时候去changeCb,不会影响input输入
    const changeValueHandle = useDebounceFn(
      (e) => {
        const curVal = e.target.value

        list.forEach((item: any) => {
          if (item.id === id) {
            item.value = curVal
          }
        })
      },
      { wait: 500 },
    )

    const changeValueCb = () => {
      changeDataHandle &&
        changeDataHandle(
          list?.map((item: any) => ({
            type: item.type,
            value:
              item.type === "IMAGE" && Array.isArray(item.value) ? item.value?.[0] : item.value,
          })),
        )
    }

    const UploadCallBack = async (resList: any[]) => {
      const newList = [...list]
      newList.forEach((item: any) => {
        if (item.id === id) {
          item.value = resList?.[0]?.url
        }
      })
      changeDataHandle &&
        changeDataHandle(
          newList?.map((item: any) => ({
            type: item.type,
            value:
              item.type === "IMAGE" && Array.isArray(item.value) ? item.value?.[0] : item.value,
          })),
        )
    }

    const removeHandle = () => {
      const newList = list.filter((item: any) => item.id !== data.id)
      setList(newList)
      changeDataHandle &&
        changeDataHandle(
          newList?.map((item: any) => ({
            type: item.type,
            value:
              item.type === "IMAGE" && Array.isArray(item.value) ? item.value?.[0] : item.value,
          })),
        )
    }
    return (
      <>
        {(type === "TEXT" || type === "text") && (
          <Input.TextArea
            defaultValue={value}
            onChange={changeValueHandle.run}
            showCount
            maxLength={1000}
            onBlur={changeValueCb}
            allowClear
          />
        )}

        {(type === "IMAGE" || type === "image") && (
          <UploadFile
            key={"imageText" + data.id}
            uploadSuccess={UploadCallBack}
            initFiles={value || []}
            maxCount={1}
          />
        )}

        <DeleteOutlined onClick={removeHandle} />
      </>
    )
  }

多个tab内容更新,对比两个新旧对象,只传递更新过的内容

import deepEqual from "fast-deep-equal"
/**
 * @param newObj 新的对象
 * @param oldObj 旧的对象
 * @param deepIgnoreKeys 忽略深层比较的对象名称数组
 * @returns 返回一个新的对象,包含更新过的字段
 */
function getDiffObj(newObj: any, oldObj: any, deepIgnoreKeys?: string[]) {
  const diffObj: any = {}
  if ((typeof newObj !== "object" || typeof oldObj !== "object") && newObj !== oldObj) {
    return newObj
  }
  if (newObj === null || oldObj === null) {
    return newObj
  }

  if (Array.isArray(newObj) && Array.isArray(oldObj)) {
    if (newObj.length !== oldObj.length) {
      return newObj
    }
    if (!deepEqual(newObj, oldObj)) {
      return newObj
    }
    return oldObj
  }
  if (typeof newObj === "object" && typeof oldObj === "object") {
    for (const key in newObj) {
      if (!deepEqual(newObj[key], oldObj[key])) {
        if (deepIgnoreKeys && deepIgnoreKeys.includes(key)) {
          diffObj[key] = newObj[key]
        } else {
          diffObj[key] = getDiffObj(newObj[key], oldObj[key], deepIgnoreKeys)
        }
      }
    }
  }

  return diffObj
}

export default getDiffObj