添加注册登录功能
This commit is contained in:
@@ -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')}")
|
||||
|
||||
BIN
src/flask_prompt_master/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
src/flask_prompt_master/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/flask_prompt_master/__pycache__/config.cpython-312.pyc
Normal file
BIN
src/flask_prompt_master/__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
src/flask_prompt_master/forms/__pycache__/forms.cpython-312.pyc
Normal file
BIN
src/flask_prompt_master/forms/__pycache__/forms.cpython-312.pyc
Normal file
Binary file not shown.
@@ -4,5 +4,6 @@
|
||||
"""
|
||||
|
||||
from .models import *
|
||||
from .favorites import Favorite
|
||||
|
||||
__all__ = ['User', 'Prompt', 'Feedback']
|
||||
__all__ = ['User', 'Prompt', 'Feedback', 'Favorite']
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
38
src/flask_prompt_master/models/favorites.py
Normal file
38
src/flask_prompt_master/models/favorites.py
Normal 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
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
Binary file not shown.
BIN
src/flask_prompt_master/routes/__pycache__/auth.cpython-312.pyc
Normal file
BIN
src/flask_prompt_master/routes/__pycache__/auth.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
149
src/flask_prompt_master/routes/auth.py
Normal file
149
src/flask_prompt_master/routes/auth.py
Normal 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
|
||||
})
|
||||
98
src/flask_prompt_master/routes/favorites.py
Normal file
98
src/flask_prompt_master/routes/favorites.py
Normal 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)
|
||||
@@ -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,
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
271
src/flask_prompt_master/services/auth_service.py
Normal file
271
src/flask_prompt_master/services/auth_service.py
Normal 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)}'}
|
||||
212
src/flask_prompt_master/services/favorite_service.py
Normal file
212
src/flask_prompt_master/services/favorite_service.py
Normal 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)}'}
|
||||
149
src/flask_prompt_master/templates/auth/login.html
Normal file
149
src/flask_prompt_master/templates/auth/login.html
Normal 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 %}
|
||||
198
src/flask_prompt_master/templates/auth/register.html
Normal file
198
src/flask_prompt_master/templates/auth/register.html
Normal 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 %}
|
||||
@@ -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>© 2025 提示词大师 - 让AI更好地理解您的需求</p>
|
||||
</footer>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
{% block extra_js %}{% endblock %}
|
||||
<script>
|
||||
// 自动隐藏闪现消息
|
||||
|
||||
393
src/flask_prompt_master/templates/favorites.html
Normal file
393
src/flask_prompt_master/templates/favorites.html
Normal 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 %}
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user