938 lines
35 KiB
JavaScript
938 lines
35 KiB
JavaScript
/* ziwei-power 前端逻辑 — Flomo 风格左右布局 */
|
||
(function() {
|
||
'use strict';
|
||
|
||
/* ================================================================
|
||
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 PRESET_STUDY_ITEMS = [
|
||
'晚上 11 点 30 之前睡觉',
|
||
'早上 6 点 30 之前起床',
|
||
'起床后做 30 个俯卧撑',
|
||
'销售客情能力提升',
|
||
'管理能力提升',
|
||
'产品规划有进一步的提升',
|
||
'AI 能力提升',
|
||
'知识库能力提升'
|
||
];
|
||
|
||
/* ================================================================
|
||
Toast
|
||
================================================================ */
|
||
var toastTimer = null;
|
||
function showToast(msg, type) {
|
||
type = type || 'info';
|
||
var el = document.getElementById('toast');
|
||
el.textContent = msg;
|
||
el.className = 'toast ' + type + ' show';
|
||
clearTimeout(toastTimer);
|
||
toastTimer = setTimeout(function(){ el.classList.remove('show'); }, 2500);
|
||
}
|
||
|
||
/* ================================================================
|
||
API
|
||
================================================================ */
|
||
function apiGetCheckin(date, cb) {
|
||
fetch('/api/checkin?date=' + encodeURIComponent(date))
|
||
.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) {
|
||
fetch('/api/checkin', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({date: date, data: data})
|
||
})
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(res){ if (res.ok) cb(null); else cb(res.error); })
|
||
.catch(function(e){ cb(e.message); });
|
||
}
|
||
|
||
function apiDeleteCheckin(date, cb) {
|
||
fetch('/api/checkin/' + encodeURIComponent(date), {method: 'DELETE'})
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(res){ if (res.ok) cb(null); else cb(res.error); })
|
||
.catch(function(e){ cb(e.message); });
|
||
}
|
||
|
||
function apiGetHistory(cb) {
|
||
fetch('/api/history')
|
||
.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 apiGetStats(cb) {
|
||
fetch('/api/stats')
|
||
.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); });
|
||
}
|
||
|
||
/* ================================================================
|
||
Escaping
|
||
================================================================ */
|
||
function esc(s) {
|
||
return String(s).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>');
|
||
}
|
||
|
||
/* ================================================================
|
||
Panel Switching
|
||
================================================================ */
|
||
window.switchPanel = function(name) {
|
||
activePanel = name;
|
||
var navs = document.querySelectorAll('.sidebar-nav .nav-item');
|
||
var panels = document.querySelectorAll('.main-content .panel');
|
||
|
||
for (var i=0; i<navs.length; i++) navs[i].classList.remove('active');
|
||
for (var j=0; j<panels.length; j++) panels[j].classList.remove('active');
|
||
|
||
var nav = document.querySelector('.nav-item[data-panel="' + name + '"]');
|
||
var panel = document.getElementById('panel-' + name);
|
||
if (nav) nav.classList.add('active');
|
||
if (panel) panel.classList.add('active');
|
||
|
||
if (name === 'weekly') loadWeekly();
|
||
if (name === 'history') loadHistory();
|
||
if (name === 'wishes') loadWishes();
|
||
};
|
||
|
||
/* ================================================================
|
||
Calendar
|
||
================================================================ */
|
||
|
||
/** 格式化日期 */
|
||
function fmtDate(d) {
|
||
var y = d.getFullYear();
|
||
var m = ('0'+(d.getMonth()+1)).slice(-2);
|
||
var dd = ('0'+d.getDate()).slice(-2);
|
||
return y + '-' + m + '-' + dd;
|
||
}
|
||
|
||
/** 判断两个日期是否同一天 */
|
||
function isSameDay(a, b) {
|
||
return a.getFullYear() === b.getFullYear() &&
|
||
a.getMonth() === b.getMonth() &&
|
||
a.getDate() === b.getDate();
|
||
}
|
||
|
||
/** 切换日历月份 */
|
||
window.changeCalMonth = function(dir) {
|
||
calMonth += dir;
|
||
if (calMonth > 12) { calMonth = 1; calYear++; }
|
||
if (calMonth < 1) { calMonth = 12; calYear--; }
|
||
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');
|
||
var html = '';
|
||
|
||
// 前月填充
|
||
for (var p = startDayOfWeek - 1; p >= 0; p--) {
|
||
html += '<div class="cal-cell other-month">' + (prevMonthLastDay - p) + '</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) {
|
||
cell.addEventListener('click', function() {
|
||
var date = this.dataset.date;
|
||
if (date) navigateToDate(date);
|
||
});
|
||
});
|
||
}
|
||
|
||
/** 导航到指定日期 */
|
||
function navigateToDate(date) {
|
||
selectedDate = date;
|
||
switchPanel('daily');
|
||
document.getElementById('check-date').value = date;
|
||
loadCheckin();
|
||
renderCalendar(); // 刷新日历高亮
|
||
}
|
||
|
||
/** 回到今天 */
|
||
window.goToday = function() {
|
||
var today = fmtDate(new Date());
|
||
calYear = new Date().getFullYear();
|
||
calMonth = new Date().getMonth() + 1;
|
||
renderCalendar();
|
||
navigateToDate(today);
|
||
};
|
||
|
||
/* ================================================================
|
||
Stats — 加载统计数据并更新侧边栏
|
||
================================================================ */
|
||
function loadStats() {
|
||
apiGetStats(function(err, data) {
|
||
if (!err && data) {
|
||
document.getElementById('stat-days').textContent = data.total_days;
|
||
document.getElementById('stat-morning').textContent = data.total_morning;
|
||
document.getElementById('stat-study').textContent = data.total_study;
|
||
calendarStatus = data.calendar || {};
|
||
}
|
||
renderCalendar();
|
||
});
|
||
}
|
||
|
||
/* ================================================================
|
||
Build / Fill Data
|
||
================================================================ */
|
||
|
||
function buildData() {
|
||
var morningItems = [];
|
||
var mEls = document.querySelectorAll('#morning-list .item-row input');
|
||
for (var i=0; i<mEls.length; i++) {
|
||
var v = mEls[i].value.trim();
|
||
if (v) morningItems.push(v);
|
||
}
|
||
|
||
var eveningItems = [];
|
||
var eRows = document.querySelectorAll('#evening-list .evening-row');
|
||
for (var j=0; j<eRows.length; j++) {
|
||
var cols = eRows[j].querySelectorAll('.col input');
|
||
var mistake = cols[0] ? cols[0].value.trim() : '';
|
||
var improve = cols[1] ? cols[1].value.trim() : '';
|
||
if (mistake || improve) eveningItems.push({ mistake: mistake, improvement: improve });
|
||
}
|
||
|
||
var studyItems = [];
|
||
var pItems = document.querySelectorAll('#study-list .preset-item');
|
||
for (var k=0; k<pItems.length; k++) {
|
||
var cb = pItems[k].querySelector('input[type="checkbox"]');
|
||
var noteEl = pItems[k].querySelector('.preset-note');
|
||
var nameEl = pItems[k].querySelector('.preset-name');
|
||
var name = nameEl ? nameEl.textContent : '';
|
||
if (cb && cb.checked) {
|
||
studyItems.push({ name: name, done: true, note: noteEl ? noteEl.value.trim() : '' });
|
||
} else {
|
||
studyItems.push({ name: name, done: false, note: '' });
|
||
}
|
||
}
|
||
|
||
return { morning: morningItems, evening: eveningItems, study: studyItems };
|
||
}
|
||
|
||
function fillForm(data) {
|
||
data = data || {};
|
||
var morning = data.morning || [];
|
||
var evening = data.evening || [];
|
||
var study = data.study || [];
|
||
|
||
// 早间立志
|
||
document.getElementById('morning-list').innerHTML = '';
|
||
if (morning.length === 0) morning = [''];
|
||
for (var m=0; m<morning.length; m++) addMorningRow(morning[m]);
|
||
|
||
// 责善改过
|
||
document.getElementById('evening-list').innerHTML = '';
|
||
if (evening.length === 0) evening = [{}];
|
||
for (var e=0; e<evening.length; e++) {
|
||
var item = typeof evening[e] === 'string' ? {mistake: evening[e], improvement: ''} : evening[e];
|
||
addEveningRow(item.mistake || '', item.improvement || '');
|
||
}
|
||
|
||
// 勤学 — 渲染预设项目并回填状态
|
||
var studyMap = {};
|
||
for (var si=0; si<study.length; si++) {
|
||
studyMap[study[si].name] = study[si];
|
||
}
|
||
document.getElementById('study-list').innerHTML = '';
|
||
for (var sp=0; sp<PRESET_STUDY_ITEMS.length; sp++) {
|
||
var sName = PRESET_STUDY_ITEMS[sp];
|
||
var saved = studyMap[sName] || { done: false, note: '' };
|
||
addPresetItem(sName, saved.done, saved.note || '');
|
||
}
|
||
}
|
||
|
||
/* ================================================================
|
||
Dynamic Rows
|
||
================================================================ */
|
||
|
||
function addMorningRow(val) {
|
||
var container = document.getElementById('morning-list');
|
||
var idx = container.children.length;
|
||
val = val || '';
|
||
var div = document.createElement('div');
|
||
div.className = 'item-row';
|
||
div.innerHTML = '<span class="idx">' + (idx+1) + '.</span>' +
|
||
'<input type="text" value="' + esc(val) + '" placeholder="今天最重要的一件事…">' +
|
||
'<button class="btn-del" onclick="this.parentElement.remove();renumberMorning()"><svg class="icon-sm"><use href="#icon-x"/></svg></button>';
|
||
container.appendChild(div);
|
||
renumberMorning();
|
||
}
|
||
|
||
function renumberMorning() {
|
||
var rows = document.querySelectorAll('#morning-list .item-row');
|
||
for (var i=0; i<rows.length; i++) {
|
||
var span = rows[i].querySelector('.idx');
|
||
if (span) span.textContent = (i+1) + '.';
|
||
}
|
||
}
|
||
|
||
function addEveningRow(mistake, improve) {
|
||
var container = document.getElementById('evening-list');
|
||
var idx = container.children.length;
|
||
mistake = mistake || '';
|
||
improve = improve || '';
|
||
var div = document.createElement('div');
|
||
div.className = 'evening-row';
|
||
div.innerHTML =
|
||
'<div class="evening-header">' +
|
||
'<span class="idx">' + (idx+1) + '.</span>' +
|
||
'<button class="btn-del" onclick="this.parentElement.parentElement.remove();renumberEvening()"><svg class="icon-sm"><use href="#icon-x"/></svg></button>' +
|
||
'</div>' +
|
||
'<div class="mistake-row">' +
|
||
'<div class="col"><label>错误</label><input type="text" value="' + esc(mistake) + '" placeholder="今天犯的错…"></div>' +
|
||
'<div class="col"><label>改进方案</label><input type="text" value="' + esc(improve) + '" placeholder="下次怎么做…"></div>' +
|
||
'</div>';
|
||
container.appendChild(div);
|
||
renumberEvening();
|
||
}
|
||
|
||
function renumberEvening() {
|
||
var rows = document.querySelectorAll('#evening-list .evening-row');
|
||
for (var i=0; i<rows.length; i++) {
|
||
var span = rows[i].querySelector('.idx');
|
||
if (span) span.textContent = (i+1) + '.';
|
||
}
|
||
}
|
||
|
||
/* ── 勤学预设项目 ── */
|
||
|
||
function addPresetItem(name, done, note) {
|
||
var container = document.getElementById('study-list');
|
||
var div = document.createElement('div');
|
||
div.className = 'preset-item' + (done ? ' active' : '');
|
||
var noteEsc = esc(note || '');
|
||
div.innerHTML =
|
||
'<label class="preset-check">' +
|
||
'<input type="checkbox"' + (done ? ' checked' : '') + ' onchange="togglePresetNote(this)">' +
|
||
'<span class="preset-name">' + esc(name) + '</span>' +
|
||
'</label>' +
|
||
'<textarea class="preset-note" placeholder="一句话备注…">' + noteEsc + '</textarea>';
|
||
container.appendChild(div);
|
||
}
|
||
|
||
window.togglePresetNote = function(cb) {
|
||
var item = cb.closest('.preset-item');
|
||
if (item) {
|
||
if (cb.checked) item.classList.add('active');
|
||
else { item.classList.remove('active'); item.querySelector('.preset-note').value = ''; }
|
||
}
|
||
triggerAutoSave();
|
||
};
|
||
|
||
function initStudyPresets() {
|
||
document.getElementById('study-list').innerHTML = '';
|
||
for (var i=0; i<PRESET_STUDY_ITEMS.length; i++) {
|
||
addPresetItem(PRESET_STUDY_ITEMS[i], false, '');
|
||
}
|
||
}
|
||
|
||
window.addMorning = function() {
|
||
var count = document.querySelectorAll('#morning-list .item-row').length;
|
||
if (count >= 3) { showToast('最多 3 条立志', 'error'); return; }
|
||
addMorningRow('');
|
||
};
|
||
window.addEvening = function() {
|
||
var count = document.querySelectorAll('#evening-list .evening-row').length;
|
||
if (count >= 5) { showToast('最多 5 条改过', 'error'); return; }
|
||
addEveningRow('', '');
|
||
};
|
||
|
||
/* 切换卡片编辑模式 */
|
||
window.toggleEditMode = function(btn) {
|
||
var card = btn.closest('.card');
|
||
if (!card) return;
|
||
var editing = card.classList.toggle('editing');
|
||
btn.classList.toggle('active', editing);
|
||
};
|
||
|
||
/* ================================================================
|
||
Load & Auto Save Checkin
|
||
================================================================ */
|
||
|
||
window.loadCheckin = function() {
|
||
var date = document.getElementById('check-date').value;
|
||
if (!date) return;
|
||
|
||
// 切换日期前先保存当前数据
|
||
var currentData = buildData();
|
||
var currentStr = JSON.stringify(currentData);
|
||
if (lastSavedDate && 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(){}); // 静默保存
|
||
}
|
||
}
|
||
|
||
lastSavedDate = date;
|
||
apiGetCheckin(date, function(err, row){
|
||
if (err) { initStudyPresets(); lastSavedData = '{}'; return; }
|
||
fillForm(row ? row.data : null);
|
||
lastSavedData = JSON.stringify(buildData());
|
||
});
|
||
};
|
||
|
||
/** 自动保存 — 失去焦点/勾选/删除时触发 */
|
||
function triggerAutoSave() {
|
||
var date = document.getElementById('check-date').value;
|
||
if (!date) return;
|
||
var data = buildData();
|
||
var dataStr = JSON.stringify(data);
|
||
if (dataStr === lastSavedData) return;
|
||
apiSaveCheckin(date, 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"]')) {
|
||
triggerAutoSave();
|
||
}
|
||
}, true);
|
||
|
||
// 复选框:change 事件
|
||
panel.addEventListener('change', function(e) {
|
||
if (e.target.matches('input[type="checkbox"]')) {
|
||
triggerAutoSave();
|
||
}
|
||
});
|
||
|
||
// 删除行按钮:点击后触发
|
||
panel.addEventListener('click', function(e) {
|
||
var btn = e.target.closest('.btn-del');
|
||
if (btn) {
|
||
setTimeout(triggerAutoSave, 100); // 等 DOM 更新后
|
||
}
|
||
});
|
||
}
|
||
|
||
/* ================================================================
|
||
Weekly Score
|
||
================================================================ */
|
||
var WEEKDAYS_CN = ['日','一','二','三','四','五','六'];
|
||
|
||
function getMonday(d) {
|
||
var day = d.getDay();
|
||
var diff = d.getDate() - day + (day === 0 ? -6 : 1);
|
||
var m = new Date(d);
|
||
m.setDate(diff);
|
||
return m;
|
||
}
|
||
|
||
window.changeWeek = function(dir) {
|
||
weekOffset += dir;
|
||
loadWeekly();
|
||
};
|
||
|
||
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);
|
||
|
||
document.getElementById('week-label').textContent =
|
||
fmtDate(monday) + ' ~ ' + fmtDate(sunday);
|
||
|
||
apiGetHistory(function(err, rows){
|
||
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 score = totalScore;
|
||
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 txt = '';
|
||
if (totalDays >= 7) txt = '全勤!本周每天都有记录';
|
||
else if (totalDays >= 5) txt = '良好,保持了大部分记录';
|
||
else if (totalDays >= 3) txt = '一般,需要更规律打卡';
|
||
else txt = '本周记录偏少,加油!';
|
||
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 + ' 天';
|
||
|
||
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>';
|
||
}
|
||
document.getElementById('week-days-grid').innerHTML = gridHtml;
|
||
});
|
||
}
|
||
|
||
/* ================================================================
|
||
History
|
||
================================================================ */
|
||
function loadHistory() {
|
||
apiGetHistory(function(err, rows){
|
||
var el = document.getElementById('history-grid');
|
||
if (err) {
|
||
el.innerHTML = '<div class="empty-state"><div class="empty-icon"><svg viewBox="0 0 24 24"><use href="#icon-list-bullet"/></svg></div>加载失败</div>';
|
||
return;
|
||
}
|
||
if (!rows || rows.length === 0) {
|
||
el.innerHTML = '<div class="empty-state"><div class="empty-icon"><svg viewBox="0 0 24 24"><use href="#icon-list-bullet"/></svg></div>暂无历史记录</div>';
|
||
return;
|
||
}
|
||
|
||
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 evening = (r.data.evening || []).filter(function(x){
|
||
var mst = typeof x === 'string' ? x : (x.mistake || '');
|
||
return mst && typeof mst === 'string' && mst.trim();
|
||
});
|
||
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>');
|
||
|
||
html += '<div class="history-card">' +
|
||
'<div class="info">' +
|
||
'<div class="date">' + r.date + '</div>' +
|
||
'<div class="preview">' + (tags.join('') || '暂无内容') + '</div>' +
|
||
'</div>' +
|
||
'<button class="btn-del-history" onclick="deleteHistory(\'' + r.date + '\')"><svg class="icon-xs"><use href="#icon-trash"/></svg> 删除</button>' +
|
||
'</div>';
|
||
}
|
||
el.innerHTML = html;
|
||
});
|
||
}
|
||
|
||
window.deleteHistory = function(date) {
|
||
if (!confirm('确定删除 ' + date + ' 的打卡记录吗?此操作不可恢复!')) return;
|
||
apiDeleteCheckin(date, function(err){
|
||
if (err) { showToast('删除失败: ' + err, 'error'); return; }
|
||
showToast('已删除', 'info');
|
||
loadHistory();
|
||
loadStats(); // 更新统计
|
||
});
|
||
};
|
||
|
||
/* ================================================================
|
||
Wishes — 心愿清单(四象限)
|
||
================================================================ */
|
||
var wishes = [];
|
||
var dragSourceId = null;
|
||
var dragSourceQuadrant = null;
|
||
var wishesLoaded = false;
|
||
var QUADRANTS = ['重要紧急', '重要不紧急', '紧急不重要', '不紧急不重要'];
|
||
|
||
function loadWishes() {
|
||
if (wishesLoaded) { renderWishes(); return; }
|
||
if (window.__INITIAL_WISHES__) {
|
||
wishes = window.__INITIAL_WISHES__;
|
||
wishesLoaded = true;
|
||
renderWishes();
|
||
return;
|
||
}
|
||
fetch('/api/wishes')
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(res){ if (res.ok) { wishes = res.data; wishesLoaded = true; renderWishes(); } });
|
||
}
|
||
|
||
function renderWishes() {
|
||
var emptyEl = document.getElementById('wishes-empty');
|
||
if (!wishes.length) {
|
||
QUADRANTS.forEach(function(q){ document.getElementById('quad-list-' + q).innerHTML = ''; });
|
||
if (emptyEl) emptyEl.style.display = 'block';
|
||
return;
|
||
}
|
||
if (emptyEl) emptyEl.style.display = 'none';
|
||
|
||
// 按象限分组渲染
|
||
QUADRANTS.forEach(function(quadrant) {
|
||
var list = document.getElementById('quad-list-' + quadrant);
|
||
if (!list) return;
|
||
var items = wishes.filter(function(w){ return (w.quadrant || w.priority || '重要不紧急') === quadrant; });
|
||
var html = '';
|
||
for (var i = 0; i < items.length; i++) {
|
||
var w = items[i];
|
||
var doneCls = w.done ? ' done' : '';
|
||
var deadline = w.deadline ? w.deadline : '';
|
||
html += '<div class="wish-item' + doneCls + '" draggable="true" data-id="' + w.id + '" data-quadrant="' + quadrant + '">' +
|
||
'<span class="wish-drag-handle"><svg class="icon-xs"><use href="#icon-list-bullet"/></svg></span>' +
|
||
'<input type="checkbox" class="wish-check" ' + (w.done ? 'checked' : '') + ' onchange="toggleWish(' + w.id + ', this.checked)">' +
|
||
'<span class="wish-name" title="' + esc(w.name) + '">' + esc(w.name) + '</span>' +
|
||
(deadline ? '<span class="wish-deadline">' + esc(deadline) + '</span>' : '') +
|
||
'<button class="btn-del wish-del" onclick="deleteWishItem(' + w.id + ')"><svg class="icon-xs"><use href="#icon-x"/></svg></button>' +
|
||
'</div>';
|
||
}
|
||
list.innerHTML = html;
|
||
});
|
||
|
||
bindDragEvents();
|
||
bindEditClicks();
|
||
}
|
||
|
||
/* ── 点击编辑 ── */
|
||
|
||
function bindEditClicks() {
|
||
document.querySelectorAll('#panel-wishes .wish-item').forEach(function(item) {
|
||
// 只在点击名称或日期区域时触发编辑(排除 checkbox / 拖拽手柄 / 删除按钮)
|
||
item.addEventListener('click', function(e) {
|
||
if (e.target.closest('.wish-check') || e.target.closest('.wish-drag-handle') ||
|
||
e.target.closest('.wish-del') || e.target.closest('.wish-edit-form')) return;
|
||
var id = parseInt(this.dataset.id);
|
||
var w = wishes.find(function(x){ return x.id === id; });
|
||
if (w) startEditWish(this, w);
|
||
});
|
||
});
|
||
}
|
||
|
||
function startEditWish(el, w) {
|
||
if (el.querySelector('.wish-edit-form')) return; // 已在编辑中
|
||
el.classList.add('editing');
|
||
var deadline = w.deadline || '';
|
||
var quadOpts = QUADRANTS.map(function(q){
|
||
return '<option value="' + q + '"' + (q === (w.quadrant || w.priority || '重要不紧急') ? ' selected' : '') + '>' + q + '</option>';
|
||
}).join('');
|
||
el.innerHTML =
|
||
'<div class="wish-edit-form">' +
|
||
'<input type="text" class="wish-edit-name" value="' + esc(w.name) + '" maxlength="50">' +
|
||
'<div class="wish-edit-row">' +
|
||
'<select class="wish-edit-quadrant">' + quadOpts + '</select>' +
|
||
'<input type="date" class="wish-edit-deadline" value="' + esc(deadline) + '">' +
|
||
'</div>' +
|
||
'<div class="wish-edit-actions">' +
|
||
'<button class="btn-wish-save" onclick="event.stopPropagation();saveEditWish(' + w.id + ', this)">保存</button>' +
|
||
'<button class="btn-wish-cancel" onclick="event.stopPropagation();window.renderWishes()">取消</button>' +
|
||
'</div>' +
|
||
'</div>';
|
||
}
|
||
|
||
window.saveEditWish = function(id, btn) {
|
||
var form = btn.closest('.wish-edit-form');
|
||
var name = form.querySelector('.wish-edit-name').value.trim();
|
||
if (!name) { showToast('名称不能为空', 'error'); return; }
|
||
var quadrant = form.querySelector('.wish-edit-quadrant').value;
|
||
var deadline = form.querySelector('.wish-edit-deadline').value;
|
||
fetch('/api/wishes/' + id, {
|
||
method: 'PUT',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({name: name, quadrant: quadrant, deadline: deadline})
|
||
}).then(function(r){ return r.json(); })
|
||
.then(function(res) {
|
||
if (!res.ok) { showToast(res.error, 'error'); return; }
|
||
var w = wishes.find(function(x){ return x.id === id; });
|
||
if (w) { w.name = name; w.quadrant = quadrant; w.deadline = deadline; }
|
||
renderWishes();
|
||
});
|
||
};
|
||
|
||
/* ── Drag & Drop (跨象限) ── */
|
||
|
||
function bindDragEvents() {
|
||
var items = document.querySelectorAll('#panel-wishes .wish-item');
|
||
items.forEach(function(item) {
|
||
item.addEventListener('dragstart', function(e) {
|
||
dragSourceId = parseInt(this.dataset.id);
|
||
dragSourceQuadrant = this.dataset.quadrant;
|
||
this.classList.add('dragging');
|
||
e.dataTransfer.effectAllowed = 'move';
|
||
});
|
||
item.addEventListener('dragend', function() {
|
||
this.classList.remove('dragging');
|
||
dragSourceId = null;
|
||
dragSourceQuadrant = null;
|
||
document.querySelectorAll('.quad-cell, .wish-item').forEach(function(el){ el.classList.remove('drag-over'); });
|
||
});
|
||
item.addEventListener('dragover', function(e) {
|
||
e.preventDefault();
|
||
e.dataTransfer.dropEffect = 'move';
|
||
this.classList.add('drag-over');
|
||
});
|
||
item.addEventListener('dragleave', function() {
|
||
this.classList.remove('drag-over');
|
||
});
|
||
item.addEventListener('drop', function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
this.classList.remove('drag-over');
|
||
var targetId = parseInt(this.dataset.id);
|
||
var targetQuadrant = this.dataset.quadrant;
|
||
if (dragSourceId === targetId) return;
|
||
|
||
var fromIdx = -1, toIdx = -1;
|
||
for (var j = 0; j < wishes.length; j++) {
|
||
if (wishes[j].id === dragSourceId) fromIdx = j;
|
||
if (wishes[j].id === targetId) toIdx = j;
|
||
}
|
||
if (fromIdx >= 0 && toIdx >= 0) {
|
||
var moved = wishes.splice(fromIdx, 1)[0];
|
||
wishes.splice(toIdx, 0, moved);
|
||
// 更新象限
|
||
if (dragSourceQuadrant !== targetQuadrant) {
|
||
moved.quadrant = targetQuadrant;
|
||
fetch('/api/wishes/' + moved.id, {
|
||
method: 'PUT',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({quadrant: targetQuadrant})
|
||
});
|
||
}
|
||
renderWishes();
|
||
saveWishOrder();
|
||
}
|
||
});
|
||
});
|
||
|
||
// 象限空区域接受 drop
|
||
document.querySelectorAll('.quad-cell').forEach(function(cell) {
|
||
cell.addEventListener('dragover', function(e) {
|
||
if (this.querySelector('.wish-item')) return; // 有内容时让 item 处理
|
||
e.preventDefault();
|
||
this.classList.add('drag-over');
|
||
});
|
||
cell.addEventListener('dragleave', function() { this.classList.remove('drag-over'); });
|
||
cell.addEventListener('drop', function(e) {
|
||
this.classList.remove('drag-over');
|
||
if (this.querySelector('.wish-item')) return; // 已由 item 处理
|
||
e.preventDefault();
|
||
var q = this.dataset.quadrant;
|
||
var moved = wishes.find(function(w){ return w.id === dragSourceId; });
|
||
if (moved) {
|
||
moved.quadrant = q;
|
||
fetch('/api/wishes/' + moved.id, {
|
||
method: 'PUT',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({quadrant: q})
|
||
});
|
||
renderWishes();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function saveWishOrder() {
|
||
var order = wishes.map(function(w){ return w.id; });
|
||
fetch('/api/wishes/reorder', {
|
||
method: 'PUT',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({order: order})
|
||
});
|
||
}
|
||
|
||
/* ── CRUD ── */
|
||
|
||
window.showWishForm = function() {
|
||
var form = document.getElementById('wish-form');
|
||
if (form) form.style.display = 'block';
|
||
};
|
||
|
||
window.hideWishForm = function() {
|
||
var form = document.getElementById('wish-form');
|
||
if (form) form.style.display = 'none';
|
||
document.getElementById('wish-name').value = '';
|
||
document.getElementById('wish-deadline').value = '';
|
||
var sel = document.getElementById('wish-quadrant');
|
||
if (sel) sel.value = '重要不紧急';
|
||
};
|
||
|
||
window.addWish = function() {
|
||
var name = document.getElementById('wish-name').value.trim();
|
||
if (!name) { showToast('请输入心愿名称', 'error'); return; }
|
||
var quadrant = document.getElementById('wish-quadrant').value;
|
||
var deadline = document.getElementById('wish-deadline').value;
|
||
fetch('/api/wishes', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({name: name, quadrant: quadrant, deadline: deadline})
|
||
}).then(function(r){ return r.json(); })
|
||
.then(function(res){
|
||
if (!res.ok) { showToast(res.error, 'error'); return; }
|
||
hideWishForm();
|
||
wishes.push({id: res.id, name: name, quadrant: quadrant, deadline: deadline, done: 0});
|
||
renderWishes();
|
||
});
|
||
};
|
||
|
||
window.toggleWish = function(id, done) {
|
||
fetch('/api/wishes/' + id, {
|
||
method: 'PUT',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({done: done ? 1 : 0})
|
||
});
|
||
var w = wishes.find(function(x){ return x.id === id; });
|
||
if (w) w.done = done ? 1 : 0;
|
||
renderWishes();
|
||
};
|
||
|
||
window.deleteWishItem = function(id) {
|
||
fetch('/api/wishes/' + id, {method: 'DELETE'});
|
||
wishes = wishes.filter(function(w){ return w.id !== id; });
|
||
renderWishes();
|
||
};
|
||
|
||
window.toggleWishesEdit = function(btn) {
|
||
var panel = document.getElementById('panel-wishes');
|
||
if (!panel) return;
|
||
var editing = panel.classList.toggle('editing');
|
||
btn.classList.toggle('active', editing);
|
||
};
|
||
|
||
window.renderWishes = renderWishes;
|
||
|
||
/* ================================================================
|
||
Init
|
||
================================================================ */
|
||
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;
|
||
|
||
selectedDate = todayStr;
|
||
initStudyPresets();
|
||
bindAutoSave();
|
||
lastSavedDate = todayStr;
|
||
|
||
// 从页面嵌入数据获取初始统计(0 延迟)
|
||
if (window.__INITIAL_STATS__) {
|
||
var s = window.__INITIAL_STATS__;
|
||
document.getElementById('stat-days').textContent = s.total_days;
|
||
document.getElementById('stat-morning').textContent = s.total_morning;
|
||
document.getElementById('stat-study').textContent = s.total_study;
|
||
calendarStatus = s.calendar || {};
|
||
}
|
||
|
||
loadCheckin();
|
||
loadStats(); // 异步刷新最新数据
|
||
renderCalendar(); // 同步渲染 — calendarStatus 已有值
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
})();
|