Merge branch 'recovery'
sumkim
This commit is contained in:
commit
5cef0065ad
@ -0,0 +1,37 @@
|
||||
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
|
@ -0,0 +1,8 @@
|
||||
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'
|
87
app/forms.py
87
app/forms.py
@ -0,0 +1,87 @@
|
||||
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('验证码无效或已过期')
|
@ -0,0 +1,45 @@
|
||||
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
@ -0,0 +1,290 @@
|
||||
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')
|
125
app/translations.py
Normal file
125
app/translations.py
Normal file
@ -0,0 +1,125 @@
|
||||
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)
|
117
app/utils/email.py
Normal file
117
app/utils/email.py
Normal file
@ -0,0 +1,117 @@
|
||||
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
Normal file
21
main.py
Normal file
@ -0,0 +1,21 @@
|
||||
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)
|
@ -0,0 +1,16 @@
|
||||
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
|
72
static/css/dark.css
Normal file
72
static/css/dark.css
Normal file
@ -0,0 +1,72 @@
|
||||
/* 暗色主题 */
|
||||
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;
|
||||
}
|
52
static/css/light.css
Normal file
52
static/css/light.css
Normal file
@ -0,0 +1,52 @@
|
||||
/* 亮色主题 */
|
||||
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;
|
||||
}
|
578
static/css/login.css
Normal file
578
static/css/login.css
Normal file
@ -0,0 +1,578 @@
|
||||
/* 登录页面专用样式 */
|
||||
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);
|
||||
}
|
166
static/css/register.css
Normal file
166
static/css/register.css
Normal file
@ -0,0 +1,166 @@
|
||||
/* 注册页面样式,继承登录页面样式 */
|
||||
.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;
|
||||
}
|
154
static/css/style.css
Normal file
154
static/css/style.css
Normal file
@ -0,0 +1,154 @@
|
||||
/* 基础样式 */
|
||||
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;
|
||||
}
|
78
static/js/main.js
Normal file
78
static/js/main.js
Normal file
@ -0,0 +1,78 @@
|
||||
// 主要功能和交互
|
||||
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);
|
||||
}
|
||||
}
|
241
study/bin/Activate.ps1
Normal file
241
study/bin/Activate.ps1
Normal file
@ -0,0 +1,241 @@
|
||||
<#
|
||||
.Synopsis
|
||||
Activate a Python virtual environment for the current PowerShell session.
|
||||
|
||||
.Description
|
||||
Pushes the python executable for a virtual environment to the front of the
|
||||
$Env:PATH environment variable and sets the prompt to signify that you are
|
||||
in a Python virtual environment. Makes use of the command line switches as
|
||||
well as the `pyvenv.cfg` file values present in the virtual environment.
|
||||
|
||||
.Parameter VenvDir
|
||||
Path to the directory that contains the virtual environment to activate. The
|
||||
default value for this is the parent of the directory that the Activate.ps1
|
||||
script is located within.
|
||||
|
||||
.Parameter Prompt
|
||||
The prompt prefix to display when this virtual environment is activated. By
|
||||
default, this prompt is the name of the virtual environment folder (VenvDir)
|
||||
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
|
||||
|
||||
.Example
|
||||
Activate.ps1
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -Verbose
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||
and shows extra information about the activation as it executes.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
|
||||
Activates the Python virtual environment located in the specified location.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -Prompt "MyPython"
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||
and prefixes the current prompt with the specified string (surrounded in
|
||||
parentheses) while the virtual environment is active.
|
||||
|
||||
.Notes
|
||||
On Windows, it may be required to enable this Activate.ps1 script by setting the
|
||||
execution policy for the user. You can do this by issuing the following PowerShell
|
||||
command:
|
||||
|
||||
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
|
||||
For more information on Execution Policies:
|
||||
https://go.microsoft.com/fwlink/?LinkID=135170
|
||||
|
||||
#>
|
||||
Param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[String]
|
||||
$VenvDir,
|
||||
[Parameter(Mandatory = $false)]
|
||||
[String]
|
||||
$Prompt
|
||||
)
|
||||
|
||||
<# Function declarations --------------------------------------------------- #>
|
||||
|
||||
<#
|
||||
.Synopsis
|
||||
Remove all shell session elements added by the Activate script, including the
|
||||
addition of the virtual environment's Python executable from the beginning of
|
||||
the PATH variable.
|
||||
|
||||
.Parameter NonDestructive
|
||||
If present, do not remove this function from the global namespace for the
|
||||
session.
|
||||
|
||||
#>
|
||||
function global:deactivate ([switch]$NonDestructive) {
|
||||
# Revert to original values
|
||||
|
||||
# The prior prompt:
|
||||
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
|
||||
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
|
||||
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
|
||||
}
|
||||
|
||||
# The prior PYTHONHOME:
|
||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
|
||||
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
|
||||
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
|
||||
}
|
||||
|
||||
# The prior PATH:
|
||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
|
||||
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
|
||||
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
|
||||
}
|
||||
|
||||
# Just remove the VIRTUAL_ENV altogether:
|
||||
if (Test-Path -Path Env:VIRTUAL_ENV) {
|
||||
Remove-Item -Path env:VIRTUAL_ENV
|
||||
}
|
||||
|
||||
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
|
||||
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
|
||||
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
|
||||
}
|
||||
|
||||
# Leave deactivate function in the global namespace if requested:
|
||||
if (-not $NonDestructive) {
|
||||
Remove-Item -Path function:deactivate
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.Description
|
||||
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
|
||||
given folder, and returns them in a map.
|
||||
|
||||
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
|
||||
two strings separated by `=` (with any amount of whitespace surrounding the =)
|
||||
then it is considered a `key = value` line. The left hand string is the key,
|
||||
the right hand is the value.
|
||||
|
||||
If the value starts with a `'` or a `"` then the first and last character is
|
||||
stripped from the value before being captured.
|
||||
|
||||
.Parameter ConfigDir
|
||||
Path to the directory that contains the `pyvenv.cfg` file.
|
||||
#>
|
||||
function Get-PyVenvConfig(
|
||||
[String]
|
||||
$ConfigDir
|
||||
) {
|
||||
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
|
||||
|
||||
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
|
||||
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
|
||||
|
||||
# An empty map will be returned if no config file is found.
|
||||
$pyvenvConfig = @{ }
|
||||
|
||||
if ($pyvenvConfigPath) {
|
||||
|
||||
Write-Verbose "File exists, parse `key = value` lines"
|
||||
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
|
||||
|
||||
$pyvenvConfigContent | ForEach-Object {
|
||||
$keyval = $PSItem -split "\s*=\s*", 2
|
||||
if ($keyval[0] -and $keyval[1]) {
|
||||
$val = $keyval[1]
|
||||
|
||||
# Remove extraneous quotations around a string value.
|
||||
if ("'""".Contains($val.Substring(0, 1))) {
|
||||
$val = $val.Substring(1, $val.Length - 2)
|
||||
}
|
||||
|
||||
$pyvenvConfig[$keyval[0]] = $val
|
||||
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
|
||||
}
|
||||
}
|
||||
}
|
||||
return $pyvenvConfig
|
||||
}
|
||||
|
||||
|
||||
<# Begin Activate script --------------------------------------------------- #>
|
||||
|
||||
# Determine the containing directory of this script
|
||||
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
$VenvExecDir = Get-Item -Path $VenvExecPath
|
||||
|
||||
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
|
||||
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
|
||||
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
|
||||
|
||||
# Set values required in priority: CmdLine, ConfigFile, Default
|
||||
# First, get the location of the virtual environment, it might not be
|
||||
# VenvExecDir if specified on the command line.
|
||||
if ($VenvDir) {
|
||||
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
|
||||
}
|
||||
else {
|
||||
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
|
||||
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
|
||||
Write-Verbose "VenvDir=$VenvDir"
|
||||
}
|
||||
|
||||
# Next, read the `pyvenv.cfg` file to determine any required value such
|
||||
# as `prompt`.
|
||||
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
|
||||
|
||||
# Next, set the prompt from the command line, or the config file, or
|
||||
# just use the name of the virtual environment folder.
|
||||
if ($Prompt) {
|
||||
Write-Verbose "Prompt specified as argument, using '$Prompt'"
|
||||
}
|
||||
else {
|
||||
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
|
||||
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
|
||||
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
|
||||
$Prompt = $pyvenvCfg['prompt'];
|
||||
}
|
||||
else {
|
||||
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virutal environment)"
|
||||
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
|
||||
$Prompt = Split-Path -Path $venvDir -Leaf
|
||||
}
|
||||
}
|
||||
|
||||
Write-Verbose "Prompt = '$Prompt'"
|
||||
Write-Verbose "VenvDir='$VenvDir'"
|
||||
|
||||
# Deactivate any currently active virtual environment, but leave the
|
||||
# deactivate function in place.
|
||||
deactivate -nondestructive
|
||||
|
||||
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
|
||||
# that there is an activated venv.
|
||||
$env:VIRTUAL_ENV = $VenvDir
|
||||
|
||||
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
|
||||
|
||||
Write-Verbose "Setting prompt to '$Prompt'"
|
||||
|
||||
# Set the prompt to include the env name
|
||||
# Make sure _OLD_VIRTUAL_PROMPT is global
|
||||
function global:_OLD_VIRTUAL_PROMPT { "" }
|
||||
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
|
||||
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
|
||||
|
||||
function global:prompt {
|
||||
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
|
||||
_OLD_VIRTUAL_PROMPT
|
||||
}
|
||||
}
|
||||
|
||||
# Clear PYTHONHOME
|
||||
if (Test-Path -Path Env:PYTHONHOME) {
|
||||
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
|
||||
Remove-Item -Path Env:PYTHONHOME
|
||||
}
|
||||
|
||||
# Add the venv to the PATH
|
||||
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
|
||||
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
|
66
study/bin/activate
Normal file
66
study/bin/activate
Normal file
@ -0,0 +1,66 @@
|
||||
# This file must be used with "source bin/activate" *from bash*
|
||||
# you cannot run it directly
|
||||
|
||||
deactivate () {
|
||||
# reset old environment variables
|
||||
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
|
||||
PATH="${_OLD_VIRTUAL_PATH:-}"
|
||||
export PATH
|
||||
unset _OLD_VIRTUAL_PATH
|
||||
fi
|
||||
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
|
||||
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
|
||||
export PYTHONHOME
|
||||
unset _OLD_VIRTUAL_PYTHONHOME
|
||||
fi
|
||||
|
||||
# This should detect bash and zsh, which have a hash command that must
|
||||
# be called to get it to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
|
||||
hash -r 2> /dev/null
|
||||
fi
|
||||
|
||||
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
||||
PS1="${_OLD_VIRTUAL_PS1:-}"
|
||||
export PS1
|
||||
unset _OLD_VIRTUAL_PS1
|
||||
fi
|
||||
|
||||
unset VIRTUAL_ENV
|
||||
if [ ! "${1:-}" = "nondestructive" ] ; then
|
||||
# Self destruct!
|
||||
unset -f deactivate
|
||||
fi
|
||||
}
|
||||
|
||||
# unset irrelevant variables
|
||||
deactivate nondestructive
|
||||
|
||||
VIRTUAL_ENV="/Users/lishunqin/Desktop/ziyao/study_platform/study"
|
||||
export VIRTUAL_ENV
|
||||
|
||||
_OLD_VIRTUAL_PATH="$PATH"
|
||||
PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
export PATH
|
||||
|
||||
# unset PYTHONHOME if set
|
||||
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
|
||||
# could use `if (set -u; : $PYTHONHOME) ;` in bash
|
||||
if [ -n "${PYTHONHOME:-}" ] ; then
|
||||
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
|
||||
unset PYTHONHOME
|
||||
fi
|
||||
|
||||
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
||||
_OLD_VIRTUAL_PS1="${PS1:-}"
|
||||
PS1="(study) ${PS1:-}"
|
||||
export PS1
|
||||
fi
|
||||
|
||||
# This should detect bash and zsh, which have a hash command that must
|
||||
# be called to get it to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
|
||||
hash -r 2> /dev/null
|
||||
fi
|
25
study/bin/activate.csh
Normal file
25
study/bin/activate.csh
Normal file
@ -0,0 +1,25 @@
|
||||
# This file must be used with "source bin/activate.csh" *from csh*.
|
||||
# You cannot run it directly.
|
||||
# Created by Davide Di Blasi <davidedb@gmail.com>.
|
||||
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
|
||||
|
||||
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; test "\!:*" != "nondestructive" && unalias deactivate'
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
setenv VIRTUAL_ENV "/Users/lishunqin/Desktop/ziyao/study_platform/study"
|
||||
|
||||
set _OLD_VIRTUAL_PATH="$PATH"
|
||||
setenv PATH "$VIRTUAL_ENV/bin:$PATH"
|
||||
|
||||
|
||||
set _OLD_VIRTUAL_PROMPT="$prompt"
|
||||
|
||||
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
|
||||
set prompt = "(study) $prompt"
|
||||
endif
|
||||
|
||||
alias pydoc python -m pydoc
|
||||
|
||||
rehash
|
64
study/bin/activate.fish
Normal file
64
study/bin/activate.fish
Normal file
@ -0,0 +1,64 @@
|
||||
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
|
||||
# (https://fishshell.com/); you cannot run it directly.
|
||||
|
||||
function deactivate -d "Exit virtual environment and return to normal shell environment"
|
||||
# reset old environment variables
|
||||
if test -n "$_OLD_VIRTUAL_PATH"
|
||||
set -gx PATH $_OLD_VIRTUAL_PATH
|
||||
set -e _OLD_VIRTUAL_PATH
|
||||
end
|
||||
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
|
||||
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
|
||||
set -e _OLD_VIRTUAL_PYTHONHOME
|
||||
end
|
||||
|
||||
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
|
||||
functions -e fish_prompt
|
||||
set -e _OLD_FISH_PROMPT_OVERRIDE
|
||||
functions -c _old_fish_prompt fish_prompt
|
||||
functions -e _old_fish_prompt
|
||||
end
|
||||
|
||||
set -e VIRTUAL_ENV
|
||||
if test "$argv[1]" != "nondestructive"
|
||||
# Self-destruct!
|
||||
functions -e deactivate
|
||||
end
|
||||
end
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
set -gx VIRTUAL_ENV "/Users/lishunqin/Desktop/ziyao/study_platform/study"
|
||||
|
||||
set -gx _OLD_VIRTUAL_PATH $PATH
|
||||
set -gx PATH "$VIRTUAL_ENV/bin" $PATH
|
||||
|
||||
# Unset PYTHONHOME if set.
|
||||
if set -q PYTHONHOME
|
||||
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
|
||||
set -e PYTHONHOME
|
||||
end
|
||||
|
||||
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
|
||||
# fish uses a function instead of an env var to generate the prompt.
|
||||
|
||||
# Save the current fish_prompt function as the function _old_fish_prompt.
|
||||
functions -c fish_prompt _old_fish_prompt
|
||||
|
||||
# With the original prompt function renamed, we can override with our own.
|
||||
function fish_prompt
|
||||
# Save the return status of the last command.
|
||||
set -l old_status $status
|
||||
|
||||
# Output the venv prompt; color taken from the blue of the Python logo.
|
||||
printf "%s%s%s" (set_color 4B8BBE) "(study) " (set_color normal)
|
||||
|
||||
# Restore the return status of the previous command.
|
||||
echo "exit $old_status" | .
|
||||
# Output the original/"old" prompt.
|
||||
_old_fish_prompt
|
||||
end
|
||||
|
||||
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
|
||||
end
|
8
study/bin/alembic
Executable file
8
study/bin/alembic
Executable file
@ -0,0 +1,8 @@
|
||||
#!/Users/lishunqin/Desktop/ziyao/study_platform/study/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from alembic.config import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
8
study/bin/dotenv
Executable file
8
study/bin/dotenv
Executable file
@ -0,0 +1,8 @@
|
||||
#!/Users/lishunqin/Desktop/ziyao/study_platform/study/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from dotenv.cli import cli
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(cli())
|
8
study/bin/email_validator
Executable file
8
study/bin/email_validator
Executable file
@ -0,0 +1,8 @@
|
||||
#!/Users/lishunqin/Desktop/ziyao/study_platform/study/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from email_validator import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
8
study/bin/flask
Executable file
8
study/bin/flask
Executable file
@ -0,0 +1,8 @@
|
||||
#!/Users/lishunqin/Desktop/ziyao/study_platform/study/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from flask.cli import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
8
study/bin/mako-render
Executable file
8
study/bin/mako-render
Executable file
@ -0,0 +1,8 @@
|
||||
#!/Users/lishunqin/Desktop/ziyao/study_platform/study/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from mako.cmd import cmdline
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(cmdline())
|
8
study/bin/pip
Executable file
8
study/bin/pip
Executable file
@ -0,0 +1,8 @@
|
||||
#!/Users/lishunqin/Desktop/ziyao/study_platform/study/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
8
study/bin/pip3
Executable file
8
study/bin/pip3
Executable file
@ -0,0 +1,8 @@
|
||||
#!/Users/lishunqin/Desktop/ziyao/study_platform/study/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
8
study/bin/pip3.9
Executable file
8
study/bin/pip3.9
Executable file
@ -0,0 +1,8 @@
|
||||
#!/Users/lishunqin/Desktop/ziyao/study_platform/study/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
8
study/bin/pybabel
Executable file
8
study/bin/pybabel
Executable file
@ -0,0 +1,8 @@
|
||||
#!/Users/lishunqin/Desktop/ziyao/study_platform/study/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from babel.messages.frontend import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
1
study/bin/python
Symbolic link
1
study/bin/python
Symbolic link
@ -0,0 +1 @@
|
||||
python3
|
1
study/bin/python3
Symbolic link
1
study/bin/python3
Symbolic link
@ -0,0 +1 @@
|
||||
/Applications/Xcode.app/Contents/Developer/usr/bin/python3
|
1
study/bin/python3.9
Symbolic link
1
study/bin/python3.9
Symbolic link
@ -0,0 +1 @@
|
||||
python3
|
3
study/pyvenv.cfg
Normal file
3
study/pyvenv.cfg
Normal file
@ -0,0 +1,3 @@
|
||||
home = /Applications/Xcode.app/Contents/Developer/usr/bin
|
||||
include-system-site-packages = false
|
||||
version = 3.9.6
|
223
templates/auth/login.html
Normal file
223
templates/auth/login.html
Normal file
@ -0,0 +1,223 @@
|
||||
{% 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 %}
|
479
templates/auth/register.html
Normal file
479
templates/auth/register.html
Normal file
@ -0,0 +1,479 @@
|
||||
{% 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 %}
|
34
templates/auth/reset_password.html
Normal file
34
templates/auth/reset_password.html
Normal file
@ -0,0 +1,34 @@
|
||||
{% 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 %}
|
27
templates/auth/reset_password_request.html
Normal file
27
templates/auth/reset_password_request.html
Normal file
@ -0,0 +1,27 @@
|
||||
{% 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 %}
|
83
templates/base.html
Normal file
83
templates/base.html
Normal file
@ -0,0 +1,83 @@
|
||||
<!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>
|
65
templates/main/index.html
Normal file
65
templates/main/index.html
Normal file
@ -0,0 +1,65 @@
|
||||
{% 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