412 lines
14 KiB
JavaScript
412 lines
14 KiB
JavaScript
/* ziwei-power 前端逻辑 — 通过 API 与 Flask 后端交互 */
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
/* ── 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); });
|
||
}
|
||
|
||
/* ── Tab 切换 ───────────────────────────── */
|
||
window.switchTab = function(name, ev) {
|
||
var tabs = document.querySelectorAll('.tab');
|
||
var contents = document.querySelectorAll('.tab-content');
|
||
for (var i=0; i<tabs.length; i++) tabs[i].classList.remove('active');
|
||
for (var j=0; j<contents.length; j++) contents[j].classList.remove('active');
|
||
if (ev && ev.target) ev.target.classList.add('active');
|
||
var ct = document.getElementById(name + '-tab');
|
||
if (ct) ct.classList.add('active');
|
||
|
||
if (name === 'history') loadHistory();
|
||
if (name === 'weekly') loadWeekly();
|
||
};
|
||
|
||
/* ── 打卡数据构建 ───────────────────────── */
|
||
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 eGroups = document.querySelectorAll('#evening-list .mistake-group');
|
||
for (var j=0; j<eGroups.length; j++) {
|
||
var inputs = eGroups[j].querySelectorAll('input, textarea');
|
||
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 sRows = document.querySelectorAll('#study-list .study-row');
|
||
for (var k=0; k<sRows.length; k++) {
|
||
var cb = sRows[k].querySelector('input[type="checkbox"]');
|
||
var txt = sRows[k].querySelector('input[type="text"]');
|
||
var name = txt ? txt.value.trim() : '';
|
||
if (name) {
|
||
studyItems.push({ name: name, done: cb ? cb.checked : false });
|
||
}
|
||
}
|
||
|
||
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 || '');
|
||
}
|
||
|
||
// 勤学
|
||
document.getElementById('study-list').innerHTML = '';
|
||
if (study.length === 0) study = [{name: '', done: false}];
|
||
for (var s=0; s<study.length; s++) {
|
||
addStudyRow(study[s].name || '', study[s].done || false);
|
||
}
|
||
}
|
||
|
||
/* ── 动态添加行 ──────────────────────────── */
|
||
|
||
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()">×</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 = 'item-row';
|
||
div.innerHTML = '<span class="idx">' + (idx+1) + '.</span>' +
|
||
'<div class="mistake-group">' +
|
||
'<span class="label-hint">错误</span>' +
|
||
'<input type="text" value="' + esc(mistake) + '" placeholder="今天犯的错…">' +
|
||
'<span class="label-hint">改进方案</span>' +
|
||
'<textarea placeholder="下次怎么做…">' + esc(improve) + '</textarea>' +
|
||
'</div>' +
|
||
'<button class="btn-del" onclick="this.parentElement.remove();renumberEvening()">×</button>';
|
||
container.appendChild(div);
|
||
renumberEvening();
|
||
}
|
||
|
||
function renumberEvening() {
|
||
var rows = document.querySelectorAll('#evening-list .item-row');
|
||
for (var i=0; i<rows.length; i++) {
|
||
var span = rows[i].querySelector('.idx');
|
||
if (span) span.textContent = (i+1) + '.';
|
||
}
|
||
}
|
||
|
||
function addStudyRow(name, done) {
|
||
var container = document.getElementById('study-list');
|
||
name = name || '';
|
||
done = done || false;
|
||
|
||
var div = document.createElement('div');
|
||
div.className = 'study-row';
|
||
div.innerHTML = '<input type="checkbox"' + (done ? ' checked' : '') + '>' +
|
||
'<input type="text" value="' + esc(name) + '" placeholder="学习项目名称…">' +
|
||
'<button class="btn-del" onclick="this.parentElement.remove()">×</button>';
|
||
container.appendChild(div);
|
||
}
|
||
|
||
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 .item-row').length;
|
||
if (count >= 5) { showToast('最多 5 条改过', 'error'); return; }
|
||
addEveningRow('', '');
|
||
};
|
||
window.addStudy = function() {
|
||
addStudyRow('', false);
|
||
};
|
||
|
||
/* ── 加载 & 保存 ────────────────────────── */
|
||
|
||
window.loadCheckin = function() {
|
||
var date = document.getElementById('check-date').value;
|
||
if (!date) return;
|
||
apiGetCheckin(date, function(err, row){
|
||
if (err) { fillForm(null); return; }
|
||
fillForm(row ? row.data : null);
|
||
});
|
||
};
|
||
|
||
window.saveCheckin = function() {
|
||
var date = document.getElementById('check-date').value;
|
||
if (!date) { showToast('请先选择日期!', 'error'); return; }
|
||
var data = buildData();
|
||
apiSaveCheckin(date, data, function(err){
|
||
if (err) { showToast('保存失败: ' + err, 'error'); return; }
|
||
showToast('✅ 打卡保存成功!', 'success');
|
||
});
|
||
};
|
||
|
||
/* ── 每周评分 ───────────────────────────── */
|
||
var weekOffset = 0;
|
||
|
||
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;
|
||
}
|
||
|
||
function fmtDate(d) {
|
||
var y = d.getFullYear();
|
||
var m = ('0'+(d.getMonth()+1)).slice(-2);
|
||
var day = ('0'+d.getDate()).slice(-2);
|
||
return y + '-' + m + '-' + day;
|
||
}
|
||
|
||
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;
|
||
var totalScore = 0;
|
||
var details = [];
|
||
|
||
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 = false;
|
||
for (var m=0; m<morning.length; m++) {
|
||
if (morning[m] && morning[m].trim) { if (morning[m].trim()) hasMorning = true; }
|
||
}
|
||
if (hasMorning) dayScore++;
|
||
|
||
// 改过有内容
|
||
var evening = dd.evening || [];
|
||
var hasEvening = false;
|
||
for (var e=0; e<evening.length; e++) {
|
||
var ev = evening[e];
|
||
var mst = typeof ev === 'string' ? ev : (ev.mistake || '');
|
||
if (mst.trim) { if (mst.trim()) hasEvening = true; }
|
||
}
|
||
if (hasEvening) dayScore++;
|
||
|
||
// 勤学有勾选
|
||
var study = dd.study || [];
|
||
var hasStudy = false;
|
||
for (var s=0; s<study.length; s++) {
|
||
if (study[s].done) hasStudy = true;
|
||
}
|
||
if (hasStudy) dayScore++;
|
||
|
||
totalScore += dayScore;
|
||
var icon = dayScore >= 2 ? '✅' : '⚠️';
|
||
var color = dayScore >= 2 ? '#10B981' : '#F59E0B';
|
||
details.push('<div class="detail-row" style="color:' + color + '">' +
|
||
ds + ' — 得分:' + dayScore + '/3 ' + icon + '</div>');
|
||
}
|
||
}
|
||
|
||
var score = totalDays > 0 ? Math.round((totalScore / (totalDays * 3)) * 100) : 0;
|
||
document.getElementById('weekly-score').textContent = score;
|
||
|
||
var txt = '';
|
||
if (score >= 90) txt = '🌟 卓越!磁场非常强大';
|
||
else if (score >= 70) txt = '💪 良好,继续保持';
|
||
else if (score >= 50) txt = '⚠️ 一般,需要加强';
|
||
else txt = '❌ 较弱,急需调整';
|
||
|
||
document.getElementById('score-text').textContent = txt;
|
||
document.getElementById('weekly-details').innerHTML = details.join('');
|
||
});
|
||
}
|
||
|
||
/* ── 历史记录 ───────────────────────────── */
|
||
function loadHistory() {
|
||
apiGetHistory(function(err, rows){
|
||
var el = document.getElementById('history-list');
|
||
if (err) { el.innerHTML = '<p style="text-align:center;color:#999;">加载失败</p>'; return; }
|
||
if (!rows || rows.length === 0) {
|
||
el.innerHTML = '<p style="text-align:center;color:#999;padding:40px 0;">暂无历史记录</p>';
|
||
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 && x.trim && x.trim();
|
||
});
|
||
var evening = (r.data.evening || []).filter(function(x){
|
||
var mst = typeof x === 'string' ? x : (x.mistake || '');
|
||
return mst && mst.trim && mst.trim();
|
||
});
|
||
var preview = [];
|
||
if (morning.length) preview.push('📝 立志 ' + morning.length + ' 条');
|
||
if (evening.length) preview.push('⚠️ 改过 ' + evening.length + ' 条');
|
||
|
||
html += '<div class="history-row">' +
|
||
'<div class="info">' +
|
||
'<div class="date">' + r.date + '</div>' +
|
||
'<div class="preview">' + (preview.join(' · ') || '暂无内容') + '</div>' +
|
||
'</div>' +
|
||
'<button class="btn-del-history" onclick="deleteHistory(\'' + r.date + '\')">🗑 删除</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();
|
||
});
|
||
};
|
||
|
||
/* ── HTML 转义 ──────────────────────────── */
|
||
function esc(s) {
|
||
return String(s).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>');
|
||
}
|
||
|
||
/* ── 初始化 ────────────────────────────── */
|
||
function init() {
|
||
var today = new Date().toISOString().split('T')[0];
|
||
var cd = document.getElementById('check-date');
|
||
if (cd) { cd.value = today; }
|
||
fillForm(null);
|
||
loadCheckin();
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
})();
|