1051 lines
40 KiB
JavaScript
1051 lines
40 KiB
JavaScript
/* ziwei-power 前端逻辑 — Flomo 风格左右布局 */
|
||
(function() {
|
||
'use strict';
|
||
|
||
/* ================================================================
|
||
State
|
||
================================================================ */
|
||
var calendarStatus = {}; // { "2026-W30": "pass", ... }
|
||
var calYear; // 日历显示的年
|
||
var activePanel = 'daily';
|
||
var weekOffset = 0;
|
||
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 === '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;
|
||
}
|
||
|
||
/** 获取当前周的 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();
|
||
};
|
||
|
||
/** 渲染周历 */
|
||
function renderCalendar() {
|
||
document.getElementById('cal-year-label').textContent = calYear + '年';
|
||
var yearStart = new Date(calYear, 0, 1);
|
||
var yearEnd = new Date(calYear, 11, 31);
|
||
var weeks = [];
|
||
var d = new Date(yearStart);
|
||
// 推进到第一个周一
|
||
d.setDate(d.getDate() - d.getDay() + (d.getDay() === 0 ? -6 : 1));
|
||
while (d <= yearEnd) {
|
||
var wk = fmtWeek(d);
|
||
weeks.push(wk);
|
||
d.setDate(d.getDate() + 7);
|
||
}
|
||
var todayWk = fmtWeek(new Date());
|
||
var el = document.getElementById('cal-week-list');
|
||
var html = '';
|
||
for (var i = 0; i < weeks.length; i++) {
|
||
var w = weeks[i];
|
||
var cls = 'cal-week-cell';
|
||
var status = calendarStatus[w];
|
||
if (status) cls += ' ' + status;
|
||
if (w === todayWk) cls += ' today';
|
||
if (w === selectedWeek) cls += ' selected';
|
||
html += '<div class="' + cls + '" data-week="' + w + '" title="' + w + '">' +
|
||
'W' + w.split('-W')[1] + '</div>';
|
||
}
|
||
el.innerHTML = html;
|
||
el.querySelectorAll('.cal-week-cell').forEach(function(cell) {
|
||
cell.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 = fmtWeek(new Date());
|
||
calYear = new Date().getFullYear();
|
||
renderCalendar();
|
||
navigateToWeek(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 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> 个会议' + (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 + ')' : '';
|
||
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;
|
||
|
||
// 刷新当前页
|
||
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() {
|
||
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 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.done;});
|
||
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 更新后
|
||
}
|
||
});
|
||
}
|
||
|
||
/* ================================================================
|
||
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 wk = fmtWeek(today);
|
||
document.getElementById('week-label').textContent = wk;
|
||
|
||
apiGetCheckin(wk, function(err, row){
|
||
if (err) { showToast('加载失败', 'error'); return; }
|
||
var dd = row ? row.data : null;
|
||
if (!dd) {
|
||
document.getElementById('weekly-score').textContent = '--';
|
||
document.getElementById('score-text').textContent = '本周暂无记录';
|
||
document.getElementById('score-stats').textContent = '';
|
||
document.getElementById('week-days-grid').innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
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;
|
||
if (!morningCount) {
|
||
morningCount = morning.filter(function(x){
|
||
return x && typeof x === 'object' && (x.text || '').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 score, status;
|
||
if (allThree) {
|
||
score = 60 + (morningCount - 1) * 5 + (eveningCount - 1) * 3 + studyCount * 3;
|
||
status = 'pass';
|
||
} else {
|
||
score = 0;
|
||
status = 'fail';
|
||
}
|
||
|
||
var grade = score >= 90 ? '优秀' : score >= 75 ? '良好' : score >= 60 ? '达标' : '未达标';
|
||
|
||
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);
|
||
var ratio = Math.min(score / 100, 1);
|
||
ring.setAttribute('stroke-dashoffset', C - (C * ratio));
|
||
}
|
||
|
||
var txt = grade + ' · ' + score + ' 分';
|
||
document.getElementById('score-text').textContent = txt;
|
||
document.getElementById('score-stats').textContent =
|
||
'立志 ' + morningCount + ' · 责善 ' + eveningCount + ' · 勤学 ' + studyCount + '/8';
|
||
|
||
// 渲染周统计网格(简化版)
|
||
var gridHtml = '<div class="day-cell ' + status + '">' +
|
||
'<div class="day-label">' + wk + '</div>' +
|
||
'<div class="day-date">' + grade + '</div>' +
|
||
'<div class="day-score">' + score + '<span style="font-size:11px;font-weight:400">分</span></div>' +
|
||
'<div class="day-badge">' + (status === 'pass' ? '达标' : '未达标') + '</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 (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();
|
||
});
|
||
};
|
||
|
||
/* ================================================================
|
||
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 — 蓝图六板块
|
||
================================================================ */
|
||
|
||
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', done: si.done, note: si.note}); });
|
||
}
|
||
});
|
||
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.note) extra = '<span class="bp-item-note">' + esc(item.note) + '</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();
|
||
}
|
||
})();
|