后台系统开发记录

react antd
基础组件不涉及数据都是从外层属性带过来,业务组件数据针对层级嵌套比较多的组件,数据传输可以利用数据流而不是通过属性传递;针对数据获取如果不需要重新渲染的组件,数据获取可以通过ref获取
拖拽排序组件
// 基于react-beautiful-dnd插件
// 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
手写穿梭框
// 支持左侧滚动刷新,右侧拖拽排序
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"
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