
本文教你使用 react 动态生成某年某月的所有日期,并为每个日期构建多习惯打卡行,用户可在对应单元格输入 'x' 完成标记,实现轻量级习惯追踪表。
本文教你使用 react 动态生成某年某月的所有日期,并为每个日期构建多习惯打卡行,用户可在对应单元格输入 'x' 完成标记,实现轻量级习惯追踪表。
要构建一个支持「按月选择 + 日期自动填充 + 多习惯打卡」的 React 习惯追踪表格,核心在于:正确计算当月天数、结构化渲染二维数据(日期 × 习惯)、并确保每个输入框可独立编辑。原始代码存在关键逻辑缺陷——dailyHabits.map 被错误地置于外层循环之外,导致所有日期共用同一组习惯输入框;同时未初始化打卡状态,无法持久记录 'X' 标记。
以下是优化后的完整实现方案:
✅ 正确的数据结构设计
我们需维护一个二维状态:checkmarks[dayIndex][habitIndex],表示第 dayIndex 天在第 habitIndex 个习惯上的打卡状态(如 'X' 或空字符串)。为此,推荐使用 useState 初始化为二维数组:
const [checkmarks, setCheckmarks] = useState([]);
// 初始化:每新增一天,为其创建与 habits 等长的空数组
useEffect(() => {
if (dateArray.length > 0 && dailyHabits.length > 0) {
const initialMarks = dateArray.map(() =>
Array(dailyHabits.length).fill('')
);
setCheckmarks(initialMarks);
}
}, [dateArray, dailyHabits]);
✅ 渲染逻辑:嵌套映射实现「行-列」布局
每个 ? 注意: 通过以上结构,你将获得一个真正可交互、可扩展、符合直觉的习惯打卡表格——既满足基础功能,又为后续添加统计、导出、本地存储等功能预留清晰接口。,并绑定对应打卡值及更新函数:{dateArray.map((day, dayIndex) => (
<div key="{day}" classname="date-row">
<input type="text" disabled value="{day}" classname="date-cell">
{dailyHabits.map((habit, habitIndex) => (
<input key="{habitIndex}" type="text" maxlength="{1}" value="{checkmarks[dayIndex]?.[habitIndex]" onchange="{(e)"> {
const newMarks = [...checkmarks];
newMarks[dayIndex] = [...(newMarks[dayIndex] || [])];
newMarks[dayIndex][habitIndex] = e.target.value.toUpperCase().replace(/[^X]/g, '');
setCheckmarks(newMarks);
}}
placeholder="X"
className="habit-cell"
/>
))}
</div>
))}
onChange 中使用正则 /[^X]/g 过滤非 'X' 字符,确保仅允许输入大写 X(提升用户体验)。✅ 完整可运行组件(含样式建议)
import React, { useState, useEffect } from 'react';
import './HabitTable.css';
function HabitTable() {
const [month, setMonth] = useState('');
const [year, setYear] = useState('');
const [dateArray, setDateArray] = useState([]);
const [dailyHabits, setDailyHabits] = useState([]);
const [addHabit, setAddHabit] = useState('');
const [checkmarks, setCheckmarks] = useState([]);
// 初始化打卡状态
useEffect(() => {
if (dateArray.length > 0 && dailyHabits.length > 0) {
const initial = dateArray.map(() =>
Array(dailyHabits.length).fill('')
);
setCheckmarks(initial);
}
}, [dateArray, dailyHabits]);
const addHabits = () => {
if (addHabit.trim()) {
setDailyHabits(prev => [...prev, addHabit.trim()]);
setAddHabit('');
}
};
const getDaysInMonth = (m, y) => {
return new Date(y, m, 0).getDate();
};
const handleDates = () => {
const m = parseInt(month, 10);
const y = parseInt(year, 10);
if (isNaN(m) || isNaN(y) || m 12) return;
const days = Array.from({ length: getDaysInMonth(m, y) }, (_, i) => i + 1);
setDateArray(days);
};
return (
<div classname="habit-table">
<div classname="select-dates">
<input placeholder="Month (1-12)" value="{month}" onchange="{e"> setMonth(e.target.value)}
/>
<input placeholder="Year (e.g. 2024)" value="{year}" onchange="{e"> setYear(e.target.value)}
/>
<button onclick="{handleDates}">Generate Calendar</button>
</div>
<div classname="habits-section">
<h2>Habit Tracker</h2>
<div classname="add-habit">
<input placeholder="Add new habit" value="{addHabit}" onchange="{e"> setAddHabit(e.target.value)}
/>
<button onclick="{addHabits}">+ Add</button>
</div>
{dateArray.length > 0 && dailyHabits.length > 0 ? (
<div classname="grid-container">
{/* 表头:Date + Habit names */}
<div classname="header-row">
<div classname="header-cell">Date</div>
{dailyHabits.map((h, i) => (
<div key="{i}" classname="header-cell">{h}</div>
))}
</div>
{/* 数据行 */}
{dateArray.map((day, dayIdx) => (
<div key="{day}" classname="data-row">
<div classname="date-cell">{day}</div>
{dailyHabits.map((_, habitIdx) => (
<input key="{habitIdx}" type="text" maxlength="{1}" value="{checkmarks[dayIdx]?.[habitIdx]" onchange="{(e)"> {
const newMarks = [...checkmarks];
newMarks[dayIdx] = [...(newMarks[dayIdx] || [])];
newMarks[dayIdx][habitIdx] = e.target.value.toUpperCase().replace(/[^X]/g, '');
setCheckmarks(newMarks);
}}
className="habit-input"
/>
))}
</div>
))}
</div>
) : (
<p classname="hint">Select month/year and add at least one habit to begin.</p>
)}
</div>
</div>
);
}
export default HabitTable;
? 关键注意事项
Date 构造函数中月份是 0–11,但用户输入应为 1–12,因此调用 new Date(year, month, 0) 时直接传入用户输入的 month(无需减1),因为 month=1 表示 1 月,new Date(2024, 1, 0) 返回的是 2024 年 1 月的最后一天(即 31 日)——这是正确的。maxLength={1} 和正则过滤,避免误输多余字符。.grid-container 可用 CSS Grid 实现对齐,.habit-input 设置统一宽高与居中样式,增强视觉一致性。











