From 55cd42b0c9d8483c9a499c50621e5c5cc78af346 Mon Sep 17 00:00:00 2001 From: mac Date: Mon, 1 Jun 2026 19:29:23 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=20ziwei-power?= =?UTF-8?q?=EF=BC=9A=E7=B4=AB=E5=BE=AE=E7=A3=81=E5=9C=BA=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=EF=BC=88Flask=20+=20SQLite=20+=20=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E9=89=B4=E6=9D=83=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 20 ++ app.py | 130 ++++++++++ database.py | 94 ++++++++ index.html | 552 +++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + static/app.js | 411 ++++++++++++++++++++++++++++++++ static/style.css | 313 ++++++++++++++++++++++++ templates/index.html | 82 +++++++ templates/login.html | 176 ++++++++++++++ 9 files changed, 1779 insertions(+) create mode 100644 .gitignore create mode 100644 app.py create mode 100644 database.py create mode 100644 index.html create mode 100644 requirements.txt create mode 100644 static/app.js create mode 100644 static/style.css create mode 100644 templates/index.html create mode 100644 templates/login.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d2101a --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +venv/ +env/ + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +Thumbs.db + +# Database +*.db + +# WorkBuddy +.workbuddy/ diff --git a/app.py b/app.py new file mode 100644 index 0000000..5b20d16 --- /dev/null +++ b/app.py @@ -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/', 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) diff --git a/database.py b/database.py new file mode 100644 index 0000000..834f998 --- /dev/null +++ b/database.py @@ -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 diff --git a/index.html b/index.html new file mode 100644 index 0000000..22c3f0f --- /dev/null +++ b/index.html @@ -0,0 +1,552 @@ + + + + + +紫微力量 - 磁场管理 + + + +
+
+

⚡ 紫微力量

+

立志不摇 · 责善不滥 · 改过不拖 · 勤学不辍

+
⏳ JS 加载中...
+
+ +
+ + + +
+ + +
+
+ + +
+ + +
+

🌅 早间立志 今日最重要的事(最多3条)

+
+ +
+ + +
+

⚖️ 责善·改过 今日反思(最多5条)

+
+ +
+ + +
+

📚 勤学打卡 可自由增减

+
+ +
+ + +
+ + +
+
+ +
本周
+ +
+ +
+
--
+
+
选择一周查看评分
+
+
+
+ + +
+

📜 打卡历史

+
暂无打卡记录
+
+
+ + + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..dbcbaf7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +flask==3.1.0 diff --git a/static/app.js b/static/app.js new file mode 100644 index 0000000..78d31b7 --- /dev/null +++ b/static/app.js @@ -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' + + '' + + ''; + container.appendChild(div); + renumberMorning(); + } + + function renumberMorning() { + var rows = document.querySelectorAll('#morning-list .item-row'); + for (var i=0; i' + + '
' + + '错误' + + '' + + '改进方案' + + '' + + '
' + + ''; + container.appendChild(div); + renumberEvening(); + } + + function renumberEvening() { + var rows = document.querySelectorAll('#evening-list .item-row'); + for (var i=0; i' + + '' + + ''; + 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= 2 ? '✅' : '⚠️'; + var color = dayScore >= 2 ? '#10B981' : '#F59E0B'; + details.push('
' + + ds + ' — 得分:' + dayScore + '/3 ' + icon + '
'); + } + } + + 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 = '

加载失败

'; return; } + if (!rows || rows.length === 0) { + el.innerHTML = '

暂无历史记录

'; + return; + } + + var html = ''; + for (var i=0; i' + + '
' + r.date + '
' + + '
' + (preview.join(' · ') || '暂无内容') + '
' + + '' + + '' + + ''; + } + 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,'>'); + } + + /* ── 初始化 ────────────────────────────── */ + 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(); + } +})(); diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..bfde176 --- /dev/null +++ b/static/style.css @@ -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; } diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..925d02f --- /dev/null +++ b/templates/index.html @@ -0,0 +1,82 @@ + + + + + +紫微 · 磁场管理 + + + +
+ +
+
+

⚡ 紫微 · 磁场管理

+
+ {{ username }} + 退出 +
+
+

立志不摇,责善不滥,改过不拖,勤学不辍

+
+ + + + +
+
+ + +
+ +
+

🌅 早间立志

+

今天最重要的 1~3 件事

+
+ +
+ +
+

🔍 责善 · 改过

+

今天犯的错 & 下次怎么改(最多5条)

+
+ +
+ +
+

📚 勤学打卡

+

今天学了什么

+
+ +
+ + +
+ + +
+
+ + -- + +
+
+ -- + +
+

+
+
+ + +
+
+
+ + + + diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..7c970a5 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,176 @@ + + + + + +紫微 · 登录 + + + + + + + + +