finish_preview

This commit is contained in:
superlishunqin 2025-07-09 05:22:28 +08:00
parent 5fcd6b7017
commit 5d73776a71
50 changed files with 8376 additions and 579 deletions

View File

@ -1,95 +1,41 @@
"""
Flask应用工厂
"""
from flask import Flask
from flask_mail import Mail
from config.database import init_db
from config.config import Config
from config.database import db
import re
# 初始化邮件服务
mail = Mail()
def create_app(config_name='default'):
def create_app(config_name=None):
app = Flask(__name__)
# 加载配置
app.config.from_object(Config)
# 初始化数据库
db.init_app(app)
# 初始化邮件服务
mail.init_app(app)
# 注册自定义过滤器
register_filters(app)
init_db(app)
# 注册蓝图
register_blueprints(app)
# 创建数据库表
with app.app_context():
try:
db.create_all()
print("✅ 数据库表创建/同步成功")
except Exception as e:
print(f"❌ 数据库表创建失败: {str(e)}")
return app
def register_filters(app):
"""注册自定义过滤器"""
@app.template_filter('nl2br')
def nl2br_filter(text):
"""将换行符转换为HTML <br> 标签"""
if not text:
return ''
# 将换行符替换为 <br> 标签
return text.replace('\n', '<br>')
@app.template_filter('truncate_chars')
def truncate_chars_filter(text, length=50):
"""截断字符串"""
if not text:
return ''
if len(text) <= length:
return text
return text[:length] + '...'
def register_blueprints(app):
"""注册蓝图"""
from app.views.main import main_bp
from app.views.auth import auth_bp
from app.views.main import main_bp
from app.views.user import user_bp
from app.views.admin import admin_bp
from app.views.product import product_bp
from app.views.cart import cart_bp
from app.views.address import address_bp
from app.views.order import order_bp
from app.views.payment import payment_bp
from app.views.admin import admin_bp
from app.views.address import address_bp
from app.views.upload import upload_bp
from app.views.review import review_bp
from app.views.favorite import favorite_bp
from app.views.history import history_bp
app.register_blueprint(main_bp)
app.register_blueprint(auth_bp)
app.register_blueprint(main_bp)
app.register_blueprint(user_bp)
app.register_blueprint(admin_bp)
app.register_blueprint(product_bp)
app.register_blueprint(cart_bp)
app.register_blueprint(address_bp)
app.register_blueprint(order_bp)
app.register_blueprint(payment_bp)
app.register_blueprint(admin_bp)
app.register_blueprint(address_bp)
app.register_blueprint(upload_bp)
app.register_blueprint(review_bp)
app.register_blueprint(favorite_bp)
app.register_blueprint(history_bp)
# 修复正确注册upload蓝图并设置URL前缀
try:
from app.views.upload import upload_bp
app.register_blueprint(upload_bp, url_prefix='/upload') # 添加URL前缀
print("✅ 上传功能蓝图注册成功")
except ImportError as e:
print(f"⚠️ 上传功能暂时不可用: {str(e)}")
print("✅ 商品管理蓝图注册成功")
print("✅ 购物车蓝图注册成功")
return app

View File

@ -8,11 +8,13 @@ from app.models.address import UserAddress
from app.models.order import Order, OrderItem, ShippingInfo
from app.models.payment import Payment
from app.models.review import Review
from app.models.favorite import UserFavorite
from app.models.browse_history import BrowseHistory
__all__ = [
'User', 'EmailVerification', 'AdminUser', 'OperationLog',
'Category', 'Product', 'ProductImage', 'SpecName', 'SpecValue',
'ProductInventory', 'InventoryLog', 'ProductSpecRelation',
'Cart', 'UserAddress', 'Order', 'OrderItem', 'ShippingInfo',
'Payment', 'Review'
'Payment', 'Review', 'UserFavorite', 'BrowseHistory'
]

View File

@ -0,0 +1,111 @@
"""
浏览历史模型
"""
from datetime import datetime
from config.database import db
from app.models.product import Product
from app.models.user import User
class BrowseHistory(db.Model):
__tablename__ = 'browse_history'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
product_id = db.Column(db.Integer, db.ForeignKey('products.id'), nullable=False)
viewed_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# 关联关系
user = db.relationship('User', backref='browse_history')
product = db.relationship('Product', backref='viewed_by')
# 唯一约束
__table_args__ = (db.UniqueConstraint('user_id', 'product_id', name='uk_user_product'),)
def to_dict(self):
"""转换为字典"""
return {
'id': self.id,
'user_id': self.user_id,
'product_id': self.product_id,
'viewed_at': self.viewed_at.isoformat() if self.viewed_at else None,
'product': {
'id': self.product.id,
'name': self.product.name,
'price': float(self.product.price),
'main_image': self.product.main_image,
'status': self.product.status,
'sales_count': self.product.sales_count,
'category': self.product.category.name if self.product.category else None
} if self.product else None
}
@classmethod
def add_history(cls, user_id, product_id):
"""添加浏览记录"""
# 检查商品是否存在
product = Product.query.get(product_id)
if not product:
return False, "商品不存在"
# 查找已有记录
history = cls.query.filter_by(user_id=user_id, product_id=product_id).first()
if history:
# 更新浏览时间
history.viewed_at = datetime.utcnow()
else:
# 创建新记录
history = cls(user_id=user_id, product_id=product_id)
db.session.add(history)
try:
db.session.commit()
return True, "浏览记录添加成功"
except Exception as e:
db.session.rollback()
return False, f"添加浏览记录失败: {str(e)}"
@classmethod
def get_user_history(cls, user_id, page=1, per_page=20):
"""获取用户浏览历史"""
return cls.query.filter_by(user_id=user_id) \
.join(Product) \
.filter(Product.status == 1) \
.order_by(cls.viewed_at.desc()) \
.paginate(page=page, per_page=per_page, error_out=False)
@classmethod
def get_user_history_count(cls, user_id):
"""获取用户浏览历史数量"""
return cls.query.filter_by(user_id=user_id).count()
@classmethod
def clear_user_history(cls, user_id):
"""清空用户浏览历史"""
try:
cls.query.filter_by(user_id=user_id).delete()
db.session.commit()
return True, "浏览历史清空成功"
except Exception as e:
db.session.rollback()
return False, f"清空浏览历史失败: {str(e)}"
@classmethod
def remove_history_item(cls, user_id, product_id):
"""删除单个浏览记录"""
history = cls.query.filter_by(user_id=user_id, product_id=product_id).first()
if not history:
return False, "浏览记录不存在"
db.session.delete(history)
try:
db.session.commit()
return True, "浏览记录删除成功"
except Exception as e:
db.session.rollback()
return False, f"删除浏览记录失败: {str(e)}"
def __repr__(self):
return f'<BrowseHistory {self.user_id}-{self.product_id}>'

102
app/models/favorite.py Normal file
View File

@ -0,0 +1,102 @@
"""
用户收藏模型
"""
from datetime import datetime
from config.database import db
from app.models.product import Product
from app.models.user import User
class UserFavorite(db.Model):
__tablename__ = 'user_favorites'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
product_id = db.Column(db.Integer, db.ForeignKey('products.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# 关联关系
user = db.relationship('User', backref='favorites')
product = db.relationship('Product', backref='favorited_by')
# 唯一约束
__table_args__ = (db.UniqueConstraint('user_id', 'product_id', name='uk_user_product'),)
def to_dict(self):
"""转换为字典"""
return {
'id': self.id,
'user_id': self.user_id,
'product_id': self.product_id,
'created_at': self.created_at.isoformat() if self.created_at else None,
'product': {
'id': self.product.id,
'name': self.product.name,
'price': float(self.product.price),
'main_image': self.product.main_image,
'status': self.product.status,
'sales_count': self.product.sales_count
} if self.product else None
}
@classmethod
def is_favorited(cls, user_id, product_id):
"""检查用户是否收藏了某商品"""
return cls.query.filter_by(user_id=user_id, product_id=product_id).first() is not None
@classmethod
def add_favorite(cls, user_id, product_id):
"""添加收藏"""
# 检查是否已存在
existing = cls.query.filter_by(user_id=user_id, product_id=product_id).first()
if existing:
return False, "商品已在收藏夹中"
# 检查商品是否存在
product = Product.query.get(product_id)
if not product:
return False, "商品不存在"
# 添加收藏
favorite = cls(user_id=user_id, product_id=product_id)
db.session.add(favorite)
try:
db.session.commit()
return True, "收藏成功"
except Exception as e:
db.session.rollback()
return False, f"收藏失败: {str(e)}"
@classmethod
def remove_favorite(cls, user_id, product_id):
"""取消收藏"""
favorite = cls.query.filter_by(user_id=user_id, product_id=product_id).first()
if not favorite:
return False, "商品未收藏"
db.session.delete(favorite)
try:
db.session.commit()
return True, "取消收藏成功"
except Exception as e:
db.session.rollback()
return False, f"取消收藏失败: {str(e)}"
@classmethod
def get_user_favorites(cls, user_id, page=1, per_page=20):
"""获取用户收藏列表"""
return cls.query.filter_by(user_id=user_id) \
.join(Product) \
.filter(Product.status == 1) \
.order_by(cls.created_at.desc()) \
.paginate(page=page, per_page=per_page, error_out=False)
@classmethod
def get_user_favorites_count(cls, user_id):
"""获取用户收藏数量"""
return cls.query.filter_by(user_id=user_id).count()
def __repr__(self):
return f'<UserFavorite {self.user_id}-{self.product_id}>'

View File

@ -0,0 +1,295 @@
/* 操作日志页面样式 */
.admin-logs {
padding: 0;
}
/* 统计卡片样式 */
.stats-card {
border: none;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: transform 0.2s;
}
.stats-card:hover {
transform: translateY(-2px);
}
.stats-card .card-title {
font-size: 1.8rem;
font-weight: 600;
color: #333;
margin-bottom: 0.25rem;
}
.stats-card .card-text {
color: #666;
font-size: 0.9rem;
margin-bottom: 0;
}
.icon-wrapper {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
color: white;
}
.icon-wrapper.primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.icon-wrapper.success {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.icon-wrapper.warning {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.icon-wrapper.info {
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
color: #333;
}
/* 表格样式 */
.table th {
background-color: #f8f9fa;
border-bottom: 2px solid #dee2e6;
font-weight: 600;
color: #495057;
font-size: 0.9rem;
}
.table td {
vertical-align: middle;
padding: 1rem 0.75rem;
font-size: 0.875rem;
}
.table-hover tbody tr:hover {
background-color: #f8f9fa;
}
/* 操作类型样式 */
.operation-action {
display: inline-block;
padding: 0.25rem 0.5rem;
background-color: #e9ecef;
border-radius: 6px;
font-size: 0.8rem;
font-weight: 500;
color: #495057;
}
/* 资源类型样式 */
.resource-type {
background-color: #d4edda;
color: #155724;
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
}
.resource-id {
color: #6c757d;
font-size: 0.8rem;
margin-left: 0.25rem;
}
/* 用户代理样式 */
.user-agent-wrapper {
max-width: 200px;
}
.user-agent {
display: block;
font-size: 0.8rem;
color: #6c757d;
cursor: help;
line-height: 1.2;
}
/* 徽章样式 */
.badge {
font-size: 0.7rem;
font-weight: 500;
padding: 0.3em 0.6em;
}
/* 时间显示样式 */
.table td:first-child {
white-space: nowrap;
min-width: 110px;
}
.table td:first-child small {
font-size: 0.75rem;
}
/* 空状态样式 */
.empty-state {
text-align: center;
padding: 3rem 1rem;
color: #6c757d;
}
.empty-state i {
font-size: 3rem;
margin-bottom: 1rem;
color: #dee2e6;
}
.empty-state div {
font-size: 1.1rem;
margin-bottom: 0.5rem;
}
/* 响应式设计 */
@media (max-width: 768px) {
.table-responsive {
font-size: 0.8rem;
}
.table th, .table td {
padding: 0.75rem 0.5rem;
}
.stats-card .card-title {
font-size: 1.5rem;
}
.icon-wrapper {
width: 40px;
height: 40px;
font-size: 1.2rem;
}
.user-agent-wrapper {
max-width: 150px;
}
}
/* 筛选表单样式 */
.card .form-label {
font-weight: 500;
color: #495057;
}
.form-control:focus, .form-select:focus {
border-color: #667eea;
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
}
/* 分页样式 */
.pagination .page-link {
color: #667eea;
border-color: #dee2e6;
}
.pagination .page-link:hover {
color: #495057;
background-color: #f8f9fa;
border-color: #dee2e6;
}
.pagination .page-item.active .page-link {
background-color: #667eea;
border-color: #667eea;
}
/* 代码样式 */
code {
background-color: #f8f9fa;
color: #e83e8c;
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-size: 0.8rem;
}
/* 表格滚动条样式 */
.table-responsive::-webkit-scrollbar {
height: 8px;
}
.table-responsive::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.table-responsive::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
.table-responsive::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 卡片头部样式 */
.card-header h5 {
color: #333;
font-weight: 600;
}
.card-header small {
font-weight: 400;
}
/* 筛选区域样式 */
.card-body form {
margin-bottom: 0;
}
.card-body .btn {
height: 38px;
margin-top: 0.5rem;
}
/* 日志详情样式 */
.log-detail-btn {
font-size: 0.8rem;
padding: 0.2rem 0.4rem;
border-radius: 4px;
}
/* 操作者信息样式 */
.badge.bg-warning {
background-color: #ffc107 !important;
color: #212529 !important;
}
.badge.bg-info {
background-color: #0dcaf0 !important;
color: #000 !important;
}
/* 分页信息样式 */
.card-footer {
padding: 1rem 1.5rem;
background-color: #f8f9fa !important;
border-top: 1px solid #dee2e6;
}
/* 加载状态 */
.loading {
text-align: center;
padding: 2rem;
color: #6c757d;
}
.loading i {
font-size: 2rem;
margin-bottom: 1rem;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

View File

@ -0,0 +1,202 @@
/* 订单管理样式 */
.admin-orders {
padding: 0;
}
/* 统计卡片 - 修复颜色问题,使用更高优先级 */
.admin-orders .stats-card {
background: #ffffff !important;
color: #2c3e50 !important;
border: 1px solid #e9ecef !important;
border-radius: 0.5rem !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05) !important;
transition: transform 0.2s, box-shadow 0.2s;
padding: 1.25rem !important;
}
.admin-orders .stats-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
}
.admin-orders .stats-card .card-body {
padding: 0 !important;
text-align: center;
}
.stats-number {
font-size: 2.2rem;
font-weight: bold;
color: #2c3e50 !important;
line-height: 1.2;
margin-bottom: 0.25rem;
}
.stats-label {
font-size: 0.9rem;
color: #6c757d !important;
font-weight: 500;
}
/* 状态特定颜色 - 使用更明显的颜色对比 */
.admin-orders .stats-card.pending-payment {
border-left: 4px solid #ffc107 !important;
}
.admin-orders .stats-card.pending-payment .stats-number {
color: #f39c12 !important;
}
.admin-orders .stats-card.pending-shipment {
border-left: 4px solid #17a2b8 !important;
}
.admin-orders .stats-card.pending-shipment .stats-number {
color: #17a2b8 !important;
}
.admin-orders .stats-card.shipped {
border-left: 4px solid #28a745 !important;
}
.admin-orders .stats-card.shipped .stats-number {
color: #28a745 !important;
}
.admin-orders .stats-card.completed {
border-left: 4px solid #6f42c1 !important;
}
.admin-orders .stats-card.completed .stats-number {
color: #6f42c1 !important;
}
.admin-orders .stats-card.cancelled {
border-left: 4px solid #dc3545 !important;
}
.admin-orders .stats-card.cancelled .stats-number {
color: #dc3545 !important;
}
/* 订单状态徽章 */
.order-status-1 {
background-color: #ffc107;
color: #212529;
}
.order-status-2 {
background-color: #17a2b8;
color: #fff;
}
.order-status-3 {
background-color: #28a745;
color: #fff;
}
.order-status-4 {
background-color: #fd7e14;
color: #fff;
}
.order-status-5 {
background-color: #6f42c1;
color: #fff;
}
.order-status-6 {
background-color: #dc3545;
color: #fff;
}
.order-status-7 {
background-color: #e83e8c;
color: #fff;
}
/* 表格样式 */
.table th {
background-color: #f8f9fa;
font-weight: 600;
border-bottom: 2px solid #dee2e6;
}
.table td {
vertical-align: middle;
}
.table-hover tbody tr:hover {
background-color: #f8f9fa;
}
/* 操作按钮组 */
.btn-group .btn {
border-radius: 0.375rem;
margin-right: 0.25rem;
}
.btn-group .btn:last-child {
margin-right: 0;
}
/* 商品缩略图 */
.product-thumb {
border-radius: 0.375rem;
border: 1px solid #dee2e6;
}
/* 订单详情页面 */
.admin-order-detail .card {
border: none;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.admin-order-detail .card-header {
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
.admin-order-detail .table th {
background-color: transparent;
border-bottom: 1px solid #dee2e6;
font-weight: 600;
}
.admin-order-detail .table td {
border-bottom: 1px solid #dee2e6;
}
/* 模态框样式 */
.modal-header {
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
.modal-body .form-label {
font-weight: 600;
}
/* 响应式设计 */
@media (max-width: 768px) {
.admin-orders .stats-card {
margin-bottom: 1rem;
}
.stats-number {
font-size: 1.8rem;
}
.btn-group {
flex-direction: column;
gap: 0.25rem;
}
.btn-group .btn {
margin-right: 0;
}
.table-responsive {
font-size: 0.875rem;
}
}

View File

@ -0,0 +1,351 @@
/* 用户管理页面样式 */
.admin-users {
padding: 0;
}
/* 统计卡片样式 */
.stats-card {
border: none;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: transform 0.2s;
}
.stats-card:hover {
transform: translateY(-2px);
}
.stats-card .card-title {
font-size: 1.8rem;
font-weight: 600;
color: #333;
margin-bottom: 0.25rem;
}
.stats-card .card-text {
color: #666;
font-size: 0.9rem;
margin-bottom: 0;
}
.icon-wrapper {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
color: white;
}
.icon-wrapper.primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.icon-wrapper.success {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.icon-wrapper.danger {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.icon-wrapper.info {
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
color: #333;
}
/* 用户头像样式 - 表格中的头像 */
.avatar-wrapper {
width: 48px !important;
height: 48px !important;
position: relative;
overflow: hidden !important;
flex-shrink: 0 !important;
flex-grow: 0 !important;
}
.user-avatar {
width: 48px !important;
height: 48px !important;
border-radius: 50% !important;
object-fit: cover !important;
border: 2px solid #f8f9fa !important;
display: block !important;
max-width: 48px !important;
max-height: 48px !important;
min-width: 48px !important;
min-height: 48px !important;
flex-shrink: 0 !important;
flex-grow: 0 !important;
}
.user-avatar-placeholder {
width: 48px !important;
height: 48px !important;
border-radius: 50% !important;
background: #e9ecef !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
font-size: 1.2rem !important;
color: #6c757d !important;
flex-shrink: 0 !important;
flex-grow: 0 !important;
}
/* 用户详情模态框中的头像容器 */
.user-avatar-large-wrapper {
width: 80px !important;
height: 80px !important;
margin: 0 auto !important;
overflow: hidden !important;
border-radius: 50% !important;
position: relative !important;
flex-shrink: 0 !important;
flex-grow: 0 !important;
}
/* 用户详情模态框中的头像 */
.avatar-large {
width: 80px !important;
height: 80px !important;
border-radius: 50% !important;
object-fit: cover !important;
border: 3px solid #f8f9fa !important;
display: block !important;
max-width: 80px !important;
max-height: 80px !important;
min-width: 80px !important;
min-height: 80px !important;
flex-shrink: 0 !important;
flex-grow: 0 !important;
}
.avatar-placeholder-large {
width: 80px !important;
height: 80px !important;
border-radius: 50% !important;
background: #e9ecef !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
font-size: 2rem !important;
color: #6c757d !important;
flex-shrink: 0 !important;
flex-grow: 0 !important;
}
/* 强制覆盖Bootstrap的所有可能的图片样式 */
.user-detail img,
.table img,
.modal img {
max-width: none !important;
max-height: none !important;
}
.user-detail img.avatar-large,
.modal img.avatar-large {
width: 80px !important;
height: 80px !important;
max-width: 80px !important;
max-height: 80px !important;
min-width: 80px !important;
min-height: 80px !important;
}
.table img.user-avatar {
width: 48px !important;
height: 48px !important;
max-width: 48px !important;
max-height: 48px !important;
min-width: 48px !important;
min-height: 48px !important;
}
/* 表格样式 */
.table th {
background-color: #f8f9fa;
border-bottom: 2px solid #dee2e6;
font-weight: 600;
color: #495057;
}
.table td {
vertical-align: middle;
padding: 1rem 0.75rem;
}
.table-hover tbody tr:hover {
background-color: #f8f9fa;
}
/* 按钮组样式 */
.btn-group .btn {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
/* 空状态样式 */
.empty-state {
text-align: center;
padding: 3rem 1rem;
color: #6c757d;
}
.empty-state i {
font-size: 3rem;
margin-bottom: 1rem;
color: #dee2e6;
}
.empty-state div {
font-size: 1.1rem;
margin-bottom: 0.5rem;
}
/* 用户详情信息样式 */
.user-detail {
padding: 1rem;
}
.user-info-list {
margin-top: 1rem;
}
.user-info-item {
display: flex;
align-items: center;
padding: 0.75rem 0;
border-bottom: 1px solid #f8f9fa;
}
.user-info-item:last-child {
border-bottom: none;
}
.user-info-label {
font-weight: 500;
color: #495057;
width: 120px;
flex-shrink: 0;
}
.user-info-value {
color: #333;
flex: 1;
}
/* 响应式设计 */
@media (max-width: 768px) {
.table-responsive {
font-size: 0.875rem;
}
.btn-group .btn {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
.stats-card .card-title {
font-size: 1.5rem;
}
.icon-wrapper {
width: 40px;
height: 40px;
font-size: 1.2rem;
}
.user-avatar {
width: 40px !important;
height: 40px !important;
max-width: 40px !important;
max-height: 40px !important;
min-width: 40px !important;
min-height: 40px !important;
}
.avatar-wrapper {
width: 40px !important;
height: 40px !important;
}
.user-avatar-placeholder {
width: 40px !important;
height: 40px !important;
font-size: 1rem !important;
}
}
/* 筛选表单样式 */
.card .form-label {
font-weight: 500;
color: #495057;
}
.form-control:focus, .form-select:focus {
border-color: #667eea;
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
}
/* 分页样式 */
.pagination .page-link {
color: #667eea;
border-color: #dee2e6;
}
.pagination .page-link:hover {
color: #495057;
background-color: #f8f9fa;
border-color: #dee2e6;
}
.pagination .page-item.active .page-link {
background-color: #667eea;
border-color: #667eea;
}
/* 用户详情模态框样式 */
.modal-content {
border-radius: 12px;
border: none;
box-shadow: 0 10px 30px rgba(0,0,0,0.15);
}
.modal-header {
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
border-radius: 12px 12px 0 0;
}
.modal-title {
font-weight: 600;
color: #333;
}
/* 徽章样式 */
.badge {
font-size: 0.75rem;
font-weight: 500;
padding: 0.35em 0.65em;
}
/* 加载状态 */
.loading {
text-align: center;
padding: 2rem;
color: #6c757d;
}
.loading i {
font-size: 2rem;
margin-bottom: 1rem;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

View File

@ -1,26 +1,27 @@
/* 订单结算页面样式 */
.checkout-section {
margin-bottom: 20px;
margin-bottom: 1.5rem;
}
.address-card {
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid #e9ecef;
}
.address-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
border-color: #007bff;
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.1);
}
.address-card.selected {
border-color: #007bff;
background-color: #f8f9ff;
background-color: #e7f3ff;
}
.product-item {
border-bottom: 1px solid #eee;
padding: 15px 0;
padding: 1rem 0;
border-bottom: 1px solid #e9ecef;
}
.product-item:last-child {
@ -28,19 +29,136 @@
}
.order-summary {
background-color: #f8f9fa;
border-radius: 8px;
padding: 20px;
background: #f8f9fa;
padding: 1rem;
border-radius: 0.5rem;
}
.price-row {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
margin-bottom: 0.5rem;
}
.total-price {
font-size: 1.2em;
.price-row.total-price {
font-size: 1.1rem;
font-weight: bold;
color: #e74c3c;
color: #dc3545;
}
.form-check {
padding: 0.75rem;
border: 1px solid #e9ecef;
border-radius: 0.5rem;
margin-bottom: 0.5rem;
transition: all 0.3s ease;
}
.form-check:hover {
border-color: #007bff;
background-color: #f8f9fa;
}
.form-check-input:checked + .form-check-label {
color: #007bff;
}
/* 支付方式特殊样式 */
.form-check input[type="radio"][value="simulate"]:checked + label {
color: #ffc107;
}
.form-check input[type="radio"][value="simulate"]:checked + label i {
color: #ffc107 !important;
}
/* 模拟支付说明样式 */
.alert-warning {
border-left: 4px solid #ffc107;
}
/* 响应式设计 */
@media (max-width: 768px) {
.checkout-section .col-md-4,
.checkout-section .col-md-3 {
margin-bottom: 1rem;
}
.address-card {
margin-bottom: 1rem;
}
.product-item .col-md-2,
.product-item .col-md-6 {
margin-bottom: 0.5rem;
}
}
/* 动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.alert {
animation: fadeIn 0.3s ease;
}
/* 按钮样式 */
.btn-lg {
padding: 0.75rem 1.5rem;
font-size: 1.1rem;
}
/* 卡片头部样式 */
.card-header h5 {
margin-bottom: 0;
color: #495057;
}
.card-header i {
margin-right: 0.5rem;
color: #007bff;
}
/* 表单标签样式 */
.form-check-label {
cursor: pointer;
width: 100%;
}
.form-check-label strong {
display: block;
margin-bottom: 0.25rem;
}
.form-check-label small {
color: #6c757d;
}
/* 商品图片样式 */
.product-item img {
max-height: 80px;
object-fit: cover;
}
/* 价格显示样式 */
.fw-bold {
color: #dc3545;
}
/* 面包屑导航样式 */
.breadcrumb {
background: transparent;
padding: 0;
}
.breadcrumb-item + .breadcrumb-item::before {
color: #6c757d;
}

View File

@ -0,0 +1,118 @@
/* 收藏页面样式 */
.favorite-item {
transition: all 0.3s ease;
border: 1px solid #e9ecef;
}
.favorite-item:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
.favorite-image {
width: 80px;
height: 80px;
object-fit: cover;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.favorite-image-placeholder {
width: 80px;
height: 80px;
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: #6c757d;
font-size: 2rem;
}
.favorite-checkbox {
transform: scale(1.2);
}
.empty-state {
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
}
/* 图标按钮样式 */
.icon-buttons .btn {
font-size: 1.1rem;
padding: 0.5rem;
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 2px;
transition: all 0.2s ease;
}
.icon-buttons .btn:hover {
transform: scale(1.1);
}
.icon-buttons .btn-outline-primary:hover {
background-color: #007bff;
border-color: #007bff;
color: white;
}
.icon-buttons .btn-outline-danger:hover {
background-color: #dc3545;
border-color: #dc3545;
color: white;
}
.card-title a {
color: #212529;
font-size: 0.95rem;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.card-title a:hover {
color: #007bff;
}
.favorite-item .card-body {
padding: 1rem;
}
.badge {
font-size: 0.75rem;
}
/* 工具提示样式 */
.tooltip-inner {
font-size: 0.8rem;
padding: 0.25rem 0.5rem;
}
/* 响应式设计 */
@media (max-width: 768px) {
.favorite-image, .favorite-image-placeholder {
width: 60px;
height: 60px;
}
.card-title a {
font-size: 0.9rem;
}
.icon-buttons .btn {
font-size: 1rem;
width: 35px;
height: 35px;
}
}

125
app/static/css/history.css Normal file
View File

@ -0,0 +1,125 @@
/* 浏览历史页面样式 */
.history-item {
transition: all 0.3s ease;
border: 1px solid #e9ecef;
}
.history-item:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
.history-image {
width: 80px;
height: 80px;
object-fit: cover;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.history-image-placeholder {
width: 80px;
height: 80px;
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: #6c757d;
font-size: 2rem;
}
.history-checkbox {
transform: scale(1.2);
}
.empty-state {
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
}
/* 卡片底部按钮区域样式 */
.history-item .card-footer {
background-color: #f8f9fa;
border-top: 1px solid #e9ecef;
padding: 0.75rem;
margin-top: auto;
}
.history-item .card-footer .btn-group {
display: flex;
gap: 0.25rem;
}
.history-item .card-footer .btn {
font-size: 0.8rem;
padding: 0.375rem 0.75rem;
border-radius: 4px;
transition: all 0.2s ease;
flex: 1;
}
.history-item .card-footer .btn:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.history-item .card-footer .btn-outline-primary:hover {
background-color: #007bff;
border-color: #007bff;
color: white;
}
.history-item .card-footer .btn-outline-danger:hover {
background-color: #dc3545;
border-color: #dc3545;
color: white;
}
.history-item .card-footer .btn-outline-secondary:hover {
background-color: #6c757d;
border-color: #6c757d;
color: white;
}
.card-title a {
color: #212529;
font-size: 0.95rem;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.card-title a:hover {
color: #007bff;
}
.history-item .card-body {
padding: 1rem;
}
.badge {
font-size: 0.75rem;
}
/* 响应式设计 */
@media (max-width: 768px) {
.history-image, .history-image-placeholder {
width: 60px;
height: 60px;
}
.card-title a {
font-size: 0.9rem;
}
.history-item .card-footer .btn {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
}
}

View File

@ -17,9 +17,34 @@
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
/* 欢迎横幅样式 */
/* 欢迎横幅样式 */
.jumbotron {
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%) !important;
color: white !important;
border-radius: 0.5rem !important;
}
.jumbotron h1 {
color: white !important;
font-weight: bold !important;
}
.jumbotron p {
color: white !important;
opacity: 0.9;
}
.jumbotron .btn-light {
background-color: white !important;
color: #007bff !important;
border: none !important;
font-weight: bold !important;
}
.jumbotron .btn-light:hover {
background-color: #f8f9fa !important;
color: #0056b3 !important;
}
/* 商品图片样式 */

View File

@ -1,4 +1,26 @@
/* 订单详情页面样式 */
/* 首先,重置所有可能影响的样式 */
.order-detail-card .product-item img {
all: unset !important;
}
/* 然后重新定义我们需要的样式 */
.order-detail-card .product-item img {
width: 80px !important;
height: 80px !important;
max-width: 80px !important;
max-height: 80px !important;
min-width: 80px !important;
min-height: 80px !important;
object-fit: cover !important;
border-radius: 4px !important;
border: 1px solid #ddd !important;
display: block !important;
box-sizing: border-box !important;
}
/* 订单状态时间线 */
.order-status-timeline {
position: relative;
padding-left: 30px;
@ -60,13 +82,6 @@
border-bottom: none;
}
.product-image {
width: 80px;
height: 80px;
object-fit: cover;
border-radius: 4px;
}
.info-row {
display: flex;
justify-content: space-between;
@ -78,3 +93,15 @@
font-weight: bold;
font-size: 1.2em;
}
/* 响应式设计 */
@media (max-width: 768px) {
.order-detail-card .product-item img {
width: 60px !important;
height: 60px !important;
max-width: 60px !important;
max-height: 60px !important;
min-width: 60px !important;
min-height: 60px !important;
}
}

View File

@ -1,49 +1,134 @@
/* 订单支付页面样式 */
/* 支付页面样式 */
.pay-container {
max-width: 600px;
margin: 50px auto;
margin: 2rem auto;
padding: 0 1rem;
}
.order-info {
background-color: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
background: #f8f9fa;
padding: 1rem;
border-radius: 0.5rem;
margin-bottom: 1.5rem;
}
.payment-method {
border: 2px solid #dee2e6;
border-radius: 8px;
padding: 20px;
margin-bottom: 15px;
border: 2px solid #e9ecef;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1rem;
cursor: pointer;
transition: all 0.3s ease;
}
.payment-method:hover {
border-color: #007bff;
background-color: #f8f9ff;
background-color: #f8f9fa;
}
.payment-method.selected {
border-color: #007bff;
background-color: #f8f9ff;
background-color: #e7f3ff;
}
.qr-code {
text-align: center;
padding: 30px;
background-color: #f8f9fa;
border-radius: 8px;
}
.countdown {
font-size: 1.2em;
color: #e74c3c;
font-weight: bold;
padding: 2rem;
background: #fff;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
margin: 1rem 0;
}
.payment-status {
text-align: center;
padding: 20px;
padding: 3rem 1rem;
}
.countdown {
font-weight: bold;
color: #dc3545;
font-size: 1.1rem;
}
.simulate-panel {
background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
border: 2px solid #ffc107 !important;
}
.simulate-panel h6 {
margin-bottom: 0.5rem;
}
.simulate-panel .btn {
min-width: 140px;
}
.payment-tips {
background: #f8f9fa;
padding: 1rem;
border-radius: 0.5rem;
border-left: 4px solid #007bff;
}
.payment-tips ul {
margin-bottom: 0;
padding-left: 1.2rem;
}
.payment-tips li {
margin-bottom: 0.3rem;
}
/* 响应式设计 */
@media (max-width: 768px) {
.pay-container {
margin: 1rem auto;
padding: 0 0.5rem;
}
.d-md-flex .btn {
margin-bottom: 0.5rem;
}
.simulate-panel .btn {
min-width: auto;
width: 100%;
}
}
/* 动画效果 */
.payment-status i {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
/* 按钮状态 */
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* 支付方式图标 */
.payment-method i {
min-width: 60px;
}
/* 倒计时样式 */
.countdown {
background: #fff;
padding: 0.2rem 0.5rem;
border-radius: 0.25rem;
border: 1px solid #dc3545;
}

View File

@ -1,3 +1,5 @@
/* 商品详情页样式 */
.product-card {
transition: transform 0.2s;
}
@ -55,6 +57,108 @@
cursor: not-allowed;
}
/* 商品主图轮播样式修复 */
.carousel-inner img {
/* 重置Bootstrap图片样式 */
all: unset !important;
display: block !important;
width: 100% !important;
height: 400px !important;
object-fit: cover !important;
border-radius: 8px !important;
}
/* 缩略图样式修复 */
.thumbnail-image {
/* 重置Bootstrap图片样式 */
all: unset !important;
display: block !important;
width: 100% !important;
height: 80px !important;
object-fit: cover !important;
cursor: pointer !important;
border-radius: 4px !important;
border: 2px solid #dee2e6 !important;
transition: all 0.2s ease !important;
}
.thumbnail-image:hover {
transform: scale(1.05);
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
border-color: #007bff;
}
/* 推荐商品图片样式修复 */
.product-card .card-img-top {
/* 重置Bootstrap图片样式 */
all: unset !important;
display: block !important;
width: 100% !important;
height: 200px !important;
object-fit: cover !important;
border-top-left-radius: 0.375rem !important;
border-top-right-radius: 0.375rem !important;
}
/* 商品详情标签页内的图片样式 */
.tab-content img {
/* 确保标签页内的图片不会过大 */
max-width: 100% !important;
height: auto !important;
border-radius: 4px !important;
border: 1px solid #dee2e6 !important;
}
/* 评价图片在商品详情页中的特殊样式 */
.reviews-section img {
/* 重置评价图片样式 */
all: unset !important;
display: inline-block !important;
max-width: 80px !important;
max-height: 80px !important;
width: auto !important;
height: auto !important;
object-fit: cover !important;
border-radius: 6px !important;
border: 1px solid #dee2e6 !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
margin-right: 8px !important;
margin-bottom: 8px !important;
}
.reviews-section img:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
border-color: #007bff;
}
/* 用户头像图片样式 */
.reviewer-avatar {
/* 重置用户头像样式 */
all: unset !important;
display: block !important;
width: 40px !important;
height: 40px !important;
border-radius: 50% !important;
object-fit: cover !important;
border: 2px solid #e9ecef !important;
}
/* 图片模态框样式 */
.modal-body img {
/* 模态框中的图片样式 */
all: unset !important;
display: block !important;
max-width: 100% !important;
max-height: 80vh !important;
width: auto !important;
height: auto !important;
margin: 0 auto !important;
border-radius: 8px !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important;
}
/* 响应式优化 */
@media (max-width: 768px) {
.price-section {
@ -68,4 +172,30 @@
.action-buttons .btn {
margin-bottom: 10px;
}
.carousel-inner img {
height: 300px !important;
}
.thumbnail-image {
height: 60px !important;
}
.reviews-section img {
max-width: 60px !important;
max-height: 60px !important;
}
}
/* 无图片占位符样式 */
.bg-light.d-flex {
background-color: #f8f9fa !important;
border: 2px dashed #dee2e6 !important;
}
/* 确保所有图片都有基础的重置样式 */
.product-detail img:not(.reviewer-avatar):not(.thumbnail-image):not(.card-img-top) {
max-width: 100% !important;
height: auto !important;
border-radius: 4px !important;
}

626
app/static/css/review.css Normal file
View File

@ -0,0 +1,626 @@
/* 评价功能样式 */
/* 评价表单样式 */
.product-info {
background-color: #f8f9fa;
padding: 15px;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.product-info img {
/* 图片重置样式 - 解决Bootstrap冲突 */
all: unset !important;
display: block !important;
max-width: 100% !important;
max-height: 80px !important;
width: auto !important;
height: auto !important;
object-fit: cover !important;
border-radius: 4px !important;
border: 1px solid #dee2e6 !important;
}
/* 星级评分样式 - 简化版本 */
.rating-container {
display: flex;
align-items: center;
gap: 15px;
}
.star-rating {
display: flex;
gap: 3px;
}
.star {
font-size: 2.5rem;
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
line-height: 1;
/* 默认样式:灰色 */
color: #ddd !important;
}
.star:hover {
transform: scale(1.1);
}
/* 填充状态:橙色 */
.star.filled {
color: #ff6b35 !important;
}
/* 评分文字样式 */
.rating-text {
font-weight: 600;
color: #666;
font-size: 1.1rem;
padding: 10px 15px;
background-color: #f8f9fa;
border-radius: 8px;
border: 2px solid #e9ecef;
min-width: 120px;
text-align: center;
transition: all 0.2s ease;
}
.rating-text.selected {
background-color: #ff6b35;
color: white;
border-color: #ff6b35;
}
/* 图片上传样式 */
.image-upload-container {
border: 2px dashed #ddd;
border-radius: 8px;
padding: 20px;
text-align: center;
transition: border-color 0.3s ease;
}
.image-upload-container:hover {
border-color: #007bff;
}
.upload-area {
cursor: pointer;
padding: 20px;
}
.upload-area i {
font-size: 3rem;
color: #666;
display: block;
margin-bottom: 10px;
}
/* 上传图片预览容器 - 强制控制布局 */
.uploaded-images {
display: flex !important;
flex-wrap: wrap !important;
gap: 8px !important;
margin-top: 15px !important;
justify-content: flex-start !important;
align-items: flex-start !important;
}
/* 上传图片预览尺寸 - 使用最强的样式规则 */
.image-preview {
position: relative !important;
width: 80px !important;
height: 80px !important;
max-width: 80px !important;
max-height: 80px !important;
min-width: 80px !important;
min-height: 80px !important;
border-radius: 8px !important;
overflow: hidden !important;
border: 2px solid #e9ecef !important;
flex-shrink: 0 !important;
flex-grow: 0 !important;
display: inline-block !important;
box-sizing: border-box !important;
}
/* 强制重置上传预览图片的所有样式 */
.image-preview img {
all: unset !important;
display: block !important;
width: 80px !important;
height: 80px !important;
max-width: 80px !important;
max-height: 80px !important;
min-width: 80px !important;
min-height: 80px !important;
object-fit: cover !important;
border-radius: 6px !important;
box-sizing: border-box !important;
position: relative !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
outline: none !important;
background: none !important;
}
/* 针对上传图片容器内的所有img标签 */
.uploaded-images img {
all: unset !important;
display: block !important;
width: 80px !important;
height: 80px !important;
max-width: 80px !important;
max-height: 80px !important;
min-width: 80px !important;
min-height: 80px !important;
object-fit: cover !important;
border-radius: 6px !important;
box-sizing: border-box !important;
position: relative !important;
flex-shrink: 0 !important;
flex-grow: 0 !important;
}
/* 上传图片容器的直接img子元素 */
.uploaded-images > .image-preview > img {
all: unset !important;
display: block !important;
width: 80px !important;
height: 80px !important;
max-width: 80px !important;
max-height: 80px !important;
min-width: 80px !important;
min-height: 80px !important;
object-fit: cover !important;
border-radius: 6px !important;
}
.image-preview .remove-btn {
position: absolute !important;
top: 2px !important;
right: 2px !important;
background: rgba(255, 255, 255, 0.9) !important;
border: none !important;
border-radius: 50% !important;
width: 20px !important;
height: 20px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
cursor: pointer !important;
font-size: 12px !important;
color: #dc3545 !important;
box-shadow: 0 1px 3px rgba(0,0,0,0.2) !important;
z-index: 10 !important;
}
.image-preview .remove-btn:hover {
background: rgba(255, 255, 255, 1) !important;
transform: scale(1.1) !important;
}
/* 评价列表样式 */
.review-item {
border-bottom: 1px solid #e9ecef;
padding: 20px 0;
margin-bottom: 20px;
}
.review-item:last-child {
border-bottom: none;
margin-bottom: 0;
}
.rating-display .stars {
color: #ff6b35 !important;
font-size: 1.2rem;
margin-right: 8px;
}
.review-content {
line-height: 1.6;
margin: 10px 0;
word-wrap: break-word;
}
.review-images {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.review-image-thumb {
/* 图片重置样式 - 解决Bootstrap冲突 */
all: unset !important;
display: block !important;
width: 60px !important;
height: 60px !important;
object-fit: cover !important;
border-radius: 4px !important;
border: 1px solid #e9ecef !important;
cursor: pointer !important;
transition: transform 0.2s ease !important;
}
.review-image-thumb:hover {
transform: scale(1.05);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.review-meta {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #f8f9fa;
}
/* 商品详情页评价标签页样式 */
.reviews-section {
padding: 20px 0;
}
.reviews-stats {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.rating-summary {
display: flex;
align-items: center;
gap: 20px;
margin-bottom: 15px;
}
.overall-rating {
text-align: center;
}
.overall-rating .score {
font-size: 3rem;
font-weight: bold;
color: #ff6b35;
line-height: 1;
}
.overall-rating .stars {
color: #ff6b35 !important;
font-size: 1.5rem;
}
.overall-rating .total {
color: #666;
margin-top: 5px;
}
.rating-breakdown {
flex: 1;
}
.rating-bar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.rating-bar .label {
width: 40px;
font-size: 14px;
}
.rating-bar .progress {
flex: 1;
height: 8px;
}
.rating-bar .count {
width: 40px;
text-align: right;
font-size: 14px;
color: #666;
}
.reviews-filter {
margin-bottom: 20px;
}
.reviews-filter .btn {
margin-right: 10px;
margin-bottom: 10px;
}
.review-list-item {
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 20px;
margin-bottom: 15px;
background: #fff;
}
.reviewer-info {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
}
/* 用户头像样式 - 重点修复区域 */
.reviewer-avatar {
/* 头像图片重置样式 - 强制重置所有样式 */
all: unset !important;
display: block !important;
width: 40px !important;
height: 40px !important;
max-width: 40px !important;
max-height: 40px !important;
min-width: 40px !important;
min-height: 40px !important;
border-radius: 50% !important;
object-fit: cover !important;
border: 2px solid #e9ecef !important;
box-sizing: border-box !important;
flex-shrink: 0 !important;
vertical-align: top !important;
}
/* 针对商品详情页评价容器中的头像 */
#reviewsContainer .reviewer-avatar {
/* 强制重置商品详情页评价容器中的头像 */
all: unset !important;
display: block !important;
width: 40px !important;
height: 40px !important;
max-width: 40px !important;
max-height: 40px !important;
min-width: 40px !important;
min-height: 40px !important;
border-radius: 50% !important;
object-fit: cover !important;
border: 2px solid #e9ecef !important;
box-sizing: border-box !important;
flex-shrink: 0 !important;
vertical-align: top !important;
}
/* 针对评价标签页中的头像 */
#reviews .reviewer-avatar {
/* 评价标签页中的头像 */
all: unset !important;
display: block !important;
width: 40px !important;
height: 40px !important;
max-width: 40px !important;
max-height: 40px !important;
min-width: 40px !important;
min-height: 40px !important;
border-radius: 50% !important;
object-fit: cover !important;
border: 2px solid #e9ecef !important;
box-sizing: border-box !important;
flex-shrink: 0 !important;
vertical-align: top !important;
}
.reviewer-name {
font-weight: 500;
}
.review-time {
color: #666;
font-size: 14px;
}
.empty-state {
padding: 60px 20px;
}
.empty-state i {
opacity: 0.3;
}
/* 商品详情页评价图片展示 - 重点修复区域 */
.product-review-images {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 10px;
}
.product-review-image {
/* 商品评价图片重置样式 - 强制重置所有样式 */
all: unset !important;
display: inline-block !important;
width: 80px !important;
height: 80px !important;
max-width: 80px !important;
max-height: 80px !important;
min-width: 80px !important;
min-height: 80px !important;
object-fit: cover !important;
border-radius: 6px !important;
border: 1px solid #dee2e6 !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
box-sizing: border-box !important;
vertical-align: top !important;
}
.product-review-image:hover {
transform: scale(1.05) !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important;
border-color: #007bff !important;
}
/* 特殊针对商品详情页面的评价容器 */
#reviewsContainer img:not(.reviewer-avatar) {
/* 强制重置商品详情页评价容器中的所有图片(除了头像) */
all: unset !important;
display: inline-block !important;
width: 80px !important;
height: 80px !important;
max-width: 80px !important;
max-height: 80px !important;
object-fit: cover !important;
border-radius: 6px !important;
border: 1px solid #dee2e6 !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
margin-right: 8px !important;
margin-bottom: 8px !important;
box-sizing: border-box !important;
vertical-align: top !important;
}
#reviewsContainer img:not(.reviewer-avatar):hover {
transform: scale(1.05) !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important;
border-color: #007bff !important;
}
/* 评价标签页特殊处理 */
#reviews img:not(.reviewer-avatar) {
/* 评价标签页中的图片(除了头像) */
all: unset !important;
display: inline-block !important;
width: 80px !important;
height: 80px !important;
max-width: 80px !important;
max-height: 80px !important;
object-fit: cover !important;
border-radius: 6px !important;
border: 1px solid #dee2e6 !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
margin-right: 8px !important;
margin-bottom: 8px !important;
box-sizing: border-box !important;
}
#reviews img:not(.reviewer-avatar):hover {
transform: scale(1.05) !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important;
border-color: #007bff !important;
}
/* 响应式设计 */
@media (max-width: 768px) {
.star {
font-size: 2rem;
}
.rating-summary {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.uploaded-images {
justify-content: flex-start !important;
}
/* 移动端上传图片预览更小 */
.image-preview {
width: 60px !important;
height: 60px !important;
max-width: 60px !important;
max-height: 60px !important;
min-width: 60px !important;
min-height: 60px !important;
}
.image-preview img,
.uploaded-images img,
.uploaded-images > .image-preview > img {
width: 60px !important;
height: 60px !important;
max-width: 60px !important;
max-height: 60px !important;
min-width: 60px !important;
min-height: 60px !important;
}
.image-preview .remove-btn {
width: 16px !important;
height: 16px !important;
font-size: 10px !important;
top: 1px !important;
right: 1px !important;
}
.review-image-thumb {
width: 50px !important;
height: 50px !important;
}
.product-review-image,
#reviewsContainer img:not(.reviewer-avatar),
#reviews img:not(.reviewer-avatar) {
width: 60px !important;
height: 60px !important;
max-width: 60px !important;
max-height: 60px !important;
min-width: 60px !important;
min-height: 60px !important;
}
.reviewer-avatar,
#reviewsContainer .reviewer-avatar,
#reviews .reviewer-avatar {
width: 35px !important;
height: 35px !important;
max-width: 35px !important;
max-height: 35px !important;
min-width: 35px !important;
min-height: 35px !important;
}
}
/* 加载状态 */
.loading {
opacity: 0.6;
pointer-events: none;
}
.loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
border: 2px solid #f3f3f3;
border-top: 2px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: translate(-50%, -50%) rotate(0deg); }
100% { transform: translate(-50%, -50%) rotate(360deg); }
}
/* 动画效果 */
.review-item {
animation: fadeInUp 0.3s ease;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

350
app/static/js/admin_logs.js Normal file
View File

@ -0,0 +1,350 @@
// 操作日志页面JavaScript
document.addEventListener('DOMContentLoaded', function() {
// 初始化
initializeLogManagement();
});
// 初始化日志管理功能
function initializeLogManagement() {
// 添加事件监听器
setupEventListeners();
// 初始化工具提示
initializeTooltips();
// 初始化表格
initializeTable();
}
// 设置事件监听器
function setupEventListeners() {
// 搜索表单提交
const searchForm = document.querySelector('form[method="GET"]');
if (searchForm) {
searchForm.addEventListener('submit', function(e) {
// 可以在这里添加搜索前的验证
});
}
// 用户类型筛选变更
const userTypeSelect = document.getElementById('user_type');
if (userTypeSelect) {
userTypeSelect.addEventListener('change', function() {
// 自动提交表单
this.form.submit();
});
}
// 操作类型输入框
const actionInput = document.getElementById('action');
if (actionInput) {
// 添加防抖搜索
let searchTimer;
actionInput.addEventListener('input', function() {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
// 可以实现实时搜索
}, 500);
});
}
}
// 初始化工具提示
function initializeTooltips() {
// 为用户代理字段添加工具提示
const userAgentElements = document.querySelectorAll('.user-agent');
userAgentElements.forEach(element => {
if (element.title) {
// 使用Bootstrap的tooltip
new bootstrap.Tooltip(element);
}
});
}
// 初始化表格
function initializeTable() {
// 添加表格行点击事件
const tableRows = document.querySelectorAll('.table tbody tr');
tableRows.forEach(row => {
row.addEventListener('click', function(e) {
// 如果点击的是按钮,不触发行点击事件
if (e.target.tagName === 'BUTTON' || e.target.closest('button')) {
return;
}
// 高亮选中行
tableRows.forEach(r => r.classList.remove('table-active'));
this.classList.add('table-active');
});
});
}
// 查看日志详情
function viewLogDetail(logId) {
// 发送AJAX请求获取日志详情
fetch(`/admin/logs/${logId}/detail`)
.then(response => response.json())
.then(data => {
if (data.success) {
showLogDetailModal(data.log);
} else {
showError('获取日志详情失败: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
showError('网络错误,请重试');
});
}
// 显示日志详情模态框
function showLogDetailModal(log) {
const modalHtml = `
<div class="modal fade" id="logDetailModal" 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 class="log-detail">
<div class="row">
<div class="col-md-6">
<div class="log-info-item">
<span class="log-info-label">日志ID:</span>
<span class="log-info-value">#${log.id}</span>
</div>
<div class="log-info-item">
<span class="log-info-label">操作者:</span>
<span class="log-info-value">
<span class="badge bg-${log.user_type === 2 ? 'warning' : 'info'}">
${log.user_type === 2 ? '管理员' : '用户'}
</span>
#${log.user_id || '未知'}
</span>
</div>
<div class="log-info-item">
<span class="log-info-label">操作类型:</span>
<span class="log-info-value operation-action">${log.action}</span>
</div>
<div class="log-info-item">
<span class="log-info-label">操作对象:</span>
<span class="log-info-value">
${log.resource_type || '无'}
${log.resource_id ? `#${log.resource_id}` : ''}
</span>
</div>
</div>
<div class="col-md-6">
<div class="log-info-item">
<span class="log-info-label">IP地址:</span>
<span class="log-info-value"><code>${log.ip_address || '未知'}</code></span>
</div>
<div class="log-info-item">
<span class="log-info-label">操作时间:</span>
<span class="log-info-value">${formatDateTime(log.created_at)}</span>
</div>
</div>
</div>
${log.user_agent ? `
<div class="mt-3">
<h6>用户代理信息:</h6>
<div class="user-agent-detail">
<code>${log.user_agent}</code>
</div>
</div>
` : ''}
${log.request_data ? `
<div class="mt-3">
<h6>请求数据:</h6>
<pre class="request-data"><code>${JSON.stringify(log.request_data, null, 2)}</code></pre>
</div>
` : ''}
</div>
</div>
</div>
</div>
</div>
`;
// 移除现有的模态框
const existingModal = document.getElementById('logDetailModal');
if (existingModal) {
existingModal.remove();
}
// 添加新的模态框
document.body.insertAdjacentHTML('beforeend', modalHtml);
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('logDetailModal'));
modal.show();
}
// 导出日志
function exportLogs() {
// 获取当前筛选条件
const userType = document.getElementById('user_type').value;
const action = document.getElementById('action').value;
// 构建导出URL
const params = new URLSearchParams();
if (userType) params.append('user_type', userType);
if (action) params.append('action', action);
const exportUrl = `/admin/logs/export?${params.toString()}`;
// 下载文件
window.location.href = exportUrl;
}
// 清理日志
function clearLogs() {
if (!confirm('确定要清理历史日志吗?此操作不可逆!')) {
return;
}
const daysToKeep = prompt('请输入要保留的天数例如30:', '30');
if (!daysToKeep || isNaN(daysToKeep) || daysToKeep <= 0) {
showError('请输入有效的天数');
return;
}
// 显示加载状态
showLoading();
// 发送AJAX请求
fetch('/admin/logs/clear', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
days_to_keep: parseInt(daysToKeep)
})
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
showSuccess(data.message);
// 刷新页面
setTimeout(() => {
location.reload();
}, 1000);
} else {
showError(data.message);
}
})
.catch(error => {
hideLoading();
console.error('Error:', error);
showError('网络错误,请重试');
});
}
// 搜索日志
function searchLogs() {
const searchForm = document.querySelector('form[method="GET"]');
if (searchForm) {
searchForm.submit();
}
}
// 重置搜索
function resetSearch() {
window.location.href = '/admin/logs';
}
// 格式化日期时间
function formatDateTime(dateTimeString) {
if (!dateTimeString) return '未知';
const date = new Date(dateTimeString);
return date.toLocaleString('zh-CN');
}
// 显示成功消息
function showSuccess(message) {
showAlert(message, 'success');
}
// 显示错误消息
function showError(message) {
showAlert(message, 'danger');
}
// 显示提示消息
function showAlert(message, type) {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
// 插入到页面顶部
const container = document.querySelector('.admin-content');
container.insertBefore(alertDiv, container.firstChild);
// 3秒后自动关闭
setTimeout(() => {
alertDiv.remove();
}, 3000);
}
// 显示加载状态
function showLoading() {
const loadingDiv = document.createElement('div');
loadingDiv.id = 'loading-overlay';
loadingDiv.innerHTML = `
<div class="loading-spinner">
<i class="bi bi-hourglass-split"></i>
<div>处理中...</div>
</div>
`;
loadingDiv.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
color: white;
`;
document.body.appendChild(loadingDiv);
}
// 隐藏加载状态
function hideLoading() {
const loadingDiv = document.getElementById('loading-overlay');
if (loadingDiv) {
loadingDiv.remove();
}
}
// 表格排序功能
function sortTable(column) {
// 实现表格排序功能
console.log('Sort by:', column);
}
// 批量操作功能(可选)
function bulkOperation() {
// 实现批量操作功能
const selectedLogs = document.querySelectorAll('input[type="checkbox"]:checked');
if (selectedLogs.length === 0) {
showError('请选择要操作的日志');
return;
}
// 实现批量操作逻辑
}

View File

@ -0,0 +1,201 @@
// 订单管理JavaScript功能
document.addEventListener('DOMContentLoaded', function() {
// 初始化所有模态框
const shipModal = new bootstrap.Modal(document.getElementById('shipModal'));
const refundModal = new bootstrap.Modal(document.getElementById('refundModal'));
const cancelModal = new bootstrap.Modal(document.getElementById('cancelModal'));
// 当前操作的订单ID
let currentOrderId = null;
// 显示发货模态框
window.showShipModal = function(orderId, orderSn) {
currentOrderId = orderId;
document.getElementById('shipOrderSn').value = orderSn;
shipModal.show();
};
// 显示退款模态框
window.showRefundModal = function(orderId, orderSn) {
currentOrderId = orderId;
document.getElementById('refundOrderSn').value = orderSn;
refundModal.show();
};
// 显示取消模态框
window.showCancelModal = function(orderId, orderSn) {
currentOrderId = orderId;
document.getElementById('cancelOrderSn').value = orderSn;
cancelModal.show();
};
// 处理发货表单提交
document.getElementById('shipForm').addEventListener('submit', function(e) {
e.preventDefault();
if (!currentOrderId) {
showAlert('错误', '订单ID不能为空', 'danger');
return;
}
const formData = new FormData(this);
const submitBtn = this.querySelector('button[type="submit"]');
// 显示加载状态
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<i class="bi bi-arrow-clockwise spin"></i> 处理中...';
submitBtn.disabled = true;
fetch(`/admin/orders/${currentOrderId}/ship`, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('成功', '发货成功!', 'success');
shipModal.hide();
setTimeout(() => {
location.reload();
}, 1000);
} else {
showAlert('错误', data.message || '发货失败', 'danger');
}
})
.catch(error => {
console.error('Error:', error);
showAlert('错误', '网络请求失败', 'danger');
})
.finally(() => {
// 恢复按钮状态
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
});
});
// 处理退款表单提交
document.getElementById('refundForm').addEventListener('submit', function(e) {
e.preventDefault();
if (!currentOrderId) {
showAlert('错误', '订单ID不能为空', 'danger');
return;
}
const formData = new FormData(this);
const submitBtn = this.querySelector('button[type="submit"]');
// 显示加载状态
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<i class="bi bi-arrow-clockwise spin"></i> 处理中...';
submitBtn.disabled = true;
fetch(`/admin/orders/${currentOrderId}/refund`, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('成功', '退款成功!', 'success');
refundModal.hide();
setTimeout(() => {
location.reload();
}, 1000);
} else {
showAlert('错误', data.message || '退款失败', 'danger');
}
})
.catch(error => {
console.error('Error:', error);
showAlert('错误', '网络请求失败', 'danger');
})
.finally(() => {
// 恢复按钮状态
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
});
});
// 处理取消表单提交
document.getElementById('cancelForm').addEventListener('submit', function(e) {
e.preventDefault();
if (!currentOrderId) {
showAlert('错误', '订单ID不能为空', 'danger');
return;
}
const formData = new FormData(this);
const submitBtn = this.querySelector('button[type="submit"]');
// 显示加载状态
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<i class="bi bi-arrow-clockwise spin"></i> 处理中...';
submitBtn.disabled = true;
fetch(`/admin/orders/${currentOrderId}/cancel`, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('成功', '订单取消成功!', 'success');
cancelModal.hide();
setTimeout(() => {
location.reload();
}, 1000);
} else {
showAlert('错误', data.message || '取消失败', 'danger');
}
})
.catch(error => {
console.error('Error:', error);
showAlert('错误', '网络请求失败', 'danger');
})
.finally(() => {
// 恢复按钮状态
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
});
});
// 通用提示函数
function showAlert(title, message, type) {
// 创建提示框
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
alertDiv.innerHTML = `
<strong>${title}</strong> ${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alertDiv);
// 3秒后自动关闭
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.parentNode.removeChild(alertDiv);
}
}, 3000);
}
});
// 旋转动画CSS如果需要
if (!document.querySelector('#admin-orders-style')) {
const style = document.createElement('style');
style.id = 'admin-orders-style';
style.textContent = `
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
document.head.appendChild(style);
}

View File

@ -0,0 +1,370 @@
// 用户管理页面JavaScript
document.addEventListener('DOMContentLoaded', function() {
// 初始化
initializeUserManagement();
});
// 初始化用户管理功能
function initializeUserManagement() {
// 添加事件监听器
setupEventListeners();
// 初始化头像显示
initializeAvatars();
// 强制设置头像样式
forceAvatarStyles();
}
// 设置事件监听器
function setupEventListeners() {
// 搜索表单提交
const searchForm = document.querySelector('form[method="GET"]');
if (searchForm) {
searchForm.addEventListener('submit', function(e) {
// 可以在这里添加搜索前的验证
});
}
// 状态筛选变更
const statusSelect = document.getElementById('status');
if (statusSelect) {
statusSelect.addEventListener('change', function() {
// 自动提交表单
this.form.submit();
});
}
}
// 初始化头像显示
function initializeAvatars() {
const avatars = document.querySelectorAll('.user-avatar');
avatars.forEach(avatar => {
avatar.onerror = function() {
// 如果头像加载失败,替换为默认头像
this.style.display = 'none';
const placeholder = this.parentElement.querySelector('.user-avatar-placeholder');
if (placeholder) {
placeholder.style.display = 'flex';
}
};
});
}
// 强制设置头像样式覆盖Bootstrap
function forceAvatarStyles() {
// 设置表格中的头像样式
const tableAvatars = document.querySelectorAll('.table .user-avatar');
tableAvatars.forEach(avatar => {
setAvatarStyles(avatar, '48px');
});
}
// 设置单个头像样式
function setAvatarStyles(avatar, size) {
if (!avatar) return;
// 强制设置所有相关样式
avatar.style.setProperty('width', size, 'important');
avatar.style.setProperty('height', size, 'important');
avatar.style.setProperty('max-width', size, 'important');
avatar.style.setProperty('max-height', size, 'important');
avatar.style.setProperty('min-width', size, 'important');
avatar.style.setProperty('min-height', size, 'important');
avatar.style.setProperty('border-radius', '50%', 'important');
avatar.style.setProperty('object-fit', 'cover', 'important');
avatar.style.setProperty('border', '2px solid #f8f9fa', 'important');
avatar.style.setProperty('display', 'block', 'important');
avatar.style.setProperty('flex-shrink', '0', 'important');
avatar.style.setProperty('flex-grow', '0', 'important');
// 移除可能影响的Bootstrap类
avatar.classList.remove('img-fluid', 'img-responsive', 'img-thumbnail');
// 设置父元素样式
if (avatar.parentElement) {
avatar.parentElement.style.setProperty('width', size, 'important');
avatar.parentElement.style.setProperty('height', size, 'important');
avatar.parentElement.style.setProperty('overflow', 'hidden', 'important');
avatar.parentElement.style.setProperty('flex-shrink', '0', 'important');
avatar.parentElement.style.setProperty('flex-grow', '0', 'important');
}
}
// 查看用户详情
function viewUser(userId) {
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('userDetailModal'));
const content = document.getElementById('userDetailContent');
// 显示加载状态
content.innerHTML = `
<div class="loading">
<i class="bi bi-hourglass-split"></i>
<div>加载中...</div>
</div>
`;
modal.show();
// 发送AJAX请求获取用户详情
fetch(`/admin/users/${userId}/detail`)
.then(response => response.json())
.then(data => {
if (data.success) {
renderUserDetail(data.user);
// 立即强制设置头像样式
setTimeout(() => {
forceModalAvatarStyles();
}, 50);
// 再次确保样式正确应用
setTimeout(() => {
forceModalAvatarStyles();
}, 200);
} else {
showError('获取用户详情失败: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
showError('网络错误,请重试');
});
}
// 渲染用户详情
function renderUserDetail(user) {
const content = document.getElementById('userDetailContent');
content.innerHTML = `
<div class="user-detail">
<div class="row">
<div class="col-md-3 text-center">
<div class="user-avatar-large-wrapper">
${user.avatar_url ?
`<img src="${user.avatar_url}" alt="用户头像" class="avatar-large" id="modalAvatar">` :
`<div class="avatar-placeholder-large"><i class="bi bi-person"></i></div>`
}
</div>
<h5 class="mt-3">${user.nickname || user.username}</h5>
<span class="badge bg-${user.status === 1 ? 'success' : 'danger'}">
${user.status === 1 ? '正常' : '禁用'}
</span>
</div>
<div class="col-md-9">
<div class="user-info-list">
<div class="user-info-item">
<span class="user-info-label">用户ID:</span>
<span class="user-info-value">#${user.id}</span>
</div>
<div class="user-info-item">
<span class="user-info-label">用户名:</span>
<span class="user-info-value">${user.username}</span>
</div>
<div class="user-info-item">
<span class="user-info-label">昵称:</span>
<span class="user-info-value">${user.nickname || '未设置'}</span>
</div>
<div class="user-info-item">
<span class="user-info-label">邮箱:</span>
<span class="user-info-value">${user.email || '未设置'}</span>
</div>
<div class="user-info-item">
<span class="user-info-label">手机号:</span>
<span class="user-info-value">${user.phone || '未设置'}</span>
</div>
<div class="user-info-item">
<span class="user-info-label">性别:</span>
<span class="user-info-value">${getGenderText(user.gender)}</span>
</div>
<div class="user-info-item">
<span class="user-info-label">生日:</span>
<span class="user-info-value">${user.birthday || '未设置'}</span>
</div>
<div class="user-info-item">
<span class="user-info-label">注册时间:</span>
<span class="user-info-value">${formatDateTime(user.created_at)}</span>
</div>
<div class="user-info-item">
<span class="user-info-label">最后更新:</span>
<span class="user-info-value">${formatDateTime(user.updated_at)}</span>
</div>
</div>
</div>
</div>
</div>
`;
}
// 强制设置模态框中的头像样式
function forceModalAvatarStyles() {
const modalAvatar = document.getElementById('modalAvatar');
if (modalAvatar) {
setAvatarStyles(modalAvatar, '80px');
// 设置容器样式
const wrapper = document.querySelector('.user-avatar-large-wrapper');
if (wrapper) {
wrapper.style.setProperty('width', '80px', 'important');
wrapper.style.setProperty('height', '80px', 'important');
wrapper.style.setProperty('margin', '0 auto', 'important');
wrapper.style.setProperty('overflow', 'hidden', 'important');
wrapper.style.setProperty('border-radius', '50%', 'important');
wrapper.style.setProperty('position', 'relative', 'important');
wrapper.style.setProperty('flex-shrink', '0', 'important');
wrapper.style.setProperty('flex-grow', '0', 'important');
}
}
// 通用的模态框头像处理
const modalAvatars = document.querySelectorAll('.modal .avatar-large');
modalAvatars.forEach(avatar => {
setAvatarStyles(avatar, '80px');
});
}
// 切换用户状态
function toggleUserStatus(userId, currentStatus) {
const action = currentStatus === 1 ? '禁用' : '启用';
const newStatus = currentStatus === 1 ? 0 : 1;
if (!confirm(`确定要${action}此用户吗?`)) {
return;
}
// 显示加载状态
showLoading();
// 发送AJAX请求
fetch(`/admin/users/${userId}/toggle-status`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
status: newStatus
})
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
showSuccess(data.message);
// 刷新页面
setTimeout(() => {
location.reload();
}, 1000);
} else {
showError(data.message);
}
})
.catch(error => {
hideLoading();
console.error('Error:', error);
showError('网络错误,请重试');
});
}
// 获取性别文本
function getGenderText(gender) {
switch (gender) {
case 1: return '男';
case 2: return '女';
default: return '未知';
}
}
// 格式化日期时间
function formatDateTime(dateTimeString) {
if (!dateTimeString) return '未知';
const date = new Date(dateTimeString);
return date.toLocaleString('zh-CN');
}
// 显示成功消息
function showSuccess(message) {
showAlert(message, 'success');
}
// 显示错误消息
function showError(message) {
showAlert(message, 'danger');
}
// 显示提示消息
function showAlert(message, type) {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
// 插入到页面顶部
const container = document.querySelector('.admin-content');
container.insertBefore(alertDiv, container.firstChild);
// 3秒后自动关闭
setTimeout(() => {
alertDiv.remove();
}, 3000);
}
// 显示加载状态
function showLoading() {
const loadingDiv = document.createElement('div');
loadingDiv.id = 'loading-overlay';
loadingDiv.innerHTML = `
<div class="loading-spinner">
<i class="bi bi-hourglass-split"></i>
<div>处理中...</div>
</div>
`;
loadingDiv.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
color: white;
`;
document.body.appendChild(loadingDiv);
}
// 隐藏加载状态
function hideLoading() {
const loadingDiv = document.getElementById('loading-overlay');
if (loadingDiv) {
loadingDiv.remove();
}
}
// 页面加载完成后强制设置头像样式
window.addEventListener('load', function() {
forceAvatarStyles();
});
// 定时检查并修复头像样式
setInterval(function() {
// 检查并修复表格头像
const tableAvatars = document.querySelectorAll('.table .user-avatar');
tableAvatars.forEach(avatar => {
if (avatar.offsetWidth > 60 || avatar.offsetHeight > 60) {
setAvatarStyles(avatar, '48px');
}
});
// 检查并修复模态框头像
const modalAvatars = document.querySelectorAll('.modal .avatar-large');
modalAvatars.forEach(avatar => {
if (avatar.offsetWidth > 100 || avatar.offsetHeight > 100) {
setAvatarStyles(avatar, '80px');
}
});
}, 500);

View File

@ -3,11 +3,13 @@
// 返回顶部功能
window.addEventListener('scroll', function() {
const backToTop = document.getElementById('backToTop');
if (backToTop) {
if (window.pageYOffset > 300) {
backToTop.style.display = 'block';
} else {
backToTop.style.display = 'none';
}
}
});
function scrollToTop() {
@ -20,12 +22,94 @@ function scrollToTop() {
// 购物车数量更新
function updateCartBadge(count) {
const badge = document.getElementById('cartBadge');
if (badge) {
if (count > 0) {
badge.textContent = count;
badge.style.display = 'inline-block';
} else {
badge.style.display = 'none';
}
}
}
// 通用提示函数
function showAlert(message, type = 'info', duration = 3000) {
// 移除现有的提示框
const existingAlerts = document.querySelectorAll('.custom-alert');
existingAlerts.forEach(alert => alert.remove());
// 创建新的提示框
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${getBootstrapAlertType(type)} alert-dismissible fade show custom-alert`;
alertDiv.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
min-width: 300px;
max-width: 500px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
`;
const icon = getAlertIcon(type);
alertDiv.innerHTML = `
<div class="d-flex align-items-center">
<i class="bi ${icon} me-2"></i>
<div class="flex-grow-1">${message}</div>
<button type="button" class="btn-close" onclick="this.parentElement.parentElement.remove()"></button>
</div>
`;
document.body.appendChild(alertDiv);
// 自动消失
if (duration > 0) {
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.classList.remove('show');
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 150);
}
}, duration);
}
return alertDiv;
}
// 获取Bootstrap警告类型
function getBootstrapAlertType(type) {
const typeMap = {
'success': 'success',
'error': 'danger',
'warning': 'warning',
'info': 'info'
};
return typeMap[type] || 'info';
}
// 获取警告图标
function getAlertIcon(type) {
const iconMap = {
'success': 'bi-check-circle-fill',
'error': 'bi-exclamation-triangle-fill',
'warning': 'bi-exclamation-triangle-fill',
'info': 'bi-info-circle-fill'
};
return iconMap[type] || 'bi-info-circle-fill';
}
// 确认对话框
function showConfirm(message, callback) {
if (confirm(message)) {
if (typeof callback === 'function') {
callback();
}
return true;
}
return false;
}
// 页面加载完成后的初始化
@ -41,36 +125,79 @@ document.addEventListener('DOMContentLoaded', function() {
});
// 初始化购物车数量
// TODO: 实现购物车数量获取
updateCartCount();
});
// 通用AJAX错误处理
function handleAjaxError(xhr) {
function handleAjaxError(xhr, defaultMessage = '操作失败,请稍后再试') {
if (xhr.status === 401) {
alert('请先登录');
window.location.href = '/auth/login';
} else if (xhr.status === 403) {
alert('没有权限执行此操作');
} else {
alert('操作失败,请稍后再试');
}
}
// 通用成功提示
function showSuccessMessage(message) {
// 创建临时提示框
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-success alert-dismissible fade show position-fixed success-toast';
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alertDiv);
// 3秒后自动消失
showAlert('请先登录', 'warning');
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.parentNode.removeChild(alertDiv);
window.location.href = '/auth/login';
}, 1500);
} else if (xhr.status === 403) {
showAlert('没有权限执行此操作', 'error');
} else if (xhr.status === 404) {
showAlert('请求的资源不存在', 'error');
} else if (xhr.status >= 500) {
showAlert('服务器错误,请稍后再试', 'error');
} else {
showAlert(defaultMessage, 'error');
}
}
// 通用成功提示(保持向后兼容)
function showSuccessMessage(message) {
showAlert(message, 'success');
}
// 更新购物车数量
function updateCartCount() {
fetch('/cart/count')
.then(response => response.json())
.then(data => {
if (data.success) {
updateCartBadge(data.count);
}
})
.catch(error => {
console.log('获取购物车数量失败:', error);
});
}
// 格式化价格
function formatPrice(price) {
return '¥' + parseFloat(price).toFixed(2);
}
// 格式化数字
function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
// 防抖函数
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// 节流函数
function throttle(func, limit) {
let inThrottle;
return function() {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
}
}, 3000);
}

View File

@ -26,15 +26,24 @@ function selectAddress(addressId) {
card.classList.remove('selected');
});
document.querySelector(`[data-address-id="${addressId}"]`).classList.add('selected');
const selectedCard = document.querySelector(`[data-address-id="${addressId}"]`);
if (selectedCard) {
selectedCard.classList.add('selected');
}
// 更新单选按钮
document.querySelector(`input[value="${addressId}"]`).checked = true;
const radioButton = document.querySelector(`input[value="${addressId}"]`);
if (radioButton) {
radioButton.checked = true;
}
}
// 更新运费
function updateShippingFee() {
const shippingMethod = document.querySelector('input[name="shipping_method"]:checked').value;
const shippingMethodElement = document.querySelector('input[name="shipping_method"]:checked');
if (!shippingMethodElement) return;
const shippingMethod = shippingMethodElement.value;
let fee = 0;
switch(shippingMethod) {
@ -48,20 +57,44 @@ function updateShippingFee() {
fee = 0;
}
document.getElementById('shippingFee').textContent = `¥${fee.toFixed(2)}`;
document.getElementById('totalAmount').textContent = `¥${(subtotal + fee).toFixed(2)}`;
const shippingFeeElement = document.getElementById('shippingFee');
const totalAmountElement = document.getElementById('totalAmount');
if (shippingFeeElement) {
shippingFeeElement.textContent = `¥${fee.toFixed(2)}`;
}
if (totalAmountElement) {
totalAmountElement.textContent = `¥${(subtotal + fee).toFixed(2)}`;
}
}
// 提交订单
function submitOrder() {
// 验证地址选择
if (!selectedAddressId) {
showAlert('请选择收货地址', 'warning');
return;
}
const shippingMethod = document.querySelector('input[name="shipping_method"]:checked').value;
const paymentMethod = document.querySelector('input[name="payment_method"]:checked').value;
const remark = document.getElementById('orderRemark').value;
// 获取表单数据
const shippingMethodElement = document.querySelector('input[name="shipping_method"]:checked');
const paymentMethodElement = document.querySelector('input[name="payment_method"]:checked');
const remarkElement = document.getElementById('orderRemark');
if (!shippingMethodElement) {
showAlert('请选择配送方式', 'warning');
return;
}
if (!paymentMethodElement) {
showAlert('请选择支付方式', 'warning');
return;
}
const shippingMethod = shippingMethodElement.value;
const paymentMethod = paymentMethodElement.value;
const remark = remarkElement ? remarkElement.value : '';
// 获取选中的购物车商品ID
const urlParams = new URLSearchParams(window.location.search);
@ -82,10 +115,16 @@ function submitOrder() {
// 显示加载状态
const submitBtn = document.querySelector('.btn-danger');
if (!submitBtn) {
showAlert('提交按钮未找到', 'error');
return;
}
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<i class="bi bi-hourglass-split"></i> 提交中...';
submitBtn.disabled = true;
// 提交订单
fetch('/order/create', {
method: 'POST',
headers: {
@ -93,7 +132,12 @@ function submitOrder() {
},
body: JSON.stringify(orderData)
})
.then(response => response.json())
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.success) {
showAlert('订单创建成功!正在跳转到支付页面...', 'success');
@ -101,13 +145,16 @@ function submitOrder() {
window.location.href = `/order/pay/${data.payment_sn}`;
}, 1500);
} else {
showAlert(data.message, 'error');
showAlert(data.message || '订单创建失败', 'error');
// 恢复按钮状态
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
}
})
.catch(error => {
console.error('提交订单错误:', error);
showAlert('提交订单失败,请重试', 'error');
// 恢复按钮状态
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
});

229
app/static/js/favorites.js Normal file
View File

@ -0,0 +1,229 @@
// 收藏页面JavaScript
let selectedItems = [];
let isSelectAll = false;
document.addEventListener('DOMContentLoaded', function() {
// 初始化事件监听
initEventListeners();
});
function initEventListeners() {
// 复选框变化事件
document.querySelectorAll('.favorite-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', function() {
updateSelectedItems();
});
});
}
function updateSelectedItems() {
selectedItems = [];
document.querySelectorAll('.favorite-checkbox:checked').forEach(checkbox => {
selectedItems.push(parseInt(checkbox.value));
});
// 更新按钮状态
const batchBtn = document.querySelector('[onclick="batchRemove()"]');
if (batchBtn) {
batchBtn.disabled = selectedItems.length === 0;
}
}
function toggleSelectAll() {
isSelectAll = !isSelectAll;
const checkboxes = document.querySelectorAll('.favorite-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = isSelectAll;
});
updateSelectedItems();
// 更新按钮文本
const selectAllBtn = document.querySelector('[onclick="toggleSelectAll()"]');
if (selectAllBtn) {
selectAllBtn.innerHTML = isSelectAll ?
'<i class="bi bi-square"></i> 取消全选' :
'<i class="bi bi-check-square"></i> 全选';
}
}
function removeFavorite(productId) {
if (confirm('确定要取消收藏这个商品吗?')) {
fetch('/favorite/remove', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
product_id: productId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage(data.message);
// 移除商品卡片
const itemElement = document.querySelector(`[data-product-id="${productId}"]`);
if (itemElement) {
itemElement.remove();
}
// 更新收藏数量
updateFavoriteCount();
// 检查是否为空
checkEmptyState();
} else {
showErrorMessage(data.message);
}
})
.catch(error => {
console.error('Error:', error);
showErrorMessage('操作失败,请稍后再试');
});
}
}
function batchRemove() {
if (selectedItems.length === 0) {
showErrorMessage('请选择要删除的商品');
return;
}
const modal = new bootstrap.Modal(document.getElementById('confirmModal'));
document.getElementById('confirmMessage').textContent =
`确定要取消收藏这 ${selectedItems.length} 个商品吗?`;
document.getElementById('confirmBtn').onclick = function() {
performBatchRemove();
modal.hide();
};
modal.show();
}
function performBatchRemove() {
fetch('/favorite/batch-remove', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
product_ids: selectedItems
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage(data.message);
// 移除选中的商品卡片
selectedItems.forEach(productId => {
const itemElement = document.querySelector(`[data-product-id="${productId}"]`);
if (itemElement) {
itemElement.remove();
}
});
// 重置选择状态
selectedItems = [];
isSelectAll = false;
updateSelectedItems();
// 更新收藏数量
updateFavoriteCount();
// 检查是否为空
checkEmptyState();
} else {
showErrorMessage(data.message);
}
})
.catch(error => {
console.error('Error:', error);
showErrorMessage('批量删除失败,请稍后再试');
});
}
function addToCart(productId) {
// 调用购物车添加功能
fetch('/cart/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
product_id: productId,
quantity: 1
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage(data.message);
// 更新购物车数量
if (typeof updateCartBadge === 'function') {
updateCartBadge(data.cart_count);
}
} else {
showErrorMessage(data.message);
}
})
.catch(error => {
console.error('Error:', error);
showErrorMessage('加入购物车失败,请稍后再试');
});
}
function updateFavoriteCount() {
fetch('/favorite/count')
.then(response => response.json())
.then(data => {
if (data.success) {
// 更新收藏数量显示
const badge = document.querySelector('.badge.bg-secondary');
if (badge) {
badge.textContent = `${data.favorite_count} 件商品`;
}
}
})
.catch(error => {
console.error('Error:', error);
});
}
function checkEmptyState() {
const itemsContainer = document.querySelector('.row');
const items = itemsContainer.querySelectorAll('.favorite-item');
if (items.length === 0) {
// 显示空状态
itemsContainer.innerHTML = `
<div class="col-12">
<div class="empty-state">
<div class="text-center py-5">
<i class="bi bi-heart display-1 text-muted"></i>
<h4 class="mt-3 text-muted">还没有收藏任何商品</h4>
<p class="text-muted">去逛逛收藏心仪的商品吧~</p>
<a href="/" class="btn btn-primary">
<i class="bi bi-house"></i>
</a>
</div>
</div>
</div>
`;
}
}
function showSuccessMessage(message) {
// 使用现有的消息提示函数
if (typeof showMessage === 'function') {
showMessage(message, 'success');
} else {
alert(message);
}
}
function showErrorMessage(message) {
// 使用现有的消息提示函数
if (typeof showMessage === 'function') {
showMessage(message, 'error');
} else {
alert(message);
}
}

278
app/static/js/history.js Normal file
View File

@ -0,0 +1,278 @@
// 浏览历史页面JavaScript
let selectedItems = [];
let isSelectAll = false;
document.addEventListener('DOMContentLoaded', function() {
// 初始化事件监听
initEventListeners();
});
function initEventListeners() {
// 复选框变化事件
document.querySelectorAll('.history-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', function() {
updateSelectedItems();
});
});
}
function updateSelectedItems() {
selectedItems = [];
document.querySelectorAll('.history-checkbox:checked').forEach(checkbox => {
selectedItems.push(parseInt(checkbox.value));
});
// 更新按钮状态
const batchBtn = document.querySelector('[onclick="batchRemove()"]');
if (batchBtn) {
batchBtn.disabled = selectedItems.length === 0;
}
}
function toggleSelectAll() {
isSelectAll = !isSelectAll;
const checkboxes = document.querySelectorAll('.history-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = isSelectAll;
});
updateSelectedItems();
// 更新按钮文本
const selectAllBtn = document.querySelector('[onclick="toggleSelectAll()"]');
if (selectAllBtn) {
selectAllBtn.innerHTML = isSelectAll ?
'<i class="bi bi-square"></i> 取消全选' :
'<i class="bi bi-check-square"></i> 全选';
}
}
function removeHistory(productId) {
if (confirm('确定要删除这个浏览记录吗?')) {
fetch('/history/remove', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
product_id: productId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage(data.message);
// 移除商品卡片
const itemElement = document.querySelector(`[data-product-id="${productId}"]`);
if (itemElement) {
itemElement.remove();
}
// 更新历史数量
updateHistoryCount();
// 检查是否为空
checkEmptyState();
} else {
showErrorMessage(data.message);
}
})
.catch(error => {
console.error('Error:', error);
showErrorMessage('操作失败,请稍后再试');
});
}
}
function batchRemove() {
if (selectedItems.length === 0) {
showErrorMessage('请选择要删除的商品');
return;
}
const modal = new bootstrap.Modal(document.getElementById('confirmModal'));
document.getElementById('confirmMessage').textContent =
`确定要删除这 ${selectedItems.length} 个浏览记录吗?`;
document.getElementById('confirmBtn').onclick = function() {
performBatchRemove();
modal.hide();
};
modal.show();
}
function performBatchRemove() {
fetch('/history/batch-remove', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
product_ids: selectedItems
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage(data.message);
// 移除选中的商品卡片
selectedItems.forEach(productId => {
const itemElement = document.querySelector(`[data-product-id="${productId}"]`);
if (itemElement) {
itemElement.remove();
}
});
// 重置选择状态
selectedItems = [];
isSelectAll = false;
updateSelectedItems();
// 更新历史数量
updateHistoryCount();
// 检查是否为空
checkEmptyState();
} else {
showErrorMessage(data.message);
}
})
.catch(error => {
console.error('Error:', error);
showErrorMessage('批量删除失败,请稍后再试');
});
}
function clearHistory() {
if (confirm('确定要清空所有浏览历史吗?此操作不可恢复。')) {
fetch('/history/clear', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage(data.message);
// 重新加载页面或显示空状态
location.reload();
} else {
showErrorMessage(data.message);
}
})
.catch(error => {
console.error('Error:', error);
showErrorMessage('清空历史失败,请稍后再试');
});
}
}
function addToCart(productId) {
// 调用购物车添加功能
fetch('/cart/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
product_id: productId,
quantity: 1
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage(data.message);
// 更新购物车数量
if (typeof updateCartBadge === 'function') {
updateCartBadge(data.cart_count);
}
} else {
showErrorMessage(data.message);
}
})
.catch(error => {
console.error('Error:', error);
showErrorMessage('加入购物车失败,请稍后再试');
});
}
function addToFavorites(productId) {
fetch('/favorite/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
product_id: productId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage(data.message);
} else {
showErrorMessage(data.message);
}
})
.catch(error => {
console.error('Error:', error);
showErrorMessage('收藏失败,请稍后再试');
});
}
function updateHistoryCount() {
fetch('/history/count')
.then(response => response.json())
.then(data => {
if (data.success) {
// 更新历史数量显示
const badge = document.querySelector('.badge.bg-secondary');
if (badge) {
badge.textContent = `${data.history_count} 件商品`;
}
}
})
.catch(error => {
console.error('Error:', error);
});
}
function checkEmptyState() {
const itemsContainer = document.querySelector('.row');
const items = itemsContainer.querySelectorAll('.history-item');
if (items.length === 0) {
// 显示空状态
itemsContainer.innerHTML = `
<div class="col-12">
<div class="empty-state">
<div class="text-center py-5">
<i class="bi bi-clock-history display-1 text-muted"></i>
<h4 class="mt-3 text-muted">还没有浏览任何商品</h4>
<p class="text-muted">去逛逛看看有什么好商品~</p>
<a href="/" class="btn btn-primary">
<i class="bi bi-house"></i>
</a>
</div>
</div>
</div>
`;
}
}
function showSuccessMessage(message) {
// 使用现有的消息提示函数
if (typeof showMessage === 'function') {
showMessage(message, 'success');
} else {
alert(message);
}
}
function showErrorMessage(message) {
// 使用现有的消息提示函数
if (typeof showMessage === 'function') {
showMessage(message, 'error');
} else {
alert(message);
}
}

View File

@ -1,4 +1,4 @@
// 订单详情页面脚本
// 订单详情页面脚本 - 只处理业务逻辑,不处理样式
// 取消订单
function cancelOrder(orderId) {

View File

@ -16,13 +16,16 @@ window.addEventListener('beforeunload', function() {
// 开始倒计时
function startCountdown() {
const countdownElement = document.getElementById('countdown');
if (!countdownElement) return;
countdownTimer = setInterval(() => {
timeLeft--;
const minutes = Math.floor(timeLeft / 60);
const seconds = timeLeft % 60;
document.getElementById('countdown').textContent =
countdownElement.textContent =
`${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
if (timeLeft <= 0) {
@ -45,6 +48,12 @@ function startPayment() {
return;
}
// 如果是模拟支付,直接显示控制面板,不需要调用接口
if (paymentMethod === 'simulate') {
showAlert('请使用下方控制面板完成模拟支付', 'info');
return;
}
fetch('/payment/process', {
method: 'POST',
headers: {
@ -55,7 +64,12 @@ function startPayment() {
payment_method: paymentMethod
})
})
.then(response => response.json())
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.success) {
if (data.payment_type === 'qrcode') {
@ -64,12 +78,15 @@ function startPayment() {
} else if (data.payment_type === 'redirect') {
window.open(data.pay_url, '_blank');
startStatusCheck();
} else if (data.payment_type === 'simulate') {
showAlert('模拟支付已准备就绪', 'success');
}
} else {
showAlert(data.message, 'error');
showAlert(data.message || '支付启动失败', 'error');
}
})
.catch(error => {
console.error('支付启动错误:', error);
showAlert('支付启动失败,请重试', 'error');
});
}
@ -79,6 +96,8 @@ function showQRCode(qrUrl) {
const qrArea = document.getElementById('qrCodeArea');
const qrImage = document.getElementById('qrCodeImage');
if (!qrArea || !qrImage) return;
// 这里应该使用真实的二维码生成库,现在用文本模拟
qrImage.innerHTML = `
<div style="width: 200px; height: 200px; margin: 0 auto; background: #f0f0f0;
@ -107,13 +126,21 @@ function checkPaymentStatus() {
if (!paymentSn) return;
fetch(`/payment/check_status/${paymentSn}`)
.then(response => response.json())
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.success) {
if (data.status === 2) { // 支付成功
clearInterval(statusCheckTimer);
clearInterval(countdownTimer);
showPaymentSuccess();
} else if (data.status === 3) { // 支付失败
clearInterval(statusCheckTimer);
showPaymentFail();
}
}
})
@ -124,15 +151,47 @@ function checkPaymentStatus() {
// 显示支付成功
function showPaymentSuccess() {
document.getElementById('paymentArea').style.display = 'none';
document.getElementById('paymentStatus').style.display = 'block';
const paymentArea = document.getElementById('paymentArea');
const actionButtons = document.getElementById('actionButtons');
const paymentStatus = document.getElementById('paymentStatus');
const successStatus = document.getElementById('successStatus');
if (paymentArea) paymentArea.style.display = 'none';
if (actionButtons) actionButtons.style.display = 'none';
if (paymentStatus) paymentStatus.style.display = 'block';
if (successStatus) successStatus.style.display = 'block';
showAlert('支付成功!正在跳转到订单详情...', 'success');
const orderId = document.querySelector('[data-order-id]')?.dataset.orderId;
setTimeout(() => {
if (orderId) {
window.location.href = `/order/detail/${orderId}`;
} else {
window.location.href = '/order/list';
}
}, 2000);
}
// 显示支付失败
function showPaymentFail() {
const paymentArea = document.getElementById('paymentArea');
const paymentStatus = document.getElementById('paymentStatus');
const failStatus = document.getElementById('failStatus');
if (paymentArea) paymentArea.style.display = 'none';
if (paymentStatus) paymentStatus.style.display = 'block';
if (failStatus) failStatus.style.display = 'block';
showAlert('支付失败,请重新尝试', 'error');
// 显示重试按钮
setTimeout(() => {
if (paymentArea) paymentArea.style.display = 'block';
if (paymentStatus) paymentStatus.style.display = 'none';
}, 3000);
}
// 取消订单
function cancelOrder() {
const orderId = document.querySelector('[data-order-id]')?.dataset.orderId;
@ -142,32 +201,38 @@ function cancelOrder() {
return;
}
if (confirm('确定要取消这个订单吗?')) {
showConfirm('确定要取消这个订单吗?', () => {
fetch(`/order/cancel/${orderId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.success) {
showAlert('订单已取消', 'success');
showAlert('订单已取消,正在跳转...', 'success');
setTimeout(() => {
window.location.href = '/order/list';
}, 1500);
} else {
showAlert(data.message, 'error');
showAlert(data.message || '取消订单失败', 'error');
}
})
.catch(error => {
console.error('取消订单错误:', error);
showAlert('取消失败,请重试', 'error');
});
}
});
}
// 模拟支付成功(开发测试用)
function simulatePayment() {
// 模拟支付成功
function simulatePaymentSuccess() {
const paymentSn = document.querySelector('[data-payment-sn]')?.dataset.paymentSn;
if (!paymentSn) {
@ -175,26 +240,94 @@ function simulatePayment() {
return;
}
if (confirm('这是测试功能,确定要模拟支付成功吗?')) {
// 显示处理中状态
const button = event.target;
const originalText = button.innerHTML;
button.innerHTML = '<i class="bi bi-hourglass-split"></i> 处理中...';
button.disabled = true;
fetch(`/payment/simulate_success/${paymentSn}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.success) {
showAlert('模拟支付成功', 'success');
showAlert('模拟支付成功', 'success');
setTimeout(() => {
showPaymentSuccess();
}, 1000);
} else {
showAlert(data.message, 'error');
showAlert(data.message || '模拟支付失败', 'error');
button.innerHTML = originalText;
button.disabled = false;
}
})
.catch(error => {
console.error('模拟支付错误:', error);
showAlert('模拟支付失败', 'error');
button.innerHTML = originalText;
button.disabled = false;
});
}
}
// 模拟支付失败
function simulatePaymentFail() {
const paymentSn = document.querySelector('[data-payment-sn]')?.dataset.paymentSn;
if (!paymentSn) {
showAlert('支付信息获取失败', 'error');
return;
}
showConfirm('确定要模拟支付失败吗?这将导致订单支付失败。', () => {
// 显示处理中状态
const button = event.target;
const originalText = button.innerHTML;
button.innerHTML = '<i class="bi bi-hourglass-split"></i> 处理中...';
button.disabled = true;
fetch(`/payment/simulate_fail/${paymentSn}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.success) {
showAlert('模拟支付失败!', 'warning');
setTimeout(() => {
showPaymentFail();
}, 1000);
} else {
showAlert(data.message || '模拟操作失败', 'error');
button.innerHTML = originalText;
button.disabled = false;
}
})
.catch(error => {
console.error('模拟支付失败错误:', error);
showAlert('模拟操作失败', 'error');
button.innerHTML = originalText;
button.disabled = false;
});
});
}
// 兼容旧版本的模拟支付函数
function simulatePayment() {
simulatePaymentSuccess();
}

View File

@ -22,8 +22,67 @@ document.addEventListener('DOMContentLoaded', function() {
if (typeof loadCartCount === 'function') {
loadCartCount();
}
// 添加浏览历史记录
if (window.isLoggedIn && window.productId) {
addBrowseHistory(window.productId);
}
// 检查收藏状态
if (window.isLoggedIn && window.productId) {
checkFavoriteStatus(window.productId);
}
});
// 添加浏览历史记录
function addBrowseHistory(productId) {
fetch('/history/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
product_id: productId
})
})
.then(response => response.json())
.then(data => {
// 静默添加,不需要用户感知
console.log('浏览历史记录已添加');
})
.catch(error => {
console.error('添加浏览历史失败:', error);
});
}
// 检查收藏状态
function checkFavoriteStatus(productId) {
fetch(`/favorite/check/${productId}`)
.then(response => response.json())
.then(data => {
if (data.success) {
updateFavoriteButton(data.is_favorited);
}
})
.catch(error => {
console.error('检查收藏状态失败:', error);
});
}
// 更新收藏按钮状态
function updateFavoriteButton(isFavorited) {
const favoriteBtn = document.querySelector('[onclick="addToFavorites()"]');
if (favoriteBtn) {
if (isFavorited) {
favoriteBtn.innerHTML = '<i class="bi bi-heart-fill text-danger"></i> 已收藏';
favoriteBtn.className = 'btn btn-outline-danger';
} else {
favoriteBtn.innerHTML = '<i class="bi bi-heart"></i> 收藏商品';
favoriteBtn.className = 'btn btn-outline-secondary';
}
}
}
// 规格选择
function selectSpec(button) {
const specName = button.getAttribute('data-spec-name');
@ -231,6 +290,68 @@ function buyNow() {
// 收藏商品
function addToFavorites() {
// TODO: 实现收藏功能
alert('收藏功能开发中...');
if (!window.isLoggedIn) {
if (confirm('请先登录后再收藏,是否前往登录?')) {
window.location.href = '/auth/login?next=' + encodeURIComponent(window.location.pathname);
}
return;
}
// 确保获取到商品ID
const productId = window.productId || window.currentProductId;
if (!productId) {
alert('获取商品信息失败,请刷新页面重试');
return;
}
console.log('收藏商品ID:', productId); // 调试信息
const favoriteBtn = document.querySelector('[onclick="addToFavorites()"]');
const isFavorited = favoriteBtn && favoriteBtn.innerHTML.includes('已收藏');
// 临时禁用按钮
if (favoriteBtn) {
favoriteBtn.disabled = true;
favoriteBtn.innerHTML = '<i class="bi bi-hourglass-split"></i> 处理中...';
}
fetch('/favorite/toggle', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
product_id: parseInt(productId)
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage(data.message);
updateFavoriteButton(data.is_favorited);
} else {
alert(data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('操作失败,请稍后再试');
})
.finally(() => {
// 恢复按钮状态
if (favoriteBtn) {
favoriteBtn.disabled = false;
}
});
}
// 显示成功消息
function showSuccessMessage(message) {
// 这里可以使用Toast或其他方式显示消息
if (typeof showToast === 'function') {
showToast(message, 'success');
} else {
// 简单的成功提示
alert(message);
}
}

646
app/static/js/review.js Normal file
View File

@ -0,0 +1,646 @@
// 评价功能 JavaScript
document.addEventListener('DOMContentLoaded', function() {
initializeReviewForm();
initializeImageUpload();
});
// 初始化评价表单
function initializeReviewForm() {
const starRating = document.getElementById('starRating');
const ratingInput = document.getElementById('rating');
const ratingText = document.getElementById('ratingText');
const reviewForm = document.getElementById('reviewForm');
if (starRating) {
const stars = starRating.querySelectorAll('.star');
const ratingTexts = {
1: '很差',
2: '较差',
3: '一般',
4: '满意',
5: '非常满意'
};
let currentRating = 0; // 当前选中的评分
// 初始化:设置所有星星为空心
stars.forEach(star => {
star.textContent = '☆'; // 空心星星
});
// 星级点击事件
stars.forEach((star, index) => {
star.addEventListener('click', function() {
const rating = index + 1;
setRating(rating);
});
// 鼠标悬停事件
star.addEventListener('mouseenter', function() {
const rating = index + 1;
showHoverStars(rating);
// 显示临时评分文字
const tempText = ratingTexts[rating] || '请选择评分';
ratingText.textContent = tempText;
ratingText.style.backgroundColor = '#ff6b35';
ratingText.style.color = 'white';
ratingText.style.borderColor = '#ff6b35';
});
});
// 鼠标离开星级评分区域
starRating.addEventListener('mouseleave', function() {
showSelectedStars(currentRating);
// 恢复原来的评分文字
if (currentRating > 0) {
ratingText.textContent = ratingTexts[currentRating];
ratingText.classList.add('selected');
ratingText.style.backgroundColor = '#ff6b35';
ratingText.style.color = 'white';
ratingText.style.borderColor = '#ff6b35';
} else {
ratingText.textContent = '请选择评分';
ratingText.classList.remove('selected');
ratingText.style.backgroundColor = '#f8f9fa';
ratingText.style.color = '#666';
ratingText.style.borderColor = '#e9ecef';
}
});
// 设置评分
function setRating(rating) {
currentRating = rating;
ratingInput.value = rating;
ratingText.textContent = ratingTexts[rating] || '请选择评分';
ratingText.classList.add('selected');
ratingText.style.backgroundColor = '#ff6b35';
ratingText.style.color = 'white';
ratingText.style.borderColor = '#ff6b35';
showSelectedStars(rating);
}
// 显示悬停状态的星星
function showHoverStars(rating) {
stars.forEach((star, index) => {
star.classList.remove('filled');
if (index < rating) {
star.textContent = '★'; // 实心星星
star.classList.add('filled');
} else {
star.textContent = '☆'; // 空心星星
}
});
}
// 显示选中状态的星星
function showSelectedStars(rating) {
stars.forEach((star, index) => {
star.classList.remove('filled');
if (index < rating) {
star.textContent = '★'; // 实心星星
star.classList.add('filled');
} else {
star.textContent = '☆'; // 空心星星
}
});
}
}
// 表单提交
if (reviewForm) {
reviewForm.addEventListener('submit', function(e) {
e.preventDefault();
submitReview();
});
}
}
// 初始化图片上传
function initializeImageUpload() {
const uploadArea = document.getElementById('uploadArea');
const imageInput = document.getElementById('imageInput');
const uploadedImages = document.getElementById('uploadedImages');
if (!uploadArea || !imageInput) return;
let uploadedImageUrls = [];
// 点击上传区域
uploadArea.addEventListener('click', function() {
imageInput.click();
});
// 拖拽上传
uploadArea.addEventListener('dragover', function(e) {
e.preventDefault();
this.style.borderColor = '#007bff';
});
uploadArea.addEventListener('dragleave', function(e) {
e.preventDefault();
this.style.borderColor = '#ddd';
});
uploadArea.addEventListener('drop', function(e) {
e.preventDefault();
this.style.borderColor = '#ddd';
const files = Array.from(e.dataTransfer.files);
handleFiles(files);
});
// 文件选择
imageInput.addEventListener('change', function() {
const files = Array.from(this.files);
handleFiles(files);
});
// 处理文件上传
function handleFiles(files) {
if (uploadedImageUrls.length + files.length > 5) {
showAlert('最多只能上传5张图片', 'warning');
return;
}
files.forEach(file => {
if (!file.type.startsWith('image/')) {
showAlert('只能上传图片文件', 'warning');
return;
}
if (file.size > 5 * 1024 * 1024) {
showAlert('图片大小不能超过5MB', 'warning');
return;
}
uploadImage(file);
});
}
// 上传图片到服务器
function uploadImage(file) {
const formData = new FormData();
formData.append('file', file);
// 显示上传进度
const previewElement = createImagePreview(URL.createObjectURL(file), true);
uploadedImages.appendChild(previewElement);
fetch('/review/upload_image', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
// 更新预览元素
const img = previewElement.querySelector('img');
img.src = data.url;
// 强制设置图片样式
forceImageStyles(img);
previewElement.classList.remove('uploading');
previewElement.dataset.url = data.url;
uploadedImageUrls.push(data.url);
} else {
showAlert(data.message || '图片上传失败', 'error');
previewElement.remove();
}
})
.catch(error => {
showAlert('图片上传失败', 'error');
previewElement.remove();
});
}
// 创建图片预览元素
function createImagePreview(src, isUploading = false) {
const div = document.createElement('div');
div.className = `image-preview ${isUploading ? 'uploading' : ''}`;
// 强制设置容器样式
div.style.cssText = `
position: relative !important;
width: 80px !important;
height: 80px !important;
max-width: 80px !important;
max-height: 80px !important;
min-width: 80px !important;
min-height: 80px !important;
border-radius: 8px !important;
overflow: hidden !important;
border: 2px solid #e9ecef !important;
flex-shrink: 0 !important;
flex-grow: 0 !important;
display: inline-block !important;
box-sizing: border-box !important;
margin: 0 !important;
padding: 0 !important;
vertical-align: top !important;
`;
const img = document.createElement('img');
img.src = src;
img.alt = '评价图片';
// 强制设置图片样式
forceImageStyles(img);
const removeBtn = document.createElement('button');
removeBtn.className = 'remove-btn';
removeBtn.innerHTML = '×';
removeBtn.type = 'button';
removeBtn.style.cssText = `
position: absolute !important;
top: 2px !important;
right: 2px !important;
background: rgba(255, 255, 255, 0.9) !important;
border: none !important;
border-radius: 50% !important;
width: 20px !important;
height: 20px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
cursor: pointer !important;
font-size: 12px !important;
color: #dc3545 !important;
box-shadow: 0 1px 3px rgba(0,0,0,0.2) !important;
z-index: 10 !important;
`;
removeBtn.onclick = function() {
const url = div.dataset.url;
if (url) {
uploadedImageUrls = uploadedImageUrls.filter(u => u !== url);
}
div.remove();
};
div.appendChild(img);
div.appendChild(removeBtn);
return div;
}
// 强制设置图片样式的函数
function forceImageStyles(img) {
img.style.cssText = `
display: block !important;
width: 80px !important;
height: 80px !important;
max-width: 80px !important;
max-height: 80px !important;
min-width: 80px !important;
min-height: 80px !important;
object-fit: cover !important;
border-radius: 6px !important;
box-sizing: border-box !important;
position: relative !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
outline: none !important;
background: none !important;
vertical-align: top !important;
flex-shrink: 0 !important;
flex-grow: 0 !important;
`;
// 图片加载完成后再次强制设置样式
img.onload = function() {
forceImageStyles(this);
};
}
// 获取上传的图片URL列表
window.getUploadedImages = function() {
return uploadedImageUrls;
};
}
// 提交评价
function submitReview() {
const submitBtn = document.getElementById('submitBtn');
const orderId = document.getElementById('orderId').value;
const productId = document.getElementById('productId').value;
const rating = document.getElementById('rating').value;
const content = document.getElementById('content').value;
const isAnonymous = document.getElementById('isAnonymous').checked;
// 验证
if (!rating) {
showAlert('请选择评分', 'warning');
return;
}
// 禁用提交按钮
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="bi bi-hourglass-split"></i> 提交中...';
const data = {
order_id: parseInt(orderId),
product_id: parseInt(productId),
rating: parseInt(rating),
content: content.trim(),
is_anonymous: isAnonymous,
images: window.getUploadedImages ? window.getUploadedImages() : []
};
fetch('/review/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert(data.message, 'success');
setTimeout(() => {
window.location.href = `/order/detail/${orderId}`;
}, 1500);
} else {
showAlert(data.message, 'error');
}
})
.catch(error => {
showAlert('提交失败,请重试', 'error');
})
.finally(() => {
// 恢复提交按钮
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="bi bi-check-circle"></i> 提交评价';
});
}
// 加载商品评价列表(用于商品详情页)
function loadProductReviews(productId, page = 1, rating = null) {
const reviewsContainer = document.getElementById('reviewsContainer');
if (!reviewsContainer) return;
const params = new URLSearchParams({
page: page
});
if (rating) {
params.append('rating', rating);
}
reviewsContainer.innerHTML = '<div class="text-center p-4"><i class="bi bi-hourglass-split"></i> 加载中...</div>';
fetch(`/review/product/${productId}?${params}`)
.then(response => response.json())
.then(data => {
if (data.success) {
renderReviews(data);
} else {
reviewsContainer.innerHTML = '<div class="text-center p-4 text-muted">加载失败</div>';
}
})
.catch(error => {
reviewsContainer.innerHTML = '<div class="text-center p-4 text-muted">加载失败</div>';
});
}
// 渲染评价列表
function renderReviews(data) {
const reviewsContainer = document.getElementById('reviewsContainer');
if (!reviewsContainer) return;
let html = '';
// 评价统计
if (data.stats) {
html += renderReviewStats(data.stats);
}
// 评价筛选
html += renderReviewFilter();
// 评价列表
if (data.reviews && data.reviews.length > 0) {
data.reviews.forEach(review => {
html += renderReviewItem(review);
});
// 分页
if (data.pagination && data.pagination.pages > 1) {
html += renderPagination(data.pagination);
}
} else {
html += '<div class="text-center p-4 text-muted">暂无评价</div>';
}
reviewsContainer.innerHTML = html;
}
// 渲染评价统计
function renderReviewStats(stats) {
const goodRate = stats.good_rate || 0;
const totalReviews = stats.total_reviews || 0;
let html = `
<div class="reviews-stats">
<div class="rating-summary">
<div class="overall-rating">
<div class="score">${goodRate}%</div>
<div class="total">好评率 (${totalReviews}条评价)</div>
</div>
<div class="rating-breakdown">
`;
for (let i = 5; i >= 1; i--) {
const count = stats.rating_stats[i] || 0;
const percentage = totalReviews > 0 ? (count / totalReviews * 100) : 0;
html += `
<div class="rating-bar">
<span class="label">${i}</span>
<div class="progress">
<div class="progress-bar" style="width: ${percentage}%"></div>
</div>
<span class="count">${count}</span>
</div>
`;
}
html += `
</div>
</div>
</div>
`;
return html;
}
// 渲染评价筛选
function renderReviewFilter() {
return `
<div class="reviews-filter">
<button class="btn btn-outline-primary btn-sm active" onclick="filterReviews(null)">
全部评价
</button>
<button class="btn btn-outline-primary btn-sm" onclick="filterReviews(5)">
好评 (5)
</button>
<button class="btn btn-outline-primary btn-sm" onclick="filterReviews(3)">
中评 (3)
</button>
<button class="btn btn-outline-primary btn-sm" onclick="filterReviews(1)">
差评 (1)
</button>
</div>
`;
}
// 渲染单个评价 - 修复图片和头像问题
function renderReviewItem(review) {
let html = `
<div class="review-list-item">
<div class="reviewer-info">
`;
if (review.user_avatar) {
// 用户头像 - 添加内联样式强制约束尺寸
html += `<img src="${review.user_avatar}"
class="reviewer-avatar"
alt="用户头像"
style="width: 40px !important; height: 40px !important; max-width: 40px !important; max-height: 40px !important; min-width: 40px !important; min-height: 40px !important; border-radius: 50% !important; object-fit: cover !important; border: 2px solid #e9ecef !important; display: block !important; flex-shrink: 0 !important;">`;
} else {
html += `<div class="reviewer-avatar bg-secondary d-flex align-items-center justify-content-center text-white"
style="width: 40px !important; height: 40px !important; max-width: 40px !important; max-height: 40px !important; min-width: 40px !important; min-height: 40px !important; border-radius: 50% !important; flex-shrink: 0 !important;">
<i class="bi bi-person"></i>
</div>`;
}
html += `
<div>
<div class="reviewer-name">${review.username}</div>
<div class="review-time">${new Date(review.created_at).toLocaleDateString()}</div>
</div>
</div>
<div class="rating-display mb-2">
<span class="stars">${review.rating_stars}</span>
<span class="text-muted">${review.rating}</span>
</div>
`;
if (review.content) {
html += `<p class="review-content">${review.content}</p>`;
}
if (review.images && review.images.length > 0) {
html += '<div class="product-review-images mb-2">';
review.images.forEach(imageUrl => {
// 评价图片 - 使用特殊的类名和内联样式确保图片尺寸正确
html += `<img src="${imageUrl}"
class="product-review-image"
alt="评价图片"
style="width: 80px !important; height: 80px !important; max-width: 80px !important; max-height: 80px !important; min-width: 80px !important; min-height: 80px !important; object-fit: cover !important; border-radius: 6px !important; border: 1px solid #dee2e6 !important; cursor: pointer !important; margin-right: 8px !important; margin-bottom: 8px !important; display: inline-block !important; vertical-align: top !important;"
onclick="showImageModal('${imageUrl}')">`;
});
html += '</div>';
}
html += '</div>';
return html;
}
// 渲染分页
function renderPagination(pagination) {
if (pagination.pages <= 1) return '';
let html = '<nav aria-label="评价分页"><ul class="pagination justify-content-center">';
// 上一页
if (pagination.has_prev) {
html += `<li class="page-item">
<a class="page-link" href="#" onclick="loadProductReviews(window.currentProductId, ${pagination.page - 1}); return false;">
上一页
</a>
</li>`;
}
// 页码
const startPage = Math.max(1, pagination.page - 2);
const endPage = Math.min(pagination.pages, pagination.page + 2);
for (let i = startPage; i <= endPage; i++) {
const activeClass = i === pagination.page ? 'active' : '';
html += `<li class="page-item ${activeClass}">
<a class="page-link" href="#" onclick="loadProductReviews(window.currentProductId, ${i}); return false;">
${i}
</a>
</li>`;
}
// 下一页
if (pagination.has_next) {
html += `<li class="page-item">
<a class="page-link" href="#" onclick="loadProductReviews(window.currentProductId, ${pagination.page + 1}); return false;">
下一页
</a>
</li>`;
}
html += '</ul></nav>';
return html;
}
// 筛选评价
function filterReviews(rating) {
// 更新筛选按钮状态
const filterButtons = document.querySelectorAll('.reviews-filter .btn');
filterButtons.forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
// 重新加载评价
loadProductReviews(window.currentProductId, 1, rating);
}
// 显示图片模态框
function showImageModal(imageUrl) {
const modal = document.getElementById('imageModal');
const modalImage = document.getElementById('modalImage');
if (modal && modalImage) {
modalImage.src = imageUrl;
new bootstrap.Modal(modal).show();
}
}
// 显示提示信息
function showAlert(message, type = 'info') {
// 创建警告框
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type === 'error' ? 'danger' : type} alert-dismissible fade show position-fixed`;
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
// 添加到页面
document.body.appendChild(alertDiv);
// 自动消失
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
}
// 全局变量用于存储当前商品ID
window.currentProductId = null;

View File

@ -45,8 +45,8 @@
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.orders' %}active{% endif %}"
href="#">
<a class="nav-link {% if request.endpoint.startswith('admin.order') %}active{% endif %}"
href="{{ url_for('admin.orders') }}">
<i class="bi bi-receipt"></i>
订单管理
</a>

View File

@ -0,0 +1,245 @@
{% extends "admin/base.html" %}
{% block title %}操作日志 - 太白购物商城管理后台{% endblock %}
{% block page_title %}操作日志{% endblock %}
{% block page_description %}查看系统操作日志,监控用户和管理员行为{% endblock %}
{% block extra_css %}
<link href="{{ url_for('static', filename='css/admin_logs.css') }}" rel="stylesheet">
{% endblock %}
{% block content %}
<div class="admin-logs">
<!-- 筛选条件 -->
<div class="card mb-4">
<div class="card-body">
<form method="GET" class="row g-3">
<div class="col-md-3">
<label for="user_type" class="form-label">用户类型</label>
<select class="form-select" id="user_type" name="user_type">
<option value="">全部类型</option>
<option value="1" {% if user_type == '1' %}selected{% endif %}>普通用户</option>
<option value="2" {% if user_type == '2' %}selected{% endif %}>管理员</option>
</select>
</div>
<div class="col-md-4">
<label for="action" class="form-label">操作类型</label>
<input type="text" class="form-control" id="action" name="action"
value="{{ action }}" placeholder="搜索操作类型">
</div>
<div class="col-md-2">
<label class="form-label">&nbsp;</label>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-search"></i> 搜索
</button>
<a href="{{ url_for('admin.logs') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-clockwise"></i> 重置
</a>
</div>
</div>
</form>
</div>
</div>
<!-- 日志统计 -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<h5 class="card-title">{{ logs.total }}</h5>
<p class="card-text">总日志数</p>
</div>
<div class="icon-wrapper primary">
<i class="bi bi-journal-text"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<h5 class="card-title">{{ logs.items | selectattr('user_type', 'equalto', 1) | list | length }}</h5>
<p class="card-text">用户操作</p>
</div>
<div class="icon-wrapper info">
<i class="bi bi-person"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<h5 class="card-title">{{ logs.items | selectattr('user_type', 'equalto', 2) | list | length }}</h5>
<p class="card-text">管理员操作</p>
</div>
<div class="icon-wrapper warning">
<i class="bi bi-shield-check"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<h5 class="card-title">{{ today_logs_count }}</h5>
<p class="card-text">今日操作</p>
</div>
<div class="icon-wrapper success">
<i class="bi bi-clock"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 日志列表 -->
<div class="card">
<div class="card-header bg-white">
<h5 class="mb-0">
<i class="bi bi-journal-text"></i>
操作日志
<small class="text-muted ms-2">共 {{ logs.total }} 条记录</small>
</h5>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th width="15%">时间</th>
<th width="10%">操作者</th>
<th width="15%">操作类型</th>
<th width="20%">操作内容</th>
<th width="15%">IP地址</th>
<th width="25%">用户代理</th>
</tr>
</thead>
<tbody>
{% if logs.items %}
{% for log in logs.items %}
<tr>
<td>
<div>{{ log.created_at.strftime('%Y-%m-%d') if log.created_at else '-' }}</div>
<small class="text-muted">{{ log.created_at.strftime('%H:%M:%S') if log.created_at else '' }}</small>
</td>
<td>
<div class="d-flex align-items-center">
<span class="badge bg-{{ 'warning' if log.user_type == 2 else 'info' }} me-2">
{{ '管理员' if log.user_type == 2 else '用户' }}
</span>
<span class="fw-bold">#{{ log.user_id or '-' }}</span>
</div>
</td>
<td>
<span class="operation-action">{{ log.action }}</span>
</td>
<td>
<div>
{% if log.resource_type %}
<span class="resource-type">{{ log.resource_type }}</span>
{% if log.resource_id %}
<span class="resource-id">#{{ log.resource_id }}</span>
{% endif %}
{% else %}
<span class="text-muted">-</span>
{% endif %}
</div>
</td>
<td>
<code>{{ log.ip_address or '-' }}</code>
</td>
<td>
<div class="user-agent-wrapper">
{% if log.user_agent %}
<span class="user-agent" title="{{ log.user_agent }}">
{{ log.user_agent[:50] }}{% if log.user_agent|length > 50 %}...{% endif %}
</span>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="6" class="text-center py-4">
<div class="empty-state">
<i class="bi bi-journal-x"></i>
<div>暂无操作日志</div>
{% if user_type or action %}
<small class="text-muted">尝试调整筛选条件</small>
{% endif %}
</div>
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<!-- 分页 -->
{% if logs.pages > 1 %}
<div class="card-footer bg-white">
<nav aria-label="操作日志分页">
<ul class="pagination justify-content-center mb-0">
{% if logs.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.logs', page=logs.prev_num, user_type=user_type, action=action) }}">
<i class="bi bi-chevron-left"></i>
</a>
</li>
{% endif %}
{% for page_num in logs.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %}
{% if page_num %}
{% if page_num != logs.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.logs', page=page_num, user_type=user_type, action=action) }}">
{{ page_num }}
</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link"></span>
</li>
{% endif %}
{% endfor %}
{% if logs.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.logs', page=logs.next_num, user_type=user_type, action=action) }}">
<i class="bi bi-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/admin_logs.js') }}"></script>
{% endblock %}

View File

@ -0,0 +1,426 @@
{% extends "admin/base.html" %}
{% block title %}订单详情 - 太白购物商城管理后台{% endblock %}
{% block page_title %}订单详情{% endblock %}
{% block page_description %}订单号:{{ order.order_sn }}{% endblock %}
{% block extra_css %}
<link href="{{ url_for('static', filename='css/admin_orders.css') }}" rel="stylesheet">
{% endblock %}
{% block content %}
<div class="admin-order-detail">
<div class="row">
<!-- 左侧:订单信息 -->
<div class="col-md-8">
<!-- 订单基本信息 -->
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">订单基本信息</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<table class="table table-sm">
<tr>
<th width="120">订单号:</th>
<td>{{ order.order_sn }}</td>
</tr>
<tr>
<th>用户信息:</th>
<td>
<div>{{ order.user.username }}</div>
{% if order.user.phone %}
<small class="text-muted">{{ order.user.phone }}</small>
{% endif %}
</td>
</tr>
<tr>
<th>订单状态:</th>
<td>
<span class="badge order-status-{{ order.status }}">{{ order.get_status_text() }}</span>
</td>
</tr>
<tr>
<th>支付方式:</th>
<td>
{% if order.payment_method %}
<span class="badge bg-info">{{ order.payment_method }}</span>
{% else %}
<span class="text-muted">未设置</span>
{% endif %}
</td>
</tr>
</table>
</div>
<div class="col-md-6">
<table class="table table-sm">
<tr>
<th width="120">创建时间:</th>
<td>{{ order.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
</tr>
{% if order.shipped_at %}
<tr>
<th>发货时间:</th>
<td>{{ order.shipped_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
</tr>
{% endif %}
{% if order.received_at %}
<tr>
<th>收货时间:</th>
<td>{{ order.received_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
</tr>
{% endif %}
<tr>
<th>配送方式:</th>
<td>{{ order.shipping_method or '标准配送' }}</td>
</tr>
</table>
</div>
</div>
{% if order.remark %}
<div class="mt-3">
<strong>订单备注:</strong>
<p class="text-muted mb-0">{{ order.remark }}</p>
</div>
{% endif %}
</div>
</div>
<!-- 收货信息 -->
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">收货信息</h5>
</div>
<div class="card-body">
{% set receiver = order.get_receiver_info() %}
{% if receiver %}
<div class="row">
<div class="col-md-6">
<table class="table table-sm">
<tr>
<th width="120">收货人:</th>
<td>{{ receiver.receiver_name }}</td>
</tr>
<tr>
<th>联系电话:</th>
<td>{{ receiver.receiver_phone }}</td>
</tr>
</table>
</div>
<div class="col-md-6">
<table class="table table-sm">
<tr>
<th width="120">收货地址:</th>
<td>{{ receiver.full_address }}</td>
</tr>
{% if receiver.postal_code %}
<tr>
<th>邮政编码:</th>
<td>{{ receiver.postal_code }}</td>
</tr>
{% endif %}
</table>
</div>
</div>
{% else %}
<p class="text-muted">暂无收货信息</p>
{% endif %}
</div>
</div>
<!-- 订单商品 -->
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">订单商品</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>商品信息</th>
<th>单价</th>
<th>数量</th>
<th>小计</th>
</tr>
</thead>
<tbody>
{% for item in order.order_items %}
<tr>
<td>
<div class="d-flex align-items-center">
{% if item.product_image %}
<img src="{{ item.product_image }}" alt="{{ item.product_name }}"
class="product-thumb me-3" style="width: 60px; height: 60px; object-fit: cover;">
{% endif %}
<div>
<div class="fw-bold">{{ item.product_name }}</div>
{% if item.spec_combination %}
<small class="text-muted">{{ item.spec_combination }}</small>
{% endif %}
{% if item.sku_code %}
<small class="text-muted d-block">SKU: {{ item.sku_code }}</small>
{% endif %}
</div>
</div>
</td>
<td>¥{{ "%.2f"|format(item.price) }}</td>
<td>{{ item.quantity }}</td>
<td>¥{{ "%.2f"|format(item.total_price) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 右侧:金额信息和操作 -->
<div class="col-md-4">
<!-- 金额信息 -->
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">金额信息</h5>
</div>
<div class="card-body">
<table class="table table-sm">
<tr>
<th>商品总额:</th>
<td class="text-end">¥{{ "%.2f"|format(order.total_amount) }}</td>
</tr>
<tr>
<th>运费:</th>
<td class="text-end">¥{{ "%.2f"|format(order.shipping_fee) }}</td>
</tr>
<tr class="table-active">
<th>实付金额:</th>
<td class="text-end fw-bold text-primary">¥{{ "%.2f"|format(order.actual_amount) }}</td>
</tr>
</table>
</div>
</div>
<!-- 支付信息 -->
{% if payment %}
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">支付信息</h5>
</div>
<div class="card-body">
<table class="table table-sm">
<tr>
<th>支付流水号:</th>
<td>{{ payment.payment_sn }}</td>
</tr>
<tr>
<th>支付状态:</th>
<td>
<span class="badge {% if payment.status == 2 %}bg-success{% elif payment.status == 1 %}bg-warning{% else %}bg-danger{% endif %}">
{{ payment.get_status_text() }}
</span>
</td>
</tr>
{% if payment.paid_at %}
<tr>
<th>支付时间:</th>
<td>{{ payment.paid_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
</tr>
{% endif %}
{% if payment.third_party_sn %}
<tr>
<th>第三方流水号:</th>
<td>{{ payment.third_party_sn }}</td>
</tr>
{% endif %}
</table>
</div>
</div>
{% endif %}
<!-- 物流信息 -->
{% if shipping_info %}
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">物流信息</h5>
</div>
<div class="card-body">
<table class="table table-sm">
<tr>
<th>物流公司:</th>
<td>{{ shipping_info.shipping_company }}</td>
</tr>
<tr>
<th>快递单号:</th>
<td>{{ shipping_info.tracking_number }}</td>
</tr>
<tr>
<th>物流状态:</th>
<td>
<span class="badge {% if shipping_info.shipping_status == 3 %}bg-success{% elif shipping_info.shipping_status == 2 %}bg-warning{% else %}bg-info{% endif %}">
{% if shipping_info.shipping_status == 1 %}已发货
{% elif shipping_info.shipping_status == 2 %}运输中
{% elif shipping_info.shipping_status == 3 %}已送达
{% endif %}
</span>
</td>
</tr>
</table>
</div>
</div>
{% endif %}
<!-- 操作按钮 -->
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">订单操作</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
{% if order.status == 2 %}
<button class="btn btn-success" onclick="showShipModal({{ order.id }}, '{{ order.order_sn }}')">
<i class="bi bi-truck"></i> 发货
</button>
{% endif %}
{% if order.status in [2, 3] %}
<button class="btn btn-warning" onclick="showRefundModal({{ order.id }}, '{{ order.order_sn }}')">
<i class="bi bi-arrow-return-left"></i> 退款
</button>
{% endif %}
{% if order.can_cancel() %}
<button class="btn btn-danger" onclick="showCancelModal({{ order.id }}, '{{ order.order_sn }}')">
<i class="bi bi-x-circle"></i> 取消订单
</button>
{% endif %}
<a href="{{ url_for('admin.orders') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> 返回列表
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 发货模态框 -->
<div class="modal fade" id="shipModal" 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>
<form id="shipForm">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">订单号</label>
<input type="text" class="form-control" id="shipOrderSn" readonly>
</div>
<div class="mb-3">
<label class="form-label">物流公司 <span class="text-danger">*</span></label>
<select class="form-select" name="shipping_company" required>
<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 class="mb-3">
<label class="form-label">快递单号 <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="tracking_number" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-success">
<i class="bi bi-truck"></i> 确认发货
</button>
</div>
</form>
</div>
</div>
</div>
<!-- 退款模态框 -->
<div class="modal fade" id="refundModal" 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>
<form id="refundForm">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">订单号</label>
<input type="text" class="form-control" id="refundOrderSn" readonly>
</div>
<div class="mb-3">
<label class="form-label">退款原因 <span class="text-danger">*</span></label>
<textarea class="form-control" name="refund_reason" rows="3" required placeholder="请输入退款原因"></textarea>
</div>
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle"></i>
退款后将自动恢复库存,减少销量统计
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-warning">
<i class="bi bi-arrow-return-left"></i> 确认退款
</button>
</div>
</form>
</div>
</div>
</div>
<!-- 取消模态框 -->
<div class="modal fade" id="cancelModal" 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>
<form id="cancelForm">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">订单号</label>
<input type="text" class="form-control" id="cancelOrderSn" readonly>
</div>
<div class="mb-3">
<label class="form-label">取消原因</label>
<textarea class="form-control" name="cancel_reason" rows="3" placeholder="请输入取消原因(可选)"></textarea>
</div>
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle"></i>
取消订单后将自动恢复库存,减少销量统计
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-danger">
<i class="bi bi-x-circle"></i> 确认取消
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/admin_orders.js') }}"></script>
{% endblock %}

View File

@ -0,0 +1,365 @@
{% extends "admin/base.html" %}
{% block title %}订单管理 - 太白购物商城管理后台{% endblock %}
{% block page_title %}订单管理{% endblock %}
{% block page_description %}管理系统中的所有订单{% endblock %}
{% block extra_css %}
<link href="{{ url_for('static', filename='css/admin_orders.css') }}" rel="stylesheet">
{% endblock %}
{% block content %}
<div class="admin-orders">
<!-- 统计卡片 -->
<div class="row mb-4">
<div class="col-md-2">
<div class="card stats-card">
<div class="card-body text-center">
<div class="stats-number">{{ order_stats.get(0, {}).get('count', 0) + order_stats.get(1, {}).get('count', 0) + order_stats.get(2, {}).get('count', 0) + order_stats.get(3, {}).get('count', 0) + order_stats.get(4, {}).get('count', 0) + order_stats.get(5, {}).get('count', 0) + order_stats.get(6, {}).get('count', 0) + order_stats.get(7, {}).get('count', 0) }}</div>
<div class="stats-label">总订单</div>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card stats-card pending-payment">
<div class="card-body text-center">
<div class="stats-number">{{ order_stats.get(1, {}).get('count', 0) }}</div>
<div class="stats-label">待支付</div>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card stats-card pending-shipment">
<div class="card-body text-center">
<div class="stats-number">{{ order_stats.get(2, {}).get('count', 0) }}</div>
<div class="stats-label">待发货</div>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card stats-card shipped">
<div class="card-body text-center">
<div class="stats-number">{{ order_stats.get(3, {}).get('count', 0) }}</div>
<div class="stats-label">待收货</div>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card stats-card completed">
<div class="card-body text-center">
<div class="stats-number">{{ order_stats.get(5, {}).get('count', 0) }}</div>
<div class="stats-label">已完成</div>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card stats-card cancelled">
<div class="card-body text-center">
<div class="stats-number">{{ order_stats.get(6, {}).get('count', 0) }}</div>
<div class="stats-label">已取消</div>
</div>
</div>
</div>
</div>
<!-- 搜索和筛选 -->
<div class="card mb-4">
<div class="card-body">
<form method="GET" class="row g-3">
<div class="col-md-3">
<label for="search" class="form-label">搜索</label>
<input type="text" class="form-control" id="search" name="search"
value="{{ search }}" placeholder="订单号/用户名/手机号">
</div>
<div class="col-md-2">
<label for="status" class="form-label">订单状态</label>
<select class="form-select" id="status" name="status">
<option value="">全部状态</option>
{% for status_code, status_name in ORDER_STATUS.items() %}
<option value="{{ status_code }}" {% if status == status_code|string %}selected{% endif %}>
{{ status_name }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<label for="start_date" class="form-label">开始日期</label>
<input type="date" class="form-control" id="start_date" name="start_date" value="{{ start_date }}">
</div>
<div class="col-md-2">
<label for="end_date" class="form-label">结束日期</label>
<input type="date" class="form-control" id="end_date" name="end_date" value="{{ end_date }}">
</div>
<div class="col-md-3">
<label class="form-label">&nbsp;</label>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-search"></i> 搜索
</button>
<a href="{{ url_for('admin.orders') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-clockwise"></i> 重置
</a>
</div>
</div>
</form>
</div>
</div>
<!-- 订单列表 -->
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">订单列表</h5>
</div>
<div class="card-body">
{% if orders.items %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>订单号</th>
<th>用户信息</th>
<th>订单金额</th>
<th>订单状态</th>
<th>支付方式</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for order in orders.items %}
<tr>
<td>
<div class="fw-bold">{{ order.order_sn }}</div>
<small class="text-muted">ID: {{ order.id }}</small>
</td>
<td>
<div class="fw-bold">{{ order.user.username }}</div>
{% if order.user.phone %}
<small class="text-muted">{{ order.user.phone }}</small>
{% endif %}
</td>
<td>
<div class="fw-bold text-primary">¥{{ "%.2f"|format(order.actual_amount) }}</div>
{% if order.shipping_fee > 0 %}
<small class="text-muted">含运费: ¥{{ "%.2f"|format(order.shipping_fee) }}</small>
{% endif %}
</td>
<td>
<span class="badge order-status-{{ order.status }}">{{ order.get_status_text() }}</span>
</td>
<td>
{% if order.payment_method %}
<span class="badge bg-info">{{ order.payment_method }}</span>
{% else %}
<span class="text-muted">未设置</span>
{% endif %}
</td>
<td>
<div>{{ order.created_at.strftime('%Y-%m-%d') }}</div>
<small class="text-muted">{{ order.created_at.strftime('%H:%M:%S') }}</small>
</td>
<td>
<div class="btn-group" role="group">
<a href="{{ url_for('admin.order_detail', order_id=order.id) }}"
class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i> 详情
</a>
{% if order.status == 2 %}
<button class="btn btn-sm btn-outline-success"
onclick="showShipModal({{ order.id }}, '{{ order.order_sn }}')">
<i class="bi bi-truck"></i> 发货
</button>
{% endif %}
{% if order.status in [2, 3] %}
<button class="btn btn-sm btn-outline-warning"
onclick="showRefundModal({{ order.id }}, '{{ order.order_sn }}')">
<i class="bi bi-arrow-return-left"></i> 退款
</button>
{% endif %}
{% if order.can_cancel() %}
<button class="btn btn-sm btn-outline-danger"
onclick="showCancelModal({{ order.id }}, '{{ order.order_sn }}')">
<i class="bi bi-x-circle"></i> 取消
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- 分页 -->
{% if orders.pages > 1 %}
<nav aria-label="订单分页">
<ul class="pagination justify-content-center">
{% if orders.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.orders', page=orders.prev_num, search=search, status=status, start_date=start_date, end_date=end_date) }}">
<i class="bi bi-chevron-left"></i>
</a>
</li>
{% endif %}
{% for page_num in orders.iter_pages() %}
{% if page_num %}
{% if page_num != orders.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.orders', page=page_num, search=search, status=status, start_date=start_date, end_date=end_date) }}">
{{ page_num }}
</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link"></span>
</li>
{% endif %}
{% endfor %}
{% if orders.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.orders', page=orders.next_num, search=search, status=status, start_date=start_date, end_date=end_date) }}">
<i class="bi bi-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="text-center py-4">
<i class="bi bi-inbox display-1 text-muted"></i>
<p class="text-muted mt-2">暂无订单数据</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- 发货模态框 -->
<div class="modal fade" id="shipModal" 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>
<form id="shipForm">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">订单号</label>
<input type="text" class="form-control" id="shipOrderSn" readonly>
</div>
<div class="mb-3">
<label class="form-label">物流公司 <span class="text-danger">*</span></label>
<select class="form-select" name="shipping_company" required>
<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 class="mb-3">
<label class="form-label">快递单号 <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="tracking_number" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-success">
<i class="bi bi-truck"></i> 确认发货
</button>
</div>
</form>
</div>
</div>
</div>
<!-- 退款模态框 -->
<div class="modal fade" id="refundModal" 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>
<form id="refundForm">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">订单号</label>
<input type="text" class="form-control" id="refundOrderSn" readonly>
</div>
<div class="mb-3">
<label class="form-label">退款原因 <span class="text-danger">*</span></label>
<textarea class="form-control" name="refund_reason" rows="3" required placeholder="请输入退款原因"></textarea>
</div>
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle"></i>
退款后将自动恢复库存,减少销量统计
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-warning">
<i class="bi bi-arrow-return-left"></i> 确认退款
</button>
</div>
</form>
</div>
</div>
</div>
<!-- 取消模态框 -->
<div class="modal fade" id="cancelModal" 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>
<form id="cancelForm">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">订单号</label>
<input type="text" class="form-control" id="cancelOrderSn" readonly>
</div>
<div class="mb-3">
<label class="form-label">取消原因</label>
<textarea class="form-control" name="cancel_reason" rows="3" placeholder="请输入取消原因(可选)"></textarea>
</div>
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle"></i>
取消订单后将自动恢复库存,减少销量统计
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-danger">
<i class="bi bi-x-circle"></i> 确认取消
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/admin_orders.js') }}"></script>
{% endblock %}

View File

@ -0,0 +1,279 @@
{% extends "admin/base.html" %}
{% block title %}用户管理 - 太白购物商城管理后台{% endblock %}
{% block page_title %}用户管理{% endblock %}
{% block page_description %}管理系统用户,查看用户信息和状态{% endblock %}
{% block extra_css %}
<link href="{{ url_for('static', filename='css/admin_users.css') }}" rel="stylesheet">
{% endblock %}
{% block content %}
<div class="admin-users">
<!-- 搜索和筛选 -->
<div class="card mb-4">
<div class="card-body">
<form method="GET" class="row g-3">
<div class="col-md-4">
<label for="search" class="form-label">搜索用户</label>
<input type="text" class="form-control" id="search" name="search"
value="{{ search }}" placeholder="用户名、邮箱、手机号、昵称">
</div>
<div class="col-md-3">
<label for="status" class="form-label">状态筛选</label>
<select class="form-select" id="status" name="status">
<option value="">全部状态</option>
<option value="1" {% if status == '1' %}selected{% endif %}>正常</option>
<option value="0" {% if status == '0' %}selected{% endif %}>禁用</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">&nbsp;</label>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-search"></i> 搜索
</button>
<a href="{{ url_for('admin.users') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-clockwise"></i> 重置
</a>
</div>
</div>
</form>
</div>
</div>
<!-- 用户统计 -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<h5 class="card-title">{{ users.total }}</h5>
<p class="card-text">总用户数</p>
</div>
<div class="icon-wrapper primary">
<i class="bi bi-people"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<h5 class="card-title">{{ users.items | selectattr('status', 'equalto', 1) | list | length }}</h5>
<p class="card-text">正常用户</p>
</div>
<div class="icon-wrapper success">
<i class="bi bi-person-check"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<h5 class="card-title">{{ users.items | selectattr('status', 'equalto', 0) | list | length }}</h5>
<p class="card-text">禁用用户</p>
</div>
<div class="icon-wrapper danger">
<i class="bi bi-person-x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<h5 class="card-title">{{ week_new_users }}</h5>
<p class="card-text">本周新增</p>
</div>
<div class="icon-wrapper info">
<i class="bi bi-person-plus"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 用户列表 -->
<div class="card">
<div class="card-header bg-white">
<h5 class="mb-0">
<i class="bi bi-people"></i>
用户列表
<small class="text-muted ms-2">共 {{ users.total }} 条记录</small>
</h5>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>用户ID</th>
<th>用户信息</th>
<th>联系方式</th>
<th>注册时间</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% if users.items %}
{% for user in users.items %}
<tr>
<td>
<strong>#{{ user.id }}</strong>
</td>
<td>
<div class="d-flex align-items-center">
<div class="avatar-wrapper me-3">
{% if user.avatar_url %}
<img src="{{ user.avatar_url }}" alt="头像" class="user-avatar">
{% else %}
<div class="user-avatar-placeholder">
<i class="bi bi-person"></i>
</div>
{% endif %}
</div>
<div>
<div class="fw-bold">{{ user.username }}</div>
{% if user.nickname %}
<small class="text-muted">{{ user.nickname }}</small>
{% endif %}
</div>
</div>
</td>
<td>
<div>
{% if user.email %}
<div><i class="bi bi-envelope"></i> {{ user.email }}</div>
{% endif %}
{% if user.phone %}
<div><i class="bi bi-phone"></i> {{ user.phone }}</div>
{% endif %}
{% if not user.email and not user.phone %}
<span class="text-muted">未设置</span>
{% endif %}
</div>
</td>
<td>
<div>{{ user.created_at.strftime('%Y-%m-%d') if user.created_at else '-' }}</div>
<small class="text-muted">{{ user.created_at.strftime('%H:%M:%S') if user.created_at else '' }}</small>
</td>
<td>
<span class="badge bg-{{ 'success' if user.status == 1 else 'danger' }}">
{{ '正常' if user.status == 1 else '禁用' }}
</span>
</td>
<td>
<div class="btn-group" role="group">
<button type="button" class="btn btn-sm btn-outline-primary"
onclick="viewUser({{ user.id }})">
<i class="bi bi-eye"></i> 查看
</button>
<button type="button" class="btn btn-sm btn-outline-{{ 'warning' if user.status == 1 else 'success' }}"
onclick="toggleUserStatus({{ user.id }}, {{ user.status }})">
<i class="bi bi-{{ 'person-x' if user.status == 1 else 'person-check' }}"></i>
{{ '禁用' if user.status == 1 else '启用' }}
</button>
</div>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="6" class="text-center py-4">
<div class="empty-state">
<i class="bi bi-person-slash"></i>
<div>暂无用户数据</div>
{% if search or status %}
<small class="text-muted">尝试调整搜索条件</small>
{% endif %}
</div>
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<!-- 分页 -->
{% if users.pages > 1 %}
<div class="card-footer bg-white">
<nav aria-label="用户列表分页">
<ul class="pagination justify-content-center mb-0">
{% if users.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.users', page=users.prev_num, search=search, status=status) }}">
<i class="bi bi-chevron-left"></i>
</a>
</li>
{% endif %}
{% for page_num in users.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %}
{% if page_num %}
{% if page_num != users.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.users', page=page_num, search=search, status=status) }}">
{{ page_num }}
</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link"></span>
</li>
{% endif %}
{% endfor %}
{% if users.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.users', page=users.next_num, search=search, status=status) }}">
<i class="bi bi-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
</div>
{% endif %}
</div>
</div>
<!-- 用户详情模态框 -->
<div class="modal fade" id="userDetailModal" 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="userDetailContent">
<!-- 用户详情内容将通过AJAX加载 -->
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/admin_users.js') }}"></script>
{% endblock %}

View File

@ -4,10 +4,14 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}太白购物商城{% endblock %}</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css" rel="stylesheet">
<link href="{{ url_for('static', filename='css/base.css') }}" rel="stylesheet">
{% block styles %}{% endblock %}
<!-- 自定义CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/base.css') }}">
{% block head %}{% endblock %}
</head>
<body>
<!-- 导航栏 -->
@ -29,23 +33,12 @@
<li class="nav-item">
<a class="nav-link" href="{{ url_for('main.product_list') }}">全部商品</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownCategory" role="button" data-bs-toggle="dropdown">
商品分类
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url_for('main.product_list') }}">全部分类</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('main.product_list', category_id=1) }}">手机数码</a></li>
<li><a class="dropdown-item" href="{{ url_for('main.product_list', category_id=2) }}">电脑办公</a></li>
<li><a class="dropdown-item" href="{{ url_for('main.product_list', category_id=3) }}">家居家装</a></li>
</ul>
</li>
</ul>
<!-- 搜索框 -->
<form class="d-flex me-3 search-form" method="GET" action="{{ url_for('main.product_list') }}">
<input class="form-control me-2" type="search" name="search" placeholder="搜索商品..." style="min-width: 200px;">
<form class="d-flex me-3" method="GET" action="{{ url_for('main.product_list') }}">
<input class="form-control me-2" type="search" name="search" placeholder="搜索商品..."
value="{{ request.args.get('search', '') }}">
<button class="btn btn-outline-primary" type="submit">
<i class="bi bi-search"></i>
</button>
@ -54,7 +47,7 @@
<ul class="navbar-nav">
{% if session.user_id %}
<li class="nav-item">
<a class="nav-link position-relative" href="{{ url_for('cart.index') }}" title="购物车">
<a class="nav-link position-relative" href="{{ url_for('cart.index') }}">
<i class="bi bi-cart"></i> 购物车
<span class="badge bg-danger cart-badge" id="cartBadge" style="display: none;">0</span>
</a>
@ -66,7 +59,8 @@
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url_for('user.profile') }}">个人中心</a></li>
<li><a class="dropdown-item" href="{{ url_for('user.orders') }}">我的订单</a></li>
<li><a class="dropdown-item" href="#">我的收藏</a></li>
<li><a class="dropdown-item" href="{{ url_for('favorite.index') }}">我的收藏</a></li>
<li><a class="dropdown-item" href="{{ url_for('history.index') }}">浏览历史</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">退出登录</a></li>
</ul>
@ -84,77 +78,50 @@
</div>
</nav>
<!-- 消息提示 -->
<!-- 主要内容 -->
<main class="container mt-4">
<!-- Flash消息提示 -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="container mt-3">
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<!-- 主要内容 -->
<main class="container mt-4">
{% block content %}{% endblock %}
</main>
<!-- 页脚 -->
<footer class="footer mt-auto">
<footer class="bg-light py-4 mt-5">
<div class="container">
<div class="row">
<div class="col-md-6">
<h5>太白购物商城</h5>
<p class="text-muted">您的购物首选平台</p>
<div class="mb-3">
<a href="#" class="text-muted me-3"><i class="bi bi-telephone"></i> 客服热线400-888-8888</a>
<a href="#" class="text-muted"><i class="bi bi-envelope"></i> service@taibai.com</a>
<p class="text-muted">您身边的购物专家</p>
</div>
</div>
<div class="col-md-3">
<h6>快捷导航</h6>
<ul class="list-unstyled">
<li><a href="{{ url_for('main.index') }}" class="text-muted">首页</a></li>
<li><a href="{{ url_for('main.product_list') }}" class="text-muted">全部商品</a></li>
<li><a href="#" class="text-muted">关于我们</a></li>
<li><a href="#" class="text-muted">联系我们</a></li>
</ul>
</div>
<div class="col-md-3">
<h6>客户服务</h6>
<ul class="list-unstyled">
<li><a href="#" class="text-muted">帮助中心</a></li>
<li><a href="#" class="text-muted">售后服务</a></li>
<li><a href="#" class="text-muted">配送说明</a></li>
<li><a href="#" class="text-muted">退换货政策</a></li>
</ul>
<div class="col-md-6">
<h6>联系我们</h6>
<p class="text-muted">
<i class="bi bi-envelope"></i> service@taibai-mall.com<br>
<i class="bi bi-telephone"></i> 400-888-8888
</p>
</div>
</div>
<hr>
<div class="row">
<div class="col-md-6">
<p class="text-muted small mb-0">
<i class="bi bi-shield-check"></i>
正品保证 | 7天无理由退换 | 全国包邮
</p>
</div>
<div class="col-md-6 text-md-end">
<p class="text-muted small mb-0">&copy; 2025 太白购物商城. All rights reserved.</p>
</div>
<div class="text-center text-muted">
<small>&copy; 2025 太白购物商城. 保留所有权利.</small>
</div>
</div>
</footer>
<!-- 返回顶部按钮 -->
<button type="button" class="btn btn-primary position-fixed bottom-0 end-0 m-3" id="backToTop" onclick="scrollToTop()">
<i class="bi bi-arrow-up"></i>
</button>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- 自定义JavaScript -->
<script src="{{ url_for('static', filename='js/base.js') }}"></script>
{% block scripts %}{% endblock %}
</body>

View File

@ -2,7 +2,7 @@
{% block title %}首页 - 太白购物商城{% endblock %}
{% block styles %}
{% block head %}
<link href="{{ url_for('static', filename='css/index.css') }}" rel="stylesheet">
{% endblock %}

View File

@ -133,7 +133,7 @@
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<div class="col-md-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="payment_method"
value="wechat" id="payment_wechat" checked>
@ -143,7 +143,7 @@
</label>
</div>
</div>
<div class="col-md-4">
<div class="col-md-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="payment_method"
value="alipay" id="payment_alipay">
@ -153,7 +153,7 @@
</label>
</div>
</div>
<div class="col-md-4">
<div class="col-md-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="payment_method"
value="bank" id="payment_bank">
@ -163,6 +163,25 @@
</label>
</div>
</div>
<div class="col-md-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="payment_method"
value="simulate" id="payment_simulate">
<label class="form-check-label" for="payment_simulate">
<i class="bi bi-gear-fill text-warning me-2"></i>
<strong>模拟支付</strong>
<br><small class="text-muted">测试模式</small>
</label>
</div>
</div>
</div>
<!-- 模拟支付说明 -->
<div class="alert alert-warning mt-3" id="simulatePaymentNotice" style="display: none;">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>模拟支付模式</strong><br>
这是开发测试功能,选择此支付方式后可以直接模拟支付成功或失败,无需真实付款。
实际生产环境中,此选项将被移除。
</div>
</div>
</div>
@ -221,4 +240,21 @@
{% block scripts %}
<script src="{{ url_for('static', filename='js/checkout.js') }}"></script>
<script>
// 监听支付方式变化,显示/隐藏模拟支付说明
document.addEventListener('DOMContentLoaded', function() {
const paymentMethods = document.querySelectorAll('input[name="payment_method"]');
const simulateNotice = document.getElementById('simulatePaymentNotice');
paymentMethods.forEach(method => {
method.addEventListener('change', function() {
if (this.value === 'simulate') {
simulateNotice.style.display = 'block';
} else {
simulateNotice.style.display = 'none';
}
});
});
});
</script>
{% endblock %}

View File

@ -1,10 +1,6 @@
{% extends "base.html" %}
{% block title %}订单详情 - 太白购物商城{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/order_detail.css') }}">
{% endblock %}
{% block content %}
<div class="container">
<nav aria-label="breadcrumb" class="mb-4">
@ -236,7 +232,27 @@
{% endif %}
{% if order.status == 4 %}
<a href="#" class="btn btn-outline-warning">评价商品</a>
<!-- 评价商品按钮,根据商品数量展示 -->
{% if order.order_items|length == 1 %}
<a href="{{ url_for('review.write_review', order_id=order.id, product_id=order.order_items[0].product_id) }}"
class="btn btn-outline-warning">评价商品</a>
{% else %}
<div class="dropdown d-grid">
<button class="btn btn-outline-warning dropdown-toggle" type="button" data-bs-toggle="dropdown">
评价商品
</button>
<ul class="dropdown-menu">
{% for item in order.order_items %}
<li>
<a class="dropdown-item"
href="{{ url_for('review.write_review', order_id=order.id, product_id=item.product_id) }}">
{{ item.product_name[:20] }}{% if item.product_name|length > 20 %}...{% endif %}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endif %}
<a href="{{ url_for('order.list') }}" class="btn btn-outline-primary">
@ -246,6 +262,9 @@
</div>
</div>
</div>
<!-- 重要确保order_detail.css在最后加载 -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/order_detail.css') }}">
{% endblock %}
{% block scripts %}

View File

@ -31,7 +31,13 @@
</div>
<div class="row mt-2">
<div class="col-6">
<strong>支付方式:</strong>{{ order.payment_method }}
<strong>支付方式:</strong>
{% if order.payment_method == 'wechat' %}微信支付
{% elif order.payment_method == 'alipay' %}支付宝
{% elif order.payment_method == 'bank' %}银行卡支付
{% elif order.payment_method == 'simulate' %}模拟支付
{% else %}{{ order.payment_method }}
{% endif %}
</div>
<div class="col-6 text-end">
<span class="countdown" id="countdown">14:59</span>
@ -84,17 +90,59 @@
</div>
</div>
{% endif %}
{% if order.payment_method == 'simulate' %}
<div class="payment-method selected">
<div class="d-flex align-items-center">
<i class="bi bi-gear-fill text-warning fs-1 me-3"></i>
<div>
<h6>模拟支付</h6>
<p class="text-muted mb-0">开发测试模式,可直接完成支付</p>
</div>
</div>
</div>
<!-- 模拟支付控制面板 -->
<div class="simulate-panel mt-3 p-3 border rounded bg-light">
<h6 class="text-warning"><i class="bi bi-exclamation-triangle"></i> 模拟支付控制面板</h6>
<p class="text-muted small mb-3">这是开发测试功能,您可以模拟不同的支付结果</p>
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
<button class="btn btn-success" onclick="simulatePaymentSuccess()">
<i class="bi bi-check-circle"></i> 模拟支付成功
</button>
<button class="btn btn-danger" onclick="simulatePaymentFail()">
<i class="bi bi-x-circle"></i> 模拟支付失败
</button>
</div>
<div class="mt-2 text-center">
<small class="text-muted">
<i class="bi bi-info-circle"></i>
实际生产环境中,此面板将被真实支付接口替代
</small>
</div>
</div>
{% endif %}
</div>
<!-- 支付状态 -->
<div class="payment-status" id="paymentStatus" style="display: none;">
<div id="successStatus" style="display: none;">
<i class="bi bi-check-circle-fill text-success display-1"></i>
<h5 class="mt-3">支付成功</h5>
<h5 class="mt-3 text-success">支付成功</h5>
<p class="text-muted">正在跳转到订单详情...</p>
</div>
<div id="failStatus" style="display: none;">
<i class="bi bi-x-circle-fill text-danger display-1"></i>
<h5 class="mt-3 text-danger">支付失败</h5>
<p class="text-muted">请重新选择支付方式或联系客服</p>
</div>
</div>
<!-- 操作按钮 -->
<div class="d-flex gap-2 mt-4">
<div class="d-flex gap-2 mt-4" id="actionButtons">
<button class="btn btn-primary flex-fill" onclick="startPayment()">
<i class="bi bi-credit-card"></i> 立即支付
</button>
@ -106,11 +154,17 @@
</button>
</div>
<!-- 开发测试按钮 -->
<div class="mt-3 text-center">
<button class="btn btn-warning btn-sm" onclick="simulatePayment()">
<i class="bi bi-bug"></i> 模拟支付成功(测试用)
</button>
<!-- 支付说明 -->
<div class="payment-tips mt-4">
<h6 class="text-muted">支付说明:</h6>
<ul class="text-muted small">
<li>订单有效期为15分钟请及时完成支付</li>
<li>支付成功后,订单状态将自动更新</li>
<li>如遇支付问题请联系客服400-123-4567</li>
{% if order.payment_method == 'simulate' %}
<li class="text-warning">当前为模拟支付模式,仅用于开发测试</li>
{% endif %}
</ul>
</div>
</div>
</div>

View File

@ -4,6 +4,7 @@
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/product_detail.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/review.css') }}">
{% endblock %}
{% block content %}
@ -211,7 +212,8 @@
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="reviews-tab" data-bs-toggle="tab"
data-bs-target="#reviews" type="button" role="tab">商品评价</button>
data-bs-target="#reviews" type="button" role="tab"
onclick="loadProductReviews({{ product.id }})">商品评价</button>
</li>
</ul>
@ -283,7 +285,11 @@
<div class="tab-pane fade" id="reviews" role="tabpanel">
<div class="card">
<div class="card-body">
<p class="text-muted">评价功能开发中...</p>
<div id="reviewsContainer">
<div class="text-center p-4 text-muted">
<i class="bi bi-star"></i> 点击标签页加载评价
</div>
</div>
</div>
</div>
</div>
@ -343,12 +349,29 @@
<script>
// 设置全局变量供JS使用
window.productId = {{ product.id }};
window.currentProductId = {{ product.id }};
window.isLoggedIn = {% if session.user_id %}true{% else %}false{% endif %};
</script>
<!-- 图片查看模态框 -->
<div class="modal fade" id="imageModal" 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 text-center">
<img id="modalImage" src="" class="img-fluid" alt="评价图片">
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/product_detail.js') }}"></script>
<script src="{{ url_for('static', filename='js/review.js') }}"></script>
<script>
// 处理登录状态检查
{% if not session.user_id %}

View File

@ -0,0 +1,202 @@
{% extends "base.html" %}
{% block title %}我的评价 - 太白购物商城{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/review.css') }}">
{% endblock %}
{% block content %}
<div class="row">
<!-- 侧边栏 -->
<div class="col-md-3">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-person-circle"></i> 个人中心</h5>
</div>
<div class="list-group list-group-flush">
<a href="{{ url_for('user.profile') }}" class="list-group-item list-group-item-action">
<i class="bi bi-person"></i> 基本信息
</a>
<a href="{{ url_for('user.orders') }}" class="list-group-item list-group-item-action">
<i class="bi bi-bag"></i> 我的订单
</a>
<a href="{{ url_for('address.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-geo-alt"></i> 收货地址
</a>
<a href="{{ url_for('review.my_reviews') }}" class="list-group-item list-group-item-action active">
<i class="bi bi-star"></i> 我的评价
</a>
<a href="{{ url_for('favorite.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-heart"></i> 我的收藏
</a>
<a href="{{ url_for('favorite.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-clock-history"></i> 浏览历史
</a>
</div>
</div>
</div>
<!-- 主要内容 -->
<div class="col-md-9">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-star"></i> 我的评价</h5>
</div>
<div class="card-body">
{% if reviews.items %}
{% for review in reviews.items %}
<div class="review-item">
<div class="row">
<div class="col-md-3">
<img src="{{ review.product.main_image or '/static/images/default-product.jpg' }}"
class="img-fluid rounded" alt="{{ review.product.name }}">
</div>
<div class="col-md-9">
<h6 class="mb-2">
<a href="{{ url_for('main.product_detail', product_id=review.product_id) }}"
class="text-decoration-none">{{ review.product.name }}</a>
</h6>
<!-- 评分 -->
<div class="rating-display mb-2">
<span class="stars">{{ review.get_rating_stars() }}</span>
<span class="text-muted">{{ review.rating }}分</span>
</div>
<!-- 评价内容 -->
{% if review.content %}
<p class="review-content">{{ review.content }}</p>
{% endif %}
<!-- 评价图片 -->
{% if review.get_images() %}
<div class="review-images mb-2">
{% for image_url in review.get_images() %}
<img src="{{ image_url }}" class="review-image-thumb" alt="评价图片"
onclick="showImageModal('{{ image_url }}')">
{% endfor %}
</div>
{% endif %}
<!-- 评价信息 -->
<div class="review-meta">
<small class="text-muted">
评价时间:{{ review.created_at.strftime('%Y-%m-%d %H:%M') }}
{% if review.is_anonymous %}
| 匿名评价
{% endif %}
</small>
<div class="float-end">
<button class="btn btn-outline-danger btn-sm"
onclick="deleteReview({{ review.id }})">
<i class="bi bi-trash"></i> 删除
</button>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
<!-- 分页 -->
{% if reviews.pages > 1 %}
<nav aria-label="评价分页">
<ul class="pagination justify-content-center">
{% if reviews.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('review.my_reviews', page=reviews.prev_num) }}">上一页</a>
</li>
{% endif %}
{% for page_num in reviews.iter_pages() %}
{% if page_num %}
{% if page_num != reviews.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('review.my_reviews', page=page_num) }}">{{ page_num }}</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link"></span>
</li>
{% endif %}
{% endfor %}
{% if reviews.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('review.my_reviews', page=reviews.next_num) }}">下一页</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="text-center empty-state">
<i class="bi bi-star display-1 text-muted"></i>
<h5 class="mt-3 text-muted">暂无评价</h5>
<p class="text-muted">您还没有发表过商品评价</p>
<a href="{{ url_for('main.index') }}" class="btn btn-primary">
<i class="bi bi-shop"></i> 去购物
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- 图片查看模态框 -->
<div class="modal fade" id="imageModal" 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 text-center">
<img id="modalImage" src="" class="img-fluid" alt="评价图片">
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/review.js') }}"></script>
<script>
function showImageModal(imageUrl) {
document.getElementById('modalImage').src = imageUrl;
new bootstrap.Modal(document.getElementById('imageModal')).show();
}
function deleteReview(reviewId) {
if (confirm('确定要删除这条评价吗?')) {
fetch(`/review/delete/${reviewId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert(data.message, 'success');
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
showAlert(data.message, 'error');
}
})
.catch(error => {
showAlert('删除失败', 'error');
});
}
}
</script>
{% endblock %}

View File

@ -0,0 +1,116 @@
{% extends "base.html" %}
{% block title %}评价商品 - 太白购物商城{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/review.css') }}">
{% endblock %}
{% block content %}
<div class="container">
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('main.index') }}">首页</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('order.list') }}">我的订单</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('order.detail', order_id=order.id) }}">订单详情</a></li>
<li class="breadcrumb-item active">评价商品</li>
</ol>
</nav>
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-star"></i> 评价商品</h5>
</div>
<div class="card-body">
<!-- 商品信息 -->
<div class="product-info mb-4">
<div class="row align-items-center">
<div class="col-md-2">
<img src="{{ order_item.product_image or '/static/images/default-product.jpg' }}"
class="img-fluid rounded" alt="{{ order_item.product_name }}">
</div>
<div class="col-md-10">
<h6 class="mb-1">{{ order_item.product_name }}</h6>
{% if order_item.spec_combination %}
<p class="text-muted mb-1">{{ order_item.spec_combination }}</p>
{% endif %}
<p class="text-muted mb-0">
单价:¥{{ "%.2f"|format(order_item.price) }} × {{ order_item.quantity }}
</p>
</div>
</div>
</div>
<!-- 评价表单 -->
<form id="reviewForm">
<input type="hidden" id="orderId" value="{{ order.id }}">
<input type="hidden" id="productId" value="{{ order_item.product_id }}">
<!-- 评分 -->
<div class="mb-4">
<label class="form-label"><strong>商品评分:</strong></label>
<div class="rating-container">
<div class="star-rating" id="starRating">
<span class="star" data-rating="1"></span>
<span class="star" data-rating="2"></span>
<span class="star" data-rating="3"></span>
<span class="star" data-rating="4"></span>
<span class="star" data-rating="5"></span>
</div>
<span class="rating-text" id="ratingText">请选择评分</span>
</div>
<input type="hidden" id="rating" name="rating" required>
</div>
<!-- 评价内容 -->
<div class="mb-4">
<label for="content" class="form-label"><strong>评价内容:</strong></label>
<textarea class="form-control" id="content" name="content" rows="5"
placeholder="请分享您对商品的使用感受,帮助其他买家更好地了解商品..."></textarea>
<div class="form-text">字数限制500字以内</div>
</div>
<!-- 图片上传 -->
<div class="mb-4">
<label class="form-label"><strong>上传图片:</strong>(可选)</label>
<div class="image-upload-container">
<div class="upload-area" id="uploadArea">
<i class="bi bi-cloud-upload"></i>
<p class="mb-0">点击或拖拽上传图片</p>
<small class="text-muted">支持 JPG、PNG、GIF 格式最大5MB</small>
</div>
<input type="file" id="imageInput" multiple accept="image/*" style="display: none;">
<div class="uploaded-images" id="uploadedImages"></div>
</div>
</div>
<!-- 匿名评价 -->
<div class="mb-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="isAnonymous" name="is_anonymous">
<label class="form-check-label" for="isAnonymous">
匿名评价(其他用户将看不到您的用户名)
</label>
</div>
</div>
<!-- 提交按钮 -->
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="{{ url_for('order.detail', order_id=order.id) }}"
class="btn btn-outline-secondary me-md-2">取消</a>
<button type="submit" class="btn btn-primary" id="submitBtn">
<i class="bi bi-check-circle"></i> 提交评价
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/review.js') }}"></script>
{% endblock %}

View File

@ -23,10 +23,13 @@
<a href="{{ url_for('address.index') }}" class="list-group-item list-group-item-action active">
<i class="bi bi-geo-alt"></i> 收货地址
</a>
<a href="#" class="list-group-item list-group-item-action">
<a href="{{ url_for('review.my_reviews') }}" class="list-group-item list-group-item-action">
<i class="bi bi-star"></i> 我的评价
</a>
<a href="{{ url_for('favorite.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-heart"></i> 我的收藏
</a>
<a href="#" class="list-group-item list-group-item-action">
<a href="{{ url_for('history.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-clock-history"></i> 浏览历史
</a>
</div>

View File

@ -0,0 +1,219 @@
{% extends "base.html" %}
{% block title %}我的收藏 - 太白购物商城{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/favorites.css') }}">
{% endblock %}
{% block content %}
<div class="row">
<!-- 侧边栏 -->
<div class="col-md-3">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-person-circle"></i> 个人中心</h5>
</div>
<div class="list-group list-group-flush">
<a href="{{ url_for('user.profile') }}" class="list-group-item list-group-item-action">
<i class="bi bi-person"></i> 基本信息
</a>
<a href="{{ url_for('user.orders') }}" class="list-group-item list-group-item-action">
<i class="bi bi-bag"></i> 我的订单
</a>
<a href="{{ url_for('address.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-geo-alt"></i> 收货地址
</a>
<a href="{{ url_for('review.my_reviews') }}" class="list-group-item list-group-item-action">
<i class="bi bi-star"></i> 我的评价
</a>
<a href="{{ url_for('favorite.index') }}" class="list-group-item list-group-item-action active">
<i class="bi bi-heart"></i> 我的收藏
</a>
<a href="{{ url_for('history.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-clock-history"></i> 浏览历史
</a>
</div>
</div>
</div>
<!-- 主要内容 -->
<div class="col-md-9">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5><i class="bi bi-heart text-danger"></i> 我的收藏</h5>
<div>
<span class="badge bg-secondary">共 {{ total_count }} 件商品</span>
{% if total_count > 0 %}
<button class="btn btn-sm btn-outline-secondary ms-2" onclick="toggleSelectAll()">
<i class="bi bi-check-square"></i> 全选
</button>
<button class="btn btn-sm btn-outline-danger ms-1" onclick="batchRemove()">
<i class="bi bi-trash"></i> 批量删除
</button>
{% endif %}
</div>
</div>
<div class="card-body">
{% if favorites.items %}
<div class="row">
{% for favorite in favorites.items %}
<div class="col-lg-6 col-xl-4 mb-4">
<div class="card favorite-item h-100" data-product-id="{{ favorite.product_id }}">
<div class="card-body">
<div class="d-flex">
<div class="form-check me-3">
<input class="form-check-input favorite-checkbox" type="checkbox"
value="{{ favorite.product_id }}" id="favorite-{{ favorite.product_id }}">
</div>
<div class="flex-shrink-0 me-3">
<a href="{{ url_for('main.product_detail', product_id=favorite.product_id) }}">
{% if favorite.product.main_image %}
<img src="{{ favorite.product.main_image }}"
alt="{{ favorite.product.name }}"
class="favorite-image">
{% else %}
<div class="favorite-image-placeholder">
<i class="bi bi-image"></i>
</div>
{% endif %}
</a>
</div>
<div class="flex-grow-1">
<h6 class="card-title">
<a href="{{ url_for('main.product_detail', product_id=favorite.product_id) }}"
class="text-decoration-none">
{{ favorite.product.name }}
</a>
</h6>
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-danger fw-bold">
¥{{ "%.2f"|format(favorite.product.price) }}
</span>
<small class="text-muted">
销量 {{ favorite.product.sales_count }}
</small>
</div>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
{{ favorite.created_at.strftime('%Y-%m-%d') }}
</small>
{% if favorite.product.status == 1 %}
<span class="badge bg-success">有货</span>
{% else %}
<span class="badge bg-secondary">下架</span>
{% endif %}
</div>
<div class="mt-3">
<div class="btn-group btn-group-sm w-100 icon-buttons">
{% if favorite.product.status == 1 %}
<button class="btn btn-outline-primary"
onclick="addToCart({{ favorite.product_id }})"
data-bs-toggle="tooltip" data-bs-placement="top" title="加入购物车">
<i class="bi bi-cart-plus"></i>
</button>
{% endif %}
<button class="btn btn-outline-danger"
onclick="removeFavorite({{ favorite.product_id }})"
data-bs-toggle="tooltip" data-bs-placement="top" title="取消收藏">
<i class="bi bi-heart-fill"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- 分页 -->
{% if favorites.pages > 1 %}
<nav aria-label="收藏分页">
<ul class="pagination justify-content-center">
{% if favorites.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('favorite.index', page=favorites.prev_num) }}">
<i class="bi bi-chevron-left"></i>
</a>
</li>
{% endif %}
{% for page_num in favorites.iter_pages() %}
{% if page_num %}
{% if page_num != favorites.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('favorite.index', page=page_num) }}">
{{ page_num }}
</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
{% endif %}
{% endfor %}
{% if favorites.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('favorite.index', page=favorites.next_num) }}">
<i class="bi bi-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<!-- 空状态 -->
<div class="empty-state">
<div class="text-center py-5">
<i class="bi bi-heart display-1 text-muted"></i>
<h4 class="mt-3 text-muted">还没有收藏任何商品</h4>
<p class="text-muted">去逛逛,收藏心仪的商品吧~</p>
<a href="{{ url_for('main.index') }}" class="btn btn-primary">
<i class="bi bi-house"></i> 去首页逛逛
</a>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- 确认删除模态框 -->
<div class="modal fade" id="confirmModal" 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">
<p id="confirmMessage">确定要取消收藏这些商品吗?</p>
</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="confirmBtn">确认删除</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/favorites.js') }}"></script>
{% endblock %}

View File

@ -0,0 +1,233 @@
{% extends "base.html" %}
{% block title %}浏览历史 - 太白购物商城{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/history.css') }}">
{% endblock %}
{% block content %}
<div class="row">
<!-- 侧边栏 -->
<div class="col-md-3">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-person-circle"></i> 个人中心</h5>
</div>
<div class="list-group list-group-flush">
<a href="{{ url_for('user.profile') }}" class="list-group-item list-group-item-action">
<i class="bi bi-person"></i> 基本信息
</a>
<a href="{{ url_for('user.orders') }}" class="list-group-item list-group-item-action">
<i class="bi bi-bag"></i> 我的订单
</a>
<a href="{{ url_for('address.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-geo-alt"></i> 收货地址
</a>
<a href="{{ url_for('review.my_reviews') }}" class="list-group-item list-group-item-action">
<i class="bi bi-star"></i> 我的评价
</a>
<a href="{{ url_for('favorite.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-heart"></i> 我的收藏
</a>
<a href="{{ url_for('history.index') }}" class="list-group-item list-group-item-action active">
<i class="bi bi-clock-history"></i> 浏览历史
</a>
</div>
</div>
</div>
<!-- 主要内容 -->
<div class="col-md-9">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5><i class="bi bi-clock-history text-primary"></i> 浏览历史</h5>
<div>
<span class="badge bg-secondary">共 {{ total_count }} 件商品</span>
{% if total_count > 0 %}
<button class="btn btn-sm btn-outline-secondary ms-2" onclick="toggleSelectAll()">
<i class="bi bi-check-square"></i> 全选
</button>
<button class="btn btn-sm btn-outline-danger ms-1" onclick="batchRemove()">
<i class="bi bi-trash"></i> 批量删除
</button>
<button class="btn btn-sm btn-outline-warning ms-1" onclick="clearHistory()">
<i class="bi bi-arrow-clockwise"></i> 清空历史
</button>
{% endif %}
</div>
</div>
<div class="card-body">
{% if history.items %}
<div class="row">
{% for item in history.items %}
<div class="col-lg-6 col-xl-4 mb-4">
<div class="card history-item h-100" data-product-id="{{ item.product_id }}">
<div class="card-body">
<div class="d-flex">
<div class="form-check me-3">
<input class="form-check-input history-checkbox" type="checkbox"
value="{{ item.product_id }}" id="history-{{ item.product_id }}">
</div>
<div class="flex-shrink-0 me-3">
<a href="{{ url_for('main.product_detail', product_id=item.product_id) }}">
{% if item.product.main_image %}
<img src="{{ item.product.main_image }}"
alt="{{ item.product.name }}"
class="history-image">
{% else %}
<div class="history-image-placeholder">
<i class="bi bi-image"></i>
</div>
{% endif %}
</a>
</div>
<div class="flex-grow-1">
<h6 class="card-title">
<a href="{{ url_for('main.product_detail', product_id=item.product_id) }}"
class="text-decoration-none">
{{ item.product.name }}
</a>
</h6>
<div class="mb-2">
<span class="text-danger fw-bold">
¥{{ "%.2f"|format(item.product.price) }}
</span>
<small class="text-muted ms-2">
销量 {{ item.product.sales_count }}
</small>
</div>
<div class="mb-2">
<span class="badge bg-light text-dark border">
<i class="bi bi-tag"></i> {{ item.product.category.name if item.product.category else "未分类" }}
</span>
</div>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
{{ item.viewed_at.strftime('%Y-%m-%d %H:%M') }}
</small>
{% if item.product.status == 1 %}
<span class="badge bg-success">有货</span>
{% else %}
<span class="badge bg-secondary">下架</span>
{% endif %}
</div>
<div class="mt-3">
<div class="btn-group btn-group-sm w-100">
{% if item.product.status == 1 %}
<button class="btn btn-outline-primary"
onclick="addToCart({{ item.product_id }})"
data-bs-toggle="tooltip" data-bs-placement="top" title="加入购物车">
<i class="bi bi-cart-plus"></i>
</button>
<button class="btn btn-outline-danger"
onclick="addToFavorites({{ item.product_id }})"
data-bs-toggle="tooltip" data-bs-placement="top" title="收藏商品">
<i class="bi bi-heart"></i>
</button>
{% endif %}
<button class="btn btn-outline-secondary"
onclick="removeHistory({{ item.product_id }})"
data-bs-toggle="tooltip" data-bs-placement="top" title="删除记录">
<i class="bi bi-x-circle"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- 分页 -->
{% if history.pages > 1 %}
<nav aria-label="浏览历史分页">
<ul class="pagination justify-content-center">
{% if history.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('history.index', page=history.prev_num) }}">
<i class="bi bi-chevron-left"></i>
</a>
</li>
{% endif %}
{% for page_num in history.iter_pages() %}
{% if page_num %}
{% if page_num != history.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('history.index', page=page_num) }}">
{{ page_num }}
</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
{% endif %}
{% endfor %}
{% if history.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('history.index', page=history.next_num) }}">
<i class="bi bi-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<!-- 空状态 -->
<div class="empty-state">
<div class="text-center py-5">
<i class="bi bi-clock-history display-1 text-muted"></i>
<h4 class="mt-3 text-muted">还没有浏览任何商品</h4>
<p class="text-muted">去逛逛,看看有什么好商品~</p>
<a href="{{ url_for('main.index') }}" class="btn btn-primary">
<i class="bi bi-house"></i> 去首页逛逛
</a>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- 确认删除模态框 -->
<div class="modal fade" id="confirmModal" 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">
<p id="confirmMessage">确定要删除这些浏览记录吗?</p>
</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="confirmBtn">确认删除</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/history.js') }}"></script>
{% endblock %}

View File

@ -17,12 +17,15 @@
<a href="{{ url_for('user.profile') }}" class="list-group-item list-group-item-action">
<i class="bi bi-person"></i> 基本信息
</a>
<a href="{{ url_for('user.orders') }}" class="list-group-item list-group-item-action active">
<a href="{{ url_for('order.list') }}" class="list-group-item list-group-item-action active">
<i class="bi bi-bag"></i> 我的订单
</a>
<a href="{{ url_for('address.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-geo-alt"></i> 收货地址
</a>
<a href="{{ url_for('review.my_reviews') }}" class="list-group-item list-group-item-action">
<i class="bi bi-star"></i> 我的评价
</a>
<a href="#" class="list-group-item list-group-item-action">
<i class="bi bi-heart"></i> 我的收藏
</a>
@ -133,7 +136,7 @@
<div class="order-footer">
<div class="row align-items-center">
<div class="col-md-6">
<div class="d-flex gap-2">
<div class="d-flex gap-2 flex-wrap">
{% if order.can_pay() %}
<a href="{{ url_for('order.pay', payment_sn=order.payments[0].payment_sn) }}"
class="btn btn-danger btn-sm">立即支付</a>
@ -150,7 +153,27 @@
{% endif %}
{% if order.status == 4 %}
<a href="#" class="btn btn-outline-warning btn-sm">评价商品</a>
<!-- 评价商品按钮,根据商品数量展示 -->
{% if order.order_items|length == 1 %}
<a href="{{ url_for('review.write_review', order_id=order.id, product_id=order.order_items[0].product_id) }}"
class="btn btn-outline-warning btn-sm">评价商品</a>
{% else %}
<div class="dropdown">
<button class="btn btn-outline-warning btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
评价商品
</button>
<ul class="dropdown-menu">
{% for item in order.order_items %}
<li>
<a class="dropdown-item"
href="{{ url_for('review.write_review', order_id=order.id, product_id=item.product_id) }}">
{{ item.product_name[:20] }}{% if item.product_name|length > 20 %}...{% endif %}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endif %}
</div>
</div>

View File

@ -23,10 +23,12 @@
<a href="{{ url_for('address.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-geo-alt"></i> 收货地址
</a>
<a href="#" class="list-group-item list-group-item-action">
<a href="{{ url_for('favorite.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-heart"></i> 我的收藏
<a href="{{ url_for('review.my_reviews') }}" class="list-group-item list-group-item-action">
<i class="bi bi-star"></i> 我的评价
</a>
<a href="#" class="list-group-item list-group-item-action">
<a href="{{ url_for('history.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-clock-history"></i> 浏览历史
</a>
</div>
@ -124,42 +126,76 @@
<!-- 快捷操作 -->
<div class="row mt-4">
<div class="col-md-3 mb-3">
<div class="col-md-4 mb-3">
<div class="card text-center">
<div class="card-body">
<a href="{{ url_for('user.orders') }}" class="text-decoration-none">
<i class="bi bi-bag display-4 text-primary mb-2"></i>
<h6 class="card-title">我的订单</h6>
<small class="text-muted">查看所有订单</small>
</a>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="col-md-4 mb-3">
<div class="card text-center">
<div class="card-body">
<a href="{{ url_for('cart.index') }}" class="text-decoration-none">
<i class="bi bi-cart display-4 text-success mb-2"></i>
<h6 class="card-title">购物车</h6>
<small class="text-muted">查看购物车</small>
</a>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="col-md-4 mb-3">
<div class="card text-center">
<div class="card-body">
<a href="{{ url_for('favorite.index') }}" class="text-decoration-none">
<i class="bi bi-heart display-4 text-danger mb-2"></i>
<h6 class="card-title">我的收藏</h6>
<small class="text-muted">收藏的商品</small>
</a>
</div>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="row">
<div class="col-md-4 mb-3">
<div class="card text-center">
<div class="card-body">
<a href="{{ url_for('history.index') }}" class="text-decoration-none">
<i class="bi bi-clock-history display-4 text-info mb-2"></i>
<h6 class="card-title">浏览历史</h6>
<small class="text-muted">查看浏览记录</small>
</a>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card text-center">
<div class="card-body">
<a href="{{ url_for('address.index') }}" class="text-decoration-none">
<i class="bi bi-geo-alt display-4 text-warning mb-2"></i>
<h6 class="card-title">收货地址</h6>
<small class="text-muted">管理收货地址</small>
</a>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card text-center">
<div class="card-body">
<a href="{{ url_for('review.my_reviews') }}" class="text-decoration-none">
<i class="bi bi-star display-4 text-secondary mb-2"></i>
<h6 class="card-title">我的评价</h6>
<small class="text-muted">查看我的评价</small>
</a>
</div>
</div>
</div>

View File

@ -1,292 +1,74 @@
"""
装饰器工具模块
提供登录验证权限控制等装饰器功能
装饰器工具
"""
from functools import wraps
from flask import session, redirect, url_for, flash, request, jsonify, g
from app.models.user import User
def login_required(f):
"""
登录验证装饰器
用法:
@app.route('/profile')
@login_required
def profile():
return render_template('profile.html')
功能:
- 检查用户是否已登录
- 未登录用户重定向到登录页面
- 支持AJAX请求返回JSON响应
"""
@wraps(f)
def decorated_function(*args, **kwargs):
# 检查session中是否有用户ID
if 'user_id' not in session:
# 如果是AJAX请求返回JSON响应
if request.is_json or request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'success': False,
'message': '请先登录',
'code': 'LOGIN_REQUIRED',
'redirect': url_for('auth.login')
}), 401
# 普通HTTP请求重定向到登录页
flash('请先登录后再访问该页面', 'warning')
# 保存用户想要访问的页面,登录后可以重定向回来
session['next_url'] = request.url
return redirect(url_for('auth.login'))
# 将当前用户信息加载到g对象中方便在视图函数中使用
try:
g.current_user = User.query.get(session['user_id'])
if not g.current_user or g.current_user.status != 1:
# 用户不存在或被禁用清除session
session.pop('user_id', None)
flash('账号状态异常,请重新登录', 'error')
return redirect(url_for('auth.login'))
except Exception as e:
# 数据库查询出错清除session
session.pop('user_id', None)
flash('登录状态异常,请重新登录', 'error')
return redirect(url_for('auth.login'))
return f(*args, **kwargs)
return decorated_function
from flask import session, redirect, url_for, flash, request, g
from app.models.admin import AdminUser
from app.models.operation_log import OperationLog
def admin_required(f):
"""
管理员权限验证装饰器
"""
"""管理员权限验证装饰器"""
@wraps(f)
def decorated_function(*args, **kwargs):
from app.models.admin import AdminUser
# 检查session中是否有管理员ID
if 'admin_id' not in session:
if request.is_json or request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'success': False,
'message': '需要管理员权限',
'code': 'ADMIN_REQUIRED',
'redirect': url_for('admin.login')
}), 403
flash('需要管理员权限才能访问', 'error')
flash('请先登录', 'warning')
return redirect(url_for('admin.login'))
# 加载管理员信息到g对象
try:
g.current_admin = AdminUser.query.get(session['admin_id'])
if not g.current_admin or g.current_admin.status != 1:
# 管理员不存在或被禁用清除session
session.pop('admin_id', None)
flash('管理员账号状态异常,请重新登录', 'error')
return redirect(url_for('admin.login'))
except Exception as e:
# 数据库查询出错清除session
session.pop('admin_id', None)
flash('登录状态异常,请重新登录', 'error')
# 获取管理员信息
admin = AdminUser.query.get(session['admin_id'])
if not admin or admin.status != 1:
session.clear()
flash('账号已被禁用,请联系管理员', 'error')
return redirect(url_for('admin.login'))
g.current_admin = admin
return f(*args, **kwargs)
return decorated_function
def json_required(f):
"""
JSON请求验证装饰器
用法:
@app.route('/api/upload', methods=['POST'])
@json_required
def api_upload():
data = request.get_json()
return jsonify({'success': True})
功能:
- 确保请求是JSON格式
- 非JSON请求返回错误响应
"""
def login_required(f):
"""用户登录验证装饰器"""
@wraps(f)
def decorated_function(*args, **kwargs):
if not request.is_json:
return jsonify({
'success': False,
'message': '请求必须是JSON格式',
'code': 'JSON_REQUIRED'
}), 400
if 'user_id' not in session:
flash('请先登录', 'warning')
return redirect(url_for('auth.login'))
return f(*args, **kwargs)
return decorated_function
def validate_file_upload(allowed_extensions=None, max_size=None):
"""
文件上传验证装饰器
用法:
@app.route('/upload')
@validate_file_upload(allowed_extensions={'jpg', 'png'}, max_size=2*1024*1024)
def upload_file():
file = request.files['file']
return jsonify({'success': True})
参数:
allowed_extensions: 允许的文件扩展名集合
max_size: 最大文件大小字节
"""
def log_operation(action, resource_type=None):
"""操作日志记录装饰器"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
# 检查是否有文件上传
if 'file' not in request.files:
return jsonify({
'success': False,
'message': '没有选择文件',
'code': 'NO_FILE'
}), 400
file = request.files['file']
# 检查文件名
if file.filename == '':
return jsonify({
'success': False,
'message': '没有选择文件',
'code': 'NO_FILE'
}), 400
# 检查文件扩展名
if allowed_extensions:
file_ext = file.filename.rsplit('.', 1)[1].lower() if '.' in file.filename else ''
if file_ext not in allowed_extensions:
return jsonify({
'success': False,
'message': f'不支持的文件格式,只支持: {", ".join(allowed_extensions)}',
'code': 'INVALID_FILE_TYPE'
}), 400
# 检查文件大小
if max_size:
# 获取文件大小
file.seek(0, 2) # 移动到文件末尾
file_size = file.tell()
file.seek(0) # 重置文件指针
if file_size > max_size:
size_mb = max_size / 1024 / 1024
return jsonify({
'success': False,
'message': f'文件大小超过限制,最大允许 {size_mb:.1f}MB',
'code': 'FILE_TOO_LARGE'
}), 400
return f(*args, **kwargs)
return decorated_function
return decorator
def rate_limit(max_requests=10, per_seconds=60):
"""
简单的请求频率限制装饰器
用法:
@app.route('/api/send-code')
@rate_limit(max_requests=5, per_seconds=300) # 5分钟内最多5次请求
def send_verification_code():
return jsonify({'success': True})
参数:
max_requests: 最大请求次数
per_seconds: 时间窗口
"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
# 这里可以实现基于IP或用户的请求频率限制
# 简单实现可以使用session或内存缓存
# 生产环境建议使用Redis
# 获取客户端标识IP地址或用户ID
client_id = request.remote_addr
if 'user_id' in session:
client_id = f"user_{session['user_id']}"
# 这里应该实现真正的频率限制逻辑
# 暂时跳过,返回原函数
return f(*args, **kwargs)
return decorated_function
return decorator
def log_operation(action, resource_type=None, resource_id=None):
"""
操作日志记录装饰器
用法:
@app.route('/admin/users/<int:user_id>', methods=['DELETE'])
@admin_required
@log_operation('删除用户', 'user')
def delete_user(user_id):
# 删除用户逻辑
return jsonify({'success': True})
"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
from app.models.operation_log import OperationLog
# 执行原函数
result = f(*args, **kwargs)
# 记录操作日志
try:
user_id = None
user_type = 1 # 默认普通用户
user_type = None
# 检查是否是管理员操作
if 'admin_id' in session:
user_id = session['admin_id']
user_type = 2
user_type = 2 # 管理员
elif 'user_id' in session:
user_id = session['user_id']
user_type = 1
user_type = 1 # 普通用户
# 获取资源ID如果在URL参数中
actual_resource_id = resource_id
if resource_type and not actual_resource_id:
# 尝试从URL参数中获取资源ID
for key, value in kwargs.items():
if key.endswith('_id'):
actual_resource_id = value
break
# 准备请求数据
request_data = {}
if request.method in ['POST', 'PUT', 'PATCH']:
if request.is_json:
request_data = request.get_json() or {}
else:
request_data = request.form.to_dict()
if user_id:
# 获取资源ID
resource_id = None
if 'product_id' in kwargs:
resource_id = kwargs['product_id']
elif 'category_id' in kwargs:
resource_id = kwargs['category_id']
elif 'user_id' in kwargs:
resource_id = kwargs['user_id']
# 记录日志
OperationLog.create_log(
@ -294,19 +76,16 @@ def log_operation(action, resource_type=None, resource_id=None):
user_type=user_type,
action=action,
resource_type=resource_type,
resource_id=actual_resource_id,
resource_id=resource_id,
ip_address=request.remote_addr,
user_agent=request.headers.get('User-Agent'),
request_data=request_data if request_data else None
user_agent=request.headers.get('User-Agent', ''),
request_data=dict(request.form) if request.form else None
)
except Exception as e:
# 日志记录失败不影响主要功能
print(f"记录操作日志失败: {str(e)}")
# 日志记录失败不应该影响主要功能
print(f"操作日志记录失败: {str(e)}")
return result
return decorated_function
return decorator

View File

@ -5,11 +5,14 @@ from flask import Blueprint, render_template, request, redirect, url_for, flash,
from werkzeug.security import generate_password_hash
from app.models.admin import AdminUser
from app.models.user import User
from app.models.order import Order, OrderItem, ShippingInfo
from app.models.payment import Payment
from app.models.operation_log import OperationLog
from app.utils.decorators import admin_required, log_operation
from config.database import db
from datetime import datetime, timedelta
from sqlalchemy import func
from sqlalchemy import func, or_
import json
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
@ -92,7 +95,7 @@ def dashboard():
OperationLog.created_at.desc()
).limit(10).all()
# 用户注册趋势最近7天
# 最近7天用户注册趋势
user_trend = []
for i in range(6, -1, -1):
date = datetime.now() - timedelta(days=i)
@ -207,7 +210,7 @@ def users():
search = request.args.get('search', '').strip()
if search:
query = query.filter(
db.or_(
or_(
User.username.like(f'%{search}%'),
User.email.like(f'%{search}%'),
User.phone.like(f'%{search}%'),
@ -222,7 +225,269 @@ def users():
users = query.paginate(page=page, per_page=per_page, error_out=False)
return render_template('admin/users.html', users=users, search=search, status=status)
# 计算本周新增用户数
week_start = datetime.now() - timedelta(days=7)
week_new_users = User.query.filter(User.created_at >= week_start).count()
return render_template('admin/users.html',
users=users,
search=search,
status=status,
week_new_users=week_new_users)
@admin_bp.route('/users/<int:user_id>/detail')
@admin_required
def user_detail(user_id):
"""获取用户详情"""
try:
user = User.query.get_or_404(user_id)
return jsonify({
'success': True,
'user': user.to_dict()
})
except Exception as e:
return jsonify({
'success': False,
'message': str(e)
})
@admin_bp.route('/users/<int:user_id>/toggle-status', methods=['POST'])
@admin_required
@log_operation('切换用户状态')
def toggle_user_status(user_id):
"""切换用户状态"""
try:
user = User.query.get_or_404(user_id)
data = request.get_json()
new_status = data.get('status')
if new_status not in [0, 1]:
return jsonify({
'success': False,
'message': '无效的状态值'
})
user.status = new_status
db.session.commit()
action_text = '启用' if new_status == 1 else '禁用'
return jsonify({
'success': True,
'message': f'用户已{action_text}'
})
except Exception as e:
db.session.rollback()
return jsonify({
'success': False,
'message': str(e)
})
@admin_bp.route('/orders')
@admin_required
def orders():
"""订单管理"""
page = request.args.get('page', 1, type=int)
per_page = 20
query = Order.query.order_by(Order.created_at.desc())
# 搜索功能
search = request.args.get('search', '').strip()
if search:
query = query.filter(
or_(
Order.order_sn.like(f'%{search}%'),
Order.user.has(User.username.like(f'%{search}%')),
Order.user.has(User.phone.like(f'%{search}%'))
)
)
# 状态筛选
status = request.args.get('status', '', type=str)
if status:
query = query.filter(Order.status == int(status))
# 日期筛选
start_date = request.args.get('start_date', '').strip()
end_date = request.args.get('end_date', '').strip()
if start_date:
try:
start_date_obj = datetime.strptime(start_date, '%Y-%m-%d')
query = query.filter(Order.created_at >= start_date_obj)
except ValueError:
pass
if end_date:
try:
end_date_obj = datetime.strptime(end_date, '%Y-%m-%d') + timedelta(days=1)
query = query.filter(Order.created_at < end_date_obj)
except ValueError:
pass
orders = query.paginate(page=page, per_page=per_page, error_out=False)
# 获取订单统计
order_stats = {}
for status_code, status_name in Order.STATUS_CHOICES.items():
count = Order.query.filter_by(status=status_code).count()
order_stats[status_code] = {'name': status_name, 'count': count}
return render_template('admin/orders.html',
orders=orders,
search=search,
status=status,
start_date=start_date,
end_date=end_date,
order_stats=order_stats,
ORDER_STATUS=Order.STATUS_CHOICES)
@admin_bp.route('/orders/<int:order_id>')
@admin_required
def order_detail(order_id):
"""订单详情"""
order = Order.query.get_or_404(order_id)
# 获取支付记录
payment = Payment.query.filter_by(order_id=order_id).first()
# 获取物流信息
shipping_info = ShippingInfo.query.filter_by(order_id=order_id).first()
return render_template('admin/order_detail.html',
order=order,
payment=payment,
shipping_info=shipping_info)
@admin_bp.route('/orders/<int:order_id>/ship', methods=['POST'])
@admin_required
@log_operation('订单发货')
def ship_order(order_id):
"""订单发货"""
try:
order = Order.query.get_or_404(order_id)
if order.status != Order.STATUS_PENDING_SHIPMENT:
return jsonify({'success': False, 'message': '订单状态不允许发货'})
# 获取发货信息
shipping_company = request.form.get('shipping_company', '').strip()
tracking_number = request.form.get('tracking_number', '').strip()
if not shipping_company or not tracking_number:
return jsonify({'success': False, 'message': '请填写完整的物流信息'})
# 更新订单状态
order.status = Order.STATUS_SHIPPED
order.shipped_at = datetime.utcnow()
# 创建或更新物流信息
shipping_info = ShippingInfo.query.filter_by(order_id=order_id).first()
if not shipping_info:
shipping_info = ShippingInfo(order_id=order_id)
db.session.add(shipping_info)
shipping_info.shipping_company = shipping_company
shipping_info.tracking_number = tracking_number
shipping_info.shipping_status = 1 # 已发货
db.session.commit()
return jsonify({'success': True, 'message': '发货成功'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'message': f'发货失败: {str(e)}'})
@admin_bp.route('/orders/<int:order_id>/refund', methods=['POST'])
@admin_required
@log_operation('订单退款')
def refund_order(order_id):
"""订单退款"""
try:
order = Order.query.get_or_404(order_id)
if order.status not in [Order.STATUS_PENDING_SHIPMENT, Order.STATUS_SHIPPED, Order.STATUS_REFUNDING]:
return jsonify({'success': False, 'message': '订单状态不允许退款'})
# 获取退款信息
refund_reason = request.form.get('refund_reason', '').strip()
if not refund_reason:
return jsonify({'success': False, 'message': '请填写退款原因'})
# 更新订单状态
order.status = Order.STATUS_REFUNDING
# 更新支付记录状态
payment = Payment.query.filter_by(order_id=order_id).first()
if payment:
payment.status = Payment.STATUS_REFUNDED
# 恢复库存
from app.models.product import ProductInventory
for item in order.order_items:
if item.sku_code:
sku_info = ProductInventory.query.filter_by(sku_code=item.sku_code).first()
if sku_info:
sku_info.stock += item.quantity
# 减少销量
if item.product:
item.product.sales_count = max(0, item.product.sales_count - item.quantity)
db.session.commit()
return jsonify({'success': True, 'message': '退款处理成功'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'message': f'退款失败: {str(e)}'})
@admin_bp.route('/orders/<int:order_id>/cancel', methods=['POST'])
@admin_required
@log_operation('取消订单')
def cancel_order(order_id):
"""取消订单"""
try:
order = Order.query.get_or_404(order_id)
if not order.can_cancel():
return jsonify({'success': False, 'message': '订单状态不允许取消'})
# 获取取消原因
cancel_reason = request.form.get('cancel_reason', '').strip()
# 更新订单状态
order.status = Order.STATUS_CANCELLED
# 恢复库存
from app.models.product import ProductInventory
for item in order.order_items:
if item.sku_code:
sku_info = ProductInventory.query.filter_by(sku_code=item.sku_code).first()
if sku_info:
sku_info.stock += item.quantity
# 减少销量
if item.product:
item.product.sales_count = max(0, item.product.sales_count - item.quantity)
db.session.commit()
return jsonify({'success': True, 'message': '订单已取消'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'message': f'取消失败: {str(e)}'})
@admin_bp.route('/logs')
@ -246,4 +511,63 @@ def logs():
logs = query.paginate(page=page, per_page=per_page, error_out=False)
return render_template('admin/logs.html', logs=logs, user_type=user_type, action=action)
# 计算今日操作数
today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
today_logs_count = OperationLog.query.filter(
OperationLog.created_at >= today_start
).count()
return render_template('admin/logs.html',
logs=logs,
user_type=user_type,
action=action,
today_logs_count=today_logs_count)
@admin_bp.route('/logs/<int:log_id>/detail')
@admin_required
def log_detail(log_id):
"""获取日志详情"""
try:
log = OperationLog.query.get_or_404(log_id)
return jsonify({
'success': True,
'log': log.to_dict()
})
except Exception as e:
return jsonify({
'success': False,
'message': str(e)
})
@admin_bp.route('/logs/clear', methods=['POST'])
@admin_required
@log_operation('清理操作日志')
def clear_logs():
"""清理操作日志"""
try:
data = request.get_json()
days_to_keep = data.get('days_to_keep', 30)
# 计算删除日期
delete_before = datetime.now() - timedelta(days=days_to_keep)
# 删除旧日志
deleted_count = OperationLog.query.filter(
OperationLog.created_at < delete_before
).delete()
db.session.commit()
return jsonify({
'success': True,
'message': f'已清理 {deleted_count} 条历史日志'
})
except Exception as e:
db.session.rollback()
return jsonify({
'success': False,
'message': str(e)
})

224
app/views/favorite.py Normal file
View File

@ -0,0 +1,224 @@
"""
收藏管理视图
"""
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, session
from app.models.favorite import UserFavorite
from app.models.product import Product
from app.utils.decorators import login_required
from config.database import db
favorite_bp = Blueprint('favorite', __name__, url_prefix='/favorite')
@favorite_bp.route('/')
@login_required
def index():
"""收藏夹首页"""
page = request.args.get('page', 1, type=int)
per_page = 20
# 获取用户收藏列表
favorites = UserFavorite.get_user_favorites(session['user_id'], page, per_page)
# 获取收藏总数
total_count = UserFavorite.get_user_favorites_count(session['user_id'])
return render_template('user/favorites.html',
favorites=favorites,
total_count=total_count)
@favorite_bp.route('/add', methods=['POST'])
@login_required
def add():
"""添加收藏"""
try:
data = request.get_json()
product_id = data.get('product_id')
# 调试信息
print(f"收到添加收藏请求用户ID: {session['user_id']}, 商品ID: {product_id}")
if not product_id:
return jsonify({'success': False, 'message': '商品ID不能为空'})
# 确保product_id是整数
try:
product_id = int(product_id)
except (ValueError, TypeError):
return jsonify({'success': False, 'message': '商品ID格式错误'})
success, message = UserFavorite.add_favorite(session['user_id'], product_id)
if success:
# 获取更新后的收藏数量
favorite_count = UserFavorite.get_user_favorites_count(session['user_id'])
return jsonify({
'success': True,
'message': message,
'favorite_count': favorite_count
})
else:
return jsonify({'success': False, 'message': message})
except Exception as e:
print(f"添加收藏失败: {str(e)}")
return jsonify({'success': False, 'message': f'添加收藏失败: {str(e)}'})
@favorite_bp.route('/remove', methods=['POST'])
@login_required
def remove():
"""取消收藏"""
try:
data = request.get_json()
product_id = data.get('product_id')
# 调试信息
print(f"收到取消收藏请求用户ID: {session['user_id']}, 商品ID: {product_id}")
if not product_id:
return jsonify({'success': False, 'message': '商品ID不能为空'})
# 确保product_id是整数
try:
product_id = int(product_id)
except (ValueError, TypeError):
return jsonify({'success': False, 'message': '商品ID格式错误'})
success, message = UserFavorite.remove_favorite(session['user_id'], product_id)
if success:
# 获取更新后的收藏数量
favorite_count = UserFavorite.get_user_favorites_count(session['user_id'])
return jsonify({
'success': True,
'message': message,
'favorite_count': favorite_count
})
else:
return jsonify({'success': False, 'message': message})
except Exception as e:
print(f"取消收藏失败: {str(e)}")
return jsonify({'success': False, 'message': f'取消收藏失败: {str(e)}'})
@favorite_bp.route('/toggle', methods=['POST'])
@login_required
def toggle():
"""切换收藏状态"""
try:
data = request.get_json()
product_id = data.get('product_id')
# 调试信息
print(f"收到切换收藏请求用户ID: {session['user_id']}, 商品ID: {product_id}, 数据类型: {type(product_id)}")
if not product_id:
return jsonify({'success': False, 'message': '商品ID不能为空'})
# 确保product_id是整数
try:
product_id = int(product_id)
except (ValueError, TypeError):
return jsonify({'success': False, 'message': '商品ID格式错误'})
# 检查当前是否已收藏
is_favorited = UserFavorite.is_favorited(session['user_id'], product_id)
if is_favorited:
success, message = UserFavorite.remove_favorite(session['user_id'], product_id)
action = 'removed'
else:
success, message = UserFavorite.add_favorite(session['user_id'], product_id)
action = 'added'
if success:
favorite_count = UserFavorite.get_user_favorites_count(session['user_id'])
return jsonify({
'success': True,
'message': message,
'action': action,
'is_favorited': not is_favorited,
'favorite_count': favorite_count
})
else:
return jsonify({'success': False, 'message': message})
except Exception as e:
print(f"切换收藏状态失败: {str(e)}")
return jsonify({'success': False, 'message': f'操作失败: {str(e)}'})
@favorite_bp.route('/check/<int:product_id>')
@login_required
def check(product_id):
"""检查商品是否已收藏"""
try:
is_favorited = UserFavorite.is_favorited(session['user_id'], product_id)
return jsonify({
'success': True,
'is_favorited': is_favorited
})
except Exception as e:
print(f"检查收藏状态失败: {str(e)}")
return jsonify({'success': False, 'message': str(e)})
@favorite_bp.route('/batch-remove', methods=['POST'])
@login_required
def batch_remove():
"""批量取消收藏"""
try:
data = request.get_json()
product_ids = data.get('product_ids', [])
if not product_ids:
return jsonify({'success': False, 'message': '请选择要取消的商品'})
success_count = 0
fail_count = 0
for product_id in product_ids:
try:
product_id = int(product_id)
success, _ = UserFavorite.remove_favorite(session['user_id'], product_id)
if success:
success_count += 1
else:
fail_count += 1
except (ValueError, TypeError):
fail_count += 1
message = f'成功取消收藏 {success_count} 个商品'
if fail_count > 0:
message += f',失败 {fail_count}'
# 获取更新后的收藏数量
favorite_count = UserFavorite.get_user_favorites_count(session['user_id'])
return jsonify({
'success': True,
'message': message,
'favorite_count': favorite_count
})
except Exception as e:
print(f"批量操作失败: {str(e)}")
return jsonify({'success': False, 'message': f'批量操作失败: {str(e)}'})
@favorite_bp.route('/count')
@login_required
def count():
"""获取收藏数量"""
try:
favorite_count = UserFavorite.get_user_favorites_count(session['user_id'])
return jsonify({
'success': True,
'favorite_count': favorite_count
})
except Exception as e:
print(f"获取收藏数量失败: {str(e)}")
return jsonify({'success': False, 'message': str(e)})

153
app/views/history.py Normal file
View File

@ -0,0 +1,153 @@
"""
浏览历史管理视图
"""
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, session
from app.models.browse_history import BrowseHistory
from app.models.product import Product
from app.utils.decorators import login_required
from config.database import db
history_bp = Blueprint('history', __name__, url_prefix='/history')
@history_bp.route('/')
@login_required
def index():
"""浏览历史页面"""
page = request.args.get('page', 1, type=int)
per_page = 20
# 获取用户浏览历史
history = BrowseHistory.get_user_history(session['user_id'], page, per_page)
# 获取浏览历史总数
total_count = BrowseHistory.get_user_history_count(session['user_id'])
return render_template('user/history.html',
history=history,
total_count=total_count)
@history_bp.route('/add', methods=['POST'])
@login_required
def add():
"""添加浏览记录"""
try:
data = request.get_json()
product_id = data.get('product_id')
if not product_id:
return jsonify({'success': False, 'message': '商品ID不能为空'})
success, message = BrowseHistory.add_history(session['user_id'], product_id)
if success:
# 获取更新后的浏览历史数量
history_count = BrowseHistory.get_user_history_count(session['user_id'])
return jsonify({
'success': True,
'message': message,
'history_count': history_count
})
else:
return jsonify({'success': False, 'message': message})
except Exception as e:
return jsonify({'success': False, 'message': f'添加浏览记录失败: {str(e)}'})
@history_bp.route('/remove', methods=['POST'])
@login_required
def remove():
"""删除单个浏览记录"""
try:
data = request.get_json()
product_id = data.get('product_id')
if not product_id:
return jsonify({'success': False, 'message': '商品ID不能为空'})
success, message = BrowseHistory.remove_history_item(session['user_id'], product_id)
if success:
# 获取更新后的浏览历史数量
history_count = BrowseHistory.get_user_history_count(session['user_id'])
return jsonify({
'success': True,
'message': message,
'history_count': history_count
})
else:
return jsonify({'success': False, 'message': message})
except Exception as e:
return jsonify({'success': False, 'message': f'删除浏览记录失败: {str(e)}'})
@history_bp.route('/clear', methods=['POST'])
@login_required
def clear():
"""清空浏览历史"""
try:
success, message = BrowseHistory.clear_user_history(session['user_id'])
return jsonify({
'success': success,
'message': message,
'history_count': 0 if success else BrowseHistory.get_user_history_count(session['user_id'])
})
except Exception as e:
return jsonify({'success': False, 'message': f'清空浏览历史失败: {str(e)}'})
@history_bp.route('/batch-remove', methods=['POST'])
@login_required
def batch_remove():
"""批量删除浏览记录"""
try:
data = request.get_json()
product_ids = data.get('product_ids', [])
if not product_ids:
return jsonify({'success': False, 'message': '请选择要删除的商品'})
success_count = 0
fail_count = 0
for product_id in product_ids:
success, _ = BrowseHistory.remove_history_item(session['user_id'], product_id)
if success:
success_count += 1
else:
fail_count += 1
message = f'成功删除 {success_count} 个浏览记录'
if fail_count > 0:
message += f',失败 {fail_count}'
# 获取更新后的浏览历史数量
history_count = BrowseHistory.get_user_history_count(session['user_id'])
return jsonify({
'success': True,
'message': message,
'history_count': history_count
})
except Exception as e:
return jsonify({'success': False, 'message': f'批量操作失败: {str(e)}'})
@history_bp.route('/count')
@login_required
def count():
"""获取浏览历史数量"""
try:
history_count = BrowseHistory.get_user_history_count(session['user_id'])
return jsonify({
'success': True,
'history_count': history_count
})
except Exception as e:
return jsonify({'success': False, 'message': str(e)})

View File

@ -7,6 +7,8 @@ from app.models.order import Order
from app.utils.decorators import login_required
from config.database import db
from datetime import datetime
import time
import random
payment_bp = Blueprint('payment', __name__, url_prefix='/payment')
@ -45,6 +47,9 @@ def process():
elif payment_method == 'bank':
# 银行卡支付
result = process_bank_pay(payment)
elif payment_method == 'simulate':
# 模拟支付
result = process_simulate_pay(payment)
else:
return jsonify({'success': False, 'message': '不支持的支付方式'})
@ -106,6 +111,17 @@ def process_bank_pay(payment):
}
def process_simulate_pay(payment):
"""处理模拟支付"""
return {
'success': True,
'payment_type': 'simulate',
'payment_sn': payment.payment_sn,
'amount': float(payment.amount),
'message': '模拟支付模式,可直接完成支付'
}
@payment_bp.route('/callback/wechat', methods=['POST'])
def wechat_callback():
"""微信支付回调"""
@ -201,7 +217,7 @@ def simulate_success(payment_sn):
return jsonify({'success': False, 'message': '订单已支付'})
# 模拟支付成功
result = handle_payment_success(payment_sn, f'SIMULATE_{datetime.now().timestamp()}')
result = handle_payment_success(payment_sn, f'SIMULATE_{int(time.time())}_{random.randint(1000, 9999)}')
if result == "SUCCESS":
return jsonify({'success': True, 'message': '支付成功'})
@ -210,3 +226,29 @@ def simulate_success(payment_sn):
except Exception as e:
return jsonify({'success': False, 'message': f'模拟支付失败: {str(e)}'})
@payment_bp.route('/simulate_fail/<payment_sn>', methods=['POST'])
@login_required
def simulate_fail(payment_sn):
"""模拟支付失败(开发测试用)"""
try:
user_id = session['user_id']
payment = Payment.query.filter_by(payment_sn=payment_sn).first()
if not payment or payment.order.user_id != user_id:
return jsonify({'success': False, 'message': '支付记录不存在'})
if payment.status == Payment.STATUS_SUCCESS:
return jsonify({'success': False, 'message': '订单已支付,无法模拟失败'})
# 模拟支付失败
payment.status = Payment.STATUS_FAILED
payment.third_party_sn = f'SIMULATE_FAIL_{int(time.time())}_{random.randint(1000, 9999)}'
db.session.commit()
return jsonify({'success': True, 'message': '模拟支付失败'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'message': f'模拟支付失败操作失败: {str(e)}'})

264
app/views/review.py Normal file
View File

@ -0,0 +1,264 @@
"""
评价管理视图
"""
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, session, flash, g
from sqlalchemy import func, desc
from config.database import db
from app.models.review import Review
from app.models.order import Order, OrderItem
from app.models.product import Product
from app.models.user import User
from app.utils.decorators import login_required, log_operation
from app.utils.file_upload import file_upload_handler
import json
review_bp = Blueprint('review', __name__, url_prefix='/review')
@review_bp.route('/product/<int:product_id>')
def product_reviews(product_id):
"""商品评价列表AJAX接口"""
try:
page = request.args.get('page', 1, type=int)
rating_filter = request.args.get('rating', type=int)
# 基础查询
query = Review.query.filter_by(product_id=product_id, status=1)
# 评分筛选
if rating_filter:
query = query.filter_by(rating=rating_filter)
# 分页查询
reviews = query.order_by(desc(Review.created_at)).paginate(
page=page, per_page=10, error_out=False
)
# 评价统计
stats = db.session.query(
Review.rating,
func.count(Review.id).label('count')
).filter_by(product_id=product_id, status=1).group_by(Review.rating).all()
rating_stats = {1: 0, 2: 0, 3: 0, 4: 0, 5: 0}
total_reviews = 0
for stat in stats:
rating_stats[stat.rating] = stat.count
total_reviews += stat.count
# 好评率计算
good_rate = 0
if total_reviews > 0:
good_reviews = rating_stats[4] + rating_stats[5]
good_rate = round(good_reviews / total_reviews * 100, 1)
# 转换为字典
reviews_data = []
for review in reviews.items:
review_dict = review.to_dict()
# 添加用户头像
if review.user:
review_dict['user_avatar'] = review.user.avatar_url
reviews_data.append(review_dict)
return jsonify({
'success': True,
'reviews': reviews_data,
'pagination': {
'page': reviews.page,
'pages': reviews.pages,
'per_page': reviews.per_page,
'total': reviews.total,
'has_next': reviews.has_next,
'has_prev': reviews.has_prev
},
'stats': {
'total_reviews': total_reviews,
'good_rate': good_rate,
'rating_stats': rating_stats
}
})
except Exception as e:
return jsonify({'success': False, 'message': str(e)})
@review_bp.route('/write/<int:order_id>/<int:product_id>')
@login_required
def write_review(order_id, product_id):
"""写评价页面"""
# 验证订单和商品
order = Order.query.filter_by(id=order_id, user_id=session["user_id"]).first()
if not order:
flash('订单不存在', 'error')
return redirect(url_for('order.list'))
# 检查订单状态
if order.status not in [4, 5]: # 待评价或已完成
flash('该订单暂时无法评价', 'error')
return redirect(url_for('order.detail', order_id=order_id))
# 检查商品是否在订单中
order_item = OrderItem.query.filter_by(order_id=order_id, product_id=product_id).first()
if not order_item:
flash('商品不在此订单中', 'error')
return redirect(url_for('order.detail', order_id=order_id))
# 检查是否已经评价过
existing_review = Review.query.filter_by(
user_id=session["user_id"],
product_id=product_id,
order_id=order_id
).first()
if existing_review:
flash('您已经评价过该商品', 'info')
return redirect(url_for('order.detail', order_id=order_id))
return render_template('review/write.html',
order=order,
order_item=order_item,
product=order_item.product)
@review_bp.route('/submit', methods=['POST'])
@login_required
@log_operation('提交商品评价')
def submit_review():
"""提交评价"""
try:
data = request.get_json()
order_id = data.get('order_id')
product_id = data.get('product_id')
rating = data.get('rating')
content = data.get('content', '').strip()
is_anonymous = data.get('is_anonymous', False)
images = data.get('images', [])
# 参数验证
if not all([order_id, product_id, rating]):
return jsonify({'success': False, 'message': '参数不完整'})
if not (1 <= rating <= 5):
return jsonify({'success': False, 'message': '评分必须在1-5星之间'})
# 验证订单
order = Order.query.filter_by(id=order_id, user_id=session["user_id"]).first()
if not order:
return jsonify({'success': False, 'message': '订单不存在'})
if order.status not in [4, 5]:
return jsonify({'success': False, 'message': '该订单暂时无法评价'})
# 验证商品在订单中
order_item = OrderItem.query.filter_by(order_id=order_id, product_id=product_id).first()
if not order_item:
return jsonify({'success': False, 'message': '商品不在此订单中'})
# 检查是否已评价
existing_review = Review.query.filter_by(
user_id=session["user_id"],
product_id=product_id,
order_id=order_id
).first()
if existing_review:
return jsonify({'success': False, 'message': '您已经评价过该商品'})
# 创建评价
review = Review(
user_id=session["user_id"],
product_id=product_id,
order_id=order_id,
rating=rating,
content=content if content else None,
is_anonymous=1 if is_anonymous else 0
)
# 设置图片
if images:
review.set_images(images)
db.session.add(review)
# 检查订单中所有商品是否都已评价
total_items = OrderItem.query.filter_by(order_id=order_id).count()
reviewed_items = Review.query.filter_by(order_id=order_id).count() + 1 # +1 是当前这个评价
# 如果所有商品都已评价,更新订单状态为已完成
if reviewed_items >= total_items and order.status == 4:
order.status = 5 # 已完成
db.session.commit()
return jsonify({
'success': True,
'message': '评价提交成功',
'review_id': review.id
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'message': f'提交失败: {str(e)}'})
@review_bp.route('/upload_image', methods=['POST'])
@login_required
def upload_review_image():
"""上传评价图片"""
try:
if 'file' not in request.files:
return jsonify({'success': False, 'message': '没有选择文件'})
file = request.files['file']
if file.filename == '':
return jsonify({'success': False, 'message': '没有选择文件'})
# 使用现有的文件上传处理器
result = file_upload_handler.upload_image(file, 'reviews', process_image=True)
if result['success']:
return jsonify({
'success': True,
'message': '图片上传成功',
'url': result['url']
})
else:
return jsonify({'success': False, 'message': result['error']})
except Exception as e:
return jsonify({'success': False, 'message': f'上传失败: {str(e)}'})
@review_bp.route('/my_reviews')
@login_required
def my_reviews():
"""我的评价列表"""
page = request.args.get('page', 1, type=int)
reviews = Review.query.filter_by(user_id=session["user_id"]).order_by(
desc(Review.created_at)
).paginate(page=page, per_page=10, error_out=False)
return render_template('review/my_reviews.html', reviews=reviews)
@review_bp.route('/delete/<int:review_id>', methods=['POST'])
@login_required
@log_operation('删除商品评价')
def delete_review(review_id):
"""删除评价(仅限自己的评价)"""
try:
review = Review.query.filter_by(id=review_id, user_id=session["user_id"]).first()
if not review:
return jsonify({'success': False, 'message': '评价不存在'})
db.session.delete(review)
db.session.commit()
return jsonify({'success': True, 'message': '评价删除成功'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'message': f'删除失败: {str(e)}'})