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

React工时表组件周切换数据加载功能实现求助

React工时表组件修复方案

核心问题梳理

  • 重复维护weekStartDate和startDate两个日期状态,导致数据同步混乱
  • 本地存储的timeSheetData未关联到实际渲染的data状态,加载历史数据逻辑失效
  • 未来周处理逻辑不严谨,未判断当前切换的周是否属于未来时间
  • 多个冗余状态(previousSubmissions、savedData、timeSheetData)重复存储数据,引发不一致
  • 初始周未对齐到周一(与表头Mon起始逻辑不符)

修复后的完整代码

import React, { useState, useEffect } from "react";
import Modal from "react-modal";

const TimeSheet = () => {
  const customStyles = {
    content: {
      top: "50%",
      left: "50%",
      right: "auto",
      bottom: "auto",
      marginRight: "-50%",
      transform: "translate(-50%, -50%)",
    },
  };

  // 统一用startDate管理周起始日期,初始化时对齐到周一
  const [startDate, setStartDate] = useState(() => {
    const today = new Date();
    const dayOfWeek = today.getDay(); // 0=周日,1=周一...6=周六
    const offset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek; // 对齐到最近的周一
    return new Date(today.setDate(today.getDate() + offset));
  });

  // 活跃项目模板(仅作为初始空数据模板)
  const activeProjectTemplate = [
    { project: "Project A", hours: [0, 0, 0, 0, 0, 0, 0], total: 0 },
    { project: "Project B", hours: [0, 0, 0, 0, 0, 0, 0], total: 0 },
    { project: "Project C", hours: [0, 0, 0, 0, 0, 0, 0], total: 0 },
    { project: "Project D", hours: [0, 0, 0, 0, 0, 0, 0], total: 0 },
    { project: "Project F", hours: [0, 0, 0, 0, 0, 0, 0], total: 0 },
    { project: "Project G", hours: [0, 0, 0, 0, 0, 0, 0], total: 0 },
  ];

  // 当前展示的工时数据
  const [data, setData] = useState([]);

  const days = ["Mon", "Tue", "Wed", "Thurs", "Fri", "Sat", "Sun"];

  // 日期偏移工具函数
  const dateOffset = (date, offset) => {
    const newDate = new Date(date);
    newDate.setDate(date.getDate() + offset);
    return newDate;
  };

  // 判断当前周是否为未来周
  const isFutureWeek = () => {
    const currentWeekEnd = dateOffset(startDate, 6);
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    return currentWeekEnd > today;
  };

  // 从本地存储加载对应周的数据
  const loadWeekData = () => {
    const weekKey = `timeSheet_${startDate.toISOString().slice(0, 10)}`;
    const savedData = localStorage.getItem(weekKey);
    
    if (savedData) {
      setData(JSON.parse(savedData));
    } else {
      // 当前周无数据:显示空的活跃项目表;未来周:显示空表
      setData(isFutureWeek() ? [] : [...activeProjectTemplate]);
    }
  };

  // 保存当前周数据到本地存储
  const saveWeekData = () => {
    const weekKey = `timeSheet_${startDate.toISOString().slice(0, 10)}`;
    localStorage.setItem(weekKey, JSON.stringify(data));
  };

  // 切换周时自动加载对应数据
  useEffect(() => {
    loadWeekData();
  }, [startDate]);

  // 上一周切换
  const handlePreviousWeek = () => {
    setStartDate((curr) => dateOffset(curr, -7));
  };

  // 下一周切换
  const handleNextWeek = () => {
    setStartDate((curr) => dateOffset(curr, 7));
    // 未来周自动清空数据(loadWeekData会处理)
  };

  // 添加新项目行
  const handleAddRow = () => {
    setData([
      ...data,
      {
        project: `Project ${data.length + 1}`,
        hours: [0, 0, 0, 0, 0, 0, 0],
        total: 0,
      },
    ]);
  };

  // 删除项目行
  const handleDeleteRow = (index) => {
    const updatedData = [...data];
    updatedData.splice(index, 1);
    setData(updatedData);
  };

  // 计算单项目总工时
  const calculateTotal = (hours) => {
    return hours.reduce((sum, hour) => sum + (parseInt(hour) || 0), 0);
  };

  // 工时输入变更
  const handleHoursChange = (projectIndex, dayIndex, value) => {
    const updatedData = [...data];
    updatedData[projectIndex].hours[dayIndex] = value;
    updatedData[projectIndex].total = calculateTotal(updatedData[projectIndex].hours);
    setData(updatedData);
  };

  // 保存工时(与提交合并,避免重复逻辑)
  const handleSaveSubmit = () => {
    saveWeekData();
    console.log(`已保存周数据:${startDate.toLocaleDateString('de-DE')}`);
  };

  // Modal相关逻辑(保留原功能)
  let subtitle;
  const [modalIsOpen, setIsOpen] = React.useState(false);

  function openModal() {
    setIsOpen(true);
  }

  function afterOpenModal() {
    subtitle.style.color = "#f00";
  }

  function closeModal() {
    setIsOpen(false);
  }

  return (
    <div>
      <h1>项目工时管理表</h1>
      <div>
        <button onClick={handlePreviousWeek}>上一周</button>
        <span>{`${startDate.toLocaleDateString('de-DE')} - ${dateOffset(startDate, 6).toLocaleDateString('de-DE')}`}</span>
        <button onClick={handleNextWeek}>下一周</button>
      </div>
      <table border="1">
        <thead>
          <tr>
            <th>项目</th>
            {days.map((day) => (
              <th key={day}>
                {day}
                <br />
                {dateOffset(startDate, days.indexOf(day)).toLocaleDateString('de-DE')}
              </th>
            ))}
            <th>总计</th>
            <th>操作</th>
          </tr>
        </thead>
        <tbody>
          {data.length === 0 ? (
            <tr>
              <td colSpan={9}>无数据</td>
            </tr>
          ) : (
            data.map((item, index) => (
              <tr key={index}>
                <td>{item.project}</td>
                {item.hours.map((hour, dayIndex) => (
                  <td key={dayIndex}>
                    <input
                      type="number"
                      value={hour}
                      onChange={(e) => handleHoursChange(index, dayIndex, e.target.value)}
                      min="0"
                    />
                  </td>
                ))}
                <td>{item.total}</td>
                <td>
                  <button onClick={() => handleDeleteRow(index)}>删除</button>
                </td>
              </tr>
            ))
          )}
        </tbody>
      </table>
      <button onClick={handleAddRow}>添加新项目</button>
      <button onClick={handleSaveSubmit}>保存并提交</button>

      <button onClick={openModal}>打开弹窗</button>
      <Modal
        isOpen={modalIsOpen}
        onAfterOpen={afterOpenModal}
        onRequestClose={closeModal}
        style={customStyles}
        contentLabel="示例弹窗"
      >
        <h2 ref={(_subtitle) => (subtitle = _subtitle)}>Hello</h2>
        <button onClick={closeModal}>关闭</button>
        <div>我是弹窗</div>
        <form>
          <input />
          <button>Tab导航</button>
          <button>保留在</button>
          <button>弹窗内</button>
        </form>
      </Modal>
    </div>
  );
};

export default TimeSheet;

关键修改说明

  1. 统一日期状态:删除冗余的weekStartDate,只用startDate管理周起始,初始化时自动对齐到周一,符合表头的周一起始逻辑。
  2. 数据加载与展示关联:切换周时自动调用loadWeekData,直接将本地存储的数据同步到data状态,确保展示的是对应周的内容。
  3. 未来周处理:通过isFutureWeek判断,未来周直接显示空表,当前周无保存数据时显示预设的活跃项目空表。
  4. 简化存储逻辑:删除previousSubmissions、savedData等冗余状态,直接用data作为唯一数据源,存储时仅保存当前周的data。
  5. 优化交互体验:空数据时显示提示,输入框添加min="0"限制,合并保存与提交按钮避免重复逻辑。

内容的提问来源于stack exchange,提问作者Codeworld

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 03:02:39