添加注册登录功能

This commit is contained in:
2025-08-29 00:34:40 +08:00
parent 09065f2ce7
commit 2fe3474d9e
3060 changed files with 29217 additions and 87137 deletions

View File

@@ -43,6 +43,14 @@ def create_app(config_class=None):
# 注册蓝图
from src.flask_prompt_master.routes import main_bp
app.register_blueprint(main_bp)
# 注册收藏蓝图
from src.flask_prompt_master.routes.favorites import favorites_bp
app.register_blueprint(favorites_bp)
# 注册认证蓝图
from src.flask_prompt_master.routes.auth import auth_bp
app.register_blueprint(auth_bp)
# 记录应用启动信息
app.logger.info(f"应用启动 - 环境: {os.environ.get('FLASK_ENV', 'development')}")

View File

@@ -4,5 +4,6 @@
"""
from .models import *
from .favorites import Favorite
__all__ = ['User', 'Prompt', 'Feedback']
__all__ = ['User', 'Prompt', 'Feedback', 'Favorite']

View File

@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
from datetime import datetime
from src.flask_prompt_master import db
class Favorite(db.Model):
"""收藏表"""
__tablename__ = 'favorites'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
user_id = db.Column(db.String(50), nullable=False, comment='用户ID可以是IP或session_id')
template_id = db.Column(db.Integer, nullable=True, comment='模板ID')
original_text = db.Column(db.Text, nullable=False, comment='原始输入文本')
generated_prompt = db.Column(db.Text, nullable=False, comment='生成的提示词')
system_prompt = db.Column(db.Text, nullable=True, comment='系统提示词')
category = db.Column(db.String(50), nullable=True, comment='分类')
industry = db.Column(db.String(50), nullable=True, comment='行业')
profession = db.Column(db.String(50), nullable=True, comment='职业')
tags = db.Column(db.String(500), nullable=True, comment='标签JSON格式')
notes = db.Column(db.Text, nullable=True, comment='用户备注')
created_time = db.Column(db.DateTime, default=datetime.now, comment='创建时间')
updated_time = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now, comment='更新时间')
def to_dict(self):
"""转换为字典"""
return {
'id': self.id,
'template_id': self.template_id,
'original_text': self.original_text,
'generated_prompt': self.generated_prompt,
'system_prompt': self.system_prompt,
'category': self.category,
'industry': self.industry,
'profession': self.profession,
'tags': self.tags,
'notes': self.notes,
'created_time': self.created_time.strftime('%Y-%m-%d %H:%M:%S') if self.created_time else None,
'updated_time': self.updated_time.strftime('%Y-%m-%d %H:%M:%S') if self.updated_time else None
}

View File

@@ -6,21 +6,16 @@ class User(db.Model):
uid = db.Column(db.Integer, primary_key=True)
nickname = db.Column(db.String(100), nullable=False)
mobile = db.Column(db.String(20), nullable=True)
email = db.Column(db.String(100), nullable=True)
sex = db.Column(db.Integer, nullable=False, default=0)
avatar = db.Column(db.String(64), nullable=True)
login_name = db.Column(db.String(20), nullable=False, unique=True)
mobile = db.Column(db.String(20), nullable=False)
email = db.Column(db.String(100), nullable=False)
sex = db.Column(db.Integer, nullable=False)
avatar = db.Column(db.String(64), nullable=False)
login_name = db.Column(db.String(20), nullable=False)
login_pwd = db.Column(db.String(32), nullable=False)
login_salt = db.Column(db.String(32), nullable=False)
status = db.Column(db.Integer, nullable=False, default=1)
openid = db.Column(db.String(64), unique=True)
session_key = db.Column(db.String(64))
unionid = db.Column(db.String(64), unique=True)
wx_nickname = db.Column(db.String(100))
wx_avatar = db.Column(db.String(255))
updated_time = db.Column(db.DateTime, default=datetime.utcnow)
created_time = db.Column(db.DateTime, default=datetime.utcnow)
status = db.Column(db.Integer, nullable=False)
updated_time = db.Column(db.DateTime)
created_time = db.Column(db.DateTime)
prompts = db.relationship('Prompt', backref='author', lazy='dynamic')
feedbacks = db.relationship('Feedback', backref='author', lazy='dynamic')

View File

@@ -0,0 +1,149 @@
# -*- coding: utf-8 -*-
"""
用户认证路由
"""
from flask import Blueprint, request, jsonify, render_template, session, redirect, url_for
from src.flask_prompt_master.services.auth_service import AuthService
from functools import wraps
auth_bp = Blueprint('auth', __name__)
def login_required(f):
"""登录验证装饰器"""
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
return jsonify({'success': False, 'message': '请先登录'}), 401
return f(*args, **kwargs)
return decorated_function
@auth_bp.route('/register', methods=['GET'])
def register_page():
"""注册页面"""
return render_template('auth/register.html')
@auth_bp.route('/login', methods=['GET'])
def login_page():
"""登录页面"""
return render_template('auth/login.html')
@auth_bp.route('/profile', methods=['GET'])
@login_required
def profile_page():
"""个人资料页面"""
return render_template('auth/profile.html')
@auth_bp.route('/api/register', methods=['POST'])
def register():
"""用户注册API"""
data = request.get_json()
login_name = data.get('login_name', '').strip()
login_pwd = data.get('login_pwd', '').strip()
nickname = data.get('nickname', '').strip()
email = data.get('email', '').strip() if data.get('email') else None
mobile = data.get('mobile', '').strip() if data.get('mobile') else None
sex = data.get('sex', 0)
# 验证必填字段
if not login_name or not login_pwd or not nickname:
return jsonify({'success': False, 'message': '请填写必填字段'})
result = AuthService.register(login_name, login_pwd, nickname, email, mobile, sex)
if result['success']:
# 注册成功后自动登录
session['user_id'] = result['user_id']
session['nickname'] = result['nickname']
return jsonify(result)
@auth_bp.route('/api/login', methods=['POST'])
def login():
"""用户登录API"""
data = request.get_json()
login_name = data.get('login_name', '').strip()
login_pwd = data.get('login_pwd', '').strip()
if not login_name or not login_pwd:
return jsonify({'success': False, 'message': '请填写用户名和密码'})
result = AuthService.login(login_name, login_pwd)
if result['success']:
# 登录成功保存用户信息到session
user = result['user']
session['user_id'] = user['uid']
session['nickname'] = user['nickname']
session['login_name'] = user['login_name']
return jsonify(result)
@auth_bp.route('/api/logout', methods=['POST'])
def logout():
"""用户登出API"""
session.clear()
return jsonify({'success': True, 'message': '登出成功'})
@auth_bp.route('/api/profile', methods=['GET'])
@login_required
def get_profile():
"""获取个人资料API"""
user_id = session['user_id']
result = AuthService.get_user_by_id(user_id)
return jsonify(result)
@auth_bp.route('/api/profile', methods=['PUT'])
@login_required
def update_profile():
"""更新个人资料API"""
user_id = session['user_id']
data = request.get_json()
nickname = data.get('nickname', '').strip() if data.get('nickname') else None
email = data.get('email', '').strip() if data.get('email') else None
mobile = data.get('mobile', '').strip() if data.get('mobile') else None
sex = data.get('sex')
result = AuthService.update_profile(user_id, nickname, email, mobile, sex)
if result['success'] and nickname:
session['nickname'] = nickname
return jsonify(result)
@auth_bp.route('/api/change-password', methods=['POST'])
@login_required
def change_password():
"""修改密码API"""
user_id = session['user_id']
data = request.get_json()
old_password = data.get('old_password', '').strip()
new_password = data.get('new_password', '').strip()
if not old_password or not new_password:
return jsonify({'success': False, 'message': '请填写原密码和新密码'})
result = AuthService.change_password(user_id, old_password, new_password)
return jsonify(result)
@auth_bp.route('/api/check-login', methods=['GET'])
def check_login():
"""检查登录状态API"""
if 'user_id' in session:
return jsonify({
'success': True,
'logged_in': True,
'user': {
'user_id': session['user_id'],
'nickname': session.get('nickname'),
'login_name': session.get('login_name')
}
})
else:
return jsonify({
'success': True,
'logged_in': False
})

View File

@@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
from flask import Blueprint, request, jsonify, render_template
from src.flask_prompt_master.services.favorite_service import FavoriteService
favorites_bp = Blueprint('favorites', __name__)
@favorites_bp.route('/favorites', methods=['GET'])
def favorites_page():
"""收藏页面"""
return render_template('favorites.html')
@favorites_bp.route('/api/favorites', methods=['GET'])
def get_favorites():
"""获取收藏列表"""
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
search = request.args.get('search', '')
category = request.args.get('category', 'all')
result = FavoriteService.get_favorites(
request, page, per_page, search, category
)
return jsonify(result)
@favorites_bp.route('/api/favorites', methods=['POST'])
def add_favorite():
"""添加收藏"""
data = request.get_json()
template_id = data.get('template_id')
original_text = data.get('original_text')
generated_prompt = data.get('generated_prompt')
system_prompt = data.get('system_prompt')
category = data.get('category')
industry = data.get('industry')
profession = data.get('profession')
tags = data.get('tags')
notes = data.get('notes')
if not original_text or not generated_prompt:
return jsonify({'success': False, 'message': '缺少必要参数'})
result = FavoriteService.add_favorite(
request, template_id, original_text, generated_prompt,
system_prompt, category, industry, profession, tags, notes
)
return jsonify(result)
@favorites_bp.route('/api/favorites/<int:favorite_id>', methods=['GET'])
def get_favorite_detail(favorite_id):
"""获取收藏详情"""
result = FavoriteService.get_favorite_by_id(request, favorite_id)
return jsonify(result)
@favorites_bp.route('/api/favorites/<int:favorite_id>', methods=['PUT'])
def update_favorite(favorite_id):
"""更新收藏"""
data = request.get_json()
notes = data.get('notes')
tags = data.get('tags')
result = FavoriteService.update_favorite(request, favorite_id, notes, tags)
return jsonify(result)
@favorites_bp.route('/api/favorites/<int:favorite_id>', methods=['DELETE'])
def delete_favorite(favorite_id):
"""删除收藏"""
result = FavoriteService.delete_favorite(request, favorite_id)
return jsonify(result)
@favorites_bp.route('/api/favorites/stats', methods=['GET'])
def get_favorite_stats():
"""获取收藏统计信息"""
result = FavoriteService.get_favorite_stats(request)
return jsonify(result)
@favorites_bp.route('/api/favorites/quick-add', methods=['POST'])
def quick_add_favorite():
"""快速添加收藏(从生成页面调用)"""
data = request.get_json()
template_id = data.get('template_id')
original_text = data.get('original_text')
generated_prompt = data.get('generated_prompt')
system_prompt = data.get('system_prompt')
category = data.get('category')
if not original_text or not generated_prompt:
return jsonify({'success': False, 'message': '缺少必要参数'})
result = FavoriteService.add_favorite(
request, template_id, original_text, generated_prompt,
system_prompt, category
)
return jsonify(result)

View File

@@ -139,7 +139,7 @@ def index():
return render_template('generate.html', form=form, prompt=prompt, templates=templates,
get_template_icon=get_template_icon, industries=industries,
professions=professions, categories=categories,
sub_categories=sub_categories)
sub_categories=sub_categories, selected_template_id=template_id)
return render_template('generate.html', form=form, prompt=None, templates=templates,
get_template_icon=get_template_icon, industries=industries,
professions=professions, categories=categories,

View File

@@ -0,0 +1,271 @@
# -*- coding: utf-8 -*-
"""
用户认证服务
"""
import hashlib
import os
import re
from datetime import datetime, timedelta
from src.flask_prompt_master import db
from src.flask_prompt_master.models.models import User
class AuthService:
"""用户认证服务类"""
@staticmethod
def generate_salt():
"""生成随机盐值"""
return os.urandom(16).hex()
@staticmethod
def hash_password(password, salt):
"""使用盐值加密密码"""
return hashlib.md5((password + salt).encode('utf-8')).hexdigest()
@staticmethod
def validate_password(password):
"""验证密码强度"""
if len(password) < 6:
return False, "密码长度至少6位"
return True, "密码格式正确"
@staticmethod
def validate_email(email):
"""验证邮箱格式"""
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if not re.match(pattern, email):
return False, "邮箱格式不正确"
return True, "邮箱格式正确"
@staticmethod
def validate_mobile(mobile):
"""验证手机号格式"""
pattern = r'^1[3-9]\d{9}$'
if not re.match(pattern, mobile):
return False, "手机号格式不正确"
return True, "手机号格式正确"
@staticmethod
def register(login_name, login_pwd, nickname, email=None, mobile=None, sex=0):
"""用户注册"""
try:
# 验证用户名
if len(login_name) < 3:
return {'success': False, 'message': '用户名长度至少3位'}
if len(login_name) > 20:
return {'success': False, 'message': '用户名长度不能超过20位'}
# 检查用户名是否已存在
existing_user = User.query.filter_by(login_name=login_name).first()
if existing_user:
return {'success': False, 'message': '用户名已存在'}
# 验证密码
is_valid, msg = AuthService.validate_password(login_pwd)
if not is_valid:
return {'success': False, 'message': msg}
# 验证邮箱
if email:
is_valid, msg = AuthService.validate_email(email)
if not is_valid:
return {'success': False, 'message': msg}
# 检查邮箱是否已存在
existing_email = User.query.filter_by(email=email).first()
if existing_email:
return {'success': False, 'message': '邮箱已被注册'}
# 验证手机号
if mobile:
is_valid, msg = AuthService.validate_mobile(mobile)
if not is_valid:
return {'success': False, 'message': msg}
# 检查手机号是否已存在
existing_mobile = User.query.filter_by(mobile=mobile).first()
if existing_mobile:
return {'success': False, 'message': '手机号已被注册'}
# 生成盐值和加密密码
salt = AuthService.generate_salt()
hashed_password = AuthService.hash_password(login_pwd, salt)
# 创建用户
user = User(
login_name=login_name,
login_pwd=hashed_password,
login_salt=salt,
nickname=nickname,
email=email or '', # 提供默认值
mobile=mobile or '', # 提供默认值
sex=sex,
avatar='', # 提供默认值
status=1,
created_time=datetime.now(),
updated_time=datetime.now()
)
db.session.add(user)
db.session.commit()
return {
'success': True,
'message': '注册成功',
'user_id': user.uid,
'nickname': user.nickname
}
except Exception as e:
db.session.rollback()
return {'success': False, 'message': f'注册失败: {str(e)}'}
@staticmethod
def login(login_name, login_pwd):
"""用户登录"""
try:
# 查找用户
user = User.query.filter_by(login_name=login_name).first()
if not user:
return {'success': False, 'message': '用户名或密码错误'}
# 检查用户状态
if user.status != 1:
return {'success': False, 'message': '账户已被禁用'}
# 验证密码
hashed_password = AuthService.hash_password(login_pwd, user.login_salt)
if hashed_password != user.login_pwd:
return {'success': False, 'message': '用户名或密码错误'}
# 更新最后登录时间
user.updated_time = datetime.now()
db.session.commit()
return {
'success': True,
'message': '登录成功',
'user': {
'uid': user.uid,
'login_name': user.login_name,
'nickname': user.nickname,
'email': user.email,
'mobile': user.mobile,
'sex': user.sex,
'avatar': user.avatar
}
}
except Exception as e:
db.session.rollback()
return {'success': False, 'message': f'登录失败: {str(e)}'}
@staticmethod
def get_user_by_id(user_id):
"""根据ID获取用户信息"""
try:
user = User.query.get(user_id)
if not user:
return {'success': False, 'message': '用户不存在'}
return {
'success': True,
'user': {
'uid': user.uid,
'login_name': user.login_name,
'nickname': user.nickname,
'email': user.email,
'mobile': user.mobile,
'sex': user.sex,
'avatar': user.avatar,
'status': user.status,
'created_time': user.created_time.strftime('%Y-%m-%d %H:%M:%S') if user.created_time else None
}
}
except Exception as e:
return {'success': False, 'message': f'获取用户信息失败: {str(e)}'}
@staticmethod
def update_profile(user_id, nickname=None, email=None, mobile=None, sex=None):
"""更新用户资料"""
try:
user = User.query.get(user_id)
if not user:
return {'success': False, 'message': '用户不存在'}
# 验证邮箱
if email and email != user.email:
is_valid, msg = AuthService.validate_email(email)
if not is_valid:
return {'success': False, 'message': msg}
# 检查邮箱是否已被其他用户使用
existing_email = User.query.filter_by(email=email).first()
if existing_email and existing_email.uid != user_id:
return {'success': False, 'message': '邮箱已被其他用户使用'}
# 验证手机号
if mobile and mobile != user.mobile:
is_valid, msg = AuthService.validate_mobile(mobile)
if not is_valid:
return {'success': False, 'message': msg}
# 检查手机号是否已被其他用户使用
existing_mobile = User.query.filter_by(mobile=mobile).first()
if existing_mobile and existing_mobile.uid != user_id:
return {'success': False, 'message': '手机号已被其他用户使用'}
# 更新信息
if nickname is not None:
user.nickname = nickname
if email is not None:
user.email = email
if mobile is not None:
user.mobile = mobile
if sex is not None:
user.sex = sex
user.updated_time = datetime.now()
db.session.commit()
return {'success': True, 'message': '资料更新成功'}
except Exception as e:
db.session.rollback()
return {'success': False, 'message': f'更新失败: {str(e)}'}
@staticmethod
def change_password(user_id, old_password, new_password):
"""修改密码"""
try:
user = User.query.get(user_id)
if not user:
return {'success': False, 'message': '用户不存在'}
# 验证旧密码
old_hashed = AuthService.hash_password(old_password, user.login_salt)
if old_hashed != user.login_pwd:
return {'success': False, 'message': '原密码错误'}
# 验证新密码
is_valid, msg = AuthService.validate_password(new_password)
if not is_valid:
return {'success': False, 'message': msg}
# 生成新的盐值和密码
new_salt = AuthService.generate_salt()
new_hashed = AuthService.hash_password(new_password, new_salt)
# 更新密码
user.login_pwd = new_hashed
user.login_salt = new_salt
user.updated_time = datetime.now()
db.session.commit()
return {'success': True, 'message': '密码修改成功'}
except Exception as e:
db.session.rollback()
return {'success': False, 'message': f'密码修改失败: {str(e)}'}

View File

@@ -0,0 +1,212 @@
# -*- coding: utf-8 -*-
import json
from datetime import datetime
from src.flask_prompt_master import db
from src.flask_prompt_master.models.favorites import Favorite
class FavoriteService:
"""收藏服务类"""
@staticmethod
def get_user_id(request):
"""获取用户ID"""
from flask import session
# 优先使用登录用户的ID
if 'user_id' in session:
return str(session['user_id'])
# 如果没有登录使用IP地址作为临时用户ID
return request.remote_addr
@staticmethod
def add_favorite(request, template_id, original_text, generated_prompt,
system_prompt=None, category=None, industry=None, profession=None,
tags=None, notes=None):
"""添加收藏"""
try:
user_id = FavoriteService.get_user_id(request)
# 检查是否已存在相同的收藏
existing = Favorite.query.filter_by(
user_id=user_id,
original_text=original_text,
generated_prompt=generated_prompt
).first()
if existing:
return {'success': False, 'message': '该提示词已收藏'}
# 处理标签
if tags and isinstance(tags, list):
tags = json.dumps(tags, ensure_ascii=False)
favorite = Favorite(
user_id=user_id,
template_id=template_id,
original_text=original_text,
generated_prompt=generated_prompt,
system_prompt=system_prompt,
category=category,
industry=industry,
profession=profession,
tags=tags,
notes=notes
)
db.session.add(favorite)
db.session.commit()
return {'success': True, 'message': '收藏成功', 'id': favorite.id}
except Exception as e:
db.session.rollback()
return {'success': False, 'message': f'收藏失败: {str(e)}'}
@staticmethod
def get_favorites(request, page=1, per_page=10, search=None, category=None):
"""获取收藏列表"""
try:
user_id = FavoriteService.get_user_id(request)
query = Favorite.query.filter_by(user_id=user_id)
# 搜索功能
if search:
query = query.filter(
db.or_(
Favorite.original_text.contains(search),
Favorite.generated_prompt.contains(search),
Favorite.notes.contains(search)
)
)
# 分类筛选
if category and category != 'all':
query = query.filter_by(category=category)
# 按时间倒序排列
query = query.order_by(Favorite.created_time.desc())
# 分页
pagination = query.paginate(
page=page,
per_page=per_page,
error_out=False
)
favorites = [favorite.to_dict() for favorite in pagination.items]
return {
'success': True,
'data': favorites,
'total': pagination.total,
'pages': pagination.pages,
'current_page': page,
'has_prev': pagination.has_prev,
'has_next': pagination.has_next
}
except Exception as e:
return {'success': False, 'message': f'获取收藏失败: {str(e)}'}
@staticmethod
def get_favorite_by_id(request, favorite_id):
"""根据ID获取收藏详情"""
try:
user_id = FavoriteService.get_user_id(request)
favorite = Favorite.query.filter_by(
id=favorite_id,
user_id=user_id
).first()
if not favorite:
return {'success': False, 'message': '收藏不存在'}
return {'success': True, 'data': favorite.to_dict()}
except Exception as e:
return {'success': False, 'message': f'获取收藏详情失败: {str(e)}'}
@staticmethod
def update_favorite(request, favorite_id, notes=None, tags=None):
"""更新收藏"""
try:
user_id = FavoriteService.get_user_id(request)
favorite = Favorite.query.filter_by(
id=favorite_id,
user_id=user_id
).first()
if not favorite:
return {'success': False, 'message': '收藏不存在'}
if notes is not None:
favorite.notes = notes
if tags is not None:
if isinstance(tags, list):
tags = json.dumps(tags, ensure_ascii=False)
favorite.tags = tags
favorite.updated_time = datetime.now()
db.session.commit()
return {'success': True, 'message': '更新成功'}
except Exception as e:
db.session.rollback()
return {'success': False, 'message': f'更新失败: {str(e)}'}
@staticmethod
def delete_favorite(request, favorite_id):
"""删除收藏"""
try:
user_id = FavoriteService.get_user_id(request)
favorite = Favorite.query.filter_by(
id=favorite_id,
user_id=user_id
).first()
if not favorite:
return {'success': False, 'message': '收藏不存在'}
db.session.delete(favorite)
db.session.commit()
return {'success': True, 'message': '删除成功'}
except Exception as e:
db.session.rollback()
return {'success': False, 'message': f'删除失败: {str(e)}'}
@staticmethod
def get_favorite_stats(request):
"""获取收藏统计信息"""
try:
user_id = FavoriteService.get_user_id(request)
# 总收藏数
total_count = Favorite.query.filter_by(user_id=user_id).count()
# 分类统计
category_stats = db.session.query(
Favorite.category,
db.func.count(Favorite.id)
).filter_by(user_id=user_id).group_by(Favorite.category).all()
# 最近收藏
recent_favorites = Favorite.query.filter_by(user_id=user_id)\
.order_by(Favorite.created_time.desc())\
.limit(5).all()
return {
'success': True,
'data': {
'total_count': total_count,
'category_stats': dict(category_stats),
'recent_favorites': [f.to_dict() for f in recent_favorites]
}
}
except Exception as e:
return {'success': False, 'message': f'获取统计信息失败: {str(e)}'}

View File

@@ -0,0 +1,149 @@
{% extends "base.html" %}
{% block title %}用户登录 - 提示词大师{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-4">
<div class="card shadow">
<div class="card-header text-center">
<h4 class="mb-0">用户登录</h4>
</div>
<div class="card-body">
<form id="loginForm">
<div class="mb-3">
<label for="login_name" class="form-label">用户名</label>
<input type="text" class="form-control" id="login_name" name="login_name" required>
</div>
<div class="mb-3">
<label for="login_pwd" class="form-label">密码</label>
<input type="password" class="form-control" id="login_pwd" name="login_pwd" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">登录</button>
</div>
</form>
<div class="text-center mt-3">
<p class="mb-0">还没有账号? <a href="{{ url_for('auth.register_page') }}">立即注册</a></p>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<style>
.message-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
max-width: 400px;
}
.message {
padding: 15px 20px;
margin-bottom: 10px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
animation: slideIn 0.3s ease-out;
font-weight: 500;
}
.message.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.message.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
</style>
<div id="messageContainer" class="message-container"></div>
<script>
function showMessage(message, type = 'info') {
const container = document.getElementById('messageContainer');
const messageDiv = document.createElement('div');
messageDiv.className = `message ${type}`;
messageDiv.textContent = message;
container.appendChild(messageDiv);
// 自动移除消息
setTimeout(() => {
messageDiv.style.animation = 'slideOut 0.3s ease-out';
setTimeout(() => {
if (messageDiv.parentNode) {
messageDiv.parentNode.removeChild(messageDiv);
}
}, 300);
}, 3000);
}
document.getElementById('loginForm').addEventListener('submit', function(e) {
console.log('登录表单提交事件触发');
e.preventDefault();
const formData = {
login_name: document.getElementById('login_name').value,
login_pwd: document.getElementById('login_pwd').value
};
console.log('发送登录请求:', formData);
fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
})
.then(response => response.json())
.then(data => {
if (data.success) {
// 显示成功提示
showMessage('登录成功!正在跳转到首页...', 'success');
// 延迟跳转,让用户看到提示
setTimeout(() => {
window.location.href = '/';
}, 2000);
} else {
showMessage(data.message || '登录失败', 'error');
}
})
.catch(error => {
console.error('登录失败:', error);
showMessage('登录失败,请稍后重试', 'error');
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,198 @@
{% extends "base.html" %}
{% block title %}用户注册 - 提示词大师{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-5">
<div class="card shadow">
<div class="card-header text-center">
<h4 class="mb-0">用户注册</h4>
</div>
<div class="card-body">
<form id="registerForm">
<div class="mb-3">
<label for="login_name" class="form-label">用户名 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="login_name" name="login_name" required>
<div class="form-text">用户名长度3-20位只能包含字母、数字、下划线</div>
</div>
<div class="mb-3">
<label for="nickname" class="form-label">昵称 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="nickname" name="nickname" required>
</div>
<div class="mb-3">
<label for="login_pwd" class="form-label">密码 <span class="text-danger">*</span></label>
<input type="password" class="form-control" id="login_pwd" name="login_pwd" required>
<div class="form-text">密码长度至少6位</div>
</div>
<div class="mb-3">
<label for="confirm_pwd" class="form-label">确认密码 <span class="text-danger">*</span></label>
<input type="password" class="form-control" id="confirm_pwd" name="confirm_pwd" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">邮箱</label>
<input type="email" class="form-control" id="email" name="email">
</div>
<div class="mb-3">
<label for="mobile" class="form-label">手机号</label>
<input type="tel" class="form-control" id="mobile" name="mobile">
</div>
<div class="mb-3">
<label class="form-label">性别</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="sex" id="sex_0" value="0" checked>
<label class="form-check-label" for="sex_0">保密</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="sex" id="sex_1" value="1">
<label class="form-check-label" for="sex_1"></label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="sex" id="sex_2" value="2">
<label class="form-check-label" for="sex_2"></label>
</div>
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">注册</button>
</div>
</form>
<div class="text-center mt-3">
<p class="mb-0">已有账号? <a href="{{ url_for('auth.login_page') }}">立即登录</a></p>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<style>
.message-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
max-width: 400px;
}
.message {
padding: 15px 20px;
margin-bottom: 10px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
animation: slideIn 0.3s ease-out;
font-weight: 500;
}
.message.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.message.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
</style>
<div id="messageContainer" class="message-container"></div>
<script>
function showMessage(message, type = 'info') {
const container = document.getElementById('messageContainer');
const messageDiv = document.createElement('div');
messageDiv.className = `message ${type}`;
messageDiv.textContent = message;
container.appendChild(messageDiv);
// 自动移除消息
setTimeout(() => {
messageDiv.style.animation = 'slideOut 0.3s ease-out';
setTimeout(() => {
if (messageDiv.parentNode) {
messageDiv.parentNode.removeChild(messageDiv);
}
}, 300);
}, 3000);
}
document.getElementById('registerForm').addEventListener('submit', function(e) {
console.log('表单提交事件触发');
e.preventDefault();
const password = document.getElementById('login_pwd').value;
const confirmPassword = document.getElementById('confirm_pwd').value;
console.log('密码验证:', password, confirmPassword);
if (password !== confirmPassword) {
showMessage('两次输入的密码不一致', 'error');
return;
}
const formData = {
login_name: document.getElementById('login_name').value,
login_pwd: password,
nickname: document.getElementById('nickname').value,
email: document.getElementById('email').value || null,
mobile: document.getElementById('mobile').value || null,
sex: parseInt(document.querySelector('input[name="sex"]:checked').value)
};
console.log('发送注册请求:', formData);
fetch('/api/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
})
.then(response => response.json())
.then(data => {
if (data.success) {
// 显示成功提示
showMessage('注册成功!正在跳转到首页...', 'success');
// 延迟跳转,让用户看到提示
setTimeout(() => {
window.location.href = '/';
}, 2000);
} else {
showMessage(data.message || '注册失败', 'error');
}
})
.catch(error => {
console.error('注册失败:', error);
showMessage('注册失败,请稍后重试', 'error');
});
});
</script>
{% endblock %}

View File

@@ -10,6 +10,12 @@
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<style>
/* 全局样式 */
:root {
@@ -68,6 +74,57 @@
font-size: 1.5rem;
}
.nav-links {
display: flex;
gap: 1rem;
align-items: center;
}
.nav-link {
color: var(--text-color);
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 0.5rem;
}
.nav-link:hover {
background: var(--background-color);
color: var(--primary-color);
}
.nav-link i {
font-size: 1rem;
}
.user-menu {
display: flex;
align-items: center;
}
.user-menu .dropdown-toggle {
color: var(--text-color);
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 0.5rem;
}
.user-menu .dropdown-toggle:hover {
background: var(--background-color);
color: var(--primary-color);
}
.user-menu .dropdown-menu {
min-width: 200px;
}
/* 主要内容区域 */
main {
min-height: calc(100vh - 120px);
@@ -118,6 +175,72 @@
}
</style>
{% block extra_css %}{% endblock %}
<script>
// 检查登录状态并更新用户菜单
function updateUserMenu() {
fetch('/api/check-login')
.then(response => response.json())
.then(data => {
const userMenu = document.getElementById('userMenu');
if (data.logged_in) {
userMenu.innerHTML = `
<div class="dropdown">
<a class="dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="fas fa-user"></i>
${data.user.nickname}
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/profile">个人资料</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="logout()">退出登录</a></li>
</ul>
</div>
`;
} else {
userMenu.innerHTML = `
<a href="/login" class="nav-link">
<i class="fas fa-sign-in-alt"></i>
登录
</a>
<a href="/register" class="nav-link">
<i class="fas fa-user-plus"></i>
注册
</a>
`;
}
})
.catch(error => {
console.error('检查登录状态失败:', error);
});
}
function logout() {
fetch('/api/logout', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('退出登录成功');
window.location.reload();
} else {
alert(data.message || '退出登录失败');
}
})
.catch(error => {
console.error('退出登录失败:', error);
alert('退出登录失败,请稍后重试');
});
}
// 页面加载时更新用户菜单
document.addEventListener('DOMContentLoaded', function() {
updateUserMenu();
});
</script>
</head>
<body>
<header>
@@ -126,6 +249,19 @@
<i class="fas fa-magic"></i>
提示词大师
</a>
<div class="nav-links">
<a href="{{ url_for('main.index') }}" class="nav-link">
<i class="fas fa-plus"></i>
生成提示词
</a>
<a href="{{ url_for('favorites.favorites_page') }}" class="nav-link">
<i class="fas fa-heart"></i>
我的收藏
</a>
<div class="user-menu" id="userMenu">
<!-- 用户菜单将通过JavaScript动态加载 -->
</div>
</div>
</nav>
</header>
@@ -147,6 +283,7 @@
<p>&copy; 2025 提示词大师 - 让AI更好地理解您的需求</p>
</footer>
{% block scripts %}{% endblock %}
{% block extra_js %}{% endblock %}
<script>
// 自动隐藏闪现消息

View File

@@ -0,0 +1,393 @@
{% extends "base.html" %}
{% block title %}我的收藏 - 提示词大师{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row">
<!-- 侧边栏 -->
<div class="col-md-3">
<div class="card">
<div class="card-header">
<h5 class="mb-0">收藏统计</h5>
</div>
<div class="card-body">
<div id="stats-container">
<p class="text-muted">加载中...</p>
</div>
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<h5 class="mb-0">分类筛选</h5>
</div>
<div class="card-body">
<select id="category-filter" class="form-select">
<option value="all">全部分类</option>
<option value="专业服务">专业服务</option>
<option value="产品管理">产品管理</option>
<option value="人力资源">人力资源</option>
<option value="传媒娱乐">传媒娱乐</option>
<option value="全栈开发">全栈开发</option>
<option value="公务员考试">公务员考试</option>
<option value="内容创作">内容创作</option>
<option value="农业科技">农业科技</option>
<option value="创意设计">创意设计</option>
</select>
</div>
</div>
</div>
<!-- 主内容区 -->
<div class="col-md-9">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h4 class="mb-0">我的收藏</h4>
<div class="d-flex">
<input type="text" id="search-input" class="form-control me-2" placeholder="搜索收藏...">
<button id="search-btn" class="btn btn-primary">搜索</button>
</div>
</div>
<div class="card-body">
<div id="favorites-container">
<p class="text-muted">加载中...</p>
</div>
<!-- 分页 -->
<nav id="pagination-container" class="mt-3">
<!-- 分页内容 -->
</nav>
</div>
</div>
</div>
</div>
</div>
<!-- 收藏详情模态框 -->
<div class="modal fade" id="favoriteDetailModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">收藏详情</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="favorite-detail-content">
<!-- 详情内容 -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
<button type="button" class="btn btn-danger" id="delete-favorite-btn">删除</button>
</div>
</div>
</div>
</div>
<!-- 编辑备注模态框 -->
<div class="modal fade" id="editNotesModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">编辑备注</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="notes-input" class="form-label">备注</label>
<textarea id="notes-input" class="form-control" rows="3" placeholder="添加备注..."></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="save-notes-btn">保存</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let currentPage = 1;
let currentFavoriteId = null;
// 页面加载完成后执行
$(document).ready(function() {
loadStats();
loadFavorites();
// 搜索按钮点击事件
$('#search-btn').click(function() {
currentPage = 1;
loadFavorites();
});
// 回车搜索
$('#search-input').keypress(function(e) {
if (e.which == 13) {
currentPage = 1;
loadFavorites();
}
});
// 分类筛选变化
$('#category-filter').change(function() {
currentPage = 1;
loadFavorites();
});
});
// 加载统计信息
function loadStats() {
$.get('/api/favorites/stats')
.done(function(response) {
if (response.success) {
const data = response.data;
let statsHtml = `
<div class="text-center">
<h3 class="text-primary">${data.total_count}</h3>
<p class="text-muted">总收藏数</p>
</div>
`;
if (data.recent_favorites && data.recent_favorites.length > 0) {
statsHtml += '<hr><h6>最近收藏</h6>';
data.recent_favorites.forEach(favorite => {
statsHtml += `
<div class="small text-muted mb-1">
${favorite.original_text.substring(0, 30)}...
</div>
`;
});
}
$('#stats-container').html(statsHtml);
}
})
.fail(function() {
$('#stats-container').html('<p class="text-danger">加载统计信息失败</p>');
});
}
// 加载收藏列表
function loadFavorites() {
const search = $('#search-input').val();
const category = $('#category-filter').val();
const params = {
page: currentPage,
per_page: 10,
search: search,
category: category
};
$.get('/api/favorites', params)
.done(function(response) {
if (response.success) {
renderFavorites(response.data);
renderPagination(response);
} else {
$('#favorites-container').html(`<p class="text-danger">${response.message}</p>`);
}
})
.fail(function() {
$('#favorites-container').html('<p class="text-danger">加载收藏失败</p>');
});
}
// 渲染收藏列表
function renderFavorites(favorites) {
if (favorites.length === 0) {
$('#favorites-container').html('<p class="text-muted">暂无收藏</p>');
return;
}
let html = '';
favorites.forEach(favorite => {
html += `
<div class="card mb-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<h6 class="card-title">${escapeHtml(favorite.original_text.substring(0, 50))}...</h6>
<p class="card-text text-muted small">
${escapeHtml(favorite.generated_prompt.substring(0, 100))}...
</p>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
${favorite.category || '未分类'} | ${favorite.created_time}
</small>
<div>
<button class="btn btn-sm btn-outline-primary" onclick="viewDetail(${favorite.id})">
查看详情
</button>
<button class="btn btn-sm btn-outline-secondary" onclick="editNotes(${favorite.id})">
编辑备注
</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;
});
$('#favorites-container').html(html);
}
// 渲染分页
function renderPagination(data) {
if (data.pages <= 1) {
$('#pagination-container').html('');
return;
}
let html = '<ul class="pagination justify-content-center">';
// 上一页
if (data.has_prev) {
html += `<li class="page-item"><a class="page-link" href="#" onclick="goToPage(${data.current_page - 1})">上一页</a></li>`;
}
// 页码
for (let i = 1; i <= data.pages; i++) {
if (i === data.current_page) {
html += `<li class="page-item active"><span class="page-link">${i}</span></li>`;
} else {
html += `<li class="page-item"><a class="page-link" href="#" onclick="goToPage(${i})">${i}</a></li>`;
}
}
// 下一页
if (data.has_next) {
html += `<li class="page-item"><a class="page-link" href="#" onclick="goToPage(${data.current_page + 1})">下一页</a></li>`;
}
html += '</ul>';
$('#pagination-container').html(html);
}
// 跳转到指定页面
function goToPage(page) {
currentPage = page;
loadFavorites();
}
// 查看详情
function viewDetail(favoriteId) {
currentFavoriteId = favoriteId;
$.get(`/api/favorites/${favoriteId}`)
.done(function(response) {
if (response.success) {
const favorite = response.data;
let html = `
<div class="mb-3">
<h6>原始输入</h6>
<div class="border rounded p-3 bg-light">
${escapeHtml(favorite.original_text)}
</div>
</div>
<div class="mb-3">
<h6>生成的提示词</h6>
<div class="border rounded p-3 bg-light">
<pre class="mb-0">${escapeHtml(favorite.generated_prompt)}</pre>
</div>
</div>
<div class="row">
<div class="col-md-6">
<strong>分类:</strong> ${favorite.category || '未分类'}
</div>
<div class="col-md-6">
<strong>创建时间:</strong> ${favorite.created_time}
</div>
</div>
`;
if (favorite.notes) {
html += `
<div class="mt-3">
<h6>备注</h6>
<div class="border rounded p-3 bg-light">
${escapeHtml(favorite.notes)}
</div>
</div>
`;
}
$('#favorite-detail-content').html(html);
$('#favoriteDetailModal').modal('show');
}
});
}
// 编辑备注
function editNotes(favoriteId) {
currentFavoriteId = favoriteId;
$.get(`/api/favorites/${favoriteId}`)
.done(function(response) {
if (response.success) {
$('#notes-input').val(response.data.notes || '');
$('#editNotesModal').modal('show');
}
});
}
// 保存备注
$('#save-notes-btn').click(function() {
const notes = $('#notes-input').val();
$.ajax({
url: `/api/favorites/${currentFavoriteId}`,
method: 'PUT',
contentType: 'application/json',
data: JSON.stringify({ notes: notes })
})
.done(function(response) {
if (response.success) {
$('#editNotesModal').modal('hide');
loadFavorites();
showToast('备注保存成功', 'success');
} else {
showToast(response.message, 'error');
}
});
});
// 删除收藏
$('#delete-favorite-btn').click(function() {
if (confirm('确定要删除这个收藏吗?')) {
$.ajax({
url: `/api/favorites/${currentFavoriteId}`,
method: 'DELETE'
})
.done(function(response) {
if (response.success) {
$('#favoriteDetailModal').modal('hide');
loadFavorites();
loadStats();
showToast('删除成功', 'success');
} else {
showToast(response.message, 'error');
}
});
}
});
// 工具函数
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function showToast(message, type) {
// 简单的提示功能,可以根据需要替换为更好的提示组件
alert(message);
}
</script>
{% endblock %}

View File

@@ -74,7 +74,9 @@
data-subcategory="{{ template.sub_category }}">
<input type="radio" name="template_id" id="template_{{ template.id }}"
value="{{ template.id }}"
{% if template.is_default %}checked{% endif %}>
{% if selected_template_id and selected_template_id|int == template.id %}checked
{% elif not selected_template_id and template.is_default %}checked
{% elif not selected_template_id and loop.first %}checked{% endif %}>
<label for="template_{{ template.id }}" class="template-content">
<div class="template-actions">
<button type="button" class="btn-delete"
@@ -138,6 +140,10 @@
<i class="fas fa-copy"></i>
复制提示词
</button>
<button class="btn btn-favorite" onclick="addToFavorites()">
<i class="fas fa-heart"></i>
收藏
</button>
</div>
</div>
<div class="result-content">
@@ -1024,6 +1030,33 @@ style.textContent = `
.btn-copy i {
margin-right: 6px;
}
.btn-favorite {
padding: 8px 16px;
background: #ff6b6b;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
margin-left: 8px;
}
.btn-favorite:hover {
background: #ff5252;
}
.btn-favorite.btn-success {
background: #4CAF50;
}
.btn-favorite.btn-success:hover {
background: #45a049;
}
.btn-favorite i {
margin-right: 6px;
}
`;
document.head.appendChild(style);
@@ -1231,5 +1264,84 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
});
// 页面加载时自动选择第一个模板(如果没有选中的模板)
document.addEventListener('DOMContentLoaded', function() {
const selectedTemplate = document.querySelector('input[name="template_id"]:checked');
if (!selectedTemplate) {
const firstTemplate = document.querySelector('input[name="template_id"]');
if (firstTemplate) {
firstTemplate.checked = true;
}
}
});
// 收藏功能
function addToFavorites() {
console.log('开始收藏流程...');
// 获取当前选中的模板ID
const selectedTemplate = document.querySelector('input[name="template_id"]:checked');
console.log('选中的模板:', selectedTemplate);
if (!selectedTemplate) {
alert('请先选择一个模板');
return;
}
// 获取用户输入的文本
const userInput = document.querySelector('textarea[name="input_text"]');
console.log('用户输入框:', userInput);
console.log('用户输入内容:', userInput ? userInput.value : 'null');
if (!userInput || !userInput.value.trim()) {
alert('请先输入文本内容');
return;
}
// 获取生成的提示词
const generatedText = document.querySelector('.text-content');
console.log('生成的提示词元素:', generatedText);
console.log('生成的提示词内容:', generatedText ? generatedText.textContent.substring(0, 100) + '...' : 'null');
if (!generatedText || !generatedText.textContent.trim()) {
alert('请先生成提示词');
return;
}
// 获取模板信息
const templateCard = selectedTemplate.closest('.template-card');
const category = templateCard.dataset.category;
const favoriteData = {
template_id: parseInt(selectedTemplate.value),
original_text: userInput.value.trim(),
generated_prompt: generatedText.textContent.trim(),
category: category
};
// 发送收藏请求
fetch('/api/favorites/quick-add', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(favoriteData)
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('收藏成功!');
// 可以在这里更新按钮状态
const favoriteBtn = document.querySelector('.btn-favorite');
favoriteBtn.innerHTML = '<i class="fas fa-heart"></i> 已收藏';
favoriteBtn.classList.add('btn-success');
favoriteBtn.disabled = true;
} else {
alert(data.message || '收藏失败');
}
})
.catch(error => {
console.error('收藏失败:', error);
alert('收藏失败,请稍后重试');
});
}
</script>
{% endblock %}