初始化 ziwei-power:紫微磁场管理系统(Flask + SQLite + 登录鉴权)

This commit is contained in:
mac
2026-06-01 19:29:23 +08:00
commit 55cd42b0c9
9 changed files with 1779 additions and 0 deletions
+411
View File
@@ -0,0 +1,411 @@
/* 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,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
/* ── 初始化 ────────────────────────────── */
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();
}
})();
+313
View File
@@ -0,0 +1,313 @@
:root {
--primary: #4A6CF7;
--primary-light: #EEF1FE;
--danger: #EF4444;
--danger-light: #FEF2F2;
--bg: #F5F7FB;
--card: #FFFFFF;
--text: #1F2937;
--text-muted: #9CA3AF;
--border: #E5E7EB;
--radius: 12px;
--shadow: 0 1px 3px rgba(0,0,0,0.06);
}
* { margin:0; padding:0; box-sizing:border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', sans-serif;
background: var(--bg);
color: var(--text);
max-width: 480px;
margin: 0 auto;
padding: 16px;
min-height: 100vh;
}
/* ── Header ── */
header {
text-align: center;
padding: 20px 0 12px;
}
.header-top {
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
header h1 { font-size: 22px; font-weight: 700; color: var(--primary); }
header p { font-size: 13px; color: var(--text-muted); margin-top: 4px; }
.user-bar {
position: absolute;
right: 0;
display: flex;
align-items: center;
gap: 8px;
}
.user-name {
font-size: 12px;
color: var(--text-muted);
font-weight: 500;
}
.btn-logout {
font-size: 11px;
color: var(--danger);
text-decoration: none;
padding: 2px 8px;
border: 1px solid var(--danger);
border-radius: 4px;
transition: all .2s;
}
.btn-logout:hover {
background: var(--danger);
color: #FFF;
}
/* ── Tabs ── */
.tabs {
display: flex;
background: var(--card);
border-radius: var(--radius);
padding: 4px;
margin-bottom: 16px;
box-shadow: var(--shadow);
}
.tab {
flex: 1;
padding: 10px 0;
border: none;
background: transparent;
font-size: 14px;
cursor: pointer;
border-radius: 10px;
color: var(--text-muted);
transition: all .2s;
}
.tab.active {
background: var(--primary);
color: #FFF;
font-weight: 600;
}
/* ── Tab content ── */
.tab-content { display: none; }
.tab-content.active { display: block; }
/* ── Date row ── */
.date-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
font-size: 14px;
font-weight: 600;
}
.date-row input[type="date"] {
flex: 1;
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: 8px;
font-size: 14px;
font-family: inherit;
}
/* ── Card ── */
.card {
background: var(--card);
border-radius: var(--radius);
padding: 16px;
margin-bottom: 12px;
box-shadow: var(--shadow);
}
.card h2 { font-size: 15px; margin-bottom: 2px; }
.card-desc { font-size: 12px; color: var(--text-muted); margin-bottom: 12px; }
/* ── Dynamic items ── */
.item-row {
display: flex;
align-items: flex-start;
gap: 8px;
margin-bottom: 8px;
}
.item-row input[type="text"],
.item-row textarea {
flex:1;
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: 8px;
font-size: 13px;
font-family: inherit;
resize: vertical;
}
.item-row textarea { min-height: 40px; }
.item-row .idx {
font-size: 12px;
color: var(--text-muted);
min-width: 20px;
padding-top: 10px;
}
.btn-del {
background: none;
border: none;
color: var(--danger);
font-size: 18px;
cursor: pointer;
padding: 6px 4px;
line-height: 1;
}
.btn-del:hover { opacity: 0.7; }
/* 责善改过 双输入 */
.mistake-group {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
}
.mistake-group .label-hint {
font-size: 11px;
color: var(--text-muted);
padding-left: 2px;
}
/* ── Study items ── */
.study-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.study-row input[type="text"] {
flex: 1;
padding: 6px 10px;
border: 1px solid var(--border);
border-radius: 8px;
font-size: 13px;
font-family: inherit;
}
.study-row input[type="checkbox"] {
width: 18px; height: 18px;
accent-color: var(--primary);
flex-shrink: 0;
}
/* ── Buttons ── */
.btn-add {
display: block;
width: 100%;
padding: 8px;
margin-top: 8px;
border: 1px dashed var(--primary);
background: var(--primary-light);
color: var(--primary);
font-size: 13px;
border-radius: 8px;
cursor: pointer;
transition: all .2s;
}
.btn-add:hover { background: #DDE3FD; }
.btn-save {
display: block;
width: 100%;
padding: 14px;
margin-top: 8px;
border: none;
background: var(--primary);
color: #FFF;
font-size: 16px;
font-weight: 600;
border-radius: var(--radius);
cursor: pointer;
transition: all .2s;
}
.btn-save:hover { opacity: 0.9; transform: translateY(-1px); }
/* ── Week nav ── */
.week-nav {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
padding: 16px 0;
}
.btn-nav {
padding: 8px 16px;
border: 1px solid var(--border);
background: var(--card);
border-radius: 8px;
font-size: 13px;
cursor: pointer;
}
#week-label {
font-size: 14px;
font-weight: 600;
min-width: 140px;
text-align: center;
}
/* ── Score ── */
.score-display { text-align: center; padding: 20px 0 8px; }
.score-num { font-size: 56px; font-weight: 800; color: var(--primary); }
.score-unit { font-size: 20px; color: var(--text-muted); margin-left: 4px; }
.score-text { text-align: center; font-size: 14px; margin-bottom: 16px; }
/* ── Details ── */
.details-list { margin-top: 8px; }
.details-list .detail-row {
padding: 10px 16px;
background: var(--card);
border-radius: 8px;
margin-bottom: 6px;
font-size: 13px;
box-shadow: var(--shadow);
}
/* ── History ── */
#history-list { margin-top: 4px; }
.history-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
background: var(--card);
border-radius: var(--radius);
margin-bottom: 8px;
box-shadow: var(--shadow);
}
.history-row .info { flex: 1; }
.history-row .info .date { font-weight: 600; font-size: 14px; color: var(--primary); }
.history-row .info .preview { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
.history-row .btn-del-history {
background: var(--danger-light);
border: none;
color: var(--danger);
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
flex-shrink: 0;
margin-left: 12px;
}
.history-row .btn-del-history:hover { opacity: 0.8; }
/* ── Toast ── */
.toast {
position: fixed;
top: 16px;
left: 50%;
transform: translateX(-50%);
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
z-index: 999;
opacity: 0;
pointer-events: none;
transition: opacity .3s;
white-space: nowrap;
}
.toast.show { opacity: 1; pointer-events: auto; }
.toast.success { background: #10B981; color: #FFF; }
.toast.error { background: var(--danger); color: #FFF; }
.toast.info { background: var(--primary); color: #FFF; }