Files
ziwei-power/static/app.js

673 lines
24 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 autoSaveTimer = null; // 自动保存防抖
var lastSavedData = null; // 上次保存快照
var lastSavedDate = null; // 上次保存日期
var selectedDate = null; // 日历中选中日期
/* 勤学预设项目 */
var PRESET_STUDY_ITEMS = [
'晚上 11 点 30 之前睡觉',
'早上 6 点 30 之前起床',
'起床后做 30 个俯卧撑',
'销售客情能力提升',
'管理能力提升',
'产品规划有进一步的提升',
'AI 能力提升',
'知识库能力提升'
];
/* ================================================================
Toast
================================================================ */
var toastTimer = null;
function showToast(msg, type) {
type = type || 'info';
var el = document.getElementById('toast');
el.textContent = msg;
el.className = 'toast ' + type + ' show';
clearTimeout(toastTimer);
toastTimer = setTimeout(function(){ el.classList.remove('show'); }, 2500);
}
/* ================================================================
API
================================================================ */
function apiGetCheckin(date, cb) {
fetch('/api/checkin?date=' + encodeURIComponent(date))
.then(function(r){ return r.json(); })
.then(function(res){ if (res.ok) cb(null, res.data); else cb(res.error); })
.catch(function(e){ cb(e.message); });
}
function apiSaveCheckin(date, data, cb) {
fetch('/api/checkin', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({date: date, data: data})
})
.then(function(r){ return r.json(); })
.then(function(res){ if (res.ok) cb(null); else cb(res.error); })
.catch(function(e){ cb(e.message); });
}
function apiDeleteCheckin(date, cb) {
fetch('/api/checkin/' + encodeURIComponent(date), {method: 'DELETE'})
.then(function(r){ return r.json(); })
.then(function(res){ if (res.ok) cb(null); else cb(res.error); })
.catch(function(e){ cb(e.message); });
}
function apiGetHistory(cb) {
fetch('/api/history')
.then(function(r){ return r.json(); })
.then(function(res){ if (res.ok) cb(null, res.data); else cb(res.error); })
.catch(function(e){ cb(e.message); });
}
function apiGetStats(cb) {
fetch('/api/stats')
.then(function(r){ return r.json(); })
.then(function(res){ if (res.ok) cb(null, res.data); else cb(res.error); })
.catch(function(e){ cb(e.message); });
}
/* ================================================================
Escaping
================================================================ */
function esc(s) {
return String(s).replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
/* ================================================================
Panel Switching
================================================================ */
window.switchPanel = function(name) {
activePanel = name;
var navs = document.querySelectorAll('.sidebar-nav .nav-item');
var panels = document.querySelectorAll('.main-content .panel');
for (var i=0; i<navs.length; i++) navs[i].classList.remove('active');
for (var j=0; j<panels.length; j++) panels[j].classList.remove('active');
var nav = document.querySelector('.nav-item[data-panel="' + name + '"]');
var panel = document.getElementById('panel-' + name);
if (nav) nav.classList.add('active');
if (panel) panel.classList.add('active');
if (name === 'weekly') loadWeekly();
if (name === 'history') loadHistory();
};
/* ================================================================
Calendar
================================================================ */
/** 格式化日期 */
function fmtDate(d) {
var y = d.getFullYear();
var m = ('0'+(d.getMonth()+1)).slice(-2);
var dd = ('0'+d.getDate()).slice(-2);
return y + '-' + m + '-' + dd;
}
/** 判断两个日期是否同一天 */
function isSameDay(a, b) {
return a.getFullYear() === b.getFullYear() &&
a.getMonth() === b.getMonth() &&
a.getDate() === b.getDate();
}
/** 切换日历月份 */
window.changeCalMonth = function(dir) {
calMonth += dir;
if (calMonth > 12) { calMonth = 1; calYear++; }
if (calMonth < 1) { calMonth = 12; calYear--; }
renderCalendar();
};
/** 渲染日历 */
function renderCalendar() {
var today = new Date();
var firstDay = new Date(calYear, calMonth - 1, 1);
var lastDay = new Date(calYear, calMonth, 0);
var startDayOfWeek = firstDay.getDay(); // 0=Sun
var totalDays = lastDay.getDate();
var prevMonthLastDay = new Date(calYear, calMonth - 1, 0).getDate();
// 月份标签
document.getElementById('cal-month-label').textContent =
calYear + '年 ' + calMonth + '月';
var grid = document.getElementById('cal-grid');
var html = '';
// 前月填充
for (var p = startDayOfWeek - 1; p >= 0; p--) {
html += '<div class="cal-cell other-month">' + (prevMonthLastDay - p) + '</div>';
}
// 当月
for (var d = 1; d <= totalDays; d++) {
var cur = new Date(calYear, calMonth - 1, d);
var ds = fmtDate(cur);
var classes = ['cal-cell'];
var isOtherMonth = false;
if (isSameDay(cur, today)) classes.push('today');
if (ds === selectedDate) classes.push('selected');
var status = calendarStatus[ds];
if (status) classes.push(status);
html += '<div class="' + classes.join(' ') + '" data-date="' + ds + '">' + d + '</div>';
}
// 后月填充 — 始终 42 格6行 × 7列保持高度一致
var totalCells = startDayOfWeek + totalDays;
var remaining = 42 - totalCells;
for (var n = 1; n <= remaining; n++) {
html += '<div class="cal-cell other-month">' + n + '</div>';
}
grid.innerHTML = html;
// 点击事件
grid.querySelectorAll('.cal-cell:not(.other-month)').forEach(function(cell) {
cell.addEventListener('click', function() {
var date = this.dataset.date;
if (date) navigateToDate(date);
});
});
}
/** 导航到指定日期 */
function navigateToDate(date) {
selectedDate = date;
switchPanel('daily');
document.getElementById('check-date').value = date;
loadCheckin();
renderCalendar(); // 刷新日历高亮
}
/** 回到今天 */
window.goToday = function() {
var today = fmtDate(new Date());
calYear = new Date().getFullYear();
calMonth = new Date().getMonth() + 1;
renderCalendar();
navigateToDate(today);
};
/* ================================================================
Stats — 加载统计数据并更新侧边栏
================================================================ */
function loadStats() {
apiGetStats(function(err, data) {
if (!err && data) {
document.getElementById('stat-days').textContent = data.total_days;
document.getElementById('stat-morning').textContent = data.total_morning;
document.getElementById('stat-study').textContent = data.total_study;
calendarStatus = data.calendar || {};
}
renderCalendar();
});
}
/* ================================================================
Build / Fill Data
================================================================ */
function buildData() {
var morningItems = [];
var mEls = document.querySelectorAll('#morning-list .item-row input');
for (var i=0; i<mEls.length; i++) {
var v = mEls[i].value.trim();
if (v) morningItems.push(v);
}
var eveningItems = [];
var eRows = document.querySelectorAll('#evening-list .evening-row');
for (var j=0; j<eRows.length; j++) {
var cols = eRows[j].querySelectorAll('.col input');
var mistake = cols[0] ? cols[0].value.trim() : '';
var improve = cols[1] ? cols[1].value.trim() : '';
if (mistake || improve) eveningItems.push({ mistake: mistake, improvement: improve });
}
var studyItems = [];
var pItems = document.querySelectorAll('#study-list .preset-item');
for (var k=0; k<pItems.length; k++) {
var cb = pItems[k].querySelector('input[type="checkbox"]');
var noteEl = pItems[k].querySelector('.preset-note');
var nameEl = pItems[k].querySelector('.preset-name');
var name = nameEl ? nameEl.textContent : '';
if (cb && cb.checked) {
studyItems.push({ name: name, done: true, note: noteEl ? noteEl.value.trim() : '' });
} else {
studyItems.push({ name: name, done: false, note: '' });
}
}
return { morning: morningItems, evening: eveningItems, study: studyItems };
}
function fillForm(data) {
data = data || {};
var morning = data.morning || [];
var evening = data.evening || [];
var study = data.study || [];
// 早间立志
document.getElementById('morning-list').innerHTML = '';
if (morning.length === 0) morning = [''];
for (var m=0; m<morning.length; m++) addMorningRow(morning[m]);
// 责善改过
document.getElementById('evening-list').innerHTML = '';
if (evening.length === 0) evening = [{}];
for (var e=0; e<evening.length; e++) {
var item = typeof evening[e] === 'string' ? {mistake: evening[e], improvement: ''} : evening[e];
addEveningRow(item.mistake || '', item.improvement || '');
}
// 勤学 — 渲染预设项目并回填状态
var studyMap = {};
for (var si=0; si<study.length; si++) {
studyMap[study[si].name] = study[si];
}
document.getElementById('study-list').innerHTML = '';
for (var sp=0; sp<PRESET_STUDY_ITEMS.length; sp++) {
var sName = PRESET_STUDY_ITEMS[sp];
var saved = studyMap[sName] || { done: false, note: '' };
addPresetItem(sName, saved.done, saved.note || '');
}
}
/* ================================================================
Dynamic Rows
================================================================ */
function addMorningRow(val) {
var container = document.getElementById('morning-list');
var idx = container.children.length;
val = val || '';
var div = document.createElement('div');
div.className = 'item-row';
div.innerHTML = '<span class="idx">' + (idx+1) + '.</span>' +
'<input type="text" value="' + esc(val) + '" placeholder="今天最重要的一件事…">' +
'<button class="btn-del" onclick="this.parentElement.remove();renumberMorning()"><svg class="icon-sm"><use href="#icon-x"/></svg></button>';
container.appendChild(div);
renumberMorning();
}
function renumberMorning() {
var rows = document.querySelectorAll('#morning-list .item-row');
for (var i=0; i<rows.length; i++) {
var span = rows[i].querySelector('.idx');
if (span) span.textContent = (i+1) + '.';
}
}
function addEveningRow(mistake, improve) {
var container = document.getElementById('evening-list');
var idx = container.children.length;
mistake = mistake || '';
improve = improve || '';
var div = document.createElement('div');
div.className = 'evening-row';
div.innerHTML =
'<div class="evening-header">' +
'<span class="idx">' + (idx+1) + '.</span>' +
'<button class="btn-del" onclick="this.parentElement.parentElement.remove();renumberEvening()"><svg class="icon-sm"><use href="#icon-x"/></svg></button>' +
'</div>' +
'<div class="mistake-row">' +
'<div class="col"><label>错误</label><input type="text" value="' + esc(mistake) + '" placeholder="今天犯的错…"></div>' +
'<div class="col"><label>改进方案</label><input type="text" value="' + esc(improve) + '" placeholder="下次怎么做…"></div>' +
'</div>';
container.appendChild(div);
renumberEvening();
}
function renumberEvening() {
var rows = document.querySelectorAll('#evening-list .evening-row');
for (var i=0; i<rows.length; i++) {
var span = rows[i].querySelector('.idx');
if (span) span.textContent = (i+1) + '.';
}
}
/* ── 勤学预设项目 ── */
function addPresetItem(name, done, note) {
var container = document.getElementById('study-list');
var div = document.createElement('div');
div.className = 'preset-item' + (done ? ' active' : '');
var noteEsc = esc(note || '');
div.innerHTML =
'<label class="preset-check">' +
'<input type="checkbox"' + (done ? ' checked' : '') + ' onchange="togglePresetNote(this)">' +
'<span class="preset-name">' + esc(name) + '</span>' +
'</label>' +
'<textarea class="preset-note" placeholder="一句话备注…">' + noteEsc + '</textarea>';
container.appendChild(div);
}
window.togglePresetNote = function(cb) {
var item = cb.closest('.preset-item');
if (item) {
if (cb.checked) item.classList.add('active');
else { item.classList.remove('active'); item.querySelector('.preset-note').value = ''; }
}
triggerAutoSave();
};
function initStudyPresets() {
document.getElementById('study-list').innerHTML = '';
for (var i=0; i<PRESET_STUDY_ITEMS.length; i++) {
addPresetItem(PRESET_STUDY_ITEMS[i], false, '');
}
}
window.addMorning = function() {
var count = document.querySelectorAll('#morning-list .item-row').length;
if (count >= 3) { showToast('最多 3 条立志', 'error'); return; }
addMorningRow('');
};
window.addEvening = function() {
var count = document.querySelectorAll('#evening-list .evening-row').length;
if (count >= 5) { showToast('最多 5 条改过', 'error'); return; }
addEveningRow('', '');
};
/* ================================================================
Load & Auto Save Checkin
================================================================ */
var lastSavedData = null; // 上次保存的数据快照,避免重复保存
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() {
clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(function() {
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;
showToast('已自动保存', 'info');
loadStats();
});
}, 1500);
}
/** 为每日打卡面板绑定自动保存事件 */
function bindAutoSave() {
var panel = document.getElementById('panel-daily');
if (!panel) return;
// 输入框 & 文本域input 事件
panel.addEventListener('input', function(e) {
if (e.target.matches('input[type="text"], textarea, input[type="date"]')) {
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 更新后
}
});
// 预设项目勾选切换
panel.addEventListener('change', function(e) {
if (e.target.matches('.preset-check input[type="checkbox"]')) {
triggerAutoSave();
}
});
}
/* ================================================================
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 dayScore = 0;
var morning = dd.morning || [];
var hasMorning = morning.some(function(x){ return x && typeof x === 'string' && x.trim(); });
var evening = dd.evening || [];
var hasEvening = evening.some(function(x){
var mst = typeof x === 'string' ? x : (x.mistake || '');
return mst && typeof mst === 'string' && mst.trim();
});
var study = dd.study || [];
var hasStudy = study.some(function(x){ return x.done; });
if (hasMorning) dayScore++;
if (hasEvening) dayScore++;
if (hasStudy) dayScore++;
totalScore += dayScore;
var status = dayScore >= 2 ? 'pass' : 'fail';
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 = totalDays > 0 ? Math.round((totalScore / (totalDays * 3)) * 100) : 0;
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 * score / 100));
}
var txt = '';
if (score >= 90) txt = '卓越!磁场非常强大';
else if (score >= 70) txt = '良好,继续保持';
else if (score >= 50) 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">/3</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(); // 更新统计
});
};
/* ================================================================
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();
}
})();