MaterialUI Datagrid 实现可编辑表格

最近遇到一个需求是需要用MaterialUI 实现可编辑表格,但Material UI比较符合并且比较完整的带有增删改查的可编辑DataGrid需要收费,于是就基于其免费版本的基础下做了一些修改。

需求和实现效果展示

需求

导入表格后,不符合要求的列做标红处理(比如必填项),可以筛选出出错的数据,可以对数据进行删除和修改,分页需可直接跳转到具体的某一页中

效果展示

截屏2023-02-28 12.03.17.png

可编辑修改

将DataGrid的编辑状态改成行编辑

editMode="row"

禁止其默认操作,如双击某行进入编辑模式,失去焦点变回不可编辑模式

 const handleRowEditStop = (params, event) => {
    event.defaultMuiPrevented = true;
  };
  const handleRowEditStart = (params, event) => {
    event.defaultMuiPrevented = true;
  };
  onRowEditStop={handleRowEditStop}
  onRowEditStart={handleRowEditStart}

processRowUpdate当用户执行停止编辑的操作时调用该道具。该道具使用两个参数调用:

  • 通过后具有新值的更新行valueSetter
  • 编辑单元格或行之前行的值
    保存行后,processRowUpdate必须返回将用于更新内部状态的行对象。返回的值用作调用 的参数apiRef.current.updateRows。
const processRowUpdate = (newRow) => {
    const updatedRow = { ...newRow, isNew: false };
    setRows(rows.map((row) => (row.id === newRow.id ? updatedRow : row)));
    return updatedRow;
  }

删除前判断该行是否有值控制是否显示确认删除弹框

const handleDeleteClick = (id) => () => {
    const deleteRow = rows.filter((row) => row.id === id)
    let ss = {...deleteRow[0]}
    delete ss.id;
    delete ss.isNew;
    if(JSON.stringify(ss) === '{}') {
      setRows(rows.filter((row) => row.id !== id));
    } else {
      setCommonDialog({visible: true, type: 'delete', id, value: { content: '确认删除该行吗?', confirm: '确定', cancel: '取消', title: '删除'}})
    }
  };

添加新行操作

const handleAddClick = () => {
    const id = randomId();
    setRows((oldRows) => [{ id, isNew: true }, ...oldRows]);
    setRowModesModel((oldModel) => ({
      ...oldModel,
      [id]: { mode: GridRowModes.Edit, fieldToFocus: 'name' },
    }));
  };

判断导入数据正确的条数(必填项是否不为空)

 const changeRows = async (value) => {
    setRows(value);
    setRowModesModel({});
    const front = value.filter((ele)=> !personSchema.isValidSync(ele));
    const len = value?.length;
    if(front?.length === 0) {
      setCommonDialog({visible: true, value: { content: `共导入${len}条数据,${len}条正确`, title: '导入临床信息', confirm: '确定' } })
    } else {
      const frontError = front.map((ele)=>ele.id)
      const value = () => (
        <Box>
          <Box>
            共导入{len}条数据,{len - errorSet.size}条正确,<Box component='span' sx={{color: '#FF0000'}}>{frontError.length}条错误</Box>
          </Box>
          <Box sx={{marginTop: '20px'}}>错误信息已标红,具体请查看导入列表</Box>
        </Box>
      )
      setCommonDialog({visible: true, value: {content: value(), title: '导入临床信息', confirm: '确定'}})
    }
  }

使用yulp库对数据的格式进行验证

import { string, object, number, date } from 'yup'
const personSchema = object({
  batchNo: string().required('实验测序批次编号为必填项,不能为空'),
  specimenNo: string().required('样本编号为必填项,不能为空'),
  specimenIndex: string().required('样本Index标签为必填项,不能为空'),
  age: number().required('年龄为必填项,不能为空'),
  symptom: string().required('临床症状为必填项,不能为空'),
  samplingDate: date()
                  .required('采样日期为必填项,不能为空')
                  .max(new Date(), '日期不能填今日之后')
});
export default personSchema

//返回所以验证不通过的对象
 const front = value.filter((ele)=> !personSchema.isValidSync(ele)); 
// 判断单条数据是否通过验证
await personSchema.validate(editedRow,{ abortEarly: false });

对columns中各字段的处理

必填列列名前加 *
const addIcon = (value) => {
    return <Box sx={{ fontWeight: 500, color: 'rgba(0, 0, 0, 0.87)' }}><Box component="span" sx={{color: '#ff0000'}}>*</Box>{value}</Box>
  }
renderHeader: ()=>addIcon('实验测序批次编号'),
必填项未输入则标红处理
const renderRequireCell = (props) => {
    const { value, field, id } = props;
    if(specialError.get(id + '') && specialError.get(id + '')?.includes(field)) {
      return <Box className="errorBox">{value}</Box>
    }
    if(!value) {
    return <Box className="errorBox">{value}</Box>
    } else {
      return <Box>{value}</Box>
    }
  }
可编辑框渲染
const renderEditInputCell = (params) => {
    return <EditInputCell {...params} />
  }

const EditInputCell = (props) => {
  const { id, value, field, headerName } = props;
  const { renderType } = props?.colDef
  const apiRef = useGridApiContext();
  const handleChange = async (event) => {
    if(field === 'samplingDate') {
      await apiRef.current.setEditCellValue({ id, field, value: format(event, 'yyyy/M/dd') });
    } else {
      await apiRef.current.setEditCellValue({ id, field, value: event?.target?.value || event });
    }
    apiRef.current.stopCellEditMode({ id, field });
  };
  const handleNumberChange = async (event) => {
    await apiRef.current.setEditCellValue({ id, field, value: event.target.value ? event.target.value?.replace(/[^0-9 ]/,'') : '' });
    apiRef.current.stopCellEditMode({ id, field });
  };
  switch(renderType) {
    case 'date':
      return (
        <DatePicker
          inputFormat='yyyy-MM-dd'
          value={value || null}
          defaultValue = {null}
          disableFuture
          onChange={handleChange}
          className="datepicker"
          renderInput={(params) => (
            <TextField
              {...params}
              label={undefined}
              margin="dense"
              size="small"
              className={value && new Date(value).valueOf() <= new Date().valueOf() ? 'rightTextField' : 'errorTextField'}
            />
          )}
        />
      )
    case 'select':
      const { option } = props?.colDef
      return (
        <FormControl className={field}>
          <Select
            defaultValue = ""
            labelId="demo-simple-select-label"
            id="demo-simple-select"
            value={value || ''}
            size="small"
            sx={{width: 90}}
            onChange={handleChange}
          >
            {
              option.map((ele)=>(
                <MenuItem value={ele}>{ele}</MenuItem>
              ))
            }
          </Select>
      </FormControl>
      )
    case 'number':
      return <TextField value={value} className={value ? 'rightTextField' : 'errorTextField'} onChange={handleNumberChange} label='' size="small" />
    default:
      return (
        <TextField value={value} className={field === 'symptom' && !value ? 'errorTextField' : 'rightTextField' } onChange={handleChange} label='' size="small"/>
      );
  }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容