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 import Flask
from flask_mail import Mail from config.database import init_db
from config.config import Config from config.config import Config
from config.database import db
import re
# 初始化邮件服务 def create_app(config_name=None):
mail = Mail()
def create_app(config_name='default'):
app = Flask(__name__) app = Flask(__name__)
# 加载配置
app.config.from_object(Config) app.config.from_object(Config)
# 初始化数据库 # 初始化数据库
db.init_app(app) init_db(app)
# 初始化邮件服务
mail.init_app(app)
# 注册自定义过滤器
register_filters(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.auth import auth_bp
from app.views.main import main_bp
from app.views.user import user_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.product import product_bp
from app.views.cart import cart_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.order import order_bp
from app.views.payment import payment_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(auth_bp)
app.register_blueprint(main_bp)
app.register_blueprint(user_bp) app.register_blueprint(user_bp)
app.register_blueprint(admin_bp)
app.register_blueprint(product_bp) app.register_blueprint(product_bp)
app.register_blueprint(cart_bp) app.register_blueprint(cart_bp)
app.register_blueprint(address_bp)
app.register_blueprint(order_bp) app.register_blueprint(order_bp)
app.register_blueprint(payment_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前缀 return app
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("✅ 购物车蓝图注册成功")

View File

@ -8,11 +8,13 @@ from app.models.address import UserAddress
from app.models.order import Order, OrderItem, ShippingInfo from app.models.order import Order, OrderItem, ShippingInfo
from app.models.payment import Payment from app.models.payment import Payment
from app.models.review import Review from app.models.review import Review
from app.models.favorite import UserFavorite
from app.models.browse_history import BrowseHistory
__all__ = [ __all__ = [
'User', 'EmailVerification', 'AdminUser', 'OperationLog', 'User', 'EmailVerification', 'AdminUser', 'OperationLog',
'Category', 'Product', 'ProductImage', 'SpecName', 'SpecValue', 'Category', 'Product', 'ProductImage', 'SpecName', 'SpecValue',
'ProductInventory', 'InventoryLog', 'ProductSpecRelation', 'ProductInventory', 'InventoryLog', 'ProductSpecRelation',
'Cart', 'UserAddress', 'Order', 'OrderItem', 'ShippingInfo', '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 { .checkout-section {
margin-bottom: 20px; margin-bottom: 1.5rem;
} }
.address-card { .address-card {
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
border: 2px solid #e9ecef;
} }
.address-card:hover { .address-card:hover {
transform: translateY(-2px); border-color: #007bff;
box-shadow: 0 4px 12px rgba(0,0,0,0.15); box-shadow: 0 2px 8px rgba(0, 123, 255, 0.1);
} }
.address-card.selected { .address-card.selected {
border-color: #007bff; border-color: #007bff;
background-color: #f8f9ff; background-color: #e7f3ff;
} }
.product-item { .product-item {
border-bottom: 1px solid #eee; padding: 1rem 0;
padding: 15px 0; border-bottom: 1px solid #e9ecef;
} }
.product-item:last-child { .product-item:last-child {
@ -28,19 +29,136 @@
} }
.order-summary { .order-summary {
background-color: #f8f9fa; background: #f8f9fa;
border-radius: 8px; padding: 1rem;
padding: 20px; border-radius: 0.5rem;
} }
.price-row { .price-row {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-bottom: 10px; margin-bottom: 0.5rem;
} }
.total-price { .price-row.total-price {
font-size: 1.2em; font-size: 1.1rem;
font-weight: bold; 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); box-shadow: 0 4px 10px rgba(0,0,0,0.1);
} }
/* 欢迎横幅样式 */
/* 欢迎横幅样式 */ /* 欢迎横幅样式 */
.jumbotron { .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 { .order-status-timeline {
position: relative; position: relative;
padding-left: 30px; padding-left: 30px;
@ -60,13 +82,6 @@
border-bottom: none; border-bottom: none;
} }
.product-image {
width: 80px;
height: 80px;
object-fit: cover;
border-radius: 4px;
}
.info-row { .info-row {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -78,3 +93,15 @@
font-weight: bold; font-weight: bold;
font-size: 1.2em; 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 { .pay-container {
max-width: 600px; max-width: 600px;
margin: 50px auto; margin: 2rem auto;
padding: 0 1rem;
} }
.order-info { .order-info {
background-color: #f8f9fa; background: #f8f9fa;
border-radius: 8px; padding: 1rem;
padding: 20px; border-radius: 0.5rem;
margin-bottom: 20px; margin-bottom: 1.5rem;
} }
.payment-method { .payment-method {
border: 2px solid #dee2e6; border: 2px solid #e9ecef;
border-radius: 8px; border-radius: 0.5rem;
padding: 20px; padding: 1rem;
margin-bottom: 15px; margin-bottom: 1rem;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.payment-method:hover { .payment-method:hover {
border-color: #007bff; border-color: #007bff;
background-color: #f8f9ff; background-color: #f8f9fa;
} }
.payment-method.selected { .payment-method.selected {
border-color: #007bff; border-color: #007bff;
background-color: #f8f9ff; background-color: #e7f3ff;
} }
.qr-code { .qr-code {
text-align: center; text-align: center;
padding: 30px; padding: 2rem;
background-color: #f8f9fa; background: #fff;
border-radius: 8px; border: 1px solid #dee2e6;
} border-radius: 0.5rem;
margin: 1rem 0;
.countdown {
font-size: 1.2em;
color: #e74c3c;
font-weight: bold;
} }
.payment-status { .payment-status {
text-align: center; 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 { .product-card {
transition: transform 0.2s; transition: transform 0.2s;
} }
@ -55,6 +57,108 @@
cursor: not-allowed; 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) { @media (max-width: 768px) {
.price-section { .price-section {
@ -68,4 +172,30 @@
.action-buttons .btn { .action-buttons .btn {
margin-bottom: 10px; 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() { window.addEventListener('scroll', function() {
const backToTop = document.getElementById('backToTop'); const backToTop = document.getElementById('backToTop');
if (backToTop) {
if (window.pageYOffset > 300) { if (window.pageYOffset > 300) {
backToTop.style.display = 'block'; backToTop.style.display = 'block';
} else { } else {
backToTop.style.display = 'none'; backToTop.style.display = 'none';
} }
}
}); });
function scrollToTop() { function scrollToTop() {
@ -20,12 +22,94 @@ function scrollToTop() {
// 购物车数量更新 // 购物车数量更新
function updateCartBadge(count) { function updateCartBadge(count) {
const badge = document.getElementById('cartBadge'); const badge = document.getElementById('cartBadge');
if (badge) {
if (count > 0) { if (count > 0) {
badge.textContent = count; badge.textContent = count;
badge.style.display = 'inline-block'; badge.style.display = 'inline-block';
} else { } else {
badge.style.display = 'none'; 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错误处理 // 通用AJAX错误处理
function handleAjaxError(xhr) { function handleAjaxError(xhr, defaultMessage = '操作失败,请稍后再试') {
if (xhr.status === 401) { if (xhr.status === 401) {
alert('请先登录'); showAlert('请先登录', 'warning');
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秒后自动消失
setTimeout(() => { setTimeout(() => {
if (alertDiv.parentNode) { window.location.href = '/auth/login';
alertDiv.parentNode.removeChild(alertDiv); }, 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'); 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() { 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; let fee = 0;
switch(shippingMethod) { switch(shippingMethod) {
@ -48,20 +57,44 @@ function updateShippingFee() {
fee = 0; fee = 0;
} }
document.getElementById('shippingFee').textContent = `¥${fee.toFixed(2)}`; const shippingFeeElement = document.getElementById('shippingFee');
document.getElementById('totalAmount').textContent = `¥${(subtotal + fee).toFixed(2)}`; const totalAmountElement = document.getElementById('totalAmount');
if (shippingFeeElement) {
shippingFeeElement.textContent = `¥${fee.toFixed(2)}`;
}
if (totalAmountElement) {
totalAmountElement.textContent = `¥${(subtotal + fee).toFixed(2)}`;
}
} }
// 提交订单 // 提交订单
function submitOrder() { function submitOrder() {
// 验证地址选择
if (!selectedAddressId) { if (!selectedAddressId) {
showAlert('请选择收货地址', 'warning'); showAlert('请选择收货地址', 'warning');
return; return;
} }
const shippingMethod = document.querySelector('input[name="shipping_method"]:checked').value; // 获取表单数据
const paymentMethod = document.querySelector('input[name="payment_method"]:checked').value; const shippingMethodElement = document.querySelector('input[name="shipping_method"]:checked');
const remark = document.getElementById('orderRemark').value; 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 // 获取选中的购物车商品ID
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
@ -82,10 +115,16 @@ function submitOrder() {
// 显示加载状态 // 显示加载状态
const submitBtn = document.querySelector('.btn-danger'); const submitBtn = document.querySelector('.btn-danger');
if (!submitBtn) {
showAlert('提交按钮未找到', 'error');
return;
}
const originalText = submitBtn.innerHTML; const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<i class="bi bi-hourglass-split"></i> 提交中...'; submitBtn.innerHTML = '<i class="bi bi-hourglass-split"></i> 提交中...';
submitBtn.disabled = true; submitBtn.disabled = true;
// 提交订单
fetch('/order/create', { fetch('/order/create', {
method: 'POST', method: 'POST',
headers: { headers: {
@ -93,7 +132,12 @@ function submitOrder() {
}, },
body: JSON.stringify(orderData) 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 => { .then(data => {
if (data.success) { if (data.success) {
showAlert('订单创建成功!正在跳转到支付页面...', 'success'); showAlert('订单创建成功!正在跳转到支付页面...', 'success');
@ -101,13 +145,16 @@ function submitOrder() {
window.location.href = `/order/pay/${data.payment_sn}`; window.location.href = `/order/pay/${data.payment_sn}`;
}, 1500); }, 1500);
} else { } else {
showAlert(data.message, 'error'); showAlert(data.message || '订单创建失败', 'error');
// 恢复按钮状态
submitBtn.innerHTML = originalText; submitBtn.innerHTML = originalText;
submitBtn.disabled = false; submitBtn.disabled = false;
} }
}) })
.catch(error => { .catch(error => {
console.error('提交订单错误:', error);
showAlert('提交订单失败,请重试', 'error'); showAlert('提交订单失败,请重试', 'error');
// 恢复按钮状态
submitBtn.innerHTML = originalText; submitBtn.innerHTML = originalText;
submitBtn.disabled = false; 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) { function cancelOrder(orderId) {

View File

@ -16,13 +16,16 @@ window.addEventListener('beforeunload', function() {
// 开始倒计时 // 开始倒计时
function startCountdown() { function startCountdown() {
const countdownElement = document.getElementById('countdown');
if (!countdownElement) return;
countdownTimer = setInterval(() => { countdownTimer = setInterval(() => {
timeLeft--; timeLeft--;
const minutes = Math.floor(timeLeft / 60); const minutes = Math.floor(timeLeft / 60);
const seconds = timeLeft % 60; const seconds = timeLeft % 60;
document.getElementById('countdown').textContent = countdownElement.textContent =
`${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
if (timeLeft <= 0) { if (timeLeft <= 0) {
@ -45,6 +48,12 @@ function startPayment() {
return; return;
} }
// 如果是模拟支付,直接显示控制面板,不需要调用接口
if (paymentMethod === 'simulate') {
showAlert('请使用下方控制面板完成模拟支付', 'info');
return;
}
fetch('/payment/process', { fetch('/payment/process', {
method: 'POST', method: 'POST',
headers: { headers: {
@ -55,7 +64,12 @@ function startPayment() {
payment_method: paymentMethod 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 => { .then(data => {
if (data.success) { if (data.success) {
if (data.payment_type === 'qrcode') { if (data.payment_type === 'qrcode') {
@ -64,12 +78,15 @@ function startPayment() {
} else if (data.payment_type === 'redirect') { } else if (data.payment_type === 'redirect') {
window.open(data.pay_url, '_blank'); window.open(data.pay_url, '_blank');
startStatusCheck(); startStatusCheck();
} else if (data.payment_type === 'simulate') {
showAlert('模拟支付已准备就绪', 'success');
} }
} else { } else {
showAlert(data.message, 'error'); showAlert(data.message || '支付启动失败', 'error');
} }
}) })
.catch(error => { .catch(error => {
console.error('支付启动错误:', error);
showAlert('支付启动失败,请重试', 'error'); showAlert('支付启动失败,请重试', 'error');
}); });
} }
@ -79,6 +96,8 @@ function showQRCode(qrUrl) {
const qrArea = document.getElementById('qrCodeArea'); const qrArea = document.getElementById('qrCodeArea');
const qrImage = document.getElementById('qrCodeImage'); const qrImage = document.getElementById('qrCodeImage');
if (!qrArea || !qrImage) return;
// 这里应该使用真实的二维码生成库,现在用文本模拟 // 这里应该使用真实的二维码生成库,现在用文本模拟
qrImage.innerHTML = ` qrImage.innerHTML = `
<div style="width: 200px; height: 200px; margin: 0 auto; background: #f0f0f0; <div style="width: 200px; height: 200px; margin: 0 auto; background: #f0f0f0;
@ -107,13 +126,21 @@ function checkPaymentStatus() {
if (!paymentSn) return; if (!paymentSn) return;
fetch(`/payment/check_status/${paymentSn}`) 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 => { .then(data => {
if (data.success) { if (data.success) {
if (data.status === 2) { // 支付成功 if (data.status === 2) { // 支付成功
clearInterval(statusCheckTimer); clearInterval(statusCheckTimer);
clearInterval(countdownTimer); clearInterval(countdownTimer);
showPaymentSuccess(); showPaymentSuccess();
} else if (data.status === 3) { // 支付失败
clearInterval(statusCheckTimer);
showPaymentFail();
} }
} }
}) })
@ -124,15 +151,47 @@ function checkPaymentStatus() {
// 显示支付成功 // 显示支付成功
function showPaymentSuccess() { function showPaymentSuccess() {
document.getElementById('paymentArea').style.display = 'none'; const paymentArea = document.getElementById('paymentArea');
document.getElementById('paymentStatus').style.display = 'block'; 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; const orderId = document.querySelector('[data-order-id]')?.dataset.orderId;
setTimeout(() => { setTimeout(() => {
if (orderId) {
window.location.href = `/order/detail/${orderId}`; window.location.href = `/order/detail/${orderId}`;
} else {
window.location.href = '/order/list';
}
}, 2000); }, 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() { function cancelOrder() {
const orderId = document.querySelector('[data-order-id]')?.dataset.orderId; const orderId = document.querySelector('[data-order-id]')?.dataset.orderId;
@ -142,32 +201,38 @@ function cancelOrder() {
return; return;
} }
if (confirm('确定要取消这个订单吗?')) { showConfirm('确定要取消这个订单吗?', () => {
fetch(`/order/cancel/${orderId}`, { fetch(`/order/cancel/${orderId}`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', '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 => { .then(data => {
if (data.success) { if (data.success) {
showAlert('订单已取消', 'success'); showAlert('订单已取消,正在跳转...', 'success');
setTimeout(() => { setTimeout(() => {
window.location.href = '/order/list'; window.location.href = '/order/list';
}, 1500); }, 1500);
} else { } else {
showAlert(data.message, 'error'); showAlert(data.message || '取消订单失败', 'error');
} }
}) })
.catch(error => { .catch(error => {
console.error('取消订单错误:', error);
showAlert('取消失败,请重试', 'error'); showAlert('取消失败,请重试', 'error');
}); });
} });
} }
// 模拟支付成功(开发测试用) // 模拟支付成功
function simulatePayment() { function simulatePaymentSuccess() {
const paymentSn = document.querySelector('[data-payment-sn]')?.dataset.paymentSn; const paymentSn = document.querySelector('[data-payment-sn]')?.dataset.paymentSn;
if (!paymentSn) { if (!paymentSn) {
@ -175,26 +240,94 @@ function simulatePayment() {
return; 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}`, { fetch(`/payment/simulate_success/${paymentSn}`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', '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 => { .then(data => {
if (data.success) { if (data.success) {
showAlert('模拟支付成功', 'success'); showAlert('模拟支付成功', 'success');
setTimeout(() => { setTimeout(() => {
showPaymentSuccess(); showPaymentSuccess();
}, 1000); }, 1000);
} else { } else {
showAlert(data.message, 'error'); showAlert(data.message || '模拟支付失败', 'error');
button.innerHTML = originalText;
button.disabled = false;
} }
}) })
.catch(error => { .catch(error => {
console.error('模拟支付错误:', error);
showAlert('模拟支付失败', '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') { if (typeof loadCartCount === 'function') {
loadCartCount(); 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) { function selectSpec(button) {
const specName = button.getAttribute('data-spec-name'); const specName = button.getAttribute('data-spec-name');
@ -231,6 +290,68 @@ function buyNow() {
// 收藏商品 // 收藏商品
function addToFavorites() { function addToFavorites() {
// TODO: 实现收藏功能 if (!window.isLoggedIn) {
alert('收藏功能开发中...'); 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> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.orders' %}active{% endif %}" <a class="nav-link {% if request.endpoint.startswith('admin.order') %}active{% endif %}"
href="#"> href="{{ url_for('admin.orders') }}">
<i class="bi bi-receipt"></i> <i class="bi bi-receipt"></i>
订单管理 订单管理
</a> </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 charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}太白购物商城{% endblock %}</title> <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@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="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> </head>
<body> <body>
<!-- 导航栏 --> <!-- 导航栏 -->
@ -29,23 +33,12 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ url_for('main.product_list') }}">全部商品</a> <a class="nav-link" href="{{ url_for('main.product_list') }}">全部商品</a>
</li> </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> </ul>
<!-- 搜索框 --> <!-- 搜索框 -->
<form class="d-flex me-3 search-form" method="GET" action="{{ url_for('main.product_list') }}"> <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="搜索商品..." style="min-width: 200px;"> <input class="form-control me-2" type="search" name="search" placeholder="搜索商品..."
value="{{ request.args.get('search', '') }}">
<button class="btn btn-outline-primary" type="submit"> <button class="btn btn-outline-primary" type="submit">
<i class="bi bi-search"></i> <i class="bi bi-search"></i>
</button> </button>
@ -54,7 +47,7 @@
<ul class="navbar-nav"> <ul class="navbar-nav">
{% if session.user_id %} {% if session.user_id %}
<li class="nav-item"> <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> 购物车 <i class="bi bi-cart"></i> 购物车
<span class="badge bg-danger cart-badge" id="cartBadge" style="display: none;">0</span> <span class="badge bg-danger cart-badge" id="cartBadge" style="display: none;">0</span>
</a> </a>
@ -66,7 +59,8 @@
<ul class="dropdown-menu"> <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.profile') }}">个人中心</a></li>
<li><a class="dropdown-item" href="{{ url_for('user.orders') }}">我的订单</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><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">退出登录</a></li> <li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">退出登录</a></li>
</ul> </ul>
@ -84,77 +78,50 @@
</div> </div>
</nav> </nav>
<!-- 消息提示 --> <!-- 主要内容 -->
<main class="container mt-4">
<!-- Flash消息提示 -->
{% with messages = get_flashed_messages(with_categories=true) %} {% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %} {% if messages %}
<div class="container mt-3">
{% for category, message in messages %} {% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert"> <div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
{{ message }} {{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button> <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div> </div>
{% endfor %} {% endfor %}
</div>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
<!-- 主要内容 -->
<main class="container mt-4">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>
<!-- 页脚 --> <!-- 页脚 -->
<footer class="footer mt-auto"> <footer class="bg-light py-4 mt-5">
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<h5>太白购物商城</h5> <h5>太白购物商城</h5>
<p class="text-muted">您的购物首选平台</p> <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>
</div> </div>
</div> <div class="col-md-6">
<div class="col-md-3"> <h6>联系我们</h6>
<h6>快捷导航</h6> <p class="text-muted">
<ul class="list-unstyled"> <i class="bi bi-envelope"></i> service@taibai-mall.com<br>
<li><a href="{{ url_for('main.index') }}" class="text-muted">首页</a></li> <i class="bi bi-telephone"></i> 400-888-8888
<li><a href="{{ url_for('main.product_list') }}" class="text-muted">全部商品</a></li> </p>
<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> </div>
</div> </div>
<hr> <hr>
<div class="row"> <div class="text-center text-muted">
<div class="col-md-6"> <small>&copy; 2025 太白购物商城. 保留所有权利.</small>
<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> </div>
</div> </div>
</footer> </footer>
<!-- 返回顶部按钮 --> <!-- Bootstrap JS -->
<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>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script> <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> <script src="{{ url_for('static', filename='js/base.js') }}"></script>
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}
</body> </body>

View File

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

View File

@ -133,7 +133,7 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row"> <div class="row">
<div class="col-md-4"> <div class="col-md-3">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" name="payment_method" <input class="form-check-input" type="radio" name="payment_method"
value="wechat" id="payment_wechat" checked> value="wechat" id="payment_wechat" checked>
@ -143,7 +143,7 @@
</label> </label>
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-3">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" name="payment_method" <input class="form-check-input" type="radio" name="payment_method"
value="alipay" id="payment_alipay"> value="alipay" id="payment_alipay">
@ -153,7 +153,7 @@
</label> </label>
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-3">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" name="payment_method" <input class="form-check-input" type="radio" name="payment_method"
value="bank" id="payment_bank"> value="bank" id="payment_bank">
@ -163,6 +163,25 @@
</label> </label>
</div> </div>
</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> </div>
</div> </div>
@ -221,4 +240,21 @@
{% block scripts %} {% block scripts %}
<script src="{{ url_for('static', filename='js/checkout.js') }}"></script> <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 %} {% endblock %}

View File

@ -1,10 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}订单详情 - 太白购物商城{% endblock %} {% block title %}订单详情 - 太白购物商城{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/order_detail.css') }}">
{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
<nav aria-label="breadcrumb" class="mb-4"> <nav aria-label="breadcrumb" class="mb-4">
@ -236,7 +232,27 @@
{% endif %} {% endif %}
{% if order.status == 4 %} {% 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 %} {% endif %}
<a href="{{ url_for('order.list') }}" class="btn btn-outline-primary"> <a href="{{ url_for('order.list') }}" class="btn btn-outline-primary">
@ -246,6 +262,9 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 重要确保order_detail.css在最后加载 -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/order_detail.css') }}">
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}

View File

@ -31,7 +31,13 @@
</div> </div>
<div class="row mt-2"> <div class="row mt-2">
<div class="col-6"> <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>
<div class="col-6 text-end"> <div class="col-6 text-end">
<span class="countdown" id="countdown">14:59</span> <span class="countdown" id="countdown">14:59</span>
@ -84,17 +90,59 @@
</div> </div>
</div> </div>
{% endif %} {% 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>
<!-- 支付状态 --> <!-- 支付状态 -->
<div class="payment-status" id="paymentStatus" style="display: none;"> <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> <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> <p class="text-muted">正在跳转到订单详情...</p>
</div> </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()"> <button class="btn btn-primary flex-fill" onclick="startPayment()">
<i class="bi bi-credit-card"></i> 立即支付 <i class="bi bi-credit-card"></i> 立即支付
</button> </button>
@ -106,11 +154,17 @@
</button> </button>
</div> </div>
<!-- 开发测试按钮 --> <!-- 支付说明 -->
<div class="mt-3 text-center"> <div class="payment-tips mt-4">
<button class="btn btn-warning btn-sm" onclick="simulatePayment()"> <h6 class="text-muted">支付说明:</h6>
<i class="bi bi-bug"></i> 模拟支付成功(测试用) <ul class="text-muted small">
</button> <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> </div>
</div> </div>

View File

@ -4,6 +4,7 @@
{% block head %} {% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/product_detail.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/product_detail.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/review.css') }}">
{% endblock %} {% endblock %}
{% block content %} {% block content %}
@ -211,7 +212,8 @@
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link" id="reviews-tab" data-bs-toggle="tab" <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> </li>
</ul> </ul>
@ -283,7 +285,11 @@
<div class="tab-pane fade" id="reviews" role="tabpanel"> <div class="tab-pane fade" id="reviews" role="tabpanel">
<div class="card"> <div class="card">
<div class="card-body"> <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> </div>
</div> </div>
@ -343,12 +349,29 @@
<script> <script>
// 设置全局变量供JS使用 // 设置全局变量供JS使用
window.productId = {{ product.id }}; window.productId = {{ product.id }};
window.currentProductId = {{ product.id }};
window.isLoggedIn = {% if session.user_id %}true{% else %}false{% endif %}; window.isLoggedIn = {% if session.user_id %}true{% else %}false{% endif %};
</script> </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 %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="{{ url_for('static', filename='js/product_detail.js') }}"></script> <script src="{{ url_for('static', filename='js/product_detail.js') }}"></script>
<script src="{{ url_for('static', filename='js/review.js') }}"></script>
<script> <script>
// 处理登录状态检查 // 处理登录状态检查
{% if not session.user_id %} {% 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"> <a href="{{ url_for('address.index') }}" class="list-group-item list-group-item-action active">
<i class="bi bi-geo-alt"></i> 收货地址 <i class="bi bi-geo-alt"></i> 收货地址
</a> </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> 我的收藏 <i class="bi bi-heart"></i> 我的收藏
</a> </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> 浏览历史 <i class="bi bi-clock-history"></i> 浏览历史
</a> </a>
</div> </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"> <a href="{{ url_for('user.profile') }}" class="list-group-item list-group-item-action">
<i class="bi bi-person"></i> 基本信息 <i class="bi bi-person"></i> 基本信息
</a> </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> 我的订单 <i class="bi bi-bag"></i> 我的订单
</a> </a>
<a href="{{ url_for('address.index') }}" class="list-group-item list-group-item-action"> <a href="{{ url_for('address.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-geo-alt"></i> 收货地址 <i class="bi bi-geo-alt"></i> 收货地址
</a> </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"> <a href="#" class="list-group-item list-group-item-action">
<i class="bi bi-heart"></i> 我的收藏 <i class="bi bi-heart"></i> 我的收藏
</a> </a>
@ -133,7 +136,7 @@
<div class="order-footer"> <div class="order-footer">
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-md-6"> <div class="col-md-6">
<div class="d-flex gap-2"> <div class="d-flex gap-2 flex-wrap">
{% if order.can_pay() %} {% if order.can_pay() %}
<a href="{{ url_for('order.pay', payment_sn=order.payments[0].payment_sn) }}" <a href="{{ url_for('order.pay', payment_sn=order.payments[0].payment_sn) }}"
class="btn btn-danger btn-sm">立即支付</a> class="btn btn-danger btn-sm">立即支付</a>
@ -150,7 +153,27 @@
{% endif %} {% endif %}
{% if order.status == 4 %} {% 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 %} {% endif %}
</div> </div>
</div> </div>

View File

@ -23,10 +23,12 @@
<a href="{{ url_for('address.index') }}" class="list-group-item list-group-item-action"> <a href="{{ url_for('address.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-geo-alt"></i> 收货地址 <i class="bi bi-geo-alt"></i> 收货地址
</a> </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> 我的收藏 <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>
<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> 浏览历史 <i class="bi bi-clock-history"></i> 浏览历史
</a> </a>
</div> </div>
@ -124,42 +126,76 @@
<!-- 快捷操作 --> <!-- 快捷操作 -->
<div class="row mt-4"> <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 text-center">
<div class="card-body"> <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> <i class="bi bi-bag display-4 text-primary mb-2"></i>
<h6 class="card-title">我的订单</h6> <h6 class="card-title">我的订单</h6>
<small class="text-muted">查看所有订单</small> <small class="text-muted">查看所有订单</small>
</a>
</div> </div>
</div> </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 text-center">
<div class="card-body"> <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> <i class="bi bi-cart display-4 text-success mb-2"></i>
<h6 class="card-title">购物车</h6> <h6 class="card-title">购物车</h6>
<small class="text-muted">查看购物车</small> <small class="text-muted">查看购物车</small>
</a>
</div> </div>
</div> </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 text-center">
<div class="card-body"> <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> <i class="bi bi-heart display-4 text-danger mb-2"></i>
<h6 class="card-title">我的收藏</h6> <h6 class="card-title">我的收藏</h6>
<small class="text-muted">收藏的商品</small> <small class="text-muted">收藏的商品</small>
</a>
</div>
</div> </div>
</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 text-center">
<div class="card-body"> <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> <i class="bi bi-geo-alt display-4 text-warning mb-2"></i>
<h6 class="card-title">收货地址</h6> <h6 class="card-title">收货地址</h6>
<small class="text-muted">管理收货地址</small> <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> </div>
</div> </div>

View File

@ -1,292 +1,74 @@
""" """
装饰器工具模块 装饰器工具
提供登录验证权限控制等装饰器功能
""" """
from functools import wraps from functools import wraps
from flask import session, redirect, url_for, flash, request, jsonify, g from flask import session, redirect, url_for, flash, request, g
from app.models.user import User from app.models.admin import AdminUser
from app.models.operation_log import OperationLog
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
def admin_required(f): def admin_required(f):
""" """管理员权限验证装饰器"""
管理员权限验证装饰器
"""
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
from app.models.admin import AdminUser
# 检查session中是否有管理员ID
if 'admin_id' not in session: if 'admin_id' not in session:
if request.is_json or request.headers.get('X-Requested-With') == 'XMLHttpRequest': flash('请先登录', 'warning')
return jsonify({
'success': False,
'message': '需要管理员权限',
'code': 'ADMIN_REQUIRED',
'redirect': url_for('admin.login')
}), 403
flash('需要管理员权限才能访问', 'error')
return redirect(url_for('admin.login')) return redirect(url_for('admin.login'))
# 加载管理员信息到g对象 # 获取管理员信息
try: admin = AdminUser.query.get(session['admin_id'])
g.current_admin = AdminUser.query.get(session['admin_id']) if not admin or admin.status != 1:
if not g.current_admin or g.current_admin.status != 1: session.clear()
# 管理员不存在或被禁用清除session flash('账号已被禁用,请联系管理员', 'error')
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')
return redirect(url_for('admin.login')) return redirect(url_for('admin.login'))
g.current_admin = admin
return f(*args, **kwargs) return f(*args, **kwargs)
return decorated_function return decorated_function
def json_required(f): def login_required(f):
""" """用户登录验证装饰器"""
JSON请求验证装饰器
用法:
@app.route('/api/upload', methods=['POST'])
@json_required
def api_upload():
data = request.get_json()
return jsonify({'success': True})
功能:
- 确保请求是JSON格式
- 非JSON请求返回错误响应
"""
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if not request.is_json: if 'user_id' not in session:
return jsonify({ flash('请先登录', 'warning')
'success': False, return redirect(url_for('auth.login'))
'message': '请求必须是JSON格式',
'code': 'JSON_REQUIRED'
}), 400
return f(*args, **kwargs) return f(*args, **kwargs)
return decorated_function return decorated_function
def validate_file_upload(allowed_extensions=None, max_size=None): def log_operation(action, resource_type=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 decorator(f): def decorator(f):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): 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) result = f(*args, **kwargs)
# 记录操作日志 # 记录操作日志
try: try:
user_id = None user_id = None
user_type = 1 # 默认普通用户 user_type = None
# 检查是否是管理员操作
if 'admin_id' in session: if 'admin_id' in session:
user_id = session['admin_id'] user_id = session['admin_id']
user_type = 2 user_type = 2 # 管理员
elif 'user_id' in session: elif 'user_id' in session:
user_id = session['user_id'] user_id = session['user_id']
user_type = 1 user_type = 1 # 普通用户
# 获取资源ID如果在URL参数中 if user_id:
actual_resource_id = resource_id # 获取资源ID
if resource_type and not actual_resource_id: resource_id = None
# 尝试从URL参数中获取资源ID if 'product_id' in kwargs:
for key, value in kwargs.items(): resource_id = kwargs['product_id']
if key.endswith('_id'): elif 'category_id' in kwargs:
actual_resource_id = value resource_id = kwargs['category_id']
break elif 'user_id' in kwargs:
resource_id = kwargs['user_id']
# 准备请求数据
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()
# 记录日志 # 记录日志
OperationLog.create_log( OperationLog.create_log(
@ -294,19 +76,16 @@ def log_operation(action, resource_type=None, resource_id=None):
user_type=user_type, user_type=user_type,
action=action, action=action,
resource_type=resource_type, resource_type=resource_type,
resource_id=actual_resource_id, resource_id=resource_id,
ip_address=request.remote_addr, ip_address=request.remote_addr,
user_agent=request.headers.get('User-Agent'), user_agent=request.headers.get('User-Agent', ''),
request_data=request_data if request_data else None request_data=dict(request.form) if request.form else None
) )
except Exception as e: except Exception as e:
# 日志记录失败不影响主要功能 # 日志记录失败不应该影响主要功能
print(f"记录操作日志失败: {str(e)}") print(f"操作日志记录失败: {str(e)}")
return result return result
return decorated_function return decorated_function
return decorator 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 werkzeug.security import generate_password_hash
from app.models.admin import AdminUser from app.models.admin import AdminUser
from app.models.user import User 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.models.operation_log import OperationLog
from app.utils.decorators import admin_required, log_operation from app.utils.decorators import admin_required, log_operation
from config.database import db from config.database import db
from datetime import datetime, timedelta from datetime import datetime, timedelta
from sqlalchemy import func from sqlalchemy import func, or_
import json
admin_bp = Blueprint('admin', __name__, url_prefix='/admin') admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
@ -92,7 +95,7 @@ def dashboard():
OperationLog.created_at.desc() OperationLog.created_at.desc()
).limit(10).all() ).limit(10).all()
# 用户注册趋势最近7天 # 最近7天用户注册趋势
user_trend = [] user_trend = []
for i in range(6, -1, -1): for i in range(6, -1, -1):
date = datetime.now() - timedelta(days=i) date = datetime.now() - timedelta(days=i)
@ -207,7 +210,7 @@ def users():
search = request.args.get('search', '').strip() search = request.args.get('search', '').strip()
if search: if search:
query = query.filter( query = query.filter(
db.or_( or_(
User.username.like(f'%{search}%'), User.username.like(f'%{search}%'),
User.email.like(f'%{search}%'), User.email.like(f'%{search}%'),
User.phone.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) 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') @admin_bp.route('/logs')
@ -246,4 +511,63 @@ def logs():
logs = query.paginate(page=page, per_page=per_page, error_out=False) 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 app.utils.decorators import login_required
from config.database import db from config.database import db
from datetime import datetime from datetime import datetime
import time
import random
payment_bp = Blueprint('payment', __name__, url_prefix='/payment') payment_bp = Blueprint('payment', __name__, url_prefix='/payment')
@ -45,6 +47,9 @@ def process():
elif payment_method == 'bank': elif payment_method == 'bank':
# 银行卡支付 # 银行卡支付
result = process_bank_pay(payment) result = process_bank_pay(payment)
elif payment_method == 'simulate':
# 模拟支付
result = process_simulate_pay(payment)
else: else:
return jsonify({'success': False, 'message': '不支持的支付方式'}) 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']) @payment_bp.route('/callback/wechat', methods=['POST'])
def wechat_callback(): def wechat_callback():
"""微信支付回调""" """微信支付回调"""
@ -201,7 +217,7 @@ def simulate_success(payment_sn):
return jsonify({'success': False, 'message': '订单已支付'}) 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": if result == "SUCCESS":
return jsonify({'success': True, 'message': '支付成功'}) return jsonify({'success': True, 'message': '支付成功'})
@ -210,3 +226,29 @@ def simulate_success(payment_sn):
except Exception as e: except Exception as e:
return jsonify({'success': False, 'message': f'模拟支付失败: {str(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)}'})