882 lines
33 KiB
JavaScript
882 lines
33 KiB
JavaScript
/* ziwei-power 前端逻辑 — Flomo 风格左右布局 */
|
||
(function() {
|
||
'use strict';
|
||
|
||
/* ================================================================
|
||
State
|
||
================================================================ */
|
||
var calendarStatus = {}; // { "2026-W30": "pass", ... }
|
||
var calYear;
|
||
var calMonth = new Date().getMonth(); // 月份索引 0-11
|
||
var activePanel = 'daily';
|
||
var lastSavedData = null;
|
||
var lastSavedWeek = null;
|
||
var selectedWeek = 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(week, cb) {
|
||
fetch('/api/checkin?week=' + encodeURIComponent(week))
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(res){ if (res.ok) cb(null, res.data); else cb(res.error); })
|
||
.catch(function(e){ cb(e.message); });
|
||
}
|
||
|
||
function apiSaveCheckin(week, data, cb) {
|
||
fetch('/api/checkin', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({week: week, data: data})
|
||
})
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(res){ if (res.ok) cb(null); else cb(res.error); })
|
||
.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); 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 === 'history') loadHistory();
|
||
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;
|
||
}
|
||
|
||
/** 获取当前周的 YYYY-WW 格式 */
|
||
function fmtWeek(d) {
|
||
var jan1 = new Date(d.getFullYear(), 0, 1);
|
||
var dayOfYear = Math.floor((d - jan1) / 86400000);
|
||
var week = Math.ceil((dayOfYear + jan1.getDay() + 1) / 7);
|
||
return d.getFullYear() + '-W' + ('0' + week).slice(-2);
|
||
}
|
||
|
||
/** 切换日历年 */
|
||
window.changeCalYear = function(dir) {
|
||
calYear += dir;
|
||
renderCalendar();
|
||
};
|
||
|
||
/** 切换月份 */
|
||
window.changeCalMonth = function(dir) {
|
||
calMonth += dir;
|
||
if (calMonth > 11) { calMonth = 0; calYear++; }
|
||
if (calMonth < 0) { calMonth = 11; calYear--; }
|
||
renderCalendar();
|
||
};
|
||
|
||
/** 渲染年-月-周选择器 */
|
||
function renderCalendar() {
|
||
document.getElementById('cal-year-label').textContent = calYear + '年';
|
||
document.getElementById('cal-month-label').textContent = (calMonth + 1) + '月';
|
||
|
||
// 计算该月包含的周
|
||
var firstDay = new Date(calYear, calMonth, 1);
|
||
var lastDay = new Date(calYear, calMonth + 1, 0);
|
||
var monday = new Date(firstDay);
|
||
monday.setDate(monday.getDate() - monday.getDay() + (monday.getDay() === 0 ? -6 : 1));
|
||
var weeks = [];
|
||
while (monday <= lastDay) {
|
||
weeks.push(fmtWeek(monday));
|
||
monday.setDate(monday.getDate() + 7);
|
||
}
|
||
|
||
var todayWk = fmtWeek(new Date());
|
||
var today = new Date();
|
||
var wHtml = '';
|
||
for (var i2 = 0; i2 < weeks.length; i2++) {
|
||
var w = weeks[i2];
|
||
var cls = 'cal-week-row';
|
||
var status = calendarStatus[w];
|
||
if (w === todayWk) cls += ' today';
|
||
if (w === selectedWeek) cls += ' selected';
|
||
// 计算日期范围
|
||
var monday = new Date(calYear, calMonth, 1);
|
||
while (fmtWeek(monday) !== w) monday.setDate(monday.getDate() + 7);
|
||
var sunday = new Date(monday);
|
||
sunday.setDate(sunday.getDate() + 6);
|
||
var dateRange = (monday.getMonth()+1) + '.' + monday.getDate() + ' - ' + sunday.getDate();
|
||
var statusLabel = status === 'pass' ? '达标' : (status === 'fail' ? '未达标' : '未打卡');
|
||
wHtml += '<div class="' + cls + '" data-week="' + w + '">' +
|
||
'<div class="cal-wk-info">' +
|
||
'<span class="cal-wk-label">第' + w.split('-W')[1].replace(/^0/, '') + '周</span>' +
|
||
'<span class="cal-wk-date">' + dateRange + '</span>' +
|
||
'</div>' +
|
||
'<span class="cal-wk-badge">' + statusLabel + '</span>' +
|
||
'</div>';
|
||
}
|
||
var we = document.getElementById('cal-weeks');
|
||
we.innerHTML = wHtml;
|
||
we.querySelectorAll('.cal-week-row').forEach(function(row) {
|
||
row.addEventListener('click', function() {
|
||
var wk = this.dataset.week;
|
||
if (wk) navigateToWeek(wk);
|
||
});
|
||
});
|
||
}
|
||
|
||
function navigateToWeek(week) {
|
||
selectedWeek = week;
|
||
switchPanel('daily');
|
||
document.getElementById('check-date').value = week;
|
||
loadCheckin();
|
||
renderCalendar();
|
||
}
|
||
|
||
window.goToCurrentWeek = function() {
|
||
var now = new Date();
|
||
calYear = now.getFullYear();
|
||
calMonth = now.getMonth();
|
||
renderCalendar();
|
||
navigateToWeek(fmtWeek(now));
|
||
};
|
||
|
||
/* ================================================================
|
||
Stats — 加载统计数据并更新侧边栏
|
||
================================================================ */
|
||
function loadStats() {
|
||
apiGetStats(function(err, data) {
|
||
if (!err && data) {
|
||
document.getElementById('stat-days').textContent = data.total_weeks;
|
||
document.getElementById('stat-morning').textContent = data.total_morning;
|
||
document.getElementById('stat-study').textContent = data.total_study;
|
||
calendarStatus = data.calendar || {};
|
||
}
|
||
renderCalendar();
|
||
});
|
||
}
|
||
|
||
/* ================================================================
|
||
Build / Fill Data
|
||
================================================================ */
|
||
|
||
function buildData() {
|
||
var morningItems = [];
|
||
var mRows = document.querySelectorAll('#morning-list .item-row');
|
||
for (var i=0; i<mRows.length; i++) {
|
||
var input = mRows[i].querySelector('input[type="text"]');
|
||
var noteEl = mRows[i].querySelector('.item-note-val');
|
||
var v = input ? input.value.trim() : '';
|
||
var n = noteEl ? noteEl.value.trim() : '';
|
||
if (v) morningItems.push(n ? {text: v, note: n} : v);
|
||
}
|
||
// 收集日历日程
|
||
var calEls = document.querySelectorAll('#morning-calendar-list .cal-task-item span:last-child');
|
||
for (var ci=0; ci<calEls.length; ci++) {
|
||
var txt = calEls[ci].textContent.trim();
|
||
if (txt) { morningItems.push(txt); }
|
||
}
|
||
|
||
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 sCards = document.querySelectorAll('#study-cards .study-card');
|
||
for (var k=0; k<sCards.length; k++) {
|
||
var nameEl = sCards[k].querySelector('.study-card-name');
|
||
var countEl = sCards[k].querySelector('.study-card-count');
|
||
var name = nameEl ? nameEl.textContent.trim() : '';
|
||
var count = parseInt(countEl ? countEl.textContent : '0') || 0;
|
||
studyItems.push({ name: name, count: count, last_checkin_date: sCards[k].dataset.lastDate || '' });
|
||
}
|
||
|
||
return { morning: morningItems, evening: eveningItems, study: studyItems };
|
||
}
|
||
|
||
function fillForm(data) {
|
||
data = data || {};
|
||
var morning = data.morning || [];
|
||
var evening = data.evening || [];
|
||
var study = data.study || [];
|
||
|
||
// 本周重点:拆分手动任务 vs 日历日程
|
||
document.getElementById('morning-list').innerHTML = '';
|
||
document.getElementById('morning-calendar-list').innerHTML = '';
|
||
var manualItems = [];
|
||
var calItems = [];
|
||
if (morning.length === 0) morning = [''];
|
||
for (var m=0; m<morning.length; m++) {
|
||
var txt = typeof morning[m] === 'string' ? morning[m] : (morning[m].text || '');
|
||
if (txt && txt.trim().startsWith('【')) { calItems.push(txt); }
|
||
else { manualItems.push(txt); }
|
||
}
|
||
if (manualItems.length === 0) manualItems = [''];
|
||
for (var a=0; a<manualItems.length; a++) { addMorningRow(manualItems[a]); }
|
||
if (calItems.length > 0) {
|
||
document.getElementById('morning-calendar').style.display = 'block';
|
||
for (var b=0; b<calItems.length; b++) {
|
||
var div = document.createElement('div');
|
||
div.className = 'cal-task-item';
|
||
div.innerHTML = '<span class="cal-task-dot">📅</span><span>' + esc(calItems[b]) + '</span>';
|
||
document.getElementById('morning-calendar-list').appendChild(div);
|
||
}
|
||
} else {
|
||
document.getElementById('morning-calendar').style.display = 'none';
|
||
}
|
||
|
||
// 责善改过
|
||
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];
|
||
}
|
||
var rendered = {};
|
||
document.getElementById('study-cards').innerHTML = '';
|
||
for (var sk=0; sk<study.length; sk++) {
|
||
var item = study[sk];
|
||
if (item.name) {
|
||
renderStudyCard(item.name, item.count || 0, item.last_checkin_date || '');
|
||
rendered[item.name] = true;
|
||
}
|
||
}
|
||
for (var sp=0; sp<PRESET_STUDY_ITEMS.length; sp++) {
|
||
var sName = PRESET_STUDY_ITEMS[sp];
|
||
if (!rendered[sName]) {
|
||
renderStudyCard(sName, 0, '');
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ================================================================
|
||
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 note = (val && val.note) ? val.note : '';
|
||
var hasNote = !!note;
|
||
var div = document.createElement('div');
|
||
div.className = 'item-row';
|
||
div.draggable = true;
|
||
div.innerHTML = '<span class="drag-handle" title="拖动排序">⋮⋮</span>' +
|
||
'<span class="idx">' + (idx+1) + '.</span>' +
|
||
'<input type="text" value="' + esc(text) + '" placeholder="本周最重要的一件事…">' +
|
||
'<button class="btn-note-toggle' + (hasNote ? ' has-note' : '') + '" onclick="openNoteModal(this)" title="备注">💬</button>' +
|
||
'<button class="btn-del" onclick="this.parentElement.remove();renumberMorning()"><svg class="icon-sm"><use href="#icon-x"/></svg></button>';
|
||
// 隐藏存储备注
|
||
var noteHidden = document.createElement('input');
|
||
noteHidden.type = 'hidden';
|
||
noteHidden.className = 'item-note-val';
|
||
noteHidden.value = note;
|
||
div.appendChild(noteHidden);
|
||
div.addEventListener('dragstart', dragStart);
|
||
div.addEventListener('dragover', dragOver);
|
||
div.addEventListener('drop', function(e){ dragDrop.call(this, e, 'morning-list'); });
|
||
div.addEventListener('dragend', dragEnd);
|
||
container.appendChild(div);
|
||
renumberMorning();
|
||
}
|
||
|
||
var noteModalRow = null;
|
||
|
||
window.openNoteModal = function(btn) {
|
||
noteModalRow = btn.closest('.item-row');
|
||
var noteEl = noteModalRow.querySelector('.item-note-val');
|
||
var input = noteModalRow.querySelector('input[type="text"]');
|
||
var title = input ? input.value.trim() || '未命名任务' : '备注';
|
||
document.getElementById('note-modal-title').textContent = title;
|
||
document.getElementById('note-modal-textarea').value = noteEl ? noteEl.value : '';
|
||
document.getElementById('note-modal').style.display = 'flex';
|
||
};
|
||
|
||
window.saveNoteModal = function() {
|
||
var modal = document.getElementById('note-modal');
|
||
if (!noteModalRow) return;
|
||
var noteEl = noteModalRow.querySelector('.item-note-val');
|
||
var btn = noteModalRow.querySelector('.btn-note-toggle');
|
||
var val = document.getElementById('note-modal-textarea').value.trim();
|
||
if (noteEl) noteEl.value = val;
|
||
if (btn) { if (val) { btn.classList.add('has-note'); } else { btn.classList.remove('has-note'); } }
|
||
modal.style.display = 'none';
|
||
noteModalRow = null;
|
||
triggerAutoSave();
|
||
};
|
||
|
||
window.closeNoteModal = function() {
|
||
document.getElementById('note-modal').style.display = 'none';
|
||
noteModalRow = null;
|
||
};
|
||
|
||
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.draggable = true;
|
||
div.innerHTML =
|
||
'<span class="drag-handle evening-drag" title="拖动排序">⋮⋮</span>' +
|
||
'<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="改进方案…">';
|
||
div.addEventListener('dragstart', dragStart);
|
||
div.addEventListener('dragover', dragOver);
|
||
div.addEventListener('drop', function(e){ dragDrop.call(this, e, 'evening-list'); });
|
||
div.addEventListener('dragend', dragEnd);
|
||
container.appendChild(div);
|
||
}
|
||
|
||
/* ── 勤学打卡卡片 ── */
|
||
|
||
function renderStudyCard(name, count, lastDate) {
|
||
var container = document.getElementById('study-cards');
|
||
var today = fmtDate(new Date());
|
||
var checked = count > 0;
|
||
var checkedToday = lastDate === today;
|
||
var btnText = checkedToday ? '已打卡' : '打卡';
|
||
var div = document.createElement('div');
|
||
div.className = 'study-card' + (checked ? ' checked' : '');
|
||
div.setAttribute('data-last-date', lastDate || '');
|
||
div.innerHTML =
|
||
'<button class="study-card-del" onclick="deleteStudyItem(this)" title="删除">×</button>' +
|
||
'<div class="study-card-info">' +
|
||
'<span class="study-card-name">' + esc(name) + '</span>' +
|
||
'<span class="study-card-count">' + count + '</span>' +
|
||
'</div>' +
|
||
'<button class="study-card-btn' + (checkedToday ? ' done' : '') + '" onclick="checkinStudy(this)">' + btnText + '</button>';
|
||
container.appendChild(div);
|
||
}
|
||
|
||
window.toggleStudyEdit = function() {
|
||
var panel = document.getElementById('panel-daily');
|
||
var btn = document.getElementById('study-edit-toggle');
|
||
if (!panel || !btn) return;
|
||
var editing = panel.classList.toggle('editing-study');
|
||
btn.classList.toggle('active', editing);
|
||
};
|
||
|
||
/* ── 拖拽排序 ── */
|
||
var dragSrc = null;
|
||
|
||
function dragStart(e) {
|
||
dragSrc = this;
|
||
this.classList.add('dragging');
|
||
e.dataTransfer.effectAllowed = 'move';
|
||
}
|
||
|
||
function dragOver(e) {
|
||
e.preventDefault();
|
||
e.dataTransfer.dropEffect = 'move';
|
||
if (this !== dragSrc) this.classList.add('drag-over');
|
||
}
|
||
|
||
function dragDrop(e, listId) {
|
||
e.preventDefault();
|
||
this.classList.remove('drag-over');
|
||
if (dragSrc === this) return;
|
||
var container = document.getElementById(listId);
|
||
var items = container.querySelectorAll('.' + (dragSrc.classList.contains('evening-row') ? 'evening-row' : 'item-row'));
|
||
var from = Array.prototype.indexOf.call(items, dragSrc);
|
||
var to = Array.prototype.indexOf.call(items, this);
|
||
if (from < to) { container.insertBefore(dragSrc, this.nextSibling); }
|
||
else { container.insertBefore(dragSrc, this); }
|
||
if (listId === 'morning-list') renumberMorning();
|
||
triggerAutoSave();
|
||
}
|
||
|
||
function dragEnd() {
|
||
this.classList.remove('dragging');
|
||
document.querySelectorAll('.drag-over').forEach(function(el){ el.classList.remove('drag-over'); });
|
||
dragSrc = null;
|
||
}
|
||
|
||
window.addStudyItem = function() {
|
||
var name = prompt('请输入新习惯名称:');
|
||
if (!name || !name.trim()) return;
|
||
renderStudyCard(name.trim(), 0, '');
|
||
triggerAutoSave();
|
||
};
|
||
|
||
window.deleteStudyItem = function(btn) {
|
||
var card = btn.closest('.study-card');
|
||
var nameEl = card.querySelector('.study-card-name');
|
||
if (nameEl && !confirm('确定删除「' + nameEl.textContent + '」?')) return;
|
||
card.remove();
|
||
triggerAutoSave();
|
||
};
|
||
|
||
window.checkinStudy = function(btn) {
|
||
var card = btn.closest('.study-card');
|
||
var today = fmtDate(new Date());
|
||
if (card.dataset.lastDate === today) { showToast('今天已打卡', 'info'); return; }
|
||
var countEl = card.querySelector('.study-card-count');
|
||
var count = parseInt(countEl.textContent) + 1;
|
||
countEl.textContent = count;
|
||
card.dataset.lastDate = today;
|
||
card.classList.add('checked');
|
||
btn.classList.add('done');
|
||
btn.textContent = '已打卡';
|
||
triggerAutoSave();
|
||
};
|
||
|
||
function initStudyPresets() {
|
||
document.getElementById('study-cards').innerHTML = '';
|
||
for (var i=0; i<PRESET_STUDY_ITEMS.length; i++) {
|
||
renderStudyCard(PRESET_STUDY_ITEMS[i], 0, '');
|
||
}
|
||
}
|
||
|
||
/* ── 日历同步 ── */
|
||
|
||
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> 个会议' + (res.saved_days ? ',已自动填充 <b>' + res.saved_days + '</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 : '';
|
||
var dateStr = e.date || '';
|
||
html += '<div class="sync-event">' +
|
||
'<span class="sync-event-icon">📌</span>' +
|
||
'<span class="sync-event-text">' + esc(e.summary) + '</span>' +
|
||
'<span class="sync-event-meta">' + esc(dateStr) + (timeStr ? ' ' + esc(timeStr) : '') + '</span>' +
|
||
'</div>';
|
||
});
|
||
html += '</div>';
|
||
});
|
||
list.innerHTML = html;
|
||
|
||
// 刷新当前页
|
||
loadCheckin();
|
||
})
|
||
.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; }
|
||
});
|
||
};
|
||
|
||
window.closeSyncModal = function() {
|
||
var overlay = document.getElementById('sync-modal-overlay');
|
||
if (overlay) overlay.style.display = 'none';
|
||
};
|
||
|
||
window.addMorning = function() {
|
||
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);
|
||
};
|
||
|
||
/* 切换每周打卡 Tab */
|
||
window.switchDailyTab = function(btn) {
|
||
var tab = btn.dataset.tab;
|
||
document.querySelectorAll('.daily-tab').forEach(function(t){ t.classList.remove('active'); });
|
||
btn.classList.add('active');
|
||
document.querySelectorAll('.daily-card').forEach(function(c){ c.classList.remove('active'); });
|
||
var card = document.querySelector('.daily-card[data-card="' + tab + '"]');
|
||
if (card) card.classList.add('active');
|
||
};
|
||
|
||
/* 头像菜单 */
|
||
window.toggleUserMenu = function(e) {
|
||
e.stopPropagation();
|
||
var dd = document.getElementById('user-dropdown');
|
||
if (dd) dd.style.display = dd.style.display === 'none' ? 'block' : 'none';
|
||
};
|
||
document.addEventListener('click', function() {
|
||
var dd = document.getElementById('user-dropdown');
|
||
if (dd) dd.style.display = 'none';
|
||
});
|
||
|
||
/* ================================================================
|
||
Load & Auto Save Checkin
|
||
================================================================ */
|
||
|
||
window.loadCheckin = function() {
|
||
var week = document.getElementById('check-date').value;
|
||
if (!week) return;
|
||
|
||
var currentData = buildData();
|
||
var currentStr = JSON.stringify(currentData);
|
||
if (lastSavedWeek && lastSavedData !== null && currentStr !== lastSavedData) {
|
||
var hasContent = currentData.morning.some(function(x){return x && x.trim();}) ||
|
||
currentData.evening.some(function(x){return (x.mistake||'').trim() || (x.improvement||'').trim();}) ||
|
||
currentData.study.some(function(x){return x.count > 0;});
|
||
if (hasContent) {
|
||
apiSaveCheckin(lastSavedWeek, currentData, function(){});
|
||
}
|
||
}
|
||
|
||
lastSavedWeek = week;
|
||
apiGetCheckin(week, function(err, row){
|
||
if (err) { initStudyPresets(); lastSavedData = '{}'; return; }
|
||
fillForm(row ? row.data : null);
|
||
lastSavedData = JSON.stringify(buildData());
|
||
});
|
||
};
|
||
|
||
/** 自动保存 — 失去焦点/勾选/删除时触发 */
|
||
function triggerAutoSave() {
|
||
var week = document.getElementById('check-date').value;
|
||
if (!week) return;
|
||
var data = buildData();
|
||
var dataStr = JSON.stringify(data);
|
||
if (dataStr === lastSavedData) return;
|
||
apiSaveCheckin(week, data, function(err){
|
||
if (err) { showToast('保存失败', 'error'); return; }
|
||
lastSavedData = dataStr;
|
||
loadStats();
|
||
});
|
||
}
|
||
|
||
/** 为每周打卡面板绑定自动保存事件 */
|
||
function bindAutoSave() {
|
||
var panel = document.getElementById('panel-daily');
|
||
if (!panel) return;
|
||
|
||
// 输入框 & 文本域 & 下拉选择器:失去焦点时保存
|
||
panel.addEventListener('blur', function(e) {
|
||
if (e.target.matches('input[type="text"], textarea, input[type="week"], 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 更新后
|
||
}
|
||
});
|
||
}
|
||
|
||
/* ================================================================
|
||
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 (typeof x === 'string' && x.trim()) || (typeof x === 'object' && (x.text||'').trim());
|
||
});
|
||
var evening = (r.data.evening || []).filter(function(x){
|
||
var mst = typeof x === 'string' ? x : (x.mistake || '');
|
||
return mst && typeof mst === 'string' && mst.trim();
|
||
});
|
||
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 + '/8</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(week) {
|
||
if (!confirm('确定删除 ' + week + ' 的打卡记录吗?此操作不可恢复!')) return;
|
||
apiDeleteCheckin(week, function(err){
|
||
if (err) { showToast('删除失败: ' + err, 'error'); return; }
|
||
showToast('已删除', 'info');
|
||
loadHistory();
|
||
loadStats();
|
||
});
|
||
};
|
||
|
||
/* ================================================================
|
||
Blueprint — 蓝图六板块
|
||
================================================================ */
|
||
|
||
var blueprintData = null;
|
||
var blueprintFilter = 'all';
|
||
|
||
function loadBlueprint() {
|
||
apiGetStats(function(err, data) {
|
||
if (err || !data || !data.pillar_breakdown) return;
|
||
blueprintData = data.pillar_breakdown;
|
||
renderBlueprintList();
|
||
});
|
||
}
|
||
|
||
function renderBlueprintPillars(pb) {
|
||
var container = document.getElementById('blueprint-pillars');
|
||
if (!container) return;
|
||
var html = '';
|
||
PILLARS.forEach(function(p){
|
||
html += '<div class="blueprint-pillar" data-pillar="' + p.name + '">' +
|
||
'<div class="bp-icon">' + p.emoji + '</div>' +
|
||
'<div class="bp-name">' + p.name + '</div>' +
|
||
'</div>';
|
||
});
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
function renderBlueprintList() {
|
||
var list = document.getElementById('bp-list');
|
||
var empty = document.getElementById('bp-list-empty');
|
||
if (!list || !blueprintData) return;
|
||
var items = [];
|
||
PILLARS.forEach(function(p){
|
||
var info = blueprintData[p.name] || {};
|
||
if (blueprintFilter === 'all' || blueprintFilter === 'morning') {
|
||
(info.morning_items || []).forEach(function(mi){ items.push({pillar: p, text: mi.text, type: 'morning'}); });
|
||
}
|
||
if (blueprintFilter === 'all' || blueprintFilter === 'evening') {
|
||
(info.evening_items || []).forEach(function(ei){ items.push({pillar: p, text: ei.mistake || ei.improvement, improvement: ei.improvement, type: 'evening'}); });
|
||
}
|
||
if (blueprintFilter === 'all' || blueprintFilter === 'study') {
|
||
(info.study_items || []).forEach(function(si){ items.push({pillar: p, text: si.name, type: 'study', count: si.count}); });
|
||
}
|
||
});
|
||
if (items.length === 0) {
|
||
list.innerHTML = '';
|
||
if (empty) empty.style.display = 'block';
|
||
return;
|
||
}
|
||
if (empty) empty.style.display = 'none';
|
||
var typeLabels = {morning: '立志', evening: '责善', study: '勤学'};
|
||
var pillarColors = {
|
||
'医疗服务': '#4A6CF7', '医药营销': '#D97706', '医疗支付': '#059669',
|
||
'AI 智能': '#7C3AED', '公司治理': '#DC2626', '个人修养': '#0891B2'
|
||
};
|
||
var html = '';
|
||
items.forEach(function(item){
|
||
var dot = '<span class="bp-pillar-dot" style="background:' + (pillarColors[item.pillar.name] || '#94A3B8') + '"></span>';
|
||
var extra = '';
|
||
if (item.type === 'study' && item.count > 0) extra = '<span class="bp-item-note">打卡 ' + item.count + ' 次</span>';
|
||
if (item.type === 'evening' && item.improvement) extra = '<span class="bp-item-note evening">→ ' + esc(item.improvement) + '</span>';
|
||
html += '<div class="bp-list-item">' +
|
||
dot +
|
||
'<span class="bp-item-pillar">' + item.pillar.name + '</span>' +
|
||
'<span class="bp-item-type ' + item.type + '">' + (typeLabels[item.type] || item.type) + '</span>' +
|
||
'<span class="bp-item-text">' + esc(item.text) + '</span>' +
|
||
extra +
|
||
'</div>';
|
||
});
|
||
list.innerHTML = html;
|
||
}
|
||
|
||
window.filterBlueprint = function(btn) {
|
||
blueprintFilter = btn.dataset.filter;
|
||
document.querySelectorAll('.bp-tab').forEach(function(t){ t.classList.remove('active'); });
|
||
btn.classList.add('active');
|
||
renderBlueprintList();
|
||
};
|
||
|
||
/* ================================================================
|
||
Init
|
||
================================================================ */
|
||
function init() {
|
||
var today = new Date();
|
||
calYear = today.getFullYear();
|
||
|
||
var cd = document.getElementById('check-date');
|
||
var todayWk = fmtWeek(today);
|
||
if (cd) cd.value = todayWk;
|
||
|
||
selectedWeek = todayWk;
|
||
initStudyPresets();
|
||
bindAutoSave();
|
||
lastSavedWeek = todayWk;
|
||
|
||
// 从页面嵌入数据获取初始统计(0 延迟)
|
||
if (window.__INITIAL_STATS__) {
|
||
var s = window.__INITIAL_STATS__;
|
||
document.getElementById('stat-days').textContent = s.total_weeks;
|
||
document.getElementById('stat-morning').textContent = s.total_morning;
|
||
document.getElementById('stat-study').textContent = s.total_study;
|
||
calendarStatus = s.calendar || {};
|
||
}
|
||
|
||
loadCheckin();
|
||
loadStats();
|
||
renderCalendar();
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
})();
|