commit 16fb5030cbdffd82d7689fc6c6d6625933a362c8
Author: Jay <193683414+sheephearttidy@users.noreply.github.com>
Date: Wed Aug 26 00:46:11 2026 +0800
首次完成数据库登录,注册验证。首次完成,migrate_manager.py迁移多个数据库
diff --git a/.flaskenv b/.flaskenv
new file mode 100644
index 0000000..7252d7b
--- /dev/null
+++ b/.flaskenv
@@ -0,0 +1 @@
+FLASK_APP=migrate_manager
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0cafc1c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.venv/
\ No newline at end of file
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..f6906f2
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,10 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
+# 已忽略包含查询文件的默认文件夹
+/queries/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/LoveCards.iml b/.idea/LoveCards.iml
new file mode 100644
index 0000000..bde4e67
--- /dev/null
+++ b/.idea/LoveCards.iml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/MarsCodeWorkspaceAppSettings.xml b/.idea/MarsCodeWorkspaceAppSettings.xml
new file mode 100644
index 0000000..b26fdc6
--- /dev/null
+++ b/.idea/MarsCodeWorkspaceAppSettings.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml
new file mode 100644
index 0000000..1fa01e0
--- /dev/null
+++ b/.idea/dataSources.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ mysql.8
+ true
+ com.mysql.cj.jdbc.Driver
+ jdbc:mysql://localhost:3306/test
+ $ProjectFileDir$
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..98ce2a2
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/__pycache__/migrate_manager.cpython-312.pyc b/__pycache__/migrate_manager.cpython-312.pyc
new file mode 100644
index 0000000..c5ad209
Binary files /dev/null and b/__pycache__/migrate_manager.cpython-312.pyc differ
diff --git a/app/__pycache__/app.cpython-312.pyc b/app/__pycache__/app.cpython-312.pyc
new file mode 100644
index 0000000..6208779
Binary files /dev/null and b/app/__pycache__/app.cpython-312.pyc differ
diff --git a/app/__pycache__/config.cpython-312.pyc b/app/__pycache__/config.cpython-312.pyc
new file mode 100644
index 0000000..0299ad9
Binary files /dev/null and b/app/__pycache__/config.cpython-312.pyc differ
diff --git a/app/__pycache__/migrate_manager.cpython-312.pyc b/app/__pycache__/migrate_manager.cpython-312.pyc
new file mode 100644
index 0000000..debf5a9
Binary files /dev/null and b/app/__pycache__/migrate_manager.cpython-312.pyc differ
diff --git a/app/app.py b/app/app.py
new file mode 100644
index 0000000..a1fa30d
--- /dev/null
+++ b/app/app.py
@@ -0,0 +1,18 @@
+from flask import Flask
+import config
+from model import db
+from route import public, auth, admin
+
+app = Flask(__name__)
+app.config.from_object(config)
+db.init_app(app)
+
+app.register_blueprint(public)
+app.register_blueprint(auth)
+app.register_blueprint(admin)
+
+
+
+
+if __name__ == '__main__':
+ app.run(debug=True,port=8000)
\ No newline at end of file
diff --git a/app/config.py b/app/config.py
new file mode 100644
index 0000000..1020a5b
--- /dev/null
+++ b/app/config.py
@@ -0,0 +1,18 @@
+SECRET_KEY = 'dev-secret-key'
+SQLALCHEMY_TRACK_MODIFICATIONS = False
+
+
+'''仅开发使用'''
+ADMIN = 'admin'
+ADMIN_PASSWORD = 'admin'
+
+'''ORM'''
+DB_DRIVER = 'mysqldb'
+DB_USER = 'test'
+DB_PASSWORD = 'test'
+DB_HOST = '127.0.0.1'
+DB_PORT = 3306
+DB_NAME = 'test'
+SQLALCHEMY_DATABASE_URI = (
+ f'mysql+{DB_DRIVER}://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}'
+)
\ No newline at end of file
diff --git a/app/model/Card.py b/app/model/Card.py
new file mode 100644
index 0000000..6571c3f
--- /dev/null
+++ b/app/model/Card.py
@@ -0,0 +1,6 @@
+from model.db import db
+from sqlalchemy.orm import mapped_column
+
+class Card(db.Model):
+ __tablename__ = 'card'
+ id = mapped_column(db.Integer, primary_key=True)
diff --git a/app/model/User.py b/app/model/User.py
new file mode 100644
index 0000000..49ed8fd
--- /dev/null
+++ b/app/model/User.py
@@ -0,0 +1,9 @@
+from model.db import db
+from sqlalchemy.orm import mapped_column
+
+class User(db.Model):
+ __tablename__ = 'user'
+ id = mapped_column(db.Integer, primary_key=True,autoincrement=True)
+ username = mapped_column(db.String(80), unique=True, nullable=False)
+ email = mapped_column(db.String(120), unique=True, nullable=True)
+ password = mapped_column(db.String(200), unique=True, nullable=False)
\ No newline at end of file
diff --git a/app/model/__init__.py b/app/model/__init__.py
new file mode 100644
index 0000000..c2ed94b
--- /dev/null
+++ b/app/model/__init__.py
@@ -0,0 +1,6 @@
+from model.db import db
+from model.User import User
+from model.Card import Card
+
+
+__all__ = ['db','User','Card']
\ No newline at end of file
diff --git a/app/model/__pycache__/Card.cpython-312.pyc b/app/model/__pycache__/Card.cpython-312.pyc
new file mode 100644
index 0000000..872a647
Binary files /dev/null and b/app/model/__pycache__/Card.cpython-312.pyc differ
diff --git a/app/model/__pycache__/User.cpython-312.pyc b/app/model/__pycache__/User.cpython-312.pyc
new file mode 100644
index 0000000..57a2c69
Binary files /dev/null and b/app/model/__pycache__/User.cpython-312.pyc differ
diff --git a/app/model/__pycache__/__init__.cpython-312.pyc b/app/model/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..5839399
Binary files /dev/null and b/app/model/__pycache__/__init__.cpython-312.pyc differ
diff --git a/app/model/__pycache__/db.cpython-312.pyc b/app/model/__pycache__/db.cpython-312.pyc
new file mode 100644
index 0000000..d2fb8e6
Binary files /dev/null and b/app/model/__pycache__/db.cpython-312.pyc differ
diff --git a/app/model/db.py b/app/model/db.py
new file mode 100644
index 0000000..5e79a2c
--- /dev/null
+++ b/app/model/db.py
@@ -0,0 +1,13 @@
+from sqlalchemy.orm import DeclarativeBase
+from flask_sqlalchemy import SQLAlchemy
+from sqlalchemy import MetaData
+class DataBase(DeclarativeBase):
+ meta = MetaData(naming_convention={
+ "ix": "ix_%(column_0_label)s",
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
+ "ck": "ck_%(table_name)s_%(column_0_name)s",
+ "fk": "fk_%(table_name)s_%(column_0_name)s",
+ "pk": "pk_%(table_name)s"
+ })
+
+db = SQLAlchemy()
\ No newline at end of file
diff --git a/app/route/__init__.py b/app/route/__init__.py
new file mode 100644
index 0000000..547f903
--- /dev/null
+++ b/app/route/__init__.py
@@ -0,0 +1,5 @@
+from route.public import public
+from route.auth import auth
+from route.admin import admin
+
+__all__ = ['public', 'auth', 'admin']
\ No newline at end of file
diff --git a/app/route/__pycache__/__init__.cpython-312.pyc b/app/route/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..841e2a6
Binary files /dev/null and b/app/route/__pycache__/__init__.cpython-312.pyc differ
diff --git a/app/route/__pycache__/admin.cpython-312.pyc b/app/route/__pycache__/admin.cpython-312.pyc
new file mode 100644
index 0000000..e75d32a
Binary files /dev/null and b/app/route/__pycache__/admin.cpython-312.pyc differ
diff --git a/app/route/__pycache__/auth.cpython-312.pyc b/app/route/__pycache__/auth.cpython-312.pyc
new file mode 100644
index 0000000..3ba7b4c
Binary files /dev/null and b/app/route/__pycache__/auth.cpython-312.pyc differ
diff --git a/app/route/__pycache__/public.cpython-312.pyc b/app/route/__pycache__/public.cpython-312.pyc
new file mode 100644
index 0000000..f3754be
Binary files /dev/null and b/app/route/__pycache__/public.cpython-312.pyc differ
diff --git a/app/route/admin.py b/app/route/admin.py
new file mode 100644
index 0000000..b92f454
--- /dev/null
+++ b/app/route/admin.py
@@ -0,0 +1,11 @@
+from flask import Blueprint, request, render_template
+
+admin = Blueprint('admin', __name__, url_prefix='/admin')
+
+
+@admin.route('/', methods=['GET', 'POST'])
+def admin_index():
+ if request.method == "POST":
+ username = request.form["username"]
+ password = request.form["password"]
+ return render_template("admin/admin_index.html")
\ No newline at end of file
diff --git a/app/route/auth.py b/app/route/auth.py
new file mode 100644
index 0000000..b0f52a9
--- /dev/null
+++ b/app/route/auth.py
@@ -0,0 +1,34 @@
+from flask import Blueprint, request, render_template
+from model.db import db
+from model.User import User
+
+auth = Blueprint('auth', __name__)
+
+
+@auth.route('/login', methods=['GET', 'POST'])
+def login():
+ if request.method == "POST":
+ username = request.form["username"]
+ password = request.form["password"]
+ user = db.session.execute(db.select(User).where(User.username == username)).scalar()
+ if user is None:
+ return "Username or Password is incorrect"
+ if user.password != password:
+ return "Username or Password is incorrect"
+ # 登录成功
+ return f"Login Successful {username}:{password}"
+
+ return render_template("public/user/login.html")
+
+
+@auth.route('/register', methods=["POST", "GET"])
+def register():
+ if request.method == "POST":
+ username = request.form["username"]
+ password = request.form["password"]
+ email = request.form["email"]
+ # return f"Register Successful {username}:{password}:{email}"
+ new_user = User(username=username, password=password, email=email)
+ db.session.add(new_user)
+ db.session.commit()
+ return render_template("public/user/register.html")
\ No newline at end of file
diff --git a/app/route/public.py b/app/route/public.py
new file mode 100644
index 0000000..3feb131
--- /dev/null
+++ b/app/route/public.py
@@ -0,0 +1,21 @@
+from flask import Blueprint, render_template
+
+public = Blueprint('public', __name__)
+
+
+@public.route('/')
+def index():
+ return render_template("public/index.html")
+
+
+@public.route('/about')
+def about():
+ return render_template("public/about.html")
+
+@public.route("/terms")
+def terms():
+ return "terms Page"
+
+@public.route("/privacy")
+def privacy():
+ return "privacy Page"
\ No newline at end of file
diff --git a/app/static/css/admin.css b/app/static/css/admin.css
new file mode 100644
index 0000000..8684e7b
--- /dev/null
+++ b/app/static/css/admin.css
@@ -0,0 +1,260 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: "Segoe UI", "Microsoft YaHei", -apple-system, sans-serif;
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: radial-gradient(ellipse at top, #1e293b 0%, #0f172a 65%);
+ color: #e2e8f0;
+ padding: 20px;
+ position: relative;
+}
+
+body::before {
+ content: "";
+ position: fixed;
+ inset: 0;
+ background-image:
+ linear-gradient(rgba(148, 163, 184, 0.05) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(148, 163, 184, 0.05) 1px, transparent 1px);
+ background-size: 32px 32px;
+ pointer-events: none;
+}
+
+.admin-card {
+ position: relative;
+ z-index: 1;
+ width: 100%;
+ max-width: 420px;
+ background: rgba(15, 23, 42, 0.78);
+ border: 1px solid rgba(148, 163, 184, 0.16);
+ backdrop-filter: blur(10px);
+ border-radius: 16px;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
+ padding: 40px 36px 30px;
+}
+
+.admin-header {
+ text-align: center;
+ margin-bottom: 30px;
+}
+
+.shield-badge {
+ width: 64px;
+ height: 64px;
+ margin: 0 auto 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #2563eb 0%, #06b6d4 100%);
+ box-shadow: 0 8px 24px rgba(37, 99, 235, 0.35);
+}
+
+.shield-badge svg {
+ width: 32px;
+ height: 32px;
+ fill: #ffffff;
+}
+
+.admin-header h1 {
+ font-size: 22px;
+ font-weight: 600;
+ letter-spacing: 1px;
+}
+
+.admin-header .subtitle {
+ margin-top: 6px;
+ font-size: 13px;
+ color: #94a3b8;
+ letter-spacing: 2px;
+}
+
+.form-group {
+ margin-bottom: 20px;
+}
+
+.form-group label {
+ display: block;
+ font-size: 13px;
+ color: #94a3b8;
+ margin-bottom: 8px;
+}
+
+.input-wrap {
+ position: relative;
+}
+
+.input-wrap input {
+ width: 100%;
+ padding: 12px 44px 12px 14px;
+ font-size: 15px;
+ color: #f1f5f9;
+ background: rgba(30, 41, 59, 0.85);
+ border: 1px solid rgba(148, 163, 184, 0.25);
+ border-radius: 8px;
+ outline: none;
+ transition: border-color 0.2s, box-shadow 0.2s;
+}
+
+.input-wrap input::placeholder {
+ color: #64748b;
+}
+
+.input-wrap input:focus {
+ border-color: #3b82f6;
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
+}
+
+.toggle-pwd {
+ position: absolute;
+ right: 6px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 34px;
+ height: 34px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: transparent;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ color: #64748b;
+}
+
+.toggle-pwd:hover {
+ color: #cbd5e1;
+ background: rgba(148, 163, 184, 0.12);
+}
+
+.toggle-pwd svg {
+ width: 18px;
+ height: 18px;
+ fill: currentColor;
+}
+
+.capslock-hint {
+ display: none;
+ margin-top: 6px;
+ font-size: 12px;
+ color: #f59e0b;
+}
+
+.capslock-hint.show {
+ display: block;
+}
+
+.error-msg {
+ display: none;
+ background: rgba(220, 38, 38, 0.14);
+ border: 1px solid rgba(220, 38, 38, 0.4);
+ color: #fca5a5;
+ font-size: 13px;
+ line-height: 1.5;
+ padding: 10px 12px;
+ border-radius: 8px;
+ margin-bottom: 18px;
+}
+
+.submit-btn {
+ width: 100%;
+ padding: 13px;
+ font-size: 16px;
+ font-weight: 600;
+ letter-spacing: 4px;
+ color: #ffffff;
+ background: linear-gradient(135deg, #2563eb 0%, #06b6d4 100%);
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: opacity 0.2s, transform 0.1s;
+}
+
+.submit-btn:hover:not(:disabled) {
+ opacity: 0.9;
+}
+
+.submit-btn:active:not(:disabled) {
+ transform: scale(0.98);
+}
+
+.submit-btn:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.demo-tip {
+ margin-top: 22px;
+ padding-top: 16px;
+ border-top: 1px dashed rgba(148, 163, 184, 0.2);
+ font-size: 12px;
+ color: #64748b;
+ text-align: center;
+ line-height: 1.6;
+}
+
+.success-panel {
+ display: none;
+ text-align: center;
+ padding: 10px 0;
+}
+
+.success-panel .check-icon {
+ width: 72px;
+ height: 72px;
+ margin: 0 auto 18px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ background: rgba(34, 197, 94, 0.15);
+ border: 2px solid rgba(34, 197, 94, 0.5);
+}
+
+.success-panel .check-icon svg {
+ width: 36px;
+ height: 36px;
+ stroke: #22c55e;
+ stroke-width: 3;
+ fill: none;
+}
+
+.success-panel h2 {
+ font-size: 20px;
+ margin-bottom: 8px;
+}
+
+.success-panel p {
+ font-size: 13px;
+ color: #94a3b8;
+ line-height: 1.7;
+}
+
+.back-btn {
+ margin-top: 24px;
+ padding: 10px 28px;
+ font-size: 14px;
+ color: #cbd5e1;
+ background: transparent;
+ border: 1px solid rgba(148, 163, 184, 0.35);
+ border-radius: 8px;
+ cursor: pointer;
+ transition: background 0.2s;
+}
+
+.back-btn:hover {
+ background: rgba(148, 163, 184, 0.12);
+}
+
+@media (max-width: 480px) {
+ .admin-card {
+ padding: 32px 22px 24px;
+ }
+}
\ No newline at end of file
diff --git a/app/static/css/auth.css b/app/static/css/auth.css
new file mode 100644
index 0000000..9016342
--- /dev/null
+++ b/app/static/css/auth.css
@@ -0,0 +1,71 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: "Segoe UI", "Microsoft YaHei", -apple-system, sans-serif;
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ padding: 20px;
+}
+
+.form-group {
+ margin-bottom: 20px;
+}
+
+.form-group label {
+ display: block;
+ font-size: 14px;
+ color: #555;
+ margin-bottom: 6px;
+}
+
+.form-group input {
+ width: 100%;
+ padding: 12px 14px;
+ font-size: 15px;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ outline: none;
+ transition: border-color 0.2s, box-shadow 0.2s;
+}
+
+.form-group input:focus {
+ border-color: #667eea;
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15);
+}
+
+.submit-btn {
+ width: 100%;
+ padding: 12px;
+ font-size: 16px;
+ color: #fff;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: opacity 0.2s, transform 0.1s;
+}
+
+.submit-btn:hover {
+ opacity: 0.9;
+}
+
+.submit-btn:active {
+ transform: scale(0.98);
+}
+
+.error-msg {
+ display: none;
+ background: #fdecea;
+ color: #c0392b;
+ font-size: 13px;
+ padding: 10px 12px;
+ border-radius: 8px;
+ margin-bottom: 18px;
+}
\ No newline at end of file
diff --git a/app/static/css/login.css b/app/static/css/login.css
new file mode 100644
index 0000000..d886db7
--- /dev/null
+++ b/app/static/css/login.css
@@ -0,0 +1,63 @@
+.login-card {
+ width: 100%;
+ max-width: 400px;
+ background: #ffffff;
+ border-radius: 12px;
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
+ padding: 40px 32px;
+}
+
+.login-card h1 {
+ text-align: center;
+ font-size: 24px;
+ color: #333;
+ margin-bottom: 8px;
+}
+
+.login-card .subtitle {
+ text-align: center;
+ font-size: 14px;
+ color: #888;
+ margin-bottom: 28px;
+}
+
+.form-options {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 13px;
+ margin-bottom: 24px;
+}
+
+.form-options label {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ color: #555;
+ cursor: pointer;
+}
+
+.form-options a {
+ color: #667eea;
+ text-decoration: none;
+}
+
+.form-options a:hover {
+ text-decoration: underline;
+}
+
+.register-link {
+ text-align: center;
+ font-size: 13px;
+ color: #888;
+ margin-top: 24px;
+}
+
+.register-link a {
+ color: #667eea;
+ text-decoration: none;
+}
+
+.register-link a:hover {
+ text-decoration: underline;
+}
\ No newline at end of file
diff --git a/app/static/css/register.css b/app/static/css/register.css
new file mode 100644
index 0000000..3082d26
--- /dev/null
+++ b/app/static/css/register.css
@@ -0,0 +1,67 @@
+.register-card {
+ width: 100%;
+ max-width: 400px;
+ background: #ffffff;
+ border-radius: 12px;
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
+ padding: 40px 32px;
+}
+
+.register-card h1 {
+ text-align: center;
+ font-size: 24px;
+ color: #333;
+ margin-bottom: 8px;
+}
+
+.register-card .subtitle {
+ text-align: center;
+ font-size: 14px;
+ color: #888;
+ margin-bottom: 28px;
+}
+
+.form-group .hint {
+ font-size: 12px;
+ color: #aaa;
+ margin-top: 4px;
+}
+
+.agreement {
+ display: flex;
+ align-items: flex-start;
+ gap: 8px;
+ font-size: 13px;
+ color: #555;
+ margin-bottom: 24px;
+}
+
+.agreement input {
+ margin-top: 3px;
+ cursor: pointer;
+}
+
+.agreement a {
+ color: #667eea;
+ text-decoration: none;
+}
+
+.agreement a:hover {
+ text-decoration: underline;
+}
+
+.login-link {
+ text-align: center;
+ font-size: 13px;
+ color: #888;
+ margin-top: 24px;
+}
+
+.login-link a {
+ color: #667eea;
+ text-decoration: none;
+}
+
+.login-link a:hover {
+ text-decoration: underline;
+}
\ No newline at end of file
diff --git a/app/static/js/admin.js b/app/static/js/admin.js
new file mode 100644
index 0000000..7361c3b
--- /dev/null
+++ b/app/static/js/admin.js
@@ -0,0 +1,62 @@
+var form = document.getElementById('adminLoginForm');
+var errorBox = document.getElementById('errorBox');
+var submitBtn = document.getElementById('submitBtn');
+var successPanel = document.getElementById('successPanel');
+var passwordInput = document.getElementById('password');
+
+form.addEventListener('submit', function (e) {
+ e.preventDefault();
+
+ var username = document.getElementById('username').value.trim();
+ var password = passwordInput.value;
+ var errors = [];
+
+ if (!username) {
+ errors.push('用户名不能为空');
+ }
+ if (!password) {
+ errors.push('密码不能为空');
+ } else if (password.length < 6) {
+ errors.push('密码长度至少为 6 位');
+ }
+
+ if (errors.length > 0) {
+ errorBox.textContent = errors.join(';');
+ errorBox.style.display = 'block';
+ return;
+ }
+
+ errorBox.style.display = 'none';
+ submitBtn.disabled = true;
+ submitBtn.textContent = '登录中…';
+
+ setTimeout(function () {
+ form.style.display = 'none';
+ document.getElementById('welcomeName').textContent = username;
+ successPanel.style.display = 'block';
+ submitBtn.disabled = false;
+ submitBtn.textContent = '登 录';
+ }, 900);
+});
+
+document.getElementById('togglePwd').addEventListener('click', function () {
+ var isHidden = passwordInput.type === 'password';
+ passwordInput.type = isHidden ? 'text' : 'password';
+ this.setAttribute('aria-label', isHidden ? '隐藏密码' : '显示密码');
+ this.style.color = isHidden ? '#3b82f6' : '';
+});
+
+function updateCapslockHint(e) {
+ var hint = document.getElementById('capslockHint');
+ var capsOn = e.getModifierState && e.getModifierState('CapsLock');
+ hint.classList.toggle('show', !!capsOn);
+}
+passwordInput.addEventListener('keydown', updateCapslockHint);
+passwordInput.addEventListener('keyup', updateCapslockHint);
+
+document.getElementById('backBtn').addEventListener('click', function () {
+ successPanel.style.display = 'none';
+ form.style.display = 'block';
+ form.reset();
+ document.getElementById('username').focus();
+});
\ No newline at end of file
diff --git a/app/static/js/login.js b/app/static/js/login.js
new file mode 100644
index 0000000..b50b425
--- /dev/null
+++ b/app/static/js/login.js
@@ -0,0 +1,23 @@
+document.getElementById('loginForm').addEventListener('submit', function (e) {
+ var errorBox = document.getElementById('errorBox');
+ var username = document.getElementById('username').value.trim();
+ var password = document.getElementById('password').value;
+ var errors = [];
+
+ if (!username) {
+ errors.push('用户名不能为空');
+ }
+ if (!password) {
+ errors.push('密码不能为空');
+ } else if (password.length < 6) {
+ errors.push('密码长度至少为 6 位');
+ }
+
+ if (errors.length > 0) {
+ e.preventDefault();
+ errorBox.textContent = errors.join(';');
+ errorBox.style.display = 'block';
+ } else {
+ errorBox.style.display = 'none';
+ }
+});
\ No newline at end of file
diff --git a/app/static/js/register.js b/app/static/js/register.js
new file mode 100644
index 0000000..b1b13de
--- /dev/null
+++ b/app/static/js/register.js
@@ -0,0 +1,43 @@
+document.getElementById('registerForm').addEventListener('submit', function (e) {
+ var errorBox = document.getElementById('errorBox');
+ var username = document.getElementById('username').value.trim();
+ var email = document.getElementById('email').value.trim();
+ var password = document.getElementById('password').value;
+ var confirmPassword = document.getElementById('confirmPassword').value;
+ var agree = document.getElementById('agree').checked;
+ var errors = [];
+
+ if (!username) {
+ errors.push('用户名不能为空');
+ } else if (username.length < 3 || username.length > 20) {
+ errors.push('用户名长度需在 3-20 个字符之间');
+ }
+
+ if (!email) {
+ errors.push('邮箱不能为空');
+ } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
+ errors.push('邮箱格式不正确');
+ }
+
+ if (!password) {
+ errors.push('密码不能为空');
+ } else if (password.length < 6) {
+ errors.push('密码长度至少为 6 位');
+ }
+
+ if (password !== confirmPassword) {
+ errors.push('两次输入的密码不一致');
+ }
+
+ if (!agree) {
+ errors.push('请先阅读并同意服务条款和隐私政策');
+ }
+
+ if (errors.length > 0) {
+ e.preventDefault();
+ errorBox.textContent = errors.join(';');
+ errorBox.style.display = 'block';
+ } else {
+ errorBox.style.display = 'none';
+ }
+});
\ No newline at end of file
diff --git a/app/templates/admin/admin_index.html b/app/templates/admin/admin_index.html
new file mode 100644
index 0000000..e0c800a
--- /dev/null
+++ b/app/templates/admin/admin_index.html
@@ -0,0 +1,63 @@
+
+
+
+
+
+ 后台管理系统 - 管理员登录
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 登录成功
+ 欢迎,管理员!
本页面为前端登录演示,未连接真实账号系统。
+
+
+
+ 演示提示:输入任意用户名和 6 位以上密码即可体验完整登录流程。
接入真实后端时,将表单 action 替换为实际登录接口地址,并移除脚本中的演示拦截逻辑即可。
+
+
+
+
+
\ No newline at end of file
diff --git a/app/templates/public/about.html b/app/templates/public/about.html
new file mode 100644
index 0000000..7ee13c8
--- /dev/null
+++ b/app/templates/public/about.html
@@ -0,0 +1,59 @@
+
+
+
+
+
+ 关于我 | About Me
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
关于作者
+
+
+
+
+ GitHub Profile
+
+
+
+
+ 发送邮件
+
+
+
+
+
创作经历
+
+ 我是一名热爱开源的开发者,长期活跃于 GitHub 社区。
+ 在这里,我分享我的代码片段、开源项目以及技术学习笔记。
+ 我相信代码可以改变世界,也希望通过我的项目能为社区带来一些微小的价值。
+
+
+ 主要技术栈: Python, Flask, HTML/CSS, JavaScript
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/templates/public/index.html b/app/templates/public/index.html
new file mode 100644
index 0000000..33852ef
--- /dev/null
+++ b/app/templates/public/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ Title
+
+
+
+
+
+Index
+
+
\ No newline at end of file
diff --git a/app/templates/public/user/login.html b/app/templates/public/user/login.html
new file mode 100644
index 0000000..3a23457
--- /dev/null
+++ b/app/templates/public/user/login.html
@@ -0,0 +1,49 @@
+
+
+
+
+
+ 用户登录
+
+
+
+
+
+
+ 欢迎登录
+ 请输入您的账号信息
+
+
+
+
+
+ 还没有账号?立即注册
+
+
+
+
+
\ No newline at end of file
diff --git a/app/templates/public/user/profile.html b/app/templates/public/user/profile.html
new file mode 100644
index 0000000..8619f40
--- /dev/null
+++ b/app/templates/public/user/profile.html
@@ -0,0 +1,10 @@
+
+
+
+
+ 个人主页
+
+
+
+
+
\ No newline at end of file
diff --git a/app/templates/public/user/register.html b/app/templates/public/user/register.html
new file mode 100644
index 0000000..166e757
--- /dev/null
+++ b/app/templates/public/user/register.html
@@ -0,0 +1,64 @@
+
+
+
+
+
+ 用户注册
+
+
+
+
+
+
+ 创建账号
+ 填写以下信息完成注册
+
+
+
+
+
+ 已有账号?去登录
+
+
+
+
+
\ No newline at end of file
diff --git a/migrate_manager.py b/migrate_manager.py
new file mode 100644
index 0000000..0185dc7
--- /dev/null
+++ b/migrate_manager.py
@@ -0,0 +1,10 @@
+import sys
+import os
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'app'))
+
+from flask_migrate import Migrate
+from app import app
+from model import db, User, Card
+
+migrate = Migrate(app, db)
\ No newline at end of file
diff --git a/migrations/README b/migrations/README
new file mode 100644
index 0000000..0e04844
--- /dev/null
+++ b/migrations/README
@@ -0,0 +1 @@
+Single-database configuration for Flask.
diff --git a/migrations/__pycache__/env.cpython-312.pyc b/migrations/__pycache__/env.cpython-312.pyc
new file mode 100644
index 0000000..ece34ad
Binary files /dev/null and b/migrations/__pycache__/env.cpython-312.pyc differ
diff --git a/migrations/alembic.ini b/migrations/alembic.ini
new file mode 100644
index 0000000..ec9d45c
--- /dev/null
+++ b/migrations/alembic.ini
@@ -0,0 +1,50 @@
+# A generic, single database configuration.
+
+[alembic]
+# template used to generate migration files
+# file_template = %%(rev)s_%%(slug)s
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic,flask_migrate
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[logger_flask_migrate]
+level = INFO
+handlers =
+qualname = flask_migrate
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/migrations/env.py b/migrations/env.py
new file mode 100644
index 0000000..4c97092
--- /dev/null
+++ b/migrations/env.py
@@ -0,0 +1,113 @@
+import logging
+from logging.config import fileConfig
+
+from flask import current_app
+
+from alembic import context
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+fileConfig(config.config_file_name)
+logger = logging.getLogger('alembic.env')
+
+
+def get_engine():
+ try:
+ # this works with Flask-SQLAlchemy<3 and Alchemical
+ return current_app.extensions['migrate'].db.get_engine()
+ except (TypeError, AttributeError):
+ # this works with Flask-SQLAlchemy>=3
+ return current_app.extensions['migrate'].db.engine
+
+
+def get_engine_url():
+ try:
+ return get_engine().url.render_as_string(hide_password=False).replace(
+ '%', '%%')
+ except AttributeError:
+ return str(get_engine().url).replace('%', '%%')
+
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+config.set_main_option('sqlalchemy.url', get_engine_url())
+target_db = current_app.extensions['migrate'].db
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def get_metadata():
+ if hasattr(target_db, 'metadatas'):
+ return target_db.metadatas[None]
+ return target_db.metadata
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ url = config.get_main_option("sqlalchemy.url")
+ context.configure(
+ url=url, target_metadata=get_metadata(), literal_binds=True
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+
+ # this callback is used to prevent an auto-migration from being generated
+ # when there are no changes to the schema
+ # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
+ def process_revision_directives(context, revision, directives):
+ if getattr(config.cmd_opts, 'autogenerate', False):
+ script = directives[0]
+ if script.upgrade_ops.is_empty():
+ directives[:] = []
+ logger.info('No changes in schema detected.')
+
+ conf_args = current_app.extensions['migrate'].configure_args
+ if conf_args.get("process_revision_directives") is None:
+ conf_args["process_revision_directives"] = process_revision_directives
+
+ connectable = get_engine()
+
+ with connectable.connect() as connection:
+ context.configure(
+ connection=connection,
+ target_metadata=get_metadata(),
+ **conf_args
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/migrations/script.py.mako b/migrations/script.py.mako
new file mode 100644
index 0000000..2c01563
--- /dev/null
+++ b/migrations/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
diff --git a/migrations/versions/67c0c267b827_创建表.py b/migrations/versions/67c0c267b827_创建表.py
new file mode 100644
index 0000000..df088dd
--- /dev/null
+++ b/migrations/versions/67c0c267b827_创建表.py
@@ -0,0 +1,37 @@
+"""创建表
+
+Revision ID: 67c0c267b827
+Revises:
+Create Date: 2026-08-26 00:13:19.101390
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '67c0c267b827'
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('user',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('username', sa.String(length=80), nullable=False),
+ sa.Column('email', sa.String(length=120), nullable=True),
+ sa.Column('password', sa.String(length=200), nullable=False),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('email'),
+ sa.UniqueConstraint('password'),
+ sa.UniqueConstraint('username')
+ )
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_table('user')
+ # ### end Alembic commands ###
diff --git a/migrations/versions/__pycache__/5655a068d090_创建表.cpython-312.pyc b/migrations/versions/__pycache__/5655a068d090_创建表.cpython-312.pyc
new file mode 100644
index 0000000..5640cb7
Binary files /dev/null and b/migrations/versions/__pycache__/5655a068d090_创建表.cpython-312.pyc differ
diff --git a/migrations/versions/__pycache__/67c0c267b827_创建表.cpython-312.pyc b/migrations/versions/__pycache__/67c0c267b827_创建表.cpython-312.pyc
new file mode 100644
index 0000000..7fadbf0
Binary files /dev/null and b/migrations/versions/__pycache__/67c0c267b827_创建表.cpython-312.pyc differ
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..b430e2a
Binary files /dev/null and b/requirements.txt differ