[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智能网页工作日历备忘录</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.3.0/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<!-- 🔹 Firebase SDK -->
<script src="https://www.gstatic.com/firebasejs/10.12.2/firebase-app-compat.js"></script>
<script src="https://www.gstatic.com/firebasejs/10.12.2/firebase-auth-compat.js"></script>
<script src="https://www.gstatic.com/firebasejs/10.12.2/firebase-firestore-compat.js"></script>
<!-- 原有 CSS(完整保留) -->
<style>
/* 基础样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
:root {
--primary-color: #4361ee;
--secondary-color: #3a0ca3;
--accent-color: #4cc9f0;
--light-color: #f8f9fa;
--dark-color: #212529;
--success-color: #4CAF50;
--warning-color: #ff9800;
--danger-color: #f44336;
--border-radius: 10px;
--box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08);
--transition: all 0.3s ease;
}
body {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
color: var(--dark-color);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1800px;
margin: 0 auto;
}
/* 头部样式 */
header {
text-align: center;
padding: 25px 0 30px;
margin-bottom: 25px;
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
overflow: visible;
position: relative;
z-index: 1;
}
header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(45deg,
rgba(255,255,255,0.1) 0%,
rgba(255,255,255,0.2) 25%,
transparent 50%,
rgba(0,0,0,0.1) 100%);
pointer-events: none;
}
h1 {
font-size: 2.5rem;
color: white;
margin-bottom: 10px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
position: relative;
z-index: 2;
letter-spacing: 0.5px;
}
.subtitle {
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.9);
max-width: 800px;
margin: 0 auto;
line-height: 1.6;
position: relative;
z-index: 2;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
/* 工具栏 */
.toolbar {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 25px;
padding: 15px 20px;
background: white;
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
flex-wrap: wrap;
}
.search-container {
flex: 1;
min-width: 200px;
position: relative;
}
.search-input {
width: 100%;
padding: 10px 40px 10px 40px;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 0.95rem;
transition: var(--transition);
background-color: #f8f9fa;
}
.search-input:focus {
outline: none;
border-color: var(--primary-color);
background-color: white;
box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.1);
}
.search-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: #6c757d;
font-size: 1rem;
}
.clear-search {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #6c757d;
cursor: pointer;
font-size: 1rem;
padding: 4px;
border-radius: 50%;
display: none;
transition: var(--transition);
}
.clear-search:hover {
background-color: rgba(0, 0, 0, 0.05);
color: var(--danger-color);
}
.toolbar-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.toolbar-btn {
padding: 10px 16px;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: var(--transition);
font-size: 0.9rem;
white-space: nowrap;
}
.toolbar-btn-primary {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white;
}
.toolbar-btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(67, 97, 238, 0.3);
}
.toolbar-btn-secondary {
background: #6c757d;
color: white;
}
.toolbar-btn-secondary:hover {
background: #5a6268;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.3);
}
.toolbar-btn-success {
background: var(--success-color);
color: white;
}
.toolbar-btn-success:hover {
background: #388e3c;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
}
/* 主题选择器 */
.theme-selector-container {
position: absolute;
top: 25px;
right: 25px;
display: flex;
flex-direction: column;
align-items: center;
z-index: 1000;
}
.theme-selector-btn {
width: 45px;
height: 45px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
color: white;
border: 2px solid rgba(255, 255, 255, 0.3);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
cursor: pointer;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
transition: var(--transition);
margin-bottom: 10px;
z-index: 1001;
}
.theme-selector-btn:hover {
transform: translateY(-3px) scale(1.1);
background: rgba(255, 255, 255, 0.3);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.theme-selector {
display: none;
flex-direction: column;
gap: 8px;
padding: 15px;
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(10px);
border-radius: var(--border-radius);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
max-width: 200px;
max-height: 400px;
overflow-y: auto;
margin-top: 5px;
border: 1px solid rgba(255, 255, 255, 0.2);
position: absolute;
top: 100%;
right: 0;
z-index: 1002;
}
.theme-selector.active {
display: flex;
}
.theme-color {
width: 100%;
height: 32px;
border-radius: 6px;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
padding: 0 12px;
color: white !important;
font-weight: 600;
font-size: 0.9rem;
margin: 2px 0;
min-width: 150px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.theme-color:hover {
transform: translateX(3px);
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
}
.theme-color.active {
border: 2px solid white;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.15);
}
/* 日历导航 */
.calendar-navigation {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
margin-bottom: 30px;
background: white;
padding: 15px;
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
}
.nav-button {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white;
border: none;
width: 45px;
height: 45px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 1.2rem;
transition: var(--transition);
box-shadow: 0 4px 10px rgba(67, 97, 238, 0.3);
}
.nav-button:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(67, 97, 238, 0.4);
}
.current-period {
font-size: 1.3rem;
font-weight: 700;
color: var(--primary-color);
min-width: 280px;
text-align: center;
padding: 8px 20px;
background: rgba(67, 97, 238, 0.05);
border-radius: 8px;
border: 2px solid rgba(67, 97, 238, 0.1);
}
/* 月份数量选择器 */
.month-count-selector {
display: flex;
align-items: center;
gap: 8px;
background: rgba(67, 97, 238, 0.08);
padding: 6px 12px;
border-radius: 6px;
border: 1px solid rgba(67, 97, 238, 0.15);
}
.month-count-selector label {
font-size: 0.9rem;
color: var(--primary-color);
font-weight: 600;
white-space: nowrap;
}
.month-count-selector select {
background: white;
border: 1px solid rgba(67, 97, 238, 0.3);
border-radius: 4px;
padding: 4px 8px;
font-size: 0.9rem;
color: var(--dark-color);
cursor: pointer;
transition: var(--transition);
}
/* 多个月份日历容器 */
.calendar-container {
position: relative;
display: flex;
align-items: center;
width: 100%;
}
.multi-month-calendar {
display: grid;
gap: 25px;
margin-bottom: 25px;
width: 100%;
min-height: 600px;
grid-template-columns: repeat(auto-fill, minmax(calc(50% - 12.5px), 1fr));
}
.multi-month-calendar.grid-1 {
grid-template-columns: 1fr;
}
.multi-month-calendar.grid-2 {
grid-template-columns: repeat(2, 1fr);
}
.calendar-nav-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white;
border: none;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 1.1rem;
transition: var(--transition);
box-shadow: 0 4px 10px rgba(67, 97, 238, 0.3);
z-index: 5;
opacity: 0.8;
}
.calendar-nav-btn:hover {
opacity: 1;
transform: translateY(-50%) scale(1.1);
}
.calendar-nav-btn.prev-month {
left: -15px;
}
.calendar-nav-btn.next-month {
right: -15px;
}
/* 单个月日历 */
/* 单个月日历 */
.month-calendar {
background-color: white;
border-radius: var(--border-radius);
padding: 20px;
box-shadow: var(--box-shadow);
position: relative;
min-height: 650px;
width: 100%;
overflow: hidden;
resize: horizontal;
overflow-x: auto;
min-width: 400px;
max-width: 100%;
margin: 0 auto; /* 居中 */
}
/* 🔹 1个月模式:限制宽度 + 禁用缩放 + 适当缩小单元格 */
.multi-month-calendar.grid-1 .month-calendar {
max-width: 900px; /* 限制最大宽度 */
resize: none; /* 禁用拖拽缩放 */
margin: 0 auto; /* 水平居中 */
min-height: 650px;
padding: 18px;
}
/* 单元格高度调整(1个月时略小) */
.multi-month-calendar.grid-1 .calendar-day {
min-height: 130px; /* 原为160px,现在更紧凑 */
}
/* 手机端优化 */
[url=home.php?mod=space&uid=945662]@media[/url] (max-width: 768px) {
.multi-month-calendar.grid-1 .month-calendar {
max-width: 100% !important;
padding: 12px;
min-height: 550px;
}
.multi-month-calendar.grid-1 .calendar-day {
min-height: 90px;
padding: 5px;
}
.multi-month-calendar.grid-1 .day-memo-item {
font-size: 0.75rem;
padding: 2px 4px;
}
}
/* 任务统计 */
.month-stats {
display: flex;
align-items: center;
gap: 12px;
background: rgba(67, 97, 238, 0.05);
padding: 8px 12px;
border-radius: 6px;
border: 1px solid rgba(67, 97, 238, 0.1);
font-size: 0.85rem;
color: #495057;
}
.stat-item {
display: flex;
align-items: center;
gap: 4px;
white-space: nowrap;
}
.complete-all-btn {
background: linear-gradient(135deg, var(--success-color), #2e7d32);
color: white;
border: none;
padding: 8px 15px;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
transition: var(--transition);
box-shadow: 0 3px 8px rgba(76, 175, 80, 0.3);
white-space: nowrap;
}
.complete-all-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(76, 175, 80, 0.4);
}
.month-progress {
display: flex;
align-items: center;
gap: 10px;
}
.progress-circle {
position: relative;
width: 40px;
height: 40px;
}
.progress-circle svg {
width: 100%;
height: 100%;
transform: rotate(-90deg);
}
.progress-circle-bg {
fill: none;
stroke: #e9ecef;
stroke-width: 4;
}
.progress-circle-fill {
fill: none;
stroke: var(--primary-color);
stroke-width: 4;
stroke-linecap: round;
transition: stroke-dashoffset 0.8s ease;
}
.progress-percent {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 0.8rem;
font-weight: 600;
color: var(--primary-color);
}
.weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
text-align: center;
font-weight: 600;
color: #495057;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px solid #e9ecef;
font-size: 0.9rem;
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 5px;
width: 100%;
}
/* 日历单元格 */
.calendar-day {
aspect-ratio: 1;
background-color: #f8f9fa;
border-radius: 8px;
padding: 8px;
cursor: pointer;
transition: var(--transition);
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 140px;
width: 100%;
box-sizing: border-box;
border: 1px dashed rgba(0, 0, 0, 0.15);
}
.multi-month-calendar.grid-1 .calendar-day {
min-height: 160px;
}
.calendar-day:hover {
background-color: #e9ecef;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
z-index: 2;
border: 1px solid rgba(67, 97, 238, 0.3);
}
.calendar-day.today {
background-color: rgba(67, 97, 238, 0.15);
border: 2px solid var(--primary-color);
}
.calendar-day.other-month {
opacity: 0.5;
background-color: #f0f2f5;
border: 1px dashed rgba(0, 0, 0, 0.1);
}
.day-number {
font-size: 1rem;
font-weight: 700;
color: var(--dark-color);
margin-bottom: 6px;
align-self: flex-start;
position: relative;
z-index: 2;
}
.day-memos {
flex-grow: 1;
overflow-y: auto;
font-size: 0.75rem;
line-height: 1.3;
max-height: calc(100% - 25px);
min-height: 90px;
width: 100%;
}
.day-memo-item {
padding: 4px 6px;
margin-bottom: 3px;
border-radius: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: var(--transition);
background-color: rgba(255, 255, 255, 0.7);
border-left: 3px solid;
font-size: 0.75rem;
height: 18px;
width: 100%;
box-sizing: border-box;
}
.multi-month-calendar.grid-1 .day-memo-item {
white-space: normal;
height: auto;
min-height: 18px;
max-height: 54px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
line-height: 1.3;
padding: 3px 6px;
font-size: 0.8rem;
}
.day-memo-item.completed {
opacity: 0.6;
text-decoration: line-through;
}
.memo-count {
position: absolute;
top: 8px;
right: 8px;
background-color: var(--danger-color);
color: white;
width: 18px;
height: 18px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.65rem;
font-weight: bold;
z-index: 3;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
/* 浮动按钮 */
.floating-actions {
position: fixed;
right: 25px;
bottom: 25px;
display: flex;
flex-direction: column;
gap: 12px;
z-index: 999;
}
.floating-btn {
width: 55px;
height: 55px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white;
border: none;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.3rem;
cursor: pointer;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
transition: var(--transition);
position: relative;
}
.floating-btn:hover {
transform: translateY(-5px) scale(1.1);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.floating-btn .badge {
position: absolute;
top: -4px;
right: -4px;
background-color: var(--danger-color);
color: white;
font-size: 0.65rem;
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
/* 提醒弹窗 */
.reminder-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
z-index: 2000;
align-items: center;
justify-content: center;
padding: 15px;
}
.reminder-modal.active {
display: flex;
}
.reminder-content {
background-color: white;
width: 100%;
max-width: 500px;
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
transform: translateY(-50px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.reminder-header {
padding: 20px;
background: linear-gradient(90deg, var(--danger-color), #d32f2f);
color: white;
display: flex;
justify-content: space-between;
align-items: center;
}
.reminder-title {
font-size: 1.3rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
}
.close-reminder {
background: none;
border: none;
color: white;
font-size: 1.6rem;
cursor: pointer;
transition: var(--transition);
}
.close-reminder:hover {
transform: rotate(90deg);
}
.reminder-body {
padding: 20px;
max-height: 400px;
overflow-y: auto;
}
.reminder-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.reminder-item {
padding: 15px;
background-color: #fff8e1;
border-radius: 8px;
border-left: 4px solid var(--warning-color);
transition: var(--transition);
}
.reminder-item:hover {
background-color: #fff3cd;
cursor: pointer;
}
.reminder-item-title {
font-weight: 600;
font-size: 1rem;
margin-bottom: 5px;
color: var(--dark-color);
}
.reminder-item-details {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.9rem;
color: #6c757d;
}
.reminder-actions {
padding: 15px 20px;
background-color: #f8f9fa;
border-top: 2px solid #e9ecef;
display: flex;
justify-content: space-between;
align-items: center;
}
/* 模态窗口 */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
z-index: 1000;
align-items: center;
justify-content: center;
padding: 15px;
}
.modal.active {
display: flex;
}
.modal-content {
background-color: white;
width: 100%;
max-width: 800px;
max-height: 85vh;
border-radius: var(--border-radius);
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.modal-header {
padding: 20px;
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
color: white;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-title {
font-size: 1.3rem;
font-weight: 600;
}
.close-modal {
background: none;
border: none;
color: white;
font-size: 1.6rem;
cursor: pointer;
transition: var(--transition);
}
.close-modal:hover {
transform: rotate(90deg);
}
.modal-body {
padding: 20px;
overflow-y: auto;
flex-grow: 1;
}
/* 选项卡 */
.tabs {
display: flex;
border-bottom: 2px solid #e9ecef;
margin-bottom: 20px;
flex-wrap: wrap;
}
.tab {
padding: 12px 18px;
background: none;
border: none;
font-size: 1rem;
font-weight: 600;
color: #6c757d;
cursor: pointer;
transition: var(--transition);
position: relative;
}
.tab.active {
color: var(--primary-color);
}
.tab.active::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 100%;
height: 2px;
background-color: var(--primary-color);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* 任务列表 */
.task-list {
max-height: 300px;
overflow-y: auto;
}
.task-item {
padding: 12px;
background-color: #f8f9fa;
border-radius: 8px;
margin-bottom: 12px;
border-left: 4px solid var(--primary-color);
transition: var(--transition);
}
.task-item:hover {
background-color: #e9ecef;
}
.task-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.task-title {
font-weight: 600;
font-size: 1rem;
}
.task-color {
width: 18px;
height: 18px;
border-radius: 50%;
}
.task-due {
font-size: 0.85rem;
color: #6c757d;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 5px;
}
.task-actions {
display: flex;
gap: 8px;
margin-top: 8px;
}
.task-btn {
padding: 4px 8px;
border-radius: 4px;
border: none;
cursor: pointer;
font-size: 0.8rem;
transition: var(--transition);
}
.task-btn-complete {
background-color: rgba(76, 175, 80, 0.1);
color: var(--success-color);
}
.task-btn-complete:hover {
background-color: var(--success-color);
color: white;
}
.task-btn-edit {
background-color: rgba(67, 97, 238, 0.1);
color: var(--primary-color);
}
.task-btn-edit:hover {
background-color: var(--primary-color);
color: white;
}
.task-btn-delete {
background-color: rgba(244, 67, 54, 0.1);
color: var(--danger-color);
}
.task-btn-delete:hover {
background-color: var(--danger-color);
color: white;
}
/* 表单 */
.form-group {
margin-bottom: 18px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
label {
display: block;
margin-bottom: 6px;
font-weight: 600;
color: #495057;
font-size: 0.95rem;
}
.form-control {
width: 100%;
padding: 10px;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 0.95rem;
transition: var(--transition);
}
.form-control:focus {
outline: none;
border-color: var(--primary-color);
}
textarea.form-control {
min-height: 100px;
resize: vertical;
font-family: 'Segoe UI', sans-serif;
}
.color-options {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.color-option {
width: 32px;
height: 32px;
border-radius: 50%;
cursor: pointer;
border: 2px solid transparent;
transition: var(--transition);
}
.color-option.selected {
border-color: var(--dark-color);
transform: scale(1.1);
}
.markdown-preview {
padding: 12px;
border-radius: 6px;
background-color: #f8f9fa;
border: 2px solid #e9ecef;
min-height: 120px;
max-height: 200px;
overflow-y: auto;
font-size: 0.9rem;
}
.modal-footer {
padding: 15px 20px;
background-color: #f8f9fa;
border-top: 2px solid #e9ecef;
display: flex;
justify-content: flex-end;
gap: 12px;
}
/* 按钮 */
.btn {
padding: 10px 16px;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: var(--transition);
font-size: 0.9rem;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-primary:hover {
background-color: var(--secondary-color);
transform: translateY(-2px);
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #5a6268;
transform: translateY(-2px);
}
.btn-danger {
background-color: var(--danger-color);
color: white;
}
.btn-danger:hover {
background-color: #d32f2f;
transform: translateY(-2px);
}
.btn-success {
background-color: var(--success-color);
color: white;
}
.btn-success:hover {
background-color: #388e3c;
transform: translateY(-2px);
}
.btn-full {
width: 100%;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 20px 15px;
color: #6c757d;
}
.empty-state i {
font-size: 2rem;
margin-bottom: 10px;
color: #dee2e6;
}
/* 倒计时 */
.countdown {
display: inline-block;
padding: 2px 6px;
border-radius: 10px;
font-size: 0.7rem;
font-weight: 600;
margin-left: 5px;
}
.countdown.danger {
background-color: rgba(244, 67, 54, 0.2);
color: var(--danger-color);
}
.countdown.warning {
background-color: rgba(255, 152, 0, 0.2);
color: var(--warning-color);
}
.countdown.success {
background-color: rgba(76, 175, 80, 0.2);
color: var(--success-color);
}
/* 滚动条 */
::-webkit-scrollbar {
width: 5px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 每日详情快速添加 */
.quick-add-container {
margin-bottom: 20px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 8px;
border: 2px dashed #dee2e6;
}
.quick-add-header {
font-size: 1rem;
margin-bottom: 12px;
color: #495057;
display: flex;
align-items: center;
gap: 8px;
}
.quick-add-form {
display: grid;
grid-template-columns: 1fr auto;
gap: 8px;
}
.quick-add-input {
padding: 10px 12px;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 0.95rem;
transition: var(--transition);
}
.quick-add-input:focus {
outline: none;
border-color: var(--primary-color);
}
.quick-add-button {
padding: 0 20px;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
}
.quick-add-button:hover {
background-color: var(--secondary-color);
}
/* 备忘录颜色点 */
.memo-color-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 4px;
}
/* 导出按钮容器 */
.export-buttons-container {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
margin-top: 20px;
margin-bottom: 20px;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.multi-month-calendar {
grid-template-columns: repeat(2, 1fr) !important;
}
.calendar-nav-btn {
display: none;
}
}
@media (max-width: 768px) {
h1 {
font-size: 2rem;
padding: 0 15px;
}
.subtitle {
font-size: 1rem;
padding: 0 15px;
}
.toolbar {
flex-direction: column;
align-items: stretch;
}
.search-container {
width: 100%;
}
.toolbar-buttons {
width: 100%;
justify-content: stretch;
}
.toolbar-btn {
flex: 1;
min-width: auto;
}
.calendar-navigation {
flex-direction: column;
gap: 15px;
padding: 15px 10px;
}
.current-period {
min-width: auto;
width: 100%;
font-size: 1.1rem;
}
.month-count-selector {
width: 100%;
justify-content: center;
}
.nav-button {
width: 40px;
height: 40px;
}
.multi-month-calendar {
grid-template-columns: 1fr !important;
gap: 20px;
}
.calendar-nav-btn {
display: none;
}
.month-calendar {
padding: 15px;
min-height: 600px;
resize: none;
min-width: 100%;
}
.form-row {
grid-template-columns: 1fr;
}
.modal-content {
max-height: 90vh;
}
.floating-actions {
right: 15px;
bottom: 15px;
}
.floating-btn {
width: 48px;
height: 48px;
font-size: 1.1rem;
}
.tabs {
justify-content: center;
}
.tab {
padding: 10px 12px;
font-size: 0.9rem;
}
.theme-selector-container {
top: 15px;
right: 15px;
}
.theme-selector-btn {
width: 40px;
height: 40px;
font-size: 1rem;
}
.calendar-day {
min-height: 100px;
padding: 6px;
}
}
@media (max-width: 480px) {
h1 {
font-size: 1.5rem;
}
.month-title {
font-size: 1.2rem;
}
.month-stats {
flex-direction: column;
align-items: flex-start;
gap: 6px;
}
.complete-all-btn {
padding: 6px 10px;
font-size: 0.8rem;
}
.toolbar-btn {
padding: 8px 12px;
font-size: 0.8rem;
}
.floating-btn {
width: 42px;
height: 42px;
font-size: 1rem;
}
}
/* 提示信息 */
.toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: var(--primary-color);
color: white;
padding: 12px 24px;
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
z-index: 10000;
display: none;
align-items: center;
gap: 10px;
}
.toast.show {
display: flex;
animation: fadeInUp 0.3s ease;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateX(-50%) translateY(20px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
/* 确认对话框 */
.confirmation-dialog {
background: white;
border-radius: var(--border-radius);
padding: 20px;
max-width: 400px;
width: 90%;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.confirmation-title {
font-size: 1.2rem;
margin-bottom: 10px;
color: var(--dark-color);
}
.confirmation-message {
margin-bottom: 20px;
color: #666;
}
.confirmation-buttons {
display: flex;
gap: 10px;
justify-content: flex-end;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>📅 智能网页工作日历备忘录</h1>
<p class="subtitle">同时查看多个月份日历,每天显示备忘录标题列表,支持快速操作和智能任务管理</p>
<div class="theme-selector-container">
<button class="theme-selector-btn" id="themeSelectorBtn" title="切换配色方案">
<i class="fas fa-palette"></i>
</button>
<div class="theme-selector" id="themeSelector">
<!-- 主题将通过JS生成 -->
</div>
</div>
</header>
<!-- 工具栏 -->
<div class="toolbar">
<div class="search-container">
<i class="fas fa-search search-icon"></i>
<input type="text" class="search-input" id="searchInput" placeholder="搜索备忘录...">
<button class="clear-search" id="clearSearch" title="清除搜索">
<i class="fas fa-times"></i>
</button>
</div>
<!-- 月份数量选择器 -->
<div class="month-count-selector">
<label for="monthCountSelect">显示月份:</label>
<select id="monthCountSelect">
<option value="1">1个月</option>
<option value="2" selected>2个月</option>
<option value="3">3个月</option>
<option value="4">4个月</option>
<option value="5">5个月</option>
<option value="6">6个月</option>
<option value="7">7个月</option>
<option value="8">8个月</option>
<option value="9">9个月</option>
<option value="10">10个月</option>
<option value="11">11个月</option>
<option value="12">12个月</option>
</select>
</div>
<div class="toolbar-buttons">
<button class="toolbar-btn toolbar-btn-primary" id="toolbarPublish">
<i class="fas fa-bullhorn"></i> 任务发布
</button>
<button class="toolbar-btn toolbar-btn-success" id="toolbarExport">
<i class="fas fa-file-export"></i> 数据导出
</button>
<button class="toolbar-btn toolbar-btn-secondary" id="toolbarImport">
<i class="fas fa-file-import"></i> 数据导入
</button>
</div>
</div>
<!-- 日历导航区域 -->
<div class="calendar-navigation">
<button class="nav-button" id="prevMonth">
<i class="fas fa-chevron-left"></i>
</button>
<div class="current-period" id="currentPeriod"></div>
<button class="nav-button" id="nextMonth">
<i class="fas fa-chevron-right"></i>
</button>
</div>
<!-- 多个月份日历容器 -->
<div class="calendar-container">
<button class="calendar-nav-btn prev-month" id="calendarPrevMonth" title="前一个月">
<i class="fas fa-chevron-left"></i>
</button>
<div class="multi-month-calendar grid-2" id="multiMonthCalendar">
<!-- 日历将通过JS动态生成 -->
</div>
<button class="calendar-nav-btn next-month" id="calendarNextMonth" title="后一个月">
<i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
<!-- 右侧浮动按钮 -->
<div class="floating-actions">
<button class="floating-btn" id="floatingReminder" title="到期提醒">
<i class="fas fa-bell"></i>
<span class="badge" id="reminderBadge" style="display: none;"></span>
</button>
<button class="floating-btn" id="floatingFunctions" title="功能面板">
<i class="fas fa-cog"></i>
<span class="badge" id="pendingBadge"></span>
</button>
</div>
<!-- 到期提醒弹窗 -->
<div class="reminder-modal" id="reminderModal">
<div class="reminder-content">
<div class="reminder-header">
<div class="reminder-title">
<i class="fas fa-bell"></i> 到期提醒
</div>
<button class="close-reminder" id="closeReminderModal">×</button>
</div>
<div class="reminder-body">
<div id="reminderList">
<div class="empty-state">
<i class="fas fa-bell-slash"></i>
<p>暂无到期提醒</p>
</div>
</div>
</div>
<div class="reminder-actions">
<div class="reminder-settings">
<input type="checkbox" id="autoCloseReminder">
<label for="autoCloseReminder">10秒后自动关闭</label>
</div>
<div class="export-buttons-container">
<button class="btn btn-primary" id="markAllAsRead">
<i class="fas fa-check-double"></i> 全部标记已读
</button>
<button class="btn btn-secondary" id="viewRecentTasks">
<i class="fas fa-tasks"></i> 查看最近任务
</button>
</div>
</div>
</div>
</div>
<!-- 备忘录编辑模态窗口 -->
<div class="modal" id="memoModal">
<div class="modal-content">
<div class="modal-header">
<div class="modal-title">备忘录编辑</div>
<button class="close-modal" id="closeMemoModal">×</button>
</div>
<div class="modal-body">
<form id="memoForm">
<div class="form-group">
<label for="memoTitle">标题 *</label>
<input type="text" class="form-control" id="memoTitle" required>
</div>
<div class="form-row">
<div class="form-group">
<label for="memoDate">日期 *</label>
<input type="date" class="form-control" id="memoDate" required>
</div>
<div class="form-group">
<label for="memoDueTime">截止时间</label>
<input type="datetime-local" class="form-control" id="memoDueTime">
</div>
</div>
<div class="form-group">
<label>备忘录颜色</label>
<div class="color-options" id="colorOptions">
<!-- 颜色选项将通过JS生成 -->
</div>
</div>
<div class="form-group">
<label for="memoContent">内容 (支持Markdown语法)</label>
<textarea class="form-control" id="memoContent" rows="5" placeholder="输入备忘录内容,支持Markdown语法..."></textarea>
</div>
<div class="form-group">
<label>预览</label>
<div class="markdown-preview" id="markdownPreview">
预览将在这里显示...
</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="memoCompleted"> 标记为已完成
</label>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="cancelMemo">取消</button>
<button class="btn btn-danger" id="deleteMemo">删除</button>
<button class="btn btn-primary" id="saveMemo">保存备忘录</button>
</div>
</div>
</div>
<!-- 功能面板模态窗口 -->
<div class="modal" id="functionsModal">
<div class="modal-content">
<div class="modal-header">
<div class="modal-title">功能面板</div>
<button class="close-modal" id="closeFunctionsModal">×</button>
</div>
<div class="modal-body">
<div class="tabs">
<button class="tab active" data-tab="taskPublish">任务发布</button>
<button class="tab" data-tab="recentTasks">最近任务</button>
<button class="tab" data-tab="dataManagement">数据管理</button>
<button class="tab" data-tab="exportSettings">定时导出</button>
<button class="tab" data-tab="reminderSettings">提醒设置</button>
<!-- 🔹 新增 -->
<button class="tab" data-tab="accountSync">账号同步</button>
</div>
<!-- 任务发布选项卡 -->
<div class="tab-content active" id="taskPublishTab">
<h3 style="margin-bottom: 15px;"><i class="fas fa-bullhorn"></i> 发布新任务</h3>
<div class="form-group">
<label for="taskTitle">任务标题 *</label>
<input type="text" class="form-control" id="taskTitle" placeholder="请输入任务标题" required>
</div>
<div class="form-group">
<label for="taskDescription">任务描述(支持Markdown)</label>
<textarea class="form-control" id="taskDescription" placeholder="请输入任务描述..." rows="4"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="taskStartDate">开始日期 *</label>
<input type="date" class="form-control" id="taskStartDate" required>
</div>
<div class="form-group">
<label for="taskEndDate">结束日期 *</label>
<input type="date" class="form-control" id="taskEndDate" required>
</div>
</div>
<div class="form-group">
<label for="taskColor">任务颜色</label>
<div class="color-options" id="taskColorOptions">
<!-- 颜色选项将通过JS生成 -->
</div>
</div>
<div class="form-group">
<label for="taskDueTime">每日截止时间</label>
<input type="time" class="form-control" id="taskDueTime" value="18:00">
</div>
<button class="btn btn-success btn-full" id="publishTask">
<i class="fas fa-paper-plane"></i> 发布并分配到每天
</button>
<div class="task-publish-info">
<h4><i class="fas fa-info-circle"></i> 功能说明</h4>
<p style="margin-top: 8px; line-height: 1.5;">
此功能将创建一个新任务,并自动分配到从开始日期到结束日期之间的每一天。
每天都会创建一个独立的备忘录,便于跟踪每日进度。
</p>
</div>
</div>
<!-- 最近任务选项卡 -->
<div class="tab-content" id="recentTasksTab">
<h3 style="margin-bottom: 15px;"><i class="fas fa-tasks"></i> 最近任务</h3>
<div class="task-list" id="recentTasksList">
<div class="empty-state">
<i class="fas fa-clipboard-list"></i>
<p>暂无任务,点击日历上的日期添加新任务</p>
</div>
</div>
</div>
<!-- 数据管理选项卡 -->
<div class="tab-content" id="dataManagementTab">
<h3 style="margin-bottom: 15px;"><i class="fas fa-database"></i> 数据管理</h3>
<p style="margin-bottom: 15px; color: #6c757d; line-height: 1.5;">
所有数据存储在您的浏览器本地,建议定期导出备份以防数据丢失。
</p>
<div class="data-management-buttons">
<button class="btn btn-primary" id="exportData">
<i class="fas fa-file-export"></i> 导出数据
</button>
<button class="btn btn-secondary" id="importData">
<i class="fas fa-file-import"></i> 导入数据
</button>
<button class="btn btn-danger" id="clearData">
<i class="fas fa-trash-alt"></i> 清空所有数据
</button>
<button class="btn btn-secondary" id="viewStats">
<i class="fas fa-chart-pie"></i> 查看统计
</button>
</div>
<div style="margin-top: 20px; padding: 15px; background-color: #f8f9fa; border-radius: 8px;">
<h4><i class="fas fa-info-circle"></i> 统计信息</h4>
<div style="margin-top: 10px;">
<p>总备忘录数: <span id="totalMemosStat">0</span></p>
<p>已完成: <span id="completedMemosStat">0</span></p>
<p>未完成: <span id="pendingMemosStat">0</span></p>
<p>最早备忘录: <span id="oldestMemoStat">无</span></p>
<p>最近更新: <span id="latestUpdateStat">无</span></p>
</div>
</div>
</div>
<!-- 定时导出选项卡 -->
<div class="tab-content" id="exportSettingsTab">
<h3 style="margin-bottom: 15px;"><i class="fas fa-clock"></i> 定时导出设置</h3>
<div class="form-group">
<label for="exportInterval">导出频率</label>
<select class="form-control" id="exportInterval">
<option value="never">从不</option>
<option value="daily">每天</option>
<option value="weekly">每周</option>
<option value="monthly">每月</option>
</select>
</div>
<div class="form-group">
<label for="exportTime">导出时间</label>
<input type="time" class="form-control" id="exportTime" value="23:00">
</div>
<div class="form-group">
<label for="lastExport">上次导出时间</label>
<input type="text" class="form-control" id="lastExport" value="从未导出" readonly>
</div>
<div class="export-buttons-container">
<button class="btn btn-primary" id="saveExportSettings">
<i class="fas fa-save"></i> 保存设置
</button>
<button class="btn btn-secondary" id="manualExport">
<i class="fas fa-file-export"></i> 立即导出
</button>
</div>
<div class="export-info">
<h4><i class="fas fa-info-circle"></i> 注意事项</h4>
<ul style="margin-top: 8px; padding-left: 18px; line-height: 1.5;">
<li>定时导出功能需要保持浏览器页面打开才能正常工作</li>
<li>导出的数据包含所有备忘录和设置</li>
<li>建议设置自动导出以防数据丢失</li>
</ul>
</div>
</div>
<!-- 提醒设置选项卡 -->
<div class="tab-content" id="reminderSettingsTab">
<h3 style="margin-bottom: 15px;"><i class="fas fa-bell"></i> 提醒设置</h3>
<div class="form-group">
<label for="reminderCheckInterval">检查频率</label>
<select class="form-control" id="reminderCheckInterval">
<option value="1">每1分钟</option>
<option value="5" selected>每5分钟</option>
<option value="10">每10分钟</option>
<option value="15">每15分钟</option>
<option value="30">每30分钟</option>
<option value="60">每小时</option>
</select>
</div>
<div class="form-group">
<label for="reminderAdvanceTime">提前提醒时间</label>
<select class="form-control" id="reminderAdvanceTime">
<option value="0">到期时提醒</option>
<option value="15">提前15分钟</option>
<option value="30" selected>提前30分钟</option>
<option value="60">提前1小时</option>
<option value="1440">提前1天</option>
</select>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="enableSoundReminder" checked> 启用声音提醒
</label>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="enableDesktopNotification"> 启用桌面通知(需要浏览器授权)
</label>
</div>
<div class="form-group">
<label for="reminderTest">测试提醒</label>
<button class="btn btn-warning btn-full" id="testReminder">
<i class="fas fa-bell"></i> 发送测试提醒
</button>
</div>
<button class="btn btn-primary" id="saveReminderSettings">
<i class="fas fa-save"></i> 保存提醒设置
</button>
<div class="export-info" style="margin-top: 20px;">
<h4><i class="fas fa-info-circle"></i> 提醒说明</h4>
<ul style="margin-top: 8px; padding-left: 18px; line-height: 1.5;">
<li>系统会定期检查到期备忘录并显示提醒</li>
<li>提醒弹窗会在页面右上角显示</li>
<li>已完成的备忘录不会触发提醒</li>
</ul>
</div>
</div>
<!-- 🔹 账号同步面板 -->
<div class="tab-content" id="accountSyncTab">
<h3 style="margin-bottom: 15px;"><i class="fas fa-cloud"></i> 云端同步</h3>
<div id="authStatus" style="margin-bottom: 15px; padding: 10px; background: #e9ecef; border-radius: 6px;">
未登录
</div>
<div id="authForm" style="margin-bottom: 15px;">
<div class="form-group">
<label for="syncEmail">邮箱</label>
<input type="email" class="form-control" id="syncEmail" placeholder="your@email.com">
</div>
<div class="form-group">
<label for="syncPassword">密码</label>
<input type="password" class="form-control" id="syncPassword" placeholder="至少6位">
</div>
<button class="btn btn-primary" id="loginBtn" style="margin-right: 10px;">
<i class="fas fa-sign-in-alt"></i> 登录
</button>
<button class="btn btn-secondary" id="registerBtn">
<i class="fas fa-user-plus"></i> 注册
</button>
</div>
<div class="export-buttons-container">
<button class="btn btn-success" id="syncToCloud" disabled>
<i class="fas fa-upload"></i> 上传到云端
</button>
<button class="btn btn-info" id="syncFromCloud" disabled>
<i class="fas fa-download"></i> 从云端下载
</button>
</div>
<div style="margin-top: 15px; padding: 12px; background: #fff8e1; border-radius: 6px; font-size: 0.9rem;">
<i class="fas fa-exclamation-circle"></i>
登录后即可在手机、电脑之间同步所有备忘录数据。
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="closeFunctionsModalBtn">关闭</button>
</div>
</div>
</div>
<!-- 每日备忘录详情模态窗口 -->
<div class="modal" id="dailyDetailModal">
<div class="modal-content">
<div class="modal-header">
<div class="modal-title">
<i class="fas fa-list"></i> 每日备忘录详情
<span style="font-size: 1.1rem; color: white; opacity: 0.9; margin-left: 12px; font-weight: normal;" id="dailyDetailDate"></span>
</div>
<button class="close-modal" id="closeDailyDetailModal">×</button>
</div>
<div class="modal-body">
<!-- 快速添加备忘录 -->
<div class="quick-add-container">
<div class="quick-add-header">
<i class="fas fa-plus-circle"></i> 快速添加备忘录
</div>
<div class="quick-add-form">
<input type="text" class="quick-add-input" id="quickMemoTitle" placeholder="输入备忘录标题..." maxlength="100">
<button class="quick-add-button" id="quickAddMemo">添加</button>
</div>
</div>
<!-- 备忘录列表 -->
<h3 style="margin-bottom: 12px;"><i class="fas fa-sticky-note"></i> 备忘录列表</h3>
<div style="max-height: 350px; overflow-y: auto; padding-right: 10px;" id="dailyDetailList">
<div class="empty-state">
<i class="fas fa-clipboard"></i>
<p>今天还没有备忘录,添加一个吧!</p>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="closeDailyDetailModalBtn">关闭</button>
<button class="btn btn-primary" id="addNewMemoBtn">
<i class="fas fa-plus"></i> 添加详细备忘录
</button>
</div>
</div>
</div>
<!-- 导入文件输入 -->
<input type="file" id="importFileInput" accept=".json" style="display: none;">
<!-- 确认对话框 -->
<div class="modal" id="confirmationModal" style="display: none;">
<div class="confirmation-dialog">
<div class="confirmation-title" id="confirmationTitle">确认操作</div>
<div class="confirmation-message" id="confirmationMessage"></div>
<div class="confirmation-buttons">
<button class="btn btn-secondary" id="cancelConfirm">取消</button>
<button class="btn btn-danger" id="confirmAction">确认</button>
</div>
</div>
</div>
<!-- 提示信息 -->
<div class="toast" id="toast">
<i class="fas fa-check-circle"></i>
<span id="toastMessage">操作成功</span>
</div>
<script>
// ============== 🔹 Firebase 初始化 ==============
// 🔸 替换为你的真实配置!
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "calendar-memo-app.firebaseapp.com",
projectId: "calendar-memo-app",
storageBucket: "calendar-memo-app.appspot.com",
messagingSenderId: "YOUR_SENDER_ID",
appId: "YOUR_APP_ID"
};
// 初始化 Firebase(必须放在其他代码之前)
firebase.initializeApp(firebaseConfig);
const auth = firebase.auth();
const db = firebase.firestore();
// ============== 原有应用配置和状态 ==============
const AppConfig = {
DB_NAME: 'CalendarMemoDB',
DB_VERSION: 4,
DEFAULT_MONTHS_TO_SHOW: 2,
DEFAULT_REMINDER_CHECK_INTERVAL: 5,
DEFAULT_REMINDER_ADVANCE_TIME: 30,
DEFAULT_EXPORT_TIME: '23:00',
MARKDOWN_PREVIEW_DEBOUNCE: 300
};
const AppState = {
db: null,
currentDate: new Date(),
selectedDate: new Date(),
selectedMemoId: null,
currentThemeIndex: 0,
activeTab: 'taskPublish',
dailyDetailDate: new Date(),
monthsToShow: AppConfig.DEFAULT_MONTHS_TO_SHOW,
reminderTimer: null,
dueMemosCount: 0,
isInitialized: false
};
const ReminderSettings = {
checkInterval: AppConfig.DEFAULT_REMINDER_CHECK_INTERVAL,
advanceTime: AppConfig.DEFAULT_REMINDER_ADVANCE_TIME,
enableSound: true,
enableDesktopNotification: false,
autoClose: true
};
const MONTH_NAMES = ["1月", "2月", "3月", "4月", "5月", "6月",
"7月", "8月", "9月", "10月", "11月", "12月"];
const WEEKDAYS = ["日", "一", "二", "三", "四", "五", "六"];
const COLOR_THEMES = [
{ name: "深海蓝", primary: "#4361ee", secondary: "#3a0ca3", accent: "#4cc9f0" },
{ name: "森林绿", primary: "#2ecc71", secondary: "#27ae60", accent: "#1abc9c" },
{ name: "日落橙", primary: "#ff9f1c", secondary: "#ff6b6b", accent: "#ffd166" },
{ name: "薰衣草", primary: "#9b5de5", secondary: "#f15bb5", accent: "#00bbf9" },
{ name: "岩浆红", primary: "#ef476f", secondary: "#ffd166", accent: "#06d6a0" },
{ name: "午夜紫", primary: "#7209b7", secondary: "#3a0ca3", accent: "#4361ee" },
{ name: "翡翠绿", primary: "#0fa3b1", secondary: "#5d5d81", accent: "#d9e5d6" },
{ name: "珊瑚粉", primary: "#ff6b6b", secondary: "#ff9f1c", accent: "#ffe66d" },
{ name: "深空蓝", primary: "#1a1a2e", secondary: "#16213e", accent: "#0f3460" },
{ name: "秋叶黄", primary: "#d4a373", secondary: "#ccd5ae", accent: "#e9edc9" },
{ name: "海洋之心", primary: "#00b4d8", secondary: "#0077b6", accent: "#90e0ef" },
{ name: "莫兰迪粉", primary: "#e5989b", secondary: "#b5838d", accent: "#ffb4a2" },
{ name: "极光绿", primary: "#52b788", secondary: "#40916c", accent: "#95d5b2" },
{ name: "星空紫", primary: "#7b2cbf", secondary: "#5a189a", accent: "#9d4edd" },
{ name: "暖阳橙", primary: "#fb8500", secondary: "#ff9e00", accent: "#ffb703" }
];
const MEMO_COLORS = [
"#4361ee", "#3a0ca3", "#4cc9f0", "#2ecc71", "#ff9f1c",
"#9b5de5", "#ef476f", "#7209b7", "#0fa3b1", "#ff6b6b",
"#00b4d8", "#e5989b", "#52b788", "#7b2cbf", "#fb8500"
];
// ============== 工具函数 ==============
class Utils {
static formatDate(date) {
if (!date) return '';
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
static formatDateTime(date) {
if (!date) return '';
const d = new Date(date);
return d.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
static getTodayString() {
return this.formatDate(new Date());
}
static isValidDate(dateString) {
if (!dateString) return false;
const regex = /^\d{4}-\d{2}-\d{2}$/;
if (!regex.test(dateString)) return false;
const date = new Date(dateString);
return date instanceof Date && !isNaN(date);
}
static debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
static showToast(message, type = 'success') {
const toast = document.getElementById('toast');
const messageEl = document.getElementById('toastMessage');
messageEl.textContent = message;
toast.className = `toast ${type}`;
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
static showConfirmation(title, message, onConfirm) {
const modal = document.getElementById('confirmationModal');
const titleEl = document.getElementById('confirmationTitle');
const messageEl = document.getElementById('confirmationMessage');
const confirmBtn = document.getElementById('confirmAction');
const cancelBtn = document.getElementById('cancelConfirm');
titleEl.textContent = title;
messageEl.textContent = message;
modal.style.display = 'flex';
const handleConfirm = () => {
modal.style.display = 'none';
onConfirm();
confirmBtn.removeEventListener('click', handleConfirm);
cancelBtn.removeEventListener('click', handleCancel);
};
const handleCancel = () => {
modal.style.display = 'none';
confirmBtn.removeEventListener('click', handleConfirm);
cancelBtn.removeEventListener('click', handleCancel);
};
confirmBtn.addEventListener('click', handleConfirm);
cancelBtn.addEventListener('click', handleCancel);
}
}
// ============== 数据库管理器 ==============
class DatabaseManager {
static async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(AppConfig.DB_NAME, AppConfig.DB_VERSION);
request.onerror = (event) => {
console.error("数据库打开失败:", event.target.error);
reject(new Error("无法打开数据库,请检查浏览器设置。"));
};
request.onsuccess = (event) => {
AppState.db = event.target.result;
console.log("数据库打开成功");
resolve();
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
const oldVersion = event.oldVersion;
if (oldVersion < 1) {
const memoStore = db.createObjectStore('memos', { keyPath: 'id', autoIncrement: true });
memoStore.createIndex('date', 'date', { unique: false });
memoStore.createIndex('completed', 'completed', { unique: false });
memoStore.createIndex('dueTime', 'dueTime', { unique: false });
const settingsStore = db.createObjectStore('settings', { keyPath: 'key' });
}
console.log("数据库结构创建/升级成功");
};
});
}
static async getAllMemos() {
return new Promise((resolve, reject) => {
if (!AppState.db) {
reject(new Error('数据库未初始化'));
return;
}
const transaction = AppState.db.transaction(['memos'], 'readonly');
const store = transaction.objectStore('memos');
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = (event) => reject(event.target.error);
});
}
static async saveMemo(memo) {
return new Promise((resolve, reject) => {
if (!AppState.db) {
reject(new Error('数据库未初始化'));
return;
}
const transaction = AppState.db.transaction(['memos'], 'readwrite');
const store = transaction.objectStore('memos');
memo.updatedAt = new Date().toISOString();
if (!memo.createdAt) {
memo.createdAt = memo.updatedAt;
}
const request = memo.id ? store.put(memo) : store.add(memo);
request.onsuccess = () => resolve(request.result);
request.onerror = (event) => reject(event.target.error);
});
}
static async deleteMemo(memoId) {
return new Promise((resolve, reject) => {
if (!AppState.db) {
reject(new Error('数据库未初始化'));
return;
}
const transaction = AppState.db.transaction(['memos'], 'readwrite');
const store = transaction.objectStore('memos');
const request = store.delete(memoId);
request.onsuccess = () => resolve();
request.onerror = (event) => reject(event.target.error);
});
}
static async getSetting(key) {
return new Promise((resolve, reject) => {
if (!AppState.db) {
reject(new Error('数据库未初始化'));
return;
}
const transaction = AppState.db.transaction(['settings'], 'readonly');
const store = transaction.objectStore('settings');
const request = store.get(key);
request.onsuccess = () => resolve(request.result ? request.result.value : null);
request.onerror = (event) => reject(event.target.error);
});
}
static async saveSetting(key, value) {
return new Promise((resolve, reject) => {
if (!AppState.db) {
reject(new Error('数据库未初始化'));
return;
}
const transaction = AppState.db.transaction(['settings'], 'readwrite');
const store = transaction.objectStore('settings');
const request = store.put({ key, value });
request.onsuccess = () => resolve();
request.onerror = (event) => reject(event.target.error);
});
}
static async clearAllData() {
return new Promise((resolve, reject) => {
if (!AppState.db) {
reject(new Error('数据库未初始化'));
return;
}
const transaction = AppState.db.transaction(['memos', 'settings'], 'readwrite');
const memoStore = transaction.objectStore('memos');
const settingsStore = transaction.objectStore('settings');
memoStore.clear();
settingsStore.clear();
transaction.oncomplete = () => resolve();
transaction.onerror = (event) => reject(event.target.error);
});
}
}
// ============== 🔹 云端同步管理器 ==============
class CloudSyncManager {
static getCurrentUser() {
return auth.currentUser;
}
static async login(email, password) {
try {
await auth.signInWithEmailAndPassword(email, password);
Utils.showToast('✅ 登录成功');
await this.syncFromCloud();
} catch (error) {
Utils.showToast('❌ 登录失败: ' + error.message, 'error');
}
}
static async register(email, password) {
try {
await auth.createUserWithEmailAndPassword(email, password);
Utils.showToast('✅ 注册成功,请登录');
} catch (error) {
Utils.showToast('❌ 注册失败: ' + error.message, 'error');
}
}
static async syncToCloud() {
const user = this.getCurrentUser();
if (!user) return;
try {
const memos = await DatabaseManager.getAllMemos();
const batch = db.batch();
const userRef = db.collection('users').doc(user.uid);
// 清空
const oldSnapshot = await userRef.collection('memos').get();
oldSnapshot.docs.forEach(doc => batch.delete(doc.ref));
// 写入
memos.forEach(memo => {
batch.set(userRef.collection('memos').doc(memo.id.toString()), memo);
});
await batch.commit();
Utils.showToast('✅ 数据已同步到云端');
} catch (error) {
console.error('上传失败:', error);
Utils.showToast('❌ 同步失败: ' + error.message, 'error');
}
}
static async syncFromCloud() {
const user = this.getCurrentUser();
if (!user) return;
try {
const userRef = db.collection('users').doc(user.uid);
const snapshot = await userRef.collection('memos').get();
const cloudMemos = [];
snapshot.forEach(doc => cloudMemos.push(doc.data()));
await DatabaseManager.clearAllData();
for (const memo of cloudMemos) {
if (typeof memo.id === 'string') memo.id = parseInt(memo.id);
await DatabaseManager.saveMemo(memo);
}
Utils.showToast(`✅ 同步了 ${cloudMemos.length} 条备忘录`);
CalendarManager.renderMultiMonthCalendar();
MemoManager.updateRecentTasks();
ReminderManager.updateReminderBadge();
} catch (error) {
console.error('下载失败:', error);
Utils.showToast('❌ 同步失败: ' + error.message, 'error');
}
}
}
// ============== 日历管理器 ==============
class CalendarManager {
static renderMultiMonthCalendar() {
const container = document.getElementById('multiMonthCalendar');
const periodDisplay = document.getElementById('currentPeriod');
container.innerHTML = '';
container.className = `multi-month-calendar grid-${AppState.monthsToShow <= 2 ? AppState.monthsToShow : 2}`;
const months = [];
const startMonth = new Date(AppState.currentDate);
const endMonth = new Date(AppState.currentDate);
endMonth.setMonth(endMonth.getMonth() + AppState.monthsToShow - 1);
for (let i = 0; i < AppState.monthsToShow; i++) {
const monthDate = new Date(AppState.currentDate);
monthDate.setMonth(AppState.currentDate.getMonth() + i);
months.push(monthDate);
}
if (AppState.monthsToShow === 1) {
periodDisplay.textContent = `${startMonth.getFullYear()}年${startMonth.getMonth() + 1}月`;
} else {
periodDisplay.textContent =
`${startMonth.getFullYear()}年${startMonth.getMonth() + 1}月 - ` +
`${endMonth.getFullYear()}年${endMonth.getMonth() + 1}月`;
}
months.forEach((monthDate, index) => {
const monthCalendar = this.createMonthCalendar(monthDate, index);
container.appendChild(monthCalendar);
setTimeout(() => this.loadMemosForMonth(monthDate, index), 0);
});
}
static createMonthCalendar(monthDate, index) {
const monthCalendar = document.createElement('div');
monthCalendar.className = 'month-calendar';
if (AppState.monthsToShow > 4) {
monthCalendar.classList.add('small');
}
monthCalendar.id = `monthCalendar${index}`;
monthCalendar.dataset.month = monthDate.getMonth();
monthCalendar.dataset.year = monthDate.getFullYear();
const monthName = MONTH_NAMES[monthDate.getMonth()];
monthCalendar.innerHTML = `
<div class="month-header">
<div class="month-title">
${monthDate.getFullYear()}年 ${monthName}
</div>
<div class="month-right-area">
<div class="month-stats" id="monthStats${index}">
<div class="stat-item total">
<i class="fas fa-tasks"></i>
<span class="stat-count-total">0</span>
</div>
<div class="stat-item completed">
<i class="fas fa-check-circle"></i>
<span class="stat-count-completed">0</span>
</div>
<div class="stat-item pending">
<i class="fas fa-clock"></i>
<span class="stat-count-pending">0</span>
</div>
</div>
<div class="month-progress">
<div class="progress-circle" id="progressCircle${index}">
<svg viewBox="0 0 36 36">
<path class="progress-circle-bg" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"></path>
<path class="progress-circle-fill" stroke-dasharray="0, 100" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"></path>
</svg>
<div class="progress-percent">0%</div>
</div>
</div>
<button class="complete-all-btn" data-month-index="${index}">
<i class="fas fa-check-double"></i> 一键完成
</button>
</div>
</div>
<div class="weekdays">
${WEEKDAYS.map(day => `<div>${day}</div>`).join('')}
</div>
<div class="calendar-grid" id="calendarGrid${index}">
${this.generateCalendarDays(monthDate)}
</div>
`;
const completeAllBtn = monthCalendar.querySelector('.complete-all-btn');
completeAllBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.completeAllMemosForMonth(monthDate.getMonth(), monthDate.getFullYear());
});
return monthCalendar;
}
static generateCalendarDays(monthDate) {
const firstDay = new Date(monthDate.getFullYear(), monthDate.getMonth(), 1);
const lastDay = new Date(monthDate.getFullYear(), monthDate.getMonth() + 1, 0);
const firstDayIndex = firstDay.getDay();
const prevMonthLastDay = new Date(monthDate.getFullYear(), monthDate.getMonth(), 0).getDate();
const today = new Date();
const todayString = Utils.formatDate(today);
let html = '';
for (let i = firstDayIndex; i > 0; i--) {
const dayNum = prevMonthLastDay - i + 1;
html += `<div class="calendar-day other-month">${dayNum}</div>`;
}
for (let i = 1; i <= lastDay.getDate(); i++) {
const dateString = Utils.formatDate(new Date(monthDate.getFullYear(), monthDate.getMonth(), i));
const isToday = dateString === todayString;
const dayClass = isToday ? 'calendar-day today' : 'calendar-day';
html += `
<div class="${dayClass}" data-date="${dateString}">
<div class="day-number">${i}</div>
<div class="day-memos" id="dayMemos-${dateString}"></div>
</div>
`;
}
const totalCells = 42;
const remainingCells = totalCells - (firstDayIndex + lastDay.getDate());
for (let i = 1; i <= remainingCells; i++) {
html += `<div class="calendar-day other-month">${i}</div>`;
}
return html;
}
static async loadMemosForMonth(monthDate, monthIndex) {
try {
const allMemos = await DatabaseManager.getAllMemos();
const searchTerm = document.getElementById('searchInput').value.trim().toLowerCase();
const monthStart = new Date(monthDate.getFullYear(), monthDate.getMonth(), 1);
const monthEnd = new Date(monthDate.getFullYear(), monthDate.getMonth() + 1, 0);
const monthStartStr = Utils.formatDate(monthStart);
const monthEndStr = Utils.formatDate(monthEnd);
let totalMemos = 0;
let completedMemos = 0;
const dayMemosMap = new Map();
allMemos.forEach(memo => {
if (memo.date >= monthStartStr && memo.date <= monthEndStr) {
if (searchTerm && !this.memoMatchesSearch(memo, searchTerm)) {
return;
}
totalMemos++;
if (memo.completed) completedMemos++;
if (!dayMemosMap.has(memo.date)) {
dayMemosMap.set(memo.date, []);
}
dayMemosMap.get(memo.date).push(memo);
}
});
this.updateMonthStats(monthIndex, totalMemos, completedMemos);
dayMemosMap.forEach((memos, date) => {
this.renderDayMemos(date, memos);
});
} catch (error) {
console.error('加载月份备忘录失败:', error);
}
}
static memoMatchesSearch(memo, searchTerm) {
if (!searchTerm) return true;
return (memo.title && memo.title.toLowerCase().includes(searchTerm)) ||
(memo.content && memo.content.toLowerCase().includes(searchTerm)) ||
(memo.date && memo.date.includes(searchTerm));
}
static renderDayMemos(dateString, memos) {
const dayMemosEl = document.getElementById(`dayMemos-${dateString}`);
if (!dayMemosEl) return;
dayMemosEl.innerHTML = '';
if (memos.length === 0) return;
const displayMemos = memos.slice(0, 5);
displayMemos.forEach(memo => {
const memoItem = document.createElement('div');
memoItem.className = `day-memo-item ${memo.completed ? 'completed' : ''}`;
memoItem.title = memo.title;
memoItem.dataset.id = memo.id;
memoItem.style.borderLeftColor = memo.color || MEMO_COLORS[0];
let displayText = memo.title;
if (AppState.monthsToShow > 4) {
displayText = memo.title.length > 5 ? memo.title.substring(0, 5) + '...' : memo.title;
} else if (AppState.monthsToShow > 1) {
displayText = memo.title.length > 10 ? memo.title.substring(0, 10) + '...' : memo.title;
} else {
displayText = memo.title.length > 15 ? memo.title.substring(0, 15) + '...' : memo.title;
}
memoItem.innerHTML = `
<span class="memo-color-dot" style="background-color: ${memo.color || MEMO_COLORS[0]}"></span>
${displayText}
`;
memoItem.addEventListener('click', (e) => {
e.stopPropagation();
MemoManager.openMemoModal(memo.id);
});
dayMemosEl.appendChild(memoItem);
});
if (memos.length > 5) {
const memoCount = document.createElement('div');
memoCount.className = 'memo-count';
memoCount.textContent = memos.length > 99 ? '99+' : memos.length;
dayMemosEl.parentElement.appendChild(memoCount);
}
}
static updateMonthStats(monthIndex, total, completed) {
const statsEl = document.getElementById(`monthStats${monthIndex}`);
if (!statsEl) return;
const pending = total - completed;
const progressPercent = total > 0 ? Math.round((completed / total) * 100) : 0;
statsEl.querySelector('.stat-count-total').textContent = total;
statsEl.querySelector('.stat-count-completed').textContent = completed;
statsEl.querySelector('.stat-count-pending').textContent = pending;
const progressCircle = document.getElementById(`progressCircle${monthIndex}`);
if (progressCircle) {
const fill = progressCircle.querySelector('.progress-circle-fill');
const percentText = progressCircle.querySelector('.progress-percent');
fill.style.strokeDasharray = `${progressPercent}, 100`;
percentText.textContent = `${progressPercent}%`;
}
}
static async completeAllMemosForMonth(month, year) {
Utils.showConfirmation(
'一键完成确认',
`确定要将${year}年${month + 1}月的所有备忘录标记为已完成吗?`,
async () => {
try {
const allMemos = await DatabaseManager.getAllMemos();
const monthStart = new Date(year, month, 1);
const monthEnd = new Date(year, month + 1, 0);
const monthStartStr = Utils.formatDate(monthStart);
const monthEndStr = Utils.formatDate(monthEnd);
let completedCount = 0;
const updatePromises = [];
allMemos.forEach(memo => {
if (memo.date >= monthStartStr && memo.date <= monthEndStr && !memo.completed) {
memo.completed = true;
memo.updatedAt = new Date().toISOString();
updatePromises.push(DatabaseManager.saveMemo(memo));
completedCount++;
}
});
await Promise.all(updatePromises);
Utils.showToast(`已成功将 ${completedCount} 个备忘录标记为完成!`);
this.renderMultiMonthCalendar();
MemoManager.updateRecentTasks();
ReminderManager.updateReminderBadge();
if (document.getElementById('dailyDetailModal').classList.contains('active')) {
DailyDetailManager.loadDailyDetailMemos(AppState.dailyDetailDate);
}
} catch (error) {
console.error('一键完成失败:', error);
Utils.showToast('操作失败,请重试', 'error');
}
}
);
}
}
// ============== 备忘录管理器 ==============
class MemoManager {
static debouncedPreview = null;
static async openMemoModal(memoId = null, date = null) {
const modal = document.getElementById('memoModal');
const deleteBtn = document.getElementById('deleteMemo');
const modalTitle = document.querySelector('#memoModal .modal-title');
AppState.selectedMemoId = memoId;
document.getElementById('memoForm').reset();
this.initColorPicker();
const today = Utils.formatDate(new Date());
if (date) {
document.getElementById('memoDate').value = Utils.formatDate(date);
} else {
document.getElementById('memoDate').value = today;
}
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(18, 0, 0, 0);
document.getElementById('memoDueTime').value = tomorrow.toISOString().slice(0, 16);
if (memoId) {
modalTitle.textContent = '编辑备忘录';
deleteBtn.style.display = 'inline-block';
await this.loadMemoData(memoId);
} else {
modalTitle.textContent = '新建备忘录';
deleteBtn.style.display = 'none';
}
modal.classList.add('active');
this.updateMarkdownPreview();
const memoContent = document.getElementById('memoContent');
memoContent.removeEventListener('input', this.debouncedPreview);
this.debouncedPreview = Utils.debounce(() => this.updateMarkdownPreview(), AppConfig.MARKDOWN_PREVIEW_DEBOUNCE);
memoContent.addEventListener('input', this.debouncedPreview);
}
static initColorPicker() {
const colorOptionsEl = document.getElementById('colorOptions');
colorOptionsEl.innerHTML = '';
MEMO_COLORS.forEach(color => {
const colorOption = document.createElement('div');
colorOption.className = 'color-option';
colorOption.style.backgroundColor = color;
colorOption.dataset.color = color;
colorOption.addEventListener('click', function() {
document.querySelectorAll('#colorOptions .color-option').forEach(el => {
el.classList.remove('selected');
});
this.classList.add('selected');
});
colorOptionsEl.appendChild(colorOption);
});
colorOptionsEl.firstChild.classList.add('selected');
}
static async loadMemoData(memoId) {
try {
const allMemos = await DatabaseManager.getAllMemos();
const memo = allMemos.find(m => m.id === memoId);
if (memo) {
document.getElementById('memoTitle').value = memo.title || '';
document.getElementById('memoDate').value = memo.date || '';
document.getElementById('memoDueTime').value = memo.dueTime ?
memo.dueTime.slice(0, 16) : '';
document.getElementById('memoContent').value = memo.content || '';
document.getElementById('memoCompleted').checked = memo.completed || false;
if (memo.color) {
document.querySelectorAll('#colorOptions .color-option').forEach(el => {
el.classList.remove('selected');
if (el.dataset.color === memo.color) {
el.classList.add('selected');
}
});
}
this.updateMarkdownPreview();
}
} catch (error) {
console.error('加载备忘录数据失败:', error);
Utils.showToast('加载备忘录失败', 'error');
}
}
static updateMarkdownPreview() {
const content = document.getElementById('memoContent').value;
const previewEl = document.getElementById('markdownPreview');
if (content.trim() === '') {
previewEl.innerHTML = '<p style="color: #6c757d; font-style: italic;">预览将在这里显示...</p>';
return;
}
marked.setOptions({
highlight: function(code, lang) {
if (lang && hljs.getLanguage(lang)) {
return hljs.highlight(code, { language: lang }).value;
}
return hljs.highlightAuto(code).value;
},
breaks: true,
gfm: true
});
try {
previewEl.innerHTML = marked.parse(content);
} catch (error) {
previewEl.innerHTML = '<p style="color: #f44336;">Markdown解析错误</p>';
}
}
static async saveMemo() {
const title = document.getElementById('memoTitle').value.trim();
const date = document.getElementById('memoDate').value;
const dueTime = document.getElementById('memoDueTime').value;
const content = document.getElementById('memoContent').value.trim();
const completed = document.getElementById('memoCompleted').checked;
if (!title) {
Utils.showToast('请输入备忘录标题', 'error');
return;
}
if (!date || !Utils.isValidDate(date)) {
Utils.showToast('请选择有效的日期', 'error');
return;
}
const selectedColor = document.querySelector('#colorOptions .color-option.selected').dataset.color;
const memo = {
title,
date,
dueTime: dueTime ? new Date(dueTime).toISOString() : null,
content,
color: selectedColor,
completed,
reminderShown: false
};
if (AppState.selectedMemoId) {
memo.id = AppState.selectedMemoId;
}
try {
await DatabaseManager.saveMemo(memo);
Utils.showToast('备忘录保存成功');
this.closeMemoModal();
CalendarManager.renderMultiMonthCalendar();
this.updateRecentTasks();
ReminderManager.updateReminderBadge();
if (document.getElementById('dailyDetailModal').classList.contains('active')) {
DailyDetailManager.loadDailyDetailMemos(AppState.dailyDetailDate);
}
} catch (error) {
console.error('保存备忘录失败:', error);
Utils.showToast('保存失败,请重试', 'error');
}
}
static async deleteMemo() {
if (!AppState.selectedMemoId) return;
Utils.showConfirmation(
'删除确认',
'确定要删除这个备忘录吗?此操作不可撤销。',
async () => {
try {
await DatabaseManager.deleteMemo(AppState.selectedMemoId);
Utils.showToast('备忘录删除成功');
this.closeMemoModal();
CalendarManager.renderMultiMonthCalendar();
this.updateRecentTasks();
ReminderManager.updateReminderBadge();
if (document.getElementById('dailyDetailModal').classList.contains('active')) {
DailyDetailManager.loadDailyDetailMemos(AppState.dailyDetailDate);
}
} catch (error) {
console.error('删除备忘录失败:', error);
Utils.showToast('删除失败,请重试', 'error');
}
}
);
}
static closeMemoModal() {
const modal = document.getElementById('memoModal');
modal.classList.remove('active');
AppState.selectedMemoId = null;
}
static async updateRecentTasks() {
try {
const allMemos = await DatabaseManager.getAllMemos();
const recentTasksEl = document.getElementById('recentTasksList');
recentTasksEl.innerHTML = '';
const recentMemos = allMemos
.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))
.slice(0, 10);
if (recentMemos.length === 0) {
recentTasksEl.innerHTML = `
<div class="empty-state">
<i class="fas fa-clipboard-list"></i>
<p>暂无任务,点击日历上的日期添加新任务</p>
</div>
`;
return;
}
recentMemos.forEach(memo => {
const taskItem = this.createTaskItem(memo);
recentTasksEl.appendChild(taskItem);
});
this.bindRecentTaskEvents();
} catch (error) {
console.error('更新最近任务失败:', error);
}
}
static createTaskItem(memo) {
const taskItem = document.createElement('div');
taskItem.className = 'task-item';
taskItem.style.borderLeftColor = memo.color || MEMO_COLORS[0];
let countdownHtml = '';
if (memo.dueTime && !memo.completed) {
const dueDate = new Date(memo.dueTime);
const now = new Date();
const timeDiff = dueDate - now;
const daysDiff = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
let countdownClass = 'success';
let countdownText = '';
if (daysDiff < 0) {
countdownClass = 'danger';
countdownText = `已过期 ${Math.abs(daysDiff)} 天`;
} else if (daysDiff === 0) {
countdownClass = 'danger';
countdownText = '今天到期';
} else if (daysDiff <= 3) {
countdownClass = 'warning';
countdownText = `${daysDiff} 天后到期`;
} else {
countdownText = `${daysDiff} 天后到期`;
}
countdownHtml = `<span class="countdown ${countdownClass}">${countdownText}</span>`;
}
const dueDateStr = memo.dueTime ?
new Date(memo.dueTime).toLocaleDateString('zh-CN') : '无截止日期';
const contentPreview = memo.content ?
memo.content.replace(/[#*`[\]]/g, '').substring(0, 60) +
(memo.content.length > 60 ? '...' : '') :
'无内容';
taskItem.innerHTML = `
<div class="task-header">
<div class="task-title">${memo.title || '无标题'}</div>
<div class="task-color" style="background-color: ${memo.color || MEMO_COLORS[0]}"></div>
</div>
<div class="task-due">
<i class="far fa-calendar-alt"></i> ${dueDateStr} ${countdownHtml}
</div>
<div class="task-content">${contentPreview}</div>
<div class="task-actions">
<button class="task-btn task-btn-complete" data-id="${memo.id}">
${memo.completed ? '<i class="fas fa-undo"></i> 标记为未完成' : '<i class="fas fa-check"></i> 标记为完成'}
</button>
<button class="task-btn task-btn-edit" data-id="${memo.id}">
<i class="fas fa-edit"></i> 编辑
</button>
<button class="task-btn task-btn-delete" data-id="${memo.id}">
<i class="fas fa-trash"></i> 删除
</button>
</div>
`;
return taskItem;
}
static bindRecentTaskEvents() {
const recentTasksEl = document.getElementById('recentTasksList');
if (!recentTasksEl) return;
recentTasksEl.addEventListener('click', (e) => {
const target = e.target;
if (target.classList.contains('task-btn-complete') ||
target.closest('.task-btn-complete')) {
const btn = target.classList.contains('task-btn-complete') ?
target : target.closest('.task-btn-complete');
const memoId = parseInt(btn.dataset.id);
if (memoId) {
this.toggleMemoCompletion(memoId);
}
e.stopPropagation();
}
else if (target.classList.contains('task-btn-edit') ||
target.closest('.task-btn-edit')) {
const btn = target.classList.contains('task-btn-edit') ?
target : target.closest('.task-btn-edit');
const memoId = parseInt(btn.dataset.id);
if (memoId) {
App.closeFunctionsModal();
this.openMemoModal(memoId);
}
e.stopPropagation();
}
else if (target.classList.contains('task-btn-delete') ||
target.closest('.task-btn-delete')) {
const btn = target.classList.contains('task-btn-delete') ?
target : target.closest('.task-btn-delete');
const memoId = parseInt(btn.dataset.id);
if (memoId) {
this.deleteMemoById(memoId);
}
e.stopPropagation();
}
});
}
static async toggleMemoCompletion(memoId) {
try {
const allMemos = await DatabaseManager.getAllMemos();
const memo = allMemos.find(m => m.id === memoId);
if (memo) {
memo.completed = !memo.completed;
memo.updatedAt = new Date().toISOString();
if (memo.completed) {
memo.reminderShown = false;
const reminderKey = `reminder_${memo.id}_${new Date().toDateString()}`;
localStorage.removeItem(reminderKey);
}
await DatabaseManager.saveMemo(memo);
Utils.showToast(`备忘录已${memo.completed ? '完成' : '恢复为未完成'}`);
CalendarManager.renderMultiMonthCalendar();
this.updateRecentTasks();
ReminderManager.updateReminderBadge();
const dailyDetailModal = document.getElementById('dailyDetailModal');
if (dailyDetailModal.classList.contains('active')) {
DailyDetailManager.loadDailyDetailMemos(AppState.dailyDetailDate);
}
}
} catch (error) {
console.error('切换备忘录状态失败:', error);
Utils.showToast('操作失败,请重试', 'error');
}
}
static async deleteMemoById(memoId) {
Utils.showConfirmation(
'删除确认',
'确定要删除这个备忘录吗?此操作不可撤销。',
async () => {
try {
await DatabaseManager.deleteMemo(memoId);
Utils.showToast('备忘录删除成功');
CalendarManager.renderMultiMonthCalendar();
this.updateRecentTasks();
ReminderManager.updateReminderBadge();
const dailyDetailModal = document.getElementById('dailyDetailModal');
if (dailyDetailModal.classList.contains('active')) {
DailyDetailManager.loadDailyDetailMemos(AppState.dailyDetailDate);
}
} catch (error) {
console.error('删除备忘录失败:', error);
Utils.showToast('删除失败,请重试', 'error');
}
}
);
}
}
// ============== 每日详情管理器 ==============
class DailyDetailManager {
static openDailyDetailModal(date) {
const modal = document.getElementById('dailyDetailModal');
AppState.dailyDetailDate = date;
const dateDisplay = document.getElementById('dailyDetailDate');
dateDisplay.textContent =
`${date.getFullYear()}年 ${date.getMonth() + 1}月${date.getDate()}日`;
document.getElementById('quickMemoTitle').value = '';
this.loadDailyDetailMemos(date);
modal.classList.add('active');
setTimeout(() => {
document.getElementById('quickMemoTitle').focus();
}, 100);
}
static async loadDailyDetailMemos(date) {
try {
const allMemos = await DatabaseManager.getAllMemos();
const memoListEl = document.getElementById('dailyDetailList');
const dateStr = Utils.formatDate(date);
memoListEl.innerHTML = '';
const dayMemos = allMemos.filter(memo => memo.date === dateStr);
if (dayMemos.length === 0) {
memoListEl.innerHTML = `
<div class="empty-state">
<i class="fas fa-clipboard"></i>
<p>这一天还没有备忘录,添加一个吧!</p>
</div>
`;
return;
}
dayMemos.forEach(memo => {
const taskItem = MemoManager.createTaskItem(memo);
memoListEl.appendChild(taskItem);
});
this.bindDailyDetailEvents();
} catch (error) {
console.error('加载每日详情失败:', error);
}
}
static bindDailyDetailEvents() {
const dailyDetailList = document.getElementById('dailyDetailList');
if (!dailyDetailList) return;
dailyDetailList.addEventListener('click', (e) => {
const target = e.target;
if (target.classList.contains('task-btn-complete') ||
target.closest('.task-btn-complete')) {
const btn = target.classList.contains('task-btn-complete') ?
target : target.closest('.task-btn-complete');
const memoId = parseInt(btn.dataset.id);
if (memoId) {
MemoManager.toggleMemoCompletion(memoId);
}
e.stopPropagation();
}
else if (target.classList.contains('task-btn-edit') ||
target.closest('.task-btn-edit')) {
const btn = target.classList.contains('task-btn-edit') ?
target : target.closest('.task-btn-edit');
const memoId = parseInt(btn.dataset.id);
if (memoId) {
this.closeDailyDetailModal();
MemoManager.openMemoModal(memoId);
}
e.stopPropagation();
}
else if (target.classList.contains('task-btn-delete') ||
target.closest('.task-btn-delete')) {
const btn = target.classList.contains('task-btn-delete') ?
target : target.closest('.task-btn-delete');
const memoId = parseInt(btn.dataset.id);
if (memoId) {
MemoManager.deleteMemoById(memoId);
}
e.stopPropagation();
}
});
}
static async quickAddMemo() {
const title = document.getElementById('quickMemoTitle').value.trim();
if (!title) {
Utils.showToast('请输入备忘录标题', 'error');
return;
}
const dateStr = Utils.formatDate(AppState.dailyDetailDate);
const memo = {
title: title,
date: dateStr,
content: '',
color: MEMO_COLORS[Math.floor(Math.random() * MEMO_COLORS.length)],
completed: false,
reminderShown: false
};
try {
await DatabaseManager.saveMemo(memo);
Utils.showToast('快速备忘录添加成功');
document.getElementById('quickMemoTitle').value = '';
this.loadDailyDetailMemos(AppState.dailyDetailDate);
CalendarManager.renderMultiMonthCalendar();
ReminderManager.updateReminderBadge();
} catch (error) {
console.error('快速添加备忘录失败:', error);
Utils.showToast('添加失败,请重试', 'error');
}
}
static closeDailyDetailModal() {
const modal = document.getElementById('dailyDetailModal');
modal.classList.remove('active');
}
}
// ============== 提醒管理器 ==============
class ReminderManager {
static init() {
this.loadReminderSettings();
this.startReminderChecker();
this.updateReminderBadge();
}
static async loadReminderSettings() {
try {
const checkInterval = await DatabaseManager.getSetting('reminderCheckInterval');
const advanceTime = await DatabaseManager.getSetting('reminderAdvanceTime');
const enableSound = await DatabaseManager.getSetting('enableSoundReminder');
const enableDesktop = await DatabaseManager.getSetting('enableDesktopNotification');
if (checkInterval !== null) ReminderSettings.checkInterval = parseInt(checkInterval);
if (advanceTime !== null) ReminderSettings.advanceTime = parseInt(advanceTime);
if (enableSound !== null) ReminderSettings.enableSound = enableSound === 'true';
if (enableDesktop !== null) ReminderSettings.enableDesktopNotification = enableDesktop === 'true';
document.getElementById('reminderCheckInterval').value = ReminderSettings.checkInterval;
document.getElementById('reminderAdvanceTime').value = ReminderSettings.advanceTime;
document.getElementById('enableSoundReminder').checked = ReminderSettings.enableSound;
document.getElementById('enableDesktopNotification').checked = ReminderSettings.enableDesktopNotification;
} catch (error) {
console.error('加载提醒设置失败:', error);
}
}
static async saveReminderSettings() {
try {
const checkInterval = parseInt(document.getElementById('reminderCheckInterval').value);
const advanceTime = parseInt(document.getElementById('reminderAdvanceTime').value);
const enableSound = document.getElementById('enableSoundReminder').checked;
const enableDesktop = document.getElementById('enableDesktopNotification').checked;
ReminderSettings.checkInterval = checkInterval;
ReminderSettings.advanceTime = advanceTime;
ReminderSettings.enableSound = enableSound;
ReminderSettings.enableDesktopNotification = enableDesktop;
await DatabaseManager.saveSetting('reminderCheckInterval', checkInterval.toString());
await DatabaseManager.saveSetting('reminderAdvanceTime', advanceTime.toString());
await DatabaseManager.saveSetting('enableSoundReminder', enableSound.toString());
await DatabaseManager.saveSetting('enableDesktopNotification', enableDesktop.toString());
Utils.showToast('提醒设置已保存');
this.startReminderChecker();
} catch (error) {
console.error('保存提醒设置失败:', error);
Utils.showToast('保存失败,请重试', 'error');
}
}
static startReminderChecker() {
if (AppState.reminderTimer) {
clearInterval(AppState.reminderTimer);
}
this.checkDueMemos();
const interval = ReminderSettings.checkInterval * 60 * 1000;
AppState.reminderTimer = setInterval(() => {
this.checkDueMemos();
}, interval);
}
static async checkDueMemos() {
try {
const allMemos = await DatabaseManager.getAllMemos();
const now = new Date();
const advanceTime = ReminderSettings.advanceTime * 60 * 1000;
const dueMemos = [];
const today = new Date().toDateString();
allMemos.forEach(memo => {
if (memo.dueTime && !memo.completed) {
const dueTime = new Date(memo.dueTime);
const reminderTime = new Date(dueTime.getTime() - advanceTime);
if (now >= reminderTime) {
const reminderKey = `reminder_${memo.id}_${today}`;
const shownToday = localStorage.getItem(reminderKey);
if (!shownToday) {
dueMemos.push({
...memo,
reminderKey: reminderKey
});
localStorage.setItem(reminderKey, 'true');
memo.reminderShown = true;
DatabaseManager.saveMemo(memo);
}
}
}
});
if (dueMemos.length > 0) {
this.showReminderModal(dueMemos);
}
this.updateReminderBadge();
} catch (error) {
console.error('检查到期备忘录失败:', error);
}
}
static async updateReminderBadge() {
try {
const allMemos = await DatabaseManager.getAllMemos();
const now = new Date();
const advanceTime = ReminderSettings.advanceTime * 60 * 1000;
const today = new Date().toDateString();
let count = 0;
allMemos.forEach(memo => {
if (memo.dueTime && !memo.completed) {
const dueTime = new Date(memo.dueTime);
const reminderTime = new Date(dueTime.getTime() - advanceTime);
const reminderKey = `reminder_${memo.id}_${today}`;
const shownToday = localStorage.getItem(reminderKey);
if (now >= reminderTime && !shownToday) {
count++;
}
}
});
AppState.dueMemosCount = count;
const badge = document.getElementById('reminderBadge');
const bellButton = document.getElementById('floatingReminder');
if (count > 0) {
badge.textContent = count > 99 ? '99+' : count;
badge.style.display = 'flex';
bellButton.classList.add('reminder-pulse');
} else {
badge.style.display = 'none';
bellButton.classList.remove('reminder-pulse');
}
} catch (error) {
console.error('更新提醒徽章失败:', error);
}
}
static showReminderModal(dueMemos = null) {
const modal = document.getElementById('reminderModal');
if (dueMemos && dueMemos.length > 0) {
this.displayReminders(dueMemos);
} else {
this.loadAndDisplayReminders();
}
modal.classList.add('active');
if (ReminderSettings.enableSound) {
this.playReminderSound();
}
if (ReminderSettings.autoClose) {
setTimeout(() => {
if (modal.classList.contains('active')) {
this.closeReminderModal();
}
}, 10000);
}
}
static displayReminders(memos) {
const reminderList = document.getElementById('reminderList');
reminderList.innerHTML = '';
if (memos.length === 0) {
reminderList.innerHTML = `
<div class="empty-state">
<i class="fas fa-bell-slash"></i>
<p>暂无到期提醒</p>
</div>
`;
return;
}
memos.forEach(memo => {
const reminderItem = this.createReminderItem(memo);
reminderList.appendChild(reminderItem);
});
}
static createReminderItem(memo) {
const reminderItem = document.createElement('div');
reminderItem.className = 'reminder-item';
const dueTime = new Date(memo.dueTime);
const now = new Date();
const timeDiff = dueTime - now;
const hoursDiff = Math.floor(timeDiff / (1000 * 60 * 60));
const minutesDiff = Math.floor((timeDiff % (1000 * 60 * 60)) / (1000 * 60));
let statusText = '';
let statusColor = 'var(--danger-color)';
if (timeDiff < 0) {
statusText = `已过期 ${Math.abs(hoursDiff)}小时${Math.abs(minutesDiff)}分钟`;
statusColor = 'var(--danger-color)';
} else if (timeDiff < 60 * 60 * 1000) {
statusText = `${minutesDiff}分钟后到期`;
statusColor = 'var(--warning-color)';
} else {
statusText = `${hoursDiff}小时${minutesDiff}分钟后到期`;
statusColor = 'var(--primary-color)';
}
reminderItem.innerHTML = `
<div class="reminder-item-title">${memo.title}</div>
<div class="reminder-item-details">
<span><i class="far fa-calendar"></i> ${dueTime.toLocaleDateString('zh-CN')} ${dueTime.toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit'})}</span>
<span style="color: ${statusColor}; font-weight: 600;">${statusText}</span>
</div>
`;
reminderItem.addEventListener('click', () => {
MemoManager.openMemoModal(memo.id);
this.closeReminderModal();
});
return reminderItem;
}
static async loadAndDisplayReminders() {
try {
const allMemos = await DatabaseManager.getAllMemos();
const now = new Date();
const advanceTime = ReminderSettings.advanceTime * 60 * 1000;
const today = new Date().toDateString();
const dueMemos = [];
allMemos.forEach(memo => {
if (memo.dueTime && !memo.completed) {
const dueTime = new Date(memo.dueTime);
const reminderTime = new Date(dueTime.getTime() - advanceTime);
const reminderKey = `reminder_${memo.id}_${today}`;
const shownToday = localStorage.getItem(reminderKey);
if (now >= reminderTime && !shownToday) {
dueMemos.push(memo);
}
}
});
this.displayReminders(dueMemos);
} catch (error) {
console.error('加载提醒失败:', error);
}
}
static playReminderSound() {
try {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.value = 800;
oscillator.type = 'sine';
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 1);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 1);
} catch (e) {
console.log('无法播放提示音:', e);
}
}
static closeReminderModal() {
const modal = document.getElementById('reminderModal');
modal.classList.remove('active');
}
static markAllRemindersAsRead() {
const today = new Date().toDateString();
Object.keys(localStorage).forEach(key => {
if (key.includes('reminder_') && key.includes(today)) {
localStorage.removeItem(key);
}
});
DatabaseManager.getAllMemos().then(memos => {
memos.forEach(memo => {
if (memo.reminderShown) {
memo.reminderShown = false;
DatabaseManager.saveMemo(memo);
}
});
});
this.closeReminderModal();
this.updateReminderBadge();
Utils.showToast('所有提醒已标记为已读');
}
static testReminder() {
const testMemos = [{
id: 999,
title: '测试提醒',
dueTime: new Date().toISOString(),
content: '这是一个测试提醒'
}];
this.showReminderModal(testMemos);
}
}
// ============== 主题管理器 ==============
class ThemeManager {
static init() {
this.initThemeSelector();
this.applyTheme(AppState.currentThemeIndex);
}
static initThemeSelector() {
const themeSelector = document.getElementById('themeSelector');
themeSelector.innerHTML = '';
COLOR_THEMES.forEach((theme, index) => {
const themeColor = document.createElement('div');
themeColor.className = `theme-color ${index === AppState.currentThemeIndex ? 'active' : ''}`;
themeColor.style.background = `linear-gradient(135deg, ${theme.primary}, ${theme.secondary})`;
themeColor.title = theme.name;
themeColor.dataset.index = index;
themeColor.textContent = theme.name;
themeColor.addEventListener('click', () => {
AppState.currentThemeIndex = parseInt(themeColor.dataset.index);
this.applyTheme(AppState.currentThemeIndex);
document.querySelectorAll('.theme-color').forEach(el => el.classList.remove('active'));
themeColor.classList.add('active');
themeSelector.classList.remove('active');
});
themeSelector.appendChild(themeColor);
});
}
static applyTheme(themeIndex) {
const theme = COLOR_THEMES[themeIndex];
const root = document.documentElement;
root.style.setProperty('--primary-color', theme.primary);
root.style.setProperty('--secondary-color', theme.secondary);
root.style.setProperty('--accent-color', theme.accent);
document.querySelector('header').style.background =
`linear-gradient(135deg, ${theme.primary}, ${theme.secondary})`;
document.querySelectorAll('.nav-button, .calendar-nav-btn, .floating-btn, .toolbar-btn-primary').forEach(el => {
el.style.background = `linear-gradient(135deg, ${theme.primary}, ${theme.secondary})`;
});
document.querySelectorAll('.modal-header').forEach(el => {
el.style.background = `linear-gradient(90deg, ${theme.primary}, ${theme.secondary})`;
});
document.querySelectorAll('.progress-circle-fill').forEach(el => {
el.style.stroke = theme.primary;
});
localStorage.setItem('selectedTheme', themeIndex.toString());
}
}
// ============== 数据管理器 ==============
class DataManager {
static async exportData() {
try {
const allMemos = await DatabaseManager.getAllMemos();
const settings = await this.getAllSettings();
const exportData = {
memos: allMemos,
settings: settings,
exportDate: new Date().toISOString(),
version: '2.0',
appName: '智能网页工作日历备忘录'
};
const dataStr = JSON.stringify(exportData, null, 2);
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
const exportFileName = `calendar-memo-backup-${Utils.formatDate(new Date())}.json`;
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', exportFileName);
linkElement.click();
Utils.showToast('数据导出成功!');
} catch (error) {
console.error('导出数据失败:', error);
Utils.showToast('导出失败,请重试', 'error');
}
}
static async getAllSettings() {
try {
const transaction = AppState.db.transaction(['settings'], 'readonly');
const store = transaction.objectStore('settings');
const request = store.getAll();
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = (event) => reject(event.target.error);
});
} catch (error) {
console.error('获取设置失败:', error);
return [];
}
}
static importData() {
document.getElementById('importFileInput').click();
}
static async handleFileImport(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async (e) => {
try {
const importData = JSON.parse(e.target.result);
if (!importData.memos || !Array.isArray(importData.memos)) {
throw new Error('文件格式不正确');
}
Utils.showConfirmation(
'导入确认',
`即将导入 ${importData.memos.length} 条备忘录。是否继续?`,
async () => {
try {
await DatabaseManager.clearAllData();
for (const memo of importData.memos) {
await DatabaseManager.saveMemo(memo);
}
if (importData.settings && Array.isArray(importData.settings)) {
for (const setting of importData.settings) {
await DatabaseManager.saveSetting(setting.key, setting.value);
}
}
Utils.showToast('数据导入成功!');
CalendarManager.renderMultiMonthCalendar();
MemoManager.updateRecentTasks();
ReminderManager.loadReminderSettings();
ReminderManager.startReminderChecker();
} catch (error) {
console.error('导入数据失败:', error);
Utils.showToast('导入失败,请检查文件格式', 'error');
}
}
);
} catch (error) {
console.error('解析文件失败:', error);
Utils.showToast('文件解析失败,请确保选择的是有效的备份文件', 'error');
}
event.target.value = '';
};
reader.readAsText(file);
}
static async clearAllData() {
Utils.showConfirmation(
'清空数据确认',
'确定要清空所有数据吗?此操作不可撤销!',
async () => {
try {
await DatabaseManager.clearAllData();
Utils.showToast('所有数据已清空!');
CalendarManager.renderMultiMonthCalendar();
MemoManager.updateRecentTasks();
ReminderManager.updateReminderBadge();
} catch (error) {
console.error('清空数据失败:', error);
Utils.showToast('清空数据失败,请重试', 'error');
}
}
);
}
}
// ============== 任务发布管理器 ==============
class TaskPublisher {
static init() {
this.initTaskColorPicker();
this.setDefaultDates();
}
static initTaskColorPicker() {
const colorOptionsEl = document.getElementById('taskColorOptions');
if (!colorOptionsEl) return;
colorOptionsEl.innerHTML = '';
MEMO_COLORS.forEach(color => {
const colorOption = document.createElement('div');
colorOption.className = 'color-option';
colorOption.style.backgroundColor = color;
colorOption.dataset.color = color;
colorOption.addEventListener('click', function() {
document.querySelectorAll('#taskColorOptions .color-option').forEach(el => {
el.classList.remove('selected');
});
this.classList.add('selected');
});
colorOptionsEl.appendChild(colorOption);
});
colorOptionsEl.firstChild.classList.add('selected');
}
static setDefaultDates() {
const today = new Date();
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 7);
document.getElementById('taskStartDate').value = Utils.formatDate(today);
document.getElementById('taskEndDate').value = Utils.formatDate(tomorrow);
}
static async publishTask() {
const title = document.getElementById('taskTitle').value.trim();
const description = document.getElementById('taskDescription').value.trim();
const startDate = document.getElementById('taskStartDate').value;
const endDate = document.getElementById('taskEndDate').value;
const dueTime = document.getElementById('taskDueTime').value;
if (!title) {
Utils.showToast('请输入任务标题', 'error');
return;
}
if (!startDate || !endDate || !Utils.isValidDate(startDate) || !Utils.isValidDate(endDate)) {
Utils.showToast('请选择有效的开始和结束日期', 'error');
return;
}
const start = new Date(startDate);
const end = new Date(endDate);
if (start > end) {
Utils.showToast('开始日期不能晚于结束日期', 'error');
return;
}
const days = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1;
if (days > 100) {
Utils.showConfirmation(
'确认发布',
`此任务将分配到 ${days} 天,数量较多,是否继续?`,
() => this.createTask(title, description, start, end, dueTime, days)
);
} else {
this.createTask(title, description, start, end, dueTime, days);
}
}
static async createTask(title, description, start, end, dueTime, totalDays) {
const selectedColor = document.querySelector('#taskColorOptions .color-option.selected').dataset.color;
let createdCount = 0;
const errors = [];
for (let currentDate = new Date(start); currentDate <= end; currentDate.setDate(currentDate.getDate() + 1)) {
try {
const taskDate = Utils.formatDate(currentDate);
const dueDateTime = new Date(taskDate + 'T' + dueTime);
const memo = {
title: `${title} (第${createdCount + 1}天/${totalDays}天)`,
date: taskDate,
dueTime: dueDateTime.toISOString(),
content: description,
color: selectedColor,
completed: false,
reminderShown: false
};
await DatabaseManager.saveMemo(memo);
createdCount++;
} catch (error) {
errors.push(`第${createdCount + 1}天: ${error.message}`);
}
}
let message = `任务发布完成!成功创建了 ${createdCount} 个每日任务。`;
if (errors.length > 0) {
message += ` ${errors.length} 个任务创建失败。`;
console.error('任务创建错误:', errors);
}
Utils.showToast(message);
document.getElementById('taskTitle').value = '';
document.getElementById('taskDescription').value = '';
this.setDefaultDates();
CalendarManager.renderMultiMonthCalendar();
MemoManager.updateRecentTasks();
ReminderManager.updateReminderBadge();
}
}
// ============== 🔹 扩展 App 类以支持同步 ==============
class App {
static async init() {
try {
await DatabaseManager.init();
ThemeManager.init();
this.initMonthCountSelector();
CalendarManager.renderMultiMonthCalendar();
this.initEventListeners();
ReminderManager.init();
AppState.isInitialized = true;
console.log('应用初始化完成');
} catch (error) {
console.error('应用初始化失败:', error);
Utils.showToast('应用初始化失败,请刷新页面重试', 'error');
}
}
static initMonthCountSelector() {
const monthCountSelect = document.getElementById('monthCountSelect');
const savedMonthCount = localStorage.getItem('calendarMonthCount');
if (savedMonthCount) {
AppState.monthsToShow = parseInt(savedMonthCount);
monthCountSelect.value = savedMonthCount;
}
monthCountSelect.addEventListener('change', function() {
AppState.monthsToShow = parseInt(this.value);
localStorage.setItem('calendarMonthCount', AppState.monthsToShow);
CalendarManager.renderMultiMonthCalendar();
});
}
static initEventListeners() {
document.getElementById('prevMonth').addEventListener('click', () => {
AppState.currentDate.setMonth(AppState.currentDate.getMonth() - 1);
CalendarManager.renderMultiMonthCalendar();
});
document.getElementById('nextMonth').addEventListener('click', () => {
AppState.currentDate.setMonth(AppState.currentDate.getMonth() + 1);
CalendarManager.renderMultiMonthCalendar();
});
document.getElementById('calendarPrevMonth').addEventListener('click', () => {
AppState.currentDate.setMonth(AppState.currentDate.getMonth() - 1);
CalendarManager.renderMultiMonthCalendar();
});
document.getElementById('calendarNextMonth').addEventListener('click', () => {
AppState.currentDate.setMonth(AppState.currentDate.getMonth() + 1);
CalendarManager.renderMultiMonthCalendar();
});
document.addEventListener('click', (e) => {
const calendarDay = e.target.closest('.calendar-day:not(.other-month)');
if (calendarDay && calendarDay.dataset.date) {
const [year, month, day] = calendarDay.dataset.date.split('-').map(Number);
const date = new Date(year, month - 1, day);
DailyDetailManager.openDailyDetailModal(date);
}
});
document.getElementById('saveMemo').addEventListener('click', () => MemoManager.saveMemo());
document.getElementById('deleteMemo').addEventListener('click', () => MemoManager.deleteMemo());
document.getElementById('cancelMemo').addEventListener('click', () => MemoManager.closeMemoModal());
document.getElementById('closeMemoModal').addEventListener('click', () => MemoManager.closeMemoModal());
document.getElementById('closeDailyDetailModal').addEventListener('click', () => DailyDetailManager.closeDailyDetailModal());
document.getElementById('closeDailyDetailModalBtn').addEventListener('click', () => DailyDetailManager.closeDailyDetailModal());
document.getElementById('addNewMemoBtn').addEventListener('click', () => {
DailyDetailManager.closeDailyDetailModal();
MemoManager.openMemoModal(null, AppState.dailyDetailDate);
});
document.getElementById('quickAddMemo').addEventListener('click', () => DailyDetailManager.quickAddMemo());
document.getElementById('quickMemoTitle').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
DailyDetailManager.quickAddMemo();
}
});
document.getElementById('themeSelectorBtn').addEventListener('click', (e) => {
e.stopPropagation();
document.getElementById('themeSelector').classList.toggle('active');
});
document.addEventListener('click', (e) => {
const themeSelector = document.getElementById('themeSelector');
const themeSelectorBtn = document.getElementById('themeSelectorBtn');
if (!themeSelector.contains(e.target) && !themeSelectorBtn.contains(e.target)) {
themeSelector.classList.remove('active');
}
});
document.getElementById('closeReminderModal').addEventListener('click', () => ReminderManager.closeReminderModal());
document.getElementById('markAllAsRead').addEventListener('click', () => ReminderManager.markAllRemindersAsRead());
document.getElementById('viewRecentTasks').addEventListener('click', () => {
ReminderManager.closeReminderModal();
this.openFunctionsModal('recentTasks');
});
document.getElementById('floatingReminder').addEventListener('click', () => ReminderManager.showReminderModal());
document.getElementById('floatingFunctions').addEventListener('click', () => this.openFunctionsModal('taskPublish'));
document.getElementById('toolbarPublish').addEventListener('click', () => this.openFunctionsModal('taskPublish'));
document.getElementById('toolbarExport').addEventListener('click', () => DataManager.exportData());
document.getElementById('toolbarImport').addEventListener('click', () => DataManager.importData());
document.getElementById('searchInput').addEventListener('input', Utils.debounce(() => {
CalendarManager.renderMultiMonthCalendar();
}, 300));
document.getElementById('clearSearch').addEventListener('click', () => {
document.getElementById('searchInput').value = '';
CalendarManager.renderMultiMonthCalendar();
});
document.getElementById('closeFunctionsModal').addEventListener('click', () => this.closeFunctionsModal());
document.getElementById('closeFunctionsModalBtn').addEventListener('click', () => this.closeFunctionsModal());
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
const tabName = tab.dataset.tab;
this.setActiveTab(tabName);
if (tabName === 'taskPublish') {
TaskPublisher.init();
} else if (tabName === 'recentTasks') {
MemoManager.updateRecentTasks();
}
});
});
document.getElementById('exportData').addEventListener('click', () => DataManager.exportData());
document.getElementById('importData').addEventListener('click', () => DataManager.importData());
document.getElementById('clearData').addEventListener('click', () => DataManager.clearAllData());
document.getElementById('importFileInput').addEventListener('change', (e) => DataManager.handleFileImport(e));
document.getElementById('publishTask').addEventListener('click', () => TaskPublisher.publishTask());
document.getElementById('saveReminderSettings').addEventListener('click', () => ReminderManager.saveReminderSettings());
document.getElementById('testReminder').addEventListener('click', () => ReminderManager.testReminder());
document.querySelectorAll('.modal').forEach(modal => {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.classList.remove('active');
}
});
});
document.getElementById('cancelConfirm').addEventListener('click', () => {
document.getElementById('confirmationModal').style.display = 'none';
});
// 🔹 新增:同步功能事件绑定
document.getElementById('loginBtn')?.addEventListener('click', async () => {
const email = document.getElementById('syncEmail').value.trim();
const pass = document.getElementById('syncPassword').value;
if (!email || !pass) {
Utils.showToast('请输入邮箱和密码', 'error');
return;
}
await CloudSyncManager.login(email, pass);
App.updateAuthUI();
});
document.getElementById('registerBtn')?.addEventListener('click', async () => {
const email = document.getElementById('syncEmail').value.trim();
const pass = document.getElementById('syncPassword').value;
if (!email || !pass || pass.length < 6) {
Utils.showToast('密码至少6位', 'error');
return;
}
await CloudSyncManager.register(email, pass);
});
document.getElementById('syncToCloud')?.addEventListener('click', () => CloudSyncManager.syncToCloud());
document.getElementById('syncFromCloud')?.addEventListener('click', () => CloudSyncManager.syncFromCloud());
auth.onAuthStateChanged(user => {
App.updateAuthUI();
});
}
static openFunctionsModal(tab = 'taskPublish') {
const modal = document.getElementById('functionsModal');
this.setActiveTab(tab);
if (tab === 'taskPublish') {
TaskPublisher.init();
} else if (tab === 'recentTasks') {
MemoManager.updateRecentTasks();
}
modal.classList.add('active');
}
static closeFunctionsModal() {
document.getElementById('functionsModal').classList.remove('active');
}
static setActiveTab(tabName) {
AppState.activeTab = tabName;
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.tab === tabName);
});
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.toggle('active', content.id === `${tabName}Tab`);
});
}
// 🔹 新增:更新同步 UI
static updateAuthUI() {
const user = CloudSyncManager.getCurrentUser();
const statusEl = document.getElementById('authStatus');
const btnUpload = document.getElementById('syncToCloud');
const btnDownload = document.getElementById('syncFromCloud');
const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) logoutBtn.remove();
if (user) {
const logoutHtml = `<button class="btn btn-secondary" id="logoutBtn" style="margin-left:10px; font-size:0.85rem;">退出</button>`;
statusEl.innerHTML = `已登录: ${user.email}` + logoutHtml;
btnUpload.disabled = false;
btnDownload.disabled = false;
document.getElementById('logoutBtn')?.addEventListener('click', () => {
auth.signOut();
});
} else {
statusEl.textContent = '未登录';
btnUpload.disabled = true;
btnDownload.disabled = true;
}
}
}
// ============== 启动应用 ==============
document.addEventListener('DOMContentLoaded', async () => {
await App.init();
// 初始调用 UI 更新
App.updateAuthUI();
});
</script>
</body>
</html>