Files
ziwei-power/static/app.js
mac 03745e19c3 v1.4.2 — 自动归类取代手动选择板块
- 后端关键词自动归类立志/责善/勤学到六板块
- 移除DOM板块选择器,简化UI
- 蓝图面板展示各板块立志·责善·勤学统计+详情列表
- 旧数据自动兼容、新数据无需手动选分类
2026-06-04 15:58:56 +08:00

1077 lines
40 KiB
JavaScript
Raw 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 能力提升',
'知识库能力提升'
];
/* 蓝图六板块 */
var PILLARS = [
{key:'medical_service', name:'医疗服务', emoji:'🥼'},
{key:'pharma_mkt', name:'医药营销', emoji:'💊'},
{key:'payment', name:'医疗支付', emoji:'🛡️'},
{key:'ai', name:'AI 智能', emoji:'🤖'},
{key:'governance', name:'公司治理', emoji:'🏛️'},
{key:'self_cultivation', name:'个人修养', emoji:'🎯'}
];
/* ================================================================
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();
if (name === 'blueprint') loadBlueprint();
};
/* ================================================================
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 inputs = eRows[j].querySelectorAll('input[type="text"]');
var mistake = inputs[0] ? inputs[0].value.trim() : '';
var improve = inputs[1] ? inputs[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++) {
var txt = typeof morning[m] === 'string' ? morning[m] : (morning[m].text || '');
addMorningRow(txt);
}
// 责善改过
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;
var text = typeof val === 'string' ? val : (val && val.text ? val.text : '');
text = text || '';
var div = document.createElement('div');
div.className = 'item-row';
div.innerHTML = '<span class="idx">' + (idx+1) + '.</span>' +
'<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();
}
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');
mistake = mistake || '';
improve = improve || '';
var div = document.createElement('div');
div.className = 'evening-row';
div.innerHTML =
'<button class="btn-del" onclick="this.parentElement.remove()"><svg class="icon-sm"><use href="#icon-x"/></svg></button>' +
'<input type="text" value="' + esc(mistake) + '" placeholder="犯的错误…">' +
'<input type="text" value="' + esc(improve) + '" placeholder="改进方案…">';
container.appendChild(div);
}
/* ── 勤学预设项目 ── */
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.syncCalendar = function() {
var overlay = document.getElementById('sync-modal-overlay');
var loading = document.getElementById('sync-loading');
var resultsEl = document.getElementById('sync-results');
var summary = document.getElementById('sync-summary');
var list = document.getElementById('sync-list');
var btn = document.querySelector('.btn-cal-sync');
if (!overlay) return;
// 打开弹窗,显示加载中
overlay.style.display = 'flex';
loading.style.display = 'block';
resultsEl.style.display = 'none';
if (btn) { btn.style.opacity = '0.5'; btn.disabled = true; }
fetch('/api/calendar-sync-all')
.then(function(r){ return r.json(); })
.then(function(res){
loading.style.display = 'none';
resultsEl.style.display = 'block';
if (btn) { btn.style.opacity = '1'; btn.disabled = false; }
if (!res.ok) { summary.innerHTML = '<span style="color:var(--danger)">同步失败: ' + (res.error || '未知错误') + '</span>'; return; }
var data = res.data || [];
if (data.length === 0) {
summary.innerHTML = '未查询到任何日程';
return;
}
// 统计
var totalDays = data.length;
var totalEvents = 0;
data.forEach(function(d){ totalEvents += d.events.length; });
summary.innerHTML = '同步完成!共 <b>' + totalDays + '</b> 天有日程,合计 <b>' + totalEvents + '</b> 个会议';
// 渲染结果列表
var html = '';
data.forEach(function(day) {
html += '<div class="sync-day">';
html += '<div class="sync-day-header"><span>' + day.date + '</span></div>';
day.events.forEach(function(e) {
var timeStr = e.time ? ' (' + e.time + ')' : '';
html += '<div class="sync-event">' +
'<span class="sync-event-icon">📌</span>' +
'<span class="sync-event-text">' + esc(e.summary) + '<span class="sync-event-time">' + esc(timeStr) + '</span></span>' +
'</div>';
});
html += '</div>';
});
list.innerHTML = html;
// 自动填充今天到选中日期的日程
autoFillTodayEvents(data);
})
.catch(function(){
loading.style.display = 'none';
resultsEl.style.display = 'block';
summary.innerHTML = '<span style="color:var(--danger)">网络错误,请重试</span>';
if (btn) { btn.style.opacity = '1'; btn.disabled = false; }
});
};
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';
};
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"], select')) {
triggerAutoSave();
}
}, true);
// 选择器change 事件也保存(及时响应)
panel.addEventListener('change', function(e) {
if (e.target.matches('select.morning-pillar')) {
triggerAutoSave();
}
});
// 复选框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 normalizeQuadrant(w) {
if (QUADRANTS.indexOf(w.quadrant) >= 0) return w.quadrant;
// 老数据回退: priority → quadrant
var map = {'高': '重要紧急', '中': '重要不紧急', '低': '不紧急不重要'};
return map[w.priority] || '重要不紧急';
}
function loadWishes() {
if (wishesLoaded) { renderWishes(); return; }
if (window.__INITIAL_WISHES__) {
wishes = window.__INITIAL_WISHES__;
// 规范化 quadrant
wishes.forEach(function(w){ w.quadrant = normalizeQuadrant(w); });
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 normalizeQuadrant(w) === 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 === normalizeQuadrant(w) ? ' 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})
}).catch(function(){ showToast('保存失败', 'error'); });
}
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})
}).catch(function(){ showToast('保存失败', 'error'); });
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;
/* ================================================================
Blueprint — 蓝图六板块
================================================================ */
function loadBlueprint() {
apiGetStats(function(err, data) {
if (err || !data || !data.pillar_breakdown) return;
var pb = data.pillar_breakdown;
PILLARS.forEach(function(p){
var el = document.getElementById('bp-count-' + p.name);
var info = pb[p.name] || {};
var m = info.morning || 0, e = info.evening || 0, s = info.study || 0;
if (el) el.textContent = '立志' + m + ' · 责善' + e + ' · 勤学' + s;
// 显示详情列表
var detailEl = document.getElementById('bp-detail-' + p.name);
if (detailEl) {
var html = '';
(info.morning_items || []).forEach(function(mi){
html += '<div class="bp-item" data-type="morning">🥼 ' + esc(mi.text) + '</div>';
});
(info.evening_items || []).forEach(function(ei){
html += '<div class="bp-item" data-type="evening">🔍 ' + esc(ei.mistake || ei.improvement) + '</div>';
});
(info.study_items || []).forEach(function(si){
html += '<div class="bp-item" data-type="study">📚 ' + esc(si.name) + '</div>';
});
detailEl.innerHTML = html || '<div class="bp-empty">暂无记录</div>';
}
});
});
}
/* ================================================================
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();
}
})();