React + ts (四)任务卡片托拽移动

1,069 阅读4分钟

  前面四讲我们用ts + react方式实现了一个todolist,感兴趣的朋友建议去看前三篇,本期我们实现任务list移动,大家可以下载对于的Tag代码块,结合本章内容查阅,同时喜欢的朋友顺便点个赞吧,你们的支持是我源源不断的动力

1、移动todo list中item逻辑

  移动列表中项目的方式逻辑是选中要移动的,托拽到另一个列表中的具体位置处,因此在这个过程中如下图所示:


(1)、其中我们需要通过下标拿到需要移动的卡片item,从原来任务列表中移除(removeItemAtIndexs)
(2)、移动过去,找到需要插入的任务列表,将卡片插入进去(insertItemAtIndex),具体如下

/**
* @description 移除指定位置元素
* @param list {Array} 任务列表
* @param i {number} 去除位置
* @returns list 列表
*/
export const removeItemAtIndex = <T>(list: T[], i: number) => {
  return [...list.slice(0, i), ...list.slice(i + 1)]
}
/**
* 
* @param list {array} 插入的列表
* @param item {T} 项目
* @param i {number} 插入位置
* @returns 
*/
export const insertItemAtIndex = <T>(list: T[], item: T, i: number) => {
  return [...list.slice(0, i), item, ...list.slice(i)]
}

  这里有个ts的小知识泛型,如果有一个函数需要可能去操作不同类型的数据,但是思路方式又是相同,我们就可以通过泛型的方式去定义这样的泛型函数,具体使用泛型如下:

// 泛型interface
interface ItemInfo<T> {
  children: T
}
// 泛型type
type Item<T> = {
  children: T
}
// 在使用的过程中我们可以通过
let info: Item<Array<string>> = { children: ["泛型示例"] }

  对于泛型函数,我们还可以用这么用:

function demo<T = Record<string, any>>(item: T, key: keyof T) {
  return item[key]
}

const info = { name: '123', msg: '这是一个测试内容' }

demo(info, 'msg')

  这样结合泛型,我们在项目中的语法提示就会根据数据类型进行智能提示,如下图所示;可以避免一些错误,同样很方便开发;
image.png

2、移动卡片实现

  上面我们讲了,基本去实现一个数组列表操作的移除插入的操作,这是我们实现托拽的一个基本思路,下面介绍一下我们托拽移动卡片的需求,如下图所示:
image.png
托拽功能呢我们需要借助react两个常用的开源库,具体安装如下:

yarn add react-dnd react-dnd-html5-backend

  具体如何使用大家可以去参考官方文档,本节暂时不做托拽库的使用介绍,附上官方文档

  下面我们设计一下功能的流程:

  针对于上述流程结合我们代码,程序这样去设计

下面是我的实现方法:

  • 第一步: 定义draggeditem信息存储模块
// 托拽 type.ts定义
....
....
/**
 * @description 托拽的项目
 */
export interface DragItem {
  id: string
  text: string
  type: 'COLUMN'
}

export interface AppState {
  /**
   * @description 板块list
   */
  lists: List[]
  /**
   * @description
   */
  draggedItem: DragItem | null
}

export type Action = {
  type: 'ADD_LIST'
  payload: string
} | {
  type: 'ADD_TASK'
  payload: { text: string, listId: string }
} | {
  type: "MOVE_LIST"
  payload: { dragId: string, hoverId: string }
} | {
  type: 'SET_DRAG_ITEM',
  payload: DragItem | null
}

// AppAction.ts
export const setDragedItem = (item: DragItem | null): Action => ({
  type: 'SET_DRAG_ITEM',
  payload: item
})

  • 第二步:通过AppStateContext为所有Todo模块提供draggedItem托拽的卡片信息;
import React, { createContext, useContext, Dispatch } from 'react'
import { List, Task, AppState, Action, DragItem} from './type'
import { useImmerReducer } from 'use-immer'
import { appStateReducer } from './Reducer'

export interface AppStateContextProps {
  lists: List[]
  getTasksByListId(id: string): Task[]
  dispatch: Dispatch<Action>
  draggedItem: DragItem | null
}

const AppStateContext = createContext<AppStateContextProps>(
  {} as AppStateContextProps
)

const appData: AppState = {
  lists: [
    {
      id: "0",
      text: "待处理",
      tasks: [{ id: "c0", text: "我想去新疆天山" }]
    },
    {
      id: "1",
      text: "进行中",
      tasks: [{ id: "c2", text: "不好意思居家隔离了" }]
    },
    {
      id: "2",
      text: "已完成",
      tasks: [{ id: "c3", text: "车票已经作废了" }]
    }
  ],
  draggedItem: null
}

export const AppStateProvider: React.FC<React.PropsWithChildren<{}>> = ({ children }) => {
  // 创建我们子组件中用到的数据
  const [todoInfo, dispatch] = useImmerReducer(appStateReducer, appData)

  const { lists, draggedItem } = todoInfo
  const getTasksByListId = (id: string) => {
    return lists.find((i: List) => i.id === id)?.tasks || []
  }

  return (
    <AppStateContext.Provider value={{ ...draggedItem, ...todoInfo, getTasksByListId, dispatch}}>
      {children}
    </AppStateContext.Provider>
  )
}

export const useAppState = () => {
  return useContext(AppStateContext)
}
  • 第三步: 增加修改draggedItem的分支方法
...
case "SET_DRAG_ITEM":
      drap.draggedItem = action.payload;
      break;
...
  • 第四步:在卡片组件中,我们通过react-dnd库为卡片主项目正价托拽功能,具体实现如下
import { useDrag } from "react-dnd";
import { setDragedItem } from "../../state/TodoState/Appction";
import { useAppState } from "../../state/TodoState/AppStateContext";
import { DragItem } from "../../state/TodoState/type";

/**
 * @description 托拽hooks
 */
export const useDragItem = (item: DragItem) => {
  const { dispatch } = useAppState()

  const [, drag] = useDrag({
    type: item.type,
    item: () => {
      dispatch(setDragedItem(item))
      return item
    },
    end: () => dispatch(setDragedItem(null))
  })
  return { drag }
}
import { ColumnContainer, ColumnTitle } from "../../styles"
import Card from "./Card"
import { useAppState } from "../../state/TodoState/AppStateContext"
import { Task } from "../../state/TodoState/type"
import AddNewItem from "./AddNewItem"
import { addTask, moveList } from "../../state/TodoState/Appction"
import { useRef } from "react"
import { useDragItem } from "./hooks"
import { useDrop } from "react-dnd"
import { throttle } from "lodash"

type ColumnProps = {
  text: string
  id: string
}

const Column: React.FC<ColumnProps> = ({ text, id }) => {
  
  const { draggedItem, getTasksByListId, dispatch } = useAppState()
  const tasks = getTasksByListId(id)
  
  const ref = useRef<HTMLDivElement>(null)
  // 获取执行托拽的项目
  const { drag }  = useDragItem({type: 'COLUMN', id, text})
  // 放下托拽内容
  const [, drop] = useDrop({
    accept: "COLUMN",
    hover: throttle(() => {
      // 如果没有托拽项目就不执行下放
      if(!draggedItem) {
        return
      }
      // 如果是行托拽就执行
      if (draggedItem.type === 'COLUMN') {
        // 如果是托拽内容还是在本列表内就不执行下放操作
        draggedItem.id !== id && dispatch(moveList(draggedItem.id, id))
      }
    }, 150)
  })
  // 执行drag drop
  drag(drop(ref))
  
  return (
    <ColumnContainer ref={ref}>
      <ColumnTitle>{text}</ColumnTitle>
      {
        tasks.map((i: Task) => (
          <Card id={i.id} text={i.text} key={`${i.id}${i.text}`}  />
        ))
      }
      <AddNewItem
        toggleButtonText='添加下一个任务'
        onAdd={(text: string) => dispatch(addTask(text, id))}
        ></AddNewItem>
    </ColumnContainer>
  )
}

export default Column

这样我们就实现了一个卡片托拽版本的todolist,大概实现思路在这里,代码分支tag我已上传到了gitee,感兴趣的朋友可以去下载本章的tag分支查阅,如果对你有帮助,转评赞三连随便给一个~谢谢你们的支持。