/* 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,'>'); } /* ================================================================ 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 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 += '
' + (prevMonthLastDay - p) + '
'; } // 当月 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 += '
' + d + '
'; } // 后月填充 — 始终 42 格(6行 × 7列),保持高度一致 var totalCells = startDayOfWeek + totalDays; var remaining = 42 - totalCells; for (var n = 1; n <= remaining; n++) { html += '
' + n + '
'; } 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' + '' + ''; container.appendChild(div); renumberMorning(); } function renumberMorning() { var rows = document.querySelectorAll('#morning-list .item-row'); for (var i=0; i' + (idx+1) + '.' + '' + '' + '
' + '
' + '
' + '
'; container.appendChild(div); renumberEvening(); } function renumberEvening() { var rows = document.querySelectorAll('#evening-list .evening-row'); for (var i=0; i' + '' + esc(name) + '' + '' + ''; 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= 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 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' + '
' + cell.weekday + '
' + '
' + shortDate + '
' + '
' + cell.dayScore + '
' + '
' + badgeText + '
' + ''; } document.getElementById('week-days-grid').innerHTML = gridHtml; }); } /* ================================================================ History ================================================================ */ function loadHistory() { apiGetHistory(function(err, rows){ var el = document.getElementById('history-grid'); if (err) { el.innerHTML = '
加载失败
'; return; } if (!rows || rows.length === 0) { el.innerHTML = '
暂无历史记录
'; return; } var html = ''; for (var i=0; i 立志 ' + morning.length + ''); if (evening.length) tags.push(' 改过 ' + evening.length + ''); if (study.length) tags.push(' 勤学 ' + study.length + ''); html += '
' + '
' + '
' + r.date + '
' + '
' + (tags.join('') || '暂无内容') + '
' + '
' + '' + '
'; } 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 += '
' + '' + '' + '' + esc(w.name) + '' + '' + esc(w.priority) + '' + (deadline ? '' + esc(deadline) + '' : '') + '' + '
'; } 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(); } })();