You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

问题定位

  1. 数据结构设计错误:当前每个员工对象的day属性是单个值,渲染时所有日期单元格都读取这个值,导致修改后整行同步变化。应该用对象存储每个日期对应的班次,比如shifts: {1: '早班', 5: '晚班'}的结构。
  2. handleSave逻辑错误:直接给员工对象添加/修改day属性,覆盖了单个值,导致所有单元格显示同一内容。
  3. getCurPerson无效循环:d.forEach中的条件是逗号表达式,等价于if (day),会遍历所有日期重复设置editShift,最终仅保留最后一个日期的信息。
  4. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 23:42:51