最近遇到一个需求是需要用MaterialUI 实现可编辑表格,但Material UI比较符合并且比较完整的带有增删改查的可编辑DataGrid需要收费,于是就基于其免费版本的基础下做了一些修改。
需求和实现效果展示
需求
导入表格后,不符合要求的列做标红处理(比如必填项),可以筛选出出错的数据,可以对数据进行删除和修改,分页需可直接跳转到具体的某一页中
效果展示
可编辑修改
将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"/>
);
}
}