React表格单元素修改失效:点击单个按钮整行同步变更求助
React排班表单元格修改后整行同步变更的问题修复
问题描述
我用React实现了一个排班表页面,通过遍历员工列表和当月日期生成表格。点击某一行中特定日期的'+'按钮时,期望仅修改该单元格的内容,但保存后整行所有日期单元格都会同步变更。以下是主页面和编辑表单的代码:
主页面代码
'use client' import React, { useEffect, forwardRef, useState, useRef } from 'react' import moment from 'moment' import DatePicker, { CalendarContainer } from "react-datepicker"; import "react-datepicker/dist/react-datepicker.css"; import "components/Calendar/datepicker.scss" import staff from 'data/users/staff.json'; import shifts from 'data/shifts/shifts.json'; import timetable from './page.module.scss'; import formStyle from 'components/Forms/Form.module.scss'; import AddShiftDay from 'components/Forms/Others/AddShiftDay'; /* eslint-disable react/display-name */ export default function TimeTable() { const [startDate, setStartDate] = useState(new Date()); // const [employeeShift, setEmployeeShift] = useState<any>('+'); const days = useRef<any>(null) //Filter const [searchInput, setSearchInput] = useState(''); const [filteredResults, setFilteredResults] = useState<any>(staff); //Open const [ShiftDay, setShiftDay] = useState(false); //Edit/Add const [editShift, setEditShift] = useState<any>({}) useEffect(() => { let result = [...staff]; if (searchInput) { result = result.filter((item) => Object.values(item).join(' ').toLowerCase().includes(searchInput.toLowerCase())); } setFilteredResults(result); }, [searchInput]) const getCurPerson = (e:any) => { document.body.classList.add('mso'); const getID = e.target.closest('.person').getAttribute('data-id'); const day = e.target.getAttribute('data-id'); const options:any = { month: "long" }; const month = new Intl.DateTimeFormat("en-US", options).format(startDate) const curPerson = staff.find(s => s.id == getID) const date = `${day} ${month} ${startDate.getFullYear()}` let d = [...days.current.children] d.forEach(dday => { if (dday.getAttribute('data-id'), day) { setEditShift({...curPerson, date: date, day: dday.getAttribute('data-id')}) } }) } const openEdit = (e:any) => { setShiftDay(!ShiftDay) getCurPerson(e) } const handleSave = (e:any) => { const getID = e.target.closest('form').getAttribute('data-id'); const curPerson = filteredResults.find(s => s.id == getID) const newState = filteredResults.map((obj:any) => { // 👇️ if id equals 2, update country property if (obj.id === editShift.id) { return {...obj, day: 'ASD'}; } // 👇️ otherwise return the object as is return obj; }); setFilteredResults(newState); eventClose() } console.log(editShift) const eventClose = () => { setShiftDay(false); document.body.classList.remove('mso') } const monthDays = Array.from(Array(moment(startDate).daysInMonth()).keys()) const ToggleDatePicker = forwardRef(({ value, onClick }:any, ref:any) => ( <button className="toggle-date-picker" onClick={onClick} ref={ref}> {value} </button> )) return <> <section> <header className="grid grid-cols-2 lg:grid-cols-11 gap-20 mb-20 lg:mb-40 items-center"> <h1 className="order-1 lg:col-span-2">Time Table <strong className="text-grey-light"></strong></h1> <div className="order-3 lg:order-2 col-span-2 lg:col-span-7 text-center flex flex-col"> <DatePicker selected={startDate} onChange={(date: Date) => setStartDate(date)} dateFormat="LLLL - yyyy" customInput={<ToggleDatePicker />} showMonthYearPicker calendarClassName="requirement-calendar" className="requirement-toggle" /> </div> <div className='order-2 lg:order-3 lg:col-span-2'> </div> </header> <div className="w-full h-full rounded-8 shadow-DS overflow-auto"> <div className={`${timetable.timetable}`}> <header className="grid grid-cols-12 gap-10 items-center sticky top-0 bg-white"> <div className="col-span-3 pl-10 "> <form className={`${formStyle.Form} ${formStyle.Form_filter} !mb-0`} action="" > <input className='!mb-0' type="search" name="" placeholder="Search" onChange={(e:any) => setSearchInput(e.target.value)} /> </form> </div> <div className="col-span-9 text-center"> <div className='flex justify-between'> {monthDays.map((day) => ( <span className={`${timetable.Day}`} key={day + 1}> {day + 1} </span> ))} </div> </div> </header> <AddShiftDay person={editShift} style={`${formStyle.Form} ${ShiftDay ? `${formStyle.active}` : ''}`} setEmployeeShift={setEmployeeShift} eventClose={eventClose} eventSave={handleSave} shifts={shifts} /> {filteredResults.map((person, i) => { return ( <div key={i} data-id={person.id} className={`person ${timetable.Person}`} > <div className="grid grid-cols-2 gap-10 col-span-3 items-center pl-10"> <p>{person.first_name} {person.last_name }</p> <div className='text-12'> <p><strong className='text-primary'>{person.working_hours}</strong> hrs/month</p> <p><strong className='text-secondary'>-10</strong> hours left</p> <p>{person.holidays} Holidays left</p> </div> </div> <div className="col-span-9"> <div ref={days} className='flex justify-between'> {monthDays.map((day) => ( <button key={day + 1} data-id={day+1} type="button" className={`${timetable.Person_day}`} onClick={openEdit} > {person.day ? person.day : '+'} </button> ))} </div> </div> </div> ) })} </div> </div> </section> </> }
编辑表单代码
'use client' import { FormEvent, useState } from 'react'; import styles from "../Form.module.scss"; import CancelButton from 'components/Buttons/CancelButton' import PrimaryButton from 'components/Buttons/PrimaryButton' import save from 'images/icons/save.svg'; const AddShiftDay = ({person, style, eventSave, eventClose, shifts, setEmployeeShift}:any) => { const [shift, setShift] = useState(''); const handleSubmit = (e: FormEvent) => { e.preventDefault() setEmployeeShift(shift) } return <> <form onSubmit={handleSubmit} className={`${style} xxl:h-auto overflow-auto`} data-id={person.id} > <header className="sticky top-0 bg-secondary text-white py-20 text-center z-1"> <p>Add Shift</p> </header> <div className='p-24'> <p className='text-secondary font-b mb-10'>{person.date}</p> <p className="text-20 font-b mb-10">{person.first_name} {person.last_name}</p> <p className='text-grey-light mb-20'>ID {person.id}</p> <label>Shift</label> <div className={`${styles.select} before:content-[url("/arrows/arrow-sub.svg")]`}> <select name="shift" onChange={(e) => setShift(e.target.value)}> {shifts.map((shift:any) => ( <option key={shift.name} value={shift.name}>{shift.name}</option> ))} </select> </div> <div className="grid grid-cols-2 gap-24 items-center"> <CancelButton text='Cancel' style='text-left' event={eventClose} ></CancelButton> <PrimaryButton type='submit' text="Save" style="py-12 px-24" event={eventSave} icon={save} alt='save' ></PrimaryButton> </div> </div> </form> </> } export default AddShiftDay
问题定位
- 数据结构设计错误:当前每个员工对象的
day属性是单个值,渲染时所有日期单元格都读取这个值,导致修改后整行同步变化。应该用对象存储每个日期对应的班次,比如shifts: {1: '早班', 5: '晚班'}的结构。 - handleSave逻辑错误:直接给员工对象添加/修改
day属性,覆盖了单个值,导致所有单元格显示同一内容。 - getCurPerson无效循环:
d.forEach中的条件是逗号表达式,等价于if (day),会遍历所有日期重复设置editShift,最终仅保留最后一个日期的信息。 - employeeShift状态未利用:编辑表单设置了
employeeShift,但主页面渲染时未用该值更新对应单元格,仍依赖员工对象的单个属性。
解决方案
1. 调整员工数据结构
初始化时给每个员工添加shifts属性,用于存储日期-班次的映射:
useEffect(() => { let result = [...staff].map(item => ({ ...item, shifts: item.shifts || {} // 初始化空对象存储各日期班次 })); if (searchInput) { result = result.filter((item) => Object.values(item).join(' ').toLowerCase().includes(searchInput.toLowerCase())); } setFilteredResults(result); }, [searchInput])
2. 修正getCurPerson逻辑
移除无效循环,直接记录当前编辑的员工和目标日期:
const getCurPerson = (e:any) => { document.body.classList.add('mso'); const getID = e.target.closest('.person').getAttribute('data-id'); const targetDay = e.target.getAttribute('data-id'); const options:any = { month: "long" }; const month = new Intl.DateTimeFormat("en-US", options).format(startDate) const curPerson = staff.find(s => s.id == getID) const date = `${targetDay} ${month} ${startDate.getFullYear()}` setEditShift({...curPerson, date, targetDay}) }
3. 修改handleSave逻辑
更新对应员工的指定日期班次,而非修改单个day属性:
const handleSave = (e:any, selectedShift: string) => { e.preventDefault(); const newState = filteredResults.map((obj:any) => { if (obj.id === editShift.id) { return { ...obj, shifts: { ...obj.shifts, [editShift.targetDay]: selectedShift // 仅更新目标日期的班次 } }; } return obj; }); setFilteredResults(newState); eventClose() }
4. 调整编辑表单提交逻辑
将选中的班次传递给父组件的保存方法:
// AddShiftDay组件修改handleSubmit const handleSubmit = (e: FormEvent) => { e.preventDefault() eventSave(e, shift) // 把选中的班次传给父组件 }
5. 修改单元格渲染逻辑
根据当前日期读取对应班次:
{monthDays.map((day) => ( <button key={day + 1} data-id={day+1} type="button" className={`${timetable.Person_day}`} onClick={openEdit} > {person.shifts[day+1] || '+'} {/* 读取对应日期的班次,无则显示+ */} </button> ))}
6. 移除无用状态
删除employeeShift状态,简化代码逻辑。
内容的提问来源于stack exchange,提问作者Martin Avramov
相关产品推荐
相关产品推荐

