Files
ziwei-power/static/app.js
mac 1d86d7f736 v1.1.1 — 心愿清单移至独立面板
- 从侧边栏移出,改为左侧菜单入口 → 独立页面
- 卡片式布局,对齐每周评分等面板风格
- 拖拽排序、完成勾选、编辑模式切换功能不变
2026-06-03 13:48:20 +08:00

836 lines
30 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
/* ================================================================
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 wishesLoaded = false;
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 list = document.getElementById('wishes-list');
var empty = document.getElementById('wishes-empty');
if (!list) return;
if (wishes.length === 0) {
list.innerHTML = '';
if (empty) empty.style.display = 'block';
return;
}
if (empty) empty.style.display = 'none';
var html = '';
for (var i = 0; i < wishes.length; i++) {
var w = wishes[i];
var doneCls = w.done ? ' done' : '';
var priCls = 'pri-' + (w.priority || '中');
var deadline = w.deadline ? w.deadline : '';
html += '<div class="wish-item' + doneCls + '" draggable="true" data-id="' + w.id + '">' +
'<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>' +
'<span class="wish-pri ' + priCls + '">' + esc(w.priority) + '</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();
}
/* ── Drag & Drop ── */
function bindDragEvents() {
var items = document.querySelectorAll('#wishes-list .wish-item');
items.forEach(function(item) {
item.addEventListener('dragstart', function(e) {
dragSourceId = parseInt(this.dataset.id);
this.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
});
item.addEventListener('dragend', function(e) {
this.classList.remove('dragging');
dragSourceId = null;
// 移除所有 over 状态
document.querySelectorAll('#wishes-list .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();
this.classList.remove('drag-over');
var targetId = parseInt(this.dataset.id);
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 item = wishes.splice(fromIdx, 1)[0];
wishes.splice(toIdx, 0, item);
renderWishes();
saveWishOrder();
}
});
});
}
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 = '';
document.getElementById('wish-priority').value = '中';
};
window.addWish = function() {
var name = document.getElementById('wish-name').value.trim();
if (!name) { showToast('请输入心愿名称', 'error'); return; }
var priority = document.getElementById('wish-priority').value;
var deadline = document.getElementById('wish-deadline').value;
fetch('/api/wishes', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name: name, priority: priority, 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, priority: priority, 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);
};
/* ================================================================
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();
}
})();