本文教你用 react 实现一个可选年月、自动生成当月所有日期列,并为每个日期行动态渲染多个习惯打卡输入框的交互式习惯追踪表格。
本文教你用 react 实现一个可选年月、自动生成当月所有日期列,并为每个日期行动态渲染多个习惯打卡输入框的交互式习惯追踪表格。
要构建一个支持“按月选择 + 每日多习惯打卡”的 React 表格,核心在于:动态计算当月天数 → 生成日期数组 → 为每个日期渲染一行,每行包含日期单元格 + 所有习惯的输入框(用于打“X”)。原始代码存在几个关键问题:daysArray 被声明为全局变量而非局部变量导致状态污染;dailyHabits.map 错误地放在外层循环外,导致习惯列无法与日期对齐;且未实现“X”标记逻辑和数据持久化结构。
以下是优化后的完整实现:
import React, { useState } 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('');
// ✅ 新增:用于存储打卡状态,结构为 { [date]: { [habitIndex]: 'X' | '' } }
const [checkmarks, setCheckmarks] = useState({});
// ✅ 安全获取当月天数(注意:JS中月份从0开始,所以 month-1)
const getDaysInMonth = (m, y) => {
if (!m || !y || isNaN(m) || isNaN(y)) return [];
const numDays = new Date(y, m, 0).getDate();
return Array.from({ length: numDays }, (_, i) => i + 1);
};
const handleDates = () => {
const days = getDaysInMonth(Number(month), Number(year));
setDateArray(days);
// ✅ 重置打卡状态(避免旧数据残留)
setCheckmarks({});
};
const addHabits = () => {
if (addHabit.trim()) {
setDailyHabits(prev => [...prev, addHabit.trim()]);
setAddHabit('');
}
};
// ✅ 切换打卡状态:点击输入框时填入/清空 "X"
const toggleCheckmark = (date, habitIndex) => {
setCheckmarks(prev => {
const dateKey = String(date);
const current = prev[dateKey] || {};
const newValue = current[habitIndex] === 'X' ? '' : 'X';
return {
...prev,
[dateKey]: {
...current,
[habitIndex]: newValue
}
};
});
};
return (
<div classname="habit-table">
{/* 选择年月 */}
<div classname="select-dates">
<input placeholder="月(1-12)" type="number" min="1" max="12" value="{month}" onchange="{e"> setMonth(e.target.value)}
/>
<input placeholder="年(如:2024)" type="number" min="2020" max="2100" value="{year}" onchange="{e"> setYear(e.target.value)}
/>
<button onclick="{handleDates}">生成日历</button>
</div>
{/* 添加习惯 */}
<div classname="add-habit-section">
<input placeholder="输入新习惯名称" value="{addHabit}" onchange="{e"> setAddHabit(e.target.value)}
/>
<button onclick="{addHabits}">添加习惯</button>
</div>
{/* 表格主体:日期行 × 习惯列 */}
{dateArray.length > 0 && (
<div classname="habits-grid">
<div classname="header-row">
<div classname="date-header">日期</div>
{dailyHabits.map((habit, idx) => (
<div key="{idx}" classname="habit-header">{habit}</div>
))}
</div>
{dateArray.map(day => {
const dayKey = String(day);
const dayChecks = checkmarks[dayKey] || {};
return (
<div key="{day}" classname="date-row">
<div classname="date-cell">
<input disabled value="{day}" classname="date-input">
</div>
{dailyHabits.map((_, idx) => (
<div key="{idx}" classname="check-cell">
<input type="text" maxlength="1" value="{dayChecks[idx]" onchange="{()"> toggleCheckmark(day, idx)}
className="check-input"
placeholder="X"
/>
</div>
))}
</div>
);
})}
</div>
)}
{dateArray.length === 0 && dailyHabits.length === 0 && (
<p classname="hint">请先选择年月并生成日历,再添加习惯。</p>
)}
</div>
);
}
export default HabitTable;
✅ 关键改进说明:
- 日期计算健壮性:使用 Number() 转换输入值,并增加基础校验,避免 NaN 导致异常;getDaysInMonth 返回新数组,不复用外部变量。
- 结构化打卡状态:引入 checkmarks 状态对象,以 { '1': { 0: 'X', 1: '' }, '2': { 0: '' } } 形式精准映射「日期→习惯索引→标记」,支持任意行列扩展。
- 交互体验优化:每个打卡输入框仅接受单字符(maxLength="1"),点击即切换 'X'/空值;表头分离,视觉清晰;添加空状态提示。
- 可维护性增强:CSS 类名语义化(如 .date-row, .check-cell),便于后续样式定制与响应式适配。
? 注意事项:
- 生产环境中建议对 month/year 输入做更严格的验证(如正则匹配、防负数、闰年兼容等);
- 若需持久化数据,可结合 localStorage 或后端 API,在 toggleCheckmark 后同步保存;
- 如需支持跨月连续打卡,可将 checkmarks 升级为 { [year-month-day]: { ... } } 格式,避免日期冲突。
该方案已具备完整功能闭环:用户选择年月 → 渲染当月日期行 → 添加习惯 → 在对应日期+习惯交叉格中输入“X”完成打卡,是构建个人习惯追踪系统的坚实基础。











