你是否也曾在新立Flag时信誓旦旦,却在几天后不了了之?你是否也厌倦了冰冷、枯燥的待办清单,渴望一份能真正激发内在动力的自律伙伴?
今天,我们为你带来了一款全新上架的小组件,开源自律日历 —— 「熊二的自律日历」。它不只是日历,更是一个集任务管理、目标追踪、即时激励于一体的个人成长可视化生态系统。它的开源属性,意味着你可以自由探索、修改、分享,让它真正属于你。
现在,就让我们一一揭开它的“爆火”功能面纱:
🌟 一、核心:可视化任务与成就系统
- 智能日历,一目了然:
- 不仅是日历,更是你的“坚持地图”。已完成的天数会以醒目的绿色“✓”标记,今日日期高亮显示。
- 点击任意日期,立即知晓距离今天还有几天/已经过去几天,让时间感触手可及。
- 新品亮点:月份导航与“回到今天”按钮,让你随时聚焦当下,回顾过去,展望未来。
- 灵活的任务规划器:
- 自定义字段:不只是“任务”和“预计时间”,你可以添加任何你需要的字段(如优先级、类别、能量消耗等),打造专属任务模板。
- 便捷录入:支持逐条添加,更支持批量导入文本,从笔记或清单中一键搬运,效率拉满。
- 即时反馈:勾选任务,完成效果立即生效(划线),并自动统计当日任务完成情况。
- 一键完成:当所有任务都搞定时,“一键全部完成”按钮让你享受瞬间清空的快感。
- 数据驱动的激励统计:
- 本月完成天数、历史最长连续天数、当前连续天数——三大数据直观展示你的自律曲线。
- 新品亮点:这些数字不只是冷冰冰的统计,它们是你一步步成长的勋章,激励你保持“连胜”,挑战新的记录。
🎯 二、升级:跨日目标与里程碑管理
- 长期目标追踪器:
- 设定跨天、跨月、甚至跨年的目标,指定精确的开始与结束时间。
- 目标卡片实时显示动态倒计时(精确到秒)和完成进度条,让遥远的目标变得具体且紧迫。
- 支持多个目标并行,让你的成长不再单一。
- 目标到达仪式感:
- 当设定目标时间一到,系统会弹出完成确认弹窗,并伴随你自定义的庆祝动画或视频!
- 你可以选择:播放小卡片飘落动画+音乐,或直接全屏播放你提前准备好的成功庆祝视频。
- 根据你的反馈,将目标标记为“完成”或“未完成”,形成完整的目标生命周期闭环。
✨ 三、创新:沉浸式即时激励体验
- 每日庆祝功能:
- 完成今日所有任务后,点击“完成今日任务!🎉”按钮,即可触发庆祝环节!
- 你可以自定义庆祝方式:播放一段振奋人心的音频,或播放一个搞笑/感人的视频,并随机显示你精心准备的语录。让坚持的终点充满快乐。
- 疲惫时的能量加油站:
- 全新“累了就来这里”按钮:在你意志力薄弱、想要放弃时,点击它!
- 同样支持自定义:选择播放治愈系小卡片飘落动画+音乐,或播放一段励志视频。
- 新品亮点:这个功能就像一个懂你的朋友,在你最需要的时候,给你一个暖心的拥抱和继续前行的力量。
⏰ 四、贴心:全方位时间与提醒守护
- 智能闹钟系统:
- 支持设置精确到秒的日期与时间闹钟。
- 可添加备注,如“该学习啦!”、“喝水时间”。
- 提醒方式可选:醒目的弹窗+音频,或直接播放你指定的视频(如健身教程、冥想引导)。
- 实时网络同步时钟:
- 内置大字体实时时钟,显示当前日期、星期。
- 新品亮点:支持“🔄”按钮一键同步网络时间,确保你的日程安排精准无误。
🔒 五、保障:隐私安全与开源自由
- 完全本地化存储:所有数据(任务、目标、统计、媒体文件)均存储于本地浏览器(IndexedDB),无需登录,隐私绝对安全。
- 开源免费:源代码开放!你可以:
- 学习其实现逻辑,甚至贡献代码。
- 根据个人喜好修改界面颜色、动画效果。
- 搭建属于自己的、独一无二的自励工具。
- 恢复出厂设置:提供谨慎的“清除所有数据”功能,给你重新开始的勇气和机会。
总结一下,「熊二的自律日历」不仅是一个强大的任务管理工具,更是一个充满人文关怀、能提供即时情绪价值、并且完全受你掌控的成长伙伴。
它的新品光环,源于其将自律过程中的“记录”、“追踪”、“坚持”与“奖励”、“鼓励”、“庆祝”无缝融合的创新设计。它的开源属性,则赋予了它无限的潜力和生命力,等待着社区共同丰富它、完善它。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>加油!</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: "Microsoft YaHei", "Segoe UI", Arial, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #e4e8f0 100%);
margin: 0;
padding: 20px;
min-height: 100vh;
display: flex;
gap: 30px;
flex-wrap: wrap;
justify-content: center;
color: #333;
}
/* 整体布局 */
.main-content {
flex: 1;
min-width: 700px;
max-width: 900px;
position: relative;
}
.sidebar-left {
width: 380px;
background: white;
border-radius: 20px;
padding: 25px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
height: fit-content;
}
.sidebar-right {
width: 380px;
background: white;
border-radius: 20px;
padding: 25px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
height: fit-content;
}
@media (max-width: 1600px) {
body {
flex-direction: column;
align-items: center;
}
.main-content,
.sidebar-left,
.sidebar-right {
width: 95%;
max-width: 900px;
min-width: unset;
}
}
h1 {
text-align: center;
padding-left: 240px;
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-size: 32px;
font-weight: 700;
}
.today-info {
text-align: center;
font-size: 18px;
margin-bottom: 25px;
color: #7f8c8d;
font-weight: 500;
}
/* ===== 功能按钮布局 - 两行排列 ===== */
.daily-task-btn, .long-term-btn, .encourage-settings-btn, .settings-btn,
.alarm-btn, .reset-btn {
position: absolute;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
width: 50px;
height: 50px;
border-radius: 50%;
font-size: 24px;
cursor: pointer;
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
/* 第一行按钮 */
.daily-task-btn { left: 0; top: 0; }
.long-term-btn { left: 60px; top: 0; }
.encourage-settings-btn { left: 120px; top: 0; }
.settings-btn { left: 180px; top: 0; }
/* 第二行按钮(在第一行下方) */
.alarm-btn { left: 0; top: 60px; }
.reset-btn { left: 60px; top: 60px; }
.daily-task-btn:hover, .long-term-btn:hover, .encourage-settings-btn:hover,
.settings-btn:hover, .alarm-btn:hover, .reset-btn:hover {
transform: translateY(-5px) scale(1.1);
box-shadow: 0 12px 30px rgba(102, 126, 234, 0.6);
}
/* 导航栏 */
.nav {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 25px;
gap: 20px;
}
.nav button {
background: linear-gradient(135deg, #0066ff, #00a8ff);
color: white;
border: none;
padding: 12px 24px;
border-radius: 12px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
box-shadow: 0 6px 15px rgba(0, 102, 255, 0.2);
transition: all 0.3s ease;
min-width: 120px;
}
.nav button:hover {
transform: translateY(-3px);
box-shadow: 0 10px 25px rgba(0, 102, 255, 0.3);
}
#today-btn {
background: white;
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.1);
color: #2c3e50;
border: 2px solid #e0e0e0;
}
#today-btn:hover {
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
border-color: #ccc;
}
.nav span {
font-size: 28px;
font-weight: bold;
min-width: 200px;
text-align: center;
color: #2c3e50;
}
/* 庆祝按钮 */
.celebrate-section {
text-align: center;
margin: 25px 0;
}
#celebrate-btn {
background: linear-gradient(45deg, #ff6b6b, #ffa500);
color: white;
border: none;
padding: 20px 50px;
border-radius: 50px;
font-size: 24px;
font-weight: bold;
cursor: pointer;
box-shadow: 0 12px 35px rgba(255, 107, 107, 0.4);
transition: all 0.3s ease;
letter-spacing: 1px;
}
#celebrate-btn:hover:not(:disabled) {
transform: translateY(-5px) scale(1.05);
box-shadow: 0 18px 45px rgba(255, 107, 107, 0.5);
}
#celebrate-btn:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* 日历区域 */
.weekday-header {
display: grid;
grid-template-columns: repeat(7, 1fr);
background: linear-gradient(135deg, #0066ff, #00c8ff);
color: white;
font-weight: bold;
border-radius: 16px 16px 0 0;
overflow: hidden;
box-shadow: 0 6px 20px rgba(0, 102, 255, 0.2);
}
.weekday-header div {
padding: 18px 10px;
text-align: center;
font-size: 16px;
letter-spacing: 1px;
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 12px;
padding: 20px;
background: #fff;
border-radius: 0 0 16px 16px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08);
}
.day {
height: 140px;
padding: 15px;
box-sizing: border-box;
background: #ffffff;
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
justify-content: space-between;
font-size: 18px;
color: #333;
cursor: pointer;
transition: all 0.3s ease;
user-select: none;
position: relative;
border: 2px solid transparent;
}
.day:hover {
transform: translateY(-5px);
box-shadow: 0 12px 25px rgba(0, 0, 0, 0.15);
border-color: #e0e0e0;
}
.day.other-month {
background: #f8f9fa;
color: #bbb;
}
.day.today {
background: linear-gradient(135deg, #0066ff, #00c8ff) !important;
color: white !important;
font-weight: bold;
box-shadow: 0 8px 25px rgba(0, 102, 255, 0.3);
border-color: #0066ff;
}
.day.selected {
background: linear-gradient(135deg, #ff8800, #ffbb33) !important;
color: white !important;
font-weight: bold;
box-shadow: 0 8px 25px rgba(255, 136, 0, 0.3);
border-color: #ff8800;
}
.day-number {
align-self: flex-end;
font-weight: bold;
font-size: 22px;
}
.completed-mark {
position: absolute;
top: 12px;
right: 12px;
font-size: 30px;
color: #4caf50;
text-shadow: 0 2px 8px rgba(76, 175, 80, 0.3);
}
/* 日期信息 */
.date-info {
text-align: center;
font-size: 20px;
margin-top: 30px;
padding: 25px;
background: white;
border-radius: 16px;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08);
color: #555;
font-weight: 500;
}
/* 左侧今日任务 */
.today-tasks {
margin-bottom: 30px;
}
.today-tasks h3 {
text-align: center;
margin-bottom: 25px;
color: #2c3e50;
font-size: 24px;
font-weight: 700;
position: relative;
padding-bottom: 10px;
}
.today-tasks h3::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60px;
height: 4px;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 2px;
}
.today-task-table {
width: 100%;
border-collapse: collapse;
background: #f9f9f9;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.05);
}
.today-task-table th, .today-task-table td {
padding: 14px 16px;
text-align: left;
border-bottom: 1px solid #eee;
}
.today-task-table th {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
font-weight: bold;
font-size: 16px;
}
.today-task-table tbody tr {
transition: all 0.3s ease;
}
.today-task-table tbody tr:hover {
background-color: #f0f2ff;
}
.today-task-row.completed {
opacity: 0.7;
text-decoration: line-through;
background: #f0f0f0;
}
.today-task-table td:last-child {
text-align: center;
width: 80px;
}
/* 新增:全部完成按钮样式 */
.all-complete-display-btn {
margin-top: 15px;
text-align: center;
}
.all-complete-display-btn button {
background: linear-gradient(135deg, #ffc107, #e0a800);
color: white;
border: none;
padding: 12px 30px;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 6px 15px rgba(255, 193, 7, 0.3);
transition: all 0.3s ease;
}
.all-complete-display-btn button:hover {
transform: translateY(-3px);
box-shadow: 0 10px 25px rgba(255, 193, 7, 0.4);
}
/* 右侧统计 */
.stats {
margin-bottom: 35px;
text-align: center;
padding: 30px 25px;
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
border-radius: 16px;
color: #333;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08);
}
.stats h3 {
margin: 0 0 25px 0;
font-size: 26px;
color: #2c3e50;
font-weight: 700;
}
.stats p {
margin: 18px 0;
font-size: 20px;
font-weight: 500;
}
.stats strong {
font-size: 28px;
color: #0066ff;
margin: 0 5px;
}
/* 跨日目标 */
.long-term-goals h3 {
text-align: center;
margin-bottom: 25px;
color: #2c3e50;
font-size: 24px;
font-weight: 700;
position: relative;
padding-bottom: 10px;
}
.long-term-goals h3::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60px;
height: 4px;
background: linear-gradient(90deg, #0066ff, #00c8ff);
border-radius: 2px;
}
.goal-item {
background: linear-gradient(135deg, #f9f9f9, #f0f0f0);
padding: 20px;
border-radius: 16px;
margin-bottom: 18px;
border-left: 6px solid #0066ff;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.goal-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(0, 102, 255, 0.05), rgba(0, 200, 255, 0.05));
z-index: 1;
opacity: 0;
transition: opacity 0.3s ease;
}
.goal-item:hover {
transform: translateY(-5px);
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.12);
}
.goal-item:hover::before {
opacity: 1;
}
.goal-title {
font-weight: bold;
font-size: 20px;
margin-bottom: 10px;
color: #2c3e50;
position: relative;
z-index: 2;
}
.goal-dates {
font-size: 16px;
color: #666;
margin-bottom: 15px;
position: relative;
z-index: 2;
}
.countdown {
font-size: 24px;
font-weight: bold;
color: #ff6b6b;
margin-bottom: 15px;
position: relative;
z-index: 2;
}
.progress-bar {
height: 14px;
background: #e0e0e0;
border-radius: 7px;
overflow: hidden;
position: relative;
z-index: 2;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #0066ff, #00c8ff);
border-radius: 7px;
transition: width 0.8s ease;
}
/* 我不想干了按钮 - 红色 */
.give-up-section {
margin-top: 25px;
text-align: center;
}
.give-up-btn {
background: linear-gradient(135deg, #e74c3c, #c0392b);
color: white;
border: none;
padding: 14px 28px;
border-radius: 25px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 6px 20px rgba(231, 76, 60, 0.3);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.give-up-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
transition: left 0.5s ease;
}
.give-up-btn:hover {
transform: translateY(-3px);
box-shadow: 0 10px 30px rgba(231, 76, 60, 0.4);
background: linear-gradient(135deg, #c0392b, #a93226);
}
.give-up-btn:hover::before {
left: 100%;
}
/* 鼓励模态框 */
.encourage-modal {
display: none;
position: fixed;
z-index: 2000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.85);
align-items: center;
justify-content: center;
backdrop-filter: blur(8px);
}
.encourage-content {
background: linear-gradient(135deg, #e74c3c, #c0392b);
color: white;
padding: 60px 80px;
border-radius: 30px;
text-align: center;
box-shadow: 0 25px 70px rgba(0, 0, 0, 0.5);
max-width: 700px;
animation: shakeIn 0.6s ease-out;
position: relative;
overflow: hidden;
}
.encourage-content::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 1px, transparent 1px);
background-size: 25px 25px;
z-index: 1;
animation: rotate 30s linear infinite;
}
@keyframes shakeIn {
0% { transform: scale(0.5) rotate(-5deg); opacity: 0; }
50% { transform: scale(1.1) rotate(3deg); }
70% { transform: scale(0.95) rotate(-2deg); }
100% { transform: scale(1) rotate(0); opacity: 1; }
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.encourage-content h2 {
font-size: 42px;
margin-bottom: 30px;
text-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
position: relative;
z-index: 2;
}
.encourage-content h2::before {
content: '';
display: block;
font-size: 60px;
margin-bottom: 15px;
}
.encourage-quote {
font-size: 26px;
line-height: 1.8;
margin: 30px 0;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
position: relative;
z-index: 2;
font-weight: 500;
}
.encourage-close {
margin-top: 40px;
padding: 16px 40px;
background: rgba(255, 255, 255, 0.2);
border: 3px solid white;
color: white;
border-radius: 50px;
cursor: pointer;
font-size: 20px;
font-weight: bold;
transition: all 0.3s ease;
position: relative;
z-index: 2;
letter-spacing: 1px;
}
.encourage-close:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.08);
box-shadow: 0 8px 25px rgba(255, 255, 255, 0.2);
}
/* 庆祝模态框 */
.celebrate-modal {
display: none;
position: fixed;
z-index: 2000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.85);
align-items: center;
justify-content: center;
backdrop-filter: blur(5px);
}
.celebrate-content {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
padding: 70px 90px;
border-radius: 30px;
text-align: center;
box-shadow: 0 25px 70px rgba(0, 0, 0, 0.5);
max-width: 650px;
animation: zoomIn 0.8s ease-out;
position: relative;
overflow: hidden;
}
.celebrate-content::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 1px, transparent 1px);
background-size: 30px 30px;
z-index: 1;
animation: sparkle 20s linear infinite;
}
.celebrate-content h2 {
font-size: 52px;
margin-bottom: 40px;
text-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
position: relative;
z-index: 2;
}
.celebrate-quote {
font-size: 30px;
font-style: italic;
line-height: 1.6;
margin: 40px 0;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
position: relative;
z-index: 2;
}
.celebrate-close {
margin-top: 50px;
padding: 18px 45px;
background: rgba(255, 255, 255, 0.2);
border: 3px solid white;
color: white;
border-radius: 50px;
cursor: pointer;
font-size: 22px;
font-weight: bold;
transition: all 0.3s ease;
position: relative;
z-index: 2;
letter-spacing: 1px;
}
.celebrate-close:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.08);
box-shadow: 0 8px 25px rgba(255, 255, 255, 0.2);
}
@keyframes zoomIn {
from {
transform: scale(0.3);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
@keyframes sparkle {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* 模态框通用样式 */
.modal, .daily-task-modal, .settings-modal, .long-term-modal, .encourage-settings-modal {
display: none;
position: fixed;
z-index: 1500;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
align-items: center;
justify-content: center;
backdrop-filter: blur(5px);
}
.modal-content {
background: white;
padding: 45px;
border-radius: 24px;
width: 850px;
max-width: 95%;
max-height: 85vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25);
animation: modalAppear 0.4s ease-out;
}
@keyframes modalAppear {
from {
opacity: 0;
transform: translateY(-30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
font-size: 32px;
margin-bottom: 35px;
text-align: center;
font-weight: bold;
color: #2c3e50;
position: relative;
padding-bottom: 15px;
}
.modal-header::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 80px;
height: 5px;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 3px;
}
/* 按钮样式 */
.add-field-btn, .add-row-btn {
background: linear-gradient(135deg, #17a2b8, #138496);
color: white;
border: none;
padding: 14px 24px;
border-radius: 12px;
cursor: pointer;
margin-right: 12px;
margin-bottom: 20px;
font-size: 16px;
font-weight: 600;
box-shadow: 0 6px 15px rgba(23, 162, 184, 0.3);
transition: all 0.3s ease;
}
.add-field-btn:hover, .add-row-btn:hover {
transform: translateY(-3px);
box-shadow: 0 10px 25px rgba(23, 162, 184, 0.4);
}
.delete-field {
background: none;
border: none;
color: #ff4444;
cursor: pointer;
font-size: 22px;
margin-left: 10px;
visibility: hidden;
transition: all 0.3s;
font-weight: bold;
}
th:hover .delete-field {
visibility: visible;
}
.modal-buttons {
display: flex;
justify-content: flex-end;
margin-top: 40px;
gap: 20px;
}
.modal-buttons button {
padding: 14px 32px;
border: none;
border-radius: 12px;
cursor: pointer;
font-size: 18px;
font-weight: 600;
transition: all 0.3s ease;
min-width: 140px;
}
#daily-task-save, #long-term-save, #settings-save, #encourage-settings-save {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3);
}
#daily-task-cancel, #long-term-cancel, #settings-cancel, #encourage-settings-cancel {
background: #e0e0e0;
color: #666;
}
#daily-task-save:hover, #long-term-save:hover, #settings-save:hover, #encourage-settings-save:hover {
transform: translateY(-3px);
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
}
#daily-task-cancel:hover, #long-term-cancel:hover, #settings-cancel:hover, #encourage-settings-cancel:hover {
background: #d0d0d0;
transform: translateY(-2px);
}
/* 表格样式 */
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
padding: 14px 16px;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
background-color: #f8f9fa;
font-weight: bold;
color: #444;
}
tbody tr:hover {
background-color: #f5f7ff;
}
input[type="text"], input[type="date"], textarea {
width: 100%;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 16px;
transition: all 0.3s;
}
input[type="text"]:focus, input[type="date"]:focus, textarea:focus {
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
outline: none;
}
textarea {
resize: vertical;
min-height: 150px;
}
.form-group {
margin-bottom: 25px;
}
.form-group label {
display: block;
margin-bottom: 10px;
font-weight: 600;
color: #444;
font-size: 17px;
}
/* 自定义语录 */
#quotes-list, #encourage-quotes-list {
margin-top: 15px;
}
.quote-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: #f8f9fa;
border-radius: 10px;
margin-bottom: 10px;
border-left: 4px solid #667eea;
}
.quote-delete {
background: none;
border: none;
color: #ff4444;
cursor: pointer;
font-size: 20px;
font-weight: bold;
padding: 0 8px;
}
/* 任务完成复选框 */
input[type="checkbox"] {
width: 22px;
height: 22px;
cursor: pointer;
accent-color: #4caf50;
}
/* 空状态提示 */
.empty-state {
text-align: center;
color: #999;
padding: 40px 20px;
font-size: 17px;
line-height: 1.6;
}
/* 本地文件选择样式 - 优化版 */
.file-input-wrapper {
position: relative;
display: inline-block;
width: 100%;
}
.file-input {
position: absolute;
opacity: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
.file-input-label {
display: block;
width: 100%;
padding: 16px 20px;
border: 2px dashed #667eea;
border-radius: 12px;
font-size: 16px;
background: linear-gradient(135deg, #f5f7ff, #fff);
cursor: pointer;
transition: all 0.3s;
text-align: center;
color: #667eea;
font-weight: 500;
}
.file-input-label:hover {
border-color: #764ba2;
background: linear-gradient(135deg, #f0f2ff, #f8f5ff);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.15);
}
.file-name {
margin-top: 12px;
font-size: 14px;
color: #666;
padding: 12px 16px;
background: linear-gradient(135deg, #e8f5e9, #f1f8e9);
border-radius: 10px;
border-left: 4px solid #4caf50;
display: flex;
justify-content: space-between;
align-items: center;
}
.file-name .file-size {
color: #888;
font-size: 13px;
}
.file-name .delete-file-btn {
background: none;
border: none;
color: #ff4444;
cursor: pointer;
font-size: 18px;
padding: 0 5px;
transition: transform 0.2s;
}
.file-name .delete-file-btn:hover {
transform: scale(1.2);
}
/* 上传进度条 */
.upload-progress {
margin-top: 10px;
display: none;
}
.upload-progress-bar {
height: 8px;
background: #e0e0e0;
border-radius: 4px;
overflow: hidden;
}
.upload-progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 4px;
width: 0%;
transition: width 0.3s ease;
}
.upload-progress-text {
text-align: center;
font-size: 13px;
color: #666;
margin-top: 5px;
}
/* 文件大小提示 */
.file-size-hint {
font-size: 13px;
color: #888;
margin-top: 8px;
display: flex;
align-items: center;
gap: 5px;
}
.file-size-hint .hint-icon {
color: #17a2b8;
}
/* ===== 实时时钟样式 ===== */
.realtime-clock {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
padding: 20px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 16px;
margin-bottom: 25px;
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
color: white;
}
.clock-icon {
font-size: 36px;
animation: clockPulse 1s ease-in-out infinite;
}
@keyframes clockPulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
.clock-display {
text-align: center;
}
.clock-time {
font-size: 32px;
font-weight: bold;
font-family: 'Courier New', monospace;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}
.clock-date {
font-size: 14px;
opacity: 0.9;
margin-top: 5px;
}
.clock-refresh {
background: rgba(255, 255, 255, 0.2);
border: none;
width: 40px;
height: 40px;
border-radius: 50%;
font-size: 20px;
cursor: pointer;
transition: all 0.3s ease;
}
.clock-refresh:hover {
background: rgba(255, 255, 255, 0.3);
transform: rotate(180deg);
}
/* ===== 署名样式 ===== */
.signature {
text-align: center;
margin-top: 20px;
padding: 15px;
font-size: 16px;
color: #888;
font-style: italic;
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
}
/* ===== 闹钟模态框样式 ===== */
.alarm-modal {
display: none;
position: fixed;
z-index: 1500;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
align-items: center;
justify-content: center;
backdrop-filter: blur(5px);
}
.alarm-list {
margin-top: 20px;
max-height: 300px;
overflow-y: auto;
}
.alarm-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
border-radius: 12px;
margin-bottom: 12px;
border-left: 5px solid #f39c12;
transition: all 0.3s ease;
}
.alarm-item:hover {
transform: translateX(5px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.alarm-item-info {
flex: 1;
}
.alarm-item-time {
font-size: 18px;
font-weight: bold;
color: #2c3e50;
margin-bottom: 5px;
}
.alarm-item-note {
font-size: 14px;
color: #666;
}
.alarm-item-delete {
background: none;
border: none;
color: #e74c3c;
font-size: 22px;
cursor: pointer;
padding: 5px 10px;
transition: transform 0.2s;
}
.alarm-item-delete:hover {
transform: scale(1.2);
}
.alarm-input-group {
display: flex;
gap: 15px;
flex-wrap: wrap;
margin-bottom: 15px;
}
.alarm-input-group input {
flex: 1;
min-width: 120px;
}
.alarm-input-wrapper {
display: flex;
flex-direction: column;
flex: 1;
}
.alarm-input-wrapper label {
font-size: 13px;
color: #666;
margin-bottom: 5px;
}
.radio-group {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.radio-group label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 10px 15px;
background: #f8f9fa;
border-radius: 10px;
transition: all 0.3s ease;
}
.radio-group label:hover {
background: #e9ecef;
}
.radio-group input[type="radio"] {
width: 18px;
height: 18px;
}
/* ===== 闹钟提醒模态框 ===== */
.alarm-alert-modal {
display: none;
position: fixed;
z-index: 3000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.85);
align-items: center;
justify-content: center;
backdrop-filter: blur(8px);
}
.alarm-alert-content {
background: linear-gradient(135deg, #f39c12, #e67e22);
color: white;
padding: 60px 80px;
border-radius: 30px;
text-align: center;
box-shadow: 0 25px 70px rgba(0, 0, 0, 0.5);
max-width: 600px;
animation: alarmPulse 0.5s ease-out infinite alternate;
position: relative;
}
@keyframes alarmPulse {
0% { transform: scale(1); box-shadow: 0 25px 70px rgba(0, 0, 0, 0.5); }
100% { transform: scale(1.02); box-shadow: 0 30px 80px rgba(243, 156, 18, 0.6); }
}
.alarm-alert-content h2 {
font-size: 48px;
margin-bottom: 20px;
}
.alarm-alert-time {
font-size: 36px;
font-weight: bold;
margin-bottom: 20px;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.alarm-alert-note {
font-size: 24px;
margin-bottom: 30px;
padding: 20px;
background: rgba(255, 255, 255, 0.15);
border-radius: 15px;
}
.alarm-alert-close {
padding: 16px 50px;
background: rgba(255, 255, 255, 0.2);
border: 3px solid white;
color: white;
border-radius: 50px;
cursor: pointer;
font-size: 20px;
font-weight: bold;
transition: all 0.3s ease;
}
.alarm-alert-close:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.05);
}
/* ===== 恢复出厂设置确认模态框 ===== */
.reset-confirm-modal {
display: none;
position: fixed;
z-index: 2500;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.85);
align-items: center;
justify-content: center;
backdrop-filter: blur(8px);
}
.reset-confirm-content {
background: linear-gradient(135deg, #2c3e50, #34495e);
color: white;
padding: 50px 70px;
border-radius: 25px;
text-align: center;
box-shadow: 0 25px 70px rgba(0, 0, 0, 0.5);
max-width: 550px;
animation: modalAppear 0.4s ease-out;
}
.reset-confirm-content h2 {
font-size: 32px;
margin-bottom: 30px;
color: #f39c12;
}
.reset-confirm-content p {
font-size: 22px;
line-height: 1.8;
margin-bottom: 40px;
}
.reset-confirm-buttons {
display: flex;
gap: 20px;
justify-content: center;
flex-wrap: wrap;
}
.reset-confirm-buttons button {
padding: 16px 35px;
border: none;
border-radius: 50px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-keep-going {
background: linear-gradient(135deg, #27ae60, #2ecc71);
color: white;
box-shadow: 0 6px 20px rgba(39, 174, 96, 0.4);
}
.btn-keep-going:hover {
transform: translateY(-3px) scale(1.05);
box-shadow: 0 10px 30px rgba(39, 174, 96, 0.5);
}
.btn-give-up {
background: linear-gradient(135deg, #95a5a6, #7f8c8d);
color: white;
box-shadow: 0 6px 20px rgba(149, 165, 166, 0.4);
}
.btn-give-up:hover {
transform: translateY(-3px);
box-shadow: 0 10px 30px rgba(149, 165, 166, 0.5);
}
/* ===== 小卡片动画模态框 ===== */
.cards-animation-modal {
display: none;
position: fixed;
z-index: 2000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg,
#fff0f3 0%,
#ffe4ec 25%,
#ffd6e0 50%,
#ffccd5 75%,
#ffb3c1 100%);
}
.cards-board {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
.cards-close-btn {
position: fixed;
top: 20px;
right: 20px;
z-index: 2100;
background: rgba(255, 255, 255, 0.9);
border: none;
width: 50px;
height: 50px;
border-radius: 50%;
font-size: 20px;
cursor: pointer;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
}
.cards-close-btn:hover {
transform: scale(1.1);
background: white;
}
/* ===== 视频播放模态框 ===== */
.video-modal {
display: none;
position: fixed;
z-index: 3000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.95);
align-items: center;
justify-content: center;
}
.video-modal-content {
position: relative;
max-width: 90%;
max-height: 90%;
}
.video-modal-content video {
max-width: 100%;
max-height: 85vh;
border-radius: 15px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}
.video-close-btn {
position: absolute;
top: -50px;
right: 0;
background: rgba(255, 255, 255, 0.2);
border: 2px solid white;
color: white;
padding: 10px 25px;
border-radius: 25px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s ease;
}
.video-close-btn:hover {
background: rgba(255, 255, 255, 0.3);
}
/* ===== 小卡片样式 ===== */
.card {
position: absolute;
left: 0;
top: 0;
border-radius: 14px;
opacity: 0;
cursor: grab;
user-select: none;
will-change: transform, opacity;
backface-visibility: hidden;
}
.card.show { opacity: 1; }
.card.color-pink {
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%);
border: 2px solid rgba(255,255,255,0.7);
box-shadow: 0 4px 20px rgba(255, 154, 158, 0.35);
}
.card.color-pink .card-body { color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,0.1); }
.card.color-purple {
background: linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%);
border: 2px solid rgba(255,255,255,0.7);
box-shadow: 0 4px 20px rgba(161, 140, 209, 0.35);
}
.card.color-purple .card-body { color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,0.1); }
.card.color-blue {
background: linear-gradient(135deg, #89c4f4 0%, #c5e1f5 100%);
border: 2px solid rgba(255,255,255,0.7);
box-shadow: 0 4px 20px rgba(137, 196, 244, 0.35);
}
.card.color-blue .card-body { color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,0.1); }
.card.color-mint {
background: linear-gradient(135deg, #80deea 0%, #b2ebf2 100%);
border: 2px solid rgba(255,255,255,0.7);
box-shadow: 0 4px 20px rgba(128, 222, 234, 0.35);
}
.card.color-mint .card-body { color: #4a9ea8; text-shadow: 0 1px 0 rgba(255,255,255,0.5); }
.card.color-yellow {
background: linear-gradient(135deg, #ffeaa7 0%, #fff3b0 100%);
border: 2px solid rgba(255,255,255,0.8);
box-shadow: 0 4px 20px rgba(255, 234, 167, 0.4);
}
.card.color-yellow .card-body { color: #b8860b; text-shadow: 0 1px 0 rgba(255,255,255,0.5); }
.card.color-orange {
background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
border: 2px solid rgba(255,255,255,0.7);
box-shadow: 0 4px 20px rgba(252, 182, 159, 0.35);
}
.card.color-orange .card-body { color: #d35400; text-shadow: 0 1px 0 rgba(255,255,255,0.3); }
.card.color-green {
background: linear-gradient(135deg, #a8e6cf 0%, #dcedc1 100%);
border: 2px solid rgba(255,255,255,0.7);
box-shadow: 0 4px 20px rgba(168, 230, 207, 0.35);
}
.card.color-green .card-body { color: #27ae60; text-shadow: 0 1px 0 rgba(255,255,255,0.5); }
.card.color-coral {
background: linear-gradient(135deg, #ff7675 0%, #fab1a0 100%);
border: 2px solid rgba(255,255,255,0.7);
box-shadow: 0 4px 20px rgba(255, 118, 117, 0.35);
}
.card.color-coral .card-body { color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,0.15); }
.card.color-lavender {
background: linear-gradient(135deg, #dda0dd 0%, #e6e6fa 100%);
border: 2px solid rgba(255,255,255,0.7);
box-shadow: 0 4px 20px rgba(221, 160, 221, 0.35);
}
.card.color-lavender .card-body { color: #8b008b; text-shadow: 0 1px 0 rgba(255,255,255,0.4); }
.card.color-white {
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
border: 2px solid rgba(255,200,210,0.5);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
}
.card.color-white .card-body { color: #e84393; }
.card.float-in {
animation: floatIn 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
animation-delay: var(--delay, 0s);
}
@keyframes floatIn {
0% {
opacity: 0;
transform: var(--base-transform) translateY(var(--float-y, -40px)) rotate(calc(var(--rotation) + var(--float-r, 25deg))) scale(0.5);
}
60% {
opacity: 1;
transform: var(--base-transform) translateY(5px) rotate(calc(var(--rotation) - 3deg)) scale(1.03);
}
100% {
opacity: 1;
transform: var(--base-transform) rotate(var(--rotation)) scale(1);
}
}
.card.dragging {
cursor: grabbing;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.2) !important;
z-index: 999999 !important;
}
.card-body {
padding: 14px 12px;
font-size: 17px;
line-height: 1.35;
font-weight: 600;
text-align: center;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
/* ===== 美化设置模态框 ===== */
.mode-selector {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.mode-option {
flex: 1;
min-width: 200px;
cursor: pointer;
}
.mode-option input[type="radio"] {
display: none;
}
.mode-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 15px;
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
border: 3px solid transparent;
border-radius: 16px;
transition: all 0.3s ease;
text-align: center;
}
.mode-option input[type="radio"]:checked + .mode-card {
background: linear-gradient(135deg, #667eea15, #764ba215);
border-color: #667eea;
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.2);
}
.mode-card:hover {
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
}
.mode-icon {
font-size: 36px;
margin-bottom: 10px;
}
.mode-title {
font-size: 16px;
font-weight: bold;
color: #2c3e50;
margin-bottom: 5px;
}
.mode-desc {
font-size: 12px;
color: #888;
}
/* 文件上传美化 */
.file-upload-box {
position: relative;
border: 3px dashed #667eea;
border-radius: 16px;
background: linear-gradient(135deg, #f8f9ff, #fff);
overflow: hidden;
transition: all 0.3s ease;
}
.file-upload-box:hover {
border-color: #764ba2;
background: linear-gradient(135deg, #f0f2ff, #f8f5ff);
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.15);
}
.file-upload-box .file-input {
position: absolute;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
z-index: 2;
}
.file-upload-label {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 30px 20px;
cursor: pointer;
}
.upload-icon {
font-size: 48px;
margin-bottom: 15px;
}
.upload-text {
font-size: 16px;
font-weight: 600;
color: #667eea;
margin-bottom: 8px;
}
.upload-hint {
font-size: 12px;
color: #999;
}
.selected-file {
margin-top: 12px;
padding: 15px 20px;
background: linear-gradient(135deg, #d4edda, #c3e6cb);
border-radius: 12px;
border-left: 5px solid #28a745;
display: flex;
justify-content: space-between;
align-items: center;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.selected-file .file-info {
display: flex;
align-items: center;
gap: 10px;
}
.selected-file .file-icon {
font-size: 24px;
}
.selected-file .file-details {
display: flex;
flex-direction: column;
}
.selected-file .file-name-text {
font-weight: 600;
color: #155724;
}
.selected-file .file-size-text {
font-size: 12px;
color: #666;
}
.selected-file .delete-btn {
background: #dc3545;
color: white;
border: none;
width: 30px;
height: 30px;
border-radius: 50%;
font-size: 18px;
cursor: pointer;
transition: all 0.2s ease;
}
.selected-file .delete-btn:hover {
transform: scale(1.1);
background: #c82333;
}
.input-hint {
font-size: 12px;
color: #888;
margin-top: 8px;
padding-left: 5px;
}
/* 按钮美化 */
.modal-buttons .btn-cancel {
background: linear-gradient(135deg, #e0e0e0, #d0d0d0);
color: #555;
border: none;
padding: 14px 32px;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.modal-buttons .btn-cancel:hover {
background: linear-gradient(135deg, #d0d0d0, #c0c0c0);
transform: translateY(-2px);
}
.modal-buttons .btn-save {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
padding: 14px 32px;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3);
transition: all 0.3s ease;
}
.modal-buttons .btn-save:hover {
transform: translateY(-3px);
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
}
/* 模态框内容防溢出 */
.modal-content {
max-height: 90vh;
overflow-y: auto;
overflow-x: hidden;
}
.modal-content::-webkit-scrollbar {
width: 8px;
}
.modal-content::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.modal-content::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 4px;
}
.modal-content::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #5a6fd6, #6a4190);
}
/* 表单组间距优化 */
.form-group {
margin-bottom: 25px;
}
.form-group label {
display: block;
margin-bottom: 12px;
font-weight: 600;
color: #2c3e50;
font-size: 16px;
}
.form-group textarea {
width: 100%;
padding: 15px;
border: 2px solid #e0e0e0;
border-radius: 12px;
font-size: 15px;
line-height: 1.6;
resize: vertical;
transition: all 0.3s ease;
font-family: inherit;
}
.form-group textarea:focus {
border-color: #667eea;
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.15);
outline: none;
}
.form-group textarea::placeholder {
color: #aaa;
}
/* ===== 颜色选择器美化 ===== */
.color-picker-box {
display: flex;
align-items: center;
gap: 20px;
}
.color-picker-box input[type="color"] {
width: 80px;
height: 50px;
border: none;
border-radius: 12px;
cursor: pointer;
padding: 0;
overflow: hidden;
}
.color-picker-box input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
}
.color-picker-box input[type="color"]::-webkit-color-swatch {
border: none;
border-radius: 12px;
}
.color-preview {
flex: 1;
height: 50px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: #666;
font-size: 14px;
border: 2px dashed #ddd;
transition: all 0.3s ease;
}
/* ===== 日期时间选择器美化 ===== */
.datetime-picker {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.datetime-input-wrapper {
flex: 1;
min-width: 180px;
display: flex;
flex-direction: column;
gap: 8px;
}
.datetime-label {
font-size: 13px;
color: #666;
font-weight: 500;
}
.datetime-input-wrapper input {
padding: 15px;
border: 2px solid #e0e0e0;
border-radius: 12px;
font-size: 16px;
transition: all 0.3s ease;
}
.datetime-input-wrapper input:focus {
border-color: #667eea;
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.15);
outline: none;
}
/* ===== 添加闹钟区域 ===== */
.add-alarm-section {
background: linear-gradient(135deg, #f8f9ff, #fff);
border-radius: 16px;
padding: 25px;
margin-bottom: 25px;
border: 2px solid #e8ecf5;
}
.section-title {
font-size: 18px;
color: #2c3e50;
margin: 0 0 20px 0;
padding-bottom: 12px;
border-bottom: 2px solid #e8ecf5;
}
.btn-add-alarm {
width: 100%;
padding: 16px;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3);
transition: all 0.3s ease;
margin-top: 20px;
}
.btn-add-alarm:hover {
transform: translateY(-3px);
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
}
.btn-add-alarm span {
font-size: 20px;
}
/* ===== 闹钟列表区域 ===== */
.alarm-list-section {
background: #fff;
border-radius: 16px;
padding: 20px;
border: 2px solid #e8ecf5;
}
.alarm-list {
max-height: 250px;
overflow-y: auto;
padding-right: 5px;
}
.alarm-list::-webkit-scrollbar {
width: 6px;
}
.alarm-list::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.alarm-list::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 3px;
}
.empty-alarm {
text-align: center;
color: #999;
padding: 30px;
font-size: 15px;
}
.alarm-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 18px 20px;
background: linear-gradient(135deg, #f8f9fa, #fff);
border-radius: 14px;
margin-bottom: 12px;
border-left: 5px solid #667eea;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.alarm-item:hover {
transform: translateX(5px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
}
.alarm-item:last-child {
margin-bottom: 0;
}
.alarm-item-info {
flex: 1;
}
.alarm-item-time {
font-size: 20px;
font-weight: bold;
color: #2c3e50;
margin-bottom: 6px;
display: flex;
align-items: center;
gap: 10px;
}
.alarm-item-time::before {
content: '⏰';
font-size: 18px;
}
.alarm-item-note {
font-size: 14px;
color: #666;
display: flex;
align-items: center;
gap: 8px;
}
.alarm-mode-tag {
display: inline-block;
padding: 3px 10px;
background: linear-gradient(135deg, #667eea20, #764ba220);
color: #667eea;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.alarm-item-delete {
background: linear-gradient(135deg, #ff6b6b, #ee5a5a);
border: none;
color: white;
width: 36px;
height: 36px;
border-radius: 50%;
font-size: 18px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.alarm-item-delete:hover {
transform: scale(1.15);
box-shadow: 0 4px 15px rgba(255, 107, 107, 0.4);
}
/* ===== 通用输入框美化 ===== */
.form-group input[type="text"] {
width: 100%;
padding: 15px;
border: 2px solid #e0e0e0;
border-radius: 12px;
font-size: 16px;
transition: all 0.3s ease;
}
.form-group input[type="text"]:focus {
border-color: #667eea;
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.15);
outline: none;
}
.form-group input[type="text"]::placeholder {
color: #aaa;
}
/* ===== 跨日目标模态框美化 ===== */
.add-goal-section {
background: linear-gradient(135deg, #f8f9ff, #fff);
border-radius: 16px;
padding: 25px;
margin-bottom: 25px;
border: 2px solid #e8ecf5;
}
.btn-add-goal {
width: 100%;
padding: 16px;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3);
transition: all 0.3s ease;
margin-top: 20px;
}
.btn-add-goal:hover {
transform: translateY(-3px);
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
}
.btn-add-goal span {
font-size: 20px;
}
.goal-list-section {
background: #fff;
border-radius: 16px;
padding: 20px;
border: 2px solid #e8ecf5;
}
.goal-list-container {
max-height: 300px;
overflow-y: auto;
padding-right: 5px;
}
.goal-list-container::-webkit-scrollbar {
width: 6px;
}
.goal-list-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.goal-list-container::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 3px;
}
.empty-goal {
text-align: center;
color: #999;
padding: 30px;
font-size: 15px;
}
.goal-list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 18px 20px;
background: linear-gradient(135deg, #f8f9fa, #fff);
border-radius: 14px;
margin-bottom: 12px;
border-left: 5px solid #667eea;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.goal-list-item:hover {
transform: translateX(5px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
}
.goal-list-item:last-child {
margin-bottom: 0;
}
.goal-list-item.completed {
border-left-color: #27ae60;
background: linear-gradient(135deg, #d4edda, #c3e6cb);
}
.goal-list-item.failed {
border-left-color: #e74c3c;
background: linear-gradient(135deg, #f8d7da, #f5c6cb);
opacity: 0.8;
}
.goal-item-info {
flex: 1;
}
.goal-item-name {
font-size: 18px;
font-weight: bold;
color: #2c3e50;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 10px;
}
.goal-item-name::before {
content: '🎯';
font-size: 16px;
}
.goal-item-dates {
font-size: 13px;
color: #666;
margin-bottom: 6px;
}
.goal-item-countdown {
font-size: 14px;
font-weight: 600;
color: #667eea;
}
.goal-status-tag {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
margin-left: 10px;
}
.goal-status-tag.running {
background: linear-gradient(135deg, #667eea20, #764ba220);
color: #667eea;
}
.goal-status-tag.completed {
background: linear-gradient(135deg, #27ae6020, #2ecc7120);
color: #27ae60;
}
.goal-status-tag.failed {
background: linear-gradient(135deg, #e74c3c20, #c0392b20);
color: #e74c3c;
}
.goal-item-delete {
background: linear-gradient(135deg, #ff6b6b, #ee5a5a);
border: none;
color: white;
width: 36px;
height: 36px;
border-radius: 50%;
font-size: 18px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.goal-item-delete:hover {
transform: scale(1.15);
box-shadow: 0 4px 15px rgba(255, 107, 107, 0.4);
}
/* ===== 目标完成确认模态框 ===== */
.goal-complete-modal {
display: none;
position: fixed;
z-index: 3000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.85);
align-items: center;
justify-content: center;
backdrop-filter: blur(8px);
}
.goal-complete-content {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
padding: 50px 70px;
border-radius: 30px;
text-align: center;
box-shadow: 0 25px 70px rgba(0, 0, 0, 0.5);
max-width: 500px;
animation: goalPulse 0.5s ease-out;
}
@keyframes goalPulse {
0% { transform: scale(0.8); opacity: 0; }
50% { transform: scale(1.05); }
100% { transform: scale(1); opacity: 1; }
}
.goal-complete-content h2 {
font-size: 36px;
margin-bottom: 20px;
}
.goal-complete-name {
font-size: 24px;
font-weight: bold;
margin-bottom: 25px;
padding: 15px 25px;
background: rgba(255, 255, 255, 0.15);
border-radius: 15px;
}
.goal-complete-text {
font-size: 28px;
margin-bottom: 35px;
line-height: 1.6;
}
.goal-complete-buttons {
display: flex;
gap: 20px;
justify-content: center;
flex-wrap: wrap;
}
.goal-complete-buttons button {
padding: 16px 40px;
border: none;
border-radius: 50px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-not-complete {
background: rgba(255, 255, 255, 0.2);
color: white;
border: 3px solid rgba(255, 255, 255, 0.5) !important;
}
.btn-not-complete:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.05);
}
.btn-complete {
background: linear-gradient(135deg, #27ae60, #2ecc71);
color: white;
box-shadow: 0 6px 20px rgba(39, 174, 96, 0.4);
}
.btn-complete:hover {
transform: translateY(-3px) scale(1.05);
box-shadow: 0 10px 30px rgba(39, 174, 96, 0.5);
}
.btn-confirm {
background: rgba(255, 255, 255, 0.2);
color: white;
border: 3px solid white !important;
padding: 16px 60px !important;
}
.btn-confirm:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.05);
}
/* ===== 跨日目标媒体设置区域 ===== */
.goal-media-section {
background: linear-gradient(135deg, #e8f5e9, #f1f8e9);
border-radius: 16px;
padding: 25px;
margin-bottom: 25px;
border: 2px solid #c8e6c9;
}
.goal-media-section .section-title {
color: #2e7d32;
border-bottom-color: #c8e6c9;
}
.btn-save-media {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #27ae60, #2ecc71);
color: white;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
box-shadow: 0 6px 20px rgba(39, 174, 96, 0.3);
transition: all 0.3s ease;
margin-top: 15px;
}
.btn-save-media:hover {
transform: translateY(-3px);
box-shadow: 0 10px 30px rgba(39, 174, 96, 0.4);
}
.btn-save-media span {
font-size: 18px;
}
</style>
</head>
<body>
<!-- 左侧边栏:今日任务 -->
<div class="sidebar-left">
<!-- 实时时钟显示 -->
<div class="realtime-clock">
<div class="clock-icon">🕐</div>
<div class="clock-display">
<div class="clock-time" id="realtime-clock">--:--:--</div>
<div class="clock-date" id="realtime-date">----年--月--日 星期-</div>
</div>
<button class="clock-refresh" id="sync-time-btn" title="同步网络时间">🔄</button>
</div>
<div class="today-tasks">
<h3>今日任务</h3>
<table class="today-task-table" id="today-task-display">
<tbody>
<tr><td colspan="3" style="text-align:center;color:#999;padding:30px;">点击 📝 设置今日任务</td></tr>
</tbody>
</table>
<!-- 新增:一键全部完成按钮 -->
<div class="all-complete-display-btn" id="all-complete-display-wrapper" style="display:none;">
<button id="all-complete-display">一键全部完成 ✅</button>
</div>
</div>
</div>
<!-- 中间主内容区 -->
<div class="main-content">
<!-- 功能按钮 -->
<div class="daily-task-btn" title="设置今日任务">📝</div>
<div class="alarm-btn" title="设置闹钟">⏰</div>
<div class="long-term-btn" title="管理跨日目标">🎯</div>
<div class="reset-btn" title="清除所有数据">🗑️</div>
<div class="encourage-settings-btn" title="自定义鼓励语">💪</div>
<div class="settings-btn" title="自定义庆祝内容">✨</div>
<h1>雄关漫道真如铁,而今迈步从头越!</h1>
<!-- 月份导航 -->
<div class="nav">
<button id="today-btn">回到今天</button>
<button id="prev">< 上个月</button>
<span id="month-year">2025年12月</span>
<button id="next">下个月 ></button>
</div>
<!-- 庆祝按钮 -->
<div class="celebrate-section">
<button id="celebrate-btn">完成今日任务!🎉</button>
</div>
<!-- 星期标题 -->
<div class="weekday-header">
<div>星期日</div>
<div>星期一</div>
<div>星期二</div>
<div>星期三</div>
<div>星期四</div>
<div>星期五</div>
<div>星期六</div>
</div>
<!-- 日历网格 -->
<div class="calendar-grid" id="calendar"></div>
<!-- 日期信息 -->
<div class="date-info" id="date-info">单击查看距离今天的时间</div>
<!-- 署名 -->
<div class="signature">@天方夜谭熊二</div>
</div>
<!-- 右侧边栏:统计和跨日目标 -->
<div class="sidebar-right">
<!-- 本月统计 -->
<div class="stats">
<h3>本月坚持痕迹</h3>
<p id="month-completed">本月已完成 <strong>0</strong> 天</p>
<p id="longest-streak">最长连续完成 <strong>0</strong> 天</p>
<p id="current-streak">当前连续 <strong>0</strong> 天</p>
</div>
<!-- 跨日目标 -->
<div class="long-term-goals">
<h3>跨日目标</h3>
<div id="goals-list">
<p class="empty-state">暂无跨日目标<br>点击左上角 🎯 添加</p>
</div>
</div>
<!-- 鼓励动画按钮 -->
<div class="give-up-section">
<button class="give-up-btn" id="give-up-btn">累了就来这里 💕</button>
</div>
</div>
<!-- 音频元素 -->
<audio id="celebrate-audio" preload="auto"></audio>
<audio id="encourage-audio" preload="auto"></audio>
<!-- 庆祝模态框 -->
<div class="celebrate-modal" id="celebrate-modal">
<div class="celebrate-content">
<h2>太棒了!</h2>
<div class="celebrate-quote" id="celebrate-quote"></div>
<button class="celebrate-close" id="celebrate-close">明天见~</button>
</div>
</div>
<!-- 鼓励模态框 -->
<div class="encourage-modal" id="encourage-modal">
<div class="encourage-content">
<h2>少年,振作起来!</h2>
<div class="encourage-quote" id="encourage-quote"></div>
<button class="encourage-close" id="encourage-close">坚持下去!</button>
</div>
</div>
<!-- 今日任务模态框 -->
<div class="daily-task-modal" id="daily-task-modal">
<div class="modal-content">
<div class="modal-header">今日任务规划</div>
<button class="add-field-btn" id="add-task-field">+ 添加字段</button>
<button class="add-row-btn" id="add-task-row">+ 添加任务</button>
<button class="add-row-btn" id="import-text" style="background:#28a745;">导入文本</button>
<table id="task-table" style="margin-top:20px;">
<thead id="task-head"></thead>
<tbody id="task-rows"></tbody>
</table>
<div class="modal-buttons">
<button id="daily-task-cancel">取消</button>
<button id="daily-task-save">保存并显示</button>
</div>
</div>
</div>
<!-- 设置模态框 -->
<div class="settings-modal" id="settings-modal">
<div class="modal-content" style="max-width:600px;">
<div class="modal-header">✨ 自定义庆祝内容</div>
<!-- 播放模式选择 -->
<div class="form-group">
<label>🎬 播放模式</label>
<div class="mode-selector">
<label class="mode-option">
<input type="radio" name="celebrate-mode" value="audio" checked>
<div class="mode-card">
<span class="mode-icon">🎵</span>
<span class="mode-title">音频 + 语录</span>
<span class="mode-desc">播放音乐并显示庆祝弹窗</span>
</div>
</label>
<label class="mode-option">
<input type="radio" name="celebrate-mode" value="video">
<div class="mode-card">
<span class="mode-icon">🎥</span>
<span class="mode-title">仅播放视频</span>
<span class="mode-desc">全屏播放视频,不显示弹窗</span>
</div>
</label>
</div>
</div>
<!-- 文件选择 -->
<div class="form-group">
<label>🎵 音频/视频文件</label>
<div class="file-upload-box">
<input type="file" id="local-celebrate-file" accept="audio/*,video/*" class="file-input">
<label for="local-celebrate-file" class="file-upload-label">
<span class="upload-icon">📂</span>
<span class="upload-text">点击或拖拽选择文件</span>
<span class="upload-hint">支持 MP3、MP4、WAV 等格式,最大 100MB</span>
</label>
</div>
<div id="selected-file-name" class="selected-file" style="display:none;"></div>
</div>
<!-- 语录设置 -->
<div class="form-group">
<label>📝 庆祝语录</label>
<textarea id="custom-quotes" rows="6" placeholder="输入庆祝语,每行一句
例如:
太棒了!你做到了!
坚持就是胜利!
今天的努力是明天的收获!"></textarea>
<div class="input-hint">每行一句,随机显示</div>
</div>
<div class="modal-buttons">
<button id="settings-cancel" class="btn-cancel">取消</button>
<button id="settings-save" class="btn-save">💾 保存设置</button>
</div>
</div>
</div>
<!-- 鼓励/小卡片设置模态框 -->
<div class="encourage-settings-modal" id="encourage-settings-modal">
<div class="modal-content" style="max-width:650px;">
<div class="modal-header">💪 自定义鼓励动画</div>
<!-- 播放模式选择 -->
<div class="form-group">
<label>🎬 播放模式</label>
<div class="mode-selector">
<label class="mode-option">
<input type="radio" name="encourage-mode" value="animation" checked>
<div class="mode-card">
<span class="mode-icon">🎴</span>
<span class="mode-title">动画 + 音频</span>
<span class="mode-desc">播放小卡片飘落动画和音乐</span>
</div>
</label>
<label class="mode-option">
<input type="radio" name="encourage-mode" value="video">
<div class="mode-card">
<span class="mode-icon">🎥</span>
<span class="mode-title">仅播放视频</span>
<span class="mode-desc">全屏播放视频,不显示动画</span>
</div>
</label>
</div>
</div>
<!-- 文件选择 -->
<div class="form-group">
<label>🎵 音频/视频文件</label>
<div class="file-upload-box">
<input type="file" id="local-encourage-file" accept="audio/*,video/*" class="file-input">
<label for="local-encourage-file" class="file-upload-label">
<span class="upload-icon">📂</span>
<span class="upload-text">点击或拖拽选择文件</span>
<span class="upload-hint">支持 MP3、MP4、WAV 等格式,最大 100MB</span>
</label>
</div>
<div id="selected-encourage-file-name" class="selected-file" style="display:none;"></div>
</div>
<!-- 背景颜色 -->
<div class="form-group">
<label>🎨 动画背景颜色</label>
<div class="color-picker-box">
<input type="color" id="encourage-bg-color" value="#fff0f3">
<div class="color-preview" id="color-preview">
<span>当前颜色预览</span>
</div>
</div>
</div>
<!-- 小卡片语录 -->
<div class="form-group">
<label>📝 小卡片语录</label>
<textarea id="custom-encourage-quotes" rows="5" placeholder="输入鼓励语,每行一句
例如:
少年,振作起来!
你可以的!
坚持就是胜利!"></textarea>
<div class="input-hint">每行一句,随机显示在小卡片上</div>
</div>
<!-- 小卡片Emoji -->
<div class="form-group">
<label>😊 小卡片 Emoji</label>
<input type="text" id="encourage-emojis" placeholder="💕,💗,💖,✨,🌸,🦋,⭐,🥰" style="padding:15px;font-size:18px;">
<div class="input-hint">用逗号分隔,随机显示在小卡片上</div>
</div>
<div class="modal-buttons">
<button id="encourage-settings-cancel" class="btn-cancel">取消</button>
<button id="encourage-settings-save" class="btn-save">💾 保存设置</button>
</div>
</div>
</div>
<!-- 跨日目标模态框 -->
<div class="long-term-modal" id="long-term-modal">
<div class="modal-content" style="max-width:750px;">
<div class="modal-header">🎯 跨日目标管理</div>
<!-- 小卡片动画自定义区 -->
<div class="section">
<h3>🎴 小卡片动画自定义</h3>
<div class="form-group">
<label>📝 小卡片语录(每行一句)</label>
<textarea id="goal-cards-quotes" rows="8" placeholder="每行输入一句语录
例如:
坚持就是胜利!
你超棒的!"></textarea>
</div>
<div class="form-group">
<label>😊 小卡片 Emoji(用空格分隔)</label>
<input type="text" id="goal-cards-emojis" placeholder="🎉 ✨ 🔥 💪 ❤️ 🚀">
</div>
</div>
<!-- 完成提醒设置区域 -->
<div class="goal-media-section">
<h4 class="section-title">🎉 完成提醒设置</h4>
<!-- 播放模式选择 -->
<div class="form-group">
<label>🎬 播放模式</label>
<div class="mode-selector">
<label class="mode-option">
<input type="radio" name="goal-mode" value="animation" checked>
<div class="mode-card">
<span class="mode-icon">🎴</span>
<span class="mode-title">动画 + 音频</span>
<span class="mode-desc">播放小卡片飘落动画和音乐</span>
</div>
</label>
<label class="mode-option">
<input type="radio" name="goal-mode" value="video">
<div class="mode-card">
<span class="mode-icon">🎥</span>
<span class="mode-title">仅播放视频</span>
<span class="mode-desc">全屏播放视频,不显示动画</span>
</div>
</label>
</div>
</div>
<!-- 文件选择 -->
<div class="form-group">
<label>🎵 音频/视频文件</label>
<div class="file-upload-box">
<input type="file" id="goal-media-file" accept="audio/*,video/*" class="file-input">
<label for="goal-media-file" class="file-upload-label">
<span class="upload-icon">📂</span>
<span class="upload-text">点击或拖拽选择文件</span>
<span class="upload-hint">支持 MP3、MP4、WAV 等格式,最大 100MB</span>
</label>
</div>
<div id="goal-media-file-name" class="selected-file" style="display:none;"></div>
</div>
<button class="btn-save-media" id="btn-save-goal-media">
<span>💾</span> 保存提醒设置
</button>
</div>
<!-- 添加新目标区域 -->
<div class="add-goal-section">
<h4 class="section-title">➕ 添加新目标</h4>
<div class="form-group">
<label>📌 目标名称</label>
<input type="text" id="new-goal-name" placeholder="例如:完成项目报告">
</div>
<div class="form-group">
<label>📅 时间范围</label>
<div class="datetime-picker">
<div class="datetime-input-wrapper">
<span class="datetime-label">开始时间</span>
<input type="datetime-local" id="new-goal-start" step="1">
</div>
<div class="datetime-input-wrapper">
<span class="datetime-label">结束时间</span>
<input type="datetime-local" id="new-goal-end" step="1">
</div>
</div>
</div>
<button class="btn-add-goal" id="btn-add-goal">
<span>➕</span> 添加目标
</button>
</div>
<!-- 目标列表 -->
<div class="goal-list-section">
<h4 class="section-title">📋 我的目标</h4>
<div class="goal-list-container" id="goal-list-container">
<p class="empty-goal">暂无目标,点击上方添加</p>
</div>
</div>
<div class="modal-buttons">
<button id="long-term-cancel" class="btn-cancel">关闭</button>
</div>
</div>
</div>
<!-- 闹钟设置模态框 -->
<div class="alarm-modal" id="alarm-modal">
<div class="modal-content" style="max-width:650px;">
<div class="modal-header">⏰ 闹钟设置</div>
<!-- 添加新闹钟区域 -->
<div class="add-alarm-section">
<h4 class="section-title">➕ 添加新闹钟</h4>
<!-- 日期时间选择 -->
<div class="form-group">
<label>📅 选择日期和时间</label>
<div class="datetime-picker">
<div class="datetime-input-wrapper">
<span class="datetime-label">日期</span>
<input type="date" id="alarm-date">
</div>
<div class="datetime-input-wrapper">
<span class="datetime-label">时间</span>
<input type="time" id="alarm-time" step="1">
</div>
</div>
</div>
<!-- 备注 -->
<div class="form-group">
<label>📝 备注</label>
<input type="text" id="alarm-note" placeholder="例如:该学习啦!" style="padding:15px;">
</div>
<!-- 播放模式选择 -->
<div class="form-group">
<label>🎬 提醒方式</label>
<div class="mode-selector">
<label class="mode-option">
<input type="radio" name="alarm-mode" value="audio" checked>
<div class="mode-card">
<span class="mode-icon">🔔</span>
<span class="mode-title">弹窗 + 音频</span>
<span class="mode-desc">显示提醒弹窗并播放音乐</span>
</div>
</label>
<label class="mode-option">
<input type="radio" name="alarm-mode" value="video">
<div class="mode-card">
<span class="mode-icon">🎥</span>
<span class="mode-title">播放视频</span>
<span class="mode-desc">直接播放视频,不显示弹窗</span>
</div>
</label>
</div>
</div>
<!-- 文件选择 -->
<div class="form-group">
<label>🎵 提醒音频/视频</label>
<div class="file-upload-box">
<input type="file" id="alarm-audio-file" accept="audio/*,video/*" class="file-input">
<label for="alarm-audio-file" class="file-upload-label">
<span class="upload-icon">📂</span>
<span class="upload-text">点击选择提醒铃声</span>
<span class="upload-hint">支持 MP3、MP4 等格式</span>
</label>
</div>
<div id="alarm-file-name" class="selected-file" style="display:none;"></div>
</div>
<button class="btn-add-alarm" id="add-alarm-btn">
<span>➕</span> 添加闹钟
</button>
</div>
<!-- 闹钟列表 -->
<div class="alarm-list-section">
<h4 class="section-title">📋 已设置的闹钟</h4>
<div class="alarm-list" id="alarm-list">
<p class="empty-alarm">暂无闹钟</p>
</div>
</div>
<div class="modal-buttons">
<button id="alarm-cancel" class="btn-cancel">关闭</button>
</div>
</div>
</div>
<!-- 闹钟提醒模态框 -->
<div class="alarm-alert-modal" id="alarm-alert-modal">
<div class="alarm-alert-content">
<h2>⏰ 时间到!</h2>
<div class="alarm-alert-time" id="alarm-alert-time"></div>
<div class="alarm-alert-note" id="alarm-alert-note"></div>
<button class="alarm-alert-close" id="alarm-alert-close">我知道了</button>
</div>
</div>
<!-- 闹钟提醒模态框 -->
<div class="alarm-alert-modal" id="alarm-alert-modal">
<div class="alarm-alert-content">
<h2>⏰ 时间到!</h2>
<div class="alarm-alert-time" id="alarm-alert-time"></div>
<div class="alarm-alert-note" id="alarm-alert-note"></div>
<button class="alarm-alert-close" id="alarm-alert-close">我知道了</button>
</div>
</div>
<!-- 跨日目标视频播放模态框 -->
<div class="video-modal" id="goal-video-modal">
<div class="video-modal-content">
<video id="goal-video-player" controls autoplay></video>
<button class="video-close-btn" id="goal-video-close">关闭</button>
</div>
</div>
<!-- 跨日目标完成确认模态框 -->
<div class="goal-complete-modal" id="goal-complete-modal">
<div class="goal-complete-content">
<h2 id="goal-complete-title">⏰ 目标时间到!</h2>
<div class="goal-complete-name" id="goal-complete-name"></div>
<div class="goal-complete-text" id="goal-complete-text">是否完成?</div>
<div class="goal-complete-buttons" id="goal-complete-buttons">
<button class="btn-not-complete" id="btn-not-complete">未完成 😔</button>
<button class="btn-complete" id="btn-complete">完成 🎉</button>
</div>
</div>
</div>
<!-- 闹钟视频播放模态框 -->
<div class="video-modal" id="alarm-video-modal">
<div class="video-modal-content">
<video id="alarm-video-player" controls autoplay></video>
<button class="video-close-btn" id="alarm-video-close">关闭</button>
</div>
</div>
<!-- 恢复出厂设置确认模态框 -->
<div class="reset-confirm-modal" id="reset-confirm-modal">
<div class="reset-confirm-content">
<h2>⚠️ 警告</h2>
<p>少年,难道你要就此止步吗?</p>
<div class="reset-confirm-buttons">
<button class="btn-keep-going" id="btn-keep-going">我不会放弃 💪</button>
<button class="btn-give-up" id="btn-give-up-confirm">我做不到 😔</button>
</div>
</div>
</div>
<!-- 小卡片动画模态框 -->
<div class="cards-animation-modal" id="cards-animation-modal">
<div class="cards-board" id="cards-board"></div>
<button class="cards-close-btn" id="cards-close-btn">✕ 关闭</button>
</div>
<!-- 庆祝视频播放模态框 -->
<div class="video-modal" id="celebrate-video-modal">
<div class="video-modal-content">
<video id="celebrate-video-player" controls autoplay></video>
<button class="video-close-btn" id="celebrate-video-close">关闭</button>
</div>
</div>
<!-- 鼓励视频播放模态框 -->
<div class="video-modal" id="encourage-video-modal">
<div class="video-modal-content">
<video id="encourage-video-player" controls autoplay></video>
<button class="video-close-btn" id="encourage-video-close">关闭</button>
</div>
</div>
<!-- 闹钟音频元素 -->
<audio id="alarm-audio" preload="auto"></audio>
<!-- 鼓励动画音频元素 -->
<audio id="cards-audio" preload="auto"></audio>
<script>
// ==================== IndexedDB 操作封装 ====================
const DB_NAME = 'CelebrateAppDB';
const DB_VERSION = 2;
const STORE_NAME = 'mediaFiles';
const MAX_FILE_SIZE = 100 * 1024 * 1024;
function openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME, { keyPath: 'id' });
}
};
});
}
async function saveMediaFile(id, file, onProgress) {
const db = await openDB();
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onprogress = (e) => {
if (e.lengthComputable && onProgress) {
onProgress(Math.round((e.loaded / e.total) * 100));
}
};
reader.onload = () => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const data = {
id: id,
arrayBuffer: reader.result,
name: file.name,
type: file.type,
size: file.size,
timestamp: Date.now()
};
const request = store.put(data);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
};
reader.onerror = () => reject(reader.error);
reader.readAsArrayBuffer(file);
});
}
async function getMediaFile(id) {
try {
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
} catch (err) {
console.error('获取媒体文件失败:', err);
return null;
}
}
async function deleteMediaFile(id) {
try {
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
} catch (err) {
console.error('删除媒体文件失败:', err);
}
}
function createBlobURL(arrayBuffer, mimeType) {
const blob = new Blob([arrayBuffer], { type: mimeType });
return URL.createObjectURL(blob);
}
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
}
// ==================== 网络时间同步 ====================
let networkTimeOffset = 0;
async function syncNetworkTime() {
try {
const response = await fetch('https://worldtimeapi.org/api/ip');
const data = await response.json();
const serverTime = new Date(data.datetime);
const localTime = new Date();
networkTimeOffset = serverTime - localTime;
console.log('网络时间同步成功,偏移量:', networkTimeOffset, 'ms');
return true;
} catch (err) {
console.log('网络时间同步失败,使用本地时间');
networkTimeOffset = 0;
return false;
}
}
function getCurrentTime() {
return new Date(Date.now() + networkTimeOffset);
}
// ==================== 全局变量定义 ====================
let today = getCurrentTime();
let currentYear = today.getFullYear();
let currentMonth = today.getMonth();
let celebrateBlobURL = null;
let encourageBlobURL = null;
let alarmBlobURLs = {};
let cardsBlobURL = null;
// DOM元素
const calendarGrid = document.getElementById('calendar');
const monthYearDisplay = document.getElementById('month-year');
const dateInfo = document.getElementById('date-info');
const celebrateBtn = document.getElementById('celebrate-btn');
const audio = document.getElementById('celebrate-audio');
const encourageAudio = document.getElementById('encourage-audio');
const celebrateModal = document.getElementById('celebrate-modal');
const celebrateQuote = document.getElementById('celebrate-quote');
const celebrateClose = document.getElementById('celebrate-close');
const monthCompleted = document.getElementById('month-completed');
const longestStreak = document.getElementById('longest-streak');
const currentStreak = document.getElementById('current-streak');
const goalsList = document.getElementById('goals-list');
const todayTaskDisplay = document.getElementById('today-task-display');
const allCompleteDisplayWrapper = document.getElementById('all-complete-display-wrapper');
const giveUpBtn = document.getElementById('give-up-btn');
// 闹钟相关
const alarmModal = document.getElementById('alarm-modal');
const alarmList = document.getElementById('alarm-list');
const alarmAlertModal = document.getElementById('alarm-alert-modal');
const alarmAlertTime = document.getElementById('alarm-alert-time');
const alarmAlertNote = document.getElementById('alarm-alert-note');
const alarmAudio = document.getElementById('alarm-audio');
const alarmVideoModal = document.getElementById('alarm-video-modal');
const alarmVideoPlayer = document.getElementById('alarm-video-player');
// 恢复出厂设置
const resetConfirmModal = document.getElementById('reset-confirm-modal');
// 小卡片动画
const cardsAnimationModal = document.getElementById('cards-animation-modal');
const cardsBoard = document.getElementById('cards-board');
const cardsAudio = document.getElementById('cards-audio');
// 视频模态框
const celebrateVideoModal = document.getElementById('celebrate-video-modal');
const celebrateVideoPlayer = document.getElementById('celebrate-video-player');
const encourageVideoModal = document.getElementById('encourage-video-modal');
const encourageVideoPlayer = document.getElementById('encourage-video-player');
// 模态框
const dailyTaskModal = document.getElementById('daily-task-modal');
const taskHead = document.getElementById('task-head');
const taskRows = document.getElementById('task-rows');
const settingsModal = document.getElementById('settings-modal');
const localCelebrateFile = document.getElementById('local-celebrate-file');
const selectedFileName = document.getElementById('selected-file-name');
const customQuotesTextarea = document.getElementById('custom-quotes');
const quotesList = document.getElementById('quotes-list');
const longTermModal = document.getElementById('long-term-modal');
const goalHead = document.getElementById('goal-head');
const goalRows = document.getElementById('goal-rows');
const encourageSettingsModal = document.getElementById('encourage-settings-modal');
// 本地存储数据
let dailyTaskData = JSON.parse(localStorage.getItem('dailyTaskData') || '{"fields":["任务","预计时间"],"rows":[]}');
let currentTaskFields = [...dailyTaskData.fields];
let currentTaskRows = dailyTaskData.rows.map(r => ({...r}));
let completedDates = JSON.parse(localStorage.getItem('completedDates') || '[]');
let longTermGoals = JSON.parse(localStorage.getItem('longTermGoals') || '{"rows":[]}');
// 兼容旧数据格式
if (longTermGoals.fields) {
// 转换旧格式到新格式
const newRows = longTermGoals.rows.map(row => ({
name: row["目标名称"] || '',
startDate: row["开始日期"] || '',
endDate: row["结束日期"] || '',
status: 'running',
notified: false
}));
longTermGoals = { rows: newRows };
localStorage.setItem('longTermGoals', JSON.stringify(longTermGoals));
}
let quotes = JSON.parse(localStorage.getItem('celebrateQuotes') || '["太棒了!你做到了!","坚持就是胜利!","今天的努力是明天的收获!"]');
let alarms = JSON.parse(localStorage.getItem('alarms') || '[]');
// 庆祝设置
let celebrateSettings = JSON.parse(localStorage.getItem('celebrateSettings') || '{"mode":"audio"}');
// 跨日目标完成提醒设置
let goalMediaSettings = JSON.parse(localStorage.getItem('goalMediaSettings') || JSON.stringify({
mode: "animation"
}));
let goalBlobURL = null;
// 鼓励/小卡片设置
let encourageSettings = JSON.parse(localStorage.getItem('encourageSettings') || JSON.stringify({
mode: "animation",
bgColor: "#fff0f3",
messages: ["少年,振作起来!", "你可以的!", "加油!"],
emojis: ["💕", "💗", "💖", "💝", "💓", "✨", "🌸", "🦋"]
}));
let currentGoalFields = [];
let currentGoalRows = [];
let pendingCelebrateFile = null;
let pendingEncourageFile = null;
let pendingAlarmFile = null;
let pendingCardsFile = null;
// ==================== 实时时钟 ====================
function updateRealtimeClock() {
const now = getCurrentTime();
const clockEl = document.getElementById('realtime-clock');
const dateEl = document.getElementById('realtime-date');
if (clockEl) {
clockEl.textContent = now.toLocaleTimeString('zh-CN', { hour12: false });
}
if (dateEl) {
const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
dateEl.textContent = `${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日 ${weekdays[now.getDay()]}`;
}
// 更新today
today = now;
}
// 同步时间按钮
document.getElementById('sync-time-btn')?.addEventListener('click', async () => {
const btn = document.getElementById('sync-time-btn');
btn.textContent = '⏳';
btn.disabled = true;
const success = await syncNetworkTime();
btn.textContent = success ? '✅' : '❌';
setTimeout(() => {
btn.textContent = '🔄';
btn.disabled = false;
}, 2000);
updateRealtimeClock();
renderCalendar(currentYear, currentMonth);
});
// 启动时钟
setInterval(updateRealtimeClock, 1000);
// ==================== 闹钟功能 ====================
function renderAlarmList() {
alarmList.innerHTML = '';
if (alarms.length === 0) {
alarmList.innerHTML = '<p class="empty-alarm">暂无闹钟,点击上方添加</p>';
return;
}
alarms.forEach((alarm, index) => {
const div = document.createElement('div');
div.className = 'alarm-item';
const modeText = alarm.mode === 'video' ? '🎥 视频' : '🔔 弹窗';
div.innerHTML = `
<div class="alarm-item-info">
<div class="alarm-item-time">${alarm.datetime}</div>
<div class="alarm-item-note">
${alarm.note || '无备注'}
<span class="alarm-mode-tag">${modeText}</span>
</div>
</div>
<button class="alarm-item-delete" data-index="${index}">×</button>
`;
alarmList.appendChild(div);
});
alarmList.querySelectorAll('.alarm-item-delete').forEach(btn => {
btn.onclick = async () => {
const idx = parseInt(btn.dataset.index);
const alarmId = `alarm_${alarms[idx].id}`;
await deleteMediaFile(alarmId);
alarms.splice(idx, 1);
localStorage.setItem('alarms', JSON.stringify(alarms));
renderAlarmList();
};
});
}
document.querySelector('.alarm-btn')?.addEventListener('click', () => {
renderAlarmList();
alarmModal.style.display = 'flex';
});
document.getElementById('alarm-cancel')?.addEventListener('click', () => {
alarmModal.style.display = 'none';
});
document.getElementById('add-alarm-btn')?.addEventListener('click', async () => {
const dateInput = document.getElementById('alarm-date');
const timeInput = document.getElementById('alarm-time');
const noteInput = document.getElementById('alarm-note');
const modeInputs = document.querySelectorAll('input[name="alarm-mode"]');
const fileInput = document.getElementById('alarm-audio-file');
if (!dateInput.value || !timeInput.value) {
alert('请选择日期和时间!');
return;
}
let mode = 'audio';
modeInputs.forEach(input => {
if (input.checked) mode = input.value;
});
const alarmId = Date.now();
const alarm = {
id: alarmId,
datetime: `${dateInput.value} ${timeInput.value}`,
note: noteInput.value,
mode: mode,
triggered: false
};
// 保存音频文件
if (fileInput.files[0]) {
try {
await saveMediaFile(`alarm_${alarmId}`, fileInput.files[0]);
} catch (err) {
console.error('保存闹钟音频失败:', err);
}
}
alarms.push(alarm);
localStorage.setItem('alarms', JSON.stringify(alarms));
// 清空输入
dateInput.value = '';
timeInput.value = '';
noteInput.value = '';
fileInput.value = '';
document.getElementById('alarm-file-name').style.display = 'none';
renderAlarmList();
alert('闹钟已添加!');
});
// 闹钟检查
async function checkAlarms() {
const now = getCurrentTime();
const nowStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
for (let i = 0; i < alarms.length; i++) {
const alarm = alarms[i];
if (alarm.triggered) continue;
// 比较时间(精确到秒)
const alarmTime = alarm.datetime.includes(':') && alarm.datetime.split(':').length === 3
? alarm.datetime
: alarm.datetime + ':00';
if (alarmTime <= nowStr) {
alarm.triggered = true;
localStorage.setItem('alarms', JSON.stringify(alarms));
// 获取音频
const mediaData = await getMediaFile(`alarm_${alarm.id}`);
if (alarm.mode === 'video' && mediaData && mediaData.type.startsWith('video')) {
// 视频模式
const blobURL = createBlobURL(mediaData.arrayBuffer, mediaData.type);
alarmVideoPlayer.src = blobURL;
alarmVideoModal.style.display = 'flex';
alarmVideoPlayer.play();
} else {
// 音频模式
if (mediaData) {
const blobURL = createBlobURL(mediaData.arrayBuffer, mediaData.type);
alarmAudio.src = blobURL;
}
alarmAlertTime.textContent = alarm.datetime;
alarmAlertNote.textContent = alarm.note || '时间到了!';
alarmAlertModal.style.display = 'flex';
alarmAudio.play().catch(err => console.log('闹钟音频播放失败'));
}
break;
}
}
}
setInterval(checkAlarms, 1000);
document.getElementById('alarm-alert-close')?.addEventListener('click', () => {
alarmAlertModal.style.display = 'none';
alarmAudio.pause();
alarmAudio.currentTime = 0;
});
document.getElementById('alarm-video-close')?.addEventListener('click', () => {
alarmVideoModal.style.display = 'none';
alarmVideoPlayer.pause();
alarmVideoPlayer.src = '';
});
// 闹钟文件选择显示
function updateFileDisplay(type, fileName, fileSize) {
const container = type === 'celebrate' ? selectedFileName :
type === 'cards' ? document.getElementById('selected-encourage-file-name') :
document.getElementById('selected-encourage-file-name');
if (!container) return;
if (fileName) {
const isVideo = fileName.match(/\.(mp4|webm|mov|avi)$/i);
const icon = isVideo ? '🎥' : '🎵';
container.innerHTML = `
<div class="file-info">
<span class="file-icon">${icon}</span>
<div class="file-details">
<span class="file-name-text">${fileName}</span>
<span class="file-size-text">${formatFileSize(fileSize)}</span>
</div>
</div>
<button class="delete-btn" onclick="deleteFile('${type}')">×</button>
`;
container.style.display = 'flex';
} else {
container.style.display = 'none';
}
}
// ==================== 恢复出厂设置 ====================
document.querySelector('.reset-btn')?.addEventListener('click', () => {
resetConfirmModal.style.display = 'flex';
});
document.getElementById('btn-keep-going')?.addEventListener('click', () => {
resetConfirmModal.style.display = 'none';
});
document.getElementById('btn-give-up-confirm')?.addEventListener('click', async () => {
// 清除所有localStorage
localStorage.clear();
// 清除IndexedDB
const db = await openDB();
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
store.clear();
alert('所有数据已清除,页面即将刷新...');
location.reload();
});
// ==================== 小卡片动画功能 ====================
const cardColorStyles = [
'color-pink', 'color-purple', 'color-blue', 'color-mint',
'color-yellow', 'color-orange', 'color-green', 'color-coral',
'color-lavender', 'color-white'
];
function randomFrom(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
function randomRange(min, max) {
return min + Math.random() * (max - min);
}
function createCard(board, x, y, content, layer) {
const card = document.createElement('div');
const rotation = randomRange(-18, 18);
const scale = randomRange(0.9, 1.1);
const colorStyle = randomFrom(cardColorStyles);
card.className = `card ${colorStyle} float-in`;
const isMobile = window.innerWidth <= 768;
const cardWidth = Math.round((isMobile ? 75 : 105) * 1.265);
const cardHeight = Math.round((isMobile ? 48 : 65) * 1.265);
const baseTransform = `translate(${x}px, ${y}px)`;
const floatY = randomRange(-60, 60);
const floatR = randomRange(-30, 30);
const delay = randomRange(0, 0.5);
card.style.cssText = `
width: ${cardWidth}px;
height: ${cardHeight}px;
transform: ${baseTransform} rotate(${rotation}deg) scale(${scale});
--base-transform: ${baseTransform};
--rotation: ${rotation}deg;
--delay: ${delay}s;
--float-y: ${floatY}px;
--float-r: ${floatR}deg;
z-index: ${layer};
`;
card.innerHTML = `<div class="card-body">${content}</div>`;
board.appendChild(card);
return card;
}
async function runCardsAnimation() {
cardsBoard.innerHTML = '';
const quotes = goalCardsSettings.quotes.length > 0 ? goalCardsSettings.quotes : ["目标达成!🎉", "你太棒了!💪", "坚持就是胜利!🔥"];
const emojis = goalCardsSettings.emojis.length > 0 ? goalCardsSettings.emojis : ["🎉", "✨", "🔥", "💪", "❤️", "🚀"];
// 设置背景颜色
if (encourageSettings.bgColor) {
cardsAnimationModal.style.background = encourageSettings.bgColor;
}
const isMobile = window.innerWidth <= 768;
const cardWidth = Math.round((isMobile ? 75 : 105) * 1.265);
const cardHeight = Math.round((isMobile ? 48 : 65) * 1.265);
const screenArea = window.innerWidth * window.innerHeight;
const cardsCount = Math.ceil(screenArea / (cardWidth * cardHeight * 1.1));
for (let i = 0; i < cardsCount; i++) {
setTimeout(() => {
const x = randomRange(0, window.innerWidth - cardWidth);
const y = randomRange(0, window.innerHeight - cardHeight);
const useEmoji = Math.random() > 0.5;
const content = useEmoji ? randomFrom(emojis) : randomFrom(messages);
createCard(cardsBoard, x, y, content, 100 + i);
}, i * 30);
}
}
// 累了就来这里按钮
giveUpBtn?.addEventListener('click', async () => {
if (encourageSettings.mode === 'video') {
// 视频模式
const mediaData = await getMediaFile('cards');
if (mediaData && mediaData.type.startsWith('video')) {
const blobURL = createBlobURL(mediaData.arrayBuffer, mediaData.type);
encourageVideoPlayer.src = blobURL;
encourageVideoModal.style.display = 'flex';
encourageVideoPlayer.play();
} else {
alert('未设置视频文件,请在💪设置中上传视频');
}
} else {
// 动画模式
cardsAnimationModal.style.display = 'block';
// 播放音频
const mediaData = await getMediaFile('cards');
if (mediaData) {
if (cardsBlobURL) URL.revokeObjectURL(cardsBlobURL);
cardsBlobURL = createBlobURL(mediaData.arrayBuffer, mediaData.type);
cardsAudio.src = cardsBlobURL;
cardsAudio.play().catch(err => console.log('小卡片音频播放失败'));
}
runCardsAnimation();
}
});
document.getElementById('cards-close-btn')?.addEventListener('click', () => {
cardsAnimationModal.style.display = 'none';
cardsAudio.pause();
cardsAudio.currentTime = 0;
cardsBoard.innerHTML = '';
});
// 跨日目标视频关闭
document.getElementById('goal-video-close')?.addEventListener('click', () => {
const goalVideoModal = document.getElementById('goal-video-modal');
const goalVideoPlayer = document.getElementById('goal-video-player');
if (goalVideoModal) goalVideoModal.style.display = 'none';
if (goalVideoPlayer) {
goalVideoPlayer.pause();
goalVideoPlayer.src = '';
}
// 显示确认弹窗
showGoalConfirmButtons();
});
// ==================== 鼓励设置(💪按钮) ====================
document.querySelector('.encourage-settings-btn')?.addEventListener('click', async () => {
// 填充当前设置
document.getElementById('custom-encourage-quotes').value = (encourageSettings.messages || []).join('\n');
document.getElementById('encourage-emojis').value = (encourageSettings.emojis || []).join(',');
document.getElementById('encourage-bg-color').value = encourageSettings.bgColor || '#fff0f3';
const modeRadios = document.querySelectorAll('input[name="encourage-mode"]');
modeRadios.forEach(radio => {
radio.checked = radio.value === encourageSettings.mode;
});
// 显示已选择的文件
const mediaData = await getMediaFile('cards');
if (mediaData) {
document.getElementById('selected-encourage-file-name').innerHTML = `✅ ${mediaData.name} (${formatFileSize(mediaData.size)})`;
document.getElementById('selected-encourage-file-name').style.display = 'block';
} else {
document.getElementById('selected-encourage-file-name').style.display = 'none';
}
encourageSettingsModal.style.display = 'flex';
});
document.getElementById('encourage-settings-save')?.addEventListener('click', async () => {
const file = document.getElementById('local-encourage-file').files[0];
if (file) {
try {
await saveMediaFile('cards', file);
} catch (err) {
console.error('保存文件失败:', err);
}
}
// 保存设置
const messagesText = document.getElementById('custom-encourage-quotes').value;
const emojisText = document.getElementById('encourage-emojis')?.value || '';
const bgColor = document.getElementById('encourage-bg-color')?.value || '#fff0f3';
let mode = 'animation';
document.querySelectorAll('input[name="encourage-mode"]').forEach(radio => {
if (radio.checked) mode = radio.value;
});
encourageSettings = {
mode: mode,
bgColor: bgColor,
messages: messagesText.split('\n').map(s => s.trim()).filter(s => s),
emojis: emojisText.split(/[,,\n]/).map(s => s.trim()).filter(s => s)
};
localStorage.setItem('encourageSettings', JSON.stringify(encourageSettings));
alert('鼓励设置已保存!');
encourageSettingsModal.style.display = 'none';
});
document.getElementById('encourage-settings-cancel')?.addEventListener('click', () => {
encourageSettingsModal.style.display = 'none';
});
// ==================== 庆祝功能 ====================
celebrateBtn?.addEventListener('click', async () => {
const dateStr = today.toISOString().split('T')[0];
if (!completedDates.includes(dateStr)) {
completedDates.push(dateStr);
localStorage.setItem('completedDates', JSON.stringify(completedDates));
}
renderCalendar(currentYear, currentMonth);
if (celebrateSettings.mode === 'video') {
// 视频模式
const mediaData = await getMediaFile('celebrate');
if (mediaData && mediaData.type.startsWith('video')) {
const blobURL = createBlobURL(mediaData.arrayBuffer, mediaData.type);
celebrateVideoPlayer.src = blobURL;
celebrateVideoModal.style.display = 'flex';
celebrateVideoPlayer.play();
} else {
alert('未设置视频文件,请在✨设置中上传视频');
}
} else {
// 音频模式
audio.currentTime = 0;
audio.play().catch(err => console.log('音频播放失败'));
const randomQuote = quotes[Math.floor(Math.random() * quotes.length)];
celebrateQuote.textContent = randomQuote;
celebrateModal.style.display = 'flex';
}
});
celebrateClose?.addEventListener('click', () => {
celebrateModal.style.display = 'none';
audio.pause();
audio.currentTime = 0;
});
document.getElementById('celebrate-video-close')?.addEventListener('click', () => {
celebrateVideoModal.style.display = 'none';
celebrateVideoPlayer.pause();
celebrateVideoPlayer.src = '';
});
// ==================== 庆祝设置(✨按钮) ====================
document.querySelector('.settings-btn')?.addEventListener('click', async () => {
localCelebrateFile.value = '';
const mediaData = await getMediaFile('celebrate');
if (mediaData) {
selectedFileName.innerHTML = `✅ ${mediaData.name} (${formatFileSize(mediaData.size)})`;
selectedFileName.style.display = 'block';
} else {
selectedFileName.style.display = 'none';
}
customQuotesTextarea.value = quotes.join('\n');
// 设置播放模式
const modeRadios = document.querySelectorAll('input[name="celebrate-mode"]');
modeRadios.forEach(radio => {
radio.checked = radio.value === celebrateSettings.mode;
});
settingsModal.style.display = 'flex';
});
document.getElementById('settings-save')?.addEventListener('click', async () => {
const file = localCelebrateFile.files[0];
if (file) {
try {
await saveMediaFile('celebrate', file);
await initCelebrateMedia();
} catch (err) {
console.error('保存文件失败:', err);
}
}
const newQuotes = customQuotesTextarea.value.split('\n').map(q => q.trim()).filter(q => q);
if (newQuotes.length > 0) {
quotes = newQuotes;
localStorage.setItem('celebrateQuotes', JSON.stringify(quotes));
}
// 保存播放模式
let mode = 'audio';
document.querySelectorAll('input[name="celebrate-mode"]').forEach(radio => {
if (radio.checked) mode = radio.value;
});
celebrateSettings.mode = mode;
localStorage.setItem('celebrateSettings', JSON.stringify(celebrateSettings));
alert('庆祝设置已保存!');
settingsModal.style.display = 'none';
});
document.getElementById('settings-cancel')?.addEventListener('click', () => {
settingsModal.style.display = 'none';
});
// ==================== 初始化媒体文件 ====================
async function initCelebrateMedia() {
const mediaData = await getMediaFile('celebrate');
if (mediaData) {
if (celebrateBlobURL) URL.revokeObjectURL(celebrateBlobURL);
celebrateBlobURL = createBlobURL(mediaData.arrayBuffer, mediaData.type);
audio.src = celebrateBlobURL;
}
}
// ==================== 日历相关功能 ====================
function getDaysDifference(selectedDate) {
// 去除时间部分,只比较日期
const selected = new Date(selectedDate.getFullYear(), selectedDate.getMonth(), selectedDate.getDate());
const now = new Date(today.getFullYear(), today.getMonth(), today.getDate());
const diffTime = selected - now;
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
if (diffDays === 0) return "今天";
return diffDays > 0 ? `还有 ${diffDays} 天` : `已经过去 ${-diffDays} 天`;
}
function calculateStats() {
const year = currentYear;
const month = currentMonth;
const daysInMonth = new Date(year, month + 1, 0).getDate();
let monthCount = 0;
let current = 0;
let longest = 0;
let temp = 0;
for (let d = 1; d <= daysInMonth; d++) {
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
if (completedDates.includes(dateStr)) {
monthCount++;
temp++;
longest = Math.max(longest, temp);
if (new Date(year, month, d) <= today) {
current = temp;
}
} else {
temp = 0;
}
}
monthCompleted.innerHTML = `本月已完成 <strong>${monthCount}</strong> 天`;
longestStreak.innerHTML = `最长连续完成 <strong>${longest}</strong> 天`;
currentStreak.innerHTML = `当前连续 <strong>${current}</strong> 天`;
}
function formatCountdown(ms) {
if (ms <= 0) return '已结束';
let totalSeconds = Math.floor(ms / 1000);
const years = Math.floor(totalSeconds / (365 * 24 * 60 * 60));
totalSeconds -= years * 365 * 24 * 60 * 60;
const months = Math.floor(totalSeconds / (30 * 24 * 60 * 60));
totalSeconds -= months * 30 * 24 * 60 * 60;
const days = Math.floor(totalSeconds / (24 * 60 * 60));
totalSeconds -= days * 24 * 60 * 60;
const hours = Math.floor(totalSeconds / (60 * 60));
totalSeconds -= hours * 60 * 60;
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
let result = '';
if (years > 0) result += `${years}年`;
if (months > 0) result += `${months}月`;
if (days > 0) result += `${days}日`;
if (hours > 0) result += `${hours}时`;
if (minutes > 0) result += `${minutes}分`;
result += `${seconds}秒`;
return `还剩${result}`;
}
function renderLongTermGoals() {
goalsList.innerHTML = '';
if (longTermGoals.rows.length === 0) {
goalsList.innerHTML = '<p class="empty-state">暂无跨日目标<br>点击左上角 🎯 添加</p>';
return;
}
if (window.countdownTimer) clearInterval(window.countdownTimer);
const goalElements = [];
longTermGoals.rows.forEach((goal, index) => {
const start = new Date(goal["开始日期"]);
const end = new Date(goal["结束日期"]);
if (isNaN(start) || isNaN(end)) return;
const div = document.createElement('div');
div.className = 'goal-item';
const totalMs = end - start;
const currentMs = Math.max(0, Math.min(totalMs, new Date() - start));
const progressPercent = totalMs > 0 ? Math.min(100, (currentMs / totalMs) * 100) : 0;
div.innerHTML = `
<div class="goal-title">${goal["目标名称"] || '未命名目标'}</div>
<div class="goal-dates">${goal["开始日期"]} ~ ${goal["结束日期"]}</div>
<div class="countdown" id="countdown-${index}">${formatCountdown(end - new Date())}</div>
<div class="progress-bar"><div class="progress-fill" style="width:${progressPercent}%"></div></div>
`;
goalsList.appendChild(div);
goalElements.push({
element: document.getElementById(`countdown-${index}`),
endDate: end
});
});
window.countdownTimer = setInterval(() => {
goalElements.forEach(item => {
if (item.element) {
item.element.textContent = formatCountdown(item.endDate - new Date());
}
});
}, 1000);
}
function renderCalendar(year, month) {
calendarGrid.innerHTML = '';
monthYearDisplay.textContent = `${year}年${month + 1}月`;
const firstDay = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const daysInPrevMonth = new Date(year, month, 0).getDate();
for (let i = firstDay - 1; i >= 0; i--) {
const dayNum = daysInPrevMonth - i;
const actualDate = new Date(year, month - 1, dayNum);
createDayElement(dayNum, actualDate, true);
}
for (let day = 1; day <= daysInMonth; day++) {
const actualDate = new Date(year, month, day);
createDayElement(day, actualDate, false);
}
const totalCells = calendarGrid.children.length;
const remaining = 42 - totalCells;
for (let i = 1; i <= remaining; i++) {
const actualDate = new Date(year, month + 1, i);
createDayElement(i, actualDate, true);
}
calculateStats();
renderLongTermGoals();
updateCelebrateBtn();
}
function createDayElement(displayNum, actualDate, isOtherMonth) {
const dayEl = document.createElement('div');
dayEl.classList.add('day');
if (isOtherMonth) dayEl.classList.add('other-month');
const numSpan = document.createElement('span');
numSpan.className = 'day-number';
numSpan.textContent = displayNum;
dayEl.appendChild(numSpan);
const dateStr = actualDate.toISOString().split('T')[0];
if (completedDates.includes(dateStr)) {
const mark = document.createElement('div');
mark.className = 'completed-mark';
mark.textContent = '✓';
dayEl.appendChild(mark);
}
if (actualDate.toDateString() === today.toDateString()) {
dayEl.classList.add('today');
}
dayEl.addEventListener('click', (e) => {
e.stopPropagation();
document.querySelectorAll('.day.selected').forEach(el => el.classList.remove('selected'));
if (!dayEl.classList.contains('today')) dayEl.classList.add('selected');
const formatted = actualDate.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' });
const diff = getDaysDifference(actualDate);
dateInfo.textContent = `${formatted} —— ${diff}`;
});
calendarGrid.appendChild(dayEl);
}
function updateCelebrateBtn() {
const isCurrentMonth = currentYear === today.getFullYear() && currentMonth === today.getMonth();
const dateStr = today.toISOString().split('T')[0];
const isCompleted = completedDates.includes(dateStr);
celebrateBtn.disabled = !isCurrentMonth || isCompleted;
celebrateBtn.textContent = isCompleted ? '今日已完成!✨' : (isCurrentMonth ? '完成今日任务!🎉' : '请切换到本月');
}
// ==================== 今日任务功能 ====================
document.querySelector('.daily-task-btn')?.addEventListener('click', () => {
currentTaskFields = [...dailyTaskData.fields];
currentTaskRows = dailyTaskData.rows.map(r => ({...r}));
renderDailyTaskTable();
dailyTaskModal.style.display = 'flex';
});
function renderDailyTaskTable() {
taskHead.innerHTML = '';
taskRows.innerHTML = '';
const tr = document.createElement('tr');
currentTaskFields.forEach(field => {
const th = document.createElement('th');
th.textContent = field;
const del = document.createElement('button');
del.textContent = '×';
del.className = 'delete-field';
del.onclick = () => {
if (confirm(`删除字段 "${field}"?`)) {
const idx = currentTaskFields.indexOf(field);
currentTaskFields.splice(idx, 1);
currentTaskRows.forEach(row => delete row[field]);
renderDailyTaskTable();
}
};
th.appendChild(del);
tr.appendChild(th);
});
const thAction = document.createElement('th');
thAction.textContent = '操作';
tr.appendChild(thAction);
taskHead.appendChild(tr);
currentTaskRows.forEach(row => addDailyTaskRow(row));
if (currentTaskRows.length === 0) addDailyTaskRow({});
}
function addDailyTaskRow(row = {}) {
const tr = document.createElement('tr');
currentTaskFields.forEach(field => {
const td = document.createElement('td');
const input = document.createElement('input');
input.type = 'text';
input.value = row[field] || '';
input.placeholder = field;
td.appendChild(input);
tr.appendChild(td);
});
const delTd = document.createElement('td');
const delBtn = document.createElement('button');
delBtn.textContent = '×';
delBtn.style = 'background:none;border:none;color:#ff4444;cursor:pointer;font-size:22px;font-weight:bold;padding:0 8px;';
delBtn.onclick = () => tr.remove();
delTd.appendChild(delBtn);
tr.appendChild(delTd);
taskRows.appendChild(tr);
}
document.getElementById('add-task-field')?.addEventListener('click', () => {
const name = prompt('新字段名称:');
if (name && !currentTaskFields.includes(name)) {
currentTaskFields.push(name);
renderDailyTaskTable();
}
});
document.getElementById('add-task-row')?.addEventListener('click', () => addDailyTaskRow({}));
document.getElementById('import-text')?.addEventListener('click', () => {
const text = prompt('粘贴文本(每行一个任务,用空格或制表符分隔字段):', '');
if (text) {
const lines = text.split('\n');
lines.forEach(line => {
const parts = line.trim().split(/\s+/);
if (parts.length > 0) {
const row = {};
parts.forEach((part, i) => {
const field = currentTaskFields[i] || `字段${i+1}`;
row[field] = part;
});
currentTaskRows.push(row);
}
});
renderDailyTaskTable();
}
});
document.getElementById('daily-task-save')?.addEventListener('click', () => {
const newRows = [];
taskRows.querySelectorAll('tr').forEach(tr => {
const inputs = tr.querySelectorAll('input[type="text"]');
const row = {};
currentTaskFields.forEach((f, i) => {
const val = inputs[i]?.value.trim();
if (val) row[f] = val;
});
if (Object.keys(row).length > 0) newRows.push(row);
});
dailyTaskData = { fields: currentTaskFields, rows: newRows };
localStorage.setItem('dailyTaskData', JSON.stringify(dailyTaskData));
renderTodayTaskDisplay();
dailyTaskModal.style.display = 'none';
});
document.getElementById('daily-task-cancel')?.addEventListener('click', () => {
dailyTaskModal.style.display = 'none';
});
function renderTodayTaskDisplay() {
todayTaskDisplay.innerHTML = '';
if (dailyTaskData.rows.length === 0) {
todayTaskDisplay.innerHTML = '<tr><td colspan="3" style="text-align:center;color:#999;padding:30px;">点击 📝 设置今日任务</td></tr>';
allCompleteDisplayWrapper.style.display = 'none';
return;
}
const thead = document.createElement('thead');
const trHead = document.createElement('tr');
dailyTaskData.fields.forEach(f => {
const th = document.createElement('th');
th.textContent = f;
trHead.appendChild(th);
});
const thCheck = document.createElement('th');
thCheck.textContent = '完成';
trHead.appendChild(thCheck);
thead.appendChild(trHead);
todayTaskDisplay.appendChild(thead);
const tbody = document.createElement('tbody');
dailyTaskData.rows.forEach((row, index) => {
const tr = document.createElement('tr');
if (row.completed) tr.classList.add('completed');
dailyTaskData.fields.forEach(f => {
const td = document.createElement('td');
td.textContent = row[f] || '';
tr.appendChild(td);
});
const tdCheck = document.createElement('td');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = row.completed || false;
checkbox.onchange = () => {
dailyTaskData.rows[index].completed = checkbox.checked;
localStorage.setItem('dailyTaskData', JSON.stringify(dailyTaskData));
if (checkbox.checked) tr.classList.add('completed');
else tr.classList.remove('completed');
checkAllCompleted();
};
tdCheck.appendChild(checkbox);
tr.appendChild(tdCheck);
tbody.appendChild(tr);
});
todayTaskDisplay.appendChild(tbody);
allCompleteDisplayWrapper.style.display = 'block';
checkAllCompleted();
}
function checkAllCompleted() {
const allCompleted = dailyTaskData.rows.length > 0 && dailyTaskData.rows.every(r => r.completed);
celebrateBtn.disabled = !allCompleted;
celebrateBtn.textContent = allCompleted ? '完成今日任务!🎉' : '请完成所有任务后再庆祝';
}
document.getElementById('all-complete-display')?.addEventListener('click', () => {
dailyTaskData.rows.forEach((row, index) => {
row.completed = true;
const checkbox = todayTaskDisplay.querySelectorAll('input[type="checkbox"]')[index];
if (checkbox) checkbox.checked = true;
const tr = checkbox?.closest('tr');
if (tr) tr.classList.add('completed');
});
localStorage.setItem('dailyTaskData', JSON.stringify(dailyTaskData));
checkAllCompleted();
});
// ==================== 跨日目标功能 ====================
const goalCompleteModal = document.getElementById('goal-complete-modal');
const goalCompleteName = document.getElementById('goal-complete-name');
const goalCompleteText = document.getElementById('goal-complete-text');
const goalCompleteButtons = document.getElementById('goal-complete-buttons');
let currentCompletingGoalIndex = -1;
// 格式化日期时间显示
function formatDateTime(dateTimeStr) {
if (!dateTimeStr) return '';
const date = new Date(dateTimeStr);
if (isNaN(date)) return dateTimeStr;
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}年${month}月${day}日 ${hours}:${minutes}:${seconds}`;
}
// 渲染目标列表(在模态框内)
function renderGoalListInModal() {
const container = document.getElementById('goal-list-container');
if (!container) return;
container.innerHTML = '';
if (longTermGoals.rows.length === 0) {
container.innerHTML = '<p class="empty-goal">暂无目标,点击上方添加</p>';
return;
}
longTermGoals.rows.forEach((goal, index) => {
const div = document.createElement('div');
div.className = 'goal-list-item';
// 添加状态样式
if (goal.status === 'completed') {
div.classList.add('completed');
} else if (goal.status === 'failed') {
div.classList.add('failed');
}
const startStr = formatDateTime(goal.startDate);
const endStr = formatDateTime(goal.endDate);
let statusTag = '';
if (goal.status === 'completed') {
statusTag = '<span class="goal-status-tag completed">✓ 已完成</span>';
} else if (goal.status === 'failed') {
statusTag = '<span class="goal-status-tag failed">✗ 未完成</span>';
} else {
statusTag = '<span class="goal-status-tag running">进行中</span>';
}
div.innerHTML = `
<div class="goal-item-info">
<div class="goal-item-name">${goal.name || '未命名目标'} ${statusTag}</div>
<div class="goal-item-dates">📅 ${startStr} → ${endStr}</div>
</div>
<button class="goal-item-delete" data-index="${index}">×</button>
`;
container.appendChild(div);
});
// 绑定删除事件
container.querySelectorAll('.goal-item-delete').forEach(btn => {
btn.onclick = () => {
const idx = parseInt(btn.dataset.index);
if (confirm('确定删除这个目标吗?')) {
longTermGoals.rows.splice(idx, 1);
localStorage.setItem('longTermGoals', JSON.stringify(longTermGoals));
renderGoalListInModal();
renderLongTermGoals();
}
};
});
}
// 打开跨日目标模态框
document.querySelector('.long-term-btn')?.addEventListener('click', async () => {
await initGoalMediaDisplay();
renderGoalListInModal();
longTermModal.style.display = 'flex';
});
// 添加新目标
document.getElementById('btn-add-goal')?.addEventListener('click', () => {
const nameInput = document.getElementById('new-goal-name');
const startInput = document.getElementById('new-goal-start');
const endInput = document.getElementById('new-goal-end');
if (!nameInput.value.trim()) {
alert('请输入目标名称!');
return;
}
if (!startInput.value || !endInput.value) {
alert('请选择开始和结束时间!');
return;
}
const newGoal = {
name: nameInput.value.trim(),
startDate: startInput.value,
endDate: endInput.value,
status: 'running',
notified: false
};
longTermGoals.rows.push(newGoal);
localStorage.setItem('longTermGoals', JSON.stringify(longTermGoals));
// 清空输入
nameInput.value = '';
startInput.value = '';
endInput.value = '';
renderGoalListInModal();
renderLongTermGoals();
alert('目标已添加!');
});
// 关闭模态框
document.getElementById('long-term-cancel')?.addEventListener('click', () => {
longTermModal.style.display = 'none';
});
longTermModal?.addEventListener('click', (e) => {
if (e.target === longTermModal) {
longTermModal.style.display = 'none';
}
});
// 渲染右侧跨日目标显示
function renderLongTermGoals() {
goalsList.innerHTML = '';
const activeGoals = longTermGoals.rows.filter(g => g.status === 'running');
if (activeGoals.length === 0) {
goalsList.innerHTML = '<p class="empty-state">暂无进行中的目标<br>点击左上角 🎯 添加</p>';
return;
}
if (window.countdownTimer) clearInterval(window.countdownTimer);
const goalElements = [];
activeGoals.forEach((goal, index) => {
const start = new Date(goal.startDate);
const end = new Date(goal.endDate);
if (isNaN(start) || isNaN(end)) return;
const div = document.createElement('div');
div.className = 'goal-item';
const now = getCurrentTime();
const totalMs = end - start;
const elapsedMs = Math.max(0, Math.min(totalMs, now - start));
const progressPercent = totalMs > 0 ? Math.min(100, (elapsedMs / totalMs) * 100) : 0;
const remainingMs = end - now;
div.innerHTML = `
<div class="goal-title">${goal.name || '未命名目标'}</div>
<div class="goal-dates">${formatDateTime(goal.startDate)} ~ ${formatDateTime(goal.endDate)}</div>
<div class="countdown" id="goal-countdown-${index}">${formatCountdown(remainingMs)}</div>
<div class="progress-bar"><div class="progress-fill" style="width:${progressPercent}%"></div></div>
`;
goalsList.appendChild(div);
goalElements.push({
element: document.getElementById(`goal-countdown-${index}`),
endDate: end,
goal: goal,
originalIndex: longTermGoals.rows.indexOf(goal)
});
});
// 倒计时更新和目标完成检测
window.countdownTimer = setInterval(() => {
const now = getCurrentTime();
goalElements.forEach(item => {
if (item.element) {
const remaining = item.endDate - now;
item.element.textContent = formatCountdown(remaining);
// 检测是否到期
if (remaining <= 0 && !item.goal.notified && item.goal.status === 'running') {
item.goal.notified = true;
localStorage.setItem('longTermGoals', JSON.stringify(longTermGoals));
showGoalCompleteModal(item.goal, item.originalIndex);
}
}
});
}, 1000);
}
// 显示目标完成确认模态框
async function showGoalCompleteModal(goal, index) {
currentCompletingGoalIndex = index;
goalCompleteName.textContent = goal.name || '未命名目标';
goalCompleteText.textContent = '是否完成?';
// 根据设置播放媒体
const mediaData = await getMediaFile('goalComplete');
if (goalMediaSettings.mode === 'video' && mediaData && mediaData.type.startsWith('video')) {
// 视频模式 - 播放视频
if (goalBlobURL) URL.revokeObjectURL(goalBlobURL);
goalBlobURL = createBlobURL(mediaData.arrayBuffer, mediaData.type);
const goalVideoModal = document.getElementById('goal-video-modal');
const goalVideoPlayer = document.getElementById('goal-video-player');
if (goalVideoModal && goalVideoPlayer) {
goalVideoPlayer.src = goalBlobURL;
goalVideoModal.style.display = 'flex';
goalVideoPlayer.play().catch(err => console.log('视频播放失败'));
// 视频结束后显示确认弹窗
goalVideoPlayer.onended = () => {
goalVideoModal.style.display = 'none';
showGoalConfirmButtons();
};
}
} else if (goalMediaSettings.mode === 'animation') {
// 动画模式 - 播放小卡片动画
cardsAnimationModal.style.display = 'block';
if (mediaData) {
if (cardsBlobURL) URL.revokeObjectURL(cardsBlobURL);
cardsBlobURL = createBlobURL(mediaData.arrayBuffer, mediaData.type);
cardsAudio.src = cardsBlobURL;
cardsAudio.play().catch(err => console.log('音频播放失败'));
}
runCardsAnimation();
// 3秒后显示确认弹窗
setTimeout(() => {
showGoalConfirmButtons();
}, 3000);
} else {
// 无媒体设置,直接显示确认弹窗
showGoalConfirmButtons();
}
}
// 显示目标确认按钮
function showGoalConfirmButtons() {
goalCompleteButtons.innerHTML = `
<button class="btn-not-complete" id="btn-not-complete">未完成 😔</button>
<button class="btn-complete" id="btn-complete">完成 🎉</button>
`;
goalCompleteModal.style.display = 'flex';
// 绑定按钮事件
document.getElementById('btn-not-complete').onclick = () => {
// 关闭动画
cardsAnimationModal.style.display = 'none';
cardsAudio.pause();
cardsAudio.currentTime = 0;
cardsBoard.innerHTML = '';
goalCompleteText.textContent = '下次加油!💪';
goalCompleteButtons.innerHTML = `
<button class="btn-confirm" id="btn-confirm">确定</button>
`;
document.getElementById('btn-confirm').onclick = () => {
longTermGoals.rows[currentCompletingGoalIndex].status = 'failed';
localStorage.setItem('longTermGoals', JSON.stringify(longTermGoals));
goalCompleteModal.style.display = 'none';
renderLongTermGoals();
renderGoalListInModal();
};
};
document.getElementById('btn-complete').onclick = () => {
// 关闭动画
cardsAnimationModal.style.display = 'none';
cardsAudio.pause();
cardsAudio.currentTime = 0;
cardsBoard.innerHTML = '';
goalCompleteText.textContent = '少年,你做到了!🎉';
goalCompleteButtons.innerHTML = `
<button class="btn-confirm" id="btn-confirm">确定</button>
`;
document.getElementById('btn-confirm').onclick = () => {
longTermGoals.rows[currentCompletingGoalIndex].status = 'completed';
localStorage.setItem('longTermGoals', JSON.stringify(longTermGoals));
goalCompleteModal.style.display = 'none';
renderLongTermGoals();
renderGoalListInModal();
};
};
}
// 检查是否有到期的目标(页面加载时)
function checkExpiredGoals() {
const now = getCurrentTime();
longTermGoals.rows.forEach((goal, index) => {
if (goal.status !== 'running') return;
const end = new Date(goal.endDate);
if (isNaN(end)) return;
if (now >= end && !goal.notified) {
goal.notified = true;
localStorage.setItem('longTermGoals', JSON.stringify(longTermGoals));
// 延迟显示,避免页面加载时立即弹出
setTimeout(() => {
showGoalCompleteModal(goal, index);
}, 1000);
}
});
}
let goalCardsSettings = {
quotes: ["目标达成!🎉", "你太棒了!💪", "坚持就是胜利!🔥"],
emojis: ["🎉", "✨", "🔥", "💪", "❤️", "🚀"]
};
try {
const saved = localStorage.getItem('goalCardsSettings');
if (saved) goalCardsSettings = JSON.parse(saved);
} catch(e) {}
// ==================== 跨日目标媒体设置 ====================
// 文件选择显示
document.getElementById('goal-media-file')?.addEventListener('change', (e) => {
const file = e.target.files[0];
const nameEl = document.getElementById('goal-media-file-name');
if (file && nameEl) {
const isVideo = file.name.match(/\.(mp4|webm|mov|avi)$/i);
const icon = isVideo ? '🎥' : '🎵';
nameEl.innerHTML = `
<div class="file-info">
<span class="file-icon">${icon}</span>
<div class="file-details">
<span class="file-name-text">${file.name}</span>
<span class="file-size-text">${formatFileSize(file.size)}</span>
</div>
</div>
<button class="delete-btn" onclick="this.parentElement.style.display='none';document.getElementById('goal-media-file').value='';">×</button>
`;
nameEl.style.display = 'flex';
} else if (nameEl) {
nameEl.style.display = 'none';
}
});
// 保存跨日目标媒体设置
document.getElementById('btn-save-goal-media')?.addEventListener('click', async () => {
const fileInput = document.getElementById('goal-media-file');
const file = fileInput?.files[0];
// 保存小卡片设置
const quotesText = document.getElementById('goal-cards-quotes').value.trim();
const emojisText = document.getElementById('goal-cards-emojis').value.trim();
if (quotesText) {
goalCardsSettings.quotes = quotesText.split('\n').map(q => q.trim()).filter(q => q);
}
if (emojisText) {
goalCardsSettings.emojis = emojisText.split(/\s+/).filter(e => e);
}
localStorage.setItem('goalCardsSettings', JSON.stringify(goalCardsSettings));
// 保存文件
if (file) {
try {
await saveMediaFile('goalComplete', file);
console.log('跨日目标媒体文件已保存');
} catch (err) {
console.error('保存文件失败:', err);
alert('保存文件失败,请重试!');
return;
}
}
// 保存播放模式
let mode = 'animation';
document.querySelectorAll('input[name="goal-mode"]').forEach(radio => {
if (radio.checked) mode = radio.value;
});
goalMediaSettings.mode = mode;
localStorage.setItem('goalMediaSettings', JSON.stringify(goalMediaSettings));
alert('提醒设置已保存!🎉');
});
// 初始化跨日目标媒体设置显示
async function initGoalMediaDisplay() {
document.getElementById('goal-cards-quotes').value = goalCardsSettings.quotes.join('\n');
document.getElementById('goal-cards-emojis').value = goalCardsSettings.emojis.join(' ');
// 设置播放模式
document.querySelectorAll('input[name="goal-mode"]').forEach(radio => {
radio.checked = radio.value === goalMediaSettings.mode;
});
// 显示已保存的文件
const mediaData = await getMediaFile('goalComplete');
const nameEl = document.getElementById('goal-media-file-name');
if (mediaData && nameEl) {
const isVideo = mediaData.type.startsWith('video');
const icon = isVideo ? '🎥' : '🎵';
nameEl.innerHTML = `
<div class="file-info">
<span class="file-icon">${icon}</span>
<div class="file-details">
<span class="file-name-text">${mediaData.name}</span>
<span class="file-size-text">${formatFileSize(mediaData.size)}</span>
</div>
</div>
<button class="delete-btn" onclick="deleteGoalMedia()">×</button>
`;
nameEl.style.display = 'flex';
}
}
// 删除跨日目标媒体文件
async function deleteGoalMedia() {
await deleteMediaFile('goalComplete');
const nameEl = document.getElementById('goal-media-file-name');
if (nameEl) nameEl.style.display = 'none';
document.getElementById('goal-media-file').value = '';
}
// ==================== 颜色选择器实时预览 ====================
document.getElementById('encourage-bg-color')?.addEventListener('input', (e) => {
const preview = document.getElementById('color-preview');
if (preview) {
preview.style.background = e.target.value;
preview.style.borderStyle = 'solid';
preview.style.borderColor = e.target.value;
// 根据背景色调整文字颜色
const hex = e.target.value.replace('#', '');
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
preview.style.color = brightness > 128 ? '#333' : '#fff';
}
});
// ==================== 闹钟文件选择显示(美化版) ====================
document.getElementById('alarm-audio-file')?.addEventListener('change', (e) => {
const file = e.target.files[0];
const nameEl = document.getElementById('alarm-file-name');
if (file && nameEl) {
const isVideo = file.name.match(/\.(mp4|webm|mov|avi)$/i);
const icon = isVideo ? '🎥' : '🎵';
nameEl.innerHTML = `
<div class="file-info">
<span class="file-icon">${icon}</span>
<div class="file-details">
<span class="file-name-text">${file.name}</span>
<span class="file-size-text">${formatFileSize(file.size)}</span>
</div>
</div>
<button class="delete-btn" onclick="this.parentElement.style.display='none';document.getElementById('alarm-audio-file').value='';">×</button>
`;
nameEl.style.display = 'flex';
} else if (nameEl) {
nameEl.style.display = 'none';
}
});
// ==================== 初始化 ====================
// 立即渲染日历(使用本地时间,不等待网络)
function immediateRender() {
today = new Date();
currentYear = today.getFullYear();
currentMonth = today.getMonth();
updateRealtimeClock();
renderCalendar(currentYear, currentMonth);
renderTodayTaskDisplay();
const formattedToday = today.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' });
dateInfo.textContent = `${formattedToday} —— 今天`;
}
// 立即执行渲染
immediateRender();
// 检查到期目标
setTimeout(checkExpiredGoals, 2000);
// 后台异步初始化(不阻塞页面显示)
async function asyncInit() {
// 后台同步网络时间
const synced = await syncNetworkTime();
if (synced) {
today = getCurrentTime();
currentYear = today.getFullYear();
currentMonth = today.getMonth();
renderCalendar(currentYear, currentMonth);
updateRealtimeClock();
}
// 后台加载媒体文件
await initCelebrateMedia();
}
// 延迟执行异步初始化,不阻塞页面
setTimeout(asyncInit, 100);
document.getElementById('encourage-video-close')?.addEventListener('click', () => {
document.getElementById('encourage-video-modal').style.display = 'none';
document.getElementById('encourage-video-player').pause();
});
window.addEventListener('beforeunload', () => {
if (window.countdownTimer) clearInterval(window.countdownTimer);
if (celebrateBlobURL) URL.revokeObjectURL(celebrateBlobURL);
if (encourageBlobURL) URL.revokeObjectURL(encourageBlobURL);
if (cardsBlobURL) URL.revokeObjectURL(cardsBlobURL);
});
</script>
</body>
</html>
如果你渴望一种更科学、更有趣、更具可持续性的自律方式,如果你是一名喜欢折腾、追求极致个性化的开发者或极客,那么,请不要错过这款在创意工坊刚刚上架的宝藏应用。
立即体验,用「熊二的自律日历」,开启你独一无二的可视化自律之旅,让每一天的坚持,都闪闪发光!
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END






暂无评论内容