finish_preview
This commit is contained in:
parent
5fcd6b7017
commit
5d73776a71
@ -1,95 +1,41 @@
|
||||
"""
|
||||
Flask应用工厂
|
||||
"""
|
||||
from flask import Flask
|
||||
from flask_mail import Mail
|
||||
from config.database import init_db
|
||||
from config.config import Config
|
||||
from config.database import db
|
||||
import re
|
||||
|
||||
# 初始化邮件服务
|
||||
mail = Mail()
|
||||
|
||||
|
||||
def create_app(config_name='default'):
|
||||
def create_app(config_name=None):
|
||||
app = Flask(__name__)
|
||||
|
||||
# 加载配置
|
||||
app.config.from_object(Config)
|
||||
|
||||
# 初始化数据库
|
||||
db.init_app(app)
|
||||
|
||||
# 初始化邮件服务
|
||||
mail.init_app(app)
|
||||
|
||||
# 注册自定义过滤器
|
||||
register_filters(app)
|
||||
init_db(app)
|
||||
|
||||
# 注册蓝图
|
||||
register_blueprints(app)
|
||||
|
||||
# 创建数据库表
|
||||
with app.app_context():
|
||||
try:
|
||||
db.create_all()
|
||||
print("✅ 数据库表创建/同步成功")
|
||||
except Exception as e:
|
||||
print(f"❌ 数据库表创建失败: {str(e)}")
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def register_filters(app):
|
||||
"""注册自定义过滤器"""
|
||||
|
||||
@app.template_filter('nl2br')
|
||||
def nl2br_filter(text):
|
||||
"""将换行符转换为HTML <br> 标签"""
|
||||
if not text:
|
||||
return ''
|
||||
# 将换行符替换为 <br> 标签
|
||||
return text.replace('\n', '<br>')
|
||||
|
||||
@app.template_filter('truncate_chars')
|
||||
def truncate_chars_filter(text, length=50):
|
||||
"""截断字符串"""
|
||||
if not text:
|
||||
return ''
|
||||
if len(text) <= length:
|
||||
return text
|
||||
return text[:length] + '...'
|
||||
|
||||
|
||||
def register_blueprints(app):
|
||||
"""注册蓝图"""
|
||||
from app.views.main import main_bp
|
||||
from app.views.auth import auth_bp
|
||||
from app.views.main import main_bp
|
||||
from app.views.user import user_bp
|
||||
from app.views.admin import admin_bp
|
||||
from app.views.product import product_bp
|
||||
from app.views.cart import cart_bp
|
||||
from app.views.address import address_bp
|
||||
from app.views.order import order_bp
|
||||
from app.views.payment import payment_bp
|
||||
from app.views.admin import admin_bp
|
||||
from app.views.address import address_bp
|
||||
from app.views.upload import upload_bp
|
||||
from app.views.review import review_bp
|
||||
from app.views.favorite import favorite_bp
|
||||
from app.views.history import history_bp
|
||||
|
||||
app.register_blueprint(main_bp)
|
||||
app.register_blueprint(auth_bp)
|
||||
app.register_blueprint(main_bp)
|
||||
app.register_blueprint(user_bp)
|
||||
app.register_blueprint(admin_bp)
|
||||
app.register_blueprint(product_bp)
|
||||
app.register_blueprint(cart_bp)
|
||||
app.register_blueprint(address_bp)
|
||||
app.register_blueprint(order_bp)
|
||||
app.register_blueprint(payment_bp)
|
||||
app.register_blueprint(admin_bp)
|
||||
app.register_blueprint(address_bp)
|
||||
app.register_blueprint(upload_bp)
|
||||
app.register_blueprint(review_bp)
|
||||
app.register_blueprint(favorite_bp)
|
||||
app.register_blueprint(history_bp)
|
||||
|
||||
# 修复:正确注册upload蓝图并设置URL前缀
|
||||
try:
|
||||
from app.views.upload import upload_bp
|
||||
app.register_blueprint(upload_bp, url_prefix='/upload') # 添加URL前缀
|
||||
print("✅ 上传功能蓝图注册成功")
|
||||
except ImportError as e:
|
||||
print(f"⚠️ 上传功能暂时不可用: {str(e)}")
|
||||
|
||||
print("✅ 商品管理蓝图注册成功")
|
||||
print("✅ 购物车蓝图注册成功")
|
||||
return app
|
||||
|
@ -8,11 +8,13 @@ from app.models.address import UserAddress
|
||||
from app.models.order import Order, OrderItem, ShippingInfo
|
||||
from app.models.payment import Payment
|
||||
from app.models.review import Review
|
||||
from app.models.favorite import UserFavorite
|
||||
from app.models.browse_history import BrowseHistory
|
||||
|
||||
__all__ = [
|
||||
'User', 'EmailVerification', 'AdminUser', 'OperationLog',
|
||||
'Category', 'Product', 'ProductImage', 'SpecName', 'SpecValue',
|
||||
'ProductInventory', 'InventoryLog', 'ProductSpecRelation',
|
||||
'Cart', 'UserAddress', 'Order', 'OrderItem', 'ShippingInfo',
|
||||
'Payment', 'Review'
|
||||
'Payment', 'Review', 'UserFavorite', 'BrowseHistory'
|
||||
]
|
||||
|
111
app/models/browse_history.py
Normal file
111
app/models/browse_history.py
Normal 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
102
app/models/favorite.py
Normal 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}>'
|
295
app/static/css/admin_logs.css
Normal file
295
app/static/css/admin_logs.css
Normal 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); }
|
||||
}
|
202
app/static/css/admin_orders.css
Normal file
202
app/static/css/admin_orders.css
Normal 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;
|
||||
}
|
||||
}
|
351
app/static/css/admin_users.css
Normal file
351
app/static/css/admin_users.css
Normal 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); }
|
||||
}
|
@ -1,26 +1,27 @@
|
||||
/* 订单结算页面样式 */
|
||||
.checkout-section {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.address-card {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid #e9ecef;
|
||||
}
|
||||
|
||||
.address-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.1);
|
||||
}
|
||||
|
||||
.address-card.selected {
|
||||
border-color: #007bff;
|
||||
background-color: #f8f9ff;
|
||||
background-color: #e7f3ff;
|
||||
}
|
||||
|
||||
.product-item {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 15px 0;
|
||||
padding: 1rem 0;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.product-item:last-child {
|
||||
@ -28,19 +29,136 @@
|
||||
}
|
||||
|
||||
.order-summary {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.price-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.total-price {
|
||||
font-size: 1.2em;
|
||||
.price-row.total-price {
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
color: #e74c3c;
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.form-check {
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-check:hover {
|
||||
border-color: #007bff;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.form-check-input:checked + .form-check-label {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
/* 支付方式特殊样式 */
|
||||
.form-check input[type="radio"][value="simulate"]:checked + label {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.form-check input[type="radio"][value="simulate"]:checked + label i {
|
||||
color: #ffc107 !important;
|
||||
}
|
||||
|
||||
/* 模拟支付说明样式 */
|
||||
.alert-warning {
|
||||
border-left: 4px solid #ffc107;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.checkout-section .col-md-4,
|
||||
.checkout-section .col-md-3 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.address-card {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.product-item .col-md-2,
|
||||
.product-item .col-md-6 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.alert {
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.btn-lg {
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* 卡片头部样式 */
|
||||
.card-header h5 {
|
||||
margin-bottom: 0;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.card-header i {
|
||||
margin-right: 0.5rem;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
/* 表单标签样式 */
|
||||
.form-check-label {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-check-label strong {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.form-check-label small {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* 商品图片样式 */
|
||||
.product-item img {
|
||||
max-height: 80px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* 价格显示样式 */
|
||||
.fw-bold {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
/* 面包屑导航样式 */
|
||||
.breadcrumb {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
118
app/static/css/favorites.css
Normal file
118
app/static/css/favorites.css
Normal 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
125
app/static/css/history.css
Normal 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;
|
||||
}
|
||||
}
|
@ -17,9 +17,34 @@
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* 欢迎横幅样式 */
|
||||
/* 欢迎横幅样式 */
|
||||
.jumbotron {
|
||||
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);
|
||||
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%) !important;
|
||||
color: white !important;
|
||||
border-radius: 0.5rem !important;
|
||||
}
|
||||
|
||||
.jumbotron h1 {
|
||||
color: white !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
.jumbotron p {
|
||||
color: white !important;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.jumbotron .btn-light {
|
||||
background-color: white !important;
|
||||
color: #007bff !important;
|
||||
border: none !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
.jumbotron .btn-light:hover {
|
||||
background-color: #f8f9fa !important;
|
||||
color: #0056b3 !important;
|
||||
}
|
||||
|
||||
/* 商品图片样式 */
|
||||
|
@ -1,4 +1,26 @@
|
||||
/* 订单详情页面样式 */
|
||||
|
||||
/* 首先,重置所有可能影响的样式 */
|
||||
.order-detail-card .product-item img {
|
||||
all: unset !important;
|
||||
}
|
||||
|
||||
/* 然后重新定义我们需要的样式 */
|
||||
.order-detail-card .product-item img {
|
||||
width: 80px !important;
|
||||
height: 80px !important;
|
||||
max-width: 80px !important;
|
||||
max-height: 80px !important;
|
||||
min-width: 80px !important;
|
||||
min-height: 80px !important;
|
||||
object-fit: cover !important;
|
||||
border-radius: 4px !important;
|
||||
border: 1px solid #ddd !important;
|
||||
display: block !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
/* 订单状态时间线 */
|
||||
.order-status-timeline {
|
||||
position: relative;
|
||||
padding-left: 30px;
|
||||
@ -60,13 +82,6 @@
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@ -78,3 +93,15 @@
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.order-detail-card .product-item img {
|
||||
width: 60px !important;
|
||||
height: 60px !important;
|
||||
max-width: 60px !important;
|
||||
max-height: 60px !important;
|
||||
min-width: 60px !important;
|
||||
min-height: 60px !important;
|
||||
}
|
||||
}
|
||||
|
@ -1,49 +1,134 @@
|
||||
/* 订单支付页面样式 */
|
||||
/* 支付页面样式 */
|
||||
.pay-container {
|
||||
max-width: 600px;
|
||||
margin: 50px auto;
|
||||
margin: 2rem auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.order-info {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
background: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.payment-method {
|
||||
border: 2px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.payment-method:hover {
|
||||
border-color: #007bff;
|
||||
background-color: #f8f9ff;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.payment-method.selected {
|
||||
border-color: #007bff;
|
||||
background-color: #f8f9ff;
|
||||
background-color: #e7f3ff;
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.countdown {
|
||||
font-size: 1.2em;
|
||||
color: #e74c3c;
|
||||
font-weight: bold;
|
||||
padding: 2rem;
|
||||
background: #fff;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.5rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.payment-status {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
padding: 3rem 1rem;
|
||||
}
|
||||
|
||||
.countdown {
|
||||
font-weight: bold;
|
||||
color: #dc3545;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.simulate-panel {
|
||||
background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
|
||||
border: 2px solid #ffc107 !important;
|
||||
}
|
||||
|
||||
.simulate-panel h6 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.simulate-panel .btn {
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.payment-tips {
|
||||
background: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
|
||||
.payment-tips ul {
|
||||
margin-bottom: 0;
|
||||
padding-left: 1.2rem;
|
||||
}
|
||||
|
||||
.payment-tips li {
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.pay-container {
|
||||
margin: 1rem auto;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.d-md-flex .btn {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.simulate-panel .btn {
|
||||
min-width: auto;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
.payment-status i {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 按钮状态 */
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 支付方式图标 */
|
||||
.payment-method i {
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
/* 倒计时样式 */
|
||||
.countdown {
|
||||
background: #fff;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid #dc3545;
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
/* 商品详情页样式 */
|
||||
|
||||
.product-card {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
@ -55,6 +57,108 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 商品主图轮播样式修复 */
|
||||
.carousel-inner img {
|
||||
/* 重置Bootstrap图片样式 */
|
||||
all: unset !important;
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
height: 400px !important;
|
||||
object-fit: cover !important;
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
/* 缩略图样式修复 */
|
||||
.thumbnail-image {
|
||||
/* 重置Bootstrap图片样式 */
|
||||
all: unset !important;
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
height: 80px !important;
|
||||
object-fit: cover !important;
|
||||
cursor: pointer !important;
|
||||
border-radius: 4px !important;
|
||||
border: 2px solid #dee2e6 !important;
|
||||
transition: all 0.2s ease !important;
|
||||
}
|
||||
|
||||
.thumbnail-image:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
/* 推荐商品图片样式修复 */
|
||||
.product-card .card-img-top {
|
||||
/* 重置Bootstrap图片样式 */
|
||||
all: unset !important;
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
height: 200px !important;
|
||||
object-fit: cover !important;
|
||||
border-top-left-radius: 0.375rem !important;
|
||||
border-top-right-radius: 0.375rem !important;
|
||||
}
|
||||
|
||||
/* 商品详情标签页内的图片样式 */
|
||||
.tab-content img {
|
||||
/* 确保标签页内的图片不会过大 */
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
border-radius: 4px !important;
|
||||
border: 1px solid #dee2e6 !important;
|
||||
}
|
||||
|
||||
/* 评价图片在商品详情页中的特殊样式 */
|
||||
.reviews-section img {
|
||||
/* 重置评价图片样式 */
|
||||
all: unset !important;
|
||||
display: inline-block !important;
|
||||
max-width: 80px !important;
|
||||
max-height: 80px !important;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
object-fit: cover !important;
|
||||
border-radius: 6px !important;
|
||||
border: 1px solid #dee2e6 !important;
|
||||
cursor: pointer !important;
|
||||
transition: all 0.2s ease !important;
|
||||
margin-right: 8px !important;
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
.reviews-section img:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
/* 用户头像图片样式 */
|
||||
.reviewer-avatar {
|
||||
/* 重置用户头像样式 */
|
||||
all: unset !important;
|
||||
display: block !important;
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
border-radius: 50% !important;
|
||||
object-fit: cover !important;
|
||||
border: 2px solid #e9ecef !important;
|
||||
}
|
||||
|
||||
/* 图片模态框样式 */
|
||||
.modal-body img {
|
||||
/* 模态框中的图片样式 */
|
||||
all: unset !important;
|
||||
display: block !important;
|
||||
max-width: 100% !important;
|
||||
max-height: 80vh !important;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
margin: 0 auto !important;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important;
|
||||
}
|
||||
|
||||
/* 响应式优化 */
|
||||
@media (max-width: 768px) {
|
||||
.price-section {
|
||||
@ -68,4 +172,30 @@
|
||||
.action-buttons .btn {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.carousel-inner img {
|
||||
height: 300px !important;
|
||||
}
|
||||
|
||||
.thumbnail-image {
|
||||
height: 60px !important;
|
||||
}
|
||||
|
||||
.reviews-section img {
|
||||
max-width: 60px !important;
|
||||
max-height: 60px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 无图片占位符样式 */
|
||||
.bg-light.d-flex {
|
||||
background-color: #f8f9fa !important;
|
||||
border: 2px dashed #dee2e6 !important;
|
||||
}
|
||||
|
||||
/* 确保所有图片都有基础的重置样式 */
|
||||
.product-detail img:not(.reviewer-avatar):not(.thumbnail-image):not(.card-img-top) {
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
626
app/static/css/review.css
Normal file
626
app/static/css/review.css
Normal 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
350
app/static/js/admin_logs.js
Normal 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;
|
||||
}
|
||||
|
||||
// 实现批量操作逻辑
|
||||
}
|
201
app/static/js/admin_orders.js
Normal file
201
app/static/js/admin_orders.js
Normal 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);
|
||||
}
|
370
app/static/js/admin_users.js
Normal file
370
app/static/js/admin_users.js
Normal 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);
|
@ -3,10 +3,12 @@
|
||||
// 返回顶部功能
|
||||
window.addEventListener('scroll', function() {
|
||||
const backToTop = document.getElementById('backToTop');
|
||||
if (window.pageYOffset > 300) {
|
||||
backToTop.style.display = 'block';
|
||||
} else {
|
||||
backToTop.style.display = 'none';
|
||||
if (backToTop) {
|
||||
if (window.pageYOffset > 300) {
|
||||
backToTop.style.display = 'block';
|
||||
} else {
|
||||
backToTop.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -20,14 +22,96 @@ function scrollToTop() {
|
||||
// 购物车数量更新
|
||||
function updateCartBadge(count) {
|
||||
const badge = document.getElementById('cartBadge');
|
||||
if (count > 0) {
|
||||
badge.textContent = count;
|
||||
badge.style.display = 'inline-block';
|
||||
} else {
|
||||
badge.style.display = 'none';
|
||||
if (badge) {
|
||||
if (count > 0) {
|
||||
badge.textContent = count;
|
||||
badge.style.display = 'inline-block';
|
||||
} else {
|
||||
badge.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 通用提示函数
|
||||
function showAlert(message, type = 'info', duration = 3000) {
|
||||
// 移除现有的提示框
|
||||
const existingAlerts = document.querySelectorAll('.custom-alert');
|
||||
existingAlerts.forEach(alert => alert.remove());
|
||||
|
||||
// 创建新的提示框
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = `alert alert-${getBootstrapAlertType(type)} alert-dismissible fade show custom-alert`;
|
||||
alertDiv.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
min-width: 300px;
|
||||
max-width: 500px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
`;
|
||||
|
||||
const icon = getAlertIcon(type);
|
||||
alertDiv.innerHTML = `
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi ${icon} me-2"></i>
|
||||
<div class="flex-grow-1">${message}</div>
|
||||
<button type="button" class="btn-close" onclick="this.parentElement.parentElement.remove()"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(alertDiv);
|
||||
|
||||
// 自动消失
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
if (alertDiv.parentNode) {
|
||||
alertDiv.classList.remove('show');
|
||||
setTimeout(() => {
|
||||
if (alertDiv.parentNode) {
|
||||
alertDiv.remove();
|
||||
}
|
||||
}, 150);
|
||||
}
|
||||
}, duration);
|
||||
}
|
||||
|
||||
return alertDiv;
|
||||
}
|
||||
|
||||
// 获取Bootstrap警告类型
|
||||
function getBootstrapAlertType(type) {
|
||||
const typeMap = {
|
||||
'success': 'success',
|
||||
'error': 'danger',
|
||||
'warning': 'warning',
|
||||
'info': 'info'
|
||||
};
|
||||
return typeMap[type] || 'info';
|
||||
}
|
||||
|
||||
// 获取警告图标
|
||||
function getAlertIcon(type) {
|
||||
const iconMap = {
|
||||
'success': 'bi-check-circle-fill',
|
||||
'error': 'bi-exclamation-triangle-fill',
|
||||
'warning': 'bi-exclamation-triangle-fill',
|
||||
'info': 'bi-info-circle-fill'
|
||||
};
|
||||
return iconMap[type] || 'bi-info-circle-fill';
|
||||
}
|
||||
|
||||
// 确认对话框
|
||||
function showConfirm(message, callback) {
|
||||
if (confirm(message)) {
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 页面加载完成后的初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 当前页面高亮
|
||||
@ -41,36 +125,79 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
|
||||
// 初始化购物车数量
|
||||
// TODO: 实现购物车数量获取
|
||||
updateCartCount();
|
||||
});
|
||||
|
||||
// 通用AJAX错误处理
|
||||
function handleAjaxError(xhr) {
|
||||
function handleAjaxError(xhr, defaultMessage = '操作失败,请稍后再试') {
|
||||
if (xhr.status === 401) {
|
||||
alert('请先登录');
|
||||
window.location.href = '/auth/login';
|
||||
showAlert('请先登录', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/auth/login';
|
||||
}, 1500);
|
||||
} else if (xhr.status === 403) {
|
||||
alert('没有权限执行此操作');
|
||||
showAlert('没有权限执行此操作', 'error');
|
||||
} else if (xhr.status === 404) {
|
||||
showAlert('请求的资源不存在', 'error');
|
||||
} else if (xhr.status >= 500) {
|
||||
showAlert('服务器错误,请稍后再试', 'error');
|
||||
} else {
|
||||
alert('操作失败,请稍后再试');
|
||||
showAlert(defaultMessage, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 通用成功提示
|
||||
// 通用成功提示(保持向后兼容)
|
||||
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(() => {
|
||||
if (alertDiv.parentNode) {
|
||||
alertDiv.parentNode.removeChild(alertDiv);
|
||||
}
|
||||
}, 3000);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,15 +26,24 @@ function selectAddress(addressId) {
|
||||
card.classList.remove('selected');
|
||||
});
|
||||
|
||||
document.querySelector(`[data-address-id="${addressId}"]`).classList.add('selected');
|
||||
const selectedCard = document.querySelector(`[data-address-id="${addressId}"]`);
|
||||
if (selectedCard) {
|
||||
selectedCard.classList.add('selected');
|
||||
}
|
||||
|
||||
// 更新单选按钮
|
||||
document.querySelector(`input[value="${addressId}"]`).checked = true;
|
||||
const radioButton = document.querySelector(`input[value="${addressId}"]`);
|
||||
if (radioButton) {
|
||||
radioButton.checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新运费
|
||||
function updateShippingFee() {
|
||||
const shippingMethod = document.querySelector('input[name="shipping_method"]:checked').value;
|
||||
const shippingMethodElement = document.querySelector('input[name="shipping_method"]:checked');
|
||||
if (!shippingMethodElement) return;
|
||||
|
||||
const shippingMethod = shippingMethodElement.value;
|
||||
let fee = 0;
|
||||
|
||||
switch(shippingMethod) {
|
||||
@ -48,20 +57,44 @@ function updateShippingFee() {
|
||||
fee = 0;
|
||||
}
|
||||
|
||||
document.getElementById('shippingFee').textContent = `¥${fee.toFixed(2)}`;
|
||||
document.getElementById('totalAmount').textContent = `¥${(subtotal + fee).toFixed(2)}`;
|
||||
const shippingFeeElement = document.getElementById('shippingFee');
|
||||
const totalAmountElement = document.getElementById('totalAmount');
|
||||
|
||||
if (shippingFeeElement) {
|
||||
shippingFeeElement.textContent = `¥${fee.toFixed(2)}`;
|
||||
}
|
||||
|
||||
if (totalAmountElement) {
|
||||
totalAmountElement.textContent = `¥${(subtotal + fee).toFixed(2)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 提交订单
|
||||
function submitOrder() {
|
||||
// 验证地址选择
|
||||
if (!selectedAddressId) {
|
||||
showAlert('请选择收货地址', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const shippingMethod = document.querySelector('input[name="shipping_method"]:checked').value;
|
||||
const paymentMethod = document.querySelector('input[name="payment_method"]:checked').value;
|
||||
const remark = document.getElementById('orderRemark').value;
|
||||
// 获取表单数据
|
||||
const shippingMethodElement = document.querySelector('input[name="shipping_method"]:checked');
|
||||
const paymentMethodElement = document.querySelector('input[name="payment_method"]:checked');
|
||||
const remarkElement = document.getElementById('orderRemark');
|
||||
|
||||
if (!shippingMethodElement) {
|
||||
showAlert('请选择配送方式', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!paymentMethodElement) {
|
||||
showAlert('请选择支付方式', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const shippingMethod = shippingMethodElement.value;
|
||||
const paymentMethod = paymentMethodElement.value;
|
||||
const remark = remarkElement ? remarkElement.value : '';
|
||||
|
||||
// 获取选中的购物车商品ID
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
@ -82,10 +115,16 @@ function submitOrder() {
|
||||
|
||||
// 显示加载状态
|
||||
const submitBtn = document.querySelector('.btn-danger');
|
||||
if (!submitBtn) {
|
||||
showAlert('提交按钮未找到', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const originalText = submitBtn.innerHTML;
|
||||
submitBtn.innerHTML = '<i class="bi bi-hourglass-split"></i> 提交中...';
|
||||
submitBtn.disabled = true;
|
||||
|
||||
// 提交订单
|
||||
fetch('/order/create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@ -93,7 +132,12 @@ function submitOrder() {
|
||||
},
|
||||
body: JSON.stringify(orderData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showAlert('订单创建成功!正在跳转到支付页面...', 'success');
|
||||
@ -101,13 +145,16 @@ function submitOrder() {
|
||||
window.location.href = `/order/pay/${data.payment_sn}`;
|
||||
}, 1500);
|
||||
} else {
|
||||
showAlert(data.message, 'error');
|
||||
showAlert(data.message || '订单创建失败', 'error');
|
||||
// 恢复按钮状态
|
||||
submitBtn.innerHTML = originalText;
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('提交订单错误:', error);
|
||||
showAlert('提交订单失败,请重试', 'error');
|
||||
// 恢复按钮状态
|
||||
submitBtn.innerHTML = originalText;
|
||||
submitBtn.disabled = false;
|
||||
});
|
||||
|
229
app/static/js/favorites.js
Normal file
229
app/static/js/favorites.js
Normal 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
278
app/static/js/history.js
Normal 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);
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
// 订单详情页面脚本
|
||||
// 订单详情页面脚本 - 只处理业务逻辑,不处理样式
|
||||
|
||||
// 取消订单
|
||||
function cancelOrder(orderId) {
|
||||
|
@ -16,13 +16,16 @@ window.addEventListener('beforeunload', function() {
|
||||
|
||||
// 开始倒计时
|
||||
function startCountdown() {
|
||||
const countdownElement = document.getElementById('countdown');
|
||||
if (!countdownElement) return;
|
||||
|
||||
countdownTimer = setInterval(() => {
|
||||
timeLeft--;
|
||||
|
||||
const minutes = Math.floor(timeLeft / 60);
|
||||
const seconds = timeLeft % 60;
|
||||
|
||||
document.getElementById('countdown').textContent =
|
||||
countdownElement.textContent =
|
||||
`${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
|
||||
if (timeLeft <= 0) {
|
||||
@ -45,6 +48,12 @@ function startPayment() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是模拟支付,直接显示控制面板,不需要调用接口
|
||||
if (paymentMethod === 'simulate') {
|
||||
showAlert('请使用下方控制面板完成模拟支付', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/payment/process', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@ -55,7 +64,12 @@ function startPayment() {
|
||||
payment_method: paymentMethod
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
if (data.payment_type === 'qrcode') {
|
||||
@ -64,12 +78,15 @@ function startPayment() {
|
||||
} else if (data.payment_type === 'redirect') {
|
||||
window.open(data.pay_url, '_blank');
|
||||
startStatusCheck();
|
||||
} else if (data.payment_type === 'simulate') {
|
||||
showAlert('模拟支付已准备就绪', 'success');
|
||||
}
|
||||
} else {
|
||||
showAlert(data.message, 'error');
|
||||
showAlert(data.message || '支付启动失败', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('支付启动错误:', error);
|
||||
showAlert('支付启动失败,请重试', 'error');
|
||||
});
|
||||
}
|
||||
@ -79,6 +96,8 @@ function showQRCode(qrUrl) {
|
||||
const qrArea = document.getElementById('qrCodeArea');
|
||||
const qrImage = document.getElementById('qrCodeImage');
|
||||
|
||||
if (!qrArea || !qrImage) return;
|
||||
|
||||
// 这里应该使用真实的二维码生成库,现在用文本模拟
|
||||
qrImage.innerHTML = `
|
||||
<div style="width: 200px; height: 200px; margin: 0 auto; background: #f0f0f0;
|
||||
@ -107,13 +126,21 @@ function checkPaymentStatus() {
|
||||
if (!paymentSn) return;
|
||||
|
||||
fetch(`/payment/check_status/${paymentSn}`)
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
if (data.status === 2) { // 支付成功
|
||||
clearInterval(statusCheckTimer);
|
||||
clearInterval(countdownTimer);
|
||||
showPaymentSuccess();
|
||||
} else if (data.status === 3) { // 支付失败
|
||||
clearInterval(statusCheckTimer);
|
||||
showPaymentFail();
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -124,15 +151,47 @@ function checkPaymentStatus() {
|
||||
|
||||
// 显示支付成功
|
||||
function showPaymentSuccess() {
|
||||
document.getElementById('paymentArea').style.display = 'none';
|
||||
document.getElementById('paymentStatus').style.display = 'block';
|
||||
const paymentArea = document.getElementById('paymentArea');
|
||||
const actionButtons = document.getElementById('actionButtons');
|
||||
const paymentStatus = document.getElementById('paymentStatus');
|
||||
const successStatus = document.getElementById('successStatus');
|
||||
|
||||
if (paymentArea) paymentArea.style.display = 'none';
|
||||
if (actionButtons) actionButtons.style.display = 'none';
|
||||
if (paymentStatus) paymentStatus.style.display = 'block';
|
||||
if (successStatus) successStatus.style.display = 'block';
|
||||
|
||||
showAlert('支付成功!正在跳转到订单详情...', 'success');
|
||||
|
||||
const orderId = document.querySelector('[data-order-id]')?.dataset.orderId;
|
||||
setTimeout(() => {
|
||||
window.location.href = `/order/detail/${orderId}`;
|
||||
if (orderId) {
|
||||
window.location.href = `/order/detail/${orderId}`;
|
||||
} else {
|
||||
window.location.href = '/order/list';
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// 显示支付失败
|
||||
function showPaymentFail() {
|
||||
const paymentArea = document.getElementById('paymentArea');
|
||||
const paymentStatus = document.getElementById('paymentStatus');
|
||||
const failStatus = document.getElementById('failStatus');
|
||||
|
||||
if (paymentArea) paymentArea.style.display = 'none';
|
||||
if (paymentStatus) paymentStatus.style.display = 'block';
|
||||
if (failStatus) failStatus.style.display = 'block';
|
||||
|
||||
showAlert('支付失败,请重新尝试', 'error');
|
||||
|
||||
// 显示重试按钮
|
||||
setTimeout(() => {
|
||||
if (paymentArea) paymentArea.style.display = 'block';
|
||||
if (paymentStatus) paymentStatus.style.display = 'none';
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 取消订单
|
||||
function cancelOrder() {
|
||||
const orderId = document.querySelector('[data-order-id]')?.dataset.orderId;
|
||||
@ -142,32 +201,38 @@ function cancelOrder() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirm('确定要取消这个订单吗?')) {
|
||||
showConfirm('确定要取消这个订单吗?', () => {
|
||||
fetch(`/order/cancel/${orderId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showAlert('订单已取消', 'success');
|
||||
showAlert('订单已取消,正在跳转...', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/order/list';
|
||||
}, 1500);
|
||||
} else {
|
||||
showAlert(data.message, 'error');
|
||||
showAlert(data.message || '取消订单失败', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('取消订单错误:', error);
|
||||
showAlert('取消失败,请重试', 'error');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 模拟支付成功(开发测试用)
|
||||
function simulatePayment() {
|
||||
// 模拟支付成功
|
||||
function simulatePaymentSuccess() {
|
||||
const paymentSn = document.querySelector('[data-payment-sn]')?.dataset.paymentSn;
|
||||
|
||||
if (!paymentSn) {
|
||||
@ -175,26 +240,94 @@ function simulatePayment() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirm('这是测试功能,确定要模拟支付成功吗?')) {
|
||||
fetch(`/payment/simulate_success/${paymentSn}`, {
|
||||
// 显示处理中状态
|
||||
const button = event.target;
|
||||
const originalText = button.innerHTML;
|
||||
button.innerHTML = '<i class="bi bi-hourglass-split"></i> 处理中...';
|
||||
button.disabled = true;
|
||||
|
||||
fetch(`/payment/simulate_success/${paymentSn}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showAlert('模拟支付成功!', 'success');
|
||||
setTimeout(() => {
|
||||
showPaymentSuccess();
|
||||
}, 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 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 => response.json())
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showAlert('模拟支付成功', 'success');
|
||||
showAlert('模拟支付失败!', 'warning');
|
||||
setTimeout(() => {
|
||||
showPaymentSuccess();
|
||||
showPaymentFail();
|
||||
}, 1000);
|
||||
} else {
|
||||
showAlert(data.message, 'error');
|
||||
showAlert(data.message || '模拟操作失败', 'error');
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showAlert('模拟支付失败', 'error');
|
||||
console.error('模拟支付失败错误:', error);
|
||||
showAlert('模拟操作失败', 'error');
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 兼容旧版本的模拟支付函数
|
||||
function simulatePayment() {
|
||||
simulatePaymentSuccess();
|
||||
}
|
||||
|
@ -22,8 +22,67 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
if (typeof loadCartCount === 'function') {
|
||||
loadCartCount();
|
||||
}
|
||||
|
||||
// 添加浏览历史记录
|
||||
if (window.isLoggedIn && window.productId) {
|
||||
addBrowseHistory(window.productId);
|
||||
}
|
||||
|
||||
// 检查收藏状态
|
||||
if (window.isLoggedIn && window.productId) {
|
||||
checkFavoriteStatus(window.productId);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加浏览历史记录
|
||||
function addBrowseHistory(productId) {
|
||||
fetch('/history/add', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
product_id: productId
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// 静默添加,不需要用户感知
|
||||
console.log('浏览历史记录已添加');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('添加浏览历史失败:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// 检查收藏状态
|
||||
function checkFavoriteStatus(productId) {
|
||||
fetch(`/favorite/check/${productId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
updateFavoriteButton(data.is_favorited);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('检查收藏状态失败:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新收藏按钮状态
|
||||
function updateFavoriteButton(isFavorited) {
|
||||
const favoriteBtn = document.querySelector('[onclick="addToFavorites()"]');
|
||||
if (favoriteBtn) {
|
||||
if (isFavorited) {
|
||||
favoriteBtn.innerHTML = '<i class="bi bi-heart-fill text-danger"></i> 已收藏';
|
||||
favoriteBtn.className = 'btn btn-outline-danger';
|
||||
} else {
|
||||
favoriteBtn.innerHTML = '<i class="bi bi-heart"></i> 收藏商品';
|
||||
favoriteBtn.className = 'btn btn-outline-secondary';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 规格选择
|
||||
function selectSpec(button) {
|
||||
const specName = button.getAttribute('data-spec-name');
|
||||
@ -231,6 +290,68 @@ function buyNow() {
|
||||
|
||||
// 收藏商品
|
||||
function addToFavorites() {
|
||||
// TODO: 实现收藏功能
|
||||
alert('收藏功能开发中...');
|
||||
if (!window.isLoggedIn) {
|
||||
if (confirm('请先登录后再收藏,是否前往登录?')) {
|
||||
window.location.href = '/auth/login?next=' + encodeURIComponent(window.location.pathname);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保获取到商品ID
|
||||
const productId = window.productId || window.currentProductId;
|
||||
if (!productId) {
|
||||
alert('获取商品信息失败,请刷新页面重试');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('收藏商品ID:', productId); // 调试信息
|
||||
|
||||
const favoriteBtn = document.querySelector('[onclick="addToFavorites()"]');
|
||||
const isFavorited = favoriteBtn && favoriteBtn.innerHTML.includes('已收藏');
|
||||
|
||||
// 临时禁用按钮
|
||||
if (favoriteBtn) {
|
||||
favoriteBtn.disabled = true;
|
||||
favoriteBtn.innerHTML = '<i class="bi bi-hourglass-split"></i> 处理中...';
|
||||
}
|
||||
|
||||
fetch('/favorite/toggle', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
product_id: parseInt(productId)
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showSuccessMessage(data.message);
|
||||
updateFavoriteButton(data.is_favorited);
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('操作失败,请稍后再试');
|
||||
})
|
||||
.finally(() => {
|
||||
// 恢复按钮状态
|
||||
if (favoriteBtn) {
|
||||
favoriteBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 显示成功消息
|
||||
function showSuccessMessage(message) {
|
||||
// 这里可以使用Toast或其他方式显示消息
|
||||
if (typeof showToast === 'function') {
|
||||
showToast(message, 'success');
|
||||
} else {
|
||||
// 简单的成功提示
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
646
app/static/js/review.js
Normal file
646
app/static/js/review.js
Normal 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;
|
@ -45,8 +45,8 @@
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'admin.orders' %}active{% endif %}"
|
||||
href="#">
|
||||
<a class="nav-link {% if request.endpoint.startswith('admin.order') %}active{% endif %}"
|
||||
href="{{ url_for('admin.orders') }}">
|
||||
<i class="bi bi-receipt"></i>
|
||||
订单管理
|
||||
</a>
|
||||
|
245
app/templates/admin/logs.html
Normal file
245
app/templates/admin/logs.html
Normal 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"> </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 %}
|
426
app/templates/admin/order_detail.html
Normal file
426
app/templates/admin/order_detail.html
Normal 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 %}
|
@ -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"> </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 %}
|
@ -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"> </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 %}
|
@ -4,10 +4,14 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}太白购物商城{% endblock %}</title>
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<link href="{{ url_for('static', filename='css/base.css') }}" rel="stylesheet">
|
||||
{% block styles %}{% endblock %}
|
||||
|
||||
<!-- 自定义CSS -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/base.css') }}">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<!-- 导航栏 -->
|
||||
@ -29,23 +33,12 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('main.product_list') }}">全部商品</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownCategory" role="button" data-bs-toggle="dropdown">
|
||||
商品分类
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="{{ url_for('main.product_list') }}">全部分类</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('main.product_list', category_id=1) }}">手机数码</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('main.product_list', category_id=2) }}">电脑办公</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('main.product_list', category_id=3) }}">家居家装</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<form class="d-flex me-3 search-form" method="GET" action="{{ url_for('main.product_list') }}">
|
||||
<input class="form-control me-2" type="search" name="search" placeholder="搜索商品..." style="min-width: 200px;">
|
||||
<form class="d-flex me-3" method="GET" action="{{ url_for('main.product_list') }}">
|
||||
<input class="form-control me-2" type="search" name="search" placeholder="搜索商品..."
|
||||
value="{{ request.args.get('search', '') }}">
|
||||
<button class="btn btn-outline-primary" type="submit">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
@ -54,7 +47,7 @@
|
||||
<ul class="navbar-nav">
|
||||
{% if session.user_id %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link position-relative" href="{{ url_for('cart.index') }}" title="购物车">
|
||||
<a class="nav-link position-relative" href="{{ url_for('cart.index') }}">
|
||||
<i class="bi bi-cart"></i> 购物车
|
||||
<span class="badge bg-danger cart-badge" id="cartBadge" style="display: none;">0</span>
|
||||
</a>
|
||||
@ -66,7 +59,8 @@
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="{{ url_for('user.profile') }}">个人中心</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('user.orders') }}">我的订单</a></li>
|
||||
<li><a class="dropdown-item" href="#">我的收藏</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('favorite.index') }}">我的收藏</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('history.index') }}">浏览历史</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">退出登录</a></li>
|
||||
</ul>
|
||||
@ -84,77 +78,50 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="container mt-3">
|
||||
<!-- 主要内容 -->
|
||||
<main class="container mt-4">
|
||||
<!-- Flash消息提示 -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<main class="container mt-4">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<footer class="footer mt-auto">
|
||||
<footer class="bg-light py-4 mt-5">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5>太白购物商城</h5>
|
||||
<p class="text-muted">您的购物首选平台</p>
|
||||
<div class="mb-3">
|
||||
<a href="#" class="text-muted me-3"><i class="bi bi-telephone"></i> 客服热线:400-888-8888</a>
|
||||
<a href="#" class="text-muted"><i class="bi bi-envelope"></i> service@taibai.com</a>
|
||||
</div>
|
||||
<p class="text-muted">您身边的购物专家</p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<h6>快捷导航</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="{{ url_for('main.index') }}" class="text-muted">首页</a></li>
|
||||
<li><a href="{{ url_for('main.product_list') }}" class="text-muted">全部商品</a></li>
|
||||
<li><a href="#" class="text-muted">关于我们</a></li>
|
||||
<li><a href="#" class="text-muted">联系我们</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<h6>客户服务</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="#" class="text-muted">帮助中心</a></li>
|
||||
<li><a href="#" class="text-muted">售后服务</a></li>
|
||||
<li><a href="#" class="text-muted">配送说明</a></li>
|
||||
<li><a href="#" class="text-muted">退换货政策</a></li>
|
||||
</ul>
|
||||
<div class="col-md-6">
|
||||
<h6>联系我们</h6>
|
||||
<p class="text-muted">
|
||||
<i class="bi bi-envelope"></i> service@taibai-mall.com<br>
|
||||
<i class="bi bi-telephone"></i> 400-888-8888
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p class="text-muted small mb-0">
|
||||
<i class="bi bi-shield-check"></i>
|
||||
正品保证 | 7天无理由退换 | 全国包邮
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-md-end">
|
||||
<p class="text-muted small mb-0">© 2025 太白购物商城. All rights reserved.</p>
|
||||
</div>
|
||||
<div class="text-center text-muted">
|
||||
<small>© 2025 太白购物商城. 保留所有权利.</small>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- 返回顶部按钮 -->
|
||||
<button type="button" class="btn btn-primary position-fixed bottom-0 end-0 m-3" id="backToTop" onclick="scrollToTop()">
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
</button>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- 自定义JavaScript -->
|
||||
<script src="{{ url_for('static', filename='js/base.js') }}"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
{% block title %}首页 - 太白购物商城{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
{% block head %}
|
||||
<link href="{{ url_for('static', filename='css/index.css') }}" rel="stylesheet">
|
||||
{% endblock %}
|
||||
|
||||
|
@ -133,7 +133,7 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="payment_method"
|
||||
value="wechat" id="payment_wechat" checked>
|
||||
@ -143,7 +143,7 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="payment_method"
|
||||
value="alipay" id="payment_alipay">
|
||||
@ -153,7 +153,7 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="payment_method"
|
||||
value="bank" id="payment_bank">
|
||||
@ -163,6 +163,25 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="payment_method"
|
||||
value="simulate" id="payment_simulate">
|
||||
<label class="form-check-label" for="payment_simulate">
|
||||
<i class="bi bi-gear-fill text-warning me-2"></i>
|
||||
<strong>模拟支付</strong>
|
||||
<br><small class="text-muted">测试模式</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模拟支付说明 -->
|
||||
<div class="alert alert-warning mt-3" id="simulatePaymentNotice" style="display: none;">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||
<strong>模拟支付模式</strong><br>
|
||||
这是开发测试功能,选择此支付方式后可以直接模拟支付成功或失败,无需真实付款。
|
||||
实际生产环境中,此选项将被移除。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -221,4 +240,21 @@
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/checkout.js') }}"></script>
|
||||
<script>
|
||||
// 监听支付方式变化,显示/隐藏模拟支付说明
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const paymentMethods = document.querySelectorAll('input[name="payment_method"]');
|
||||
const simulateNotice = document.getElementById('simulatePaymentNotice');
|
||||
|
||||
paymentMethods.forEach(method => {
|
||||
method.addEventListener('change', function() {
|
||||
if (this.value === 'simulate') {
|
||||
simulateNotice.style.display = 'block';
|
||||
} else {
|
||||
simulateNotice.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -1,10 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}订单详情 - 太白购物商城{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/order_detail.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
@ -236,7 +232,27 @@
|
||||
{% endif %}
|
||||
|
||||
{% if order.status == 4 %}
|
||||
<a href="#" class="btn btn-outline-warning">评价商品</a>
|
||||
<!-- 评价商品按钮,根据商品数量展示 -->
|
||||
{% if order.order_items|length == 1 %}
|
||||
<a href="{{ url_for('review.write_review', order_id=order.id, product_id=order.order_items[0].product_id) }}"
|
||||
class="btn btn-outline-warning">评价商品</a>
|
||||
{% else %}
|
||||
<div class="dropdown d-grid">
|
||||
<button class="btn btn-outline-warning dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
评价商品
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
{% for item in order.order_items %}
|
||||
<li>
|
||||
<a class="dropdown-item"
|
||||
href="{{ url_for('review.write_review', order_id=order.id, product_id=item.product_id) }}">
|
||||
{{ item.product_name[:20] }}{% if item.product_name|length > 20 %}...{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ url_for('order.list') }}" class="btn btn-outline-primary">
|
||||
@ -246,6 +262,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 重要:确保order_detail.css在最后加载 -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/order_detail.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
|
@ -31,7 +31,13 @@
|
||||
</div>
|
||||
<div class="row mt-2">
|
||||
<div class="col-6">
|
||||
<strong>支付方式:</strong>{{ order.payment_method }}
|
||||
<strong>支付方式:</strong>
|
||||
{% if order.payment_method == 'wechat' %}微信支付
|
||||
{% elif order.payment_method == 'alipay' %}支付宝
|
||||
{% elif order.payment_method == 'bank' %}银行卡支付
|
||||
{% elif order.payment_method == 'simulate' %}模拟支付
|
||||
{% else %}{{ order.payment_method }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-6 text-end">
|
||||
<span class="countdown" id="countdown">14:59</span>
|
||||
@ -84,17 +90,59 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if order.payment_method == 'simulate' %}
|
||||
<div class="payment-method selected">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi bi-gear-fill text-warning fs-1 me-3"></i>
|
||||
<div>
|
||||
<h6>模拟支付</h6>
|
||||
<p class="text-muted mb-0">开发测试模式,可直接完成支付</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模拟支付控制面板 -->
|
||||
<div class="simulate-panel mt-3 p-3 border rounded bg-light">
|
||||
<h6 class="text-warning"><i class="bi bi-exclamation-triangle"></i> 模拟支付控制面板</h6>
|
||||
<p class="text-muted small mb-3">这是开发测试功能,您可以模拟不同的支付结果</p>
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
|
||||
<button class="btn btn-success" onclick="simulatePaymentSuccess()">
|
||||
<i class="bi bi-check-circle"></i> 模拟支付成功
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="simulatePaymentFail()">
|
||||
<i class="bi bi-x-circle"></i> 模拟支付失败
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-center">
|
||||
<small class="text-muted">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
实际生产环境中,此面板将被真实支付接口替代
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- 支付状态 -->
|
||||
<div class="payment-status" id="paymentStatus" style="display: none;">
|
||||
<i class="bi bi-check-circle-fill text-success display-1"></i>
|
||||
<h5 class="mt-3">支付成功</h5>
|
||||
<p class="text-muted">正在跳转到订单详情...</p>
|
||||
<div id="successStatus" style="display: none;">
|
||||
<i class="bi bi-check-circle-fill text-success display-1"></i>
|
||||
<h5 class="mt-3 text-success">支付成功</h5>
|
||||
<p class="text-muted">正在跳转到订单详情...</p>
|
||||
</div>
|
||||
|
||||
<div id="failStatus" style="display: none;">
|
||||
<i class="bi bi-x-circle-fill text-danger display-1"></i>
|
||||
<h5 class="mt-3 text-danger">支付失败</h5>
|
||||
<p class="text-muted">请重新选择支付方式或联系客服</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<div class="d-flex gap-2 mt-4" id="actionButtons">
|
||||
<button class="btn btn-primary flex-fill" onclick="startPayment()">
|
||||
<i class="bi bi-credit-card"></i> 立即支付
|
||||
</button>
|
||||
@ -106,11 +154,17 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 开发测试按钮 -->
|
||||
<div class="mt-3 text-center">
|
||||
<button class="btn btn-warning btn-sm" onclick="simulatePayment()">
|
||||
<i class="bi bi-bug"></i> 模拟支付成功(测试用)
|
||||
</button>
|
||||
<!-- 支付说明 -->
|
||||
<div class="payment-tips mt-4">
|
||||
<h6 class="text-muted">支付说明:</h6>
|
||||
<ul class="text-muted small">
|
||||
<li>订单有效期为15分钟,请及时完成支付</li>
|
||||
<li>支付成功后,订单状态将自动更新</li>
|
||||
<li>如遇支付问题,请联系客服:400-123-4567</li>
|
||||
{% if order.payment_method == 'simulate' %}
|
||||
<li class="text-warning">当前为模拟支付模式,仅用于开发测试</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/product_detail.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/review.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@ -211,7 +212,8 @@
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="reviews-tab" data-bs-toggle="tab"
|
||||
data-bs-target="#reviews" type="button" role="tab">商品评价</button>
|
||||
data-bs-target="#reviews" type="button" role="tab"
|
||||
onclick="loadProductReviews({{ product.id }})">商品评价</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@ -283,7 +285,11 @@
|
||||
<div class="tab-pane fade" id="reviews" role="tabpanel">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p class="text-muted">评价功能开发中...</p>
|
||||
<div id="reviewsContainer">
|
||||
<div class="text-center p-4 text-muted">
|
||||
<i class="bi bi-star"></i> 点击标签页加载评价
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -343,12 +349,29 @@
|
||||
<script>
|
||||
// 设置全局变量供JS使用
|
||||
window.productId = {{ product.id }};
|
||||
window.currentProductId = {{ product.id }};
|
||||
window.isLoggedIn = {% if session.user_id %}true{% else %}false{% endif %};
|
||||
</script>
|
||||
|
||||
<!-- 图片查看模态框 -->
|
||||
<div class="modal fade" id="imageModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">查看图片</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<img id="modalImage" src="" class="img-fluid" alt="评价图片">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/product_detail.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/review.js') }}"></script>
|
||||
<script>
|
||||
// 处理登录状态检查
|
||||
{% if not session.user_id %}
|
||||
|
202
app/templates/review/my_reviews.html
Normal file
202
app/templates/review/my_reviews.html
Normal 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 %}
|
116
app/templates/review/write.html
Normal file
116
app/templates/review/write.html
Normal 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 %}
|
@ -23,10 +23,13 @@
|
||||
<a href="{{ url_for('address.index') }}" class="list-group-item list-group-item-action active">
|
||||
<i class="bi bi-geo-alt"></i> 收货地址
|
||||
</a>
|
||||
<a href="#" class="list-group-item list-group-item-action">
|
||||
<a href="{{ url_for('review.my_reviews') }}" class="list-group-item list-group-item-action">
|
||||
<i class="bi bi-star"></i> 我的评价
|
||||
</a>
|
||||
<a href="{{ url_for('favorite.index') }}" class="list-group-item list-group-item-action">
|
||||
<i class="bi bi-heart"></i> 我的收藏
|
||||
</a>
|
||||
<a href="#" class="list-group-item list-group-item-action">
|
||||
<a href="{{ url_for('history.index') }}" class="list-group-item list-group-item-action">
|
||||
<i class="bi bi-clock-history"></i> 浏览历史
|
||||
</a>
|
||||
</div>
|
||||
|
219
app/templates/user/favorites.html
Normal file
219
app/templates/user/favorites.html
Normal 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 %}
|
233
app/templates/user/history.html
Normal file
233
app/templates/user/history.html
Normal 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 %}
|
@ -17,12 +17,15 @@
|
||||
<a href="{{ url_for('user.profile') }}" class="list-group-item list-group-item-action">
|
||||
<i class="bi bi-person"></i> 基本信息
|
||||
</a>
|
||||
<a href="{{ url_for('user.orders') }}" class="list-group-item list-group-item-action active">
|
||||
<a href="{{ url_for('order.list') }}" class="list-group-item list-group-item-action active">
|
||||
<i class="bi bi-bag"></i> 我的订单
|
||||
</a>
|
||||
<a href="{{ url_for('address.index') }}" class="list-group-item list-group-item-action">
|
||||
<i class="bi bi-geo-alt"></i> 收货地址
|
||||
</a>
|
||||
<a href="{{ url_for('review.my_reviews') }}" class="list-group-item list-group-item-action">
|
||||
<i class="bi bi-star"></i> 我的评价
|
||||
</a>
|
||||
<a href="#" class="list-group-item list-group-item-action">
|
||||
<i class="bi bi-heart"></i> 我的收藏
|
||||
</a>
|
||||
@ -133,7 +136,7 @@
|
||||
<div class="order-footer">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex gap-2">
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
{% if order.can_pay() %}
|
||||
<a href="{{ url_for('order.pay', payment_sn=order.payments[0].payment_sn) }}"
|
||||
class="btn btn-danger btn-sm">立即支付</a>
|
||||
@ -150,7 +153,27 @@
|
||||
{% endif %}
|
||||
|
||||
{% if order.status == 4 %}
|
||||
<a href="#" class="btn btn-outline-warning btn-sm">评价商品</a>
|
||||
<!-- 评价商品按钮,根据商品数量展示 -->
|
||||
{% if order.order_items|length == 1 %}
|
||||
<a href="{{ url_for('review.write_review', order_id=order.id, product_id=order.order_items[0].product_id) }}"
|
||||
class="btn btn-outline-warning btn-sm">评价商品</a>
|
||||
{% else %}
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-warning btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
评价商品
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
{% for item in order.order_items %}
|
||||
<li>
|
||||
<a class="dropdown-item"
|
||||
href="{{ url_for('review.write_review', order_id=order.id, product_id=item.product_id) }}">
|
||||
{{ item.product_name[:20] }}{% if item.product_name|length > 20 %}...{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -23,10 +23,12 @@
|
||||
<a href="{{ url_for('address.index') }}" class="list-group-item list-group-item-action">
|
||||
<i class="bi bi-geo-alt"></i> 收货地址
|
||||
</a>
|
||||
<a href="#" class="list-group-item list-group-item-action">
|
||||
<a href="{{ url_for('favorite.index') }}" class="list-group-item list-group-item-action">
|
||||
<i class="bi bi-heart"></i> 我的收藏
|
||||
<a href="{{ url_for('review.my_reviews') }}" class="list-group-item list-group-item-action">
|
||||
<i class="bi bi-star"></i> 我的评价
|
||||
</a>
|
||||
<a href="#" class="list-group-item list-group-item-action">
|
||||
<a href="{{ url_for('history.index') }}" class="list-group-item list-group-item-action">
|
||||
<i class="bi bi-clock-history"></i> 浏览历史
|
||||
</a>
|
||||
</div>
|
||||
@ -124,42 +126,76 @@
|
||||
|
||||
<!-- 快捷操作 -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<i class="bi bi-bag display-4 text-primary mb-2"></i>
|
||||
<h6 class="card-title">我的订单</h6>
|
||||
<small class="text-muted">查看所有订单</small>
|
||||
<a href="{{ url_for('user.orders') }}" class="text-decoration-none">
|
||||
<i class="bi bi-bag display-4 text-primary mb-2"></i>
|
||||
<h6 class="card-title">我的订单</h6>
|
||||
<small class="text-muted">查看所有订单</small>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<i class="bi bi-cart display-4 text-success mb-2"></i>
|
||||
<h6 class="card-title">购物车</h6>
|
||||
<small class="text-muted">查看购物车</small>
|
||||
<a href="{{ url_for('cart.index') }}" class="text-decoration-none">
|
||||
<i class="bi bi-cart display-4 text-success mb-2"></i>
|
||||
<h6 class="card-title">购物车</h6>
|
||||
<small class="text-muted">查看购物车</small>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<i class="bi bi-heart display-4 text-danger mb-2"></i>
|
||||
<h6 class="card-title">我的收藏</h6>
|
||||
<small class="text-muted">收藏的商品</small>
|
||||
<a href="{{ url_for('favorite.index') }}" class="text-decoration-none">
|
||||
<i class="bi bi-heart display-4 text-danger mb-2"></i>
|
||||
<h6 class="card-title">我的收藏</h6>
|
||||
<small class="text-muted">收藏的商品</small>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<a href="{{ url_for('history.index') }}" class="text-decoration-none">
|
||||
<i class="bi bi-clock-history display-4 text-info mb-2"></i>
|
||||
<h6 class="card-title">浏览历史</h6>
|
||||
<small class="text-muted">查看浏览记录</small>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<i class="bi bi-geo-alt display-4 text-warning mb-2"></i>
|
||||
<h6 class="card-title">收货地址</h6>
|
||||
<small class="text-muted">管理收货地址</small>
|
||||
<a href="{{ url_for('address.index') }}" class="text-decoration-none">
|
||||
<i class="bi bi-geo-alt display-4 text-warning mb-2"></i>
|
||||
<h6 class="card-title">收货地址</h6>
|
||||
<small class="text-muted">管理收货地址</small>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<a href="{{ url_for('review.my_reviews') }}" class="text-decoration-none">
|
||||
<i class="bi bi-star display-4 text-secondary mb-2"></i>
|
||||
<h6 class="card-title">我的评价</h6>
|
||||
<small class="text-muted">查看我的评价</small>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,312 +1,91 @@
|
||||
"""
|
||||
装饰器工具模块
|
||||
提供登录验证、权限控制等装饰器功能
|
||||
装饰器工具
|
||||
"""
|
||||
from functools import wraps
|
||||
from flask import session, redirect, url_for, flash, request, jsonify, g
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
def login_required(f):
|
||||
"""
|
||||
登录验证装饰器
|
||||
|
||||
用法:
|
||||
@app.route('/profile')
|
||||
@login_required
|
||||
def profile():
|
||||
return render_template('profile.html')
|
||||
|
||||
功能:
|
||||
- 检查用户是否已登录
|
||||
- 未登录用户重定向到登录页面
|
||||
- 支持AJAX请求返回JSON响应
|
||||
"""
|
||||
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
# 检查session中是否有用户ID
|
||||
if 'user_id' not in session:
|
||||
# 如果是AJAX请求,返回JSON响应
|
||||
if request.is_json or request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '请先登录',
|
||||
'code': 'LOGIN_REQUIRED',
|
||||
'redirect': url_for('auth.login')
|
||||
}), 401
|
||||
|
||||
# 普通HTTP请求,重定向到登录页
|
||||
flash('请先登录后再访问该页面', 'warning')
|
||||
# 保存用户想要访问的页面,登录后可以重定向回来
|
||||
session['next_url'] = request.url
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
# 将当前用户信息加载到g对象中,方便在视图函数中使用
|
||||
try:
|
||||
g.current_user = User.query.get(session['user_id'])
|
||||
if not g.current_user or g.current_user.status != 1:
|
||||
# 用户不存在或被禁用,清除session
|
||||
session.pop('user_id', None)
|
||||
flash('账号状态异常,请重新登录', 'error')
|
||||
return redirect(url_for('auth.login'))
|
||||
except Exception as e:
|
||||
# 数据库查询出错,清除session
|
||||
session.pop('user_id', None)
|
||||
flash('登录状态异常,请重新登录', 'error')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
from flask import session, redirect, url_for, flash, request, g
|
||||
from app.models.admin import AdminUser
|
||||
from app.models.operation_log import OperationLog
|
||||
|
||||
|
||||
def admin_required(f):
|
||||
"""
|
||||
管理员权限验证装饰器
|
||||
"""
|
||||
|
||||
"""管理员权限验证装饰器"""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
from app.models.admin import AdminUser
|
||||
|
||||
# 检查session中是否有管理员ID
|
||||
if 'admin_id' not in session:
|
||||
if request.is_json or request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '需要管理员权限',
|
||||
'code': 'ADMIN_REQUIRED',
|
||||
'redirect': url_for('admin.login')
|
||||
}), 403
|
||||
|
||||
flash('需要管理员权限才能访问', 'error')
|
||||
flash('请先登录', 'warning')
|
||||
return redirect(url_for('admin.login'))
|
||||
|
||||
# 加载管理员信息到g对象
|
||||
try:
|
||||
g.current_admin = AdminUser.query.get(session['admin_id'])
|
||||
if not g.current_admin or g.current_admin.status != 1:
|
||||
# 管理员不存在或被禁用,清除session
|
||||
session.pop('admin_id', None)
|
||||
flash('管理员账号状态异常,请重新登录', 'error')
|
||||
return redirect(url_for('admin.login'))
|
||||
except Exception as e:
|
||||
# 数据库查询出错,清除session
|
||||
session.pop('admin_id', None)
|
||||
flash('登录状态异常,请重新登录', 'error')
|
||||
# 获取管理员信息
|
||||
admin = AdminUser.query.get(session['admin_id'])
|
||||
if not admin or admin.status != 1:
|
||||
session.clear()
|
||||
flash('账号已被禁用,请联系管理员', 'error')
|
||||
return redirect(url_for('admin.login'))
|
||||
|
||||
g.current_admin = admin
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
def json_required(f):
|
||||
"""
|
||||
JSON请求验证装饰器
|
||||
|
||||
用法:
|
||||
@app.route('/api/upload', methods=['POST'])
|
||||
@json_required
|
||||
def api_upload():
|
||||
data = request.get_json()
|
||||
return jsonify({'success': True})
|
||||
|
||||
功能:
|
||||
- 确保请求是JSON格式
|
||||
- 非JSON请求返回错误响应
|
||||
"""
|
||||
|
||||
def login_required(f):
|
||||
"""用户登录验证装饰器"""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not request.is_json:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '请求必须是JSON格式',
|
||||
'code': 'JSON_REQUIRED'
|
||||
}), 400
|
||||
|
||||
if 'user_id' not in session:
|
||||
flash('请先登录', 'warning')
|
||||
return redirect(url_for('auth.login'))
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
def validate_file_upload(allowed_extensions=None, max_size=None):
|
||||
"""
|
||||
文件上传验证装饰器
|
||||
|
||||
用法:
|
||||
@app.route('/upload')
|
||||
@validate_file_upload(allowed_extensions={'jpg', 'png'}, max_size=2*1024*1024)
|
||||
def upload_file():
|
||||
file = request.files['file']
|
||||
return jsonify({'success': True})
|
||||
|
||||
参数:
|
||||
allowed_extensions: 允许的文件扩展名集合
|
||||
max_size: 最大文件大小(字节)
|
||||
"""
|
||||
|
||||
def log_operation(action, resource_type=None):
|
||||
"""操作日志记录装饰器"""
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
# 检查是否有文件上传
|
||||
if 'file' not in request.files:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '没有选择文件',
|
||||
'code': 'NO_FILE'
|
||||
}), 400
|
||||
|
||||
file = request.files['file']
|
||||
|
||||
# 检查文件名
|
||||
if file.filename == '':
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '没有选择文件',
|
||||
'code': 'NO_FILE'
|
||||
}), 400
|
||||
|
||||
# 检查文件扩展名
|
||||
if allowed_extensions:
|
||||
file_ext = file.filename.rsplit('.', 1)[1].lower() if '.' in file.filename else ''
|
||||
if file_ext not in allowed_extensions:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'不支持的文件格式,只支持: {", ".join(allowed_extensions)}',
|
||||
'code': 'INVALID_FILE_TYPE'
|
||||
}), 400
|
||||
|
||||
# 检查文件大小
|
||||
if max_size:
|
||||
# 获取文件大小
|
||||
file.seek(0, 2) # 移动到文件末尾
|
||||
file_size = file.tell()
|
||||
file.seek(0) # 重置文件指针
|
||||
|
||||
if file_size > max_size:
|
||||
size_mb = max_size / 1024 / 1024
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'文件大小超过限制,最大允许 {size_mb:.1f}MB',
|
||||
'code': 'FILE_TOO_LARGE'
|
||||
}), 400
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def rate_limit(max_requests=10, per_seconds=60):
|
||||
"""
|
||||
简单的请求频率限制装饰器
|
||||
|
||||
用法:
|
||||
@app.route('/api/send-code')
|
||||
@rate_limit(max_requests=5, per_seconds=300) # 5分钟内最多5次请求
|
||||
def send_verification_code():
|
||||
return jsonify({'success': True})
|
||||
|
||||
参数:
|
||||
max_requests: 最大请求次数
|
||||
per_seconds: 时间窗口(秒)
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
# 这里可以实现基于IP或用户的请求频率限制
|
||||
# 简单实现可以使用session或内存缓存
|
||||
# 生产环境建议使用Redis
|
||||
|
||||
# 获取客户端标识(IP地址或用户ID)
|
||||
client_id = request.remote_addr
|
||||
if 'user_id' in session:
|
||||
client_id = f"user_{session['user_id']}"
|
||||
|
||||
# 这里应该实现真正的频率限制逻辑
|
||||
# 暂时跳过,返回原函数
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def log_operation(action, resource_type=None, resource_id=None):
|
||||
"""
|
||||
操作日志记录装饰器
|
||||
|
||||
用法:
|
||||
@app.route('/admin/users/<int:user_id>', methods=['DELETE'])
|
||||
@admin_required
|
||||
@log_operation('删除用户', 'user')
|
||||
def delete_user(user_id):
|
||||
# 删除用户逻辑
|
||||
return jsonify({'success': True})
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
from app.models.operation_log import OperationLog
|
||||
|
||||
# 执行原函数
|
||||
result = f(*args, **kwargs)
|
||||
|
||||
# 记录操作日志
|
||||
try:
|
||||
user_id = None
|
||||
user_type = 1 # 默认普通用户
|
||||
user_type = None
|
||||
|
||||
# 检查是否是管理员操作
|
||||
if 'admin_id' in session:
|
||||
user_id = session['admin_id']
|
||||
user_type = 2
|
||||
user_type = 2 # 管理员
|
||||
elif 'user_id' in session:
|
||||
user_id = session['user_id']
|
||||
user_type = 1
|
||||
user_type = 1 # 普通用户
|
||||
|
||||
# 获取资源ID(如果在URL参数中)
|
||||
actual_resource_id = resource_id
|
||||
if resource_type and not actual_resource_id:
|
||||
# 尝试从URL参数中获取资源ID
|
||||
for key, value in kwargs.items():
|
||||
if key.endswith('_id'):
|
||||
actual_resource_id = value
|
||||
break
|
||||
|
||||
# 准备请求数据
|
||||
request_data = {}
|
||||
if request.method in ['POST', 'PUT', 'PATCH']:
|
||||
if request.is_json:
|
||||
request_data = request.get_json() or {}
|
||||
else:
|
||||
request_data = request.form.to_dict()
|
||||
|
||||
# 记录日志
|
||||
OperationLog.create_log(
|
||||
user_id=user_id,
|
||||
user_type=user_type,
|
||||
action=action,
|
||||
resource_type=resource_type,
|
||||
resource_id=actual_resource_id,
|
||||
ip_address=request.remote_addr,
|
||||
user_agent=request.headers.get('User-Agent'),
|
||||
request_data=request_data if request_data else None
|
||||
)
|
||||
if user_id:
|
||||
# 获取资源ID
|
||||
resource_id = None
|
||||
if 'product_id' in kwargs:
|
||||
resource_id = kwargs['product_id']
|
||||
elif 'category_id' in kwargs:
|
||||
resource_id = kwargs['category_id']
|
||||
elif 'user_id' in kwargs:
|
||||
resource_id = kwargs['user_id']
|
||||
|
||||
# 记录日志
|
||||
OperationLog.create_log(
|
||||
user_id=user_id,
|
||||
user_type=user_type,
|
||||
action=action,
|
||||
resource_type=resource_type,
|
||||
resource_id=resource_id,
|
||||
ip_address=request.remote_addr,
|
||||
user_agent=request.headers.get('User-Agent', ''),
|
||||
request_data=dict(request.form) if request.form else None
|
||||
)
|
||||
except Exception as e:
|
||||
# 日志记录失败不影响主要功能
|
||||
print(f"记录操作日志失败: {str(e)}")
|
||||
# 日志记录失败不应该影响主要功能
|
||||
print(f"操作日志记录失败: {str(e)}")
|
||||
|
||||
return result
|
||||
|
||||
return decorated_function
|
||||
|
||||
return decorator
|
||||
|
||||
|
@ -5,11 +5,14 @@ from flask import Blueprint, render_template, request, redirect, url_for, flash,
|
||||
from werkzeug.security import generate_password_hash
|
||||
from app.models.admin import AdminUser
|
||||
from app.models.user import User
|
||||
from app.models.order import Order, OrderItem, ShippingInfo
|
||||
from app.models.payment import Payment
|
||||
from app.models.operation_log import OperationLog
|
||||
from app.utils.decorators import admin_required, log_operation
|
||||
from config.database import db
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy import func, or_
|
||||
import json
|
||||
|
||||
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||||
|
||||
@ -92,7 +95,7 @@ def dashboard():
|
||||
OperationLog.created_at.desc()
|
||||
).limit(10).all()
|
||||
|
||||
# 用户注册趋势(最近7天)
|
||||
# 最近7天用户注册趋势
|
||||
user_trend = []
|
||||
for i in range(6, -1, -1):
|
||||
date = datetime.now() - timedelta(days=i)
|
||||
@ -207,7 +210,7 @@ def users():
|
||||
search = request.args.get('search', '').strip()
|
||||
if search:
|
||||
query = query.filter(
|
||||
db.or_(
|
||||
or_(
|
||||
User.username.like(f'%{search}%'),
|
||||
User.email.like(f'%{search}%'),
|
||||
User.phone.like(f'%{search}%'),
|
||||
@ -222,7 +225,269 @@ def users():
|
||||
|
||||
users = query.paginate(page=page, per_page=per_page, error_out=False)
|
||||
|
||||
return render_template('admin/users.html', users=users, search=search, status=status)
|
||||
# 计算本周新增用户数
|
||||
week_start = datetime.now() - timedelta(days=7)
|
||||
week_new_users = User.query.filter(User.created_at >= week_start).count()
|
||||
|
||||
return render_template('admin/users.html',
|
||||
users=users,
|
||||
search=search,
|
||||
status=status,
|
||||
week_new_users=week_new_users)
|
||||
|
||||
|
||||
@admin_bp.route('/users/<int:user_id>/detail')
|
||||
@admin_required
|
||||
def user_detail(user_id):
|
||||
"""获取用户详情"""
|
||||
try:
|
||||
user = User.query.get_or_404(user_id)
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'user': user.to_dict()
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': str(e)
|
||||
})
|
||||
|
||||
|
||||
@admin_bp.route('/users/<int:user_id>/toggle-status', methods=['POST'])
|
||||
@admin_required
|
||||
@log_operation('切换用户状态')
|
||||
def toggle_user_status(user_id):
|
||||
"""切换用户状态"""
|
||||
try:
|
||||
user = User.query.get_or_404(user_id)
|
||||
data = request.get_json()
|
||||
new_status = data.get('status')
|
||||
|
||||
if new_status not in [0, 1]:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '无效的状态值'
|
||||
})
|
||||
|
||||
user.status = new_status
|
||||
db.session.commit()
|
||||
|
||||
action_text = '启用' if new_status == 1 else '禁用'
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'用户已{action_text}'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': str(e)
|
||||
})
|
||||
|
||||
|
||||
@admin_bp.route('/orders')
|
||||
@admin_required
|
||||
def orders():
|
||||
"""订单管理"""
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = 20
|
||||
|
||||
query = Order.query.order_by(Order.created_at.desc())
|
||||
|
||||
# 搜索功能
|
||||
search = request.args.get('search', '').strip()
|
||||
if search:
|
||||
query = query.filter(
|
||||
or_(
|
||||
Order.order_sn.like(f'%{search}%'),
|
||||
Order.user.has(User.username.like(f'%{search}%')),
|
||||
Order.user.has(User.phone.like(f'%{search}%'))
|
||||
)
|
||||
)
|
||||
|
||||
# 状态筛选
|
||||
status = request.args.get('status', '', type=str)
|
||||
if status:
|
||||
query = query.filter(Order.status == int(status))
|
||||
|
||||
# 日期筛选
|
||||
start_date = request.args.get('start_date', '').strip()
|
||||
end_date = request.args.get('end_date', '').strip()
|
||||
|
||||
if start_date:
|
||||
try:
|
||||
start_date_obj = datetime.strptime(start_date, '%Y-%m-%d')
|
||||
query = query.filter(Order.created_at >= start_date_obj)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if end_date:
|
||||
try:
|
||||
end_date_obj = datetime.strptime(end_date, '%Y-%m-%d') + timedelta(days=1)
|
||||
query = query.filter(Order.created_at < end_date_obj)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
orders = query.paginate(page=page, per_page=per_page, error_out=False)
|
||||
|
||||
# 获取订单统计
|
||||
order_stats = {}
|
||||
for status_code, status_name in Order.STATUS_CHOICES.items():
|
||||
count = Order.query.filter_by(status=status_code).count()
|
||||
order_stats[status_code] = {'name': status_name, 'count': count}
|
||||
|
||||
return render_template('admin/orders.html',
|
||||
orders=orders,
|
||||
search=search,
|
||||
status=status,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
order_stats=order_stats,
|
||||
ORDER_STATUS=Order.STATUS_CHOICES)
|
||||
|
||||
|
||||
@admin_bp.route('/orders/<int:order_id>')
|
||||
@admin_required
|
||||
def order_detail(order_id):
|
||||
"""订单详情"""
|
||||
order = Order.query.get_or_404(order_id)
|
||||
|
||||
# 获取支付记录
|
||||
payment = Payment.query.filter_by(order_id=order_id).first()
|
||||
|
||||
# 获取物流信息
|
||||
shipping_info = ShippingInfo.query.filter_by(order_id=order_id).first()
|
||||
|
||||
return render_template('admin/order_detail.html',
|
||||
order=order,
|
||||
payment=payment,
|
||||
shipping_info=shipping_info)
|
||||
|
||||
|
||||
@admin_bp.route('/orders/<int:order_id>/ship', methods=['POST'])
|
||||
@admin_required
|
||||
@log_operation('订单发货')
|
||||
def ship_order(order_id):
|
||||
"""订单发货"""
|
||||
try:
|
||||
order = Order.query.get_or_404(order_id)
|
||||
|
||||
if order.status != Order.STATUS_PENDING_SHIPMENT:
|
||||
return jsonify({'success': False, 'message': '订单状态不允许发货'})
|
||||
|
||||
# 获取发货信息
|
||||
shipping_company = request.form.get('shipping_company', '').strip()
|
||||
tracking_number = request.form.get('tracking_number', '').strip()
|
||||
|
||||
if not shipping_company or not tracking_number:
|
||||
return jsonify({'success': False, 'message': '请填写完整的物流信息'})
|
||||
|
||||
# 更新订单状态
|
||||
order.status = Order.STATUS_SHIPPED
|
||||
order.shipped_at = datetime.utcnow()
|
||||
|
||||
# 创建或更新物流信息
|
||||
shipping_info = ShippingInfo.query.filter_by(order_id=order_id).first()
|
||||
if not shipping_info:
|
||||
shipping_info = ShippingInfo(order_id=order_id)
|
||||
db.session.add(shipping_info)
|
||||
|
||||
shipping_info.shipping_company = shipping_company
|
||||
shipping_info.tracking_number = tracking_number
|
||||
shipping_info.shipping_status = 1 # 已发货
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True, 'message': '发货成功'})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'message': f'发货失败: {str(e)}'})
|
||||
|
||||
|
||||
@admin_bp.route('/orders/<int:order_id>/refund', methods=['POST'])
|
||||
@admin_required
|
||||
@log_operation('订单退款')
|
||||
def refund_order(order_id):
|
||||
"""订单退款"""
|
||||
try:
|
||||
order = Order.query.get_or_404(order_id)
|
||||
|
||||
if order.status not in [Order.STATUS_PENDING_SHIPMENT, Order.STATUS_SHIPPED, Order.STATUS_REFUNDING]:
|
||||
return jsonify({'success': False, 'message': '订单状态不允许退款'})
|
||||
|
||||
# 获取退款信息
|
||||
refund_reason = request.form.get('refund_reason', '').strip()
|
||||
|
||||
if not refund_reason:
|
||||
return jsonify({'success': False, 'message': '请填写退款原因'})
|
||||
|
||||
# 更新订单状态
|
||||
order.status = Order.STATUS_REFUNDING
|
||||
|
||||
# 更新支付记录状态
|
||||
payment = Payment.query.filter_by(order_id=order_id).first()
|
||||
if payment:
|
||||
payment.status = Payment.STATUS_REFUNDED
|
||||
|
||||
# 恢复库存
|
||||
from app.models.product import ProductInventory
|
||||
for item in order.order_items:
|
||||
if item.sku_code:
|
||||
sku_info = ProductInventory.query.filter_by(sku_code=item.sku_code).first()
|
||||
if sku_info:
|
||||
sku_info.stock += item.quantity
|
||||
|
||||
# 减少销量
|
||||
if item.product:
|
||||
item.product.sales_count = max(0, item.product.sales_count - item.quantity)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True, 'message': '退款处理成功'})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'message': f'退款失败: {str(e)}'})
|
||||
|
||||
|
||||
@admin_bp.route('/orders/<int:order_id>/cancel', methods=['POST'])
|
||||
@admin_required
|
||||
@log_operation('取消订单')
|
||||
def cancel_order(order_id):
|
||||
"""取消订单"""
|
||||
try:
|
||||
order = Order.query.get_or_404(order_id)
|
||||
|
||||
if not order.can_cancel():
|
||||
return jsonify({'success': False, 'message': '订单状态不允许取消'})
|
||||
|
||||
# 获取取消原因
|
||||
cancel_reason = request.form.get('cancel_reason', '').strip()
|
||||
|
||||
# 更新订单状态
|
||||
order.status = Order.STATUS_CANCELLED
|
||||
|
||||
# 恢复库存
|
||||
from app.models.product import ProductInventory
|
||||
for item in order.order_items:
|
||||
if item.sku_code:
|
||||
sku_info = ProductInventory.query.filter_by(sku_code=item.sku_code).first()
|
||||
if sku_info:
|
||||
sku_info.stock += item.quantity
|
||||
|
||||
# 减少销量
|
||||
if item.product:
|
||||
item.product.sales_count = max(0, item.product.sales_count - item.quantity)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True, 'message': '订单已取消'})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'message': f'取消失败: {str(e)}'})
|
||||
|
||||
|
||||
@admin_bp.route('/logs')
|
||||
@ -246,4 +511,63 @@ def logs():
|
||||
|
||||
logs = query.paginate(page=page, per_page=per_page, error_out=False)
|
||||
|
||||
return render_template('admin/logs.html', logs=logs, user_type=user_type, action=action)
|
||||
# 计算今日操作数
|
||||
today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
today_logs_count = OperationLog.query.filter(
|
||||
OperationLog.created_at >= today_start
|
||||
).count()
|
||||
|
||||
return render_template('admin/logs.html',
|
||||
logs=logs,
|
||||
user_type=user_type,
|
||||
action=action,
|
||||
today_logs_count=today_logs_count)
|
||||
|
||||
|
||||
@admin_bp.route('/logs/<int:log_id>/detail')
|
||||
@admin_required
|
||||
def log_detail(log_id):
|
||||
"""获取日志详情"""
|
||||
try:
|
||||
log = OperationLog.query.get_or_404(log_id)
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'log': log.to_dict()
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': str(e)
|
||||
})
|
||||
|
||||
|
||||
@admin_bp.route('/logs/clear', methods=['POST'])
|
||||
@admin_required
|
||||
@log_operation('清理操作日志')
|
||||
def clear_logs():
|
||||
"""清理操作日志"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
days_to_keep = data.get('days_to_keep', 30)
|
||||
|
||||
# 计算删除日期
|
||||
delete_before = datetime.now() - timedelta(days=days_to_keep)
|
||||
|
||||
# 删除旧日志
|
||||
deleted_count = OperationLog.query.filter(
|
||||
OperationLog.created_at < delete_before
|
||||
).delete()
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'已清理 {deleted_count} 条历史日志'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': str(e)
|
||||
})
|
||||
|
224
app/views/favorite.py
Normal file
224
app/views/favorite.py
Normal 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
153
app/views/history.py
Normal 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)})
|
@ -7,6 +7,8 @@ from app.models.order import Order
|
||||
from app.utils.decorators import login_required
|
||||
from config.database import db
|
||||
from datetime import datetime
|
||||
import time
|
||||
import random
|
||||
|
||||
payment_bp = Blueprint('payment', __name__, url_prefix='/payment')
|
||||
|
||||
@ -45,6 +47,9 @@ def process():
|
||||
elif payment_method == 'bank':
|
||||
# 银行卡支付
|
||||
result = process_bank_pay(payment)
|
||||
elif payment_method == 'simulate':
|
||||
# 模拟支付
|
||||
result = process_simulate_pay(payment)
|
||||
else:
|
||||
return jsonify({'success': False, 'message': '不支持的支付方式'})
|
||||
|
||||
@ -106,6 +111,17 @@ def process_bank_pay(payment):
|
||||
}
|
||||
|
||||
|
||||
def process_simulate_pay(payment):
|
||||
"""处理模拟支付"""
|
||||
return {
|
||||
'success': True,
|
||||
'payment_type': 'simulate',
|
||||
'payment_sn': payment.payment_sn,
|
||||
'amount': float(payment.amount),
|
||||
'message': '模拟支付模式,可直接完成支付'
|
||||
}
|
||||
|
||||
|
||||
@payment_bp.route('/callback/wechat', methods=['POST'])
|
||||
def wechat_callback():
|
||||
"""微信支付回调"""
|
||||
@ -201,7 +217,7 @@ def simulate_success(payment_sn):
|
||||
return jsonify({'success': False, 'message': '订单已支付'})
|
||||
|
||||
# 模拟支付成功
|
||||
result = handle_payment_success(payment_sn, f'SIMULATE_{datetime.now().timestamp()}')
|
||||
result = handle_payment_success(payment_sn, f'SIMULATE_{int(time.time())}_{random.randint(1000, 9999)}')
|
||||
|
||||
if result == "SUCCESS":
|
||||
return jsonify({'success': True, 'message': '支付成功'})
|
||||
@ -210,3 +226,29 @@ def simulate_success(payment_sn):
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': f'模拟支付失败: {str(e)}'})
|
||||
|
||||
|
||||
@payment_bp.route('/simulate_fail/<payment_sn>', methods=['POST'])
|
||||
@login_required
|
||||
def simulate_fail(payment_sn):
|
||||
"""模拟支付失败(开发测试用)"""
|
||||
try:
|
||||
user_id = session['user_id']
|
||||
payment = Payment.query.filter_by(payment_sn=payment_sn).first()
|
||||
|
||||
if not payment or payment.order.user_id != user_id:
|
||||
return jsonify({'success': False, 'message': '支付记录不存在'})
|
||||
|
||||
if payment.status == Payment.STATUS_SUCCESS:
|
||||
return jsonify({'success': False, 'message': '订单已支付,无法模拟失败'})
|
||||
|
||||
# 模拟支付失败
|
||||
payment.status = Payment.STATUS_FAILED
|
||||
payment.third_party_sn = f'SIMULATE_FAIL_{int(time.time())}_{random.randint(1000, 9999)}'
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True, 'message': '模拟支付失败'})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'message': f'模拟支付失败操作失败: {str(e)}'})
|
||||
|
264
app/views/review.py
Normal file
264
app/views/review.py
Normal 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)}'})
|
Loading…
x
Reference in New Issue
Block a user