初始化 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

20
.gitignore vendored Normal file
View File

@@ -0,0 +1,20 @@
# Python
__pycache__/
*.py[cod]
*.egg-info/
venv/
env/
# IDE
.vscode/
.idea/
# OS
.DS_Store
Thumbs.db
# Database
*.db
# WorkBuddy
.workbuddy/

130
app.py Normal file
View File

@@ -0,0 +1,130 @@
# -*- coding: utf-8 -*-
"""ziwei-power Flask 应用 — 磁场管理打卡系统"""
import os
from datetime import timedelta
from functools import wraps
from flask import Flask, render_template, request, jsonify, session, redirect, url_for
from werkzeug.security import generate_password_hash, check_password_hash
from database import init_db, get_checkin, save_checkin, delete_checkin, get_all_checkins
app = Flask(__name__)
app.secret_key = os.urandom(24)
# 会话持久化30 天
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=30)
# ── 用户库 ────────────────────────────────────────────
USERS = {
'qiukai': {
'password_hash': generate_password_hash('AiAlex2018$'),
'name': '凯哥'
}
}
# ── 登录检查装饰器 ────────────────────────────────────
def login_required(f):
@wraps(f)
def decorated(*args, **kwargs):
if not session.get('logged_in'):
return redirect(url_for('login_page', next=request.path))
return f(*args, **kwargs)
return decorated
# ── 登录路由 ──────────────────────────────────────────
@app.route('/login', methods=['GET', 'POST'])
def login_page():
if request.method == 'POST':
username = request.form.get('username', '').strip()
password = request.form.get('password', '')
remember = request.form.get('remember') == 'on'
user = USERS.get(username)
if user and check_password_hash(user['password_hash'], password):
session.permanent = remember
session['logged_in'] = True
session['username'] = username
session['display_name'] = user['name']
next_url = request.args.get('next', '/')
return jsonify({'ok': True, 'next': next_url})
else:
return jsonify({'ok': False, 'error': '账号或密码错误'}), 401
# GET如果已登录直接跳转
if session.get('logged_in'):
return redirect(url_for('index'))
return render_template('login.html')
@app.route('/logout')
def logout():
session.clear()
return redirect(url_for('login_page'))
# ── 主页 ──────────────────────────────────────────────
@app.route('/')
@login_required
def index():
return render_template('index.html',
username=session.get('display_name', session.get('username', '')))
# ── API ──────────────────────────────────────────────
@app.route('/api/checkin', methods=['GET'])
@login_required
def api_get_checkin():
date = request.args.get('date', '')
if not date:
return jsonify({'ok': False, 'error': '缺少 date 参数'}), 400
row = get_checkin(date)
return jsonify({'ok': True, 'data': row})
@app.route('/api/checkin', methods=['POST'])
@login_required
def api_save_checkin():
body = request.get_json(force=True)
date = body.get('date', '')
if not date:
return jsonify({'ok': False, 'error': '缺少 date 字段'}), 400
data = body.get('data', {})
save_checkin(date, data)
return jsonify({'ok': True})
@app.route('/api/checkin/<date>', methods=['DELETE'])
@login_required
def api_delete_checkin(date):
delete_checkin(date)
return jsonify({'ok': True})
@app.route('/api/history', methods=['GET'])
@login_required
def api_history():
rows = get_all_checkins()
return jsonify({'ok': True, 'data': rows})
@app.route('/api/user', methods=['GET'])
@login_required
def api_user():
return jsonify({
'ok': True,
'username': session.get('username'),
'display_name': session.get('display_name')
})
# ── 启动 ──────────────────────────────────────────────
if __name__ == '__main__':
init_db()
app.run(host='0.0.0.0', port=5053, debug=False)

94
database.py Normal file
View File

@@ -0,0 +1,94 @@
# -*- coding: utf-8 -*-
"""ziwei-power SQLite 数据库操作层"""
import sqlite3
import json
import os
from datetime import datetime
DB_DIR = os.path.join(os.path.expanduser('~'), '.workbuddy', 'data', 'ziwei-power')
os.makedirs(DB_DIR, exist_ok=True)
DB_PATH = os.path.join(DB_DIR, 'ziwei_power.db')
def get_conn():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def init_db():
"""初始化数据库表"""
conn = get_conn()
conn.execute('''
CREATE TABLE IF NOT EXISTS checkins (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT UNIQUE NOT NULL,
data TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
''')
conn.commit()
conn.close()
def get_checkin(date_str):
"""获取某天的打卡记录,返回 dict 或 None"""
conn = get_conn()
row = conn.execute('SELECT * FROM checkins WHERE date = ?', (date_str,)).fetchone()
conn.close()
if row:
return {
'id': row['id'],
'date': row['date'],
'data': json.loads(row['data']),
'created_at': row['created_at'],
'updated_at': row['updated_at']
}
return None
def save_checkin(date_str, data_dict):
"""保存或更新打卡记录"""
now = datetime.now().isoformat()
conn = get_conn()
existing = conn.execute('SELECT id FROM checkins WHERE date = ?', (date_str,)).fetchone()
json_data = json.dumps(data_dict, ensure_ascii=False)
if existing:
conn.execute(
'UPDATE checkins SET data = ?, updated_at = ? WHERE date = ?',
(json_data, now, date_str)
)
else:
conn.execute(
'INSERT INTO checkins (date, data, created_at, updated_at) VALUES (?, ?, ?, ?)',
(date_str, json_data, now, now)
)
conn.commit()
conn.close()
def delete_checkin(date_str):
"""删除某天的打卡记录"""
conn = get_conn()
conn.execute('DELETE FROM checkins WHERE date = ?', (date_str,))
conn.commit()
conn.close()
def get_all_checkins():
"""获取所有打卡记录,按日期倒序"""
conn = get_conn()
rows = conn.execute('SELECT * FROM checkins ORDER BY date DESC').fetchall()
conn.close()
results = []
for row in rows:
results.append({
'id': row['id'],
'date': row['date'],
'data': json.loads(row['data']),
'created_at': row['created_at'],
'updated_at': row['updated_at']
})
return results

552
index.html Normal file
View File

@@ -0,0 +1,552 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>紫微力量 - 磁场管理</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;background:linear-gradient(135deg,#1a1a2e 0%,#16213e 50%,#0f3460 100%);min-height:100vh;padding:20px}
.container{max-width:800px;margin:0 auto;background:#fff;border-radius:20px;box-shadow:0 20px 60px rgba(0,0,0,.4);overflow:hidden}
.header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;padding:24px 30px 30px;text-align:center}
.header h1{font-size:28px;margin-bottom:6px}
.header .motto{font-size:13px;opacity:.85}
.js-badge{display:inline-block;background:rgba(255,255,255,.2);padding:3px 10px;border-radius:10px;font-size:11px;margin-top:8px}
.tabs{display:flex;border-bottom:2px solid #e0e0e0}
.tab{flex:1;padding:14px;text-align:center;cursor:pointer;background:#f8f9fa;border:none;font-size:15px;color:#666;transition:all .2s}
.tab:hover{background:#eef0ff}
.tab.active{background:#fff;color:#667eea;font-weight:bold;border-bottom:3px solid #667eea}
.tab-content{display:none;padding:20px}
.tab-content.active{display:block}
.date-row{display:flex;gap:10px;align-items:center;flex-wrap:wrap;padding:0 0 16px}
.date-row input[type="date"]{padding:10px 14px;border:2px solid #ddd;border-radius:8px;font-size:14px;flex:1;min-width:140px}
.date-row button{padding:10px 18px;border:none;border-radius:8px;cursor:pointer;font-size:14px;white-space:nowrap;font-weight:500}
.btn-save{background:#667eea;color:#fff}
.btn-save:hover{background:#5568d3}
.btn-save:active{transform:scale(.97)}
.btn-outline{background:#fff;color:#667eea;border:2px solid #667eea!important}
.btn-outline:hover{background:#eef0ff}
/* Section */
.section{margin-bottom:18px;padding:16px;background:#f6f7fb;border-radius:12px}
.section h2{font-size:16px;margin-bottom:10px;color:#333;display:flex;align-items:center;gap:8px}
.section h2 span{font-size:22px}
.section h2 .hint{font-size:11px;color:#999;margin-left:auto;font-weight:normal}
/* 动态条目 */
.item-row{display:flex;gap:8px;align-items:flex-start;margin-bottom:8px}
.item-row .item-num{width:24px;height:24px;border-radius:50%;background:#667eea;color:#fff;font-size:11px;display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:10px}
.item-row textarea{flex:1;padding:10px;border:2px solid #e0e0e0;border-radius:8px;font-size:13px;font-family:inherit;resize:vertical;min-height:40px}
.item-row textarea:focus{outline:none;border-color:#667eea}
.item-action{display:flex;flex-direction:column;gap:2px}
.item-action button{width:28px;height:28px;border-radius:6px;border:none;cursor:pointer;font-size:16px;display:flex;align-items:center;justify-content:center;transition:all .15s}
.btn-add{background:#10b981;color:#fff}
.btn-add:hover{background:#059669}
.btn-del{background:#ef4444;color:#fff}
.btn-del:hover{background:#dc2626}
/* Check items */
.check-item{padding:10px 12px;background:#fff;border-radius:8px;border-left:4px solid #667eea;margin-bottom:8px;display:flex;align-items:center;gap:10px}
.check-item label{flex:1;cursor:pointer;font-size:14px;line-height:1.5;display:flex;align-items:center;gap:10px}
.check-item input[type="checkbox"]{width:18px;height:18px;cursor:pointer;accent-color:#667eea;flex-shrink:0}
.check-item .del-btn{width:24px;height:24px;border-radius:6px;border:none;cursor:pointer;font-size:12px;background:#fee2e2;color:#ef4444;flex-shrink:0;display:flex;align-items:center;justify-content:center}
.check-item .del-btn:hover{background:#fca5a5;color:#dc2626}
.add-study-btn{display:flex;align-items:center;gap:6px;padding:8px 14px;border:2px dashed #667eea;border-radius:8px;background:transparent;color:#667eea;cursor:pointer;font-size:13px;margin-top:6px;width:100%;justify-content:center}
.add-study-btn:hover{background:#eef0ff}
.save-btn{display:block;width:100%;padding:14px;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;border:none;border-radius:12px;font-size:16px;cursor:pointer;margin-top:10px;transition:all .2s;font-weight:500}
.save-btn:hover{opacity:.9}
.save-btn:active{transform:scale(.98)}
/* Toast */
.toast{position:fixed;top:20px;left:50%;transform:translateX(-50%);padding:12px 28px;border-radius:10px;color:#fff;font-size:14px;z-index:9999;font-weight:500;animation:toastIn .3s ease;box-shadow:0 8px 30px rgba(0,0,0,.2)}
.toast.success{background:#10b981}
.toast.error{background:#ef4444}
.toast.info{background:#667eea}
@keyframes toastIn{from{opacity:0;transform:translateX(-50%) translateY(-20px)}to{opacity:1;transform:translateX(-50%) translateY(0)}}
/* Week navigator */
.week-nav{display:flex;align-items:center;gap:10px;justify-content:center;margin-bottom:16px}
.week-nav button{width:36px;height:36px;border-radius:50%;border:2px solid #667eea;background:#fff;color:#667eea;font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center}
.week-nav button:hover{background:#667eea;color:#fff}
.week-nav .week-label{font-size:14px;font-weight:500;color:#333;min-width:160px;text-align:center}
/* Score */
.score-box{text-align:center;padding:30px 20px;background:#f6f7fb;border-radius:12px}
.score-bar-wrap{height:8px;background:#e5e7eb;border-radius:4px;margin:16px 0;overflow:hidden}
.score-bar{height:100%;border-radius:4px;background:linear-gradient(90deg,#667eea,#764ba2);transition:width .5s ease}
.score-num{font-size:64px;font-weight:bold;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.score-txt{font-size:16px;color:#666;margin-top:6px}
.score-detail{text-align:left;font-size:13px;margin-top:16px}
.score-detail .day-row{display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid #eee;font-size:12px}
.history-item{padding:14px;border-bottom:1px solid #eee}
.history-item .date{font-weight:bold;color:#667eea}
.history-item .note{font-size:12px;color:#888;margin-top:4px;line-height:1.5}
.empty-state{text-align:center;color:#999;padding:40px;font-size:14px}
/* 紧凑标签 */
.tag{display:inline-block;padding:2px 8px;border-radius:6px;font-size:11px;margin-right:4px}
.tag.done{background:#d1fae5;color:#065f46}
.tag.miss{background:#fee2e2;color:#991b1b}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>⚡ 紫微力量</h1>
<p class="motto">立志不摇 · 责善不滥 · 改过不拖 · 勤学不辍</p>
<div class="js-badge" id="js-check">⏳ JS 加载中...</div>
</div>
<div class="tabs">
<button class="tab active" onclick="switchTab('daily',event)">📅 每日打卡</button>
<button class="tab" onclick="switchTab('weekly',event)">📊 每周评分</button>
<button class="tab" onclick="switchTab('history',event)">📜 历史记录</button>
</div>
<!-- ========== 每日打卡 ========== -->
<div id="daily-tab" class="tab-content active">
<div class="date-row">
<input type="date" id="check-date" onchange="loadChecklist()">
<button class="btn-save" onclick="saveChecklist()">💾 保存</button>
</div>
<!-- 早间立志最多3条 -->
<div class="section">
<h2><span>🌅</span> 早间立志 <span class="hint">今日最重要的事最多3条</span></h2>
<div id="morning-items"><!-- 动态生成 --></div>
<button class="add-study-btn" onclick="addMorningItem()" id="add-morning-btn">+ 增加一条</button>
</div>
<!-- 责善·改过最多5条 -->
<div class="section">
<h2><span>⚖️</span> 责善·改过 <span class="hint">今日反思最多5条</span></h2>
<div id="review-items"><!-- 动态生成 --></div>
<button class="add-study-btn" onclick="addReviewItem()" id="add-review-btn">+ 增加一条</button>
</div>
<!-- 勤学打卡:动态增加 -->
<div class="section">
<h2><span>📚</span> 勤学打卡 <span class="hint">可自由增减</span></h2>
<div id="study-items"><!-- 动态生成 --></div>
<button class="add-study-btn" onclick="addStudyItem()" id="add-study-btn">+ 增加项目</button>
</div>
<button class="save-btn" onclick="saveChecklist()">💾 保存今日打卡</button>
</div>
<!-- ========== 每周评分 ========== -->
<div id="weekly-tab" class="tab-content">
<div class="week-nav">
<button onclick="prevWeek()"></button>
<div class="week-label" id="week-label">本周</div>
<button onclick="nextWeek()"></button>
</div>
<button class="btn-outline" style="width:100%;margin-bottom:16px" onclick="goThisWeek()">📍 回到本周</button>
<div class="score-box">
<div class="score-num" id="weekly-score">--</div>
<div class="score-bar-wrap"><div class="score-bar" id="score-bar" style="width:0%"></div></div>
<div class="score-txt" id="score-text">选择一周查看评分</div>
<div class="score-detail" id="weekly-details"></div>
</div>
</div>
<!-- ========== 历史记录 ========== -->
<div id="history-tab" class="tab-content">
<h2 style="font-size:17px;margin-bottom:16px;display:flex;align-items:center;gap:8px">📜 打卡历史</h2>
<div id="history-list"><div class="empty-state">暂无打卡记录</div></div>
</div>
</div>
<script>
'use strict';
document.getElementById('js-check').textContent = '✅ JS 已就绪';
document.getElementById('js-check').style.background = 'rgba(16,185,129,.3)';
var STORAGE_KEY = 'ziwei_power_data';
var WEEK_OFFSET = 0; // 周评分偏移量0=本周,-1=上周1=下周)
// ========== Toast ==========
function showToast(msg, type) {
var t = document.createElement('div');
t.className = 'toast ' + (type || 'info');
t.textContent = msg;
document.body.appendChild(t);
setTimeout(function(){ t.remove(); }, 2500);
}
// ========== 数据 ==========
function getData() {
try { var r = localStorage.getItem(STORAGE_KEY); return r ? JSON.parse(r) : {}; }
catch(e) { return {}; }
}
function saveData(d) {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(d)); return true; }
catch(e) { return false; }
}
// ========== Tab ==========
function switchTab(tabName, 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 el = document.getElementById(tabName + '-tab');
if (el) el.classList.add('active');
if (tabName === 'history') loadHistory();
if (tabName === 'weekly') { WEEK_OFFSET = 0; calculateWeeklyScore(); }
}
// ========== 早间立志:动态条目 ==========
var morningCount = 1;
function renderMorningItems(values) {
var container = document.getElementById('morning-items');
var html = '';
values = values || [''];
morningCount = values.length;
for (var i = 0; i < values.length; i++) {
html += '<div class="item-row" id="morning-row-' + i + '">' +
'<div class="item-num">' + (i + 1) + '</div>' +
'<textarea id="morning-' + i + '" placeholder="第' + (i + 1) + '件最重要的事..." rows="1">' + (values[i] || '') + '</textarea>' +
(values.length > 1 ? '<div class="item-action"><button class="btn-del" onclick="removeMorning(' + i + ')">×</button></div>' : '') +
'</div>';
}
container.innerHTML = html;
updateMorningAddBtn();
}
function updateMorningAddBtn() {
var btn = document.getElementById('add-morning-btn');
btn.style.display = morningCount >= 3 ? 'none' : 'flex';
}
function addMorningItem() {
if (morningCount >= 3) return;
var vals = [];
for (var i = 0; i < morningCount; i++) {
var el = document.getElementById('morning-' + i);
vals.push(el ? el.value : '');
}
vals.push('');
renderMorningItems(vals);
}
function removeMorning(idx) {
if (morningCount <= 1) return;
var vals = [];
for (var i = 0; i < morningCount; i++) {
if (i !== idx) {
var el = document.getElementById('morning-' + i);
vals.push(el ? el.value : '');
}
}
renderMorningItems(vals);
}
function getMorningValues() {
var vals = [];
for (var i = 0; i < morningCount; i++) {
var el = document.getElementById('morning-' + i);
if (el) vals.push(el.value);
}
return vals;
}
// ========== 责善·改过:动态条目 ==========
var reviewCount = 1;
function renderReviewItems(mistakes, improvements) {
var container = document.getElementById('review-items');
var html = '';
mistakes = mistakes || [''];
improvements = improvements || [''];
reviewCount = mistakes.length;
for (var i = 0; i < mistakes.length; i++) {
html += '<div class="item-row" id="review-row-' + i + '">' +
'<div class="item-num" style="background:#764ba2">' + (i + 1) + '</div>' +
'<div style="flex:1;display:flex;flex-direction:column;gap:4px">' +
'<textarea id="mistake-' + i + '" placeholder="犯的错/负能量源..." rows="1">' + (mistakes[i] || '') + '</textarea>' +
'<textarea id="improve-' + i + '" placeholder="下次1步改进行动..." rows="1">' + (improvements[i] || '') + '</textarea>' +
'</div>' +
(mistakes.length > 1 ? '<div class="item-action"><button class="btn-del" onclick="removeReview(' + i + ')">×</button></div>' : '') +
'</div>';
}
container.innerHTML = html;
updateReviewAddBtn();
}
function updateReviewAddBtn() {
var btn = document.getElementById('add-review-btn');
btn.style.display = reviewCount >= 5 ? 'none' : 'flex';
}
function addReviewItem() {
if (reviewCount >= 5) return;
var ms = [], ims = [];
for (var i = 0; i < reviewCount; i++) {
var me = document.getElementById('mistake-' + i);
var ie = document.getElementById('improve-' + i);
ms.push(me ? me.value : '');
ims.push(ie ? ie.value : '');
}
ms.push(''); ims.push('');
renderReviewItems(ms, ims);
}
function removeReview(idx) {
if (reviewCount <= 1) return;
var ms = [], ims = [];
for (var i = 0; i < reviewCount; i++) {
if (i !== idx) {
var me = document.getElementById('mistake-' + i);
var ie = document.getElementById('improve-' + i);
ms.push(me ? me.value : '');
ims.push(ie ? ie.value : '');
}
}
renderReviewItems(ms, ims);
}
function getReviewValues() {
var mistakes = [], improvements = [];
for (var i = 0; i < reviewCount; i++) {
var me = document.getElementById('mistake-' + i);
var ie = document.getElementById('improve-' + i);
mistakes.push(me ? me.value : '');
improvements.push(ie ? ie.value : '');
}
return { mistakes: mistakes, improvements: improvements };
}
// ========== 勤学打卡:动态增减 ==========
var studyItems = [];
function renderStudyItems() {
var container = document.getElementById('study-items');
if (studyItems.length === 0) {
container.innerHTML = '<div style="color:#999;font-size:13px;padding:8px">暂无项目,点击下方按钮添加</div>';
return;
}
var html = '';
for (var i = 0; i < studyItems.length; i++) {
html += '<div class="check-item">' +
'<label><input type="checkbox" id="study-' + i + '" ' + (studyItems[i].done ? 'checked' : '') + '>' + studyItems[i].name + '</label>' +
'<button class="del-btn" onclick="removeStudy(' + i + ')">×</button>' +
'</div>';
}
container.innerHTML = html;
}
function addStudyItem() {
var name = prompt('输入勤学项目名称(如:读一篇文献):');
if (!name || !name.trim()) return;
studyItems.push({ name: name.trim(), done: false });
renderStudyItems();
}
function removeStudy(idx) {
studyItems.splice(idx, 1);
renderStudyItems();
}
function getStudyValues() {
var result = [];
for (var i = 0; i < studyItems.length; i++) {
var cb = document.getElementById('study-' + i);
result.push({ name: studyItems[i].name, done: cb ? cb.checked : false });
}
return result;
}
// ========== 加载/保存打卡 ==========
function loadChecklist() {
var dateEl = document.getElementById('check-date');
if (!dateEl || !dateEl.value) return;
var date = dateEl.value;
var all = getData();
var day = all[date] || {};
// 早间立志
var mvals = day.morningItems || [''];
renderMorningItems(mvals);
// 责善·改过
var rvals = day.reviewItems || { mistakes: [''], improvements: [''] };
renderReviewItems(rvals.mistakes || [''], rvals.improvements || ['']);
// 勤学打卡:恢复当天保存的项目列表
if (day.studyItems && day.studyItems.length > 0) {
studyItems = day.studyItems;
} else {
// 默认三项
studyItems = [
{ name: 'AI能力有进步', done: false },
{ name: '管理能力有进步', done: false },
{ name: '知识储备有进步', done: false }
];
}
renderStudyItems();
}
function saveChecklist() {
var dateEl = document.getElementById('check-date');
if (!dateEl || !dateEl.value) { showToast('请先选择日期', 'error'); return; }
var date = dateEl.value;
var all = getData();
all[date] = {
morningItems: getMorningValues(),
reviewItems: getReviewValues(),
studyItems: getStudyValues(),
savedAt: new Date().toISOString()
};
if (saveData(all)) {
showToast('✅ ' + date + ' 打卡保存成功!', 'success');
} else {
showToast('❌ 保存失败', 'error');
}
}
// ========== 每周评分 ==========
function getMonday(date) {
var d = new Date(date);
var day = d.getDay();
var diff = d.getDate() - day + (day === 0 ? -6 : 1);
d.setDate(diff);
return new Date(d);
}
function formatWeekLabel(monday) {
var sun = new Date(monday);
sun.setDate(monday.getDate() + 6);
var m = monday.getMonth() + 1;
var d1 = monday.getDate();
var s = sun.getMonth() + 1;
var d2 = sun.getDate();
return m + '/' + d1 + ' - ' + s + '/' + d2;
}
function prevWeek() { WEEK_OFFSET--; calculateWeeklyScore(); }
function nextWeek() { WEEK_OFFSET++; calculateWeeklyScore(); }
function goThisWeek() { WEEK_OFFSET = 0; calculateWeeklyScore(); }
function calculateWeeklyScore() {
var today = new Date();
var baseMonday = getMonday(today);
baseMonday.setDate(baseMonday.getDate() + WEEK_OFFSET * 7);
var monday = new Date(baseMonday);
var label = formatWeekLabel(monday);
if (WEEK_OFFSET === 0) label = '本周(' + label + '';
else if (WEEK_OFFSET === -1) label = '上周(' + label + '';
document.getElementById('week-label').textContent = label;
var all = getData();
var totalDays = 0;
var totalScore = 0;
var maxScore = 0;
var details = [];
for (var i = 0; i < 7; i++) {
var cur = new Date(monday);
cur.setDate(monday.getDate() + i);
var ds = cur.toISOString().split('T')[0];
var dayData = all[ds];
if (dayData) {
totalDays++;
var dayMax = 0, dayGot = 0;
// 立志得分:填写了的重要事项数 / 总条目数
if (dayData.morningItems && dayData.morningItems.length > 0) {
dayMax += dayData.morningItems.length;
for (var j = 0; j < dayData.morningItems.length; j++) {
if (dayData.morningItems[j] && dayData.morningItems[j].trim()) dayGot++;
}
}
// 改过得分:每条反思都有文字
if (dayData.reviewItems && dayData.reviewItems.mistakes) {
dayMax += dayData.reviewItems.mistakes.length * 2; // 错 + 改进
for (var k = 0; k < dayData.reviewItems.mistakes.length; k++) {
if (dayData.reviewItems.mistakes[k] && dayData.reviewItems.mistakes[k].trim()) dayGot++;
if (dayData.reviewItems.improvements && dayData.reviewItems.improvements[k] && dayData.reviewItems.improvements[k].trim()) dayGot++;
}
}
// 勤学得分
if (dayData.studyItems && dayData.studyItems.length > 0) {
dayMax += dayData.studyItems.length;
for (var m = 0; m < dayData.studyItems.length; m++) {
if (dayData.studyItems[m].done) dayGot++;
}
}
totalScore += dayGot;
maxScore += dayMax;
var dayPct = dayMax > 0 ? Math.round((dayGot / dayMax) * 100) : 0;
var icon = dayPct >= 70 ? '✅' : dayPct >= 40 ? '⚠️' : '❌';
details.push('<div class="day-row">' +
'<span style="color:#667eea;min-width:90px">' + ds + '</span>' +
'<span style="flex:1">' + dayGot + '/' + dayMax + '</span>' +
'<span>' + icon + ' ' + dayPct + '%</span></div>');
}
}
var score = maxScore > 0 ? Math.round((totalScore / maxScore) * 100) : 0;
document.getElementById('weekly-score').textContent = score;
document.getElementById('score-bar').style.width = 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.length > 0
? '<div style="font-size:12px;color:#999;margin-bottom:8px">每日得分详情:</div>' + details.join('')
: '<div style="text-align:center;color:#999;padding:16px">本周暂无打卡记录</div>';
}
// ========== 历史 ==========
function loadHistory() {
var all = getData();
var dates = Object.keys(all).sort().reverse();
var el = document.getElementById('history-list');
if (dates.length === 0) {
el.innerHTML = '<div class="empty-state">暂无打卡记录</div>';
return;
}
var html = '';
for (var i = 0; i < dates.length; i++) {
var date = dates[i];
var dd = all[date];
var morningCount = dd.morningItems ? dd.morningItems.filter(function(v){return v && v.trim()}).length : 0;
var reviewCount = dd.reviewItems && dd.reviewItems.mistakes ? dd.reviewItems.mistakes.filter(function(v){return v && v.trim()}).length : 0;
var studyDone = dd.studyItems ? dd.studyItems.filter(function(v){return v.done}).length : 0;
var studyTotal = dd.studyItems ? dd.studyItems.length : 0;
html += '<div class="history-item">' +
'<div class="date">' + date + '</div>' +
'<div class="note">' +
(morningCount > 0 ? '<span class="tag done">立志 ' + morningCount + '条</span>' : '<span class="tag miss">未立志</span>') +
(reviewCount > 0 ? '<span class="tag done">改过 ' + reviewCount + '条</span>' : '<span class="tag miss">未改过</span>') +
'<span class="tag ' + (studyDone > 0 ? 'done' : 'miss') + '">勤学 ' + studyDone + '/' + studyTotal + '</span>' +
'</div></div>';
}
el.innerHTML = html;
}
// ========== 初始化 ==========
(function init() {
var today = new Date().toISOString().split('T')[0];
var cd = document.getElementById('check-date');
if (cd) cd.value = today;
// 初始化默认 study items
studyItems = [
{ name: 'AI能力有进步', done: false },
{ name: '管理能力有进步', done: false },
{ name: '知识储备有进步', done: false }
];
loadChecklist();
})();
</script>
</body>
</html>

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
flask==3.1.0

411
static/app.js Normal file
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
static/style.css Normal file
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; }

82
templates/index.html Normal file
View File

@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>紫微 · 磁场管理</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div id="toast" class="toast"></div>
<header>
<div class="header-top">
<h1>⚡ 紫微 · 磁场管理</h1>
<div class="user-bar">
<span class="user-name">{{ username }}</span>
<a href="/logout" class="btn-logout" onclick="return confirm('确定退出登录?')">退出</a>
</div>
</div>
<p>立志不摇,责善不滥,改过不拖,勤学不辍</p>
</header>
<nav class="tabs">
<button class="tab active" onclick="switchTab('daily',event)">📅 每日打卡</button>
<button class="tab" onclick="switchTab('weekly',event)">📊 每周评分</button>
<button class="tab" onclick="switchTab('history',event)">📜 历史记录</button>
</nav>
<!-- ── 每日打卡 ── -->
<section id="daily-tab" class="tab-content active">
<div class="date-row">
<label>日期:</label>
<input type="date" id="check-date" onchange="loadCheckin()">
</div>
<div class="card">
<h2>🌅 早间立志</h2>
<p class="card-desc">今天最重要的 1~3 件事</p>
<div id="morning-list"></div>
<button class="btn-add" onclick="addMorning()">+ 增加一条最多3条</button>
</div>
<div class="card">
<h2>🔍 责善 · 改过</h2>
<p class="card-desc">今天犯的错 & 下次怎么改最多5条</p>
<div id="evening-list"></div>
<button class="btn-add" onclick="addEvening()">+ 增加一条最多5条</button>
</div>
<div class="card">
<h2>📚 勤学打卡</h2>
<p class="card-desc">今天学了什么</p>
<div id="study-list"></div>
<button class="btn-add" onclick="addStudy()">+ 增加项目</button>
</div>
<button class="btn-save" onclick="saveCheckin()">💾 保存今日打卡</button>
</section>
<!-- ── 每周评分 ── -->
<section id="weekly-tab" class="tab-content">
<div class="week-nav">
<button class="btn-nav" onclick="changeWeek(-1)">◀ 上周</button>
<span id="week-label">--</span>
<button class="btn-nav" onclick="changeWeek(1)">下周 ▶</button>
</div>
<div class="score-display">
<span class="score-num" id="weekly-score">--</span>
<span class="score-unit"></span>
</div>
<p class="score-text" id="score-text"></p>
<div id="weekly-details" class="details-list"></div>
</section>
<!-- ── 历史记录 ── -->
<section id="history-tab" class="tab-content">
<div id="history-list"></div>
</section>
<script src="/static/app.js"></script>
</body>
</html>

176
templates/login.html Normal file
View File

@@ -0,0 +1,176 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>紫微 · 登录</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.login-card {
background: #FFF;
border-radius: 16px;
padding: 40px 32px;
width: 100%;
max-width: 380px;
box-shadow: 0 20px 60px rgba(0,0,0,0.15);
}
.login-card h1 {
text-align: center;
font-size: 24px;
color: #4A6CF7;
margin-bottom: 4px;
}
.login-card .sub {
text-align: center;
font-size: 13px;
color: #9CA3AF;
margin-bottom: 28px;
}
.form-group {
margin-bottom: 18px;
}
.form-group label {
display: block;
font-size: 13px;
font-weight: 600;
color: #374151;
margin-bottom: 6px;
}
.form-group input {
width: 100%;
padding: 12px 14px;
border: 1px solid #E5E7EB;
border-radius: 10px;
font-size: 15px;
font-family: inherit;
transition: border-color .2s;
background: #F9FAFB;
}
.form-group input:focus {
outline: none;
border-color: #4A6CF7;
background: #FFF;
box-shadow: 0 0 0 3px rgba(74,108,247,0.1);
}
.remember-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 22px;
font-size: 13px;
color: #6B7280;
}
.remember-row input[type="checkbox"] {
width: 16px; height: 16px;
accent-color: #4A6CF7;
}
.btn-login {
width: 100%;
padding: 13px;
border: none;
background: #4A6CF7;
color: #FFF;
font-size: 16px;
font-weight: 600;
border-radius: 10px;
cursor: pointer;
transition: all .2s;
}
.btn-login:hover { opacity: 0.9; transform: translateY(-1px); }
.btn-login:active { transform: translateY(0); }
.btn-login.loading {
opacity: 0.7;
pointer-events: none;
}
.error-msg {
text-align: center;
color: #EF4444;
font-size: 13px;
margin-top: 12px;
display: none;
}
.error-msg.show { display: block; }
.footer-text {
text-align: center;
font-size: 12px;
color: #9CA3AF;
margin-top: 24px;
}
</style>
</head>
<body>
<div class="login-card">
<h1>⚡ 紫微 · 磁场管理</h1>
<p class="sub">立志不摇,责善不滥,改过不拖,勤学不辍</p>
<form id="login-form" onsubmit="doLogin(event)">
<div class="form-group">
<label>账号</label>
<input type="text" id="username" placeholder="请输入账号" autocomplete="username" required autofocus>
</div>
<div class="form-group">
<label>密码</label>
<input type="password" id="password" placeholder="请输入密码" autocomplete="current-password" required>
</div>
<div class="remember-row">
<input type="checkbox" id="remember" checked>
<label for="remember">记住登录30 天有效)</label>
</div>
<button type="submit" class="btn-login" id="btn-login">登 录</button>
</form>
<div class="error-msg" id="error-msg"></div>
<p class="footer-text">紫微 · 天机阁磁场守护系统</p>
</div>
<script>
(function(){
'use strict';
window.doLogin = function(e) {
e.preventDefault();
var btn = document.getElementById('btn-login');
var err = document.getElementById('error-msg');
btn.classList.add('loading');
btn.textContent = '登录中…';
err.classList.remove('show');
var formData = new FormData();
formData.append('username', document.getElementById('username').value);
formData.append('password', document.getElementById('password').value);
if (document.getElementById('remember').checked) {
formData.append('remember', 'on');
}
fetch('/login', { method: 'POST', body: formData })
.then(function(r) { return r.json().then(function(d){ return {status: r.status, body: d}; }); })
.then(function(res) {
if (res.body.ok) {
window.location.href = res.body.next;
} else {
err.textContent = res.body.error || '登录失败';
err.classList.add('show');
}
})
.catch(function() {
err.textContent = '网络错误,请重试';
err.classList.add('show');
})
.finally(function() {
btn.classList.remove('loading');
btn.textContent = '登 录';
});
};
})();
</script>
</body>
</html>