
本文教你使用 react 动态生成某年某月的完整日期列,并为每个日期搭配多个可编辑的习惯打卡项(如填入 'x'),实现结构清晰、响应式的习惯追踪表格。
本文教你使用 react 动态生成某年某月的完整日期列,并为每个日期搭配多个可编辑的习惯打卡项(如填入 'x'),实现结构清晰、响应式的习惯追踪表格。
要构建一个支持「按月/年筛选 → 显示当月所有日期 → 每日多习惯打卡」的 Habit 表格,核心在于:正确计算当月天数、结构化渲染“行-日期 × 列-习惯”二维布局、并确保状态管理可扩展。原始代码存在几个关键问题:daysArray 未声明为局部变量导致副作用;dailyHabits.map 错误地放在外层 div.days 中,导致习惯栏与日期错位;且缺少对用户打卡数据(如 '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('');
// ✅ 新增:二维状态存储打卡记录,格式为 record[day][habitIndex] = 'X' | ''
const [records, setRecords] = useState({});
// 计算当月总天数并生成日期数组 [1, 2, ..., 30/31]
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);
// ✅ 初始化空记录:每个日期对应一个习惯数组
const initRecords = {};
days.forEach(day => {
initRecords[day] = Array(dailyHabits.length).fill('');
});
setRecords(initRecords);
};
const addHabits = () => {
if (addHabit.trim() === '') return;
const updated = [...dailyHabits, addHabit];
setDailyHabits(updated);
setAddHabit('');
// ✅ 同步更新 records:为新习惯补全所有日期的空字段
setRecords(prev => {
const newRecords = { ...prev };
Object.keys(newRecords).forEach(day => {
newRecords[day] = [...newRecords[day], ''];
});
return newRecords;
});
};
// ✅ 处理单个单元格打卡(如点击输入 'X')
const handleCellChange = (day, habitIndex, value) => {
setRecords(prev => ({
...prev,
[day]: prev[day].map((v, i) => (i === habitIndex ? value : v))
}));
};
return (
<div classname="habit-table-container">
{/* 选择年月 */}
<div classname="select-dates">
<input placeholder="Month (1-12)" type="number" min="1" max="12" value="{month}" onchange="{(e)"> setMonth(e.target.value)}
/>
<input placeholder="Year (e.g. 2024)" type="number" min="2000" max="2100" value="{year}" onchange="{(e)"> setYear(e.target.value)}
/>
<button onclick="{handleDates}">Generate Calendar</button>
</div>
{/* 添加习惯 */}
<div classname="add-habit-section">
<input placeholder="Enter new habit" value="{addHabit}" onchange="{(e)"> setAddHabit(e.target.value)}
/>
<button onclick="{addHabits}">+ Add Habit</button>
</div>
{/* 表格主体:每行 = 1 天,每列 = 1 习惯 */}
{dateArray.length > 0 && (
<div classname="habit-grid">
{/* 表头:Date + 习惯名 */}
<div classname="grid-row header">
<div classname="grid-cell date-header">Date</div>
{dailyHabits.map((habit, idx) => (
<div key="{`header-${idx}`}" classname="grid-cell habit-header">
{habit}
</div>
))}
</div>
{/* 数据行 */}
{dateArray.map((day) => (
<div key="{day}" classname="grid-row">
<div classname="grid-cell date-cell">{day}</div>
{dailyHabits.map((_, idx) => (
<div key="{`${day}-${idx}`}" classname="grid-cell input-cell">
<input type="text" maxlength="1" value="{records[day]?.[idx]" onchange="{(e)"> handleCellChange(day, idx, e.target.value.toUpperCase())}
placeholder="X"
/>
</div>
))}
</div>
))}
</div>
)}
{dateArray.length === 0 && (
<p classname="hint">? Select month & year, then click "Generate Calendar"</p>
)}
</div>
);
}
export default HabitTable;
✅ 关键改进说明:
-
状态解耦:用
records对象精确管理records[day][habitIndex],避免嵌套渲染混乱; - 动态列同步:新增习惯时自动为所有已有日期扩展一列空值;
-
受控输入:每个打卡单元格为受控组件,支持实时输入
'X'、清空或修改; -
健壮性增强:输入框添加
type="number"、min/max限制,防止非法月份/年份; -
语义化结构:采用 CSS Grid 或 Flex 布局(
.habit-grid,.grid-row)替代原始堆叠div,便于样式控制与响应式适配。
? 小贴士:若需持久化打卡数据,可将 records 序列化后存入 localStorage 或对接后端 API;进阶还可加入「全选当日」「批量清除」「统计完成率」等功能模块。
这样,你就能拥有一个真正可用、可扩展、符合 React 最佳实践的习惯追踪表格了。











