前言
本工具只需要将下列todo_list.html
跟manifest.json
保存在本地并放在同一目录下,然后浏览器打开todo_list.html即可
todo_list.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>
<meta name="description" content="零依赖的每日工作计划管理工具,支持拖拽排序、Excel导入导出、离线使用">
<!-- PWA配置 -->
<link rel="manifest" href="./manifest.json">
<meta name="theme-color" content="#0078f5">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary-color: #0078f5;
--secondary-color: #f8f9fa;
--border-color: #e1e5e9;
--text-color: #333;
--text-light: #666;
--success-color: #28a745;
--warning-color: #ffc107;
--danger-color: #dc3545;
--priority-high: #dc3545;
--priority-medium: #ffc107;
--priority-low: #28a745;
--shadow: 0 2px 4px rgba(0,0,0,0.1);
--border-radius: 8px;
}
[data-theme="dark"] {
--primary-color: #4a9eff;
--secondary-color: #1a1a1a;
--border-color: #444;
--text-color: #e1e5e9;
--text-light: #aaa;
--secondary-color-light: #2d2d2d;
--shadow: 0 2px 4px rgba(255,255,255,0.1);
--success-color: #4caf50;
--warning-color: #ff9800;
--danger-color: #f44336;
--priority-high: #f44336;
--priority-medium: #ff9800;
--priority-low: #4caf50;
}
/* 深色主题专用样式 */
[data-theme="dark"] .header {
background: var(--secondary-color-light);
color: var(--text-color);
}
[data-theme="dark"] .task-cell {
background: #2a2a2a;
}
[data-theme="dark"] .task-cell:hover {
background: linear-gradient(135deg, rgba(74, 158, 255, 0.15), rgba(74, 158, 255, 0.08));
}
[data-theme="dark"] .task-card {
background: linear-gradient(135deg, #333, #2a2a2a);
color: var(--text-color);
border-color: var(--border-color);
}
[data-theme="dark"] .task-card:hover {
background: linear-gradient(135deg, #3a3a3a, #333);
}
[data-theme="dark"] .modal-content {
background: var(--secondary-color-light);
color: var(--text-color);
}
[data-theme="dark"] .form-control {
background: #333;
color: var(--text-color);
border-color: var(--border-color);
}
[data-theme="dark"] .form-control:focus {
border-color: var(--primary-color);
}
[data-theme="dark"] .properties-panel {
background: var(--secondary-color-light);
color: var(--text-color);
}
[data-theme="dark"] .day-header {
background: linear-gradient(135deg, #333, #2a2a2a);
color: var(--text-color);
}
[data-theme="dark"] .day-header:hover {
background: linear-gradient(135deg, #3a3a3a, #333);
}
[data-theme="dark"] .time-label {
background: linear-gradient(135deg, #333, #2a2a2a);
color: var(--text-color);
}
[data-theme="dark"] .time-label:hover {
background: linear-gradient(135deg, #3a3a3a, #333);
}
[data-theme="dark"] .drop-zone {
border-color: var(--border-color);
color: var(--text-light);
}
[data-theme="dark"] .drop-zone.dragover {
border-color: var(--primary-color);
background: rgba(74, 158, 255, 0.1);
}
[data-theme="green"] {
--primary-color: #28a745;
--secondary-color: #f0f8f0;
--border-color: #c3e6c3;
--text-color: #2d5a2d;
--text-light: #5a7a5a;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background-color: var(--secondary-color);
color: var(--text-color);
line-height: 1.6;
overflow-x: hidden;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
display: grid;
grid-template-columns: 1fr;
gap: 20px;
height: 100vh;
}
.header {
grid-column: 1 / -1;
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
background: white;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
margin-bottom: 20px;
}
.header-info {
display: flex;
align-items: center;
gap: 20px;
}
.date-display {
font-size: 1.2em;
font-weight: bold;
color: var(--primary-color);
}
.week-info {
color: var(--text-light);
}
.controls {
display: flex;
gap: 10px;
align-items: center;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: var(--border-radius);
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 5px;
}
.btn-primary {
background: var(--primary-color);
color: white;
}
.btn-secondary {
background: var(--secondary-color);
color: var(--text-color);
border: 1px solid var(--border-color);
}
.btn:hover {
transform: translateY(-1px);
box-shadow: var(--shadow);
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
@keyframes fadeIn {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.main-content {
background: white;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
overflow: hidden;
}
.weekly-grid {
display: grid;
grid-template-columns: 120px repeat(7, 1fr);
height: calc(100vh - 200px);
min-height: 600px;
border: 2px solid var(--border-color);
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: var(--shadow);
}
.time-slot-header {
background: linear-gradient(135deg, var(--primary-color), #0056b3);
color: white;
padding: 15px 8px;
text-align: center;
font-weight: 600;
border-bottom: 2px solid var(--border-color);
border-right: 2px solid var(--border-color);
font-size: 14px;
letter-spacing: 0.5px;
text-shadow: 0 1px 2px rgba(0,0,0,0.1);
display: flex;
align-items: center;
justify-content: center;
}
.day-header {
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
padding: 15px 8px;
text-align: center;
font-weight: 600;
border-bottom: 2px solid var(--border-color);
border-right: 2px solid var(--border-color);
font-size: 14px;
color: var(--text-color);
letter-spacing: 0.5px;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.day-header:hover {
background: linear-gradient(135deg, #e9ecef, #dee2e6);
transform: translateY(-1px);
}
.day-header.today {
background: linear-gradient(135deg, var(--primary-color), #0056b3);
color: white;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
text-shadow: 0 1px 2px rgba(0,0,0,0.2);
}
.time-label {
background: linear-gradient(135deg, #f1f3f4, #e8eaed);
padding: 20px 15px;
text-align: center;
font-weight: 600;
border-right: 2px solid var(--border-color);
border-bottom: 2px solid var(--border-color);
writing-mode: vertical-lr;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: var(--text-color);
letter-spacing: 1px;
min-height: 140px;
transition: all 0.3s ease;
}
.time-label:hover {
background: linear-gradient(135deg, #e8eaed, #dadce0);
transform: translateX(-2px);
}
.task-cell {
border-right: 2px solid var(--border-color);
border-bottom: 2px solid var(--border-color);
padding: 12px;
min-height: 140px;
position: relative;
overflow-y: auto;
background: #fafbfc;
transition: all 0.3s ease;
}
.task-cell:hover {
background: linear-gradient(135deg, rgba(0, 120, 245, 0.08), rgba(0, 120, 245, 0.04));
box-shadow: inset 0 0 10px rgba(0, 120, 245, 0.1);
transform: scale(1.02);
}
.task-card {
background: linear-gradient(135deg, #ffffff, #f8f9fa);
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 12px;
margin-bottom: 8px;
cursor: move;
transition: all 0.3s ease;
position: relative;
font-size: 13px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.task-card:hover {
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
transform: translateY(-2px) scale(1.02);
background: linear-gradient(135deg, #ffffff, #f0f2f5);
}
.task-card.dragging {
opacity: 0.5;
transform: rotate(5deg);
}
.task-card.done {
opacity: 0.6;
text-decoration: line-through;
}
.task-card.high-priority {
border-left: 3px solid var(--priority-high);
}
.task-card.medium-priority {
border-left: 3px solid var(--priority-medium);
}
.task-card.low-priority {
border-left: 3px solid var(--priority-low);
}
.task-checkbox {
margin-right: 5px;
cursor: pointer;
}
.task-title {
font-weight: bold;
margin-bottom: 2px;
word-break: break-word;
}
.task-time {
font-size: 11px;
color: var(--text-light);
margin-bottom: 2px;
}
.task-note {
font-size: 11px;
color: var(--text-light);
word-break: break-word;
}
.task-actions {
position: absolute;
top: 2px;
right: 2px;
opacity: 0;
transition: opacity 0.3s;
}
.task-card:hover .task-actions {
opacity: 1;
}
.delete-btn {
background: linear-gradient(135deg, var(--danger-color), #c82333);
color: white;
border: none;
border-radius: 50%;
width: 22px;
height: 22px;
font-size: 12px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 6px rgba(220, 53, 69, 0.3);
transition: all 0.3s ease;
}
.delete-btn:hover {
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.4);
}
.add-task-btn {
position: absolute;
bottom: 8px;
right: 8px;
background: linear-gradient(135deg, var(--primary-color), #0056b3);
color: white;
border: none;
border-radius: 50%;
width: 28px;
height: 28px;
font-size: 16px;
cursor: pointer;
opacity: 0.7;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 120, 245, 0.3);
}
.task-cell:hover .add-task-btn {
opacity: 1;
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(0, 120, 245, 0.4);
}
.properties-panel {
background: white;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
padding: 20px;
height: fit-content;
position: sticky;
top: 20px;
}
.properties-panel h3 {
margin-bottom: 15px;
color: var(--primary-color);
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
font-size: 14px;
}
.form-control {
width: 100%;
padding: 8px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
font-size: 14px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.5;
}
/* 统一时间输入框和备注文本框的字体样式 */
input[type="time"].form-control,
textarea.form-control {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--text-color);
}
/* 确保备注文本框的字体一致性 */
#taskNote {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
font-size: 14px;
line-height: 1.5;
resize: vertical;
}
.form-control:focus {
outline: none;
border-color: var(--primary-color);
}
.priority-selector {
display: flex;
gap: 5px;
}
.priority-btn {
flex: 1;
padding: 5px;
border: 1px solid var(--border-color);
background: white;
cursor: pointer;
font-size: 12px;
border-radius: var(--border-radius);
}
.priority-btn.active {
background: var(--primary-color);
color: white;
}
.repeat-selector {
display: flex;
gap: 5px;
}
.repeat-btn {
flex: 1;
padding: 5px;
border: 1px solid var(--border-color);
background: white;
cursor: pointer;
font-size: 12px;
border-radius: var(--border-radius);
}
.repeat-btn.active {
background: var(--primary-color);
color: white;
}
.weekday-selector {
display: flex;
gap: 5px;
flex-wrap: wrap;
}
.weekday-btn {
flex: 1;
min-width: 50px;
padding: 5px;
border: 1px solid var(--border-color);
background: white;
cursor: pointer;
font-size: 12px;
border-radius: var(--border-radius);
}
.weekday-btn.active {
background: var(--primary-color);
color: white;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1000;
}
.modal-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 30px;
border-radius: var(--border-radius);
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: var(--text-light);
}
.theme-selector {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.theme-btn {
width: 30px;
height: 30px;
border-radius: 50%;
border: 2px solid var(--border-color);
cursor: pointer;
}
.theme-btn.active {
border-color: var(--primary-color);
}
.drop-zone {
border: 2px dashed var(--border-color);
border-radius: var(--border-radius);
padding: 20px;
text-align: center;
color: var(--text-light);
margin-bottom: 15px;
}
.drop-zone.dragover {
border-color: var(--primary-color);
background: rgba(0, 120, 245, 0.1);
}
@media (max-width: 768px) {
.container {
grid-template-columns: 1fr;
padding: 10px;
}
.weekly-grid {
grid-template-columns: 80px repeat(7, 1fr);
font-size: 12px;
min-height: 500px;
}
.time-label {
padding: 15px 8px;
font-size: 14px;
min-height: 120px;
}
.task-cell {
padding: 8px;
min-height: 120px;
}
.day-header, .time-slot-header {
padding: 12px 6px;
font-size: 12px;
}
.task-card {
padding: 8px;
font-size: 12px;
margin-bottom: 6px;
}
.add-task-btn {
width: 24px;
height: 24px;
font-size: 14px;
bottom: 6px;
right: 6px;
}
.header {
flex-direction: column;
gap: 10px;
align-items: stretch;
}
.controls {
justify-content: center;
flex-wrap: wrap;
}
}
.toast {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 4px;
color: white;
font-weight: 500;
z-index: 10000;
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease;
}
.toast.show {
opacity: 1;
transform: translateX(0);
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.hidden {
display: none !important;
}
</style>
</head>
<body>
<div class="container">
<header class="header">
<div class="header-info">
<div class="date-display" id="currentDate"></div>
<div class="week-info" id="weekInfo"></div>
</div>
<div class="controls">
<button class="btn btn-primary" onclick="addTaskToPool()">+ 添加任务</button>
<button class="btn btn-secondary" onclick="clearWeek()">清空本周</button>
<button class="btn btn-primary" onclick="exportToExcel()">📊 CSV导出</button>
<button class="btn btn-secondary" onclick="showImportModal()">📥 CSV导入</button>
<button class="btn btn-outline" onclick="showSettingsModal()">⚙️ 设置</button>
</div>
</header>
<main class="main-content">
<div class="weekly-grid" id="weeklyGrid">
<div class="time-slot-header">时间</div>
<div class="day-header" data-day="1">周一</div>
<div class="day-header" data-day="2">周二</div>
<div class="day-header" data-day="3">周三</div>
<div class="day-header" data-day="4">周四</div>
<div class="day-header" data-day="5">周五</div>
<div class="day-header" data-day="6">周六</div>
<div class="day-header" data-day="7">周日</div>
<div class="time-label">上午<br>08:00-12:00</div>
<div class="task-cell" data-day="1" data-slot="AM" onclick="addTask(1, 'AM')" ondrop="drop(event, 'grid')" ondragover="allowDrop(event)"></div>
<div class="task-cell" data-day="2" data-slot="AM" onclick="addTask(2, 'AM')" ondrop="drop(event, 'grid')" ondragover="allowDrop(event)"></div>
<div class="task-cell" data-day="3" data-slot="AM" onclick="addTask(3, 'AM')" ondrop="drop(event, 'grid')" ondragover="allowDrop(event)"></div>
<div class="task-cell" data-day="4" data-slot="AM" onclick="addTask(4, 'AM')" ondrop="drop(event, 'grid')" ondragover="allowDrop(event)"></div>
<div class="task-cell" data-day="5" data-slot="AM" onclick="addTask(5, 'AM')" ondrop="drop(event, 'grid')" ondragover="allowDrop(event)"></div>
<div class="task-cell" data-day="6" data-slot="AM" onclick="addTask(6, 'AM')" ondrop="drop(event, 'grid')" ondragover="allowDrop(event)"></div>
<div class="task-cell" data-day="7" data-slot="AM" onclick="addTask(7, 'AM')" ondrop="drop(event, 'grid')" ondragover="allowDrop(event)"></div>
<div class="time-label">下午<br>13:00-18:00</div>
<div class="task-cell" data-day="1" data-slot="PM" onclick="addTask(1, 'PM')" ondrop="drop(event, 'grid')" ondragover="allowDrop(event)"></div>
<div class="task-cell" data-day="2" data-slot="PM" onclick="addTask(2, 'PM')" ondrop="drop(event, 'grid')" ondragover="allowDrop(event)"></div>
<div class="task-cell" data-day="3" data-slot="PM" onclick="addTask(3, 'PM')" ondrop="drop(event, 'grid')" ondragover="allowDrop(event)"></div>
<div class="task-cell" data-day="4" data-slot="PM" onclick="addTask(4, 'PM')" ondrop="drop(event, 'grid')" ondragover="allowDrop(event)"></div>
<div class="task-cell" data-day="5" data-slot="PM" onclick="addTask(5, 'PM')" ondrop="drop(event, 'grid')" ondragover="allowDrop(event)"></div>
<div class="task-cell" data-day="6" data-slot="PM" onclick="addTask(6, 'PM')" ondrop="drop(event, 'grid')" ondragover="allowDrop(event)"></div>
<div class="task-cell" data-day="7" data-slot="PM" onclick="addTask(7, 'PM')" ondrop="drop(event, 'grid')" ondragover="allowDrop(event)"></div>
<div class="time-label">晚上<br>19:00-23:00</div>
<div class="task-cell" data-day="1" data-slot="EVENING" onclick="addTask(1, 'EVENING')" ondrop="drop(event, 'grid')" ondragover="allowDrop(event)"></div>
<div class="task-cell" data-day="2" data-slot="EVENING" onclick="addTask(2, 'EVENING')" ondrop="drop(event, 'grid')" ondragover="allowDrop(event)"></div>
<div class="task-cell" data-day="3" data-slot="EVENING" onclick="addTask(3, 'EVENING')" ondrop="drop(event, 'grid')" ondragover="allowDrop(event)"></div>
<div class="task-cell" data-day="4" data-slot="EVENING" onclick="addTask(4, 'EVENING')" ondrop="drop(event, 'grid')" ondragover="allowDrop(event)"></div>
<div class="task-cell" data-day="5" data-slot="EVENING" onclick="addTask(5, 'EVENING')" ondrop="drop(event, 'grid')" ondragover="allowDrop(event)"></div>
<div class="task-cell" data-day="6" data-slot="EVENING" onclick="addTask(6, 'EVENING')" ondrop="drop(event, 'grid')" ondragover="allowDrop(event)"></div>
<div class="task-cell" data-day="7" data-slot="EVENING" onclick="addTask(7, 'EVENING')" ondrop="drop(event, 'grid')" ondragover="allowDrop(event)"></div>
</div>
</main>
<!-- 删除右侧面板 -->
<!-- <aside class="properties-panel">
<h3>任务详情</h3>
<div id="taskProperties">
<p style="color: var(--text-light); text-align: center;">选择任务查看详情</p>
</div>
</aside> -->
</div>
<!-- 任务编辑模态框 -->
<div class="modal" id="taskModal">
<div class="modal-content">
<div class="modal-header">
<h3 id="modalTitle">添加任务</h3>
<button class="close-btn" onclick="closeModal()">×</button>
</div>
<form id="taskForm" onsubmit="saveTask(event)">
<div class="form-group">
<label>任务标题</label>
<input type="text" class="form-control" id="taskTitle" required>
</div>
<div class="form-group">
<label>开始时间</label>
<input type="time" class="form-control" id="taskStart" required>
</div>
<div class="form-group">
<label>结束时间</label>
<input type="time" class="form-control" id="taskEnd" required>
</div>
<div class="form-group">
<label>优先级</label>
<div class="priority-selector">
<button type="button" class="priority-btn" data-priority="3" onclick="selectPriority(3)">低</button>
<button type="button" class="priority-btn" data-priority="2" onclick="selectPriority(2)">中</button>
<button type="button" class="priority-btn" data-priority="1" onclick="selectPriority(1)">高</button>
</div>
</div>
<div class="form-group">
<label>星期选择</label>
<div class="weekday-selector">
<button type="button" class="weekday-btn" data-day="1" onclick="toggleWeekday(1)">周一</button>
<button type="button" class="weekday-btn" data-day="2" onclick="toggleWeekday(2)">周二</button>
<button type="button" class="weekday-btn" data-day="3" onclick="toggleWeekday(3)">周三</button>
<button type="button" class="weekday-btn" data-day="4" onclick="toggleWeekday(4)">周四</button>
<button type="button" class="weekday-btn" data-day="5" onclick="toggleWeekday(5)">周五</button>
<button type="button" class="weekday-btn" data-day="6" onclick="toggleWeekday(6)">周六</button>
<button type="button" class="weekday-btn" data-day="7" onclick="toggleWeekday(7)">周日</button>
<button type="button" class="weekday-btn" data-day="all" onclick="toggleAllWeekdays()" style="background: #ff6b6b; color: white;">每天</button>
</div>
</div>
<div class="form-group">
<label>备注</label>
<textarea class="form-control" id="taskNote" rows="3"></textarea>
</div>
<div style="display: flex; gap: 10px; justify-content: flex-end;">
<button type="button" class="btn btn-secondary" onclick="closeModal()">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
<!-- 导入模态框 -->
<div class="modal" id="importModal">
<div class="modal-content" style="max-width: 600px; width: 90%;">
<div class="modal-header">
<h3 style="margin: 0; color: var(--primary-color);">
<span style="margin-right: 8px;">📥</span>CSV任务导入
</h3>
<button class="close-btn" onclick="closeImportModal()" style="font-size: 24px; color: var(--text-light);">×</button>
</div>
<div class="modal-body" style="padding: 30px;">
<!-- 文件上传区域 -->
<div class="import-upload-area" style="
border: 2px dashed var(--border-color);
border-radius: 12px;
padding: 40px 20px;
text-align: center;
transition: all 0.3s ease;
margin-bottom: 25px;
background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
" onmouseover="this.style.borderColor='var(--primary-color)'; this.style.background='linear-gradient(135deg, #f0f4ff 0%, #e8f0ff 100%)';"
onmouseout="this.style.borderColor='var(--border-color)'; this.style.background='linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%)';">
<div style="font-size: 48px; margin-bottom: 15px; color: var(--primary-color);">📁</div>
<h4 style="margin: 0 0 10px 0; color: var(--text-color);">拖拽文件到此处或点击选择</h4>
<p style="margin: 0 0 15px 0; color: var(--text-light); font-size: 14px;">
支持CSV格式文件,文件大小不超过5MB
</p>
<input type="file" id="fileInput" accept=".csv" onchange="handleFileSelect(event)" style="
display: none;
">
<div style="text-align: center;">
<button type="button" onclick="event.stopPropagation(); document.getElementById('fileInput').click()" class="btn btn-primary" style="
background: linear-gradient(135deg, var(--primary-color), #0056b3);
border: none;
padding: 14px 32px;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
color: white;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.2);
margin: 10px auto;
display: inline-block;
" onmouseover="this.style.transform='translateY(-1px)'; this.style.boxShadow='0 4px 12px rgba(0, 123, 255, 0.3)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 2px 8px rgba(0, 123, 255, 0.2)'">
📁 选择CSV文件
</button>
</div>
</div>
<!-- 文件信息 -->
<div id="fileInfo" style="display: none; margin-bottom: 20px; padding: 15px; background: #f8f9ff; border-radius: 8px;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div>
<strong id="fileName" style="color: var(--text-color);"></strong>
<span id="fileSize" style="color: var(--text-light); margin-left: 10px; font-size: 12px;"></span>
</div>
<button onclick="clearFile()" style="background: none; border: none; color: var(--text-light); cursor: pointer; font-size: 18px;">×</button>
</div>
</div>
<!-- 预览区域 -->
<div id="importPreview" style="display: none;">
<div style="display: flex; align-items: center; margin-bottom: 15px; padding: 15px; background: linear-gradient(135deg, #e8f5e8, #f0f8f0); border-radius: 8px;">
<div style="font-size: 24px; margin-right: 12px;">✅</div>
<div>
<h4 style="margin: 0 0 5px 0; color: var(--text-color);">
找到 <span id="taskCount" style="color: var(--primary-color); font-weight: bold;">0</span> 个有效任务
</h4>
<p style="margin: 0; color: var(--text-light); font-size: 14px;">请确认导入内容是否正确</p>
</div>
</div>
<div style="max-height: 300px; overflow-y: auto; border: 1px solid var(--border-color); border-radius: 8px; background: white;">
<div id="previewList" style="padding: 0;"></div>
</div>
</div>
<!-- 导入说明 -->
<div style="margin-top: 20px; padding: 15px; background: #fff8e1; border-left: 4px solid #ffa726; border-radius: 4px;">
<h5 style="margin: 0 0 10px 0; color: #e65100;">📋 CSV文件格式说明</h5>
<p style="margin: 0 0 8px 0; font-size: 14px; color: #666;">
请确保CSV文件包含以下列:<strong>星期,时段,标题,开始时间,结束时间,优先级,备注</strong>
</p>
<p style="margin: 0 0 8px 0; font-size: 13px; color: #888;">
例如:周一,上午,团队会议,09:00,10:00,高,讨论项目进度
</p>
<div style="margin-top: 10px;">
<a href="weekly_template.csv" download="每周任务模板.csv" style="
display: inline-block;
padding: 8px 16px;
background: var(--primary-color);
color: white;
text-decoration: none;
border-radius: 6px;
font-size: 13px;
transition: all 0.3s ease;
" onmouseover="this.style.background='#0056b3'" onmouseout="this.style.background='var(--primary-color)'">
📥 下载CSV模板文件
</a>
</div>
</div>
</div>
<div class="modal-footer" style="padding: 20px 30px; border-top: 1px solid var(--border-color); display: flex; justify-content: flex-end; gap: 12px;">
<button class="btn btn-secondary" onclick="closeImportModal()" style="padding: 10px 20px; border-radius: 6px;">取消</button>
<button class="btn btn-primary" onclick="confirmImport()" id="confirmImportBtn" disabled style="padding: 10px 20px; border-radius: 6px; background: linear-gradient(135deg, var(--primary-color), #0056b3);">
确认导入
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 设置模态框 -->
<div class="modal" id="settingsModal">
<div class="modal-content">
<div class="modal-header">
<h3>设置</h3>
<button class="close-btn" onclick="closeSettingsModal()">×</button>
</div>
<div class="form-group">
<label>主题</label>
<div class="theme-selector">
<button type="button" class="theme-btn" style="background: white; border: 1px solid #ddd;" onclick="setTheme('light')" title="浅色"></button>
<button type="button" class="theme-btn" style="background: #1a1a1a;" onclick="setTheme('dark')" title="深色"></button>
<button type="button" class="theme-btn" style="background: #f0f8f0;" onclick="setTheme('green')" title="护眼绿"></button>
</div>
</div>
<div class="form-group">
<label>时段设置</label>
<div style="margin-bottom: 10px;">
<label style="font-size: 14px; margin-bottom: 5px; display: block;">上午时段</label>
<div style="display: flex; align-items: center; gap: 10px;">
<select class="form-control" id="morningStart" style="width: 100px;">
<option value="06:00">06:00</option>
<option value="06:30">06:30</option>
<option value="07:00">07:00</option>
<option value="07:30">07:30</option>
<option value="08:00" selected>08:00</option>
<option value="08:30">08:30</option>
<option value="09:00">09:00</option>
<option value="09:30">09:30</option>
<option value="10:00">10:00</option>
<option value="10:30">10:30</option>
</select>
<span>至</span>
<select class="form-control" id="morningEnd" style="width: 100px;">
<option value="10:00">10:00</option>
<option value="10:30">10:30</option>
<option value="11:00">11:00</option>
<option value="11:30">11:30</option>
<option value="12:00" selected>12:00</option>
<option value="12:30">12:30</option>
<option value="13:00">13:00</option>
<option value="13:30">13:30</option>
<option value="14:00">14:00</option>
<option value="14:30">14:30</option>
</select>
</div>
</div>
<div style="margin-bottom: 10px;">
<label style="font-size: 14px; margin-bottom: 5px; display: block;">下午时段</label>
<div style="display: flex; align-items: center; gap: 10px;">
<select class="form-control" id="afternoonStart" style="width: 100px;">
<option value="12:00">12:00</option>
<option value="12:30">12:30</option>
<option value="13:00" selected>13:00</option>
<option value="13:30">13:30</option>
<option value="14:00">14:00</option>
<option value="14:30">14:30</option>
<option value="15:00">15:00</option>
<option value="15:30">15:30</option>
</select>
<span>至</span>
<select class="form-control" id="afternoonEnd" style="width: 100px;">
<option value="16:00">16:00</option>
<option value="16:30">16:30</option>
<option value="17:00">17:00</option>
<option value="17:30">17:30</option>
<option value="18:00" selected>18:00</option>
<option value="18:30">18:30</option>
<option value="19:00">19:00</option>
<option value="19:30">19:30</option>
<option value="20:00">20:00</option>
<option value="20:30">20:30</option>
</select>
</div>
</div>
<div style="margin-bottom: 10px;">
<label style="font-size: 14px; margin-bottom: 5px; display: block;">晚上时段</label>
<div style="display: flex; align-items: center; gap: 10px;">
<select class="form-control" id="eveningStart" style="width: 100px;">
<option value="18:00">18:00</option>
<option value="18:30">18:30</option>
<option value="19:00" selected>19:00</option>
<option value="19:30">19:30</option>
<option value="20:00">20:00</option>
<option value="20:30">20:30</option>
<option value="21:00">21:00</option>
<option value="21:30">21:30</option>
</select>
<span>至</span>
<select class="form-control" id="eveningEnd" style="width: 100px;">
<option value="21:00">21:00</option>
<option value="21:30">21:30</option>
<option value="22:00">22:00</option>
<option value="22:30">22:30</option>
<option value="23:00" selected>23:00</option>
<option value="23:30">23:30</option>
<option value="24:00">24:00</option>
</select>
</div>
</div>
</div>
<div style="display: flex; gap: 10px; justify-content: flex-end;">
<button class="btn btn-primary" onclick="saveSettings()">保存设置</button>
</div>
</div>
</div>
<!-- 提示消息 -->
<div class="toast" id="toast"></div>
<script>
// 全局变量
let tasks = [];
let currentEditingTask = null;
let draggedTask = null;
let importedData = null;
// 工具函数
function generateId() {
return Math.random().toString(36).substr(2, 9);
}
function showToast(message, type = 'success') {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.style.background = type === 'error' ? 'var(--danger-color)' : 'var(--success-color)';
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 3000);
}
function saveToLocalStorage(taskData = null) {
const dataToSave = taskData || tasks;
localStorage.setItem('weeklyTasks', JSON.stringify(dataToSave));
if (taskData === null) {
showToast('已自动保存到本地');
}
}
function loadFromLocalStorage() {
const saved = localStorage.getItem('weeklyTasks');
if (saved) {
try {
const parsed = JSON.parse(saved);
tasks = Array.isArray(parsed) ? parsed : [];
return tasks;
} catch (error) {
console.error('解析本地存储数据失败:', error);
tasks = [];
return [];
}
}
tasks = [];
return [];
}
function updateDateDisplay() {
const now = new Date();
const options = { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' };
document.getElementById('currentDate').textContent = now.toLocaleDateString('zh-CN', options);
const startOfWeek = new Date(now);
startOfWeek.setDate(now.getDate() - now.getDay() + 1);
const endOfWeek = new Date(startOfWeek);
endOfWeek.setDate(startOfWeek.getDate() + 6);
const weekNumber = Math.ceil((now - new Date(now.getFullYear(), 0, 1)) / 604800000);
document.getElementById('weekInfo').textContent = `第${weekNumber}周 ${startOfWeek.getMonth()+1}/${startOfWeek.getDate()}-${endOfWeek.getMonth()+1}/${endOfWeek.getDate()}`;
// 高亮今天(排除周日)
const today = now.getDay() || 7;
document.querySelectorAll('.day-header').forEach(header => {
header.classList.remove('today');
if (parseInt(header.dataset.day) === today && today !== 7) {
header.classList.add('today');
}
});
}
function renderTasks() {
// 清空所有任务显示
document.querySelectorAll('.task-card').forEach(card => card.remove());
document.querySelectorAll('#taskPool .task-card').forEach(card => card.remove());
tasks.forEach(task => {
const taskElement = createTaskElement(task);
if (task.slot === 'POOL') {
document.getElementById('taskPool').appendChild(taskElement);
} else {
const cell = document.querySelector(`[data-day="${task.weekday}"][data-slot="${task.slot}"]`);
if (cell) {
cell.appendChild(taskElement);
}
}
});
}
function createTaskElement(task) {
const div = document.createElement('div');
div.className = `task-card ${task.done ? 'done' : ''} ${getPriorityClass(task.priority)}`;
div.draggable = true;
div.dataset.taskId = task.id;
div.innerHTML = `
<div style="display: flex; align-items: flex-start; gap: 5px;">
<input type="checkbox" class="task-checkbox" ${task.done ? 'checked' : ''}
onchange="toggleTask('${task.id}')">
<div style="flex: 1;">
<div class="task-title">${task.title}</div>
${task.start && task.end ? `<div class="task-time">${task.start}-${task.end}</div>` : ''}
${task.note ? `<div class="task-note">${task.note}</div>` : ''}
</div>
</div>
<div class="task-actions">
<button class="delete-btn" onclick="deleteTask('${task.id}')">×</button>
</div>
`;
div.addEventListener('dragstart', (e) => {
draggedTask = task;
e.dataTransfer.effectAllowed = 'move';
setTimeout(() => div.classList.add('dragging'), 0);
});
div.addEventListener('dragend', () => {
div.classList.remove('dragging');
draggedTask = null;
});
// 确保任务卡片可点击
div.style.cursor = 'pointer';
div.addEventListener('click', (e) => {
e.stopPropagation();
if (!e.target.classList.contains('task-checkbox') && !e.target.classList.contains('delete-btn')) {
console.log('点击任务,准备编辑:', task);
editTask(task);
}
});
return div;
}
function getPriorityClass(priority) {
switch (priority) {
case 1: return 'high-priority';
case 2: return 'medium-priority';
case 3: return 'low-priority';
default: return '';
}
}
function allowDrop(ev) {
ev.preventDefault();
}
function drop(ev, targetType) {
ev.preventDefault();
if (!draggedTask) return;
if (targetType === 'pool') {
// 拖到任务池,只更新位置,不清除原任务
draggedTask.slot = 'POOL';
draggedTask.weekday = null;
} else {
const cell = ev.target.closest('.task-cell');
if (cell) {
if (draggedTask.slot === 'POOL') {
// 从任务池拖到网格,创建副本
const newTask = {
...draggedTask,
id: generateId(),
weekday: parseInt(cell.dataset.day),
slot: cell.dataset.slot,
order: Date.now()
};
tasks.push(newTask);
} else {
// 网格间移动,直接更新位置
draggedTask.weekday = parseInt(cell.dataset.day);
draggedTask.slot = cell.dataset.slot;
}
}
}
renderTasks();
saveToLocalStorage();
}
function addTask(day, slot) {
currentEditingTask = null;
document.getElementById('modalTitle').textContent = '添加任务';
document.getElementById('taskForm').reset();
// 设置默认时间
if (slot) {
const morningRange = localStorage.getItem('morningRange') || '08:00-12:00';
const afternoonRange = localStorage.getItem('afternoonRange') || '13:00-18:00';
const eveningRange = localStorage.getItem('eveningRange') || '19:00-23:00';
let startTime, endTime;
if (slot === 'AM') {
const times = morningRange.split('-');
startTime = times[0];
endTime = times[1];
} else if (slot === 'PM') {
const times = afternoonRange.split('-');
startTime = times[0];
endTime = times[1];
} else if (slot === 'EVENING') {
const times = eveningRange.split('-');
startTime = times[0];
endTime = times[1];
}
if (startTime && endTime) {
document.getElementById('taskStart').value = startTime;
document.getElementById('taskEnd').value = endTime;
}
}
// 设置默认优先级
selectPriority(2);
// 设置默认星期几
if (day) {
// 清除所有选中状态
document.querySelectorAll('.weekday-btn[data-day="1"], .weekday-btn[data-day="2"], .weekday-btn[data-day="3"], .weekday-btn[data-day="4"], .weekday-btn[data-day="5"], .weekday-btn[data-day="6"], .weekday-btn[data-day="7"], .weekday-btn[data-day="all"]').forEach(btn => {
btn.classList.remove('active');
});
// 选中对应的星期
const weekdayBtn = document.querySelector(`.weekday-btn[data-day="${day}"]`);
if (weekdayBtn) {
weekdayBtn.classList.add('active');
}
}
document.getElementById('taskModal').style.display = 'block';
// 存储临时位置信息
window.tempTaskLocation = { day, slot };
}
function addTaskToPool() {
addTask(null, null);
}
function editTask(task) {
console.log('开始编辑任务:', task);
if (!task) {
console.error('任务对象为空');
return;
}
currentEditingTask = task;
document.getElementById('modalTitle').textContent = '编辑任务';
document.getElementById('taskTitle').value = task.title;
document.getElementById('taskStart').value = task.start || '';
document.getElementById('taskEnd').value = task.end || '';
document.getElementById('taskNote').value = task.note || '';
selectPriority(task.priority || 2);
// 重置星期选择
document.querySelectorAll('.weekday-btn[data-day="1"], .weekday-btn[data-day="2"], .weekday-btn[data-day="3"], .weekday-btn[data-day="4"], .weekday-btn[data-day="5"], .weekday-btn[data-day="6"], .weekday-btn[data-day="7"], .weekday-btn[data-day="all"]').forEach(btn => {
btn.classList.remove('active');
});
// 如果任务有星期信息,选中对应的星期
if (task.weekday) {
const weekdayBtn = document.querySelector(`.weekday-btn[data-day="${task.weekday}"]`);
if (weekdayBtn) {
weekdayBtn.classList.add('active');
}
}
console.log('打开模态框');
const modal = document.getElementById('taskModal');
modal.style.display = 'block';
modal.style.zIndex = '10000'; // 确保模态框在最上层
// 确保模态框可见
setTimeout(() => {
modal.style.opacity = '1';
}, 10);
}
function saveTask(event) {
event.preventDefault();
const title = document.getElementById('taskTitle').value.trim();
if (!title) {
showToast('请输入任务标题', 'error');
return;
}
const start = document.getElementById('taskStart').value;
const end = document.getElementById('taskEnd').value;
const note = document.getElementById('taskNote').value;
const priority = parseInt(document.querySelector('.priority-btn.active').dataset.priority);
if (currentEditingTask) {
// 编辑现有任务
currentEditingTask.title = title;
currentEditingTask.start = start;
currentEditingTask.end = end;
currentEditingTask.note = note;
currentEditingTask.priority = priority;
} else {
// 创建新任务 - 支持多星期选择
const location = window.tempTaskLocation;
const selectedWeekdays = getSelectedWeekdays();
if (selectedWeekdays.length > 0) {
// 选择了具体星期,为每个选中的星期创建任务
selectedWeekdays.forEach(weekday => {
const newTask = {
id: generateId(),
title,
start,
end,
note,
priority,
done: false,
doneAt: null,
weekday: weekday,
slot: location?.slot || 'AM',
order: Date.now()
};
tasks.push(newTask);
});
} else {
// 没有选择星期,根据位置信息创建任务
if (location && location.day && location.slot) {
// 有明确位置信息,创建到指定位置
const newTask = {
id: generateId(),
title,
start,
end,
note,
priority,
done: false,
doneAt: null,
weekday: location.day,
slot: location.slot,
order: Date.now()
};
tasks.push(newTask);
} else {
// 没有位置信息(比如从任务池添加),放到任务池
const newTask = {
id: generateId(),
title,
start,
end,
note,
priority,
done: false,
doneAt: null,
weekday: null,
slot: 'POOL',
order: Date.now()
};
tasks.push(newTask);
}
}
}
renderTasks();
saveToLocalStorage();
closeModal();
showToast('任务保存成功', 'success');
}
function toggleTask(taskId) {
const task = tasks.find(t => t.id === taskId);
if (task) {
task.done = !task.done;
task.doneAt = task.done ? new Date().toISOString() : null;
renderTasks();
saveToLocalStorage();
}
}
function deleteTask(taskId) {
if (confirm('确定要删除这个任务吗?')) {
tasks = tasks.filter(t => t.id !== taskId);
renderTasks();
saveToLocalStorage();
showToast('任务已删除');
}
}
function selectPriority(priority) {
document.querySelectorAll('.priority-btn').forEach(btn => {
btn.classList.remove('active');
});
document.querySelector(`[data-priority="${priority}"]`).classList.add('active');
}
function toggleWeekday(day) {
const btn = document.querySelector(`.weekday-btn[data-day="${day}"]`);
btn.classList.toggle('active');
}
function toggleAllWeekdays() {
const allBtn = document.querySelector('.weekday-btn[data-day="all"]');
const isAllSelected = allBtn.classList.contains('active');
if (isAllSelected) {
// 取消全选
allBtn.classList.remove('active');
document.querySelectorAll('.weekday-btn[data-day="1"], .weekday-btn[data-day="2"], .weekday-btn[data-day="3"], .weekday-btn[data-day="4"], .weekday-btn[data-day="5"], .weekday-btn[data-day="6"], .weekday-btn[data-day="7"]').forEach(btn => {
btn.classList.remove('active');
});
} else {
// 全选
allBtn.classList.add('active');
document.querySelectorAll('.weekday-btn[data-day="1"], .weekday-btn[data-day="2"], .weekday-btn[data-day="3"], .weekday-btn[data-day="4"], .weekday-btn[data-day="5"], .weekday-btn[data-day="6"], .weekday-btn[data-day="7"]').forEach(btn => {
btn.classList.add('active');
});
}
}
function getSelectedWeekdays() {
const selectedBtns = document.querySelectorAll('.weekday-btn[data-day="1"].active, .weekday-btn[data-day="2"].active, .weekday-btn[data-day="3"].active, .weekday-btn[data-day="4"].active, .weekday-btn[data-day="5"].active, .weekday-btn[data-day="6"].active, .weekday-btn[data-day="7"].active');
return Array.from(selectedBtns).map(btn => parseInt(btn.dataset.day));
}
function closeModal() {
document.getElementById('taskModal').style.display = 'none';
currentEditingTask = null;
window.tempTaskLocation = null;
// 重置星期选择
document.querySelectorAll('.weekday-btn').forEach(btn => {
btn.classList.remove('active');
});
}
// 删除loadSampleData函数
// 该函数已被移除,不再提供示例数据功能
// 添加测试任务用于验证编辑功能
function clearWeek() {
if (confirm('确定要清空本周所有任务吗?')) {
tasks = tasks.filter(task => task.slot === 'POOL');
renderTasks();
saveToLocalStorage();
showToast('已清空本周任务');
}
}
function loadSampleData() {
if (confirm('加载示例数据会覆盖当前所有任务,确定继续吗?')) {
tasks = [
{
id: generateId(),
title: '晨间会议',
start: '09:00',
end: '09:30',
note: '周例会,讨论本周工作计划',
priority: 1,
done: false,
weekday: 1,
slot: 'AM',
order: 1
},
{
id: generateId(),
title: '项目开发',
start: '14:00',
end: '17:00',
note: '完成新功能开发',
priority: 2,
done: false,
weekday: 2,
slot: 'PM',
order: 2
},
{
id: generateId(),
title: '健身运动',
start: '19:00',
end: '20:00',
note: '跑步30分钟',
priority: 3,
done: false,
weekday: 3,
slot: 'EVENING',
order: 3
}
];
renderTasks();
saveToLocalStorage();
showToast('已加载示例数据');
}
}
// Excel导入导出功能
function exportToExcel() {
const tasks = loadFromLocalStorage() || [];
// 扩展CSV格式定义,包含所有任务属性
const CSV_HEADERS = ['星期', '时段', '任务标题', '开始时间', '结束时间', '优先级', '是否完成', '备注', '重复类型', '选定星期', '任务ID'];
// 映射表 - 统一标准
const WEEKDAY_MAP = {
1: '周一', 2: '周二', 3: '周三', 4: '周四', 5: '周五', 6: '周六', 7: '周日',
null: '任务池'
};
const SLOT_MAP = {
'AM': '上午', 'PM': '下午', 'EVENING': '晚上', 'POOL': '任务池'
};
const PRIORITY_MAP = {
1: '高', 2: '中', 3: '低'
};
const REPEAT_TYPE_MAP = {
'once': '单次', 'daily': '每天', 'weekly': '每周'
};
// 构建CSV数据
const csvData = [CSV_HEADERS];
// 确保tasks是数组并过滤有效任务
const taskArray = Array.isArray(tasks) ? tasks : [];
const validTasks = taskArray.filter(task =>
task &&
typeof task === 'object' &&
task.title && task.title.trim() !== ''
);
// 转换任务数据
validTasks.forEach(task => {
// 处理选定星期
let selectedWeekdaysText = '';
if (task.selectedWeekdays && Array.isArray(task.selectedWeekdays) && task.selectedWeekdays.length > 0) {
selectedWeekdaysText = task.selectedWeekdays.map(day => WEEKDAY_MAP[day] || day).join(',');
}
csvData.push([
WEEKDAY_MAP[task.weekday] || '任务池',
SLOT_MAP[task.slot] || '任务池',
task.title || '',
task.start || '',
task.end || '',
PRIORITY_MAP[task.priority] || '中',
task.done ? '是' : '否',
task.note || '',
REPEAT_TYPE_MAP[task.repeatType] || '单次',
selectedWeekdaysText,
task.id || ''
]);
});
// 生成CSV内容
const csvContent = csvData.map(row =>
row.map(cell => {
const str = String(cell || '');
// 处理需要引号的字段
if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
return '"' + str.replace(/"/g, '""') + '"';
}
return str;
}).join(',')
).join('\r\n');
// 创建并下载文件
const blob = new Blob(['\ufeff' + csvContent], {
type: 'text/csv;charset=utf-8;'
});
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `weekly_tasks_${new Date().toISOString().split('T')[0]}.csv`;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showToast(`成功导出 ${validTasks.length} 个任务`);
}
function showImportModal() {
document.getElementById('importModal').style.display = 'block';
document.getElementById('importPreview').style.display = 'none';
}
function closeImportModal() {
document.getElementById('importModal').style.display = 'none';
importedData = null;
}
function handleFileSelect(event) {
const file = event.target.files[0];
if (file) {
// 显示文件信息
document.getElementById('fileInfo').style.display = 'block';
document.getElementById('fileName').textContent = file.name;
document.getElementById('fileSize').textContent = formatFileSize(file.size);
parseExcelFile(file);
}
}
function clearFile() {
document.getElementById('fileInput').value = '';
document.getElementById('fileInfo').style.display = 'none';
document.getElementById('importPreview').style.display = 'none';
importedData = null;
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// 统一CSV解析函数
function parseCSVLine(line) {
const cells = [];
let current = '';
let inQuotes = false;
let i = 0;
while (i < line.length) {
const char = line[i];
if (char === '"') {
if (inQuotes && line[i + 1] === '"') {
current += '"';
i += 2;
} else {
inQuotes = !inQuotes;
i++;
}
} else if (char === ',' && !inQuotes) {
cells.push(current);
current = '';
i++;
} else {
current += char;
i++;
}
}
cells.push(current);
return cells;
}
function parseExcelFile(file) {
const reader = new FileReader();
reader.onload = function(e) {
const text = e.target.result;
const lines = text.replace(/\r\n/g, '\n').split('\n');
const newTasks = [];
// 标准映射表 - 与导出保持一致
const WEEKDAY_MAP = {
'周一': 1, '周二': 2, '周三': 3, '周四': 4, '周五': 5, '周六': 6, '周日': 7,
'星期一': 1, '星期二': 2, '星期三': 3, '星期四': 4, '星期五': 5, '星期六': 6, '星期日': 7,
'1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7,
'monday': 1, 'tuesday': 2, 'wednesday': 3, 'thursday': 4, 'friday': 5, 'saturday': 6, 'sunday': 7,
'mon': 1, 'tue': 2, 'wed': 3, 'thu': 4, 'fri': 5, 'sat': 6, 'sun': 7,
'任务池': null
};
const SLOT_MAP = {
'上午': 'AM', '下午': 'PM', '晚上': 'EVENING', '任务池': 'POOL',
'AM': 'AM', 'PM': 'PM', 'EVENING': 'EVENING', 'POOL': 'POOL',
'morning': 'AM', 'afternoon': 'PM', 'evening': 'EVENING'
};
const PRIORITY_MAP = {
'高': 1, '中': 2, '低': 3,
'high': 1, 'medium': 2, 'low': 3,
'1': 1, '2': 2, '3': 3
};
const COMPLETION_MAP = {
'是': true, '否': false,
'true': true, 'false': false,
'1': true, '0': false,
'yes': true, 'no': false
};
const REPEAT_TYPE_MAP = {
'单次': 'once', '每天': 'daily', '每周': 'weekly',
'once': 'once', 'daily': 'daily', 'weekly': 'weekly'
};
// 跳过标题行,处理数据行
for (let i = 1; i < lines.length; i++) {
const line = lines[i].trim();
if (!line || line === '') continue;
// 跳过表头行
if (line.includes('星期') && line.includes('任务标题')) {
continue;
}
try {
// 解析CSV行
const cells = parseCSVLine(line);
if (!cells || cells.length < 7) {
console.warn(`跳过第 ${i + 1} 行: 列数不足`);
continue;
}
// 清理数据
const cleanCells = cells.map(cell => cell.trim().replace(/^"|"$/g, ''));
// 提取字段(支持新旧格式)
const weekdayText = cleanCells[0] || '周一';
const slotText = cleanCells[1] || '上午';
const title = cleanCells[2] || '';
const start = cleanCells[3] || '';
const end = cleanCells[4] || '';
const priorityText = cleanCells[5] || '中';
const completionText = cleanCells[6] || '否';
const note = cleanCells[7] || '';
const repeatTypeText = cleanCells[8] || '单次'; // 新字段
const selectedWeekdaysText = cleanCells[9] || ''; // 新字段
const taskId = cleanCells[10] || generateId(); // 新字段
// 跳过空标题
if (!title || title.trim() === '') {
continue;
}
// 解析选定星期
let selectedWeekdays = [];
if (selectedWeekdaysText && selectedWeekdaysText.trim()) {
const weekdayNames = selectedWeekdaysText.split(',').map(name => name.trim());
selectedWeekdays = weekdayNames.map(name => WEEKDAY_MAP[name]).filter(day => day !== undefined && day !== null);
}
// 创建任务对象
const task = {
id: taskId.trim() || generateId(),
title: title.trim(),
start: start.trim(),
end: end.trim(),
priority: PRIORITY_MAP[priorityText.toLowerCase()] || 2,
done: COMPLETION_MAP[completionText.toLowerCase()] || false,
note: note.trim(),
weekday: WEEKDAY_MAP[weekdayText] !== undefined ? WEEKDAY_MAP[weekdayText] : 1,
slot: SLOT_MAP[slotText] || 'AM',
order: Date.now() + i * 1000,
repeatType: REPEAT_TYPE_MAP[repeatTypeText] || 'once',
selectedWeekdays: selectedWeekdays,
doneAt: null
};
newTasks.push(task);
} catch (error) {
console.warn(`解析第 ${i + 1} 行失败:`, error.message);
}
}
if (newTasks.length > 0) {
importedData = newTasks;
document.getElementById('taskCount').textContent = newTasks.length;
// 显示预览
const previewList = document.getElementById('previewList');
if (previewList) {
const slots = { AM: '上午', PM: '下午', EVENING: '晚上', POOL: '任务池' };
const weekdays = ['', '周一', '周二', '周三', '周四', '周五', '周六', '周日'];
const priorityColors = { 1: '#ff5252', 2: '#ffa726', 3: '#66bb6a' };
const repeatTypes = { 'once': '单次', 'daily': '每天', 'weekly': '每周' };
previewList.innerHTML = newTasks.slice(0, 10).map(task =>
`<div style="padding: 15px; border-bottom: 1px solid var(--border-color); display: flex; align-items: center; gap: 12px; transition: background-color 0.2s;" onmouseover="this.style.backgroundColor='#f8f9ff'" onmouseout="this.style.backgroundColor=''">
<div style="width: 4px; height: 40px; border-radius: 2px; background: ${priorityColors[task.priority]}; flex-shrink: 0;"></div>
<div style="flex: 1;">
<div style="font-weight: 600; color: var(--text-color); margin-bottom: 4px;">${task.title}</div>
<div style="font-size: 13px; color: var(--text-light);">
${task.weekday ? weekdays[task.weekday] : '任务池'} •
${slots[task.slot] || '未知时段'} •
${task.start} - ${task.end} •
${repeatTypes[task.repeatType] || '单次'}
</div>
</div>
<div style="font-size: 12px; color: ${priorityColors[task.priority]}; font-weight: 500;">
${task.priority === 1 ? '高' : task.priority === 2 ? '中' : '低'}优先级
</div>
</div>`
).join('');
if (newTasks.length > 10) {
previewList.innerHTML += `
<div style="padding: 20px; text-align: center; color: var(--text-light); font-size: 14px;">
📊 还有 <strong style="color: var(--primary-color);">${newTasks.length - 10}</strong> 个任务未显示
</div>`;
}
}
document.getElementById('importPreview').style.display = 'block';
document.getElementById('confirmImportBtn').disabled = false;
} else {
showToast('未找到有效任务数据,请检查CSV格式', 'error');
}
};
reader.onerror = function() {
showToast('文件读取失败,请重试', 'error');
};
reader.readAsText(file, 'UTF-8');
}
function confirmImport() {
if (!importedData || !Array.isArray(importedData) || importedData.length === 0) {
showToast('没有可导入的数据', 'error');
return;
}
try {
// 获取现有任务
const currentTasks = loadFromLocalStorage() || [];
const currentTaskArray = Array.isArray(currentTasks) ? currentTasks : [];
// 处理导入的任务,确保数据完整性
const processedTasks = importedData.map(task => {
const processedTask = {
id: task.id || generateId(),
title: task.title || '',
start: task.start || '',
end: task.end || '',
priority: task.priority || 2,
done: task.done || false,
note: task.note || '',
weekday: task.weekday,
slot: task.slot || 'AM',
order: task.order || Date.now(),
repeatType: task.repeatType || 'once',
selectedWeekdays: task.selectedWeekdays || [],
doneAt: task.doneAt || null
};
// 如果是重复任务且在任务池中,确保正确设置
if (processedTask.repeatType !== 'once' && (!processedTask.weekday || processedTask.slot === 'POOL')) {
processedTask.weekday = null;
processedTask.slot = 'POOL';
}
return processedTask;
});
// 合并任务(避免重复ID)
const existingIds = new Set(currentTaskArray.map(task => task.id));
const newTasks = processedTasks.filter(task => !existingIds.has(task.id));
const updatedTasks = [...currentTaskArray, ...newTasks];
// 保存到本地存储并更新全局tasks变量
tasks = updatedTasks;
saveToLocalStorage(updatedTasks);
closeImportModal();
// 显示成功通知
const successToast = document.createElement('div');
successToast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: linear-gradient(135deg, #4caf50, #45a049);
color: white;
padding: 20px 30px;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(76, 175, 80, 0.3);
font-size: 16px;
font-weight: 500;
z-index: 10000;
animation: slideIn 0.3s ease-out;
`;
successToast.innerHTML = `
<div style="display: flex; align-items: center; gap: 12px;">
<span style="font-size: 24px;">✅</span>
<div>
<div style="font-weight: 600;">导入成功!</div>
<div style="font-size: 14px; opacity: 0.9;">已添加 ${newTasks.length} 个任务${processedTasks.length > newTasks.length ? `(跳过 ${processedTasks.length - newTasks.length} 个重复任务)` : ''}</div>
</div>
</div>
`;
document.body.appendChild(successToast);
setTimeout(() => {
successToast.style.animation = 'slideOut 0.3s ease-in';
setTimeout(() => successToast.remove(), 300);
}, 3000);
renderTasks();
// 清空导入数据
importedData = null;
} catch (error) {
console.error('导入失败:', error);
showToast('导入失败,请重试', 'error');
}
}
function showSettingsModal() {
document.getElementById('settingsModal').style.display = 'block';
loadSettings();
}
function closeSettingsModal() {
document.getElementById('settingsModal').style.display = 'none';
}
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
// 更新主题按钮状态
document.querySelectorAll('.theme-btn').forEach(function(btn) {
btn.classList.remove('active');
});
if (event && event.target) {
event.target.classList.add('active');
}
}
function loadSettings() {
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);
// 高亮当前主题按钮
const themeBtns = document.querySelectorAll('.theme-btn');
if (savedTheme === 'light' && themeBtns[0]) themeBtns[0].classList.add('active');
if (savedTheme === 'dark' && themeBtns[1]) themeBtns[1].classList.add('active');
if (savedTheme === 'green' && themeBtns[2]) themeBtns[2].classList.add('active');
// 加载时段设置
const morningRange = localStorage.getItem('morningRange') || '08:00-12:00';
const afternoonRange = localStorage.getItem('afternoonRange') || '13:00-18:00';
const eveningRange = localStorage.getItem('eveningRange') || '19:00-23:00';
// 解析并设置上午时段
const morningTimes = morningRange.split('-');
if (morningTimes.length === 2) {
document.getElementById('morningStart').value = morningTimes[0];
document.getElementById('morningEnd').value = morningTimes[1];
}
// 解析并设置下午时段
const afternoonTimes = afternoonRange.split('-');
if (afternoonTimes.length === 2) {
document.getElementById('afternoonStart').value = afternoonTimes[0];
document.getElementById('afternoonEnd').value = afternoonTimes[1];
}
// 解析并设置晚上时段
const eveningTimes = eveningRange.split('-');
if (eveningTimes.length === 2) {
document.getElementById('eveningStart').value = eveningTimes[0];
document.getElementById('eveningEnd').value = eveningTimes[1];
}
}
function updateTimeLabels() {
const morningRange = localStorage.getItem('morningRange') || '08:00-12:00';
const afternoonRange = localStorage.getItem('afternoonRange') || '13:00-18:00';
const eveningRange = localStorage.getItem('eveningRange') || '19:00-23:00';
const timeLabels = document.querySelectorAll('.time-label');
if (timeLabels[0]) timeLabels[0].innerHTML = `上午<br>${morningRange}`;
if (timeLabels[1]) timeLabels[1].innerHTML = `下午<br>${afternoonRange}`;
if (timeLabels[2]) timeLabels[2].innerHTML = `晚上<br>${eveningRange}`;
}
function saveSettings() {
// 保存时段设置
const morningStart = document.getElementById('morningStart').value;
const morningEnd = document.getElementById('morningEnd').value;
const afternoonStart = document.getElementById('afternoonStart').value;
const afternoonEnd = document.getElementById('afternoonEnd').value;
const eveningStart = document.getElementById('eveningStart').value;
const eveningEnd = document.getElementById('eveningEnd').value;
const morningRange = `${morningStart}-${morningEnd}`;
const afternoonRange = `${afternoonStart}-${afternoonEnd}`;
const eveningRange = `${eveningStart}-${eveningEnd}`;
localStorage.setItem('morningRange', morningRange);
localStorage.setItem('afternoonRange', afternoonRange);
localStorage.setItem('eveningRange', eveningRange);
// 更新时段标签显示
updateTimeLabels();
showToast('设置已保存');
closeSettingsModal();
}
// 键盘快捷键
// 移除不存在的dropZone事件监听器
// 这些事件监听器引用了不存在的dropZone元素
// 键盘快捷键
document.addEventListener('keydown', (e) => {
if (e.ctrlKey) {
switch (e.key) {
case 'n':
e.preventDefault();
addTaskToPool();
break;
case 's':
e.preventDefault();
saveToLocalStorage();
break;
case 'e':
e.preventDefault();
exportToExcel();
break;
}
}
});
// 全局错误处理
window.addEventListener('error', function(e) {
console.error('JavaScript错误:', e.error);
console.error('错误信息:', e.message);
console.error('错误位置:', e.filename, '行', e.lineno);
});
// 初始化
document.addEventListener('DOMContentLoaded', () => {
updateDateDisplay();
loadFromLocalStorage();
loadSettings();
updateTimeLabels();
renderTasks(); // 添加任务渲染,确保页面加载时显示任务
// 每分钟更新日期显示
setInterval(updateDateDisplay, 60000);
console.log('页面加载完成,任务数量:', tasks.length);
console.log('任务列表:', tasks);
});
// 点击模态框外部关闭
window.onclick = function(event) {
const modals = ['taskModal', 'importModal', 'settingsModal'];
modals.forEach(modalId => {
const modal = document.getElementById(modalId);
if (event.target === modal) {
if (modalId === 'taskModal') {
closeModal();
} else if (modalId === 'importModal') {
closeImportModal();
} else if (modalId === 'settingsModal') {
closeSettingsModal();
}
}
});
};
</script>
</body>
</html>
manifest.json
{
"name": "每日工作计划备忘录",
"short_name": "工作计划",
"description": "零依赖的每日工作计划管理工具,支持拖拽排序、Excel导入导出、离线使用",
"start_url": "./index.html",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#0078f5",
"icons": [
{
"src": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkyIiBoZWlnaHQ9IjE5MiIgdmlld0JveD0iMCAwIDE5MiAxOTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxOTIiIGhlaWdodD0iMTkyIiByeD0iMjQiIGZpbGw9IiMwMDc4RjUiLz4KPHN2ZyB4PSI0OCIgeT0iNDgiIHdpZHRoPSI5NiIgaGVpZ2h0PSI5NiIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJ3aGl0ZSI+CjxwYXRoIGQ9Ik0xOSA0SDVjLTEuMTEgMC0yIC44OS0yIDJ2MTJjMCAxLjExLjg5IDIgMiAyaDE0YzEuMTEgMCAyLS44OSAyLTJWNmMwLTEuMTEtLjg5LTItMi0yem0wIDE0SDVWNmgxNHYxMnptLTcuNTEtNC4yOUwxMiAxNGwtMi40OS0yLjI5TDcgMTIuNTEgMTIgMTlsNS01LjQ5LTEuNTEtMS41MXoiLz4KPC9zdmc+Cjwvc3ZnPgo=",
"sizes": "192x192",
"type": "image/svg+xml"
},
{
"src": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiByeD0iNjQiIGZpbGw9IiMwMDc4RjUiLz4KPHN2ZyB4PSIxMjgiIHk9IjEyOCIgd2lkdGg9IjI1NiIgaGVpZ2h0PSIyNTYiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPgo8cGF0aCBkPSJNMTkgNEg1Yy0xLjExIDAtMiAuODktMiAydjEyYzAgMS4xMS44OSAyIDIgMmgxNGMxLjExIDAgMi0uODkgMi0yVjZjMC0xLjExLS44OS0yLTItMnptMCAxNEg1VjZoMTR2MTJ6bS03LjUxLTQuMjlMMTIgMTRsLTIuNDktMi4yOUw3IDEyLjUxIDEyIDE5bDUtNS40OS0xLjUxLTEuNTF6Ii8+Cjwvc3ZnPgo8L3N2Zz4K",
"sizes": "512x512",
"type": "image/svg+xml"
}
],
"categories": ["productivity", "utilities"],
"lang": "zh-CN"
}