v2.0.0 — 日打卡彻底改为周打卡

This commit is contained in:
mac
2026-07-24 16:20:40 +08:00
parent 350c9fb877
commit 5b637e7fed
5 changed files with 286 additions and 336 deletions

View File

@@ -5,13 +5,13 @@
/* ================================================================
State
================================================================ */
var calendarStatus = {}; // { "2026-06-01": "pass", ... }
var calYear, calMonth; // 日历显示的月份
var activePanel = 'daily'; // 当前面板
var weekOffset = 0; // 每周评分偏移
var lastSavedData = null; // 上次保存快照
var lastSavedDate = null; // 上次保存日期
var selectedDate = null; // 日历中选中日期
var calendarStatus = {}; // { "2026-W30": "pass", ... }
var calYear; // 日历显示的
var activePanel = 'daily';
var weekOffset = 0;
var lastSavedData = null;
var lastSavedWeek = null;
var selectedWeek = null;
/* 勤学预设项目 */
var PRESET_STUDY_ITEMS = [
@@ -51,18 +51,18 @@
/* ================================================================
API
================================================================ */
function apiGetCheckin(date, cb) {
fetch('/api/checkin?date=' + encodeURIComponent(date))
function apiGetCheckin(week, cb) {
fetch('/api/checkin?week=' + encodeURIComponent(week))
.then(function(r){ return r.json(); })
.then(function(res){ if (res.ok) cb(null, res.data); else cb(res.error); })
.catch(function(e){ cb(e.message); });
}
function apiSaveCheckin(date, data, cb) {
function apiSaveCheckin(week, data, cb) {
fetch('/api/checkin', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({date: date, data: data})
body: JSON.stringify({week: week, data: data})
})
.then(function(r){ return r.json(); })
.then(function(res){ if (res.ok) cb(null); else cb(res.error); })
@@ -131,91 +131,69 @@
return y + '-' + m + '-' + dd;
}
/** 判断两个日期是否同一天 */
function isSameDay(a, b) {
return a.getFullYear() === b.getFullYear() &&
a.getMonth() === b.getMonth() &&
a.getDate() === b.getDate();
/** 获取当前周的 YYYY-WW 格式 */
function fmtWeek(d) {
var jan1 = new Date(d.getFullYear(), 0, 1);
var dayOfYear = Math.floor((d - jan1) / 86400000);
var week = Math.ceil((dayOfYear + jan1.getDay() + 1) / 7);
return d.getFullYear() + '-W' + ('0' + week).slice(-2);
}
/** 切换日历月份 */
window.changeCalMonth = function(dir) {
calMonth += dir;
if (calMonth > 12) { calMonth = 1; calYear++; }
if (calMonth < 1) { calMonth = 12; calYear--; }
/** 切换日历 */
window.changeCalYear = function(dir) {
calYear += dir;
renderCalendar();
};
/** 渲染历 */
/** 渲染历 */
function renderCalendar() {
var today = new Date();
var firstDay = new Date(calYear, calMonth - 1, 1);
var lastDay = new Date(calYear, calMonth, 0);
var startDayOfWeek = firstDay.getDay(); // 0=Sun
var totalDays = lastDay.getDate();
var prevMonthLastDay = new Date(calYear, calMonth - 1, 0).getDate();
// 月份标签
document.getElementById('cal-month-label').textContent =
calYear + '年 ' + calMonth + '月';
var grid = document.getElementById('cal-grid');
document.getElementById('cal-year-label').textContent = calYear + '年';
var yearStart = new Date(calYear, 0, 1);
var yearEnd = new Date(calYear, 11, 31);
var weeks = [];
var d = new Date(yearStart);
// 推进到第一个周一
d.setDate(d.getDate() - d.getDay() + (d.getDay() === 0 ? -6 : 1));
while (d <= yearEnd) {
var wk = fmtWeek(d);
weeks.push(wk);
d.setDate(d.getDate() + 7);
}
var todayWk = fmtWeek(new Date());
var el = document.getElementById('cal-week-list');
var html = '';
// 前月填充
for (var p = startDayOfWeek - 1; p >= 0; p--) {
html += '<div class="cal-cell other-month">' + (prevMonthLastDay - p) + '</div>';
for (var i = 0; i < weeks.length; i++) {
var w = weeks[i];
var cls = 'cal-week-cell';
var status = calendarStatus[w];
if (status) cls += ' ' + status;
if (w === todayWk) cls += ' today';
if (w === selectedWeek) cls += ' selected';
html += '<div class="' + cls + '" data-week="' + w + '" title="' + w + '">' +
'W' + w.split('-W')[1] + '</div>';
}
// 当月
for (var d = 1; d <= totalDays; d++) {
var cur = new Date(calYear, calMonth - 1, d);
var ds = fmtDate(cur);
var classes = ['cal-cell'];
var isOtherMonth = false;
if (isSameDay(cur, today)) classes.push('today');
if (ds === selectedDate) classes.push('selected');
var status = calendarStatus[ds];
if (status) classes.push(status);
html += '<div class="' + classes.join(' ') + '" data-date="' + ds + '">' + d + '</div>';
}
// 后月填充 — 始终 42 格6行 × 7列保持高度一致
var totalCells = startDayOfWeek + totalDays;
var remaining = 42 - totalCells;
for (var n = 1; n <= remaining; n++) {
html += '<div class="cal-cell other-month">' + n + '</div>';
}
grid.innerHTML = html;
// 点击事件
grid.querySelectorAll('.cal-cell:not(.other-month)').forEach(function(cell) {
el.innerHTML = html;
el.querySelectorAll('.cal-week-cell').forEach(function(cell) {
cell.addEventListener('click', function() {
var date = this.dataset.date;
if (date) navigateToDate(date);
var wk = this.dataset.week;
if (wk) navigateToWeek(wk);
});
});
}
/** 导航到指定日期 */
function navigateToDate(date) {
selectedDate = date;
function navigateToWeek(week) {
selectedWeek = week;
switchPanel('daily');
document.getElementById('check-date').value = date;
document.getElementById('check-date').value = week;
loadCheckin();
renderCalendar(); // 刷新日历高亮
renderCalendar();
}
/** 回到今天 */
window.goToday = function() {
var today = fmtDate(new Date());
window.goToCurrentWeek = function() {
var now = fmtWeek(new Date());
calYear = new Date().getFullYear();
calMonth = new Date().getMonth() + 1;
renderCalendar();
navigateToDate(today);
navigateToWeek(now);
};
/* ================================================================
@@ -224,7 +202,7 @@
function loadStats() {
apiGetStats(function(err, data) {
if (!err && data) {
document.getElementById('stat-days').textContent = data.total_days;
document.getElementById('stat-days').textContent = data.total_weeks;
document.getElementById('stat-morning').textContent = data.total_morning;
document.getElementById('stat-study').textContent = data.total_study;
calendarStatus = data.calendar || {};
@@ -318,7 +296,7 @@
var div = document.createElement('div');
div.className = 'item-row';
div.innerHTML = '<span class="idx">' + (idx+1) + '.</span>' +
'<input type="text" value="' + esc(text) + '" placeholder="今天最重要的一件事…">' +
'<input type="text" value="' + esc(text) + '" placeholder="本周最重要的一件事…">' +
'<button class="btn-del" onclick="this.parentElement.remove();renumberMorning()"><svg class="icon-sm"><use href="#icon-x"/></svg></button>';
container.appendChild(div);
renumberMorning();
@@ -410,7 +388,7 @@
var totalDays = data.length;
var totalEvents = 0;
data.forEach(function(d){ totalEvents += d.events.length; });
summary.innerHTML = '同步完成!共 <b>' + totalDays + '</b> 有日程,合计 <b>' + totalEvents + '</b> 个会议' + (res.saved_days ? ',已自动填充 <b>' + res.saved_days + '</b> ' : '');
summary.innerHTML = '同步完成!共 <b>' + totalDays + '</b> 有日程,合计 <b>' + totalEvents + '</b> 个会议' + (res.saved_days ? ',已自动填充 <b>' + res.saved_days + '</b> ' : '');
// 渲染结果列表
var html = '';
@@ -428,8 +406,8 @@
});
list.innerHTML = html;
// 自动填充今天到选中日期的日程
autoFillTodayEvents(data);
// 刷新当前页
loadCheckin();
})
.catch(function(){
loading.style.display = 'none';
@@ -439,30 +417,6 @@
});
};
function autoFillTodayEvents(data) {
var todayDate = document.getElementById('check-date').value;
var added = 0;
data.forEach(function(day) {
if (day.date !== todayDate) return;
day.events.forEach(function(e) {
var text = e.time ? '【' + e.time + '】' + e.summary : e.summary;
if (e.location) text += ' @' + e.location;
var existing = document.querySelectorAll('#morning-list input[type="text"]');
var already = false;
existing.forEach(function(inp){ if (inp.value.indexOf(e.summary) >= 0) already = true; });
if (already) return;
var count = document.querySelectorAll('#morning-list .item-row').length;
if (count >= 3) return;
addMorningRow(text);
added++;
});
});
if (added > 0) {
showToast('已自动填充 ' + added + ' 条今日日程', 'info');
triggerAutoSave();
}
}
window.closeSyncModal = function() {
var overlay = document.getElementById('sync-modal-overlay');
if (overlay) overlay.style.display = 'none';
@@ -492,24 +446,22 @@
================================================================ */
window.loadCheckin = function() {
var date = document.getElementById('check-date').value;
if (!date) return;
var week = document.getElementById('check-date').value;
if (!week) return;
// 切换日期前先保存当前数据
var currentData = buildData();
var currentStr = JSON.stringify(currentData);
if (lastSavedDate && lastSavedData !== null && currentStr !== lastSavedData) {
// 有未保存的变更,先静默保存
if (lastSavedWeek && lastSavedData !== null && currentStr !== lastSavedData) {
var hasContent = currentData.morning.some(function(x){return x && x.trim();}) ||
currentData.evening.some(function(x){return (x.mistake||'').trim() || (x.improvement||'').trim();}) ||
currentData.study.some(function(x){return x.done;});
if (hasContent) {
apiSaveCheckin(lastSavedDate, currentData, function(){}); // 静默保存
apiSaveCheckin(lastSavedWeek, currentData, function(){});
}
}
lastSavedDate = date;
apiGetCheckin(date, function(err, row){
lastSavedWeek = week;
apiGetCheckin(week, function(err, row){
if (err) { initStudyPresets(); lastSavedData = '{}'; return; }
fillForm(row ? row.data : null);
lastSavedData = JSON.stringify(buildData());
@@ -518,26 +470,26 @@
/** 自动保存 — 失去焦点/勾选/删除时触发 */
function triggerAutoSave() {
var date = document.getElementById('check-date').value;
if (!date) return;
var week = document.getElementById('check-date').value;
if (!week) return;
var data = buildData();
var dataStr = JSON.stringify(data);
if (dataStr === lastSavedData) return;
apiSaveCheckin(date, data, function(err){
apiSaveCheckin(week, data, function(err){
if (err) { showToast('保存失败', 'error'); return; }
lastSavedData = dataStr;
loadStats();
});
}
/** 为每打卡面板绑定自动保存事件 */
/** 为每打卡面板绑定自动保存事件 */
function bindAutoSave() {
var panel = document.getElementById('panel-daily');
if (!panel) return;
// 输入框 & 文本域 & 下拉选择器:失去焦点时保存
panel.addEventListener('blur', function(e) {
if (e.target.matches('input[type="text"], textarea, input[type="date"], select')) {
if (e.target.matches('input[type="text"], textarea, input[type="week"], select')) {
triggerAutoSave();
}
}, true);
@@ -586,97 +538,70 @@
function loadWeekly() {
var today = new Date();
today.setDate(today.getDate() + weekOffset * 7);
var monday = getMonday(today);
var sunday = new Date(monday);
sunday.setDate(monday.getDate() + 6);
var wk = fmtWeek(today);
document.getElementById('week-label').textContent = wk;
document.getElementById('week-label').textContent =
fmtDate(monday) + ' ~ ' + fmtDate(sunday);
apiGetHistory(function(err, rows){
apiGetCheckin(wk, function(err, row){
if (err) { showToast('加载失败', 'error'); return; }
var map = {};
for (var i=0; i<rows.length; i++) map[rows[i].date] = rows[i].data;
var totalDays = 0, totalScore = 0, dayCells = [];
for (var j=0; j<7; j++) {
var cur = new Date(monday);
cur.setDate(monday.getDate() + j);
var ds = fmtDate(cur);
var dd = map[ds];
if (dd) {
totalDays++;
var morning = dd.morning || [];
var evening = dd.evening || [];
var study = dd.study || [];
var morningCount = morning.filter(function(x){ return x && typeof x === 'string' && x.trim(); }).length;
var eveningCount = evening.filter(function(x){
var mst = typeof x === 'string' ? x : (x.mistake || '');
return mst && typeof mst === 'string' && mst.trim();
}).length;
var studyCount = study.filter(function(x){ return x.done; }).length;
var allThree = morningCount > 0 && eveningCount > 0 && studyCount > 0;
var dayScore, status;
if (allThree) {
dayScore = 60 + (morningCount - 1 + eveningCount - 1 + studyCount - 1) * 5;
status = 'pass';
} else {
dayScore = (morningCount + eveningCount + studyCount) * 5;
status = 'fail';
}
totalScore += dayScore;
dayCells.push({
ds: ds, dayScore: dayScore, status: status,
weekday: '周' + WEEKDAYS_CN[cur.getDay()]
});
} else {
dayCells.push({
ds: ds, dayScore: 0, status: 'empty',
weekday: '周' + WEEKDAYS_CN[cur.getDay()]
});
}
var dd = row ? row.data : null;
if (!dd) {
document.getElementById('weekly-score').textContent = '--';
document.getElementById('score-text').textContent = '本周暂无记录';
document.getElementById('score-stats').textContent = '';
document.getElementById('week-days-grid').innerHTML = '';
return;
}
var score = totalScore;
var morning = dd.morning || [];
var evening = dd.evening || [];
var study = dd.study || [];
var morningCount = morning.filter(function(x){ return x && typeof x === 'string' && x.trim(); }).length;
if (!morningCount) {
morningCount = morning.filter(function(x){
return x && typeof x === 'object' && (x.text || '').trim();
}).length;
}
var eveningCount = evening.filter(function(x){
var mst = typeof x === 'string' ? x : (x.mistake || '');
return mst && typeof mst === 'string' && mst.trim();
}).length;
var studyCount = study.filter(function(x){ return x.done; }).length;
var allThree = morningCount > 0 && eveningCount > 0 && studyCount > 0;
var score, status;
if (allThree) {
score = 60 + (morningCount - 1) * 5 + (eveningCount - 1) * 3 + studyCount * 3;
status = 'pass';
} else {
score = 0;
status = 'fail';
}
var grade = score >= 90 ? '优秀' : score >= 75 ? '良好' : score >= 60 ? '达标' : '未达标';
document.getElementById('weekly-score').textContent = score;
var ring = document.querySelector('.score-ring');
if (ring) {
var R = 50, C = 2 * Math.PI * R;
ring.setAttribute('stroke-dasharray', C);
ring.setAttribute('stroke-dashoffset', C - (C * totalDays / 7));
var ratio = Math.min(score / 100, 1);
ring.setAttribute('stroke-dashoffset', C - (C * ratio));
}
var txt = '';
if (totalDays >= 7) txt = '全勤!本周每天都有记录';
else if (totalDays >= 5) txt = '良好,保持了大部分记录';
else if (totalDays >= 3) txt = '一般,需要更规律打卡';
else txt = '本周记录偏少,加油!';
var txt = grade + ' · ' + score + ' 分';
document.getElementById('score-text').textContent = txt;
var completedDays = dayCells.filter(function(c){ return c.status === 'pass'; }).length;
var totalFilled = dayCells.filter(function(c){ return c.status !== 'empty'; }).length;
document.getElementById('score-stats').textContent =
'本周填卡 ' + totalFilled + '/7 天 · 达标 ' + completedDays + '';
'立志 ' + morningCount + ' · 责善 ' + eveningCount + ' · 勤学 ' + studyCount + '/8';
var gridHtml = '';
for (var g=0; g<dayCells.length; g++) {
var cell = dayCells[g];
var dateParts = cell.ds.split('-');
var shortDate = dateParts[1] + '/' + dateParts[2];
var badgeText = cell.status === 'pass' ? '达标' : (cell.status === 'fail' ? '未达标' : '未打卡');
gridHtml += '<div class="day-cell ' + cell.status + '">' +
'<div class="day-label">' + cell.weekday + '</div>' +
'<div class="day-date">' + shortDate + '</div>' +
'<div class="day-score">' + cell.dayScore + '<span style="font-size:11px;font-weight:400">分</span></div>' +
'<div class="day-badge">' + badgeText + '</div>' +
'</div>';
}
// 渲染周统计网格(简化版)
var gridHtml = '<div class="day-cell ' + status + '">' +
'<div class="day-label">' + wk + '</div>' +
'<div class="day-date">' + grade + '</div>' +
'<div class="day-score">' + score + '<span style="font-size:11px;font-weight:400">分</span></div>' +
'<div class="day-badge">' + (status === 'pass' ? '达标' : '未达标') + '</div>' +
'</div>';
document.getElementById('week-days-grid').innerHTML = gridHtml;
});
}
@@ -699,7 +624,9 @@
var html = '';
for (var i=0; i<rows.length; i++) {
var r = rows[i];
var morning = (r.data.morning || []).filter(function(x){ return x && typeof x === 'string' && x.trim(); });
var morning = (r.data.morning || []).filter(function(x){
return (typeof x === 'string' && x.trim()) || (typeof x === 'object' && (x.text||'').trim());
});
var evening = (r.data.evening || []).filter(function(x){
var mst = typeof x === 'string' ? x : (x.mistake || '');
return mst && typeof mst === 'string' && mst.trim();
@@ -707,9 +634,9 @@
var study = (r.data.study || []).filter(function(x){ return x.done; });
var tags = [];
if (morning.length) tags.push('<span><svg class="icon-xs" style="color:var(--primary)"><use href="#icon-sun"/></svg> 立志 ' + morning.length + '</span>');
if (evening.length) tags.push('<span><svg class="icon-xs" style="color:var(--danger)"><use href="#icon-magnifying-glass"/></svg> 改过 ' + evening.length + '</span>');
if (study.length) tags.push('<span><svg class="icon-xs" style="color:#059669"><use href="#icon-book-open"/></svg> 勤学 ' + study.length + '</span>');
if (morning.length) tags.push('<span><svg class="icon-xs" style="color:var(--primary)"><use href="#icon-sun"/></svg> ' + morning.length + '</span>');
if (evening.length) tags.push('<span><svg class="icon-xs" style="color:var(--danger)"><use href="#icon-magnifying-glass"/></svg> ' + evening.length + '</span>');
if (study.length) tags.push('<span><svg class="icon-xs" style="color:#059669"><use href="#icon-book-open"/></svg> ' + study.length + '/8</span>');
html += '<div class="history-card">' +
'<div class="info">' +
@@ -723,13 +650,13 @@
});
}
window.deleteHistory = function(date) {
if (!confirm('确定删除 ' + date + ' 的打卡记录吗?此操作不可恢复!')) return;
apiDeleteCheckin(date, function(err){
window.deleteHistory = function(week) {
if (!confirm('确定删除 ' + week + ' 的打卡记录吗?此操作不可恢复!')) return;
apiDeleteCheckin(week, function(err){
if (err) { showToast('删除失败: ' + err, 'error'); return; }
showToast('已删除', 'info');
loadHistory();
loadStats(); // 更新统计
loadStats();
});
};
@@ -1091,29 +1018,28 @@
function init() {
var today = new Date();
calYear = today.getFullYear();
calMonth = today.getMonth() + 1;
var cd = document.getElementById('check-date');
var todayStr = fmtDate(today);
if (cd) cd.value = todayStr;
var todayWk = fmtWeek(today);
if (cd) cd.value = todayWk;
selectedDate = todayStr;
selectedWeek = todayWk;
initStudyPresets();
bindAutoSave();
lastSavedDate = todayStr;
lastSavedWeek = todayWk;
// 从页面嵌入数据获取初始统计0 延迟)
if (window.__INITIAL_STATS__) {
var s = window.__INITIAL_STATS__;
document.getElementById('stat-days').textContent = s.total_days;
document.getElementById('stat-days').textContent = s.total_weeks;
document.getElementById('stat-morning').textContent = s.total_morning;
document.getElementById('stat-study').textContent = s.total_study;
calendarStatus = s.calendar || {};
}
loadCheckin();
loadStats(); // 异步刷新最新数据
renderCalendar(); // 同步渲染 — calendarStatus 已有值
loadStats();
renderCalendar();
}
if (document.readyState === 'loading') {