Files
schedule-planner/schedule-tool.jsx
2026-04-06 13:46:31 +00:00

450 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from "react";
const STORAGE_KEY = "mama-schedule-v1";
const WEEK_SCHEDULE = {
周一: {
fixed: [
{ time: "5:45", task: "起床 · 深度工作 ⭐", type: "work" },
{ time: "6:45", task: "孩子起床 · 做早饭", type: "family" },
{ time: "8:00", task: "孩子自己上学 · 继续工作黄金时段", type: "work" },
{ time: "12:00", task: "午饭", type: "life" },
{ time: "13:00", task: "下午工作 + 家务30分钟", type: "work" },
{ time: "15:30", task: "出门接孩子", type: "family" },
{ time: "16:40", task: "接孩子回家", type: "family" },
{ time: "17:30", task: "做晚饭", type: "life" },
{ time: "19:00", task: "陪练琴 / 作业", type: "family" },
{ time: "20:00", task: "陪读书", type: "family" },
{ time: "20:30", task: "孩子入睡 · 轻量工作 + 日记", type: "work" },
{ time: "22:00", task: "🌙 洗漱睡觉", type: "sleep" },
],
note: "最自由的一天,保护上午深度工作时间",
},
周二: {
fixed: [
{ time: "5:45", task: "起床 · 早晨个人时间 / 日记", type: "self" },
{ time: "6:45", task: "孩子起床 · 做早饭", type: "family" },
{ time: "7:40", task: "送孩子出门", type: "family" },
{ time: "8:30", task: "去朋友家工作 ⭐", type: "work" },
{ time: "15:30", task: "出门接孩子", type: "family" },
{ time: "16:40", task: "回家 · 做晚饭", type: "life" },
{ time: "19:00", task: "陪练琴 / 作业", type: "family" },
{ time: "20:00", task: "陪读书", type: "family" },
{ time: "20:30", task: "孩子入睡 · 轻量工作 + 回消息", type: "work" },
{ time: "22:00", task: "🌙 洗漱睡觉", type: "sleep" },
],
note: "朋友家是深度工作最佳环境,珍惜这段时间",
},
周三: {
fixed: [
{ time: "5:45", task: "起床 · 轻量工作 / 回消息", type: "work" },
{ time: "6:45", task: "孩子起床 · 做早饭", type: "family" },
{ time: "7:40", task: "送孩子 → 直接去健身 🏋️", type: "self" },
{ time: "10:30", task: "回家 · 洗澡", type: "self" },
{ time: "11:00", task: "深度工作 ⭐", type: "work" },
{ time: "12:30", task: "午饭", type: "life" },
{ time: "13:00", task: "继续工作", type: "work" },
{ time: "15:30", task: "出门接孩子", type: "family" },
{ time: "16:40", task: "回家", type: "family" },
{ time: "17:30", task: "做晚饭(简化)", type: "life" },
{ time: "19:00", task: "陪练琴 / 作业", type: "family" },
{ time: "20:00", task: "陪读书", type: "family" },
{ time: "20:30", task: "孩子入睡 · 只写日记,不工作", type: "self" },
{ time: "22:00", task: "🌙 洗漱睡觉", type: "sleep" },
],
note: "健身日晚上不安排工作,上午已补回来了",
},
周四: {
fixed: [
{ time: "5:45", task: "起床 · 深度工作 ⭐", type: "work" },
{ time: "6:45", task: "孩子起床 · 做早饭", type: "family" },
{ time: "7:40", task: "送孩子出门", type: "family" },
{ time: "8:00", task: "回家 · 继续深度工作", type: "work" },
{ time: "12:00", task: "午饭", type: "life" },
{ time: "13:00", task: "工作 + 家务30分钟", type: "work" },
{ time: "15:30", task: "出门接孩子", type: "family" },
{ time: "16:40", task: "接孩子回家", type: "family" },
{ time: "17:30", task: "做晚饭", type: "life" },
{ time: "19:00", task: "陪练琴 / 作业", type: "family" },
{ time: "20:00", task: "陪读书", type: "family" },
{ time: "20:30", task: "孩子入睡 · 轻量工作 + 日记", type: "work" },
{ time: "22:00", task: "🌙 洗漱睡觉", type: "sleep" },
],
note: "早晨是黄金深度工作时间,下午被接送切割",
},
周五: {
fixed: [
{ time: "5:45", task: "起床 · 轻量工作 / 回消息", type: "work" },
{ time: "6:45", task: "孩子起床 · 做早饭", type: "family" },
{ time: "7:40", task: "送孩子 → 直接去健身 🏋️", type: "self" },
{ time: "10:30", task: "回家 · 洗澡", type: "self" },
{ time: "11:00", task: "深度工作 ⭐", type: "work" },
{ time: "12:30", task: "午饭", type: "life" },
{ time: "13:00", task: "继续工作", type: "work" },
{ time: "15:30", task: "出门接孩子", type: "family" },
{ time: "16:40", task: "回家", type: "family" },
{ time: "17:30", task: "做晚饭(简化)", type: "life" },
{ time: "19:00", task: "陪练琴 / 作业", type: "family" },
{ time: "20:00", task: "陪读书", type: "family" },
{ time: "20:30", task: "孩子入睡 · 只写日记,不工作", type: "self" },
{ time: "22:00", task: "🌙 洗漱睡觉", type: "sleep" },
],
note: "周五健身日,晚上放松,迎接周末",
},
};
const TYPE_STYLE = {
work: { bg: "#e8f4fd", dot: "#3b82f6", label: "工作" },
family: { bg: "#fef3c7", dot: "#f59e0b", label: "家庭" },
self: { bg: "#f0fdf4", dot: "#22c55e", label: "自我" },
life: { bg: "#fdf4ff", dot: "#a855f7", label: "生活" },
sleep: { bg: "#f1f5f9", dot: "#64748b", label: "睡眠" },
};
const DAYS = ["周一", "周二", "周三", "周四", "周五"];
const WEEKS = ["第1-2周", "第3-4周", "第5-6周"];
const SLEEP_TARGETS = { "第1-2周": "23:00前", "第3-4周": "22:30前", "第5-6周": "22:00前" };
function loadData() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : {};
} catch { return {}; }
}
function saveData(data) {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } catch {}
}
export default function App() {
const [tab, setTab] = useState("today");
const [selectedDay, setSelectedDay] = useState(DAYS[new Date().getDay() - 1] || "周一");
const [data, setData] = useState(loadData);
const [newTask, setNewTask] = useState({ text: "", type: "brain" });
const [weekReview, setWeekReview] = useState({ sleep: "", wins: "", issues: "", next: "" });
const [currentWeek, setCurrentWeek] = useState("第1-2周");
const [showAddTask, setShowAddTask] = useState(false);
const todayKey = new Date().toISOString().slice(0, 10);
const tasks = data[todayKey]?.tasks || [];
const reviews = data.reviews || {};
function updateData(newData) {
setData(newData);
saveData(newData);
}
function addTask() {
if (!newTask.text.trim()) return;
const updated = {
...data,
[todayKey]: {
...data[todayKey],
tasks: [...tasks, { id: Date.now(), text: newTask.text, type: newTask.type, done: false }],
},
};
updateData(updated);
setNewTask({ text: "", type: "brain" });
setShowAddTask(false);
}
function toggleTask(id) {
const updated = {
...data,
[todayKey]: {
...data[todayKey],
tasks: tasks.map(t => t.id === id ? { ...t, done: !t.done } : t),
},
};
updateData(updated);
}
function deleteTask(id) {
const updated = {
...data,
[todayKey]: {
...data[todayKey],
tasks: tasks.filter(t => t.id !== id),
},
};
updateData(updated);
}
function saveReview() {
const weekKey = `${currentWeek}-${new Date().toISOString().slice(0, 7)}`;
const updated = {
...data,
reviews: { ...reviews, [weekKey]: { ...weekReview, date: new Date().toLocaleDateString("zh-CN") } },
};
updateData(updated);
setWeekReview({ sleep: "", wins: "", issues: "", next: "" });
alert("回顾已保存 ✓");
}
const brainTasks = tasks.filter(t => t.type === "brain");
const lightTasks = tasks.filter(t => t.type === "light");
return (
<div style={{ fontFamily: "'Noto Serif SC', 'Georgia', serif", minHeight: "100vh", background: "#faf8f5", color: "#2c2c2c" }}>
<style>{`
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;500;600&display=swap');
* { box-sizing: border-box; margin: 0; padding: 0; }
button { cursor: pointer; font-family: inherit; }
textarea, input { font-family: inherit; }
.tab-btn { background: none; border: none; padding: 10px 18px; font-size: 14px; color: #999; border-bottom: 2px solid transparent; transition: all 0.2s; }
.tab-btn.active { color: #2c2c2c; border-bottom-color: #2c2c2c; font-weight: 500; }
.tab-btn:hover { color: #2c2c2c; }
.day-btn { background: none; border: 1px solid #e0dbd4; border-radius: 20px; padding: 6px 14px; font-size: 13px; color: #666; transition: all 0.2s; }
.day-btn.active { background: #2c2c2c; color: #fff; border-color: #2c2c2c; }
.day-btn:hover:not(.active) { border-color: #999; color: #333; }
.task-card { background: #fff; border-radius: 12px; padding: 14px 16px; margin-bottom: 10px; border: 1px solid #ede8e1; display: flex; align-items: center; gap: 12px; transition: all 0.2s; }
.task-card:hover { border-color: #ccc; }
.task-card.done { opacity: 0.45; }
.check-btn { width: 22px; height: 22px; border-radius: 50%; border: 2px solid #d0c9bf; background: none; flex-shrink: 0; transition: all 0.2s; display: flex; align-items: center; justify-content: center; }
.check-btn.done { background: #2c2c2c; border-color: #2c2c2c; color: #fff; }
.check-btn:hover { border-color: #2c2c2c; }
.del-btn { background: none; border: none; color: #ccc; font-size: 16px; padding: 2px 6px; border-radius: 4px; margin-left: auto; flex-shrink: 0; }
.del-btn:hover { color: #e55; background: #fff0f0; }
.timeline-item { display: flex; gap: 14px; margin-bottom: 0; position: relative; }
.timeline-item:not(:last-child)::before { content: ''; position: absolute; left: 36px; top: 28px; bottom: -2px; width: 1px; background: #ede8e1; }
.add-btn { background: #2c2c2c; color: #fff; border: none; border-radius: 10px; padding: 10px 20px; font-size: 14px; display: flex; align-items: center; gap: 6px; }
.add-btn:hover { background: #444; }
.input-field { border: 1px solid #e0dbd4; border-radius: 10px; padding: 10px 14px; font-size: 14px; width: 100%; outline: none; background: #fff; }
.input-field:focus { border-color: #2c2c2c; }
.select-field { border: 1px solid #e0dbd4; border-radius: 10px; padding: 10px 14px; font-size: 14px; background: #fff; outline: none; }
.select-field:focus { border-color: #2c2c2c; }
.section-title { font-size: 13px; font-weight: 500; color: #888; letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: 12px; }
.review-input { border: 1px solid #e0dbd4; border-radius: 10px; padding: 12px 14px; font-size: 14px; width: 100%; resize: none; outline: none; background: #fff; min-height: 70px; line-height: 1.6; }
.review-input:focus { border-color: #2c2c2c; }
.week-badge { display: inline-block; background: #f0ebe3; border-radius: 6px; padding: 3px 10px; font-size: 12px; color: #666; }
.progress-bar { height: 6px; background: #ede8e1; border-radius: 3px; overflow: hidden; }
.progress-fill { height: 100%; background: #2c2c2c; border-radius: 3px; transition: width 0.5s; }
.empty-state { text-align: center; padding: 30px 20px; color: #bbb; font-size: 14px; line-height: 1.8; }
`}</style>
{/* Header */}
<div style={{ background: "#fff", borderBottom: "1px solid #ede8e1", padding: "20px 20px 0" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 16 }}>
<div>
<div style={{ fontSize: 11, color: "#aaa", letterSpacing: "0.1em", marginBottom: 4 }}>我的时间管理</div>
<h1 style={{ fontSize: 22, fontWeight: 600, letterSpacing: "-0.02em" }}>早睡计划</h1>
</div>
<div style={{ textAlign: "right" }}>
<div style={{ fontSize: 11, color: "#aaa", marginBottom: 4 }}>目标睡眠时间</div>
<div style={{ fontSize: 15, fontWeight: 500, color: "#2c7a2c" }}>{SLEEP_TARGETS[currentWeek]}</div>
<select value={currentWeek} onChange={e => setCurrentWeek(e.target.value)} style={{ marginTop: 4, border: "1px solid #e0dbd4", borderRadius: 6, padding: "3px 8px", fontSize: 11, color: "#666", background: "#fff", cursor: "pointer" }}>
{WEEKS.map(w => <option key={w}>{w}</option>)}
</select>
</div>
</div>
{/* 6周进度 */}
<div style={{ marginBottom: 16 }}>
<div style={{ display: "flex", justifyContent: "space-between", fontSize: 11, color: "#aaa", marginBottom: 6 }}>
<span>6周养成计划</span>
<span>{currentWeek}</span>
</div>
<div className="progress-bar">
<div className="progress-fill" style={{ width: currentWeek === "第1-2周" ? "33%" : currentWeek === "第3-4周" ? "66%" : "100%" }} />
</div>
</div>
{/* Tabs */}
<div style={{ display: "flex", gap: 4 }}>
{[["today", "今日任务"], ["schedule", "日程表"], ["review", "周回顾"]].map(([key, label]) => (
<button key={key} className={`tab-btn ${tab === key ? "active" : ""}`} onClick={() => setTab(key)}>{label}</button>
))}
</div>
</div>
<div style={{ padding: "20px 20px 40px", maxWidth: 600, margin: "0 auto" }}>
{/* ===== 今日任务 ===== */}
{tab === "today" && (
<div>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 20 }}>
<div>
<div style={{ fontSize: 16, fontWeight: 500 }}>{new Date().toLocaleDateString("zh-CN", { month: "long", day: "numeric", weekday: "long" })}</div>
<div style={{ fontSize: 12, color: "#aaa", marginTop: 2 }}>记录今天要处理的事情</div>
</div>
<button className="add-btn" onClick={() => setShowAddTask(!showAddTask)}>
<span style={{ fontSize: 18, lineHeight: 1 }}>+</span>
</button>
</div>
{showAddTask && (
<div style={{ background: "#fff", border: "1px solid #ede8e1", borderRadius: 14, padding: 16, marginBottom: 20 }}>
<input
className="input-field"
placeholder="任务内容..."
value={newTask.text}
onChange={e => setNewTask({ ...newTask, text: e.target.value })}
onKeyDown={e => e.key === "Enter" && addTask()}
style={{ marginBottom: 10 }}
autoFocus
/>
<div style={{ display: "flex", gap: 10, alignItems: "center" }}>
<select className="select-field" value={newTask.type} onChange={e => setNewTask({ ...newTask, type: e.target.value })}>
<option value="brain">🧠 需要脑力</option>
<option value="light"> 不需要思考</option>
</select>
<button onClick={addTask} style={{ background: "#2c2c2c", color: "#fff", border: "none", borderRadius: 10, padding: "10px 18px", fontSize: 14, whiteSpace: "nowrap" }}>确认</button>
<button onClick={() => setShowAddTask(false)} style={{ background: "none", border: "1px solid #e0dbd4", borderRadius: 10, padding: "10px 14px", fontSize: 14, color: "#666" }}>取消</button>
</div>
</div>
)}
{/* 脑力任务 */}
<div style={{ marginBottom: 24 }}>
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 12 }}>
<span style={{ fontSize: 18 }}>🧠</span>
<div>
<div style={{ fontSize: 14, fontWeight: 500 }}>需要脑力的任务</div>
<div style={{ fontSize: 11, color: "#aaa" }}>安排在早晨 5:45 或上午时段</div>
</div>
</div>
{brainTasks.length === 0 ? (
<div className="empty-state">还没有脑力任务<br />点击添加任务 选择需要脑力</div>
) : brainTasks.map(t => (
<div key={t.id} className={`task-card ${t.done ? "done" : ""}`}>
<button className={`check-btn ${t.done ? "done" : ""}`} onClick={() => toggleTask(t.id)}>
{t.done && <span style={{ fontSize: 11 }}></span>}
</button>
<span style={{ fontSize: 14, flex: 1, textDecoration: t.done ? "line-through" : "none" }}>{t.text}</span>
<button className="del-btn" onClick={() => deleteTask(t.id)}>×</button>
</div>
))}
</div>
{/* 轻量任务 */}
<div>
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 12 }}>
<span style={{ fontSize: 18 }}></span>
<div>
<div style={{ fontSize: 14, fontWeight: 500 }}>不需要思考的任务</div>
<div style={{ fontSize: 11, color: "#aaa" }}>留到晚上 20:30 后处理</div>
</div>
</div>
{lightTasks.length === 0 ? (
<div className="empty-state">还没有轻量任务<br />点击添加任务 选择不需要思考</div>
) : lightTasks.map(t => (
<div key={t.id} className={`task-card ${t.done ? "done" : ""}`}>
<button className={`check-btn ${t.done ? "done" : ""}`} onClick={() => toggleTask(t.id)}>
{t.done && <span style={{ fontSize: 11 }}></span>}
</button>
<span style={{ fontSize: 14, flex: 1, textDecoration: t.done ? "line-through" : "none" }}>{t.text}</span>
<button className="del-btn" onClick={() => deleteTask(t.id)}>×</button>
</div>
))}
</div>
{tasks.length > 0 && (
<div style={{ marginTop: 20, background: "#fff", borderRadius: 12, padding: "12px 16px", border: "1px solid #ede8e1" }}>
<div style={{ display: "flex", justifyContent: "space-between", fontSize: 13, color: "#666" }}>
<span>完成进度</span>
<span>{tasks.filter(t => t.done).length} / {tasks.length} </span>
</div>
<div className="progress-bar" style={{ marginTop: 8 }}>
<div className="progress-fill" style={{ width: `${tasks.length ? (tasks.filter(t => t.done).length / tasks.length * 100) : 0}%` }} />
</div>
</div>
)}
</div>
)}
{/* ===== 日程表 ===== */}
{tab === "schedule" && (
<div>
<div style={{ display: "flex", gap: 8, marginBottom: 20, flexWrap: "wrap" }}>
{DAYS.map(d => (
<button key={d} className={`day-btn ${selectedDay === d ? "active" : ""}`} onClick={() => setSelectedDay(d)}>{d}</button>
))}
</div>
{WEEK_SCHEDULE[selectedDay] && (
<>
<div style={{ background: "#fff8e7", border: "1px solid #f5e6bb", borderRadius: 10, padding: "10px 14px", marginBottom: 20, fontSize: 13, color: "#8a6c00" }}>
💡 {WEEK_SCHEDULE[selectedDay].note}
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
{WEEK_SCHEDULE[selectedDay].fixed.map((item, i) => {
const style = TYPE_STYLE[item.type];
return (
<div key={i} className="timeline-item" style={{ padding: "4px 0" }}>
<div style={{ width: 58, flexShrink: 0, fontSize: 12, color: "#999", paddingTop: 8, textAlign: "right", fontVariantNumeric: "tabular-nums" }}>{item.time}</div>
<div style={{ width: 14, flexShrink: 0, display: "flex", flexDirection: "column", alignItems: "center", paddingTop: 10 }}>
<div style={{ width: 8, height: 8, borderRadius: "50%", background: style.dot, flexShrink: 0 }} />
</div>
<div style={{ flex: 1, background: item.type === "sleep" ? "#f1f5f9" : "#fff", border: `1px solid ${item.type === "sleep" ? "#cbd5e1" : "#ede8e1"}`, borderRadius: 10, padding: "8px 12px", marginBottom: 6 }}>
<span style={{ fontSize: 13.5, fontWeight: item.type === "sleep" ? 500 : 400 }}>{item.task}</span>
<span style={{ marginLeft: 8, fontSize: 10, color: style.dot, background: style.bg, padding: "2px 6px", borderRadius: 4 }}>{style.label}</span>
</div>
</div>
);
})}
</div>
</>
)}
</div>
)}
{/* ===== 周回顾 ===== */}
{tab === "review" && (
<div>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 20 }}>
<div>
<div style={{ fontSize: 16, fontWeight: 500 }}>每周回顾</div>
<div style={{ fontSize: 12, color: "#aaa", marginTop: 2 }}>每周花10分钟复盘调整下周计划</div>
</div>
<span className="week-badge">{currentWeek}</span>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
{[
{ key: "sleep", label: "😴 这周睡眠情况", placeholder: "实际几点睡的?有没有改善?" },
{ key: "wins", label: "✨ 做得好的地方", placeholder: "哪些时间安排运转良好?" },
{ key: "issues", label: "🔧 遇到的问题", placeholder: "哪里卡住了?什么原因?" },
{ key: "next", label: "🎯 下周调整计划", placeholder: "下周想改变什么?具体怎么做?" },
].map(({ key, label, placeholder }) => (
<div key={key}>
<div style={{ fontSize: 14, fontWeight: 500, marginBottom: 8 }}>{label}</div>
<textarea
className="review-input"
placeholder={placeholder}
value={weekReview[key]}
onChange={e => setWeekReview({ ...weekReview, [key]: e.target.value })}
/>
</div>
))}
<button onClick={saveReview} style={{ background: "#2c2c2c", color: "#fff", border: "none", borderRadius: 12, padding: "14px", fontSize: 15, fontFamily: "inherit", marginTop: 4 }}>
保存本周回顾
</button>
</div>
{/* 历史回顾 */}
{Object.keys(reviews).length > 0 && (
<div style={{ marginTop: 32 }}>
<div className="section-title">历史回顾</div>
{Object.entries(reviews).reverse().map(([key, r]) => (
<div key={key} style={{ background: "#fff", border: "1px solid #ede8e1", borderRadius: 12, padding: 16, marginBottom: 12 }}>
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 10 }}>
<span className="week-badge">{key.split("-")[0]}{key.split("-")[1] ? "-" + key.split("-")[1] : ""}</span>
<span style={{ fontSize: 12, color: "#aaa" }}>{r.date}</span>
</div>
{r.sleep && <div style={{ fontSize: 13, marginBottom: 6 }}><span style={{ color: "#aaa" }}>睡眠</span>{r.sleep}</div>}
{r.wins && <div style={{ fontSize: 13, marginBottom: 6 }}><span style={{ color: "#aaa" }}>亮点</span>{r.wins}</div>}
{r.issues && <div style={{ fontSize: 13, marginBottom: 6 }}><span style={{ color: "#aaa" }}>问题</span>{r.issues}</div>}
{r.next && <div style={{ fontSize: 13 }}><span style={{ color: "#aaa" }}>下周</span>{r.next}</div>}
</div>
))}
</div>
)}
</div>
)}
</div>
</div>
);
}