Compare commits
No commits in common. "796b6dbe62d59bca713063c8b5646ea886aef327" and "df65462344253a7a20be7911fc32406406c55428" have entirely different histories.
796b6dbe62
...
df65462344
56
.gitignore
vendored
56
.gitignore
vendored
@ -1,16 +1,56 @@
|
|||||||
venv/
|
|
||||||
env/
|
|
||||||
.venv/
|
|
||||||
.env/
|
|
||||||
study/
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
study/
|
||||||
|
ENV/
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Flask实例文件夹
|
||||||
|
instance/*
|
||||||
|
!instance/.gitkeep
|
||||||
|
|
||||||
|
# 环境变量文件
|
||||||
|
.env
|
||||||
|
.flaskenv
|
||||||
|
|
||||||
|
# 数据库文件
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
# 日志文件
|
# 日志文件
|
||||||
*.log
|
*.log
|
||||||
logs/
|
logs/
|
||||||
log/
|
|
||||||
# 其他常见排除项
|
# IDE相关
|
||||||
.DS_Store
|
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.DS_Store
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# 迁移文件夹中的版本文件
|
||||||
|
migrations/versions/
|
||||||
|
|
||||||
|
# 开发环境的静态文件或上传文件
|
||||||
|
uploads/
|
@ -1,37 +0,0 @@
|
|||||||
from flask import Flask, request, session
|
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
|
||||||
from flask_migrate import Migrate
|
|
||||||
from flask_login import LoginManager
|
|
||||||
from flask_babel import Babel
|
|
||||||
from app.config import Config
|
|
||||||
import os
|
|
||||||
|
|
||||||
db = SQLAlchemy()
|
|
||||||
migrate = Migrate()
|
|
||||||
login_manager = LoginManager()
|
|
||||||
login_manager.login_view = 'auth.login'
|
|
||||||
babel = Babel()
|
|
||||||
|
|
||||||
|
|
||||||
def create_app(config_class=Config):
|
|
||||||
app = Flask(__name__, template_folder='../templates', static_folder='../static')
|
|
||||||
app.config.from_object(config_class)
|
|
||||||
|
|
||||||
db.init_app(app)
|
|
||||||
migrate.init_app(app, db)
|
|
||||||
login_manager.init_app(app)
|
|
||||||
babel.init_app(app)
|
|
||||||
|
|
||||||
from app.routes import main_bp, auth_bp
|
|
||||||
app.register_blueprint(main_bp)
|
|
||||||
app.register_blueprint(auth_bp, url_prefix='/auth')
|
|
||||||
|
|
||||||
@babel.localeselector
|
|
||||||
def get_locale():
|
|
||||||
# 如果用户已经选择了语言,从session中获取
|
|
||||||
if 'language' in session:
|
|
||||||
return session['language']
|
|
||||||
# 否则尝试匹配请求的语言
|
|
||||||
return request.accept_languages.best_match(app.config['LANGUAGES'])
|
|
||||||
|
|
||||||
return app
|
|
@ -1,10 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-key-should-be-changed'
|
|
||||||
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://study_platform:sumkimadmin@27.124.22.104:3306/study_platform'
|
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
|
||||||
LANGUAGES = ['en', 'zh']
|
|
||||||
DEFAULT_LANGUAGE = 'zh'
|
|
||||||
|
|
||||||
#aaaa
|
|
87
app/forms.py
87
app/forms.py
@ -1,87 +0,0 @@
|
|||||||
from flask_wtf import FlaskForm
|
|
||||||
from wtforms import StringField, PasswordField, BooleanField, SubmitField
|
|
||||||
from wtforms.validators import DataRequired, Email, EqualTo, Length, ValidationError
|
|
||||||
from app.models import User
|
|
||||||
|
|
||||||
|
|
||||||
class LoginForm(FlaskForm):
|
|
||||||
email = StringField('Email', validators=[DataRequired(), Email()])
|
|
||||||
password = PasswordField('Password', validators=[DataRequired()])
|
|
||||||
remember_me = BooleanField('Remember Me')
|
|
||||||
submit = SubmitField('Sign In')
|
|
||||||
|
|
||||||
|
|
||||||
class RegistrationForm(FlaskForm):
|
|
||||||
email = StringField('Email', validators=[DataRequired(), Email()])
|
|
||||||
username = StringField('Username', validators=[DataRequired()])
|
|
||||||
password = PasswordField('Password', validators=[
|
|
||||||
DataRequired(),
|
|
||||||
Length(min=8, message='Password must be at least 8 characters long')
|
|
||||||
])
|
|
||||||
password2 = PasswordField('Repeat Password', validators=[
|
|
||||||
DataRequired(),
|
|
||||||
EqualTo('password', message='Passwords must match')
|
|
||||||
])
|
|
||||||
submit = SubmitField('Register')
|
|
||||||
|
|
||||||
def validate_email(self, email):
|
|
||||||
user = User.query.filter_by(email=email.data).first()
|
|
||||||
if user is not None:
|
|
||||||
raise ValidationError('Email already registered.')
|
|
||||||
|
|
||||||
|
|
||||||
class ResetPasswordRequestForm(FlaskForm):
|
|
||||||
email = StringField('Email', validators=[DataRequired(), Email()])
|
|
||||||
submit = SubmitField('Request Password Reset')
|
|
||||||
|
|
||||||
|
|
||||||
class ResetPasswordForm(FlaskForm):
|
|
||||||
password = PasswordField('New Password', validators=[
|
|
||||||
DataRequired(),
|
|
||||||
Length(min=8, message='Password must be at least 8 characters long')
|
|
||||||
])
|
|
||||||
password2 = PasswordField('Repeat Password', validators=[
|
|
||||||
DataRequired(),
|
|
||||||
EqualTo('password', message='Passwords must match')
|
|
||||||
])
|
|
||||||
submit = SubmitField('Reset Password')
|
|
||||||
|
|
||||||
|
|
||||||
class EmailVerificationForm(FlaskForm):
|
|
||||||
email = StringField('Email', validators=[DataRequired(), Email()])
|
|
||||||
submit = SubmitField('发送验证码')
|
|
||||||
|
|
||||||
def validate_email(self, email):
|
|
||||||
user = User.query.filter_by(email=email.data).first()
|
|
||||||
if user is not None:
|
|
||||||
raise ValidationError('该邮箱已被注册')
|
|
||||||
|
|
||||||
# 验证邮箱是否属于允许的域名
|
|
||||||
if not email.data.endswith('@sq0715.com'):
|
|
||||||
pass # 可以在这里添加邮箱域名限制逻辑
|
|
||||||
|
|
||||||
|
|
||||||
class RegistrationForm(FlaskForm):
|
|
||||||
email = StringField('Email', validators=[DataRequired(), Email()])
|
|
||||||
verification_code = StringField('验证码', validators=[DataRequired(), Length(min=6, max=6)])
|
|
||||||
username = StringField('用户名', validators=[DataRequired()])
|
|
||||||
password = PasswordField('密码', validators=[
|
|
||||||
DataRequired(),
|
|
||||||
Length(min=8, message='密码至少需要8个字符')
|
|
||||||
])
|
|
||||||
password2 = PasswordField('确认密码', validators=[
|
|
||||||
DataRequired(),
|
|
||||||
EqualTo('password', message='两次输入的密码必须匹配')
|
|
||||||
])
|
|
||||||
submit = SubmitField('注册')
|
|
||||||
|
|
||||||
def validate_email(self, email):
|
|
||||||
user = User.query.filter_by(email=email.data).first()
|
|
||||||
if user is not None:
|
|
||||||
raise ValidationError('该邮箱已被注册')
|
|
||||||
|
|
||||||
def validate_verification_code(self, verification_code):
|
|
||||||
from app.utils.email import verify_code
|
|
||||||
|
|
||||||
if not verify_code(self.email.data, verification_code.data):
|
|
||||||
raise ValidationError('验证码无效或已过期')
|
|
@ -1,45 +0,0 @@
|
|||||||
from app import db, login_manager
|
|
||||||
from flask_login import UserMixin
|
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
|
||||||
from datetime import datetime
|
|
||||||
import uuid
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
@login_manager.user_loader
|
|
||||||
def load_user(user_id):
|
|
||||||
return User.query.get(user_id)
|
|
||||||
|
|
||||||
|
|
||||||
# 添加验证码模型
|
|
||||||
class VerificationCode(db.Model):
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
email = db.Column(db.String(120), nullable=False)
|
|
||||||
code = db.Column(db.String(10), nullable=False)
|
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
|
||||||
expires_at = db.Column(db.DateTime, default=lambda: datetime.utcnow() + timedelta(minutes=10))
|
|
||||||
is_used = db.Column(db.Boolean, default=False)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_expired(self):
|
|
||||||
return datetime.utcnow() > self.expires_at
|
|
||||||
class User(UserMixin, db.Model):
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
|
||||||
username = db.Column(db.String(64), nullable=False)
|
|
||||||
password_hash = db.Column(db.String(128))
|
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
|
||||||
last_login = db.Column(db.DateTime)
|
|
||||||
is_active = db.Column(db.Boolean, default=True)
|
|
||||||
language = db.Column(db.String(2), default='zh')
|
|
||||||
theme = db.Column(db.String(10), default='light')
|
|
||||||
reset_token = db.Column(db.String(36), unique=True)
|
|
||||||
|
|
||||||
def set_password(self, password):
|
|
||||||
self.password_hash = generate_password_hash(password)
|
|
||||||
|
|
||||||
def check_password(self, password):
|
|
||||||
return check_password_hash(self.password_hash, password)
|
|
||||||
|
|
||||||
def generate_reset_token(self):
|
|
||||||
self.reset_token = str(uuid.uuid4())
|
|
||||||
return self.reset_token
|
|
290
app/routes.py
290
app/routes.py
@ -1,290 +0,0 @@
|
|||||||
from flask import Blueprint, render_template, redirect, url_for, flash, request, session, jsonify
|
|
||||||
from flask_login import login_user, logout_user, current_user, login_required
|
|
||||||
from werkzeug.urls import url_parse
|
|
||||||
from app import db
|
|
||||||
from app.models import User
|
|
||||||
from app.forms import LoginForm, RegistrationForm, ResetPasswordRequestForm, ResetPasswordForm
|
|
||||||
from app.translations import get_text
|
|
||||||
from datetime import datetime
|
|
||||||
from app.utils.email import generate_verification_code, send_verification_email, save_verification_code
|
|
||||||
from app.forms import EmailVerificationForm, RegistrationForm
|
|
||||||
import re
|
|
||||||
from flask import send_file
|
|
||||||
from PIL import Image, ImageDraw
|
|
||||||
import io
|
|
||||||
from flask import current_app as app
|
|
||||||
|
|
||||||
main_bp = Blueprint('main', __name__)
|
|
||||||
auth_bp = Blueprint('auth', __name__)
|
|
||||||
|
|
||||||
|
|
||||||
# 辅助函数
|
|
||||||
def _get_translation(key):
|
|
||||||
lang = session.get('language', 'zh')
|
|
||||||
return get_text(key, lang)
|
|
||||||
|
|
||||||
|
|
||||||
# 主页路由
|
|
||||||
@main_bp.route('/')
|
|
||||||
def index():
|
|
||||||
if not current_user.is_authenticated:
|
|
||||||
return redirect(url_for('auth.login'))
|
|
||||||
return render_template('main/index.html')
|
|
||||||
|
|
||||||
|
|
||||||
# 切换语言
|
|
||||||
@main_bp.route('/language/<lang>')
|
|
||||||
def set_language(lang):
|
|
||||||
# 确保只接受有效的语言
|
|
||||||
if lang in ['zh', 'en']:
|
|
||||||
session['language'] = lang
|
|
||||||
if current_user.is_authenticated:
|
|
||||||
current_user.language = lang
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# 确保重定向回正确的页面
|
|
||||||
next_page = request.args.get('next') or request.referrer or url_for('main.index')
|
|
||||||
return redirect(next_page)
|
|
||||||
|
|
||||||
|
|
||||||
# 切换主题
|
|
||||||
@main_bp.route('/theme/<theme>')
|
|
||||||
def set_theme(theme):
|
|
||||||
if theme not in ['light', 'dark']:
|
|
||||||
theme = 'light'
|
|
||||||
session['theme'] = theme
|
|
||||||
if current_user.is_authenticated:
|
|
||||||
current_user.theme = theme
|
|
||||||
db.session.commit()
|
|
||||||
next_page = request.args.get('next') or request.referrer or url_for('main.index')
|
|
||||||
return redirect(next_page)
|
|
||||||
|
|
||||||
|
|
||||||
# 认证路由
|
|
||||||
@auth_bp.route('/login', methods=['GET', 'POST'])
|
|
||||||
def login():
|
|
||||||
if current_user.is_authenticated:
|
|
||||||
return redirect(url_for('main.index'))
|
|
||||||
|
|
||||||
form = LoginForm()
|
|
||||||
if form.validate_on_submit():
|
|
||||||
user = User.query.filter_by(email=form.email.data).first()
|
|
||||||
if user is None or not user.check_password(form.password.data):
|
|
||||||
flash(_get_translation('invalid_credentials'))
|
|
||||||
return redirect(url_for('auth.login'))
|
|
||||||
|
|
||||||
login_user(user, remember=form.remember_me.data)
|
|
||||||
|
|
||||||
# 更新最后登录时间
|
|
||||||
user.last_login = datetime.utcnow()
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# 同步用户的语言和主题设置
|
|
||||||
session['language'] = user.language
|
|
||||||
session['theme'] = user.theme
|
|
||||||
|
|
||||||
flash(_get_translation('login_success'))
|
|
||||||
|
|
||||||
next_page = request.args.get('next')
|
|
||||||
if not next_page or url_parse(next_page).netloc != '':
|
|
||||||
next_page = url_for('main.index')
|
|
||||||
return redirect(next_page)
|
|
||||||
|
|
||||||
# 添加对模板的函数
|
|
||||||
def _(key):
|
|
||||||
return _get_translation(key)
|
|
||||||
|
|
||||||
return render_template('auth/login.html', form=form, _=_, hide_language_switch=True, hide_theme_switch=True)
|
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route('/register', methods=['GET', 'POST'])
|
|
||||||
def register():
|
|
||||||
if current_user.is_authenticated:
|
|
||||||
return redirect(url_for('main.index'))
|
|
||||||
|
|
||||||
email_form = EmailVerificationForm()
|
|
||||||
registration_form = RegistrationForm()
|
|
||||||
|
|
||||||
# 处理发送验证码请求
|
|
||||||
if 'send_code' in request.form and email_form.validate_on_submit():
|
|
||||||
email = email_form.email.data
|
|
||||||
code = generate_verification_code()
|
|
||||||
|
|
||||||
# 保存验证码
|
|
||||||
save_verification_code(email, code)
|
|
||||||
|
|
||||||
# 发送验证码邮件
|
|
||||||
if send_verification_email(email, code):
|
|
||||||
flash(_get_translation('verification_code_sent'))
|
|
||||||
|
|
||||||
# 将邮箱保存到表单,用于下一步注册
|
|
||||||
return render_template(
|
|
||||||
'auth/register.html',
|
|
||||||
email_form=email_form,
|
|
||||||
registration_form=registration_form,
|
|
||||||
email_verified=True,
|
|
||||||
verified_email=email
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
flash(_get_translation('email_send_failed'))
|
|
||||||
|
|
||||||
# 处理注册表单提交
|
|
||||||
if 'register' in request.form and registration_form.validate_on_submit():
|
|
||||||
user = User(
|
|
||||||
email=registration_form.email.data,
|
|
||||||
username=registration_form.username.data,
|
|
||||||
language=session.get('language', 'zh'),
|
|
||||||
theme=session.get('theme', 'light')
|
|
||||||
)
|
|
||||||
user.set_password(registration_form.password.data)
|
|
||||||
db.session.add(user)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
flash(_get_translation('account_created'))
|
|
||||||
return redirect(url_for('auth.login'))
|
|
||||||
|
|
||||||
# 检查是否有保存的已验证邮箱
|
|
||||||
email_verified = False
|
|
||||||
verified_email = request.args.get('email', '')
|
|
||||||
|
|
||||||
if verified_email:
|
|
||||||
email_verified = True
|
|
||||||
registration_form.email.data = verified_email
|
|
||||||
|
|
||||||
# 添加对模板的函数
|
|
||||||
def _(key):
|
|
||||||
return _get_translation(key)
|
|
||||||
|
|
||||||
return render_template(
|
|
||||||
'auth/register.html',
|
|
||||||
email_form=email_form,
|
|
||||||
registration_form=registration_form,
|
|
||||||
email_verified=email_verified,
|
|
||||||
verified_email=verified_email,
|
|
||||||
_=_,
|
|
||||||
hide_language_switch=True,
|
|
||||||
hide_theme_switch=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route('/send_verification_code', methods=['POST'])
|
|
||||||
def send_verification_code():
|
|
||||||
try:
|
|
||||||
email = request.form.get('email')
|
|
||||||
|
|
||||||
if not email or not re.match(r'[^@]+@[^@]+\.[^@]+', email):
|
|
||||||
return jsonify({'success': False, 'message': _get_translation('invalid_email')})
|
|
||||||
|
|
||||||
# 检查邮箱是否已被注册
|
|
||||||
try:
|
|
||||||
if User.query.filter_by(email=email).first():
|
|
||||||
return jsonify({'success': False, 'message': _get_translation('email_already_registered')})
|
|
||||||
except Exception as e:
|
|
||||||
app.logger.error(f"数据库查询错误: {str(e)}")
|
|
||||||
return jsonify({'success': False, 'message': _get_translation('server_error')})
|
|
||||||
|
|
||||||
# 生成并保存验证码
|
|
||||||
try:
|
|
||||||
code = generate_verification_code()
|
|
||||||
save_verification_code(email, code)
|
|
||||||
except Exception as e:
|
|
||||||
app.logger.error(f"验证码保存错误: {str(e)}")
|
|
||||||
return jsonify({'success': False, 'message': _get_translation('server_error')})
|
|
||||||
|
|
||||||
# 发送验证码邮件
|
|
||||||
try:
|
|
||||||
if send_verification_email(email, code):
|
|
||||||
return jsonify({'success': True, 'message': _get_translation('verification_code_sent')})
|
|
||||||
else:
|
|
||||||
return jsonify({'success': False, 'message': _get_translation('email_send_failed')})
|
|
||||||
except Exception as e:
|
|
||||||
app.logger.error(f"邮件发送错误: {str(e)}")
|
|
||||||
return jsonify({'success': False, 'message': _get_translation('email_send_failed')})
|
|
||||||
except Exception as e:
|
|
||||||
app.logger.error(f"验证码发送路由错误: {str(e)}")
|
|
||||||
return jsonify({'success': False, 'message': _get_translation('server_error')})
|
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route('/reset_password_request', methods=['GET', 'POST'])
|
|
||||||
def reset_password_request():
|
|
||||||
if current_user.is_authenticated:
|
|
||||||
return redirect(url_for('main.index'))
|
|
||||||
|
|
||||||
form = ResetPasswordRequestForm()
|
|
||||||
if form.validate_on_submit():
|
|
||||||
user = User.query.filter_by(email=form.email.data).first()
|
|
||||||
if user:
|
|
||||||
token = user.generate_reset_token()
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# 这里应该发送邮件,但目前只是模拟
|
|
||||||
# 实际项目中需要集成邮件发送功能
|
|
||||||
flash(_get_translation('password_reset_sent'))
|
|
||||||
|
|
||||||
# 用于演示,实际项目中应该通过邮件发送这个链接
|
|
||||||
reset_url = url_for('auth.reset_password', token=token, _external=True)
|
|
||||||
print(f"Password reset URL: {reset_url}")
|
|
||||||
|
|
||||||
return redirect(url_for('auth.login'))
|
|
||||||
|
|
||||||
return render_template('auth/reset_password_request.html', form=form)
|
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route('/reset_password/<token>', methods=['GET', 'POST'])
|
|
||||||
def reset_password(token):
|
|
||||||
if current_user.is_authenticated:
|
|
||||||
return redirect(url_for('main.index'))
|
|
||||||
|
|
||||||
user = User.query.filter_by(reset_token=token).first()
|
|
||||||
if not user:
|
|
||||||
flash(_get_translation('invalid_reset_token'))
|
|
||||||
return redirect(url_for('auth.login'))
|
|
||||||
|
|
||||||
form = ResetPasswordForm()
|
|
||||||
if form.validate_on_submit():
|
|
||||||
user.set_password(form.password.data)
|
|
||||||
user.reset_token = None
|
|
||||||
db.session.commit()
|
|
||||||
flash(_get_translation('password_reset_success'))
|
|
||||||
return redirect(url_for('auth.login'))
|
|
||||||
|
|
||||||
return render_template('auth/reset_password.html', form=form)
|
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route('/logout')
|
|
||||||
@login_required
|
|
||||||
def logout():
|
|
||||||
logout_user()
|
|
||||||
return redirect(url_for('auth.login'))
|
|
||||||
|
|
||||||
|
|
||||||
@main_bp.route('/api/placeholder/<width>/<height>')
|
|
||||||
def placeholder(width, height):
|
|
||||||
"""生成并返回指定尺寸的占位图像"""
|
|
||||||
try:
|
|
||||||
width = int(width)
|
|
||||||
height = int(height)
|
|
||||||
except ValueError:
|
|
||||||
width = 100
|
|
||||||
height = 100
|
|
||||||
|
|
||||||
# 限制最大尺寸
|
|
||||||
width = min(width, 1200)
|
|
||||||
height = min(height, 1200)
|
|
||||||
|
|
||||||
# 创建简单的占位图
|
|
||||||
img = Image.new('RGB', (width, height), color=(220, 220, 220))
|
|
||||||
d = ImageDraw.Draw(img)
|
|
||||||
|
|
||||||
# 绘制边框
|
|
||||||
d.rectangle([0, 0, width - 1, height - 1], outline=(200, 200, 200))
|
|
||||||
|
|
||||||
# 绘制文本
|
|
||||||
text = f"{width}x{height}"
|
|
||||||
|
|
||||||
# 将图像转换为字节流
|
|
||||||
img_byte_arr = io.BytesIO()
|
|
||||||
img.save(img_byte_arr, format='PNG')
|
|
||||||
img_byte_arr.seek(0)
|
|
||||||
|
|
||||||
return send_file(img_byte_arr, mimetype='image/png')
|
|
@ -1,125 +0,0 @@
|
|||||||
translations = {
|
|
||||||
'en': {
|
|
||||||
'login': 'Login',
|
|
||||||
'register': 'Register',
|
|
||||||
'forgot_password': 'Forgot Password?',
|
|
||||||
'email': 'Email',
|
|
||||||
'password': 'Password',
|
|
||||||
'remember_me': 'Remember Me (7 days)',
|
|
||||||
'sign_in': 'Sign In',
|
|
||||||
'username': 'Username',
|
|
||||||
'repeat_password': 'Repeat Password',
|
|
||||||
'reset_password': 'Reset Password',
|
|
||||||
'submit': 'Submit',
|
|
||||||
'welcome': 'Welcome to Study Platform',
|
|
||||||
'welcome_back': 'Welcome back, please log in to your account',
|
|
||||||
'switch_language': 'Switch Language',
|
|
||||||
'switch_theme': 'Switch Theme',
|
|
||||||
'logout': 'Logout',
|
|
||||||
'my_dashboard': 'My Dashboard',
|
|
||||||
'network_drive': 'Network Drive',
|
|
||||||
'email_system': 'Email System',
|
|
||||||
'code_compiler': 'Code Compiler',
|
|
||||||
'file_submission': 'File Submission',
|
|
||||||
'video_platform': 'Video Platform',
|
|
||||||
'blog_space': 'Blog Space',
|
|
||||||
'ai_platform': 'AI Platform',
|
|
||||||
'code_repository': 'Code Repository',
|
|
||||||
'password_reset_sent': 'Check your email for password reset instructions',
|
|
||||||
'invalid_reset_token': 'Invalid or expired token',
|
|
||||||
'password_reset_success': 'Your password has been reset',
|
|
||||||
'account_created': 'Account created successfully! You can now log in',
|
|
||||||
'login_success': 'Login successful',
|
|
||||||
'account_login': 'Account Login',
|
|
||||||
'qr_login': 'QR Code Login',
|
|
||||||
'email_placeholder': 'Please enter your email',
|
|
||||||
'password_placeholder': 'Please enter your password',
|
|
||||||
'other_login_methods': 'Other Login Methods',
|
|
||||||
'wechat_login': 'WeChat Login',
|
|
||||||
'qq_login': 'QQ Login',
|
|
||||||
'scan_qr_code': 'Please scan the QR code to log in',
|
|
||||||
'email_required': 'Email is required',
|
|
||||||
'invalid_email': 'Invalid email format',
|
|
||||||
'password_required': 'Password is required',
|
|
||||||
'password_too_short': 'Password must be at least 8 characters',
|
|
||||||
'no_account': 'Don\'t have an account?',
|
|
||||||
'privacy_policy': 'Privacy Policy',
|
|
||||||
'terms_of_service': 'Terms of Service',
|
|
||||||
'verification_code': 'Verification Code',
|
|
||||||
'send_verification_code': 'Send Verification Code',
|
|
||||||
'verification_code_sent': 'Verification code has been sent to your email',
|
|
||||||
'email_send_failed': 'Failed to send email, please try again later',
|
|
||||||
'email_already_registered': 'This email is already registered',
|
|
||||||
'resend_code': 'Resend Code',
|
|
||||||
'email_verification': 'Email Verification',
|
|
||||||
'next_step': 'Next Step',
|
|
||||||
'back': 'Back',
|
|
||||||
'account_info': 'Account Information',
|
|
||||||
'registration_success': 'Registration Successful',
|
|
||||||
'verification_code_invalid': 'Invalid verification code',
|
|
||||||
'verification_code_expired': 'Verification code has expired'
|
|
||||||
},
|
|
||||||
'zh': {
|
|
||||||
'verification_code': '验证码',
|
|
||||||
'send_verification_code': '发送验证码',
|
|
||||||
'verification_code_sent': '验证码已发送到您的邮箱',
|
|
||||||
'email_send_failed': '邮件发送失败,请稍后重试',
|
|
||||||
'email_already_registered': '该邮箱已被注册',
|
|
||||||
'resend_code': '重新发送',
|
|
||||||
'email_verification': '邮箱验证',
|
|
||||||
'next_step': '下一步',
|
|
||||||
'back': '返回',
|
|
||||||
'account_info': '账号信息',
|
|
||||||
'registration_success': '注册成功',
|
|
||||||
'verification_code_invalid': '验证码无效',
|
|
||||||
'verification_code_expired': '验证码已过期',
|
|
||||||
'login': '登录',
|
|
||||||
'register': '注册',
|
|
||||||
'forgot_password': '忘记密码?',
|
|
||||||
'email': '邮箱',
|
|
||||||
'password': '密码',
|
|
||||||
'remember_me': '记住我(7天内免登录)',
|
|
||||||
'sign_in': '登录',
|
|
||||||
'username': '用户名',
|
|
||||||
'repeat_password': '确认密码',
|
|
||||||
'reset_password': '重置密码',
|
|
||||||
'submit': '提交',
|
|
||||||
'welcome': '欢迎使用学习平台',
|
|
||||||
'welcome_back': '欢迎回来,请登录您的账户',
|
|
||||||
'switch_language': '切换语言',
|
|
||||||
'switch_theme': '切换主题',
|
|
||||||
'logout': '退出登录',
|
|
||||||
'my_dashboard': '我的面板',
|
|
||||||
'network_drive': '网盘系统',
|
|
||||||
'email_system': '邮箱系统',
|
|
||||||
'code_compiler': '在线代码编译',
|
|
||||||
'file_submission': '文件提交系统',
|
|
||||||
'video_platform': '在线视频网站',
|
|
||||||
'blog_space': '博客空间',
|
|
||||||
'ai_platform': 'AI 平台',
|
|
||||||
'code_repository': '代码仓库',
|
|
||||||
'password_reset_sent': '密码重置邮件已发送,请查收',
|
|
||||||
'invalid_reset_token': '无效或已过期的令牌',
|
|
||||||
'password_reset_success': '密码重置成功',
|
|
||||||
'account_created': '账户创建成功!您现在可以登录',
|
|
||||||
'login_success': '登录成功',
|
|
||||||
'account_login': '账号密码登录',
|
|
||||||
'qr_login': '扫码登录',
|
|
||||||
'email_placeholder': '请输入邮箱地址',
|
|
||||||
'password_placeholder': '请输入密码',
|
|
||||||
'other_login_methods': '其他登录方式',
|
|
||||||
'wechat_login': '微信登录',
|
|
||||||
'qq_login': 'QQ登录',
|
|
||||||
'scan_qr_code': '请扫描二维码登录',
|
|
||||||
'email_required': '邮箱不能为空',
|
|
||||||
'invalid_email': '邮箱格式不正确',
|
|
||||||
'password_required': '密码不能为空',
|
|
||||||
'password_too_short': '密码至少需要8个字符',
|
|
||||||
'no_account': '还没有账号?',
|
|
||||||
'privacy_policy': '隐私政策',
|
|
||||||
'terms_of_service': '服务条款'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_text(key, lang):
|
|
||||||
return translations.get(lang, {}).get(key, key)
|
|
@ -1,117 +0,0 @@
|
|||||||
import smtplib
|
|
||||||
import random
|
|
||||||
import string
|
|
||||||
from email.mime.text import MIMEText
|
|
||||||
from email.mime.multipart import MIMEMultipart
|
|
||||||
from flask import current_app, session
|
|
||||||
from app import db
|
|
||||||
from app.models import VerificationCode
|
|
||||||
|
|
||||||
|
|
||||||
# 邮件配置
|
|
||||||
EMAIL_CONFIG = {
|
|
||||||
'host': 'mail.sq0715.com',
|
|
||||||
'port': 587,
|
|
||||||
'username': 'sumkim@sq0715.com',
|
|
||||||
'password': 'sumkim0715',
|
|
||||||
'from_email': 'sumkim@sq0715.com',
|
|
||||||
'from_name': 'QINAI_OFFICIAL'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def generate_verification_code(length=6):
|
|
||||||
"""生成6位数字验证码"""
|
|
||||||
return ''.join(random.choices(string.digits, k=length))
|
|
||||||
|
|
||||||
|
|
||||||
def save_verification_code(email, code):
|
|
||||||
"""保存验证码到数据库"""
|
|
||||||
# 删除之前的验证码
|
|
||||||
VerificationCode.query.filter_by(email=email).delete()
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# 创建新验证码记录
|
|
||||||
verification = VerificationCode(email=email, code=code)
|
|
||||||
db.session.add(verification)
|
|
||||||
db.session.commit()
|
|
||||||
return verification
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def verify_code(email, code):
|
|
||||||
"""验证验证码是否正确且在有效期内"""
|
|
||||||
verification = VerificationCode.query.filter_by(
|
|
||||||
email=email,
|
|
||||||
code=code,
|
|
||||||
is_used=False
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if not verification:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 验证成功后标记为已使用
|
|
||||||
verification.is_used = True
|
|
||||||
db.session.commit()
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def send_verification_email(to_email, code):
|
|
||||||
"""发送验证码邮件"""
|
|
||||||
subject = "【高可用学习平台】您的注册验证码"
|
|
||||||
|
|
||||||
# 创建邮件正文,支持HTML格式
|
|
||||||
html_content = f"""
|
|
||||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #eee; border-radius: 10px; background-color: #f9f9f9;">
|
|
||||||
<h2 style="color: #4a89dc; text-align: center;">高可用学习平台 - 邮箱验证</h2>
|
|
||||||
<p>您好,</p>
|
|
||||||
<p>感谢您注册高可用学习平台。请使用以下验证码完成注册:</p>
|
|
||||||
<div style="background-color: #4a89dc; color: white; font-size: 24px; font-weight: bold; text-align: center; padding: 15px; border-radius: 5px; letter-spacing: 5px; margin: 20px 0;">
|
|
||||||
{code}
|
|
||||||
</div>
|
|
||||||
<p>验证码有效期为10分钟,请尽快完成注册。</p>
|
|
||||||
<p>如果您没有进行注册操作,请忽略此邮件。</p>
|
|
||||||
<p style="margin-top: 30px; padding-top: 10px; border-top: 1px solid #eee; font-size: 12px; color: #666;">
|
|
||||||
此邮件由系统自动发送,请勿直接回复。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
|
|
||||||
plain_text = f"""
|
|
||||||
高可用学习平台 - 邮箱验证
|
|
||||||
|
|
||||||
您好,
|
|
||||||
|
|
||||||
感谢您注册高可用学习平台。请使用以下验证码完成注册:
|
|
||||||
|
|
||||||
{code}
|
|
||||||
|
|
||||||
验证码有效期为10分钟,请尽快完成注册。
|
|
||||||
|
|
||||||
如果您没有进行注册操作,请忽略此邮件。
|
|
||||||
|
|
||||||
此邮件由系统自动发送,请勿直接回复。
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 创建MIMEMultipart对象
|
|
||||||
message = MIMEMultipart("alternative")
|
|
||||||
message["Subject"] = subject
|
|
||||||
message["From"] = f"{EMAIL_CONFIG['from_name']} <{EMAIL_CONFIG['from_email']}>"
|
|
||||||
message["To"] = to_email
|
|
||||||
|
|
||||||
# 添加文本和HTML版本
|
|
||||||
message.attach(MIMEText(plain_text, "plain"))
|
|
||||||
message.attach(MIMEText(html_content, "html"))
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 连接到SMTP服务器
|
|
||||||
server = smtplib.SMTP(EMAIL_CONFIG['host'], EMAIL_CONFIG['port'])
|
|
||||||
server.starttls() # 启用TLS加密
|
|
||||||
server.login(EMAIL_CONFIG['username'], EMAIL_CONFIG['password'])
|
|
||||||
|
|
||||||
# 发送邮件
|
|
||||||
server.sendmail(EMAIL_CONFIG['from_email'], to_email, message.as_string())
|
|
||||||
server.quit()
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"发送邮件失败: {str(e)}")
|
|
||||||
return False
|
|
21
main.py
21
main.py
@ -1,21 +0,0 @@
|
|||||||
from app import create_app, db
|
|
||||||
from app.models import User
|
|
||||||
import pymysql
|
|
||||||
|
|
||||||
app = create_app()
|
|
||||||
|
|
||||||
@app.before_first_request
|
|
||||||
def check_db_connection():
|
|
||||||
try:
|
|
||||||
# 使用SQLAlchemy检查连接
|
|
||||||
db.session.execute("SELECT 1")
|
|
||||||
app.logger.info("数据库连接成功")
|
|
||||||
except Exception as e:
|
|
||||||
app.logger.error(f"数据库连接错误: {str(e)}")
|
|
||||||
|
|
||||||
@app.shell_context_processor
|
|
||||||
def make_shell_context():
|
|
||||||
return {'db': db, 'User': User}
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
app.run(host='0.0.0.0', port=40911, debug=True)
|
|
@ -1,16 +0,0 @@
|
|||||||
Flask==2.0.1
|
|
||||||
Flask-Login==0.5.0
|
|
||||||
Flask-WTF==0.15.1
|
|
||||||
Flask-Migrate==3.1.0
|
|
||||||
Flask-Babel==2.0.0
|
|
||||||
PyMySQL==1.0.2
|
|
||||||
email-validator==1.1.3
|
|
||||||
python-dotenv==0.19.0
|
|
||||||
Werkzeug==2.0.3
|
|
||||||
sqlalchemy==1.4.46
|
|
||||||
# 以下是添加的电子邮件相关库
|
|
||||||
Flask-Mail==0.9.1
|
|
||||||
# 如果需要异步发送邮件
|
|
||||||
# celery==5.2.7
|
|
||||||
pillow
|
|
||||||
cryptography
|
|
@ -1,72 +0,0 @@
|
|||||||
/* 暗色主题 */
|
|
||||||
body.theme-dark {
|
|
||||||
background-color: #212529;
|
|
||||||
color: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-dark .auth-form {
|
|
||||||
background-color: #343a40;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-dark .service-card {
|
|
||||||
background-color: #343a40;
|
|
||||||
box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-dark .dashboard-header {
|
|
||||||
border-bottom-color: #495057;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-dark .language-switch a,
|
|
||||||
body.theme-dark .theme-switch a {
|
|
||||||
color: #6ea8fe;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-dark .language-switch a:hover,
|
|
||||||
body.theme-dark .theme-switch a:hover {
|
|
||||||
color: #9ec5fe;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-dark .auth-form a {
|
|
||||||
color: #6ea8fe;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-dark .auth-form a:hover {
|
|
||||||
color: #9ec5fe;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-dark .service-card p {
|
|
||||||
color: #adb5bd;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-dark .alert-info {
|
|
||||||
background-color: #032830;
|
|
||||||
color: #6edff6;
|
|
||||||
border-color: #055160;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-dark input.form-control,
|
|
||||||
body.theme-dark input.form-check-input {
|
|
||||||
background-color: #495057;
|
|
||||||
border-color: #6c757d;
|
|
||||||
color: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-dark input.form-control:focus {
|
|
||||||
background-color: #495057;
|
|
||||||
color: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-dark label {
|
|
||||||
color: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-dark .btn-outline-danger {
|
|
||||||
color: #ea868f;
|
|
||||||
border-color: #ea868f;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-dark .btn-outline-danger:hover {
|
|
||||||
background-color: #dc3545;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
/* 亮色主题 */
|
|
||||||
body.theme-light {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
color: #212529;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-light .auth-form {
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-light .service-card {
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-light .dashboard-header {
|
|
||||||
border-bottom-color: #dee2e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-light .language-switch a,
|
|
||||||
body.theme-light .theme-switch a {
|
|
||||||
color: #0d6efd;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-light .language-switch a:hover,
|
|
||||||
body.theme-light .theme-switch a:hover {
|
|
||||||
color: #0a58ca;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-light .auth-form a {
|
|
||||||
color: #0d6efd;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-light .auth-form a:hover {
|
|
||||||
color: #0a58ca;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-light .alert-info {
|
|
||||||
background-color: #cff4fc;
|
|
||||||
color: #055160;
|
|
||||||
border-color: #b6effb;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-light input.form-control,
|
|
||||||
body.theme-light input.form-check-input {
|
|
||||||
background-color: #fff;
|
|
||||||
border-color: #ced4da;
|
|
||||||
color: #212529;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-light label {
|
|
||||||
color: #212529;
|
|
||||||
}
|
|
@ -1,578 +0,0 @@
|
|||||||
/* 登录页面专用样式 */
|
|
||||||
body {
|
|
||||||
background-color: var(--bg-color);
|
|
||||||
background-image: url('https://source.unsplash.com/random/1920x1080/?technology');
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 100vh;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay {
|
|
||||||
background-color: rgba(0, 0, 0, 0.4);
|
|
||||||
backdrop-filter: blur(5px);
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-toggle {
|
|
||||||
position: absolute;
|
|
||||||
top: 20px;
|
|
||||||
right: 20px;
|
|
||||||
z-index: 10;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
|
||||||
backdrop-filter: blur(5px);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
font-size: 18px;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-toggle:hover {
|
|
||||||
transform: rotate(45deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.language-selector {
|
|
||||||
position: absolute;
|
|
||||||
top: 20px;
|
|
||||||
left: 20px;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.language-selector select {
|
|
||||||
padding: 8px 15px;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
|
||||||
backdrop-filter: blur(5px);
|
|
||||||
color: white;
|
|
||||||
font-size: 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
outline: none;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.language-selector select:hover {
|
|
||||||
background-color: rgba(255, 255, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.language-selector select option {
|
|
||||||
background-color: var(--card-bg);
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex: 1;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-container {
|
|
||||||
background-color: var(--card-bg);
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
|
|
||||||
width: 450px;
|
|
||||||
padding: 35px;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
animation: fadeIn 0.5s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from { opacity: 0; transform: translateY(20px); }
|
|
||||||
to { opacity: 1; transform: translateY(0); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-container::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: -50px;
|
|
||||||
left: -50px;
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
border-radius: 50%;
|
|
||||||
opacity: 0.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-container::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
bottom: -50px;
|
|
||||||
right: -50px;
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
background-color: var(--secondary-color);
|
|
||||||
border-radius: 50%;
|
|
||||||
opacity: 0.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo img {
|
|
||||||
width: 90px;
|
|
||||||
height: 90px;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
padding: 5px;
|
|
||||||
background-color: #fff;
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo img:hover {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
color: var(--text-color);
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
text-align: center;
|
|
||||||
color: var(--light-text);
|
|
||||||
margin-bottom: 30px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-container {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab {
|
|
||||||
flex: 1;
|
|
||||||
text-align: center;
|
|
||||||
padding: 12px 0;
|
|
||||||
cursor: pointer;
|
|
||||||
border-bottom: 2px solid var(--border-color);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab.active {
|
|
||||||
border-bottom: 2px solid var(--primary-color);
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 22px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: var(--text-color);
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-with-icon {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-icon {
|
|
||||||
position: absolute;
|
|
||||||
left: 15px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
color: var(--light-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control {
|
|
||||||
width: 100%;
|
|
||||||
height: 48px;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0 15px 0 45px;
|
|
||||||
font-size: 15px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
background-color: var(--card-bg);
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control:focus {
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
box-shadow: 0 0 0 3px rgba(74, 137, 220, 0.2);
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.password-toggle {
|
|
||||||
position: absolute;
|
|
||||||
right: 15px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--light-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.validation-message {
|
|
||||||
margin-top: 6px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--error-color);
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.validation-message.show {
|
|
||||||
display: block;
|
|
||||||
animation: shake 0.5s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shake {
|
|
||||||
0%, 100% { transform: translateX(0); }
|
|
||||||
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
|
|
||||||
20%, 40%, 60%, 80% { transform: translateX(5px); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.remember-forgot {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remember-me {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-checkbox {
|
|
||||||
position: relative;
|
|
||||||
padding-left: 30px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
user-select: none;
|
|
||||||
color: var(--light-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-checkbox input {
|
|
||||||
position: absolute;
|
|
||||||
opacity: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
height: 0;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkmark {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
height: 18px;
|
|
||||||
width: 18px;
|
|
||||||
background-color: var(--card-bg);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 3px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-checkbox:hover input ~ .checkmark {
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-checkbox input:checked ~ .checkmark {
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkmark:after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-checkbox input:checked ~ .checkmark:after {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-checkbox .checkmark:after {
|
|
||||||
left: 6px;
|
|
||||||
top: 2px;
|
|
||||||
width: 4px;
|
|
||||||
height: 9px;
|
|
||||||
border: solid white;
|
|
||||||
border-width: 0 2px 2px 0;
|
|
||||||
transform: rotate(45deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.forgot-password a {
|
|
||||||
color: var(--primary-color);
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.forgot-password a:hover {
|
|
||||||
color: var(--primary-hover);
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-login {
|
|
||||||
width: 100%;
|
|
||||||
height: 48px;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-login:hover {
|
|
||||||
background-color: var(--primary-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-login:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-login .loading {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
from { transform: translate(-50%, -50%) rotate(0deg); }
|
|
||||||
to { transform: translate(-50%, -50%) rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-login.loading-state {
|
|
||||||
color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-login.loading-state .loading {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin: 30px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider::before,
|
|
||||||
.divider::after {
|
|
||||||
content: "";
|
|
||||||
flex: 1;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider span {
|
|
||||||
padding: 0 15px;
|
|
||||||
color: var(--light-text);
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-login {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 20px;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-btn {
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: rgba(245, 245, 245, 0.1);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-btn::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: -100%;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(
|
|
||||||
90deg,
|
|
||||||
rgba(255, 255, 255, 0) 0%,
|
|
||||||
rgba(255, 255, 255, 0.2) 50%,
|
|
||||||
rgba(255, 255, 255, 0) 100%
|
|
||||||
);
|
|
||||||
transition: left 0.7s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-btn:hover {
|
|
||||||
transform: translateY(-3px);
|
|
||||||
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-btn:hover::before {
|
|
||||||
left: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-btn img {
|
|
||||||
width: 25px;
|
|
||||||
height: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-btn.wechat {
|
|
||||||
background-color: rgba(9, 187, 7, 0.1);
|
|
||||||
border-color: rgba(9, 187, 7, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-btn.qq {
|
|
||||||
background-color: rgba(18, 183, 245, 0.1);
|
|
||||||
border-color: rgba(18, 183, 245, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.signup {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 25px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--light-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.signup a {
|
|
||||||
color: var(--primary-color);
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 600;
|
|
||||||
transition: color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signup a:hover {
|
|
||||||
color: var(--primary-hover);
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qr-code-login {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qr-code {
|
|
||||||
width: 180px;
|
|
||||||
height: 180px;
|
|
||||||
margin: 20px auto;
|
|
||||||
padding: 10px;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qr-code img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qr-code-tip {
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--light-text);
|
|
||||||
margin-bottom: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
text-align: center;
|
|
||||||
padding: 20px;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer a {
|
|
||||||
color: rgba(255, 255, 255, 0.9);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式设计 */
|
|
||||||
@media (max-width: 576px) {
|
|
||||||
.login-container {
|
|
||||||
width: 100%;
|
|
||||||
padding: 25px;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-toggle,
|
|
||||||
.language-selector {
|
|
||||||
top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo img {
|
|
||||||
width: 70px;
|
|
||||||
height: 70px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-login {
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-btn {
|
|
||||||
width: 45px;
|
|
||||||
height: 45px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-container {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 暗色模式适配 */
|
|
||||||
body.theme-dark .overlay {
|
|
||||||
background-color: rgba(0, 0, 0, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-dark .login-container {
|
|
||||||
background-color: rgba(45, 45, 45, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-dark .logo img {
|
|
||||||
background-color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-dark .checkmark {
|
|
||||||
background-color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-dark .form-control {
|
|
||||||
background-color: rgba(60, 60, 60, 0.8);
|
|
||||||
color: #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-dark .social-btn {
|
|
||||||
background-color: rgba(50, 50, 50, 0.5);
|
|
||||||
}
|
|
@ -1,166 +0,0 @@
|
|||||||
/* 注册页面样式,继承登录页面样式 */
|
|
||||||
.step-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-container::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 15px;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 2px;
|
|
||||||
background-color: var(--border-color);
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step {
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: var(--bg-color);
|
|
||||||
border: 2px solid var(--border-color);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--light-text);
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step.active {
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step.completed {
|
|
||||||
background-color: var(--success-color);
|
|
||||||
border-color: var(--success-color);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-label {
|
|
||||||
position: absolute;
|
|
||||||
top: 40px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--light-text);
|
|
||||||
width: 80px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.verification-container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.verification-input {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.send-code-btn {
|
|
||||||
white-space: nowrap;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 10px 15px;
|
|
||||||
font-size: 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
height: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.send-code-btn:hover {
|
|
||||||
background-color: var(--primary-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.send-code-btn:disabled {
|
|
||||||
background-color: var(--border-color);
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.countdown {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--light-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.registration-step {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.registration-step.active {
|
|
||||||
display: block;
|
|
||||||
animation: fadeIn 0.5s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from { opacity: 0; transform: translateY(10px); }
|
|
||||||
to { opacity: 1; transform: translateY(0); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-back {
|
|
||||||
background-color: var(--bg-color);
|
|
||||||
color: var(--text-color);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-next {
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.strength-meter {
|
|
||||||
height: 4px;
|
|
||||||
background-color: var(--border-color);
|
|
||||||
margin-top: 10px;
|
|
||||||
border-radius: 2px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.strength-meter-fill {
|
|
||||||
height: 100%;
|
|
||||||
width: 0;
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.strength-meter-fill.weak {
|
|
||||||
width: 25%;
|
|
||||||
background-color: #e74c3c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.strength-meter-fill.medium {
|
|
||||||
width: 50%;
|
|
||||||
background-color: #f39c12;
|
|
||||||
}
|
|
||||||
|
|
||||||
.strength-meter-fill.strong {
|
|
||||||
width: 75%;
|
|
||||||
background-color: #3498db;
|
|
||||||
}
|
|
||||||
|
|
||||||
.strength-meter-fill.very-strong {
|
|
||||||
width: 100%;
|
|
||||||
background-color: #2ecc71;
|
|
||||||
}
|
|
||||||
|
|
||||||
.password-strength-text {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--light-text);
|
|
||||||
text-align: right;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
@ -1,154 +0,0 @@
|
|||||||
/* 基础样式 */
|
|
||||||
body {
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
min-height: 100vh;
|
|
||||||
transition: background-color 0.3s, color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 语言和主题切换 */
|
|
||||||
.language-switch, .theme-switch {
|
|
||||||
text-align: right;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.language-switch a, .theme-switch a {
|
|
||||||
text-decoration: none;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 登录和注册表单 */
|
|
||||||
.auth-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 80vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-form {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 400px;
|
|
||||||
padding: 25px;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-form h2 {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
color: #dc3545;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
display: block;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 仪表盘 */
|
|
||||||
.dashboard-container {
|
|
||||||
padding: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
padding-bottom: 15px;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.services-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-card {
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 0 15px rgba(0, 0, 0, 0.05);
|
|
||||||
padding: 20px;
|
|
||||||
transition: transform 0.3s, box-shadow 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-card:hover {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-card h3 {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-card p {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式设计 */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.dashboard-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.services-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-form {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 通用样式 */
|
|
||||||
.btn {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flashes {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 消息提示 */
|
|
||||||
.alert {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
padding: 12px 20px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 表单元素 */
|
|
||||||
.form-control:focus {
|
|
||||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
|
||||||
border-color: #86b7fe;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 主页卡片特效 */
|
|
||||||
.service-card .btn {
|
|
||||||
width: 100%;
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 200px;
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 1.2 MiB |
@ -1,78 +0,0 @@
|
|||||||
// 主要功能和交互
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// 初始化提示框
|
|
||||||
const toasts = document.querySelectorAll('.toast');
|
|
||||||
if (toasts.length) {
|
|
||||||
toasts.forEach(toast => {
|
|
||||||
new bootstrap.Toast(toast).show();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 密码强度检查
|
|
||||||
const passwordInputs = document.querySelectorAll('input[type="password"]');
|
|
||||||
passwordInputs.forEach(input => {
|
|
||||||
input.addEventListener('input', function() {
|
|
||||||
checkPasswordStrength(this);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 主题切换效果
|
|
||||||
const themeSwitch = document.querySelector('.theme-switch a');
|
|
||||||
if (themeSwitch) {
|
|
||||||
themeSwitch.addEventListener('click', function(e) {
|
|
||||||
// 先进行动画效果,然后让链接正常跳转
|
|
||||||
document.body.style.opacity = '0.5';
|
|
||||||
setTimeout(() => {
|
|
||||||
document.body.style.opacity = '1';
|
|
||||||
}, 300);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 服务卡片hover效果增强
|
|
||||||
const serviceCards = document.querySelectorAll('.service-card');
|
|
||||||
serviceCards.forEach(card => {
|
|
||||||
card.addEventListener('mouseenter', function() {
|
|
||||||
this.style.transform = 'translateY(-10px)';
|
|
||||||
});
|
|
||||||
card.addEventListener('mouseleave', function() {
|
|
||||||
this.style.transform = 'translateY(0)';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 密码强度检查函数
|
|
||||||
function checkPasswordStrength(input) {
|
|
||||||
const password = input.value;
|
|
||||||
let strength = 0;
|
|
||||||
|
|
||||||
// 长度检查
|
|
||||||
if (password.length >= 8) strength += 1;
|
|
||||||
|
|
||||||
// 包含数字
|
|
||||||
if (/\d/.test(password)) strength += 1;
|
|
||||||
|
|
||||||
// 包含小写字母
|
|
||||||
if (/[a-z]/.test(password)) strength += 1;
|
|
||||||
|
|
||||||
// 包含大写字母
|
|
||||||
if (/[A-Z]/.test(password)) strength += 1;
|
|
||||||
|
|
||||||
// 包含特殊字符
|
|
||||||
if (/[^A-Za-z0-9]/.test(password)) strength += 1;
|
|
||||||
|
|
||||||
// 移除旧的强度指示器
|
|
||||||
const oldIndicator = input.parentNode.querySelector('.password-strength');
|
|
||||||
if (oldIndicator) oldIndicator.remove();
|
|
||||||
|
|
||||||
// 如果密码长度大于0,显示强度指示器
|
|
||||||
if (password.length > 0) {
|
|
||||||
const strengthText = ['很弱', '弱', '中等', '强', '很强'][Math.min(strength, 4)];
|
|
||||||
const strengthClass = ['very-weak', 'weak', 'medium', 'strong', 'very-strong'][Math.min(strength, 4)];
|
|
||||||
|
|
||||||
const indicator = document.createElement('div');
|
|
||||||
indicator.className = `password-strength ${strengthClass}`;
|
|
||||||
indicator.textContent = `密码强度: ${strengthText}`;
|
|
||||||
|
|
||||||
input.parentNode.appendChild(indicator);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,223 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}{{ _('login') }} - 高可用学习平台{% endblock %}
|
|
||||||
|
|
||||||
{% block extra_css %}
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/login.css') }}">
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="overlay"></div>
|
|
||||||
|
|
||||||
<div class="theme-toggle" id="theme-toggle">
|
|
||||||
{% if session.get('theme', 'light') == 'light' %}☀️{% else %}🌙{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="language-selector">
|
|
||||||
<select id="language-select">
|
|
||||||
<option value="zh" {% if session.get('language', 'zh') == 'zh' %}selected{% endif %}>简体中文</option>
|
|
||||||
<option value="en" {% if session.get('language', 'zh') == 'en' %}selected{% endif %}>English</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="main-container">
|
|
||||||
<div class="login-container">
|
|
||||||
<div class="logo">
|
|
||||||
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo" onerror="this.src='/api/placeholder/90/90'">
|
|
||||||
</div>
|
|
||||||
<h1>高可用学习平台</h1>
|
|
||||||
<p class="subtitle">{{ _('welcome_back') }}</p>
|
|
||||||
|
|
||||||
<div class="tab-container">
|
|
||||||
<div class="tab active" id="account-tab">{{ _('account_login') }}</div>
|
|
||||||
<div class="tab" id="qr-tab">{{ _('qr_login') }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="account-login">
|
|
||||||
<form id="login-form" method="post" action="{{ url_for('auth.login') }}">
|
|
||||||
{{ form.hidden_tag() }}
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="email">{{ _('email') }}</label>
|
|
||||||
<div class="input-with-icon">
|
|
||||||
<span class="input-icon">📧</span>
|
|
||||||
{{ form.email(class="form-control", placeholder=_('email_placeholder')) }}
|
|
||||||
</div>
|
|
||||||
{% for error in form.email.errors %}
|
|
||||||
<div class="validation-message show">{{ error }}</div>
|
|
||||||
{% endfor %}
|
|
||||||
<div class="validation-message" id="email-error"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password">{{ _('password') }}</label>
|
|
||||||
<div class="input-with-icon">
|
|
||||||
<span class="input-icon">🔒</span>
|
|
||||||
{{ form.password(class="form-control", placeholder=_('password_placeholder')) }}
|
|
||||||
<span class="password-toggle" id="password-toggle">👁️</span>
|
|
||||||
</div>
|
|
||||||
{% for error in form.password.errors %}
|
|
||||||
<div class="validation-message show">{{ error }}</div>
|
|
||||||
{% endfor %}
|
|
||||||
<div class="validation-message" id="password-error"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="remember-forgot">
|
|
||||||
<label class="custom-checkbox">
|
|
||||||
{{ form.remember_me(type="checkbox") }}
|
|
||||||
<span class="checkmark"></span>
|
|
||||||
{{ _('remember_me') }}
|
|
||||||
</label>
|
|
||||||
<div class="forgot-password">
|
|
||||||
<a href="{{ url_for('auth.reset_password_request') }}">{{ _('forgot_password') }}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn-login" id="login-button">
|
|
||||||
<span>{{ _('login') }}</span>
|
|
||||||
<span class="loading">⟳</span>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="divider">
|
|
||||||
<span>{{ _('other_login_methods') }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="social-login">
|
|
||||||
<div class="social-btn wechat" title="{{ _('wechat_login') }}">
|
|
||||||
<img src="/api/placeholder/25/25" alt="WeChat">
|
|
||||||
</div>
|
|
||||||
<div class="social-btn qq" title="{{ _('qq_login') }}">
|
|
||||||
<img src="/api/placeholder/25/25" alt="QQ">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="signup">
|
|
||||||
{{ _('no_account') }} <a href="{{ url_for('auth.register') }}">{{ _('register') }}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="qr-code-login" style="display: none;">
|
|
||||||
<div class="qr-code">
|
|
||||||
<img src="/api/placeholder/180/180" alt="QR Code">
|
|
||||||
</div>
|
|
||||||
<p class="qr-code-tip">{{ _('scan_qr_code') }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<p>© 2025 顺钦毕业论文_高可用学习平台 | <a href="#">{{ _('privacy_policy') }}</a> | <a href="#">{{ _('terms_of_service') }}</a></p>
|
|
||||||
</footer>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block extra_js %}
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// 主题切换
|
|
||||||
const themeToggle = document.getElementById('theme-toggle');
|
|
||||||
|
|
||||||
themeToggle.addEventListener('click', function() {
|
|
||||||
const currentTheme = "{{ session.get('theme', 'light') }}";
|
|
||||||
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
|
||||||
window.location.href = "{{ url_for('main.set_theme', theme='') }}" + newTheme;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 语言选择器
|
|
||||||
const languageSelect = document.getElementById('language-select');
|
|
||||||
languageSelect.addEventListener('change', function() {
|
|
||||||
window.location.href = "{{ url_for('main.set_language', lang='') }}" + this.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 密码可见性切换
|
|
||||||
const passwordInput = document.getElementById('password');
|
|
||||||
const passwordToggle = document.getElementById('password-toggle');
|
|
||||||
|
|
||||||
if (passwordToggle && passwordInput) {
|
|
||||||
passwordToggle.addEventListener('click', function() {
|
|
||||||
const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
|
|
||||||
passwordInput.setAttribute('type', type);
|
|
||||||
passwordToggle.innerHTML = type === 'password' ? '👁️' : '👁️🗨️';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 账号/扫码登录切换
|
|
||||||
const accountTab = document.getElementById('account-tab');
|
|
||||||
const qrTab = document.getElementById('qr-tab');
|
|
||||||
const accountLogin = document.getElementById('account-login');
|
|
||||||
const qrCodeLogin = document.getElementById('qr-code-login');
|
|
||||||
|
|
||||||
if (accountTab && qrTab && accountLogin && qrCodeLogin) {
|
|
||||||
accountTab.addEventListener('click', function() {
|
|
||||||
accountTab.classList.add('active');
|
|
||||||
qrTab.classList.remove('active');
|
|
||||||
accountLogin.style.display = 'block';
|
|
||||||
qrCodeLogin.style.display = 'none';
|
|
||||||
});
|
|
||||||
|
|
||||||
qrTab.addEventListener('click', function() {
|
|
||||||
qrTab.classList.add('active');
|
|
||||||
accountTab.classList.remove('active');
|
|
||||||
qrCodeLogin.style.display = 'block';
|
|
||||||
accountLogin.style.display = 'none';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 表单验证
|
|
||||||
const loginForm = document.getElementById('login-form');
|
|
||||||
const emailInput = document.getElementById('email');
|
|
||||||
const emailError = document.getElementById('email-error');
|
|
||||||
const passwordError = document.getElementById('password-error');
|
|
||||||
const loginButton = document.getElementById('login-button');
|
|
||||||
|
|
||||||
if (loginForm && emailInput && passwordInput) {
|
|
||||||
emailInput.addEventListener('input', validateEmail);
|
|
||||||
passwordInput.addEventListener('input', validatePassword);
|
|
||||||
|
|
||||||
loginForm.addEventListener('submit', function(e) {
|
|
||||||
if (!validateEmail() || !validatePassword()) {
|
|
||||||
e.preventDefault();
|
|
||||||
} else {
|
|
||||||
loginButton.classList.add('loading-state');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateEmail() {
|
|
||||||
if (!emailInput || !emailError) return true;
|
|
||||||
|
|
||||||
const value = emailInput.value.trim();
|
|
||||||
if (value === '') {
|
|
||||||
emailError.textContent = '{{ _("email_required") }}';
|
|
||||||
emailError.classList.add('show');
|
|
||||||
return false;
|
|
||||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
||||||
emailError.textContent = '{{ _("invalid_email") }}';
|
|
||||||
emailError.classList.add('show');
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
emailError.classList.remove('show');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function validatePassword() {
|
|
||||||
if (!passwordInput || !passwordError) return true;
|
|
||||||
|
|
||||||
const value = passwordInput.value;
|
|
||||||
if (value === '') {
|
|
||||||
passwordError.textContent = '{{ _("password_required") }}';
|
|
||||||
passwordError.classList.add('show');
|
|
||||||
return false;
|
|
||||||
} else if (value.length < 8) {
|
|
||||||
passwordError.textContent = '{{ _("password_too_short") }}';
|
|
||||||
passwordError.classList.add('show');
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
passwordError.classList.remove('show');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
@ -1,479 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}{{ _('register') }} - 高可用学习平台{% endblock %}
|
|
||||||
|
|
||||||
{% block extra_css %}
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/login.css') }}">
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/register.css') }}">
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block full_content %}
|
|
||||||
<div class="overlay"></div>
|
|
||||||
|
|
||||||
<div class="theme-toggle" id="theme-toggle">
|
|
||||||
{% if session.get('theme', 'light') == 'light' %}☀️{% else %}🌙{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="language-selector">
|
|
||||||
<select id="language-select">
|
|
||||||
<option value="zh" {% if session.get('language', 'zh') == 'zh' %}selected{% endif %}>简体中文</option>
|
|
||||||
<option value="en" {% if session.get('language', 'zh') == 'en' %}selected{% endif %}>English</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="main-container">
|
|
||||||
<div class="login-container">
|
|
||||||
<div class="logo">
|
|
||||||
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo" onerror="this.src='/api/placeholder/90/90'">
|
|
||||||
</div>
|
|
||||||
<h1>高可用学习平台</h1>
|
|
||||||
<p class="subtitle">{{ _('create_account') }}</p>
|
|
||||||
|
|
||||||
<div class="step-container">
|
|
||||||
<div class="step {% if not email_verified %}active{% else %}completed{% endif %}" id="step-1">1
|
|
||||||
<div class="step-label">{{ _('email_verification') }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="step {% if email_verified %}active{% endif %}" id="step-2">2
|
|
||||||
<div class="step-label">{{ _('account_info') }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="step" id="step-3">3
|
|
||||||
<div class="step-label">{{ _('registration_success') }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="registration-step {% if not email_verified %}active{% endif %}" id="step-1-content">
|
|
||||||
<form id="email-verification-form" method="post" action="{{ url_for('auth.register') }}">
|
|
||||||
{{ email_form.hidden_tag() }}
|
|
||||||
<input type="hidden" name="send_code" value="1">
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="email">{{ _('email') }}</label>
|
|
||||||
<div class="verification-container">
|
|
||||||
<div class="input-with-icon verification-input">
|
|
||||||
<span class="input-icon">📧</span>
|
|
||||||
{{ email_form.email(class="form-control", placeholder=_('email_placeholder'), id="verification-email") }}
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="send-code-btn" id="send-code-btn">
|
|
||||||
{{ _('send_verification_code') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{% for error in email_form.email.errors %}
|
|
||||||
<div class="validation-message show">{{ error }}</div>
|
|
||||||
{% endfor %}
|
|
||||||
<div class="validation-message" id="email-error"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-buttons">
|
|
||||||
<a href="{{ url_for('auth.login') }}" class="btn btn-login btn-back">{{ _('back_to_login') }}</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="registration-step {% if email_verified %}active{% endif %}" id="step-2-content">
|
|
||||||
<form id="registration-form" method="post" action="{{ url_for('auth.register') }}">
|
|
||||||
{{ registration_form.hidden_tag() }}
|
|
||||||
<input type="hidden" name="register" value="1">
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="email">{{ _('email') }}</label>
|
|
||||||
<div class="input-with-icon">
|
|
||||||
<span class="input-icon">📧</span>
|
|
||||||
{{ registration_form.email(class="form-control", readonly="readonly", value=verified_email) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="verification_code">{{ _('verification_code') }}</label>
|
|
||||||
<div class="input-with-icon">
|
|
||||||
<span class="input-icon">🔑</span>
|
|
||||||
{{ registration_form.verification_code(class="form-control", placeholder=_('enter_verification_code')) }}
|
|
||||||
</div>
|
|
||||||
{% for error in registration_form.verification_code.errors %}
|
|
||||||
<div class="validation-message show">{{ error }}</div>
|
|
||||||
{% endfor %}
|
|
||||||
<div class="validation-message" id="code-error"></div>
|
|
||||||
<div class="countdown" id="resend-countdown"></div>
|
|
||||||
<button type="button" class="btn-link" id="resend-btn" style="display: none; background: none; border: none; color: var(--primary-color); padding: 0; margin-top: 5px; cursor: pointer; text-decoration: underline;">{{ _('resend_code') }}</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="username">{{ _('username') }}</label>
|
|
||||||
<div class="input-with-icon">
|
|
||||||
<span class="input-icon">👤</span>
|
|
||||||
{{ registration_form.username(class="form-control", placeholder=_('enter_username')) }}
|
|
||||||
</div>
|
|
||||||
{% for error in registration_form.username.errors %}
|
|
||||||
<div class="validation-message show">{{ error }}</div>
|
|
||||||
{% endfor %}
|
|
||||||
<div class="validation-message" id="username-error"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password">{{ _('password') }}</label>
|
|
||||||
<div class="input-with-icon">
|
|
||||||
<span class="input-icon">🔒</span>
|
|
||||||
{{ registration_form.password(class="form-control", placeholder=_('enter_password'), id="register-password") }}
|
|
||||||
<span class="password-toggle" id="password-toggle">👁️</span>
|
|
||||||
</div>
|
|
||||||
{% for error in registration_form.password.errors %}
|
|
||||||
<div class="validation-message show">{{ error }}</div>
|
|
||||||
{% endfor %}
|
|
||||||
<div class="validation-message" id="password-error"></div>
|
|
||||||
<div class="strength-meter">
|
|
||||||
<div class="strength-meter-fill" id="strength-meter-fill"></div>
|
|
||||||
</div>
|
|
||||||
<div class="password-strength-text" id="password-strength-text"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password2">{{ _('repeat_password') }}</label>
|
|
||||||
<div class="input-with-icon">
|
|
||||||
<span class="input-icon">🔒</span>
|
|
||||||
{{ registration_form.password2(class="form-control", placeholder=_('repeat_password_placeholder')) }}
|
|
||||||
</div>
|
|
||||||
{% for error in registration_form.password2.errors %}
|
|
||||||
<div class="validation-message show">{{ error }}</div>
|
|
||||||
{% endfor %}
|
|
||||||
<div class="validation-message" id="password2-error"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-buttons">
|
|
||||||
<button type="button" class="btn btn-login btn-back" id="back-to-step1">{{ _('back') }}</button>
|
|
||||||
<button type="submit" class="btn btn-login btn-next">{{ _('register') }}</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<p>© 2023 高可用学习平台 | <a href="#">{{ _('privacy_policy') }}</a> | <a href="#">{{ _('terms_of_service') }}</a></p>
|
|
||||||
</footer>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block extra_js %}
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// 主题切换
|
|
||||||
const themeToggle = document.getElementById('theme-toggle');
|
|
||||||
|
|
||||||
themeToggle.addEventListener('click', function() {
|
|
||||||
const currentTheme = "{{ session.get('theme', 'light') }}";
|
|
||||||
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
|
||||||
window.location.href = "{{ url_for('main.set_theme', theme='') }}" + newTheme + "?next={{ request.path|urlencode }}";
|
|
||||||
});
|
|
||||||
|
|
||||||
// 语言选择器
|
|
||||||
const languageSelect = document.getElementById('language-select');
|
|
||||||
languageSelect.addEventListener('change', function() {
|
|
||||||
window.location.href = "{{ url_for('main.set_language', lang='') }}" + this.value + "?next={{ request.path|urlencode }}";
|
|
||||||
});
|
|
||||||
|
|
||||||
// 密码可见性切换
|
|
||||||
const passwordInput = document.getElementById('register-password');
|
|
||||||
const passwordToggle = document.getElementById('password-toggle');
|
|
||||||
|
|
||||||
if (passwordToggle && passwordInput) {
|
|
||||||
passwordToggle.addEventListener('click', function() {
|
|
||||||
const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
|
|
||||||
passwordInput.setAttribute('type', type);
|
|
||||||
passwordToggle.innerHTML = type === 'password' ? '👁️' : '👁️🗨️';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 密码强度检测
|
|
||||||
if (passwordInput) {
|
|
||||||
const strengthMeter = document.getElementById('strength-meter-fill');
|
|
||||||
const strengthText = document.getElementById('password-strength-text');
|
|
||||||
|
|
||||||
passwordInput.addEventListener('input', function() {
|
|
||||||
const password = this.value;
|
|
||||||
const strength = checkPasswordStrength(password);
|
|
||||||
|
|
||||||
// 更新强度指示器
|
|
||||||
strengthMeter.className = 'strength-meter-fill';
|
|
||||||
|
|
||||||
if (password.length === 0) {
|
|
||||||
strengthMeter.style.width = '0';
|
|
||||||
strengthText.textContent = '';
|
|
||||||
} else if (strength < 2) {
|
|
||||||
strengthMeter.classList.add('weak');
|
|
||||||
strengthText.textContent = '{{ _("password_weak") }}';
|
|
||||||
} else if (strength < 3) {
|
|
||||||
strengthMeter.classList.add('medium');
|
|
||||||
strengthText.textContent = '{{ _("password_medium") }}';
|
|
||||||
} else if (strength < 4) {
|
|
||||||
strengthMeter.classList.add('strong');
|
|
||||||
strengthText.textContent = '{{ _("password_strong") }}';
|
|
||||||
} else {
|
|
||||||
strengthMeter.classList.add('very-strong');
|
|
||||||
strengthText.textContent = '{{ _("password_very_strong") }}';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查密码强度的函数
|
|
||||||
function checkPasswordStrength(password) {
|
|
||||||
let strength = 0;
|
|
||||||
|
|
||||||
if (password.length >= 8) strength++;
|
|
||||||
if (/[a-z]/.test(password)) strength++;
|
|
||||||
if (/[A-Z]/.test(password)) strength++;
|
|
||||||
if (/[0-9]/.test(password)) strength++;
|
|
||||||
if (/[^a-zA-Z0-9]/.test(password)) strength++;
|
|
||||||
|
|
||||||
return strength;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 注册表单验证
|
|
||||||
const registrationForm = document.getElementById('registration-form');
|
|
||||||
const verificationCodeInput = document.getElementById('verification_code');
|
|
||||||
const usernameInput = document.getElementById('username');
|
|
||||||
const password2Input = document.getElementById('password2');
|
|
||||||
|
|
||||||
const codeError = document.getElementById('code-error');
|
|
||||||
const usernameError = document.getElementById('username-error');
|
|
||||||
const password2Error = document.getElementById('password2-error');
|
|
||||||
|
|
||||||
if (registrationForm) {
|
|
||||||
registrationForm.addEventListener('submit', function(e) {
|
|
||||||
let isValid = true;
|
|
||||||
|
|
||||||
// 验证码验证
|
|
||||||
if (verificationCodeInput.value.trim() === '') {
|
|
||||||
codeError.textContent = '{{ _("verification_code_required") }}';
|
|
||||||
codeError.classList.add('show');
|
|
||||||
isValid = false;
|
|
||||||
} else if (verificationCodeInput.value.length !== 6) {
|
|
||||||
codeError.textContent = '{{ _("verification_code_invalid") }}';
|
|
||||||
codeError.classList.add('show');
|
|
||||||
isValid = false;
|
|
||||||
} else {
|
|
||||||
codeError.classList.remove('show');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用户名验证
|
|
||||||
if (usernameInput.value.trim() === '') {
|
|
||||||
usernameError.textContent = '{{ _("username_required") }}';
|
|
||||||
usernameError.classList.add('show');
|
|
||||||
isValid = false;
|
|
||||||
} else {
|
|
||||||
usernameError.classList.remove('show');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 密码匹配验证
|
|
||||||
if (password2Input.value !== passwordInput.value) {
|
|
||||||
password2Error.textContent = '{{ _("passwords_not_match") }}';
|
|
||||||
password2Error.classList.add('show');
|
|
||||||
isValid = false;
|
|
||||||
} else {
|
|
||||||
password2Error.classList.remove('show');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isValid) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 邮箱验证表单
|
|
||||||
const emailVerificationForm = document.getElementById('email-verification-form');
|
|
||||||
const emailInput = document.getElementById('verification-email');
|
|
||||||
const emailError = document.getElementById('email-error');
|
|
||||||
const sendCodeBtn = document.getElementById('send-code-btn');
|
|
||||||
|
|
||||||
if (emailVerificationForm) {
|
|
||||||
emailVerificationForm.addEventListener('submit', function(e) {
|
|
||||||
if (emailInput.value.trim() === '') {
|
|
||||||
e.preventDefault();
|
|
||||||
emailError.textContent = '{{ _("email_required") }}';
|
|
||||||
emailError.classList.add('show');
|
|
||||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailInput.value.trim())) {
|
|
||||||
e.preventDefault();
|
|
||||||
emailError.textContent = '{{ _("invalid_email") }}';
|
|
||||||
emailError.classList.add('show');
|
|
||||||
} else {
|
|
||||||
emailError.classList.remove('show');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 动态发送验证码
|
|
||||||
sendCodeBtn.addEventListener('click', function(e) {
|
|
||||||
if (emailInput.value.trim() === '' || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailInput.value.trim())) {
|
|
||||||
return; // 让表单自己处理验证
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
// 禁用按钮
|
|
||||||
sendCodeBtn.disabled = true;
|
|
||||||
sendCodeBtn.textContent = '{{ _("sending") }}...';
|
|
||||||
|
|
||||||
// 发送Ajax请求
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.open('POST', '{{ url_for("auth.send_verification_code") }}', true);
|
|
||||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
|
||||||
xhr.onreadystatechange = function() {
|
|
||||||
if (xhr.readyState === 4) {
|
|
||||||
if (xhr.status === 200) {
|
|
||||||
try {
|
|
||||||
const response = JSON.parse(xhr.responseText);
|
|
||||||
if (response.success) {
|
|
||||||
// 显示成功消息
|
|
||||||
showNotification(response.message, 'success');
|
|
||||||
|
|
||||||
// 存储邮箱地址
|
|
||||||
const verifiedEmail = emailInput.value.trim();
|
|
||||||
|
|
||||||
// 显示下一步
|
|
||||||
document.getElementById('step-1').classList.remove('active');
|
|
||||||
document.getElementById('step-1').classList.add('completed');
|
|
||||||
document.getElementById('step-2').classList.add('active');
|
|
||||||
|
|
||||||
document.getElementById('step-1-content').classList.remove('active');
|
|
||||||
document.getElementById('step-2-content').classList.add('active');
|
|
||||||
|
|
||||||
// 设置邮箱字段
|
|
||||||
document.getElementById('email').value = verifiedEmail;
|
|
||||||
|
|
||||||
// 启动倒计时
|
|
||||||
startResendCountdown();
|
|
||||||
} else {
|
|
||||||
// 显示错误消息
|
|
||||||
showNotification(response.message, 'error');
|
|
||||||
sendCodeBtn.disabled = false;
|
|
||||||
sendCodeBtn.textContent = '{{ _("send_verification_code") }}';
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
showNotification('{{ _("server_error") }}', 'error');
|
|
||||||
sendCodeBtn.disabled = false;
|
|
||||||
sendCodeBtn.textContent = '{{ _("send_verification_code") }}';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showNotification('{{ _("server_error") }}', 'error');
|
|
||||||
sendCodeBtn.disabled = false;
|
|
||||||
sendCodeBtn.textContent = '{{ _("send_verification_code") }}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xhr.send('email=' + encodeURIComponent(emailInput.value.trim()));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回按钮
|
|
||||||
const backToStep1 = document.getElementById('back-to-step1');
|
|
||||||
if (backToStep1) {
|
|
||||||
backToStep1.addEventListener('click', function() {
|
|
||||||
document.getElementById('step-1').classList.add('active');
|
|
||||||
document.getElementById('step-1').classList.remove('completed');
|
|
||||||
document.getElementById('step-2').classList.remove('active');
|
|
||||||
|
|
||||||
document.getElementById('step-1-content').classList.add('active');
|
|
||||||
document.getElementById('step-2-content').classList.remove('active');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重发验证码
|
|
||||||
const resendBtn = document.getElementById('resend-btn');
|
|
||||||
const resendCountdown = document.getElementById('resend-countdown');
|
|
||||||
|
|
||||||
if (resendBtn) {
|
|
||||||
resendBtn.addEventListener('click', function() {
|
|
||||||
// 禁用按钮
|
|
||||||
resendBtn.style.display = 'none';
|
|
||||||
|
|
||||||
// 发送Ajax请求
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.open('POST', '{{ url_for("auth.send_verification_code") }}', true);
|
|
||||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
|
||||||
xhr.onreadystatechange = function() {
|
|
||||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
|
||||||
try {
|
|
||||||
const response = JSON.parse(xhr.responseText);
|
|
||||||
if (response.success) {
|
|
||||||
showNotification(response.message, 'success');
|
|
||||||
startResendCountdown();
|
|
||||||
} else {
|
|
||||||
showNotification(response.message, 'error');
|
|
||||||
resendBtn.style.display = 'block';
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
showNotification('{{ _("server_error") }}', 'error');
|
|
||||||
resendBtn.style.display = 'block';
|
|
||||||
}
|
|
||||||
} else if (xhr.readyState === 4) {
|
|
||||||
showNotification('{{ _("server_error") }}', 'error');
|
|
||||||
resendBtn.style.display = 'block';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xhr.send('email=' + encodeURIComponent(document.getElementById('email').value));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证码倒计时
|
|
||||||
function startResendCountdown() {
|
|
||||||
let seconds = 60;
|
|
||||||
resendCountdown.textContent = `{{ _("resend_code_in") }} ${seconds} {{ _("seconds") }}`;
|
|
||||||
resendCountdown.style.display = 'block';
|
|
||||||
resendBtn.style.display = 'none';
|
|
||||||
|
|
||||||
const interval = setInterval(function() {
|
|
||||||
seconds--;
|
|
||||||
resendCountdown.textContent = `{{ _("resend_code_in") }} ${seconds} {{ _("seconds") }}`;
|
|
||||||
|
|
||||||
if (seconds <= 0) {
|
|
||||||
clearInterval(interval);
|
|
||||||
resendCountdown.style.display = 'none';
|
|
||||||
resendBtn.style.display = 'block';
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示通知
|
|
||||||
function showNotification(message, type) {
|
|
||||||
// 检查是否已有通知
|
|
||||||
let notification = document.querySelector('.notification');
|
|
||||||
if (notification) {
|
|
||||||
document.body.removeChild(notification);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建新通知
|
|
||||||
notification = document.createElement('div');
|
|
||||||
notification.className = `notification ${type}`;
|
|
||||||
|
|
||||||
const icon = type === 'success' ? '✓' : '✗';
|
|
||||||
|
|
||||||
notification.innerHTML = `
|
|
||||||
<div class="notification-icon">${icon}</div>
|
|
||||||
<div class="notification-content">
|
|
||||||
<p>${message}</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.body.appendChild(notification);
|
|
||||||
|
|
||||||
// 显示通知
|
|
||||||
setTimeout(() => {
|
|
||||||
notification.classList.add('show');
|
|
||||||
}, 10);
|
|
||||||
|
|
||||||
// 自动隐藏
|
|
||||||
setTimeout(() => {
|
|
||||||
notification.classList.remove('show');
|
|
||||||
setTimeout(() => {
|
|
||||||
try {
|
|
||||||
document.body.removeChild(notification);
|
|
||||||
} catch (e) {
|
|
||||||
// 忽略可能的错误
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果是第二步,启动倒计时
|
|
||||||
{% if email_verified %}
|
|
||||||
startResendCountdown();
|
|
||||||
{% endif %}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
@ -1,34 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}{{ _('reset_password') }} - 高可用学习平台{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="auth-container">
|
|
||||||
<div class="auth-form">
|
|
||||||
<h2>{{ _('reset_password') }}</h2>
|
|
||||||
<form method="post" action="{{ url_for('auth.reset_password', token=request.view_args['token']) }}">
|
|
||||||
{{ form.hidden_tag() }}
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
{{ form.password.label(class="form-label") }}
|
|
||||||
{{ form.password(class="form-control") }}
|
|
||||||
{% for error in form.password.errors %}
|
|
||||||
<span class="error">{{ error }}</span>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
{{ form.password2.label(class="form-label") }}
|
|
||||||
{{ form.password2(class="form-control") }}
|
|
||||||
{% for error in form.password2.errors %}
|
|
||||||
<span class="error">{{ error }}</span>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
{{ form.submit(class="btn btn-primary") }}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -1,27 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}{{ _('reset_password') }} - 高可用学习平台{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="auth-container">
|
|
||||||
<div class="auth-form">
|
|
||||||
<h2>{{ _('reset_password') }}</h2>
|
|
||||||
<form method="post" action="{{ url_for('auth.reset_password_request') }}">
|
|
||||||
{{ form.hidden_tag() }}
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
{{ form.email.label(class="form-label") }}
|
|
||||||
{{ form.email(class="form-control") }}
|
|
||||||
{% for error in form.email.errors %}
|
|
||||||
<span class="error">{{ error }}</span>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
{{ form.submit(class="btn btn-primary") }}
|
|
||||||
<a href="{{ url_for('auth.login') }}">{{ _('login') }}</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -1,83 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="{{ session.get('language', 'zh') }}">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>{% block title %}高可用学习平台{% endblock %}</title>
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
|
|
||||||
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--primary-color: #4a89dc;
|
|
||||||
--primary-hover: #3b78c4;
|
|
||||||
--secondary-color: #5cb85c;
|
|
||||||
--text-color: #333;
|
|
||||||
--light-text: #666;
|
|
||||||
--bg-color: #f5f7fa;
|
|
||||||
--card-bg: #ffffff;
|
|
||||||
--border-color: #ddd;
|
|
||||||
--error-color: #e74c3c;
|
|
||||||
--success-color: #2ecc71;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.theme-dark {
|
|
||||||
--primary-color: #5a9aed;
|
|
||||||
--primary-hover: #4a89dc;
|
|
||||||
--secondary-color: #6bc76b;
|
|
||||||
--text-color: #f1f1f1;
|
|
||||||
--light-text: #aaa;
|
|
||||||
--bg-color: #1a1a1a;
|
|
||||||
--card-bg: #2c2c2c;
|
|
||||||
--border-color: #444;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
|
||||||
|
|
||||||
{% if session.get('theme', 'light') == 'dark' %}
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/dark.css') }}" id="theme-css">
|
|
||||||
{% else %}
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/light.css') }}" id="theme-css">
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% block extra_css %}{% endblock %}
|
|
||||||
</head>
|
|
||||||
<body class="theme-{{ session.get('theme', 'light') }}">
|
|
||||||
{% block full_content %}
|
|
||||||
<div class="container">
|
|
||||||
{% if not hide_language_switch|default(false) %}
|
|
||||||
<div class="language-switch">
|
|
||||||
<a href="{{ url_for('main.set_language', lang='zh', next=request.path) }}">中文</a> |
|
|
||||||
<a href="{{ url_for('main.set_language', lang='en', next=request.path) }}">English</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if not hide_theme_switch|default(false) %}
|
|
||||||
<div class="theme-switch">
|
|
||||||
{% if session.get('theme', 'light') == 'light' %}
|
|
||||||
<a href="{{ url_for('main.set_theme', theme='dark', next=request.path) }}">暗色模式</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="{{ url_for('main.set_theme', theme='light', next=request.path) }}">亮色模式</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% with messages = get_flashed_messages() %}
|
|
||||||
{% if messages %}
|
|
||||||
<div class="flashes">
|
|
||||||
{% for message in messages %}
|
|
||||||
<div class="alert alert-info">{{ message }}</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
|
|
||||||
{% block content %}{% endblock %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
|
||||||
{% block extra_js %}{% endblock %}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,65 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}{{ _('my_dashboard') }} - 高可用学习平台{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="dashboard-container">
|
|
||||||
<header class="dashboard-header">
|
|
||||||
<h1>{{ _('welcome') }}</h1>
|
|
||||||
<div class="user-info">
|
|
||||||
<span>{{ current_user.username }}</span>
|
|
||||||
<a href="{{ url_for('auth.logout') }}" class="btn btn-sm btn-outline-danger">{{ _('logout') }}</a>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="services-grid">
|
|
||||||
<div class="service-card">
|
|
||||||
<h3>{{ _('network_drive') }}</h3>
|
|
||||||
<p>安全存储和共享文件</p>
|
|
||||||
<a href="https://pan.sq0715.com" class="btn btn-primary" target="_blank">访问网盘</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="service-card">
|
|
||||||
<h3>{{ _('email_system') }}</h3>
|
|
||||||
<p>您的专属邮箱系统</p>
|
|
||||||
<a href="https://mail.sq0715.com" class="btn btn-primary" target="_blank">访问邮箱</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="service-card">
|
|
||||||
<h3>{{ _('code_compiler') }}</h3>
|
|
||||||
<p>在线编译和运行代码</p>
|
|
||||||
<a href="#" class="btn btn-primary" target="_blank">访问编译器</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="service-card">
|
|
||||||
<h3>{{ _('file_submission') }}</h3>
|
|
||||||
<p>提交和管理作业文件</p>
|
|
||||||
<a href="#" class="btn btn-primary" target="_blank">访问提交系统</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="service-card">
|
|
||||||
<h3>{{ _('video_platform') }}</h3>
|
|
||||||
<p>观看和分享视频内容</p>
|
|
||||||
<a href="#" class="btn btn-primary" target="_blank">访问视频平台</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="service-card">
|
|
||||||
<h3>{{ _('blog_space') }}</h3>
|
|
||||||
<p>发布和阅读博客文章</p>
|
|
||||||
<a href="#" class="btn btn-primary" target="_blank">访问博客</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="service-card">
|
|
||||||
<h3>{{ _('ai_platform') }}</h3>
|
|
||||||
<p>AI助手和工具</p>
|
|
||||||
<a href="https://sqai.online" class="btn btn-primary" target="_blank">访问AI平台</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="service-card">
|
|
||||||
<h3>{{ _('code_repository') }}</h3>
|
|
||||||
<p>代码版本控制</p>
|
|
||||||
<a href="https://git.sq0715.com" class="btn btn-primary" target="_blank">访问代码仓库</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
Loading…
x
Reference in New Issue
Block a user