初始化 ziwei-power:紫微磁场管理系统(Flask + SQLite + 登录鉴权)
This commit is contained in:
20
.gitignore
vendored
Normal file
20
.gitignore
vendored
Normal 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
130
app.py
Normal 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
94
database.py
Normal 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
552
index.html
Normal 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
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
flask==3.1.0
|
||||
411
static/app.js
Normal file
411
static/app.js
Normal 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,'&').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();
|
||||
}
|
||||
})();
|
||||
313
static/style.css
Normal file
313
static/style.css
Normal 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
82
templates/index.html
Normal 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
176
templates/login.html
Normal 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>
|
||||
Reference in New Issue
Block a user