[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">
<!-- 任务提醒系统配置 -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<link rel="stylesheet" href="styles.css">
<!-- SheetJS库用于Excel导出 -->
<script src="./xlsx.full.min.js"></script>
</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="week-navigation">
<button class="btn btn-outline week-nav-btn" title="上一周">
<span class="nav-arrow">‹</span>
<span class="nav-text">上一周</span>
</button>
<button class="btn btn-outline week-nav-btn" title="下一周">
<span class="nav-text">下一周</span>
<span class="nav-arrow">›</span>
</button>
<button class="btn btn-secondary week-nav-btn" title="回到本周">
<span class="nav-text">本周</span>
</button>
</div>
<div class="controls">
<button class="btn btn-primary">+ 添加任务</button>
<button class="btn btn-secondary">清空本周</button>
<div class="export-dropdown">
<button class="btn btn-primary dropdown-toggle"><span class="icon icon-chart"></span> 导出日/周报</button>
<div class="dropdown-menu" id="exportDropdown">
<button class="dropdown-item"><span class="icon icon-chart"></span> 导出日报</button>
<button class="dropdown-item"><span class="icon icon-chart"></span> 导出周报</button>
</div>
</div>
<div class="backup-dropdown">
<button class="btn btn-secondary dropdown-toggle"><span class="icon icon-database"></span> 数据备份</button>
<div class="dropdown-menu" id="backupDropdown">
<button class="dropdown-item"><span class="icon icon-export"></span>导出CSV备份</button>
<button class="dropdown-item"><span class="icon icon-import"></span>导入CSV还原</button>
</div>
</div>
<button class="btn btn-outline"><span class="icon icon-settings"></span> 设置</button>
</div>
</header>
<!-- 任务进度条 -->
<div class="progress-container" id="progressContainer">
<div class="progress-header">
<span class="progress-title"><span class="icon icon-chart"></span> 本周任务进度</span>
<span class="progress-stats">
<span class="progress-completed">0</span>/<span class="progress-total">0</span> 任务完成
</span>
</div>
<div class="progress-content">
<div class="progress-bar-wrapper">
<div class="progress-bar">
<div class="progress-fill"></div>
</div>
</div>
<div class="progress-percentage">0%</div>
</div>
</div>
<main class="main-content">
<div class="table-title-container">
<h1 class="table-title" id="tableTitle">每周工作计划表</h1>
<input type="text" class="table-title-input" id="tableTitleInput" style="display: none;">
</div>
<div class="weekly-grid" id="weeklyGrid">
<div class="time-slot-header"><span class="desktop-text">时间</span><span class="mobile-text">时间</span></div>
<div class="day-header" data-day="1">
<span class="desktop-text">周一</span>
<span class="mobile-text">
上午
<span class="mobile-time-range" id="mobileTimeRangeAM">08:00-12:00</span>
</span>
</div>
<div class="day-header" data-day="2">
<span class="desktop-text">周二</span>
<span class="mobile-text">
下午
<span class="mobile-time-range" id="mobileTimeRangePM">13:00-18:00</span>
</span>
</div>
<div class="day-header" data-day="3">
<span class="desktop-text">周三</span>
<span class="mobile-text">
晚上
<span class="mobile-time-range" id="mobileTimeRangeEVENING">19:00-23:00</span>
</span>
</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" data-mobile-day="1"><span class="desktop-text">上午<br>08:00-12:00</span><span class="mobile-text">周一</span></div>
<div class="task-cell" data-day="1" data-slot="AM"></div>
<div class="task-cell" data-day="2" data-slot="AM"></div>
<div class="task-cell" data-day="3" data-slot="AM"></div>
<div class="task-cell" data-day="4" data-slot="AM"></div>
<div class="task-cell" data-day="5" data-slot="AM"></div>
<div class="task-cell" data-day="6" data-slot="AM"></div>
<div class="task-cell" data-day="7" data-slot="AM"></div>
<div class="time-label" data-mobile-day="2"><span class="desktop-text">下午<br>13:00-18:00</span><span class="mobile-text">周二</span></div>
<div class="task-cell" data-day="1" data-slot="PM"></div>
<div class="task-cell" data-day="2" data-slot="PM"></div>
<div class="task-cell" data-day="3" data-slot="PM"></div>
<div class="task-cell" data-day="4" data-slot="PM"></div>
<div class="task-cell" data-day="5" data-slot="PM"></div>
<div class="task-cell" data-day="6" data-slot="PM"></div>
<div class="task-cell" data-day="7" data-slot="PM"></div>
<div class="time-label" data-mobile-day="3"><span class="desktop-text">晚上<br>19:00-23:00</span><span class="mobile-text">周三</span></div>
<div class="task-cell" data-day="1" data-slot="EVENING"></div>
<div class="task-cell" data-day="2" data-slot="EVENING"></div>
<div class="task-cell" data-day="3" data-slot="EVENING"></div>
<div class="task-cell" data-day="4" data-slot="EVENING"></div>
<div class="task-cell" data-day="5" data-slot="EVENING"></div>
<div class="task-cell" data-day="6" data-slot="EVENING"></div>
<div class="task-cell" data-day="7" data-slot="EVENING"></div>
<!-- 手机端额外的时间标签 -->
<div class="time-label mobile-only" data-mobile-day="4"><span class="mobile-text">周四</span></div>
<div class="time-label mobile-only" data-mobile-day="5"><span class="mobile-text">周五</span></div>
<div class="time-label mobile-only" data-mobile-day="6"><span class="mobile-text">周六</span></div>
<div class="time-label mobile-only" data-mobile-day="7"><span class="mobile-text">周日</span></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">×</button>
</div>
<form id="taskForm">
<div class="form-group">
<label>任务标题</label>
<input type="text" class="form-control" id="taskTitle" required>
</div>
<div class="form-group" style="display: flex; gap: 15px;">
<div style="flex: 1;">
<label>开始时间</label>
<input type="time" class="form-control" id="taskStart" required>
</div>
<div style="flex: 1;">
<label>结束时间</label>
<input type="time" class="form-control" id="taskEnd" required>
</div>
</div>
<div style="font-size: 12px; color: #666; margin-top: 5px; margin-bottom: 10px;">
<span class="icon icon-chart"></span> 提示:可以直接手动输入时间,格式为 HH:MM
</div>
<div class="form-group">
<label>优先级</label>
<div class="priority-selector">
<button type="button" class="priority-btn" data-priority="3">低</button>
<button type="button" class="priority-btn" data-priority="2">中</button>
<button type="button" class="priority-btn" data-priority="1">高</button>
</div>
</div>
<div class="form-group">
<label>星期选择</label>
<div class="weekday-selector">
<button type="button" class="weekday-btn" data-day="1">周一</button>
<button type="button" class="weekday-btn" data-day="2">周二</button>
<button type="button" class="weekday-btn" data-day="3">周三</button>
<button type="button" class="weekday-btn" data-day="4">周四</button>
<button type="button" class="weekday-btn" data-day="5">周五</button>
<button type="button" class="weekday-btn" data-day="6">周六</button>
<button type="button" class="weekday-btn" data-day="7">周日</button>
<button type="button" class="weekday-btn" data-day="all">每天</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">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
<!-- 导入模态框 -->
<div class="modal" id="importModal">
<div class="modal-content">
<div class="modal-header">
<h3>
<span class="icon icon-import"></span>CSV任务导入
</h3>
<button class="close-btn">×</button>
</div>
<div class="modal-body">
<!-- 文件上传区域 -->
<div class="import-upload-area">
<div class="upload-icon"></div>
<h4>拖拽文件到此处或点击选择</h4>
<p>支持CSV格式文件,文件大小不超过5MB</p>
<input type="file" id="fileInput" accept=".csv">
<button type="button" class="btn btn-primary">
<span class="icon icon-folder"></span> 选择CSV文件
</button>
</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 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;">
<span class="icon icon-check" style="width: 24px; height: 24px; display: inline-block;"></span>
</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 class="import-instructions">
<h5><span class="icon icon-chart"></span> CSV文件格式说明</h5>
<p>请确保CSV文件包含以下列:<strong>星期,时段,标题,开始时间,结束时间,优先级,备注</strong></p>
<p>例如:周一,上午,团队会议,09:00,10:00,高,讨论项目进度</p>
<div class="encoding-tip">
<h6><span class="icon icon-warning"></span> 编码提示</h6>
<p>如果导入后出现乱码,请将CSV文件保存为UTF-8编码格式。在Excel中:另存为 → CSV UTF-8格式</p>
</div>
<a href="weekly_template.csv" class="download-template" download="每周任务模板.csv">
<span class="icon icon-download"></span> 下载CSV模板文件
</a>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary">取消</button>
<button class="btn btn-primary" id="confirmImportBtn" disabled>
确认导入
</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">×</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;" title="浅色"></button>
<button type="button" class="theme-btn" style="background: #1a1a1a;" title="深色"></button>
<button type="button" class="theme-btn" style="background: #f0f8f0;" title="护眼绿"></button>
</div>
</div>
<div class="form-group">
<label>显示设置</label>
<div style="margin-bottom: 12px; border: 1px solid var(--border-color); padding: 12px; border-radius: 8px;">
<div style="display: flex; gap: 20px; margin-bottom: 8px;">
<label style="font-size: 14px; margin: 0; display: flex; align-items: center;">
<input type="checkbox" id="hideSaturday" style="margin-right: 8px;">
隐藏周六
</label>
<label style="font-size: 14px; margin: 0; display: flex; align-items: center;">
<input type="checkbox" id="hideSunday" style="margin-right: 8px;">
隐藏周日
</label>
</div>
<div style="font-size: 12px; color: #666;">
可以根据需要选择隐藏周六或周日列,适合灵活安排工作时间的用户
</div>
</div>
</div>
<div class="form-group">
<label>时段设置</label>
<div style="background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 4px; padding: 8px; margin-bottom: 12px; font-size: 13px; color: #856404;">
<span class="icon icon-chart"></span> 提示:上午时段和下午时段为必选,不可取消显示
</div>
<!-- 手机端时间范围显示设置 -->
<div class="time-range-config">
<label>
<input type="checkbox" id="showTimeRange" checked style="margin-right: 8px;">
显示时间范围
</label>
<div class="config-description">
控制PC端和手机端是否显示时间段的具体时间范围
</div>
</div>
<!-- 上午时段设置 -->
<div class="time-slot-config">
<div class="time-slot-header">
<label>
<input type="checkbox" id="showMorning" checked style="margin-right: 8px;" disabled>
显示上午时段 (必选)
</label>
</div>
<div class="time-controls">
<select class="form-control" id="morningStart" style="width: 100px;">
<option value="01:00">01:00</option>
<option value="01:30">01:30</option>
<option value="02:00">02:00</option>
<option value="02:30">02:30</option>
<option value="03:00">03:00</option>
<option value="03:30">03:30</option>
<option value="04:00">04:00</option>
<option value="04:30">04:30</option>
<option value="05:00">05:00</option>
<option value="05:30">05:30</option>
<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>
<option value="11:00">11:00</option>
<option value="11:30">11:30</option>
</select>
<span>至</span>
<select class="form-control" id="morningEnd" style="width: 100px;">
<option value="01:00">01:00</option>
<option value="01:30">01:30</option>
<option value="02:00">02:00</option>
<option value="02:30">02:30</option>
<option value="03:00">03:00</option>
<option value="03:30">03:30</option>
<option value="04:00">04:00</option>
<option value="04:30">04:30</option>
<option value="05:00">05:00</option>
<option value="05:30">05:30</option>
<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">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>
<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>
<option value="15:00">15:00</option>
<option value="15:30">15:30</option>
<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">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>
<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">23:00</option>
<option value="23:30">23:30</option>
<option value="24:00">24:00</option>
</select>
</div>
</div>
<!-- 下午时段设置 -->
<div class="time-slot-config">
<div class="time-slot-header">
<label>
<input type="checkbox" id="showAfternoon" checked style="margin-right: 8px;" disabled>
显示下午时段 (必选)
</label>
</div>
<div class="time-controls">
<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 class="time-slot-config">
<div class="time-slot-header">
<label>
<input type="checkbox" id="showEvening" checked style="margin-right: 8px;">
显示晚上时段
</label>
</div>
<div class="time-controls">
<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 class="form-group">
<label>任务提醒设置</label>
<div style="background: #e3f2fd; border: 1px solid #bbdefb; border-radius: 4px; padding: 8px; margin-bottom: 12px; font-size: 13px; color: #1976d2;">
<span class="icon icon-chart"></span> 任务提醒系统:在任务开始和结束时间自动提醒,支持语音播报和弹窗通知
</div>
<div style="margin-bottom: 12px; border: 1px solid var(--border-color); padding: 12px; border-radius: 8px;">
<label style="font-size: 14px; margin: 0; display: flex; align-items: center;">
<input type="checkbox" id="enableTaskReminders" checked style="margin-right: 8px;">
启用任务提醒系统
</label>
<div style="font-size: 12px; color: #666; margin-top: 4px;">
启用后将在任务开始和结束时间自动提醒
</div>
</div>
<div style="margin-bottom: 12px; border: 1px solid var(--border-color); padding: 12px; border-radius: 8px;">
<label style="font-size: 14px; margin: 0; display: flex; align-items: center;">
<input type="checkbox" id="enableVoiceReminders" checked style="margin-right: 8px;">
启用语音提醒
</label>
<div style="font-size: 12px; color: #666; margin-top: 4px;">
启用语音播报提醒(需要浏览器支持)
</div>
</div>
<div style="margin-bottom: 12px; border: 1px solid var(--border-color); padding: 12px; border-radius: 8px;">
<label style="font-size: 14px; margin: 0; display: flex; align-items: center;">
<input type="checkbox" id="enablePopupReminders" checked style="margin-right: 8px;">
启用弹窗提醒
</label>
<div style="font-size: 12px; color: #666; margin-top: 4px;">
启用弹窗通知提醒(支持后台提醒)
</div>
</div>
<div style="margin-bottom: 12px; border: 1px solid var(--border-color); padding: 12px; border-radius: 8px;">
<label style="font-size: 14px; margin: 0; display: flex; align-items: center;">
<input type="checkbox" id="enableBackgroundReminders" checked style="margin-right: 8px;">
启用后台提醒
</label>
<div style="font-size: 12px; color: #666; margin-top: 4px;">
即使浏览器标签页不活跃也能收到提醒
</div>
</div>
<!-- 测试提醒功能 -->
<div style="margin-bottom: 12px; border: 1px solid var(--border-color); padding: 12px; border-radius: 8px; text-align: center;">
<div style="font-size: 14px; margin-bottom: 10px; color: #666;">测试提醒功能</div>
<div style="display: flex; gap: 10px; justify-content: center; flex-wrap: wrap;">
<button type="button" class="btn btn-primary" style="font-size: 12px; padding: 8px 15px;">
测试开始提醒
</button>
<button type="button" class="btn btn-success" style="font-size: 12px; padding: 8px 15px;">
测试结束提醒
</button>
<button type="button" class="btn btn-warning" style="font-size: 12px; padding: 8px 15px;">
测试语音播报
</button>
</div>
<div style="font-size: 11px; color: #999; margin-top: 8px;">
点击按钮测试语音和弹窗提醒功能
</div>
</div>
</div>
<div style="display: flex; gap: 10px; justify-content: flex-end;">
<button class="btn btn-primary">保存设置</button>
</div>
</div>
</div>
<!-- 日报导出模态框 -->
<div class="modal" id="dailyExportModal">
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h3 style="margin: 0; color: var(--primary-color);">
<span class="icon icon-chart"></span> 导出日报
</h3>
<button class="close-btn">×</button>
</div>
<div class="modal-body" style="padding: 30px;">
<div class="form-group">
<label style="font-size: 16px; font-weight: 500; margin-bottom: 15px; display: block;">选择要导出的日期:</label>
<div class="weekday-selector" style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;">
<button type="button" class="export-day-btn" data-day="1">周一</button>
<button type="button" class="export-day-btn" data-day="2">周二</button>
<button type="button" class="export-day-btn" data-day="3">周三</button>
<button type="button" class="export-day-btn" data-day="4">周四</button>
<button type="button" class="export-day-btn" data-day="5">周五</button>
<button type="button" class="export-day-btn" data-day="6">周六</button>
<button type="button" class="export-day-btn" data-day="7">周日</button>
</div>
</div>
<div style="background: #f8f9fa; border-radius: 8px; padding: 15px; margin-top: 20px;">
<p style="margin: 0; font-size: 14px; color: #666;">
<strong><span class="icon icon-chart"></span> 导出说明:</strong><br>
• 将导出选定日期的所有时段任务<br>
• 文件格式:Excel (.xlsx)<br>
• 包含任务标题、时间、优先级等信息
</p>
</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">取消</button>
<button class="btn btn-primary" id="exportDailyBtn" disabled>
<span class="icon icon-chart"></span> 导出日报
</button>
</div>
</div>
</div>
<!-- 提示消息 -->
<div class="toast" id="toast"></div>
<!-- 删除确认模态框 -->
<div class="modal" id="deleteConfirmModal">
<div class="modal-content" style="max-width: 400px; text-align: center;">
<div class="modal-header" style="border-bottom: none; padding-bottom: 10px;">
<h3 style="margin: 0; color: var(--primary-color); font-size: 18px;">
<span class="icon icon-warning"></span> 确认删除
</h3>
</div>
<div class="modal-body" style="padding: 20px 30px;">
<p style="margin: 0; font-size: 16px; color: var(--text-color); line-height: 1.5;">
确定要删除这个任务吗?
</p>
<p style="margin: 10px 0 0 0; font-size: 14px; color: #666;">
此操作无法撤销
</p>
</div>
<div class="modal-footer" style="padding: 20px 30px; border-top: 1px solid var(--border-color); display: flex; justify-content: center; gap: 15px;">
<button class="btn btn-secondary" style="min-width: 80px;">取消</button>
<button class="btn btn-danger" style="min-width: 80px; background: #dc3545; border-color: #dc3545;">删除</button>
</div>
</div>
</div>
<!-- 任务提醒模态框 -->
<div class="reminder-modal" id="reminderModal">
<div class="reminder-content">
<div class="reminder-header">
<div class="reminder-title" id="reminderTitle">任务提醒</div>
<div class="reminder-subtitle" id="reminderSubtitle">您有新的任务提醒</div>
<button class="close-btn" style="position: absolute; top: 15px; right: 15px; background: none; border: none; font-size: 24px; cursor: pointer; color: #666;">×</button>
</div>
<div class="reminder-task-info">
<div class="reminder-task-title" id="reminderTaskTitle">任务标题</div>
<div class="reminder-task-time" id="reminderTaskTime">时间信息</div>
<div class="reminder-task-note" id="reminderTaskNote">备注信息</div>
</div>
<div class="reminder-actions" id="reminderActions">
<!-- 开始时间提醒的按钮 -->
<button class="reminder-btn reminder-btn-primary" id="startReminderBtn" style="display: none;">确认</button>
<!-- 结束时间提醒的按钮 -->
<button class="reminder-btn reminder-btn-success" id="endReminderConfirmBtn" style="display: none;">确认完成</button>
<button class="reminder-btn reminder-btn-secondary" id="endReminderDelayBtn" style="display: none;">5分钟后再提醒</button>
</div>
<div class="reminder-settings">
<label>
<input type="checkbox" id="reminderVoiceCheckbox" checked> 启用语音提醒
</label>
<label>
<input type="checkbox" id="reminderPopupCheckbox" checked> 启用弹窗提醒
</label>
</div>
</div>
</div>
<script>
// 全局变量
let tasks = [];
let currentEditingTask = null;
let draggedTask = null;
let importedData = null;
// 周导航相关变量
let currentWeekOffset = 0; // 相对于当前周的偏移量,0表示本周,-1表示上周,1表示下周
let currentDisplayDate = new Date(); // 当前显示的日期
// 任务提醒系统相关变量
let reminderSystem = {
enabled: true,
voiceEnabled: true,
popupEnabled: true,
backgroundEnabled: true,
currentReminder: null,
reminderTimeouts: new Map(),
speechSynthesis: null,
notificationPermission: 'default',
// 新增:提醒记录,防止重复提醒
reminderHistory: new Map(), // 格式: 'taskId-type-date' -> timestamp
lastCheckTime: null, // 记录上次检查的时间
// 页面可见性相关
isPageVisible: true, // 页面是否可见
visibilityChangeHandlers: [], // 页面可见性变化处理器
// 新增:自动确认定时器
autoConfirmTimeout: null // 30秒自动确认定时器
};
// 初始化Page Visibility API
function initPageVisibility() {
// 检查浏览器支持
let hidden, visibilityChange;
if (typeof document.hidden !== "undefined") {
hidden = "hidden";
visibilityChange = "visibilitychange";
} else if (typeof document.msHidden !== "undefined") {
hidden = "msHidden";
visibilityChange = "msvisibilitychange";
} else if (typeof document.webkitHidden !== "undefined") {
hidden = "webkitHidden";
visibilityChange = "webkitvisibilitychange";
}
if (typeof document[hidden] !== "undefined") {
// 初始状态
reminderSystem.isPageVisible = !document[hidden];
// 监听可见性变化
document.addEventListener(visibilityChange, function() {
const wasVisible = reminderSystem.isPageVisible;
reminderSystem.isPageVisible = !document[hidden];
console.log(`页面可见性变化: ${wasVisible ? '可见' : '隐藏'} -> ${reminderSystem.isPageVisible ? '可见' : '隐藏'}`);
// 触发处理器
reminderSystem.visibilityChangeHandlers.forEach(handler => {
try {
handler(reminderSystem.isPageVisible, wasVisible);
} catch (error) {
console.error('页面可见性变化处理器错误:', error);
}
});
}, false);
console.log('Page Visibility API 初始化成功');
} else {
console.warn('浏览器不支持 Page Visibility API');
}
}
// 初始化语音合成
function initSpeechSynthesis() {
console.log('=== 初始化语音合成 ===');
if ('speechSynthesis' in window) {
reminderSystem.speechSynthesis = window.speechSynthesis;
console.log('语音合成对象已设置');
// 语音列表可能需要时间加载,设置监听器
const loadVoices = () => {
const voices = reminderSystem.speechSynthesis.getVoices();
console.log('语音列表加载完成,可用语音数量:', voices.length);
if (voices.length > 0) {
console.log('可用语音:', voices.map(v => `${v.name} (${v.lang})`));
// 尝试找到中文语音
const chineseVoice = voices.find(voice =>
voice.lang.includes('zh') || voice.lang.includes('cmn')
);
if (chineseVoice) {
console.log('找到中文语音:', chineseVoice.name);
reminderSystem.speechSynthesis.defaultVoice = chineseVoice;
} else {
console.log('未找到中文语音,将使用默认语音');
}
} else {
console.log('暂无可用语音,可能还在加载中');
}
};
// 立即尝试加载
loadVoices();
// 监听语音变化事件(某些浏览器需要)
if (reminderSystem.speechSynthesis.onvoiceschanged !== undefined) {
reminderSystem.speechSynthesis.onvoiceschanged = loadVoices;
}
console.log('语音合成初始化完成');
} else {
console.warn('浏览器不支持语音合成功能');
}
}
// 语音播报
function speakMessage(message, forceSpeak = false) {
console.log('=== 语音播报调试信息 ===');
console.log('消息:', message);
console.log('强制播放:', forceSpeak);
console.log('语音启用状态:', reminderSystem.voiceEnabled);
console.log('语音合成对象:', reminderSystem.speechSynthesis);
console.log('页面可见状态:', reminderSystem.isPageVisible);
if (!reminderSystem.voiceEnabled) {
console.log('语音提醒已禁用');
return;
}
if (!reminderSystem.speechSynthesis) {
console.log('语音合成不可用');
return;
}
// 移除页面可见性检查,允许在后台播放语音
// 现代浏览器的语音合成API支持在后台标签页播放
console.log('执行语音播报(支持后台播放)');
try {
console.log('开始执行语音播报...');
// 停止当前播放的语音
if (reminderSystem.speechSynthesis.speaking) {
console.log('停止当前播放的语音');
reminderSystem.speechSynthesis.cancel();
}
// 等待一小段时间确保cancel完成
setTimeout(() => {
const utterance = new SpeechSynthesisUtterance(message);
utterance.lang = 'zh-CN';
utterance.rate = 0.9;
utterance.pitch = 1.0;
utterance.volume = 1.0;
// 获取可用语音
const voices = reminderSystem.speechSynthesis.getVoices();
console.log('可用语音数量:', voices.length);
// 如果有中文语音,使用中文语音
const chineseVoice = voices.find(voice =>
voice.lang.includes('zh') || voice.lang.includes('cmn')
);
if (chineseVoice) {
console.log('使用中文语音:', chineseVoice.name);
utterance.voice = chineseVoice;
} else {
console.log('未找到中文语音,使用默认语音');
}
utterance.onstart = () => {
console.log('✅ 语音播报开始:', message);
};
utterance.onend = () => {
console.log('✅ 语音播报完成');
};
utterance.onerror = (error) => {
console.error('❌ 语音播报错误:', error);
// 如果语音播报失败,尝试重新播放一次
if (!error.error || error.error !== 'interrupted') {
console.log('尝试重新播放语音...');
setTimeout(() => {
reminderSystem.speechSynthesis.speak(utterance);
}, 500);
}
};
console.log('调用speechSynthesis.speak()');
reminderSystem.speechSynthesis.speak(utterance);
console.log('speak()调用完成');
}, 100);
} catch (error) {
console.error('❌ 语音播报失败:', error);
}
}
// 显示弹窗提醒
function showPopupReminder(task, type) {
if (!reminderSystem.popupEnabled) {
console.log('弹窗提醒已禁用');
return;
}
try {
const modal = document.getElementById('reminderModal');
if (!modal) {
console.error('找不到提醒模态框元素');
return;
}
// 如果已经有提醒在显示,先关闭
if (reminderSystem.currentReminder) {
console.log('已有提醒在显示,关闭当前提醒');
modal.style.display = 'none';
// 清除之前的自动确认定时器
if (reminderSystem.autoConfirmTimeout) {
clearTimeout(reminderSystem.autoConfirmTimeout);
reminderSystem.autoConfirmTimeout = null;
}
}
const title = document.getElementById('reminderTitle');
const subtitle = document.getElementById('reminderSubtitle');
const taskTitle = document.getElementById('reminderTaskTitle');
const taskTime = document.getElementById('reminderTaskTime');
const taskNote = document.getElementById('reminderTaskNote');
const startBtn = document.getElementById('startReminderBtn');
const endConfirmBtn = document.getElementById('endReminderConfirmBtn');
const endDelayBtn = document.getElementById('endReminderDelayBtn');
// 验证所有必需的元素
if (!title || !subtitle || !taskTitle || !taskTime || !taskNote || !startBtn || !endConfirmBtn || !endDelayBtn) {
console.error('提醒模态框元素不完整');
return;
}
// 设置任务信息
taskTitle.textContent = task.title || '未命名任务';
taskTime.textContent = task.start && task.end ? `${task.start} - ${task.end}` : '无时间设置';
taskNote.textContent = task.note || '无备注';
// 设置提醒类型
if (type === 'start') {
title.textContent = '任务开始提醒';
subtitle.textContent = '任务时间到了,请开始执行';
startBtn.style.display = 'inline-block';
endConfirmBtn.style.display = 'none';
endDelayBtn.style.display = 'none';
} else if (type === 'end') {
title.textContent = '任务结束提醒';
subtitle.textContent = '任务时间到了,您完成了任务了吗?';
startBtn.style.display = 'none';
endConfirmBtn.style.display = 'inline-block';
endDelayBtn.style.display = 'inline-block';
}
// 存储当前提醒的任务信息
reminderSystem.currentReminder = { task, type, timestamp: Date.now() };
// 显示模态框
modal.style.display = 'block';
// 自动聚焦到确认按钮
setTimeout(() => {
try {
if (type === 'start' && startBtn) {
startBtn.focus();
} else if (type === 'end' && endConfirmBtn) {
endConfirmBtn.focus();
}
} catch (focusError) {
console.warn('聚焦按钮失败:', focusError);
}
}, 100);
// 设置30秒自动确认
reminderSystem.autoConfirmTimeout = setTimeout(() => {
if (reminderSystem.currentReminder &&
reminderSystem.currentReminder.task.id === task.id &&
reminderSystem.currentReminder.type === type) {
console.log(`30秒无操作,自动确认${type === 'start' ? '开始' : '结束'}提醒: ${task.title}`);
if (type === 'start') {
confirmStartReminder();
} else if (type === 'end') {
confirmEndReminder();
}
}
}, 30000); // 30秒后自动确认
console.log(`弹窗提醒已显示: ${task.title} (${type})`);
} catch (error) {
console.error('显示弹窗提醒时发生错误:', error);
}
}
// 确认开始提醒
function confirmStartReminder() {
if (reminderSystem.currentReminder) {
const { task } = reminderSystem.currentReminder;
showToast(`已确认开始执行任务:${task.title}`, 'success');
document.getElementById('reminderModal').style.display = 'none';
reminderSystem.currentReminder = null;
}
}
// 确认结束提醒
function confirmEndReminder() {
if (reminderSystem.currentReminder) {
const { task } = reminderSystem.currentReminder;
// 标记任务为完成
task.done = true;
task.doneAt = new Date().toISOString();
renderTasks();
saveToLocalStorage();
showToast(`任务已完成:${task.title}`, 'success');
document.getElementById('reminderModal').style.display = 'none';
reminderSystem.currentReminder = null;
}
}
// 延迟结束提醒
function delayEndReminder() {
if (reminderSystem.currentReminder) {
const { task } = reminderSystem.currentReminder;
showToast(`5分钟后将再次提醒任务:${task.title}`, 'info');
document.getElementById('reminderModal').style.display = 'none';
// 5分钟后再次提醒
const timeoutId = setTimeout(() => {
if (reminderSystem.enabled) {
// 延迟提醒不受历史记录限制
showTaskReminder(task, 'end');
}
}, 5 * 60 * 1000);
// 存储延迟提醒的超时ID
reminderSystem.reminderTimeouts.set(`${task.id}-delay-end`, timeoutId);
reminderSystem.currentReminder = null;
}
}
// 清理过期的提醒记录
function cleanupReminderHistory() {
const now = new Date();
const currentDateStr = now.toDateString();
// 删除不是今天的提醒记录
for (const [key, timestamp] of reminderSystem.reminderHistory.entries()) {
const recordDate = new Date(timestamp).toDateString();
if (recordDate !== currentDateStr) {
reminderSystem.reminderHistory.delete(key);
}
}
}
// 重置提醒历史记录(用于测试或手动重置)
function resetReminderHistory() {
reminderSystem.reminderHistory.clear();
reminderSystem.lastCheckTime = null;
console.log('提醒历史记录已重置');
}
// 显示任务提醒
function showTaskReminder(task, type) {
if (!reminderSystem.enabled) {
console.log('提醒系统已禁用,跳过提醒');
return;
}
// 验证任务数据
if (!task || !task.id || !task.title) {
console.error('无效的任务数据:', task);
return;
}
// 如果任务已完成,跳过结束提醒
if (type === 'end' && task.done) {
console.log(`任务 ${task.title} 已完成,跳过结束提醒`);
return;
}
// 记录实际提醒时间
const actualTime = new Date().toLocaleTimeString('zh-CN', { hour12: false });
console.log(`显示${type === 'start' ? '开始' : '结束'}提醒: ${task.title} (${actualTime})`);
try {
// 检查页面是否可见,决定提醒方式
const isPageVisible = reminderSystem.isPageVisible;
console.log(`页面可见状态: ${isPageVisible ? '可见' : '隐藏'}`);
// 语音提醒和弹窗提醒同时进行(同步执行)
// 1. 先播放语音
if (reminderSystem.voiceEnabled) {
if (type === 'start') {
speakMessage(`${task.title}开始执行`, true); // 强制播放
} else if (type === 'end') {
speakMessage(`${task.title}任务时间到了,您完成了任务了吗?`, true); // 强制播放
}
}
// 2. 同时显示弹窗提醒(无论页面是否可见)
if (reminderSystem.popupEnabled) {
showPopupReminder(task, type);
}
// 页面不可见时的补充处理
if (!isPageVisible) {
console.log('页面不可见,使用浏览器通知作为补充提醒');
}
// 浏览器通知(页面不可见时强制使用,可见时作为补充)
if ((reminderSystem.backgroundEnabled || !isPageVisible) && 'Notification' in window && reminderSystem.notificationPermission === 'granted') {
try {
const notificationTitle = type === 'start' ? '📅 任务开始提醒' : '⏰ 任务结束提醒';
const notificationBody = type === 'start'
? `${task.title} - 现在开始执行`
: `${task.title} - 任务时间到了,您完成了吗?`;
const notification = new Notification(notificationTitle, {
body: notificationBody,
icon: '/favicon.ico',
tag: `task-${task.id}-${type}`,
requireInteraction: true,
silent: false, // 确保有声音提示
badge: '/favicon.ico',
timestamp: Date.now(),
actions: type === 'end' ? [
{ action: 'complete', title: '标记完成' },
{ action: 'delay', title: '延迟5分钟' }
] : []
});
// 点击通知时的处理
notification.onclick = function() {
console.log('用户点击了通知');
window.focus();
this.close();
// 如果页面现在可见,显示弹窗
if (reminderSystem.isPageVisible && reminderSystem.popupEnabled) {
showPopupReminder(task, type);
}
};
// 处理通知操作
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
navigator.serviceWorker.addEventListener('message', function(event) {
if (event.data.action === 'complete' && event.data.taskId === task.id) {
markTaskComplete(task.id);
} else if (event.data.action === 'delay' && event.data.taskId === task.id) {
delayEndReminder(task);
}
});
}
// 自动关闭通知(如果页面变为可见)
const closeOnVisible = (isVisible) => {
if (isVisible && notification) {
notification.close();
// 移除监听器
const index = reminderSystem.visibilityChangeHandlers.indexOf(closeOnVisible);
if (index > -1) {
reminderSystem.visibilityChangeHandlers.splice(index, 1);
}
}
};
reminderSystem.visibilityChangeHandlers.push(closeOnVisible);
console.log(`发送${isPageVisible ? '补充' : '后台'}通知: ${notificationTitle}`);
} catch (error) {
console.error('浏览器通知失败:', error);
// 如果通知失败且页面不可见,尝试其他方式
if (!isPageVisible) {
console.log('通知失败,尝试使用标题闪烁提醒');
flashPageTitle(task, type);
}
}
} else if (!isPageVisible) {
// 如果没有通知权限但页面不可见,使用标题闪烁
console.log('无通知权限,使用标题闪烁提醒');
flashPageTitle(task, type);
}
} catch (error) {
console.error('显示任务提醒时发生错误:', error);
}
}
// 检查任务是否需要提醒
function checkTaskReminders() {
if (!reminderSystem.enabled) return;
const now = new Date();
const currentTime = now.toTimeString().slice(0, 5); // HH:MM 格式
const currentDay = now.getDay() || 7; // 转换为1-7格式
const currentDateStr = now.toDateString(); // 获取当前日期字符串
const currentTimeWithSeconds = now.toTimeString().slice(0, 8); // HH:MM:SS 格式,用于精确记录
// 获取当前小时和分钟
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
console.log(`检查提醒 - 当前时间: ${currentTimeWithSeconds}, 星期: ${currentDay}`);
tasks.forEach(task => {
if (task.done || !task.start || !task.end || !task.weekday) return;
// 只检查今天的任务
if (task.weekday !== currentDay) return;
const taskId = task.id;
// 解析任务开始时间
const [startHour, startMin] = task.start.split(':').map(Number);
const [endHour, endMin] = task.end.split(':').map(Number);
// 检查开始时间提醒(允许1分钟内的误差)
if (startHour === currentHour &&
(startMin === currentMinute || startMin === currentMinute - 1)) {
const reminderKey = `${taskId}-start-${currentDateStr}`;
// 检查今天是否已经提醒过
if (!reminderSystem.reminderHistory.has(reminderKey)) {
console.log(`触发开始提醒: ${task.title} at ${currentTime}`);
reminderSystem.reminderHistory.set(reminderKey, now.getTime());
showTaskReminder(task, 'start');
}
}
// 检查结束时间提醒(允许1分钟内的误差)
if (endHour === currentHour &&
(endMin === currentMinute || endMin === currentMinute - 1)) {
const reminderKey = `${taskId}-end-${currentDateStr}`;
// 检查今天是否已经提醒过
if (!reminderSystem.reminderHistory.has(reminderKey)) {
console.log(`触发结束提醒: ${task.title} at ${currentTime}`);
reminderSystem.reminderHistory.set(reminderKey, now.getTime());
showTaskReminder(task, 'end');
}
}
});
// 清理过期的提醒记录(保留今天的记录)
cleanupReminderHistory();
}
// 请求通知权限
async function requestNotificationPermission() {
if ('Notification' in window && reminderSystem.notificationPermission === 'default') {
try {
const permission = await Notification.requestPermission();
reminderSystem.notificationPermission = permission;
if (permission === 'granted') {
showToast('已获得通知权限,支持后台提醒', 'success');
}
} catch (error) {
console.error('请求通知权限失败:', error);
}
}
}
// 加载提醒系统设置
function loadReminderSettings() {
reminderSystem.enabled = localStorage.getItem('enableTaskReminders') !== 'false';
reminderSystem.voiceEnabled = localStorage.getItem('enableVoiceReminders') !== 'false';
reminderSystem.popupEnabled = localStorage.getItem('enablePopupReminders') !== 'false';
reminderSystem.backgroundEnabled = localStorage.getItem('enableBackgroundReminders') !== 'false';
// 更新设置界面
document.getElementById('enableTaskReminders').checked = reminderSystem.enabled;
document.getElementById('enableVoiceReminders').checked = reminderSystem.voiceEnabled;
document.getElementById('enablePopupReminders').checked = reminderSystem.popupEnabled;
document.getElementById('enableBackgroundReminders').checked = reminderSystem.backgroundEnabled;
// 更新提醒模态框中的复选框
const reminderVoiceCheckbox = document.getElementById('reminderVoiceCheckbox');
const reminderPopupCheckbox = document.getElementById('reminderPopupCheckbox');
if (reminderVoiceCheckbox) {
reminderVoiceCheckbox.checked = reminderSystem.voiceEnabled;
}
if (reminderPopupCheckbox) {
reminderPopupCheckbox.checked = reminderSystem.popupEnabled;
}
}
// 保存提醒系统设置
function saveReminderSettings() {
reminderSystem.enabled = document.getElementById('enableTaskReminders').checked;
reminderSystem.voiceEnabled = document.getElementById('enableVoiceReminders').checked;
reminderSystem.popupEnabled = document.getElementById('enablePopupReminders').checked;
reminderSystem.backgroundEnabled = document.getElementById('enableBackgroundReminders').checked;
localStorage.setItem('enableTaskReminders', reminderSystem.enabled);
localStorage.setItem('enableVoiceReminders', reminderSystem.voiceEnabled);
localStorage.setItem('enablePopupReminders', reminderSystem.popupEnabled);
localStorage.setItem('enableBackgroundReminders', reminderSystem.backgroundEnabled);
showToast('提醒设置已保存', 'success');
}
// 工具函数
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;
const weekKey = getWeekKey(currentDisplayDate);
// 获取所有周的数据
let allWeeksData = {};
try {
const saved = localStorage.getItem('allWeeklyTasks');
if (saved) {
allWeeksData = JSON.parse(saved);
}
} catch (error) {
console.error('解析多周数据失败:', error);
allWeeksData = {};
}
// 保存当前周的数据
allWeeksData[weekKey] = dataToSave;
localStorage.setItem('allWeeklyTasks', JSON.stringify(allWeeksData));
// 兼容旧版本:如果是本周,也保存到旧的存储位置
if (currentWeekOffset === 0) {
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 loadWeekTasks() {
const weekKey = getWeekKey(currentDisplayDate);
// 尝试从多周数据中加载
try {
const allWeeksData = localStorage.getItem('allWeeklyTasks');
if (allWeeksData) {
const parsed = JSON.parse(allWeeksData);
if (parsed[weekKey]) {
tasks = Array.isArray(parsed[weekKey]) ? parsed[weekKey] : [];
return tasks;
}
}
} catch (error) {
console.error('解析多周数据失败:', error);
}
// 如果是本周且多周数据中没有,尝试从旧版本数据加载
if (currentWeekOffset === 0) {
return loadFromLocalStorage();
}
// 其他周没有数据,返回空数组
tasks = [];
return [];
}
// 周导航相关函数
function getWeekStartDate(date, weekOffset = 0) {
const targetDate = new Date(date);
targetDate.setDate(targetDate.getDate() + (weekOffset * 7));
const startOfWeek = new Date(targetDate);
startOfWeek.setDate(targetDate.getDate() - targetDate.getDay() + 1);
return startOfWeek;
}
function getWeekEndDate(startDate) {
const endDate = new Date(startDate);
endDate.setDate(startDate.getDate() + 6);
return endDate;
}
function getWeekNumber(date) {
const start = new Date(date.getFullYear(), 0, 1);
const days = Math.floor((date - start) / (24 * 60 * 60 * 1000));
return Math.ceil((days + start.getDay() + 1) / 7);
}
function getWeekKey(date) {
const startOfWeek = getWeekStartDate(date);
return `${startOfWeek.getFullYear()}-W${getWeekNumber(startOfWeek)}`;
}
function navigateWeek(direction) {
currentWeekOffset += direction;
currentDisplayDate = getWeekStartDate(new Date(), currentWeekOffset);
updateDateDisplay();
loadWeekTasks();
renderTasks();
}
function navigateToCurrentWeek() {
currentWeekOffset = 0;
currentDisplayDate = new Date();
updateDateDisplay();
loadWeekTasks();
renderTasks();
}
function updateDateDisplay() {
const now = new Date();
const displayDate = currentWeekOffset === 0 ? now : currentDisplayDate;
// 显示当前日期(如果是本周)或显示周信息
if (currentWeekOffset === 0) {
const options = { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' };
document.getElementById('currentDate').textContent = now.toLocaleDateString('zh-CN', options);
} else {
const startOfWeek = getWeekStartDate(new Date(), currentWeekOffset);
const options = { year: 'numeric', month: 'long', day: 'numeric' };
document.getElementById('currentDate').textContent = `${startOfWeek.toLocaleDateString('zh-CN', options)} 的周`;
}
const startOfWeek = getWeekStartDate(new Date(), currentWeekOffset);
const endOfWeek = getWeekEndDate(startOfWeek);
const weekNumber = getWeekNumber(startOfWeek);
const weekPrefix = currentWeekOffset === 0 ? '第' :
currentWeekOffset < 0 ? '第' : '第';
const weekSuffix = currentWeekOffset === 0 ? '周 (本周)' :
currentWeekOffset < 0 ? '周 (过去)' : '周 (未来)';
document.getElementById('weekInfo').textContent = `${weekPrefix}${weekNumber}${weekSuffix} ${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 (currentWeekOffset === 0 && parseInt(header.dataset.day) === today && today !== 7) {
header.classList.add('today');
}
});
}
// 更新任务进度条
function updateProgressBar() {
const totalTasks = tasks.length;
const completedTasks = tasks.filter(task => task.done).length;
const percentage = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
// 更新进度条显示
const progressContainer = document.getElementById('progressContainer');
if (progressContainer) {
const totalElement = progressContainer.querySelector('.progress-total');
const completedElement = progressContainer.querySelector('.progress-completed');
const percentageElement = progressContainer.querySelector('.progress-percentage');
const progressFill = progressContainer.querySelector('.progress-fill');
if (totalElement) totalElement.textContent = totalTasks;
if (completedElement) completedElement.textContent = completedTasks;
if (percentageElement) percentageElement.textContent = `${percentage}%`;
if (progressFill) {
progressFill.style.width = `${percentage}%`;
// 根据完成度设置颜色
if (percentage === 100) {
progressFill.style.background = 'linear-gradient(90deg, #4CAF50, #66BB6A)';
} else if (percentage >= 70) {
progressFill.style.background = 'linear-gradient(90deg, #2196F3, #42A5F5)';
} else if (percentage >= 30) {
progressFill.style.background = 'linear-gradient(90deg, #FF9800, #FFB74D)';
} else {
progressFill.style.background = 'linear-gradient(90deg, #F44336, #EF5350)';
}
}
}
}
function renderTasks() {
// 清空所有任务显示
document.querySelectorAll('.task-card').forEach(card => card.remove());
document.querySelectorAll('#taskPool .task-card').forEach(card => card.remove());
// 获取显示/隐藏设置
const showMorning = localStorage.getItem('showMorning') !== 'false';
const showAfternoon = localStorage.getItem('showAfternoon') !== 'false';
const showEvening = localStorage.getItem('showEvening') !== 'false';
tasks.forEach(task => {
const taskElement = createTaskElement(task);
if (task.slot === 'POOL') {
document.getElementById('taskPool').appendChild(taskElement);
} else {
// 检查时段是否被隐藏
let shouldShow = true;
if (task.slot === '上午' && !showMorning) shouldShow = false;
if (task.slot === '下午' && !showAfternoon) shouldShow = false;
if (task.slot === '晚上' && !showEvening) shouldShow = false;
if (shouldShow) {
const cell = document.querySelector(`[data-day="${task.weekday}"][data-slot="${task.slot}"]`);
if (cell) {
cell.appendChild(taskElement);
}
} else {
// 如果时段被隐藏,将任务移到任务池
task.slot = 'POOL';
task.weekday = null;
document.getElementById('taskPool').appendChild(taskElement);
}
}
});
// 更新任务进度条
updateProgressBar();
}
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' : ''}
>
<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">×</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;
});
// 添加触摸拖拽支持
let touchStartX, touchStartY, isDragging = false, touchTarget = null;
div.addEventListener('touchstart', (e) => {
if (e.target.classList.contains('task-checkbox') || e.target.classList.contains('delete-btn')) {
return;
}
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
isDragging = false;
touchTarget = null;
}, { passive: false });
div.addEventListener('touchmove', (e) => {
if (e.target.classList.contains('task-checkbox') || e.target.classList.contains('delete-btn')) {
return;
}
e.preventDefault();
const touch = e.touches[0];
const deltaX = Math.abs(touch.clientX - touchStartX);
const deltaY = Math.abs(touch.clientY - touchStartY);
if (!isDragging && (deltaX > 10 || deltaY > 10)) {
isDragging = true;
draggedTask = task;
div.classList.add('dragging');
div.style.position = 'fixed';
div.style.zIndex = '1000';
div.style.pointerEvents = 'none';
div.style.opacity = '0.8';
}
if (isDragging) {
div.style.left = (touch.clientX - 50) + 'px';
div.style.top = (touch.clientY - 25) + 'px';
// 查找触摸点下的目标元素
const elementBelow = document.elementFromPoint(touch.clientX, touch.clientY);
if (elementBelow) {
const taskCell = elementBelow.closest('.task-cell');
if (taskCell && taskCell !== touchTarget) {
if (touchTarget) {
touchTarget.classList.remove('drag-over');
}
touchTarget = taskCell;
taskCell.classList.add('drag-over');
} else if (!taskCell && touchTarget) {
touchTarget.classList.remove('drag-over');
touchTarget = null;
}
}
}
}, { passive: false });
div.addEventListener('touchend', (e) => {
if (isDragging) {
e.preventDefault();
// 重置样式
div.style.position = '';
div.style.zIndex = '';
div.style.pointerEvents = '';
div.style.opacity = '';
div.style.left = '';
div.style.top = '';
div.classList.remove('dragging');
if (touchTarget) {
touchTarget.classList.remove('drag-over');
// 执行拖放操作
const fakeEvent = {
preventDefault: () => {},
dataTransfer: {
getData: () => task.id
}
};
drop(fakeEvent, 'grid', touchTarget);
touchTarget = null;
}
draggedTask = null;
isDragging = false;
} else if (!isDragging) {
// 如果没有拖拽,则处理点击事件
if (!e.target.classList.contains('task-checkbox') && !e.target.classList.contains('delete-btn')) {
console.log('点击任务,准备编辑:', task);
editTask(task);
}
}
}, { passive: false });
// 确保任务卡片可点击(桌面端)
div.style.cursor = 'pointer';
div.addEventListener('click', (e) => {
if (isDragging) return; // 如果正在拖拽,忽略点击
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, targetElement = null) {
ev.preventDefault();
if (!draggedTask) return;
if (targetType === 'pool') {
// 拖到任务池,只更新位置,不清除原任务
draggedTask.slot = 'POOL';
draggedTask.weekday = null;
} else {
const cell = targetElement || 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);
// 清除所有星期选择状态
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 (day) {
// 如果指定了具体星期,选中对应的星期
const weekdayBtn = document.querySelector(`.weekday-btn[data-day="${day}"]`);
if (weekdayBtn) {
weekdayBtn.classList.add('active');
}
} else {
// 如果没有指定星期(从顶部按钮添加任务),自动选择当前星期几
const today = new Date();
const currentDay = today.getDay(); // 0是周日,1-6是周一到周六
const dayToSelect = currentDay === 0 ? 7 : currentDay; // 转换为1-7的格式
const weekdayBtn = document.querySelector(`.weekday-btn[data-day="${dayToSelect}"]`);
if (weekdayBtn) {
weekdayBtn.classList.add('active');
}
}
document.getElementById('taskModal').style.display = 'block';
// 存储临时位置信息
window.tempTaskLocation = { day, slot };
}
function addTaskToPool() {
// 获取当前时间
const now = new Date();
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
const currentTime = currentHour * 60 + currentMinute; // 转换为分钟数便于比较
// 获取时间段设置
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';
// 解析时间范围
function parseTimeRange(range) {
const [start, end] = range.split('-');
const [startHour, startMin] = start.split(':').map(Number);
const [endHour, endMin] = end.split(':').map(Number);
return {
start: startHour * 60 + startMin,
end: endHour * 60 + endMin
};
}
const morning = parseTimeRange(morningRange);
const afternoon = parseTimeRange(afternoonRange);
const evening = parseTimeRange(eveningRange);
// 判断当前时间属于哪个时间段
let targetSlot = null;
if (currentTime >= morning.start && currentTime <= morning.end) {
targetSlot = 'AM';
} else if (currentTime >= afternoon.start && currentTime <= afternoon.end) {
targetSlot = 'PM';
} else if (currentTime >= evening.start && currentTime <= evening.end) {
targetSlot = 'EVENING';
} else {
// 如果不在任何时间段内,根据最接近的时间段来判断
const distanceToMorning = Math.min(
Math.abs(currentTime - morning.start),
Math.abs(currentTime - morning.end)
);
const distanceToAfternoon = Math.min(
Math.abs(currentTime - afternoon.start),
Math.abs(currentTime - afternoon.end)
);
const distanceToEvening = Math.min(
Math.abs(currentTime - evening.start),
Math.abs(currentTime - evening.end)
);
const minDistance = Math.min(distanceToMorning, distanceToAfternoon, distanceToEvening);
if (minDistance === distanceToMorning) {
targetSlot = 'AM';
} else if (minDistance === distanceToAfternoon) {
targetSlot = 'PM';
} else {
targetSlot = 'EVENING';
}
}
// 获取当前星期几(如果是本周)或默认选择周一(如果是其他周)
let dayToSelect;
if (currentWeekOffset === 0) {
// 本周:使用实际的当前星期几
const today = new Date();
const currentDay = today.getDay(); // 0是周日,1-6是周一到周六
dayToSelect = currentDay === 0 ? 7 : currentDay; // 转换为1-7的格式
} else {
// 其他周:默认选择周一
dayToSelect = 1;
}
// 调用addTask函数,传入选择的星期几和自动判断的时间段
addTask(dayToSelect, targetSlot);
}
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 determineSlotByTime(startTime) {
if (!startTime) return 'AM'; // 默认上午
// 获取时间段设置
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';
// 解析时间范围
function parseTimeRange(range) {
const [start, end] = range.split('-');
const [startHour, startMin] = start.split(':').map(Number);
const [endHour, endMin] = end.split(':').map(Number);
return {
start: startHour * 60 + startMin,
end: endHour * 60 + endMin
};
}
const [startHour, startMin] = startTime.split(':').map(Number);
const startMinutes = startHour * 60 + startMin;
const morning = parseTimeRange(morningRange);
const afternoon = parseTimeRange(afternoonRange);
const evening = parseTimeRange(eveningRange);
// 根据开始时间判断时段
if (startMinutes >= morning.start && startMinutes <= morning.end) {
return 'AM';
} else if (startMinutes >= afternoon.start && startMinutes <= afternoon.end) {
return 'PM';
} else if (startMinutes >= evening.start && startMinutes <= evening.end) {
return 'EVENING';
} else {
// 如果不在任何时间段内,根据最接近的时间段来判断
const distanceToMorning = Math.min(
Math.abs(startMinutes - morning.start),
Math.abs(startMinutes - morning.end)
);
const distanceToAfternoon = Math.min(
Math.abs(startMinutes - afternoon.start),
Math.abs(startMinutes - afternoon.end)
);
const distanceToEvening = Math.min(
Math.abs(startMinutes - evening.start),
Math.abs(startMinutes - evening.end)
);
const minDistance = Math.min(distanceToMorning, distanceToAfternoon, distanceToEvening);
if (minDistance === distanceToMorning) {
return 'AM';
} else if (minDistance === distanceToAfternoon) {
return 'PM';
} else {
return 'EVENING';
}
}
}
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;
// 如果任务有具体的星期,根据时间重新判断时段
if (currentEditingTask.weekday) {
currentEditingTask.slot = determineSlotByTime(start);
}
} else {
// 创建新任务 - 支持多星期选择
const location = window.tempTaskLocation;
const selectedWeekdays = getSelectedWeekdays();
// 根据用户选择的时间自动判断时段
const determinedSlot = determineSlotByTime(start);
if (selectedWeekdays.length > 0) {
// 选择了具体星期,为每个选中的星期创建任务
selectedWeekdays.forEach(weekday => {
const newTask = {
id: generateId(),
title,
start,
end,
note,
priority,
done: false,
doneAt: null,
weekday: weekday,
slot: determinedSlot,
order: Date.now()
};
tasks.push(newTask);
});
} else {
// 没有选择星期,根据位置信息创建任务
if (location && location.day) {
// 有明确位置信息,但时段根据时间自动判断
const newTask = {
id: generateId(),
title,
start,
end,
note,
priority,
done: false,
doneAt: null,
weekday: location.day,
slot: determinedSlot,
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();
}
}
let taskToDelete = null;
function deleteTask(taskId) {
taskToDelete = taskId;
document.getElementById('deleteConfirmModal').style.display = 'block';
}
function closeDeleteConfirmModal() {
document.getElementById('deleteConfirmModal').style.display = 'none';
taskToDelete = null;
}
function confirmDeleteTask() {
if (taskToDelete) {
tasks = tasks.filter(t => t.id !== taskToDelete);
renderTasks();
saveToLocalStorage();
showToast('任务已删除');
closeDeleteConfirmModal();
}
}
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() {
// 示例数据功能已移除
showToast('示例数据功能已移除');
}
// Excel导入导出功能
function exportToExcel() {
// 获取所有周的数据 - 修复数据获取逻辑
let allWeeksData = {};
try {
const saved = localStorage.getItem('allWeeklyTasks');
if (saved) {
allWeeksData = JSON.parse(saved);
}
} catch (error) {
console.error('解析多周数据失败:', error);
allWeeksData = {};
}
// 修复:确保当前周数据总是被包含
const currentWeekTasks = loadFromLocalStorage() || [];
const currentWeekKey = getWeekKey(new Date());
// 合并当前周数据到总数据中
if (currentWeekTasks.length > 0 || !allWeeksData[currentWeekKey]) {
allWeeksData[currentWeekKey] = currentWeekTasks;
}
// 扩展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];
let totalTasks = 0;
// 遍历所有周的数据
Object.keys(allWeeksData).sort().forEach(weekKey => {
const weekTasks = allWeeksData[weekKey];
if (!Array.isArray(weekTasks)) return;
// 过滤有效任务
const validTasks = weekTasks.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([
weekKey, // 周标识
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 || ''
]);
totalTasks++;
});
});
// 修复:总是允许导出,即使是空数据也提供模板
if (totalTasks === 0) {
// 添加空模板行以便导入测试
csvData.push([
currentWeekKey,
'周一',
'上午',
'示例任务',
'09:00',
'10:00',
'中',
'否',
'这是一个示例任务',
'单次',
'',
'sample_task_id'
]);
totalTasks = 1;
}
// 生成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');
// 创建并下载文件 - 添加UTF-8 BOM以确保Office兼容性
const blob = new Blob(['\uFEFF' + csvContent], {
type: 'text/csv;charset=utf-8;'
});
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
// 生成文件名
const now = new Date();
const dateStr = now.toISOString().split('T')[0];
const weekCount = Object.keys(allWeeksData).length;
link.download = `全部任务备份_${weekCount}周_${dateStr}.csv`;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showToast(`成功导出${totalTasks}个任务(${weekCount}周数据)到CSV文件`);
}
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) {
let text = e.target.result;
// 处理编码问题 - 自动检测和修复
try {
// 检测BOM标记
if (text.charCodeAt(0) === 0xFEFF || text.charCodeAt(0) === 0xFFFE) {
text = text.slice(1);
}
// 检测乱码并尝试修复
if (/[����]/.test(text) || /\x00/.test(text)) {
console.warn('检测到编码问题,尝试重新读取...');
// 这里可以添加GBK解码逻辑,但浏览器原生不支持
// 提示用户使用UTF-8编码的文件
showToast('文件编码可能有问题,请确保使用UTF-8编码保存CSV文件', 'warning');
}
} catch (encodingError) {
console.warn('编码处理失败:', encodingError);
}
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, '礼拜二': 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, 'pool': null
};
// 增强的时段映射
const SLOT_MAP = {
'上午': 'AM', '下午': 'PM', '晚上': 'EVENING', '任务池': 'POOL',
'AM': 'AM', 'PM': 'PM', 'EVENING': 'EVENING', 'POOL': 'POOL',
'am': 'AM', 'pm': 'PM', 'evening': 'EVENING',
'morning': 'AM', 'afternoon': 'PM', 'night': 'EVENING',
'早': 'AM', '午': 'PM', '晚': 'EVENING'
};
// 优先级映射
const PRIORITY_MAP = {
'高': 1, '中': 2, '低': 3,
'high': 1, 'medium': 2, 'low': 3,
'1': 1, '2': 2, '3': 3,
'紧急': 1, '一般': 2, '普通': 3
};
// 完成状态映射
const COMPLETION_MAP = {
'是': true, '否': false,
'true': true, 'false': false,
'1': true, '0': false,
'yes': true, 'no': false,
'已完成': true, '未完成': false,
'完成': true, '未完成': false
};
const REPEAT_TYPE_MAP = {
'单次': 'once', '每天': 'daily', '每周': 'weekly',
'once': 'once', 'daily': 'daily', 'weekly': 'weekly',
'一次': 'once', '每日': 'daily', '每周': 'weekly'
};
// 智能检测数据起始行
let dataStartRow = 1;
let headerFound = false;
for (let i = 0; i < Math.min(10, lines.length); i++) {
const line = lines[i].trim();
if (!line) continue;
const lowerLine = line.toLowerCase();
if (lowerLine.includes('星期') || lowerLine.includes('weekday') ||
lowerLine.includes('星期') || lowerLine.includes('标题')) {
dataStartRow = i + 1;
headerFound = true;
break;
}
}
// 处理数据行
for (let i = dataStartRow; i < lines.length; i++) {
const line = lines[i].trim();
if (!line || line === '' || line.match(/^,+$/)) 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, '').replace(/""/g, '"')
).filter(cell => cell !== '');
if (cleanCells.length < 3) {
console.warn(`跳过第 ${i + 1} 行: 数据列数不足`);
continue;
}
// 智能字段提取 - 支持灵活的列顺序,包括周标识
let weekIdentifier = '';
let weekdayText = '周一';
let slotText = '上午';
let title = '';
let start = '';
let end = '';
let priorityText = '中';
let completionText = '否';
let note = '';
// 智能字段解析 - 根据列数确定格式
if (cleanCells.length >= 9) {
// 9列格式:周标识,星期,时段,标题,开始,结束,优先级,完成,备注
weekIdentifier = cleanCells[0] || '';
weekdayText = cleanCells[1] || '周一';
slotText = cleanCells[2] || '上午';
title = cleanCells[3] || '';
start = cleanCells[4] || '';
end = cleanCells[5] || '';
priorityText = cleanCells[6] || '中';
completionText = cleanCells[7] || '否';
note = cleanCells[8] || '';
} else if (cleanCells.length >= 8) {
// 8列格式:星期,时段,标题,开始,结束,优先级,完成,备注
weekdayText = cleanCells[0] || '周一';
slotText = cleanCells[1] || '上午';
title = cleanCells[2] || '';
start = cleanCells[3] || '';
end = cleanCells[4] || '';
priorityText = cleanCells[5] || '中';
completionText = cleanCells[6] || '否';
note = cleanCells[7] || '';
} else if (cleanCells.length >= 7) {
// 7列格式:星期,时段,标题,开始,结束,优先级,完成
weekdayText = cleanCells[0] || '周一';
slotText = cleanCells[1] || '上午';
title = cleanCells[2] || '';
start = cleanCells[3] || '';
end = cleanCells[4] || '';
priorityText = cleanCells[5] || '中';
completionText = cleanCells[6] || '否';
} else if (cleanCells.length >= 6) {
// 6列格式:星期,时段,标题,开始,结束,优先级
weekdayText = cleanCells[0] || '周一';
slotText = cleanCells[1] || '上午';
title = cleanCells[2] || '';
start = cleanCells[3] || '';
end = cleanCells[4] || '';
priorityText = cleanCells[5] || '中';
} else if (cleanCells.length >= 5) {
// 5列格式:星期,时段,标题,开始,结束
weekdayText = cleanCells[0] || '周一';
slotText = cleanCells[1] || '上午';
title = cleanCells[2] || '';
start = cleanCells[3] || '';
end = cleanCells[4] || '';
} else if (cleanCells.length >= 3) {
// 3列最小格式:星期,时段,标题
weekdayText = cleanCells[0] || '周一';
slotText = cleanCells[1] || '上午';
title = cleanCells[2] || '';
} else {
console.warn(`跳过第 ${i + 1} 行: 列数不足 (${cleanCells.length} < 3)`);
continue;
}
// 修复:允许空标题的任务,跳过真正的无效数据
if (title === '标题' || title.toLowerCase() === 'title' || title === '任务标题') {
continue;
}
// 智能星期映射
let weekday = 1;
const weekdayKey = weekdayText.toString().trim();
if (WEEKDAY_MAP[weekdayKey] !== undefined) {
weekday = WEEKDAY_MAP[weekdayKey];
} else {
// 尝试数字转换
const numWeekday = parseInt(weekdayKey);
if (!isNaN(numWeekday) && numWeekday >= 1 && numWeekday <= 7) {
weekday = numWeekday;
} else {
console.warn(`无法识别的星期: ${weekdayKey}, 默认设为周一`);
}
}
// 智能时段映射
const slotKey = slotText.toString().trim();
const slot = SLOT_MAP[slotKey] || 'AM';
// 创建任务对象
const task = {
id: generateId(),
title: title.trim(),
start: start.trim(),
end: end.trim(),
priority: PRIORITY_MAP[priorityText.toString().toLowerCase()] || 2,
done: COMPLETION_MAP[completionText.toString().toLowerCase()] || false,
note: note.trim(),
weekday: weekday,
slot: slot,
order: Date.now() + i * 1000,
repeatType: 'once',
selectedWeekdays: [],
doneAt: null,
weekIdentifier: weekIdentifier.trim() // 添加周标识
};
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;">
<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;
}
// 保存导入数据到本地变量,防止被清空
const currentImportData = [...importedData];
try {
// 获取现有的多周数据
let allWeeklyTasks = {};
try {
allWeeklyTasks = JSON.parse(localStorage.getItem('allWeeklyTasks') || '{}');
} catch (e) {
console.warn('解析现有数据失败,使用空对象', e);
allWeeklyTasks = {};
}
// 按周标识分组导入的任务
const tasksByWeek = {};
let tasksWithoutWeekId = [];
// 处理导入的任务,确保数据完整性并去重
const processedTasks = [];
const duplicateCheck = new Set();
// 获取当前所有任务用于去重检查
let allExistingTasks = [];
Object.values(allWeeklyTasks).forEach(weekTasks => {
allExistingTasks = allExistingTasks.concat(weekTasks);
});
currentImportData.forEach(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,
weekIdentifier: task.weekIdentifier || ''
};
// 如果是重复任务且在任务池中,确保正确设置
if (processedTask.repeatType !== 'once' && (!processedTask.weekday || processedTask.slot === 'POOL')) {
processedTask.weekday = null;
processedTask.slot = 'POOL';
}
// 创建去重key:星期+时段+标题(去除空格和大小写)
const duplicateKey = `${processedTask.weekday}_${processedTask.slot}_${processedTask.title.trim().toLowerCase()}`;
// 检查是否已存在相同任务(包括导入中和现有任务)
const isExisting = allExistingTasks.some(existingTask =>
existingTask.weekday === processedTask.weekday &&
existingTask.slot === processedTask.slot &&
existingTask.title.trim().toLowerCase() === processedTask.title.trim().toLowerCase()
);
if (!duplicateCheck.has(duplicateKey) && !isExisting) {
duplicateCheck.add(duplicateKey);
processedTasks.push(processedTask);
} else {
console.log('跳过重复任务:', processedTask.title);
}
});
// 按周标识分组任务
processedTasks.forEach(task => {
if (task.weekIdentifier && task.weekIdentifier.trim()) {
const weekId = task.weekIdentifier.trim();
if (!tasksByWeek[weekId]) {
tasksByWeek[weekId] = [];
}
// 移除weekIdentifier属性,因为它只用于导入时分组
const { weekIdentifier, ...taskWithoutWeekId } = task;
tasksByWeek[weekId].push(taskWithoutWeekId);
} else {
// 没有周标识的任务,添加到当前周
const { weekIdentifier, ...taskWithoutWeekId } = task;
tasksWithoutWeekId.push(taskWithoutWeekId);
}
});
let totalNewTasks = 0;
let totalSkippedTasks = 0;
// 将按周分组的任务合并到对应周
Object.keys(tasksByWeek).forEach(weekId => {
const weekTasks = tasksByWeek[weekId];
const existingWeekTasks = allWeeklyTasks[weekId] || [];
const existingIds = new Set(existingWeekTasks.map(task => task.id));
const newWeekTasks = weekTasks.filter(task => !existingIds.has(task.id));
allWeeklyTasks[weekId] = [...existingWeekTasks, ...newWeekTasks];
totalNewTasks += newWeekTasks.length;
totalSkippedTasks += weekTasks.length - newWeekTasks.length;
});
// 处理没有周标识的任务,添加到当前周
if (tasksWithoutWeekId.length > 0) {
const currentWeekId = getWeekKey(currentDisplayDate);
const existingCurrentWeekTasks = allWeeklyTasks[currentWeekId] || [];
const existingIds = new Set(existingCurrentWeekTasks.map(task => task.id));
const newCurrentWeekTasks = tasksWithoutWeekId.filter(task => !existingIds.has(task.id));
allWeeklyTasks[currentWeekId] = [...existingCurrentWeekTasks, ...newCurrentWeekTasks];
totalNewTasks += newCurrentWeekTasks.length;
totalSkippedTasks += tasksWithoutWeekId.length - newCurrentWeekTasks.length;
}
// 保存多周数据
localStorage.setItem('allWeeklyTasks', JSON.stringify(allWeeklyTasks));
// 更新当前周的任务显示
const currentWeekId = getWeekKey(currentDisplayDate);
tasks = allWeeklyTasks[currentWeekId] || [];
saveToLocalStorage(tasks);
closeImportModal();
// 显示成功通知 - 确保使用正确的变量名
const totalImported = processedTasks.length;
const totalDuplicates = currentImportData.length - processedTasks.length;
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 class="icon icon-check" style="width: 24px; height: 24px; display: inline-block;"></span>
<div>
<div style="font-weight: 600;">导入成功!</div>
<div style="font-size: 14px; opacity: 0.9;">
${totalImported > 0 ? `成功导入 ${totalImported} 个任务` : '没有新任务需要导入'}
${totalDuplicates > 0 ? `<br>跳过 ${totalDuplicates} 个重复任务` : ''}
</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.message || '未知错误'), 'error');
return; // 防止继续执行
}
}
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 showMorning = true; // 上午时段必选
const showAfternoon = true; // 下午时段必选
const showEvening = localStorage.getItem('showEvening') !== 'false';
const showTimeRange = localStorage.getItem('showTimeRange') !== 'false';
// 加载周末显示设置
const hideSaturday = localStorage.getItem('hideSaturday') === 'true';
const hideSunday = localStorage.getItem('hideSunday') === 'true';
// 解析并设置上午时段
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];
}
// 设置显示/隐藏复选框 - 上午和下午强制选中
document.getElementById('showMorning').checked = true;
document.getElementById('showAfternoon').checked = true;
document.getElementById('showEvening').checked = showEvening;
document.getElementById('showTimeRange').checked = showTimeRange;
// 设置周末显示复选框
document.getElementById('hideSaturday').checked = hideSaturday;
document.getElementById('hideSunday').checked = hideSunday;
// 应用周末显示设置
toggleWeekendDisplay();
// 加载提醒系统设置
loadReminderSettings();
}
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 showMorning = localStorage.getItem('showMorning') !== 'false';
const showAfternoon = localStorage.getItem('showAfternoon') !== 'false';
const showEvening = localStorage.getItem('showEvening') !== 'false';
const showTimeRange = localStorage.getItem('showTimeRange') !== 'false';
const timeLabels = document.querySelectorAll('.time-label');
const weeklyGrid = document.querySelector('.weekly-grid');
// 更新上午时段
if (timeLabels[0]) {
timeLabels[0].style.display = showMorning ? '' : 'none';
const desktopText = timeLabels[0].querySelector('.desktop-text');
if (desktopText) {
desktopText.innerHTML = showTimeRange ? `上午<br>${morningRange}` : '上午';
}
// 隐藏对应的任务单元格
const morningCells = document.querySelectorAll('[data-slot="AM"]');
morningCells.forEach(cell => {
cell.style.display = showMorning ? '' : 'none';
});
}
// 更新下午时段
if (timeLabels[1]) {
timeLabels[1].style.display = showAfternoon ? '' : 'none';
const desktopText = timeLabels[1].querySelector('.desktop-text');
if (desktopText) {
desktopText.innerHTML = showTimeRange ? `下午<br>${afternoonRange}` : '下午';
}
// 隐藏对应的任务单元格
const afternoonCells = document.querySelectorAll('[data-slot="PM"]');
afternoonCells.forEach(cell => {
cell.style.display = showAfternoon ? '' : 'none';
});
}
// 更新晚上时段
if (timeLabels[2]) {
if (window.innerWidth <= 480) {
// 手机端:使用强制的display:none确保兼容性
// 晚上列相关元素:表头data-day="3" + 所有data-slot="EVENING"的任务单元格
const eveningColumn = [
document.querySelector('.day-header[data-day="3"]'),
...document.querySelectorAll('[data-slot="EVENING"]')
];
eveningColumn.forEach(element => {
if (element) {
if (showEvening) {
element.classList.remove('mobile-hidden');
element.style.display = '';
} else {
element.classList.add('mobile-hidden');
element.style.display = 'none !important';
}
}
});
} else {
// 桌面端:保持原有逻辑
timeLabels[2].style.display = showEvening ? '' : 'none';
const eveningCells = document.querySelectorAll('[data-slot="EVENING"]');
eveningCells.forEach(cell => {
cell.style.display = showEvening ? '' : 'none';
});
}
const desktopText = timeLabels[2].querySelector('.desktop-text');
if (desktopText) {
desktopText.innerHTML = showTimeRange ? `晚上<br>${eveningRange}` : '晚上';
}
}
// 动态调整网格模板(桌面端和手机端)
if (window.innerWidth <= 480) {
// 手机端:添加CSS类控制网格布局
const weeklyGrid = document.querySelector('.weekly-grid');
if (weeklyGrid) {
weeklyGrid.classList.remove('evening-hidden', 'evening-visible');
weeklyGrid.classList.add(showEvening ? 'evening-visible' : 'evening-hidden');
// 强制重新渲染网格布局
weeklyGrid.style.display = 'none';
weeklyGrid.offsetHeight; // 触发重排
weeklyGrid.style.display = 'grid';
}
// 手机端:重新调用toggleWeekendDisplay来更新完整的网格布局
toggleWeekendDisplay();
} else {
// 桌面端:只调整行模板
if (showEvening) {
// 显示晚上时段:标题行 + 三个时段行
weeklyGrid.style.gridTemplateRows = 'auto 1fr 1fr 1fr';
} else {
// 隐藏晚上时段:标题行 + 两个时段行(平分空间)
weeklyGrid.style.gridTemplateRows = 'auto 1fr 1fr';
}
}
// 更新手机端时间范围显示
updateMobileTimeRanges(morningRange, afternoonRange, eveningRange, showTimeRange);
}
function updateMobileTimeRanges(morningRange, afternoonRange, eveningRange, showTimeRange) {
// 更新手机端时间范围显示
const mobileTimeRangeAM = document.getElementById('mobileTimeRangeAM');
const mobileTimeRangePM = document.getElementById('mobileTimeRangePM');
const mobileTimeRangeEVENING = document.getElementById('mobileTimeRangeEVENING');
if (mobileTimeRangeAM) {
mobileTimeRangeAM.textContent = morningRange;
mobileTimeRangeAM.style.display = showTimeRange ? 'block' : 'none';
}
if (mobileTimeRangePM) {
mobileTimeRangePM.textContent = afternoonRange;
mobileTimeRangePM.style.display = showTimeRange ? 'block' : 'none';
}
if (mobileTimeRangeEVENING) {
mobileTimeRangeEVENING.textContent = eveningRange;
mobileTimeRangeEVENING.style.display = showTimeRange ? 'block' : 'none';
}
}
function repositionMobileTimeLabels(hideSaturday, hideSunday) {
// 在新的移动端布局中,时间标签已经通过CSS正确定位
// 这个函数现在主要用于处理隐藏逻辑,CSS已经处理了基本定位
// 移动端布局:8行(标题+周一到周日)x 4列(时间+上午+下午+晚上)
}
function repositionMobileElements(hideSaturday, hideSunday, showEvening) {
// 只在手机端执行
if (window.innerWidth > 480) return;
// 创建可见天数的映射
const dayMapping = [];
for (let day = 1; day <= 7; day++) {
if (day === 6 && hideSaturday) continue;
if (day === 7 && hideSunday) continue;
dayMapping.push(day);
}
// 重新定位可见元素的网格位置
dayMapping.forEach((day, index) => {
const actualRow = index + 2; // +2 因为第一行是表头
// 设置时间标签位置(只对没有hidden类的元素设置)
const timeLabel = document.querySelector(`.time-label[data-mobile-day="${day}"]`);
if (timeLabel && !timeLabel.classList.contains('hidden')) {
timeLabel.style.gridRow = `${actualRow}`;
}
// 设置任务单元格位置(只对没有hidden类的元素设置)
const amCell = document.querySelector(`.task-cell[data-day="${day}"][data-slot="AM"]`);
if (amCell && !amCell.classList.contains('hidden')) {
amCell.style.gridColumn = '2';
amCell.style.gridRow = `${actualRow}`;
}
const pmCell = document.querySelector(`.task-cell[data-day="${day}"][data-slot="PM"]`);
if (pmCell && !pmCell.classList.contains('hidden')) {
pmCell.style.gridColumn = '3';
pmCell.style.gridRow = `${actualRow}`;
}
const eveningCell = document.querySelector(`.task-cell[data-day="${day}"][data-slot="EVENING"]`);
if (eveningCell && !eveningCell.classList.contains('hidden')) {
if (showEvening) {
eveningCell.style.gridColumn = '4';
eveningCell.style.gridRow = `${actualRow}`;
eveningCell.style.display = '';
} else {
// 当晚上时段隐藏时,确保晚上单元格不显示
eveningCell.style.display = 'none';
}
}
});
}
function toggleWeekendDisplay() {
const hideSaturday = localStorage.getItem('hideSaturday') === 'true';
const hideSunday = localStorage.getItem('hideSunday') === 'true';
const weeklyGrid = document.querySelector('.weekly-grid');
if (window.innerWidth <= 480) {
// 手机端:参考PC端逻辑,使用统一的hidden类
// 周六相关元素:任务单元格和手机端时间标签
const saturdayElements = [
...document.querySelectorAll('.task-cell[data-day="6"]'),
...document.querySelectorAll('.time-label[data-mobile-day="6"]')
];
// 周日相关元素:任务单元格和手机端时间标签
const sundayElements = [
...document.querySelectorAll('.task-cell[data-day="7"]'),
...document.querySelectorAll('.time-label[data-mobile-day="7"]')
];
// 控制周六的显示/隐藏
saturdayElements.forEach(element => {
if (hideSaturday) {
element.classList.add('hidden');
} else {
element.classList.remove('hidden');
}
});
// 控制周日的显示/隐藏
sundayElements.forEach(element => {
if (hideSunday) {
element.classList.add('hidden');
} else {
element.classList.remove('hidden');
}
});
} else {
// 桌面端:保持原有逻辑
const saturdayElements = document.querySelectorAll('[data-day="6"]');
const sundayElements = document.querySelectorAll('[data-day="7"]');
saturdayElements.forEach(element => {
if (hideSaturday) {
element.classList.add('hidden');
} else {
element.classList.remove('hidden');
}
});
sundayElements.forEach(element => {
if (hideSunday) {
element.classList.add('hidden');
} else {
element.classList.remove('hidden');
}
});
}
// 动态调整网格模板
if (window.innerWidth <= 480) {
// 手机端:调整行数和列数
let visibleDays = 7;
if (hideSaturday) visibleDays--;
if (hideSunday) visibleDays--;
// 检查晚上时段是否隐藏(从localStorage读取)
const showEvening = localStorage.getItem('showEvening') !== 'false';
let visibleTimeSlots = showEvening ? 3 : 2; // 上午、下午、晚上
// 添加CSS类来控制网格布局 - 增强浏览器兼容性
weeklyGrid.classList.remove('evening-hidden', 'evening-visible');
weeklyGrid.classList.add(showEvening ? 'evening-visible' : 'evening-hidden');
// 手机端网格:动态列数和行数
const columnTemplate = showEvening ? '45px 1fr 1fr 1fr' : '45px 1fr 1fr';
weeklyGrid.style.gridTemplateColumns = columnTemplate;
weeklyGrid.style.gridTemplateRows = `auto repeat(${visibleDays}, 1fr)`;
// 强制重新渲染网格布局
weeklyGrid.style.display = 'none';
weeklyGrid.offsetHeight; // 触发重排
weeklyGrid.style.display = 'grid';
console.log('手机端网格调整:', {
showEvening,
columnTemplate,
visibleDays,
gridClass: showEvening ? 'evening-visible' : 'evening-hidden'
});
// 动态设置元素的网格位置
repositionMobileElements(hideSaturday, hideSunday, showEvening);
// 确保网格高度适应内容
const headerHeight = 160;
const availableHeight = window.innerHeight - headerHeight;
weeklyGrid.style.height = `${Math.max(400, availableHeight)}px`;
weeklyGrid.style.maxHeight = `${availableHeight}px`;
} else {
// 桌面端:调整列数
let visibleDays = 7;
if (hideSaturday) visibleDays--;
if (hideSunday) visibleDays--;
// 根据屏幕尺寸设置不同的第一列宽度
let firstColumnWidth = '150px';
if (window.innerWidth <= 768) {
firstColumnWidth = '80px';
}
// 设置新的网格模板
weeklyGrid.style.gridTemplateColumns = `${firstColumnWidth} repeat(${visibleDays}, 1fr)`;
}
}
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);
// 保存显示/隐藏设置 - 上午和下午强制为true
localStorage.setItem('showMorning', true);
localStorage.setItem('showAfternoon', true);
localStorage.setItem('showEvening', document.getElementById('showEvening').checked);
localStorage.setItem('showTimeRange', document.getElementById('showTimeRange').checked);
// 保存周末显示设置
localStorage.setItem('hideSaturday', document.getElementById('hideSaturday').checked);
localStorage.setItem('hideSunday', document.getElementById('hideSunday').checked);
// 应用周末显示设置
toggleWeekendDisplay();
// 更新时段标签显示和时段可见性
updateTimeLabels();
renderTasks();
// 保存提醒系统设置
saveReminderSettings();
showToast('设置已保存');
closeSettingsModal();
}
// 标题编辑功能
function editTitle() {
const titleElement = document.getElementById('tableTitle');
const inputElement = document.getElementById('tableTitleInput');
titleElement.style.display = 'none';
inputElement.style.display = 'inline-block';
inputElement.value = titleElement.textContent;
inputElement.focus();
inputElement.select();
}
function saveTitle() {
const titleElement = document.getElementById('tableTitle');
const inputElement = document.getElementById('tableTitleInput');
const newTitle = inputElement.value.trim();
if (newTitle) {
titleElement.textContent = newTitle;
// 保存到localStorage
localStorage.setItem('tableTitle', newTitle);
}
titleElement.style.display = 'inline-block';
inputElement.style.display = 'none';
}
function handleTitleKeypress(event) {
if (event.key === 'Enter') {
saveTitle();
} else if (event.key === 'Escape') {
const titleElement = document.getElementById('tableTitle');
const inputElement = document.getElementById('tableTitleInput');
titleElement.style.display = 'inline-block';
inputElement.style.display = 'none';
}
}
function loadTableTitle() {
const savedTitle = localStorage.getItem('tableTitle');
if (savedTitle) {
document.getElementById('tableTitle').textContent = savedTitle;
}
}
// 键盘快捷键
// 移除不存在的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);
});
// 窗口大小变化时重新调整布局
window.addEventListener('resize', () => {
toggleWeekendDisplay();
});
// 初始化
document.addEventListener('DOMContentLoaded', () => {
updateDateDisplay();
loadWeekTasks(); // 使用新的多周数据加载函数
loadSettings();
loadTableTitle(); // 加载保存的标题
// 初始化手机端网格布局CSS类
if (window.innerWidth <= 480) {
const weeklyGrid = document.querySelector('.weekly-grid');
const showEvening = localStorage.getItem('showEvening') !== 'false';
if (weeklyGrid) {
weeklyGrid.classList.remove('evening-hidden', 'evening-visible');
weeklyGrid.classList.add(showEvening ? 'evening-visible' : 'evening-hidden');
}
}
toggleWeekendDisplay(); // 应用周末显示设置
updateTimeLabels();
renderTasks(); // 添加任务渲染,确保页面加载时显示任务
// 初始化任务提醒系统
initSpeechSynthesis();
initPageVisibility(); // 初始化页面可见性API
requestNotificationPermission();
// 注册Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('Service Worker注册成功:', registration);
})
.catch(error => {
console.log('Service Worker注册失败:', error);
});
}
// 添加提醒模态框复选框的事件监听器
const reminderVoiceCheckbox = document.getElementById('reminderVoiceCheckbox');
const reminderPopupCheckbox = document.getElementById('reminderPopupCheckbox');
if (reminderVoiceCheckbox) {
reminderVoiceCheckbox.addEventListener('change', function() {
reminderSystem.voiceEnabled = this.checked;
localStorage.setItem('enableVoiceReminders', this.checked);
});
}
if (reminderPopupCheckbox) {
reminderPopupCheckbox.addEventListener('change', function() {
reminderSystem.popupEnabled = this.checked;
localStorage.setItem('enablePopupReminders', this.checked);
});
}
// 初始化提醒系统设置
loadReminderSettings();
// 添加用户交互监听器以启用语音播报(绕过自动播放限制)
let userInteracted = false;
const enableVoiceOnInteraction = () => {
if (!userInteracted) {
userInteracted = true;
console.log('用户已交互,语音播报已启用');
// 移除监听器
document.removeEventListener('click', enableVoiceOnInteraction);
document.removeEventListener('keydown', enableVoiceOnInteraction);
document.removeEventListener('touchstart', enableVoiceOnInteraction);
}
};
document.addEventListener('click', enableVoiceOnInteraction);
document.addEventListener('keydown', enableVoiceOnInteraction);
document.addEventListener('touchstart', enableVoiceOnInteraction);
// 启动提醒检查定时器(每10秒检查一次,提高精度)
setInterval(checkTaskReminders, 10000);
// 每分钟更新日期显示
setInterval(updateDateDisplay, 60000);
// 延迟执行初始检查,确保所有组件都已初始化
setTimeout(() => {
console.log('=== 系统初始化完成 ===');
console.log('页面加载完成,任务数量:', tasks.length);
console.log('任务列表:', tasks);
console.log('提醒系统状态:', {
enabled: reminderSystem.enabled,
voice: reminderSystem.voiceEnabled,
popup: reminderSystem.popupEnabled,
background: reminderSystem.backgroundEnabled
});
// 立即执行一次提醒检查,确保不遗漏当前时间的提醒
if (reminderSystem.enabled) {
console.log('执行初始提醒检查');
checkTaskReminders();
}
console.log('任务提醒系统已启动(检查频率:每10秒)');
}, 500); // 减少延迟到500ms,更快响应
});
// 导出功能相关函数
let selectedExportDay = null;
// 切换导出下拉菜单
function toggleExportDropdown() {
const dropdown = document.getElementById('exportDropdown');
const toggle = document.querySelector('.export-dropdown .dropdown-toggle');
if (dropdown.classList.contains('show')) {
dropdown.classList.remove('show');
toggle.classList.remove('active');
} else {
dropdown.classList.add('show');
toggle.classList.add('active');
}
}
function toggleBackupDropdown() {
const dropdown = document.getElementById('backupDropdown');
const toggle = document.querySelector('.backup-dropdown .dropdown-toggle');
if (dropdown.classList.contains('show')) {
dropdown.classList.remove('show');
toggle.classList.remove('active');
} else {
dropdown.classList.add('show');
toggle.classList.add('active');
}
}
// 显示日报导出模态框
function showDailyExportModal() {
const modal = document.getElementById('dailyExportModal');
modal.style.display = 'block';
// 清除之前的选择
modal.querySelectorAll('.export-day-btn').forEach(btn => {
btn.classList.remove('selected');
});
// 自动选择当天对应的星期几
const today = new Date();
const currentDay = today.getDay(); // 0是周日,1-6是周一到周六
const dayToSelect = currentDay === 0 ? 7 : currentDay; // 转换为1-7的格式
// 自动选择当天
selectedExportDay = dayToSelect;
const selectedBtn = modal.querySelector(`.export-day-btn[data-day="${dayToSelect}"]`);
if (selectedBtn) {
selectedBtn.classList.add('selected');
}
// 启用导出按钮
document.getElementById('exportDailyBtn').disabled = false;
// 关闭下拉菜单
document.getElementById('exportDropdown').classList.remove('show');
document.querySelector('.dropdown-toggle').classList.remove('active');
}
// 关闭日报导出模态框
function closeDailyExportModal() {
document.getElementById('dailyExportModal').style.display = 'none';
selectedExportDay = null;
}
// 选择导出日期
function selectExportDay(day) {
selectedExportDay = day;
// 更新按钮状态
const modal = document.getElementById('dailyExportModal');
modal.querySelectorAll('.export-day-btn').forEach(btn => {
btn.classList.remove('selected');
});
const selectedBtn = modal.querySelector(`.export-day-btn[data-day="${day}"]`);
if (selectedBtn) {
selectedBtn.classList.add('selected');
}
// 启用导出按钮
document.getElementById('exportDailyBtn').disabled = false;
}
// 导出日报
function exportDailyReport() {
if (!selectedExportDay) {
showToast('请先选择要导出的日期', 'error');
return;
}
// 获取当前周的任务数据
const tasks = loadWeekTasks() || [];
const dayNames = ['', '周一', '周二', '周三', '周四', '周五', '周六', '周日'];
const slotNames = { 'AM': '上午', 'PM': '下午', 'EVENING': '晚上' };
const priorityNames = { 1: '高', 2: '中', 3: '低' };
// 筛选选定日期的任务
const dayTasks = tasks.filter(task => task.weekday === selectedExportDay);
if (dayTasks.length === 0) {
showToast(`${dayNames[selectedExportDay]}没有任务数据`, 'error');
return;
}
// 创建工作簿
const wb = XLSX.utils.book_new();
// 准备数据
const wsData = [
['时段', '任务标题', '开始时间', '结束时间', '优先级', '完成状态', '备注']
];
// 按时段分组并排序
const timeSlots = ['AM', 'PM', 'EVENING'];
timeSlots.forEach(slot => {
const slotTasks = dayTasks.filter(task => task.slot === slot)
.sort((a, b) => (a.start || '').localeCompare(b.start || ''));
if (slotTasks.length > 0) {
slotTasks.forEach(task => {
wsData.push([
slotNames[task.slot] || task.slot,
task.title || '',
task.start || '',
task.end || '',
priorityNames[task.priority] || '中',
task.done ? '已完成' : '未完成',
task.note || ''
]);
});
}
});
// 创建工作表
const ws = XLSX.utils.aoa_to_sheet(wsData);
// 设置列宽
ws['!cols'] = [
{ wch: 10 }, // 时段
{ wch: 30 }, // 任务标题
{ wch: 12 }, // 开始时间
{ wch: 12 }, // 结束时间
{ wch: 10 }, // 优先级
{ wch: 12 }, // 完成状态
{ wch: 25 } // 备注
];
// 添加工作表到工作簿
XLSX.utils.book_append_sheet(wb, ws, `${dayNames[selectedExportDay]}日报`);
// 生成文件名
const today = new Date();
const fileName = `${dayNames[selectedExportDay]}日报_${today.getFullYear()}年${(today.getMonth()+1).toString().padStart(2,'0')}月${today.getDate().toString().padStart(2,'0')}日.xlsx`;
// 导出文件
XLSX.writeFile(wb, fileName);
showToast(`成功导出${dayNames[selectedExportDay]}日报,共${dayTasks.length}个任务`);
closeDailyExportModal();
}
// 导出周报
function exportWeeklyReport() {
// 获取当前周的任务数据
const tasks = loadWeekTasks() || [];
const dayNames = ['', '周一', '周二', '周三', '周四', '周五', '周六', '周日'];
const slotNames = { 'AM': '上午', 'PM': '下午', 'EVENING': '晚上' };
const priorityNames = { 1: '高', 2: '中', 3: '低' };
// 筛选有效任务(排除任务池)
const weekTasks = tasks.filter(task => task.weekday && task.weekday >= 1 && task.weekday <= 7);
if (weekTasks.length === 0) {
showToast('本周没有任务数据', 'error');
return;
}
// 创建工作簿
const wb = XLSX.utils.book_new();
// 准备数据
const wsData = [
['星期', '时段', '任务标题', '开始时间', '结束时间', '优先级', '完成状态', '备注']
];
// 按星期和时段排序
const sortedTasks = weekTasks.sort((a, b) => {
if (a.weekday !== b.weekday) return a.weekday - b.weekday;
const slotOrder = { 'AM': 1, 'PM': 2, 'EVENING': 3 };
if (a.slot !== b.slot) return (slotOrder[a.slot] || 4) - (slotOrder[b.slot] || 4);
return (a.start || '').localeCompare(b.start || '');
});
sortedTasks.forEach(task => {
wsData.push([
dayNames[task.weekday] || '',
slotNames[task.slot] || task.slot,
task.title || '',
task.start || '',
task.end || '',
priorityNames[task.priority] || '中',
task.done ? '已完成' : '未完成',
task.note || ''
]);
});
// 创建工作表
const ws = XLSX.utils.aoa_to_sheet(wsData);
// 设置列宽
ws['!cols'] = [
{ wch: 10 }, // 星期
{ wch: 10 }, // 时段
{ wch: 30 }, // 任务标题
{ wch: 12 }, // 开始时间
{ wch: 12 }, // 结束时间
{ wch: 10 }, // 优先级
{ wch: 12 }, // 完成状态
{ wch: 25 } // 备注
];
// 添加工作表到工作簿
XLSX.utils.book_append_sheet(wb, ws, '周报');
// 生成文件名
const today = new Date();
const fileName = `周报_${today.getFullYear()}年${(today.getMonth()+1).toString().padStart(2,'0')}月${today.getDate().toString().padStart(2,'0')}日.xlsx`;
// 导出文件
XLSX.writeFile(wb, fileName);
showToast(`成功导出周报,共${weekTasks.length}个任务`);
// 关闭下拉菜单
document.getElementById('exportDropdown').classList.remove('show');
document.querySelector('.dropdown-toggle').classList.remove('active');
}
// 点击外部关闭下拉菜单
document.addEventListener('click', function(event) {
// 关闭导出下拉菜单
const exportDropdown = document.querySelector('.export-dropdown');
if (exportDropdown && !exportDropdown.contains(event.target)) {
document.getElementById('exportDropdown').classList.remove('show');
document.querySelector('.export-dropdown .dropdown-toggle').classList.remove('active');
}
// 关闭数据备份下拉菜单
const backupDropdown = document.querySelector('.backup-dropdown');
if (backupDropdown && !backupDropdown.contains(event.target)) {
document.getElementById('backupDropdown').classList.remove('show');
document.querySelector('.backup-dropdown .dropdown-toggle').classList.remove('active');
}
});
// 点击模态框外部关闭
window.onclick = function(event) {
const modals = ['taskModal', 'importModal', 'settingsModal', 'dailyExportModal', 'deleteConfirmModal', 'reminderModal'];
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();
} else if (modalId === 'dailyExportModal') {
closeDailyExportModal();
} else if (modalId === 'deleteConfirmModal') {
closeDeleteConfirmModal();
} else if (modalId === 'reminderModal') {
closeReminderModal();
}
}
});
};
// 监听窗口大小变化,实时调整布局
window.addEventListener('resize', function() {
toggleWeekendDisplay();
});
// 页面加载完成后初始化网格布局
window.addEventListener('load', function() {
// 确保手机端网格布局正确初始化
if (window.innerWidth <= 480) {
toggleWeekendDisplay();
}
});
// 关闭提醒模态框
function closeReminderModal() {
document.getElementById('reminderModal').style.display = 'none';
reminderSystem.currentReminder = null;
// 清除自动确认定时器
if (reminderSystem.autoConfirmTimeout) {
clearTimeout(reminderSystem.autoConfirmTimeout);
reminderSystem.autoConfirmTimeout = null;
}
}
// 测试开始提醒
function testStartReminder() {
console.log('执行开始提醒测试');
const testTask = {
id: 'test-start-' + Date.now(),
title: '测试任务(开始提醒)',
start: '09:00',
end: '10:00',
note: '这是一个测试任务,用于测试开始提醒功能',
weekday: 1,
slot: 'AM',
done: false
};
showTaskReminder(testTask, 'start');
}
// 测试结束提醒
function testEndReminder() {
console.log('执行结束提醒测试');
const testTask = {
id: 'test-end-' + Date.now(),
title: '测试任务(结束提醒)',
start: '09:00',
end: '10:00',
note: '这是一个测试任务,用于测试结束提醒功能',
weekday: 1,
slot: 'AM',
done: false
};
showTaskReminder(testTask, 'end');
}
// 测试语音播报
function testVoiceOnly() {
console.log('=== 开始测试语音播报 ===');
console.log('浏览器语音合成支持:', 'speechSynthesis' in window);
console.log('提醒系统语音设置:', reminderSystem.voiceEnabled);
console.log('语音合成对象:', reminderSystem.speechSynthesis);
if (!('speechSynthesis' in window)) {
alert('您的浏览器不支持语音合成功能!');
return;
}
if (!reminderSystem.voiceEnabled) {
alert('语音提醒已禁用,请在设置中启用语音提醒!');
return;
}
// 直接调用语音播报
speakMessage('这是一个语音播报测试,如果您能听到这段话,说明语音功能正常工作', true);
// 显示提示
showToast('正在测试语音播报,请检查是否有声音', 'info');
}
// 调试函数:显示提醒系统状态
function debugReminderSystem() {
console.log('=== 提醒系统状态 ===');
console.log('系统启用:', reminderSystem.enabled);
console.log('语音启用:', reminderSystem.voiceEnabled);
console.log('弹窗启用:', reminderSystem.popupEnabled);
console.log('后台通知启用:', reminderSystem.backgroundEnabled);
console.log('通知权限:', reminderSystem.notificationPermission);
console.log('当前提醒:', reminderSystem.currentReminder);
console.log('提醒历史记录数量:', reminderSystem.reminderHistory.size);
console.log('上次检查时间:', reminderSystem.lastCheckTime);
console.log('活跃超时数量:', reminderSystem.reminderTimeouts.size);
// 显示今天的任务
const now = new Date();
const currentDay = now.getDay() || 7;
const todayTasks = tasks.filter(task => task.weekday === currentDay && !task.done);
console.log(`今天(星期${currentDay})的未完成任务:`, todayTasks);
return {
system: reminderSystem,
todayTasks: todayTasks
};
}
// 手动触发提醒检查(用于调试)
function manualCheckReminders() {
console.log('手动触发提醒检查');
checkTaskReminders();
}
// 页面标题闪烁提醒(后台提醒的备用方案)
function flashPageTitle(task, type) {
const originalTitle = document.title;
const alertTitle = type === 'start'
? `🔔 ${task.title} 开始执行!`
: `⏰ ${task.title} 时间到!`;
let isFlashing = true;
let flashCount = 0;
const maxFlashes = 10; // 最多闪烁10次
const flashInterval = setInterval(() => {
if (!isFlashing || flashCount >= maxFlashes) {
document.title = originalTitle;
clearInterval(flashInterval);
return;
}
document.title = flashCount % 2 === 0 ? alertTitle : originalTitle;
flashCount++;
}, 1000);
// 当页面变为可见时停止闪烁
const stopFlashing = (isVisible) => {
if (isVisible) {
isFlashing = false;
document.title = originalTitle;
clearInterval(flashInterval);
// 移除监听器
const index = reminderSystem.visibilityChangeHandlers.indexOf(stopFlashing);
if (index > -1) {
reminderSystem.visibilityChangeHandlers.splice(index, 1);
}
// 页面变为可见时显示弹窗
if (reminderSystem.popupEnabled) {
showPopupReminder(task, type);
}
}
};
reminderSystem.visibilityChangeHandlers.push(stopFlashing);
console.log('开始标题闪烁提醒');
}
// 标记任务完成
function markTaskComplete(taskId) {
const task = tasks.find(t => t.id === taskId);
if (task) {
task.done = true;
saveTasks();
renderTasks();
console.log(`任务 ${task.title} 已标记为完成`);
showToast('任务已标记为完成', 'success');
}
}
</script>
</body>
</html>