diff --git a/app/__init__.py b/app/__init__.py index e69de29..324f0a4 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -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 \ No newline at end of file diff --git a/app/config.py b/app/config.py index e69de29..379f51b 100644 --- a/app/config.py +++ b/app/config.py @@ -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' diff --git a/app/forms.py b/app/forms.py index e69de29..fd85dea 100644 --- a/app/forms.py +++ b/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('验证码无效或已过期') \ No newline at end of file diff --git a/app/models.py b/app/models.py index e69de29..4d323e5 100644 --- a/app/models.py +++ b/app/models.py @@ -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 diff --git a/app/routes.py b/app/routes.py index e69de29..3834093 100644 --- a/app/routes.py +++ b/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/') +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/') +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/', 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//') +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') \ No newline at end of file diff --git a/app/translations.py b/app/translations.py new file mode 100644 index 0000000..0bde8e5 --- /dev/null +++ b/app/translations.py @@ -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) \ No newline at end of file diff --git a/app/utils.py b/app/utils/__init__.py similarity index 100% rename from app/utils.py rename to app/utils/__init__.py diff --git a/app/utils/email.py b/app/utils/email.py new file mode 100644 index 0000000..bfd35ec --- /dev/null +++ b/app/utils/email.py @@ -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""" +
+

高可用学习平台 - 邮箱验证

+

您好,

+

感谢您注册高可用学习平台。请使用以下验证码完成注册:

+
+ {code} +
+

验证码有效期为10分钟,请尽快完成注册。

+

如果您没有进行注册操作,请忽略此邮件。

+

+ 此邮件由系统自动发送,请勿直接回复。 +

+
+ """ + + 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 \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..a36699a --- /dev/null +++ b/main.py @@ -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) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e69de29..f68a16f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 \ No newline at end of file diff --git a/static/css/dark.css b/static/css/dark.css new file mode 100644 index 0000000..f116803 --- /dev/null +++ b/static/css/dark.css @@ -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; +} \ No newline at end of file diff --git a/static/css/light.css b/static/css/light.css new file mode 100644 index 0000000..f81ca1e --- /dev/null +++ b/static/css/light.css @@ -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; +} \ No newline at end of file diff --git a/static/css/login.css b/static/css/login.css new file mode 100644 index 0000000..7adb1ae --- /dev/null +++ b/static/css/login.css @@ -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); +} \ No newline at end of file diff --git a/static/css/register.css b/static/css/register.css new file mode 100644 index 0000000..a04e582 --- /dev/null +++ b/static/css/register.css @@ -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; +} \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..285a279 --- /dev/null +++ b/static/css/style.css @@ -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; +} \ No newline at end of file diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..41d6ce7 --- /dev/null +++ b/static/js/main.js @@ -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); + } +} \ No newline at end of file diff --git a/study/bin/Activate.ps1 b/study/bin/Activate.ps1 new file mode 100644 index 0000000..2fb3852 --- /dev/null +++ b/study/bin/Activate.ps1 @@ -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" diff --git a/study/bin/activate b/study/bin/activate new file mode 100644 index 0000000..f7a0969 --- /dev/null +++ b/study/bin/activate @@ -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 diff --git a/study/bin/activate.csh b/study/bin/activate.csh new file mode 100644 index 0000000..30d333e --- /dev/null +++ b/study/bin/activate.csh @@ -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 . +# Ported to Python 3.3 venv by Andrew Svetlov + +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 diff --git a/study/bin/activate.fish b/study/bin/activate.fish new file mode 100644 index 0000000..5207515 --- /dev/null +++ b/study/bin/activate.fish @@ -0,0 +1,64 @@ +# This file must be used with "source /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 diff --git a/study/bin/alembic b/study/bin/alembic new file mode 100755 index 0000000..f649864 --- /dev/null +++ b/study/bin/alembic @@ -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()) diff --git a/study/bin/dotenv b/study/bin/dotenv new file mode 100755 index 0000000..7ce6f46 --- /dev/null +++ b/study/bin/dotenv @@ -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()) diff --git a/study/bin/email_validator b/study/bin/email_validator new file mode 100755 index 0000000..c9db5d0 --- /dev/null +++ b/study/bin/email_validator @@ -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()) diff --git a/study/bin/flask b/study/bin/flask new file mode 100755 index 0000000..91bc453 --- /dev/null +++ b/study/bin/flask @@ -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()) diff --git a/study/bin/mako-render b/study/bin/mako-render new file mode 100755 index 0000000..a25da95 --- /dev/null +++ b/study/bin/mako-render @@ -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()) diff --git a/study/bin/pip b/study/bin/pip new file mode 100755 index 0000000..7b3c15f --- /dev/null +++ b/study/bin/pip @@ -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()) diff --git a/study/bin/pip3 b/study/bin/pip3 new file mode 100755 index 0000000..7b3c15f --- /dev/null +++ b/study/bin/pip3 @@ -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()) diff --git a/study/bin/pip3.9 b/study/bin/pip3.9 new file mode 100755 index 0000000..7b3c15f --- /dev/null +++ b/study/bin/pip3.9 @@ -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()) diff --git a/study/bin/pybabel b/study/bin/pybabel new file mode 100755 index 0000000..9c3ec6a --- /dev/null +++ b/study/bin/pybabel @@ -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()) diff --git a/study/bin/python b/study/bin/python new file mode 120000 index 0000000..b8a0adb --- /dev/null +++ b/study/bin/python @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/study/bin/python3 b/study/bin/python3 new file mode 120000 index 0000000..975a95f --- /dev/null +++ b/study/bin/python3 @@ -0,0 +1 @@ +/Applications/Xcode.app/Contents/Developer/usr/bin/python3 \ No newline at end of file diff --git a/study/bin/python3.9 b/study/bin/python3.9 new file mode 120000 index 0000000..b8a0adb --- /dev/null +++ b/study/bin/python3.9 @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/study/pyvenv.cfg b/study/pyvenv.cfg new file mode 100644 index 0000000..8e83703 --- /dev/null +++ b/study/pyvenv.cfg @@ -0,0 +1,3 @@ +home = /Applications/Xcode.app/Contents/Developer/usr/bin +include-system-site-packages = false +version = 3.9.6 diff --git a/templates/auth/login.html b/templates/auth/login.html new file mode 100644 index 0000000..a38441d --- /dev/null +++ b/templates/auth/login.html @@ -0,0 +1,223 @@ +{% extends "base.html" %} + +{% block title %}{{ _('login') }} - 高可用学习平台{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+ +
+ {% if session.get('theme', 'light') == 'light' %}☀️{% else %}🌙{% endif %} +
+ +
+ +
+ +
+ +
+ + +{% endblock %} + +{% block extra_js %} + +{% endblock %} \ No newline at end of file diff --git a/templates/auth/register.html b/templates/auth/register.html new file mode 100644 index 0000000..c40ffd4 --- /dev/null +++ b/templates/auth/register.html @@ -0,0 +1,479 @@ +{% extends "base.html" %} + +{% block title %}{{ _('register') }} - 高可用学习平台{% endblock %} + +{% block extra_css %} + + +{% endblock %} + +{% block full_content %} +
+ +
+ {% if session.get('theme', 'light') == 'light' %}☀️{% else %}🌙{% endif %} +
+ +
+ +
+ +
+ +
+ + +{% endblock %} + +{% block extra_js %} + +{% endblock %} diff --git a/templates/auth/reset_password.html b/templates/auth/reset_password.html new file mode 100644 index 0000000..38ab5b0 --- /dev/null +++ b/templates/auth/reset_password.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} + +{% block title %}{{ _('reset_password') }} - 高可用学习平台{% endblock %} + +{% block content %} +
+
+

{{ _('reset_password') }}

+
+ {{ form.hidden_tag() }} + +
+ {{ form.password.label(class="form-label") }} + {{ form.password(class="form-control") }} + {% for error in form.password.errors %} + {{ error }} + {% endfor %} +
+ +
+ {{ form.password2.label(class="form-label") }} + {{ form.password2(class="form-control") }} + {% for error in form.password2.errors %} + {{ error }} + {% endfor %} +
+ +
+ {{ form.submit(class="btn btn-primary") }} +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/auth/reset_password_request.html b/templates/auth/reset_password_request.html new file mode 100644 index 0000000..039a8f5 --- /dev/null +++ b/templates/auth/reset_password_request.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} + +{% block title %}{{ _('reset_password') }} - 高可用学习平台{% endblock %} + +{% block content %} +
+
+

{{ _('reset_password') }}

+
+ {{ form.hidden_tag() }} + +
+ {{ form.email.label(class="form-label") }} + {{ form.email(class="form-control") }} + {% for error in form.email.errors %} + {{ error }} + {% endfor %} +
+ +
+ {{ form.submit(class="btn btn-primary") }} + {{ _('login') }} +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..de232e2 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,83 @@ + + + + + + {% block title %}高可用学习平台{% endblock %} + + + + + + + {% if session.get('theme', 'light') == 'dark' %} + + {% else %} + + {% endif %} + + {% block extra_css %}{% endblock %} + + + {% block full_content %} +
+ {% if not hide_language_switch|default(false) %} +
+ 中文 | + English +
+ {% endif %} + + {% if not hide_theme_switch|default(false) %} +
+ {% if session.get('theme', 'light') == 'light' %} + 暗色模式 + {% else %} + 亮色模式 + {% endif %} +
+ {% endif %} + + {% with messages = get_flashed_messages() %} + {% if messages %} +
+ {% for message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+ {% endblock %} + + + + {% block extra_js %}{% endblock %} + + \ No newline at end of file diff --git a/templates/main/index.html b/templates/main/index.html new file mode 100644 index 0000000..9f70445 --- /dev/null +++ b/templates/main/index.html @@ -0,0 +1,65 @@ +{% extends "base.html" %} + +{% block title %}{{ _('my_dashboard') }} - 高可用学习平台{% endblock %} + +{% block content %} +
+
+

{{ _('welcome') }}

+ +
+ +
+
+

{{ _('network_drive') }}

+

安全存储和共享文件

+ 访问网盘 +
+ +
+

{{ _('email_system') }}

+

您的专属邮箱系统

+ 访问邮箱 +
+ +
+

{{ _('code_compiler') }}

+

在线编译和运行代码

+ 访问编译器 +
+ +
+

{{ _('file_submission') }}

+

提交和管理作业文件

+ 访问提交系统 +
+ +
+

{{ _('video_platform') }}

+

观看和分享视频内容

+ 访问视频平台 +
+ +
+

{{ _('blog_space') }}

+

发布和阅读博客文章

+ 访问博客 +
+ +
+

{{ _('ai_platform') }}

+

AI助手和工具

+ 访问AI平台 +
+ +
+

{{ _('code_repository') }}

+

代码版本控制

+ 访问代码仓库 +
+
+
+{% endblock %} \ No newline at end of file