feat: 紫微·磁场管理打卡系统
- Flomo 风格左右布局(侧边栏+主内容区) - 日历导航:月视图,三色状态圆点,点击日期快速切换 - 四层修为:立志/责善/改过/勤学 - 勤学 8 个固定预设项目,勾选后显示备注 - 编辑后 1.5 秒自动保存,切换日期静默保存 - Flask + SQLite,端口 5056
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/
|
||||||
185
app.py
Normal file
185
app.py
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
# -*- 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/stats', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def api_stats():
|
||||||
|
"""返回统计数据和日历状态映射"""
|
||||||
|
rows = get_all_checkins()
|
||||||
|
|
||||||
|
total_days = len(rows)
|
||||||
|
total_morning = 0
|
||||||
|
total_evening = 0
|
||||||
|
total_study = 0
|
||||||
|
calendar = {}
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
d = row['date']
|
||||||
|
data = row['data']
|
||||||
|
morning = data.get('morning', [])
|
||||||
|
evening = data.get('evening', [])
|
||||||
|
study = data.get('study', [])
|
||||||
|
|
||||||
|
# 统计条目
|
||||||
|
total_morning += sum(1 for x in morning if isinstance(x, str) and x.strip())
|
||||||
|
total_evening += sum(1 for x in evening if (
|
||||||
|
isinstance(x, str) and x.strip() or
|
||||||
|
isinstance(x, dict) and (x.get('mistake', '') or '').strip()
|
||||||
|
))
|
||||||
|
total_study += sum(1 for x in study if x.get('done'))
|
||||||
|
|
||||||
|
# 评分
|
||||||
|
day_score = 0
|
||||||
|
has_morning = any(isinstance(x, str) and x.strip() for x in morning)
|
||||||
|
has_evening = any(
|
||||||
|
(isinstance(x, str) and x.strip()) or
|
||||||
|
(isinstance(x, dict) and (x.get('mistake', '') or '').strip())
|
||||||
|
for x in evening
|
||||||
|
)
|
||||||
|
has_study = any(x.get('done') for x in study)
|
||||||
|
if has_morning:
|
||||||
|
day_score += 1
|
||||||
|
if has_evening:
|
||||||
|
day_score += 1
|
||||||
|
if has_study:
|
||||||
|
day_score += 1
|
||||||
|
|
||||||
|
calendar[d] = 'pass' if day_score >= 2 else 'fail'
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'ok': True,
|
||||||
|
'total_days': total_days,
|
||||||
|
'total_morning': total_morning,
|
||||||
|
'total_evening': total_evening,
|
||||||
|
'total_study': total_study,
|
||||||
|
'calendar': calendar
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@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=5056, 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
|
||||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
flask==3.1.0
|
||||||
658
static/app.js
Normal file
658
static/app.js
Normal file
@@ -0,0 +1,658 @@
|
|||||||
|
/* ziwei-power 前端逻辑 — Flomo 风格左右布局 */
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
State
|
||||||
|
================================================================ */
|
||||||
|
var calendarStatus = {}; // { "2026-06-01": "pass", ... }
|
||||||
|
var calYear, calMonth; // 日历显示的月份
|
||||||
|
var activePanel = 'daily'; // 当前面板
|
||||||
|
var weekOffset = 0; // 每周评分偏移
|
||||||
|
var autoSaveTimer = null; // 自动保存防抖
|
||||||
|
var lastSavedData = null; // 上次保存快照
|
||||||
|
var lastSavedDate = null; // 上次保存日期
|
||||||
|
|
||||||
|
/* 勤学预设项目 */
|
||||||
|
var PRESET_STUDY_ITEMS = [
|
||||||
|
'晚上 11 点 30 之前睡觉',
|
||||||
|
'早上 6 点 30 之前起床',
|
||||||
|
'起床后做 30 个俯卧撑',
|
||||||
|
'销售客情能力提升',
|
||||||
|
'管理能力提升',
|
||||||
|
'产品规划有进一步的提升',
|
||||||
|
'AI 能力提升',
|
||||||
|
'知识库能力提升'
|
||||||
|
];
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
Toast
|
||||||
|
================================================================ */
|
||||||
|
var toastTimer = null;
|
||||||
|
function showToast(msg, type) {
|
||||||
|
type = type || 'info';
|
||||||
|
var el = document.getElementById('toast');
|
||||||
|
el.textContent = msg;
|
||||||
|
el.className = 'toast ' + type + ' show';
|
||||||
|
clearTimeout(toastTimer);
|
||||||
|
toastTimer = setTimeout(function(){ el.classList.remove('show'); }, 2500);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
API
|
||||||
|
================================================================ */
|
||||||
|
function apiGetCheckin(date, cb) {
|
||||||
|
fetch('/api/checkin?date=' + encodeURIComponent(date))
|
||||||
|
.then(function(r){ return r.json(); })
|
||||||
|
.then(function(res){ if (res.ok) cb(null, res.data); else cb(res.error); })
|
||||||
|
.catch(function(e){ cb(e.message); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function apiSaveCheckin(date, data, cb) {
|
||||||
|
fetch('/api/checkin', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({date: date, data: data})
|
||||||
|
})
|
||||||
|
.then(function(r){ return r.json(); })
|
||||||
|
.then(function(res){ if (res.ok) cb(null); else cb(res.error); })
|
||||||
|
.catch(function(e){ cb(e.message); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function apiDeleteCheckin(date, cb) {
|
||||||
|
fetch('/api/checkin/' + encodeURIComponent(date), {method: 'DELETE'})
|
||||||
|
.then(function(r){ return r.json(); })
|
||||||
|
.then(function(res){ if (res.ok) cb(null); else cb(res.error); })
|
||||||
|
.catch(function(e){ cb(e.message); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function apiGetHistory(cb) {
|
||||||
|
fetch('/api/history')
|
||||||
|
.then(function(r){ return r.json(); })
|
||||||
|
.then(function(res){ if (res.ok) cb(null, res.data); else cb(res.error); })
|
||||||
|
.catch(function(e){ cb(e.message); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function apiGetStats(cb) {
|
||||||
|
fetch('/api/stats')
|
||||||
|
.then(function(r){ return r.json(); })
|
||||||
|
.then(function(res){ if (res.ok) cb(null, res.data); else cb(res.error); })
|
||||||
|
.catch(function(e){ cb(e.message); });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
Escaping
|
||||||
|
================================================================ */
|
||||||
|
function esc(s) {
|
||||||
|
return String(s).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
Panel Switching
|
||||||
|
================================================================ */
|
||||||
|
window.switchPanel = function(name) {
|
||||||
|
activePanel = name;
|
||||||
|
var navs = document.querySelectorAll('.sidebar-nav .nav-item');
|
||||||
|
var panels = document.querySelectorAll('.main-content .panel');
|
||||||
|
|
||||||
|
for (var i=0; i<navs.length; i++) navs[i].classList.remove('active');
|
||||||
|
for (var j=0; j<panels.length; j++) panels[j].classList.remove('active');
|
||||||
|
|
||||||
|
var nav = document.querySelector('.nav-item[data-panel="' + name + '"]');
|
||||||
|
var panel = document.getElementById('panel-' + name);
|
||||||
|
if (nav) nav.classList.add('active');
|
||||||
|
if (panel) panel.classList.add('active');
|
||||||
|
|
||||||
|
if (name === 'weekly') loadWeekly();
|
||||||
|
if (name === 'history') loadHistory();
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
Calendar
|
||||||
|
================================================================ */
|
||||||
|
|
||||||
|
/** 格式化日期 */
|
||||||
|
function fmtDate(d) {
|
||||||
|
var y = d.getFullYear();
|
||||||
|
var m = ('0'+(d.getMonth()+1)).slice(-2);
|
||||||
|
var dd = ('0'+d.getDate()).slice(-2);
|
||||||
|
return y + '-' + m + '-' + dd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断两个日期是否同一天 */
|
||||||
|
function isSameDay(a, b) {
|
||||||
|
return a.getFullYear() === b.getFullYear() &&
|
||||||
|
a.getMonth() === b.getMonth() &&
|
||||||
|
a.getDate() === b.getDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 切换日历月份 */
|
||||||
|
window.changeCalMonth = function(dir) {
|
||||||
|
calMonth += dir;
|
||||||
|
if (calMonth > 12) { calMonth = 1; calYear++; }
|
||||||
|
if (calMonth < 1) { calMonth = 12; calYear--; }
|
||||||
|
renderCalendar();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 渲染日历 */
|
||||||
|
function renderCalendar() {
|
||||||
|
var today = new Date();
|
||||||
|
var firstDay = new Date(calYear, calMonth - 1, 1);
|
||||||
|
var lastDay = new Date(calYear, calMonth, 0);
|
||||||
|
var startDayOfWeek = firstDay.getDay(); // 0=Sun
|
||||||
|
var totalDays = lastDay.getDate();
|
||||||
|
var prevMonthLastDay = new Date(calYear, calMonth - 1, 0).getDate();
|
||||||
|
|
||||||
|
// 月份标签
|
||||||
|
document.getElementById('cal-month-label').textContent =
|
||||||
|
calYear + '年 ' + calMonth + '月';
|
||||||
|
|
||||||
|
var grid = document.getElementById('cal-grid');
|
||||||
|
var html = '';
|
||||||
|
|
||||||
|
// 前月填充
|
||||||
|
for (var p = startDayOfWeek - 1; p >= 0; p--) {
|
||||||
|
html += '<div class="cal-cell other-month">' + (prevMonthLastDay - p) + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当月
|
||||||
|
for (var d = 1; d <= totalDays; d++) {
|
||||||
|
var cur = new Date(calYear, calMonth - 1, d);
|
||||||
|
var ds = fmtDate(cur);
|
||||||
|
var classes = ['cal-cell'];
|
||||||
|
var isOtherMonth = false;
|
||||||
|
|
||||||
|
if (isSameDay(cur, today)) classes.push('today');
|
||||||
|
var status = calendarStatus[ds];
|
||||||
|
if (status) classes.push(status);
|
||||||
|
|
||||||
|
html += '<div class="' + classes.join(' ') + '" data-date="' + ds + '">' + d + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后月填充 — 始终 42 格(6行 × 7列),保持高度一致
|
||||||
|
var totalCells = startDayOfWeek + totalDays;
|
||||||
|
var remaining = 42 - totalCells;
|
||||||
|
for (var n = 1; n <= remaining; n++) {
|
||||||
|
html += '<div class="cal-cell other-month">' + n + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.innerHTML = html;
|
||||||
|
|
||||||
|
// 点击事件
|
||||||
|
grid.querySelectorAll('.cal-cell:not(.other-month)').forEach(function(cell) {
|
||||||
|
cell.addEventListener('click', function() {
|
||||||
|
var date = this.dataset.date;
|
||||||
|
if (date) navigateToDate(date);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导航到指定日期 */
|
||||||
|
function navigateToDate(date) {
|
||||||
|
switchPanel('daily');
|
||||||
|
document.getElementById('check-date').value = date;
|
||||||
|
loadCheckin();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 回到今天 */
|
||||||
|
window.goToday = function() {
|
||||||
|
var today = fmtDate(new Date());
|
||||||
|
calYear = new Date().getFullYear();
|
||||||
|
calMonth = new Date().getMonth() + 1;
|
||||||
|
renderCalendar();
|
||||||
|
navigateToDate(today);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
Stats — 加载统计数据并更新侧边栏
|
||||||
|
================================================================ */
|
||||||
|
function loadStats() {
|
||||||
|
apiGetStats(function(err, data) {
|
||||||
|
if (err) return;
|
||||||
|
// 更新统计数字
|
||||||
|
document.getElementById('stat-days').textContent = data.total_days;
|
||||||
|
document.getElementById('stat-morning').textContent = data.total_morning;
|
||||||
|
document.getElementById('stat-study').textContent = data.total_study;
|
||||||
|
|
||||||
|
// 更新日历状态
|
||||||
|
calendarStatus = data.calendar || {};
|
||||||
|
renderCalendar();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
Build / Fill Data
|
||||||
|
================================================================ */
|
||||||
|
|
||||||
|
function buildData() {
|
||||||
|
var morningItems = [];
|
||||||
|
var mEls = document.querySelectorAll('#morning-list .item-row input');
|
||||||
|
for (var i=0; i<mEls.length; i++) {
|
||||||
|
var v = mEls[i].value.trim();
|
||||||
|
if (v) morningItems.push(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
var eveningItems = [];
|
||||||
|
var eRows = document.querySelectorAll('#evening-list .evening-row');
|
||||||
|
for (var j=0; j<eRows.length; j++) {
|
||||||
|
var cols = eRows[j].querySelectorAll('.col input');
|
||||||
|
var mistake = cols[0] ? cols[0].value.trim() : '';
|
||||||
|
var improve = cols[1] ? cols[1].value.trim() : '';
|
||||||
|
if (mistake || improve) eveningItems.push({ mistake: mistake, improvement: improve });
|
||||||
|
}
|
||||||
|
|
||||||
|
var studyItems = [];
|
||||||
|
var pItems = document.querySelectorAll('#study-list .preset-item');
|
||||||
|
for (var k=0; k<pItems.length; k++) {
|
||||||
|
var cb = pItems[k].querySelector('input[type="checkbox"]');
|
||||||
|
var noteEl = pItems[k].querySelector('.preset-note');
|
||||||
|
var nameEl = pItems[k].querySelector('.preset-name');
|
||||||
|
var name = nameEl ? nameEl.textContent : '';
|
||||||
|
if (cb && cb.checked) {
|
||||||
|
studyItems.push({ name: name, done: true, note: noteEl ? noteEl.value.trim() : '' });
|
||||||
|
} else {
|
||||||
|
studyItems.push({ name: name, done: false, note: '' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { morning: morningItems, evening: eveningItems, study: studyItems };
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillForm(data) {
|
||||||
|
data = data || {};
|
||||||
|
var morning = data.morning || [];
|
||||||
|
var evening = data.evening || [];
|
||||||
|
var study = data.study || [];
|
||||||
|
|
||||||
|
// 早间立志
|
||||||
|
document.getElementById('morning-list').innerHTML = '';
|
||||||
|
if (morning.length === 0) morning = [''];
|
||||||
|
for (var m=0; m<morning.length; m++) addMorningRow(morning[m]);
|
||||||
|
|
||||||
|
// 责善改过
|
||||||
|
document.getElementById('evening-list').innerHTML = '';
|
||||||
|
if (evening.length === 0) evening = [{}];
|
||||||
|
for (var e=0; e<evening.length; e++) {
|
||||||
|
var item = typeof evening[e] === 'string' ? {mistake: evening[e], improvement: ''} : evening[e];
|
||||||
|
addEveningRow(item.mistake || '', item.improvement || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 勤学 — 渲染预设项目并回填状态
|
||||||
|
var studyMap = {};
|
||||||
|
for (var si=0; si<study.length; si++) {
|
||||||
|
studyMap[study[si].name] = study[si];
|
||||||
|
}
|
||||||
|
document.getElementById('study-list').innerHTML = '';
|
||||||
|
for (var sp=0; sp<PRESET_STUDY_ITEMS.length; sp++) {
|
||||||
|
var sName = PRESET_STUDY_ITEMS[sp];
|
||||||
|
var saved = studyMap[sName] || { done: false, note: '' };
|
||||||
|
addPresetItem(sName, saved.done, saved.note || '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
Dynamic Rows
|
||||||
|
================================================================ */
|
||||||
|
|
||||||
|
function addMorningRow(val) {
|
||||||
|
var container = document.getElementById('morning-list');
|
||||||
|
var idx = container.children.length;
|
||||||
|
val = val || '';
|
||||||
|
var div = document.createElement('div');
|
||||||
|
div.className = 'item-row';
|
||||||
|
div.innerHTML = '<span class="idx">' + (idx+1) + '.</span>' +
|
||||||
|
'<input type="text" value="' + esc(val) + '" placeholder="今天最重要的一件事…">' +
|
||||||
|
'<button class="btn-del" onclick="this.parentElement.remove();renumberMorning()"><svg class="icon-sm"><use href="#icon-x"/></svg></button>';
|
||||||
|
container.appendChild(div);
|
||||||
|
renumberMorning();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renumberMorning() {
|
||||||
|
var rows = document.querySelectorAll('#morning-list .item-row');
|
||||||
|
for (var i=0; i<rows.length; i++) {
|
||||||
|
var span = rows[i].querySelector('.idx');
|
||||||
|
if (span) span.textContent = (i+1) + '.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addEveningRow(mistake, improve) {
|
||||||
|
var container = document.getElementById('evening-list');
|
||||||
|
var idx = container.children.length;
|
||||||
|
mistake = mistake || '';
|
||||||
|
improve = improve || '';
|
||||||
|
var div = document.createElement('div');
|
||||||
|
div.className = 'evening-row';
|
||||||
|
div.innerHTML =
|
||||||
|
'<div class="evening-header">' +
|
||||||
|
'<span class="idx">' + (idx+1) + '.</span>' +
|
||||||
|
'<button class="btn-del" onclick="this.parentElement.parentElement.remove();renumberEvening()"><svg class="icon-sm"><use href="#icon-x"/></svg></button>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="mistake-row">' +
|
||||||
|
'<div class="col"><label>错误</label><input type="text" value="' + esc(mistake) + '" placeholder="今天犯的错…"></div>' +
|
||||||
|
'<div class="col"><label>改进方案</label><input type="text" value="' + esc(improve) + '" placeholder="下次怎么做…"></div>' +
|
||||||
|
'</div>';
|
||||||
|
container.appendChild(div);
|
||||||
|
renumberEvening();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renumberEvening() {
|
||||||
|
var rows = document.querySelectorAll('#evening-list .evening-row');
|
||||||
|
for (var i=0; i<rows.length; i++) {
|
||||||
|
var span = rows[i].querySelector('.idx');
|
||||||
|
if (span) span.textContent = (i+1) + '.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 勤学预设项目 ── */
|
||||||
|
|
||||||
|
function addPresetItem(name, done, note) {
|
||||||
|
var container = document.getElementById('study-list');
|
||||||
|
var div = document.createElement('div');
|
||||||
|
div.className = 'preset-item' + (done ? ' active' : '');
|
||||||
|
var noteEsc = esc(note || '');
|
||||||
|
div.innerHTML =
|
||||||
|
'<label class="preset-check">' +
|
||||||
|
'<input type="checkbox"' + (done ? ' checked' : '') + ' onchange="togglePresetNote(this)">' +
|
||||||
|
'<span class="preset-name">' + esc(name) + '</span>' +
|
||||||
|
'</label>' +
|
||||||
|
'<textarea class="preset-note" placeholder="一句话备注…">' + noteEsc + '</textarea>';
|
||||||
|
container.appendChild(div);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.togglePresetNote = function(cb) {
|
||||||
|
var item = cb.closest('.preset-item');
|
||||||
|
if (item) {
|
||||||
|
if (cb.checked) item.classList.add('active');
|
||||||
|
else { item.classList.remove('active'); item.querySelector('.preset-note').value = ''; }
|
||||||
|
}
|
||||||
|
triggerAutoSave();
|
||||||
|
};
|
||||||
|
|
||||||
|
function initStudyPresets() {
|
||||||
|
document.getElementById('study-list').innerHTML = '';
|
||||||
|
for (var i=0; i<PRESET_STUDY_ITEMS.length; i++) {
|
||||||
|
addPresetItem(PRESET_STUDY_ITEMS[i], false, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addMorning = function() {
|
||||||
|
var count = document.querySelectorAll('#morning-list .item-row').length;
|
||||||
|
if (count >= 3) { showToast('最多 3 条立志', 'error'); return; }
|
||||||
|
addMorningRow('');
|
||||||
|
};
|
||||||
|
window.addEvening = function() {
|
||||||
|
var count = document.querySelectorAll('#evening-list .evening-row').length;
|
||||||
|
if (count >= 5) { showToast('最多 5 条改过', 'error'); return; }
|
||||||
|
addEveningRow('', '');
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
Load & Auto Save Checkin
|
||||||
|
================================================================ */
|
||||||
|
var lastSavedData = null; // 上次保存的数据快照,避免重复保存
|
||||||
|
|
||||||
|
window.loadCheckin = function() {
|
||||||
|
var date = document.getElementById('check-date').value;
|
||||||
|
if (!date) return;
|
||||||
|
|
||||||
|
// 切换日期前先保存当前数据
|
||||||
|
var currentData = buildData();
|
||||||
|
var currentStr = JSON.stringify(currentData);
|
||||||
|
if (lastSavedDate && lastSavedData !== null && currentStr !== lastSavedData) {
|
||||||
|
// 有未保存的变更,先静默保存
|
||||||
|
var hasContent = currentData.morning.some(function(x){return x && x.trim();}) ||
|
||||||
|
currentData.evening.some(function(x){return (x.mistake||'').trim() || (x.improvement||'').trim();}) ||
|
||||||
|
currentData.study.some(function(x){return x.done;});
|
||||||
|
if (hasContent) {
|
||||||
|
apiSaveCheckin(lastSavedDate, currentData, function(){}); // 静默保存
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastSavedDate = date;
|
||||||
|
apiGetCheckin(date, function(err, row){
|
||||||
|
if (err) { initStudyPresets(); lastSavedData = '{}'; return; }
|
||||||
|
fillForm(row ? row.data : null);
|
||||||
|
lastSavedData = JSON.stringify(buildData());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 自动保存(带防抖) */
|
||||||
|
function triggerAutoSave() {
|
||||||
|
clearTimeout(autoSaveTimer);
|
||||||
|
autoSaveTimer = setTimeout(function() {
|
||||||
|
var date = document.getElementById('check-date').value;
|
||||||
|
if (!date) return;
|
||||||
|
var data = buildData();
|
||||||
|
var dataStr = JSON.stringify(data);
|
||||||
|
if (dataStr === lastSavedData) return; // 无变化,跳过
|
||||||
|
apiSaveCheckin(date, data, function(err){
|
||||||
|
if (err) { showToast('自动保存失败', 'error'); return; }
|
||||||
|
lastSavedData = dataStr;
|
||||||
|
showToast('已自动保存', 'info');
|
||||||
|
loadStats();
|
||||||
|
});
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 为每日打卡面板绑定自动保存事件 */
|
||||||
|
function bindAutoSave() {
|
||||||
|
var panel = document.getElementById('panel-daily');
|
||||||
|
if (!panel) return;
|
||||||
|
|
||||||
|
// 输入框 & 文本域:input 事件
|
||||||
|
panel.addEventListener('input', function(e) {
|
||||||
|
if (e.target.matches('input[type="text"], textarea, input[type="date"]')) {
|
||||||
|
triggerAutoSave();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 复选框:change 事件
|
||||||
|
panel.addEventListener('change', function(e) {
|
||||||
|
if (e.target.matches('input[type="checkbox"]')) {
|
||||||
|
triggerAutoSave();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 删除行按钮:点击后触发
|
||||||
|
panel.addEventListener('click', function(e) {
|
||||||
|
var btn = e.target.closest('.btn-del');
|
||||||
|
if (btn) {
|
||||||
|
setTimeout(triggerAutoSave, 100); // 等 DOM 更新后
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 预设项目勾选切换
|
||||||
|
panel.addEventListener('change', function(e) {
|
||||||
|
if (e.target.matches('.preset-check input[type="checkbox"]')) {
|
||||||
|
triggerAutoSave();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
Weekly Score
|
||||||
|
================================================================ */
|
||||||
|
var WEEKDAYS_CN = ['日','一','二','三','四','五','六'];
|
||||||
|
|
||||||
|
function getMonday(d) {
|
||||||
|
var day = d.getDay();
|
||||||
|
var diff = d.getDate() - day + (day === 0 ? -6 : 1);
|
||||||
|
var m = new Date(d);
|
||||||
|
m.setDate(diff);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.changeWeek = function(dir) {
|
||||||
|
weekOffset += dir;
|
||||||
|
loadWeekly();
|
||||||
|
};
|
||||||
|
|
||||||
|
function loadWeekly() {
|
||||||
|
var today = new Date();
|
||||||
|
today.setDate(today.getDate() + weekOffset * 7);
|
||||||
|
var monday = getMonday(today);
|
||||||
|
var sunday = new Date(monday);
|
||||||
|
sunday.setDate(monday.getDate() + 6);
|
||||||
|
|
||||||
|
document.getElementById('week-label').textContent =
|
||||||
|
fmtDate(monday) + ' ~ ' + fmtDate(sunday);
|
||||||
|
|
||||||
|
apiGetHistory(function(err, rows){
|
||||||
|
if (err) { showToast('加载失败', 'error'); return; }
|
||||||
|
var map = {};
|
||||||
|
for (var i=0; i<rows.length; i++) map[rows[i].date] = rows[i].data;
|
||||||
|
|
||||||
|
var totalDays = 0, totalScore = 0, dayCells = [];
|
||||||
|
|
||||||
|
for (var j=0; j<7; j++) {
|
||||||
|
var cur = new Date(monday);
|
||||||
|
cur.setDate(monday.getDate() + j);
|
||||||
|
var ds = fmtDate(cur);
|
||||||
|
var dd = map[ds];
|
||||||
|
|
||||||
|
if (dd) {
|
||||||
|
totalDays++;
|
||||||
|
var dayScore = 0;
|
||||||
|
var morning = dd.morning || [];
|
||||||
|
var hasMorning = morning.some(function(x){ return x && typeof x === 'string' && x.trim(); });
|
||||||
|
var evening = dd.evening || [];
|
||||||
|
var hasEvening = evening.some(function(x){
|
||||||
|
var mst = typeof x === 'string' ? x : (x.mistake || '');
|
||||||
|
return mst && typeof mst === 'string' && mst.trim();
|
||||||
|
});
|
||||||
|
var study = dd.study || [];
|
||||||
|
var hasStudy = study.some(function(x){ return x.done; });
|
||||||
|
if (hasMorning) dayScore++;
|
||||||
|
if (hasEvening) dayScore++;
|
||||||
|
if (hasStudy) dayScore++;
|
||||||
|
totalScore += dayScore;
|
||||||
|
var status = dayScore >= 2 ? 'pass' : 'fail';
|
||||||
|
|
||||||
|
dayCells.push({
|
||||||
|
ds: ds, dayScore: dayScore, status: status,
|
||||||
|
weekday: '周' + WEEKDAYS_CN[cur.getDay()]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dayCells.push({
|
||||||
|
ds: ds, dayScore: 0, status: 'empty',
|
||||||
|
weekday: '周' + WEEKDAYS_CN[cur.getDay()]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var score = totalDays > 0 ? Math.round((totalScore / (totalDays * 3)) * 100) : 0;
|
||||||
|
document.getElementById('weekly-score').textContent = score;
|
||||||
|
|
||||||
|
var ring = document.querySelector('.score-ring');
|
||||||
|
if (ring) {
|
||||||
|
var R = 50, C = 2 * Math.PI * R;
|
||||||
|
ring.setAttribute('stroke-dasharray', C);
|
||||||
|
ring.setAttribute('stroke-dashoffset', C - (C * score / 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
var txt = '';
|
||||||
|
if (score >= 90) txt = '卓越!磁场非常强大';
|
||||||
|
else if (score >= 70) txt = '良好,继续保持';
|
||||||
|
else if (score >= 50) txt = '一般,需要加强';
|
||||||
|
else txt = '较弱,急需调整';
|
||||||
|
document.getElementById('score-text').textContent = txt;
|
||||||
|
|
||||||
|
var completedDays = dayCells.filter(function(c){ return c.status === 'pass'; }).length;
|
||||||
|
var totalFilled = dayCells.filter(function(c){ return c.status !== 'empty'; }).length;
|
||||||
|
document.getElementById('score-stats').textContent =
|
||||||
|
'本周填卡 ' + totalFilled + '/7 天 · 达标 ' + completedDays + ' 天';
|
||||||
|
|
||||||
|
var gridHtml = '';
|
||||||
|
for (var g=0; g<dayCells.length; g++) {
|
||||||
|
var cell = dayCells[g];
|
||||||
|
var dateParts = cell.ds.split('-');
|
||||||
|
var shortDate = dateParts[1] + '/' + dateParts[2];
|
||||||
|
var badgeText = cell.status === 'pass' ? '完成' : (cell.status === 'fail' ? '未达标' : '未打卡');
|
||||||
|
gridHtml += '<div class="day-cell ' + cell.status + '">' +
|
||||||
|
'<div class="day-label">' + cell.weekday + '</div>' +
|
||||||
|
'<div class="day-date">' + shortDate + '</div>' +
|
||||||
|
'<div class="day-score">' + cell.dayScore + '<span style="font-size:11px;font-weight:400">/3</span></div>' +
|
||||||
|
'<div class="day-badge">' + badgeText + '</div>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
document.getElementById('week-days-grid').innerHTML = gridHtml;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
History
|
||||||
|
================================================================ */
|
||||||
|
function loadHistory() {
|
||||||
|
apiGetHistory(function(err, rows){
|
||||||
|
var el = document.getElementById('history-grid');
|
||||||
|
if (err) {
|
||||||
|
el.innerHTML = '<div class="empty-state"><div class="empty-icon"><svg viewBox="0 0 24 24"><use href="#icon-list-bullet"/></svg></div>加载失败</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!rows || rows.length === 0) {
|
||||||
|
el.innerHTML = '<div class="empty-state"><div class="empty-icon"><svg viewBox="0 0 24 24"><use href="#icon-list-bullet"/></svg></div>暂无历史记录</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var html = '';
|
||||||
|
for (var i=0; i<rows.length; i++) {
|
||||||
|
var r = rows[i];
|
||||||
|
var morning = (r.data.morning || []).filter(function(x){ return x && typeof x === 'string' && x.trim(); });
|
||||||
|
var evening = (r.data.evening || []).filter(function(x){
|
||||||
|
var mst = typeof x === 'string' ? x : (x.mistake || '');
|
||||||
|
return mst && typeof mst === 'string' && mst.trim();
|
||||||
|
});
|
||||||
|
var study = (r.data.study || []).filter(function(x){ return x.done; });
|
||||||
|
|
||||||
|
var tags = [];
|
||||||
|
if (morning.length) tags.push('<span><svg class="icon-xs" style="color:var(--primary)"><use href="#icon-sun"/></svg> 立志 ' + morning.length + '</span>');
|
||||||
|
if (evening.length) tags.push('<span><svg class="icon-xs" style="color:var(--danger)"><use href="#icon-magnifying-glass"/></svg> 改过 ' + evening.length + '</span>');
|
||||||
|
if (study.length) tags.push('<span><svg class="icon-xs" style="color:#059669"><use href="#icon-book-open"/></svg> 勤学 ' + study.length + '</span>');
|
||||||
|
|
||||||
|
html += '<div class="history-card">' +
|
||||||
|
'<div class="info">' +
|
||||||
|
'<div class="date">' + r.date + '</div>' +
|
||||||
|
'<div class="preview">' + (tags.join('') || '暂无内容') + '</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<button class="btn-del-history" onclick="deleteHistory(\'' + r.date + '\')"><svg class="icon-xs"><use href="#icon-trash"/></svg> 删除</button>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
el.innerHTML = html;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.deleteHistory = function(date) {
|
||||||
|
if (!confirm('确定删除 ' + date + ' 的打卡记录吗?此操作不可恢复!')) return;
|
||||||
|
apiDeleteCheckin(date, function(err){
|
||||||
|
if (err) { showToast('删除失败: ' + err, 'error'); return; }
|
||||||
|
showToast('已删除', 'info');
|
||||||
|
loadHistory();
|
||||||
|
loadStats(); // 更新统计
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
Init
|
||||||
|
================================================================ */
|
||||||
|
function init() {
|
||||||
|
var today = new Date();
|
||||||
|
calYear = today.getFullYear();
|
||||||
|
calMonth = today.getMonth() + 1;
|
||||||
|
|
||||||
|
var cd = document.getElementById('check-date');
|
||||||
|
var todayStr = fmtDate(today);
|
||||||
|
if (cd) cd.value = todayStr;
|
||||||
|
|
||||||
|
initStudyPresets();
|
||||||
|
bindAutoSave();
|
||||||
|
lastSavedDate = todayStr;
|
||||||
|
loadCheckin();
|
||||||
|
loadStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
})();
|
||||||
1052
static/style.css
Normal file
1052
static/style.css
Normal file
File diff suppressed because it is too large
Load Diff
66
templates/icons.html
Normal file
66
templates/icons.html
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
|
||||||
|
<!-- 用户 -->
|
||||||
|
<symbol id="icon-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"/><path d="M4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z"/>
|
||||||
|
</symbol>
|
||||||
|
<!-- 锁 -->
|
||||||
|
<symbol id="icon-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z"/>
|
||||||
|
</symbol>
|
||||||
|
<!-- 闪电 -->
|
||||||
|
<symbol id="icon-bolt" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75Z"/>
|
||||||
|
</symbol>
|
||||||
|
<!-- 日历 -->
|
||||||
|
<symbol id="icon-calendar" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M6.75 2.994v2.25m10.5-2.25v2.25m-14.252 13.5V7.491a2.25 2.25 0 0 1 2.25-2.25h13.5a2.25 2.25 0 0 1 2.25 2.25v13.5m-16.5 0a2.25 2.25 0 0 0 2.25 2.25h13.5a2.25 2.25 0 0 0 2.25-2.25m-16.5 0v-6.75a2.25 2.25 0 0 1 2.25-2.25h13.5a2.25 2.25 0 0 1 2.25 2.25v6.75m-16.5 0h16.5"/>
|
||||||
|
</symbol>
|
||||||
|
<!-- 图表 -->
|
||||||
|
<symbol id="icon-chart-bar" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75Zm6.75-4.5c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625Zm6.75-4.5c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z"/>
|
||||||
|
</symbol>
|
||||||
|
<!-- 文档列表 -->
|
||||||
|
<symbol id="icon-list-bullet" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/>
|
||||||
|
</symbol>
|
||||||
|
<!-- 放大镜 -->
|
||||||
|
<symbol id="icon-magnifying-glass" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"/>
|
||||||
|
</symbol>
|
||||||
|
<!-- 书本 -->
|
||||||
|
<symbol id="icon-book-open" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25"/>
|
||||||
|
</symbol>
|
||||||
|
<!-- 太阳 -->
|
||||||
|
<symbol id="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"/>
|
||||||
|
</symbol>
|
||||||
|
<!-- 加号 -->
|
||||||
|
<symbol id="icon-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M12 4.5v15m7.5-7.5h-15"/>
|
||||||
|
</symbol>
|
||||||
|
<!-- 删除 -->
|
||||||
|
<symbol id="icon-trash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12.78 0a.5.5 0 0 0 .5-.5v-1.5m-.5.5h.5"/><path d="M4.772 5.79A48.212 48.212 0 0 1 8.25 3.311m0 0a48.212 48.212 0 0 1 6.678-.774M12 3.75c2.23 0 4.27.656 5.65 1.751"/>
|
||||||
|
</symbol>
|
||||||
|
<!-- 勾选 -->
|
||||||
|
<symbol id="icon-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="m4.5 12.75 6 6 9-13.5"/>
|
||||||
|
</symbol>
|
||||||
|
<!-- 关闭 -->
|
||||||
|
<symbol id="icon-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M6 18 18 6M6 6l12 12"/>
|
||||||
|
</symbol>
|
||||||
|
<!-- 登出 -->
|
||||||
|
<symbol id="icon-logout" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M15.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15m3 0 3-3m0 0-3-3m3 3H9"/>
|
||||||
|
</symbol>
|
||||||
|
<!-- 左箭头 -->
|
||||||
|
<symbol id="icon-chevron-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M15.75 19.5 8.25 12l7.5-7.5"/>
|
||||||
|
</symbol>
|
||||||
|
<!-- 右箭头 -->
|
||||||
|
<symbol id="icon-chevron-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M8.25 4.5l7.5 7.5-7.5 7.5"/>
|
||||||
|
</symbol>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.4 KiB |
185
templates/index.html
Normal file
185
templates/index.html
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
<!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="{{ url_for('static', filename='style.css') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% include "icons.html" %}
|
||||||
|
|
||||||
|
<div id="toast" class="toast"></div>
|
||||||
|
|
||||||
|
<div class="app-layout">
|
||||||
|
|
||||||
|
<!-- ═══ 左侧边栏 ═══ -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
|
||||||
|
<!-- 用户信息 -->
|
||||||
|
<div class="sidebar-user">
|
||||||
|
<div class="user-avatar">{{ username[0] }}</div>
|
||||||
|
<div class="user-info">
|
||||||
|
<span class="user-name">{{ username }}</span>
|
||||||
|
<span class="user-tag">道阁 · 紫微</span>
|
||||||
|
</div>
|
||||||
|
<a href="/logout" class="btn-logout-icon" title="退出登录" onclick="return confirm('确定退出登录?')">
|
||||||
|
<svg class="icon-sm"><use href="#icon-logout"/></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计信息 -->
|
||||||
|
<div class="sidebar-stats" id="sidebar-stats">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-num" id="stat-days">--</span>
|
||||||
|
<span class="stat-label">打卡天</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-num" id="stat-morning">--</span>
|
||||||
|
<span class="stat-label">立志</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-num" id="stat-study">--</span>
|
||||||
|
<span class="stat-label">勤学</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 日历 -->
|
||||||
|
<div class="calendar-widget">
|
||||||
|
<div class="cal-header">
|
||||||
|
<button class="cal-nav" onclick="changeCalMonth(-1)">
|
||||||
|
<svg class="icon-sm"><use href="#icon-chevron-left"/></svg>
|
||||||
|
</button>
|
||||||
|
<span class="cal-month-label" id="cal-month-label">2026年 6月</span>
|
||||||
|
<button class="cal-nav" onclick="changeCalMonth(1)">
|
||||||
|
<svg class="icon-sm"><use href="#icon-chevron-right"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="cal-weekdays">
|
||||||
|
<span>日</span><span>一</span><span>二</span><span>三</span><span>四</span><span>五</span><span>六</span>
|
||||||
|
</div>
|
||||||
|
<div class="cal-grid" id="cal-grid"></div>
|
||||||
|
<div class="cal-legend">
|
||||||
|
<span class="legend-dot pass"></span> 达标
|
||||||
|
<span class="legend-dot fail"></span> 未达标
|
||||||
|
<span class="legend-dot empty"></span> 未打卡
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 功能入口 -->
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
<a class="nav-item active" data-panel="daily" onclick="switchPanel('daily')">
|
||||||
|
<svg class="icon-sm"><use href="#icon-calendar"/></svg>
|
||||||
|
每日打卡
|
||||||
|
</a>
|
||||||
|
<a class="nav-item" data-panel="weekly" onclick="switchPanel('weekly')">
|
||||||
|
<svg class="icon-sm"><use href="#icon-chart-bar"/></svg>
|
||||||
|
每周评分
|
||||||
|
</a>
|
||||||
|
<a class="nav-item" data-panel="history" onclick="switchPanel('history')">
|
||||||
|
<svg class="icon-sm"><use href="#icon-list-bullet"/></svg>
|
||||||
|
历史记录
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- 底部新建按钮 -->
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<button class="btn-new-day" onclick="goToday()">
|
||||||
|
<svg class="icon-sm"><use href="#icon-sun"/></svg>
|
||||||
|
回到今天
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- ═══ 右侧主内容区 ═══ -->
|
||||||
|
<main class="main-content">
|
||||||
|
|
||||||
|
<!-- ── 每日打卡面板 ── -->
|
||||||
|
<section class="panel active" id="panel-daily">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h2>每日打卡</h2>
|
||||||
|
<input type="date" id="check-date" class="check-date-input" onchange="loadCheckin()">
|
||||||
|
</div>
|
||||||
|
<div class="daily-grid">
|
||||||
|
<div class="card card-morning">
|
||||||
|
<div class="card-head">
|
||||||
|
<svg class="icon-h2"><use href="#icon-sun"/></svg>
|
||||||
|
<h3>早间立志</h3>
|
||||||
|
</div>
|
||||||
|
<p class="card-desc">今天最重要的 1~3 件事</p>
|
||||||
|
<div id="morning-list"></div>
|
||||||
|
<button class="btn-add" onclick="addMorning()">
|
||||||
|
<svg class="icon-sm"><use href="#icon-plus"/></svg> 增加一条
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card card-evening">
|
||||||
|
<div class="card-head">
|
||||||
|
<svg class="icon-h2"><use href="#icon-magnifying-glass"/></svg>
|
||||||
|
<h3>责善 · 改过</h3>
|
||||||
|
</div>
|
||||||
|
<p class="card-desc">今天犯的错 & 改进方案(最多5条)</p>
|
||||||
|
<div id="evening-list"></div>
|
||||||
|
<button class="btn-add" onclick="addEvening()">
|
||||||
|
<svg class="icon-sm"><use href="#icon-plus"/></svg> 增加一条
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card card-study">
|
||||||
|
<div class="card-head">
|
||||||
|
<svg class="icon-h2"><use href="#icon-book-open"/></svg>
|
||||||
|
<h3>勤学打卡</h3>
|
||||||
|
</div>
|
||||||
|
<p class="card-desc">今日修行清单</p>
|
||||||
|
<div id="study-list"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ── 每周评分面板 ── -->
|
||||||
|
<section class="panel" id="panel-weekly">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h2>每周评分</h2>
|
||||||
|
</div>
|
||||||
|
<div class="week-nav">
|
||||||
|
<button class="btn-nav" onclick="changeWeek(-1)">
|
||||||
|
<svg class="icon-nav"><use href="#icon-chevron-left"/></svg> 上周
|
||||||
|
</button>
|
||||||
|
<span id="week-label">--</span>
|
||||||
|
<button class="btn-nav" onclick="changeWeek(1)">
|
||||||
|
下周 <svg class="icon-nav"><use href="#icon-chevron-right"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="weekly-overview">
|
||||||
|
<div class="score-circle">
|
||||||
|
<svg viewBox="0 0 120 120" class="score-svg">
|
||||||
|
<circle cx="60" cy="60" r="50" fill="none" stroke="#E5E7EB" stroke-width="8"/>
|
||||||
|
<circle cx="60" cy="60" r="50" fill="none" stroke="#4A6CF7" stroke-width="8"
|
||||||
|
stroke-dasharray="314" stroke-dashoffset="314" stroke-linecap="round"
|
||||||
|
class="score-ring" transform="rotate(-90,60,60)"/>
|
||||||
|
</svg>
|
||||||
|
<div class="score-inner">
|
||||||
|
<span class="score-num" id="weekly-score">--</span>
|
||||||
|
<span class="score-unit">分</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="score-meta">
|
||||||
|
<span class="score-text" id="score-text">选择一周查看评分</span>
|
||||||
|
<div class="score-stats" id="score-stats"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="week-days-grid" class="week-days-grid"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ── 历史记录面板 ── -->
|
||||||
|
<section class="panel" id="panel-history">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h2>历史记录</h2>
|
||||||
|
</div>
|
||||||
|
<div id="history-grid" class="history-grid"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='app.js') }}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
259
templates/login.html
Normal file
259
templates/login.html
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
<!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>
|
||||||
|
:root {
|
||||||
|
--primary: #4A6CF7;
|
||||||
|
--primary-light: #EEF1FE;
|
||||||
|
--primary-dark: #3B5DE7;
|
||||||
|
--danger: #EF4444;
|
||||||
|
--bg: #F5F7FB;
|
||||||
|
--card: #FFFFFF;
|
||||||
|
--text: #1F2937;
|
||||||
|
--text-muted: #9CA3AF;
|
||||||
|
--border: #E5E7EB;
|
||||||
|
--radius: 12px;
|
||||||
|
--shadow: 0 4px 24px rgba(74,108,247,0.10);
|
||||||
|
}
|
||||||
|
* { 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);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.login-card {
|
||||||
|
background: var(--card);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 48px 40px 40px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
box-shadow: var(--shadow), 0 1px 3px rgba(0,0,0,0.06);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.login-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0; right: 0;
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(90deg, var(--primary), #818CF8, var(--primary));
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes shimmer {
|
||||||
|
0%,100% { background-position:0% 50%; }
|
||||||
|
50% { background-position:100% 50%; }
|
||||||
|
}
|
||||||
|
.login-card .logo-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.login-card .logo-row .icon-logo { width:28px; height:28px; color:var(--primary); }
|
||||||
|
.login-card h1 {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text);
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
}
|
||||||
|
.login-card .sub {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 36px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.form-group { margin-bottom: 20px; }
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.form-group .input-wrap {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.form-group .input-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 14px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px 12px 42px;
|
||||||
|
border: 1.5px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-size: 15px;
|
||||||
|
font-family: inherit;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary);
|
||||||
|
background: var(--card);
|
||||||
|
box-shadow: 0 0 0 3px rgba(74,108,247,0.10);
|
||||||
|
}
|
||||||
|
.form-group input::placeholder { color:var(--text-muted); }
|
||||||
|
.remember-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.remember-row input[type="checkbox"] {
|
||||||
|
width: 16px; height: 16px;
|
||||||
|
accent-color: var(--primary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.btn-login {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px;
|
||||||
|
border: none;
|
||||||
|
background: var(--primary);
|
||||||
|
color: #FFF;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-family: inherit;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
.btn-login:hover {
|
||||||
|
background: var(--primary-dark);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 16px rgba(74,108,247,0.30);
|
||||||
|
}
|
||||||
|
.btn-login:active { transform:translateY(0); }
|
||||||
|
.btn-login.loading { opacity:0.7; pointer-events:none; }
|
||||||
|
.error-msg {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--danger);
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
background: #FEF2F2;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: none;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.error-msg.show { display:block; }
|
||||||
|
.footer-text {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-top: 28px;
|
||||||
|
}
|
||||||
|
.toast {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) translateY(-20px);
|
||||||
|
padding: 12px 28px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
z-index: 9999;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: all 0.3s;
|
||||||
|
white-space: nowrap;
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.12);
|
||||||
|
}
|
||||||
|
.toast.show { opacity:1; transform:translateX(-50%) translateY(0); pointer-events:auto; }
|
||||||
|
.toast.success { background:#10B981; color:#FFF; }
|
||||||
|
.toast.error { background:var(--danger); color:#FFF; }
|
||||||
|
.toast.info { background:var(--primary); color:#FFF; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% include "icons.html" %}
|
||||||
|
|
||||||
|
<div id="toast" class="toast"></div>
|
||||||
|
|
||||||
|
<div class="login-card">
|
||||||
|
<div class="logo-row">
|
||||||
|
<svg class="icon-logo"><use href="#icon-bolt"/></svg>
|
||||||
|
<h1>紫微 · 磁场管理</h1>
|
||||||
|
</div>
|
||||||
|
<p class="sub">立志不摇,责善不滥,改过不拖,勤学不辍</p>
|
||||||
|
|
||||||
|
<form id="login-form" onsubmit="doLogin(event)">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">账号</label>
|
||||||
|
<div class="input-wrap">
|
||||||
|
<svg class="input-icon"><use href="#icon-user"/></svg>
|
||||||
|
<input type="text" id="username" placeholder="请输入账号" autocomplete="username" required autofocus>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">密码</label>
|
||||||
|
<div class="input-wrap">
|
||||||
|
<svg class="input-icon"><use href="#icon-lock"/></svg>
|
||||||
|
<input type="password" id="password" placeholder="请输入密码" autocomplete="current-password" required>
|
||||||
|
</div>
|
||||||
|
</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.showToast = function(msg, type){
|
||||||
|
var t = document.getElementById('toast');
|
||||||
|
t.textContent = msg;
|
||||||
|
t.className = 'toast ' + (type||'info');
|
||||||
|
setTimeout(function(){ t.classList.add('show'); }, 10);
|
||||||
|
setTimeout(function(){ t.classList.remove('show'); }, 2500);
|
||||||
|
};
|
||||||
|
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