develop-progress-1
This commit is contained in:
commit
67d19911b7
21
.env.example
Normal file
21
.env.example
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Flask配置
|
||||||
|
FLASK_ENV=development
|
||||||
|
SECRET_KEY=your-secret-key-here-change-in-production
|
||||||
|
PORT=5000
|
||||||
|
# 数据库配置
|
||||||
|
MYSQL_HOST=119.91.236.167
|
||||||
|
MYSQL_PORT=3306
|
||||||
|
MYSQL_USER=Language_learning
|
||||||
|
MYSQL_PASSWORD=cosyvoice
|
||||||
|
MYSQL_DB=language_learning
|
||||||
|
# 邮箱配置
|
||||||
|
EMAIL_HOST=smtp.qq.com
|
||||||
|
EMAIL_PORT=587
|
||||||
|
EMAIL_ENCRYPTION=starttls
|
||||||
|
EMAIL_USERNAME=3399560459@qq.com
|
||||||
|
EMAIL_PASSWORD=fzwhyirhbqdzcjgf
|
||||||
|
EMAIL_FROM=3399560459@qq.com
|
||||||
|
EMAIL_FROM_NAME=儿童语言学习系统
|
||||||
|
# 其他配置
|
||||||
|
MAX_CONTENT_LENGTH=104857600
|
||||||
|
VERIFICATION_CODE_EXPIRE_MINUTES=5
|
||||||
86
.gitignore
vendored
Normal file
86
.gitignore
vendored
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
# Virtual Environment
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
.venv/
|
||||||
|
.env
|
||||||
|
.ENV/
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
# Flask
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
# Database
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
# Logs
|
||||||
|
logs/*.log
|
||||||
|
*.log
|
||||||
|
# Uploads
|
||||||
|
uploads/
|
||||||
|
tmp/
|
||||||
|
# Docker
|
||||||
|
.dockerignore
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.production
|
||||||
|
# Coverage
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
# Backup files
|
||||||
|
*.bak
|
||||||
|
*.backup
|
||||||
|
*.orig
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
1
.python-version
Normal file
1
.python-version
Normal file
@ -0,0 +1 @@
|
|||||||
|
3.10.16
|
||||||
31
app/__init__.py
Normal file
31
app/__init__.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from flask import Flask
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_login import LoginManager
|
||||||
|
from config import config
|
||||||
|
|
||||||
|
# 初始化扩展
|
||||||
|
db = SQLAlchemy()
|
||||||
|
login_manager = LoginManager()
|
||||||
|
|
||||||
|
def create_app(config_name=None):
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# 加载配置
|
||||||
|
config_name = config_name or 'default'
|
||||||
|
app.config.from_object(config[config_name])
|
||||||
|
|
||||||
|
# 初始化扩展
|
||||||
|
db.init_app(app)
|
||||||
|
login_manager.init_app(app)
|
||||||
|
login_manager.login_view = 'auth.login'
|
||||||
|
login_manager.login_message = '请先登录访问此页面'
|
||||||
|
login_manager.login_message_category = 'info'
|
||||||
|
|
||||||
|
# 注册蓝图
|
||||||
|
from app.routes.auth import auth_bp
|
||||||
|
from app.routes.main import main_bp
|
||||||
|
|
||||||
|
app.register_blueprint(main_bp)
|
||||||
|
app.register_blueprint(auth_bp, url_prefix='/auth')
|
||||||
|
|
||||||
|
return app
|
||||||
85
app/models/__init__.py
Normal file
85
app/models/__init__.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_login import UserMixin
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
from app import db
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
|
class User(UserMixin, db.Model):
|
||||||
|
__tablename__ = 'users'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
email = db.Column(db.String(255), unique=True, nullable=False, index=True)
|
||||||
|
password_hash = db.Column(db.String(255), nullable=False)
|
||||||
|
name = db.Column(db.String(100), nullable=False)
|
||||||
|
age = db.Column(db.SmallInteger, nullable=False)
|
||||||
|
gender = db.Column(db.SmallInteger, nullable=False, comment='0-男, 1-女')
|
||||||
|
parent_contact = db.Column(db.String(255), nullable=True, comment='家长联系方式')
|
||||||
|
is_verified = db.Column(db.Boolean, default=False, comment='邮箱是否验证')
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow, index=True)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
|
||||||
|
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 __repr__(self):
|
||||||
|
return f'<User {self.email}>'
|
||||||
|
|
||||||
|
class EmailVerification(db.Model):
|
||||||
|
__tablename__ = 'email_verifications'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
email = db.Column(db.String(255), nullable=False, index=True)
|
||||||
|
verification_code = db.Column(db.String(6), nullable=False, comment='6位数字验证码')
|
||||||
|
expires_at = db.Column(db.DateTime, nullable=False, index=True)
|
||||||
|
is_used = db.Column(db.Boolean, default=False, comment='是否已使用')
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate_code(cls, email, expire_minutes=5):
|
||||||
|
"""生成验证码"""
|
||||||
|
# 清理过期的验证码
|
||||||
|
cls.query.filter(
|
||||||
|
cls.email == email,
|
||||||
|
cls.expires_at < datetime.utcnow()
|
||||||
|
).delete()
|
||||||
|
|
||||||
|
# 生成6位数字验证码
|
||||||
|
code = ''.join(random.choices(string.digits, k=6))
|
||||||
|
expires_at = datetime.utcnow() + timedelta(minutes=expire_minutes)
|
||||||
|
|
||||||
|
verification = cls(
|
||||||
|
email=email,
|
||||||
|
verification_code=code,
|
||||||
|
expires_at=expires_at
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(verification)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return code
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def verify_code(cls, email, code):
|
||||||
|
"""验证验证码"""
|
||||||
|
verification = cls.query.filter(
|
||||||
|
cls.email == email,
|
||||||
|
cls.verification_code == code,
|
||||||
|
cls.expires_at > datetime.utcnow(),
|
||||||
|
cls.is_used == False
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if verification:
|
||||||
|
verification.is_used = True
|
||||||
|
db.session.commit()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<EmailVerification {self.email}>'
|
||||||
1
app/routes/__init__.py
Normal file
1
app/routes/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# 路由包初始化文件
|
||||||
216
app/routes/auth.py
Normal file
216
app/routes/auth.py
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
from flask import Blueprint, render_template, request, flash, redirect, url_for, jsonify
|
||||||
|
from flask_login import login_user, logout_user, login_required, current_user
|
||||||
|
from app.models import User, EmailVerification
|
||||||
|
from app import db, login_manager
|
||||||
|
from utils import send_verification_email, send_password_reset_email
|
||||||
|
import re
|
||||||
|
|
||||||
|
auth_bp = Blueprint('auth', __name__)
|
||||||
|
|
||||||
|
@login_manager.user_loader
|
||||||
|
def load_user(user_id):
|
||||||
|
return User.query.get(int(user_id))
|
||||||
|
|
||||||
|
@auth_bp.route('/register', methods=['GET', 'POST'])
|
||||||
|
def register():
|
||||||
|
"""用户注册"""
|
||||||
|
if request.method == 'POST':
|
||||||
|
try:
|
||||||
|
# 获取表单数据
|
||||||
|
email = request.form.get('email', '').strip().lower()
|
||||||
|
password = request.form.get('password', '').strip()
|
||||||
|
confirm_password = request.form.get('confirm_password', '').strip()
|
||||||
|
name = request.form.get('name', '').strip()
|
||||||
|
age = request.form.get('age', '').strip()
|
||||||
|
gender = request.form.get('gender', '').strip()
|
||||||
|
parent_contact = request.form.get('parent_contact', '').strip()
|
||||||
|
verification_code = request.form.get('verification_code', '').strip()
|
||||||
|
|
||||||
|
# 验证必填字段
|
||||||
|
if not all([email, password, confirm_password, name, age, gender, verification_code]):
|
||||||
|
flash('请填写所有必填字段', 'error')
|
||||||
|
return render_template('auth/register.html')
|
||||||
|
|
||||||
|
# 邮箱格式验证
|
||||||
|
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||||
|
if not re.match(email_pattern, email):
|
||||||
|
flash('请输入有效的邮箱地址', 'error')
|
||||||
|
return render_template('auth/register.html')
|
||||||
|
|
||||||
|
# 密码验证
|
||||||
|
if len(password) < 6:
|
||||||
|
flash('密码长度至少6位', 'error')
|
||||||
|
return render_template('auth/register.html')
|
||||||
|
|
||||||
|
if password != confirm_password:
|
||||||
|
flash('两次输入的密码不一致', 'error')
|
||||||
|
return render_template('auth/register.html')
|
||||||
|
|
||||||
|
# 验证年龄
|
||||||
|
try:
|
||||||
|
age = int(age)
|
||||||
|
if age < 3 or age > 6:
|
||||||
|
flash('年龄必须在3-6岁之间', 'error')
|
||||||
|
return render_template('auth/register.html')
|
||||||
|
except ValueError:
|
||||||
|
flash('请输入有效的年龄', 'error')
|
||||||
|
return render_template('auth/register.html')
|
||||||
|
|
||||||
|
# 验证性别
|
||||||
|
if gender not in ['0', '1']:
|
||||||
|
flash('请选择性别', 'error')
|
||||||
|
return render_template('auth/register.html')
|
||||||
|
|
||||||
|
# 检查邮箱是否已注册
|
||||||
|
existing_user = User.query.filter_by(email=email).first()
|
||||||
|
if existing_user:
|
||||||
|
flash('该邮箱已被注册', 'error')
|
||||||
|
return render_template('auth/register.html')
|
||||||
|
|
||||||
|
# 验证邮箱验证码
|
||||||
|
if not EmailVerification.verify_code(email, verification_code):
|
||||||
|
flash('验证码错误或已过期', 'error')
|
||||||
|
return render_template('auth/register.html')
|
||||||
|
|
||||||
|
# 创建新用户
|
||||||
|
new_user = User(
|
||||||
|
email=email,
|
||||||
|
name=name,
|
||||||
|
age=age,
|
||||||
|
gender=int(gender),
|
||||||
|
parent_contact=parent_contact if parent_contact else None,
|
||||||
|
is_verified=True # 通过邮箱验证码验证,标记为已验证
|
||||||
|
)
|
||||||
|
new_user.set_password(password)
|
||||||
|
|
||||||
|
db.session.add(new_user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash('注册成功!请登录', 'success')
|
||||||
|
return redirect(url_for('auth.login'))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
flash('注册失败,请重试', 'error')
|
||||||
|
return render_template('auth/register.html')
|
||||||
|
|
||||||
|
return render_template('auth/register.html')
|
||||||
|
|
||||||
|
@auth_bp.route('/login', methods=['GET', 'POST'])
|
||||||
|
def login():
|
||||||
|
"""用户登录"""
|
||||||
|
if request.method == 'POST':
|
||||||
|
email = request.form.get('email', '').strip().lower()
|
||||||
|
password = request.form.get('password', '').strip()
|
||||||
|
remember = bool(request.form.get('remember'))
|
||||||
|
|
||||||
|
if not email or not password:
|
||||||
|
flash('请输入邮箱和密码', 'error')
|
||||||
|
return render_template('auth/login.html')
|
||||||
|
|
||||||
|
user = User.query.filter_by(email=email).first()
|
||||||
|
|
||||||
|
if user and user.check_password(password):
|
||||||
|
login_user(user, remember=remember)
|
||||||
|
flash(f'欢迎回来,{user.name}!', 'success')
|
||||||
|
|
||||||
|
# 重定向到之前访问的页面,如果没有则到主页
|
||||||
|
next_page = request.args.get('next')
|
||||||
|
if next_page:
|
||||||
|
return redirect(next_page)
|
||||||
|
return redirect(url_for('main.dashboard'))
|
||||||
|
else:
|
||||||
|
flash('邮箱或密码错误', 'error')
|
||||||
|
|
||||||
|
return render_template('auth/login.html')
|
||||||
|
|
||||||
|
@auth_bp.route('/logout')
|
||||||
|
@login_required
|
||||||
|
def logout():
|
||||||
|
"""用户登出"""
|
||||||
|
logout_user()
|
||||||
|
flash('已成功登出', 'success')
|
||||||
|
return redirect(url_for('main.index'))
|
||||||
|
|
||||||
|
@auth_bp.route('/forgot-password', methods=['GET', 'POST'])
|
||||||
|
def forgot_password():
|
||||||
|
"""忘记密码"""
|
||||||
|
if request.method == 'POST':
|
||||||
|
email = request.form.get('email', '').strip().lower()
|
||||||
|
verification_code = request.form.get('verification_code', '').strip()
|
||||||
|
new_password = request.form.get('new_password', '').strip()
|
||||||
|
confirm_password = request.form.get('confirm_password', '').strip()
|
||||||
|
|
||||||
|
# 检查用户是否存在
|
||||||
|
user = User.query.filter_by(email=email).first()
|
||||||
|
if not user:
|
||||||
|
flash('该邮箱未注册', 'error')
|
||||||
|
return render_template('auth/forgot_password.html')
|
||||||
|
|
||||||
|
# 如果是重置密码请求
|
||||||
|
if verification_code and new_password:
|
||||||
|
if len(new_password) < 6:
|
||||||
|
flash('密码长度至少6位', 'error')
|
||||||
|
return render_template('auth/forgot_password.html')
|
||||||
|
|
||||||
|
if new_password != confirm_password:
|
||||||
|
flash('两次输入的密码不一致', 'error')
|
||||||
|
return render_template('auth/forgot_password.html')
|
||||||
|
|
||||||
|
# 验证验证码
|
||||||
|
if not EmailVerification.verify_code(email, verification_code):
|
||||||
|
flash('验证码错误或已过期', 'error')
|
||||||
|
return render_template('auth/forgot_password.html')
|
||||||
|
|
||||||
|
# 更新密码
|
||||||
|
user.set_password(new_password)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash('密码重置成功,请使用新密码登录', 'success')
|
||||||
|
return redirect(url_for('auth.login'))
|
||||||
|
|
||||||
|
return render_template('auth/forgot_password.html')
|
||||||
|
|
||||||
|
@auth_bp.route('/send-verification-code', methods=['POST'])
|
||||||
|
def send_verification_code():
|
||||||
|
"""发送邮箱验证码"""
|
||||||
|
try:
|
||||||
|
email = request.json.get('email', '').strip().lower()
|
||||||
|
code_type = request.json.get('type', 'register') # register 或 reset_password
|
||||||
|
|
||||||
|
if not email:
|
||||||
|
return jsonify({'success': False, 'message': '请输入邮箱地址'})
|
||||||
|
|
||||||
|
# 邮箱格式验证
|
||||||
|
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||||
|
if not re.match(email_pattern, email):
|
||||||
|
return jsonify({'success': False, 'message': '请输入有效的邮箱地址'})
|
||||||
|
|
||||||
|
# 根据类型检查邮箱
|
||||||
|
if code_type == 'register':
|
||||||
|
# 注册时检查邮箱是否已存在
|
||||||
|
existing_user = User.query.filter_by(email=email).first()
|
||||||
|
if existing_user:
|
||||||
|
return jsonify({'success': False, 'message': '该邮箱已被注册'})
|
||||||
|
elif code_type == 'reset_password':
|
||||||
|
# 重置密码时检查邮箱是否存在
|
||||||
|
existing_user = User.query.filter_by(email=email).first()
|
||||||
|
if not existing_user:
|
||||||
|
return jsonify({'success': False, 'message': '该邮箱未注册'})
|
||||||
|
|
||||||
|
# 生成验证码
|
||||||
|
verification_code = EmailVerification.generate_code(email, expire_minutes=5)
|
||||||
|
|
||||||
|
# 发送邮件
|
||||||
|
if code_type == 'register':
|
||||||
|
success = send_verification_email(email, verification_code)
|
||||||
|
else:
|
||||||
|
success = send_password_reset_email(email, verification_code)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return jsonify({'success': True, 'message': '验证码已发送到您的邮箱,5分钟内有效'})
|
||||||
|
else:
|
||||||
|
return jsonify({'success': False, 'message': '验证码发送失败,请重试'})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'success': False, 'message': '发送失败,请重试'})
|
||||||
17
app/routes/main.py
Normal file
17
app/routes/main.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from flask import Blueprint, render_template
|
||||||
|
from flask_login import current_user
|
||||||
|
|
||||||
|
main_bp = Blueprint('main', __name__)
|
||||||
|
|
||||||
|
@main_bp.route('/')
|
||||||
|
def index():
|
||||||
|
"""首页"""
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
|
@main_bp.route('/dashboard')
|
||||||
|
def dashboard():
|
||||||
|
"""用户主页(需要登录)"""
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
return render_template('dashboard.html')
|
||||||
|
else:
|
||||||
|
return render_template('index.html')
|
||||||
470
app/static/css/style.css
Normal file
470
app/static/css/style.css
Normal file
@ -0,0 +1,470 @@
|
|||||||
|
/* 全局样式 */
|
||||||
|
:root {
|
||||||
|
--primary-color: #4a90e2;
|
||||||
|
--secondary-color: #7b68ee;
|
||||||
|
--success-color: #28a745;
|
||||||
|
--warning-color: #ffc107;
|
||||||
|
--danger-color: #dc3545;
|
||||||
|
--info-color: #17a2b8;
|
||||||
|
--light-color: #f8f9fa;
|
||||||
|
--dark-color: #343a40;
|
||||||
|
--pink-color: #e91e63;
|
||||||
|
|
||||||
|
/* === Kid-Friendly Colors === */
|
||||||
|
--kid-primary: #FFC107; /* Amber */
|
||||||
|
--kid-secondary: #00BCD4; /* Cyan */
|
||||||
|
--kid-accent: #FF5722; /* Deep Orange */
|
||||||
|
--kid-bg: #F0F8FF; /* AliceBlue */
|
||||||
|
--kid-text: #5D4037; /* Brown */
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Helvetica Neue', 'Arial', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #444;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 导航栏 */
|
||||||
|
.navbar-brand {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.navbar-brand i {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Kid-Friendly Auth Styles === */
|
||||||
|
.auth-page-container {
|
||||||
|
background-color: var(--kid-bg);
|
||||||
|
background-image:
|
||||||
|
radial-gradient(circle at 20% 20%, rgba(0, 188, 212, 0.1) 8%, transparent 0),
|
||||||
|
radial-gradient(circle at 80% 70%, rgba(255, 193, 7, 0.1) 8%, transparent 0);
|
||||||
|
background-size: 250px 250px;
|
||||||
|
}
|
||||||
|
.kid-auth-card {
|
||||||
|
border: none;
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
.kid-auth-header {
|
||||||
|
background: linear-gradient(135deg, var(--kid-primary), #FFD54F);
|
||||||
|
color: white;
|
||||||
|
padding: 2rem 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 5px solid #ffb300;
|
||||||
|
position: relative;
|
||||||
|
clip-path: polygon(0 0, 100% 0, 100% 85%, 0 100%);
|
||||||
|
margin-bottom: -2rem;
|
||||||
|
}
|
||||||
|
.kid-auth-header .icon {
|
||||||
|
font-size: 3.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
transform: rotate(-10deg) scale(1.1);
|
||||||
|
display: inline-block;
|
||||||
|
color: white;
|
||||||
|
text-shadow: 2px 2px 5px rgba(0,0,0,0.25);
|
||||||
|
}
|
||||||
|
.kid-auth-header h2 {
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 2rem;
|
||||||
|
margin: 0;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
.kid-auth-card .card-body {
|
||||||
|
padding-top: 3rem !important;
|
||||||
|
}
|
||||||
|
.kid-auth-card .form-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--kid-text);
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
.kid-auth-card .form-control, .kid-auth-card .form-select {
|
||||||
|
border-radius: 30px;
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-top: .6rem;
|
||||||
|
padding-bottom: .6rem;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.kid-auth-card .form-control:focus, .kid-auth-card .form-select:focus {
|
||||||
|
border-color: var(--kid-secondary);
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(0,188,212, 0.2);
|
||||||
|
}
|
||||||
|
.kid-auth-card .input-group-text {
|
||||||
|
border-radius: 30px 0 0 30px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border: 2px solid #e0e0e0; border-right: none;
|
||||||
|
color: var(--kid-secondary);
|
||||||
|
}
|
||||||
|
.kid-auth-card .input-group .form-control { border-radius: 0 30px 30px 0; }
|
||||||
|
.kid-auth-card .input-group .btn {
|
||||||
|
border-radius: 0 30px 30px 0 !important;
|
||||||
|
border-color: #e0e0e0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.kid-auth-card .btn-primary, .btn-kid-accent {
|
||||||
|
background: linear-gradient(45deg, var(--kid-accent), #FF8A65);
|
||||||
|
border: none;
|
||||||
|
border-radius: 30px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
box-shadow: 0 4px 12px rgba(255, 87, 34, 0.3);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
.kid-auth-card .btn-primary:hover, .btn-kid-accent:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 15px rgba(255, 87, 34, 0.45);
|
||||||
|
}
|
||||||
|
.kid-auth-card a {
|
||||||
|
color: var(--kid-secondary);
|
||||||
|
text-decoration: none !important;
|
||||||
|
transition: color .2s;
|
||||||
|
}
|
||||||
|
.kid-auth-card a:hover { color: var(--kid-accent); }
|
||||||
|
|
||||||
|
/* === 修复后的轮播图样式 === */
|
||||||
|
.hero-carousel-fixed {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-carousel-fixed .carousel-item {
|
||||||
|
height: 70vh;
|
||||||
|
min-height: 500px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-carousel-fixed .carousel-item img {
|
||||||
|
object-fit: cover;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 漂浮装饰元素 */
|
||||||
|
.floating-decorations {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 2;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-emoji {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
animation: floatAnimation 4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-emoji:nth-child(1) { top: 15%; left: 10%; animation-delay: 0s; }
|
||||||
|
.float-emoji:nth-child(2) { top: 25%; right: 15%; animation-delay: 1s; }
|
||||||
|
.float-emoji:nth-child(3) { bottom: 30%; left: 15%; animation-delay: 2s; }
|
||||||
|
.float-emoji:nth-child(4) { bottom: 20%; right: 10%; animation-delay: 3s; }
|
||||||
|
|
||||||
|
@keyframes floatAnimation {
|
||||||
|
0%, 100% { transform: translateY(0px) rotate(0deg); opacity: 0.7; }
|
||||||
|
50% { transform: translateY(-20px) rotate(5deg); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 轮播内容样式 */
|
||||||
|
.hero-carousel-fixed .carousel-caption {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 10;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 800px;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
padding: 8px 20px;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 3.5rem;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.1;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
text-shadow: 2px 2px 10px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title .highlight {
|
||||||
|
background: linear-gradient(45deg, #FFD700, #FFA500);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-subtitle {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
opacity: 0.95;
|
||||||
|
text-shadow: 1px 1px 5px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-hero {
|
||||||
|
background: linear-gradient(45deg, #FF6B6B, #4ECDC4);
|
||||||
|
border: none;
|
||||||
|
padding: 15px 35px;
|
||||||
|
border-radius: 50px;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
box-shadow: 0 8px 25px rgba(0,0,0,0.3);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-hero:hover {
|
||||||
|
transform: translateY(-3px) scale(1.05);
|
||||||
|
box-shadow: 0 12px 35px rgba(0,0,0,0.4);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 轮播指示器和控制器 */
|
||||||
|
.hero-carousel-fixed .carousel-indicators {
|
||||||
|
bottom: 30px;
|
||||||
|
z-index: 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-carousel-fixed .carousel-indicators button {
|
||||||
|
width: 15px !important;
|
||||||
|
height: 15px !important;
|
||||||
|
border-radius: 50% !important;
|
||||||
|
margin: 0 10px !important;
|
||||||
|
background: rgba(255, 255, 255, 0.5) !important;
|
||||||
|
border: 2px solid white !important;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-carousel-fixed .carousel-indicators .active {
|
||||||
|
background: white !important;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-carousel-fixed .carousel-control-prev,
|
||||||
|
.hero-carousel-fixed .carousel-control-next {
|
||||||
|
z-index: 15;
|
||||||
|
width: 60px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-carousel-fixed .carousel-control-prev:hover,
|
||||||
|
.hero-carousel-fixed .carousel-control-next:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 波浪分隔符 */
|
||||||
|
.wavy-divider {
|
||||||
|
height: 100px;
|
||||||
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 320'%3e%3cpath fill='%23ffffff' fill-opacity='1' d='M0,192L80,176C160,160,320,128,480,133.3C640,139,800,181,960,186.7C1120,192,1280,160,1360,144L1440,128L1440,0L1360,0C1280,0,1120,0,960,0C800,0,640,0,480,0C320,0,160,0,80,0L0,0Z'%3e%3c/path%3e%3c/svg%3e");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
margin-top: -100px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面区块通用样式 */
|
||||||
|
.page-section {
|
||||||
|
padding: 80px 0;
|
||||||
|
}
|
||||||
|
.bg-light-kid {
|
||||||
|
background-color: #fefcfa;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
.section-title h2 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--kid-text);
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
.section-title h2::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 60px;
|
||||||
|
height: 5px;
|
||||||
|
background: linear-gradient(to right, var(--kid-primary), var(--kid-accent));
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 10px auto 0;
|
||||||
|
}
|
||||||
|
.section-title p {
|
||||||
|
color: #777;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 新特色卡片 */
|
||||||
|
.kid-feature-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.kid-feature-card:hover {
|
||||||
|
transform: translateY(-8px);
|
||||||
|
box-shadow: 0 15px 40px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.kid-feature-card .icon-bubble {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.kid-feature-card .icon-bubble i {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
.kid-feature-card h5 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--kid-text);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.kid-feature-card p {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 新场景卡片 */
|
||||||
|
.scenario-card-new {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 8px 25px rgba(0,0,0,0.07);
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
.scenario-card-new:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 12px 35px rgba(0,0,0,0.12);
|
||||||
|
}
|
||||||
|
.scenario-card-new .scenario-icon {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 0 auto 1.5rem;
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
color: #2196F3;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
.scenario-card-new h5 {
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.scenario-card-new p {
|
||||||
|
color: #777;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
.scenario-card-new .tags {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.scenario-card-new .tag {
|
||||||
|
background-color: #eee;
|
||||||
|
color: #555;
|
||||||
|
padding: 5px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin: 0 5px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 新CTA区块 */
|
||||||
|
.cta-section-kid {
|
||||||
|
background: linear-gradient(135deg, var(--kid-secondary), var(--kid-primary));
|
||||||
|
color: white;
|
||||||
|
padding: 100px 0;
|
||||||
|
}
|
||||||
|
.cta-section-kid h2 {
|
||||||
|
font-size: 2.8rem;
|
||||||
|
font-weight: 800;
|
||||||
|
text-shadow: 1px 1px 3px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
.cta-section-kid p {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hero-carousel-fixed .carousel-item {
|
||||||
|
height: 60vh;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-subtitle {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-emoji {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-hero {
|
||||||
|
padding: 12px 25px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-carousel-fixed .carousel-caption {
|
||||||
|
bottom: 15%;
|
||||||
|
width: 95%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-pink {
|
||||||
|
color: var(--pink-color) !important;
|
||||||
|
}
|
||||||
177
app/static/js/main.js
Normal file
177
app/static/js/main.js
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
// 全局JavaScript功能
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// 自动隐藏提示消息
|
||||||
|
const alerts = document.querySelectorAll('.alert');
|
||||||
|
alerts.forEach(function(alert) {
|
||||||
|
setTimeout(function() {
|
||||||
|
alert.style.opacity = '0';
|
||||||
|
setTimeout(function() {
|
||||||
|
alert.style.display = 'none';
|
||||||
|
}, 300);
|
||||||
|
}, 5000); // 5秒后自动隐藏
|
||||||
|
});
|
||||||
|
// 平滑滚动
|
||||||
|
const links = document.querySelectorAll('a[href^="#"]');
|
||||||
|
links.forEach(function(link) {
|
||||||
|
link.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const target = document.querySelector(this.getAttribute('href'));
|
||||||
|
if (target) {
|
||||||
|
target.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// 表单验证增强
|
||||||
|
const forms = document.querySelectorAll('form');
|
||||||
|
forms.forEach(function(form) {
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
const submitBtn = form.querySelector('button[type="submit"]');
|
||||||
|
if (submitBtn && !submitBtn.disabled) {
|
||||||
|
// 防止重复提交
|
||||||
|
setTimeout(function() {
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// 输入框焦点效果
|
||||||
|
const inputs = document.querySelectorAll('.form-control');
|
||||||
|
inputs.forEach(function(input) {
|
||||||
|
input.addEventListener('focus', function() {
|
||||||
|
this.parentNode.classList.add('focused');
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('blur', function() {
|
||||||
|
this.parentNode.classList.remove('focused');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// 工具提示初始化
|
||||||
|
if (typeof bootstrap !== 'undefined') {
|
||||||
|
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||||
|
tooltipTriggerList.map(function(tooltipTriggerEl) {
|
||||||
|
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||||
|
});
|
||||||
|
// 弹出框初始化
|
||||||
|
const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
|
||||||
|
popoverTriggerList.map(function(popoverTriggerEl) {
|
||||||
|
return new bootstrap.Popover(popoverTriggerEl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 邮箱验证码倒计时功能
|
||||||
|
window.startVerificationCountdown = function(buttonId, duration = 60) {
|
||||||
|
const button = document.getElementById(buttonId);
|
||||||
|
if (!button) return;
|
||||||
|
|
||||||
|
let count = duration;
|
||||||
|
const originalText = button.textContent;
|
||||||
|
|
||||||
|
button.disabled = true;
|
||||||
|
|
||||||
|
const timer = setInterval(function() {
|
||||||
|
button.textContent = `${count}秒后重试`;
|
||||||
|
count--;
|
||||||
|
|
||||||
|
if (count < 0) {
|
||||||
|
clearInterval(timer);
|
||||||
|
button.disabled = false;
|
||||||
|
button.textContent = originalText === '发送验证码' ? '重新发送' : originalText;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return timer;
|
||||||
|
};
|
||||||
|
// AJAX请求封装
|
||||||
|
window.sendAjaxRequest = function(url, data, successCallback, errorCallback) {
|
||||||
|
fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
if (successCallback) successCallback(data);
|
||||||
|
} else {
|
||||||
|
if (errorCallback) errorCallback(data.message || '请求失败');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('请求错误:', error);
|
||||||
|
if (errorCallback) errorCallback('网络错误,请检查连接');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// 显示加载状态
|
||||||
|
window.showLoading = function(button, loadingText = '处理中...') {
|
||||||
|
if (!button) return;
|
||||||
|
|
||||||
|
button.disabled = true;
|
||||||
|
const originalHTML = button.innerHTML;
|
||||||
|
button.innerHTML = `<i class="fas fa-spinner fa-spin me-2"></i>${loadingText}`;
|
||||||
|
|
||||||
|
return function() {
|
||||||
|
button.disabled = false;
|
||||||
|
button.innerHTML = originalHTML;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
// 显示消息提示
|
||||||
|
window.showMessage = function(message, type = 'info') {
|
||||||
|
const alertContainer = document.querySelector('.container');
|
||||||
|
if (!alertContainer) return;
|
||||||
|
const alertDiv = document.createElement('div');
|
||||||
|
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
|
||||||
|
alertDiv.innerHTML = `
|
||||||
|
<i class="fas fa-${type === 'error' ? 'exclamation-triangle' : type === 'success' ? 'check-circle' : 'info-circle'} me-2"></i>
|
||||||
|
${message}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
alertContainer.insertBefore(alertDiv, alertContainer.firstChild);
|
||||||
|
|
||||||
|
// 自动隐藏
|
||||||
|
setTimeout(function() {
|
||||||
|
alertDiv.style.opacity = '0';
|
||||||
|
setTimeout(function() {
|
||||||
|
if (alertDiv.parentNode) {
|
||||||
|
alertDiv.parentNode.removeChild(alertDiv);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}, 5000);
|
||||||
|
};
|
||||||
|
// 验证邮箱格式
|
||||||
|
window.validateEmail = function(email) {
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
return emailRegex.test(email);
|
||||||
|
};
|
||||||
|
// 验证密码强度
|
||||||
|
window.validatePassword = function(password) {
|
||||||
|
if (password.length < 6) {
|
||||||
|
return { valid: false, message: '密码长度至少6位' };
|
||||||
|
}
|
||||||
|
return { valid: true, message: '密码强度可以' };
|
||||||
|
};
|
||||||
|
// 数字输入限制
|
||||||
|
window.restrictToNumbers = function(inputElement) {
|
||||||
|
inputElement.addEventListener('input', function(e) {
|
||||||
|
e.target.value = e.target.value.replace(/[^0-9]/g, '');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// 初始化数字验证码输入框
|
||||||
|
const codeInputs = document.querySelectorAll('input[name="verification_code"]');
|
||||||
|
codeInputs.forEach(function(input) {
|
||||||
|
window.restrictToNumbers(input);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// 全局错误处理
|
||||||
|
window.addEventListener('error', function(e) {
|
||||||
|
console.error('全局错误:', e.error);
|
||||||
|
});
|
||||||
|
// 全局未处理的Promise拒绝
|
||||||
|
window.addEventListener('unhandledrejection', function(e) {
|
||||||
|
console.error('未处理的Promise拒绝:', e.reason);
|
||||||
|
});
|
||||||
206
app/templates/auth/forgot_password.html
Normal file
206
app/templates/auth/forgot_password.html
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}找回密码 - 儿童语言学习系统{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="auth-page-container">
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6 col-lg-5">
|
||||||
|
<div class="card kid-auth-card shadow-lg">
|
||||||
|
<div class="kid-auth-header" style="background: linear-gradient(135deg, var(--kid-secondary), #4DD0E1);">
|
||||||
|
<i class="fas fa-magic icon"></i>
|
||||||
|
<h2 class="fw-bold">忘记密码了?</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-4 p-md-5">
|
||||||
|
<p class="text-center text-muted mb-4">别担心,我们用魔法帮你找回!</p>
|
||||||
|
|
||||||
|
<form method="POST" id="forgotPasswordForm">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email" class="form-label">邮箱地址</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="fas fa-envelope"></i></span>
|
||||||
|
<input type="email" class="form-control" id="email" name="email"
|
||||||
|
placeholder="请输入您的注册邮箱" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="verification_code" class="form-label">邮箱验证码</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" id="verification_code" name="verification_code"
|
||||||
|
placeholder="6位验证码" maxlength="6" required>
|
||||||
|
<button type="button" class="btn btn-outline-primary" id="sendCodeBtn">
|
||||||
|
发送验证码
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="new_password" class="form-label">新密码</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="fas fa-lock"></i></span>
|
||||||
|
<input type="password" class="form-control" id="new_password" name="new_password"
|
||||||
|
placeholder="至少6位新密码" required minlength="6">
|
||||||
|
<button class="btn btn-outline-secondary" type="button" id="toggleNewPassword">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="confirm_password" class="form-label">确认新密码</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="fas fa-lock"></i></span>
|
||||||
|
<input type="password" class="form-control" id="confirm_password" name="confirm_password"
|
||||||
|
placeholder="再次输入新密码" required minlength="6">
|
||||||
|
<button class="btn btn-outline-secondary" type="button" id="toggleConfirmPassword">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary w-100 py-2 mb-3" id="submitBtn">
|
||||||
|
<i class="fas fa-check-circle me-2"></i>重置密码
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<a href="{{ url_for('auth.login') }}" class="small">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i>我想起来了,返回登录
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
let countdownTimer = null;
|
||||||
|
|
||||||
|
// 密码显示/隐藏切换
|
||||||
|
function setupPasswordToggle(toggleId, inputId) {
|
||||||
|
const toggle = document.getElementById(toggleId);
|
||||||
|
const input = document.getElementById(inputId);
|
||||||
|
|
||||||
|
toggle.addEventListener('click', function() {
|
||||||
|
const type = input.getAttribute('type') === 'password' ? 'text' : 'password';
|
||||||
|
input.setAttribute('type', type);
|
||||||
|
|
||||||
|
const icon = toggle.querySelector('i');
|
||||||
|
icon.classList.toggle('fa-eye');
|
||||||
|
icon.classList.toggle('fa-eye-slash');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupPasswordToggle('toggleNewPassword', 'new_password');
|
||||||
|
setupPasswordToggle('toggleConfirmPassword', 'confirm_password');
|
||||||
|
|
||||||
|
// 发送验证码
|
||||||
|
const sendCodeBtn = document.getElementById('sendCodeBtn');
|
||||||
|
sendCodeBtn.addEventListener('click', function() {
|
||||||
|
const email = document.getElementById('email').value.trim();
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
alert('请先输入邮箱地址');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(email)) {
|
||||||
|
alert('请输入有效的邮箱地址');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 禁用按钮
|
||||||
|
sendCodeBtn.disabled = true;
|
||||||
|
sendCodeBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>发送中...';
|
||||||
|
|
||||||
|
// 发送验证码
|
||||||
|
fetch('/auth/send-verification-code', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: email,
|
||||||
|
type: 'reset_password'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert('验证码已发送到您的邮箱!');
|
||||||
|
startCountdown();
|
||||||
|
} else {
|
||||||
|
alert(data.message || '发送失败,请重试');
|
||||||
|
sendCodeBtn.disabled = false;
|
||||||
|
sendCodeBtn.innerHTML = '发送验证码';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert('发送失败,请检查网络连接');
|
||||||
|
sendCodeBtn.disabled = false;
|
||||||
|
sendCodeBtn.innerHTML = '发送验证码';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 倒计时
|
||||||
|
function startCountdown() {
|
||||||
|
let count = 60;
|
||||||
|
countdownTimer = setInterval(() => {
|
||||||
|
sendCodeBtn.innerHTML = `${count}秒后重试`;
|
||||||
|
count--;
|
||||||
|
|
||||||
|
if (count < 0) {
|
||||||
|
clearInterval(countdownTimer);
|
||||||
|
sendCodeBtn.disabled = false;
|
||||||
|
sendCodeBtn.innerHTML = '重新发送';
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单验证
|
||||||
|
const forgotPasswordForm = document.getElementById('forgotPasswordForm');
|
||||||
|
forgotPasswordForm.addEventListener('submit', function(e) {
|
||||||
|
const newPassword = document.getElementById('new_password').value;
|
||||||
|
const confirmPassword = document.getElementById('confirm_password').value;
|
||||||
|
const verificationCode = document.getElementById('verification_code').value.trim();
|
||||||
|
|
||||||
|
if (!verificationCode || verificationCode.length !== 6) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('请输入6位验证码');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword.length < 6) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('密码长度至少6位');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('两次输入的密码不一致');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示提交状态
|
||||||
|
const submitBtn = document.getElementById('submitBtn');
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>重置中...';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证码输入限制
|
||||||
|
const verificationInput = document.getElementById('verification_code');
|
||||||
|
verificationInput.addEventListener('input', function(e) {
|
||||||
|
e.target.value = e.target.value.replace(/[^0-9]/g, '');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
108
app/templates/auth/login.html
Normal file
108
app/templates/auth/login.html
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}登录 - 儿童语言学习系统{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="auth-page-container">
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6 col-lg-5">
|
||||||
|
<div class="card kid-auth-card shadow-lg">
|
||||||
|
<div class="kid-auth-header">
|
||||||
|
<i class="fas fa-rocket icon"></i>
|
||||||
|
<h2 class="fw-bold">欢迎回来!</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-4 p-md-5">
|
||||||
|
<p class="text-center text-muted mb-4">继续你的语言学习大冒险!</p>
|
||||||
|
|
||||||
|
<form method="POST" id="loginForm">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email" class="form-label">邮箱地址</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="fas fa-envelope"></i></span>
|
||||||
|
<input type="email" class="form-control" id="email" name="email"
|
||||||
|
placeholder="请输入您的邮箱" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">密码</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="fas fa-lock"></i></span>
|
||||||
|
<input type="password" class="form-control" id="password" name="password"
|
||||||
|
placeholder="请输入您的密码" required>
|
||||||
|
<button class="btn btn-outline-secondary" type="button" id="togglePassword">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="remember" name="remember">
|
||||||
|
<label class="form-check-label" for="remember">记住我</label>
|
||||||
|
</div>
|
||||||
|
<a href="{{ url_for('auth.forgot_password') }}" class="small">忘记密码?</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary w-100 py-2 mb-3">
|
||||||
|
<i class="fas fa-paper-plane me-2"></i>登录出发!
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr class="my-3">
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="mb-0">还没有账户?
|
||||||
|
<a href="{{ url_for('auth.register') }}" class="fw-bold" style="color: var(--kid-accent);">
|
||||||
|
立即加入
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// 密码显示/隐藏切换
|
||||||
|
const togglePassword = document.getElementById('togglePassword');
|
||||||
|
const passwordInput = document.getElementById('password');
|
||||||
|
|
||||||
|
togglePassword.addEventListener('click', function() {
|
||||||
|
const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
|
||||||
|
passwordInput.setAttribute('type', type);
|
||||||
|
|
||||||
|
const icon = togglePassword.querySelector('i');
|
||||||
|
icon.classList.toggle('fa-eye');
|
||||||
|
icon.classList.toggle('fa-eye-slash');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表单验证
|
||||||
|
const loginForm = document.getElementById('loginForm');
|
||||||
|
loginForm.addEventListener('submit', function(e) {
|
||||||
|
const email = document.getElementById('email').value.trim();
|
||||||
|
const password = document.getElementById('password').value.trim();
|
||||||
|
|
||||||
|
if (!email || !password) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('请填写完整的登录信息');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简单的邮箱格式验证
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(email)) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('请输入有效的邮箱地址');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
261
app/templates/auth/register.html
Normal file
261
app/templates/auth/register.html
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}注册 - 儿童语言学习系统{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="auth-page-container">
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8 col-lg-7">
|
||||||
|
<div class="card kid-auth-card shadow-lg">
|
||||||
|
<div class="kid-auth-header">
|
||||||
|
<i class="fas fa-star icon"></i>
|
||||||
|
<h2 class="fw-bold">加入我们!</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-4 p-md-5">
|
||||||
|
<p class="text-center text-muted mb-4">开启一段奇妙的语言学习之旅!</p>
|
||||||
|
|
||||||
|
<form method="POST" id="registerForm">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="name" class="form-label">姓名 <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control" id="name" name="name"
|
||||||
|
placeholder="孩子的姓名" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="age" class="form-label">年龄 <span class="text-danger">*</span></label>
|
||||||
|
<select class="form-select" id="age" name="age" required>
|
||||||
|
<option value="">请选择年龄</option>
|
||||||
|
<option value="3">3岁</option>
|
||||||
|
<option value="4">4岁</option>
|
||||||
|
<option value="5">5岁</option>
|
||||||
|
<option value="6">6岁</option>
|
||||||
|
<option value="7">7岁</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">性别 <span class="text-danger">*</span></label>
|
||||||
|
<div class="d-flex gap-4">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="gender" id="gender_male" value="0" required>
|
||||||
|
<label class="form-check-label" for="gender_male">
|
||||||
|
<i class="fas fa-mars text-primary me-1"></i>小王子
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="gender" id="gender_female" value="1" required>
|
||||||
|
<label class="form-check-label" for="gender_female">
|
||||||
|
<i class="fas fa-venus text-pink me-1"></i>小公主
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email" class="form-label">邮箱地址 <span class="text-danger">*</span></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="fas fa-envelope"></i></span>
|
||||||
|
<input type="email" class="form-control" id="email" name="email"
|
||||||
|
placeholder="请输入邮箱地址" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="verification_code" class="form-label">邮箱验证码 <span class="text-danger">*</span></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" id="verification_code" name="verification_code"
|
||||||
|
placeholder="请输入6位验证码" maxlength="6" required>
|
||||||
|
<button type="button" class="btn btn-outline-primary" id="sendCodeBtn">
|
||||||
|
发送验证码
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="password" class="form-label">密码 <span class="text-danger">*</span></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<i class="fas fa-lock"></i>
|
||||||
|
</span>
|
||||||
|
<input type="password" class="form-control" id="password" name="password"
|
||||||
|
placeholder="至少6位密码" required minlength="6">
|
||||||
|
<button class="btn btn-outline-secondary" type="button" id="togglePassword">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="confirm_password" class="form-label">确认密码 <span class="text-danger">*</span></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<i class="fas fa-lock"></i>
|
||||||
|
</span>
|
||||||
|
<input type="password" class="form-control" id="confirm_password" name="confirm_password"
|
||||||
|
placeholder="再次输入密码" required minlength="6">
|
||||||
|
<button class="btn btn-outline-secondary" type="button" id="toggleConfirmPassword">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="parent_contact" class="form-label">家长联系方式 <span class="text-muted">(可选)</span></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="fas fa-phone-alt"></i></span>
|
||||||
|
<input type="text" class="form-control" id="parent_contact" name="parent_contact"
|
||||||
|
placeholder="手机号或微信号,便于沟通">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary w-100 py-2 my-3" id="submitBtn">
|
||||||
|
<i class="fas fa-user-plus me-2"></i>完成注册
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr class="my-2">
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="mb-0">已有账户?
|
||||||
|
<a href="{{ url_for('auth.login') }}" class="fw-bold" style="color: var(--kid-accent);">
|
||||||
|
马上登录
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
let countdownTimer = null;
|
||||||
|
|
||||||
|
// 密码显示/隐藏切换
|
||||||
|
function setupPasswordToggle(toggleId, inputId) {
|
||||||
|
const toggle = document.getElementById(toggleId);
|
||||||
|
const input = document.getElementById(inputId);
|
||||||
|
|
||||||
|
toggle.addEventListener('click', function() {
|
||||||
|
const type = input.getAttribute('type') === 'password' ? 'text' : 'password';
|
||||||
|
input.setAttribute('type', type);
|
||||||
|
|
||||||
|
const icon = toggle.querySelector('i');
|
||||||
|
icon.classList.toggle('fa-eye');
|
||||||
|
icon.classList.toggle('fa-eye-slash');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupPasswordToggle('togglePassword', 'password');
|
||||||
|
setupPasswordToggle('toggleConfirmPassword', 'confirm_password');
|
||||||
|
|
||||||
|
// 发送验证码
|
||||||
|
const sendCodeBtn = document.getElementById('sendCodeBtn');
|
||||||
|
sendCodeBtn.addEventListener('click', function() {
|
||||||
|
const email = document.getElementById('email').value.trim();
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
alert('请先输入邮箱地址');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(email)) {
|
||||||
|
alert('请输入有效的邮箱地址');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 禁用按钮
|
||||||
|
sendCodeBtn.disabled = true;
|
||||||
|
sendCodeBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>发送中...';
|
||||||
|
|
||||||
|
// 发送验证码
|
||||||
|
fetch('/auth/send-verification-code', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: email,
|
||||||
|
type: 'register'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert('验证码已发送到您的邮箱!');
|
||||||
|
startCountdown();
|
||||||
|
} else {
|
||||||
|
alert(data.message || '发送失败,请重试');
|
||||||
|
sendCodeBtn.disabled = false;
|
||||||
|
sendCodeBtn.innerHTML = '发送验证码';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert('发送失败,请检查网络连接');
|
||||||
|
sendCodeBtn.disabled = false;
|
||||||
|
sendCodeBtn.innerHTML = '发送验证码';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 倒计时
|
||||||
|
function startCountdown() {
|
||||||
|
let count = 60;
|
||||||
|
countdownTimer = setInterval(() => {
|
||||||
|
sendCodeBtn.innerHTML = `${count}秒后重试`;
|
||||||
|
count--;
|
||||||
|
|
||||||
|
if (count < 0) {
|
||||||
|
clearInterval(countdownTimer);
|
||||||
|
sendCodeBtn.disabled = false;
|
||||||
|
sendCodeBtn.innerHTML = '重新发送';
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单验证
|
||||||
|
const registerForm = document.getElementById('registerForm');
|
||||||
|
registerForm.addEventListener('submit', function(e) {
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
const confirmPassword = document.getElementById('confirm_password').value;
|
||||||
|
const verificationCode = document.getElementById('verification_code').value.trim();
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('两次输入的密码不一致');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 6) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('密码长度至少6位');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!verificationCode || verificationCode.length !== 6) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('请输入6位验证码');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示提交状态
|
||||||
|
const submitBtn = document.getElementById('submitBtn');
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>注册中...';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证码输入限制
|
||||||
|
const verificationInput = document.getElementById('verification_code');
|
||||||
|
verificationInput.addEventListener('input', function(e) {
|
||||||
|
e.target.value = e.target.value.replace(/[^0-9]/g, '');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
99
app/templates/base.html
Normal file
99
app/templates/base.html
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}儿童语言学习系统{% endblock %}</title>
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<link href="https://cdn.bootcdn.net/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<!-- Font Awesome -->
|
||||||
|
<link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||||
|
<!-- Custom CSS -->
|
||||||
|
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
|
||||||
|
|
||||||
|
{% block head %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- 导航栏 -->
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-light bg-white shadow-sm">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand fw-bold text-primary" href="{{ url_for('main.index') }}">
|
||||||
|
<i class="fas fa-child me-2"></i>儿童语言学习系统
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav ms-auto">
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
|
||||||
|
<i class="fas fa-user-circle me-1"></i>{{ current_user.name }}
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('main.dashboard') }}">
|
||||||
|
<i class="fas fa-tachometer-alt me-2"></i>学习主页
|
||||||
|
</a></li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">
|
||||||
|
<i class="fas fa-sign-out-alt me-2"></i>退出登录
|
||||||
|
</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for('auth.login') }}">
|
||||||
|
<i class="fas fa-sign-in-alt me-1"></i>登录
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link btn btn-primary text-white ms-2 px-3" href="{{ url_for('auth.register') }}">
|
||||||
|
<i class="fas fa-user-plus me-1"></i>注册
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Flash消息 -->
|
||||||
|
<div class="container mt-3">
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
|
||||||
|
<i class="fas fa-{{ 'exclamation-triangle' if category == 'error' else 'check-circle' if category == 'success' else 'info-circle' }} me-2"></i>
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主要内容 -->
|
||||||
|
<main>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- 页脚 -->
|
||||||
|
<footer class="bg-light py-4 mt-auto">
|
||||||
|
<div class="container text-center">
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
© 2025 儿童语言学习系统 - 让孩子快乐学语言
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Bootstrap JS -->
|
||||||
|
<script src="https://cdn.bootcdn.net/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<!-- Custom JS -->
|
||||||
|
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||||
|
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
272
app/templates/dashboard.html
Normal file
272
app/templates/dashboard.html
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}学习主页 - 儿童语言学习系统{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-4">
|
||||||
|
<!-- 欢迎区域 -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="welcome-card bg-gradient-primary text-white rounded-4 p-4">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<h2 class="fw-bold mb-2">
|
||||||
|
<i class="fas fa-sun me-2"></i>
|
||||||
|
早安,{{ current_user.name }}小朋友!
|
||||||
|
</h2>
|
||||||
|
<p class="mb-0 opacity-90">
|
||||||
|
准备好今天的语言学习之旅了吗?让我们一起探索有趣的世界吧!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 text-end">
|
||||||
|
<i class="fas fa-rocket" style="font-size: 4rem; opacity: 0.3;"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 快速开始区域 -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<h3 class="fw-bold mb-3">
|
||||||
|
<i class="fas fa-play-circle text-primary me-2"></i>快速开始
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4 mb-5">
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="quick-action-card h-100">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-body text-center p-4">
|
||||||
|
<div class="action-icon mb-3">
|
||||||
|
<i class="fas fa-microphone text-primary"></i>
|
||||||
|
</div>
|
||||||
|
<h5 class="fw-bold mb-2">录制声音</h5>
|
||||||
|
<p class="text-muted small mb-3">
|
||||||
|
上传你的声音样本,让AI学会说话像你一样
|
||||||
|
</p>
|
||||||
|
<button class="btn btn-primary" disabled>
|
||||||
|
<i class="fas fa-upload me-2"></i>开始录制
|
||||||
|
</button>
|
||||||
|
<div class="mt-2">
|
||||||
|
<small class="text-muted">即将上线</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="quick-action-card h-100">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-body text-center p-4">
|
||||||
|
<div class="action-icon mb-3">
|
||||||
|
<i class="fas fa-comments text-success"></i>
|
||||||
|
</div>
|
||||||
|
<h5 class="fw-bold mb-2">开始对话</h5>
|
||||||
|
<p class="text-muted small mb-3">
|
||||||
|
选择有趣的场景,和AI朋友一起聊天学习
|
||||||
|
</p>
|
||||||
|
<button class="btn btn-success" disabled>
|
||||||
|
<i class="fas fa-play me-2"></i>选择场景
|
||||||
|
</button>
|
||||||
|
<div class="mt-2">
|
||||||
|
<small class="text-muted">即将上线</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="quick-action-card h-100">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-body text-center p-4">
|
||||||
|
<div class="action-icon mb-3">
|
||||||
|
<i class="fas fa-chart-line text-warning"></i>
|
||||||
|
</div>
|
||||||
|
<h5 class="fw-bold mb-2">查看进度</h5>
|
||||||
|
<p class="text-muted small mb-3">
|
||||||
|
了解你的学习情况,看看哪里可以做得更好
|
||||||
|
</p>
|
||||||
|
<button class="btn btn-warning" disabled>
|
||||||
|
<i class="fas fa-eye me-2"></i>查看报告
|
||||||
|
</button>
|
||||||
|
<div class="mt-2">
|
||||||
|
<small class="text-muted">即将上线</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 学习统计 -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<h3 class="fw-bold mb-3">
|
||||||
|
<i class="fas fa-trophy text-warning me-2"></i>学习成就
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4 mb-5">
|
||||||
|
<div class="col-6 col-md-3">
|
||||||
|
<div class="stat-card text-center">
|
||||||
|
<div class="card border-0 bg-light">
|
||||||
|
<div class="card-body p-3">
|
||||||
|
<i class="fas fa-clock text-primary mb-2" style="font-size: 2rem;"></i>
|
||||||
|
<h4 class="fw-bold mb-0">0</h4>
|
||||||
|
<small class="text-muted">学习时长(分钟)</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6 col-md-3">
|
||||||
|
<div class="stat-card text-center">
|
||||||
|
<div class="card border-0 bg-light">
|
||||||
|
<div class="card-body p-3">
|
||||||
|
<i class="fas fa-comments text-success mb-2" style="font-size: 2rem;"></i>
|
||||||
|
<h4 class="fw-bold mb-0">0</h4>
|
||||||
|
<small class="text-muted">对话次数</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6 col-md-3">
|
||||||
|
<div class="stat-card text-center">
|
||||||
|
<div class="card border-0 bg-light">
|
||||||
|
<div class="card-body p-3">
|
||||||
|
<i class="fas fa-star text-warning mb-2" style="font-size: 2rem;"></i>
|
||||||
|
<h4 class="fw-bold mb-0">0</h4>
|
||||||
|
<small class="text-muted">获得星星</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6 col-md-3">
|
||||||
|
<div class="stat-card text-center">
|
||||||
|
<div class="card border-0 bg-light">
|
||||||
|
<div class="card-body p-3">
|
||||||
|
<i class="fas fa-medal text-info mb-2" style="font-size: 2rem;"></i>
|
||||||
|
<h4 class="fw-bold mb-0">0</h4>
|
||||||
|
<small class="text-muted">完成场景</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 推荐场景预览 -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<h3 class="fw-bold mb-3">
|
||||||
|
<i class="fas fa-sparkles text-info me-2"></i>推荐场景
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="scenario-preview-card">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="d-flex align-items-start mb-3">
|
||||||
|
<div class="scenario-icon me-3">
|
||||||
|
<i class="fas fa-handshake text-primary"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h6 class="fw-bold mb-1">和小明交朋友</h6>
|
||||||
|
<small class="text-muted">社交互动</small>
|
||||||
|
</div>
|
||||||
|
<span class="badge bg-primary">简单</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted small mb-3">
|
||||||
|
学习如何与新朋友进行自我介绍和基础交流
|
||||||
|
</p>
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div class="scenario-stats">
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="fas fa-users me-1"></i>0人完成
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-outline-primary btn-sm" disabled>
|
||||||
|
即将开放
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="scenario-preview-card">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="d-flex align-items-start mb-3">
|
||||||
|
<div class="scenario-icon me-3">
|
||||||
|
<i class="fas fa-utensils text-success"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h6 class="fw-bold mb-1">餐厅点餐</h6>
|
||||||
|
<small class="text-muted">日常生活</small>
|
||||||
|
</div>
|
||||||
|
<span class="badge bg-warning">中等</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted small mb-3">
|
||||||
|
学习在餐厅如何礼貌地点餐和与服务员交流
|
||||||
|
</p>
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div class="scenario-stats">
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="fas fa-users me-1"></i>0人完成
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-outline-success btn-sm" disabled>
|
||||||
|
即将开放
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="scenario-preview-card">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="d-flex align-items-start mb-3">
|
||||||
|
<div class="scenario-icon me-3">
|
||||||
|
<i class="fas fa-gamepad text-warning"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h6 class="fw-bold mb-1">邀请朋友玩游戏</h6>
|
||||||
|
<small class="text-muted">游戏娱乐</small>
|
||||||
|
</div>
|
||||||
|
<span class="badge bg-warning">中等</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted small mb-3">
|
||||||
|
学习如何邀请朋友一起玩游戏并协调活动
|
||||||
|
</p>
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div class="scenario-stats">
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="fas fa-users me-1"></i>0人完成
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-outline-warning btn-sm" disabled>
|
||||||
|
即将开放
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
204
app/templates/index.html
Normal file
204
app/templates/index.html
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}首页 - 儿童语言学习系统{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<!-- 1. 修复后的轮播图英雄区域 -->
|
||||||
|
<div id="heroCarousel" class="carousel slide hero-carousel-fixed" data-bs-ride="carousel">
|
||||||
|
<!-- 指示器 -->
|
||||||
|
<div class="carousel-indicators">
|
||||||
|
<button type="button" data-bs-target="#heroCarousel" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Slide 1"></button>
|
||||||
|
<button type="button" data-bs-target="#heroCarousel" data-bs-slide-to="1" aria-label="Slide 2"></button>
|
||||||
|
<button type="button" data-bs-target="#heroCarousel" data-bs-slide-to="2" aria-label="Slide 3"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 轮播内容 -->
|
||||||
|
<div class="carousel-inner">
|
||||||
|
<!-- 第一个轮播项:语音克隆 -->
|
||||||
|
<div class="carousel-item active">
|
||||||
|
<img src="https://images.unsplash.com/photo-1503454537195-1dcabb73ffb9?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80" class="d-block w-100" alt="孩子在学习">
|
||||||
|
<div class="carousel-overlay"></div>
|
||||||
|
<div class="floating-decorations">
|
||||||
|
<div class="float-emoji">🎤</div>
|
||||||
|
<div class="float-emoji">🗣️</div>
|
||||||
|
<div class="float-emoji">✨</div>
|
||||||
|
<div class="float-emoji">🎵</div>
|
||||||
|
</div>
|
||||||
|
<div class="carousel-caption d-md-block">
|
||||||
|
<div class="hero-badge">🎉 AI语音黑科技</div>
|
||||||
|
<h1 class="hero-title">
|
||||||
|
<span class="highlight">听自己的声音</span><br>
|
||||||
|
快乐学说话!
|
||||||
|
</h1>
|
||||||
|
<p class="hero-subtitle">
|
||||||
|
哇!我们有个超酷的魔法,能让电脑学会说话像你一样好听!
|
||||||
|
再也不怕开口说话啦~
|
||||||
|
</p>
|
||||||
|
{% if not current_user.is_authenticated %}
|
||||||
|
<a href="{{ url_for('auth.register') }}" class="btn btn-hero">
|
||||||
|
<i class="fas fa-rocket me-2"></i>来试试这个魔法
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ url_for('main.dashboard') }}" class="btn btn-hero">
|
||||||
|
<i class="fas fa-play me-2"></i>开始我的冒险
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第二个轮播项:场景对话 -->
|
||||||
|
<div class="carousel-item">
|
||||||
|
<img src="https://images.unsplash.com/photo-1560472354-b33ff0c44a43?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80" class="d-block w-100" alt="儿童游戏场景">
|
||||||
|
<div class="carousel-overlay"></div>
|
||||||
|
<div class="floating-decorations">
|
||||||
|
<div class="float-emoji">🏪</div>
|
||||||
|
<div class="float-emoji">🍕</div>
|
||||||
|
<div class="float-emoji">👫</div>
|
||||||
|
<div class="float-emoji">🎭</div>
|
||||||
|
</div>
|
||||||
|
<div class="carousel-caption d-md-block">
|
||||||
|
<div class="hero-badge">🎮 超多好玩场景</div>
|
||||||
|
<h1 class="hero-title">
|
||||||
|
<span class="highlight">像玩游戏一样</span><br>
|
||||||
|
练习说话!
|
||||||
|
</h1>
|
||||||
|
<p class="hero-subtitle">
|
||||||
|
去餐厅点好吃的、跟小朋友交朋友、当个小老板...
|
||||||
|
在各种好玩的情景里,自然而然学会表达!
|
||||||
|
</p>
|
||||||
|
<a href="{{ url_for('main.dashboard') if current_user.is_authenticated else url_for('auth.login') }}" class="btn btn-hero">
|
||||||
|
<i class="fas fa-gamepad me-2"></i>去玩场景游戏
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第三个轮播项:智能评估 -->
|
||||||
|
<div class="carousel-item">
|
||||||
|
<img src="https://images.unsplash.com/photo-1509909756405-be0199881695?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80" class="d-block w-100" alt="孩子学习成长">
|
||||||
|
<div class="carousel-overlay"></div>
|
||||||
|
<div class="floating-decorations">
|
||||||
|
<div class="float-emoji">📊</div>
|
||||||
|
<div class="float-emoji">🌟</div>
|
||||||
|
<div class="float-emoji">🏆</div>
|
||||||
|
<div class="float-emoji">💡</div>
|
||||||
|
</div>
|
||||||
|
<div class="carousel-caption d-md-block">
|
||||||
|
<div class="hero-badge">🎯 智能小助手</div>
|
||||||
|
<h1 class="hero-title">
|
||||||
|
<span class="highlight">每次进步</span><br>
|
||||||
|
我们都看得见!
|
||||||
|
</h1>
|
||||||
|
<p class="hero-subtitle">
|
||||||
|
AI老师会仔细听你说的每一句话,告诉你哪里说得棒棒的,
|
||||||
|
哪里还能更棒!就像有个贴心的小伙伴在身边~
|
||||||
|
</p>
|
||||||
|
<a href="{{ url_for('main.dashboard') if current_user.is_authenticated else url_for('auth.login') }}" class="btn btn-hero">
|
||||||
|
<i class="fas fa-chart-line me-2"></i>看看我的成长
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 控制器 -->
|
||||||
|
<button class="carousel-control-prev" type="button" data-bs-target="#heroCarousel" data-bs-slide="prev">
|
||||||
|
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||||
|
<span class="visually-hidden">Previous</span>
|
||||||
|
</button>
|
||||||
|
<button class="carousel-control-next" type="button" data-bs-target="#heroCarousel" data-bs-slide="next">
|
||||||
|
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||||
|
<span class="visually-hidden">Next</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 波浪分隔符 -->
|
||||||
|
<div class="wavy-divider"></div>
|
||||||
|
|
||||||
|
<!-- 2. 特色功能 -->
|
||||||
|
<section class="page-section bg-light-kid">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-title">
|
||||||
|
<h2>为什么选择我们?</h2>
|
||||||
|
<p>创新技术 × 教育理念 = 完美的学习体验</p>
|
||||||
|
</div>
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-6 col-lg-3">
|
||||||
|
<div class="kid-feature-card">
|
||||||
|
<div class="icon-bubble" style="background-color: #e3f2fd;"><i class="fas fa-microphone-alt text-primary"></i></div>
|
||||||
|
<h5>语音克隆技术</h5>
|
||||||
|
<p>克隆孩子的声音,听到"自己"说话,激发学习兴趣。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 col-lg-3">
|
||||||
|
<div class="kid-feature-card">
|
||||||
|
<div class="icon-bubble" style="background-color: #e0f7fa;"><i class="fas fa-robot" style="color:#00BCD4;"></i></div>
|
||||||
|
<h5>智能对话系统</h5>
|
||||||
|
<p>AI老师提供个性化、有趣的对话学习体验。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 col-lg-3">
|
||||||
|
<div class="kid-feature-card">
|
||||||
|
<div class="icon-bubble" style="background-color: #fffde7;"><i class="fas fa-chart-line" style="color:#FFC107;"></i></div>
|
||||||
|
<h5>科学评估体系</h5>
|
||||||
|
<p>基于官方指南的四维度评估,科学跟踪进展。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 col-lg-3">
|
||||||
|
<div class="kid-feature-card">
|
||||||
|
<div class="icon-bubble" style="background-color: #fbe9e7;"><i class="fas fa-gamepad" style="color:#FF5722;"></i></div>
|
||||||
|
<h5>场景化学习</h5>
|
||||||
|
<p>丰富的生活场景,在真实对话中提升语言能力。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 3. 学习场景展示 -->
|
||||||
|
<section class="page-section">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-title">
|
||||||
|
<h2>丰富的学习场景</h2>
|
||||||
|
<p>在游戏中学习,在对话中成长</p>
|
||||||
|
</div>
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="scenario-card-new">
|
||||||
|
<div class="scenario-icon"><i class="fas fa-handshake"></i></div>
|
||||||
|
<h5>社交互动</h5>
|
||||||
|
<p>学习如何交朋友、邀请游戏,培养社交语言技能。</p>
|
||||||
|
<div class="tags"><span class="tag">交朋友</span><span class="tag">团队合作</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="scenario-card-new">
|
||||||
|
<div class="scenario-icon" style="background-color: #e8f5e9; color: #4CAF50;"><i class="fas fa-utensils"></i></div>
|
||||||
|
<h5>日常生活</h5>
|
||||||
|
<p>模拟餐厅点餐、超市购物等真实生活情景。</p>
|
||||||
|
<div class="tags"><span class="tag">点餐</span><span class="tag">购物</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="scenario-card-new">
|
||||||
|
<div class="scenario-icon" style="background-color: #fff3e0; color: #FB8C00;"><i class="fas fa-puzzle-piece"></i></div>
|
||||||
|
<h5>游戏娱乐</h5>
|
||||||
|
<p>在角色扮演和趣味问答中,快乐地组织语言。</p>
|
||||||
|
<div class="tags"><span class="tag">益智游戏</span><span class="tag">角色扮演</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 4. 行动号召 -->
|
||||||
|
<section class="page-section cta-section-kid">
|
||||||
|
<div class="container text-center">
|
||||||
|
<h2>准备好开始奇妙的语言之旅了吗?</h2>
|
||||||
|
<p class="mb-4">立即加入我们,让孩子在快乐中成长,在对话中发现语言的魅力!</p>
|
||||||
|
{% if not current_user.is_authenticated %}
|
||||||
|
<a href="{{ url_for('auth.register') }}" class="btn btn-lg btn-kid-accent shadow-lg">
|
||||||
|
<i class="fas fa-rocket me-2"></i>立即开始免费体验
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
50
config/__init__.py
Normal file
50
config/__init__.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import os
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
# 基础配置
|
||||||
|
SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-secret-key-here-change-in-production'
|
||||||
|
|
||||||
|
# 数据库配置
|
||||||
|
MYSQL_HOST = os.environ.get('MYSQL_HOST', '119.91.236.167')
|
||||||
|
MYSQL_PORT = int(os.environ.get('MYSQL_PORT', 3306))
|
||||||
|
MYSQL_USER = os.environ.get('MYSQL_USER', 'Language_learning')
|
||||||
|
MYSQL_PASSWORD = os.environ.get('MYSQL_PASSWORD', 'cosyvoice')
|
||||||
|
MYSQL_DB = os.environ.get('MYSQL_DB', 'language_learning')
|
||||||
|
|
||||||
|
SQLALCHEMY_DATABASE_URI = f'mysql+pymysql://{MYSQL_USER}:{MYSQL_PASSWORD}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DB}?charset=utf8mb4'
|
||||||
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
SQLALCHEMY_ENGINE_OPTIONS = {
|
||||||
|
'pool_pre_ping': True,
|
||||||
|
'pool_recycle': 300,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 邮箱配置
|
||||||
|
EMAIL_HOST = os.environ.get('EMAIL_HOST', 'smtp.qq.com')
|
||||||
|
EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 587))
|
||||||
|
EMAIL_ENCRYPTION = os.environ.get('EMAIL_ENCRYPTION', 'starttls')
|
||||||
|
EMAIL_USERNAME = os.environ.get('EMAIL_USERNAME', '3399560459@qq.com')
|
||||||
|
EMAIL_PASSWORD = os.environ.get('EMAIL_PASSWORD', 'fzwhyirhbqdzcjgf')
|
||||||
|
EMAIL_FROM = os.environ.get('EMAIL_FROM', '3399560459@qq.com')
|
||||||
|
EMAIL_FROM_NAME = os.environ.get('EMAIL_FROM_NAME', 'BOOKSYSTEM_OFFICIAL')
|
||||||
|
|
||||||
|
# Session配置
|
||||||
|
PERMANENT_SESSION_LIFETIME = timedelta(hours=24)
|
||||||
|
|
||||||
|
# 验证码配置
|
||||||
|
VERIFICATION_CODE_EXPIRE_MINUTES = 5
|
||||||
|
|
||||||
|
# 文件上传配置
|
||||||
|
MAX_CONTENT_LENGTH = 100 * 1024 * 1024 # 100MB
|
||||||
|
|
||||||
|
class DevelopmentConfig(Config):
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
class ProductionConfig(Config):
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
config = {
|
||||||
|
'development': DevelopmentConfig,
|
||||||
|
'production': ProductionConfig,
|
||||||
|
'default': DevelopmentConfig
|
||||||
|
}
|
||||||
0
logs/.gitkeep
Normal file
0
logs/.gitkeep
Normal file
11
requirements.txt
Normal file
11
requirements.txt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
Flask==2.3.3
|
||||||
|
Flask-SQLAlchemy==3.0.5
|
||||||
|
Flask-Login==0.6.3
|
||||||
|
Flask-WTF==1.1.1
|
||||||
|
WTForms==3.0.1
|
||||||
|
PyMySQL==1.1.0
|
||||||
|
bcrypt==4.0.1
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
email-validator==2.0.0
|
||||||
|
cryptography==41.0.4
|
||||||
|
Werkzeug==2.3.7
|
||||||
133
run.py
Normal file
133
run.py
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from flask import Flask
|
||||||
|
from app import create_app, db
|
||||||
|
from app.models import User, EmailVerification
|
||||||
|
|
||||||
|
# 配置日志
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s %(levelname)s %(name)s %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler('logs/app.log'),
|
||||||
|
logging.StreamHandler()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建Flask应用
|
||||||
|
app = create_app(os.environ.get('FLASK_ENV', 'development'))
|
||||||
|
|
||||||
|
# 创建CLI命令
|
||||||
|
@app.cli.command()
|
||||||
|
def init_db():
|
||||||
|
"""初始化数据库"""
|
||||||
|
try:
|
||||||
|
# 创建所有表
|
||||||
|
with app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
print("✅ 数据库表创建成功")
|
||||||
|
|
||||||
|
# 创建默认数据
|
||||||
|
create_default_data()
|
||||||
|
print("✅ 默认数据创建成功")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 数据库初始化失败: {str(e)}")
|
||||||
|
|
||||||
|
@app.cli.command()
|
||||||
|
def drop_db():
|
||||||
|
"""删除所有数据库表"""
|
||||||
|
try:
|
||||||
|
with app.app_context():
|
||||||
|
db.drop_all()
|
||||||
|
print("✅ 数据库表删除成功")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 数据库表删除失败: {str(e)}")
|
||||||
|
|
||||||
|
@app.cli.command()
|
||||||
|
def reset_db():
|
||||||
|
"""重置数据库"""
|
||||||
|
try:
|
||||||
|
with app.app_context():
|
||||||
|
db.drop_all()
|
||||||
|
db.create_all()
|
||||||
|
create_default_data()
|
||||||
|
print("✅ 数据库重置成功")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 数据库重置失败: {str(e)}")
|
||||||
|
|
||||||
|
def create_default_data():
|
||||||
|
"""创建默认数据"""
|
||||||
|
with app.app_context():
|
||||||
|
# 检查是否已有数据
|
||||||
|
if User.query.first():
|
||||||
|
print("⚠️ 数据库已有数据,跳过默认数据创建")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 可以在这里创建测试用户或其他默认数据
|
||||||
|
print("ℹ️ 暂无默认数据需要创建")
|
||||||
|
|
||||||
|
@app.cli.command()
|
||||||
|
def test_email():
|
||||||
|
"""测试邮件发送功能"""
|
||||||
|
try:
|
||||||
|
from utils import send_verification_email
|
||||||
|
|
||||||
|
test_email = input("请输入测试邮箱地址: ").strip()
|
||||||
|
if not test_email:
|
||||||
|
print("❌ 邮箱地址不能为空")
|
||||||
|
return
|
||||||
|
|
||||||
|
test_code = "123456"
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
success = send_verification_email(test_email, test_code)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print(f"✅ 测试邮件发送成功到: {test_email}")
|
||||||
|
else:
|
||||||
|
print("❌ 测试邮件发送失败")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 邮件测试失败: {str(e)}")
|
||||||
|
|
||||||
|
@app.cli.command()
|
||||||
|
def clean_expired_codes():
|
||||||
|
"""清理过期的验证码"""
|
||||||
|
try:
|
||||||
|
with app.app_context():
|
||||||
|
expired_codes = EmailVerification.query.filter(
|
||||||
|
EmailVerification.expires_at < datetime.utcnow()
|
||||||
|
)
|
||||||
|
count = expired_codes.count()
|
||||||
|
expired_codes.delete()
|
||||||
|
db.session.commit()
|
||||||
|
print(f"✅ 清理了 {count} 个过期验证码")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 清理过期验证码失败: {str(e)}")
|
||||||
|
|
||||||
|
@app.shell_context_processor
|
||||||
|
def make_shell_context():
|
||||||
|
"""为flask shell命令提供上下文"""
|
||||||
|
return {
|
||||||
|
'db': db,
|
||||||
|
'User': User,
|
||||||
|
'EmailVerification': EmailVerification
|
||||||
|
}
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# 确保日志目录存在
|
||||||
|
os.makedirs('logs', exist_ok=True)
|
||||||
|
|
||||||
|
# 运行应用
|
||||||
|
port = int(os.environ.get('PORT', 50003))
|
||||||
|
# 强制开启Debug模式,便于前端开发
|
||||||
|
debug = True
|
||||||
|
|
||||||
|
print("🚀 启动儿童语言学习系统...")
|
||||||
|
print(f"📱 访问地址: http://localhost:{port}")
|
||||||
|
print(f"🔧 调试模式: {debug}")
|
||||||
|
print(f"📊 数据库: {app.config['SQLALCHEMY_DATABASE_URI']}")
|
||||||
|
|
||||||
|
app.run(host='0.0.0.0', port=port, debug=debug)
|
||||||
70
setup_python_env.sh
Executable file
70
setup_python_env.sh
Executable file
@ -0,0 +1,70 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "🔧 开始配置Python 3.10.16环境..."
|
||||||
|
|
||||||
|
# 颜色定义
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
echo -e "${YELLOW}📋 步骤1: 检查当前配置${NC}"
|
||||||
|
echo "当前.zshrc中的pyenv配置:"
|
||||||
|
tail -5 ~/.zshrc | grep -E "(PYENV|pyenv)" || echo "未找到pyenv配置"
|
||||||
|
|
||||||
|
echo -e "\n${YELLOW}📋 步骤2: 设置本地Python版本${NC}"
|
||||||
|
pyenv local 3.10.16
|
||||||
|
echo "已设置本地Python版本为3.10.16"
|
||||||
|
|
||||||
|
echo -e "\n${YELLOW}📋 步骤3: 验证Python版本${NC}"
|
||||||
|
echo "当前Python版本: $(python --version)"
|
||||||
|
echo "Python路径: $(which python)"
|
||||||
|
|
||||||
|
echo -e "\n${YELLOW}📋 步骤4: 重新创建虚拟环境${NC}"
|
||||||
|
# 删除现有虚拟环境
|
||||||
|
if [ -d "venv" ]; then
|
||||||
|
echo "删除现有虚拟环境..."
|
||||||
|
rm -rf venv
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 创建新的虚拟环境
|
||||||
|
echo "创建新的虚拟环境..."
|
||||||
|
python -m venv venv
|
||||||
|
|
||||||
|
# 激活虚拟环境
|
||||||
|
echo "激活虚拟环境..."
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
echo "虚拟环境中的Python版本: $(python --version)"
|
||||||
|
|
||||||
|
echo -e "\n${YELLOW}📋 步骤5: 更新.gitignore${NC}"
|
||||||
|
# 检查.gitignore中是否已有venv/
|
||||||
|
if ! grep -q "venv/" .gitignore 2>/dev/null; then
|
||||||
|
echo "venv/" >> .gitignore
|
||||||
|
echo "已添加venv/到.gitignore"
|
||||||
|
else
|
||||||
|
echo ".gitignore中已存在venv/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\n${YELLOW}📋 步骤6: 验证配置${NC}"
|
||||||
|
echo "检查.python-version文件:"
|
||||||
|
if [ -f ".python-version" ]; then
|
||||||
|
echo "✅ .python-version存在,内容: $(cat .python-version)"
|
||||||
|
else
|
||||||
|
echo "❌ .python-version文件不存在"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\n${GREEN}✅ 环境配置完成!${NC}"
|
||||||
|
echo -e "${GREEN}📝 重要提示:${NC}"
|
||||||
|
echo "1. 虚拟环境已激活,Python版本: $(python --version)"
|
||||||
|
echo "2. 每次进入项目目录时,使用: source venv/bin/activate"
|
||||||
|
echo "3. 退出虚拟环境使用: deactivate"
|
||||||
|
echo "4. .python-version文件确保项目目录始终使用Python 3.10.16"
|
||||||
|
|
||||||
|
echo -e "\n${YELLOW}🧪 测试建议:${NC}"
|
||||||
|
echo "重新打开终端,进入项目目录,运行以下命令测试:"
|
||||||
|
echo "cd $(pwd)"
|
||||||
|
echo "python --version # 应该显示Python 3.10.16"
|
||||||
|
echo "source venv/bin/activate"
|
||||||
|
echo "python --version # 应该显示Python 3.10.16"
|
||||||
|
|
||||||
124
utils/__init__.py
Normal file
124
utils/__init__.py
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import smtplib
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.utils import formataddr
|
||||||
|
from flask import current_app
|
||||||
|
import logging
|
||||||
|
|
||||||
|
def _send_email(recipient, subject, html_body):
|
||||||
|
"""
|
||||||
|
统一的邮件发送内部函数.
|
||||||
|
:param recipient: 收件人邮箱
|
||||||
|
:param subject: 邮件主题
|
||||||
|
:param html_body: HTML格式的邮件内容
|
||||||
|
:return: True if success, False otherwise.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 创建邮件对象
|
||||||
|
msg = MIMEMultipart()
|
||||||
|
|
||||||
|
# 使用 formataddr 来正确编码包含非ASCII字符(如中文)的发件人名称
|
||||||
|
msg['From'] = formataddr((
|
||||||
|
current_app.config['EMAIL_FROM_NAME'],
|
||||||
|
current_app.config['EMAIL_FROM']
|
||||||
|
))
|
||||||
|
|
||||||
|
msg['To'] = recipient
|
||||||
|
msg['Subject'] = subject
|
||||||
|
|
||||||
|
msg.attach(MIMEText(html_body, 'html', 'utf-8'))
|
||||||
|
|
||||||
|
# 发送邮件
|
||||||
|
server = smtplib.SMTP(current_app.config['EMAIL_HOST'], current_app.config['EMAIL_PORT'])
|
||||||
|
if current_app.config['EMAIL_ENCRYPTION'] == 'starttls':
|
||||||
|
server.starttls()
|
||||||
|
server.login(current_app.config['EMAIL_USERNAME'], current_app.config['EMAIL_PASSWORD'])
|
||||||
|
server.send_message(msg)
|
||||||
|
server.quit()
|
||||||
|
|
||||||
|
current_app.logger.info(f"成功发送邮件到: {recipient}, 主题: {subject}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# 统一记录错误日志
|
||||||
|
current_app.logger.error(f"发送邮件到 {recipient} 失败 (主题: {subject}): {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def send_verification_email(email, verification_code):
|
||||||
|
"""发送注册验证码邮件"""
|
||||||
|
subject = '【儿童语言学习系统】邮箱验证码'
|
||||||
|
body = f"""
|
||||||
|
<html>
|
||||||
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||||
|
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||||
|
<div style="text-align: center; margin-bottom: 30px;">
|
||||||
|
<h2 style="color: #4a90e2;">儿童语言学习系统</h2>
|
||||||
|
<div style="height: 3px; background: linear-gradient(to right, #4a90e2, #7b68ee); margin: 10px auto; width: 100px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background: #f8f9fa; padding: 25px; border-radius: 10px; margin: 20px 0;">
|
||||||
|
<h3 style="color: #2c3e50; margin-bottom: 15px;">您的验证码</h3>
|
||||||
|
<div style="background: white; padding: 20px; border-radius: 8px; text-align: center; margin: 15px 0;">
|
||||||
|
<span style="font-size: 32px; font-weight: bold; color: #4a90e2; letter-spacing: 8px;">{verification_code}</span>
|
||||||
|
</div>
|
||||||
|
<p style="color: #666; margin-top: 15px;">
|
||||||
|
<strong>注意:</strong>验证码5分钟内有效,请及时使用。如果不是您本人操作,请忽略此邮件。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background: #fff3cd; padding: 15px; border-radius: 8px; border-left: 4px solid #ffc107;">
|
||||||
|
<h4 style="color: #856404; margin-bottom: 10px;">安全提示</h4>
|
||||||
|
<p style="color: #856404; margin: 0;">
|
||||||
|
为了您的账号安全,请不要将验证码泄露给他人。我们不会主动向您索要验证码。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 30px; text-align: center; color: #6c757d; font-size: 14px;">
|
||||||
|
<p>此邮件由系统自动发送,请勿直接回复</p>
|
||||||
|
<p style="margin-top: 5px;">© 儿童语言学习系统 - 让孩子快乐学语言</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
return _send_email(email, subject, body)
|
||||||
|
|
||||||
|
def send_password_reset_email(email, verification_code):
|
||||||
|
"""发送密码重置邮件"""
|
||||||
|
subject = '【儿童语言学习系统】密码重置验证码'
|
||||||
|
body = f"""
|
||||||
|
<html>
|
||||||
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||||
|
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||||
|
<div style="text-align: center; margin-bottom: 30px;">
|
||||||
|
<h2 style="color: #dc3545;">密码重置</h2>
|
||||||
|
<div style="height: 3px; background: linear-gradient(to right, #dc3545, #fd7e14); margin: 10px auto; width: 100px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background: #f8f9fa; padding: 25px; border-radius: 10px; margin: 20px 0;">
|
||||||
|
<h3 style="color: #2c3e50; margin-bottom: 15px;">密码重置验证码</h3>
|
||||||
|
<div style="background: white; padding: 20px; border-radius: 8px; text-align: center; margin: 15px 0;">
|
||||||
|
<span style="font-size: 32px; font-weight: bold; color: #dc3545; letter-spacing: 8px;">{verification_code}</span>
|
||||||
|
</div>
|
||||||
|
<p style="color: #666; margin-top: 15px;">
|
||||||
|
<strong>注意:</strong>验证码5分钟内有效,请及时使用。如果不是您本人操作,请立即修改密码并联系我们。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background: #f8d7da; padding: 15px; border-radius: 8px; border-left: 4px solid #dc3545;">
|
||||||
|
<h4 style="color: #721c24; margin-bottom: 10px;">重要提醒</h4>
|
||||||
|
<p style="color: #721c24; margin: 0;">
|
||||||
|
如果您没有申请密码重置,请忽略此邮件。为了账号安全,建议您定期更换密码。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 30px; text-align: center; color: #6c757d; font-size: 14px;">
|
||||||
|
<p>此邮件由系统自动发送,请勿直接回复</p>
|
||||||
|
<p style="margin-top: 5px;">© 儿童语言学习系统 - 让孩子快乐学语言</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
# 调用统一的发送函数
|
||||||
|
return _send_email(email, subject, body)
|
||||||
Loading…
x
Reference in New Issue
Block a user