Merge branch 'recovery'

sumkim
This commit is contained in:
superlishunqin 2025-04-22 22:50:02 +08:00
commit 5cef0065ad
39 changed files with 3231 additions and 0 deletions

View File

@ -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

View File

@ -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'

View File

@ -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('验证码无效或已过期')

View File

@ -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

View File

@ -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
View 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
View 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
View 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)

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
python3

1
study/bin/python3 Symbolic link
View File

@ -0,0 +1 @@
/Applications/Xcode.app/Contents/Developer/usr/bin/python3

1
study/bin/python3.9 Symbolic link
View File

@ -0,0 +1 @@
python3

3
study/pyvenv.cfg Normal file
View 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
View 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 %}

View 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 %}

View 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 %}

View 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
View 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
View 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 %}